7#include <packager/hls/base/master_playlist.h>
18#include <absl/log/check.h>
19#include <absl/log/log.h>
20#include <absl/strings/str_format.h>
21#include <absl/strings/str_join.h>
23#include <packager/cea_caption.h>
24#include <packager/file.h>
25#include <packager/hls/base/media_playlist.h>
26#include <packager/hls/base/tag.h>
27#include <packager/kv_pairs/kv_pairs.h>
28#include <packager/macros/logging.h>
29#include <packager/media/base/fourccs.h>
30#include <packager/version/version.h>
35const char* kDefaultAudioGroupId =
"default-audio-group";
36const char* kDefaultSubtitleGroupId =
"default-text-group";
37const char* kUnexpectedGroupId =
"unexpected-group";
39void AppendVersionString(std::string* content) {
40 const std::string version = GetPackagerVersion();
43 absl::StrAppendFormat(content,
"## Generated with %s version %s\n",
44 GetPackagerProjectUrl().c_str(), version.c_str());
50 std::set<std::string> audio_codecs;
51 std::set<std::string> text_codecs;
52 const std::string* audio_group_id =
nullptr;
53 const std::string* text_group_id =
nullptr;
54 bool have_instream_closed_caption =
false;
62 uint64_t max_audio_bitrate = 0;
63 uint64_t avg_audio_bitrate = 0;
70 const MediaPlaylist* playlist;
76uint64_t GetMaximumMaxBitrate(
const std::list<const MediaPlaylist*> playlists) {
78 for (
const auto& playlist : playlists) {
84uint64_t GetMaximumAvgBitrate(
const std::list<const MediaPlaylist*> playlists) {
86 for (
const auto& playlist : playlists) {
92std::set<std::string> GetGroupCodecString(
93 const std::list<const MediaPlaylist*>& group) {
94 std::set<std::string> codecs;
96 for (
const MediaPlaylist* playlist : group) {
97 codecs.insert(playlist->codec());
105 auto wvtt = codecs.find(
"wvtt");
106 if (wvtt != codecs.end()) {
111 auto ttml = codecs.find(
"ttml");
112 if (ttml != codecs.end()) {
114 codecs.insert(
"stpp.ttml.im1t");
120std::list<Variant> AudioGroupsToVariants(
121 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
122 std::list<Variant> variants;
124 for (
const auto& group : groups) {
126 variant.audio_group_id = &group.first;
127 variant.max_audio_bitrate = GetMaximumMaxBitrate(group.second);
128 variant.avg_audio_bitrate = GetMaximumAvgBitrate(group.second);
129 variant.audio_codecs = GetGroupCodecString(group.second);
131 variants.push_back(variant);
136 if (variants.empty()) {
137 variants.emplace_back();
143const char* GetGroupId(
const MediaPlaylist& playlist) {
144 const std::string& group_id = playlist.group_id();
146 if (!group_id.empty()) {
147 return group_id.c_str();
150 switch (playlist.stream_type()) {
151 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
152 return kDefaultAudioGroupId;
154 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
155 return kDefaultSubtitleGroupId;
158 return kUnexpectedGroupId;
162std::list<Variant> SubtitleGroupsToVariants(
163 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
164 std::list<Variant> variants;
166 for (
const auto& group : groups) {
168 variant.text_group_id = &group.first;
169 variant.text_codecs = GetGroupCodecString(group.second);
171 variants.push_back(variant);
176 if (variants.empty()) {
177 variants.emplace_back();
183std::list<Variant> BuildVariants(
184 const std::map<std::string, std::list<const MediaPlaylist*>>& audio_groups,
185 const std::map<std::string, std::list<const MediaPlaylist*>>&
187 const bool have_instream_closed_caption) {
188 std::list<Variant> audio_variants = AudioGroupsToVariants(audio_groups);
189 std::list<Variant> subtitle_variants =
190 SubtitleGroupsToVariants(subtitle_groups);
192 DCHECK_GE(audio_variants.size(), 1u);
193 DCHECK_GE(subtitle_variants.size(), 1u);
195 std::list<Variant> merged;
197 for (
const auto& audio_variant : audio_variants) {
198 for (
const auto& subtitle_variant : subtitle_variants) {
199 Variant base_variant;
200 base_variant.audio_codecs = audio_variant.audio_codecs;
201 base_variant.text_codecs = subtitle_variant.text_codecs;
202 base_variant.audio_group_id = audio_variant.audio_group_id;
203 base_variant.text_group_id = subtitle_variant.text_group_id;
204 base_variant.max_audio_bitrate = audio_variant.max_audio_bitrate;
205 base_variant.avg_audio_bitrate = audio_variant.avg_audio_bitrate;
206 base_variant.have_instream_closed_caption = have_instream_closed_caption;
207 merged.push_back(base_variant);
211 DCHECK_GE(merged.size(), 1u);
216void BuildStreamInfTag(
const MediaPlaylist& playlist,
217 const Variant& variant,
218 const std::string& base_url,
222 std::string tag_name;
223 switch (playlist.stream_type()) {
224 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
225 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
226 tag_name =
"#EXT-X-STREAM-INF";
228 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
229 tag_name =
"#EXT-X-I-FRAME-STREAM-INF";
232 NOTIMPLEMENTED() <<
"Cannot build STREAM-INFO tag for type "
233 <<
static_cast<int>(playlist.stream_type());
236 Tag tag(tag_name, out);
238 tag.AddNumber(
"BANDWIDTH", playlist.
MaxBitrate() + variant.max_audio_bitrate);
239 tag.AddNumber(
"AVERAGE-BANDWIDTH",
240 playlist.
AvgBitrate() + variant.avg_audio_bitrate);
242 std::vector<std::string> all_codecs;
243 all_codecs.push_back(playlist.codec());
244 all_codecs.insert(all_codecs.end(), variant.audio_codecs.begin(),
245 variant.audio_codecs.end());
246 all_codecs.insert(all_codecs.end(), variant.text_codecs.begin(),
247 variant.text_codecs.end());
248 tag.AddQuotedString(
"CODECS", absl::StrJoin(all_codecs,
","));
250 if (playlist.supplemental_codec() !=
"" &&
251 playlist.compatible_brand() != media::FOURCC_NULL) {
252 std::vector<std::string> supplemental_codecs;
253 supplemental_codecs.push_back(playlist.supplemental_codec());
254 supplemental_codecs.push_back(FourCCToString(playlist.compatible_brand()));
255 tag.AddQuotedString(
"SUPPLEMENTAL-CODECS",
256 absl::StrJoin(supplemental_codecs,
"/"));
262 const bool is_iframe_playlist =
263 playlist.stream_type() ==
264 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly;
267 tag.AddNumberPair(
"RESOLUTION", width,
'x', height);
271 if (!is_iframe_playlist) {
274 tag.AddFloat(
"FRAME-RATE", frame_rate);
278 if (!video_range.empty())
279 tag.AddString(
"VIDEO-RANGE", video_range);
282 if (!is_iframe_playlist) {
283 if (variant.audio_group_id) {
284 tag.AddQuotedString(
"AUDIO", *variant.audio_group_id);
287 if (variant.text_group_id) {
288 tag.AddQuotedString(
"SUBTITLES", *variant.text_group_id);
291 if (variant.have_instream_closed_caption) {
292 tag.AddQuotedString(
"CLOSED-CAPTIONS",
"CC");
294 tag.AddString(
"CLOSED-CAPTIONS",
"NONE");
298 if (is_iframe_playlist) {
299 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
302 absl::StrAppendFormat(out,
"\n%s%s\n", base_url.c_str(),
303 playlist.file_name().c_str());
309void BuildMediaTag(
const MediaPlaylist& playlist,
310 const std::string& group_id,
313 const std::string& base_url,
318 Tag tag(
"#EXT-X-MEDIA", out);
321 switch (playlist.stream_type()) {
322 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
323 tag.AddString(
"TYPE",
"AUDIO");
326 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
327 tag.AddString(
"TYPE",
"SUBTITLES");
331 NOTIMPLEMENTED() <<
"Cannot build media tag for type "
332 <<
static_cast<int>(playlist.stream_type());
336 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
337 tag.AddQuotedString(
"GROUP-ID", group_id);
339 const std::string& language = playlist.
language();
340 if (!language.empty()) {
341 tag.AddQuotedString(
"LANGUAGE", language);
344 tag.AddQuotedString(
"NAME", playlist.name());
347 tag.AddString(
"DEFAULT",
"YES");
349 tag.AddString(
"DEFAULT",
"NO");
352 tag.AddString(
"AUTOSELECT",
"YES");
355 if (playlist.stream_type() ==
356 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
357 playlist.forced_subtitle()) {
358 tag.AddString(
"FORCED",
"YES");
361 const std::vector<std::string>& characteristics = playlist.characteristics();
362 if (!characteristics.empty()) {
363 tag.AddQuotedString(
"CHARACTERISTICS", absl::StrJoin(characteristics,
","));
366 const MediaPlaylist::MediaPlaylistStreamType kAudio =
367 MediaPlaylist::MediaPlaylistStreamType::kAudio;
368 if (playlist.stream_type() == kAudio) {
373 std::string channel_string =
375 tag.AddQuotedString(
"CHANNELS", channel_string);
380 std::string channel_string =
382 tag.AddQuotedString(
"CHANNELS", channel_string);
390 std::string channel_string = std::to_string(playlist.
GetNumChannels());
391 tag.AddQuotedString(
"CHANNELS", channel_string);
397bool PlaylistOrderFn(
const MediaPlaylist*& a,
const MediaPlaylist*& b) {
398 return a->GetMediaInfo().index() < b->GetMediaInfo().index();
401bool MediaTagsOrderByIndexFn(
const MediaTags& a,
const MediaTags& b) {
402 return a.playlist->GetMediaInfo().index() <
403 b.playlist->GetMediaInfo().index();
406bool MediaTagsOrderByGroupIdFn(
const MediaTags& a,
const MediaTags& b) {
407 return a.group_id < b.group_id;
410void BuildCeaMediaTag(
const CeaCaption& caption, std::string* out) {
411 Tag tag(
"#EXT-X-MEDIA", out);
412 tag.AddString(
"TYPE",
"CLOSED-CAPTIONS");
413 tag.AddQuotedString(
"GROUP-ID",
"CC");
414 tag.AddQuotedString(
"NAME", caption.name);
415 if (!caption.language.empty()) {
416 tag.AddQuotedString(
"LANGUAGE", caption.language);
418 if (caption.is_default)
419 tag.AddString(
"DEFAULT",
"YES");
421 tag.AddString(
"DEFAULT",
"NO");
422 if (caption.autoselect)
423 tag.AddString(
"AUTOSELECT",
"YES");
425 tag.AddString(
"AUTOSELECT",
"NO");
426 tag.AddQuotedString(
"INSTREAM-ID", caption.channel);
430void AppendPlaylists(
const std::string& default_audio_language,
431 const std::string& default_text_language,
432 const std::vector<CeaCaption>& closed_captions,
433 const std::string& base_url,
434 const std::list<MediaPlaylist*>& playlists,
435 std::string* content) {
436 std::list<MediaTags> audio_playlists;
437 std::list<MediaTags> subtitle_playlists;
438 std::list<const MediaPlaylist*> video_playlists;
439 std::list<const MediaPlaylist*> iframe_playlists;
441 bool has_index =
true;
450 for (
const MediaPlaylist* playlist : playlists) {
451 has_index = has_index && playlist->GetMediaInfo().has_index();
453 switch (playlist->stream_type()) {
454 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
455 audio_playlists.push_back({playlist, GetGroupId(*playlist),
459 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
460 video_playlists.push_back(playlist);
462 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
463 iframe_playlists.push_back(playlist);
465 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
466 subtitle_playlists.push_back({playlist, GetGroupId(*playlist),
471 NOTIMPLEMENTED() <<
static_cast<int>(playlist->stream_type())
478 video_playlists.sort(PlaylistOrderFn);
479 iframe_playlists.sort(PlaylistOrderFn);
480 audio_playlists.sort(MediaTagsOrderByIndexFn);
481 subtitle_playlists.sort(MediaTagsOrderByIndexFn);
485 audio_playlists.sort(MediaTagsOrderByGroupIdFn);
486 subtitle_playlists.sort(MediaTagsOrderByGroupIdFn);
502 std::map<std::string, std::set<std::string>> audio_group_languages;
503 for (
auto& pl : audio_playlists) {
504 if (pl.playlist->is_dvs()) {
509 pl.is_autoselect =
true;
512 const std::string language = pl.playlist->language();
513 auto& languages = audio_group_languages[pl.group_id];
514 if (languages.find(language) == languages.end()) {
515 pl.is_default = !language.empty() && language == default_audio_language;
516 pl.is_autoselect =
true;
517 languages.insert(language);
521 std::map<std::string, std::set<std::string>> subtitle_group_languages;
522 for (
auto& pl : subtitle_playlists) {
523 const std::string language = pl.playlist->language();
524 auto& languages = subtitle_group_languages[pl.group_id];
525 if (languages.find(language) == languages.end()) {
526 pl.is_default = !language.empty() && language == default_text_language;
527 pl.is_autoselect =
true;
528 languages.insert(language);
530 if (pl.playlist->forced_subtitle()) {
531 pl.is_autoselect =
true;
535 if (!audio_playlists.empty()) {
536 content->append(
"\n");
537 for (
const auto& pl : audio_playlists) {
538 BuildMediaTag(*pl.playlist, pl.group_id, pl.is_default, pl.is_autoselect,
543 if (!subtitle_playlists.empty()) {
544 content->append(
"\n");
545 for (
const auto& pl : subtitle_playlists) {
546 BuildMediaTag(*pl.playlist, pl.group_id, pl.is_default, pl.is_autoselect,
555 std::map<std::string, std::list<const MediaPlaylist*>> audio_playlist_groups;
556 std::map<std::string, std::list<const MediaPlaylist*>>
557 subtitle_playlist_groups;
558 for (
const auto& pl : audio_playlists) {
559 audio_playlist_groups[pl.group_id].push_back(pl.playlist);
561 for (
const auto& pl : subtitle_playlists) {
562 subtitle_playlist_groups[pl.group_id].push_back(pl.playlist);
565 if (!closed_captions.empty()) {
566 content->append(
"\n");
567 for (
const auto& caption : closed_captions) {
568 BuildCeaMediaTag(caption, content);
572 std::list<Variant> variants =
573 BuildVariants(audio_playlist_groups, subtitle_playlist_groups,
574 !closed_captions.empty());
575 for (
const auto& variant : variants) {
576 if (video_playlists.empty())
578 content->append(
"\n");
579 for (
const auto& playlist : video_playlists) {
580 BuildStreamInfTag(*playlist, variant, base_url, content);
584 if (!iframe_playlists.empty()) {
585 content->append(
"\n");
586 for (
const auto& playlist : iframe_playlists) {
588 BuildStreamInfTag(*playlist, Variant(), base_url, content);
593 if (!audio_playlist_groups.empty() && video_playlists.empty() &&
594 subtitle_playlist_groups.empty()) {
595 content->append(
"\n");
596 for (
const auto& playlist_group : audio_playlist_groups) {
602 variant.audio_group_id = &playlist_group.first;
603 for (
const auto& playlist : playlist_group.second) {
604 BuildStreamInfTag(*playlist, variant, base_url, content);
613 const std::string& default_audio_language,
614 const std::string& default_text_language,
615 const std::vector<CeaCaption>& closed_captions,
616 bool is_independent_segments,
617 bool create_session_keys)
618 : file_name_(file_name),
619 default_audio_language_(default_audio_language),
620 default_text_language_(default_text_language),
621 closed_captions_(closed_captions),
622 is_independent_segments_(is_independent_segments),
623 create_session_keys_(create_session_keys) {}
625MasterPlaylist::~MasterPlaylist() {}
628 const std::string& base_url,
629 const std::string& output_dir,
630 const std::list<MediaPlaylist*>& playlists) {
631 std::string content =
"#EXTM3U\n";
632 AppendVersionString(&content);
634 if (is_independent_segments_) {
635 content.append(
"\n#EXT-X-INDEPENDENT-SEGMENTS\n");
639 if (create_session_keys_) {
640 std::set<std::string> session_keys;
641 for (
const auto& playlist : playlists) {
642 for (
const auto& entry : playlist->entries()) {
643 if (entry->type() == HlsEntry::EntryType::kExtKey) {
644 auto encryption_entry =
646 session_keys.emplace(
647 encryption_entry->ToString(
"#EXT-X-SESSION-KEY"));
652 for (
const auto& session_key : session_keys)
653 content.append(session_key +
"\n");
656 AppendPlaylists(default_audio_language_, default_text_language_,
657 closed_captions_, base_url, playlists, &content);
660 if (content == written_playlist_)
663 auto file_path = std::filesystem::u8path(output_dir) / file_name_;
664 if (!File::WriteFileAtomically(file_path.string().c_str(), content)) {
665 LOG(ERROR) <<
"Failed to write master playlist to: " << file_path.string();
668 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.