Skip to content

Commit d88ed27

Browse files
authored
feat: EXT-X-SESSION-KEY support (#36) (#1427)
1 parent ddeacb2 commit d88ed27

File tree

11 files changed

+183
-65
lines changed

11 files changed

+183
-65
lines changed

docs/source/options/hls_options.rst

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,9 @@ HLS options
9292
--force_cl_index
9393

9494
True forces the muxer to order streams in the order given
95-
on the command-line. False uses the previous unordered behavior.
95+
on the command-line. False uses the previous unordered behavior.
96+
97+
--create_session_keys
98+
99+
Playback of Offline HLS assets shall use EXT-X-SESSION-KEY to declare all
100+
eligible content keys in the master playlist.

include/packager/hls_params.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@ struct HlsParams {
7070
/// playlist. A negative number indicates a negative time offset from the end
7171
/// of the last media segment in the playlist.
7272
std::optional<double> start_time_offset;
73+
/// Create EXT-X-SESSION-KEY in master playlist
74+
bool create_session_keys;
7375
};
7476

7577
} // namespace shaka

packager/app/hls_flags.cc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,8 @@ ABSL_FLAG(std::optional<double>,
4646
"beginning of the playlist. A negative number indicates a "
4747
"negative time offset from the end of the last media segment "
4848
"in the playlist.");
49+
ABSL_FLAG(bool,
50+
create_session_keys,
51+
false,
52+
"Playback of Offline HLS assets shall use EXT-X-SESSION-KEY "
53+
"to declare all eligible content keys in the master playlist.");

packager/app/hls_flags.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,6 @@ ABSL_DECLARE_FLAG(std::string, hls_key_uri);
1616
ABSL_DECLARE_FLAG(std::string, hls_playlist_type);
1717
ABSL_DECLARE_FLAG(int32_t, hls_media_sequence_number);
1818
ABSL_DECLARE_FLAG(std::optional<double>, hls_start_time_offset);
19+
ABSL_DECLARE_FLAG(bool, create_session_keys);
1920

2021
#endif // PACKAGER_APP_HLS_FLAGS_H_

packager/app/packager_main.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -543,6 +543,7 @@ std::optional<PackagingParams> GetPackagingParams() {
543543
hls_params.media_sequence_number =
544544
absl::GetFlag(FLAGS_hls_media_sequence_number);
545545
hls_params.start_time_offset = absl::GetFlag(FLAGS_hls_start_time_offset);
546+
hls_params.create_session_keys = absl::GetFlag(FLAGS_create_session_keys);
546547

547548
TestParams& test_params = packaging_params.test_params;
548549
test_params.dump_stream_info = absl::GetFlag(FLAGS_dump_stream_info);

packager/hls/base/master_playlist.cc

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -550,11 +550,13 @@ void AppendPlaylists(const std::string& default_audio_language,
550550
MasterPlaylist::MasterPlaylist(const std::filesystem::path& file_name,
551551
const std::string& default_audio_language,
552552
const std::string& default_text_language,
553-
bool is_independent_segments)
553+
bool is_independent_segments,
554+
bool create_session_keys)
554555
: file_name_(file_name),
555556
default_audio_language_(default_audio_language),
556557
default_text_language_(default_text_language),
557-
is_independent_segments_(is_independent_segments) {}
558+
is_independent_segments_(is_independent_segments),
559+
create_session_keys_(create_session_keys) {}
558560

559561
MasterPlaylist::~MasterPlaylist() {}
560562

@@ -568,6 +570,23 @@ bool MasterPlaylist::WriteMasterPlaylist(
568570
if (is_independent_segments_) {
569571
content.append("\n#EXT-X-INDEPENDENT-SEGMENTS\n");
570572
}
573+
574+
// Iterate over the playlists and add the session keys to the master playlist.
575+
if (create_session_keys_) {
576+
std::set<std::string> session_keys;
577+
for (const auto& playlist : playlists) {
578+
for (const auto& entry : playlist->entries()) {
579+
if (entry->type() == HlsEntry::EntryType::kExtKey) {
580+
auto encryption_entry = dynamic_cast<EncryptionInfoEntry*>(entry.get());
581+
session_keys.emplace(encryption_entry->ToString("#EXT-X-SESSION-KEY"));
582+
}
583+
}
584+
}
585+
// session_keys will now contain all the unique session keys.
586+
for (const auto& session_key : session_keys)
587+
content.append(session_key + "\n");
588+
}
589+
571590
AppendPlaylists(default_audio_language_, default_text_language_, base_url,
572591
playlists, &content);
573592

packager/hls/base/master_playlist.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ class MasterPlaylist {
2828
MasterPlaylist(const std::filesystem::path& file_name,
2929
const std::string& default_audio_language,
3030
const std::string& default_text_language,
31-
const bool is_independent_segments);
31+
const bool is_independent_segments,
32+
const bool create_session_keys = false);
3233
virtual ~MasterPlaylist();
3334

3435
/// Writes Master Playlist to output_dir + <name of playlist>.
@@ -53,6 +54,7 @@ class MasterPlaylist {
5354
const std::string default_audio_language_;
5455
const std::string default_text_language_;
5556
bool is_independent_segments_;
57+
bool create_session_keys_;
5658
};
5759

5860
} // namespace hls

packager/hls/base/master_playlist_unittest.cc

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const uint32_t kEC3JocComplexityZero = 0;
4040
const uint32_t kEC3JocComplexity = 16;
4141
const bool kAC4IMSFlagEnabled = true;
4242
const bool kAC4CBIFlagEnabled = true;
43+
const bool kCreateSessionKeys = true;
4344

4445
std::unique_ptr<MockMediaPlaylist> CreateVideoPlaylist(
4546
const std::string& filename,
@@ -143,7 +144,8 @@ class MasterPlaylistTest : public ::testing::Test {
143144
: master_playlist_(new MasterPlaylist(kDefaultMasterPlaylistName,
144145
kDefaultAudioLanguage,
145146
kDefaultTextLanguage,
146-
!kIsIndependentSegments)),
147+
!kIsIndependentSegments,
148+
kCreateSessionKeys)),
147149
test_output_dir_("memory://test_dir"),
148150
master_playlist_path_(std::filesystem::u8path(test_output_dir_) /
149151
kDefaultMasterPlaylistName) {}
@@ -849,6 +851,55 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistAudioOnly) {
849851
ASSERT_EQ(expected, actual);
850852
}
851853

854+
TEST_F(MasterPlaylistTest, WriteMasterPlaylistWithEncryption) {
855+
std::unique_ptr<MockMediaPlaylist> media_playlists[] = {
856+
// VIDEO
857+
CreateVideoPlaylist("video-1.m3u8", "sdvideocodec", 300000, 200000),
858+
859+
// AUDIO
860+
CreateAudioPlaylist("audio-1.m3u8", "audio 1", "audio-group-1",
861+
"audiocodec", "en", 2, 50000, 30000,
862+
kEC3JocComplexityZero, !kAC4IMSFlagEnabled,
863+
!kAC4CBIFlagEnabled),
864+
};
865+
866+
// Add all the media playlists to the master playlist.
867+
std::list<MediaPlaylist*> media_playlist_list;
868+
for (const auto& media_playlist : media_playlists) {
869+
media_playlist.get()->AddEncryptionInfoForTesting(
870+
MediaPlaylist::EncryptionMethod::kSampleAes, "http://example.com", "",
871+
"0x12345678", "com.widevine", "1/2/4");
872+
media_playlist_list.push_back(media_playlist.get());
873+
}
874+
875+
const char kBaseUrl[] = "http://playlists.org/";
876+
EXPECT_TRUE(master_playlist_->WriteMasterPlaylist(kBaseUrl, test_output_dir_,
877+
media_playlist_list));
878+
879+
std::string actual;
880+
ASSERT_TRUE(
881+
File::ReadFileToString(master_playlist_path_.string().c_str(), &actual));
882+
883+
// Expected master playlist content with encryption.
884+
std::string expected =
885+
"#EXTM3U\n"
886+
"## Generated with https://github.com/shaka-project/shaka-packager "
887+
"version test\n"
888+
"#EXT-X-SESSION-KEY:METHOD=SAMPLE-AES,URI=\"http://example.com\","
889+
"IV=0x12345678,KEYFORMATVERSIONS=\"1/2/4\",KEYFORMAT=\"com.widevine\"\n"
890+
"\n"
891+
"#EXT-X-MEDIA:TYPE=AUDIO,URI=\"http://playlists.org/audio-1.m3u8\","
892+
"GROUP-ID=\"audio-group-1\",LANGUAGE=\"en\",NAME=\"audio 1\","
893+
"DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\"\n"
894+
"\n"
895+
"#EXT-X-STREAM-INF:BANDWIDTH=350000,AVERAGE-BANDWIDTH=230000,"
896+
"CODECS=\"sdvideocodec,audiocodec\",RESOLUTION=800x600,"
897+
"AUDIO=\"audio-group-1\",CLOSED-CAPTIONS=NONE\n"
898+
"http://playlists.org/video-1.m3u8\n";
899+
900+
ASSERT_EQ(expected, actual);
901+
}
902+
852903
TEST_F(MasterPlaylistTest, WriteMasterPlaylistAudioOnlyJOC) {
853904
const uint64_t kAudioChannels = 6;
854905
const uint64_t kAudioMaxBitrate = 50000;

packager/hls/base/media_playlist.cc

Lines changed: 55 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,12 @@ std::string CreatePlaylistHeader(
165165
return header;
166166
}
167167

168+
169+
} // namespace
170+
171+
HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {}
172+
HlsEntry::~HlsEntry() {}
173+
168174
class SegmentInfoEntry : public HlsEntry {
169175
public:
170176
// If |use_byte_range| true then this will append EXT-X-BYTERANGE
@@ -233,29 +239,44 @@ std::string SegmentInfoEntry::ToString() {
233239
return result;
234240
}
235241

236-
class EncryptionInfoEntry : public HlsEntry {
242+
243+
class DiscontinuityEntry : public HlsEntry {
237244
public:
238-
EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method,
239-
const std::string& url,
240-
const std::string& key_id,
241-
const std::string& iv,
242-
const std::string& key_format,
243-
const std::string& key_format_versions);
245+
DiscontinuityEntry();
244246

245247
std::string ToString() override;
246248

247249
private:
248-
EncryptionInfoEntry(const EncryptionInfoEntry&) = delete;
249-
EncryptionInfoEntry& operator=(const EncryptionInfoEntry&) = delete;
250-
251-
const MediaPlaylist::EncryptionMethod method_;
252-
const std::string url_;
253-
const std::string key_id_;
254-
const std::string iv_;
255-
const std::string key_format_;
256-
const std::string key_format_versions_;
250+
DiscontinuityEntry(const DiscontinuityEntry&) = delete;
251+
DiscontinuityEntry& operator=(const DiscontinuityEntry&) = delete;
257252
};
258253

254+
DiscontinuityEntry::DiscontinuityEntry()
255+
: HlsEntry(HlsEntry::EntryType::kExtDiscontinuity) {}
256+
257+
std::string DiscontinuityEntry::ToString() {
258+
return "#EXT-X-DISCONTINUITY";
259+
}
260+
261+
class PlacementOpportunityEntry : public HlsEntry {
262+
public:
263+
PlacementOpportunityEntry();
264+
265+
std::string ToString() override;
266+
267+
private:
268+
PlacementOpportunityEntry(const PlacementOpportunityEntry&) = delete;
269+
PlacementOpportunityEntry& operator=(const PlacementOpportunityEntry&) =
270+
delete;
271+
};
272+
273+
PlacementOpportunityEntry::PlacementOpportunityEntry()
274+
: HlsEntry(HlsEntry::EntryType::kExtPlacementOpportunity) {}
275+
276+
std::string PlacementOpportunityEntry::ToString() {
277+
return "#EXT-X-PLACEMENT-OPPORTUNITY";
278+
}
279+
259280
EncryptionInfoEntry::EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method,
260281
const std::string& url,
261282
const std::string& key_id,
@@ -271,8 +292,14 @@ EncryptionInfoEntry::EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method,
271292
key_format_versions_(key_format_versions) {}
272293

273294
std::string EncryptionInfoEntry::ToString() {
295+
return ToString("");
296+
}
297+
298+
std::string EncryptionInfoEntry::ToString(std::string tag_name) {
274299
std::string tag_string;
275-
Tag tag("#EXT-X-KEY", &tag_string);
300+
if (tag_name.empty())
301+
tag_name = "#EXT-X-KEY";
302+
Tag tag(tag_name, &tag_string);
276303

277304
if (method_ == MediaPlaylist::EncryptionMethod::kSampleAes) {
278305
tag.AddString("METHOD", "SAMPLE-AES");
@@ -303,48 +330,6 @@ std::string EncryptionInfoEntry::ToString() {
303330
return tag_string;
304331
}
305332

306-
class DiscontinuityEntry : public HlsEntry {
307-
public:
308-
DiscontinuityEntry();
309-
310-
std::string ToString() override;
311-
312-
private:
313-
DiscontinuityEntry(const DiscontinuityEntry&) = delete;
314-
DiscontinuityEntry& operator=(const DiscontinuityEntry&) = delete;
315-
};
316-
317-
DiscontinuityEntry::DiscontinuityEntry()
318-
: HlsEntry(HlsEntry::EntryType::kExtDiscontinuity) {}
319-
320-
std::string DiscontinuityEntry::ToString() {
321-
return "#EXT-X-DISCONTINUITY";
322-
}
323-
324-
class PlacementOpportunityEntry : public HlsEntry {
325-
public:
326-
PlacementOpportunityEntry();
327-
328-
std::string ToString() override;
329-
330-
private:
331-
PlacementOpportunityEntry(const PlacementOpportunityEntry&) = delete;
332-
PlacementOpportunityEntry& operator=(const PlacementOpportunityEntry&) =
333-
delete;
334-
};
335-
336-
PlacementOpportunityEntry::PlacementOpportunityEntry()
337-
: HlsEntry(HlsEntry::EntryType::kExtPlacementOpportunity) {}
338-
339-
std::string PlacementOpportunityEntry::ToString() {
340-
return "#EXT-X-PLACEMENT-OPPORTUNITY";
341-
}
342-
343-
} // namespace
344-
345-
HlsEntry::HlsEntry(HlsEntry::EntryType type) : type_(type) {}
346-
HlsEntry::~HlsEntry() {}
347-
348333
MediaPlaylist::MediaPlaylist(const HlsParams& hls_params,
349334
const std::string& file_name,
350335
const std::string& name,
@@ -383,6 +368,17 @@ void MediaPlaylist::SetForcedSubtitleForTesting(const bool forced_subtitle) {
383368
forced_subtitle_ = forced_subtitle;
384369
}
385370

371+
void MediaPlaylist::AddEncryptionInfoForTesting(
372+
MediaPlaylist::EncryptionMethod method,
373+
const std::string& url,
374+
const std::string& key_id,
375+
const std::string& iv,
376+
const std::string& key_format,
377+
const std::string& key_format_versions) {
378+
entries_.emplace_back(new EncryptionInfoEntry(
379+
method, url, key_id, iv, key_format, key_format_versions));
380+
}
381+
386382
bool MediaPlaylist::SetMediaInfo(const MediaInfo& media_info) {
387383
const int32_t time_scale = GetTimeScale(media_info);
388384
if (time_scale == 0) {

packager/hls/base/media_playlist.h

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ class MediaPlaylist {
8383
const std::string& codec() const { return codec_; }
8484
const std::string& supplemental_codec() const { return supplemental_codec_; }
8585
const media::FourCC& compatible_brand() const { return compatible_brand_; }
86+
const std::list<std::unique_ptr<HlsEntry>>& entries() const {
87+
return entries_;
88+
}
8689

8790
/// For testing only.
8891
void SetStreamTypeForTesting(MediaPlaylistStreamType stream_type);
@@ -100,6 +103,14 @@ class MediaPlaylist {
100103
void SetCharacteristicsForTesting(
101104
const std::vector<std::string>& characteristics);
102105

106+
/// For testing only.
107+
void AddEncryptionInfoForTesting(MediaPlaylist::EncryptionMethod method,
108+
const std::string& url,
109+
const std::string& key_id,
110+
const std::string& iv,
111+
const std::string& key_format,
112+
const std::string& key_format_versions);
113+
103114
/// This must succeed before calling any other public methods.
104115
/// @param media_info is the info of the segments that are going to be added
105116
/// to this playlist.
@@ -310,6 +321,30 @@ class MediaPlaylist {
310321
DISALLOW_COPY_AND_ASSIGN(MediaPlaylist);
311322
};
312323

324+
class EncryptionInfoEntry : public HlsEntry {
325+
public:
326+
EncryptionInfoEntry(MediaPlaylist::EncryptionMethod method,
327+
const std::string& url,
328+
const std::string& key_id,
329+
const std::string& iv,
330+
const std::string& key_format,
331+
const std::string& key_format_versions);
332+
333+
std::string ToString() override;
334+
std::string ToString(std::string);
335+
336+
private:
337+
EncryptionInfoEntry(const EncryptionInfoEntry&) = delete;
338+
EncryptionInfoEntry& operator=(const EncryptionInfoEntry&) = delete;
339+
340+
const MediaPlaylist::EncryptionMethod method_;
341+
const std::string url_;
342+
const std::string key_id_;
343+
const std::string iv_;
344+
const std::string key_format_;
345+
const std::string key_format_versions_;
346+
};
347+
313348
} // namespace hls
314349
} // namespace shaka
315350

0 commit comments

Comments
 (0)