MP4 Muxer implementation.
Change-Id: I2aeb506e5bf7e2301e855456688273fe5f3988a2
This commit is contained in:
parent
01646837e1
commit
1b5c3b5316
|
@ -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<MediaStream*>::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<MediaSample> 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
|
||||
|
|
|
@ -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<MediaSample> 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<MediaStream*>& 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<MediaStream*> streams_;
|
||||
EncryptorSource* const encryptor_source_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(Muxer);
|
||||
};
|
||||
|
|
|
@ -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 <string>
|
||||
|
||||
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_
|
|
@ -7,6 +7,8 @@
|
|||
#ifndef PACKAGER_FILE_FILE_H_
|
||||
#define PACKAGER_FILE_FILE_H_
|
||||
|
||||
#include <string>
|
||||
|
||||
#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);
|
||||
};
|
||||
|
||||
|
|
|
@ -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_
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@ class LocalFile : public File {
|
|||
virtual bool Open() OVERRIDE;
|
||||
|
||||
private:
|
||||
std::string file_name_;
|
||||
std::string file_mode_;
|
||||
FILE* internal_file_;
|
||||
|
||||
|
|
|
@ -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 <typename T>
|
||||
bool OptimizeSampleEntries(std::vector<T>* entries, T* default_value) {
|
||||
DCHECK(entries != NULL && default_value != NULL);
|
||||
DCHECK(!entries->empty());
|
||||
|
||||
typename std::vector<T>::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<MediaSample> 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<MediaSample> 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
|
|
@ -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 <vector>
|
||||
|
||||
#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<MediaSample> 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<MediaSample> 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<BufferWriter> data_;
|
||||
scoped_ptr<BufferWriter> aux_data_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(MP4Fragmenter);
|
||||
};
|
||||
|
||||
} // namespace mp4
|
||||
} // namespace media
|
||||
|
||||
#endif // MEDIA_MP4_MP4_FRAGMENTER_H_
|
|
@ -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<MediaStream*>& 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<BufferWriter> 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<SegmentReference>& 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<BufferWriter> 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
|
|
@ -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<MediaStream*>& streams) OVERRIDE;
|
||||
|
||||
protected:
|
||||
virtual Status FinalizeSegment() OVERRIDE;
|
||||
|
||||
private:
|
||||
// Write segment to file.
|
||||
Status WriteSegment();
|
||||
|
||||
scoped_ptr<SegmentType> styp_;
|
||||
uint32 num_segments_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(MP4GeneralSegmenter);
|
||||
};
|
||||
|
||||
} // namespace mp4
|
||||
} // namespace media
|
||||
|
||||
#endif // MEDIA_MP4_MP4_GENERAL_SEGMENTER_H_
|
|
@ -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<FileType> ftyp(new FileType);
|
||||
scoped_ptr<Movie> 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<VideoStreamInfo*>(streams()[i]->info().get()),
|
||||
&trak,
|
||||
i + 1);
|
||||
break;
|
||||
case kStreamAudio:
|
||||
GenerateAudioTrak(
|
||||
static_cast<AudioStreamInfo*>(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<MediaSample> 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
|
|
@ -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<MediaSample> 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<MP4Segmenter> segmenter_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(MP4Muxer);
|
||||
};
|
||||
|
||||
} // namespace mp4
|
||||
} // namespace media
|
||||
|
||||
#endif // MEDIA_MP4_MP4_MUXER_H_
|
|
@ -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 <algorithm>
|
||||
|
||||
#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<double>(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<MediaStream*>& 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<VideoStreamInfo*>(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<MP4Fragmenter*>::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<Track>::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<MediaSample> 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<uint64>::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<MP4Fragmenter*>::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<MP4Fragmenter*>::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
|
|
@ -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 <map>
|
||||
#include <vector>
|
||||
|
||||
#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<MediaStream*>& streams);
|
||||
|
||||
virtual Status Finalize();
|
||||
|
||||
virtual Status AddSample(const MediaStream* stream,
|
||||
scoped_refptr<MediaSample> 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<FileType> ftyp_;
|
||||
scoped_ptr<Movie> moov_;
|
||||
scoped_ptr<MovieFragment> moof_;
|
||||
scoped_ptr<BufferWriter> fragment_buffer_;
|
||||
scoped_ptr<SegmentIndex> sidx_;
|
||||
std::vector<MP4Fragmenter*> fragmenters_;
|
||||
std::vector<uint64> segment_durations_;
|
||||
std::map<const MediaStream*, uint32> stream_map_;
|
||||
bool segment_initialized_;
|
||||
bool end_of_segment_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(MP4Segmenter);
|
||||
};
|
||||
|
||||
} // namespace mp4
|
||||
} // namespace media
|
||||
|
||||
#endif // MEDIA_MP4_MP4_SEGMENTER_H_
|
|
@ -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<MediaStream*>& 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, FileCloser> 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<BufferWriter> 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<File, FileCloser> 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<uint8[]> 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<SegmentReference>& 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
|
|
@ -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<MediaStream*>& streams) OVERRIDE;
|
||||
virtual Status Finalize() OVERRIDE;
|
||||
|
||||
protected:
|
||||
virtual Status FinalizeSegment() OVERRIDE;
|
||||
|
||||
private:
|
||||
scoped_ptr<SegmentIndex> vod_sidx_;
|
||||
scoped_ptr<File, FileCloser> temp_file_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(MP4VODSegmenter);
|
||||
};
|
||||
|
||||
} // namespace mp4
|
||||
} // namespace media
|
||||
|
||||
#endif // MEDIA_MP4_MP4_VOD_SEGMENTER_H_
|
|
@ -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<MediaSample> sample) {
|
||||
scoped_refptr<MediaSample> 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<const char*> {};
|
||||
typedef Muxer* CreateMuxerFunc(const std::string& input_file_name,
|
||||
EncryptorSource* encryptor_source);
|
||||
|
||||
TEST_P(PackagerTest, Remux) {
|
||||
Demuxer demuxer(
|
||||
GetTestDataFilePath(GetParam()).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();
|
||||
Muxer* CreateTestingMuxer(const std::string& input_file_name,
|
||||
EncryptorSource* encryptor_source) {
|
||||
MuxerOptions options;
|
||||
return new TestingMuxer(options, NULL);
|
||||
}
|
||||
|
||||
Muxer::Options options;
|
||||
TestingMuxer muxer(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);
|
||||
}
|
||||
|
||||
ASSERT_OK(muxer.AddStream(demuxer.streams(0)));
|
||||
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<const char*, CreateMuxerFunc*> > {};
|
||||
|
||||
TEST_P(PackagerTest, Remux) {
|
||||
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.streams().size();
|
||||
for (int i = 0; i < demuxer.streams().size(); ++i) {
|
||||
LOG(INFO) << "Streams " << i << " " << demuxer.streams()[i]->ToString();
|
||||
}
|
||||
|
||||
FixedEncryptorSource encryptor_source(
|
||||
kKeyIdHex, kKeyHex, kPsshHex, kClearMilliseconds);
|
||||
EXPECT_OK(encryptor_source.Initialize());
|
||||
|
||||
scoped_ptr<Muxer> 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
|
||||
|
|
29
packager.gyp
29
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',
|
||||
],
|
||||
|
|
Loading…
Reference in New Issue