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:
parent
78c3b8dafa
commit
84406a8123
201
src/mp4-test.cc
201
src/mp4-test.cc
|
@ -28,7 +28,7 @@
|
||||||
// You should have received a copy of the GNU General Public License
|
// You should have received a copy of the GNU General Public License
|
||||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
// 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 <fcntl.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
@ -83,79 +83,111 @@ std::string Digest(const FileSlice *slice) {
|
||||||
return ::moonfire_nvr::ToHex(digest->Finalize());
|
return ::moonfire_nvr::ToHex(digest->Finalize());
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(Mp4SampleTablePiecesTest, Stts) {
|
TEST(Mp4SampleTablePiecesTest, AllSyncFrames) {
|
||||||
|
Recording recording;
|
||||||
SampleIndexEncoder encoder;
|
SampleIndexEncoder encoder;
|
||||||
|
encoder.Init(&recording, 42);
|
||||||
for (int i = 1; i <= 5; ++i) {
|
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;
|
Mp4SampleTablePieces pieces;
|
||||||
std::string error_message;
|
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.
|
// included.
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(pieces.Init(&recording, 2, 10, 2, 2 + 4 + 6 + 8, &error_message))
|
||||||
pieces.Init(encoder.data(), 2, 10, 1, 1 + 2 + 3 + 4, &error_message))
|
|
||||||
<< error_message;
|
<< error_message;
|
||||||
|
|
||||||
EXPECT_EQ(3, pieces.stts_entry_count());
|
EXPECT_EQ(3, pieces.stts_entry_count());
|
||||||
const char kExpectedEntries[] =
|
const char kExpectedStts[] =
|
||||||
"00 00 00 01 00 00 00 02 "
|
"00 00 00 01 00 00 00 04 " // run length / timestamps.
|
||||||
"00 00 00 01 00 00 00 03 "
|
"00 00 00 01 00 00 00 06 "
|
||||||
"00 00 00 01 00 00 00 04";
|
"00 00 00 01 00 00 00 08";
|
||||||
EXPECT_EQ(kExpectedEntries, ToHex(pieces.stts_entries(), true));
|
EXPECT_EQ(kExpectedStts, ToHex(pieces.stts_entries(), true));
|
||||||
}
|
|
||||||
|
|
||||||
TEST(Mp4SampleTablePiecesTest, SttsAfterSyncSample) {
|
// Initial index "10" as given above.
|
||||||
SampleIndexEncoder encoder;
|
EXPECT_EQ(3, pieces.stss_entry_count());
|
||||||
for (int i = 1; i <= 5; ++i) {
|
const char kExpectedStss[] = "00 00 00 0a 00 00 00 0b 00 00 00 0c";
|
||||||
encoder.AddSample(i, 2 * i, i == 1);
|
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());
|
EXPECT_EQ(3, pieces.stsz_entry_count());
|
||||||
const char kExpectedEntries[] = "00 00 00 04 00 00 00 06 00 00 00 08";
|
const char kExpectedStsz[] = "00 00 00 06 00 00 00 09 00 00 00 0c";
|
||||||
EXPECT_EQ(kExpectedEntries, ToHex(pieces.stsz_entries(), true));
|
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 {
|
class IntegrationTest : public testing::Test {
|
||||||
|
@ -167,20 +199,24 @@ class IntegrationTest : public testing::Test {
|
||||||
CHECK_EQ(0, ret) << strerror(ret);
|
CHECK_EQ(0, ret) << strerror(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
void CopyMp4ToSingleRecording() {
|
Recording CopyMp4ToSingleRecording() {
|
||||||
std::string error_message;
|
std::string error_message;
|
||||||
|
Recording recording;
|
||||||
SampleIndexEncoder index;
|
SampleIndexEncoder index;
|
||||||
|
|
||||||
|
// Set start time to 2015-04-26 00:00:00 UTC.
|
||||||
|
index.Init(&recording, UINT64_C(1430006400) * kTimeUnitsPerSecond);
|
||||||
SampleFileWriter writer(tmpdir_.get());
|
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)) {
|
if (!writer.Open("clip.sample", &error_message)) {
|
||||||
ADD_FAILURE() << "open clip.sample: " << error_message;
|
ADD_FAILURE() << "open clip.sample: " << error_message;
|
||||||
return;
|
return recording;
|
||||||
}
|
}
|
||||||
auto in = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
auto in = GetRealVideoSource()->OpenFile("../src/testdata/clip.mp4",
|
||||||
&error_message);
|
&error_message);
|
||||||
if (in == nullptr) {
|
if (in == nullptr) {
|
||||||
ADD_FAILURE() << "open clip.mp4" << error_message;
|
ADD_FAILURE() << "open clip.mp4" << error_message;
|
||||||
return;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
video_sample_entry_.width = in->stream()->codec->width;
|
video_sample_entry_.width = in->stream()->codec->width;
|
||||||
|
@ -189,7 +225,7 @@ class IntegrationTest : public testing::Test {
|
||||||
in->stream()->codec->height,
|
in->stream()->codec->height,
|
||||||
&video_sample_entry_.data, &error_message)) {
|
&video_sample_entry_.data, &error_message)) {
|
||||||
ADD_FAILURE() << "GetH264SampleEntry: " << error_message;
|
ADD_FAILURE() << "GetH264SampleEntry: " << error_message;
|
||||||
return;
|
return recording;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (true) {
|
while (true) {
|
||||||
|
@ -197,30 +233,28 @@ class IntegrationTest : public testing::Test {
|
||||||
if (!in->GetNext(&pkt, &error_message)) {
|
if (!in->GetNext(&pkt, &error_message)) {
|
||||||
if (!error_message.empty()) {
|
if (!error_message.empty()) {
|
||||||
ADD_FAILURE() << "GetNext: " << error_message;
|
ADD_FAILURE() << "GetNext: " << error_message;
|
||||||
return;
|
return recording;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!writer.Write(GetData(pkt), &error_message)) {
|
if (!writer.Write(GetData(pkt), &error_message)) {
|
||||||
ADD_FAILURE() << "Write: " << error_message;
|
ADD_FAILURE() << "Write: " << error_message;
|
||||||
return;
|
return recording;
|
||||||
}
|
}
|
||||||
index.AddSample(pkt.pkt()->duration, pkt.pkt()->size, pkt.is_key());
|
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;
|
ADD_FAILURE() << "Close: " << error_message;
|
||||||
}
|
}
|
||||||
recording_.video_index = index.data().as_string();
|
return recording;
|
||||||
|
|
||||||
// Set start time to 2015-04-26 00:00:00 UTC.
|
|
||||||
recording_.start_time_90k = UINT64_C(1430006400) * kTimeUnitsPerSecond;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::unique_ptr<VirtualFile> CreateMp4FromSingleRecording() {
|
std::unique_ptr<VirtualFile> CreateMp4FromSingleRecording(
|
||||||
|
const Recording &recording) {
|
||||||
Mp4FileBuilder builder;
|
Mp4FileBuilder builder;
|
||||||
builder.SetSampleEntry(video_sample_entry_);
|
builder.SetSampleEntry(video_sample_entry_);
|
||||||
builder.Append(Recording(recording_), 0,
|
builder.Append(Recording(recording), 0,
|
||||||
std::numeric_limits<int32_t>::max());
|
std::numeric_limits<int32_t>::max());
|
||||||
std::string error_message;
|
std::string error_message;
|
||||||
auto mp4 = builder.Build(&error_message);
|
auto mp4 = builder.Build(&error_message);
|
||||||
|
@ -283,20 +317,25 @@ class IntegrationTest : public testing::Test {
|
||||||
std::string tmpdir_path_;
|
std::string tmpdir_path_;
|
||||||
std::unique_ptr<File> tmpdir_;
|
std::unique_ptr<File> tmpdir_;
|
||||||
std::string etag_;
|
std::string etag_;
|
||||||
Recording recording_;
|
|
||||||
VideoSampleEntry video_sample_entry_;
|
VideoSampleEntry video_sample_entry_;
|
||||||
};
|
};
|
||||||
|
|
||||||
TEST_F(IntegrationTest, RoundTrip) {
|
TEST_F(IntegrationTest, RoundTrip) {
|
||||||
CopyMp4ToSingleRecording();
|
Recording recording = CopyMp4ToSingleRecording();
|
||||||
auto f = CreateMp4FromSingleRecording();
|
if (HasFailure()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto f = CreateMp4FromSingleRecording(recording);
|
||||||
WriteMp4(f.get());
|
WriteMp4(f.get());
|
||||||
CompareMp4s();
|
CompareMp4s();
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_F(IntegrationTest, Metadata) {
|
TEST_F(IntegrationTest, Metadata) {
|
||||||
CopyMp4ToSingleRecording();
|
Recording recording = CopyMp4ToSingleRecording();
|
||||||
auto f = CreateMp4FromSingleRecording();
|
if (HasFailure()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
auto f = CreateMp4FromSingleRecording(recording);
|
||||||
|
|
||||||
// This test is brittle, which is the point. Any time the digest comparison
|
// 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!
|
// here fails, it can be updated, but the etag must change as well!
|
||||||
|
|
20
src/mp4.cc
20
src/mp4.cc
|
@ -576,15 +576,26 @@ class Mp4File : public VirtualFile {
|
||||||
|
|
||||||
namespace internal {
|
namespace internal {
|
||||||
|
|
||||||
bool Mp4SampleTablePieces::Init(re2::StringPiece video_index_blob,
|
bool Mp4SampleTablePieces::Init(const Recording *recording,
|
||||||
int sample_entry_index, int32_t sample_offset,
|
int sample_entry_index, int32_t sample_offset,
|
||||||
int32_t start_90k, int32_t end_90k,
|
int32_t start_90k, int32_t end_90k,
|
||||||
std::string *error_message) {
|
std::string *error_message) {
|
||||||
video_index_blob_ = video_index_blob;
|
|
||||||
sample_entry_index_ = sample_entry_index;
|
sample_entry_index_ = sample_entry_index;
|
||||||
sample_offset_ = sample_offset;
|
sample_offset_ = sample_offset;
|
||||||
desired_end_90k_ = end_90k;
|
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()) {
|
if (!it.done() && !it.is_key()) {
|
||||||
*error_message = "First frame must be a key frame.";
|
*error_message = "First frame must be a key frame.";
|
||||||
return false;
|
return false;
|
||||||
|
@ -615,6 +626,7 @@ bool Mp4SampleTablePieces::Init(re2::StringPiece video_index_blob,
|
||||||
actual_end_90k_ = it.end_90k();
|
actual_end_90k_ = it.end_90k();
|
||||||
}
|
}
|
||||||
sample_pos_.end = it.pos();
|
sample_pos_.end = it.pos();
|
||||||
|
}
|
||||||
if (it.has_error()) {
|
if (it.has_error()) {
|
||||||
*error_message = it.error();
|
*error_message = it.error();
|
||||||
return false;
|
return false;
|
||||||
|
@ -724,7 +736,7 @@ std::unique_ptr<VirtualFile> Mp4FileBuilder::Build(std::string *error_message) {
|
||||||
return std::unique_ptr<VirtualFile>();
|
return std::unique_ptr<VirtualFile>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!segment->pieces.Init(segment->recording.video_index,
|
if (!segment->pieces.Init(&segment->recording,
|
||||||
1, // sample entry index
|
1, // sample entry index
|
||||||
sample_offset, segment->rel_start_90k,
|
sample_offset, segment->rel_start_90k,
|
||||||
segment->rel_end_90k, error_message)) {
|
segment->rel_end_90k, error_message)) {
|
||||||
|
|
|
@ -55,8 +55,7 @@ class Mp4SampleTablePieces {
|
||||||
Mp4SampleTablePieces(const Mp4SampleTablePieces &) = delete;
|
Mp4SampleTablePieces(const Mp4SampleTablePieces &) = delete;
|
||||||
void operator=(const Mp4SampleTablePieces &) = delete;
|
void operator=(const Mp4SampleTablePieces &) = delete;
|
||||||
|
|
||||||
// |video_index_blob|, which must outlive the Mp4SampleTablePieces, should
|
// |recording| must outlive the Mp4SampleTablePieces.
|
||||||
// be the contents of the video_index field for this recording.
|
|
||||||
//
|
//
|
||||||
// |sample_entry_index| should be the (1-based) index into the "stsd" box
|
// |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
|
// 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
|
// 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
|
// <= |end_90k|. TODO: support edit lists and duration trimming to produce
|
||||||
// the exact correct time range.
|
// 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,
|
int32_t sample_offset, int32_t start_90k, int32_t end_90k,
|
||||||
std::string *error_message);
|
std::string *error_message);
|
||||||
|
|
||||||
|
@ -100,8 +99,6 @@ class Mp4SampleTablePieces {
|
||||||
bool FillStscEntries(std::string *s, std::string *error_message) const;
|
bool FillStscEntries(std::string *s, std::string *error_message) const;
|
||||||
bool FillStszEntries(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
|
// After Init(), |begin_| will be on the first sample after the start of the
|
||||||
// range (or it will be done()).
|
// range (or it will be done()).
|
||||||
SampleIndexIterator begin_;
|
SampleIndexIterator begin_;
|
||||||
|
|
|
@ -54,23 +54,33 @@ namespace {
|
||||||
|
|
||||||
// Example from design/schema.md.
|
// Example from design/schema.md.
|
||||||
TEST(SampleIndexTest, EncodeExample) {
|
TEST(SampleIndexTest, EncodeExample) {
|
||||||
|
Recording recording;
|
||||||
SampleIndexEncoder encoder;
|
SampleIndexEncoder encoder;
|
||||||
|
encoder.Init(&recording, 1000);
|
||||||
encoder.AddSample(10, 1000, true);
|
encoder.AddSample(10, 1000, true);
|
||||||
encoder.AddSample(9, 10, false);
|
encoder.AddSample(9, 10, false);
|
||||||
encoder.AddSample(11, 15, false);
|
encoder.AddSample(11, 15, false);
|
||||||
encoder.AddSample(10, 12, false);
|
encoder.AddSample(10, 12, false);
|
||||||
encoder.AddSample(10, 1050, true);
|
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) {
|
TEST(SampleIndexTest, RoundTrip) {
|
||||||
|
Recording recording;
|
||||||
SampleIndexEncoder encoder;
|
SampleIndexEncoder encoder;
|
||||||
|
encoder.Init(&recording, 1000);
|
||||||
encoder.AddSample(10, 30000, true);
|
encoder.AddSample(10, 30000, true);
|
||||||
encoder.AddSample(9, 1000, false);
|
encoder.AddSample(9, 1000, false);
|
||||||
encoder.AddSample(11, 1100, false);
|
encoder.AddSample(11, 1100, false);
|
||||||
encoder.AddSample(18, 31000, true);
|
encoder.AddSample(18, 31000, true);
|
||||||
|
|
||||||
SampleIndexIterator it = SampleIndexIterator(encoder.data());
|
SampleIndexIterator it = SampleIndexIterator(recording.video_index);
|
||||||
std::string error_message;
|
std::string error_message;
|
||||||
ASSERT_FALSE(it.done()) << it.error();
|
ASSERT_FALSE(it.done()) << it.error();
|
||||||
EXPECT_EQ(10, it.duration_90k());
|
EXPECT_EQ(10, it.duration_90k());
|
||||||
|
|
|
@ -41,6 +41,19 @@
|
||||||
|
|
||||||
namespace moonfire_nvr {
|
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,
|
void SampleIndexEncoder::AddSample(int32_t duration_90k, int32_t bytes,
|
||||||
bool is_key) {
|
bool is_key) {
|
||||||
CHECK_GE(duration_90k, 0);
|
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_;
|
int32_t duration_delta = duration_90k - prev_duration_90k_;
|
||||||
prev_duration_90k_ = duration_90k;
|
prev_duration_90k_ = duration_90k;
|
||||||
int32_t bytes_delta;
|
int32_t bytes_delta;
|
||||||
|
recording_->end_time_90k += duration_90k;
|
||||||
|
recording_->sample_file_bytes += bytes;
|
||||||
|
++recording_->video_samples;
|
||||||
if (is_key) {
|
if (is_key) {
|
||||||
bytes_delta = bytes - prev_bytes_key_;
|
bytes_delta = bytes - prev_bytes_key_;
|
||||||
prev_bytes_key_ = bytes;
|
prev_bytes_key_ = bytes;
|
||||||
|
++recording_->video_sync_samples;
|
||||||
} else {
|
} else {
|
||||||
bytes_delta = bytes - prev_bytes_nonkey_;
|
bytes_delta = bytes - prev_bytes_nonkey_;
|
||||||
prev_bytes_nonkey_ = bytes;
|
prev_bytes_nonkey_ = bytes;
|
||||||
}
|
}
|
||||||
uint32_t zigzagged_bytes_delta = Zigzag32(bytes_delta);
|
uint32_t zigzagged_bytes_delta = Zigzag32(bytes_delta);
|
||||||
AppendVar32((Zigzag32(duration_delta) << 1) | is_key, &data_);
|
AppendVar32((Zigzag32(duration_delta) << 1) | is_key,
|
||||||
AppendVar32(zigzagged_bytes_delta, &data_);
|
&recording_->video_index);
|
||||||
}
|
AppendVar32(zigzagged_bytes_delta, &recording_->video_index);
|
||||||
|
|
||||||
void SampleIndexEncoder::Clear() {
|
|
||||||
data_.clear();
|
|
||||||
prev_duration_90k_ = 0;
|
|
||||||
prev_bytes_key_ = 0;
|
|
||||||
prev_bytes_nonkey_ = 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void SampleIndexIterator::Next() {
|
void SampleIndexIterator::Next() {
|
||||||
|
@ -194,6 +205,7 @@ bool SampleFileWriter::Close(std::string *sha1, std::string *error_message) {
|
||||||
bool ok = !corrupt_;
|
bool ok = !corrupt_;
|
||||||
file_.reset();
|
file_.reset();
|
||||||
*sha1 = sha1_->Finalize();
|
*sha1 = sha1_->Finalize();
|
||||||
|
sha1_ = Digest::SHA1();
|
||||||
pos_ = 0;
|
pos_ = 0;
|
||||||
corrupt_ = false;
|
corrupt_ = false;
|
||||||
return ok;
|
return ok;
|
||||||
|
|
|
@ -44,27 +44,45 @@
|
||||||
|
|
||||||
#include "crypto.h"
|
#include "crypto.h"
|
||||||
#include "filesystem.h"
|
#include "filesystem.h"
|
||||||
|
#include "uuid.h"
|
||||||
|
|
||||||
namespace moonfire_nvr {
|
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 {
|
class SampleIndexEncoder {
|
||||||
public:
|
public:
|
||||||
SampleIndexEncoder() { Clear(); }
|
SampleIndexEncoder() {}
|
||||||
void AddSample(int32_t duration_90k, int32_t bytes, bool is_key);
|
SampleIndexEncoder(const SampleIndexEncoder &) = delete;
|
||||||
void Clear();
|
void operator=(const SampleIndexEncoder &) = delete;
|
||||||
|
|
||||||
// Return the current data, which is invalidated by the next call to
|
void Init(Recording *recording, int64_t start_time_90k);
|
||||||
// AddSample() or Clear().
|
void AddSample(int32_t duration_90k, int32_t bytes, bool is_key);
|
||||||
re2::StringPiece data() { return data_; }
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::string data_;
|
Recording *recording_;
|
||||||
int32_t prev_duration_90k_;
|
int32_t prev_duration_90k_ = 0;
|
||||||
int32_t prev_bytes_key_;
|
int32_t prev_bytes_key_ = 0;
|
||||||
int32_t prev_bytes_nonkey_;
|
int32_t prev_bytes_nonkey_ = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Iterates through an encoded index, decoding on the fly. Copyable.
|
// Iterates through an encoded index, decoding on the fly. Copyable.
|
||||||
|
@ -176,19 +194,6 @@ struct VideoSampleEntry {
|
||||||
uint16_t height = 0;
|
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
|
} // namespace moonfire_nvr
|
||||||
|
|
||||||
#endif // MOONFIRE_NVR_RECORDING_H
|
#endif // MOONFIRE_NVR_RECORDING_H
|
||||||
|
|
|
@ -81,13 +81,14 @@ create table recording (
|
||||||
end_time_90k integer,
|
end_time_90k integer,
|
||||||
|
|
||||||
video_samples 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
|
video_index blob
|
||||||
);
|
);
|
||||||
|
|
||||||
-- A concrete box derived from a ISO/IEC 14496-12 section 8.5.2
|
-- A concrete box derived from a ISO/IEC 14496-12 section 8.5.2
|
||||||
-- VisualSampleEntry box. Describes the codec, width, height, etc.
|
-- VisualSampleEntry box. Describes the codec, width, height, etc.
|
||||||
create table visual_sample_entry (
|
create table video_sample_entry (
|
||||||
-- A SHA-1 hash of |bytes|.
|
-- A SHA-1 hash of |bytes|.
|
||||||
sha1 blob primary key,
|
sha1 blob primary key,
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue