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