diff --git a/media/base/muxer.cc b/media/base/muxer.cc index 1723cecde9..ca069f7d5b 100644 --- a/media/base/muxer.cc +++ b/media/base/muxer.cc @@ -10,8 +10,8 @@ namespace media { -Muxer::Muxer(const Options& options, EncryptorSource* encrytor_source) : - encryptor_source_(encrytor_source), encryptor_(NULL) {} +Muxer::Muxer(const MuxerOptions& options, EncryptorSource* encrytor_source) + : options_(options), encryptor_source_(encrytor_source) {} Muxer::~Muxer() {} @@ -25,7 +25,6 @@ Status Muxer::Run() { DCHECK(!streams_.empty()); Status status; - // Start the streams. for (std::vector::iterator it = streams_.begin(); it != streams_.end(); @@ -35,15 +34,21 @@ Status Muxer::Run() { return status; } + uint32 current_stream_id = 0; while (status.ok()) { - // TODO(kqyang): Need to do some proper synchronization between streams. scoped_refptr sample; - status = streams_[0]->PullSample(&sample); + status = streams_[current_stream_id]->PullSample(&sample); if (!status.ok()) break; - status = AddSample(streams_[0], sample); + status = AddSample(streams_[current_stream_id], sample); + + // Switch to next stream if the current stream is ready for fragmentation. + if (status.Matches(Status(error::FRAGMENT_FINALIZED, ""))) { + current_stream_id = (current_stream_id + 1) % streams_.size(); + status.Clear(); + } } - return status.Matches(Status(error::EOF, "")) ? Status::OK : status; + return status.Matches(Status(error::END_OF_STREAM, "")) ? Status::OK : status; } } // namespace media diff --git a/media/base/muxer.h b/media/base/muxer.h index 5d641377b9..704686af38 100644 --- a/media/base/muxer.h +++ b/media/base/muxer.h @@ -1,6 +1,8 @@ // Copyright (c) 2013 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. +// +// Defines the muxer interface. #ifndef MEDIA_BASE_MUXER_H_ #define MEDIA_BASE_MUXER_H_ @@ -9,92 +11,46 @@ #include "base/memory/ref_counted.h" #include "base/memory/scoped_ptr.h" +#include "media/base/muxer_options.h" #include "media/base/status.h" namespace media { -class Encryptor; class EncryptorSource; class MediaSample; class MediaStream; class Muxer { public: - struct Options { - // Generate a single segment for each media presentation. This option - // should be set for on demand profile. - bool single_segment; - - // Segment duration. If single_segment is specified, this parameter sets - // the duration of a subsegment. A subsegment can contain one to several - // fragments. - int segment_duration; - - // Fragment duration. Should not be larger than the segment duration. - int fragment_duration; - - // Force segments to begin with stream access points. Segment duration may - // not be exactly what asked by segment_duration. - bool segment_sap_aligned; - - // Force fragments to begin with stream access points. Fragment duration - // may not be exactly what asked by segment_duration. Imply - // segment_sap_aligned. - bool fragment_sap_aligned; - - // Set the number of subsegments in each SIDX box. If 0, a single SIDX box - // is used per segment. If -1, no SIDX box is used. Otherwise, the Muxer - // will pack N subsegments in the root SIDX of the segment, with - // segment_duration/N/fragment_duration fragments per subsegment. - int num_subsegments_per_sidx; - - // Output file name. If segment_template is not specified, the Muxer - // generates this single output file with all segments concatenated; - // Otherwise, it specifies the init segment name. - std::string output_file_name; - - // Specify output segment name pattern for generated segments. It can - // furthermore be configured by using a subset of the SegmentTemplate - // identifiers: $RepresentationID$, $Number$, $Bandwidth$ and $Time. - // Optional. - std::string segment_template; - - // Specify a directory for temporary file creation. - std::string temp_directory_name; - }; - - // Muxer does not take over the ownership of encryptor_source. - Muxer(const Options& options, EncryptorSource* encryptor_source); + Muxer(const MuxerOptions& options, EncryptorSource* encryptor_source); virtual ~Muxer(); + // Initialize the muxer. Must be called after connecting all the streams. + virtual Status Initialize() = 0; + + // Final clean up. + virtual Status Finalize() = 0; + // Adds video/audio stream. - // Returns OK on success. virtual Status AddStream(MediaStream* stream); // Adds new media sample. virtual Status AddSample(const MediaStream* stream, scoped_refptr sample) = 0; - // Final clean up. - virtual Status Finalize() = 0; - // Drives the remuxing from muxer side (pull). virtual Status Run(); - uint32 num_streams() const { - return streams_.size(); - } + const std::vector& streams() const { return streams_; } - MediaStream* streams(uint32 index) const { - if (index >= streams_.size()) - return NULL; - return streams_[index]; - } + protected: + const MuxerOptions& options() const { return options_; } + EncryptorSource* encryptor_source() { return encryptor_source_; } private: - EncryptorSource* encryptor_source_; - Encryptor* encryptor_; + MuxerOptions options_; std::vector streams_; + EncryptorSource* const encryptor_source_; DISALLOW_COPY_AND_ASSIGN(Muxer); }; diff --git a/media/base/muxer_options.h b/media/base/muxer_options.h new file mode 100644 index 0000000000..f215fb8d62 --- /dev/null +++ b/media/base/muxer_options.h @@ -0,0 +1,59 @@ +// Copyright (c) 2013 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. + +#ifndef MEDIA_BASE_MUXER_OPTIONS_H_ +#define MEDIA_BASE_MUXER_OPTIONS_H_ + +#include + +namespace media { + +struct MuxerOptions { + // Generate a single segment for each media presentation. This option + // should be set for on demand profile. + bool single_segment; + + // Segment duration in seconds. If single_segment is specified, this parameter + // sets the duration of a subsegment; otherwise, this parameter sets the + // duration of a segment. A segment can contain one to many fragments. + double segment_duration; + + // Fragment duration in seconds. Should not be larger than the segment + // duration. + double fragment_duration; + + // Force segments to begin with stream access points. Segment duration may + // not be exactly what asked by segment_duration. + bool segment_sap_aligned; + + // Force fragments to begin with stream access points. Fragment duration + // may not be exactly what asked by segment_duration. Setting to true implies + // that segment_sap_aligned is true as well. + bool fragment_sap_aligned; + + // For ISO BMFF only. + // Set the number of subsegments in each SIDX box. If 0, a single SIDX box + // is used per segment. If -1, no SIDX box is used. Otherwise, the Muxer + // will pack N subsegments in the root SIDX of the segment, with + // segment_duration/N/fragment_duration fragments per subsegment. + int num_subsegments_per_sidx; + + // Output file name. If segment_template is not specified, the Muxer + // generates this single output file with all segments concatenated; + // Otherwise, it specifies the init segment name. + std::string output_file_name; + + // Specify output segment name pattern for generated segments. It can + // furthermore be configured by using a subset of the SegmentTemplate + // identifiers: $RepresentationID$, $Number$, $Bandwidth$ and $Time. + // Optional. + std::string segment_template; + + // Specify the temporary file for on demand media file creation. + std::string temp_file_name; +}; + +} // namespace media + +#endif // MEDIA_BASE_MUXER_OPTIONS_H_ diff --git a/media/file/file.h b/media/file/file.h index 6aa7e23951..bb0cbeec5d 100644 --- a/media/file/file.h +++ b/media/file/file.h @@ -7,6 +7,8 @@ #ifndef PACKAGER_FILE_FILE_H_ #define PACKAGER_FILE_FILE_H_ +#include + #include "base/basictypes.h" namespace media { @@ -55,6 +57,9 @@ class File { // Return whether we're currently at eof. virtual bool Eof() = 0; + // Return the file name. + const std::string& file_name() const { return file_name_; } + // ************************************************************ // * Static Methods: File-on-the-filesystem status // ************************************************************ @@ -64,7 +69,7 @@ class File { static int64 GetFileSize(const char* fname); protected: - File() {} + explicit File(const std::string& file_name) : file_name_(file_name) {} // Do *not* call the destructor directly (with the "delete" keyword) // nor use scoped_ptr; instead use Close(). virtual ~File() {} @@ -77,6 +82,7 @@ class File { // LocalFile, MemFile based on prefix. static File* Create(const char* fname, const char* mode); + std::string file_name_; DISALLOW_COPY_AND_ASSIGN(File); }; diff --git a/media/file/file_closer.h b/media/file/file_closer.h new file mode 100644 index 0000000000..fcbc8e7101 --- /dev/null +++ b/media/file/file_closer.h @@ -0,0 +1,25 @@ +// Copyright (c) 2013 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style license tha can be +// found in the LICENSE file. + +#ifndef MEDIA_FILE_FILE_CLOSER_H_ +#define MEDIA_FILE_FILE_CLOSER_H_ + +#include "base/logging.h" +#include "media/file/file.h" + +namespace media { + +// Use by scoped_ptr to automatically close the file when it goes out of scope. +struct FileCloser { + inline void operator()(File* file) const { + if (file != NULL && !file->Close()) { + LOG(WARNING) << "Failed to close the file properly: " + << file->file_name(); + } + } +}; + +} // namespace media + +#endif // MEDIA_FILE_FILE_CLOSER_H_ diff --git a/media/file/local_file.cc b/media/file/local_file.cc index 2dbde5b288..e5db3f8883 100644 --- a/media/file/local_file.cc +++ b/media/file/local_file.cc @@ -10,11 +10,11 @@ namespace media { LocalFile::LocalFile(const char* name, const char* mode) - : File(), file_name_(name), file_mode_(mode), internal_file_(NULL) {} + : File(name), file_mode_(mode), internal_file_(NULL) {} bool LocalFile::Open() { internal_file_ = - file_util::OpenFile(base::FilePath(file_name_), file_mode_.c_str()); + file_util::OpenFile(base::FilePath(file_name()), file_mode_.c_str()); return (internal_file_ != NULL); } @@ -50,7 +50,7 @@ int64 LocalFile::Size() { } int64 file_size; - if (!file_util::GetFileSize(base::FilePath(file_name_), &file_size)) { + if (!file_util::GetFileSize(base::FilePath(file_name()), &file_size)) { LOG(ERROR) << "Cannot get file size."; return -1; } diff --git a/media/file/local_file.h b/media/file/local_file.h index 40b7d32c62..2a7376c67d 100644 --- a/media/file/local_file.h +++ b/media/file/local_file.h @@ -31,7 +31,6 @@ class LocalFile : public File { virtual bool Open() OVERRIDE; private: - std::string file_name_; std::string file_mode_; FILE* internal_file_; diff --git a/media/mp4/mp4_fragmenter.cc b/media/mp4/mp4_fragmenter.cc new file mode 100644 index 0000000000..0941e07bbf --- /dev/null +++ b/media/mp4/mp4_fragmenter.cc @@ -0,0 +1,239 @@ +// Copyright (c) 2013 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. + +#include "media/mp4/mp4_fragmenter.h" + +#include "media/base/aes_encryptor.h" +#include "media/base/buffer_reader.h" +#include "media/base/buffer_writer.h" +#include "media/base/encryptor_source.h" +#include "media/base/media_sample.h" +#include "media/mp4/box_definitions.h" +#include "media/mp4/cenc.h" + +namespace { +const uint64 kInvalidTime = kuint64max; + +// Optimize sample entries table. If all values in |entries| are identical, +// then |entries| is cleared and the value is assigned to |default_value|; +// otherwise it is a NOP. Return true if the table is optimized. +template +bool OptimizeSampleEntries(std::vector* entries, T* default_value) { + DCHECK(entries != NULL && default_value != NULL); + DCHECK(!entries->empty()); + + typename std::vector::const_iterator it = entries->begin(); + T value = *it; + for (; it < entries->end(); ++it) + if (value != *it) + return false; + + // Clear |entries| if it contains only one value. + entries->clear(); + *default_value = value; + return true; +} +} // namespace + +namespace media { +namespace mp4 { + +MP4Fragmenter::MP4Fragmenter(TrackFragment* traf, + EncryptorSource* encryptor_source, + int64 clear_time, + uint8 nalu_length_size) + : encryptor_source_(encryptor_source), + nalu_length_size_(nalu_length_size), + traf_(traf), + fragment_finalized_(false), + fragment_duration_(0), + earliest_presentation_time_(0), + first_sap_time_(0), + clear_time_(clear_time) {} + +MP4Fragmenter::~MP4Fragmenter() {} + +Status MP4Fragmenter::AddSample(scoped_refptr sample) { + CHECK(sample->pts() >= sample->dts()); + CHECK(sample->duration() > 0); + + if (ShouldEncryptFragment()) { + Status status = EncryptSample(sample); + if (!status.ok()) + return status; + } + + // Fill in sample parameters. It will be optimized later. + traf_->runs[0].sample_sizes.push_back(sample->data_size()); + traf_->runs[0].sample_durations.push_back(sample->duration()); + traf_->runs[0].sample_flags.push_back( + sample->is_key_frame() ? 0 : TrackFragmentHeader::kNonKeySampleMask); + traf_->runs[0] + .sample_composition_time_offsets.push_back(sample->pts() - sample->dts()); + if (sample->pts() != sample->dts()) { + traf_->runs[0].flags |= + TrackFragmentRun::kSampleCompTimeOffsetsPresentMask; + } + + data_->AppendArray(sample->data(), sample->data_size()); + fragment_duration_ += sample->duration(); + + if (earliest_presentation_time_ > sample->pts()) + earliest_presentation_time_ = sample->pts(); + + if (sample->is_key_frame()) { + if (kInvalidTime == first_sap_time_) + first_sap_time_ = sample->pts(); + } + return Status::OK; +} + +void MP4Fragmenter::InitializeFragment() { + fragment_finalized_ = false; + traf_->decode_time.decode_time += fragment_duration_; + traf_->auxiliary_size.sample_info_sizes.clear(); + traf_->auxiliary_offset.offsets.clear(); + traf_->runs.clear(); + traf_->runs.resize(1); + traf_->runs[0].flags = TrackFragmentRun::kDataOffsetPresentMask; + traf_->header.flags = TrackFragmentHeader::kDefaultBaseIsMoofMask; + fragment_duration_ = 0; + earliest_presentation_time_ = kInvalidTime; + first_sap_time_ = kInvalidTime; + data_.reset(new BufferWriter()); + aux_data_.reset(new BufferWriter()); + + if (ShouldEncryptFragment()) { + if (!IsSubsampleEncryptionRequired()) { + DCHECK(encryptor_source_ != NULL); + traf_->auxiliary_size.default_sample_info_size = + encryptor_source_->encryptor()->iv().size(); + } + } +} + +void MP4Fragmenter::FinalizeFragment() { + if (ShouldEncryptFragment()) { + DCHECK(encryptor_source_ != NULL); + + // The offset will be adjusted in Segmenter when we know moof size. + traf_->auxiliary_offset.offsets.push_back(0); + + // Optimize saiz box. + SampleAuxiliaryInformationSize& saiz = traf_->auxiliary_size; + saiz.sample_count = traf_->runs[0].sample_sizes.size(); + if (!saiz.sample_info_sizes.empty()) { + if (!OptimizeSampleEntries(&saiz.sample_info_sizes, + &saiz.default_sample_info_size)) { + saiz.default_sample_info_size = 0; + } + } + } else if (encryptor_source_ && clear_time_ > 0) { + // This fragment should be in clear. + // We generate at most two sample description entries, encrypted entry and + // clear entry. The 1-based clear entry index is always 2. + const uint32 kClearSampleDescriptionIndex = 2; + + traf_->header.flags |= + TrackFragmentHeader::kSampleDescriptionIndexPresentMask; + traf_->header.sample_description_index = kClearSampleDescriptionIndex; + clear_time_ -= fragment_duration_; + } + + // Optimize trun box. + traf_->runs[0].sample_count = traf_->runs[0].sample_sizes.size(); + if (OptimizeSampleEntries(&traf_->runs[0].sample_durations, + &traf_->header.default_sample_duration)) { + traf_->header.flags |= + TrackFragmentHeader::kDefaultSampleDurationPresentMask; + } else { + traf_->runs[0].flags |= TrackFragmentRun::kSampleDurationPresentMask; + } + if (OptimizeSampleEntries(&traf_->runs[0].sample_sizes, + &traf_->header.default_sample_size)) { + traf_->header.flags |= TrackFragmentHeader::kDefaultSampleSizePresentMask; + } else { + traf_->runs[0].flags |= TrackFragmentRun::kSampleSizePresentMask; + } + if (OptimizeSampleEntries(&traf_->runs[0].sample_flags, + &traf_->header.default_sample_flags)) { + traf_->header.flags |= TrackFragmentHeader::kDefaultSampleFlagsPresentMask; + } else { + traf_->runs[0].flags |= TrackFragmentRun::kSampleFlagsPresentMask; + } + + fragment_finalized_ = true; +} + +void MP4Fragmenter::GenerateSegmentReference(SegmentReference* reference) { + // TODO(kqyang): support daisy chain?? + reference->reference_type = false; + reference->subsegment_duration = fragment_duration_; + reference->starts_with_sap = StartsWithSAP(); + if (kInvalidTime == first_sap_time_) { + reference->sap_type = SegmentReference::TypeUnknown; + reference->sap_delta_time = 0; + } else { + reference->sap_type = SegmentReference::Type1; + reference->sap_delta_time = first_sap_time_ - earliest_presentation_time_; + } + reference->earliest_presentation_time = earliest_presentation_time_; +} + +void MP4Fragmenter::EncryptBytes(uint8* data, uint32 size) { + CHECK(encryptor_source_->encryptor()->Encrypt(data, size, data)); +} + +Status MP4Fragmenter::EncryptSample(scoped_refptr sample) { + DCHECK(encryptor_source_ != NULL && encryptor_source_->encryptor() != NULL); + + FrameCENCInfo cenc_info(encryptor_source_->encryptor()->iv()); + uint8* data = sample->writable_data(); + if (!IsSubsampleEncryptionRequired()) { + EncryptBytes(data, sample->data_size()); + } else { + BufferReader reader(data, sample->data_size()); + while (reader.HasBytes(1)) { + uint64 nalu_length; + if (!reader.ReadNBytesInto8(&nalu_length, nalu_length_size_)) + return Status(error::MUXER_FAILURE, "Fail to read nalu_length."); + + SubsampleEntry subsample; + subsample.clear_bytes = nalu_length_size_ + 1; + subsample.cypher_bytes = nalu_length - 1; + if (!reader.SkipBytes(nalu_length)) { + return Status(error::MUXER_FAILURE, + "Sample size does not match nalu_length."); + } + + EncryptBytes(data + subsample.clear_bytes, subsample.cypher_bytes); + cenc_info.AddSubsample(subsample); + data += nalu_length_size_ + nalu_length; + } + + // The length of per-sample auxiliary datum, defined in CENC ch. 7. + traf_->auxiliary_size.sample_info_sizes.push_back(cenc_info.ComputeSize()); + } + + cenc_info.Write(aux_data_.get()); + encryptor_source_->encryptor()->UpdateIv(); + return Status::OK; +} + +bool MP4Fragmenter::StartsWithSAP() { + DCHECK(!traf_->runs.empty()); + uint32 start_sample_flag; + if (traf_->runs[0].flags & TrackFragmentRun::kSampleFlagsPresentMask) { + DCHECK(!traf_->runs[0].sample_flags.empty()); + start_sample_flag = traf_->runs[0].sample_flags[0]; + } else { + DCHECK(traf_->header.flags & + TrackFragmentHeader::kDefaultSampleFlagsPresentMask); + start_sample_flag = traf_->header.default_sample_flags; + } + return (start_sample_flag & TrackFragmentHeader::kNonKeySampleMask) == 0; +} + +} // namespace mp4 +} // namespace media diff --git a/media/mp4/mp4_fragmenter.h b/media/mp4/mp4_fragmenter.h new file mode 100644 index 0000000000..725beff573 --- /dev/null +++ b/media/mp4/mp4_fragmenter.h @@ -0,0 +1,96 @@ +// Copyright (c) 2013 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. +// +// MP4Fragmenter is responsible for the generation of MP4 fragments, i.e. traf +// and the corresponding mdat. The samples are also encrypted if encryption is +// requested. + +#ifndef MEDIA_MP4_MP4_FRAGMENTER_H_ +#define MEDIA_MP4_MP4_FRAGMENTER_H_ + +#include + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "media/base/status.h" + +namespace media { + +class BufferWriter; +class EncryptorSource; +class MediaSample; + +namespace mp4 { + +class SegmentReference; +class TrackFragment; + +class MP4Fragmenter { + public: + // Caller retains the ownership of |traf| and |encryptor_source|. + // |clear_time| specifies clear time in the current track timescale. + // |nalu_length_size| specifies NAL unit length size, for subsample + // encryption. + MP4Fragmenter(TrackFragment* traf, + EncryptorSource* encryptor_source, + int64 clear_time, + uint8 nalu_length_size); + ~MP4Fragmenter(); + + virtual Status AddSample(scoped_refptr sample); + + // Initialize the fragment with default data. + void InitializeFragment(); + + // Finalize and optimize the fragment. + void FinalizeFragment(); + + // Fill in |reference| with current fragment information. + void GenerateSegmentReference(SegmentReference* reference); + + uint64 fragment_duration() const { return fragment_duration_; } + uint64 first_sap_time() const { return first_sap_time_; } + uint64 earliest_presentation_time() const { + return earliest_presentation_time_; + } + bool fragment_finalized() const { return fragment_finalized_; } + BufferWriter* data() { return data_.get(); } + BufferWriter* aux_data() { return aux_data_.get(); } + + private: + void EncryptBytes(uint8* data, uint32 size); + Status EncryptSample(scoped_refptr sample); + + // Should we enable encrytion for the current fragment? + bool ShouldEncryptFragment() { + return (encryptor_source_ != NULL && clear_time_ <= 0); + } + + // Should we enable subsample encryption? + bool IsSubsampleEncryptionRequired() { return nalu_length_size_ != 0; } + + // Check if the current fragment starts with SAP. + bool StartsWithSAP(); + + EncryptorSource* encryptor_source_; + // If this stream contains AVC, subsample encryption specifies that the size + // and type of NAL units remain unencrypted. This field specifies the size of + // the size field. Can be 1, 2 or 4 bytes. + uint8 nalu_length_size_; + TrackFragment* traf_; + bool fragment_finalized_; + uint64 fragment_duration_; + uint64 earliest_presentation_time_; + uint64 first_sap_time_; + int64 clear_time_; + scoped_ptr data_; + scoped_ptr aux_data_; + + DISALLOW_COPY_AND_ASSIGN(MP4Fragmenter); +}; + +} // namespace mp4 +} // namespace media + +#endif // MEDIA_MP4_MP4_FRAGMENTER_H_ diff --git a/media/mp4/mp4_general_segmenter.cc b/media/mp4/mp4_general_segmenter.cc new file mode 100644 index 0000000000..75922bdb53 --- /dev/null +++ b/media/mp4/mp4_general_segmenter.cc @@ -0,0 +1,163 @@ +// Copyright (c) 2013 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. + +#include "media/mp4/mp4_general_segmenter.h" + +#include "base/strings/string_number_conversions.h" +#include "base/strings/string_util.h" +#include "media/base/buffer_writer.h" +#include "media/base/media_stream.h" +#include "media/base/muxer_options.h" +#include "media/file/file.h" +#include "media/mp4/box_definitions.h" + +namespace media { +namespace mp4 { + +MP4GeneralSegmenter::MP4GeneralSegmenter(const MuxerOptions& options, + FileType* ftyp, + Movie* moov) + : MP4Segmenter(options, ftyp, moov), + styp_(new SegmentType), + num_segments_(0) { + // Use the same brands for styp as ftyp. + styp_->major_brand = ftyp->major_brand; + styp_->compatible_brands = ftyp->compatible_brands; +} + +MP4GeneralSegmenter::~MP4GeneralSegmenter() {} + +Status MP4GeneralSegmenter::Initialize( + EncryptorSource* encryptor_source, + const std::vector& streams) { + Status status = MP4Segmenter::Initialize(encryptor_source, streams); + if (!status.ok()) + return status; + + DCHECK(ftyp() != NULL && moov() != NULL); + // Generate the output file with init segment. + File* file = File::Open(options().output_file_name.c_str(), "w"); + if (file == NULL) { + return Status(error::FILE_FAILURE, + "Cannot open file for write " + options().output_file_name); + } + scoped_ptr buffer(new BufferWriter); + ftyp()->Write(buffer.get()); + moov()->Write(buffer.get()); + status = buffer->WriteToFile(file); + if (!file->Close()) { + LOG(WARNING) << "Failed to close the file properly: " + << options().output_file_name; + } + return status; +} + +Status MP4GeneralSegmenter::FinalizeSegment() { + Status status = MP4Segmenter::FinalizeSegment(); + if (!status.ok()) + return status; + + DCHECK(sidx() != NULL); + // earliest_presentation_time is the earliest presentation time of any + // access unit in the reference stream in the first subsegment. + // It will be re-calculated later when subsegments are finalized. + sidx()->earliest_presentation_time = + sidx()->references[0].earliest_presentation_time; + + if (options().num_subsegments_per_sidx <= 0) + return WriteSegment(); + + // sidx() contains pre-generated segment references with one reference per + // fragment. Calculate |num_fragments_per_subsegment| and combine + // pre-generated references into final subsegment references. + uint32 num_fragments = sidx()->references.size(); + uint32 num_fragments_per_subsegment = + (num_fragments - 1) / options().num_subsegments_per_sidx + 1; + if (num_fragments_per_subsegment <= 1) + return WriteSegment(); + + uint32 frag_index = 0; + uint32 subseg_index = 0; + std::vector& refs = sidx()->references; + uint64 first_sap_time = + refs[0].sap_delta_time + refs[0].earliest_presentation_time; + for (uint32 i = 1; i < num_fragments; ++i) { + refs[subseg_index].referenced_size += refs[i].referenced_size; + refs[subseg_index].subsegment_duration += refs[i].subsegment_duration; + refs[subseg_index].earliest_presentation_time = + std::min(refs[subseg_index].earliest_presentation_time, + refs[i].earliest_presentation_time); + if (refs[subseg_index].sap_type == SegmentReference::TypeUnknown && + refs[i].sap_type != SegmentReference::TypeUnknown) { + refs[subseg_index].sap_type = refs[i].sap_type; + first_sap_time = + refs[i].sap_delta_time + refs[i].earliest_presentation_time; + } + if (++frag_index >= num_fragments_per_subsegment) { + // Calculate sap delta time w.r.t. sidx_->earliest_presentation_time. + if (refs[subseg_index].sap_type != SegmentReference::TypeUnknown) { + refs[subseg_index].sap_delta_time = + first_sap_time - refs[subseg_index].earliest_presentation_time; + } + if (++i >= num_fragments) + break; + refs[++subseg_index] = refs[i]; + first_sap_time = + refs[i].sap_delta_time + refs[i].earliest_presentation_time; + frag_index = 1; + } + } + + refs.resize(options().num_subsegments_per_sidx); + + // earliest_presentation_time is the earliest presentation time of any + // access unit in the reference stream in the first subsegment. + sidx()->earliest_presentation_time = refs[0].earliest_presentation_time; + + return WriteSegment(); +} + +Status MP4GeneralSegmenter::WriteSegment() { + DCHECK(sidx() != NULL && fragment_buffer() != NULL && styp_ != NULL); + + scoped_ptr buffer(new BufferWriter()); + File* file; + std::string file_name; + if (options().segment_template.empty()) { + // Append the segment to output file if segment template is not specified. + file_name = options().output_file_name.c_str(); + file = File::Open(file_name.c_str(), "a+"); + if (file == NULL) { + return Status( + error::FILE_FAILURE, + "Cannot open file for append " + options().output_file_name); + } + } else { + // TODO(kqyang): generate the segment template name. + file_name = options().segment_template; + ReplaceSubstringsAfterOffset( + &file_name, 0, "$Number$", base::UintToString(++num_segments_)); + file = File::Open(file_name.c_str(), "w"); + if (file == NULL) { + return Status(error::FILE_FAILURE, + "Cannot open file for write " + file_name); + } + styp_->Write(buffer.get()); + } + + // If num_subsegments_per_sidx is negative, no SIDX box is generated. + if (options().num_subsegments_per_sidx >= 0) + sidx()->Write(buffer.get()); + + Status status = buffer->WriteToFile(file); + if (status.ok()) + status = fragment_buffer()->WriteToFile(file); + + if (!file->Close()) + LOG(WARNING) << "Failed to close the file properly: " << file_name; + return status; +} + +} // namespace mp4 +} // namespace media diff --git a/media/mp4/mp4_general_segmenter.h b/media/mp4/mp4_general_segmenter.h new file mode 100644 index 0000000000..13fa5367ac --- /dev/null +++ b/media/mp4/mp4_general_segmenter.h @@ -0,0 +1,55 @@ +// Copyright (c) 2013 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. +// +// Segmenter for MP4 live, main and simple profiles. The generated media file +// could contain one to many segments with segment duration defined by +// |MuxerOptions.segment_duration|. A segment could contain one to many +// subsegments defined by |num_subsegments_per_sidx|. A subsegment could +// contain one to many fragments with fragment duration defined by +// |MuxerOptions.fragment_duration|. The actual segment or fragment duration +// may not match the defined duration exactly but in a best effort basic, i.e. +// the segmenter tries to end segment/fragment at the first sample with +// overall segment/fragment duration not smaller than defined duration and +// yet meet SAP requirements. The generated segments are written into files +// defined by |MuxerOptions.segment_template| if it is defined; otherwise, +// the segments are appended to the main output file defined by +// |MuxerOptions.output_file_name|. + +#ifndef MEDIA_MP4_MP4_GENERAL_SEGMENTER_H_ +#define MEDIA_MP4_MP4_GENERAL_SEGMENTER_H_ + +#include "media/mp4/mp4_segmenter.h" + +namespace media { +namespace mp4 { + +struct SegmentType; + +class MP4GeneralSegmenter : public MP4Segmenter { + public: + // Caller transfers the ownership of |ftyp| and |moov| to this class. + MP4GeneralSegmenter(const MuxerOptions& options, FileType* ftyp, Movie* moov); + ~MP4GeneralSegmenter(); + + // MP4Segmenter implementations. + virtual Status Initialize(EncryptorSource* encryptor_source, + const std::vector& streams) OVERRIDE; + + protected: + virtual Status FinalizeSegment() OVERRIDE; + + private: + // Write segment to file. + Status WriteSegment(); + + scoped_ptr styp_; + uint32 num_segments_; + + DISALLOW_COPY_AND_ASSIGN(MP4GeneralSegmenter); +}; + +} // namespace mp4 +} // namespace media + +#endif // MEDIA_MP4_MP4_GENERAL_SEGMENTER_H_ diff --git a/media/mp4/mp4_muxer.cc b/media/mp4/mp4_muxer.cc new file mode 100644 index 0000000000..acc35d40ef --- /dev/null +++ b/media/mp4/mp4_muxer.cc @@ -0,0 +1,223 @@ +// Copyright (c) 2013 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. + +#include "media/mp4/mp4_muxer.h" + +#include "base/time/time.h" +#include "media/base/aes_encryptor.h" +#include "media/base/audio_stream_info.h" +#include "media/base/encryptor_source.h" +#include "media/base/media_sample.h" +#include "media/base/media_stream.h" +#include "media/base/video_stream_info.h" +#include "media/file/file.h" +#include "media/mp4/box_definitions.h" +#include "media/mp4/es_descriptor.h" +#include "media/mp4/fourccs.h" +#include "media/mp4/mp4_general_segmenter.h" +#include "media/mp4/mp4_vod_segmenter.h" + +namespace { +// The version of cenc implemented here. CENC 4. +const int kCencSchemeVersion = 0x00010000; + +// Get time in seconds since midnight, Jan. 1, 1904, in UTC Time. +uint64 IsoTimeNow() { + // Time in seconds from Jan. 1, 1904 to epoch time, i.e. Jan. 1, 1970. + const uint64 kIsomTimeOffset = 2082844800l; + return kIsomTimeOffset + base::Time::Now().ToDoubleT(); +} +} // namespace + +namespace media { +namespace mp4 { + +MP4Muxer::MP4Muxer(const MuxerOptions& options, + EncryptorSource* encryptor_source) + : Muxer(options, encryptor_source) {} +MP4Muxer::~MP4Muxer() {} + +Status MP4Muxer::Initialize() { + DCHECK(!streams().empty()); + + scoped_ptr ftyp(new FileType); + scoped_ptr moov(new Movie); + + ftyp->major_brand = FOURCC_DASH; + ftyp->compatible_brands.push_back(FOURCC_ISO6); + ftyp->compatible_brands.push_back(FOURCC_MP41); + if (streams().size() == 1 && + streams()[0]->info()->stream_type() == kStreamVideo) + ftyp->compatible_brands.push_back(FOURCC_AVC1); + + moov->header.creation_time = IsoTimeNow(); + moov->header.modification_time = IsoTimeNow(); + moov->header.next_track_id = streams().size() + 1; + + moov->tracks.resize(streams().size()); + moov->extends.tracks.resize(streams().size()); + + // Initialize tracks. + for (uint32 i = 0; i < streams().size(); ++i) { + Track& trak = moov->tracks[i]; + trak.header.track_id = i + 1; + + TrackExtends& trex = moov->extends.tracks[i]; + trex.track_id = trak.header.track_id; + trex.default_sample_description_index = 1; + + switch (streams()[i]->info()->stream_type()) { + case kStreamVideo: + GenerateVideoTrak( + static_cast(streams()[i]->info().get()), + &trak, + i + 1); + break; + case kStreamAudio: + GenerateAudioTrak( + static_cast(streams()[i]->info().get()), + &trak, + i + 1); + break; + default: + NOTIMPLEMENTED() << "Not implemented for stream type: " + << streams()[i]->info()->stream_type(); + } + } + + if (IsEncryptionRequired()) { + moov->pssh.resize(1); + GeneratePssh(&moov->pssh[0]); + } + + if (options().single_segment) { + segmenter_.reset( + new MP4VODSegmenter(options(), ftyp.release(), moov.release())); + } else { + segmenter_.reset( + new MP4GeneralSegmenter(options(), ftyp.release(), moov.release())); + } + return segmenter_->Initialize(encryptor_source(), streams()); +} + +Status MP4Muxer::Finalize() { + DCHECK(segmenter_ != NULL); + return segmenter_->Finalize(); +} + +Status MP4Muxer::AddSample(const MediaStream* stream, + scoped_refptr sample) { + DCHECK(segmenter_ != NULL); + return segmenter_->AddSample(stream, sample); +} + +void MP4Muxer::InitializeTrak(const StreamInfo* info, Track* trak) { + uint64 now = IsoTimeNow(); + trak->header.creation_time = now; + trak->header.modification_time = now; + trak->header.duration = 0; + trak->media.header.creation_time = now; + trak->media.header.modification_time = now; + trak->media.header.timescale = info->time_scale(); + trak->media.header.duration = 0; + if (!info->language().empty()) { + DCHECK_EQ(info->language().size(), + arraysize(trak->media.header.language) - 1); + strcpy(trak->media.header.language, info->language().c_str()); + trak->media.header.language[info->language().size()] = '\0'; + } +} + +void MP4Muxer::GenerateVideoTrak(const VideoStreamInfo* video_info, + Track* trak, + uint32 track_id) { + InitializeTrak(video_info, trak); + + trak->header.width = video_info->width(); + trak->header.height = video_info->height(); + trak->media.handler.type = kVideo; + + VideoSampleEntry video; + video.format = FOURCC_AVC1; + video.width = video_info->width(); + video.height = video_info->height(); + video.avcc.data = video_info->extra_data(); + + SampleDescription& sample_description = + trak->media.information.sample_table.description; + sample_description.type = kVideo; + sample_description.video_entries.push_back(video); + + if (IsEncryptionRequired()) { + DCHECK(encryptor_source() != NULL); + // Add a second entry for clear content if needed. + if (encryptor_source()->clear_milliseconds() > 0) + sample_description.video_entries.push_back(video); + + VideoSampleEntry& encrypted_video = sample_description.video_entries[0]; + GenerateSinf(&encrypted_video.sinf, encrypted_video.format); + encrypted_video.format = FOURCC_ENCV; + } +} + +void MP4Muxer::GenerateAudioTrak(const AudioStreamInfo* audio_info, + Track* trak, + uint32 track_id) { + InitializeTrak(audio_info, trak); + + trak->header.volume = 0x100; + trak->media.handler.type = kAudio; + + AudioSampleEntry audio; + audio.format = FOURCC_MP4A; + audio.channelcount = audio_info->num_channels(); + audio.samplesize = audio_info->sample_bits(); + audio.samplerate = audio_info->sampling_frequency(); + + if (kCodecAAC) { + audio.esds.es_descriptor.set_object_type(kISO_14496_3); // MPEG4 AAC. + } else { + // Do we need to support other types? + NOTIMPLEMENTED(); + } + audio.esds.es_descriptor.set_esid(track_id); + audio.esds.es_descriptor.set_decoder_specific_info(audio_info->extra_data()); + + SampleDescription& sample_description = + trak->media.information.sample_table.description; + sample_description.type = kAudio; + sample_description.audio_entries.push_back(audio); + + if (IsEncryptionRequired()) { + DCHECK(encryptor_source() != NULL); + // Add a second entry for clear content if needed. + if (encryptor_source()->clear_milliseconds() > 0) + sample_description.audio_entries.push_back(audio); + + AudioSampleEntry& encrypted_audio = sample_description.audio_entries[0]; + GenerateSinf(&encrypted_audio.sinf, encrypted_audio.format); + encrypted_audio.format = FOURCC_ENCA; + } +} + +void MP4Muxer::GeneratePssh(ProtectionSystemSpecificHeader* pssh) { + DCHECK(encryptor_source() != NULL); + pssh->system_id = encryptor_source()->key_system_id(); + pssh->data = encryptor_source()->pssh(); +} + +void MP4Muxer::GenerateSinf(ProtectionSchemeInfo* sinf, FourCC old_type) { + DCHECK(encryptor_source() != NULL); + DCHECK(encryptor_source()->encryptor() != NULL); + sinf->format.format = old_type; + sinf->type.type = FOURCC_CENC; + sinf->type.version = kCencSchemeVersion; + sinf->info.track_encryption.is_encrypted = true; + sinf->info.track_encryption.default_iv_size = + encryptor_source()->encryptor()->iv().size(); + sinf->info.track_encryption.default_kid = encryptor_source()->key_id(); +} + +} // namespace mp4 +} // namespace media diff --git a/media/mp4/mp4_muxer.h b/media/mp4/mp4_muxer.h new file mode 100644 index 0000000000..94440c3391 --- /dev/null +++ b/media/mp4/mp4_muxer.h @@ -0,0 +1,65 @@ +// Copyright (c) 2013 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. +// +// Implements MP4 Muxer. + +#ifndef MEDIA_MP4_MP4_MUXER_H_ +#define MEDIA_MP4_MP4_MUXER_H_ + +#include "media/base/muxer.h" +#include "media/mp4/fourccs.h" + +namespace media { + +class AudioStreamInfo; +class StreamInfo; +class VideoStreamInfo; + +namespace mp4 { + +class MP4Segmenter; + +struct ProtectionSchemeInfo; +struct ProtectionSystemSpecificHeader; +struct Track; + +class MP4Muxer : public Muxer { + public: + MP4Muxer(const MuxerOptions& options, EncryptorSource* encryptor_source); + virtual ~MP4Muxer(); + + // Muxer implementations. + virtual Status Initialize() OVERRIDE; + virtual Status Finalize() OVERRIDE; + virtual Status AddSample(const MediaStream* stream, + scoped_refptr sample) OVERRIDE; + + private: + // Generate Audio/Video Track atom. + void InitializeTrak(const StreamInfo* info, Track* trak); + void GenerateAudioTrak(const AudioStreamInfo* audio_info, + Track* trak, + uint32 track_id); + void GenerateVideoTrak(const VideoStreamInfo* video_info, + Track* trak, + uint32 track_id); + + // Generate Pssh atom. + void GeneratePssh(ProtectionSystemSpecificHeader* pssh); + + // Generates a sinf atom with CENC encryption parameters. + void GenerateSinf(ProtectionSchemeInfo* sinf, FourCC old_type); + + // Should we enable encrytion? + bool IsEncryptionRequired() { return (encryptor_source() != NULL); } + + scoped_ptr segmenter_; + + DISALLOW_COPY_AND_ASSIGN(MP4Muxer); +}; + +} // namespace mp4 +} // namespace media + +#endif // MEDIA_MP4_MP4_MUXER_H_ diff --git a/media/mp4/mp4_segmenter.cc b/media/mp4/mp4_segmenter.cc new file mode 100644 index 0000000000..3169dd6ccc --- /dev/null +++ b/media/mp4/mp4_segmenter.cc @@ -0,0 +1,244 @@ +// Copyright (c) 2013 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. + +#include "media/mp4/mp4_segmenter.h" + +#include + +#include "base/stl_util.h" +#include "media/base/buffer_writer.h" +#include "media/base/encryptor_source.h" +#include "media/base/media_sample.h" +#include "media/base/media_stream.h" +#include "media/base/muxer_options.h" +#include "media/base/video_stream_info.h" +#include "media/mp4/box_definitions.h" +#include "media/mp4/mp4_fragmenter.h" + +namespace { +uint64 Rescale(uint64 time_in_old_scale, uint32 old_scale, uint32 new_scale) { + return static_cast(time_in_old_scale) / old_scale * new_scale; +} +} // namespace + +namespace media { +namespace mp4 { + +MP4Segmenter::MP4Segmenter(const MuxerOptions& options, + FileType* ftyp, + Movie* moov) + : options_(options), + fragment_buffer_(new BufferWriter()), + ftyp_(ftyp), + moov_(moov), + moof_(new MovieFragment()), + sidx_(new SegmentIndex()), + segment_initialized_(false), + end_of_segment_(false) {} + +MP4Segmenter::~MP4Segmenter() { STLDeleteElements(&fragmenters_); } + +Status MP4Segmenter::Initialize(EncryptorSource* encryptor_source, + const std::vector& streams) { + DCHECK_LT(0, streams.size()); + moof_->header.sequence_number = 0; + + moof_->tracks.resize(streams.size()); + segment_durations_.resize(streams.size()); + fragmenters_.resize(streams.size()); + for (uint32 i = 0; i < streams.size(); ++i) { + stream_map_[streams[i]] = i; + moof_->tracks[i].header.track_id = i + 1; + uint8 nalu_length_size = 0; + if (streams[i]->info()->stream_type() == kStreamVideo) { + VideoStreamInfo* video = + static_cast(streams[i]->info().get()); + nalu_length_size = video->nalu_length_size(); + // We use the first video stream as the reference stream. + if (sidx_->reference_id == 0) + sidx_->reference_id = i + 1; + } + int64 clear_time = 0; + if (encryptor_source) { + clear_time = encryptor_source->clear_milliseconds() / 1000.0 * + streams[i]->info()->time_scale(); + } + fragmenters_[i] = new MP4Fragmenter( + &moof_->tracks[i], encryptor_source, clear_time, nalu_length_size); + } + + // Choose the first stream if there is no VIDEO. + if (sidx_->reference_id == 0) + sidx_->reference_id = 1; + sidx_->timescale = streams[GetReferenceStreamId()]->info()->time_scale(); + + // Use the reference stream's time scale as movie time scale. + moov_->header.timescale = sidx_->timescale; + InitializeFragments(); + return Status::OK; +} + +Status MP4Segmenter::Finalize() { + end_of_segment_ = true; + for (std::vector::iterator it = fragmenters_.begin(); + it != fragmenters_.end(); + ++it) { + Status status = FinalizeFragment(*it); + if (!status.ok()) + return status; + } + + // Set tracks and moov durations. + // Note that the updated moov box will be written to output file for VOD case + // only. + for (std::vector::iterator track = moov_->tracks.begin(); + track != moov_->tracks.end(); + ++track) { + track->header.duration = Rescale(track->media.header.duration, + track->media.header.timescale, + moov_->header.timescale); + if (track->header.duration > moov_->header.duration) + moov_->header.duration = track->header.duration; + } + + return Status::OK; +} + +Status MP4Segmenter::AddSample(const MediaStream* stream, + scoped_refptr sample) { + // Find the fragmenter for this stream. + DCHECK(stream != NULL && stream_map_.find(stream) != stream_map_.end()); + uint32 stream_id = stream_map_[stream]; + MP4Fragmenter* fragmenter = fragmenters_[stream_id]; + + // Set default sample duration if it has not been set yet. + if (moov_->extends.tracks[stream_id].default_sample_duration == 0) { + moov_->extends.tracks[stream_id].default_sample_duration = + sample->duration(); + } + + if (!segment_initialized_) { + InitializeSegment(); + segment_initialized_ = true; + } + + if (fragmenter->fragment_finalized()) { + return Status(error::FRAGMENT_FINALIZED, + "Current fragment is finalized already."); + } + + bool finalize_fragment = false; + if (fragmenter->fragment_duration() >= + options_.fragment_duration * stream->info()->time_scale()) { + if (sample->is_key_frame() || !options_.fragment_sap_aligned) { + finalize_fragment = true; + } + } + if (segment_durations_[stream_id] >= + options_.segment_duration * stream->info()->time_scale()) { + if (sample->is_key_frame() || !options_.segment_sap_aligned) { + end_of_segment_ = true; + finalize_fragment = true; + } + } + + Status status; + if (finalize_fragment) { + status = FinalizeFragment(fragmenter); + if (!status.ok()) + return status; + } + + status = fragmenter->AddSample(sample); + if (!status.ok()) + return status; + + moov_->tracks[stream_id].media.header.duration += sample->duration(); + segment_durations_[stream_id] += sample->duration(); + return Status::OK; +} + +void MP4Segmenter::InitializeSegment() { + sidx_->references.clear(); + end_of_segment_ = false; + std::vector::iterator it = segment_durations_.begin(); + for (; it != segment_durations_.end(); ++it) + *it = 0; +} + +Status MP4Segmenter::FinalizeSegment() { + segment_initialized_ = false; + return Status::OK; +} + +uint32 MP4Segmenter::GetReferenceStreamId() { + DCHECK(sidx_ != NULL); + return sidx_->reference_id - 1; +} + +void MP4Segmenter::InitializeFragments() { + ++moof_->header.sequence_number; + for (std::vector::iterator it = fragmenters_.begin(); + it != fragmenters_.end(); + ++it) { + (*it)->InitializeFragment(); + } +} + +Status MP4Segmenter::FinalizeFragment(MP4Fragmenter* fragmenter) { + fragmenter->FinalizeFragment(); + + // Check if all tracks are ready for fragmentation. + for (std::vector::iterator it = fragmenters_.begin(); + it != fragmenters_.end(); + ++it) { + if (!(*it)->fragment_finalized()) + return Status::OK; + } + + MediaData mdat; + // Fill in data offsets. Data offset base is moof size + mdat box size. + // (mdat is still empty, mdat size is the same as mdat box size). + uint64 base = moof_->ComputeSize() + mdat.ComputeSize(); + for (uint i = 0; i < moof_->tracks.size(); ++i) { + TrackFragment& traf = moof_->tracks[i]; + MP4Fragmenter* fragmenter = fragmenters_[i]; + if (fragmenter->aux_data()->Size() > 0) { + traf.auxiliary_offset.offsets[0] += base; + base += fragmenter->aux_data()->Size(); + } + traf.runs[0].data_offset += base; + base += fragmenter->data()->Size(); + } + + // Generate segment reference. + sidx_->references.resize(sidx_->references.size() + 1); + fragmenters_[GetReferenceStreamId()]->GenerateSegmentReference( + &sidx_->references[sidx_->references.size() - 1]); + sidx_->references[sidx_->references.size() - 1].referenced_size = base; + + // Write the fragment to buffer. + moof_->Write(fragment_buffer_.get()); + + for (uint i = 0; i < moof_->tracks.size(); ++i) { + MP4Fragmenter* fragmenter = fragmenters_[i]; + mdat.data_size = + fragmenter->aux_data()->Size() + fragmenter->data()->Size(); + mdat.Write(fragment_buffer_.get()); + if (fragmenter->aux_data()->Size()) { + fragment_buffer_->AppendBuffer(*fragmenter->aux_data()); + } + fragment_buffer_->AppendBuffer(*fragmenter->data()); + } + + InitializeFragments(); + + if (end_of_segment_) + return FinalizeSegment(); + + return Status::OK; +} + +} // namespace mp4 +} // namespace media diff --git a/media/mp4/mp4_segmenter.h b/media/mp4/mp4_segmenter.h new file mode 100644 index 0000000000..6e0f2f6533 --- /dev/null +++ b/media/mp4/mp4_segmenter.h @@ -0,0 +1,90 @@ +// Copyright (c) 2013 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. +// +// This class defines the MP4 Segmenter which is responsible for organizing +// MP4 fragments into segments/subsegments and package into a MP4 file. +// Inherited by MP4GeneralSegmenter and MP4VODSegmenter. MP4VODSegmenter defines +// the segmenter for DASH Video-On-Demand with a single segment for each media +// presentation while MP4GeneralSegmenter handles all other cases including +// DASH live profile. + +#ifndef MEDIA_MP4_MP4_SEGMENTER_H_ +#define MEDIA_MP4_MP4_SEGMENTER_H_ + +#include +#include + +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "media/base/status.h" + +namespace media { + +struct MuxerOptions; + +class BufferWriter; +class EncryptorSource; +class MediaSample; +class MediaStream; + +namespace mp4 { + +class MP4Fragmenter; + +struct FileType; +struct Movie; +struct MovieFragment; +struct SegmentIndex; + +class MP4Segmenter { + public: + // Caller transfers the ownership of |ftyp| and |moov| to this class. + MP4Segmenter(const MuxerOptions& options, FileType* ftyp, Movie* moov); + virtual ~MP4Segmenter(); + + // Initialize the segmenter. Caller retains the ownership of + // |encryptor_source|. |encryptor_source| can be NULL. + virtual Status Initialize(EncryptorSource* encryptor_source, + const std::vector& streams); + + virtual Status Finalize(); + + virtual Status AddSample(const MediaStream* stream, + scoped_refptr sample); + + protected: + void InitializeSegment(); + virtual Status FinalizeSegment(); + + uint32 GetReferenceStreamId(); + + const MuxerOptions& options() const { return options_; } + FileType* ftyp() { return ftyp_.get(); } + Movie* moov() { return moov_.get(); } + BufferWriter* fragment_buffer() { return fragment_buffer_.get(); } + SegmentIndex* sidx() { return sidx_.get(); } + + private: + void InitializeFragments(); + Status FinalizeFragment(MP4Fragmenter* fragment); + + const MuxerOptions& options_; + scoped_ptr ftyp_; + scoped_ptr moov_; + scoped_ptr moof_; + scoped_ptr fragment_buffer_; + scoped_ptr sidx_; + std::vector fragmenters_; + std::vector segment_durations_; + std::map stream_map_; + bool segment_initialized_; + bool end_of_segment_; + + DISALLOW_COPY_AND_ASSIGN(MP4Segmenter); +}; + +} // namespace mp4 +} // namespace media + +#endif // MEDIA_MP4_MP4_SEGMENTER_H_ diff --git a/media/mp4/mp4_vod_segmenter.cc b/media/mp4/mp4_vod_segmenter.cc new file mode 100644 index 0000000000..64bb1b3cd4 --- /dev/null +++ b/media/mp4/mp4_vod_segmenter.cc @@ -0,0 +1,140 @@ +// Copyright (c) 2013 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. + +#include "media/mp4/mp4_vod_segmenter.h" + +#include "media/base/buffer_writer.h" +#include "media/base/media_stream.h" +#include "media/base/muxer_options.h" +#include "media/file/file.h" +#include "media/mp4/box_definitions.h" + +namespace media { +namespace mp4 { + +MP4VODSegmenter::MP4VODSegmenter(const MuxerOptions& options, + FileType* ftyp, + Movie* moov) + : MP4Segmenter(options, ftyp, moov) {} +MP4VODSegmenter::~MP4VODSegmenter() {} + +Status MP4VODSegmenter::Initialize(EncryptorSource* encryptor_source, + const std::vector& streams) { + Status status = MP4Segmenter::Initialize(encryptor_source, streams); + if (!status.ok()) + return status; + temp_file_.reset(File::Open(options().temp_file_name.c_str(), "w")); + if (temp_file_ == NULL) { + return Status(error::FILE_FAILURE, + "Cannot open file to write " + options().temp_file_name); + } + return Status::OK; +} + +Status MP4VODSegmenter::Finalize() { + DCHECK(temp_file_ != NULL && ftyp() != NULL && moov() != NULL && + vod_sidx_ != NULL); + + Status status = MP4Segmenter::Finalize(); + if (!status.ok()) + return status; + + // Close the temp file to prepare for reading later. + if (!temp_file_.release()->Close()) { + return Status(error::FILE_FAILURE, + "Cannot close the temp file " + options().temp_file_name); + } + + scoped_ptr file( + File::Open(options().output_file_name.c_str(), "w")); + if (file == NULL) { + return Status(error::FILE_FAILURE, + "Cannot open file to write " + options().output_file_name); + } + + // Write ftyp, moov and sidx to output file. + scoped_ptr buffer(new BufferWriter()); + ftyp()->Write(buffer.get()); + moov()->Write(buffer.get()); + vod_sidx_->Write(buffer.get()); + status = buffer->WriteToFile(file.get()); + if (!status.ok()) + return status; + + // Load the temp file and write to output file. + scoped_ptr temp_file( + File::Open(options().temp_file_name.c_str(), "r")); + if (temp_file == NULL) { + return Status(error::FILE_FAILURE, + "Cannot open file to read " + options().temp_file_name); + } + + const int kBufSize = 0x40000; // 256KB. + scoped_ptr buf(new uint8[kBufSize]); + while (!temp_file->Eof()) { + int64 size = temp_file->Read(buf.get(), kBufSize); + if (size <= 0) { + return Status(error::FILE_FAILURE, + "Failed to read file " + options().temp_file_name); + } + int64 size_written = file->Write(buf.get(), size); + if (size_written != size) { + return Status(error::FILE_FAILURE, + "Failed to write file " + options().output_file_name); + } + } + return Status::OK; +} + +Status MP4VODSegmenter::FinalizeSegment() { + Status status = MP4Segmenter::FinalizeSegment(); + if (!status.ok()) + return status; + + DCHECK(sidx() != NULL && fragment_buffer() != NULL); + // sidx() contains pre-generated segment references with one reference per + // fragment. In VOD, this segment is converted into a subsegment, i.e. one + // reference, which contains all the fragments in sidx(). + std::vector& refs = sidx()->references; + SegmentReference& vod_ref = refs[0]; + uint64 first_sap_time = + refs[0].sap_delta_time + refs[0].earliest_presentation_time; + for (uint32 i = 1; i < sidx()->references.size(); ++i) { + vod_ref.referenced_size += refs[i].referenced_size; + // TODO(kqyang): should we calculate subsegment duration by subtracting + // earliest_presentation time instead? + vod_ref.subsegment_duration += refs[i].subsegment_duration; + vod_ref.earliest_presentation_time = std::min( + vod_ref.earliest_presentation_time, refs[i].earliest_presentation_time); + + if (vod_ref.sap_type == SegmentReference::TypeUnknown && + refs[i].sap_type != SegmentReference::TypeUnknown) { + vod_ref.sap_type = refs[i].sap_type; + first_sap_time = + refs[i].sap_delta_time + refs[i].earliest_presentation_time; + } + } + // Calculate sap delta time w.r.t. earliest_presentation_time. + if (vod_ref.sap_type != SegmentReference::TypeUnknown) { + vod_ref.sap_delta_time = + first_sap_time - vod_ref.earliest_presentation_time; + } + + // Create segment if it does not exist yet. + if (vod_sidx_ == NULL) { + vod_sidx_.reset(new SegmentIndex()); + vod_sidx_->reference_id = sidx()->reference_id; + vod_sidx_->timescale = sidx()->timescale; + // earliest_presentation_time is the earliest presentation time of any + // access unit in the reference stream in the first subsegment. + vod_sidx_->earliest_presentation_time = vod_ref.earliest_presentation_time; + } + vod_sidx_->references.push_back(vod_ref); + + // Append fragment buffer to temp file. + return fragment_buffer()->WriteToFile(temp_file_.get()); +} + +} // namespace mp4 +} // namespace media diff --git a/media/mp4/mp4_vod_segmenter.h b/media/mp4/mp4_vod_segmenter.h new file mode 100644 index 0000000000..f90a4c24c2 --- /dev/null +++ b/media/mp4/mp4_vod_segmenter.h @@ -0,0 +1,50 @@ +// Copyright (c) 2013 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. +// +// Segmenter for MP4 Dash Video-On-Demand profile. A single MP4 file with a +// single segment is created, i.e. with only one SIDX box. The generated media +// file could contain one to many subsegments with subsegment duration +// defined by|MuxerOptions.segment_duration|. A subsegment could contain one +// to many fragments with fragment duration defined by +// |MuxerOptions.fragment_duration|. The actual subsegment or fragment duration +// may not match the defined duration exactly but in a best effort basic, i.e. +// the segmenter tries to end subsegment/fragment at the first sample with +// overall subsegment/fragment duration not smaller than defined duration and +// yet meet SAP requirements. VOD segmenter ignores +// |MuxerOptions.num_subsegments_per_sidx|. + +#ifndef MEDIA_MP4_MP4_VOD_SEGMENTER_H_ +#define MEDIA_MP4_MP4_VOD_SEGMENTER_H_ + +#include "media/file/file_closer.h" +#include "media/mp4/mp4_segmenter.h" + +namespace media { +namespace mp4 { + +class MP4VODSegmenter : public MP4Segmenter { + public: + // Caller transfers the ownership of |ftyp| and |moov| to this class. + MP4VODSegmenter(const MuxerOptions& options, FileType* ftyp, Movie* moov); + ~MP4VODSegmenter(); + + // MP4Segmenter implementations. + virtual Status Initialize(EncryptorSource* encryptor_source, + const std::vector& streams) OVERRIDE; + virtual Status Finalize() OVERRIDE; + + protected: + virtual Status FinalizeSegment() OVERRIDE; + + private: + scoped_ptr vod_sidx_; + scoped_ptr temp_file_; + + DISALLOW_COPY_AND_ASSIGN(MP4VODSegmenter); +}; + +} // namespace mp4 +} // namespace media + +#endif // MEDIA_MP4_MP4_VOD_SEGMENTER_H_ diff --git a/media/test/packager_test.cc b/media/test/packager_test.cc index 318a76ffe9..68b079a4a7 100644 --- a/media/test/packager_test.cc +++ b/media/test/packager_test.cc @@ -2,34 +2,57 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +#include "base/strings/string_number_conversions.h" #include "media/base/demuxer.h" +#include "media/base/fixed_encryptor_source.h" #include "media/base/media_sample.h" #include "media/base/media_stream.h" #include "media/base/muxer.h" +#include "media/base/muxer_options.h" #include "media/base/status_test_util.h" #include "media/base/stream_info.h" #include "media/base/test_data_util.h" +#include "media/mp4/mp4_muxer.h" #include "testing/gtest/include/gtest/gtest.h" +using ::testing::Combine; +using ::testing::Values; +using ::testing::ValuesIn; + namespace { const char* kMediaFiles[] = {"bear-1280x720.mp4", "bear-1280x720-av_frag.mp4"}; -} + +// Encryption constants. +const char kKeyIdHex[] = "e5007e6e9dcd5ac095202ed3758382cd"; +const char kKeyHex[] = "6fc96fe628a265b13aeddec0bc421f4d"; +const char kPsshHex[] = + "08011210e5007e6e9dcd5ac095202ed3" + "758382cd1a0d7769646576696e655f746573742211544553545f" + "434f4e54454e545f49445f312a025344"; +const uint32 kClearMilliseconds = 1500; + +} // namespace namespace media { class TestingMuxer : public Muxer { public: - TestingMuxer(const Options& options, EncryptorSource* encryptor_source) + TestingMuxer(const MuxerOptions& options, EncryptorSource* encryptor_source) : Muxer(options, encryptor_source) {} + virtual Status Initialize() OVERRIDE { + DVLOG(1) << "Initialize is called."; + return Status::OK; + } + virtual Status AddSample(const MediaStream* stream, - scoped_refptr sample) { + scoped_refptr sample) OVERRIDE { DVLOG(1) << "Add Sample: " << sample->ToString(); DVLOG(2) << "To Stream: " << stream->ToString(); return Status::OK; } - virtual Status Finalize() { + virtual Status Finalize() OVERRIDE { DVLOG(1) << "Finalize is called."; return Status::OK; } @@ -38,31 +61,80 @@ class TestingMuxer : public Muxer { DISALLOW_COPY_AND_ASSIGN(TestingMuxer); }; -class PackagerTest : public ::testing::TestWithParam {}; +typedef Muxer* CreateMuxerFunc(const std::string& input_file_name, + EncryptorSource* encryptor_source); + +Muxer* CreateTestingMuxer(const std::string& input_file_name, + EncryptorSource* encryptor_source) { + MuxerOptions options; + return new TestingMuxer(options, NULL); +} + +Muxer* CreateNormalMP4Muxer(const std::string& input_file_name, + EncryptorSource* encryptor_source) { + MuxerOptions options; + options.single_segment = true; + options.segment_duration = 0.005; + options.fragment_duration = 0.002; + options.segment_sap_aligned = true; + options.fragment_sap_aligned = true; + options.num_subsegments_per_sidx = 1; + options.output_file_name = "/tmp/clear_" + input_file_name; + options.segment_template = "/tmp/template$Number$.m4s"; + options.temp_file_name = "/tmp/tmp.mp4"; + return new mp4::MP4Muxer(options, NULL); +} + +Muxer* CreateEncryptionMP4Muxer(const std::string& input_file_name, + EncryptorSource* encryptor_source) { + MuxerOptions options; + options.single_segment = true; + options.segment_duration = 0.005; + options.fragment_duration = 0.002; + options.segment_sap_aligned = true; + options.fragment_sap_aligned = true; + options.num_subsegments_per_sidx = 1; + options.output_file_name = "/tmp/enc_" + input_file_name; + options.segment_template = "/tmp/template$Number$.m4s"; + options.temp_file_name = "/tmp/tmp.mp4"; + return new mp4::MP4Muxer(options, encryptor_source); +} + +class PackagerTest : public ::testing::TestWithParam< + ::std::tr1::tuple > {}; TEST_P(PackagerTest, Remux) { - Demuxer demuxer( - GetTestDataFilePath(GetParam()).value(), NULL); + std::string file_name = ::std::tr1::get<0>(GetParam()); + CreateMuxerFunc* CreateMuxer = ::std::tr1::get<1>(GetParam()); + + Demuxer demuxer(GetTestDataFilePath(file_name).value(), NULL); ASSERT_OK(demuxer.Initialize()); - LOG(INFO) << "Num Streams: " << demuxer.num_streams(); - for (int i = 0; i < demuxer.num_streams(); ++i) { - LOG(INFO) << "Streams " << i << " " << demuxer.streams(i)->ToString(); + LOG(INFO) << "Num Streams: " << demuxer.streams().size(); + for (int i = 0; i < demuxer.streams().size(); ++i) { + LOG(INFO) << "Streams " << i << " " << demuxer.streams()[i]->ToString(); } - Muxer::Options options; - TestingMuxer muxer(options, NULL); + FixedEncryptorSource encryptor_source( + kKeyIdHex, kKeyHex, kPsshHex, kClearMilliseconds); + EXPECT_OK(encryptor_source.Initialize()); - ASSERT_OK(muxer.AddStream(demuxer.streams(0))); + scoped_ptr muxer(CreateMuxer(file_name, &encryptor_source)); + + ASSERT_OK(muxer->AddStream(demuxer.streams()[0])); + ASSERT_OK(muxer->Initialize()); // Starts remuxing process. ASSERT_OK(demuxer.Run()); - ASSERT_OK(muxer.Finalize()); + ASSERT_OK(muxer->Finalize()); } INSTANTIATE_TEST_CASE_P(PackagerE2ETest, PackagerTest, - ::testing::ValuesIn(kMediaFiles)); + Combine(ValuesIn(kMediaFiles), + Values(&CreateTestingMuxer, + &CreateNormalMP4Muxer, + &CreateEncryptionMP4Muxer))); } // namespace media diff --git a/packager.gyp b/packager.gyp index 60ffd2e9d9..2631adcc97 100644 --- a/packager.gyp +++ b/packager.gyp @@ -13,11 +13,17 @@ 'target_name': 'media_base', 'type': 'static_library', 'sources': [ + 'media/base/aes_encryptor.cc', + 'media/base/aes_encryptor.h', 'media/base/audio_stream_info.cc', 'media/base/audio_stream_info.h', 'media/base/bit_reader.cc', 'media/base/bit_reader.h', 'media/base/buffers.h', + 'media/base/buffer_reader.cc', + 'media/base/buffer_reader.h', + 'media/base/buffer_writer.cc', + 'media/base/buffer_writer.h', 'media/base/byte_queue.cc', 'media/base/byte_queue.h', 'media/base/container_names.cc', @@ -29,7 +35,10 @@ 'media/base/decrypt_config.cc', 'media/base/decrypt_config.h', 'media/base/decryptor_source.h', + 'media/base/encryptor_source.cc', 'media/base/encryptor_source.h', + 'media/base/fixed_encryptor_source.cc', + 'media/base/fixed_encryptor_source.h', 'media/base/limits.h', 'media/base/media_parser.h', 'media/base/media_sample.cc', @@ -48,6 +57,7 @@ ], 'dependencies': [ 'base/base.gyp:base', + 'third_party/openssl/openssl.gyp:openssl', ], }, { @@ -68,13 +78,16 @@ 'target_name': 'media_base_unittest', 'type': 'executable', 'sources': [ + 'media/base/aes_encryptor_unittest.cc', 'media/base/bit_reader_unittest.cc', + 'media/base/buffer_writer_unittest.cc', 'media/base/container_names_unittest.cc', 'media/base/status_test_util.h', 'media/base/status_test_util_unittest.cc', 'media/base/status_unittest.cc', ], 'dependencies': [ + 'file', 'media_base', 'media_test_support', 'testing/gtest.gyp:gtest', @@ -87,10 +100,14 @@ 'sources': [ 'media/mp4/aac.cc', 'media/mp4/aac.h', + 'media/mp4/box.cc', + 'media/mp4/box.h', + 'media/mp4/box_buffer_interface.h', 'media/mp4/box_definitions.cc', 'media/mp4/box_definitions.h', 'media/mp4/box_reader.cc', 'media/mp4/box_reader.h', + 'media/mp4/box_writer.h', 'media/mp4/cenc.cc', 'media/mp4/cenc.h', 'media/mp4/chunk_info_iterator.cc', @@ -102,8 +119,18 @@ 'media/mp4/es_descriptor.cc', 'media/mp4/es_descriptor.h', 'media/mp4/fourccs.h', + 'media/mp4/mp4_fragmenter.cc', + 'media/mp4/mp4_fragmenter.h', + 'media/mp4/mp4_general_segmenter.cc', + 'media/mp4/mp4_general_segmenter.h', 'media/mp4/mp4_media_parser.cc', 'media/mp4/mp4_media_parser.h', + 'media/mp4/mp4_muxer.cc', + 'media/mp4/mp4_muxer.h', + 'media/mp4/mp4_segmenter.cc', + 'media/mp4/mp4_segmenter.h', + 'media/mp4/mp4_vod_segmenter.cc', + 'media/mp4/mp4_vod_segmenter.h', 'media/mp4/offset_byte_queue.cc', 'media/mp4/offset_byte_queue.h', 'media/mp4/rcheck.h', @@ -121,6 +148,7 @@ 'type': 'executable', 'sources': [ 'media/mp4/aac_unittest.cc', + 'media/mp4/box_definitions_unittest.cc', 'media/mp4/box_reader_unittest.cc', 'media/mp4/chunk_info_iterator_unittest.cc', 'media/mp4/composition_offset_iterator_unittest.cc', @@ -144,6 +172,7 @@ 'sources': [ 'media/file/file.cc', 'media/file/file.h', + 'media/file/file_closer.h', 'media/file/local_file.cc', 'media/file/local_file.h', ],