Shaka Packager SDK
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 
25 namespace shaka {
26 namespace hls {
27 namespace {
28 const char* kDefaultAudioGroupId = "default-audio-group";
29 const char* kDefaultSubtitleGroupId = "default-text-group";
30 const char* kUnexpectedGroupId = "unexpected-group";
31 
32 void 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.
42 struct 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 
58 uint64_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 
66 uint64_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 
74 std::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 
102 std::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 
125 const 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 
144 std::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 
165 std::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 
197 void 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.
288 void 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 
376 void 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 
430 bool ListOrderFn(const MediaPlaylist*& a, const MediaPlaylist*& b) {
431  return a->GetMediaInfo().index() < b->GetMediaInfo().index();
432 }
433 
434 bool 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 
442 void 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 
550 MasterPlaylist::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 
561 MasterPlaylist::~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.
Definition: crypto_flags.cc:66