7#include <packager/hls/base/master_playlist.h>
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>
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>
27#include "packager/kv_pairs/kv_pairs.h"
28#include "packager/utils/string_trim_split.h"
33const char* kDefaultAudioGroupId =
"default-audio-group";
34const char* kDefaultSubtitleGroupId =
"default-text-group";
35const char* kUnexpectedGroupId =
"unexpected-group";
37void AppendVersionString(std::string* content) {
38 const std::string version = GetPackagerVersion();
41 absl::StrAppendFormat(content,
"## Generated with %s version %s\n",
42 GetPackagerProjectUrl().c_str(), version.c_str());
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;
60 uint64_t max_audio_bitrate = 0;
61 uint64_t avg_audio_bitrate = 0;
64uint64_t GetMaximumMaxBitrate(
const std::list<const MediaPlaylist*> playlists) {
66 for (
const auto& playlist : playlists) {
67 max = std::max(max, playlist->MaxBitrate());
72uint64_t GetMaximumAvgBitrate(
const std::list<const MediaPlaylist*> playlists) {
74 for (
const auto& playlist : playlists) {
75 max = std::max(max, playlist->AvgBitrate());
80std::set<std::string> GetGroupCodecString(
81 const std::list<const MediaPlaylist*>& group) {
82 std::set<std::string> codecs;
84 for (
const MediaPlaylist* playlist : group) {
85 codecs.insert(playlist->codec());
93 auto wvtt = codecs.find(
"wvtt");
94 if (wvtt != codecs.end()) {
99 auto ttml = codecs.find(
"ttml");
100 if (ttml != codecs.end()) {
102 codecs.insert(
"stpp.ttml.im1t");
108std::list<Variant> AudioGroupsToVariants(
109 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
110 std::list<Variant> variants;
112 for (
const auto& group : groups) {
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);
119 variants.push_back(variant);
124 if (variants.empty()) {
125 variants.emplace_back();
131const char* GetGroupId(
const MediaPlaylist& playlist) {
132 const std::string& group_id = playlist.group_id();
134 if (!group_id.empty()) {
135 return group_id.c_str();
138 switch (playlist.stream_type()) {
139 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
140 return kDefaultAudioGroupId;
142 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
143 return kDefaultSubtitleGroupId;
146 return kUnexpectedGroupId;
150std::list<Variant> SubtitleGroupsToVariants(
151 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
152 std::list<Variant> variants;
154 for (
const auto& group : groups) {
156 variant.text_group_id = &group.first;
157 variant.text_codecs = GetGroupCodecString(group.second);
159 variants.push_back(variant);
164 if (variants.empty()) {
165 variants.emplace_back();
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*>>&
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);
180 DCHECK_GE(audio_variants.size(), 1u);
181 DCHECK_GE(subtitle_variants.size(), 1u);
183 std::list<Variant> merged;
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);
199 DCHECK_GE(merged.size(), 1u);
204void BuildStreamInfTag(
const MediaPlaylist& playlist,
205 const Variant& variant,
206 const std::string& base_url,
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";
216 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
217 tag_name =
"#EXT-X-I-FRAME-STREAM-INF";
220 NOTIMPLEMENTED() <<
"Cannot build STREAM-INFO tag for type "
221 <<
static_cast<int>(playlist.stream_type());
224 Tag tag(tag_name, out);
226 tag.AddNumber(
"BANDWIDTH", playlist.MaxBitrate() + variant.max_audio_bitrate);
227 tag.AddNumber(
"AVERAGE-BANDWIDTH",
228 playlist.AvgBitrate() + variant.avg_audio_bitrate);
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,
","));
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,
"/"));
250 const bool is_iframe_playlist =
251 playlist.stream_type() ==
252 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly;
254 if (playlist.GetDisplayResolution(&width, &height)) {
255 tag.AddNumberPair(
"RESOLUTION", width,
'x', height);
259 if (!is_iframe_playlist) {
260 const double frame_rate = playlist.GetFrameRate();
262 tag.AddFloat(
"FRAME-RATE", frame_rate);
265 const std::string video_range = playlist.GetVideoRange();
266 if (!video_range.empty())
267 tag.AddString(
"VIDEO-RANGE", video_range);
270 if (!is_iframe_playlist) {
271 if (variant.audio_group_id) {
272 tag.AddQuotedString(
"AUDIO", *variant.audio_group_id);
275 if (variant.text_group_id) {
276 tag.AddQuotedString(
"SUBTITLES", *variant.text_group_id);
279 if (variant.have_instream_closed_caption) {
280 tag.AddQuotedString(
"CLOSED-CAPTIONS",
"CC");
282 tag.AddString(
"CLOSED-CAPTIONS",
"NONE");
286 if (is_iframe_playlist) {
287 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
290 absl::StrAppendFormat(out,
"\n%s%s\n", base_url.c_str(),
291 playlist.file_name().c_str());
297void BuildMediaTag(
const MediaPlaylist& playlist,
298 const std::string& group_id,
301 const std::string& base_url,
306 Tag tag(
"#EXT-X-MEDIA", out);
309 switch (playlist.stream_type()) {
310 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
311 tag.AddString(
"TYPE",
"AUDIO");
314 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
315 tag.AddString(
"TYPE",
"SUBTITLES");
319 NOTIMPLEMENTED() <<
"Cannot build media tag for type "
320 <<
static_cast<int>(playlist.stream_type());
324 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
325 tag.AddQuotedString(
"GROUP-ID", group_id);
327 const std::string& language = playlist.language();
328 if (!language.empty()) {
329 tag.AddQuotedString(
"LANGUAGE", language);
332 tag.AddQuotedString(
"NAME", playlist.name());
335 tag.AddString(
"DEFAULT",
"YES");
337 tag.AddString(
"DEFAULT",
"NO");
340 tag.AddString(
"AUTOSELECT",
"YES");
343 if (playlist.stream_type() ==
344 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
345 playlist.forced_subtitle()) {
346 tag.AddString(
"FORCED",
"YES");
349 const std::vector<std::string>& characteristics = playlist.characteristics();
350 if (!characteristics.empty()) {
351 tag.AddQuotedString(
"CHARACTERISTICS", absl::StrJoin(characteristics,
","));
354 const MediaPlaylist::MediaPlaylistStreamType kAudio =
355 MediaPlaylist::MediaPlaylistStreamType::kAudio;
356 if (playlist.stream_type() == kAudio) {
357 if (playlist.GetEC3JocComplexity() != 0) {
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()) {
368 std::string channel_string =
369 std::to_string(playlist.GetNumChannels()) +
"/IMSA";
370 tag.AddQuotedString(
"CHANNELS", channel_string);
378 std::string channel_string = std::to_string(playlist.GetNumChannels());
379 tag.AddQuotedString(
"CHANNELS", channel_string);
386 std::list<std::pair<std::string, std::list<const MediaPlaylist*>>>& groups,
387 const std::string& default_language,
388 const std::string& base_url,
390 for (
const auto& group : groups) {
391 const std::string& group_id = group.first;
392 const auto& playlists = group.second;
405 std::set<std::string> languages;
407 for (
const auto& playlist : playlists) {
408 bool is_default =
false;
409 bool is_autoselect =
false;
411 if (playlist->is_dvs()) {
416 is_autoselect =
true;
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;
423 languages.insert(language);
427 if (playlist->stream_type() ==
428 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
429 playlist->forced_subtitle()) {
430 is_autoselect =
true;
433 BuildMediaTag(*playlist, group_id, is_default, is_autoselect, base_url,
439bool ListOrderFn(
const MediaPlaylist*& a,
const MediaPlaylist*& b) {
440 return a->GetMediaInfo().index() < b->GetMediaInfo().index();
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();
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);
459 if (caption.is_default)
460 tag.AddString(
"DEFAULT",
"YES");
462 tag.AddString(
"DEFAULT",
"NO");
463 if (caption.autoselect)
464 tag.AddString(
"AUTOSELECT",
"YES");
466 tag.AddString(
"AUTOSELECT",
"NO");
467 tag.AddQuotedString(
"INSTREAM-ID", caption.channel);
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;
483 bool has_index =
true;
485 for (
const MediaPlaylist* playlist : playlists) {
486 has_index = has_index && playlist->GetMediaInfo().has_index();
488 switch (playlist->stream_type()) {
489 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
490 audio_playlist_groups[GetGroupId(*playlist)].push_back(playlist);
492 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
493 video_playlists.push_back(playlist);
495 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
496 iframe_playlists.push_back(playlist);
498 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
499 subtitle_playlist_groups[GetGroupId(*playlist)].push_back(playlist);
502 NOTIMPLEMENTED() <<
static_cast<int>(playlist->stream_type())
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());
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);
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);
525 video_playlists.sort(ListOrderFn);
526 iframe_playlists.sort(ListOrderFn);
529 if (!audio_playlist_groups.empty()) {
530 content->append(
"\n");
531 BuildMediaTags(audio_groups_list, default_audio_language, base_url,
535 if (!subtitle_playlist_groups.empty()) {
536 content->append(
"\n");
537 BuildMediaTags(subtitle_groups_list, default_text_language, base_url,
541 if (!closed_captions.empty()) {
542 content->append(
"\n");
543 for (
const auto& caption : closed_captions) {
544 BuildCeaMediaTag(caption, content);
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())
554 content->append(
"\n");
555 for (
const auto& playlist : video_playlists) {
556 BuildStreamInfTag(*playlist, variant, base_url, content);
560 if (!iframe_playlists.empty()) {
561 content->append(
"\n");
562 for (
const auto& playlist : iframe_playlists) {
564 BuildStreamInfTag(*playlist, Variant(), base_url, content);
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) {
578 variant.audio_group_id = &playlist_group.first;
579 for (
const auto& playlist : playlist_group.second) {
580 BuildStreamInfTag(*playlist, variant, base_url, content);
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) {}
601MasterPlaylist::~MasterPlaylist() {}
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);
610 if (is_independent_segments_) {
611 content.append(
"\n#EXT-X-INDEPENDENT-SEGMENTS\n");
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 =
622 session_keys.emplace(
623 encryption_entry->ToString(
"#EXT-X-SESSION-KEY"));
628 for (
const auto& session_key : session_keys)
629 content.append(session_key +
"\n");
632 AppendPlaylists(default_audio_language_, default_text_language_,
633 closed_captions_, base_url, playlists, &content);
636 if (content == written_playlist_)
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();
644 written_playlist_ = content;
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.