diff --git a/docs/source/options/hls_options.rst b/docs/source/options/hls_options.rst index 8199195d26..06e6e73e54 100644 --- a/docs/source/options/hls_options.rst +++ b/docs/source/options/hls_options.rst @@ -92,4 +92,9 @@ HLS options --force_cl_index True forces the muxer to order streams in the order given - on the command-line. False uses the previous unordered behavior. \ No newline at end of file + on the command-line. False uses the previous unordered behavior. + +--create_session_keys + + Playback of Offline HLS assets shall use EXT-X-SESSION-KEY to declare all + eligible content keys in the master playlist. \ No newline at end of file diff --git a/include/packager/hls_params.h b/include/packager/hls_params.h index e8c86aa906..fd2b182246 100644 --- a/include/packager/hls_params.h +++ b/include/packager/hls_params.h @@ -70,6 +70,8 @@ struct HlsParams { /// playlist. A negative number indicates a negative time offset from the end /// of the last media segment in the playlist. std::optional start_time_offset; + /// Create EXT-X-SESSION-KEY in master playlist + bool create_session_keys; }; } // namespace shaka diff --git a/packager/app/hls_flags.cc b/packager/app/hls_flags.cc index 653830ca4f..31bd51f9da 100644 --- a/packager/app/hls_flags.cc +++ b/packager/app/hls_flags.cc @@ -46,3 +46,8 @@ ABSL_FLAG(std::optional, "beginning of the playlist. A negative number indicates a " "negative time offset from the end of the last media segment " "in the playlist."); +ABSL_FLAG(bool, + create_session_keys, + false, + "Playback of Offline HLS assets shall use EXT-X-SESSION-KEY " + "to declare all eligible content keys in the master playlist."); diff --git a/packager/app/hls_flags.h b/packager/app/hls_flags.h index 5bb7ae754a..4d5af70dfe 100644 --- a/packager/app/hls_flags.h +++ b/packager/app/hls_flags.h @@ -16,5 +16,6 @@ ABSL_DECLARE_FLAG(std::string, hls_key_uri); ABSL_DECLARE_FLAG(std::string, hls_playlist_type); ABSL_DECLARE_FLAG(int32_t, hls_media_sequence_number); ABSL_DECLARE_FLAG(std::optional, hls_start_time_offset); +ABSL_DECLARE_FLAG(bool, create_session_keys); #endif // PACKAGER_APP_HLS_FLAGS_H_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 8e542aeef6..b7845bfad1 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -543,6 +543,7 @@ std::optional GetPackagingParams() { hls_params.media_sequence_number = absl::GetFlag(FLAGS_hls_media_sequence_number); hls_params.start_time_offset = absl::GetFlag(FLAGS_hls_start_time_offset); + hls_params.create_session_keys = absl::GetFlag(FLAGS_create_session_keys); TestParams& test_params = packaging_params.test_params; test_params.dump_stream_info = absl::GetFlag(FLAGS_dump_stream_info); diff --git a/packager/hls/base/master_playlist.cc b/packager/hls/base/master_playlist.cc index 8cc5eb714d..20fab442f8 100644 --- a/packager/hls/base/master_playlist.cc +++ b/packager/hls/base/master_playlist.cc @@ -550,11 +550,13 @@ void AppendPlaylists(const std::string& default_audio_language, MasterPlaylist::MasterPlaylist(const std::filesystem::path& file_name, const std::string& default_audio_language, const std::string& default_text_language, - bool is_independent_segments) + bool is_independent_segments, + bool create_session_keys) : file_name_(file_name), default_audio_language_(default_audio_language), default_text_language_(default_text_language), - is_independent_segments_(is_independent_segments) {} + is_independent_segments_(is_independent_segments), + create_session_keys_(create_session_keys) {} MasterPlaylist::~MasterPlaylist() {} @@ -568,6 +570,23 @@ bool MasterPlaylist::WriteMasterPlaylist( if (is_independent_segments_) { content.append("\n#EXT-X-INDEPENDENT-SEGMENTS\n"); } + + // Iterate over the playlists and add the session keys to the master playlist. + if (create_session_keys_) { + std::set session_keys; + for (const auto& playlist : playlists) { + for (const auto& entry : playlist->entries()) { + if (entry->type() == HlsEntry::EntryType::kExtKey) { + auto encryption_entry = dynamic_cast(entry.get()); + session_keys.emplace(encryption_entry->ToString("#EXT-X-SESSION-KEY")); + } + } + } + // session_keys will now contain all the unique session keys. + for (const auto& session_key : session_keys) + content.append(session_key + "\n"); + } + AppendPlaylists(default_audio_language_, default_text_language_, base_url, playlists, &content); diff --git a/packager/hls/base/master_playlist.h b/packager/hls/base/master_playlist.h index a6708514ba..77af37cea9 100644 --- a/packager/hls/base/master_playlist.h +++ b/packager/hls/base/master_playlist.h @@ -28,7 +28,8 @@ class MasterPlaylist { MasterPlaylist(const std::filesystem::path& file_name, const std::string& default_audio_language, const std::string& default_text_language, - const bool is_independent_segments); + const bool is_independent_segments, + const bool create_session_keys = false); virtual ~MasterPlaylist(); /// Writes Master Playlist to output_dir + . @@ -53,6 +54,7 @@ class MasterPlaylist { const std::string default_audio_language_; const std::string default_text_language_; bool is_independent_segments_; + bool create_session_keys_; }; } // namespace hls diff --git a/packager/hls/base/master_playlist_unittest.cc b/packager/hls/base/master_playlist_unittest.cc index 4294e05259..d2f7d99e04 100644 --- a/packager/hls/base/master_playlist_unittest.cc +++ b/packager/hls/base/master_playlist_unittest.cc @@ -40,6 +40,7 @@ const uint32_t kEC3JocComplexityZero = 0; const uint32_t kEC3JocComplexity = 16; const bool kAC4IMSFlagEnabled = true; const bool kAC4CBIFlagEnabled = true; +const bool kCreateSessionKeys = true; std::unique_ptr CreateVideoPlaylist( const std::string& filename, @@ -143,7 +144,8 @@ class MasterPlaylistTest : public ::testing::Test { : master_playlist_(new MasterPlaylist(kDefaultMasterPlaylistName, kDefaultAudioLanguage, kDefaultTextLanguage, - !kIsIndependentSegments)), + !kIsIndependentSegments, + kCreateSessionKeys)), test_output_dir_("memory://test_dir"), master_playlist_path_(std::filesystem::u8path(test_output_dir_) / kDefaultMasterPlaylistName) {} @@ -849,6 +851,55 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistAudioOnly) { ASSERT_EQ(expected, actual); } +TEST_F(MasterPlaylistTest, WriteMasterPlaylistWithEncryption) { + std::unique_ptr media_playlists[] = { + // VIDEO + CreateVideoPlaylist("video-1.m3u8", "sdvideocodec", 300000, 200000), + + // AUDIO + CreateAudioPlaylist("audio-1.m3u8", "audio 1", "audio-group-1", + "audiocodec", "en", 2, 50000, 30000, + kEC3JocComplexityZero, !kAC4IMSFlagEnabled, + !kAC4CBIFlagEnabled), + }; + + // Add all the media playlists to the master playlist. + std::list media_playlist_list; + for (const auto& media_playlist : media_playlists) { + media_playlist.get()->AddEncryptionInfoForTesting( + MediaPlaylist::EncryptionMethod::kSampleAes, "http://example.com", "", + "0x12345678", "com.widevine", "1/2/4"); + media_playlist_list.push_back(media_playlist.get()); + } + + const char kBaseUrl[] = "http://playlists.org/"; + EXPECT_TRUE(master_playlist_->WriteMasterPlaylist(kBaseUrl, test_output_dir_, + media_playlist_list)); + + std::string actual; + ASSERT_TRUE( + File::ReadFileToString(master_playlist_path_.string().c_str(), &actual)); + + // Expected master playlist content with encryption. + std::string expected = + "#EXTM3U\n" + "## Generated with https://github.com/shaka-project/shaka-packager " + "version test\n" + "#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI=\"http://example.com\"," + "IV=0x12345678,KEYFORMATVERSIONS=\"1/2/4\",KEYFORMAT=\"com.widevine\"\n" + "\n" + "#EXT-X-MEDIA:TYPE=AUDIO,URI=\"http://playlists.org/audio-1.m3u8\"," + "GROUP-ID=\"audio-group-1\",LANGUAGE=\"en\",NAME=\"audio 1\"," + "DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\"\n" + "\n" + "#EXT-X-STREAM-INF:BANDWIDTH=350000,AVERAGE-BANDWIDTH=230000," + "CODECS=\"sdvideocodec,audiocodec\",RESOLUTION=800x600," + "AUDIO=\"audio-group-1\",CLOSED-CAPTIONS=NONE\n" + "http://playlists.org/video-1.m3u8\n"; + + ASSERT_EQ(expected, actual); +} + TEST_F(MasterPlaylistTest, WriteMasterPlaylistAudioOnlyJOC) { const uint64_t kAudioChannels = 6; const uint64_t kAudioMaxBitrate = 50000; diff --git a/packager/hls/base/media_playlist.cc b/packager/hls/base/media_playlist.cc index b480c0e2ce..85a8a9d145 100644 --- a/packager/hls/base/media_playlist.cc +++ b/packager/hls/base/media_playlist.cc @@ -165,6 +165,12 @@ std::string CreatePlaylistHeader( return header; } + +} // namespace + +HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {} +HlsEntry::~HlsEntry() {} + class SegmentInfoEntry : public HlsEntry { public: // If |use_byte_range| true then this will append EXT-X-BYTERANGE @@ -233,75 +239,6 @@ std::string SegmentInfoEntry::ToString() { return result; } -class EncryptionInfoEntry : public HlsEntry { - public: - EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method, - const std::string& url, - const std::string& key_id, - const std::string& iv, - const std::string& key_format, - const std::string& key_format_versions); - - std::string ToString() override; - - private: - EncryptionInfoEntry(const EncryptionInfoEntry&) = delete; - EncryptionInfoEntry& operator=(const EncryptionInfoEntry&) = delete; - - const MediaPlaylist::EncryptionMethod method_; - const std::string url_; - const std::string key_id_; - const std::string iv_; - const std::string key_format_; - const std::string key_format_versions_; -}; - -EncryptionInfoEntry::EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method, - const std::string& url, - const std::string& key_id, - const std::string& iv, - const std::string& key_format, - const std::string& key_format_versions) - : HlsEntry(HlsEntry::EntryType::kExtKey), - method_(method), - url_(url), - key_id_(key_id), - iv_(iv), - key_format_(key_format), - key_format_versions_(key_format_versions) {} - -std::string EncryptionInfoEntry::ToString() { - std::string tag_string; - Tag tag("#EXT-X-KEY", &tag_string); - - if (method_ == MediaPlaylist::EncryptionMethod::kSampleAes) { - tag.AddString("METHOD", "SAMPLE-AES"); - } else if (method_ == MediaPlaylist::EncryptionMethod::kAes128) { - tag.AddString("METHOD", "AES-128"); - } else if (method_ == MediaPlaylist::EncryptionMethod::kSampleAesCenc) { - tag.AddString("METHOD", "SAMPLE-AES-CTR"); - } else { - DCHECK(method_ == MediaPlaylist::EncryptionMethod::kNone); - tag.AddString("METHOD", "NONE"); - } - - tag.AddQuotedString("URI", url_); - - if (!key_id_.empty()) { - tag.AddString("KEYID", key_id_); - } - if (!iv_.empty()) { - tag.AddString("IV", iv_); - } - if (!key_format_versions_.empty()) { - tag.AddQuotedString("KEYFORMATVERSIONS", key_format_versions_); - } - if (!key_format_.empty()) { - tag.AddQuotedString("KEYFORMAT", key_format_); - } - - return tag_string; -} class DiscontinuityEntry : public HlsEntry { public: @@ -340,10 +277,58 @@ std::string PlacementOpportunityEntry::ToString() { return "#EXT-X-PLACEMENT-OPPORTUNITY"; } -} // namespace +EncryptionInfoEntry::EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method, + const std::string& url, + const std::string& key_id, + const std::string& iv, + const std::string& key_format, + const std::string& key_format_versions) + : HlsEntry(HlsEntry::EntryType::kExtKey), + method_(method), + url_(url), + key_id_(key_id), + iv_(iv), + key_format_(key_format), + key_format_versions_(key_format_versions) {} -HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {} -HlsEntry::~HlsEntry() {} +std::string EncryptionInfoEntry::ToString() { + return ToString(""); +} + +std::string EncryptionInfoEntry::ToString(std::string tag_name) { + std::string tag_string; + if (tag_name.empty()) + tag_name = "#EXT-X-KEY"; + Tag tag(tag_name, &tag_string); + + if (method_ == MediaPlaylist::EncryptionMethod::kSampleAes) { + tag.AddString("METHOD", "SAMPLE-AES"); + } else if (method_ == MediaPlaylist::EncryptionMethod::kAes128) { + tag.AddString("METHOD", "AES-128"); + } else if (method_ == MediaPlaylist::EncryptionMethod::kSampleAesCenc) { + tag.AddString("METHOD", "SAMPLE-AES-CTR"); + } else { + DCHECK(method_ == MediaPlaylist::EncryptionMethod::kNone); + tag.AddString("METHOD", "NONE"); + } + + tag.AddQuotedString("URI", url_); + + if (!key_id_.empty()) { + tag.AddString("KEYID", key_id_); + } + if (!iv_.empty()) { + tag.AddString("IV", iv_); + } + if (!key_format_versions_.empty()) { + tag.AddQuotedString("KEYFORMATVERSIONS", key_format_versions_); + } + if (!key_format_.empty()) { + tag.AddQuotedString("KEYFORMAT", key_format_); + } + + return tag_string; +} MediaPlaylist::MediaPlaylist(const HlsParams& hls_params, const std::string& file_name, @@ -383,6 +368,17 @@ void MediaPlaylist::SetForcedSubtitleForTesting(const bool forced_subtitle) { forced_subtitle_ = forced_subtitle; } +void MediaPlaylist::AddEncryptionInfoForTesting( + MediaPlaylist::EncryptionMethod method, + const std::string& url, + const std::string& key_id, + const std::string& iv, + const std::string& key_format, + const std::string& key_format_versions) { + entries_.emplace_back(new EncryptionInfoEntry( + method, url, key_id, iv, key_format, key_format_versions)); +} + bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) { const int32_t time_scale = GetTimeScale(media_info); if (time_scale == 0) { diff --git a/packager/hls/base/media_playlist.h b/packager/hls/base/media_playlist.h index 75127ccf7c..81cb8b95a2 100644 --- a/packager/hls/base/media_playlist.h +++ b/packager/hls/base/media_playlist.h @@ -83,6 +83,9 @@ class MediaPlaylist { const std::string& codec() const { return codec_; } const std::string& supplemental_codec() const { return supplemental_codec_; } const media::FourCC& compatible_brand() const { return compatible_brand_; } + const std::list>& entries() const { + return entries_; + } /// For testing only. void SetStreamTypeForTesting(MediaPlaylistStreamType stream_type); @@ -100,6 +103,14 @@ class MediaPlaylist { void SetCharacteristicsForTesting( const std::vector& characteristics); + /// For testing only. + void AddEncryptionInfoForTesting(MediaPlaylist::EncryptionMethod method, + const std::string& url, + const std::string& key_id, + const std::string& iv, + const std::string& key_format, + const std::string& key_format_versions); + /// This must succeed before calling any other public methods. /// @param media_info is the info of the segments that are going to be added /// to this playlist. @@ -310,6 +321,30 @@ class MediaPlaylist { DISALLOW_COPY_AND_ASSIGN(MediaPlaylist); }; +class EncryptionInfoEntry : public HlsEntry { + public: + EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method, + const std::string& url, + const std::string& key_id, + const std::string& iv, + const std::string& key_format, + const std::string& key_format_versions); + + std::string ToString() override; + std::string ToString(std::string); + + private: + EncryptionInfoEntry(const EncryptionInfoEntry&) = delete; + EncryptionInfoEntry& operator=(const EncryptionInfoEntry&) = delete; + + const MediaPlaylist::EncryptionMethod method_; + const std::string url_; + const std::string key_id_; + const std::string iv_; + const std::string key_format_; + const std::string key_format_versions_; +}; + } // namespace hls } // namespace shaka diff --git a/packager/hls/base/simple_hls_notifier.cc b/packager/hls/base/simple_hls_notifier.cc index 2ab7087aa5..7b795f83af 100644 --- a/packager/hls/base/simple_hls_notifier.cc +++ b/packager/hls/base/simple_hls_notifier.cc @@ -284,7 +284,8 @@ SimpleHlsNotifier::SimpleHlsNotifier(const HlsParams& hls_params) : 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)); + default_text_language, hls_params.is_independent_segments, + hls_params.create_session_keys)); } SimpleHlsNotifier::~SimpleHlsNotifier() {}