7 #include <packager/hls/base/master_playlist.h>
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>
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>
28 const char* kDefaultAudioGroupId =
"default-audio-group";
29 const char* kDefaultSubtitleGroupId =
"default-text-group";
30 const char* kUnexpectedGroupId =
"unexpected-group";
32 void AppendVersionString(std::string* content) {
33 const std::string version = GetPackagerVersion();
36 absl::StrAppendFormat(content,
"## Generated with %s version %s\n",
37 GetPackagerProjectUrl().c_str(), version.c_str());
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;
54 uint64_t max_audio_bitrate = 0;
55 uint64_t avg_audio_bitrate = 0;
58 uint64_t GetMaximumMaxBitrate(
const std::list<const MediaPlaylist*> playlists) {
60 for (
const auto& playlist : playlists) {
61 max = std::max(max, playlist->MaxBitrate());
66 uint64_t GetMaximumAvgBitrate(
const std::list<const MediaPlaylist*> playlists) {
68 for (
const auto& playlist : playlists) {
69 max = std::max(max, playlist->AvgBitrate());
74 std::set<std::string> GetGroupCodecString(
75 const std::list<const MediaPlaylist*>& group) {
76 std::set<std::string> codecs;
78 for (
const MediaPlaylist* playlist : group) {
79 codecs.insert(playlist->codec());
87 auto wvtt = codecs.find(
"wvtt");
88 if (wvtt != codecs.end()) {
93 auto ttml = codecs.find(
"ttml");
94 if (ttml != codecs.end()) {
96 codecs.insert(
"stpp.ttml.im1t");
102 std::list<Variant> AudioGroupsToVariants(
103 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
104 std::list<Variant> variants;
106 for (
const auto& group : groups) {
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);
113 variants.push_back(variant);
118 if (variants.empty()) {
119 variants.emplace_back();
125 const char* GetGroupId(
const MediaPlaylist& playlist) {
126 const std::string& group_id = playlist.group_id();
128 if (!group_id.empty()) {
129 return group_id.c_str();
132 switch (playlist.stream_type()) {
133 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
134 return kDefaultAudioGroupId;
136 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
137 return kDefaultSubtitleGroupId;
140 return kUnexpectedGroupId;
144 std::list<Variant> SubtitleGroupsToVariants(
145 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
146 std::list<Variant> variants;
148 for (
const auto& group : groups) {
150 variant.text_group_id = &group.first;
151 variant.text_codecs = GetGroupCodecString(group.second);
153 variants.push_back(variant);
158 if (variants.empty()) {
159 variants.emplace_back();
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*>>&
169 std::list<Variant> audio_variants = AudioGroupsToVariants(audio_groups);
170 std::list<Variant> subtitle_variants =
171 SubtitleGroupsToVariants(subtitle_groups);
173 DCHECK_GE(audio_variants.size(), 1u);
174 DCHECK_GE(subtitle_variants.size(), 1u);
176 std::list<Variant> merged;
178 for (
const auto& audio_variant : audio_variants) {
179 for (
const auto& subtitle_variant : subtitle_variants) {
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;
188 merged.push_back(variant);
192 DCHECK_GE(merged.size(), 1u);
197 void BuildStreamInfTag(
const MediaPlaylist& playlist,
198 const Variant& variant,
199 const std::string& base_url,
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";
209 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
210 tag_name =
"#EXT-X-I-FRAME-STREAM-INF";
213 NOTIMPLEMENTED() <<
"Cannot build STREAM-INFO tag for type "
214 <<
static_cast<int>(playlist.stream_type());
217 Tag tag(tag_name, out);
219 tag.AddNumber(
"BANDWIDTH", playlist.MaxBitrate() + variant.max_audio_bitrate);
220 tag.AddNumber(
"AVERAGE-BANDWIDTH",
221 playlist.AvgBitrate() + variant.avg_audio_bitrate);
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,
","));
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,
"/"));
242 if (playlist.GetDisplayResolution(&width, &height)) {
243 tag.AddNumberPair(
"RESOLUTION", width,
'x', height);
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();
253 tag.AddFloat(
"FRAME-RATE", frame_rate);
256 const std::string video_range = playlist.GetVideoRange();
257 if (!video_range.empty())
258 tag.AddString(
"VIDEO-RANGE", video_range);
261 if (variant.audio_group_id) {
262 tag.AddQuotedString(
"AUDIO", *variant.audio_group_id);
265 if (variant.text_group_id) {
266 tag.AddQuotedString(
"SUBTITLES", *variant.text_group_id);
274 tag.AddString(
"CLOSED-CAPTIONS",
"NONE");
276 if (playlist.stream_type() ==
277 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) {
278 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
281 absl::StrAppendFormat(out,
"\n%s%s\n", base_url.c_str(),
282 playlist.file_name().c_str());
288 void BuildMediaTag(
const MediaPlaylist& playlist,
289 const std::string& group_id,
292 const std::string& base_url,
297 Tag tag(
"#EXT-X-MEDIA", out);
300 switch (playlist.stream_type()) {
301 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
302 tag.AddString(
"TYPE",
"AUDIO");
305 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
306 tag.AddString(
"TYPE",
"SUBTITLES");
310 NOTIMPLEMENTED() <<
"Cannot build media tag for type "
311 <<
static_cast<int>(playlist.stream_type());
315 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
316 tag.AddQuotedString(
"GROUP-ID", group_id);
318 const std::string& language = playlist.language();
319 if (!language.empty()) {
320 tag.AddQuotedString(
"LANGUAGE", language);
323 tag.AddQuotedString(
"NAME", playlist.name());
326 tag.AddString(
"DEFAULT",
"YES");
328 tag.AddString(
"DEFAULT",
"NO");
331 tag.AddString(
"AUTOSELECT",
"YES");
334 if (playlist.stream_type() ==
335 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
336 playlist.forced_subtitle()) {
337 tag.AddString(
"FORCED",
"YES");
340 const std::vector<std::string>& characteristics = playlist.characteristics();
341 if (!characteristics.empty()) {
342 tag.AddQuotedString(
"CHARACTERISTICS", absl::StrJoin(characteristics,
","));
345 const MediaPlaylist::MediaPlaylistStreamType kAudio =
346 MediaPlaylist::MediaPlaylistStreamType::kAudio;
347 if (playlist.stream_type() == kAudio) {
348 if (playlist.GetEC3JocComplexity() != 0) {
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()) {
359 std::string channel_string =
360 std::to_string(playlist.GetNumChannels()) +
"/IMSA";
361 tag.AddQuotedString(
"CHANNELS", channel_string);
369 std::string channel_string = std::to_string(playlist.GetNumChannels());
370 tag.AddQuotedString(
"CHANNELS", channel_string);
377 std::list<std::pair<std::string, std::list<const MediaPlaylist*>>>& groups,
378 const std::string& default_language,
379 const std::string& base_url,
381 for (
const auto& group : groups) {
382 const std::string& group_id = group.first;
383 const auto& playlists = group.second;
396 std::set<std::string> languages;
398 for (
const auto& playlist : playlists) {
399 bool is_default =
false;
400 bool is_autoselect =
false;
402 if (playlist->is_dvs()) {
407 is_autoselect =
true;
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;
414 languages.insert(language);
418 if (playlist->stream_type() ==
419 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
420 playlist->forced_subtitle()) {
421 is_autoselect =
true;
424 BuildMediaTag(*playlist, group_id, is_default, is_autoselect, base_url,
430 bool ListOrderFn(
const MediaPlaylist*& a,
const MediaPlaylist*& b) {
431 return a->GetMediaInfo().index() < b->GetMediaInfo().index();
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();
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;
453 bool has_index =
true;
455 for (
const MediaPlaylist* playlist : playlists) {
456 has_index = has_index && playlist->GetMediaInfo().has_index();
458 switch (playlist->stream_type()) {
459 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
460 audio_playlist_groups[GetGroupId(*playlist)].push_back(playlist);
462 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
463 video_playlists.push_back(playlist);
465 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
466 iframe_playlists.push_back(playlist);
468 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
469 subtitle_playlist_groups[GetGroupId(*playlist)].push_back(playlist);
472 NOTIMPLEMENTED() <<
static_cast<int>(playlist->stream_type())
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());
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);
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);
495 video_playlists.sort(ListOrderFn);
496 iframe_playlists.sort(ListOrderFn);
499 if (!audio_playlist_groups.empty()) {
500 content->append(
"\n");
501 BuildMediaTags(audio_groups_list, default_audio_language, base_url,
505 if (!subtitle_playlist_groups.empty()) {
506 content->append(
"\n");
507 BuildMediaTags(subtitle_groups_list, default_text_language, base_url,
511 std::list<Variant> variants =
512 BuildVariants(audio_playlist_groups, subtitle_playlist_groups);
513 for (
const auto& variant : variants) {
514 if (video_playlists.empty())
516 content->append(
"\n");
517 for (
const auto& playlist : video_playlists) {
518 BuildStreamInfTag(*playlist, variant, base_url, content);
522 if (!iframe_playlists.empty()) {
523 content->append(
"\n");
524 for (
const auto& playlist : iframe_playlists) {
526 BuildStreamInfTag(*playlist, Variant(), base_url, content);
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) {
540 variant.audio_group_id = &playlist_group.first;
541 for (
const auto& playlist : playlist_group.second) {
542 BuildStreamInfTag(*playlist, variant, base_url, content);
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) {}
561 MasterPlaylist::~MasterPlaylist() {}
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);
570 if (is_independent_segments_) {
571 content.append(
"\n#EXT-X-INDEPENDENT-SEGMENTS\n");
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) {
581 session_keys.emplace(encryption_entry->ToString(
"#EXT-X-SESSION-KEY"));
586 for (
const auto& session_key : session_keys)
587 content.append(session_key +
"\n");
590 AppendPlaylists(default_audio_language_, default_text_language_, base_url,
591 playlists, &content);
594 if (content == written_playlist_)
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();
602 written_playlist_ = content;
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.