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;
68 const MediaPlaylist* playlist;
74uint64_t GetMaximumMaxBitrate(
const std::list<const MediaPlaylist*> playlists) {
76 for (
const auto& playlist : playlists) {
82uint64_t GetMaximumAvgBitrate(
const std::list<const MediaPlaylist*> playlists) {
84 for (
const auto& playlist : playlists) {
90std::set<std::string> GetGroupCodecString(
91 const std::list<const MediaPlaylist*>& group) {
92 std::set<std::string> codecs;
94 for (
const MediaPlaylist* playlist : group) {
95 codecs.insert(playlist->codec());
103 auto wvtt = codecs.find(
"wvtt");
104 if (wvtt != codecs.end()) {
109 auto ttml = codecs.find(
"ttml");
110 if (ttml != codecs.end()) {
112 codecs.insert(
"stpp.ttml.im1t");
118std::list<Variant> AudioGroupsToVariants(
119 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
120 std::list<Variant> variants;
122 for (
const auto& group : groups) {
124 variant.audio_group_id = &group.first;
125 variant.max_audio_bitrate = GetMaximumMaxBitrate(group.second);
126 variant.avg_audio_bitrate = GetMaximumAvgBitrate(group.second);
127 variant.audio_codecs = GetGroupCodecString(group.second);
129 variants.push_back(variant);
134 if (variants.empty()) {
135 variants.emplace_back();
141const char* GetGroupId(
const MediaPlaylist& playlist) {
142 const std::string& group_id = playlist.group_id();
144 if (!group_id.empty()) {
145 return group_id.c_str();
148 switch (playlist.stream_type()) {
149 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
150 return kDefaultAudioGroupId;
152 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
153 return kDefaultSubtitleGroupId;
156 return kUnexpectedGroupId;
160std::list<Variant> SubtitleGroupsToVariants(
161 const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
162 std::list<Variant> variants;
164 for (
const auto& group : groups) {
166 variant.text_group_id = &group.first;
167 variant.text_codecs = GetGroupCodecString(group.second);
169 variants.push_back(variant);
174 if (variants.empty()) {
175 variants.emplace_back();
181std::list<Variant> BuildVariants(
182 const std::map<std::string, std::list<const MediaPlaylist*>>& audio_groups,
183 const std::map<std::string, std::list<const MediaPlaylist*>>&
185 const bool have_instream_closed_caption) {
186 std::list<Variant> audio_variants = AudioGroupsToVariants(audio_groups);
187 std::list<Variant> subtitle_variants =
188 SubtitleGroupsToVariants(subtitle_groups);
190 DCHECK_GE(audio_variants.size(), 1u);
191 DCHECK_GE(subtitle_variants.size(), 1u);
193 std::list<Variant> merged;
195 for (
const auto& audio_variant : audio_variants) {
196 for (
const auto& subtitle_variant : subtitle_variants) {
197 Variant base_variant;
198 base_variant.audio_codecs = audio_variant.audio_codecs;
199 base_variant.text_codecs = subtitle_variant.text_codecs;
200 base_variant.audio_group_id = audio_variant.audio_group_id;
201 base_variant.text_group_id = subtitle_variant.text_group_id;
202 base_variant.max_audio_bitrate = audio_variant.max_audio_bitrate;
203 base_variant.avg_audio_bitrate = audio_variant.avg_audio_bitrate;
204 base_variant.have_instream_closed_caption = have_instream_closed_caption;
205 merged.push_back(base_variant);
209 DCHECK_GE(merged.size(), 1u);
214void BuildStreamInfTag(
const MediaPlaylist& playlist,
215 const Variant& variant,
216 const std::string& base_url,
220 std::string tag_name;
221 switch (playlist.stream_type()) {
222 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
223 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
224 tag_name =
"#EXT-X-STREAM-INF";
226 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
227 tag_name =
"#EXT-X-I-FRAME-STREAM-INF";
230 NOTIMPLEMENTED() <<
"Cannot build STREAM-INFO tag for type "
231 <<
static_cast<int>(playlist.stream_type());
234 Tag tag(tag_name, out);
236 tag.AddNumber(
"BANDWIDTH", playlist.
MaxBitrate() + variant.max_audio_bitrate);
237 tag.AddNumber(
"AVERAGE-BANDWIDTH",
238 playlist.
AvgBitrate() + variant.avg_audio_bitrate);
240 std::vector<std::string> all_codecs;
241 all_codecs.push_back(playlist.codec());
242 all_codecs.insert(all_codecs.end(), variant.audio_codecs.begin(),
243 variant.audio_codecs.end());
244 all_codecs.insert(all_codecs.end(), variant.text_codecs.begin(),
245 variant.text_codecs.end());
246 tag.AddQuotedString(
"CODECS", absl::StrJoin(all_codecs,
","));
248 if (playlist.supplemental_codec() !=
"" &&
249 playlist.compatible_brand() != media::FOURCC_NULL) {
250 std::vector<std::string> supplemental_codecs;
251 supplemental_codecs.push_back(playlist.supplemental_codec());
252 supplemental_codecs.push_back(FourCCToString(playlist.compatible_brand()));
253 tag.AddQuotedString(
"SUPPLEMENTAL-CODECS",
254 absl::StrJoin(supplemental_codecs,
"/"));
260 const bool is_iframe_playlist =
261 playlist.stream_type() ==
262 MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly;
265 tag.AddNumberPair(
"RESOLUTION", width,
'x', height);
269 if (!is_iframe_playlist) {
272 tag.AddFloat(
"FRAME-RATE", frame_rate);
276 if (!video_range.empty())
277 tag.AddString(
"VIDEO-RANGE", video_range);
280 if (!is_iframe_playlist) {
281 if (variant.audio_group_id) {
282 tag.AddQuotedString(
"AUDIO", *variant.audio_group_id);
285 if (variant.text_group_id) {
286 tag.AddQuotedString(
"SUBTITLES", *variant.text_group_id);
289 if (variant.have_instream_closed_caption) {
290 tag.AddQuotedString(
"CLOSED-CAPTIONS",
"CC");
292 tag.AddString(
"CLOSED-CAPTIONS",
"NONE");
296 if (is_iframe_playlist) {
297 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
300 absl::StrAppendFormat(out,
"\n%s%s\n", base_url.c_str(),
301 playlist.file_name().c_str());
307void BuildMediaTag(
const MediaPlaylist& playlist,
308 const std::string& group_id,
311 const std::string& base_url,
316 Tag tag(
"#EXT-X-MEDIA", out);
319 switch (playlist.stream_type()) {
320 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
321 tag.AddString(
"TYPE",
"AUDIO");
324 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
325 tag.AddString(
"TYPE",
"SUBTITLES");
329 NOTIMPLEMENTED() <<
"Cannot build media tag for type "
330 <<
static_cast<int>(playlist.stream_type());
334 tag.AddQuotedString(
"URI", base_url + playlist.file_name());
335 tag.AddQuotedString(
"GROUP-ID", group_id);
337 const std::string& language = playlist.
language();
338 if (!language.empty()) {
339 tag.AddQuotedString(
"LANGUAGE", language);
342 tag.AddQuotedString(
"NAME", playlist.name());
345 tag.AddString(
"DEFAULT",
"YES");
347 tag.AddString(
"DEFAULT",
"NO");
350 tag.AddString(
"AUTOSELECT",
"YES");
353 if (playlist.stream_type() ==
354 MediaPlaylist::MediaPlaylistStreamType::kSubtitle &&
355 playlist.forced_subtitle()) {
356 tag.AddString(
"FORCED",
"YES");
359 const std::vector<std::string>& characteristics = playlist.characteristics();
360 if (!characteristics.empty()) {
361 tag.AddQuotedString(
"CHARACTERISTICS", absl::StrJoin(characteristics,
","));
364 const MediaPlaylist::MediaPlaylistStreamType kAudio =
365 MediaPlaylist::MediaPlaylistStreamType::kAudio;
366 if (playlist.stream_type() == kAudio) {
371 std::string channel_string =
373 tag.AddQuotedString(
"CHANNELS", channel_string);
378 std::string channel_string =
380 tag.AddQuotedString(
"CHANNELS", channel_string);
388 std::string channel_string = std::to_string(playlist.
GetNumChannels());
389 tag.AddQuotedString(
"CHANNELS", channel_string);
395bool PlaylistOrderFn(
const MediaPlaylist*& a,
const MediaPlaylist*& b) {
396 return a->GetMediaInfo().index() < b->GetMediaInfo().index();
399bool MediaTagsOrderByIndexFn(
const MediaTags& a,
const MediaTags& b) {
400 return a.playlist->GetMediaInfo().index() <
401 b.playlist->GetMediaInfo().index();
404bool MediaTagsOrderByGroupIdFn(
const MediaTags& a,
const MediaTags& b) {
405 return a.group_id < b.group_id;
408void BuildCeaMediaTag(
const CeaCaption& caption, std::string* out) {
409 Tag tag(
"#EXT-X-MEDIA", out);
410 tag.AddString(
"TYPE",
"CLOSED-CAPTIONS");
411 tag.AddQuotedString(
"GROUP-ID",
"CC");
412 tag.AddQuotedString(
"NAME", caption.name);
413 if (!caption.language.empty()) {
414 tag.AddQuotedString(
"LANGUAGE", caption.language);
416 if (caption.is_default)
417 tag.AddString(
"DEFAULT",
"YES");
419 tag.AddString(
"DEFAULT",
"NO");
420 if (caption.autoselect)
421 tag.AddString(
"AUTOSELECT",
"YES");
423 tag.AddString(
"AUTOSELECT",
"NO");
424 tag.AddQuotedString(
"INSTREAM-ID", caption.channel);
428void AppendPlaylists(
const std::string& default_audio_language,
429 const std::string& default_text_language,
430 const std::vector<CeaCaption>& closed_captions,
431 const std::string& base_url,
432 const std::list<MediaPlaylist*>& playlists,
433 std::string* content) {
434 std::list<MediaTags> audio_playlists;
435 std::list<MediaTags> subtitle_playlists;
436 std::list<const MediaPlaylist*> video_playlists;
437 std::list<const MediaPlaylist*> iframe_playlists;
439 bool has_index =
true;
448 for (
const MediaPlaylist* playlist : playlists) {
449 has_index = has_index && playlist->GetMediaInfo().has_index();
451 switch (playlist->stream_type()) {
452 case MediaPlaylist::MediaPlaylistStreamType::kAudio:
453 audio_playlists.push_back({playlist, GetGroupId(*playlist),
457 case MediaPlaylist::MediaPlaylistStreamType::kVideo:
458 video_playlists.push_back(playlist);
460 case MediaPlaylist::MediaPlaylistStreamType::kVideoIFramesOnly:
461 iframe_playlists.push_back(playlist);
463 case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
464 subtitle_playlists.push_back({playlist, GetGroupId(*playlist),
469 NOTIMPLEMENTED() <<
static_cast<int>(playlist->stream_type())
476 video_playlists.sort(PlaylistOrderFn);
477 iframe_playlists.sort(PlaylistOrderFn);
478 audio_playlists.sort(MediaTagsOrderByIndexFn);
479 subtitle_playlists.sort(MediaTagsOrderByIndexFn);
483 audio_playlists.sort(MediaTagsOrderByGroupIdFn);
484 subtitle_playlists.sort(MediaTagsOrderByGroupIdFn);
500 std::map<std::string, std::set<std::string>> audio_group_languages;
501 for (
auto& pl : audio_playlists) {
502 if (pl.playlist->is_dvs()) {
507 pl.is_autoselect =
true;
510 const std::string language = pl.playlist->language();
511 auto& languages = audio_group_languages[pl.group_id];
512 if (languages.find(language) == languages.end()) {
513 pl.is_default = !language.empty() && language == default_audio_language;
514 pl.is_autoselect =
true;
515 languages.insert(language);
519 std::map<std::string, std::set<std::string>> subtitle_group_languages;
520 for (
auto& pl : subtitle_playlists) {
521 const std::string language = pl.playlist->language();
522 auto& languages = subtitle_group_languages[pl.group_id];
523 if (languages.find(language) == languages.end()) {
524 pl.is_default = !language.empty() && language == default_text_language;
525 pl.is_autoselect =
true;
526 languages.insert(language);
528 if (pl.playlist->forced_subtitle()) {
529 pl.is_autoselect =
true;
533 if (!audio_playlists.empty()) {
534 content->append(
"\n");
535 for (
const auto& pl : audio_playlists) {
536 BuildMediaTag(*pl.playlist, pl.group_id, pl.is_default, pl.is_autoselect,
541 if (!subtitle_playlists.empty()) {
542 content->append(
"\n");
543 for (
const auto& pl : subtitle_playlists) {
544 BuildMediaTag(*pl.playlist, pl.group_id, pl.is_default, pl.is_autoselect,
553 std::map<std::string, std::list<const MediaPlaylist*>> audio_playlist_groups;
554 std::map<std::string, std::list<const MediaPlaylist*>>
555 subtitle_playlist_groups;
556 for (
const auto& pl : audio_playlists) {
557 audio_playlist_groups[pl.group_id].push_back(pl.playlist);
559 for (
const auto& pl : subtitle_playlists) {
560 subtitle_playlist_groups[pl.group_id].push_back(pl.playlist);
563 if (!closed_captions.empty()) {
564 content->append(
"\n");
565 for (
const auto& caption : closed_captions) {
566 BuildCeaMediaTag(caption, content);
570 std::list<Variant> variants =
571 BuildVariants(audio_playlist_groups, subtitle_playlist_groups,
572 !closed_captions.empty());
573 for (
const auto& variant : variants) {
574 if (video_playlists.empty())
576 content->append(
"\n");
577 for (
const auto& playlist : video_playlists) {
578 BuildStreamInfTag(*playlist, variant, base_url, content);
582 if (!iframe_playlists.empty()) {
583 content->append(
"\n");
584 for (
const auto& playlist : iframe_playlists) {
586 BuildStreamInfTag(*playlist, Variant(), base_url, content);
591 if (!audio_playlist_groups.empty() && video_playlists.empty() &&
592 subtitle_playlist_groups.empty()) {
593 content->append(
"\n");
594 for (
const auto& playlist_group : audio_playlist_groups) {
600 variant.audio_group_id = &playlist_group.first;
601 for (
const auto& playlist : playlist_group.second) {
602 BuildStreamInfTag(*playlist, variant, base_url, content);
611 const std::string& default_audio_language,
612 const std::string& default_text_language,
613 const std::vector<CeaCaption>& closed_captions,
614 bool is_independent_segments,
615 bool create_session_keys)
616 : file_name_(file_name),
617 default_audio_language_(default_audio_language),
618 default_text_language_(default_text_language),
619 closed_captions_(closed_captions),
620 is_independent_segments_(is_independent_segments),
621 create_session_keys_(create_session_keys) {}
623MasterPlaylist::~MasterPlaylist() {}
626 const std::string& base_url,
627 const std::string& output_dir,
628 const std::list<MediaPlaylist*>& playlists) {
629 std::string content =
"#EXTM3U\n";
630 AppendVersionString(&content);
632 if (is_independent_segments_) {
633 content.append(
"\n#EXT-X-INDEPENDENT-SEGMENTS\n");
637 if (create_session_keys_) {
638 std::set<std::string> session_keys;
639 for (
const auto& playlist : playlists) {
640 for (
const auto& entry : playlist->entries()) {
641 if (entry->type() == HlsEntry::EntryType::kExtKey) {
642 auto encryption_entry =
644 session_keys.emplace(
645 encryption_entry->ToString(
"#EXT-X-SESSION-KEY"));
650 for (
const auto& session_key : session_keys)
651 content.append(session_key +
"\n");
654 AppendPlaylists(default_audio_language_, default_text_language_,
655 closed_captions_, base_url, playlists, &content);
658 if (content == written_playlist_)
661 auto file_path = std::filesystem::u8path(output_dir) / file_name_;
662 if (!File::WriteFileAtomically(file_path.string().c_str(), content)) {
663 LOG(ERROR) <<
"Failed to write master playlist to: " << file_path.string();
666 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.