diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 55ef983..5ebc8fe 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 diff --git a/src/coding.h b/src/coding.h index f31efe0..f0e8513 100644 --- a/src/coding.h +++ b/src/coding.h @@ -144,6 +144,11 @@ inline void AppendU32(uint32_t in, std::string *out) { out->append(reinterpret_cast(&net), sizeof(uint32_t)); } +inline void Append32(int32_t in, std::string *out) { + int32_t net = ToNetwork32(in); + out->append(reinterpret_cast(&net), sizeof(int32_t)); +} + } // namespace moonfire_nvr #endif // MOONFIRE_NVR_CODING_H diff --git a/src/http.cc b/src/http.cc index d50607c..231f403 100644 --- a/src/http.cc +++ b/src/http.cc @@ -40,6 +40,8 @@ #include #include +#include + #include #include #include @@ -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 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(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, diff --git a/src/http.h b/src/http.h index 334bb62..b9fa38c 100644 --- a/src/http.h +++ b/src/http.h @@ -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; + + 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 diff --git a/src/mp4-test.cc b/src/mp4-test.cc new file mode 100644 index 0000000..b16643a --- /dev/null +++ b/src/mp4-test.cc @@ -0,0 +1,158 @@ +// This file is part of Moonfire DVR, a security camera digital video recorder. +// Copyright (C) 2015 Scott Lamb +// +// 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 . +// +// mp4_test.cc: tests of the mp4.h interface. + +#include +#include +#include + +#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(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(); +} diff --git a/src/mp4.cc b/src/mp4.cc new file mode 100644 index 0000000..c17fedf --- /dev/null +++ b/src/mp4.cc @@ -0,0 +1,165 @@ +// This file is part of Moonfire DVR, a security camera network video recorder. +// Copyright (C) 2016 Scott Lamb +// +// 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 . +// +// 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 diff --git a/src/mp4.h b/src/mp4.h new file mode 100644 index 0000000..8e11515 --- /dev/null +++ b/src/mp4.h @@ -0,0 +1,125 @@ +// This file is part of Moonfire NVR, a security camera network video recorder. +// Copyright (C) 2016 Scott Lamb +// +// 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 . +// +// 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