From 07378e806c4f6c1b2b228003ce824a1441b96b14 Mon Sep 17 00:00:00 2001 From: Jacob Trimble Date: Wed, 28 Oct 2015 10:23:08 -0700 Subject: [PATCH] Added WebM muxer. * Added WebM muxer that supports both single and multi-segment modes. * WebM muxer supports MPD generation. * Enabled WebM muxer and demuxer in packager. b/22463220 b/25089245 b/25089714 Change-Id: I9f6e8fc51e08fbb1d685229c5cb54ea60f7bed8f --- packager/app/packager_main.cc | 25 +- packager/media/file/file.cc | 40 +++ packager/media/file/file.h | 14 + packager/media/formats/webm/mkv_writer.cc | 86 ++++++ packager/media/formats/webm/mkv_writer.h | 73 +++++ .../formats/webm/multi_segment_segmenter.cc | 91 ++++++ .../formats/webm/multi_segment_segmenter.h | 58 ++++ packager/media/formats/webm/seek_head.cc | 123 ++++++++ packager/media/formats/webm/seek_head.h | 52 ++++ packager/media/formats/webm/segmenter.cc | 275 ++++++++++++++++++ packager/media/formats/webm/segmenter.h | 154 ++++++++++ .../formats/webm/single_segment_segmenter.cc | 88 ++++++ .../formats/webm/single_segment_segmenter.h | 63 ++++ .../webm/two_pass_single_segment_segmenter.cc | 184 ++++++++++++ .../webm/two_pass_single_segment_segmenter.h | 52 ++++ packager/media/formats/webm/webm.gyp | 15 + .../media/formats/webm/webm_media_parser.cc | 31 +- packager/media/formats/webm/webm_muxer.cc | 111 +++++++ packager/media/formats/webm/webm_muxer.h | 44 +++ 19 files changed, 1566 insertions(+), 13 deletions(-) create mode 100644 packager/media/formats/webm/mkv_writer.cc create mode 100644 packager/media/formats/webm/mkv_writer.h create mode 100644 packager/media/formats/webm/multi_segment_segmenter.cc create mode 100644 packager/media/formats/webm/multi_segment_segmenter.h create mode 100644 packager/media/formats/webm/seek_head.cc create mode 100644 packager/media/formats/webm/seek_head.h create mode 100644 packager/media/formats/webm/segmenter.cc create mode 100644 packager/media/formats/webm/segmenter.h create mode 100644 packager/media/formats/webm/single_segment_segmenter.cc create mode 100644 packager/media/formats/webm/single_segment_segmenter.h create mode 100644 packager/media/formats/webm/two_pass_single_segment_segmenter.cc create mode 100644 packager/media/formats/webm/two_pass_single_segment_segmenter.h create mode 100644 packager/media/formats/webm/webm_muxer.cc create mode 100644 packager/media/formats/webm/webm_muxer.h diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index ab986b2630..47696ca329 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -19,6 +19,7 @@ #include "packager/base/logging.h" #include "packager/base/stl_util.h" #include "packager/base/strings/string_split.h" +#include "packager/base/strings/string_util.h" #include "packager/base/strings/stringprintf.h" #include "packager/base/threading/simple_thread.h" #include "packager/base/time/clock.h" @@ -31,6 +32,7 @@ #include "packager/media/event/vod_media_info_dump_muxer_listener.h" #include "packager/media/file/file.h" #include "packager/media/formats/mp4/mp4_muxer.h" +#include "packager/media/formats/webm/webm_muxer.h" #include "packager/mpd/base/dash_iop_mpd_notifier.h" #include "packager/mpd/base/media_info.pb.h" #include "packager/mpd/base/mpd_builder.h" @@ -181,6 +183,25 @@ bool StreamInfoToTextMediaInfo(const StreamDescriptor& stream_descriptor, return true; } +scoped_ptr CreateOutputMuxer(const MuxerOptions& options) { + // TODO(modmaker): Add a config option for output format + const std::string& file_name = options.output_file_name; + if (base::EndsWith(file_name, ".webm", + base::CompareCase::INSENSITIVE_ASCII)) { + return scoped_ptr(new webm::WebMMuxer(options)); + } else if (base::EndsWith(file_name, ".mp4", + base::CompareCase::INSENSITIVE_ASCII) || + base::EndsWith(file_name, ".m4a", + base::CompareCase::INSENSITIVE_ASCII) || + base::EndsWith(file_name, ".m4v", + base::CompareCase::INSENSITIVE_ASCII)) { + return scoped_ptr(new mp4::MP4Muxer(options)); + } else { + LOG(ERROR) << "Unrecognized output format " << file_name; + return NULL; + } +} + bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors, const MuxerOptions& muxer_options, FakeClock* fake_clock, @@ -261,7 +282,9 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors, } DCHECK(!remux_jobs->empty()); - scoped_ptr muxer(new mp4::MP4Muxer(stream_muxer_options)); + scoped_ptr muxer(CreateOutputMuxer(stream_muxer_options)); + if (!muxer) + return false; if (FLAGS_use_fake_clock_for_muxer) muxer->set_clock(fake_clock); if (key_source) { diff --git a/packager/media/file/file.cc b/packager/media/file/file.cc index f495e70ec1..a2929de0c3 100644 --- a/packager/media/file/file.cc +++ b/packager/media/file/file.cc @@ -6,6 +6,8 @@ #include "packager/media/file/file.h" +#include + #include #include "packager/base/logging.h" #include "packager/base/memory/scoped_ptr.h" @@ -205,5 +207,43 @@ bool File::Copy(const char* from_file_name, const char* to_file_name) { return true; } +int64_t File::CopyFile(File* source, File* destination) { + return CopyFile(source, destination, kWholeFile); +} + +int64_t File::CopyFile(File* source, File* destination, int64_t max_copy) { + DCHECK(source); + DCHECK(destination); + if (max_copy < 0) + max_copy = std::numeric_limits::max(); + + const int64_t kBufferSize = 0x40000; // 256KB. + scoped_ptr buffer(new uint8_t[kBufferSize]); + int64_t bytes_copied = 0; + while (bytes_copied < max_copy) { + const int64_t size = std::min(kBufferSize, max_copy - bytes_copied); + const int64_t bytes_read = source->Read(buffer.get(), size); + if (bytes_read < 0) + return bytes_read; + if (bytes_read == 0) + break; + + int64_t total_bytes_written = 0; + while (total_bytes_written < bytes_read) { + const int64_t bytes_written = destination->Write( + buffer.get() + total_bytes_written, bytes_read - total_bytes_written); + if (bytes_written < 0) + return bytes_written; + + total_bytes_written += bytes_written; + } + + DCHECK_EQ(total_bytes_written, bytes_read); + bytes_copied += bytes_read; + } + + return bytes_copied; +} + } // namespace media } // namespace edash_packager diff --git a/packager/media/file/file.h b/packager/media/file/file.h index 9c4489061b..f1a54d035b 100644 --- a/packager/media/file/file.h +++ b/packager/media/file/file.h @@ -17,6 +17,7 @@ namespace edash_packager { namespace media { extern const char* kLocalFilePrefix; +const int64_t kWholeFile = -1; /// Define an abstract file interface. class File { @@ -110,6 +111,19 @@ class File { /// @return true on success, false otherwise. static bool Copy(const char* from_file_name, const char* to_file_name); + /// Copies the contents from source to destination. + /// @param source The file to copy from. + /// @param destination The file to copy to. + /// @return Number of bytes written, or a value < 0 on error. + static int64_t CopyFile(File* source, File* destination); + + /// Copies the contents from source to destination. + /// @param source The file to copy from. + /// @param destination The file to copy to. + /// @param max_copy The maximum number of bytes to copy; < 0 to copy to EOF. + /// @return Number of bytes written, or a value < 0 on error. + static int64_t CopyFile(File* source, File* destination, int64_t max_copy); + protected: explicit File(const std::string& file_name) : file_name_(file_name) {} /// Do *not* call the destructor directly (with the "delete" keyword) diff --git a/packager/media/formats/webm/mkv_writer.cc b/packager/media/formats/webm/mkv_writer.cc new file mode 100644 index 0000000000..634c4d5800 --- /dev/null +++ b/packager/media/formats/webm/mkv_writer.cc @@ -0,0 +1,86 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webm/mkv_writer.h" + +namespace edash_packager { +namespace media { + +MkvWriter::MkvWriter() : position_(0) {} + +MkvWriter::~MkvWriter() {} + +Status MkvWriter::Open(const std::string& name) { + DCHECK(!file_); + file_.reset(File::Open(name.c_str(), "w")); + if (!file_) + return Status(error::FILE_FAILURE, "Unable to open file for writing."); + + // This may produce an error message; however there isn't a seekable method + // on File. + seekable_ = file_->Seek(0); + position_ = 0; + return Status::OK; +} + +mkvmuxer::int32 MkvWriter::Write(const void* buf, mkvmuxer::uint32 len) { + DCHECK(file_); + + const char* data = reinterpret_cast(buf); + int64_t total_bytes_written = 0; + while (total_bytes_written < len) { + const int64_t written = + file_->Write(data + total_bytes_written, len - total_bytes_written); + if (written < 0) + return written; + + total_bytes_written += written; + } + + DCHECK_EQ(total_bytes_written, len); + position_ += len; + return 0; +} + +int64_t MkvWriter::WriteFromFile(File* source) { + return WriteFromFile(source, kWholeFile); +} + +int64_t MkvWriter::WriteFromFile(File* source, uint64_t max_copy) { + DCHECK(file_); + + const int64_t size = File::CopyFile(source, file_.get(), max_copy); + if (size < 0) + return size; + + position_ += size; + return size; +} + +mkvmuxer::int64 MkvWriter::Position() const { + return position_; +} + +mkvmuxer::int32 MkvWriter::Position(mkvmuxer::int64 position) { + DCHECK(file_); + + if (file_->Seek(position)) { + position_ = position; + return 0; + } else { + return -1; + } +} + +bool MkvWriter::Seekable() const { + return seekable_; +} + +void MkvWriter::ElementStartNotify(mkvmuxer::uint64 element_id, + mkvmuxer::int64 position) {} + +} // namespace media +} // namespace edash_packager diff --git a/packager/media/formats/webm/mkv_writer.h b/packager/media/formats/webm/mkv_writer.h new file mode 100644 index 0000000000..8705a3cc59 --- /dev/null +++ b/packager/media/formats/webm/mkv_writer.h @@ -0,0 +1,73 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MEDIA_FORMATS_WEBM_MKV_WRITER_H_ +#define MEDIA_FORMATS_WEBM_MKV_WRITER_H_ + +#include + +#include "packager/base/memory/scoped_ptr.h" +#include "packager/media/base/status.h" +#include "packager/media/file/file_closer.h" +#include "packager/third_party/libwebm/src/mkvmuxer.hpp" + +namespace edash_packager { +namespace media { + +/// An implementation of IMkvWriter using our File type. +class MkvWriter : public mkvmuxer::IMkvWriter { + public: + MkvWriter(); + ~MkvWriter() override; + + /// Opens the given file for writing. This MUST be called before any other + /// calls. + /// @param name The path to the file to open. + /// @return Whether the operation succeeded. + Status Open(const std::string& name); + + /// Writes out @a len bytes of @a buf. + /// @return 0 on success. + mkvmuxer::int32 Write(const void* buf, mkvmuxer::uint32 len) override; + /// @return The offset of the output position from the beginning of the + /// output. + mkvmuxer::int64 Position() const override; + /// Set the current File position. + /// @return 0 on success. + mkvmuxer::int32 Position(mkvmuxer::int64 position) override; + /// @return true if the writer is seekable. + bool Seekable() const override; + /// Element start notification. Called whenever an element identifier is about + /// to be written to the stream. @a element_id is the element identifier, and + /// @a position is the location in the WebM stream where the first octet of + /// the element identifier will be written. + /// Note: the |MkvId| enumeration in webmids.hpp defines element values. + void ElementStartNotify(mkvmuxer::uint64 element_id, + mkvmuxer::int64 position) override; + + /// Writes the contents of the given file to this file. + /// @return The number of bytes written; or < 0 on error. + int64_t WriteFromFile(File* source); + /// Writes the contents of the given file to this file, up to a maximum + /// number of bytes. If @a max_copy is negative, will copy to EOF. + /// @return The number of bytes written; or < 0 on error. + int64_t WriteFromFile(File* source, uint64_t max_copy); + + File* file() { return file_.get(); } + + private: + scoped_ptr file_; + // Keep track of the position and whether we can seek. + mkvmuxer::int64 position_; + bool seekable_; + + DISALLOW_COPY_AND_ASSIGN(MkvWriter); +}; + +} // namespace media +} // namespace edash_packager + +#endif // MEDIA_FORMATS_WEBM_MKV_WRITER_H_ diff --git a/packager/media/formats/webm/multi_segment_segmenter.cc b/packager/media/formats/webm/multi_segment_segmenter.cc new file mode 100644 index 0000000000..4f6da88146 --- /dev/null +++ b/packager/media/formats/webm/multi_segment_segmenter.cc @@ -0,0 +1,91 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webm/multi_segment_segmenter.h" + +#include "packager/media/base/media_stream.h" +#include "packager/media/base/muxer_options.h" +#include "packager/media/base/muxer_util.h" +#include "packager/media/base/stream_info.h" +#include "packager/media/event/muxer_listener.h" +#include "packager/third_party/libwebm/src/mkvmuxer.hpp" + +namespace edash_packager { +namespace media { +namespace webm { +MultiSegmentSegmenter::MultiSegmentSegmenter(const MuxerOptions& options) + : Segmenter(options), num_segment_(0) {} + +MultiSegmentSegmenter::~MultiSegmentSegmenter() {} + +bool MultiSegmentSegmenter::GetInitRangeStartAndEnd(uint32_t* start, + uint32_t* end) { + return false; +} + +bool MultiSegmentSegmenter::GetIndexRangeStartAndEnd(uint32_t* start, + uint32_t* end) { + return false; +} + +Status MultiSegmentSegmenter::DoInitialize(scoped_ptr writer) { + writer_ = writer.Pass(); + return WriteSegmentHeader(0, writer_.get()); +} + +Status MultiSegmentSegmenter::DoFinalize() { + return FinalizeSegment(); +} + +Status MultiSegmentSegmenter::FinalizeSegment() { + if (!cluster()->Finalize()) + return Status(error::FILE_FAILURE, "Error finalizing segment."); + + if (muxer_listener()) { + const uint64_t size = cluster()->Size(); + const uint64_t start_webm_timecode = cluster()->timecode(); + const uint64_t start_timescale = FromWebMTimecode(start_webm_timecode); + const uint64_t length = static_cast( + cluster_length_sec() * stream()->info()->time_scale()); + muxer_listener()->OnNewSegment(start_timescale, length, size); + } + + VLOG(1) << "WEBM file '" << writer_->file()->file_name() << "' finalized."; + return Status::OK; +} + +Status MultiSegmentSegmenter::NewSubsegment(uint64_t start_timescale) { + if (cluster() && !cluster()->Finalize()) + return Status(error::FILE_FAILURE, "Error finalizing segment."); + + uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale); + return SetCluster(start_webm_timecode, 0, writer_.get()); +} + +Status MultiSegmentSegmenter::NewSegment(uint64_t start_timescale) { + if (cluster()) { + Status temp = FinalizeSegment(); + if (!temp.ok()) + return temp; + } + + // Create a new file for the new segment. + std::string segment_name = + GetSegmentName(options().segment_template, start_timescale, num_segment_, + options().bandwidth); + writer_.reset(new MkvWriter); + Status status = writer_->Open(segment_name); + if (!status.ok()) + return status; + num_segment_++; + + uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale); + return SetCluster(start_webm_timecode, 0, writer_.get()); +} + +} // namespace webm +} // namespace media +} // namespace edash_packager diff --git a/packager/media/formats/webm/multi_segment_segmenter.h b/packager/media/formats/webm/multi_segment_segmenter.h new file mode 100644 index 0000000000..74015630ab --- /dev/null +++ b/packager/media/formats/webm/multi_segment_segmenter.h @@ -0,0 +1,58 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MEDIA_FORMATS_WEBM_MULTI_SEGMENT_SEGMENTER_H_ +#define MEDIA_FORMATS_WEBM_MULTI_SEGMENT_SEGMENTER_H_ + +#include "packager/media/formats/webm/segmenter.h" + +#include "packager/base/memory/scoped_ptr.h" +#include "packager/media/base/status.h" +#include "packager/media/formats/webm/mkv_writer.h" + +namespace edash_packager { +namespace media { + +struct MuxerOptions; + +namespace webm { + +/// An implementation of a Segmenter for a multi-segment. Since this does not +/// use seeking, it does not matter if the underlying files support seeking. +class MultiSegmentSegmenter : public Segmenter { + public: + explicit MultiSegmentSegmenter(const MuxerOptions& options); + ~MultiSegmentSegmenter() override; + + /// @name Segmenter implementation overrides. + /// @{ + bool GetInitRangeStartAndEnd(uint32_t* start, uint32_t* end) override; + bool GetIndexRangeStartAndEnd(uint32_t* start, uint32_t* end) override; + /// @} + + protected: + // Segmenter implementation overrides. + Status DoInitialize(scoped_ptr writer) override; + Status DoFinalize() override; + + private: + // Segmenter implementation overrides. + Status NewSubsegment(uint64_t start_timescale) override; + Status NewSegment(uint64_t start_timescale) override; + + Status FinalizeSegment(); + + scoped_ptr writer_; + uint32_t num_segment_; + + DISALLOW_COPY_AND_ASSIGN(MultiSegmentSegmenter); +}; + +} // namespace webm +} // namespace media +} // namespace edash_packager + +#endif // MEDIA_FORMATS_WEBM_MULTI_SEGMENT_SEGMENTER_H_ diff --git a/packager/media/formats/webm/seek_head.cc b/packager/media/formats/webm/seek_head.cc new file mode 100644 index 0000000000..60a3ca6792 --- /dev/null +++ b/packager/media/formats/webm/seek_head.cc @@ -0,0 +1,123 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webm/seek_head.h" + +#include + +#include "packager/third_party/libwebm/src/mkvmuxerutil.hpp" +#include "packager/third_party/libwebm/src/webmids.hpp" + +namespace edash_packager { +namespace media { +namespace { +const mkvmuxer::uint64 kElementIds[] = {mkvmuxer::kMkvCluster, + mkvmuxer::kMkvCues, mkvmuxer::kMkvInfo, + mkvmuxer::kMkvTracks}; +const int kElementIdCount = arraysize(kElementIds); + +uint64_t MaxSeekEntrySize() { + const uint64_t max_entry_payload_size = + EbmlElementSize( + mkvmuxer::kMkvSeekID, + static_cast(std::numeric_limits::max())) + + EbmlElementSize(mkvmuxer::kMkvSeekPosition, + std::numeric_limits::max()); + const uint64_t max_entry_size = + EbmlMasterElementSize(mkvmuxer::kMkvSeek, max_entry_payload_size) + + max_entry_payload_size; + + return max_entry_size; +} +} // namespace + +SeekHead::SeekHead() + : cluster_pos_(-1), + cues_pos_(-1), + info_pos_(-1), + tracks_pos_(-1), + wrote_void_(false) {} + +SeekHead::~SeekHead() {} + +bool SeekHead::Write(mkvmuxer::IMkvWriter* writer) { + std::vector element_sizes; + const uint64_t payload_size = GetPayloadSize(&element_sizes); + + if (payload_size == 0) { + return true; + } + + const int64_t start_pos = writer->Position(); + if (!WriteEbmlMasterElement(writer, mkvmuxer::kMkvSeekHead, payload_size)) + return false; + + const int64_t positions[] = {cluster_pos_, cues_pos_, info_pos_, tracks_pos_}; + for (int i = 0; i < kElementIdCount; ++i) { + if (element_sizes[i] == 0) + continue; + + const mkvmuxer::uint64 position = + static_cast(positions[i]); + if (!WriteEbmlMasterElement(writer, mkvmuxer::kMkvSeek, element_sizes[i]) || + !WriteEbmlElement(writer, mkvmuxer::kMkvSeekID, kElementIds[i]) || + !WriteEbmlElement(writer, mkvmuxer::kMkvSeekPosition, position)) + return false; + } + + // If we wrote void before, then fill in the extra with void. + if (wrote_void_) { + const uint64_t max_payload_size = kElementIdCount * MaxSeekEntrySize(); + const uint64_t total_void_size = + EbmlMasterElementSize(mkvmuxer::kMkvSeekHead, max_payload_size) + + max_payload_size; + + const uint64_t extra_void = + total_void_size - (writer->Position() - start_pos); + if (!WriteVoidElement(writer, extra_void)) + return false; + } + + return true; +} + +bool SeekHead::WriteVoid(mkvmuxer::IMkvWriter* writer) { + const uint64_t payload_size = kElementIdCount * MaxSeekEntrySize(); + const uint64_t total_size = + EbmlMasterElementSize(mkvmuxer::kMkvSeekHead, payload_size) + + payload_size; + + wrote_void_ = true; + const uint64_t written = WriteVoidElement(writer, total_size); + if (!written) + return false; + + return true; +} + +uint64_t SeekHead::GetPayloadSize(std::vector* data) { + const int64_t positions[] = {cluster_pos_, cues_pos_, info_pos_, tracks_pos_}; + uint64_t total_payload_size = 0; + data->resize(kElementIdCount); + for (int i = 0; i < kElementIdCount; ++i) { + if (positions[i] < 0) { + (*data)[i] = 0; + continue; + } + + const mkvmuxer::uint64 position = + static_cast(positions[i]); + (*data)[i] = EbmlElementSize(mkvmuxer::kMkvSeekID, kElementIds[i]) + + EbmlElementSize(mkvmuxer::kMkvSeekPosition, position); + total_payload_size += + data->at(i) + EbmlMasterElementSize(mkvmuxer::kMkvSeek, data->at(i)); + } + + return total_payload_size; +} + +} // namespace media +} // namespace edash_packager diff --git a/packager/media/formats/webm/seek_head.h b/packager/media/formats/webm/seek_head.h new file mode 100644 index 0000000000..438f4b0aac --- /dev/null +++ b/packager/media/formats/webm/seek_head.h @@ -0,0 +1,52 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MEDIA_FORMATS_WEBM_SEEK_HEAD_H_ +#define MEDIA_FORMATS_WEBM_SEEK_HEAD_H_ + +#include +#include + +#include "base/macros.h" +#include "packager/third_party/libwebm/src/mkvmuxer.hpp" + +namespace edash_packager { +namespace media { + +/// Used to write the SeekHead to the output stream. This supports non-seekable +/// files and setting the values before write; this also supports updating. +class SeekHead { + public: + SeekHead(); + ~SeekHead(); + + /// Writes the seek head to the given writer. This should only be called + /// once. For seekable files, use WriteVoid first, then call this method. + bool Write(mkvmuxer::IMkvWriter* writer); + /// Writes a void element large enough to fit the SeekHead. + bool WriteVoid(mkvmuxer::IMkvWriter* writer); + + void set_cluster_pos(uint64_t pos) { cluster_pos_ = pos; } + void set_cues_pos(uint64_t pos) { cues_pos_ = pos; } + void set_info_pos(uint64_t pos) { info_pos_ = pos; } + void set_tracks_pos(uint64_t pos) { tracks_pos_ = pos; } + + private: + uint64_t GetPayloadSize(std::vector* data); + + int64_t cluster_pos_; + int64_t cues_pos_; + int64_t info_pos_; + int64_t tracks_pos_; + bool wrote_void_; + + DISALLOW_COPY_AND_ASSIGN(SeekHead); +}; + +} // namespace media +} // namespace edash_packager + +#endif // MEDIA_FORMATS_WEBM_SEEK_HEAD_H_ diff --git a/packager/media/formats/webm/segmenter.cc b/packager/media/formats/webm/segmenter.cc new file mode 100644 index 0000000000..b781168699 --- /dev/null +++ b/packager/media/formats/webm/segmenter.cc @@ -0,0 +1,275 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webm/segmenter.h" + +#include "packager/base/time/time.h" +#include "packager/media/base/audio_stream_info.h" +#include "packager/media/base/media_sample.h" +#include "packager/media/base/media_stream.h" +#include "packager/media/base/muxer_options.h" +#include "packager/media/base/stream_info.h" +#include "packager/media/base/video_stream_info.h" +#include "packager/media/event/muxer_listener.h" +#include "packager/media/event/progress_listener.h" +#include "packager/third_party/libwebm/src/mkvmuxerutil.hpp" +#include "packager/third_party/libwebm/src/webmids.hpp" + +namespace edash_packager { +namespace media { +namespace webm { +namespace { +int64_t kTimecodeScale = 1000000; +int64_t kSecondsToNs = 1000000000L; +} // namespace + +Segmenter::Segmenter(const MuxerOptions& options) + : options_(options), + stream_(NULL), + muxer_listener_(NULL), + progress_listener_(NULL), + progress_target_(0), + accumulated_progress_(0), + total_duration_(0), + sample_duration_(0), + segment_payload_pos_(0), + cluster_length_sec_(0), + segment_length_sec_(0), + track_id_(0) {} + +Segmenter::~Segmenter() {} + +Status Segmenter::Initialize(scoped_ptr writer, + MediaStream* streams, + ProgressListener* progress_listener, + MuxerListener* muxer_listener, + KeySource* encryption_key_source) { + muxer_listener_ = muxer_listener; + stream_ = streams; + + // Use media duration as progress target. + progress_target_ = stream_->info()->duration(); + progress_listener_ = progress_listener; + + segment_info_.Init(); + segment_info_.set_timecode_scale(kTimecodeScale); + if (options().single_segment) { + // Set an initial duration so the duration element is written; will be + // overwritten at the end. + segment_info_.set_duration(1); + } + + // Create the track info. + Status status; + switch (stream_->info()->stream_type()) { + case kStreamVideo: + status = CreateVideoTrack( + static_cast(stream_->info().get())); + break; + case kStreamAudio: + status = CreateAudioTrack( + static_cast(stream_->info().get())); + break; + default: + NOTIMPLEMENTED() << "Not implemented for stream type: " + << stream_->info()->stream_type(); + status = Status(error::UNIMPLEMENTED, "Not implemented for stream type"); + } + if (!status.ok()) + return status; + + return DoInitialize(writer.Pass()); +} + +Status Segmenter::Finalize() { + segment_info_.set_duration(FromBMFFTimescale(total_duration_)); + return DoFinalize(); +} + +Status Segmenter::AddSample(scoped_refptr sample) { + if (sample_duration_ == 0) { + sample_duration_ = sample->duration(); + if (muxer_listener_) + muxer_listener_->OnSampleDurationReady(sample_duration_); + } + + UpdateProgress(sample->duration()); + + // Create a new cluster if needed. + Status status; + if (!cluster_) { + status = NewSegment(sample->pts()); + } else if (segment_length_sec_ >= options_.segment_duration) { + if (sample->is_key_frame() || !options_.segment_sap_aligned) { + status = NewSegment(sample->pts()); + segment_length_sec_ = 0; + cluster_length_sec_ = 0; + } + } else if (cluster_length_sec_ >= options_.fragment_duration) { + if (sample->is_key_frame() || !options_.fragment_sap_aligned) { + status = NewSubsegment(sample->pts()); + cluster_length_sec_ = 0; + } + } + if (!status.ok()) + return status; + + const int64_t time_ns = + sample->pts() * kSecondsToNs / stream_->info()->time_scale(); + if (!cluster_->AddFrame(sample->data(), sample->data_size(), track_id_, + time_ns, sample->is_key_frame())) { + LOG(ERROR) << "Error adding sample to segment."; + return Status(error::FILE_FAILURE, "Error adding sample to segment."); + } + const double duration_sec = + static_cast(sample->duration()) / stream_->info()->time_scale(); + cluster_length_sec_ += duration_sec; + segment_length_sec_ += duration_sec; + total_duration_ += sample->duration(); + + return Status::OK; +} + +float Segmenter::GetDuration() const { + return static_cast(segment_info_.duration()) * + segment_info_.timecode_scale() / kSecondsToNs; +} + +uint64_t Segmenter::FromBMFFTimescale(uint64_t time_timescale) { + // Convert the time from BMFF time_code to WebM timecode scale. + const int64_t time_ns = + kSecondsToNs * time_timescale / stream_->info()->time_scale(); + return time_ns / segment_info_.timecode_scale(); +} + +uint64_t Segmenter::FromWebMTimecode(uint64_t time_webm_timecode) { + // Convert the time to BMFF time_code from WebM timecode scale. + const int64_t time_ns = time_webm_timecode * segment_info_.timecode_scale(); + return time_ns * stream_->info()->time_scale() / kSecondsToNs; +} + +Status Segmenter::WriteSegmentHeader(uint64_t file_size, MkvWriter* writer) { + Status error_status(error::FILE_FAILURE, "Error writing segment header."); + + if (!WriteEbmlHeader(writer)) + return error_status; + + if (WriteID(writer, mkvmuxer::kMkvSegment) != 0) + return error_status; + + const uint64_t segment_size_size = 8; + segment_payload_pos_ = writer->Position() + segment_size_size; + if (file_size > 0) { + // We want the size of the segment element, so subtract the header. + if (WriteUIntSize(writer, file_size - segment_payload_pos_, + segment_size_size) != 0) + return error_status; + if (!seek_head_.Write(writer)) + return error_status; + } else { + if (SerializeInt(writer, mkvmuxer::kEbmlUnknownValue, segment_size_size) != + 0) + return error_status; + // We don't know the header size, so write a placeholder. + if (!seek_head_.WriteVoid(writer)) + return error_status; + } + + if (!segment_info_.Write(writer)) + return error_status; + + if (!tracks_.Write(writer)) + return error_status; + + return Status::OK; +} + +Status Segmenter::SetCluster(uint64_t start_webm_timecode, + uint64_t position, + MkvWriter* writer) { + const uint64_t scale = segment_info_.timecode_scale(); + cluster_.reset(new mkvmuxer::Cluster(start_webm_timecode, position, scale)); + cluster_->Init(writer); + return Status::OK; +} + +void Segmenter::UpdateProgress(uint64_t progress) { + accumulated_progress_ += progress; + if (!progress_listener_ || progress_target_ == 0) + return; + // It might happen that accumulated progress exceeds progress_target due to + // computation errors, e.g. rounding error. Cap it so it never reports > 100% + // progress. + if (accumulated_progress_ >= progress_target_) { + progress_listener_->OnProgress(1.0); + } else { + progress_listener_->OnProgress(static_cast(accumulated_progress_) / + progress_target_); + } +} + +Status Segmenter::CreateVideoTrack(VideoStreamInfo* info) { + // The seed is only used to create a UID which we overwrite later. + unsigned int seed = 0; + mkvmuxer::VideoTrack* track = new mkvmuxer::VideoTrack(&seed); + if (!track) + return Status(error::INTERNAL_ERROR, "Failed to create video track."); + + if (info->codec() == kCodecVP8) { + track->set_codec_id(mkvmuxer::Tracks::kVp8CodecId); + } else if (info->codec() == kCodecVP9) { + track->set_codec_id(mkvmuxer::Tracks::kVp9CodecId); + } else { + LOG(ERROR) << "Only VP8 and VP9 video codec is supported."; + return Status(error::UNIMPLEMENTED, + "Only VP8 and VP9 video codecs are supported."); + } + + track->set_uid(info->track_id()); + track->set_type(mkvmuxer::Tracks::kVideo); + track->set_width(info->width()); + track->set_height(info->height()); + track->set_display_height(info->height()); + track->set_display_width(info->width() * info->pixel_width() / + info->pixel_height()); + track->set_language(info->language().c_str()); + + tracks_.AddTrack(track, info->track_id()); + track_id_ = track->number(); + return Status::OK; +} + +Status Segmenter::CreateAudioTrack(AudioStreamInfo* info) { + // The seed is only used to create a UID which we overwrite later. + unsigned int seed = 0; + mkvmuxer::AudioTrack* track = new mkvmuxer::AudioTrack(&seed); + if (!track) + return Status(error::INTERNAL_ERROR, "Failed to create audio track."); + + if (info->codec() == kCodecOpus) { + track->set_codec_id(mkvmuxer::Tracks::kOpusCodecId); + } else if (info->codec() == kCodecVorbis) { + track->set_codec_id(mkvmuxer::Tracks::kVorbisCodecId); + } else { + LOG(ERROR) << "Only Vorbis and Opus audio codec is supported."; + return Status(error::UNIMPLEMENTED, + "Only Vorbis and Opus audio codecs are supported."); + } + + track->set_uid(info->track_id()); + track->set_language(info->language().c_str()); + track->set_type(mkvmuxer::Tracks::kAudio); + track->set_sample_rate(info->sampling_frequency()); + track->set_channels(info->num_channels()); + + tracks_.AddTrack(track, info->track_id()); + track_id_ = track->number(); + return Status::OK; +} + +} // namespace webm +} // namespace media +} // namespace edash_packager diff --git a/packager/media/formats/webm/segmenter.h b/packager/media/formats/webm/segmenter.h new file mode 100644 index 0000000000..992b52629c --- /dev/null +++ b/packager/media/formats/webm/segmenter.h @@ -0,0 +1,154 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MEDIA_FORMATS_WEBM_SEGMENTER_H_ +#define MEDIA_FORMATS_WEBM_SEGMENTER_H_ + +#include "packager/base/memory/ref_counted.h" +#include "packager/base/memory/scoped_ptr.h" +#include "packager/media/base/status.h" +#include "packager/media/formats/webm/mkv_writer.h" +#include "packager/media/formats/webm/seek_head.h" +#include "packager/third_party/libwebm/src/mkvmuxer.hpp" + +namespace edash_packager { +namespace media { + +struct MuxerOptions; + +class AudioStreamInfo; +class KeySource; +class MediaSample; +class MediaStream; +class MuxerListener; +class ProgressListener; +class StreamInfo; +class VideoStreamInfo; + +namespace webm { + +class Segmenter { + public: + explicit Segmenter(const MuxerOptions& options); + virtual ~Segmenter(); + + /// Initialize the segmenter. + /// Calling other public methods of this class without this method returning + /// Status::OK results in an undefined behavior. + /// @param writer contains the output file (or init file in multi-segment). + /// @param streams contains the MediaStream to be segmented. + /// @param muxer_listener receives muxer events. Can be NULL. + /// @param encryption_key_source points to the key source which contains + /// the encryption keys. It can be NULL to indicate that no encryption + /// is required. + /// @return OK on success, an error status otherwise. + Status Initialize(scoped_ptr writer, + MediaStream* streams, + ProgressListener* progress_listener, + MuxerListener* muxer_listener, + KeySource* encryption_key_source); + + /// Finalize the segmenter. + /// @return OK on success, an error status otherwise. + Status Finalize(); + + /// Add sample to the indicated stream. + /// @param sample points to the sample to be added. + /// @return OK on success, an error status otherwise. + Status AddSample(scoped_refptr sample); + + // TODO(modmaker): Add key source support. + + /// @return true if there is an initialization range, while setting @a start + /// and @a end; or false if initialization range does not apply. + virtual bool GetInitRangeStartAndEnd(uint32_t* start, uint32_t* end) = 0; + + /// @return true if there is an index byte range, while setting @a start + /// and @a end; or false if index byte range does not apply. + virtual bool GetIndexRangeStartAndEnd(uint32_t* start, uint32_t* end) = 0; + + /// @return The total length, in seconds, of segmented media files. + float GetDuration() const; + + protected: + /// Converts the given time in ISO BMFF timescale to the current WebM + /// timecode. + uint64_t FromBMFFTimescale(uint64_t time_timescale); + /// Converts the given time in WebM timecode to ISO BMFF timescale. + uint64_t FromWebMTimecode(uint64_t time_webm_timecode); + /// Writes the Segment header to @a writer. + Status WriteSegmentHeader(uint64_t file_size, MkvWriter* writer); + /// Creates a Cluster object with the given parameters. + Status SetCluster(uint64_t start_webm_timecode, + uint64_t position, + MkvWriter* writer); + + /// Update segmentation progress using ProgressListener. + void UpdateProgress(uint64_t progress); + void set_progress_target(uint64_t target) { progress_target_ = target; } + + const MuxerOptions& options() const { return options_; } + mkvmuxer::Cluster* cluster() { return cluster_.get(); } + mkvmuxer::Cues* cues() { return &cues_; } + MuxerListener* muxer_listener() { return muxer_listener_; } + MediaStream* stream() { return stream_; } + SeekHead* seek_head() { return &seek_head_; } + + int track_id() const { return track_id_; } + uint64_t segment_payload_pos() const { return segment_payload_pos_; } + double cluster_length_sec() const { return cluster_length_sec_; } + + virtual Status DoInitialize(scoped_ptr writer) = 0; + virtual Status DoFinalize() = 0; + + private: + Status CreateVideoTrack(VideoStreamInfo* info); + Status CreateAudioTrack(AudioStreamInfo* info); + + // This is called when there needs to be a new subsegment. This does nothing + // in single-segment mode. In multi-segment mode this creates a new Cluster + // element. + virtual Status NewSubsegment(uint64_t start_timescale) = 0; + // This is called when there needs to be a new segment. In single-segment + // mode, this creates a new Cluster element. In multi-segment mode this + // creates a new output file. + virtual Status NewSegment(uint64_t start_timescale) = 0; + // This is called when a segment ends. This is called right before a call + // to NewSegment and at the start of Finalize. + Status FinalizeSegment(uint64_t end_timescale); + + const MuxerOptions& options_; + + scoped_ptr cluster_; + mkvmuxer::Cues cues_; + SeekHead seek_head_; + mkvmuxer::SegmentInfo segment_info_; + mkvmuxer::Tracks tracks_; + + MediaStream* stream_; + MuxerListener* muxer_listener_; + ProgressListener* progress_listener_; + uint64_t progress_target_; + uint64_t accumulated_progress_; + uint64_t total_duration_; + uint64_t sample_duration_; + // The position (in bytes) of the start of the Segment payload in the init + // file. This is also the size of the header before the SeekHead. + uint64_t segment_payload_pos_; + + double cluster_length_sec_; + double segment_length_sec_; + + int track_id_; + + DISALLOW_COPY_AND_ASSIGN(Segmenter); +}; + +} // namespace webm +} // namespace media +} // namespace edash_packager + +#endif // MEDIA_FORMATS_WEBM_SEGMENTER_H_ diff --git a/packager/media/formats/webm/single_segment_segmenter.cc b/packager/media/formats/webm/single_segment_segmenter.cc new file mode 100644 index 0000000000..e4cf7c6a07 --- /dev/null +++ b/packager/media/formats/webm/single_segment_segmenter.cc @@ -0,0 +1,88 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webm/single_segment_segmenter.h" + +#include "packager/media/base/muxer_options.h" +#include "packager/third_party/libwebm/src/mkvmuxer.hpp" + +namespace edash_packager { +namespace media { +namespace webm { + +SingleSegmentSegmenter::SingleSegmentSegmenter(const MuxerOptions& options) + : Segmenter(options), init_end_(0), index_start_(0) {} + +SingleSegmentSegmenter::~SingleSegmentSegmenter() {} + +Status SingleSegmentSegmenter::DoInitialize(scoped_ptr writer) { + writer_ = writer.Pass(); + Status ret = WriteSegmentHeader(0, writer_.get()); + init_end_ = writer_->Position() - 1; + seek_head()->set_cluster_pos(init_end_ + 1); + return ret; +} + +Status SingleSegmentSegmenter::DoFinalize() { + if (!cluster()->Finalize()) + return Status(error::FILE_FAILURE, "Error finalizing cluster."); + + // Write the Cues to the end of the file. + index_start_ = writer_->Position(); + if (!cues()->Write(writer_.get())) + return Status(error::FILE_FAILURE, "Error writing Cues data."); + + uint64_t file_size = writer_->Position(); + writer_->Position(0); + + Status status = WriteSegmentHeader(file_size, writer_.get()); + writer_->Position(file_size); + return status; +} + +Status SingleSegmentSegmenter::NewSubsegment(uint64_t start_timescale) { + return Status::OK; +} + +Status SingleSegmentSegmenter::NewSegment(uint64_t start_timescale) { + if (cluster() && !cluster()->Finalize()) + return Status(error::FILE_FAILURE, "Error finalizing cluster."); + + // Create a new Cue point. + uint64_t position = writer_->Position(); + uint64_t start_webm_timecode = FromBMFFTimescale(start_timescale); + + mkvmuxer::CuePoint* cue_point = new mkvmuxer::CuePoint; + cue_point->set_time(start_webm_timecode); + cue_point->set_track(track_id()); + cue_point->set_cluster_pos(position - segment_payload_pos()); + if (!cues()->AddCue(cue_point)) + return Status(error::INTERNAL_ERROR, "Error adding CuePoint."); + + return SetCluster(start_webm_timecode, position, writer_.get()); +} + +bool SingleSegmentSegmenter::GetInitRangeStartAndEnd(uint32_t* start, + uint32_t* end) { + // The init range is the header, from the start of the file to the size of + // the header. + *start = 0; + *end = init_end_; + return true; +} + +bool SingleSegmentSegmenter::GetIndexRangeStartAndEnd(uint32_t* start, + uint32_t* end) { + // The index is the Cues element, which is always placed at the end of the + // file. + *start = index_start_; + *end = writer_->file()->Size(); + return true; +} + +} // namespace webm +} // namespace media +} // namespace edash_packager diff --git a/packager/media/formats/webm/single_segment_segmenter.h b/packager/media/formats/webm/single_segment_segmenter.h new file mode 100644 index 0000000000..a53e85b2b9 --- /dev/null +++ b/packager/media/formats/webm/single_segment_segmenter.h @@ -0,0 +1,63 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MEDIA_FORMATS_WEBM_SINGLE_SEGMENT_SEGMENTER_H_ +#define MEDIA_FORMATS_WEBM_SINGLE_SEGMENT_SEGMENTER_H_ + +#include "packager/media/formats/webm/segmenter.h" + +#include "packager/base/memory/scoped_ptr.h" +#include "packager/media/base/status.h" +#include "packager/media/formats/webm/mkv_writer.h" + +namespace edash_packager { +namespace media { + +struct MuxerOptions; + +namespace webm { + +/// An implementation of a Segmenter for a single-segment. This assumes that +/// the output file is seekable. For non-seekable files, use +/// TwoPassSingleSegmentSegmenter. +class SingleSegmentSegmenter : public Segmenter { + public: + explicit SingleSegmentSegmenter(const MuxerOptions& options); + ~SingleSegmentSegmenter() override; + + /// @name Segmenter implementation overrides. + /// @{ + bool GetInitRangeStartAndEnd(uint32_t* start, uint32_t* end) override; + bool GetIndexRangeStartAndEnd(uint32_t* start, uint32_t* end) override; + /// @} + + protected: + MkvWriter* writer() { return writer_.get(); } + void set_init_end(uint64_t end) { init_end_ = end; } + void set_index_start(uint64_t start) { index_start_ = start; } + void set_writer(scoped_ptr writer) { writer_ = writer.Pass(); } + + // Segmenter implementation overrides. + Status DoInitialize(scoped_ptr writer) override; + Status DoFinalize() override; + + private: + // Segmenter implementation overrides. + Status NewSubsegment(uint64_t start_timescale) override; + Status NewSegment(uint64_t start_timescale) override; + + scoped_ptr writer_; + uint32_t init_end_; + uint32_t index_start_; + + DISALLOW_COPY_AND_ASSIGN(SingleSegmentSegmenter); +}; + +} // namespace webm +} // namespace media +} // namespace edash_packager + +#endif // MEDIA_FORMATS_WEBM_SINGLE_SEGMENT_SEGMENTER_H_ diff --git a/packager/media/formats/webm/two_pass_single_segment_segmenter.cc b/packager/media/formats/webm/two_pass_single_segment_segmenter.cc new file mode 100644 index 0000000000..b62a512f4b --- /dev/null +++ b/packager/media/formats/webm/two_pass_single_segment_segmenter.cc @@ -0,0 +1,184 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webm/two_pass_single_segment_segmenter.h" + +#include +#include +#include + +#include + +#include "packager/base/files/file_path.h" +#include "packager/base/strings/stringprintf.h" +#include "packager/base/threading/platform_thread.h" +#include "packager/base/time/time.h" +#include "packager/media/base/media_sample.h" +#include "packager/media/base/media_stream.h" +#include "packager/media/base/muxer_options.h" +#include "packager/media/base/stream_info.h" +#include "packager/third_party/libwebm/src/mkvmuxer.hpp" +#include "packager/third_party/libwebm/src/mkvmuxerutil.hpp" +#include "packager/third_party/libwebm/src/webmids.hpp" + +namespace edash_packager { +namespace media { +namespace webm { +namespace { +/// Create the temp file name using process/thread id and current time. +std::string TempFileName(const MuxerOptions& options) { + // TODO: Move to a common util function and remove other uses. + int32_t tid = static_cast(base::PlatformThread::CurrentId()); + int64_t rand = 0; + if (RAND_bytes(reinterpret_cast(&rand), sizeof(rand)) != 1) { + LOG(WARNING) << "RAND_bytes failed with error: " + << ERR_error_string(ERR_get_error(), NULL); + rand = base::Time::Now().ToInternalValue(); + } + std::string file_prefix = + base::StringPrintf("packager-tempfile-%x-%" PRIx64 ".tmp", tid, rand); + return base::FilePath(options.temp_dir).Append(file_prefix).value(); +} + +// Skips a given number of bytes in a file by reading. This allows +// forward-seeking in non-seekable files. +bool ReadSkip(File* file, int64_t byte_count) { + const int64_t kBufferSize = 0x40000; // 256KB. + scoped_ptr buffer(new char[kBufferSize]); + int64_t bytes_read = 0; + while (bytes_read < byte_count) { + int64_t size = std::min(kBufferSize, byte_count - bytes_read); + int64_t result = file->Read(buffer.get(), size); + // Only give success if there are no errors, not at EOF, and read exactly + // byte_count bytes. + if (result <= 0) + return false; + + bytes_read += result; + } + + DCHECK_EQ(bytes_read, byte_count); + return true; +} +} // namespace + +TwoPassSingleSegmentSegmenter::TwoPassSingleSegmentSegmenter( + const MuxerOptions& options) + : SingleSegmentSegmenter(options) {} + +TwoPassSingleSegmentSegmenter::~TwoPassSingleSegmentSegmenter() {} + +Status +TwoPassSingleSegmentSegmenter::DoInitialize(scoped_ptr writer) { + // Assume the amount of time to copy the temp file as the same amount + // of time as to make it. + set_progress_target(stream()->info()->duration() * 2); + + real_writer_ = writer.Pass(); + + std::string temp_name = TempFileName(options()); + scoped_ptr temp(new MkvWriter); + Status status = temp->Open(temp_name); + if (!status.ok()) + return status; + + return SingleSegmentSegmenter::DoInitialize(temp.Pass()); +} + +Status TwoPassSingleSegmentSegmenter::DoFinalize() { + if (!cluster()->Finalize()) + return Status(error::FILE_FAILURE, "Error finalizing cluster."); + + // Write the Cues to the end of the temp file. + uint64_t cues_pos = writer()->Position(); + set_index_start(cues_pos); + seek_head()->set_cues_pos(cues_pos); + if (!cues()->Write(writer())) + return Status(error::FILE_FAILURE, "Error writing Cues data."); + + // Write the header to the real output file. + Status temp = WriteSegmentHeader(writer()->Position(), real_writer_.get()); + if (!temp.ok()) + return temp; + + // Close the temp file and open it for reading. + std::string temp_name = writer()->file()->file_name(); + set_writer(scoped_ptr()); + scoped_ptr temp_reader(File::Open(temp_name.c_str(), "r")); + if (!temp_reader) + return Status(error::FILE_FAILURE, "Error opening temp file."); + + // Skip the header that has already been written. + uint64_t header_size = real_writer_->Position(); + if (!ReadSkip(temp_reader.get(), header_size)) + return Status(error::FILE_FAILURE, "Error reading temp file."); + + // Copy the rest of the data over. + if (!CopyFileWithClusterRewrite(temp_reader.get(), real_writer_.get(), + cluster()->Size())) + return Status(error::FILE_FAILURE, "Error copying temp file."); + + // Close and delete the temp file. + temp_reader.reset(); + if (!File::Delete(temp_name.c_str())) { + LOG(WARNING) << "Unable to delete temporary file " << temp_name; + } + + // Set the writer back to the real file so GetIndexRangeStartAndEnd works. + set_writer(real_writer_.Pass()); + return Status::OK; +} + +bool TwoPassSingleSegmentSegmenter::CopyFileWithClusterRewrite( + File* source, + MkvWriter* dest, + uint64_t last_size) { + const int cluster_id_size = mkvmuxer::GetUIntSize(mkvmuxer::kMkvCluster); + const int cluster_size_size = 8; // The size of the Cluster size integer. + const int cluster_header_size = cluster_id_size + cluster_size_size; + + // We are at the start of a cluster, so copy the ID. + if (dest->WriteFromFile(source, cluster_id_size) != cluster_id_size) + return false; + + for (int i = 0; i < cues()->cue_entries_size() - 1; ++i) { + // Write the size of the cluster. + const mkvmuxer::CuePoint* cue = cues()->GetCueByIndex(i); + const mkvmuxer::CuePoint* next_cue = cues()->GetCueByIndex(i + 1); + const int64_t cluster_payload_size = + next_cue->cluster_pos() - cue->cluster_pos() - cluster_header_size; + if (mkvmuxer::WriteUIntSize(dest, cluster_payload_size, cluster_size_size)) + return false; + if (!ReadSkip(source, cluster_size_size)) + return false; + + // Copy the cluster and the next cluster's ID. + int64_t to_copy = cluster_payload_size + cluster_id_size; + if (dest->WriteFromFile(source, to_copy) != to_copy) + return false; + + // Update the progress; need to convert from WebM timecode to ISO BMFF. + const uint64_t webm_delta_time = next_cue->time() - cue->time(); + const uint64_t delta_time = FromWebMTimecode(webm_delta_time); + UpdateProgress(delta_time); + } + + // The last cluster takes up until the cues. + const uint64_t last_cluster_payload_size = last_size - cluster_header_size; + if (mkvmuxer::WriteUIntSize(dest, last_cluster_payload_size, + cluster_size_size)) + return false; + if (!ReadSkip(source, cluster_size_size)) + return false; + + // Copy the remaining data (i.e. Cues data). + return dest->WriteFromFile(source) == + static_cast(last_cluster_payload_size + cues()->Size()); +} + +} // namespace webm +} // namespace media +} // namespace edash_packager diff --git a/packager/media/formats/webm/two_pass_single_segment_segmenter.h b/packager/media/formats/webm/two_pass_single_segment_segmenter.h new file mode 100644 index 0000000000..8293ab9bc2 --- /dev/null +++ b/packager/media/formats/webm/two_pass_single_segment_segmenter.h @@ -0,0 +1,52 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MEDIA_FORMATS_WEBM_TWO_PASS_SINGLE_SEGMENT_SEGMENTER_H_ +#define MEDIA_FORMATS_WEBM_TWO_PASS_SINGLE_SEGMENT_SEGMENTER_H_ + +#include "packager/media/formats/webm/single_segment_segmenter.h" + +#include "packager/base/memory/scoped_ptr.h" +#include "packager/media/base/status.h" +#include "packager/media/formats/webm/mkv_writer.h" + +namespace edash_packager { +namespace media { + +struct MuxerOptions; + +namespace webm { + +/// An implementation of a Segmenter for a single-segment that performs two +/// passes. This does not use seeking and is used for non-seekable files. +class TwoPassSingleSegmentSegmenter : public SingleSegmentSegmenter { + public: + explicit TwoPassSingleSegmentSegmenter(const MuxerOptions& options); + ~TwoPassSingleSegmentSegmenter() override; + + // Segmenter implementation overrides. + Status DoInitialize(scoped_ptr writer) override; + Status DoFinalize() override; + + private: + /// Copies the data from source to destination while rewriting the Cluster + /// sizes to the correct values. This assumes that both @a source and + /// @a dest are at the same position and that the headers have already + /// been copied. + bool CopyFileWithClusterRewrite(File* source, + MkvWriter* dest, + uint64_t last_size); + + scoped_ptr real_writer_; + + DISALLOW_COPY_AND_ASSIGN(TwoPassSingleSegmentSegmenter); +}; + +} // namespace webm +} // namespace media +} // namespace edash_packager + +#endif // MEDIA_FORMATS_WEBM_TWO_PASS_SINGLE_SEGMENT_SEGMENTER_H_ diff --git a/packager/media/formats/webm/webm.gyp b/packager/media/formats/webm/webm.gyp index 4e16917ad8..13e26331c6 100644 --- a/packager/media/formats/webm/webm.gyp +++ b/packager/media/formats/webm/webm.gyp @@ -13,6 +13,18 @@ 'target_name': 'webm', 'type': '<(component)', 'sources': [ + 'mkv_writer.cc', + 'mkv_writer.h', + 'multi_segment_segmenter.cc', + 'multi_segment_segmenter.h', + 'seek_head.cc', + 'seek_head.h', + 'segmenter.cc', + 'segmenter.h', + 'single_segment_segmenter.cc', + 'single_segment_segmenter.h', + 'two_pass_single_segment_segmenter.cc', + 'two_pass_single_segment_segmenter.h', 'webm_audio_client.cc', 'webm_audio_client.h', 'webm_cluster_parser.cc', @@ -31,6 +43,8 @@ 'webm_parser.h', 'webm_media_parser.cc', 'webm_media_parser.h', + 'webm_muxer.cc', + 'webm_muxer.h', 'webm_tracks_parser.cc', 'webm_tracks_parser.h', 'webm_video_client.cc', @@ -39,6 +53,7 @@ 'webm_webvtt_parser.h' ], 'dependencies': [ + '../../../third_party/boringssl/boringssl.gyp:boringssl', '../../../third_party/libwebm/libwebm.gyp:libwebm', '../../base/media_base.gyp:base', '../../filters/filters.gyp:filters' diff --git a/packager/media/formats/webm/webm_media_parser.cc b/packager/media/formats/webm/webm_media_parser.cc index 82848a7150..25297e39b6 100644 --- a/packager/media/formats/webm/webm_media_parser.cc +++ b/packager/media/formats/webm/webm_media_parser.cc @@ -182,17 +182,25 @@ int WebMMediaParser::ParseInfoAndTracks(const uint8_t* data, int size) { int64_t duration_in_us = info_parser.duration() * timecode_scale_in_us; std::vector> streams; - scoped_refptr audio_stream_info = - tracks_parser.audio_stream_info(); - streams.push_back(tracks_parser.audio_stream_info()); - streams.back()->set_duration(duration_in_us); - if (streams.back()->is_encrypted()) - OnEncryptedMediaInitData(tracks_parser.audio_encryption_key_id()); + AudioCodec audio_codec = kCodecOpus; + if (tracks_parser.audio_stream_info()) { + streams.push_back(tracks_parser.audio_stream_info()); + streams.back()->set_duration(duration_in_us); + if (streams.back()->is_encrypted()) + OnEncryptedMediaInitData(tracks_parser.audio_encryption_key_id()); + audio_codec = tracks_parser.audio_stream_info()->codec(); + } else { + VLOG(1) << "No audio track info found."; + } - streams.push_back(tracks_parser.video_stream_info()); - streams.back()->set_duration(duration_in_us); - if (streams.back()->is_encrypted()) - OnEncryptedMediaInitData(tracks_parser.video_encryption_key_id()); + if (tracks_parser.video_stream_info()) { + streams.push_back(tracks_parser.video_stream_info()); + streams.back()->set_duration(duration_in_us); + if (streams.back()->is_encrypted()) + OnEncryptedMediaInitData(tracks_parser.video_encryption_key_id()); + } else { + VLOG(1) << "No video track info found."; + } init_cb_.Run(streams); @@ -203,8 +211,7 @@ int WebMMediaParser::ParseInfoAndTracks(const uint8_t* data, int size) { tracks_parser.GetVideoDefaultDuration(timecode_scale_in_us), tracks_parser.text_tracks(), tracks_parser.ignored_tracks(), tracks_parser.audio_encryption_key_id(), - tracks_parser.video_encryption_key_id(), audio_stream_info->codec(), - new_sample_cb_)); + tracks_parser.video_encryption_key_id(), audio_codec, new_sample_cb_)); return bytes_parsed; } diff --git a/packager/media/formats/webm/webm_muxer.cc b/packager/media/formats/webm/webm_muxer.cc new file mode 100644 index 0000000000..4f7f6534af --- /dev/null +++ b/packager/media/formats/webm/webm_muxer.cc @@ -0,0 +1,111 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/formats/webm/webm_muxer.h" + +#include "packager/media/base/media_sample.h" +#include "packager/media/base/media_stream.h" +#include "packager/media/base/stream_info.h" +#include "packager/media/formats/webm/mkv_writer.h" +#include "packager/media/formats/webm/multi_segment_segmenter.h" +#include "packager/media/formats/webm/single_segment_segmenter.h" +#include "packager/media/formats/webm/two_pass_single_segment_segmenter.h" + +namespace edash_packager { +namespace media { +namespace webm { + +WebMMuxer::WebMMuxer(const MuxerOptions& options) : Muxer(options) {} +WebMMuxer::~WebMMuxer() {} + +Status WebMMuxer::Initialize() { + CHECK_EQ(streams().size(), 1U); + + scoped_ptr writer(new MkvWriter); + Status status = writer->Open(options().output_file_name); + if (!status.ok()) + return status; + + if (!options().single_segment) { + segmenter_.reset(new MultiSegmentSegmenter(options())); + } else if (writer->Seekable()) { + segmenter_.reset(new SingleSegmentSegmenter(options())); + } else { + segmenter_.reset(new TwoPassSingleSegmentSegmenter(options())); + } + + Status initialized = + segmenter_->Initialize(writer.Pass(), streams()[0], progress_listener(), + muxer_listener(), encryption_key_source()); + + if (!initialized.ok()) + return initialized; + + FireOnMediaStartEvent(); + return Status::OK; +} + +Status WebMMuxer::Finalize() { + DCHECK(segmenter_); + Status segmenter_finalized = segmenter_->Finalize(); + + if (!segmenter_finalized.ok()) + return segmenter_finalized; + + FireOnMediaEndEvent(); + LOG(INFO) << "WEBM file '" << options().output_file_name << "' finalized."; + return Status::OK; +} + +Status WebMMuxer::DoAddSample(const MediaStream* stream, + scoped_refptr sample) { + DCHECK(segmenter_); + DCHECK(stream == streams()[0]); + return segmenter_->AddSample(sample); +} + +void WebMMuxer::FireOnMediaStartEvent() { + if (!muxer_listener()) + return; + + DCHECK(!streams().empty()) << "Media started without a stream."; + + const uint32_t timescale = streams().front()->info()->time_scale(); + muxer_listener()->OnMediaStart(options(), *streams().front()->info(), + timescale, MuxerListener::kContainerWebM); +} + +void WebMMuxer::FireOnMediaEndEvent() { + if (!muxer_listener()) + return; + + uint32_t init_range_start = 0; + uint32_t init_range_end = 0; + const bool has_init_range = + segmenter_->GetInitRangeStartAndEnd(&init_range_start, &init_range_end); + + uint32_t index_range_start = 0; + uint32_t index_range_end = 0; + const bool has_index_range = segmenter_->GetIndexRangeStartAndEnd( + &index_range_start, &index_range_end); + + const float duration_seconds = segmenter_->GetDuration(); + + const int64_t file_size = + File::GetFileSize(options().output_file_name.c_str()); + if (file_size <= 0) { + LOG(ERROR) << "Invalid file size: " << file_size; + return; + } + + muxer_listener()->OnMediaEnd(has_init_range, init_range_start, init_range_end, + has_index_range, index_range_start, + index_range_end, duration_seconds, file_size); +} + +} // namespace webm +} // namespace media +} // namespace edash_packager diff --git a/packager/media/formats/webm/webm_muxer.h b/packager/media/formats/webm/webm_muxer.h new file mode 100644 index 0000000000..74198d5921 --- /dev/null +++ b/packager/media/formats/webm/webm_muxer.h @@ -0,0 +1,44 @@ +// Copyright 2015 Google Inc. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef MEDIA_FORMATS_WEBM_WEBM_MUXER_H_ +#define MEDIA_FORMATS_WEBM_WEBM_MUXER_H_ + +#include "packager/media/base/muxer.h" + +namespace edash_packager { +namespace media { +namespace webm { + +class Segmenter; + +/// Implements WebM Muxer. +class WebMMuxer : public Muxer { + public: + /// Create a WebMMuxer object from MuxerOptions. + explicit WebMMuxer(const MuxerOptions& options); + ~WebMMuxer() override; + + private: + // Muxer implementation overrides. + Status Initialize() override; + Status Finalize() override; + Status DoAddSample(const MediaStream* stream, + scoped_refptr sample) override; + + void FireOnMediaStartEvent(); + void FireOnMediaEndEvent(); + + scoped_ptr segmenter_; + + DISALLOW_COPY_AND_ASSIGN(WebMMuxer); +}; + +} // namespace webm +} // namespace media +} // namespace edash_packager + +#endif // MEDIA_FORMATS_WEBM_WEBM_MUXER_H_