parent
ddeacb2525
commit
d88ed2798c
|
@ -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.
|
||||
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.
|
|
@ -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
|
||||
|
|
|
@ -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.");
|
||||
|
|
|
@ -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_
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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() {}
|
||||
|
|
Loading…
Reference in New Issue