feat: EXT-X-SESSION-KEY support (#36) (#1427)

This commit is contained in:
SteveR-PMP 2024-10-25 09:55:27 -07:00 committed by GitHub
parent ddeacb2525
commit d88ed2798c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 196 additions and 78 deletions

View File

@ -93,3 +93,8 @@ HLS options
True forces the muxer to order streams in the order given
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.

View File

@ -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<double> start_time_offset;
/// Create EXT-X-SESSION-KEY in master playlist
bool create_session_keys;
};
} // namespace shaka

View File

@ -46,3 +46,8 @@ ABSL_FLAG(std::optional<double>,
"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.");

View File

@ -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<double>, hls_start_time_offset);
ABSL_DECLARE_FLAG(bool, create_session_keys);
#endif // PACKAGER_APP_HLS_FLAGS_H_

View File

@ -543,6 +543,7 @@ std::optional<PackagingParams> 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);

View File

@ -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<std::string> session_keys;
for (const auto& playlist : playlists) {
for (const auto& entry : playlist->entries()) {
if (entry->type() == HlsEntry::EntryType::kExtKey) {
auto encryption_entry = dynamic_cast<EncryptionInfoEntry*>(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);

View File

@ -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 + <name of playlist>.
@ -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

View File

@ -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<MockMediaPlaylist> 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<MockMediaPlaylist> 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<MediaPlaylist*> 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;

View File

@ -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) {

View File

@ -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<std::unique_ptr<HlsEntry>>& entries() const {
return entries_;
}
/// For testing only.
void SetStreamTypeForTesting(MediaPlaylistStreamType stream_type);
@ -100,6 +103,14 @@ class MediaPlaylist {
void SetCharacteristicsForTesting(
const std::vector<std::string>& 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

View File

@ -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() {}