First portion of .mp4 generation logic.

This commit is contained in:
Scott Lamb 2016-01-09 12:02:36 -08:00
parent c294d751b6
commit 30e0f73ae0
7 changed files with 515 additions and 6 deletions

View File

@ -49,6 +49,7 @@ set(MOONFIRE_NVR_SRCS
h264.cc
http.cc
moonfire-nvr.cc
mp4.cc
profiler.cc
recording.cc
sqlite.cc
@ -66,7 +67,7 @@ install_programs(/bin FILES moonfire-nvr)
include_directories(${GTest_INCLUDE_DIR})
include_directories(${GMock_INCLUDE_DIR})
foreach(test coding crypto h264 http moonfire-nvr recording sqlite string)
foreach(test coding crypto h264 http moonfire-nvr mp4 recording sqlite string)
add_executable(${test}-test ${test}-test.cc testutil.cc)
target_link_libraries(${test}-test GTest GMock moonfire-nvr-lib)
add_test(NAME ${test}-test

View File

@ -144,6 +144,11 @@ inline void AppendU32(uint32_t in, std::string *out) {
out->append(reinterpret_cast<const char *>(&net), sizeof(uint32_t));
}
inline void Append32(int32_t in, std::string *out) {
int32_t net = ToNetwork32(in);
out->append(reinterpret_cast<const char *>(&net), sizeof(int32_t));
}
} // namespace moonfire_nvr
#endif // MOONFIRE_NVR_CODING_H

View File

@ -40,6 +40,8 @@
#include <sys/stat.h>
#include <sys/types.h>
#include <memory>
#include <event2/buffer.h>
#include <event2/event.h>
#include <event2/keyvalq_struct.h>
@ -183,6 +185,26 @@ bool EvBuffer::AddFile(int fd, ev_off_t offset, ev_off_t length,
return true;
}
bool FillerFileSlice::AddRange(ByteRange range, EvBuffer *buf,
std::string *error_message) const {
std::unique_ptr<std::string> s(new std::string);
s->reserve(size_);
if (!fn_(s.get(), error_message)) {
return false;
}
if (s->size() != size_) {
*error_message = StrCat("Expected filled slice to be ", size_,
" bytes; got ", s->size(), " bytes.");
return false;
}
std::string *unowned_s = s.release();
buf->AddReference(unowned_s->data() + range.begin,
range.size(), [](const void *, size_t, void *s) {
delete reinterpret_cast<std::string *>(s);
}, unowned_s);
return true;
}
void HttpSendError(evhttp_request *req, int http_err, const std::string &prefix,
int posix_err) {
evhttp_send_error(req, http_err,

View File

@ -82,6 +82,13 @@ class EvBuffer {
bool AddFile(int fd, ev_off_t offset, ev_off_t length,
std::string *error_message);
void AddReference(const void *data, size_t datlen,
evbuffer_ref_cleanup_cb cleanupfn, void *cleanupfn_arg) {
CHECK_EQ(
0, evbuffer_add_reference(buf_, data, datlen, cleanupfn, cleanupfn_arg))
<< strerror(errno);
}
private:
struct evbuffer *buf_;
};
@ -91,6 +98,7 @@ struct ByteRange {
ByteRange(int64_t begin, int64_t end) : begin(begin), end(end) {}
int64_t begin = 0;
int64_t end = 0; // exclusive.
int64_t size() const { return end - begin; }
bool operator==(const ByteRange &o) const {
return begin == o.begin && end == o.end;
}
@ -105,20 +113,45 @@ inline std::ostream &operator<<(std::ostream &out, const ByteRange &range) {
void HttpSendError(evhttp_request *req, int http_err, const std::string &prefix,
int posix_errno);
class VirtualFile {
class FileSlice {
public:
virtual ~FileSlice() {}
virtual int64_t size() const = 0;
virtual bool AddRange(ByteRange range, EvBuffer *buf,
std::string *error_message) const = 0;
};
class VirtualFile : public FileSlice {
public:
virtual ~VirtualFile() {}
// Return the given property of the file.
virtual int64_t size() const = 0;
virtual time_t last_modified() const = 0;
virtual std::string etag() const = 0;
virtual std::string mime_type() const = 0;
virtual std::string filename() const = 0; // for logging.
};
// Add the given range of the file to the buffer.
virtual bool AddRange(ByteRange range, EvBuffer *buf,
std::string *error_message) const = 0;
// A FileSlice of a pre-defined length which calls a function which fills the
// slice on demand. The FillerFileSlice is responsible for subsetting.
class FillerFileSlice : public FileSlice {
public:
using FillFunction =
std::function<bool(std::string *slice, std::string *error_message)>;
void Init(size_t size, FillFunction fn) {
fn_ = fn;
size_ = size;
}
int64_t size() const final { return size_; }
bool AddRange(ByteRange range, EvBuffer *buf,
std::string *error_message) const final;
private:
FillFunction fn_;
size_t size_;
};
// Serve an HTTP request |req| from |file|, handling byte range and

158
src/mp4-test.cc Normal file
View File

@ -0,0 +1,158 @@
// This file is part of Moonfire DVR, a security camera digital video recorder.
// Copyright (C) 2015 Scott Lamb <slamb@slamb.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// 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.
#include <gflags/gflags.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "http.h"
#include "mp4.h"
#include "string.h"
DECLARE_bool(alsologtostderr);
using moonfire_nvr::internal::Mp4SampleTablePieces;
namespace moonfire_nvr {
namespace {
std::string ToHex(const FileSlice *slice) {
EvBuffer buf;
std::string error_message;
size_t size = slice->size();
CHECK(slice->AddRange(ByteRange(0, size), &buf, &error_message))
<< error_message;
CHECK_EQ(size, evbuffer_get_length(buf.get()));
return ::moonfire_nvr::ToHex(re2::StringPiece(
reinterpret_cast<const char *>(evbuffer_pullup(buf.get(), size)), size));
}
TEST(Mp4SampleTablePiecesTest, Stts) {
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.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()));
}
TEST(Mp4SampleTablePiecesTest, SttsAfterSyncSample) {
SampleIndexEncoder encoder;
for (int i = 1; i <= 5; ++i) {
encoder.AddSample(i, 2 * i, i == 1);
}
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()));
}
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()));
}
TEST(Mp4SampleTablePiecesTest, Stsc) {
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(1, pieces.stsc_entry_count());
const char kExpectedEntries[] = "00 00 00 0a 00 00 00 04 00 00 00 02";
EXPECT_EQ(kExpectedEntries, ToHex(pieces.stsc_entries()));
}
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()));
}
} // namespace
} // namespace moonfire_nvr
int main(int argc, char **argv) {
FLAGS_alsologtostderr = true;
google::ParseCommandLineFlags(&argc, &argv, true);
testing::InitGoogleTest(&argc, argv);
google::InitGoogleLogging(argv[0]);
return RUN_ALL_TESTS();
}

165
src/mp4.cc Normal file
View File

@ -0,0 +1,165 @@
// This file is part of Moonfire DVR, a security camera network video recorder.
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// 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.cc: implementation of mp4.h interface.
#include "mp4.h"
#include "coding.h"
namespace moonfire_nvr {
namespace internal {
bool Mp4SampleTablePieces::Init(re2::StringPiece video_index_blob,
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;
}
// Process this frame.
frames_++;
if (it.is_key()) {
key_frames_++;
}
// This is the current best candidate to end.
actual_end_90k_ = it.end_90k();
}
sample_pos_.end = it.pos();
if (it.has_error()) {
*error_message = it.error();
return false;
}
VLOG(1) << "requested ts [" << start_90k << ", " << end_90k << "), got ts ["
<< begin_.start_90k() << ", " << actual_end_90k_ << "), " << frames_
<< " frames (" << key_frames_
<< " key), byte positions: " << sample_pos_;
stts_entries_.Init(2 * sizeof(int32_t) * stts_entry_count(),
[this](std::string *s, std::string *error_message) {
return FillSttsEntries(s, error_message);
});
stss_entries_.Init(sizeof(int32_t) * stss_entry_count(),
[this](std::string *s, std::string *error_message) {
return FillStssEntries(s, error_message);
});
stsc_entries_.Init(3 * sizeof(int32_t) * stsc_entry_count(),
[this](std::string *s, std::string *error_message) {
return FillStscEntries(s, error_message);
});
stsz_entries_.Init(sizeof(int32_t) * stsz_entry_count(),
[this](std::string *s, std::string *error_message) {
return FillStszEntries(s, error_message);
});
return true;
}
bool Mp4SampleTablePieces::FillSttsEntries(std::string *s,
std::string *error_message) const {
SampleIndexIterator it;
for (it = begin_; !it.done() && it.start_90k() < desired_end_90k_;
it.Next()) {
AppendU32(1, s);
AppendU32(it.duration_90k(), s);
}
if (it.has_error()) {
*error_message = it.error();
return false;
}
return true;
}
bool Mp4SampleTablePieces::FillStssEntries(std::string *s,
std::string *error_message) const {
SampleIndexIterator it;
uint32_t sample_num = sample_offset_;
for (it = begin_; !it.done() && it.start_90k() < desired_end_90k_;
it.Next()) {
if (it.is_key()) {
Append32(sample_num, s);
}
sample_num++;
}
if (it.has_error()) {
*error_message = it.error();
return false;
}
return true;
}
bool Mp4SampleTablePieces::FillStscEntries(std::string *s,
std::string *error_message) const {
Append32(sample_offset_, s);
Append32(frames_, s);
Append32(sample_entry_index_, s);
return true;
}
bool Mp4SampleTablePieces::FillStszEntries(std::string *s,
std::string *error_message) const {
SampleIndexIterator it;
for (it = begin_; !it.done() && it.start_90k() < desired_end_90k_;
it.Next()) {
Append32(it.bytes(), s);
}
if (it.has_error()) {
*error_message = it.error();
return false;
}
return true;
}
} // namespace internal
} // namespace moonfire_nvr

125
src/mp4.h Normal file
View File

@ -0,0 +1,125 @@
// This file is part of Moonfire NVR, a security camera network video recorder.
// Copyright (C) 2016 Scott Lamb <slamb@slamb.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// 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.h: interface for building VirtualFiles representing ISO/IEC 14496-12
// (ISO base media format / MPEG-4 / .mp4) video. These can be constructed
// from one or more recordings and are suitable for HTTP range serving or
// download.
#ifndef MOONFIRE_NVR_MP4_H
#define MOONFIRE_NVR_MP4_H
#include "recording.h"
#include "http.h"
namespace moonfire_nvr {
namespace internal {
// Represents pieces of .mp4 sample tables for one recording. Many recordings,
// and thus many of these objects, may be spliced together into a single
// virtual .mp4 file. For internal use by Mp4FileBuilder. Exposed for testing.
class Mp4SampleTablePieces {
public:
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.
//
// |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
// be shared with other recordings.
//
// |sample_offset| should be the (1-based) index of the first sample in
// this file. It should be 1 + the sum of all previous Mp4SampleTablePieces'
// samples() values.
//
// |start_90k| and |end_90k| should be relative to the start of the recording.
// They indicate the *desired* time range. The *actual* time range will be
// 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,
int32_t sample_offset, int32_t start_90k, int32_t end_90k,
std::string *error_message);
int32_t stts_entry_count() const { return frames_; }
const FileSlice *stts_entries() const { return &stts_entries_; }
int32_t stss_entry_count() const { return key_frames_; }
const FileSlice *stss_entries() const { return &stss_entries_; }
int32_t stsc_entry_count() const { return 1; }
const FileSlice *stsc_entries() const { return &stsc_entries_; }
int32_t stsz_entry_count() const { return frames_; }
const FileSlice *stsz_entries() const { return &stsz_entries_; }
int32_t samples() const { return frames_; }
// Return the byte range in the sample file of the frames represented here.
ByteRange sample_pos() const { return sample_pos_; }
uint64_t duration_90k() const { return actual_end_90k_ - begin_.start_90k(); }
private:
bool FillSttsEntries(std::string *s, std::string *error_message) const;
bool FillStssEntries(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;
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_;
ByteRange sample_pos_;
FillerFileSlice stts_entries_;
FillerFileSlice stss_entries_;
FillerFileSlice stsc_entries_;
FillerFileSlice stsz_entries_;
int sample_entry_index_ = -1;
int32_t sample_offset_ = -1;
int32_t desired_end_90k_ = -1;
int32_t actual_end_90k_ = -1;
int32_t frames_ = 0;
int32_t key_frames_ = 0;
};
} // namespace internal
} // namespace moonfire_nvr
#endif // MOONFIRE_NVR_MP4_H