// Copyright 2016 Google Inc. All rights reserved. // // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd #include "packager/hls/base/master_playlist.h" #include #include "packager/base/files/file_path.h" #include "packager/base/strings/string_number_conversions.h" #include "packager/base/strings/stringprintf.h" #include "packager/file/file.h" #include "packager/hls/base/media_playlist.h" #include "packager/version/version.h" namespace shaka { namespace hls { namespace { class Tag { public: Tag(const std::string& name, std::string* buffer) : buffer_(buffer) { base::StringAppendF(buffer_, "%s:", name.c_str()); } void AddString(const std::string& key, const std::string& value) { NextField(); base::StringAppendF(buffer_, "%s=%s", key.c_str(), value.c_str()); } void AddQuotedString(const std::string& key, const std::string& value) { NextField(); base::StringAppendF(buffer_, "%s=\"%s\"", key.c_str(), value.c_str()); } void AddNumber(const std::string& key, uint64_t value) { NextField(); base::StringAppendF(buffer_, "%s=%" PRIu64, key.c_str(), value); } void AddResolution(const std::string& key, uint32_t width, uint32_t height) { NextField(); base::StringAppendF(buffer_, "%s=%" PRIu32 "x%" PRIu32, key.c_str(), width, height); } private: Tag(const Tag&) = delete; Tag& operator=(const Tag&) = delete; std::string* buffer_; size_t fields = 0; void NextField() { if (fields++) { buffer_->append(","); } } }; void BuildAudioTag(const std::string& base_url, const std::string& group_id, const MediaPlaylist& audio_playlist, bool is_default, bool is_autoselect, std::string* out) { DCHECK(out); Tag tag("#EXT-X-MEDIA", out); tag.AddString("TYPE", "AUDIO"); tag.AddQuotedString("URI", base_url + audio_playlist.file_name()); tag.AddQuotedString("GROUP-ID", group_id); const std::string& language = audio_playlist.GetLanguage(); if (!language.empty()) { tag.AddQuotedString("LANGUAGE", language); } tag.AddQuotedString("NAME", audio_playlist.name()); if (is_default) { tag.AddString("DEFAULT", "YES"); } if (is_autoselect) { tag.AddString("AUTOSELECT", "YES"); } tag.AddQuotedString("CHANNELS", std::to_string(audio_playlist.GetNumChannels())); out->append("\n"); } void BuildVideoTag(const MediaPlaylist& playlist, uint64_t max_audio_bitrate, const std::string& audio_codec, const std::string* audio_group_id, const std::string& base_url, std::string* out) { DCHECK(out); const uint64_t bitrate = playlist.Bitrate() + max_audio_bitrate; uint32_t width; uint32_t height; CHECK(playlist.GetDisplayResolution(&width, &height)); std::string codecs = playlist.codec(); if (!audio_codec.empty()) { base::StringAppendF(&codecs, ",%s", audio_codec.c_str()); } Tag tag("#EXT-X-STREAM-INF", out); tag.AddNumber("BANDWIDTH", bitrate); tag.AddQuotedString("CODECS", codecs); tag.AddResolution("RESOLUTION", width, height); if (audio_group_id) { tag.AddQuotedString("AUDIO", *audio_group_id); } base::StringAppendF(out, "\n%s%s\n", base_url.c_str(), playlist.file_name().c_str()); } } // namespace MasterPlaylist::MasterPlaylist(const std::string& file_name, const std::string& default_language) : file_name_(file_name), default_language_(default_language) {} MasterPlaylist::~MasterPlaylist() {} void MasterPlaylist::AddMediaPlaylist(MediaPlaylist* media_playlist) { DCHECK(media_playlist); switch (media_playlist->stream_type()) { case MediaPlaylist::MediaPlaylistStreamType::kAudio: { const std::string& group_id = media_playlist->group_id(); audio_playlist_groups_[group_id].push_back(media_playlist); break; } case MediaPlaylist::MediaPlaylistStreamType::kVideo: { video_playlists_.push_back(media_playlist); break; } default: { NOTIMPLEMENTED() << static_cast(media_playlist->stream_type()) << " not handled."; break; } } // Sometimes we need to iterate over all playlists, so keep a collection // of all playlists to make iterating easier. all_playlists_.push_back(media_playlist); } bool MasterPlaylist::WriteMasterPlaylist(const std::string& base_url, const std::string& output_dir) { // TODO(rkuroiwa): Handle audio only. std::string audio_output; std::string video_output; for (const auto& group_id_audio_playlists : audio_playlist_groups_) { const std::string& group_id = group_id_audio_playlists.first; const std::list& audio_playlists = group_id_audio_playlists.second; // Tracks the language of the playlist in this group. // According to HLS spec: https://goo.gl/MiqjNd 4.3.4.1.1. Rendition Groups // - A Group MUST NOT have more than one member with a DEFAULT attribute of // YES. // - Each EXT-X-MEDIA tag with an AUTOSELECT=YES attribute SHOULD have a // combination of LANGUAGE[RFC5646], ASSOC-LANGUAGE, FORCED, and // CHARACTERISTICS attributes that is distinct from those of other // AUTOSELECT=YES members of its Group. // We tag the first rendition encountered with a particular language with // 'AUTOSELECT'; it is tagged with 'DEFAULT' too if the language matches // |default_language_|. std::set languages; uint64_t max_audio_bitrate = 0; for (const MediaPlaylist* audio_playlist : audio_playlists) { bool is_default = false; bool is_autoselect = false; const std::string language = audio_playlist->GetLanguage(); if (languages.find(language) == languages.end()) { is_default = !language.empty() && language == default_language_; is_autoselect = true; languages.insert(language); } BuildAudioTag(base_url, group_id, *audio_playlist, is_default, is_autoselect, &audio_output); const uint64_t audio_bitrate = audio_playlist->Bitrate(); if (audio_bitrate > max_audio_bitrate) max_audio_bitrate = audio_bitrate; } for (const MediaPlaylist* video_playlist : video_playlists_) { // Assume all codecs are the same for same group ID. const std::string& audio_codec = audio_playlists.front()->codec(); uint32_t video_width; uint32_t video_height; CHECK(video_playlist->GetDisplayResolution(&video_width, &video_height)); BuildVideoTag(*video_playlist, max_audio_bitrate, audio_codec, &group_id, base_url, &video_output); } } if (audio_playlist_groups_.empty()) { for (const MediaPlaylist* video_playlist : video_playlists_) { BuildVideoTag(*video_playlist, 0, "", nullptr, base_url, &video_output); } } const std::string version = GetPackagerVersion(); std::string version_line; if (!version.empty()) { version_line = base::StringPrintf("## Generated with %s version %s\n", GetPackagerProjectUrl().c_str(), version.c_str()); } std::string content = "#EXTM3U\n" + version_line + audio_output + video_output; // Skip if the playlist is already written. if (content == written_playlist_) return true; std::string file_path = base::FilePath::FromUTF8Unsafe(output_dir) .Append(base::FilePath::FromUTF8Unsafe(file_name_)) .AsUTF8Unsafe(); if (!File::WriteFileAtomically(file_path.c_str(), content)) { LOG(ERROR) << "Failed to write master playlist to: " << file_path; return false; } written_playlist_ = content; return true; } } // namespace hls } // namespace shaka