// Copyright 2014 Google LLC. 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 #if defined(OS_WIN) #include #include #endif // defined(OS_WIN) #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include ABSL_FLAG(bool, dump_stream_info, false, "Dump demuxed stream info."); ABSL_FLAG(bool, licenses, false, "Dump licenses."); ABSL_FLAG(bool, quiet, false, "When enabled, LOG(INFO) output is suppressed."); ABSL_FLAG(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."); ABSL_FLAG(std::string, test_packager_version, "", "Packager version for testing. Should be used for testing only."); ABSL_FLAG(bool, single_threaded, false, "If enabled, only use one thread when generating content."); namespace shaka { namespace { const char kUsage[] = "%s [flags] ...\n\n" " stream_descriptor consists of comma separated field_name/value pairs:\n" " field_name=value,[field_name=value,]...\n" " Supported field names are as follows (names in parenthesis are alias):\n" " - input (in): Required input/source media file path or network stream\n" " URL.\n" " - stream_selector (stream): Required field with value 'audio',\n" " 'video', 'text', or stream number (zero based).\n" " - output (out,init_segment): Required output file (single file) or\n" " initialization file path (multiple file).\n" " - segment_template (segment): Optional value which specifies the\n" " naming pattern for the segment files, and that the stream should be\n" " split into multiple files. Its presence should be consistent across\n" " streams.\n" " - bandwidth (bw): Optional value which contains a user-specified\n" " maximum bit rate for the stream, in bits/sec. If specified, this\n" " value is propagated to (HLS) EXT-X-STREAM-INF:BANDWIDTH or (DASH)\n" " Representation@bandwidth and the $Bandwidth$ template parameter for\n" " segment names. If not specified, the bandwidth value is estimated\n" " from content bitrate. Note that it only affects the generated\n" " manifests/playlists; it has no effect on the media content itself.\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 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" " - drm_label: Optional value for custom DRM label, which defines the\n" " encryption key applied to the stream. Typical values include AUDIO,\n" " SD, HD, UHD1, UHD2. For raw key, it should be a label defined in\n" " --keys. If not provided, the DRM label is derived from stream type\n" " (video, audio), resolution, etc.\n" " Note that it is case sensitive.\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" " - hls_name: Used for HLS audio to set the NAME attribute for\n" " EXT-X-MEDIA. Defaults to the base of the playlist name.\n" " - hls_group_id: Used for HLS audio to set the GROUP-ID attribute for\n" " EXT-X-MEDIA. Defaults to 'audio' if not specified.\n" " - playlist_name: The HLS playlist file to create. Usually ends with\n" " '.m3u8', and is relative to --hls_master_playlist_output. If\n" " unspecified, defaults to something of the form 'stream_0.m3u8',\n" " 'stream_1.m3u8', 'stream_2.m3u8', etc.\n" " - iframe_playlist_name: The optional HLS I-Frames only playlist file\n" " to create. Usually ends with '.m3u8', and is relative to\n" " hls_master_playlist_output. Should only be set for video streams. If\n" " unspecified, no I-Frames only playlist is created.\n" " - hls_characteristics (charcs): Optional colon/semicolon separated\n" " list of values for the CHARACTERISTICS attribute for EXT-X-MEDIA.\n" " See CHARACTERISTICS attribute in http://bit.ly/2OOUkdB for details.\n" " - dash_accessibilities (accessibilities): Optional semicolon separated\n" " list of values for DASH Accessibility elements. The value should be\n" " in the format: scheme_id_uri=value.\n" " - dash_roles (roles): Optional semicolon separated list of values for\n" " DASH Role elements. The value should be one of: caption, subtitle,\n" " main, alternate, supplementary, commentary, description and dub. See\n" " DASH (ISO/IEC 23009-1) specification for details.\n"; // Labels for parameters in RawKey key info. const char kDrmLabelLabel[] = "label"; const char kKeyIdLabel[] = "key_id"; const char kKeyLabel[] = "key"; const char kKeyIvLabel[] = "iv"; enum ExitStatus { kSuccess = 0, kArgumentValidationFailed, kPackagingFailed, kInternalError, }; bool GetWidevineSigner(WidevineSigner* signer) { signer->signer_name = absl::GetFlag(FLAGS_signer); if (!absl::GetFlag(FLAGS_aes_signing_key).bytes.empty()) { signer->signing_key_type = WidevineSigner::SigningKeyType::kAes; signer->aes.key = absl::GetFlag(FLAGS_aes_signing_key).bytes; signer->aes.iv = absl::GetFlag(FLAGS_aes_signing_iv).bytes; } else if (!absl::GetFlag(FLAGS_rsa_signing_key_path).empty()) { signer->signing_key_type = WidevineSigner::SigningKeyType::kRsa; if (!File::ReadFileToString( absl::GetFlag(FLAGS_rsa_signing_key_path).c_str(), &signer->rsa.key)) { LOG(ERROR) << "Failed to read from '" << absl::GetFlag(FLAGS_rsa_signing_key_path) << "'."; return false; } } return true; } bool GetHlsPlaylistType(const std::string& playlist_type, HlsPlaylistType* playlist_type_enum) { if (absl::AsciiStrToUpper(playlist_type) == "VOD") { *playlist_type_enum = HlsPlaylistType::kVod; } else if (absl::AsciiStrToUpper(playlist_type) == "LIVE") { *playlist_type_enum = HlsPlaylistType::kLive; } else if (absl::AsciiStrToUpper(playlist_type) == "EVENT") { *playlist_type_enum = HlsPlaylistType::kEvent; } else { LOG(ERROR) << "Unrecognized playlist type " << playlist_type; return false; } return true; } bool GetProtectionScheme(uint32_t* protection_scheme) { if (absl::GetFlag(FLAGS_protection_scheme) == "cenc") { *protection_scheme = EncryptionParams::kProtectionSchemeCenc; return true; } if (absl::GetFlag(FLAGS_protection_scheme) == "cbc1") { *protection_scheme = EncryptionParams::kProtectionSchemeCbc1; return true; } if (absl::GetFlag(FLAGS_protection_scheme) == "cbcs") { *protection_scheme = EncryptionParams::kProtectionSchemeCbcs; return true; } if (absl::GetFlag(FLAGS_protection_scheme) == "cens") { *protection_scheme = EncryptionParams::kProtectionSchemeCens; return true; } LOG(ERROR) << "Unrecognized protection_scheme " << absl::GetFlag(FLAGS_protection_scheme); return false; } bool ParseKeys(const std::string& keys, RawKeyParams* raw_key) { std::vector keys_data = SplitAndTrimSkipEmpty(keys, ','); for (const std::string& key_data : keys_data) { std::vector string_pairs = SplitStringIntoKeyValuePairs(key_data, '=', ':'); std::map value_map; for (const auto& string_pair : string_pairs) value_map[string_pair.first] = string_pair.second; const std::string drm_label = value_map[kDrmLabelLabel]; if (raw_key->key_map.find(drm_label) != raw_key->key_map.end()) { LOG(ERROR) << "Seeing duplicated DRM label '" << drm_label << "'."; return false; } auto& key_info = raw_key->key_map[drm_label]; if (value_map[kKeyIdLabel].empty() || !shaka::ValidHexStringToBytes(value_map[kKeyIdLabel], &key_info.key_id)) { LOG(ERROR) << "Empty key id or invalid hex string for key id: " << value_map[kKeyIdLabel]; return false; } if (value_map[kKeyLabel].empty() || !shaka::ValidHexStringToBytes(value_map[kKeyLabel], &key_info.key)) { LOG(ERROR) << "Empty key or invalid hex string for key: " << value_map[kKeyLabel]; return false; } if (!value_map[kKeyIvLabel].empty()) { if (!raw_key->iv.empty()) { LOG(ERROR) << "IV already specified with --iv"; return false; } if (!shaka::ValidHexStringToBytes(value_map[kKeyIvLabel], &key_info.iv)) { LOG(ERROR) << "Empty IV or invalid hex string for IV: " << value_map[kKeyIvLabel]; return false; } } } return true; } bool GetRawKeyParams(RawKeyParams* raw_key) { raw_key->iv = absl::GetFlag(FLAGS_iv).bytes; raw_key->pssh = absl::GetFlag(FLAGS_pssh).bytes; if (!absl::GetFlag(FLAGS_keys).empty()) { if (!ParseKeys(absl::GetFlag(FLAGS_keys), raw_key)) { LOG(ERROR) << "Failed to parse --keys " << absl::GetFlag(FLAGS_keys); return false; } } else { // An empty StreamLabel specifies the default key info. RawKeyParams::KeyInfo& key_info = raw_key->key_map[""]; key_info.key_id = absl::GetFlag(FLAGS_key_id).bytes; key_info.key = absl::GetFlag(FLAGS_key).bytes; } return true; } bool ParseAdCues(const std::string& ad_cues, std::vector* cuepoints) { // Track if optional field is supplied consistently across all cue points. size_t duration_count = 0; std::vector ad_cues_vec = SplitAndTrimSkipEmpty(ad_cues, ';'); for (const std::string& ad_cue : ad_cues_vec) { Cuepoint cuepoint; std::vector split_ad_cue = SplitAndTrimSkipEmpty(ad_cue, ','); if (split_ad_cue.size() > 2) { LOG(ERROR) << "Failed to parse --ad_cues " << ad_cues << " Each ad cue must contain no more than 2 components."; } if (!absl::SimpleAtod(split_ad_cue.front(), &cuepoint.start_time_in_seconds)) { LOG(ERROR) << "Failed to parse --ad_cues " << ad_cues << " Start time component must be of type double."; return false; } if (split_ad_cue.size() > 1) { duration_count++; if (!absl::SimpleAtod(split_ad_cue[1], &cuepoint.duration_in_seconds)) { LOG(ERROR) << "Failed to parse --ad_cues " << ad_cues << " Duration component must be of type double."; return false; } } cuepoints->push_back(cuepoint); } if (duration_count > 0 && duration_count != cuepoints->size()) { LOG(ERROR) << "Failed to parse --ad_cues " << ad_cues << " Duration component is optional. However if it is supplied," << " it must be supplied consistently across all cuepoints."; return false; } return true; } bool ParseProtectionSystems(const std::string& protection_systems_str, ProtectionSystem* protection_systems) { *protection_systems = ProtectionSystem::kNone; std::map mapping = { {"common", ProtectionSystem::kCommon}, {"commonsystem", ProtectionSystem::kCommon}, {"fairplay", ProtectionSystem::kFairPlay}, {"marlin", ProtectionSystem::kMarlin}, {"playready", ProtectionSystem::kPlayReady}, {"widevine", ProtectionSystem::kWidevine}, }; std::vector protection_systems_vec = SplitAndTrimSkipEmpty(absl::AsciiStrToLower(protection_systems_str), ','); for (const std::string& protection_system : protection_systems_vec) { auto iter = mapping.find(protection_system); if (iter == mapping.end()) { LOG(ERROR) << "Seeing unrecognized protection system: " << protection_system; return false; } *protection_systems |= iter->second; } return true; } std::optional GetPackagingParams() { PackagingParams packaging_params; packaging_params.temp_dir = absl::GetFlag(FLAGS_temp_dir); packaging_params.single_threaded = absl::GetFlag(FLAGS_single_threaded); AdCueGeneratorParams& ad_cue_generator_params = packaging_params.ad_cue_generator_params; if (!ParseAdCues(absl::GetFlag(FLAGS_ad_cues), &ad_cue_generator_params.cue_points)) { return std::nullopt; } ChunkingParams& chunking_params = packaging_params.chunking_params; chunking_params.segment_duration_in_seconds = absl::GetFlag(FLAGS_segment_duration); chunking_params.subsegment_duration_in_seconds = absl::GetFlag(FLAGS_fragment_duration); chunking_params.low_latency_dash_mode = absl::GetFlag(FLAGS_low_latency_dash_mode); chunking_params.segment_sap_aligned = absl::GetFlag(FLAGS_segment_sap_aligned); chunking_params.subsegment_sap_aligned = absl::GetFlag(FLAGS_fragment_sap_aligned); int num_key_providers = 0; EncryptionParams& encryption_params = packaging_params.encryption_params; if (absl::GetFlag(FLAGS_enable_widevine_encryption)) { encryption_params.key_provider = KeyProvider::kWidevine; ++num_key_providers; } if (absl::GetFlag(FLAGS_enable_playready_encryption)) { encryption_params.key_provider = KeyProvider::kPlayReady; ++num_key_providers; } if (absl::GetFlag(FLAGS_enable_raw_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_raw_key_encryption can be enabled."; return std::nullopt; } if (!ParseProtectionSystems(absl::GetFlag(FLAGS_protection_systems), &encryption_params.protection_systems)) { return std::nullopt; } if (encryption_params.key_provider != KeyProvider::kNone) { encryption_params.clear_lead_in_seconds = absl::GetFlag(FLAGS_clear_lead); if (!GetProtectionScheme(&encryption_params.protection_scheme)) return std::nullopt; encryption_params.crypt_byte_block = absl::GetFlag(FLAGS_crypt_byte_block); encryption_params.skip_byte_block = absl::GetFlag(FLAGS_skip_byte_block); encryption_params.crypto_period_duration_in_seconds = absl::GetFlag(FLAGS_crypto_period_duration); encryption_params.vp9_subsample_encryption = absl::GetFlag(FLAGS_vp9_subsample_encryption); encryption_params.stream_label_func = std::bind( &Packager::DefaultStreamLabelFunction, absl::GetFlag(FLAGS_max_sd_pixels), absl::GetFlag(FLAGS_max_hd_pixels), absl::GetFlag(FLAGS_max_uhd1_pixels), std::placeholders::_1); encryption_params.playready_extra_header_data = absl::GetFlag(FLAGS_playready_extra_header_data); } switch (encryption_params.key_provider) { case KeyProvider::kWidevine: { WidevineEncryptionParams& widevine = encryption_params.widevine; widevine.key_server_url = absl::GetFlag(FLAGS_key_server_url); widevine.content_id = absl::GetFlag(FLAGS_content_id).bytes; widevine.policy = absl::GetFlag(FLAGS_policy); widevine.group_id = absl::GetFlag(FLAGS_group_id).bytes; widevine.enable_entitlement_license = absl::GetFlag(FLAGS_enable_entitlement_license); if (!GetWidevineSigner(&widevine.signer)) return std::nullopt; break; } case KeyProvider::kPlayReady: { PlayReadyEncryptionParams& playready = encryption_params.playready; playready.key_server_url = absl::GetFlag(FLAGS_playready_server_url); playready.program_identifier = absl::GetFlag(FLAGS_program_identifier); break; } case KeyProvider::kRawKey: { if (!GetRawKeyParams(&encryption_params.raw_key)) return std::nullopt; break; } case KeyProvider::kNone: break; } num_key_providers = 0; DecryptionParams& decryption_params = packaging_params.decryption_params; if (absl::GetFlag(FLAGS_enable_widevine_decryption)) { decryption_params.key_provider = KeyProvider::kWidevine; ++num_key_providers; } if (absl::GetFlag(FLAGS_enable_raw_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_raw_key_decryption can be enabled."; return std::nullopt; } switch (decryption_params.key_provider) { case KeyProvider::kWidevine: { WidevineDecryptionParams& widevine = decryption_params.widevine; widevine.key_server_url = absl::GetFlag(FLAGS_key_server_url); if (!GetWidevineSigner(&widevine.signer)) return std::nullopt; break; } case KeyProvider::kRawKey: { if (!GetRawKeyParams(&decryption_params.raw_key)) return std::nullopt; break; } case KeyProvider::kPlayReady: case KeyProvider::kNone: break; } Mp4OutputParams& mp4_params = packaging_params.mp4_output_params; mp4_params.generate_sidx_in_media_segments = absl::GetFlag(FLAGS_generate_sidx_in_media_segments); mp4_params.include_pssh_in_stream = absl::GetFlag(FLAGS_mp4_include_pssh_in_stream); mp4_params.low_latency_dash_mode = absl::GetFlag(FLAGS_low_latency_dash_mode); packaging_params.transport_stream_timestamp_offset_ms = absl::GetFlag(FLAGS_transport_stream_timestamp_offset_ms); packaging_params.default_text_zero_bias_ms = absl::GetFlag(FLAGS_default_text_zero_bias_ms); packaging_params.output_media_info = absl::GetFlag(FLAGS_output_media_info); MpdParams& mpd_params = packaging_params.mpd_params; mpd_params.mpd_output = absl::GetFlag(FLAGS_mpd_output); std::vector base_urls = SplitAndTrimSkipEmpty(absl::GetFlag(FLAGS_base_urls), ','); mpd_params.base_urls = base_urls; mpd_params.min_buffer_time = absl::GetFlag(FLAGS_min_buffer_time); mpd_params.minimum_update_period = absl::GetFlag(FLAGS_minimum_update_period); mpd_params.suggested_presentation_delay = absl::GetFlag(FLAGS_suggested_presentation_delay); mpd_params.time_shift_buffer_depth = absl::GetFlag(FLAGS_time_shift_buffer_depth); mpd_params.preserved_segments_outside_live_window = absl::GetFlag(FLAGS_preserved_segments_outside_live_window); mpd_params.use_segment_list = absl::GetFlag(FLAGS_dash_force_segment_list); if (!absl::GetFlag(FLAGS_utc_timings).empty()) { std::vector pairs = SplitStringIntoKeyValuePairs( absl::GetFlag(FLAGS_utc_timings), '=', ','); if (pairs.empty()) { LOG(ERROR) << "Invalid --utc_timings scheme_id_uri/value pairs."; return std::nullopt; } for (const auto& string_pair : pairs) { mpd_params.utc_timings.push_back({string_pair.first, string_pair.second}); } } mpd_params.default_language = absl::GetFlag(FLAGS_default_language); mpd_params.default_text_language = absl::GetFlag(FLAGS_default_text_language); mpd_params.generate_static_live_mpd = absl::GetFlag(FLAGS_generate_static_live_mpd); mpd_params.generate_dash_if_iop_compliant_mpd = absl::GetFlag(FLAGS_generate_dash_if_iop_compliant_mpd); mpd_params.allow_approximate_segment_timeline = absl::GetFlag(FLAGS_allow_approximate_segment_timeline); mpd_params.allow_codec_switching = absl::GetFlag(FLAGS_allow_codec_switching); mpd_params.include_mspr_pro = absl::GetFlag(FLAGS_include_mspr_pro_for_playready); mpd_params.low_latency_dash_mode = absl::GetFlag(FLAGS_low_latency_dash_mode); HlsParams& hls_params = packaging_params.hls_params; if (!GetHlsPlaylistType(absl::GetFlag(FLAGS_hls_playlist_type), &hls_params.playlist_type)) { return std::nullopt; } hls_params.master_playlist_output = absl::GetFlag(FLAGS_hls_master_playlist_output); hls_params.base_url = absl::GetFlag(FLAGS_hls_base_url); hls_params.key_uri = absl::GetFlag(FLAGS_hls_key_uri); hls_params.time_shift_buffer_depth = absl::GetFlag(FLAGS_time_shift_buffer_depth); hls_params.preserved_segments_outside_live_window = absl::GetFlag(FLAGS_preserved_segments_outside_live_window); hls_params.default_language = absl::GetFlag(FLAGS_default_language); hls_params.default_text_language = absl::GetFlag(FLAGS_default_text_language); hls_params.media_sequence_number = absl::GetFlag(FLAGS_hls_media_sequence_number); TestParams& test_params = packaging_params.test_params; test_params.dump_stream_info = absl::GetFlag(FLAGS_dump_stream_info); test_params.inject_fake_clock = absl::GetFlag(FLAGS_use_fake_clock_for_muxer); if (!absl::GetFlag(FLAGS_test_packager_version).empty()) test_params.injected_library_version = absl::GetFlag(FLAGS_test_packager_version); return packaging_params; } int PackagerMain(int argc, char** argv) { absl::FlagsUsageConfig flag_config; flag_config.version_string = []() -> std::string { return "packager version " + shaka::Packager::GetLibraryVersion() + "\n"; }; flag_config.contains_help_flags = [](absl::string_view flag_file_name) -> bool { return true; }; absl::SetFlagsUsageConfig(flag_config); auto usage = absl::StrFormat(kUsage, argv[0]); absl::SetProgramUsageMessage(usage); auto remaining_args = absl::ParseCommandLine(argc, argv); if (absl::GetFlag(FLAGS_licenses)) { for (const char* line : kLicenseNotice) std::cout << line << std::endl; return kSuccess; } if (remaining_args.size() < 2) { std::cerr << "Usage: " << absl::ProgramUsageMessage(); return kSuccess; } if (absl::GetFlag(FLAGS_quiet)) { absl::SetMinLogLevel(absl::LogSeverityAtLeast::kWarning); } absl::InitializeLog(); if (!ValidateWidevineCryptoFlags() || !ValidateRawKeyCryptoFlags() || !ValidatePRCryptoFlags() || !ValidateCryptoFlags() || !ValidateRetiredFlags()) { return kArgumentValidationFailed; } std::optional packaging_params = GetPackagingParams(); if (!packaging_params) return kArgumentValidationFailed; std::vector stream_descriptors; for (size_t i = 1; i < remaining_args.size(); ++i) { std::optional stream_descriptor = ParseStreamDescriptor(remaining_args[i]); if (!stream_descriptor) return kArgumentValidationFailed; stream_descriptors.push_back(stream_descriptor.value()); } Packager packager; 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; } if (!absl::GetFlag(FLAGS_quiet)) printf("Packaging completed successfully.\n"); return kSuccess; } } // namespace } // namespace shaka #if defined(OS_WIN) // Windows wmain, which converts wide character arguments to UTF-8. int wmain(int argc, wchar_t* argv[], wchar_t* envp[]) { std::unique_ptr> utf8_argv( new char*[argc], [argc](char** utf8_args) { // TODO(tinskip): This leaks, but if this code is enabled, it crashes. // Figure out why. I suspect gflags does something funny with the // argument array. // for (int idx = 0; idx < argc; ++idx) // delete[] utf8_args[idx]; delete[] utf8_args; }); std::wstring_convert> converter; for (int idx = 0; idx < argc; ++idx) { std::string utf8_arg(converter.to_bytes(argv[idx])); utf8_arg += '\0'; utf8_argv[idx] = new char[utf8_arg.size()]; memcpy(utf8_argv[idx], &utf8_arg[0], utf8_arg.size()); } // Because we just converted wide character args into UTF8, and because // std::filesystem::u8path is used to interpret all std::string paths as // UTF8, we should set the locale to UTF8 as well, for the transition point // to C library functions like fopen to work correctly with non-ASCII paths. std::setlocale(LC_ALL, ".UTF8"); return shaka::PackagerMain(argc, utf8_argv.get()); } #else int main(int argc, char** argv) { return shaka::PackagerMain(argc, argv); } #endif // defined(OS_WIN)