Shaka Packager SDK
Loading...
Searching...
No Matches
master_playlist.cc
1// Copyright 2016 Google LLC. All rights reserved.
2//
3// Use of this source code is governed by a BSD-style
4// license that can be found in the LICENSE file or at
5// https://developers.google.com/open-source/licenses/bsd
6
7#include <packager/hls/base/master_playlist.h>
8
9#include <algorithm> // std::max
10#include <cstdint>
11#include <filesystem>
12#include <set>
13#include <vector>
14
15#include <absl/log/check.h>
16#include <absl/log/log.h>
17#include <absl/strings/numbers.h>
18#include <absl/strings/str_format.h>
19#include <absl/strings/str_join.h>
20
21#include <packager/file.h>
22#include <packager/hls/base/media_playlist.h>
23#include <packager/hls/base/tag.h>
24#include <packager/macros/logging.h>
25#include <packager/version/version.h>
26
27#include "packager/kv_pairs/kv_pairs.h"
28#include "packager/utils/string_trim_split.h"
29
30namespace shaka {
31namespace hls {
32namespace {
33const char* kDefaultAudioGroupId = "default-audio-group";
34const char* kDefaultSubtitleGroupId = "default-text-group";
35const char* kUnexpectedGroupId = "unexpected-group";
36
37void AppendVersionString(std::string* content) {
38 const std::string version = GetPackagerVersion();
39 if (version.empty())
40 return;
41 absl::StrAppendFormat(content, "## Generated with %s version %s\n",
42 GetPackagerProjectUrl().c_str(), version.c_str());
43}
44
45// This structure roughly maps to the Variant stream in HLS specification.
46// Each variant specifies zero or one audio group and zero or one text group.
47struct Variant {
48 std::set<std::string> audio_codecs;
49 std::set<std::string> text_codecs;
50 const std::string* audio_group_id = nullptr;
51 const std::string* text_group_id = nullptr;
52 bool have_instream_closed_caption = false;
53 // The bitrates should be the sum of audio bitrate and text bitrate.
54 // However, given the constraints and assumptions, it makes sense to exclude
55 // text bitrate out of the calculation:
56 // - Text streams usually have a very small negligible bitrate.
57 // - Text does not have constant bitrates. To avoid fluctuation, an arbitrary
58 // value is assigned to the text bitrates in the parser. It does not make
59 // sense to take that text bitrate into account here.
60 uint64_t max_audio_bitrate = 0;
61 uint64_t avg_audio_bitrate = 0;
62};
63
64// Pairs a media playlist with the EXT-X-MEDIA tag attributes computed for it.
65// Resolving the attributes (group id, default/autoselect) up front lets us
66// emit tags in input order without re-grouping by GROUP-ID first.
67struct MediaTags {
68 const MediaPlaylist* playlist;
69 std::string group_id;
70 bool is_default;
71 bool is_autoselect;
72};
73
74uint64_t GetMaximumMaxBitrate(const std::list<const MediaPlaylist*> playlists) {
75 uint64_t max = 0;
76 for (const auto& playlist : playlists) {
77 max = std::max(max, playlist->MaxBitrate());
78 }
79 return max;
80}
81
82uint64_t GetMaximumAvgBitrate(const std::list<const MediaPlaylist*> playlists) {
83 uint64_t max = 0;
84 for (const auto& playlist : playlists) {
85 max = std::max(max, playlist->AvgBitrate());
86 }
87 return max;
88}
89
90std::set<std::string> GetGroupCodecString(
91 const std::list<const MediaPlaylist*>& group) {
92 std::set<std::string> codecs;
93
94 for (const MediaPlaylist* playlist : group) {
95 codecs.insert(playlist->codec());
96 }
97
98 // To support some older players, we cannot include "wvtt" in the codec
99 // string. As per HLS guidelines, "wvtt" is optional. When it is included, it
100 // can cause playback errors on some Apple produces. Excluding it allows
101 // playback on all Apple products. See
102 // https://github.com/shaka-project/shaka-packager/issues/402 for all details.
103 auto wvtt = codecs.find("wvtt");
104 if (wvtt != codecs.end()) {
105 codecs.erase(wvtt);
106 }
107 // TTML is specified using 'stpp.ttml.im1t'; see section 5.10 of
108 // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices
109 auto ttml = codecs.find("ttml");
110 if (ttml != codecs.end()) {
111 codecs.erase(ttml);
112 codecs.insert("stpp.ttml.im1t");
113 }
114
115 return codecs;
116}
117
118std::list<Variant> AudioGroupsToVariants(
119 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
120 std::list<Variant> variants;
121
122 for (const auto& group : groups) {
123 Variant variant;
124 variant.audio_group_id = &group.first;
125 variant.max_audio_bitrate = GetMaximumMaxBitrate(group.second);
126 variant.avg_audio_bitrate = GetMaximumAvgBitrate(group.second);
127 variant.audio_codecs = GetGroupCodecString(group.second);
128
129 variants.push_back(variant);
130 }
131
132 // Make sure we return at least one variant so create a null variant if there
133 // are no variants.
134 if (variants.empty()) {
135 variants.emplace_back();
136 }
137
138 return variants;
139}
140
141const char* GetGroupId(const MediaPlaylist& playlist) {
142 const std::string& group_id = playlist.group_id();
143
144 if (!group_id.empty()) {
145 return group_id.c_str();
146 }
147
148 switch (playlist.stream_type()) {
149 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
150 return kDefaultAudioGroupId;
151
152 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
153 return kDefaultSubtitleGroupId;
154
155 default:
156 return kUnexpectedGroupId;
157 }
158}
159
160std::list<Variant> SubtitleGroupsToVariants(
161 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
162 std::list<Variant> variants;
163
164 for (const auto& group : groups) {
165 Variant variant;
166 variant.text_group_id = &group.first;
167 variant.text_codecs = GetGroupCodecString(group.second);
168
169 variants.push_back(variant);
170 }
171
172 // Make sure we return at least one variant so create a null variant if there
173 // are no variants.
174 if (variants.empty()) {
175 variants.emplace_back();
176 }
177
178 return variants;
179}
180
181std::list<Variant> BuildVariants(
182 const std::map<std::string, std::list<const MediaPlaylist*>>& audio_groups,
183 const std::map<std::string, std::list<const MediaPlaylist*>>&
184 subtitle_groups,
185 const bool have_instream_closed_caption) {
186 std::list<Variant> audio_variants = AudioGroupsToVariants(audio_groups);
187 std::list<Variant> subtitle_variants =
188 SubtitleGroupsToVariants(subtitle_groups);
189
190 DCHECK_GE(audio_variants.size(), 1u);
191 DCHECK_GE(subtitle_variants.size(), 1u);
192
193 std::list<Variant> merged;
194
195 for (const auto& audio_variant : audio_variants) {
196 for (const auto& subtitle_variant : subtitle_variants) {
197 Variant base_variant;
198 base_variant.audio_codecs = audio_variant.audio_codecs;
199 base_variant.text_codecs = subtitle_variant.text_codecs;
200 base_variant.audio_group_id = audio_variant.audio_group_id;
201 base_variant.text_group_id = subtitle_variant.text_group_id;
202 base_variant.max_audio_bitrate = audio_variant.max_audio_bitrate;
203 base_variant.avg_audio_bitrate = audio_variant.avg_audio_bitrate;
204 base_variant.have_instream_closed_caption = have_instream_closed_caption;
205 merged.push_back(base_variant);
206 }
207 }
208
209 DCHECK_GE(merged.size(), 1u);
210
211 return merged;
212}
213
214void BuildStreamInfTag(const MediaPlaylist& playlist,
215 const Variant& variant,
216 const std::string& base_url,
217 std::string* out) {
218 DCHECK(out);
219
220 std::string tag_name;
221 switch (playlist.stream_type()) {
222 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
223 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
224 tag_name = "#EXT-X-STREAM-INF";
225 break;
226 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
227 tag_name = "#EXT-X-I-FRAME-STREAM-INF";
228 break;
229 default:
230 NOTIMPLEMENTED() << "Cannot build STREAM-INFO tag for type "
231 << static_cast<int>(playlist.stream_type());
232 break;
233 }
234 Tag tag(tag_name, out);
235
236 tag.AddNumber("BANDWIDTH", playlist.MaxBitrate() + variant.max_audio_bitrate);
237 tag.AddNumber("AVERAGE-BANDWIDTH",
238 playlist.AvgBitrate() + variant.avg_audio_bitrate);
239
240 std::vector<std::string> all_codecs;
241 all_codecs.push_back(playlist.codec());
242 all_codecs.insert(all_codecs.end(), variant.audio_codecs.begin(),
243 variant.audio_codecs.end());
244 all_codecs.insert(all_codecs.end(), variant.text_codecs.begin(),
245 variant.text_codecs.end());
246 tag.AddQuotedString("CODECS", absl::StrJoin(all_codecs, ","));
247
248 if (playlist.supplemental_codec() != "" &&
249 playlist.compatible_brand() != media::FOURCC_NULL) {
250 std::vector<std::string> supplemental_codecs;
251 supplemental_codecs.push_back(playlist.supplemental_codec());
252 supplemental_codecs.push_back(FourCCToString(playlist.compatible_brand()));
253 tag.AddQuotedString("SUPPLEMENTAL-CODECS",
254 absl::StrJoin(supplemental_codecs, "/"));
255 }
256
257 uint32_t width;
258 uint32_t height;
259
260 const bool is_iframe_playlist =
261 playlist.stream_type() ==
262 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly;
263
264 if (playlist.GetDisplayResolution(&width, &height)) {
265 tag.AddNumberPair("RESOLUTION", width, 'x', height);
266
267 // Right now the frame-rate returned may not be accurate in some scenarios.
268 // TODO(kqyang): Fix frame-rate computation.
269 if (!is_iframe_playlist) {
270 const double frame_rate = playlist.GetFrameRate();
271 if (frame_rate > 0)
272 tag.AddFloat("FRAME-RATE", frame_rate);
273 }
274
275 const std::string video_range = playlist.GetVideoRange();
276 if (!video_range.empty())
277 tag.AddString("VIDEO-RANGE", video_range);
278 }
279
280 if (!is_iframe_playlist) {
281 if (variant.audio_group_id) {
282 tag.AddQuotedString("AUDIO", *variant.audio_group_id);
283 }
284
285 if (variant.text_group_id) {
286 tag.AddQuotedString("SUBTITLES", *variant.text_group_id);
287 }
288
289 if (variant.have_instream_closed_caption) {
290 tag.AddQuotedString("CLOSED-CAPTIONS", "CC");
291 } else {
292 tag.AddString("CLOSED-CAPTIONS", "NONE");
293 }
294 }
295
296 if (is_iframe_playlist) {
297 tag.AddQuotedString("URI", base_url + playlist.file_name());
298 out->append("\n");
299 } else {
300 absl::StrAppendFormat(out, "\n%s%s\n", base_url.c_str(),
301 playlist.file_name().c_str());
302 }
303}
304
305// Need to pass in |group_id| as it may have changed to a new default when
306// grouped with other playlists.
307void BuildMediaTag(const MediaPlaylist& playlist,
308 const std::string& group_id,
309 bool is_default,
310 bool is_autoselect,
311 const std::string& base_url,
312 std::string* out) {
313 // Tag attributes should follow the order as defined in
314 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-3.5
315
316 Tag tag("#EXT-X-MEDIA", out);
317
318 // We should only be making media tags for audio and text.
319 switch (playlist.stream_type()) {
320 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
321 tag.AddString("TYPE", "AUDIO");
322 break;
323
324 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
325 tag.AddString("TYPE", "SUBTITLES");
326 break;
327
328 default:
329 NOTIMPLEMENTED() << "Cannot build media tag for type "
330 << static_cast<int>(playlist.stream_type());
331 break;
332 }
333
334 tag.AddQuotedString("URI", base_url + playlist.file_name());
335 tag.AddQuotedString("GROUP-ID", group_id);
336
337 const std::string& language = playlist.language();
338 if (!language.empty()) {
339 tag.AddQuotedString("LANGUAGE", language);
340 }
341
342 tag.AddQuotedString("NAME", playlist.name());
343
344 if (is_default) {
345 tag.AddString("DEFAULT", "YES");
346 } else {
347 tag.AddString("DEFAULT", "NO");
348 }
349 if (is_autoselect) {
350 tag.AddString("AUTOSELECT", "YES");
351 }
352
353 if (playlist.stream_type() ==
354 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
355 playlist.forced_subtitle()) {
356 tag.AddString("FORCED", "YES");
357 }
358
359 const std::vector<std::string>& characteristics = playlist.characteristics();
360 if (!characteristics.empty()) {
361 tag.AddQuotedString("CHARACTERISTICS", absl::StrJoin(characteristics, ","));
362 }
363
364 const MediaPlaylist::MediaPlaylistStreamType kAudio =
365 MediaPlaylist::MediaPlaylistStreamType::kAudio;
366 if (playlist.stream_type() == kAudio) {
367 if (playlist.GetEC3JocComplexity() != 0) {
368 // HLS Authoring Specification for Apple Devices Appendices documents how
369 // to handle Dolby Digital Plus JOC content.
370 // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices/hls_authoring_specification_for_apple_devices_appendices
371 std::string channel_string =
372 std::to_string(playlist.GetEC3JocComplexity()) + "/JOC";
373 tag.AddQuotedString("CHANNELS", channel_string);
374 } else if (playlist.GetAC4ImsFlag() || playlist.GetAC4CbiFlag()) {
375 // Dolby has qualified using IMSA to present AC4 immersive audio (IMS and
376 // CBI without object-based audio) for Dolby internal use only. IMSA is
377 // not included in any publicly-available specifications as of June, 2020.
378 std::string channel_string =
379 std::to_string(playlist.GetNumChannels()) + "/IMSA";
380 tag.AddQuotedString("CHANNELS", channel_string);
381 } else {
382 // According to HLS spec:
383 // https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis 4.4.6.1.
384 // CHANNELS is a quoted-string that specifies an ordered,
385 // slash-separated ("/") list of parameters. The first parameter is a
386 // count of audio channels, and the second parameter identifies the
387 // encoding of object-based audio used by the Rendition.
388 std::string channel_string = std::to_string(playlist.GetNumChannels());
389 tag.AddQuotedString("CHANNELS", channel_string);
390 }
391 }
392 out->append("\n");
393}
394
395bool PlaylistOrderFn(const MediaPlaylist*& a, const MediaPlaylist*& b) {
396 return a->GetMediaInfo().index() < b->GetMediaInfo().index();
397}
398
399bool MediaTagsOrderByIndexFn(const MediaTags& a, const MediaTags& b) {
400 return a.playlist->GetMediaInfo().index() <
401 b.playlist->GetMediaInfo().index();
402}
403
404bool MediaTagsOrderByGroupIdFn(const MediaTags& a, const MediaTags& b) {
405 return a.group_id < b.group_id;
406}
407
408void BuildCeaMediaTag(const CeaCaption& caption, std::string* out) {
409 Tag tag("#EXT-X-MEDIA", out);
410 tag.AddString("TYPE", "CLOSED-CAPTIONS");
411 tag.AddQuotedString("GROUP-ID", "CC");
412 tag.AddQuotedString("NAME", caption.name);
413 if (!caption.language.empty()) {
414 tag.AddQuotedString("LANGUAGE", caption.language);
415 }
416 if (caption.is_default)
417 tag.AddString("DEFAULT", "YES");
418 else
419 tag.AddString("DEFAULT", "NO");
420 if (caption.autoselect)
421 tag.AddString("AUTOSELECT", "YES");
422 else
423 tag.AddString("AUTOSELECT", "NO");
424 tag.AddQuotedString("INSTREAM-ID", caption.channel);
425 out->append("\n");
426}
427
428void AppendPlaylists(const std::string& default_audio_language,
429 const std::string& default_text_language,
430 const std::vector<CeaCaption>& closed_captions,
431 const std::string& base_url,
432 const std::list<MediaPlaylist*>& playlists,
433 std::string* content) {
434 std::list<MediaTags> audio_playlists;
435 std::list<MediaTags> subtitle_playlists;
436 std::list<const MediaPlaylist*> video_playlists;
437 std::list<const MediaPlaylist*> iframe_playlists;
438
439 bool has_index = true;
440
441 // First pass: classify playlists and capture their group ids. The
442 // is_default/is_autoselect flags are left at their default values here and
443 // assigned in a second pass below, after sorting, so that "first rendition
444 // encountered per (group, language)" always refers to the first rendition
445 // in the final output order rather than the arbitrary arrival order of the
446 // input list (which, with --force_cl_index on by default, is typically
447 // different from the output order).
448 for (const MediaPlaylist* playlist : playlists) {
449 has_index = has_index && playlist->GetMediaInfo().has_index();
450
451 switch (playlist->stream_type()) {
452 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
453 audio_playlists.push_back({playlist, GetGroupId(*playlist),
454 /*is_default=*/false,
455 /*is_autoselect=*/false});
456 break;
457 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
458 video_playlists.push_back(playlist);
459 break;
460 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
461 iframe_playlists.push_back(playlist);
462 break;
463 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
464 subtitle_playlists.push_back({playlist, GetGroupId(*playlist),
465 /*is_default=*/false,
466 /*is_autoselect=*/false});
467 break;
468 default:
469 NOTIMPLEMENTED() << static_cast<int>(playlist->stream_type())
470 << " not handled.";
471 break;
472 }
473 }
474
475 if (has_index) {
476 video_playlists.sort(PlaylistOrderFn);
477 iframe_playlists.sort(PlaylistOrderFn);
478 audio_playlists.sort(MediaTagsOrderByIndexFn);
479 subtitle_playlists.sort(MediaTagsOrderByIndexFn);
480 } else {
481 // When there's no index, sort by group_id to maintain consistent ordering
482 // (matching the original behavior of using std::map which sorts by key).
483 audio_playlists.sort(MediaTagsOrderByGroupIdFn);
484 subtitle_playlists.sort(MediaTagsOrderByGroupIdFn);
485 }
486
487 // Second pass: now that the audio and subtitle lists are in their final
488 // output order, assign the is_default/is_autoselect flags.
489 //
490 // According to HLS spec: https://goo.gl/MiqjNd 4.3.4.1.1. Rendition Groups
491 // - A Group MUST NOT have more than one member with a DEFAULT attribute of
492 // YES.
493 // - Each EXT-X-MEDIA tag with an AUTOSELECT=YES attribute SHOULD have a
494 // combination of LANGUAGE[RFC5646], ASSOC-LANGUAGE, FORCED, and
495 // CHARACTERISTICS attributes that is distinct from those of other
496 // AUTOSELECT=YES members of its Group.
497 // We tag the first rendition in a group with a particular language with
498 // 'AUTOSELECT'; it is tagged with 'DEFAULT' too if the language matches
499 // the default language.
500 std::map<std::string, std::set<std::string>> audio_group_languages;
501 for (auto& pl : audio_playlists) {
502 if (pl.playlist->is_dvs()) {
503 // According to HLS Authoring Specification for Apple Devices
504 // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices#overview
505 // section 2.13 If you provide DVS, the AUTOSELECT attribute MUST have
506 // a value of "YES".
507 pl.is_autoselect = true;
508 continue;
509 }
510 const std::string language = pl.playlist->language();
511 auto& languages = audio_group_languages[pl.group_id];
512 if (languages.find(language) == languages.end()) {
513 pl.is_default = !language.empty() && language == default_audio_language;
514 pl.is_autoselect = true;
515 languages.insert(language);
516 }
517 }
518
519 std::map<std::string, std::set<std::string>> subtitle_group_languages;
520 for (auto& pl : subtitle_playlists) {
521 const std::string language = pl.playlist->language();
522 auto& languages = subtitle_group_languages[pl.group_id];
523 if (languages.find(language) == languages.end()) {
524 pl.is_default = !language.empty() && language == default_text_language;
525 pl.is_autoselect = true;
526 languages.insert(language);
527 }
528 if (pl.playlist->forced_subtitle()) {
529 pl.is_autoselect = true;
530 }
531 }
532
533 if (!audio_playlists.empty()) {
534 content->append("\n");
535 for (const auto& pl : audio_playlists) {
536 BuildMediaTag(*pl.playlist, pl.group_id, pl.is_default, pl.is_autoselect,
537 base_url, content);
538 }
539 }
540
541 if (!subtitle_playlists.empty()) {
542 content->append("\n");
543 for (const auto& pl : subtitle_playlists) {
544 BuildMediaTag(*pl.playlist, pl.group_id, pl.is_default, pl.is_autoselect,
545 base_url, content);
546 }
547 }
548
549 // BuildVariants() (and the audio-only fallback below) still need playlists
550 // bucketed by GROUP-ID, so rebuild those groups here from the already-emitted
551 // tag list. The tag emission above intentionally ignored grouping in order
552 // to honor input order.
553 std::map<std::string, std::list<const MediaPlaylist*>> audio_playlist_groups;
554 std::map<std::string, std::list<const MediaPlaylist*>>
555 subtitle_playlist_groups;
556 for (const auto& pl : audio_playlists) {
557 audio_playlist_groups[pl.group_id].push_back(pl.playlist);
558 }
559 for (const auto& pl : subtitle_playlists) {
560 subtitle_playlist_groups[pl.group_id].push_back(pl.playlist);
561 }
562
563 if (!closed_captions.empty()) {
564 content->append("\n");
565 for (const auto& caption : closed_captions) {
566 BuildCeaMediaTag(caption, content);
567 }
568 }
569
570 std::list<Variant> variants =
571 BuildVariants(audio_playlist_groups, subtitle_playlist_groups,
572 !closed_captions.empty());
573 for (const auto& variant : variants) {
574 if (video_playlists.empty())
575 break;
576 content->append("\n");
577 for (const auto& playlist : video_playlists) {
578 BuildStreamInfTag(*playlist, variant, base_url, content);
579 }
580 }
581
582 if (!iframe_playlists.empty()) {
583 content->append("\n");
584 for (const auto& playlist : iframe_playlists) {
585 // I-Frame playlists do not have variant. Just use the default.
586 BuildStreamInfTag(*playlist, Variant(), base_url, content);
587 }
588 }
589
590 // Generate audio-only master playlist when there are no videos and subtitles.
591 if (!audio_playlist_groups.empty() && video_playlists.empty() &&
592 subtitle_playlist_groups.empty()) {
593 content->append("\n");
594 for (const auto& playlist_group : audio_playlist_groups) {
595 Variant variant;
596 // Populate |audio_group_id|, which will be propagated to "AUDIO" field.
597 // Leaving other fields, e.g. xxx_audio_bitrate in |Variant|, as
598 // null/empty/zero intentionally as the information is already available
599 // in audio |playlist|.
600 variant.audio_group_id = &playlist_group.first;
601 for (const auto& playlist : playlist_group.second) {
602 BuildStreamInfTag(*playlist, variant, base_url, content);
603 }
604 }
605 }
606}
607
608} // namespace
609
610MasterPlaylist::MasterPlaylist(const std::filesystem::path& file_name,
611 const std::string& default_audio_language,
612 const std::string& default_text_language,
613 const std::vector<CeaCaption>& closed_captions,
614 bool is_independent_segments,
615 bool create_session_keys)
616 : file_name_(file_name),
617 default_audio_language_(default_audio_language),
618 default_text_language_(default_text_language),
619 closed_captions_(closed_captions),
620 is_independent_segments_(is_independent_segments),
621 create_session_keys_(create_session_keys) {}
622
623MasterPlaylist::~MasterPlaylist() {}
624
626 const std::string& base_url,
627 const std::string& output_dir,
628 const std::list<MediaPlaylist*>& playlists) {
629 std::string content = "#EXTM3U\n";
630 AppendVersionString(&content);
631
632 if (is_independent_segments_) {
633 content.append("\n#EXT-X-INDEPENDENT-SEGMENTS\n");
634 }
635
636 // Iterate over the playlists and add the session keys to the master playlist.
637 if (create_session_keys_) {
638 std::set<std::string> session_keys;
639 for (const auto& playlist : playlists) {
640 for (const auto& entry : playlist->entries()) {
641 if (entry->type() == HlsEntry::EntryType::kExtKey) {
642 auto encryption_entry =
643 dynamic_cast<EncryptionInfoEntry*>(entry.get());
644 session_keys.emplace(
645 encryption_entry->ToString("#EXT-X-SESSION-KEY"));
646 }
647 }
648 }
649 // session_keys will now contain all the unique session keys.
650 for (const auto& session_key : session_keys)
651 content.append(session_key + "\n");
652 }
653
654 AppendPlaylists(default_audio_language_, default_text_language_,
655 closed_captions_, base_url, playlists, &content);
656
657 // Skip if the playlist is already written.
658 if (content == written_playlist_)
659 return true;
660
661 auto file_path = std::filesystem::u8path(output_dir) / file_name_;
662 if (!File::WriteFileAtomically(file_path.string().c_str(), content)) {
663 LOG(ERROR) << "Failed to write master playlist to: " << file_path.string();
664 return false;
665 }
666 written_playlist_ = content;
667 return true;
668}
669
670} // namespace hls
671} // namespace shaka
MasterPlaylist(const std::filesystem::path &file_name, const std::string &default_audio_language, const std::string &default_text_language, const std::vector< CeaCaption > &closed_captions, const bool is_independent_segments, const bool create_session_keys=false)
virtual bool WriteMasterPlaylist(const std::string &base_url, const std::string &output_dir, const std::list< MediaPlaylist * > &playlists)
virtual bool GetAC4ImsFlag() const
virtual double GetFrameRate() const
virtual uint64_t AvgBitrate() const
virtual bool GetDisplayResolution(uint32_t *width, uint32_t *height) const
virtual int GetEC3JocComplexity() const
virtual uint64_t MaxBitrate() const
virtual int GetNumChannels() const
virtual std::string GetVideoRange() const
const std::string & language() const
virtual bool GetAC4CbiFlag() const
All the methods that are virtual are virtual for mocking.