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