diff --git a/packager/app/fixed_key_encryption_flags.h b/packager/app/fixed_key_encryption_flags.h index 02afb3844c..bf26bff6a4 100644 --- a/packager/app/fixed_key_encryption_flags.h +++ b/packager/app/fixed_key_encryption_flags.h @@ -11,6 +11,7 @@ #include +// TODO(kqyang): s/fixed/raw/. DECLARE_bool(enable_fixed_key_encryption); DECLARE_bool(enable_fixed_key_decryption); DECLARE_string(key_id); diff --git a/packager/app/hls_flags.cc b/packager/app/hls_flags.cc index b344f583b1..0afad75f01 100644 --- a/packager/app/hls_flags.cc +++ b/packager/app/hls_flags.cc @@ -13,5 +13,5 @@ DEFINE_string(hls_master_playlist_output, DEFINE_string(hls_base_url, "", - "The base URL for the Media Playlists and TS files listed in the " - "playlists. This is the prefix for the files."); + "The base URL for the Media Playlists and media files listed in " + "the playlists. This is the prefix for the files."); diff --git a/packager/app/libcrypto_threading.cc b/packager/app/libcrypto_threading.cc index 5fc8e5cdc7..eb80585d0f 100644 --- a/packager/app/libcrypto_threading.cc +++ b/packager/app/libcrypto_threading.cc @@ -9,7 +9,6 @@ #include #include -#include #include "packager/base/logging.h" #include "packager/base/synchronization/lock.h" diff --git a/packager/app/mpd_flags.cc b/packager/app/mpd_flags.cc index 9646ca3726..4c4bcb8b10 100644 --- a/packager/app/mpd_flags.cc +++ b/packager/app/mpd_flags.cc @@ -8,6 +8,7 @@ #include "packager/app/mpd_flags.h" +// TODO(kqyang): Rename to generate_static_live_mpd. DEFINE_bool(generate_static_mpd, false, "Set to true to generate static mpd. If segment_template is " @@ -37,7 +38,7 @@ DEFINE_double(minimum_update_period, 5.0, "Indicates to the player how often to refresh the media " "presentation description in seconds. This value is used for " - "live profile only."); + "dynamic MPD only."); DEFINE_double(time_shift_buffer_depth, 1800.0, "Guaranteed duration of the time shifting buffer for dynamic " @@ -45,7 +46,7 @@ DEFINE_double(time_shift_buffer_depth, DEFINE_double(suggested_presentation_delay, 0.0, "Specifies a delay, in seconds, to be added to the media " - "presentation time. This value is used for live profile only."); + "presentation time. This value is used for dynamic MPD only."); DEFINE_string(default_language, "", "Any tracks tagged with this language will have " @@ -53,5 +54,5 @@ DEFINE_string(default_language, "player to choose the correct default language for the content."); DEFINE_bool(generate_dash_if_iop_compliant_mpd, true, - "Try to generate DASH-IF IOPv3 compliant MPD. This is best effort " + "Try to generate DASH-IF IOP compliant MPD. This is best effort " "and does not guarantee compliance."); diff --git a/packager/app/muxer_flags.cc b/packager/app/muxer_flags.cc index 4c31fbdb00..a18a4e7754 100644 --- a/packager/app/muxer_flags.cc +++ b/packager/app/muxer_flags.cc @@ -44,3 +44,9 @@ DEFINE_string(temp_dir, DEFINE_bool(mp4_include_pssh_in_stream, true, "MP4 only: include pssh in the encrypted stream."); +DEFINE_bool(mp4_use_decoding_timestamp_in_timeline, + false, + "If set, decoding timestamp instead of presentation timestamp will " + "be used when generating media timeline, e.g. timestamps in sidx " + "and mpd. This is to workaround a Chromium bug that decoding " + "timestamp is used in buffered range, https://crbug.com/398130."); diff --git a/packager/app/muxer_flags.h b/packager/app/muxer_flags.h index 608529a351..a88a777a3d 100644 --- a/packager/app/muxer_flags.h +++ b/packager/app/muxer_flags.h @@ -19,5 +19,6 @@ DECLARE_bool(fragment_sap_aligned); DECLARE_int32(num_subsegments_per_sidx); DECLARE_string(temp_dir); DECLARE_bool(mp4_include_pssh_in_stream); +DECLARE_bool(mp4_use_decoding_timestamp_in_timeline); #endif // APP_MUXER_FLAGS_H_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 2c19ef8f53..307b4dabae 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -7,9 +7,9 @@ #include #include +#include "packager/app/crypto_flags.h" #include "packager/app/fixed_key_encryption_flags.h" #include "packager/app/hls_flags.h" -#include "packager/app/libcrypto_threading.h" #include "packager/app/mpd_flags.h" #include "packager/app/muxer_flags.h" #include "packager/app/packager_util.h" @@ -19,35 +19,13 @@ #include "packager/app/widevine_encryption_flags.h" #include "packager/base/at_exit.h" #include "packager/base/command_line.h" -#include "packager/base/files/file_path.h" #include "packager/base/logging.h" -#include "packager/base/path_service.h" +#include "packager/base/optional.h" +#include "packager/base/strings/string_number_conversions.h" #include "packager/base/strings/string_split.h" #include "packager/base/strings/stringprintf.h" -#include "packager/base/threading/simple_thread.h" -#include "packager/base/time/clock.h" -#include "packager/hls/base/hls_notifier.h" -#include "packager/hls/base/simple_hls_notifier.h" -#include "packager/media/base/container_names.h" -#include "packager/media/base/fourccs.h" -#include "packager/media/base/key_source.h" -#include "packager/media/base/muxer_options.h" -#include "packager/media/base/muxer_util.h" -#include "packager/media/chunking/chunking_handler.h" -#include "packager/media/crypto/encryption_handler.h" -#include "packager/media/demuxer/demuxer.h" -#include "packager/media/event/hls_notify_muxer_listener.h" -#include "packager/media/event/mpd_notify_muxer_listener.h" -#include "packager/media/event/vod_media_info_dump_muxer_listener.h" #include "packager/media/file/file.h" -#include "packager/media/formats/mp2t/ts_muxer.h" -#include "packager/media/formats/mp4/mp4_muxer.h" -#include "packager/media/formats/webm/webm_muxer.h" -#include "packager/media/trick_play/trick_play_handler.h" -#include "packager/mpd/base/dash_iop_mpd_notifier.h" -#include "packager/mpd/base/media_info.pb.h" -#include "packager/mpd/base/mpd_builder.h" -#include "packager/mpd/base/simple_mpd_notifier.h" +#include "packager/packager.h" #include "packager/version/version.h" #if defined(OS_WIN) @@ -56,11 +34,6 @@ #include #endif // defined(OS_WIN) -DEFINE_bool(use_fake_clock_for_muxer, - false, - "Set to true to use a fake clock for muxer. With this flag set, " - "creation time and modification time in outputs are set to 0. " - "Should only be used for testing."); DEFINE_bool(override_version, false, "Override packager version in the generated outputs with " @@ -72,7 +45,6 @@ DEFINE_string(test_version, "false. Should be used for testing only."); namespace shaka { -namespace media { namespace { const char kUsage[] = @@ -96,10 +68,12 @@ const char kUsage[] = " segment names. If not specified, its value may be estimated.\n" " - language (lang): Optional value which contains a user-specified\n" " language tag. If specified, this value overrides any language\n" - " metadata in the input track.\n" + " metadata in the input stream.\n" " - output_format (format): Optional value which specifies the format\n" " of the output files (MP4 or WebM). If not specified, it will be\n" " derived from the file extension of the output file.\n" + " - skip_encryption=0|1: Optional. Defaults to 0 if not specified. If\n" + " it is set to 1, no encryption of the stream will be made.\n" " - trick_play_factor (tpf): Optional value which specifies the trick\n" " play, a.k.a. trick mode, stream sampling rate among key frames.\n" " If specified, the output is a trick play stream.\n" @@ -107,14 +81,10 @@ const char kUsage[] = " name of the output stream. This is not (necessarily) the same as\n" " output. This is used as the NAME attribute for EXT-X-MEDIA\n" " - hls_group_id: Required for audio when outputting HLS.\n" - " The group ID for the output stream. For HLS this is used as the\n" - " GROUP-ID attribute for EXT-X-MEDIA.\n" + " The group ID for the output stream. This is used as the GROUP-ID\n" + " attribute for EXT-X-MEDIA.\n" " - playlist_name: Required for HLS output.\n" - " Name of the playlist for the stream. Usually ends with '.m3u8'.\n" - " - skip_encryption=0|1: Optional. Defaults to 0 if not specified. If\n" - " it is set to 1, no encryption of the stream will be made.\n"; - -const char kMediaInfoSuffix[] = ".media_info"; + " Name of the playlist for the stream. Usually ends with '.m3u8'.\n"; enum ExitStatus { kSuccess = 0, @@ -123,444 +93,200 @@ enum ExitStatus { kInternalError, }; -// TODO(rkuroiwa): Write TTML and WebVTT parser (demuxing) for a better check -// and for supporting live/segmenting (muxing). With a demuxer and a muxer, -// CreateRemuxJobs() shouldn't treat text as a special case. -std::string DetermineTextFileFormat(const std::string& file) { - std::string content; - if (!File::ReadFileToString(file.c_str(), &content)) { - LOG(ERROR) << "Failed to open file " << file - << " to determine file format."; - return ""; +base::Optional GetPackagingParams() { + PackagingParams packaging_params; + + ChunkingParams& chunking_params = packaging_params.chunking_params; + chunking_params.segment_duration_in_seconds = FLAGS_segment_duration; + chunking_params.subsegment_duration_in_seconds = FLAGS_fragment_duration; + chunking_params.segment_sap_aligned = FLAGS_segment_sap_aligned; + chunking_params.subsegment_sap_aligned = FLAGS_fragment_sap_aligned; + + int num_key_providers = 0; + EncryptionParams& encryption_params = packaging_params.encryption_params; + if (FLAGS_enable_widevine_encryption) { + encryption_params.key_provider = KeyProvider::kWidevine; + ++num_key_providers; } - MediaContainerName container_name = DetermineContainer( - reinterpret_cast(content.data()), content.size()); - if (container_name == CONTAINER_WEBVTT) { - return "vtt"; - } else if (container_name == CONTAINER_TTML) { - return "ttml"; + if (FLAGS_enable_playready_encryption) { + encryption_params.key_provider = KeyProvider::kPlayready; + ++num_key_providers; + } + if (FLAGS_enable_fixed_key_encryption) { + encryption_params.key_provider = KeyProvider::kRawKey; + ++num_key_providers; + } + if (num_key_providers > 1) { + LOG(ERROR) << "Only one of --enable_widevine_encryption, " + "--enable_playready_encryption, " + "--enable_fixed_key_encryption can be enabled."; + return base::nullopt; } - return ""; -} - -} // namespace - -// A fake clock that always return time 0 (epoch). Should only be used for -// testing. -class FakeClock : public base::Clock { - public: - base::Time Now() override { return base::Time(); } -}; - -// Demux, Mux(es) and worker thread used to remux a source file/stream. -class RemuxJob : public base::SimpleThread { - public: - RemuxJob(std::unique_ptr demuxer) - : SimpleThread("RemuxJob"), demuxer_(std::move(demuxer)) {} - - ~RemuxJob() override {} - - Demuxer* demuxer() { return demuxer_.get(); } - Status status() { return status_; } - - private: - void Run() override { - DCHECK(demuxer_); - status_ = demuxer_->Run(); + if (encryption_params.key_provider != KeyProvider::kNone) { + encryption_params.clear_lead_in_seconds = FLAGS_clear_lead; + encryption_params.protection_scheme = FLAGS_protection_scheme; + encryption_params.crypto_period_duration_in_seconds = + FLAGS_crypto_period_duration; + encryption_params.vp9_subsample_encryption = FLAGS_vp9_subsample_encryption; + encryption_params.stream_label_func = std::bind( + &EncryptionParams::DefaultStreamLabelFunction, FLAGS_max_sd_pixels, + FLAGS_max_hd_pixels, FLAGS_max_uhd1_pixels, std::placeholders::_1); } + switch (encryption_params.key_provider) { + case KeyProvider::kWidevine: { + WidevineEncryptionParams& widevine = encryption_params.widevine; + widevine.key_server_url = FLAGS_key_server_url; + widevine.include_common_pssh = FLAGS_include_common_pssh; - std::unique_ptr demuxer_; - Status status_; - - DISALLOW_COPY_AND_ASSIGN(RemuxJob); -}; - -bool StreamInfoToTextMediaInfo(const StreamDescriptor& stream_descriptor, - const MuxerOptions& stream_muxer_options, - MediaInfo* text_media_info) { - const std::string& language = stream_descriptor.language; - std::string format = DetermineTextFileFormat(stream_descriptor.input); - if (format.empty()) { - LOG(ERROR) << "Failed to determine the text file format for " - << stream_descriptor.input; - return false; - } - - if (!File::Copy(stream_descriptor.input.c_str(), - stream_muxer_options.output_file_name.c_str())) { - LOG(ERROR) << "Failed to copy the input file (" << stream_descriptor.input - << ") to output file (" << stream_muxer_options.output_file_name - << ")."; - return false; - } - - text_media_info->set_media_file_name(stream_muxer_options.output_file_name); - text_media_info->set_container_type(MediaInfo::CONTAINER_TEXT); - - if (stream_muxer_options.bandwidth != 0) { - text_media_info->set_bandwidth(stream_muxer_options.bandwidth); - } else { - // Text files are usually small and since the input is one file; there's no - // way for the player to do ranged requests. So set this value to something - // reasonable. - text_media_info->set_bandwidth(256); - } - - MediaInfo::TextInfo* text_info = text_media_info->mutable_text_info(); - text_info->set_format(format); - if (!language.empty()) - text_info->set_language(language); - - return true; -} - -std::shared_ptr CreateOutputMuxer(const MuxerOptions& options, - MediaContainerName container) { - if (container == CONTAINER_WEBM) { - return std::shared_ptr(new webm::WebMMuxer(options)); - } else if (container == CONTAINER_MPEG2TS) { - return std::shared_ptr(new mp2t::TsMuxer(options)); - } else { - DCHECK_EQ(container, CONTAINER_MOV); - return std::shared_ptr(new mp4::MP4Muxer(options)); - } -} - -bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors, - const ChunkingOptions& chunking_options, - const EncryptionOptions& encryption_options, - const MuxerOptions& muxer_options, - FakeClock* fake_clock, - KeySource* encryption_key_source, - MpdNotifier* mpd_notifier, - hls::HlsNotifier* hls_notifier, - std::vector>* remux_jobs) { - // No notifiers OR (mpd_notifier XOR hls_notifier); which is NAND. - DCHECK(!(mpd_notifier && hls_notifier)); - DCHECK(remux_jobs); - - std::shared_ptr trick_play_handler; - - std::string previous_input; - std::string previous_stream_selector; - int stream_number = 0; - for (StreamDescriptorList::const_iterator - stream_iter = stream_descriptors.begin(); - stream_iter != stream_descriptors.end(); - ++stream_iter, ++stream_number) { - // Process stream descriptor. - MuxerOptions stream_muxer_options(muxer_options); - stream_muxer_options.output_file_name = stream_iter->output; - if (!stream_iter->segment_template.empty()) { - if (!ValidateSegmentTemplate(stream_iter->segment_template)) { - LOG(ERROR) << "ERROR: segment template with '" - << stream_iter->segment_template << "' is invalid."; - return false; + if (!base::HexStringToBytes(FLAGS_content_id, &widevine.content_id)) { + LOG(ERROR) << "Invalid content_id hex string specified."; + return base::nullopt; } - stream_muxer_options.segment_template = stream_iter->segment_template; - } - stream_muxer_options.bandwidth = stream_iter->bandwidth; - - if (stream_iter->stream_selector == "text" && - stream_iter->output_format != CONTAINER_MOV) { - MediaInfo text_media_info; - if (!StreamInfoToTextMediaInfo(*stream_iter, stream_muxer_options, - &text_media_info)) { - return false; + widevine.policy = FLAGS_policy; + widevine.signer.signer_name = FLAGS_signer; + if (!FLAGS_aes_signing_key.empty() && !FLAGS_rsa_signing_key_path.empty()) { + LOG(ERROR) << "Only one of --aes_signing_key and " + "--rsa_signing_key_path is needed."; + return base::nullopt; } - - if (mpd_notifier) { - uint32_t unused; - if (!mpd_notifier->NotifyNewContainer(text_media_info, &unused)) { - LOG(ERROR) << "Failed to process text file " << stream_iter->input; - } else { - mpd_notifier->Flush(); + WidevineSigner& signer = widevine.signer; + if (!FLAGS_aes_signing_key.empty()) { + // TODO(kqyang): Take care of hex conversion and file read here. + signer.signing_key_type = WidevineSigner::SigningKeyType::kAes; + signer.aes.key = FLAGS_aes_signing_key; + signer.aes.iv = FLAGS_aes_signing_iv; + } + if (!FLAGS_rsa_signing_key_path.empty()) { + signer.signing_key_type = WidevineSigner::SigningKeyType::kRsa; + if (!media::File::ReadFileToString(FLAGS_rsa_signing_key_path.c_str(), + &signer.rsa.key)) { + LOG(ERROR) << "Failed to read from '" << FLAGS_rsa_signing_key_path + << "'."; + return base::nullopt; } - } else if (FLAGS_output_media_info) { - VodMediaInfoDumpMuxerListener::WriteMediaInfoToFile( - text_media_info, - stream_muxer_options.output_file_name + kMediaInfoSuffix); - } else { - NOTIMPLEMENTED() - << "--mpd_output or --output_media_info flags are " - "required for text output. Skipping manifest related output for " - << stream_iter->input; } - continue; + break; } + case KeyProvider::kPlayready: { + PlayreadyEncryptionParams& playready = encryption_params.playready; + playready.key_server_url = FLAGS_playready_server_url; + playready.program_identifier = FLAGS_program_identifier; + playready.ca_file = FLAGS_ca_file; + playready.client_cert_file = FLAGS_client_cert_file; + playready.client_cert_private_key_file = + FLAGS_client_cert_private_key_file; + playready.client_cert_private_key_password = + FLAGS_client_cert_private_key_password; + playready.key_id = FLAGS_playready_key_id; + playready.key = FLAGS_playready_key; + break; + } + case KeyProvider::kRawKey: { + RawKeyEncryptionParams& raw_key = encryption_params.raw_key; + raw_key.iv = FLAGS_iv; + raw_key.pssh = FLAGS_pssh; + // An empty TrackType specifies the default KeyPair. + RawKeyEncryptionParams::KeyPair& key_pair = raw_key.key_map[""]; + // TODO(kqyang): Take care of hex conversion here. + key_pair.key_id = FLAGS_key_id; + key_pair.key = FLAGS_key; + break; + } + case KeyProvider::kNone: + break; + } - 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 decryption_key_source( - CreateDecryptionKeySource()); - if (!decryption_key_source) - return false; - demuxer->SetKeySource(std::move(decryption_key_source)); + num_key_providers = 0; + DecryptionParams& decryption_params = packaging_params.decryption_params; + if (FLAGS_enable_widevine_decryption) { + decryption_params.key_provider = KeyProvider::kWidevine; + ++num_key_providers; + } + if (FLAGS_enable_fixed_key_decryption) { + decryption_params.key_provider = KeyProvider::kRawKey; + ++num_key_providers; + } + if (num_key_providers > 1) { + LOG(ERROR) << "Only one of --enable_widevine_decryption, " + "--enable_fixed_key_decryption can be enabled."; + return base::nullopt; + } + switch (decryption_params.key_provider) { + case KeyProvider::kWidevine: { + WidevineDecryptionParams& widevine = decryption_params.widevine; + widevine.key_server_url = FLAGS_key_server_url; + + widevine.signer.signer_name = FLAGS_signer; + if (!FLAGS_aes_signing_key.empty() && !FLAGS_rsa_signing_key_path.empty()) { + LOG(ERROR) << "Only one of --aes_signing_key and " + "--rsa_signing_key_path is needed."; + return base::nullopt; } - remux_jobs->emplace_back(new RemuxJob(std::move(demuxer))); - trick_play_handler.reset(); - previous_input = stream_iter->input; - // Skip setting up muxers if output is not needed. - if (stream_iter->output.empty() && stream_iter->segment_template.empty()) - continue; - } - DCHECK(!remux_jobs->empty()); - - // Each stream selector requires an individual trick play handler. - // E.g., an input with two video streams needs two trick play handlers. - // TODO(hmchen): add a test case in packager_test.py for two video streams - // input. - if (stream_iter->stream_selector != previous_stream_selector) { - previous_stream_selector = stream_iter->stream_selector; - trick_play_handler.reset(); - } - - std::shared_ptr muxer( - CreateOutputMuxer(stream_muxer_options, stream_iter->output_format)); - if (FLAGS_use_fake_clock_for_muxer) muxer->set_clock(fake_clock); - - std::unique_ptr muxer_listener; - DCHECK(!(FLAGS_output_media_info && mpd_notifier)); - if (FLAGS_output_media_info) { - const std::string output_media_info_file_name = - stream_muxer_options.output_file_name + kMediaInfoSuffix; - std::unique_ptr - vod_media_info_dump_muxer_listener( - new VodMediaInfoDumpMuxerListener(output_media_info_file_name)); - muxer_listener = std::move(vod_media_info_dump_muxer_listener); - } - if (mpd_notifier) { - std::unique_ptr mpd_notify_muxer_listener( - new MpdNotifyMuxerListener(mpd_notifier)); - muxer_listener = std::move(mpd_notify_muxer_listener); - } - - if (hls_notifier) { - // TODO(rkuroiwa): Do some smart stuff to group the audios, e.g. detect - // languages. - std::string group_id = stream_iter->hls_group_id; - std::string name = stream_iter->hls_name; - std::string hls_playlist_name = stream_iter->hls_playlist_name; - if (group_id.empty()) - group_id = "audio"; - if (name.empty()) - name = base::StringPrintf("stream_%d", stream_number); - if (hls_playlist_name.empty()) - hls_playlist_name = base::StringPrintf("stream_%d.m3u8", stream_number); - - muxer_listener.reset(new HlsNotifyMuxerListener(hls_playlist_name, name, - group_id, hls_notifier)); - } - - if (muxer_listener) - muxer->SetMuxerListener(std::move(muxer_listener)); - - // Create a new trick_play_handler. Note that the stream_decriptors - // are sorted so that for the same input and stream_selector, the main - // stream is always the last one following the trick play streams. - if (stream_iter->trick_play_factor > 0) { - if (!trick_play_handler) { - trick_play_handler.reset(new TrickPlayHandler()); + WidevineSigner& signer = widevine.signer; + if (!FLAGS_aes_signing_key.empty()) { + // TODO(kqyang): Take care of hex conversion and file read here. + signer.signing_key_type = WidevineSigner::SigningKeyType::kAes; + signer.aes.key = FLAGS_aes_signing_key; + signer.aes.iv = FLAGS_aes_signing_iv; } - trick_play_handler->SetHandlerForTrickPlay(stream_iter->trick_play_factor, - std::move(muxer)); - if (trick_play_handler->IsConnected()) - continue; - } else if (trick_play_handler) { - trick_play_handler->SetHandlerForMainStream(std::move(muxer)); - DCHECK(trick_play_handler->IsConnected()); - continue; - } - - std::vector> handlers; - - auto chunking_handler = std::make_shared(chunking_options); - handlers.push_back(chunking_handler); - - Status status; - if (encryption_key_source && !stream_iter->skip_encryption) { - auto new_encryption_options = encryption_options; - // Use Sample AES in MPEG2TS. - // TODO(kqyang): Consider adding a new flag to enable Sample AES as we - // will support CENC in TS in the future. - if (stream_iter->output_format == CONTAINER_MPEG2TS) { - LOG(INFO) << "Use Apple Sample AES encryption for MPEG2TS."; - new_encryption_options.protection_scheme = - kAppleSampleAesProtectionScheme; + if (!FLAGS_rsa_signing_key_path.empty()) { + signer.signing_key_type = WidevineSigner::SigningKeyType::kRsa; + if (!media::File::ReadFileToString(FLAGS_rsa_signing_key_path.c_str(), + &signer.rsa.key)) { + LOG(ERROR) << "Failed to read from '" << FLAGS_rsa_signing_key_path + << "'."; + return base::nullopt; + } } - handlers.emplace_back( - new EncryptionHandler(new_encryption_options, encryption_key_source)); + break; } - - // If trick_play_handler is available, muxer should already be connected to - // trick_play_handler. - if (trick_play_handler) { - handlers.push_back(trick_play_handler); - } else { - handlers.push_back(std::move(muxer)); + case KeyProvider::kRawKey: { + RawKeyDecryptionParams& raw_key = decryption_params.raw_key; + // An empty TrackType specifies the default KeyPair. + RawKeyDecryptionParams::KeyPair& key_pair = raw_key.key_map[""]; + // TODO(kqyang): Take care of hex conversion here. + key_pair.key_id = FLAGS_key_id; + key_pair.key = FLAGS_key; + break; } - - auto* demuxer = remux_jobs->back()->demuxer(); - const std::string& stream_selector = stream_iter->stream_selector; - status.Update(demuxer->SetHandler(stream_selector, chunking_handler)); - status.Update(ConnectHandlers(handlers)); - - if (!status.ok()) { - LOG(ERROR) << "Failed to setup graph: " << status; - return false; - } - if (!stream_iter->language.empty()) - demuxer->SetLanguageOverride(stream_selector, stream_iter->language); + case KeyProvider::kNone: + case KeyProvider::kPlayready: + break; } - // 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; - } + Mp4OutputParams& mp4_params = packaging_params.mp4_output_params; + mp4_params.num_subsegments_per_sidx = FLAGS_num_subsegments_per_sidx; + if (FLAGS_mp4_use_decoding_timestamp_in_timeline) { + LOG(WARNING) << "Flag --mp4_use_decoding_timestamp_in_timeline is set. " + "Note that it is a temporary hack to workaround Chromium " + "bug https://crbug.com/398130. The flag may be removed " + "when the Chromium bug is fixed."; } - return true; -} + mp4_params.use_decoding_timestamp_in_timeline = + FLAGS_mp4_use_decoding_timestamp_in_timeline; + mp4_params.include_pssh_in_stream = FLAGS_mp4_include_pssh_in_stream; -Status RunRemuxJobs(const std::vector>& remux_jobs) { - // Start the job threads. - for (const std::unique_ptr& job : remux_jobs) - job->Start(); + packaging_params.output_media_info = FLAGS_output_media_info; - // Wait for all jobs to complete or an error occurs. - Status status; - bool all_joined; - do { - all_joined = true; - for (const std::unique_ptr& job : remux_jobs) { - if (job->HasBeenJoined()) { - status = job->status(); - if (!status.ok()) - break; - } else { - all_joined = false; - job->Join(); - } - } - } while (!all_joined && status.ok()); + MpdParams& mpd_params = packaging_params.mpd_params; + mpd_params.generate_static_live_mpd = FLAGS_generate_static_mpd; + mpd_params.mpd_output = FLAGS_mpd_output; + mpd_params.base_urls = base::SplitString( + FLAGS_base_urls, ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); + mpd_params.generate_dash_if_iop_compliant_mpd = + FLAGS_generate_dash_if_iop_compliant_mpd; + mpd_params.minimum_update_period = FLAGS_minimum_update_period; + mpd_params.min_buffer_time = FLAGS_min_buffer_time; + mpd_params.time_shift_buffer_depth = FLAGS_time_shift_buffer_depth; + mpd_params.suggested_presentation_delay = FLAGS_suggested_presentation_delay; + mpd_params.default_language = FLAGS_default_language; - return status; -} + HlsParams& hls_params = packaging_params.hls_params; + hls_params.master_playlist_output = FLAGS_hls_master_playlist_output; + hls_params.base_url = FLAGS_hls_base_url; -bool RunPackager(const StreamDescriptorList& stream_descriptors) { - if (FLAGS_output_media_info && !FLAGS_mpd_output.empty()) { - NOTIMPLEMENTED() << "ERROR: --output_media_info and --mpd_output do not " - "work together."; - return false; - } - - // Since there isn't a muxer listener that can output both MPD and HLS, - // disallow specifying both MPD and HLS flags. - if (!FLAGS_mpd_output.empty() && !FLAGS_hls_master_playlist_output.empty()) { - LOG(ERROR) << "Cannot output both MPD and HLS."; - return false; - } - - ChunkingOptions chunking_options = GetChunkingOptions(); - EncryptionOptions encryption_options = GetEncryptionOptions(); - - MuxerOptions muxer_options = GetMuxerOptions(); - - DCHECK(!stream_descriptors.empty()); - // On demand profile generates single file segment while live profile - // generates multiple segments specified using segment template. - const bool on_demand_dash_profile = - stream_descriptors.begin()->segment_template.empty(); - for (const auto& stream_descriptor : stream_descriptors) { - if (on_demand_dash_profile != stream_descriptor.segment_template.empty()) { - LOG(ERROR) << "Inconsistent stream descriptor specification: " - "segment_template should be specified for none or all " - "stream descriptors."; - return false; - } - } - if (FLAGS_output_media_info && !on_demand_dash_profile) { - // TODO(rkuroiwa, kqyang): Support partial media info dump for live. - NOTIMPLEMENTED() << "ERROR: --output_media_info is only supported for " - "on-demand profile (not using segment_template)."; - return false; - } - - MpdOptions mpd_options = GetMpdOptions(on_demand_dash_profile); - - // Create encryption key source if needed. - std::unique_ptr encryption_key_source; - if (FLAGS_enable_widevine_encryption || FLAGS_enable_fixed_key_encryption || - FLAGS_enable_playready_encryption) { - if (encryption_options.protection_scheme == FOURCC_NULL) - return false; - encryption_key_source = - CreateEncryptionKeySource(encryption_options.protection_scheme); - if (!encryption_key_source) - return false; - } - - std::unique_ptr mpd_notifier; - if (!FLAGS_mpd_output.empty()) { - std::vector base_urls = base::SplitString( - FLAGS_base_urls, ",", base::KEEP_WHITESPACE, base::SPLIT_WANT_NONEMPTY); - if (FLAGS_generate_dash_if_iop_compliant_mpd) { - mpd_notifier.reset( - new DashIopMpdNotifier(mpd_options, base_urls, FLAGS_mpd_output)); - } else { - mpd_notifier.reset( - new SimpleMpdNotifier(mpd_options, base_urls, FLAGS_mpd_output)); - } - if (!mpd_notifier->Init()) { - LOG(ERROR) << "MpdNotifier failed to initialize."; - return false; - } - } - - std::unique_ptr hls_notifier; - if (!FLAGS_hls_master_playlist_output.empty()) { - base::FilePath master_playlist_path( - base::FilePath::FromUTF8Unsafe(FLAGS_hls_master_playlist_output)); - base::FilePath master_playlist_name = master_playlist_path.BaseName(); - - hls_notifier.reset(new hls::SimpleHlsNotifier( - hls::HlsNotifier::HlsProfile::kOnDemandProfile, FLAGS_hls_base_url, - master_playlist_path.DirName().AsEndingWithSeparator().AsUTF8Unsafe(), - master_playlist_name.AsUTF8Unsafe())); - } - - std::vector> remux_jobs; - FakeClock fake_clock; - if (!CreateRemuxJobs(stream_descriptors, chunking_options, encryption_options, - muxer_options, &fake_clock, encryption_key_source.get(), - mpd_notifier.get(), hls_notifier.get(), &remux_jobs)) { - return false; - } - - Status status = RunRemuxJobs(remux_jobs); - if (!status.ok()) { - LOG(ERROR) << "Packaging Error: " << status.ToString(); - return false; - } - - if (hls_notifier) { - if (!hls_notifier->Flush()) - return false; - } - if (mpd_notifier) { - if (!mpd_notifier->Flush()) - return false; - } - - printf("Packaging completed successfully.\n"); - return true; + return packaging_params; } int PackagerMain(int argc, char** argv) { @@ -589,18 +315,35 @@ int PackagerMain(int argc, char** argv) { if (FLAGS_override_version) SetPackagerVersionForTesting(FLAGS_test_version); - LibcryptoThreading libcrypto_threading; - // TODO(tinskip): Make InsertStreamDescriptor a member of - // StreamDescriptorList. - StreamDescriptorList stream_descriptors; + base::Optional packaging_params = GetPackagingParams(); + if (!packaging_params) + return kArgumentValidationFailed; + + std::vector stream_descriptors; for (int i = 1; i < argc; ++i) { - if (!InsertStreamDescriptor(argv[i], &stream_descriptors)) + base::Optional stream_descriptor = + ParseStreamDescriptor(argv[i]); + if (!stream_descriptor) return kArgumentValidationFailed; + stream_descriptors.push_back(stream_descriptor.value()); } - return RunPackager(stream_descriptors) ? kSuccess : kPackagingFailed; + ShakaPackager packager; + media::Status status = + packager.Initialize(packaging_params.value(), stream_descriptors); + if (!status.ok()) { + LOG(ERROR) << "Failed to initialize packager: " << status.ToString(); + return kArgumentValidationFailed; + } + status = packager.Run(); + if (!status.ok()) { + LOG(ERROR) << "Packaging Error: " << status.ToString(); + return kPackagingFailed; + } + printf("Packaging completed successfully.\n"); + return kSuccess; } -} // namespace media +} // namespace } // namespace shaka #if defined(OS_WIN) @@ -622,10 +365,10 @@ int wmain(int argc, wchar_t* argv[], wchar_t* envp[]) { utf8_argv[idx] = new char[utf8_arg.size()]; memcpy(utf8_argv[idx], &utf8_arg[0], utf8_arg.size()); } - return shaka::media::PackagerMain(argc, utf8_argv.get()); + return shaka::PackagerMain(argc, utf8_argv.get()); } #else int main(int argc, char** argv) { - return shaka::media::PackagerMain(argc, argv); + return shaka::PackagerMain(argc, argv); } #endif // defined(OS_WIN) diff --git a/packager/app/packager_util.cc b/packager/app/packager_util.cc index 975cb8dbd9..9a0ce4302a 100644 --- a/packager/app/packager_util.cc +++ b/packager/app/packager_util.cc @@ -6,17 +6,9 @@ #include "packager/app/packager_util.h" -#include -#include - -#include "packager/app/crypto_flags.h" -#include "packager/app/fixed_key_encryption_flags.h" -#include "packager/app/mpd_flags.h" -#include "packager/app/muxer_flags.h" -#include "packager/app/playready_key_encryption_flags.h" -#include "packager/app/widevine_encryption_flags.h" #include "packager/base/logging.h" #include "packager/base/strings/string_number_conversions.h" +#include "packager/base/strings/string_split.h" #include "packager/media/base/fixed_key_source.h" #include "packager/media/base/media_handler.h" #include "packager/media/base/muxer_options.h" @@ -28,14 +20,7 @@ #include "packager/media/crypto/encryption_handler.h" #include "packager/media/file/file.h" #include "packager/mpd/base/mpd_options.h" - -DEFINE_bool(mp4_use_decoding_timestamp_in_timeline, - false, - "If set, decoding timestamp instead of presentation timestamp will " - "be used when generating media timeline, e.g. timestamps in sidx " - "and mpd. This is to workaround a Chromium bug that decoding " - "timestamp is used in buffered range, https://crbug.com/398130."); -DEFINE_bool(dump_stream_info, false, "Dump demuxed stream info."); +#include "packager/packager.h" namespace shaka { namespace media { @@ -58,169 +43,200 @@ FourCC GetProtectionScheme(const std::string& protection_scheme) { } // namespace -std::unique_ptr CreateSigner() { - std::unique_ptr signer; - - if (!FLAGS_aes_signing_key.empty()) { - signer.reset(AesRequestSigner::CreateSigner( - FLAGS_signer, FLAGS_aes_signing_key, FLAGS_aes_signing_iv)); - if (!signer) { - LOG(ERROR) << "Cannot create an AES signer object from '" - << FLAGS_aes_signing_key << "':'" << FLAGS_aes_signing_iv - << "'."; - return std::unique_ptr(); - } - } else if (!FLAGS_rsa_signing_key_path.empty()) { - std::string rsa_private_key; - if (!File::ReadFileToString(FLAGS_rsa_signing_key_path.c_str(), - &rsa_private_key)) { - LOG(ERROR) << "Failed to read from '" << FLAGS_rsa_signing_key_path - << "'."; - return std::unique_ptr(); - } - signer.reset(RsaRequestSigner::CreateSigner(FLAGS_signer, rsa_private_key)); - if (!signer) { - LOG(ERROR) << "Cannot create a RSA signer object from '" - << FLAGS_rsa_signing_key_path << "'."; - return std::unique_ptr(); - } +std::unique_ptr CreateSigner(const WidevineSigner& signer) { + std::unique_ptr request_signer; + switch (signer.signing_key_type) { + case WidevineSigner::SigningKeyType::kAes: + request_signer.reset(AesRequestSigner::CreateSigner( + signer.signer_name, signer.aes.key, signer.aes.iv)); + break; + case WidevineSigner::SigningKeyType::kRsa: + request_signer.reset( + RsaRequestSigner::CreateSigner(signer.signer_name, signer.rsa.key)); + break; + case WidevineSigner::SigningKeyType::kNone: + break; } - return signer; + if (!request_signer) + LOG(ERROR) << "Failed to create the signer object."; + return request_signer; } -std::unique_ptr CreateEncryptionKeySource(FourCC protection_scheme) { +std::unique_ptr CreateEncryptionKeySource( + FourCC protection_scheme, + const EncryptionParams& encryption_params) { std::unique_ptr encryption_key_source; - if (FLAGS_enable_widevine_encryption) { - std::unique_ptr widevine_key_source( - new WidevineKeySource(FLAGS_key_server_url, FLAGS_include_common_pssh)); - widevine_key_source->set_protection_scheme(protection_scheme); - if (!FLAGS_signer.empty()) { - std::unique_ptr request_signer(CreateSigner()); - if (!request_signer) + switch (encryption_params.key_provider) { + case KeyProvider::kWidevine: { + const WidevineEncryptionParams& widevine = encryption_params.widevine; + if (widevine.key_server_url.empty()) { + LOG(ERROR) << "'key_server_url' should not be empty."; return std::unique_ptr(); - widevine_key_source->set_signer(std::move(request_signer)); - } + } + if (widevine.content_id.empty()) { + LOG(ERROR) << "'content_id' should not be empty."; + return std::unique_ptr(); + } + std::unique_ptr widevine_key_source( + new WidevineKeySource(widevine.key_server_url, + widevine.include_common_pssh)); + widevine_key_source->set_protection_scheme(protection_scheme); + if (!widevine.signer.signer_name.empty()) { + std::unique_ptr request_signer( + CreateSigner(widevine.signer)); + if (!request_signer) + return std::unique_ptr(); + widevine_key_source->set_signer(std::move(request_signer)); + } - std::vector content_id; - if (!base::HexStringToBytes(FLAGS_content_id, &content_id)) { - LOG(ERROR) << "Invalid content_id hex string specified."; - return std::unique_ptr(); + Status status = + widevine_key_source->FetchKeys(widevine.content_id, widevine.policy); + if (!status.ok()) { + LOG(ERROR) << "Widevine encryption key source failed to fetch keys: " + << status.ToString(); + return std::unique_ptr(); + } + encryption_key_source = std::move(widevine_key_source); + break; } - Status status = widevine_key_source->FetchKeys(content_id, FLAGS_policy); - if (!status.ok()) { - LOG(ERROR) << "Widevine encryption key source failed to fetch keys: " - << status.ToString(); - return std::unique_ptr(); + case KeyProvider::kRawKey: { + const RawKeyEncryptionParams& raw_key = encryption_params.raw_key; + const std::string kDefaultTrackType; + // TODO(kqyang): Refactor FixedKeySource. + encryption_key_source = FixedKeySource::CreateFromHexStrings( + raw_key.key_map.find("")->second.key_id, + raw_key.key_map.find("")->second.key, raw_key.pssh, raw_key.iv); + break; } - encryption_key_source = std::move(widevine_key_source); - } else if (FLAGS_enable_fixed_key_encryption) { - encryption_key_source = FixedKeySource::CreateFromHexStrings( - FLAGS_key_id, FLAGS_key, FLAGS_pssh, FLAGS_iv); - } else if (FLAGS_enable_playready_encryption) { - if (!FLAGS_playready_key_id.empty() && !FLAGS_playready_key.empty()) { - encryption_key_source = PlayReadyKeySource::CreateFromKeyAndKeyId( - FLAGS_playready_key_id, FLAGS_playready_key); - } else if (!FLAGS_playready_server_url.empty() && - !FLAGS_program_identifier.empty()) { - std::unique_ptr playready_key_source; - if (!FLAGS_client_cert_file.empty() && - !FLAGS_client_cert_private_key_file.empty() && - !FLAGS_client_cert_private_key_password.empty()) { - playready_key_source.reset(new PlayReadyKeySource( - FLAGS_playready_server_url, - FLAGS_client_cert_file, - FLAGS_client_cert_private_key_file, - FLAGS_client_cert_private_key_password)); + case KeyProvider::kPlayready: { + const PlayreadyEncryptionParams& playready = encryption_params.playready; + if (!playready.key_id.empty() && !playready.key.empty()) { + encryption_key_source = PlayReadyKeySource::CreateFromKeyAndKeyId( + playready.key_id, playready.key); + } else if (!playready.key_server_url.empty() && + !playready.program_identifier.empty()) { + std::unique_ptr playready_key_source; + if (!playready.client_cert_file.empty() && + !playready.client_cert_private_key_file.empty() && + !playready.client_cert_private_key_password.empty()) { + playready_key_source.reset(new PlayReadyKeySource( + playready.key_server_url, playready.client_cert_file, + playready.client_cert_private_key_file, + playready.client_cert_private_key_password)); + } else { + playready_key_source.reset( + new PlayReadyKeySource(playready.key_server_url)); + } + if (!playready.ca_file.empty()) { + playready_key_source->SetCaFile(playready.ca_file); + } + playready_key_source->FetchKeysWithProgramIdentifier( + playready.program_identifier); + encryption_key_source = std::move(playready_key_source); } else { - playready_key_source.reset(new PlayReadyKeySource( - FLAGS_playready_server_url)); + LOG(ERROR) << "Error creating PlayReady key source."; + return std::unique_ptr(); } - if (!FLAGS_ca_file.empty()) { - playready_key_source->SetCaFile(FLAGS_ca_file); - } - playready_key_source->FetchKeysWithProgramIdentifier(FLAGS_program_identifier); - encryption_key_source = std::move(playready_key_source); - } else { - LOG(ERROR) << "Error creating PlayReady key source."; - return std::unique_ptr(); + break; } + case KeyProvider::kNone: + break; } return encryption_key_source; } -std::unique_ptr CreateDecryptionKeySource() { +std::unique_ptr CreateDecryptionKeySource( + const DecryptionParams& decryption_params) { std::unique_ptr decryption_key_source; - if (FLAGS_enable_widevine_decryption) { - std::unique_ptr widevine_key_source( - new WidevineKeySource(FLAGS_key_server_url, FLAGS_include_common_pssh)); - if (!FLAGS_signer.empty()) { - std::unique_ptr request_signer(CreateSigner()); - if (!request_signer) + switch (decryption_params.key_provider) { + case KeyProvider::kWidevine: { + const WidevineDecryptionParams& widevine = decryption_params.widevine; + if (widevine.key_server_url.empty()) { + LOG(ERROR) << "'key_server_url' should not be empty."; return std::unique_ptr(); - widevine_key_source->set_signer(std::move(request_signer)); - } + } + std::unique_ptr widevine_key_source( + new WidevineKeySource(widevine.key_server_url, + true /* commmon pssh, does not matter here */)); + if (!widevine.signer.signer_name.empty()) { + std::unique_ptr request_signer( + CreateSigner(widevine.signer)); + if (!request_signer) + return std::unique_ptr(); + widevine_key_source->set_signer(std::move(request_signer)); + } - decryption_key_source = std::move(widevine_key_source); - } else if (FLAGS_enable_fixed_key_decryption) { - const char kNoPssh[] = ""; - const char kNoIv[] = ""; - decryption_key_source = FixedKeySource::CreateFromHexStrings( - FLAGS_key_id, FLAGS_key, kNoPssh, kNoIv); + decryption_key_source = std::move(widevine_key_source); + break; + } + case KeyProvider::kRawKey: { + const RawKeyDecryptionParams& raw_key = decryption_params.raw_key; + const char kNoPssh[] = ""; + const char kNoIv[] = ""; + decryption_key_source = FixedKeySource::CreateFromHexStrings( + raw_key.key_map.find("")->second.key_id, + raw_key.key_map.find("")->second.key, kNoPssh, kNoIv); + break; + } + case KeyProvider::kNone: + case KeyProvider::kPlayready: + break; } return decryption_key_source; } -ChunkingOptions GetChunkingOptions() { +ChunkingOptions GetChunkingOptions(const ChunkingParams& chunking_params) { ChunkingOptions chunking_options; - chunking_options.segment_duration_in_seconds = FLAGS_segment_duration; - chunking_options.subsegment_duration_in_seconds = FLAGS_fragment_duration; - chunking_options.segment_sap_aligned = FLAGS_segment_sap_aligned; - chunking_options.subsegment_sap_aligned = FLAGS_fragment_sap_aligned; + chunking_options.segment_duration_in_seconds = + chunking_params.segment_duration_in_seconds; + chunking_options.subsegment_duration_in_seconds = + chunking_params.subsegment_duration_in_seconds; + chunking_options.segment_sap_aligned = chunking_params.segment_sap_aligned; + chunking_options.subsegment_sap_aligned = + chunking_params.subsegment_sap_aligned; return chunking_options; } -EncryptionOptions GetEncryptionOptions() { +EncryptionOptions GetEncryptionOptions( + const EncryptionParams& encryption_params) { EncryptionOptions encryption_options; - encryption_options.clear_lead_in_seconds = FLAGS_clear_lead; + encryption_options.clear_lead_in_seconds = + encryption_params.clear_lead_in_seconds; encryption_options.protection_scheme = - GetProtectionScheme(FLAGS_protection_scheme); - encryption_options.max_sd_pixels = FLAGS_max_sd_pixels; - encryption_options.max_hd_pixels = FLAGS_max_hd_pixels; - encryption_options.max_uhd1_pixels = FLAGS_max_uhd1_pixels; + GetProtectionScheme(encryption_params.protection_scheme); encryption_options.crypto_period_duration_in_seconds = - FLAGS_crypto_period_duration; - encryption_options.vp9_subsample_encryption = FLAGS_vp9_subsample_encryption; + encryption_params.crypto_period_duration_in_seconds; + encryption_options.vp9_subsample_encryption = + encryption_params.vp9_subsample_encryption; + encryption_options.stream_label_func = encryption_params.stream_label_func; return encryption_options; } -MuxerOptions GetMuxerOptions() { +MuxerOptions GetMuxerOptions(const std::string& temp_dir, + const Mp4OutputParams& mp4_params) { MuxerOptions muxer_options; - muxer_options.num_subsegments_per_sidx = FLAGS_num_subsegments_per_sidx; - muxer_options.mp4_include_pssh_in_stream = FLAGS_mp4_include_pssh_in_stream; - if (FLAGS_mp4_use_decoding_timestamp_in_timeline) { - LOG(WARNING) << "Flag --mp4_use_decoding_timestamp_in_timeline is set. " - "Note that it is a temporary hack to workaround Chromium " - "bug https://crbug.com/398130. The flag may be removed " - "when the Chromium bug is fixed."; - } + muxer_options.num_subsegments_per_sidx = mp4_params.num_subsegments_per_sidx; + muxer_options.mp4_include_pssh_in_stream = mp4_params.include_pssh_in_stream; muxer_options.mp4_use_decoding_timestamp_in_timeline = - FLAGS_mp4_use_decoding_timestamp_in_timeline; - muxer_options.temp_dir = FLAGS_temp_dir; + mp4_params.use_decoding_timestamp_in_timeline; + muxer_options.temp_dir = temp_dir; return muxer_options; } -MpdOptions GetMpdOptions(bool on_demand_profile) { +MpdOptions GetMpdOptions(bool on_demand_profile, const MpdParams& mpd_params) { MpdOptions mpd_options; mpd_options.dash_profile = on_demand_profile ? DashProfile::kOnDemand : DashProfile::kLive; - mpd_options.mpd_type = (on_demand_profile || FLAGS_generate_static_mpd) - ? MpdType::kStatic - : MpdType::kDynamic; - mpd_options.minimum_update_period = FLAGS_minimum_update_period; - mpd_options.min_buffer_time = FLAGS_min_buffer_time; - mpd_options.time_shift_buffer_depth = FLAGS_time_shift_buffer_depth; - mpd_options.suggested_presentation_delay = FLAGS_suggested_presentation_delay; - mpd_options.default_language = FLAGS_default_language; + mpd_options.mpd_type = + (on_demand_profile || mpd_params.generate_static_live_mpd) + ? MpdType::kStatic + : MpdType::kDynamic; + mpd_options.minimum_update_period = mpd_params.minimum_update_period; + mpd_options.min_buffer_time = mpd_params.min_buffer_time; + mpd_options.time_shift_buffer_depth = mpd_params.time_shift_buffer_depth; + mpd_options.suggested_presentation_delay = + mpd_params.suggested_presentation_delay; + mpd_options.default_language = mpd_params.default_language; return mpd_options; } diff --git a/packager/app/packager_util.h b/packager/app/packager_util.h index 77dd951e77..d6aafa319b 100644 --- a/packager/app/packager_util.h +++ b/packager/app/packager_util.h @@ -13,13 +13,21 @@ #include +#include "packager/base/optional.h" #include "packager/media/base/fourccs.h" +#include "packager/packager.h" DECLARE_bool(dump_stream_info); namespace shaka { +// TODO(kqyang): Should we consolidate XxxParams and XxxOptions? +struct ChunkingParams; +struct DecryptionParams; +struct EncryptionParams; +struct Mp4OutputParams; struct MpdOptions; +struct MpdParams; namespace media { @@ -36,25 +44,30 @@ struct MuxerOptions; /// encryption. /// @return A std::unique_ptr containing a new KeySource, or nullptr if /// encryption is not required. -std::unique_ptr CreateEncryptionKeySource(FourCC protection_scheme); +std::unique_ptr CreateEncryptionKeySource( + FourCC protection_scheme, + const EncryptionParams& encryption_params); /// Create KeySource based on provided command line options for content /// decryption. Does not fetch keys. /// @return A std::unique_ptr containing a new KeySource, or nullptr if /// decryption is not required. -std::unique_ptr CreateDecryptionKeySource(); +std::unique_ptr CreateDecryptionKeySource( + const DecryptionParams& decryption_params); /// @return ChunkingOptions from provided command line options. -ChunkingOptions GetChunkingOptions(); +ChunkingOptions GetChunkingOptions(const ChunkingParams& chunking_params); /// @return EncryptionOptions from provided command line options. -EncryptionOptions GetEncryptionOptions(); +EncryptionOptions GetEncryptionOptions( + const EncryptionParams& encryption_params); /// @return MuxerOptions from provided command line options. -MuxerOptions GetMuxerOptions(); +MuxerOptions GetMuxerOptions(const std::string& temp_dir, + const Mp4OutputParams& mp4_params); /// @return MpdOptions from provided command line options. -MpdOptions GetMpdOptions(bool on_demand_profile); +MpdOptions GetMpdOptions(bool on_demand_profile, const MpdParams& mpd_params); /// Connect handlers in the vector. /// @param handlers A vector of media handlers to be conncected. the handlers diff --git a/packager/app/stream_descriptor.cc b/packager/app/stream_descriptor.cc index dd374c9ad3..e12259f3c4 100644 --- a/packager/app/stream_descriptor.cc +++ b/packager/app/stream_descriptor.cc @@ -6,15 +6,12 @@ #include "packager/app/stream_descriptor.h" -#include "packager/app/packager_util.h" #include "packager/base/logging.h" #include "packager/base/strings/string_number_conversions.h" #include "packager/base/strings/string_split.h" -#include "packager/media/base/container_names.h" #include "packager/media/base/language_utils.h" namespace shaka { -namespace media { namespace { @@ -74,22 +71,16 @@ FieldType GetFieldType(const std::string& field_name) { } // anonymous namespace -StreamDescriptor::StreamDescriptor() {} - -StreamDescriptor::~StreamDescriptor() {} - -bool InsertStreamDescriptor(const std::string& descriptor_string, - StreamDescriptorList* descriptor_list) { +base::Optional ParseStreamDescriptor( + const std::string& descriptor_string) { StreamDescriptor descriptor; // Split descriptor string into name/value pairs. base::StringPairs pairs; - if (!base::SplitStringIntoKeyValuePairs(descriptor_string, - '=', - ',', + if (!base::SplitStringIntoKeyValuePairs(descriptor_string, '=', ',', &pairs)) { LOG(ERROR) << "Invalid stream descriptors name/value pairs."; - return false; + return base::nullopt; } for (base::StringPairs::const_iterator iter = pairs.begin(); iter != pairs.end(); ++iter) { @@ -110,28 +101,23 @@ bool InsertStreamDescriptor(const std::string& descriptor_string, unsigned bw; if (!base::StringToUint(iter->second, &bw)) { LOG(ERROR) << "Non-numeric bandwidth specified."; - return false; + return base::nullopt; } descriptor.bandwidth = bw; break; } case kLanguageField: { + // TODO(kqyang): Move to packager.cc. std::string language = LanguageToISO_639_2(iter->second); if (language == "und") { LOG(ERROR) << "Unknown/invalid language specified: " << iter->second; - return false; + return base::nullopt; } descriptor.language = language; break; } case kOutputFormatField: { - MediaContainerName output_format = - DetermineContainerFromFormatName(iter->second); - if (output_format == CONTAINER_UNKNOWN) { - LOG(ERROR) << "Unrecognized output format " << iter->second; - return false; - } - descriptor.output_format = output_format; + descriptor.output_format = iter->second; break; } case kHlsNameField: { @@ -151,11 +137,11 @@ bool InsertStreamDescriptor(const std::string& descriptor_string, if (!base::StringToUint(iter->second, &factor)) { LOG(ERROR) << "Non-numeric trick play factor " << iter->second << " specified."; - return false; + return base::nullopt; } if (factor == 0) { LOG(ERROR) << "Stream trick_play_factor should be > 0."; - return false; + return base::nullopt; } descriptor.trick_play_factor = factor; break; @@ -165,11 +151,11 @@ bool InsertStreamDescriptor(const std::string& descriptor_string, if (!base::StringToUint(iter->second, &skip_encryption_value)) { LOG(ERROR) << "Non-numeric option for skip encryption field " "specified (" << iter->second << ")."; - return false; + return base::nullopt; } if (skip_encryption_value > 1) { LOG(ERROR) << "skip_encryption should be either 0 or 1."; - return false; + return base::nullopt; } descriptor.skip_encryption = skip_encryption_value > 0; @@ -178,61 +164,10 @@ bool InsertStreamDescriptor(const std::string& descriptor_string, default: LOG(ERROR) << "Unknown field in stream descriptor (\"" << iter->first << "\")."; - return false; + return base::nullopt; } } - // Validate and insert the descriptor - if (descriptor.input.empty()) { - LOG(ERROR) << "Stream input not specified."; - return false; - } - if (!FLAGS_dump_stream_info && descriptor.stream_selector.empty()) { - LOG(ERROR) << "Stream stream_selector not specified."; - return false; - } - - if (descriptor.output_format == CONTAINER_UNKNOWN) { - const std::string& output_name = descriptor.output.empty() - ? descriptor.segment_template - : descriptor.output; - if (!output_name.empty()) { - descriptor.output_format = DetermineContainerFromFileName(output_name); - if (descriptor.output_format == CONTAINER_UNKNOWN) { - LOG(ERROR) << "Unable to determine output format for file " - << output_name; - return false; - } - } - } - - if (descriptor.output_format == MediaContainerName::CONTAINER_MPEG2TS) { - if (descriptor.segment_template.empty()) { - LOG(ERROR) << "Please specify segment_template. Single file TS output is " - "not supported."; - return false; - } - // Note that MPEG2 TS doesn't need a separate initialization segment, so - // output field is not needed. - if (!descriptor.output.empty()) { - LOG(WARNING) << "TS output '" << descriptor.output - << "' ignored. TS muxer does not support initialization " - "segment generation."; - } - } - - // For TS output, segment template is sufficient, and does not require an - // output entry. - const bool output_specified = - !descriptor.output.empty() || - (descriptor.output_format == CONTAINER_MPEG2TS && - !descriptor.segment_template.empty()); - if (!FLAGS_dump_stream_info && !output_specified) { - LOG(ERROR) << "Stream output not specified."; - return false; - } - descriptor_list->insert(descriptor); - return true; + return descriptor; } -} // namespace media } // namespace shaka diff --git a/packager/app/stream_descriptor.h b/packager/app/stream_descriptor.h index 4ac538a612..5a146ba767 100644 --- a/packager/app/stream_descriptor.h +++ b/packager/app/stream_descriptor.h @@ -7,54 +7,12 @@ #ifndef APP_STREAM_DESCRIPTOR_H_ #define APP_STREAM_DESCRIPTOR_H_ -#include - -#include #include -#include "packager/media/base/container_names.h" +#include "packager/base/optional.h" +#include "packager/packager.h" namespace shaka { -namespace media { - -/// Defines a single input/output stream, it's input source, output destination, -/// stream selector, and optional segment template and user-specified bandwidth. -struct StreamDescriptor { - StreamDescriptor(); - ~StreamDescriptor(); - - std::string stream_selector; - std::string input; - std::string output; - std::string segment_template; - uint32_t bandwidth = 0; - std::string language; - MediaContainerName output_format = CONTAINER_UNKNOWN; - std::string hls_name; - std::string hls_group_id; - std::string hls_playlist_name; - uint32_t trick_play_factor = 0; - bool skip_encryption = false; -}; - -class StreamDescriptorCompareFn { - public: - bool operator()(const StreamDescriptor& a, const StreamDescriptor& b) { - if (a.input == b.input) { - if (a.stream_selector == b.stream_selector) - // Stream with high trick_play_factor is at the beginning. - return a.trick_play_factor > b.trick_play_factor; - else - return a.stream_selector < b.stream_selector; - } - - return a.input < b.input; - } -}; - -/// Sorted list of StreamDescriptor. -typedef std::multiset - StreamDescriptorList; /// Parses a descriptor string, and inserts into sorted list of stream /// descriptors. @@ -63,10 +21,9 @@ typedef std::multiset /// @param descriptor_list is a pointer to the sorted descriptor list into /// which the new descriptor should be inserted. /// @return true if successful, false otherwise. May print error messages. -bool InsertStreamDescriptor(const std::string& descriptor_string, - StreamDescriptorList* descriptor_list); +base::Optional ParseStreamDescriptor( + const std::string& descriptor_string); -} // namespace media } // namespace shaka #endif // APP_STREAM_DESCRIPTOR_H_ diff --git a/packager/media/base/muxer_util.cc b/packager/media/base/muxer_util.cc index 765d535677..77e72664fd 100644 --- a/packager/media/base/muxer_util.cc +++ b/packager/media/base/muxer_util.cc @@ -154,29 +154,5 @@ std::string GetSegmentName(const std::string& segment_template, return segment_name; } -KeySource::TrackType GetTrackTypeForEncryption(const StreamInfo& stream_info, - uint32_t max_sd_pixels, - uint32_t max_hd_pixels, - uint32_t max_uhd1_pixels) { - if (stream_info.stream_type() == kStreamAudio) - return KeySource::TRACK_TYPE_AUDIO; - - if (stream_info.stream_type() != kStreamVideo) - return KeySource::TRACK_TYPE_UNKNOWN; - - DCHECK_EQ(kStreamVideo, stream_info.stream_type()); - const VideoStreamInfo& video_stream_info = - static_cast(stream_info); - uint32_t pixels = video_stream_info.width() * video_stream_info.height(); - if (pixels > max_uhd1_pixels) { - return KeySource::TRACK_TYPE_UHD2; - } else if (pixels > max_hd_pixels) { - return KeySource::TRACK_TYPE_UHD1; - } else if (pixels > max_sd_pixels) { - return KeySource::TRACK_TYPE_HD; - } - return KeySource::TRACK_TYPE_SD; -} - } // namespace media } // namespace shaka diff --git a/packager/media/base/muxer_util.h b/packager/media/base/muxer_util.h index a2f7cd4bdb..08b4caa8a2 100644 --- a/packager/media/base/muxer_util.h +++ b/packager/media/base/muxer_util.h @@ -39,18 +39,6 @@ std::string GetSegmentName(const std::string& segment_template, uint32_t segment_index, uint32_t bandwidth); -/// Determine the track type for encryption from input. -/// @param stream_info is the info of the stream. -/// @param max_sd_pixels is the maximum number of pixels to be considered SD. -/// @param max_hd_pixels is the maximum number of pixels to be considered HD. -/// @param max_uhd1_pixels is the maximum number of pixels to be considered UHD1. -/// Anything above is UHD2. -/// @return track type for encryption. -KeySource::TrackType GetTrackTypeForEncryption(const StreamInfo& stream_info, - uint32_t max_sd_pixels, - uint32_t max_hd_pixels, - uint32_t max_uhd1_pixels); - } // namespace media } // namespace shaka diff --git a/packager/media/crypto/encryption_handler.cc b/packager/media/crypto/encryption_handler.cc index 0e45bd9fa4..ec5855c118 100644 --- a/packager/media/crypto/encryption_handler.cc +++ b/packager/media/crypto/encryption_handler.cc @@ -56,28 +56,35 @@ uint8_t GetNaluLengthSize(const StreamInfo& stream_info) { return video_stream_info.nalu_length_size(); } -KeySource::TrackType GetTrackTypeForEncryption(const StreamInfo& stream_info, - uint32_t max_sd_pixels, - uint32_t max_hd_pixels, - uint32_t max_uhd1_pixels) { - if (stream_info.stream_type() == kStreamAudio) - return KeySource::TRACK_TYPE_AUDIO; - - if (stream_info.stream_type() != kStreamVideo) - return KeySource::TRACK_TYPE_UNKNOWN; - - DCHECK_EQ(kStreamVideo, stream_info.stream_type()); - const VideoStreamInfo& video_stream_info = - static_cast(stream_info); - uint32_t pixels = video_stream_info.width() * video_stream_info.height(); - if (pixels <= max_sd_pixels) { +// TODO(kqyang): Update KeySource to accept string base stream label. +KeySource::TrackType ToTrackType(const std::string& track_type) { + if (track_type == "SD") return KeySource::TRACK_TYPE_SD; - } else if (pixels <= max_hd_pixels) { + if (track_type == "HD") return KeySource::TRACK_TYPE_HD; - } else if (pixels <= max_uhd1_pixels) { - return KeySource::TRACK_TYPE_UHD1; + if (track_type == "AUDIO") + return KeySource::TRACK_TYPE_AUDIO; + return KeySource::TRACK_TYPE_SD; +} + +KeySource::TrackType GetTrackTypeForEncryption( + const StreamInfo& stream_info, + const std::function& + stream_label_func) { + EncryptionParams::EncryptedStreamAttributes stream_attributes; + if (stream_info.stream_type() == kStreamAudio) { + stream_attributes.stream_type = + EncryptionParams::EncryptedStreamAttributes::kAudio; + } else if (stream_info.stream_type() == kStreamVideo) { + const VideoStreamInfo& video_stream_info = + static_cast(stream_info); + stream_attributes.stream_type = + EncryptionParams::EncryptedStreamAttributes::kVideo; + stream_attributes.oneof.video.width = video_stream_info.width(); + stream_attributes.oneof.video.height = video_stream_info.height(); } - return KeySource::TRACK_TYPE_UHD2; + return ToTrackType(stream_label_func(stream_attributes)); } } // namespace @@ -89,6 +96,9 @@ EncryptionHandler::EncryptionHandler( EncryptionHandler::~EncryptionHandler() {} Status EncryptionHandler::InitializeInternal() { + if (!encryption_options_.stream_label_func) { + return Status(error::INVALID_ARGUMENT, "Stream label function not set."); + } if (num_input_streams() != 1 || next_output_stream_index() != 1) { return Status(error::INVALID_ARGUMENT, "Expects exactly one input and output."); @@ -142,8 +152,7 @@ Status EncryptionHandler::ProcessStreamInfo(StreamInfo* stream_info) { codec_ = stream_info->codec(); nalu_length_size_ = GetNaluLengthSize(*stream_info); track_type_ = GetTrackTypeForEncryption( - *stream_info, encryption_options_.max_sd_pixels, - encryption_options_.max_hd_pixels, encryption_options_.max_uhd1_pixels); + *stream_info, encryption_options_.stream_label_func); switch (codec_) { case kCodecVP9: if (encryption_options_.vp9_subsample_encryption) diff --git a/packager/media/crypto/encryption_handler.h b/packager/media/crypto/encryption_handler.h index e825280e62..5c795cf7f6 100644 --- a/packager/media/crypto/encryption_handler.h +++ b/packager/media/crypto/encryption_handler.h @@ -9,6 +9,7 @@ #include "packager/media/base/key_source.h" #include "packager/media/base/media_handler.h" +#include "packager/packager.h" namespace shaka { namespace media { @@ -25,24 +26,16 @@ struct EncryptionOptions { double clear_lead_in_seconds = 0; /// The protection scheme: 'cenc', 'cens', 'cbc1', 'cbcs'. FourCC protection_scheme = FOURCC_cenc; - /// The threshold to determine whether a video track should be considered as - /// SD. If the max pixels per frame is no higher than max_sd_pixels, i.e. - /// [0, max_sd_pixels], it is SD. - uint32_t max_sd_pixels = 0; - /// The threshold to determine whether a video track should be considered as - /// HD. If the max pixels per frame is higher than max_sd_pixels, but no - /// higher than max_hd_pixels, i.e. (max_sd_pixels, max_hd_pixels], it is HD. - uint32_t max_hd_pixels = 0; - /// The threshold to determine whether a video track should be considered as - /// UHD1. If the max pixels per frame is higher than max_hd_pixels, but no - /// higher than max_uhd1_pixels, i.e. (max_hd_pixels, max_uhd1_pixels], it is - /// UHD1. Otherwise it is UHD2. - uint32_t max_uhd1_pixels = 0; /// Crypto period duration in seconds. A positive value means key rotation is /// enabled, the key source must support key rotation in this case. double crypto_period_duration_in_seconds = 0; - // Enable/disable subsample encryption for VP9. + /// Enable/disable subsample encryption for VP9. bool vp9_subsample_encryption = true; + /// Stream label function used to get the label of the encrypted stream. Must + /// be set. + std::function + stream_label_func; }; class EncryptionHandler : public MediaHandler { diff --git a/packager/media/crypto/encryption_handler_unittest.cc b/packager/media/crypto/encryption_handler_unittest.cc index a7ff9cb681..edafd6a394 100644 --- a/packager/media/crypto/encryption_handler_unittest.cc +++ b/packager/media/crypto/encryption_handler_unittest.cc @@ -33,6 +33,9 @@ using ::testing::Values; using ::testing::ValuesIn; using ::testing::WithParamInterface; +const char kAudioStreamLabel[] = "AUDIO"; +const char kSdVideoStreamLabel[] = "SD"; + const uint8_t kKeyId[]{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, @@ -82,8 +85,21 @@ class EncryptionHandlerTest : public MediaHandlerTestBase { void SetUp() override { SetUpEncryptionHandler(EncryptionOptions()); } void SetUpEncryptionHandler(const EncryptionOptions& encryption_options) { + EncryptionOptions new_encryption_options = encryption_options; + if (!encryption_options.stream_label_func) { + // Setup default stream label function. + new_encryption_options.stream_label_func = + [](const EncryptionParams::EncryptedStreamAttributes& + stream_attributes) { + if (stream_attributes.stream_type == + EncryptionParams::EncryptedStreamAttributes::kAudio) { + return kAudioStreamLabel; + } + return kSdVideoStreamLabel; + }; + } encryption_handler_.reset( - new EncryptionHandler(encryption_options, &mock_key_source_)); + new EncryptionHandler(new_encryption_options, &mock_key_source_)); SetUpGraph(1 /* one input */, 1 /* one output */, encryption_handler_); } @@ -624,74 +640,59 @@ INSTANTIATE_TEST_CASE_P(AppleSampleAes, Values(kCodecAAC, kCodecH264), Values(kVp9SubsampleEncryption))); -namespace { - -const uint32_t kMaxSdPixels = 100u; -const uint32_t kMaxHdPixels = 200u; -const uint32_t kMaxUhd1Pixels = 300u; - -struct TrackTypeTestCase { - uint16_t width; - uint16_t height; - KeySource::TrackType track_type; -}; - -const TrackTypeTestCase kTrackTypeTestCases[] = { - TrackTypeTestCase{10, 10, KeySource::TRACK_TYPE_SD}, - TrackTypeTestCase{11, 9, KeySource::TRACK_TYPE_SD}, - TrackTypeTestCase{11, 10, KeySource::TRACK_TYPE_HD}, - TrackTypeTestCase{20, 10, KeySource::TRACK_TYPE_HD}, - TrackTypeTestCase{10, 20, KeySource::TRACK_TYPE_HD}, - TrackTypeTestCase{19, 10, KeySource::TRACK_TYPE_HD}, - TrackTypeTestCase{21, 10, KeySource::TRACK_TYPE_UHD1}, - TrackTypeTestCase{29, 10, KeySource::TRACK_TYPE_UHD1}, - TrackTypeTestCase{30, 10, KeySource::TRACK_TYPE_UHD1}, - TrackTypeTestCase{20, 15, KeySource::TRACK_TYPE_UHD1}, - TrackTypeTestCase{20, 16, KeySource::TRACK_TYPE_UHD2}, - TrackTypeTestCase{1000, 1000, KeySource::TRACK_TYPE_UHD2}, -}; - -} // namespace - -class EncryptionHandlerTrackTypeTest - : public EncryptionHandlerTest, - public WithParamInterface { +class EncryptionHandlerTrackTypeTest : public EncryptionHandlerTest { public: void SetUp() override { - EncryptionOptions encryption_options; - encryption_options.max_sd_pixels = kMaxSdPixels; - encryption_options.max_hd_pixels = kMaxHdPixels; - encryption_options.max_uhd1_pixels = kMaxUhd1Pixels; - SetUpEncryptionHandler(encryption_options); } }; TEST_F(EncryptionHandlerTrackTypeTest, AudioTrackType) { + EncryptionParams::EncryptedStreamAttributes captured_stream_attributes; EncryptionOptions encryption_options; + encryption_options.stream_label_func = + [&captured_stream_attributes]( + const EncryptionParams::EncryptedStreamAttributes& + stream_attributes) { + captured_stream_attributes = stream_attributes; + return kAudioStreamLabel; + }; SetUpEncryptionHandler(encryption_options); EXPECT_CALL(mock_key_source_, GetKey(KeySource::TRACK_TYPE_AUDIO, _)) .WillOnce( DoAll(SetArgPointee<1>(GetMockEncryptionKey()), Return(Status::OK))); ASSERT_OK(Process(GetAudioStreamInfoStreamData(kStreamIndex, kTimeScale))); + EXPECT_EQ(EncryptionParams::EncryptedStreamAttributes::kAudio, + captured_stream_attributes.stream_type); } -TEST_P(EncryptionHandlerTrackTypeTest, VideoTrackType) { - TrackTypeTestCase test_case = GetParam(); - EXPECT_CALL(mock_key_source_, GetKey(test_case.track_type, _)) +TEST_F(EncryptionHandlerTrackTypeTest, VideoTrackType) { + EncryptionParams::EncryptedStreamAttributes captured_stream_attributes; + EncryptionOptions encryption_options; + encryption_options.stream_label_func = + [&captured_stream_attributes]( + const EncryptionParams::EncryptedStreamAttributes& + stream_attributes) { + captured_stream_attributes = stream_attributes; + return kSdVideoStreamLabel; + }; + SetUpEncryptionHandler(encryption_options); + EXPECT_CALL(mock_key_source_, GetKey(KeySource::TRACK_TYPE_SD, _)) .WillOnce( DoAll(SetArgPointee<1>(GetMockEncryptionKey()), Return(Status::OK))); std::unique_ptr stream_data = GetVideoStreamInfoStreamData(kStreamIndex, kTimeScale); VideoStreamInfo* video_stream_info = reinterpret_cast(stream_data->stream_info.get()); - video_stream_info->set_width(test_case.width); - video_stream_info->set_height(test_case.height); + video_stream_info->set_width(12); + video_stream_info->set_height(34); ASSERT_OK(Process(std::move(stream_data))); + EXPECT_EQ(EncryptionParams::EncryptedStreamAttributes::kVideo, + captured_stream_attributes.stream_type); + EXPECT_EQ(video_stream_info->width(), + captured_stream_attributes.oneof.video.width); + EXPECT_EQ(video_stream_info->height(), + captured_stream_attributes.oneof.video.height); } -INSTANTIATE_TEST_CASE_P(VideoResolutions, - EncryptionHandlerTrackTypeTest, - ValuesIn(kTrackTypeTestCases)); - } // namespace media } // namespace shaka diff --git a/packager/media/test/packager_test.cc b/packager/media/test/packager_test.cc deleted file mode 100644 index e45e0ee6a6..0000000000 --- a/packager/media/test/packager_test.cc +++ /dev/null @@ -1,383 +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 - -#include "packager/base/files/file_util.h" -#include "packager/base/strings/string_number_conversions.h" -#include "packager/base/strings/stringprintf.h" -#include "packager/base/time/clock.h" -#include "packager/media/base/fixed_key_source.h" -#include "packager/media/base/fourccs.h" -#include "packager/media/base/muxer.h" -#include "packager/media/base/muxer_util.h" -#include "packager/media/base/stream_info.h" -#include "packager/media/base/test/status_test_util.h" -#include "packager/media/chunking/chunking_handler.h" -#include "packager/media/crypto/encryption_handler.h" -#include "packager/media/demuxer/demuxer.h" -#include "packager/media/formats/mp4/mp4_muxer.h" -#include "packager/media/test/test_data_util.h" - -using ::testing::ValuesIn; - -namespace shaka { -namespace media { -namespace { - -const char* kMediaFiles[] = {"bear-640x360.mp4", "bear-640x360-av_frag.mp4", - "bear-640x360.ts"}; - -// Muxer options. -const double kSegmentDurationInSeconds = 1.0; -const double kFragmentDurationInSecodns = 0.1; -const bool kSegmentSapAligned = true; -const bool kFragmentSapAligned = true; -const int kNumSubsegmentsPerSidx = 2; - -const char kOutputVideo[] = "output_video"; -const char kOutputVideo2[] = "output_video_2"; -const char kOutputAudio[] = "output_audio"; -const char kOutputAudio2[] = "output_audio_2"; -const char kOutputNone[] = ""; - -const char kSegmentTemplate[] = "template$Number$.m4s"; -const char kSegmentTemplateOutputPattern[] = "template%d.m4s"; - -const bool kSingleSegment = true; -const bool kMultipleSegments = false; -const bool kEnableEncryption = true; -const bool kDisableEncryption = false; - -// Encryption constants. -const char kKeyIdHex[] = "e5007e6e9dcd5ac095202ed3758382cd"; -const char kKeyHex[] = "6fc96fe628a265b13aeddec0bc421f4d"; -const double kClearLeadInSeconds = 1.5; -const double kCryptoDurationInSeconds = 0; // Key rotation is disabled. - -// Track resolution constants. -const uint32_t kMaxSDPixels = 640 * 480; -const uint32_t kMaxHDPixels = 1920 * 1080; -const uint32_t kMaxUHD1Pixels = 4096 * 2160; - -} // namespace - -class FakeClock : public base::Clock { - public: - // Fake the clock to return NULL time. - base::Time Now() override { return base::Time(); } -}; - -class PackagerTestBasic : public ::testing::TestWithParam { - public: - PackagerTestBasic() {} - - void SetUp() override { - // Create a test directory for testing, will be deleted after test. - ASSERT_TRUE(base::CreateNewTempDirectory( - base::FilePath::FromUTF8Unsafe("packager_").value(), &test_directory_)); - - // Copy the input to test directory for easy reference. - ASSERT_TRUE(base::CopyFile(GetTestDataFilePath(GetParam()), - test_directory_.AppendASCII(GetParam()))); - } - - void TearDown() override { base::DeleteFile(test_directory_, true); } - - std::string GetFullPath(const std::string& file_name); - // Check if |file1| and |file2| are the same. - bool ContentsEqual(const std::string& file1, const std::string file2); - - ChunkingOptions SetupChunkingOptions(); - EncryptionOptions SetupEncryptionOptions(); - MuxerOptions SetupMuxerOptions(const std::string& output, - bool single_segment); - void Remux(const std::string& input, - const std::string& video_output, - const std::string& audio_output, - bool single_segment, - bool enable_encryption); - - void Decrypt(const std::string& input, - const std::string& video_output, - const std::string& audio_output); - - protected: - base::FilePath test_directory_; - FakeClock fake_clock_; -}; - -std::string PackagerTestBasic::GetFullPath(const std::string& file_name) { - return test_directory_.Append( - base::FilePath::FromUTF8Unsafe(file_name)).AsUTF8Unsafe(); -} - -bool PackagerTestBasic::ContentsEqual(const std::string& file1, - const std::string file2) { - return base::ContentsEqual(test_directory_.AppendASCII(file1), - test_directory_.AppendASCII(file2)); -} - -MuxerOptions PackagerTestBasic::SetupMuxerOptions(const std::string& output, - bool single_segment) { - MuxerOptions options; - options.num_subsegments_per_sidx = kNumSubsegmentsPerSidx; - options.output_file_name = GetFullPath(output); - if (!single_segment) - options.segment_template = GetFullPath(kSegmentTemplate); - options.temp_dir = test_directory_.AsUTF8Unsafe(); - return options; -} - -ChunkingOptions PackagerTestBasic::SetupChunkingOptions() { - ChunkingOptions options; - options.segment_duration_in_seconds = kSegmentDurationInSeconds; - options.subsegment_duration_in_seconds = kFragmentDurationInSecodns; - options.segment_sap_aligned = kSegmentSapAligned; - options.subsegment_sap_aligned = kFragmentSapAligned; - return options; -} - -EncryptionOptions PackagerTestBasic::SetupEncryptionOptions() { - EncryptionOptions options; - options.clear_lead_in_seconds = kClearLeadInSeconds; - options.protection_scheme = FOURCC_cenc; - options.max_sd_pixels = kMaxSDPixels; - options.max_hd_pixels = kMaxHDPixels; - options.max_uhd1_pixels = kMaxUHD1Pixels; - options.crypto_period_duration_in_seconds = kCryptoDurationInSeconds; - return options; -} - -void PackagerTestBasic::Remux(const std::string& input, - const std::string& video_output, - const std::string& audio_output, - bool single_segment, - bool enable_encryption) { - CHECK(!video_output.empty() || !audio_output.empty()); - - Demuxer demuxer(GetFullPath(input)); - std::unique_ptr encryption_key_source( - FixedKeySource::CreateFromHexStrings(kKeyIdHex, kKeyHex, "", "")); - DCHECK(encryption_key_source); - - std::shared_ptr muxer_video; - if (!video_output.empty()) { - muxer_video.reset( - new mp4::MP4Muxer(SetupMuxerOptions(video_output, single_segment))); - muxer_video->set_clock(&fake_clock_); - - auto chunking_handler = - std::make_shared(SetupChunkingOptions()); - ASSERT_OK(demuxer.SetHandler("video", chunking_handler)); - if (enable_encryption) { - auto encryption_handler = std::make_shared( - SetupEncryptionOptions(), encryption_key_source.get()); - ASSERT_OK(chunking_handler->SetHandler(0, encryption_handler)); - ASSERT_OK(encryption_handler->SetHandler(0, muxer_video)); - } else { - ASSERT_OK(chunking_handler->SetHandler(0, muxer_video)); - } - } - - std::shared_ptr muxer_audio; - if (!audio_output.empty()) { - muxer_audio.reset( - new mp4::MP4Muxer(SetupMuxerOptions(audio_output, single_segment))); - muxer_audio->set_clock(&fake_clock_); - - auto chunking_handler = - std::make_shared(SetupChunkingOptions()); - ASSERT_OK(demuxer.SetHandler("audio", chunking_handler)); - if (enable_encryption) { - auto encryption_handler = std::make_shared( - SetupEncryptionOptions(), encryption_key_source.get()); - ASSERT_OK(chunking_handler->SetHandler(0, encryption_handler)); - ASSERT_OK(encryption_handler->SetHandler(0, muxer_audio)); - } else { - ASSERT_OK(chunking_handler->SetHandler(0, muxer_audio)); - } - } - - ASSERT_OK(demuxer.Initialize()); - // Start remuxing process. - ASSERT_OK(demuxer.Run()); -} - -void PackagerTestBasic::Decrypt(const std::string& input, - const std::string& video_output, - const std::string& audio_output) { - CHECK(!video_output.empty() || !audio_output.empty()); - - Demuxer demuxer(GetFullPath(input)); - std::unique_ptr decryption_key_source( - FixedKeySource::CreateFromHexStrings(kKeyIdHex, kKeyHex, "", "")); - ASSERT_TRUE(decryption_key_source); - demuxer.SetKeySource(std::move(decryption_key_source)); - - std::shared_ptr muxer; - if (!video_output.empty()) { - muxer.reset(new mp4::MP4Muxer(SetupMuxerOptions(video_output, true))); - } - if (!audio_output.empty()) { - muxer.reset(new mp4::MP4Muxer(SetupMuxerOptions(audio_output, true))); - } - ASSERT_TRUE(muxer); - muxer->set_clock(&fake_clock_); - - auto chunking_handler = - std::make_shared(SetupChunkingOptions()); - ASSERT_OK(demuxer.SetHandler("0", chunking_handler)); - ASSERT_OK(chunking_handler->SetHandler(0, muxer)); - - ASSERT_OK(demuxer.Initialize()); - ASSERT_OK(demuxer.Run()); -} - -TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedVideo) { - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputVideo, - kOutputNone, - kSingleSegment, - kDisableEncryption)); -} - -TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedAudio) { - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputNone, - kOutputAudio, - kSingleSegment, - kDisableEncryption)); -} - -TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedVideo) { - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputVideo, - kOutputNone, - kSingleSegment, - kEnableEncryption)); - - ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputVideo, - kOutputVideo2, - kOutputNone)); -} - -TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedAudio) { - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputNone, - kOutputAudio, - kSingleSegment, - kEnableEncryption)); - - ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputAudio, - kOutputNone, - kOutputAudio2)); -} - - -class PackagerTest : public PackagerTestBasic { - public: - void SetUp() override { - PackagerTestBasic::SetUp(); - - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputVideo, - kOutputNone, - kSingleSegment, - kDisableEncryption)); - - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputNone, - kOutputAudio, - kSingleSegment, - kDisableEncryption)); - } -}; - -TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedVideoAgain) { - // Take the muxer output and feed into muxer again. The new muxer output - // should contain the same contents as the previous muxer output. - ASSERT_NO_FATAL_FAILURE(Remux(kOutputVideo, - kOutputVideo2, - kOutputNone, - kSingleSegment, - kDisableEncryption)); - EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); -} - -TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedAudioAgain) { - // Take the muxer output and feed into muxer again. The new muxer output - // should contain the same contents as the previous muxer output. - ASSERT_NO_FATAL_FAILURE(Remux(kOutputAudio, - kOutputNone, - kOutputAudio2, - kSingleSegment, - kDisableEncryption)); - EXPECT_TRUE(ContentsEqual(kOutputAudio, kOutputAudio2)); -} - -TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedSeparateAudioVideo) { - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputVideo2, - kOutputAudio2, - kSingleSegment, - kDisableEncryption)); - - // Compare the output with single muxer output. They should match. - EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); - EXPECT_TRUE(ContentsEqual(kOutputAudio, kOutputAudio2)); -} - -TEST_P(PackagerTest, MP4MuxerMultiSegmentsUnencryptedVideo) { - ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), - kOutputVideo2, - kOutputNone, - kMultipleSegments, - kDisableEncryption)); - - // Find and concatenates the segments. - const std::string kOutputVideoSegmentsCombined = - std::string(kOutputVideo) + "_combined"; - base::FilePath output_path = - test_directory_.AppendASCII(kOutputVideoSegmentsCombined); - ASSERT_TRUE( - base::CopyFile(test_directory_.AppendASCII(kOutputVideo2), output_path)); - - const int kStartSegmentIndex = 1; // start from one. - int segment_index = kStartSegmentIndex; - while (true) { - base::FilePath segment_path = test_directory_.AppendASCII( - base::StringPrintf(kSegmentTemplateOutputPattern, segment_index)); - if (!base::PathExists(segment_path)) - break; - - std::string segment_content; - ASSERT_TRUE(base::ReadFileToString(segment_path, &segment_content)); - EXPECT_TRUE(base::AppendToFile(output_path, segment_content.data(), - static_cast(segment_content.size()))); - - ++segment_index; - } - // We should have at least one segment. - ASSERT_LT(kStartSegmentIndex, segment_index); - - // Feed the combined file into muxer again. The new muxer output should be - // the same as by just feeding the input to muxer. - ASSERT_NO_FATAL_FAILURE(Remux(kOutputVideoSegmentsCombined, - kOutputVideo2, - kOutputNone, - kSingleSegment, - kDisableEncryption)); - EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); -} - -INSTANTIATE_TEST_CASE_P(PackagerEndToEnd, - PackagerTestBasic, - ValuesIn(kMediaFiles)); -INSTANTIATE_TEST_CASE_P(PackagerEndToEnd, PackagerTest, ValuesIn(kMediaFiles)); - -} // namespace media -} // namespace shaka diff --git a/packager/packager.cc b/packager/packager.cc index bc0b61f377..3de11620a1 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -549,9 +549,9 @@ std::string EncryptionParams::DefaultStreamLabelFunction( int max_hd_pixels, int max_uhd1_pixels, const EncryptedStreamAttributes& stream_attributes) { - if (stream_info.stream_type == EncryptedStreamAttributes::kAudio) + if (stream_attributes.stream_type == EncryptedStreamAttributes::kAudio) return "AUDIO"; - if (stream_info.stream_type == EncryptedStreamAttributes::kVideo) { + if (stream_attributes.stream_type == EncryptedStreamAttributes::kVideo) { const int pixels = stream_attributes.oneof.video.width * stream_attributes.oneof.video.height; if (pixels <= max_sd_pixels) return "SD"; diff --git a/packager/packager.gyp b/packager/packager.gyp index 6d0e9c48aa..760b5753dd 100644 --- a/packager/packager.gyp +++ b/packager/packager.gyp @@ -10,36 +10,16 @@ ], 'targets': [ { - 'target_name': 'packager', - 'type': 'executable', + 'target_name': 'libpackager', + 'type': 'static_library', 'sources': [ - 'app/crypto_flags.cc', - 'app/crypto_flags.h', - 'app/fixed_key_encryption_flags.cc', - 'app/fixed_key_encryption_flags.h', - 'app/hls_flags.cc', - 'app/hls_flags.h', + 'packager.cc', + 'packager.h', + # TODO(kqyang): Clean up the file path. 'app/libcrypto_threading.cc', 'app/libcrypto_threading.h', - 'app/mpd_flags.cc', - 'app/mpd_flags.h', - 'app/muxer_flags.cc', - 'app/muxer_flags.h', - 'app/packager_main.cc', 'app/packager_util.cc', 'app/packager_util.h', - 'app/playready_key_encryption_flags.cc', - 'app/playready_key_encryption_flags.h', - 'app/retired_flags.cc', - 'app/retired_flags.h', - 'app/stream_descriptor.cc', - 'app/stream_descriptor.h', - 'app/validate_flag.cc', - 'app/validate_flag.h', - 'app/vlog_flags.cc', - 'app/vlog_flags.h', - 'app/widevine_encryption_flags.cc', - 'app/widevine_encryption_flags.h', ], 'dependencies': [ 'hls/hls.gyp:hls_builder', @@ -59,6 +39,39 @@ 'third_party/boringssl/boringssl.gyp:boringssl', 'third_party/gflags/gflags.gyp:gflags', ], + }, + { + 'target_name': 'packager', + 'type': 'executable', + 'sources': [ + 'app/crypto_flags.cc', + 'app/crypto_flags.h', + 'app/fixed_key_encryption_flags.cc', + 'app/fixed_key_encryption_flags.h', + 'app/hls_flags.cc', + 'app/hls_flags.h', + 'app/mpd_flags.cc', + 'app/mpd_flags.h', + 'app/muxer_flags.cc', + 'app/muxer_flags.h', + 'app/packager_main.cc', + 'app/playready_key_encryption_flags.cc', + 'app/playready_key_encryption_flags.h', + 'app/retired_flags.cc', + 'app/retired_flags.h', + 'app/stream_descriptor.cc', + 'app/stream_descriptor.h', + 'app/validate_flag.cc', + 'app/validate_flag.h', + 'app/vlog_flags.cc', + 'app/vlog_flags.h', + 'app/widevine_encryption_flags.cc', + 'app/widevine_encryption_flags.h', + ], + 'dependencies': [ + 'libpackager', + 'third_party/gflags/gflags.gyp:gflags', + ], 'conditions': [ ['profiling==1', { 'dependencies': [ @@ -86,19 +99,10 @@ 'target_name': 'packager_test', 'type': '<(gtest_target_type)', 'sources': [ - 'media/test/packager_test.cc', + 'packager_test.cc', ], 'dependencies': [ - 'media/codecs/codecs.gyp:codecs', - 'media/chunking/chunking.gyp:chunking', - 'media/demuxer/demuxer.gyp:demuxer', - 'media/file/file.gyp:file', - 'media/formats/mp2t/mp2t.gyp:mp2t', - 'media/formats/mp4/mp4.gyp:mp4', - 'media/formats/mpeg/mpeg.gyp:mpeg', - 'media/formats/webm/webm.gyp:webm', - 'media/formats/webvtt/webvtt.gyp:webvtt', - 'media/formats/wvm/wvm.gyp:wvm', + 'libpackager', 'media/test/media_test.gyp:media_test_support', 'testing/gtest.gyp:gtest', ], diff --git a/packager/packager.h b/packager/packager.h index 6d7469e07a..70158bd7d7 100644 --- a/packager/packager.h +++ b/packager/packager.h @@ -8,6 +8,7 @@ #define PACKAGER_PACKAGER_H_ #include +#include #include #include #include diff --git a/packager/packager_test.cc b/packager/packager_test.cc new file mode 100644 index 0000000000..ac7dbe10ff --- /dev/null +++ b/packager/packager_test.cc @@ -0,0 +1,143 @@ +// Copyright 2017 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 + +#include "packager/base/files/file_util.h" +#include "packager/media/base/test/status_test_util.h" +#include "packager/media/test/test_data_util.h" +#include "packager/packager.h" + +namespace shaka { +namespace { + +using media::Status; +const char kTestFile[] = "bear-640x360.mp4"; +const char kOutputVideo[] = "output_video.mp4"; +const char kOutputVideoTemplate[] = "output_video_$Number$.m4s"; +const char kOutputAudio[] = "output_audio.mp4"; +const char kOutputMpd[] = "output.mpd"; + +const double kSegmentDurationInSeconds = 1.0; +const char kKeyIdHex[] = "e5007e6e9dcd5ac095202ed3758382cd"; +const char kKeyHex[] = "6fc96fe628a265b13aeddec0bc421f4d"; +const double kClearLeadInSeconds = 1.0; + +} // namespace + +class PackagerTest : public ::testing::Test { + public: + PackagerTest() {} + + void SetUp() override { + // Create a test directory for testing, will be deleted after test. + ASSERT_TRUE(base::CreateNewTempDirectory( + base::FilePath::FromUTF8Unsafe("packager_").value(), &test_directory_)); + } + + void TearDown() override { base::DeleteFile(test_directory_, true); } + + std::string GetFullPath(const std::string& file_name) { + return test_directory_.Append(base::FilePath::FromUTF8Unsafe(file_name)) + .AsUTF8Unsafe(); + } + + PackagingParams SetupPackagingParams() { + PackagingParams packaging_params; + packaging_params.temp_dir = test_directory_.AsUTF8Unsafe(); + packaging_params.chunking_params.segment_duration_in_seconds = + kSegmentDurationInSeconds; + packaging_params.mpd_params.mpd_output = GetFullPath(kOutputMpd); + + packaging_params.encryption_params.clear_lead_in_seconds = + kClearLeadInSeconds; + packaging_params.encryption_params.key_provider = KeyProvider::kRawKey; + packaging_params.encryption_params.raw_key.key_map[""].key_id = kKeyIdHex; + packaging_params.encryption_params.raw_key.key_map[""].key = kKeyHex; + return packaging_params; + } + + std::vector SetupStreamDescriptors() { + std::vector stream_descriptors; + StreamDescriptor stream_descriptor; + + stream_descriptor.input = + media::GetTestDataFilePath(kTestFile).AsUTF8Unsafe(); + stream_descriptor.stream_selector = "video"; + stream_descriptor.output = GetFullPath(kOutputVideo); + stream_descriptors.push_back(stream_descriptor); + + stream_descriptor.input = + media::GetTestDataFilePath(kTestFile).AsUTF8Unsafe(); + stream_descriptor.stream_selector = "audio"; + stream_descriptor.output = GetFullPath(kOutputAudio); + stream_descriptors.push_back(stream_descriptor); + + return stream_descriptors; + } + + protected: + base::FilePath test_directory_; +}; + +TEST_F(PackagerTest, Success) { + ShakaPackager packager; + ASSERT_OK( + packager.Initialize(SetupPackagingParams(), SetupStreamDescriptors())); + ASSERT_OK(packager.Run()); +} + +TEST_F(PackagerTest, MissingStreamDescriptors) { + std::vector stream_descriptors; + ShakaPackager packager; + auto status = packager.Initialize(SetupPackagingParams(), stream_descriptors); + ASSERT_EQ(media::error::INVALID_ARGUMENT, status.error_code()); +} + +TEST_F(PackagerTest, MixingSegmentTemplateAndSingleSegment) { + std::vector stream_descriptors; + StreamDescriptor stream_descriptor; + + stream_descriptor.input = + media::GetTestDataFilePath(kTestFile).AsUTF8Unsafe(); + stream_descriptor.stream_selector = "video"; + stream_descriptor.output = GetFullPath(kOutputVideo); + stream_descriptor.segment_template = GetFullPath(kOutputVideoTemplate); + stream_descriptors.push_back(stream_descriptor); + + stream_descriptor.input = + media::GetTestDataFilePath(kTestFile).AsUTF8Unsafe(); + stream_descriptor.stream_selector = "audio"; + stream_descriptor.output = GetFullPath(kOutputAudio); + stream_descriptor.segment_template.clear(); + stream_descriptors.push_back(stream_descriptor); + + ShakaPackager packager; + auto status = packager.Initialize(SetupPackagingParams(), stream_descriptors); + ASSERT_EQ(media::error::INVALID_ARGUMENT, status.error_code()); +} + +TEST_F(PackagerTest, SegmentAlignedAndSubsegmentNotAligned) { + auto packaging_params = SetupPackagingParams(); + packaging_params.chunking_params.segment_sap_aligned = true; + packaging_params.chunking_params.subsegment_sap_aligned = false; + ShakaPackager packager; + ASSERT_OK(packager.Initialize(packaging_params, SetupStreamDescriptors())); + ASSERT_OK(packager.Run()); +} + +TEST_F(PackagerTest, SegmentNotAlignedButSubsegmentAligned) { + auto packaging_params = SetupPackagingParams(); + packaging_params.chunking_params.segment_sap_aligned = false; + packaging_params.chunking_params.subsegment_sap_aligned = true; + ShakaPackager packager; + auto status = packager.Initialize(packaging_params, SetupStreamDescriptors()); + ASSERT_EQ(media::error::INVALID_ARGUMENT, status.error_code()); +} + +// TODO(kqyang): Add more tests. + +} // namespace shaka