From 8f2cd6da917dd20b2c9599cb8e24b174aa1f9308 Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Tue, 21 Feb 2017 10:36:50 -0800 Subject: [PATCH] Implements Demuxer and Muxer media handlers - Also sets up the packaging and verify it works. Some of the changes are temporary to get the integration going. Change-Id: I0cf6c379d185e157808acabb9ef58ff93d4a39ae --- packager/app/packager_main.cc | 50 ++-- packager/app/packager_util.cc | 69 +---- packager/app/packager_util.h | 28 +-- packager/app/test/packager_test.py | 28 ++- .../testdata/bear-640x360-a-por-BR-golden.mp4 | Bin 0 -> 43688 bytes .../testdata/bear-640x360-a-por-golden.mp4 | Bin 0 -> 43688 bytes .../bear-640x360-av-por-BR-golden.mpd | 23 ++ .../testdata/bear-640x360-av-por-golden.mpd | 23 ++ packager/media/base/demuxer.cc | 238 ++++++++++++------ packager/media/base/demuxer.h | 83 ++++-- packager/media/base/media_base.gyp | 2 - packager/media/base/media_handler.h | 4 + packager/media/base/media_stream.cc | 111 -------- packager/media/base/media_stream.h | 80 ------ packager/media/base/muxer.cc | 75 ++---- packager/media/base/muxer.h | 34 ++- packager/media/formats/mp2t/ts_muxer.cc | 16 +- packager/media/formats/mp2t/ts_muxer.h | 5 +- packager/media/formats/mp2t/ts_segmenter.h | 1 - packager/media/formats/mp2t/ts_writer.h | 4 +- packager/media/formats/mp4/mp4_muxer.cc | 33 +-- packager/media/formats/mp4/mp4_muxer.h | 5 +- .../formats/mp4/multi_segment_segmenter.cc | 1 - packager/media/formats/mp4/segmenter.cc | 67 +++-- packager/media/formats/mp4/segmenter.h | 9 +- .../formats/mp4/single_segment_segmenter.cc | 1 - .../formats/webm/multi_segment_segmenter.cc | 1 - packager/media/formats/webm/segmenter.cc | 1 - .../webm/two_pass_single_segment_segmenter.cc | 1 - packager/media/formats/webm/webm_muxer.cc | 15 +- packager/media/formats/webm/webm_muxer.h | 5 +- packager/media/test/packager_test.cc | 154 ++---------- 32 files changed, 439 insertions(+), 728 deletions(-) create mode 100644 packager/app/test/testdata/bear-640x360-a-por-BR-golden.mp4 create mode 100644 packager/app/test/testdata/bear-640x360-a-por-golden.mp4 create mode 100644 packager/app/test/testdata/bear-640x360-av-por-BR-golden.mpd create mode 100644 packager/app/test/testdata/bear-640x360-av-por-golden.mpd delete mode 100644 packager/media/base/media_stream.cc delete mode 100644 packager/media/base/media_stream.h diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 060b5d895b..284dc95a8f 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -168,10 +168,6 @@ class RemuxJob : public base::SimpleThread { ~RemuxJob() override {} - void AddMuxer(std::unique_ptr mux) { - muxers_.push_back(std::move(mux)); - } - Demuxer* demuxer() { return demuxer_.get(); } Status status() { return status_; } @@ -182,7 +178,6 @@ class RemuxJob : public base::SimpleThread { } std::unique_ptr demuxer_; - std::vector> muxers_; Status status_; DISALLOW_COPY_AND_ASSIGN(RemuxJob); @@ -227,15 +222,15 @@ bool StreamInfoToTextMediaInfo(const StreamDescriptor& stream_descriptor, return true; } -std::unique_ptr CreateOutputMuxer(const MuxerOptions& options, +std::shared_ptr CreateOutputMuxer(const MuxerOptions& options, MediaContainerName container) { if (container == CONTAINER_WEBM) { - return std::unique_ptr(new webm::WebMMuxer(options)); + return std::shared_ptr(new webm::WebMMuxer(options)); } else if (container == CONTAINER_MPEG2TS) { - return std::unique_ptr(new mp2t::TsMuxer(options)); + return std::shared_ptr(new mp2t::TsMuxer(options)); } else { DCHECK_EQ(container, CONTAINER_MOV); - return std::unique_ptr(new mp4::MP4Muxer(options)); + return std::shared_ptr(new mp4::MP4Muxer(options)); } } @@ -300,6 +295,7 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors, if (stream_iter->input != previous_input) { // New remux job needed. Create demux and job thread. std::unique_ptr demuxer(new Demuxer(stream_iter->input)); + demuxer->set_dump_stream_info(FLAGS_dump_stream_info); if (FLAGS_enable_widevine_decryption || FLAGS_enable_fixed_key_decryption) { std::unique_ptr key_source(CreateDecryptionKeySource()); @@ -307,23 +303,15 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors, return false; demuxer->SetKeySource(std::move(key_source)); } - Status status = demuxer->Initialize(); - if (!status.ok()) { - LOG(ERROR) << "Demuxer failed to initialize: " << status.ToString(); - return false; - } - if (FLAGS_dump_stream_info) { - printf("\nFile \"%s\":\n", stream_iter->input.c_str()); - DumpStreamInfo(demuxer->streams()); - if (stream_iter->output.empty()) - continue; // just need stream info. - } remux_jobs->emplace_back(new RemuxJob(std::move(demuxer))); previous_input = stream_iter->input; + // Skip setting up muxers if output is not needed. + if (stream_iter->output.empty()) + continue; } DCHECK(!remux_jobs->empty()); - std::unique_ptr muxer( + std::shared_ptr muxer( CreateOutputMuxer(stream_muxer_options, stream_iter->output_format)); if (FLAGS_use_fake_clock_for_muxer) muxer->set_clock(fake_clock); @@ -373,15 +361,25 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors, if (muxer_listener) muxer->SetMuxerListener(std::move(muxer_listener)); - if (!AddStreamToMuxer(remux_jobs->back()->demuxer()->streams(), - stream_iter->stream_selector, - stream_iter->language, - muxer.get())) { + auto* demuxer = remux_jobs->back()->demuxer(); + const std::string& stream_selector = stream_iter->stream_selector; + Status status = demuxer->SetHandler(stream_selector, std::move(muxer)); + if (!status.ok()) { + LOG(ERROR) << "Demuxer::SetHandler failed " << status; return false; } - remux_jobs->back()->AddMuxer(std::move(muxer)); + if (!stream_iter->language.empty()) + demuxer->SetLanguageOverride(stream_selector, stream_iter->language); } + // Initialize processing graph. + for (const std::unique_ptr& job : *remux_jobs) { + Status status = job->demuxer()->Initialize(); + if (!status.ok()) { + LOG(ERROR) << "Failed to initialize processing graph " << status; + return false; + } + } return true; } diff --git a/packager/app/packager_util.cc b/packager/app/packager_util.cc index bf46af06a4..13158ebf2b 100644 --- a/packager/app/packager_util.cc +++ b/packager/app/packager_util.cc @@ -17,15 +17,12 @@ #include "packager/base/logging.h" #include "packager/base/strings/string_number_conversions.h" #include "packager/media/base/fixed_key_source.h" -#include "packager/media/base/media_stream.h" -#include "packager/media/base/muxer.h" #include "packager/media/base/muxer_options.h" #include "packager/media/base/playready_key_source.h" #include "packager/media/base/request_signer.h" -#include "packager/media/base/stream_info.h" #include "packager/media/base/widevine_key_source.h" #include "packager/media/file/file.h" -#include "packager/mpd/base/mpd_builder.h" +#include "packager/mpd/base/mpd_options.h" DEFINE_bool(mp4_use_decoding_timestamp_in_timeline, false, @@ -38,12 +35,6 @@ DEFINE_bool(dump_stream_info, false, "Dump demuxed stream info."); namespace shaka { namespace media { -void DumpStreamInfo(const std::vector>& streams) { - printf("Found %zu stream(s).\n", streams.size()); - for (size_t i = 0; i < streams.size(); ++i) - printf("Stream [%zu] %s\n", i, streams[i]->info()->ToString().c_str()); -} - std::unique_ptr CreateSigner() { std::unique_ptr signer; @@ -196,63 +187,5 @@ bool GetMpdOptions(bool on_demand_profile, MpdOptions* mpd_options) { return true; } -MediaStream* FindFirstStreamOfType( - const std::vector>& streams, - StreamType stream_type) { - for (const std::unique_ptr& stream : streams) { - if (stream->info()->stream_type() == stream_type) - return stream.get(); - } - return nullptr; -} -MediaStream* FindFirstVideoStream( - const std::vector>& streams) { - return FindFirstStreamOfType(streams, kStreamVideo); -} -MediaStream* FindFirstAudioStream( - const std::vector>& streams) { - return FindFirstStreamOfType(streams, kStreamAudio); -} - -bool AddStreamToMuxer(const std::vector>& streams, - const std::string& stream_selector, - const std::string& language_override, - Muxer* muxer) { - DCHECK(muxer); - - MediaStream* stream = nullptr; - if (stream_selector == "video") { - stream = FindFirstVideoStream(streams); - } else if (stream_selector == "audio") { - stream = FindFirstAudioStream(streams); - } else { - // Expect stream_selector to be a zero based stream id. - size_t stream_id; - if (!base::StringToSizeT(stream_selector, &stream_id) || - stream_id >= streams.size()) { - LOG(ERROR) << "Invalid argument --stream=" << stream_selector << "; " - << "should be 'audio', 'video', or a number within [0, " - << streams.size() - 1 << "]."; - return false; - } - stream = streams[stream_id].get(); - DCHECK(stream); - } - - // This could occur only if stream_selector=audio|video and the corresponding - // stream does not exist in the input. - if (!stream) { - LOG(ERROR) << "No " << stream_selector << " stream found in the input."; - return false; - } - - if (!language_override.empty()) { - stream->info()->set_language(language_override); - } - - muxer->AddStream(stream); - return true; -} - } // namespace media } // namespace shaka diff --git a/packager/app/packager_util.h b/packager/app/packager_util.h index ec1abad0dd..684ced2ffd 100644 --- a/packager/app/packager_util.h +++ b/packager/app/packager_util.h @@ -6,13 +6,12 @@ // // Packager utility functions. -#ifndef APP_PACKAGER_UTIL_H_ -#define APP_PACKAGER_UTIL_H_ +#ifndef PACKAGER_APP_PACKAGER_UTIL_H_ +#define PACKAGER_APP_PACKAGER_UTIL_H_ #include + #include -#include -#include DECLARE_bool(dump_stream_info); @@ -23,13 +22,8 @@ struct MpdOptions; namespace media { class KeySource; -class MediaStream; -class Muxer; struct MuxerOptions; -/// Print all the stream info for the provided strings to standard output. -void DumpStreamInfo(const std::vector>& streams); - /// Create KeySource based on provided command line options for content /// encryption. Also fetches keys. /// @return A std::unique_ptr containing a new KeySource, or nullptr if @@ -48,21 +42,7 @@ bool GetMuxerOptions(MuxerOptions* muxer_options); /// Fill MpdOptions members using provided command line options. bool GetMpdOptions(bool on_demand_profile, MpdOptions* mpd_options); -/// Select and add a stream from a provided set to a muxer. -/// @param streams contains the set of MediaStreams from which to select. -/// @param stream_selector is a string containing one of the following values: -/// "audio" to select the first audio track, "video" to select the first -/// video track, or a decimal number indicating which track number to -/// select (start at "1"). -/// @param language_override is a string which, if non-empty, overrides the -/// stream's language metadata. -/// @return true if successful, false otherwise. -bool AddStreamToMuxer(const std::vector>& streams, - const std::string& stream_selector, - const std::string& language_override, - Muxer* muxer); - } // namespace media } // namespace shaka -#endif // APP_PACKAGER_UTIL_H_ +#endif // PACKAGER_APP_PACKAGER_UTIL_H_ diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index 51e86d9944..aa16c2d0e1 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -92,6 +92,22 @@ class PackagerAppTest(unittest.TestCase): self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4') self._DiffGold(self.mpd_output, 'bear-640x360-av-golden.mpd') + def testPackageAudioVideoWithLanguageOverride(self): + self.packager.Package( + self._GetStreams(['audio', 'video'], language_override='por-BR'), + self._GetFlags()) + self._DiffGold(self.output[0], 'bear-640x360-a-por-golden.mp4') + self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4') + self._DiffGold(self.mpd_output, 'bear-640x360-av-por-golden.mpd') + + def testPackageAudioVideoWithLanguageOverrideWithSubtag(self): + self.packager.Package( + self._GetStreams(['audio', 'video'], language_override='por-BR'), + self._GetFlags()) + self._DiffGold(self.output[0], 'bear-640x360-a-por-BR-golden.mp4') + self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4') + self._DiffGold(self.mpd_output, 'bear-640x360-av-por-BR-golden.mpd') + # Package all video, audio, and text. def testPackageVideoAudioText(self): audio_video_streams = self._GetStreams(['audio', 'video']) @@ -438,6 +454,7 @@ class PackagerAppTest(unittest.TestCase): def _GetStreams(self, stream_descriptors, + language_override=None, output_format=None, live=False, test_files=None): @@ -466,9 +483,6 @@ class PackagerAppTest(unittest.TestCase): 'input=%s,stream=%s,init_segment=%s-init.mp4,' 'segment_template=%s-$Number$.m4s' % (test_file, stream_descriptor, output_prefix, output_prefix)) - if output_format: - stream += ',format=%s' % output_format - streams.append(stream) self.output.append(output_prefix) else: output = '%s.%s' % ( @@ -476,10 +490,12 @@ class PackagerAppTest(unittest.TestCase): self._GetExtension(stream_descriptor, output_format)) stream = ('input=%s,stream=%s,output=%s' % (test_file, stream_descriptor, output)) - if output_format: - stream += ',format=%s' % output_format - streams.append(stream) self.output.append(output) + if output_format: + stream += ',format=%s' % output_format + if language_override: + stream += ',lang=%s' % language_override + streams.append(stream) return streams def _GetExtension(self, stream_descriptor, output_format): diff --git a/packager/app/test/testdata/bear-640x360-a-por-BR-golden.mp4 b/packager/app/test/testdata/bear-640x360-a-por-BR-golden.mp4 new file mode 100644 index 0000000000000000000000000000000000000000..d50176a494bb6f4f2ccf39e57cae4f942c03a25e GIT binary patch literal 43688 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

C@Y&$3r;|X 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

C@Y&$3r;|X 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 z + + + + + + output_video.mp4 + + + + + + + + + output_audio.mp4 + + + + + + + diff --git a/packager/app/test/testdata/bear-640x360-av-por-golden.mpd b/packager/app/test/testdata/bear-640x360-av-por-golden.mpd new file mode 100644 index 0000000000..30687a598c --- /dev/null +++ b/packager/app/test/testdata/bear-640x360-av-por-golden.mpd @@ -0,0 +1,23 @@ + + + + + + + output_video.mp4 + + + + + + + + + output_audio.mp4 + + + + + + + diff --git a/packager/media/base/demuxer.cc b/packager/media/base/demuxer.cc index ccd71cae6e..93e90e1962 100644 --- a/packager/media/base/demuxer.cc +++ b/packager/media/base/demuxer.cc @@ -6,12 +6,14 @@ #include "packager/media/base/demuxer.h" +#include + #include "packager/base/bind.h" #include "packager/base/logging.h" +#include "packager/base/strings/string_number_conversions.h" #include "packager/media/base/decryptor_source.h" #include "packager/media/base/key_source.h" #include "packager/media/base/media_sample.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/stream_info.h" #include "packager/media/file/file.h" #include "packager/media/formats/mp2t/mp2t_media_parser.h" @@ -28,19 +30,45 @@ const size_t kBufSize = 0x200000; // 2MB // samples before seeing init_event, something is not right. The number // set here is arbitrary though. const size_t kQueuedSamplesLimit = 10000; +const int kInvalidStreamIndex = -1; +const int kBaseVideoOutputStreamIndex = 0x100; +const int kBaseAudioOutputStreamIndex = 0x200; + +std::string GetStreamLabel(int stream_index) { + switch (stream_index) { + case kBaseVideoOutputStreamIndex: + return "video"; + case kBaseAudioOutputStreamIndex: + return "audio"; + default: + return base::IntToString(stream_index); + } +} + +bool GetStreamIndex(const std::string& stream_label, int* stream_index) { + DCHECK(stream_index); + if (stream_label == "video") { + *stream_index = kBaseVideoOutputStreamIndex; + } else if (stream_label == "audio") { + *stream_index = kBaseAudioOutputStreamIndex; + } else { + // Expect stream_label to be a zero based stream id. + if (!base::StringToInt(stream_label, stream_index)) { + LOG(ERROR) << "Invalid argument --stream=" << stream_label << "; " + << "should be 'audio', 'video', or a number"; + return false; + } + } + return true; +} + } namespace shaka { namespace media { Demuxer::Demuxer(const std::string& file_name) - : file_name_(file_name), - media_file_(NULL), - init_event_received_(false), - container_name_(CONTAINER_UNKNOWN), - buffer_(new uint8_t[kBufSize]), - cancelled_(false) { -} + : file_name_(file_name), buffer_(new uint8_t[kBufSize]) {} Demuxer::~Demuxer() { if (media_file_) @@ -51,9 +79,74 @@ void Demuxer::SetKeySource(std::unique_ptr key_source) { key_source_ = std::move(key_source); } -Status Demuxer::Initialize() { +Status Demuxer::Run() { + LOG(INFO) << "Demuxer::Run() on file '" << file_name_ << "'."; + Status status = InitializeParser(); + // ParserInitEvent callback is called after a few calls to Parse(), which sets + // up the streams. Only after that, we can verify the outputs below. + while (!all_streams_ready_ && status.ok()) + status.Update(Parse()); + // If no output is defined, then return success after receiving all stream + // info. + if (all_streams_ready_ && output_handlers().empty()) + return Status::OK; + // Check if all specified outputs exists. + for (const auto& pair : output_handlers()) { + if (std::find(stream_indexes_.begin(), stream_indexes_.end(), pair.first) == + stream_indexes_.end()) { + LOG(ERROR) << "Invalid argument, stream=" << GetStreamLabel(pair.first) + << " not available."; + return Status(error::INVALID_ARGUMENT, "Stream not available"); + } + } + + while (!cancelled_ && status.ok()) + status.Update(Parse()); + if (cancelled_ && status.ok()) + return Status(error::CANCELLED, "Demuxer run cancelled"); + + if (status.error_code() == error::END_OF_STREAM) { + for (int stream_index : stream_indexes_) { + status = FlushStream(stream_index); + if (!status.ok()) + return status; + } + return Status::OK; + } + return status; +} + +void Demuxer::Cancel() { + cancelled_ = true; +} + +Status Demuxer::SetHandler(const std::string& stream_label, + std::shared_ptr handler) { + int stream_index = kInvalidStreamIndex; + if (!GetStreamIndex(stream_label, &stream_index)) { + return Status(error::INVALID_ARGUMENT, + "Invalid stream: " + stream_label); + } + return MediaHandler::SetHandler(stream_index, std::move(handler)); +} + +void Demuxer::SetLanguageOverride(const std::string& stream_label, + const std::string& language_override) { + int stream_index = kInvalidStreamIndex; + if (!GetStreamIndex(stream_label, &stream_index)) + LOG(WARNING) << "Invalid stream for language override " << stream_label; + language_overrides_[stream_index] = language_override; +} + +Demuxer::QueuedSample::QueuedSample(uint32_t local_track_id, + std::shared_ptr local_sample) + : track_id(local_track_id), sample(local_sample) {} + +Demuxer::QueuedSample::~QueuedSample() {} + +Status Demuxer::InitializeParser() { DCHECK(!media_file_); - DCHECK(!init_event_received_); + DCHECK(!all_streams_ready_); LOG(INFO) << "Initialize Demuxer for file '" << file_name_ << "'."; @@ -105,34 +198,65 @@ Status Demuxer::Initialize() { // Handle trailing 'moov'. if (container_name_ == CONTAINER_MOV) static_cast(parser_.get())->LoadMoov(file_name_); - if (!parser_->Parse(buffer_.get(), bytes_read)) { - init_parsing_status_ = - Status(error::PARSER_FAILURE, "Cannot parse media file " + file_name_); + return Status(error::PARSER_FAILURE, + "Cannot parse media file " + file_name_); } - - // Parse until init event received or on error. - while (!init_event_received_ && init_parsing_status_.ok()) - init_parsing_status_ = Parse(); - // Defer error reporting if init completed successfully. - return init_event_received_ ? Status::OK : init_parsing_status_; + return Status::OK; } void Demuxer::ParserInitEvent( const std::vector>& stream_infos) { - init_event_received_ = true; - for (const std::shared_ptr& stream_info : stream_infos) - streams_.emplace_back(new MediaStream(stream_info, this)); -} + if (dump_stream_info_) { + printf("\nFile \"%s\":\n", file_name_.c_str()); + printf("Found %zu stream(s).\n", stream_infos.size()); + for (size_t i = 0; i < stream_infos.size(); ++i) + printf("Stream [%zu] %s\n", i, stream_infos[i]->ToString().c_str()); + } -Demuxer::QueuedSample::QueuedSample(uint32_t local_track_id, - std::shared_ptr local_sample) - : track_id(local_track_id), sample(local_sample) {} -Demuxer::QueuedSample::~QueuedSample() {} + int base_stream_index = 0; + bool video_handler_set = + output_handlers().find(kBaseVideoOutputStreamIndex) != + output_handlers().end(); + bool audio_handler_set = + output_handlers().find(kBaseAudioOutputStreamIndex) != + output_handlers().end(); + for (const std::shared_ptr& stream_info : stream_infos) { + int stream_index = base_stream_index; + if (video_handler_set && stream_info->stream_type() == kStreamVideo) { + stream_index = kBaseVideoOutputStreamIndex; + // Only for the first video stream. + video_handler_set = false; + } + if (audio_handler_set && stream_info->stream_type() == kStreamAudio) { + stream_index = kBaseAudioOutputStreamIndex; + // Only for the first audio stream. + audio_handler_set = false; + } + + const bool handler_set = + output_handlers().find(stream_index) != output_handlers().end(); + if (handler_set) { + track_id_to_stream_index_map_[stream_info->track_id()] = stream_index; + stream_indexes_.push_back(stream_index); + auto iter = language_overrides_.find(stream_index); + if (iter != language_overrides_.end() && + stream_info->stream_type() != kStreamVideo) { + stream_info->set_language(iter->second); + } + DispatchStreamInfo(stream_index, stream_info); + } else { + track_id_to_stream_index_map_[stream_info->track_id()] = + kInvalidStreamIndex; + } + ++base_stream_index; + } + all_streams_ready_ = true; +} bool Demuxer::NewSampleEvent(uint32_t track_id, const std::shared_ptr& sample) { - if (!init_event_received_) { + if (!all_streams_ready_) { if (queued_samples_.size() >= kQueuedSamplesLimit) { LOG(ERROR) << "Queued samples limit reached: " << kQueuedSamplesLimit; return false; @@ -152,46 +276,19 @@ bool Demuxer::NewSampleEvent(uint32_t track_id, bool Demuxer::PushSample(uint32_t track_id, const std::shared_ptr& sample) { - for (const std::unique_ptr& stream : streams_) { - if (track_id == stream->info()->track_id()) { - Status status = stream->PushSample(sample); - if (!status.ok()) - LOG(ERROR) << "Demuxer::PushSample failed with " << status; - return status.ok(); - } + auto stream_index_iter = track_id_to_stream_index_map_.find(track_id); + if (stream_index_iter == track_id_to_stream_index_map_.end()) { + LOG(ERROR) << "Track " << track_id << " not found."; + return false; } - LOG(ERROR) << "Track " << track_id << " not found."; - return false; -} - -Status Demuxer::Run() { - Status status; - - LOG(INFO) << "Demuxer::Run() on file '" << file_name_ << "'."; - - // Start the streams. - for (const std::unique_ptr& stream : streams_) { - status = stream->Start(MediaStream::kPush); - if (!status.ok()) - return status; + if (stream_index_iter->second == kInvalidStreamIndex) + return true; + Status status = DispatchMediaSample(stream_index_iter->second, sample); + if (!status.ok()) { + LOG(ERROR) << "Failed to process sample " << stream_index_iter->second + << " " << status; } - - while (!cancelled_ && (status = Parse()).ok()) - continue; - - if (cancelled_ && status.ok()) - return Status(error::CANCELLED, "Demuxer run cancelled"); - - if (status.error_code() == error::END_OF_STREAM) { - // Push EOS sample to muxer to indicate end of stream. - const std::shared_ptr& sample = MediaSample::CreateEOSBuffer(); - for (const std::unique_ptr& stream : streams_) { - status = stream->PushSample(sample); - if (!status.ok()) - return status; - } - } - return status; + return status.ok(); } Status Demuxer::Parse() { @@ -199,11 +296,6 @@ Status Demuxer::Parse() { DCHECK(parser_); DCHECK(buffer_); - // Return early and avoid call Parse(...) again if it has already failed at - // the initialization. - if (!init_parsing_status_.ok()) - return init_parsing_status_; - int64_t bytes_read = media_file_->Read(buffer_.get(), kBufSize); if (bytes_read == 0) { if (!parser_->Flush()) @@ -219,9 +311,5 @@ Status Demuxer::Parse() { "Cannot parse media file " + file_name_); } -void Demuxer::Cancel() { - cancelled_ = true; -} - } // namespace media } // namespace shaka diff --git a/packager/media/base/demuxer.h b/packager/media/base/demuxer.h index 48ea2418b8..d2527e0fea 100644 --- a/packager/media/base/demuxer.h +++ b/packager/media/base/demuxer.h @@ -13,6 +13,7 @@ #include "packager/base/compiler_specific.h" #include "packager/media/base/container_names.h" +#include "packager/media/base/media_handler.h" #include "packager/media/base/status.h" namespace shaka { @@ -28,7 +29,7 @@ class StreamInfo; /// Demuxer is responsible for extracting elementary stream samples from a /// media file, e.g. an ISO BMFF file. -class Demuxer { +class Demuxer : public MediaHandler { public: /// @param file_name specifies the input source. It uses prefix matching to /// create a proper File object. The user can extend File to support @@ -42,36 +43,52 @@ class Demuxer { /// demuxed. void SetKeySource(std::unique_ptr key_source); - /// Initialize the Demuxer. Calling other public methods of this class - /// without this method returning OK, results in an undefined behavior. - /// This method primes the demuxer by parsing portions of the media file to - /// extract stream information. - /// @return OK on success. - Status Initialize(); - /// Drive the remuxing from demuxer side (push). Read the file and push /// the Data to Muxer until Eof. Status Run(); - /// Read from the source and send it to the parser. - Status Parse(); - /// Cancel a demuxing job in progress. Will cause @a Run to exit with an error /// status of type CANCELLED. void Cancel(); - /// @return Streams in the media container being demuxed. The caller cannot - /// add or remove streams from the returned vector, but the caller is - /// allowed to change the internal state of the streams in the vector - /// through MediaStream APIs. - const std::vector>& streams() { - return streams_; - } - /// @return Container name (type). Value is CONTAINER_UNKNOWN if the demuxer /// is not initialized. MediaContainerName container_name() { return container_name_; } + /// Set the handler for the specified stream. + /// @param stream_label can be 'audio', 'video', or stream number (zero + /// based). + /// @param handler is the handler for the specified stream. + Status SetHandler(const std::string& stream_label, + std::shared_ptr handler); + + /// Override the language in the specified stream. If the specified stream is + /// a video stream or invalid, this function is a no-op. + /// @param stream_label can be 'audio', 'video', or stream number (zero + /// based). + /// @param language_override is the new language. + void SetLanguageOverride(const std::string& stream_label, + const std::string& language_override); + + void set_dump_stream_info(bool dump_stream_info) { + dump_stream_info_ = dump_stream_info; + } + + protected: + /// @name MediaHandler implementation overrides. + /// @{ + Status InitializeInternal() override { return Status::OK; } + Status Process(std::unique_ptr stream_data) override { + return Status(error::INTERNAL_ERROR, + "Demuxer should not be the downstream handler."); + } + bool ValidateOutputStreamIndex(int stream_index) const override { + // We don't know if the stream is valid or not when setting up the graph. + // Will validate the stream index later when stream info is available. + return true; + } + /// @} + private: Demuxer(const Demuxer&) = delete; Demuxer& operator=(const Demuxer&) = delete; @@ -84,6 +101,11 @@ class Demuxer { std::shared_ptr sample; }; + // Initialize the parser. This method primes the demuxer by parsing portions + // of the media file to extract stream information. + // @return OK on success. + Status InitializeParser(); + // Parser init event. void ParserInitEvent(const std::vector>& streams); // Parser new sample event handler. Queues the samples if init event has not @@ -95,18 +117,29 @@ class Demuxer { bool PushSample(uint32_t track_id, const std::shared_ptr& sample); + // Read from the source and send it to the parser. + Status Parse(); + std::string file_name_; - File* media_file_; - bool init_event_received_; - Status init_parsing_status_; + File* media_file_ = nullptr; + // A stream is considered ready after receiving the stream info. + bool all_streams_ready_ = false; // Queued samples received in NewSampleEvent() before ParserInitEvent(). std::deque queued_samples_; std::unique_ptr parser_; - std::vector> streams_; - MediaContainerName container_name_; + // TrackId -> StreamIndex map. + std::map track_id_to_stream_index_map_; + // The list of stream indexes in the above map (in the same order as the input + // stream info vector). + std::vector stream_indexes_; + // StreamIndex -> language_override map. + std::map language_overrides_; + MediaContainerName container_name_ = CONTAINER_UNKNOWN; std::unique_ptr buffer_; std::unique_ptr key_source_; - bool cancelled_; + bool cancelled_ = false; + // Whether to dump stream info when it is received. + bool dump_stream_info_ = false; }; } // namespace media diff --git a/packager/media/base/media_base.gyp b/packager/media/base/media_base.gyp index 608d46bb9e..5ecc2fb0b7 100644 --- a/packager/media/base/media_base.gyp +++ b/packager/media/base/media_base.gyp @@ -59,8 +59,6 @@ 'media_parser.h', 'media_sample.cc', 'media_sample.h', - 'media_stream.cc', - 'media_stream.h', 'muxer.cc', 'muxer.h', 'muxer_options.cc', diff --git a/packager/media/base/media_handler.h b/packager/media/base/media_handler.h index 560549aa0f..8c44c6f6b1 100644 --- a/packager/media/base/media_handler.h +++ b/packager/media/base/media_handler.h @@ -172,6 +172,10 @@ class MediaHandler { int num_input_streams() const { return num_input_streams_; } int next_output_stream_index() const { return next_output_stream_index_; } + const std::map, int>>& + output_handlers() { + return output_handlers_; + } private: MediaHandler(const MediaHandler&) = delete; diff --git a/packager/media/base/media_stream.cc b/packager/media/base/media_stream.cc deleted file mode 100644 index ef7f55d3c2..0000000000 --- a/packager/media/base/media_stream.cc +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2014 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -#include "packager/media/base/media_stream.h" - -#include "packager/base/logging.h" -#include "packager/base/strings/stringprintf.h" -#include "packager/media/base/demuxer.h" -#include "packager/media/base/media_sample.h" -#include "packager/media/base/muxer.h" -#include "packager/media/base/stream_info.h" - -namespace shaka { -namespace media { - -MediaStream::MediaStream(std::shared_ptr info, Demuxer* demuxer) - : info_(info), demuxer_(demuxer), muxer_(NULL), state_(kIdle) {} - -MediaStream::~MediaStream() {} - -Status MediaStream::PullSample(std::shared_ptr* sample) { - DCHECK(state_ == kPulling || state_ == kIdle); - - // Trigger a new parse in demuxer if no more samples. - while (samples_.empty()) { - Status status = demuxer_->Parse(); - if (!status.ok()) - return status; - } - - *sample = samples_.front(); - samples_.pop_front(); - return Status::OK; -} - -Status MediaStream::PushSample(const std::shared_ptr& sample) { - switch (state_) { - case kIdle: - case kPulling: - samples_.push_back(sample); - return Status::OK; - case kDisconnected: - return Status::OK; - case kPushing: - return muxer_->AddSample(this, sample); - default: - NOTREACHED() << "Unexpected State " << state_; - return Status::UNKNOWN; - } -} - -void MediaStream::Connect(Muxer* muxer) { - DCHECK(muxer); - DCHECK(!muxer_); - state_ = kConnected; - muxer_ = muxer; -} - -Status MediaStream::Start(MediaStreamOperation operation) { - DCHECK(demuxer_); - DCHECK(operation == kPush || operation == kPull); - - switch (state_) { - case kIdle: - // Disconnect the stream if it is not connected to a muxer. - state_ = kDisconnected; - samples_.clear(); - return Status::OK; - case kConnected: - state_ = (operation == kPush) ? kPushing : kPulling; - if (operation == kPush) { - // Push samples in the queue to muxer if there is any. - while (!samples_.empty()) { - Status status = muxer_->AddSample(this, samples_.front()); - if (!status.ok()) - return status; - samples_.pop_front(); - } - } else { - // We need to disconnect all its peer streams which are not connected - // to a muxer. - for (size_t i = 0; i < demuxer_->streams().size(); ++i) { - Status status = demuxer_->streams()[i]->Start(operation); - if (!status.ok()) - return status; - } - } - return Status::OK; - case kPulling: - DCHECK(operation == kPull); - return Status::OK; - default: - NOTREACHED() << "Unexpected State " << state_; - return Status::UNKNOWN; - } -} - -const std::shared_ptr MediaStream::info() const { - return info_; -} - -std::string MediaStream::ToString() const { - return base::StringPrintf("state: %d\n samples in the queue: %zu\n %s", - state_, samples_.size(), info_->ToString().c_str()); -} - -} // namespace media -} // namespace shaka diff --git a/packager/media/base/media_stream.h b/packager/media/base/media_stream.h deleted file mode 100644 index 0335b68a3b..0000000000 --- a/packager/media/base/media_stream.h +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2014 Google Inc. All rights reserved. -// -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file or at -// https://developers.google.com/open-source/licenses/bsd - -#ifndef MEDIA_BASE_MEDIA_STREAM_H_ -#define MEDIA_BASE_MEDIA_STREAM_H_ - -#include -#include - -#include "packager/media/base/status.h" - -namespace shaka { -namespace media { - -class Demuxer; -class Muxer; -class MediaSample; -class StreamInfo; - -/// MediaStream connects Demuxer to Muxer. It is an abstraction for a media -/// elementary stream. -class MediaStream { - public: - enum MediaStreamOperation { - kPush, - kPull, - }; - /// Create MediaStream from StreamInfo and Demuxer. - /// @param demuxer cannot be NULL. - MediaStream(std::shared_ptr info, Demuxer* demuxer); - ~MediaStream(); - - /// Connect the stream to Muxer. - /// @param muxer cannot be NULL. - void Connect(Muxer* muxer); - - /// Start the stream for pushing or pulling. - Status Start(MediaStreamOperation operation); - - /// Push sample to Muxer (triggered by Demuxer). - Status PushSample(const std::shared_ptr& sample); - - /// Pull sample from Demuxer (triggered by Muxer). - Status PullSample(std::shared_ptr* sample); - - Demuxer* demuxer() { return demuxer_; } - Muxer* muxer() { return muxer_; } - const std::shared_ptr info() const; - - /// @return a human-readable string describing |*this|. - std::string ToString() const; - - private: - MediaStream(const MediaStream&) = delete; - MediaStream& operator=(const MediaStream&) = delete; - - // State transition diagram available @ http://goo.gl/ThJQbl. - enum State { - kIdle, - kConnected, - kDisconnected, - kPushing, - kPulling, - }; - - std::shared_ptr info_; - Demuxer* demuxer_; - Muxer* muxer_; - State state_; - // An internal buffer to store samples temporarily. - std::deque> samples_; -}; - -} // namespace media -} // namespace shaka - -#endif // MEDIA_BASE_MEDIA_STREAM_H_ diff --git a/packager/media/base/muxer.cc b/packager/media/base/muxer.cc index 194fa1d0db..d3cda19f4a 100644 --- a/packager/media/base/muxer.cc +++ b/packager/media/base/muxer.cc @@ -10,14 +10,12 @@ #include "packager/media/base/fourccs.h" #include "packager/media/base/media_sample.h" -#include "packager/media/base/media_stream.h" namespace shaka { namespace media { Muxer::Muxer(const MuxerOptions& options) : options_(options), - initialized_(false), encryption_key_source_(NULL), max_sd_pixels_(0), max_hd_pixels_(0), @@ -47,46 +45,6 @@ void Muxer::SetKeySource(KeySource* encryption_key_source, protection_scheme_ = protection_scheme; } -void Muxer::AddStream(MediaStream* stream) { - DCHECK(stream); - stream->Connect(this); - streams_.push_back(stream); -} - -Status Muxer::Run() { - DCHECK(!streams_.empty()); - - Status status; - // Start the streams. - for (std::vector::iterator it = streams_.begin(); - it != streams_.end(); - ++it) { - status = (*it)->Start(MediaStream::kPull); - if (!status.ok()) - return status; - } - - uint32_t current_stream_id = 0; - while (status.ok()) { - if (cancelled_) - return Status(error::CANCELLED, "muxer run cancelled"); - - std::shared_ptr sample; - status = streams_[current_stream_id]->PullSample(&sample); - if (!status.ok()) - break; - status = AddSample(streams_[current_stream_id], sample); - - // Switch to next stream if the current stream is ready for fragmentation. - if (status.error_code() == error::FRAGMENT_FINALIZED) { - current_stream_id = (current_stream_id + 1) % streams_.size(); - status.Clear(); - } - } - // Finalize the muxer after reaching end of stream. - return status.error_code() == error::END_OF_STREAM ? Finalize() : status; -} - void Muxer::Cancel() { cancelled_ = true; } @@ -100,26 +58,21 @@ void Muxer::SetProgressListener( progress_listener_ = std::move(progress_listener); } -Status Muxer::AddSample(const MediaStream* stream, - std::shared_ptr sample) { - DCHECK(std::find(streams_.begin(), streams_.end(), stream) != streams_.end()); - - if (!initialized_) { - Status status = Initialize(); - if (!status.ok()) - return status; - initialized_ = true; +Status Muxer::Process(std::unique_ptr stream_data) { + Status status; + switch (stream_data->stream_data_type) { + case StreamDataType::kStreamInfo: + streams_.push_back(std::move(stream_data->stream_info)); + return InitializeMuxer(); + case StreamDataType::kMediaSample: + return DoAddSample(stream_data->media_sample); + default: + VLOG(3) << "Stream data type " + << static_cast(stream_data->stream_data_type) << " ignored."; + break; } - if (sample->end_of_stream()) { - // EOS sample should be sent only when the sample was pushed from Demuxer - // to Muxer. In this case, there should be only one stream in Muxer. - DCHECK_EQ(1u, streams_.size()); - return Finalize(); - } else if (sample->is_encrypted()) { - LOG(ERROR) << "Unable to multiplex encrypted media sample"; - return Status(error::INTERNAL_ERROR, "Encrypted media sample."); - } - return DoAddSample(stream, sample); + // No dispatch for muxer. + return Status::OK; } } // namespace media diff --git a/packager/media/base/muxer.h b/packager/media/base/muxer.h index ae518cc575..9811b3c169 100644 --- a/packager/media/base/muxer.h +++ b/packager/media/base/muxer.h @@ -14,6 +14,7 @@ #include "packager/base/time/clock.h" #include "packager/media/base/fourccs.h" +#include "packager/media/base/media_handler.h" #include "packager/media/base/muxer_options.h" #include "packager/media/base/status.h" #include "packager/media/event/muxer_listener.h" @@ -29,7 +30,7 @@ class MediaStream; /// Muxer is responsible for taking elementary stream samples and producing /// media containers. An optional KeySource can be provided to Muxer /// to generate encrypted outputs. -class Muxer { +class Muxer : public MediaHandler { public: explicit Muxer(const MuxerOptions& options); virtual ~Muxer(); @@ -65,12 +66,6 @@ class Muxer { double crypto_period_duration_in_seconds, FourCC protection_scheme); - /// Add video/audio stream. - void AddStream(MediaStream* stream); - - /// Drive the remuxing from muxer side (pull). - Status Run(); - /// Cancel a muxing job in progress. Will cause @a Run to exit with an error /// status of type CANCELLED. void Cancel(); @@ -83,7 +78,9 @@ class Muxer { /// @param progress_listener should not be NULL. void SetProgressListener(std::unique_ptr progress_listener); - const std::vector& streams() const { return streams_; } + const std::vector>& streams() const { + return streams_; + } /// Inject clock, mainly used for testing. /// The injected clock will be used to generate the creation time-stamp and @@ -96,6 +93,13 @@ class Muxer { } protected: + /// @name MediaHandler implementation overrides. + /// @{ + Status InitializeInternal() override { return Status::OK; } + Status Process(std::unique_ptr stream_data) override; + Status FlushStream(int input_stream_index) override { return Finalize(); } + /// @} + const MuxerOptions& options() const { return options_; } KeySource* encryption_key_source() { return encryption_key_source_; @@ -113,25 +117,17 @@ class Muxer { FourCC protection_scheme() const { return protection_scheme_; } private: - friend class MediaStream; // Needed to access AddSample. - - // Add new media sample. - Status AddSample(const MediaStream* stream, - std::shared_ptr sample); - // Initialize the muxer. - virtual Status Initialize() = 0; + virtual Status InitializeMuxer() = 0; // Final clean up. virtual Status Finalize() = 0; // AddSample implementation. - virtual Status DoAddSample(const MediaStream* stream, - std::shared_ptr sample) = 0; + virtual Status DoAddSample(std::shared_ptr sample) = 0; MuxerOptions options_; - bool initialized_; - std::vector streams_; + std::vector> streams_; KeySource* encryption_key_source_; uint32_t max_sd_pixels_; uint32_t max_hd_pixels_; diff --git a/packager/media/formats/mp2t/ts_muxer.cc b/packager/media/formats/mp2t/ts_muxer.cc index 75ea2cfd88..db688c728c 100644 --- a/packager/media/formats/mp2t/ts_muxer.cc +++ b/packager/media/formats/mp2t/ts_muxer.cc @@ -17,15 +17,14 @@ const uint32_t kTsTimescale = 90000; TsMuxer::TsMuxer(const MuxerOptions& muxer_options) : Muxer(muxer_options) {} TsMuxer::~TsMuxer() {} -Status TsMuxer::Initialize() { +Status TsMuxer::InitializeMuxer() { if (streams().size() > 1u) return Status(error::MUXER_FAILURE, "Cannot handle more than one streams."); segmenter_.reset(new TsSegmenter(options(), muxer_listener())); - Status status = - segmenter_->Initialize(*streams()[0]->info(), encryption_key_source(), - max_sd_pixels(), max_hd_pixels(), - max_uhd1_pixels(), clear_lead_in_seconds()); + Status status = segmenter_->Initialize( + *streams()[0], encryption_key_source(), max_sd_pixels(), max_hd_pixels(), + max_uhd1_pixels(), clear_lead_in_seconds()); FireOnMediaStartEvent(); return status; } @@ -35,16 +34,15 @@ Status TsMuxer::Finalize() { return segmenter_->Finalize(); } -Status TsMuxer::DoAddSample(const MediaStream* stream, - std::shared_ptr sample) { +Status TsMuxer::DoAddSample(std::shared_ptr sample) { return segmenter_->AddSample(sample); } void TsMuxer::FireOnMediaStartEvent() { if (!muxer_listener()) return; - muxer_listener()->OnMediaStart(options(), *streams().front()->info(), - kTsTimescale, MuxerListener::kContainerWebM); + muxer_listener()->OnMediaStart(options(), *streams().front(), kTsTimescale, + MuxerListener::kContainerWebM); } void TsMuxer::FireOnMediaEndEvent() { diff --git a/packager/media/formats/mp2t/ts_muxer.h b/packager/media/formats/mp2t/ts_muxer.h index d36b423686..3308023e23 100644 --- a/packager/media/formats/mp2t/ts_muxer.h +++ b/packager/media/formats/mp2t/ts_muxer.h @@ -24,10 +24,9 @@ class TsMuxer : public Muxer { private: // Muxer implementation. - Status Initialize() override; + Status InitializeMuxer() override; Status Finalize() override; - Status DoAddSample(const MediaStream* stream, - std::shared_ptr sample) override; + Status DoAddSample(std::shared_ptr sample) override; void FireOnMediaStartEvent(); void FireOnMediaEndEvent(); diff --git a/packager/media/formats/mp2t/ts_segmenter.h b/packager/media/formats/mp2t/ts_segmenter.h index 2a0496dc95..47ff1e7e9f 100644 --- a/packager/media/formats/mp2t/ts_segmenter.h +++ b/packager/media/formats/mp2t/ts_segmenter.h @@ -8,7 +8,6 @@ #define PACKAGER_MEDIA_FORMATS_MP2T_TS_SEGMENTER_H_ #include -#include "packager/media/base/media_stream.h" #include "packager/media/base/muxer_options.h" #include "packager/media/base/status.h" #include "packager/media/file/file.h" diff --git a/packager/media/formats/mp2t/ts_writer.h b/packager/media/formats/mp2t/ts_writer.h index d40ce95ddb..66f96039fa 100644 --- a/packager/media/formats/mp2t/ts_writer.h +++ b/packager/media/formats/mp2t/ts_writer.h @@ -12,7 +12,6 @@ #include #include -#include "packager/media/base/media_stream.h" #include "packager/media/file/file.h" #include "packager/media/file/file_closer.h" #include "packager/media/formats/mp2t/continuity_counter.h" @@ -21,6 +20,9 @@ namespace shaka { namespace media { + +class StreamInfo; + namespace mp2t { /// This class takes PesPackets, encapsulates them into TS packets, and write diff --git a/packager/media/formats/mp4/mp4_muxer.cc b/packager/media/formats/mp4/mp4_muxer.cc index 7d135bf844..67f08f9e3f 100644 --- a/packager/media/formats/mp4/mp4_muxer.cc +++ b/packager/media/formats/mp4/mp4_muxer.cc @@ -13,7 +13,6 @@ #include "packager/media/base/fourccs.h" #include "packager/media/base/key_source.h" #include "packager/media/base/media_sample.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/video_stream_info.h" #include "packager/media/codecs/es_descriptor.h" #include "packager/media/event/muxer_listener.h" @@ -82,7 +81,7 @@ FourCC CodecToFourCC(Codec codec) { MP4Muxer::MP4Muxer(const MuxerOptions& options) : Muxer(options) {} MP4Muxer::~MP4Muxer() {} -Status MP4Muxer::Initialize() { +Status MP4Muxer::InitializeMuxer() { DCHECK(!streams().empty()); std::unique_ptr ftyp(new FileType); @@ -91,10 +90,9 @@ Status MP4Muxer::Initialize() { 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) { + if (streams().size() == 1 && streams()[0]->stream_type() == kStreamVideo) { const FourCC codec_fourcc = CodecToFourCC( - static_cast(streams()[0]->info().get())->codec()); + static_cast(streams()[0].get())->codec()); if (codec_fourcc != FOURCC_NULL) ftyp->compatible_brands.push_back(codec_fourcc); } @@ -115,22 +113,18 @@ Status MP4Muxer::Initialize() { trex.track_id = trak.header.track_id; trex.default_sample_description_index = 1; - switch (streams()[i]->info()->stream_type()) { + switch (streams()[i]->stream_type()) { case kStreamVideo: - GenerateVideoTrak( - static_cast(streams()[i]->info().get()), - &trak, - i + 1); + GenerateVideoTrak(static_cast(streams()[i].get()), + &trak, i + 1); break; case kStreamAudio: - GenerateAudioTrak( - static_cast(streams()[i]->info().get()), - &trak, - i + 1); + GenerateAudioTrak(static_cast(streams()[i].get()), + &trak, i + 1); break; default: NOTIMPLEMENTED() << "Not implemented for stream type: " - << streams()[i]->info()->stream_type(); + << streams()[i]->stream_type(); } } @@ -167,10 +161,9 @@ Status MP4Muxer::Finalize() { return Status::OK; } -Status MP4Muxer::DoAddSample(const MediaStream* stream, - std::shared_ptr sample) { +Status MP4Muxer::DoAddSample(std::shared_ptr sample) { DCHECK(segmenter_); - return segmenter_->AddSample(stream, sample); + return segmenter_->AddSample(*streams()[0], sample); } void MP4Muxer::InitializeTrak(const StreamInfo* info, Track* trak) { @@ -355,9 +348,7 @@ void MP4Muxer::FireOnMediaStartEvent() { DCHECK(!streams().empty()) << "Media started without a stream."; const uint32_t timescale = segmenter_->GetReferenceTimeScale(); - muxer_listener()->OnMediaStart(options(), - *streams().front()->info(), - timescale, + muxer_listener()->OnMediaStart(options(), *streams().front(), timescale, MuxerListener::kContainerMp4); } diff --git a/packager/media/formats/mp4/mp4_muxer.h b/packager/media/formats/mp4/mp4_muxer.h index 4b2508aed0..5a16a2ce52 100644 --- a/packager/media/formats/mp4/mp4_muxer.h +++ b/packager/media/formats/mp4/mp4_muxer.h @@ -35,10 +35,9 @@ class MP4Muxer : public Muxer { private: // Muxer implementation overrides. - Status Initialize() override; + Status InitializeMuxer() override; Status Finalize() override; - Status DoAddSample(const MediaStream* stream, - std::shared_ptr sample) override; + Status DoAddSample(std::shared_ptr sample) override; // Generate Audio/Video Track box. void InitializeTrak(const StreamInfo* info, Track* trak); diff --git a/packager/media/formats/mp4/multi_segment_segmenter.cc b/packager/media/formats/mp4/multi_segment_segmenter.cc index 2319b5e469..0ddff48301 100644 --- a/packager/media/formats/mp4/multi_segment_segmenter.cc +++ b/packager/media/formats/mp4/multi_segment_segmenter.cc @@ -11,7 +11,6 @@ #include "packager/base/strings/string_number_conversions.h" #include "packager/base/strings/string_util.h" #include "packager/media/base/buffer_writer.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/muxer_options.h" #include "packager/media/base/muxer_util.h" #include "packager/media/event/muxer_listener.h" diff --git a/packager/media/formats/mp4/segmenter.cc b/packager/media/formats/mp4/segmenter.cc index 7553c4690d..2ce2f417a1 100644 --- a/packager/media/formats/mp4/segmenter.cc +++ b/packager/media/formats/mp4/segmenter.cc @@ -13,7 +13,6 @@ #include "packager/media/base/buffer_writer.h" #include "packager/media/base/key_source.h" #include "packager/media/base/media_sample.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/muxer_options.h" #include "packager/media/base/muxer_util.h" #include "packager/media/base/video_stream_info.h" @@ -162,16 +161,17 @@ Segmenter::Segmenter(const MuxerOptions& options, Segmenter::~Segmenter() {} -Status Segmenter::Initialize(const std::vector& streams, - MuxerListener* muxer_listener, - ProgressListener* progress_listener, - KeySource* encryption_key_source, - uint32_t max_sd_pixels, - uint32_t max_hd_pixels, - uint32_t max_uhd1_pixels, - double clear_lead_in_seconds, - double crypto_period_duration_in_seconds, - FourCC protection_scheme) { +Status Segmenter::Initialize( + const std::vector>& streams, + MuxerListener* muxer_listener, + ProgressListener* progress_listener, + KeySource* encryption_key_source, + uint32_t max_sd_pixels, + uint32_t max_hd_pixels, + uint32_t max_uhd1_pixels, + double clear_lead_in_seconds, + double crypto_period_duration_in_seconds, + FourCC protection_scheme) { DCHECK_LT(0u, streams.size()); muxer_listener_ = muxer_listener; progress_listener_ = progress_listener; @@ -184,22 +184,19 @@ Status Segmenter::Initialize(const std::vector& streams, const bool kInitialEncryptionInfo = true; for (uint32_t i = 0; i < streams.size(); ++i) { - stream_map_[streams[i]] = i; moof_->tracks[i].header.track_id = i + 1; - if (streams[i]->info()->stream_type() == kStreamVideo) { + if (streams[i]->stream_type() == kStreamVideo) { // Use the first video stream as the reference stream (which is 1-based). if (sidx_->reference_id == 0) sidx_->reference_id = i + 1; } if (!encryption_key_source) { - fragmenters_[i].reset( - new Fragmenter(streams[i]->info(), &moof_->tracks[i])); + fragmenters_[i].reset(new Fragmenter(streams[i], &moof_->tracks[i])); continue; } - KeySource::TrackType track_type = - GetTrackTypeForEncryption(*streams[i]->info(), max_sd_pixels, - max_hd_pixels, max_uhd1_pixels); + KeySource::TrackType track_type = GetTrackTypeForEncryption( + *streams[i], max_sd_pixels, max_hd_pixels, max_uhd1_pixels); SampleDescription& description = moov_->tracks[i].media.information.sample_table.description; ProtectionPattern pattern = @@ -224,12 +221,11 @@ Status Segmenter::Initialize(const std::vector& streams, } fragmenters_[i].reset(new KeyRotationFragmenter( - moof_.get(), streams[i]->info(), &moof_->tracks[i], - encryption_key_source, track_type, - crypto_period_duration_in_seconds * streams[i]->info()->time_scale(), - clear_lead_in_seconds * streams[i]->info()->time_scale(), - protection_scheme, pattern.crypt_byte_block, pattern.skip_byte_block, - muxer_listener_)); + moof_.get(), streams[i], &moof_->tracks[i], encryption_key_source, + track_type, + crypto_period_duration_in_seconds * streams[i]->time_scale(), + clear_lead_in_seconds * streams[i]->time_scale(), protection_scheme, + pattern.crypt_byte_block, pattern.skip_byte_block, muxer_listener_)); continue; } @@ -262,10 +258,9 @@ Status Segmenter::Initialize(const std::vector& streams, } fragmenters_[i].reset(new EncryptingFragmenter( - streams[i]->info(), &moof_->tracks[i], std::move(encryption_key), - clear_lead_in_seconds * streams[i]->info()->time_scale(), - protection_scheme, pattern.crypt_byte_block, pattern.skip_byte_block, - muxer_listener_)); + streams[i], &moof_->tracks[i], std::move(encryption_key), + clear_lead_in_seconds * streams[i]->time_scale(), protection_scheme, + pattern.crypt_byte_block, pattern.skip_byte_block, muxer_listener_)); } if (options_.mp4_use_decoding_timestamp_in_timeline) { @@ -276,10 +271,10 @@ Status Segmenter::Initialize(const std::vector& streams, // 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(); + sidx_->timescale = streams[GetReferenceStreamId()]->time_scale(); // Use media duration as progress target. - progress_target_ = streams[GetReferenceStreamId()]->info()->duration(); + progress_target_ = streams[GetReferenceStreamId()]->duration(); // Use the reference stream's time scale as movie time scale. moov_->header.timescale = sidx_->timescale; @@ -320,12 +315,10 @@ Status Segmenter::Finalize() { return DoFinalize(); } -Status Segmenter::AddSample(const MediaStream* stream, +Status Segmenter::AddSample(const StreamInfo& stream_info, std::shared_ptr sample) { - // Find the fragmenter for this stream. - DCHECK(stream); - DCHECK(stream_map_.find(stream) != stream_map_.end()); - uint32_t stream_id = stream_map_[stream]; + // TODO(kqyang): Stream id should be passed in. + const uint32_t stream_id = 0; Fragmenter* fragmenter = fragmenters_[stream_id].get(); // Set default sample duration if it has not been set yet. @@ -341,14 +334,14 @@ Status Segmenter::AddSample(const MediaStream* stream, bool finalize_fragment = false; if (fragmenter->fragment_duration() >= - options_.fragment_duration * stream->info()->time_scale()) { + options_.fragment_duration * stream_info.time_scale()) { if (sample->is_key_frame() || !options_.fragment_sap_aligned) { finalize_fragment = true; } } bool finalize_segment = false; if (segment_durations_[stream_id] >= - options_.segment_duration * stream->info()->time_scale()) { + options_.segment_duration * stream_info.time_scale()) { if (sample->is_key_frame() || !options_.segment_sap_aligned) { finalize_segment = true; finalize_fragment = true; diff --git a/packager/media/formats/mp4/segmenter.h b/packager/media/formats/mp4/segmenter.h index aa3274a63c..6279b32ad4 100644 --- a/packager/media/formats/mp4/segmenter.h +++ b/packager/media/formats/mp4/segmenter.h @@ -23,9 +23,9 @@ struct MuxerOptions; class BufferWriter; class KeySource; class MediaSample; -class MediaStream; class MuxerListener; class ProgressListener; +class StreamInfo; namespace mp4 { @@ -69,7 +69,7 @@ class Segmenter { /// @param protection_scheme specifies the protection scheme: 'cenc', 'cens', /// 'cbc1', 'cbcs'. /// @return OK on success, an error status otherwise. - Status Initialize(const std::vector& streams, + Status Initialize(const std::vector>& streams, MuxerListener* muxer_listener, ProgressListener* progress_listener, KeySource* encryption_key_source, @@ -85,11 +85,9 @@ class Segmenter { Status Finalize(); /// Add sample to the indicated stream. - /// @param stream points to the stream to which the sample belongs. It cannot - /// be NULL. /// @param sample points to the sample to be added. /// @return OK on success, an error status otherwise. - Status AddSample(const MediaStream* stream, + Status AddSample(const StreamInfo& stream_Info, std::shared_ptr sample); /// @return true if there is an initialization range, while setting @a offset @@ -145,7 +143,6 @@ class Segmenter { std::unique_ptr sidx_; std::vector> fragmenters_; std::vector segment_durations_; - std::map stream_map_; MuxerListener* muxer_listener_; ProgressListener* progress_listener_; uint64_t progress_target_; diff --git a/packager/media/formats/mp4/single_segment_segmenter.cc b/packager/media/formats/mp4/single_segment_segmenter.cc index 4d8a3b6f9d..048b9a7fcf 100644 --- a/packager/media/formats/mp4/single_segment_segmenter.cc +++ b/packager/media/formats/mp4/single_segment_segmenter.cc @@ -9,7 +9,6 @@ #include #include "packager/media/base/buffer_writer.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/muxer_options.h" #include "packager/media/event/muxer_listener.h" #include "packager/media/event/progress_listener.h" diff --git a/packager/media/formats/webm/multi_segment_segmenter.cc b/packager/media/formats/webm/multi_segment_segmenter.cc index 337746f16e..d085641f8b 100644 --- a/packager/media/formats/webm/multi_segment_segmenter.cc +++ b/packager/media/formats/webm/multi_segment_segmenter.cc @@ -6,7 +6,6 @@ #include "packager/media/formats/webm/multi_segment_segmenter.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/muxer_options.h" #include "packager/media/base/muxer_util.h" #include "packager/media/base/stream_info.h" diff --git a/packager/media/formats/webm/segmenter.cc b/packager/media/formats/webm/segmenter.cc index ea376ff165..354725fcc9 100644 --- a/packager/media/formats/webm/segmenter.cc +++ b/packager/media/formats/webm/segmenter.cc @@ -9,7 +9,6 @@ #include "packager/base/time/time.h" #include "packager/media/base/audio_stream_info.h" #include "packager/media/base/media_sample.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/muxer_options.h" #include "packager/media/base/muxer_util.h" #include "packager/media/base/stream_info.h" diff --git a/packager/media/formats/webm/two_pass_single_segment_segmenter.cc b/packager/media/formats/webm/two_pass_single_segment_segmenter.cc index bced213c50..cc3e1ce885 100644 --- a/packager/media/formats/webm/two_pass_single_segment_segmenter.cc +++ b/packager/media/formats/webm/two_pass_single_segment_segmenter.cc @@ -9,7 +9,6 @@ #include #include "packager/media/base/media_sample.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/muxer_options.h" #include "packager/media/base/stream_info.h" #include "packager/media/file/file_util.h" diff --git a/packager/media/formats/webm/webm_muxer.cc b/packager/media/formats/webm/webm_muxer.cc index 0191121c24..d4ac7bc043 100644 --- a/packager/media/formats/webm/webm_muxer.cc +++ b/packager/media/formats/webm/webm_muxer.cc @@ -8,7 +8,6 @@ #include "packager/media/base/fourccs.h" #include "packager/media/base/media_sample.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/stream_info.h" #include "packager/media/formats/webm/mkv_writer.h" #include "packager/media/formats/webm/multi_segment_segmenter.h" @@ -22,7 +21,7 @@ namespace webm { WebMMuxer::WebMMuxer(const MuxerOptions& options) : Muxer(options) {} WebMMuxer::~WebMMuxer() {} -Status WebMMuxer::Initialize() { +Status WebMMuxer::InitializeMuxer() { CHECK_EQ(streams().size(), 1U); if (crypto_period_duration_in_seconds() > 0) { @@ -50,7 +49,7 @@ Status WebMMuxer::Initialize() { } Status initialized = segmenter_->Initialize( - std::move(writer), streams()[0]->info().get(), progress_listener(), + std::move(writer), streams()[0].get(), progress_listener(), muxer_listener(), encryption_key_source(), max_sd_pixels(), max_hd_pixels(), max_uhd1_pixels(), clear_lead_in_seconds()); @@ -73,10 +72,8 @@ Status WebMMuxer::Finalize() { return Status::OK; } -Status WebMMuxer::DoAddSample(const MediaStream* stream, - std::shared_ptr sample) { +Status WebMMuxer::DoAddSample(std::shared_ptr sample) { DCHECK(segmenter_); - DCHECK(stream == streams()[0]); return segmenter_->AddSample(sample); } @@ -86,9 +83,9 @@ void WebMMuxer::FireOnMediaStartEvent() { DCHECK(!streams().empty()) << "Media started without a stream."; - const uint32_t timescale = streams().front()->info()->time_scale(); - muxer_listener()->OnMediaStart(options(), *streams().front()->info(), - timescale, MuxerListener::kContainerWebM); + const uint32_t timescale = streams().front()->time_scale(); + muxer_listener()->OnMediaStart(options(), *streams().front(), timescale, + MuxerListener::kContainerWebM); } void WebMMuxer::FireOnMediaEndEvent() { diff --git a/packager/media/formats/webm/webm_muxer.h b/packager/media/formats/webm/webm_muxer.h index 5d869949b8..ff7e3b13bb 100644 --- a/packager/media/formats/webm/webm_muxer.h +++ b/packager/media/formats/webm/webm_muxer.h @@ -24,10 +24,9 @@ class WebMMuxer : public Muxer { private: // Muxer implementation overrides. - Status Initialize() override; + Status InitializeMuxer() override; Status Finalize() override; - Status DoAddSample(const MediaStream* stream, - std::shared_ptr sample) override; + Status DoAddSample(std::shared_ptr sample) override; void FireOnMediaStartEvent(); void FireOnMediaEndEvent(); diff --git a/packager/media/test/packager_test.cc b/packager/media/test/packager_test.cc index d89e33eff4..a8ae1be2d9 100644 --- a/packager/media/test/packager_test.cc +++ b/packager/media/test/packager_test.cc @@ -13,7 +13,6 @@ #include "packager/media/base/demuxer.h" #include "packager/media/base/fixed_key_source.h" #include "packager/media/base/fourccs.h" -#include "packager/media/base/media_stream.h" #include "packager/media/base/muxer.h" #include "packager/media/base/muxer_util.h" #include "packager/media/base/stream_info.h" @@ -50,7 +49,6 @@ const bool kSingleSegment = true; const bool kMultipleSegments = false; const bool kEnableEncryption = true; const bool kDisableEncryption = false; -const char kNoLanguageOverride[] = ""; // Encryption constants. const char kKeyIdHex[] = "e5007e6e9dcd5ac095202ed3758382cd"; @@ -63,24 +61,6 @@ const uint32_t kMaxSDPixels = 640 * 480; const uint32_t kMaxHDPixels = 1920 * 1080; const uint32_t kMaxUHD1Pixels = 4096 * 2160; -MediaStream* FindFirstStreamOfType( - const std::vector>& streams, - StreamType stream_type) { - for (const std::unique_ptr& stream : streams) { - if (stream->info()->stream_type() == stream_type) - return stream.get(); - } - return nullptr; -} -MediaStream* FindFirstVideoStream( - const std::vector>& streams) { - return FindFirstStreamOfType(streams, kStreamVideo); -} -MediaStream* FindFirstAudioStream( - const std::vector>& streams) { - return FindFirstStreamOfType(streams, kStreamAudio); -} - } // namespace class FakeClock : public base::Clock { @@ -114,8 +94,7 @@ class PackagerTestBasic : public ::testing::TestWithParam { const std::string& video_output, const std::string& audio_output, bool single_segment, - bool enable_encryption, - const std::string& override_language); + bool enable_encryption); void Decrypt(const std::string& input, const std::string& video_output, @@ -157,59 +136,46 @@ void PackagerTestBasic::Remux(const std::string& input, const std::string& video_output, const std::string& audio_output, bool single_segment, - bool enable_encryption, - const std::string& language_override) { + bool enable_encryption) { CHECK(!video_output.empty() || !audio_output.empty()); Demuxer demuxer(GetFullPath(input)); - ASSERT_OK(demuxer.Initialize()); std::unique_ptr encryption_key_source( FixedKeySource::CreateFromHexStrings(kKeyIdHex, kKeyHex, "", "")); DCHECK(encryption_key_source); - std::unique_ptr muxer_video; + std::shared_ptr muxer_video; if (!video_output.empty()) { muxer_video.reset( new mp4::MP4Muxer(SetupOptions(video_output, single_segment))); muxer_video->set_clock(&fake_clock_); - MediaStream* stream = FindFirstVideoStream(demuxer.streams()); - if (!language_override.empty()) { - stream->info()->set_language(language_override); - ASSERT_EQ(language_override, stream->info()->language()); - } - muxer_video->AddStream(stream); - if (enable_encryption) { muxer_video->SetKeySource(encryption_key_source.get(), kMaxSDPixels, kMaxHDPixels, kMaxUHD1Pixels, kClearLeadInSeconds, kCryptoDurationInSeconds, FOURCC_cenc); } + ASSERT_OK(demuxer.SetHandler("video", muxer_video)); } - std::unique_ptr muxer_audio; + std::shared_ptr muxer_audio; if (!audio_output.empty()) { muxer_audio.reset( new mp4::MP4Muxer(SetupOptions(audio_output, single_segment))); muxer_audio->set_clock(&fake_clock_); - MediaStream* stream = FindFirstAudioStream(demuxer.streams()); - if (!language_override.empty()) { - stream->info()->set_language(language_override); - ASSERT_EQ(language_override, stream->info()->language()); - } - muxer_audio->AddStream(stream); - if (enable_encryption) { muxer_audio->SetKeySource(encryption_key_source.get(), kMaxSDPixels, kMaxHDPixels, kMaxUHD1Pixels, kClearLeadInSeconds, kCryptoDurationInSeconds, FOURCC_cenc); } + ASSERT_OK(demuxer.SetHandler("audio", muxer_audio)); } + ASSERT_OK(demuxer.Initialize()); // Start remuxing process. ASSERT_OK(demuxer.Run()); } @@ -224,25 +190,20 @@ void PackagerTestBasic::Decrypt(const std::string& input, FixedKeySource::CreateFromHexStrings(kKeyIdHex, kKeyHex, "", "")); ASSERT_TRUE(decryption_key_source); demuxer.SetKeySource(std::move(decryption_key_source)); - ASSERT_OK(demuxer.Initialize()); - std::unique_ptr muxer; - MediaStream* stream(NULL); + std::shared_ptr muxer; if (!video_output.empty()) { muxer.reset( new mp4::MP4Muxer(SetupOptions(video_output, true))); - stream = FindFirstVideoStream(demuxer.streams()); } if (!audio_output.empty()) { muxer.reset( new mp4::MP4Muxer(SetupOptions(audio_output, true))); - stream = FindFirstAudioStream(demuxer.streams()); } ASSERT_TRUE(muxer); - ASSERT_TRUE(stream != NULL); - ASSERT_TRUE(stream->info()->is_encrypted()); muxer->set_clock(&fake_clock_); - muxer->AddStream(stream); + ASSERT_OK(demuxer.SetHandler("0", muxer)); + ASSERT_OK(demuxer.Initialize()); ASSERT_OK(demuxer.Run()); } @@ -252,8 +213,7 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedVideo) { kOutputVideo, kOutputNone, kSingleSegment, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); } TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedAudio) { @@ -261,8 +221,7 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedAudio) { kOutputNone, kOutputAudio, kSingleSegment, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); } TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedVideo) { @@ -270,8 +229,7 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedVideo) { kOutputVideo, kOutputNone, kSingleSegment, - kEnableEncryption, - kNoLanguageOverride)); + kEnableEncryption)); ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputVideo, kOutputVideo2, @@ -283,76 +241,13 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedAudio) { kOutputNone, kOutputAudio, kSingleSegment, - kEnableEncryption, - kNoLanguageOverride)); + kEnableEncryption)); ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputAudio, kOutputNone, kOutputAudio2)); } -TEST_P(PackagerTestBasic, MP4MuxerLanguageWithoutSubtag) { - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputNone, - kOutputAudio, - kSingleSegment, - kDisableEncryption, - "por")); - - Demuxer demuxer(GetFullPath(kOutputAudio)); - ASSERT_OK(demuxer.Initialize()); - - MediaStream* stream = FindFirstAudioStream(demuxer.streams()); - ASSERT_EQ("por", stream->info()->language()); -} - -TEST_P(PackagerTestBasic, MP4MuxerLanguageWithSubtag) { - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputNone, - kOutputAudio, - kSingleSegment, - kDisableEncryption, - "por-BR")); - - Demuxer demuxer(GetFullPath(kOutputAudio)); - ASSERT_OK(demuxer.Initialize()); - - MediaStream* stream = FindFirstAudioStream(demuxer.streams()); - ASSERT_EQ("por", stream->info()->language()); -} - -TEST_P(PackagerTestBasic, GetTrackTypeForEncryption) { - Demuxer demuxer(GetFullPath(GetParam())); - ASSERT_OK(demuxer.Initialize()); - - MediaStream* video_stream = FindFirstVideoStream(demuxer.streams()); - MediaStream* audio_stream = FindFirstAudioStream(demuxer.streams()); - - // Typical resolution constraints should set the resolution in the SD range - KeySource::TrackType track_type = GetTrackTypeForEncryption( - *video_stream->info(), kMaxSDPixels, kMaxHDPixels, kMaxUHD1Pixels); - ASSERT_EQ(FixedKeySource::GetTrackTypeFromString("SD"), track_type); - - // Setting the max SD value to 1 should set the resolution in the HD range - track_type = GetTrackTypeForEncryption( - *video_stream->info(), 1, kMaxHDPixels, kMaxUHD1Pixels); - ASSERT_EQ(FixedKeySource::GetTrackTypeFromString("HD"), track_type); - - // Setting the max HD value to 2 should set the resolution in the UHD1 range - track_type = GetTrackTypeForEncryption( - *video_stream->info(), 1, 2, kMaxUHD1Pixels); - ASSERT_EQ(FixedKeySource::GetTrackTypeFromString("UHD1"), track_type); - - // Setting the max UHD1 value to 3 should set the resolution in the UHD2 range - track_type = GetTrackTypeForEncryption( - *video_stream->info(), 1, 2, 3); - ASSERT_EQ(FixedKeySource::GetTrackTypeFromString("UHD2"), track_type); - - // Audio stream should always set the track_type to AUDIO - track_type = GetTrackTypeForEncryption( - *audio_stream->info(), kMaxSDPixels, kMaxHDPixels, kMaxUHD1Pixels); - ASSERT_EQ(FixedKeySource::GetTrackTypeFromString("AUDIO"), track_type); -} class PackagerTest : public PackagerTestBasic { public: @@ -363,15 +258,13 @@ class PackagerTest : public PackagerTestBasic { kOutputVideo, kOutputNone, kSingleSegment, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputNone, kOutputAudio, kSingleSegment, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); } }; @@ -382,8 +275,7 @@ TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedVideoAgain) { kOutputVideo2, kOutputNone, kSingleSegment, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); } @@ -394,8 +286,7 @@ TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedAudioAgain) { kOutputNone, kOutputAudio2, kSingleSegment, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); EXPECT_TRUE(ContentsEqual(kOutputAudio, kOutputAudio2)); } @@ -404,8 +295,7 @@ TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedSeparateAudioVideo) { kOutputVideo2, kOutputAudio2, kSingleSegment, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); // Compare the output with single muxer output. They should match. EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); @@ -417,8 +307,7 @@ TEST_P(PackagerTest, MP4MuxerMultiSegmentsUnencryptedVideo) { kOutputVideo2, kOutputNone, kMultipleSegments, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); // Find and concatenates the segments. const std::string kOutputVideoSegmentsCombined = @@ -452,8 +341,7 @@ TEST_P(PackagerTest, MP4MuxerMultiSegmentsUnencryptedVideo) { kOutputVideo2, kOutputNone, kSingleSegment, - kDisableEncryption, - kNoLanguageOverride)); + kDisableEncryption)); EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); }