diff --git a/src/mp4-test.cc b/src/mp4-test.cc index 2ecd6f4..3678427 100644 --- a/src/mp4-test.cc +++ b/src/mp4-test.cc @@ -28,7 +28,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . // -// mp4_test.cc: tests of the mp4.h interface. +// mp4-test.cc: tests of the mp4.h interface. #include #include @@ -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 CreateMp4FromSingleRecording() { + std::unique_ptr CreateMp4FromSingleRecording( + const Recording &recording) { Mp4FileBuilder builder; builder.SetSampleEntry(video_sample_entry_); - builder.Append(Recording(recording_), 0, + builder.Append(Recording(recording), 0, std::numeric_limits::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 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! diff --git a/src/mp4.cc b/src/mp4.cc index fc22594..8d6f05e 100644 --- a/src/mp4.cc +++ b/src/mp4.cc @@ -576,45 +576,57 @@ 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_); - if (!it.done() && !it.is_key()) { - *error_message = "First frame must be a key frame."; - return false; - } - for (; !it.done(); it.Next()) { - VLOG(3) << "Processing frame with start " << it.start_90k() - << (it.is_key() ? " (key)" : " (non-key)"); - // Find boundaries. - if (it.start_90k() <= start_90k && it.is_key()) { - VLOG(3) << "...new start candidate."; - begin_ = it; - sample_pos_.begin = begin_.pos(); - frames_ = 0; - key_frames_ = 0; - } - if (it.start_90k() >= end_90k) { - VLOG(3) << "...past end."; - break; + 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; } + for (; !it.done(); it.Next()) { + VLOG(3) << "Processing frame with start " << it.start_90k() + << (it.is_key() ? " (key)" : " (non-key)"); + // Find boundaries. + if (it.start_90k() <= start_90k && it.is_key()) { + VLOG(3) << "...new start candidate."; + begin_ = it; + sample_pos_.begin = begin_.pos(); + frames_ = 0; + key_frames_ = 0; + } + if (it.start_90k() >= end_90k) { + VLOG(3) << "...past end."; + break; + } - // Process this frame. - frames_++; - if (it.is_key()) { - key_frames_++; - } + // Process this frame. + frames_++; + if (it.is_key()) { + key_frames_++; + } - // This is the current best candidate to end. - actual_end_90k_ = it.end_90k(); + // This is the current best candidate to end. + actual_end_90k_ = it.end_90k(); + } + sample_pos_.end = it.pos(); } - sample_pos_.end = it.pos(); if (it.has_error()) { *error_message = it.error(); return false; @@ -724,7 +736,7 @@ std::unique_ptr Mp4FileBuilder::Build(std::string *error_message) { return std::unique_ptr(); } - 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)) { diff --git a/src/mp4.h b/src/mp4.h index 47a1b80..63d3464 100644 --- a/src/mp4.h +++ b/src/mp4.h @@ -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_; diff --git a/src/recording-test.cc b/src/recording-test.cc index cc57665..e6d1d51 100644 --- a/src/recording-test.cc +++ b/src/recording-test.cc @@ -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()); diff --git a/src/recording.cc b/src/recording.cc index 1cfb2a9..af1e2ca 100644 --- a/src/recording.cc +++ b/src/recording.cc @@ -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; diff --git a/src/recording.h b/src/recording.h index 2f074b0..43c89b6 100644 --- a/src/recording.h +++ b/src/recording.h @@ -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 diff --git a/src/schema.sql b/src/schema.sql index 1238b5f..09a1744 100644 --- a/src/schema.sql +++ b/src/schema.sql @@ -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,