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_cation =
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_cation = 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,
"/"));
249 if (playlist.GetDisplayResolution(&width, &height)) {
250 tag.AddNumberPair(
"RESOLUTION", width,
'x', height);
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();
260 tag.AddFloat(
"FRAME-RATE", frame_rate);
263 const std::string video_range = playlist.GetVideoRange();
264 if (!video_range.empty())
265 tag.AddString(
"VIDEO-RANGE", video_range);
268 if (variant.audio_group_id) {
269 tag.AddQuotedString(
"AUDIO", *variant.audio_group_id);
272 if (variant.text_group_id) {
273 tag.AddQuotedString(
"SUBTITLES", *variant.text_group_id);
276 if (variant.have_instream_closed_cation) {
277 tag.AddQuotedString(
"CLOSED-CAPTIONS",
"CC");
279 tag.AddString(
"CLOSED-CAPTIONS",
"NONE");
282 if (playlist.stream_type() ==
283 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly) {
284 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
287 absl::StrAppendFormat(out,
"\n%s%s\n", base_url.c_str(),
288 playlist.file_name().c_str());
294void BuildMediaTag(
const MediaPlaylist& playlist,
295 const std::string& group_id,
298 const std::string& base_url,
303 Tag tag(
"#EXT-X-MEDIA", out);
306 switch (playlist.stream_type()) {
307 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
308 tag.AddString(
"TYPE",
"AUDIO");
311 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
312 tag.AddString(
"TYPE",
"SUBTITLES");
316 NOTIMPLEMENTED() <<
"Cannot build media tag for type "
317 <<
static_cast<int>(playlist.stream_type());
321 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
322 tag.AddQuotedString(
"GROUP-ID", group_id);
324 const std::string& language = playlist.language();
325 if (!language.empty()) {
326 tag.AddQuotedString(
"LANGUAGE", language);
329 tag.AddQuotedString(
"NAME", playlist.name());
332 tag.AddString(
"DEFAULT",
"YES");
334 tag.AddString(
"DEFAULT",
"NO");
337 tag.AddString(
"AUTOSELECT",
"YES");
340 if (playlist.stream_type() ==
341 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
342 playlist.forced_subtitle()) {
343 tag.AddString(
"FORCED",
"YES");
346 const std::vector<std::string>& characteristics = playlist.characteristics();
347 if (!characteristics.empty()) {
348 tag.AddQuotedString(
"CHARACTERISTICS", absl::StrJoin(characteristics,
","));
351 const MediaPlaylist::MediaPlaylistStreamType kAudio =
352 MediaPlaylist::MediaPlaylistStreamType::kAudio;
353 if (playlist.stream_type() == kAudio) {
354 if (playlist.GetEC3JocComplexity() != 0) {
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()) {
365 std::string channel_string =
366 std::to_string(playlist.GetNumChannels()) +
"/IMSA";
367 tag.AddQuotedString(
"CHANNELS", channel_string);
375 std::string channel_string = std::to_string(playlist.GetNumChannels());
376 tag.AddQuotedString(
"CHANNELS", channel_string);
383 std::list<std::pair<std::string, std::list<const MediaPlaylist*>>>& groups,
384 const std::string& default_language,
385 const std::string& base_url,
387 for (
const auto& group : groups) {
388 const std::string& group_id = group.first;
389 const auto& playlists = group.second;
402 std::set<std::string> languages;
404 for (
const auto& playlist : playlists) {
405 bool is_default =
false;
406 bool is_autoselect =
false;
408 if (playlist->is_dvs()) {
413 is_autoselect =
true;
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;
420 languages.insert(language);
424 if (playlist->stream_type() ==
425 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
426 playlist->forced_subtitle()) {
427 is_autoselect =
true;
430 BuildMediaTag(*playlist, group_id, is_default, is_autoselect, base_url,
436bool ListOrderFn(
const MediaPlaylist*& a,
const MediaPlaylist*& b) {
437 return a->GetMediaInfo().index() < b->GetMediaInfo().index();
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();
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);
456 if (caption.is_default)
457 tag.AddString(
"DEFAULT",
"YES");
459 tag.AddString(
"DEFAULT",
"NO");
460 if (caption.autoselect)
461 tag.AddString(
"AUTOSELECT",
"YES");
463 tag.AddString(
"AUTOSELECT",
"NO");
464 tag.AddQuotedString(
"INSTREAM-ID", caption.channel);
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;
480 bool has_index =
true;
482 for (
const MediaPlaylist* playlist : playlists) {
483 has_index = has_index && playlist->GetMediaInfo().has_index();
485 switch (playlist->stream_type()) {
486 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
487 audio_playlist_groups[GetGroupId(*playlist)].push_back(playlist);
489 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
490 video_playlists.push_back(playlist);
492 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
493 iframe_playlists.push_back(playlist);
495 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
496 subtitle_playlist_groups[GetGroupId(*playlist)].push_back(playlist);
499 NOTIMPLEMENTED() <<
static_cast<int>(playlist->stream_type())
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());
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);
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);
522 video_playlists.sort(ListOrderFn);
523 iframe_playlists.sort(ListOrderFn);
526 if (!audio_playlist_groups.empty()) {
527 content->append(
"\n");
528 BuildMediaTags(audio_groups_list, default_audio_language, base_url,
532 if (!subtitle_playlist_groups.empty()) {
533 content->append(
"\n");
534 BuildMediaTags(subtitle_groups_list, default_text_language, base_url,
538 if (!closed_captions.empty()) {
539 content->append(
"\n");
540 for (
const auto& caption : closed_captions) {
541 BuildCeaMediaTag(caption, content);
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())
551 content->append(
"\n");
552 for (
const auto& playlist : video_playlists) {
553 BuildStreamInfTag(*playlist, variant, base_url, content);
557 if (!iframe_playlists.empty()) {
558 content->append(
"\n");
559 for (
const auto& playlist : iframe_playlists) {
561 BuildStreamInfTag(*playlist, Variant(), base_url, content);
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) {
575 variant.audio_group_id = &playlist_group.first;
576 for (
const auto& playlist : playlist_group.second) {
577 BuildStreamInfTag(*playlist, variant, base_url, content);
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) {}
598MasterPlaylist::~MasterPlaylist() {}
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);
607 if (is_independent_segments_) {
608 content.append(
"\n#EXT-X-INDEPENDENT-SEGMENTS\n");
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) {
618 session_keys.emplace(encryption_entry->ToString(
"#EXT-X-SESSION-KEY"));
623 for (
const auto& session_key : session_keys)
624 content.append(session_key +
"\n");
627 AppendPlaylists(default_audio_language_, default_text_language_,
628 closed_captions_, base_url, playlists, &content);
631 if (content == written_playlist_)
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();
639 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.