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