Add a fast path to Mp4SampleTablePieces::Init.

This avoids iteration through the video index for the "interior" recordings of
a virtual file. This takes generating the size of a ~8-hour / 15 fps file from
about 60 ms to about 10 ms. I expect better savings on a Raspberry Pi 2, for
longer records, and for higher frame rates. The total time here can be
significant; one one ~day-long recording on the Pi, it was several seconds.
I'm optimistic this will help with that.

It'd also be possible to optimize DecodeVar32 (perhaps by unrolling the loop)
but better to remove a call than to optimize one.

To add the fast path, we need a new field "video_sync_samples" in the
recording table to calculate the length of the stss table. Storage cost should
be minimal; I think typically two bytes in SQLite's record format (serial type
1, value < 128), described here: <https://www.sqlite.org/fileformat2.html>.
This commit is contained in:
Scott Lamb 2016-01-14 15:41:45 -08:00
parent 78c3b8dafa
commit 84406a8123
7 changed files with 230 additions and 154 deletions

View File

@ -28,7 +28,7 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// mp4_test.cc: tests of the mp4.h interface.
// mp4-test.cc: tests of the mp4.h interface.
#include <fcntl.h>
#include <sys/stat.h>
@ -83,79 +83,111 @@ std::string Digest(const FileSlice *slice) {
return ::moonfire_nvr::ToHex(digest->Finalize());
}
TEST(Mp4SampleTablePiecesTest, Stts) {
TEST(Mp4SampleTablePiecesTest, AllSyncFrames) {
Recording recording;
SampleIndexEncoder encoder;
encoder.Init(&recording, 42);
for (int i = 1; i <= 5; ++i) {
encoder.AddSample(i, 2 * i, true);
int64_t sample_duration_90k = 2 * i;
int64_t sample_bytes = 3 * i;
encoder.AddSample(sample_duration_90k, sample_bytes, true);
}
Mp4SampleTablePieces pieces;
std::string error_message;
// Time range [1, 1 + 2 + 3 + 4) means the 2nd, 3rd, 4th samples should be
// Time range [2, 2 + 4 + 6 + 8) means the 2nd, 3rd, 4th samples should be
// included.
ASSERT_TRUE(
pieces.Init(encoder.data(), 2, 10, 1, 1 + 2 + 3 + 4, &error_message))
ASSERT_TRUE(pieces.Init(&recording, 2, 10, 2, 2 + 4 + 6 + 8, &error_message))
<< error_message;
EXPECT_EQ(3, pieces.stts_entry_count());
const char kExpectedEntries[] =
"00 00 00 01 00 00 00 02 "
"00 00 00 01 00 00 00 03 "
"00 00 00 01 00 00 00 04";
EXPECT_EQ(kExpectedEntries, ToHex(pieces.stts_entries(), true));
}
const char kExpectedStts[] =
"00 00 00 01 00 00 00 04 " // run length / timestamps.
"00 00 00 01 00 00 00 06 "
"00 00 00 01 00 00 00 08";
EXPECT_EQ(kExpectedStts, ToHex(pieces.stts_entries(), true));
TEST(Mp4SampleTablePiecesTest, SttsAfterSyncSample) {
SampleIndexEncoder encoder;
for (int i = 1; i <= 5; ++i) {
encoder.AddSample(i, 2 * i, i == 1);
}
// Initial index "10" as given above.
EXPECT_EQ(3, pieces.stss_entry_count());
const char kExpectedStss[] = "00 00 00 0a 00 00 00 0b 00 00 00 0c";
EXPECT_EQ(kExpectedStss, ToHex(pieces.stss_entries(), true));
Mp4SampleTablePieces pieces;
std::string error_message;
// Because only the 1st frame is a sync sample, it will be included also.
ASSERT_TRUE(
pieces.Init(encoder.data(), 2, 10, 1, 1 + 2 + 3 + 4, &error_message))
<< error_message;
EXPECT_EQ(4, pieces.stts_entry_count());
const char kExpectedEntries[] =
"00 00 00 01 00 00 00 01 "
"00 00 00 01 00 00 00 02 "
"00 00 00 01 00 00 00 03 "
"00 00 00 01 00 00 00 04";
EXPECT_EQ(kExpectedEntries, ToHex(pieces.stts_entries(), true));
}
TEST(Mp4SampleTablePiecesTest, Stss) {
SampleIndexEncoder encoder;
encoder.AddSample(1, 1, true);
encoder.AddSample(1, 1, false);
encoder.AddSample(1, 1, true);
encoder.AddSample(1, 1, false);
Mp4SampleTablePieces pieces;
std::string error_message;
ASSERT_TRUE(pieces.Init(encoder.data(), 2, 10, 0, 4, &error_message))
<< error_message;
EXPECT_EQ(2, pieces.stss_entry_count());
const char kExpectedSampleNumbers[] = "00 00 00 0a 00 00 00 0c";
EXPECT_EQ(kExpectedSampleNumbers, ToHex(pieces.stss_entries(), true));
}
TEST(Mp4SampleTablePiecesTest, Stsz) {
SampleIndexEncoder encoder;
for (int i = 1; i <= 5; ++i) {
encoder.AddSample(i, 2 * i, true);
}
Mp4SampleTablePieces pieces;
std::string error_message;
// Time range [1, 1 + 2 + 3 + 4) means the 2nd, 3rd, 4th samples should be
// included.
ASSERT_TRUE(
pieces.Init(encoder.data(), 2, 10, 1, 1 + 2 + 3 + 4, &error_message))
<< error_message;
EXPECT_EQ(3, pieces.stsz_entry_count());
const char kExpectedEntries[] = "00 00 00 04 00 00 00 06 00 00 00 08";
EXPECT_EQ(kExpectedEntries, ToHex(pieces.stsz_entries(), true));
const char kExpectedStsz[] = "00 00 00 06 00 00 00 09 00 00 00 0c";
EXPECT_EQ(kExpectedStsz, ToHex(pieces.stsz_entries(), true));
}
TEST(Mp4SampleTablePiecesTest, HalfSyncFrames) {
Recording recording;
SampleIndexEncoder encoder;
encoder.Init(&recording, 42);
for (int i = 1; i <= 5; ++i) {
int64_t sample_duration_90k = 2 * i;
int64_t sample_bytes = 3 * i;
encoder.AddSample(sample_duration_90k, sample_bytes, (i % 2) == 1);
}
Mp4SampleTablePieces pieces;
std::string error_message;
// Time range [2 + 4 + 6, 2 + 4 + 6 + 8) means the 4th samples should be
// included. The 3rd gets pulled in also because it is a sync frame and the
// 4th is not.
ASSERT_TRUE(
pieces.Init(&recording, 2, 10, 2 + 4 + 6, 2 + 4 + 6 + 8, &error_message))
<< error_message;
EXPECT_EQ(2, pieces.stts_entry_count());
const char kExpectedStts[] =
"00 00 00 01 00 00 00 06 "
"00 00 00 01 00 00 00 08";
EXPECT_EQ(kExpectedStts, ToHex(pieces.stts_entries(), true));
EXPECT_EQ(1, pieces.stss_entry_count());
const char kExpectedStss[] = "00 00 00 0a";
EXPECT_EQ(kExpectedStss, ToHex(pieces.stss_entries(), true));
EXPECT_EQ(2, pieces.stsz_entry_count());
const char kExpectedStsz[] = "00 00 00 09 00 00 00 0c";
EXPECT_EQ(kExpectedStsz, ToHex(pieces.stsz_entries(), true));
}
TEST(Mp4SampleTablePiecesTest, FastPath) {
Recording recording;
SampleIndexEncoder encoder;
encoder.Init(&recording, 42);
for (int i = 1; i <= 5; ++i) {
int64_t sample_duration_90k = 2 * i;
int64_t sample_bytes = 3 * i;
encoder.AddSample(sample_duration_90k, sample_bytes, (i % 2) == 1);
}
auto total_duration_90k = recording.end_time_90k - recording.start_time_90k;
Mp4SampleTablePieces pieces;
std::string error_message;
// Time range [0, end - start) means to pull in everything.
// This uses a fast path which can determine the size without examining the
// index.
ASSERT_TRUE(
pieces.Init(&recording, 2, 10, 0, total_duration_90k, &error_message))
<< error_message;
EXPECT_EQ(5, pieces.stts_entry_count());
const char kExpectedStts[] =
"00 00 00 01 00 00 00 02 "
"00 00 00 01 00 00 00 04 "
"00 00 00 01 00 00 00 06 "
"00 00 00 01 00 00 00 08 "
"00 00 00 01 00 00 00 0a";
EXPECT_EQ(kExpectedStts, ToHex(pieces.stts_entries(), true));
EXPECT_EQ(3, pieces.stss_entry_count());
const char kExpectedStss[] = "00 00 00 0a 00 00 00 0c 00 00 00 0e";
EXPECT_EQ(kExpectedStss, ToHex(pieces.stss_entries(), true));
EXPECT_EQ(5, pieces.stsz_entry_count());
const char kExpectedStsz[] =
"00 00 00 03 00 00 00 06 00 00 00 09 00 00 00 0c 00 00 00 0f";
EXPECT_EQ(kExpectedStsz, ToHex(pieces.stsz_entries(), true));
}
class IntegrationTest : public testing::Test {
@ -167,20 +199,24 @@ class IntegrationTest : public testing::Test {
CHECK_EQ(0, ret) << strerror(ret);
}
void CopyMp4ToSingleRecording() {
Recording CopyMp4ToSingleRecording() {
std::string error_message;
Recording recording;
SampleIndexEncoder index;
// Set start time to 2015-04-26 00:00:00 UTC.
index.Init(&recording, UINT64_C(1430006400) * kTimeUnitsPerSecond);
SampleFileWriter writer(tmpdir_.get());
recording_.sample_file_path = StrCat(tmpdir_path_, "/clip.sample");
recording.sample_file_path = StrCat(tmpdir_path_, "/clip.sample");
if (!writer.Open("clip.sample", &error_message)) {
ADD_FAILURE() << "open clip.sample: " << error_message;
return;
return recording;
}
auto in = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
&error_message);
if (in == nullptr) {
ADD_FAILURE() << "open clip.mp4" << error_message;
return;
return recording;
}
video_sample_entry_.width = in->stream()->codec->width;
@ -189,7 +225,7 @@ class IntegrationTest : public testing::Test {
in->stream()->codec->height,
&video_sample_entry_.data, &error_message)) {
ADD_FAILURE() << "GetH264SampleEntry: " << error_message;
return;
return recording;
}
while (true) {
@ -197,30 +233,28 @@ class IntegrationTest : public testing::Test {
if (!in->GetNext(&pkt, &error_message)) {
if (!error_message.empty()) {
ADD_FAILURE() << "GetNext: " << error_message;
return;
return recording;
}
break;
}
if (!writer.Write(GetData(pkt), &error_message)) {
ADD_FAILURE() << "Write: " << error_message;
return;
return recording;
}
index.AddSample(pkt.pkt()->duration, pkt.pkt()->size, pkt.is_key());
}
if (!writer.Close(&recording_.sample_file_sha1, &error_message)) {
if (!writer.Close(&recording.sample_file_sha1, &error_message)) {
ADD_FAILURE() << "Close: " << error_message;
}
recording_.video_index = index.data().as_string();
// Set start time to 2015-04-26 00:00:00 UTC.
recording_.start_time_90k = UINT64_C(1430006400) * kTimeUnitsPerSecond;
return recording;
}
std::unique_ptr<VirtualFile> CreateMp4FromSingleRecording() {
std::unique_ptr<VirtualFile> CreateMp4FromSingleRecording(
const Recording &recording) {
Mp4FileBuilder builder;
builder.SetSampleEntry(video_sample_entry_);
builder.Append(Recording(recording_), 0,
builder.Append(Recording(recording), 0,
std::numeric_limits<int32_t>::max());
std::string error_message;
auto mp4 = builder.Build(&error_message);
@ -283,20 +317,25 @@ class IntegrationTest : public testing::Test {
std::string tmpdir_path_;
std::unique_ptr<File> tmpdir_;
std::string etag_;
Recording recording_;
VideoSampleEntry video_sample_entry_;
};
TEST_F(IntegrationTest, RoundTrip) {
CopyMp4ToSingleRecording();
auto f = CreateMp4FromSingleRecording();
Recording recording = CopyMp4ToSingleRecording();
if (HasFailure()) {
return;
}
auto f = CreateMp4FromSingleRecording(recording);
WriteMp4(f.get());
CompareMp4s();
}
TEST_F(IntegrationTest, Metadata) {
CopyMp4ToSingleRecording();
auto f = CreateMp4FromSingleRecording();
Recording recording = CopyMp4ToSingleRecording();
if (HasFailure()) {
return;
}
auto f = CreateMp4FromSingleRecording(recording);
// This test is brittle, which is the point. Any time the digest comparison
// here fails, it can be updated, but the etag must change as well!

View File

@ -576,15 +576,26 @@ class Mp4File : public VirtualFile {
namespace internal {
bool Mp4SampleTablePieces::Init(re2::StringPiece video_index_blob,
bool Mp4SampleTablePieces::Init(const Recording *recording,
int sample_entry_index, int32_t sample_offset,
int32_t start_90k, int32_t end_90k,
std::string *error_message) {
video_index_blob_ = video_index_blob;
sample_entry_index_ = sample_entry_index;
sample_offset_ = sample_offset;
desired_end_90k_ = end_90k;
SampleIndexIterator it = SampleIndexIterator(video_index_blob_);
SampleIndexIterator it = SampleIndexIterator(recording->video_index);
auto recording_duration_90k =
recording->end_time_90k - recording->start_time_90k;
bool fast_path = start_90k == 0 && end_90k >= recording_duration_90k;
if (fast_path) {
VLOG(1) << "Fast path, frames=" << recording->video_samples
<< ", key=" << recording->video_sync_samples;
sample_pos_.end = recording->sample_file_bytes;
begin_ = it;
frames_ = recording->video_samples;
key_frames_ = recording->video_sync_samples;
actual_end_90k_ = recording_duration_90k;
} else {
if (!it.done() && !it.is_key()) {
*error_message = "First frame must be a key frame.";
return false;
@ -615,6 +626,7 @@ bool Mp4SampleTablePieces::Init(re2::StringPiece video_index_blob,
actual_end_90k_ = it.end_90k();
}
sample_pos_.end = it.pos();
}
if (it.has_error()) {
*error_message = it.error();
return false;
@ -724,7 +736,7 @@ std::unique_ptr<VirtualFile> Mp4FileBuilder::Build(std::string *error_message) {
return std::unique_ptr<VirtualFile>();
}
if (!segment->pieces.Init(segment->recording.video_index,
if (!segment->pieces.Init(&segment->recording,
1, // sample entry index
sample_offset, segment->rel_start_90k,
segment->rel_end_90k, error_message)) {

View File

@ -55,8 +55,7 @@ class Mp4SampleTablePieces {
Mp4SampleTablePieces(const Mp4SampleTablePieces &) = delete;
void operator=(const Mp4SampleTablePieces &) = delete;
// |video_index_blob|, which must outlive the Mp4SampleTablePieces, should
// be the contents of the video_index field for this recording.
// |recording| must outlive the Mp4SampleTablePieces.
//
// |sample_entry_index| should be the (1-based) index into the "stsd" box
// of an entry matching this recording's video_sample_entry_sha1. It may
@ -71,7 +70,7 @@ class Mp4SampleTablePieces {
// from the last sync sample <= |start_90k| to the last sample with start time
// <= |end_90k|. TODO: support edit lists and duration trimming to produce
// the exact correct time range.
bool Init(re2::StringPiece video_index_blob, int sample_entry_index,
bool Init(const Recording *recording, int sample_entry_index,
int32_t sample_offset, int32_t start_90k, int32_t end_90k,
std::string *error_message);
@ -100,8 +99,6 @@ class Mp4SampleTablePieces {
bool FillStscEntries(std::string *s, std::string *error_message) const;
bool FillStszEntries(std::string *s, std::string *error_message) const;
re2::StringPiece video_index_blob_;
// After Init(), |begin_| will be on the first sample after the start of the
// range (or it will be done()).
SampleIndexIterator begin_;

View File

@ -54,23 +54,33 @@ namespace {
// Example from design/schema.md.
TEST(SampleIndexTest, EncodeExample) {
Recording recording;
SampleIndexEncoder encoder;
encoder.Init(&recording, 1000);
encoder.AddSample(10, 1000, true);
encoder.AddSample(9, 10, false);
encoder.AddSample(11, 15, false);
encoder.AddSample(10, 12, false);
encoder.AddSample(10, 1050, true);
ASSERT_EQ("29 d0 0f 02 14 08 0a 02 05 01 64", ToHex(encoder.data(), true));
EXPECT_EQ("29 d0 0f 02 14 08 0a 02 05 01 64",
ToHex(recording.video_index, true));
EXPECT_EQ(1000, recording.start_time_90k);
EXPECT_EQ(1000 + 10 + 9 + 11 + 10 + 10, recording.end_time_90k);
EXPECT_EQ(1000 + 10 + 15 + 12 + 1050, recording.sample_file_bytes);
EXPECT_EQ(5, recording.video_samples);
EXPECT_EQ(2, recording.video_sync_samples);
}
TEST(SampleIndexTest, RoundTrip) {
Recording recording;
SampleIndexEncoder encoder;
encoder.Init(&recording, 1000);
encoder.AddSample(10, 30000, true);
encoder.AddSample(9, 1000, false);
encoder.AddSample(11, 1100, false);
encoder.AddSample(18, 31000, true);
SampleIndexIterator it = SampleIndexIterator(encoder.data());
SampleIndexIterator it = SampleIndexIterator(recording.video_index);
std::string error_message;
ASSERT_FALSE(it.done()) << it.error();
EXPECT_EQ(10, it.duration_90k());

View File

@ -41,6 +41,19 @@
namespace moonfire_nvr {
void SampleIndexEncoder::Init(Recording *recording, int64_t start_time_90k) {
recording_ = recording;
recording_->start_time_90k = start_time_90k;
recording_->end_time_90k = start_time_90k;
recording_->sample_file_bytes = 0;
recording_->video_samples = 0;
recording_->video_sync_samples = 0;
recording_->video_index.clear();
prev_duration_90k_ = 0;
prev_bytes_key_ = 0;
prev_bytes_nonkey_ = 0;
}
void SampleIndexEncoder::AddSample(int32_t duration_90k, int32_t bytes,
bool is_key) {
CHECK_GE(duration_90k, 0);
@ -48,23 +61,21 @@ void SampleIndexEncoder::AddSample(int32_t duration_90k, int32_t bytes,
int32_t duration_delta = duration_90k - prev_duration_90k_;
prev_duration_90k_ = duration_90k;
int32_t bytes_delta;
recording_->end_time_90k += duration_90k;
recording_->sample_file_bytes += bytes;
++recording_->video_samples;
if (is_key) {
bytes_delta = bytes - prev_bytes_key_;
prev_bytes_key_ = bytes;
++recording_->video_sync_samples;
} else {
bytes_delta = bytes - prev_bytes_nonkey_;
prev_bytes_nonkey_ = bytes;
}
uint32_t zigzagged_bytes_delta = Zigzag32(bytes_delta);
AppendVar32((Zigzag32(duration_delta) << 1) | is_key, &data_);
AppendVar32(zigzagged_bytes_delta, &data_);
}
void SampleIndexEncoder::Clear() {
data_.clear();
prev_duration_90k_ = 0;
prev_bytes_key_ = 0;
prev_bytes_nonkey_ = 0;
AppendVar32((Zigzag32(duration_delta) << 1) | is_key,
&recording_->video_index);
AppendVar32(zigzagged_bytes_delta, &recording_->video_index);
}
void SampleIndexIterator::Next() {
@ -194,6 +205,7 @@ bool SampleFileWriter::Close(std::string *sha1, std::string *error_message) {
bool ok = !corrupt_;
file_.reset();
*sha1 = sha1_->Finalize();
sha1_ = Digest::SHA1();
pos_ = 0;
corrupt_ = false;
return ok;

View File

@ -44,27 +44,45 @@
#include "crypto.h"
#include "filesystem.h"
#include "uuid.h"
namespace moonfire_nvr {
constexpr uint32_t kTimeUnitsPerSecond = 90000;
constexpr int64_t kTimeUnitsPerSecond = 90000;
// Encodes a sample index.
// Various fields from the "recording" table which are useful when viewing
// recordings.
struct Recording {
int64_t rowid = -1;
std::string sample_file_path;
std::string sample_file_sha1;
Uuid sample_file_uuid;
// Fields populated by SampleIndexEncoder.
int64_t start_time_90k = -1;
int64_t end_time_90k = -1;
int64_t sample_file_bytes = -1;
int64_t video_samples = -1;
int64_t video_sync_samples = -1;
std::string video_sample_entry_sha1;
std::string video_index;
};
// Reusable object to encode sample index data to a Recording object.
class SampleIndexEncoder {
public:
SampleIndexEncoder() { Clear(); }
void AddSample(int32_t duration_90k, int32_t bytes, bool is_key);
void Clear();
SampleIndexEncoder() {}
SampleIndexEncoder(const SampleIndexEncoder &) = delete;
void operator=(const SampleIndexEncoder &) = delete;
// Return the current data, which is invalidated by the next call to
// AddSample() or Clear().
re2::StringPiece data() { return data_; }
void Init(Recording *recording, int64_t start_time_90k);
void AddSample(int32_t duration_90k, int32_t bytes, bool is_key);
private:
std::string data_;
int32_t prev_duration_90k_;
int32_t prev_bytes_key_;
int32_t prev_bytes_nonkey_;
Recording *recording_;
int32_t prev_duration_90k_ = 0;
int32_t prev_bytes_key_ = 0;
int32_t prev_bytes_nonkey_ = 0;
};
// Iterates through an encoded index, decoding on the fly. Copyable.
@ -176,19 +194,6 @@ struct VideoSampleEntry {
uint16_t height = 0;
};
// Various fields from the "recording" table which are useful when viewing
// recordings.
struct Recording {
int64_t start_time_90k = -1;
int64_t end_time_90k = -1;
int64_t sample_file_bytes = -1;
std::string sample_file_path;
std::string sample_file_uuid;
std::string sample_file_sha1;
std::string video_sample_entry_sha1;
std::string video_index;
};
} // namespace moonfire_nvr
#endif // MOONFIRE_NVR_RECORDING_H

View File

@ -81,13 +81,14 @@ create table recording (
end_time_90k integer,
video_samples integer,
video_sample_entry_sha1 blob references visual_sample_entry (sha1),
video_sync_samples integer,
video_sample_entry_sha1 blob references video_sample_entry (sha1),
video_index blob
);
-- A concrete box derived from a ISO/IEC 14496-12 section 8.5.2
-- VisualSampleEntry box. Describes the codec, width, height, etc.
create table visual_sample_entry (
create table video_sample_entry (
-- A SHA-1 hash of |bytes|.
sha1 blob primary key,