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_cation = 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_cation = 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 if (playlist.GetDisplayResolution(&width, &height)) {
250 tag.AddNumberPair("RESOLUTION", width, 'x', height);
251
252 // Right now the frame-rate returned may not be accurate in some scenarios.
253 // TODO(kqyang): Fix frame-rate computation.
254 const bool is_iframe_playlist =
255 playlist.stream_type() ==
256 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly;
257 if (!is_iframe_playlist) {
258 const double frame_rate = playlist.GetFrameRate();
259 if (frame_rate > 0)
260 tag.AddFloat("FRAME-RATE", frame_rate);
261 }
262
263 const std::string video_range = playlist.GetVideoRange();
264 if (!video_range.empty())
265 tag.AddString("VIDEO-RANGE", video_range);
266 }
267
268 if (variant.audio_group_id) {
269 tag.AddQuotedString("AUDIO", *variant.audio_group_id);
270 }
271
272 if (variant.text_group_id) {
273 tag.AddQuotedString("SUBTITLES", *variant.text_group_id);
274 }
275
276 if (variant.have_instream_closed_cation) {
277 tag.AddQuotedString("CLOSED-CAPTIONS", "CC");
278 } else {
279 tag.AddString("CLOSED-CAPTIONS", "NONE");
280 }
281
282 if (playlist.stream_type() ==
283 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) {
284 tag.AddQuotedString("URI", base_url + playlist.file_name());
285 out->append("\n");
286 } else {
287 absl::StrAppendFormat(out, "\n%s%s\n", base_url.c_str(),
288 playlist.file_name().c_str());
289 }
290}
291
292// Need to pass in |group_id| as it may have changed to a new default when
293// grouped with other playlists.
294void BuildMediaTag(const MediaPlaylist& playlist,
295 const std::string& group_id,
296 bool is_default,
297 bool is_autoselect,
298 const std::string& base_url,
299 std::string* out) {
300 // Tag attributes should follow the order as defined in
301 // https://tools.ietf.org/html/draft-pantos-http-live-streaming-23#section-3.5
302
303 Tag tag("#EXT-X-MEDIA", out);
304
305 // We should only be making media tags for audio and text.
306 switch (playlist.stream_type()) {
307 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
308 tag.AddString("TYPE", "AUDIO");
309 break;
310
311 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
312 tag.AddString("TYPE", "SUBTITLES");
313 break;
314
315 default:
316 NOTIMPLEMENTED() << "Cannot build media tag for type "
317 << static_cast<int>(playlist.stream_type());
318 break;
319 }
320
321 tag.AddQuotedString("URI", base_url + playlist.file_name());
322 tag.AddQuotedString("GROUP-ID", group_id);
323
324 const std::string& language = playlist.language();
325 if (!language.empty()) {
326 tag.AddQuotedString("LANGUAGE", language);
327 }
328
329 tag.AddQuotedString("NAME", playlist.name());
330
331 if (is_default) {
332 tag.AddString("DEFAULT", "YES");
333 } else {
334 tag.AddString("DEFAULT", "NO");
335 }
336 if (is_autoselect) {
337 tag.AddString("AUTOSELECT", "YES");
338 }
339
340 if (playlist.stream_type() ==
341 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
342 playlist.forced_subtitle()) {
343 tag.AddString("FORCED", "YES");
344 }
345
346 const std::vector<std::string>& characteristics = playlist.characteristics();
347 if (!characteristics.empty()) {
348 tag.AddQuotedString("CHARACTERISTICS", absl::StrJoin(characteristics, ","));
349 }
350
351 const MediaPlaylist::MediaPlaylistStreamType kAudio =
352 MediaPlaylist::MediaPlaylistStreamType::kAudio;
353 if (playlist.stream_type() == kAudio) {
354 if (playlist.GetEC3JocComplexity() != 0) {
355 // HLS Authoring Specification for Apple Devices Appendices documents how
356 // to handle Dolby Digital Plus JOC content.
357 // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices/hls_authoring_specification_for_apple_devices_appendices
358 std::string channel_string =
359 std::to_string(playlist.GetEC3JocComplexity()) + "/JOC";
360 tag.AddQuotedString("CHANNELS", channel_string);
361 } else if (playlist.GetAC4ImsFlag() || playlist.GetAC4CbiFlag()) {
362 // Dolby has qualified using IMSA to present AC4 immersive audio (IMS and
363 // CBI without object-based audio) for Dolby internal use only. IMSA is
364 // not included in any publicly-available specifications as of June, 2020.
365 std::string channel_string =
366 std::to_string(playlist.GetNumChannels()) + "/IMSA";
367 tag.AddQuotedString("CHANNELS", channel_string);
368 } else {
369 // According to HLS spec:
370 // https://tools.ietf.org/html/draft-pantos-hls-rfc8216bis 4.4.6.1.
371 // CHANNELS is a quoted-string that specifies an ordered,
372 // slash-separated ("/") list of parameters. The first parameter is a
373 // count of audio channels, and the second parameter identifies the
374 // encoding of object-based audio used by the Rendition.
375 std::string channel_string = std::to_string(playlist.GetNumChannels());
376 tag.AddQuotedString("CHANNELS", channel_string);
377 }
378 }
379 out->append("\n");
380}
381
382void BuildMediaTags(
383 std::list<std::pair<std::string, std::list<const MediaPlaylist*>>>& groups,
384 const std::string& default_language,
385 const std::string& base_url,
386 std::string* out) {
387 for (const auto& group : groups) {
388 const std::string& group_id = group.first;
389 const auto& playlists = group.second;
390
391 // Tracks the language of the playlist in this group.
392 // According to HLS spec: https://goo.gl/MiqjNd 4.3.4.1.1. Rendition Groups
393 // - A Group MUST NOT have more than one member with a DEFAULT attribute of
394 // YES.
395 // - Each EXT-X-MEDIA tag with an AUTOSELECT=YES attribute SHOULD have a
396 // combination of LANGUAGE[RFC5646], ASSOC-LANGUAGE, FORCED, and
397 // CHARACTERISTICS attributes that is distinct from those of other
398 // AUTOSELECT=YES members of its Group.
399 // We tag the first rendition encountered with a particular language with
400 // 'AUTOSELECT'; it is tagged with 'DEFAULT' too if the language matches
401 // |default_language_|.
402 std::set<std::string> languages;
403
404 for (const auto& playlist : playlists) {
405 bool is_default = false;
406 bool is_autoselect = false;
407
408 if (playlist->is_dvs()) {
409 // According to HLS Authoring Specification for Apple Devices
410 // https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices#overview
411 // section 2.13 If you provide DVS, the AUTOSELECT attribute MUST have
412 // a value of "YES".
413 is_autoselect = true;
414 } else {
415 const std::string language = playlist->language();
416 if (languages.find(language) == languages.end()) {
417 is_default = !language.empty() && language == default_language;
418 is_autoselect = true;
419
420 languages.insert(language);
421 }
422 }
423
424 if (playlist->stream_type() ==
425 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
426 playlist->forced_subtitle()) {
427 is_autoselect = true;
428 }
429
430 BuildMediaTag(*playlist, group_id, is_default, is_autoselect, base_url,
431 out);
432 }
433 }
434}
435
436bool ListOrderFn(const MediaPlaylist*& a, const MediaPlaylist*& b) {
437 return a->GetMediaInfo().index() < b->GetMediaInfo().index();
438}
439
440bool GroupOrderFn(std::pair<std::string, std::list<const MediaPlaylist*>>& a,
441 std::pair<std::string, std::list<const MediaPlaylist*>>& b) {
442 a.second.sort(ListOrderFn);
443 b.second.sort(ListOrderFn);
444 return a.second.front()->GetMediaInfo().index() <
445 b.second.front()->GetMediaInfo().index();
446}
447
448void BuildCeaMediaTag(const CeaCaption& caption, std::string* out) {
449 Tag tag("#EXT-X-MEDIA", out);
450 tag.AddString("TYPE", "CLOSED-CAPTIONS");
451 tag.AddQuotedString("GROUP-ID", "CC");
452 tag.AddQuotedString("NAME", caption.name);
453 if (!caption.language.empty()) {
454 tag.AddQuotedString("LANGUAGE", caption.language);
455 }
456 if (caption.is_default)
457 tag.AddString("DEFAULT", "YES");
458 else
459 tag.AddString("DEFAULT", "NO");
460 if (caption.autoselect)
461 tag.AddString("AUTOSELECT", "YES");
462 else
463 tag.AddString("AUTOSELECT", "NO");
464 tag.AddQuotedString("INSTREAM-ID", caption.channel);
465 out->append("\n");
466}
467
468void AppendPlaylists(const std::string& default_audio_language,
469 const std::string& default_text_language,
470 const std::vector<CeaCaption>& closed_captions,
471 const std::string& base_url,
472 const std::list<MediaPlaylist*>& playlists,
473 std::string* content) {
474 std::map<std::string, std::list<const MediaPlaylist*>> audio_playlist_groups;
475 std::map<std::string, std::list<const MediaPlaylist*>>
476 subtitle_playlist_groups;
477 std::list<const MediaPlaylist*> video_playlists;
478 std::list<const MediaPlaylist*> iframe_playlists;
479
480 bool has_index = true;
481
482 for (const MediaPlaylist* playlist : playlists) {
483 has_index = has_index && playlist->GetMediaInfo().has_index();
484
485 switch (playlist->stream_type()) {
486 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
487 audio_playlist_groups[GetGroupId(*playlist)].push_back(playlist);
488 break;
489 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
490 video_playlists.push_back(playlist);
491 break;
492 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
493 iframe_playlists.push_back(playlist);
494 break;
495 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
496 subtitle_playlist_groups[GetGroupId(*playlist)].push_back(playlist);
497 break;
498 default:
499 NOTIMPLEMENTED() << static_cast<int>(playlist->stream_type())
500 << " not handled.";
501 }
502 }
503
504 // convert the std::map to std::list and reorder it if indexes were provided
505 std::list<std::pair<std::string, std::list<const MediaPlaylist*>>>
506 audio_groups_list(audio_playlist_groups.begin(),
507 audio_playlist_groups.end());
508 std::list<std::pair<std::string, std::list<const MediaPlaylist*>>>
509 subtitle_groups_list(subtitle_playlist_groups.begin(),
510 subtitle_playlist_groups.end());
511 if (has_index) {
512 audio_groups_list.sort(GroupOrderFn);
513 for (const auto& group : audio_groups_list) {
514 std::list<const MediaPlaylist*> group_playlists = group.second;
515 group_playlists.sort(ListOrderFn);
516 }
517 subtitle_groups_list.sort(GroupOrderFn);
518 for (const auto& group : subtitle_groups_list) {
519 std::list<const MediaPlaylist*> group_playlists = group.second;
520 group_playlists.sort(ListOrderFn);
521 }
522 video_playlists.sort(ListOrderFn);
523 iframe_playlists.sort(ListOrderFn);
524 }
525
526 if (!audio_playlist_groups.empty()) {
527 content->append("\n");
528 BuildMediaTags(audio_groups_list, default_audio_language, base_url,
529 content);
530 }
531
532 if (!subtitle_playlist_groups.empty()) {
533 content->append("\n");
534 BuildMediaTags(subtitle_groups_list, default_text_language, base_url,
535 content);
536 }
537
538 if (!closed_captions.empty()) {
539 content->append("\n");
540 for (const auto& caption : closed_captions) {
541 BuildCeaMediaTag(caption, content);
542 }
543 }
544
545 std::list<Variant> variants =
546 BuildVariants(audio_playlist_groups, subtitle_playlist_groups,
547 !closed_captions.empty());
548 for (const auto& variant : variants) {
549 if (video_playlists.empty())
550 break;
551 content->append("\n");
552 for (const auto& playlist : video_playlists) {
553 BuildStreamInfTag(*playlist, variant, base_url, content);
554 }
555 }
556
557 if (!iframe_playlists.empty()) {
558 content->append("\n");
559 for (const auto& playlist : iframe_playlists) {
560 // I-Frame playlists do not have variant. Just use the default.
561 BuildStreamInfTag(*playlist, Variant(), base_url, content);
562 }
563 }
564
565 // Generate audio-only master playlist when there are no videos and subtitles.
566 if (!audio_playlist_groups.empty() && video_playlists.empty() &&
567 subtitle_playlist_groups.empty()) {
568 content->append("\n");
569 for (const auto& playlist_group : audio_groups_list) {
570 Variant variant;
571 // Populate |audio_group_id|, which will be propagated to "AUDIO" field.
572 // Leaving other fields, e.g. xxx_audio_bitrate in |Variant|, as
573 // null/empty/zero intentionally as the information is already available
574 // in audio |playlist|.
575 variant.audio_group_id = &playlist_group.first;
576 for (const auto& playlist : playlist_group.second) {
577 BuildStreamInfTag(*playlist, variant, base_url, content);
578 }
579 }
580 }
581}
582
583} // namespace
584
585MasterPlaylist::MasterPlaylist(const std::filesystem::path& file_name,
586 const std::string& default_audio_language,
587 const std::string& default_text_language,
588 const std::vector<CeaCaption>& closed_captions,
589 bool is_independent_segments,
590 bool create_session_keys)
591 : file_name_(file_name),
592 default_audio_language_(default_audio_language),
593 default_text_language_(default_text_language),
594 closed_captions_(closed_captions),
595 is_independent_segments_(is_independent_segments),
596 create_session_keys_(create_session_keys) {}
597
598MasterPlaylist::~MasterPlaylist() {}
599
601 const std::string& base_url,
602 const std::string& output_dir,
603 const std::list<MediaPlaylist*>& playlists) {
604 std::string content = "#EXTM3U\n";
605 AppendVersionString(&content);
606
607 if (is_independent_segments_) {
608 content.append("\n#EXT-X-INDEPENDENT-SEGMENTS\n");
609 }
610
611 // Iterate over the playlists and add the session keys to the master playlist.
612 if (create_session_keys_) {
613 std::set<std::string> session_keys;
614 for (const auto& playlist : playlists) {
615 for (const auto& entry : playlist->entries()) {
616 if (entry->type() == HlsEntry::EntryType::kExtKey) {
617 auto encryption_entry = dynamic_cast<EncryptionInfoEntry*>(entry.get());
618 session_keys.emplace(encryption_entry->ToString("#EXT-X-SESSION-KEY"));
619 }
620 }
621 }
622 // session_keys will now contain all the unique session keys.
623 for (const auto& session_key : session_keys)
624 content.append(session_key + "\n");
625 }
626
627 AppendPlaylists(default_audio_language_, default_text_language_,
628 closed_captions_, base_url, playlists, &content);
629
630 // Skip if the playlist is already written.
631 if (content == written_playlist_)
632 return true;
633
634 auto file_path = std::filesystem::u8path(output_dir) / file_name_;
635 if (!File::WriteFileAtomically(file_path.string().c_str(), content)) {
636 LOG(ERROR) << "Failed to write master playlist to: " << file_path.string();
637 return false;
638 }
639 written_playlist_ = content;
640 return true;
641}
642
643} // namespace hls
644} // 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.