// Copyright 2016 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 "packager/hls/base/simple_hls_notifier.h" #include #include #include #include #include #include #include #include "packager/file/file_util.h" #include "packager/media/base/protection_system_ids.h" #include "packager/media/base/protection_system_specific_info.h" #include "packager/media/base/proto_json_util.h" #include "packager/media/base/widevine_pssh_data.pb.h" ABSL_FLAG(bool, enable_legacy_widevine_hls_signaling, false, "Specifies whether Legacy Widevine HLS, i.e. v1 is signalled in " "the media playlist. Applies to Widevine protection system in HLS " "with SAMPLE-AES only."); namespace shaka { namespace hls { namespace { const char kUriBase64Prefix[] = "data:text/plain;base64,"; const char kUriFairPlayPrefix[] = "skd://"; const char kWidevineDashIfIopUUID[] = "urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed"; bool IsWidevineSystemId(const std::vector& system_id) { return system_id.size() == std::size(media::kWidevineSystemId) && std::equal(system_id.begin(), system_id.end(), media::kWidevineSystemId); } bool IsCommonSystemId(const std::vector& system_id) { return system_id.size() == std::size(media::kCommonSystemId) && std::equal(system_id.begin(), system_id.end(), media::kCommonSystemId); } bool IsFairPlaySystemId(const std::vector& system_id) { return system_id.size() == std::size(media::kFairPlaySystemId) && std::equal(system_id.begin(), system_id.end(), media::kFairPlaySystemId); } std::string Base64EncodeData(const std::string& prefix, const std::string& data) { std::string data_base64; absl::Base64Escape(data, &data_base64); return prefix + data_base64; } std::string VectorToString(const std::vector& v) { return std::string(v.begin(), v.end()); } // Segment URL is relative to either output directory or the directory // containing the media playlist depends on whether base_url is set. std::string GenerateSegmentUrl(const std::string& segment_name, const std::string& base_url, const std::string& output_dir, const std::string& playlist_file_name) { auto output_path = std::filesystem::u8path(output_dir); if (!base_url.empty()) { // Media segment URL is base_url + segment path relative to output // directory. return base_url + MakePathRelative(segment_name, output_path); } // Media segment URL is segment path relative to the directory containing the // playlist. const std::filesystem::path playlist_dir = (output_path / playlist_file_name).parent_path() / ""; return MakePathRelative(segment_name, playlist_dir); } MediaInfo MakeMediaInfoPathsRelativeToPlaylist( const MediaInfo& media_info, const std::string& base_url, const std::string& output_dir, const std::string& playlist_name) { MediaInfo media_info_copy = media_info; if (media_info_copy.has_init_segment_name()) { media_info_copy.set_init_segment_url( GenerateSegmentUrl(media_info_copy.init_segment_name(), base_url, output_dir, playlist_name)); } if (media_info_copy.has_media_file_name()) { media_info_copy.set_media_file_url( GenerateSegmentUrl(media_info_copy.media_file_name(), base_url, output_dir, playlist_name)); } if (media_info_copy.has_segment_template()) { media_info_copy.set_segment_template_url( GenerateSegmentUrl(media_info_copy.segment_template(), base_url, output_dir, playlist_name)); } return media_info_copy; } bool WidevinePsshToJson(const std::vector& pssh_box, const std::vector& key_id, std::string* pssh_json) { std::unique_ptr pssh_builder = media::PsshBoxBuilder::ParseFromBox(pssh_box.data(), pssh_box.size()); if (!pssh_builder) { LOG(ERROR) << "Failed to parse PSSH box."; return false; } media::WidevinePsshData pssh_proto; if (!pssh_proto.ParseFromArray(pssh_builder->pssh_data().data(), pssh_builder->pssh_data().size())) { LOG(ERROR) << "Failed to parse protection_system_specific_data."; return false; } media::WidevineHeader widevine_header; if (pssh_proto.has_provider()) { widevine_header.set_provider(pssh_proto.provider()); } else { LOG(WARNING) << "Missing provider in Widevine PSSH. The content may not " "play in some devices."; } if (pssh_proto.has_content_id()) { widevine_header.set_content_id(pssh_proto.content_id()); } else { LOG(WARNING) << "Missing content_id in Widevine PSSH. The content may not " "play in some devices."; } // Place the current |key_id| to the front and converts all key_id to hex // format. widevine_header.add_key_ids(absl::BytesToHexString(absl::string_view( reinterpret_cast(key_id.data()), key_id.size()))); for (const std::string& key_id_in_pssh : pssh_proto.key_id()) { const std::string key_id_hex = absl::BytesToHexString( absl::string_view(reinterpret_cast(key_id_in_pssh.data()), key_id_in_pssh.size())); if (widevine_header.key_ids(0) != key_id_hex) widevine_header.add_key_ids(key_id_hex); } *pssh_json = media::MessageToJsonString(widevine_header); return true; } std::optional StringToEncryptionMethod( const std::string& method) { if (method == "cenc") { return MediaPlaylist::EncryptionMethod::kSampleAesCenc; } if (method == "cbcs") { return MediaPlaylist::EncryptionMethod::kSampleAes; } if (method == "cbca") { // cbca is a place holder for sample aes. return MediaPlaylist::EncryptionMethod::kSampleAes; } return std::nullopt; } void NotifyEncryptionToMediaPlaylist( MediaPlaylist::EncryptionMethod encryption_method, const std::string& uri, const std::vector& key_id, const std::vector& iv, const std::string& key_format, const std::string& key_format_version, MediaPlaylist* media_playlist) { std::string iv_string; if (!iv.empty()) { iv_string = "0x" + absl::BytesToHexString(absl::string_view( reinterpret_cast(iv.data()), iv.size())); } std::string key_id_string; if (!key_id.empty()) { key_id_string = "0x" + absl::BytesToHexString(absl::string_view( reinterpret_cast(key_id.data()), key_id.size())); } media_playlist->AddEncryptionInfo( encryption_method, uri, key_id_string, iv_string, key_format, key_format_version); } // Creates JSON format and the format similar to MPD. bool HandleWidevineKeyFormats( MediaPlaylist::EncryptionMethod encryption_method, const std::vector& key_id, const std::vector& iv, const std::vector& protection_system_specific_data, MediaPlaylist* media_playlist) { if (absl::GetFlag(FLAGS_enable_legacy_widevine_hls_signaling) && encryption_method == MediaPlaylist::EncryptionMethod::kSampleAes) { // This format allows SAMPLE-AES only. std::string key_uri_data; if (!WidevinePsshToJson(protection_system_specific_data, key_id, &key_uri_data)) { return false; } std::string key_uri_data_base64 = Base64EncodeData(kUriBase64Prefix, key_uri_data); NotifyEncryptionToMediaPlaylist(encryption_method, key_uri_data_base64, std::vector(), iv, "com.widevine", "1", media_playlist); } std::string pssh_as_string( reinterpret_cast(protection_system_specific_data.data()), protection_system_specific_data.size()); std::string key_uri_data_base64 = Base64EncodeData(kUriBase64Prefix, pssh_as_string); NotifyEncryptionToMediaPlaylist(encryption_method, key_uri_data_base64, key_id, iv, kWidevineDashIfIopUUID, "1", media_playlist); return true; } bool WriteMediaPlaylist(const std::string& output_dir, MediaPlaylist* playlist) { auto file_path = std::filesystem::u8path(output_dir) / playlist->file_name(); if (!playlist->WriteToFile(file_path)) { LOG(ERROR) << "Failed to write playlist " << file_path.string(); return false; } return true; } } // namespace MediaPlaylistFactory::~MediaPlaylistFactory() {} std::unique_ptr MediaPlaylistFactory::Create( const HlsParams& hls_params, const std::string& file_name, const std::string& name, const std::string& group_id) { return std::unique_ptr( new MediaPlaylist(hls_params, file_name, name, group_id)); } SimpleHlsNotifier::SimpleHlsNotifier(const HlsParams& hls_params) : HlsNotifier(hls_params), media_playlist_factory_(new MediaPlaylistFactory()) { const auto master_playlist_path = std::filesystem::u8path(hls_params.master_playlist_output); master_playlist_dir_ = master_playlist_path.parent_path().string(); const std::string& default_audio_langauge = hls_params.default_language; const std::string& default_text_language = hls_params.default_text_language.empty() ? hls_params.default_language : hls_params.default_text_language; master_playlist_.reset(new MasterPlaylist( master_playlist_path.filename(), default_audio_langauge, default_text_language, hls_params.is_independent_segments)); } SimpleHlsNotifier::~SimpleHlsNotifier() {} bool SimpleHlsNotifier::Init() { return true; } bool SimpleHlsNotifier::NotifyNewStream(const MediaInfo& media_info, const std::string& playlist_name, const std::string& name, const std::string& group_id, uint32_t* stream_id) { DCHECK(stream_id); const std::string relative_playlist_path = MakePathRelative( playlist_name, std::filesystem::u8path(master_playlist_dir_)); std::unique_ptr media_playlist = media_playlist_factory_->Create(hls_params(), relative_playlist_path, name, group_id); MediaInfo adjusted_media_info = MakeMediaInfoPathsRelativeToPlaylist( media_info, hls_params().base_url, master_playlist_dir_, media_playlist->file_name()); if (!media_playlist->SetMediaInfo(adjusted_media_info)) { LOG(ERROR) << "Failed to set media info for playlist " << playlist_name; return false; } MediaPlaylist::EncryptionMethod encryption_method = MediaPlaylist::EncryptionMethod::kNone; if (media_info.protected_content().has_protection_scheme()) { const std::string& protection_scheme = media_info.protected_content().protection_scheme(); std::optional enc_method = StringToEncryptionMethod(protection_scheme); if (!enc_method) { LOG(ERROR) << "Failed to recognize protection scheme " << protection_scheme; return false; } encryption_method = enc_method.value(); } absl::MutexLock lock(&lock_); *stream_id = sequence_number_++; media_playlists_.push_back(media_playlist.get()); stream_map_[*stream_id].reset( new StreamEntry{std::move(media_playlist), encryption_method}); return true; } bool SimpleHlsNotifier::NotifySampleDuration(uint32_t stream_id, int32_t sample_duration) { absl::MutexLock lock(&lock_); auto stream_iterator = stream_map_.find(stream_id); if (stream_iterator == stream_map_.end()) { LOG(ERROR) << "Cannot find stream with ID: " << stream_id; return false; } auto& media_playlist = stream_iterator->second->media_playlist; media_playlist->SetSampleDuration(sample_duration); return true; } bool SimpleHlsNotifier::NotifyNewSegment(uint32_t stream_id, const std::string& segment_name, int64_t start_time, int64_t duration, uint64_t start_byte_offset, uint64_t size) { absl::MutexLock lock(&lock_); auto stream_iterator = stream_map_.find(stream_id); if (stream_iterator == stream_map_.end()) { LOG(ERROR) << "Cannot find stream with ID: " << stream_id; return false; } auto& media_playlist = stream_iterator->second->media_playlist; const std::string& segment_url = GenerateSegmentUrl(segment_name, hls_params().base_url, master_playlist_dir_, media_playlist->file_name()); media_playlist->AddSegment(segment_url, start_time, duration, start_byte_offset, size); // Update target duration. int32_t longest_segment_duration = static_cast(ceil(media_playlist->GetLongestSegmentDuration())); bool target_duration_updated = false; if (longest_segment_duration > target_duration_) { target_duration_ = longest_segment_duration; target_duration_updated = true; } // Update the playlists when there is new segments in live mode. if (hls_params().playlist_type == HlsPlaylistType::kLive || hls_params().playlist_type == HlsPlaylistType::kEvent) { // Update all playlists if target duration is updated. if (target_duration_updated) { for (MediaPlaylist* playlist : media_playlists_) { playlist->SetTargetDuration(target_duration_); if (!WriteMediaPlaylist(master_playlist_dir_, playlist)) return false; } } else { if (!WriteMediaPlaylist(master_playlist_dir_, media_playlist.get())) return false; } if (!master_playlist_->WriteMasterPlaylist( hls_params().base_url, master_playlist_dir_, media_playlists_)) { LOG(ERROR) << "Failed to write master playlist."; return false; } } return true; } bool SimpleHlsNotifier::NotifyKeyFrame(uint32_t stream_id, int64_t timestamp, uint64_t start_byte_offset, uint64_t size) { absl::MutexLock lock(&lock_); auto stream_iterator = stream_map_.find(stream_id); if (stream_iterator == stream_map_.end()) { LOG(ERROR) << "Cannot find stream with ID: " << stream_id; return false; } auto& media_playlist = stream_iterator->second->media_playlist; media_playlist->AddKeyFrame(timestamp, start_byte_offset, size); return true; } bool SimpleHlsNotifier::NotifyCueEvent(uint32_t stream_id, int64_t timestamp) { absl::MutexLock lock(&lock_); auto stream_iterator = stream_map_.find(stream_id); if (stream_iterator == stream_map_.end()) { LOG(ERROR) << "Cannot find stream with ID: " << stream_id; return false; } auto& media_playlist = stream_iterator->second->media_playlist; media_playlist->AddPlacementOpportunity(); return true; } bool SimpleHlsNotifier::NotifyEncryptionUpdate( uint32_t stream_id, const std::vector& key_id, const std::vector& system_id, const std::vector& iv, const std::vector& protection_system_specific_data) { absl::MutexLock lock(&lock_); auto stream_iterator = stream_map_.find(stream_id); if (stream_iterator == stream_map_.end()) { LOG(ERROR) << "Cannot find stream with ID: " << stream_id; return false; } std::unique_ptr& media_playlist = stream_iterator->second->media_playlist; const MediaPlaylist::EncryptionMethod encryption_method = stream_iterator->second->encryption_method; LOG_IF(WARNING, encryption_method == MediaPlaylist::EncryptionMethod::kNone) << "Got encryption notification but the encryption method is NONE"; if (IsWidevineSystemId(system_id)) { return HandleWidevineKeyFormats(encryption_method, key_id, iv, protection_system_specific_data, media_playlist.get()); } // Key Id does not need to be specified with "identity" and "sdk". const std::vector empty_key_id; if (IsCommonSystemId(system_id)) { std::string key_uri = hls_params().key_uri; if (key_uri.empty()) { // Use key_id as the key_uri. The player needs to have custom logic to // convert it to the actual key uri. std::string key_uri_data = VectorToString(key_id); key_uri = Base64EncodeData(kUriBase64Prefix, key_uri_data); } NotifyEncryptionToMediaPlaylist(encryption_method, key_uri, empty_key_id, iv, "identity", "", media_playlist.get()); return true; } if (IsFairPlaySystemId(system_id)) { std::string key_uri = hls_params().key_uri; if (key_uri.empty()) { // Use key_id as the key_uri. The player needs to have custom logic to // convert it to the actual key uri. std::string key_uri_data = VectorToString(key_id); key_uri = Base64EncodeData(kUriFairPlayPrefix, key_uri_data); } // FairPlay defines IV to be carried with the key, not the playlist. const std::vector empty_iv; NotifyEncryptionToMediaPlaylist(encryption_method, key_uri, empty_key_id, empty_iv, "com.apple.streamingkeydelivery", "1", media_playlist.get()); return true; } LOG(WARNING) << "HLS: Ignore unknown or unsupported system ID: " << absl::BytesToHexString(absl::string_view( reinterpret_cast(system_id.data()), system_id.size())); return true; } bool SimpleHlsNotifier::Flush() { absl::MutexLock lock(&lock_); for (MediaPlaylist* playlist : media_playlists_) { playlist->SetTargetDuration(target_duration_); if (!WriteMediaPlaylist(master_playlist_dir_, playlist)) return false; } if (!master_playlist_->WriteMasterPlaylist( hls_params().base_url, master_playlist_dir_, media_playlists_)) { LOG(ERROR) << "Failed to write master playlist."; return false; } return true; } } // namespace hls } // namespace shaka