2016-03-25 08:39:07 +00:00
|
|
|
// 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"
|
|
|
|
|
2018-01-31 20:46:21 +00:00
|
|
|
#include <algorithm> // std::max
|
|
|
|
|
2016-03-25 08:39:07 +00:00
|
|
|
#include <inttypes.h>
|
|
|
|
|
2016-08-19 22:32:27 +00:00
|
|
|
#include "packager/base/files/file_path.h"
|
2016-03-25 08:39:07 +00:00
|
|
|
#include "packager/base/strings/string_number_conversions.h"
|
|
|
|
#include "packager/base/strings/stringprintf.h"
|
2017-07-10 18:26:22 +00:00
|
|
|
#include "packager/file/file.h"
|
2016-03-25 08:39:07 +00:00
|
|
|
#include "packager/hls/base/media_playlist.h"
|
2018-01-31 21:02:28 +00:00
|
|
|
#include "packager/hls/base/tag.h"
|
2016-07-07 19:34:07 +00:00
|
|
|
#include "packager/version/version.h"
|
2016-03-25 08:39:07 +00:00
|
|
|
|
2016-05-20 21:19:33 +00:00
|
|
|
namespace shaka {
|
2016-03-25 08:39:07 +00:00
|
|
|
namespace hls {
|
2017-06-14 19:25:17 +00:00
|
|
|
namespace {
|
2018-01-31 19:12:16 +00:00
|
|
|
const char* kDefaultAudioGroupId = "default-audio-group";
|
2018-01-31 19:14:15 +00:00
|
|
|
const char* kDefaultSubtitleGroupId = "default-text-group";
|
|
|
|
const char* kUnexpectedGroupId = "unexpected-group";
|
2018-01-31 19:12:16 +00:00
|
|
|
|
2018-01-31 20:46:21 +00:00
|
|
|
struct Variant {
|
|
|
|
std::string audio_codec;
|
|
|
|
const std::string* audio_group_id = nullptr;
|
2018-01-31 19:14:15 +00:00
|
|
|
const std::string* text_group_id = nullptr;
|
2018-01-31 20:46:21 +00:00
|
|
|
uint64_t audio_bitrate = 0;
|
|
|
|
};
|
|
|
|
|
|
|
|
uint64_t MaxBitrate(const std::list<const MediaPlaylist*> playlists) {
|
|
|
|
uint64_t max = 0;
|
|
|
|
for (const auto& playlist : playlists) {
|
|
|
|
max = std::max(max, playlist->Bitrate());
|
|
|
|
}
|
|
|
|
return max;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string GetAudioGroupCodecString(
|
|
|
|
const std::list<const MediaPlaylist*>& group) {
|
|
|
|
// TODO(vaage): Should be a concatenation of all the codecs in the group.
|
|
|
|
return group.front()->codec();
|
|
|
|
}
|
|
|
|
|
|
|
|
std::list<Variant> AudioGroupsToVariants(
|
|
|
|
const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
|
|
|
|
std::list<Variant> variants;
|
|
|
|
|
|
|
|
for (const auto& group : groups) {
|
|
|
|
Variant variant;
|
|
|
|
variant.audio_codec = GetAudioGroupCodecString(group.second);
|
|
|
|
variant.audio_group_id = &group.first;
|
|
|
|
variant.audio_bitrate = MaxBitrate(group.second);
|
|
|
|
|
|
|
|
variants.push_back(variant);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure we return at least one variant so create a null variant if there
|
|
|
|
// are no variants.
|
|
|
|
if (variants.empty()) {
|
|
|
|
variants.emplace_back();
|
|
|
|
}
|
|
|
|
|
|
|
|
return variants;
|
|
|
|
}
|
2017-06-14 19:25:17 +00:00
|
|
|
|
2018-01-31 19:12:16 +00:00
|
|
|
const char* GetGroupId(const MediaPlaylist& playlist) {
|
|
|
|
const std::string& group_id = playlist.group_id();
|
2018-01-31 19:14:15 +00:00
|
|
|
|
|
|
|
if (!group_id.empty()) {
|
|
|
|
return group_id.c_str();
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (playlist.stream_type()) {
|
|
|
|
case MediaPlaylist::MediaPlaylistStreamType::kAudio:
|
|
|
|
return kDefaultAudioGroupId;
|
|
|
|
|
|
|
|
case MediaPlaylist::MediaPlaylistStreamType::kSubtitle:
|
|
|
|
return kDefaultSubtitleGroupId;
|
|
|
|
|
|
|
|
default:
|
|
|
|
return kUnexpectedGroupId;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::list<Variant> SubtitleGroupsToVariants(
|
|
|
|
const std::map<std::string, std::list<const MediaPlaylist*>>& groups) {
|
|
|
|
std::list<Variant> variants;
|
|
|
|
|
|
|
|
for (const auto& group : groups) {
|
|
|
|
Variant variant;
|
|
|
|
variant.text_group_id = &group.first;
|
|
|
|
|
|
|
|
variants.push_back(variant);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Make sure we return at least one variant so create a null variant if there
|
|
|
|
// are no variants.
|
|
|
|
if (variants.empty()) {
|
|
|
|
variants.emplace_back();
|
|
|
|
}
|
|
|
|
|
|
|
|
return variants;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::list<Variant> BuildVariants(
|
|
|
|
const std::map<std::string, std::list<const MediaPlaylist*>>& audio_groups,
|
|
|
|
const std::map<std::string, std::list<const MediaPlaylist*>>&
|
|
|
|
subtitle_groups) {
|
|
|
|
std::list<Variant> audio_variants = AudioGroupsToVariants(audio_groups);
|
|
|
|
std::list<Variant> subtitle_variants =
|
|
|
|
SubtitleGroupsToVariants(subtitle_groups);
|
|
|
|
|
|
|
|
DCHECK_GE(audio_variants.size(), 1u);
|
|
|
|
DCHECK_GE(subtitle_variants.size(), 1u);
|
|
|
|
|
|
|
|
std::list<Variant> merged;
|
|
|
|
|
|
|
|
for (const auto& audio_variant : audio_variants) {
|
|
|
|
for (const auto& subtitle_variant : subtitle_variants) {
|
|
|
|
Variant variant;
|
|
|
|
variant.audio_codec = audio_variant.audio_codec;
|
|
|
|
variant.audio_group_id = audio_variant.audio_group_id;
|
|
|
|
variant.text_group_id = subtitle_variant.text_group_id;
|
|
|
|
variant.audio_bitrate = audio_variant.audio_bitrate;
|
|
|
|
|
|
|
|
merged.push_back(variant);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
DCHECK_GE(merged.size(), 1u);
|
|
|
|
|
|
|
|
return merged;
|
2018-01-31 19:12:16 +00:00
|
|
|
}
|
|
|
|
|
2018-01-31 20:23:20 +00:00
|
|
|
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) {
|
2017-12-09 01:53:17 +00:00
|
|
|
DCHECK(out);
|
|
|
|
|
2018-01-31 20:23:20 +00:00
|
|
|
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()));
|
|
|
|
|
2017-12-09 01:53:17 +00:00
|
|
|
out->append("\n");
|
|
|
|
}
|
|
|
|
|
2018-01-31 20:23:20 +00:00
|
|
|
void BuildVideoTag(const MediaPlaylist& playlist,
|
|
|
|
uint64_t max_audio_bitrate,
|
|
|
|
const std::string& audio_codec,
|
|
|
|
const std::string* audio_group_id,
|
2018-01-31 19:14:15 +00:00
|
|
|
const std::string* text_group_id,
|
2018-01-31 20:23:20 +00:00
|
|
|
const std::string& base_url,
|
|
|
|
std::string* out) {
|
2017-06-14 19:25:17 +00:00
|
|
|
DCHECK(out);
|
2018-01-31 20:23:20 +00:00
|
|
|
|
|
|
|
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);
|
2018-01-31 21:51:57 +00:00
|
|
|
tag.AddNumberPair("RESOLUTION", width, 'x', height);
|
2017-06-14 19:25:17 +00:00
|
|
|
|
|
|
|
if (audio_group_id) {
|
2018-01-31 20:23:20 +00:00
|
|
|
tag.AddQuotedString("AUDIO", *audio_group_id);
|
2017-06-14 19:25:17 +00:00
|
|
|
}
|
|
|
|
|
2018-01-31 19:14:15 +00:00
|
|
|
if (text_group_id) {
|
|
|
|
tag.AddQuotedString("SUBTITLES", *text_group_id);
|
|
|
|
}
|
|
|
|
|
2018-01-31 20:23:20 +00:00
|
|
|
base::StringAppendF(out, "\n%s%s\n", base_url.c_str(),
|
|
|
|
playlist.file_name().c_str());
|
2017-06-14 19:25:17 +00:00
|
|
|
}
|
2018-01-31 19:14:15 +00:00
|
|
|
|
|
|
|
void BuildSubtitleTag(const MediaPlaylist& playlist,
|
|
|
|
const std::string& base_url,
|
|
|
|
const std::string& group_id,
|
|
|
|
std::string* out) {
|
|
|
|
Tag tag("#EXT-X-MEDIA", out);
|
|
|
|
|
|
|
|
tag.AddString("TYPE", "SUBTITLES");
|
|
|
|
tag.AddQuotedString("URI", base_url + playlist.file_name());
|
|
|
|
tag.AddQuotedString("GROUP-ID", group_id);
|
|
|
|
const std::string& language = playlist.GetLanguage();
|
|
|
|
if (!language.empty()) {
|
|
|
|
tag.AddQuotedString("LANGUAGE", language);
|
|
|
|
}
|
|
|
|
tag.AddQuotedString("NAME", playlist.name());
|
|
|
|
|
|
|
|
out->append("\n");
|
|
|
|
}
|
2017-06-14 19:25:17 +00:00
|
|
|
} // namespace
|
|
|
|
|
2018-01-17 23:43:41 +00:00
|
|
|
MasterPlaylist::MasterPlaylist(const std::string& file_name,
|
|
|
|
const std::string& default_language)
|
|
|
|
: file_name_(file_name), default_language_(default_language) {}
|
2016-03-25 08:39:07 +00:00
|
|
|
MasterPlaylist::~MasterPlaylist() {}
|
|
|
|
|
|
|
|
void MasterPlaylist::AddMediaPlaylist(MediaPlaylist* media_playlist) {
|
2017-06-14 20:45:32 +00:00
|
|
|
DCHECK(media_playlist);
|
|
|
|
switch (media_playlist->stream_type()) {
|
2018-01-30 03:17:21 +00:00
|
|
|
case MediaPlaylist::MediaPlaylistStreamType::kAudio: {
|
2018-01-31 19:12:16 +00:00
|
|
|
std::string group_id = GetGroupId(*media_playlist);
|
2017-06-14 20:45:32 +00:00
|
|
|
audio_playlist_groups_[group_id].push_back(media_playlist);
|
|
|
|
break;
|
|
|
|
}
|
2018-01-30 03:17:21 +00:00
|
|
|
case MediaPlaylist::MediaPlaylistStreamType::kVideo: {
|
2017-06-14 20:45:32 +00:00
|
|
|
video_playlists_.push_back(media_playlist);
|
|
|
|
break;
|
|
|
|
}
|
2018-01-31 19:14:15 +00:00
|
|
|
case MediaPlaylist::MediaPlaylistStreamType::kSubtitle: {
|
|
|
|
std::string group_id = GetGroupId(*media_playlist);
|
|
|
|
subtitle_playlist_groups_[group_id].push_back(media_playlist);
|
|
|
|
break;
|
|
|
|
}
|
2017-06-14 20:45:32 +00:00
|
|
|
default: {
|
|
|
|
NOTIMPLEMENTED() << static_cast<int>(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);
|
2016-03-25 08:39:07 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
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;
|
2018-01-31 19:14:15 +00:00
|
|
|
std::string subtitle_output;
|
2018-01-31 20:46:21 +00:00
|
|
|
|
|
|
|
// Write out all the audio tags.
|
|
|
|
for (const auto& group : audio_playlist_groups_) {
|
|
|
|
const auto& group_id = group.first;
|
|
|
|
const auto& playlists = group.second;
|
2016-03-25 08:39:07 +00:00
|
|
|
|
2018-01-17 23:43:41 +00:00
|
|
|
// 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<std::string> languages;
|
|
|
|
|
2018-01-31 20:46:21 +00:00
|
|
|
for (const auto& playlist : playlists) {
|
2018-01-17 23:43:41 +00:00
|
|
|
bool is_default = false;
|
|
|
|
bool is_autoselect = false;
|
2018-01-31 20:46:21 +00:00
|
|
|
|
|
|
|
const std::string language = playlist->GetLanguage();
|
2018-01-17 23:43:41 +00:00
|
|
|
if (languages.find(language) == languages.end()) {
|
|
|
|
is_default = !language.empty() && language == default_language_;
|
|
|
|
is_autoselect = true;
|
|
|
|
languages.insert(language);
|
|
|
|
}
|
|
|
|
|
2018-01-31 20:46:21 +00:00
|
|
|
BuildAudioTag(base_url, group_id, *playlist, is_default, is_autoselect,
|
|
|
|
&audio_output);
|
2016-03-25 08:39:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-01-31 19:14:15 +00:00
|
|
|
// Write out all the text tags.
|
|
|
|
for (const auto& group : subtitle_playlist_groups_) {
|
|
|
|
const auto& group_id = group.first;
|
|
|
|
const auto& playlists = group.second;
|
|
|
|
for (const auto& playlist : playlists) {
|
|
|
|
BuildSubtitleTag(*playlist, base_url, group_id, &subtitle_output);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
std::list<Variant> variants =
|
|
|
|
BuildVariants(audio_playlist_groups_, subtitle_playlist_groups_);
|
2018-01-31 20:46:21 +00:00
|
|
|
|
|
|
|
// Write all the video tags out.
|
|
|
|
for (const auto& playlist : video_playlists_) {
|
|
|
|
for (const auto& variant : variants) {
|
|
|
|
BuildVideoTag(*playlist, variant.audio_bitrate, variant.audio_codec,
|
2018-01-31 19:14:15 +00:00
|
|
|
variant.audio_group_id, variant.text_group_id, base_url,
|
|
|
|
&video_output);
|
2016-03-25 08:39:07 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-07-07 19:34:07 +00:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
|
2018-01-31 20:46:21 +00:00
|
|
|
std::string content = "";
|
2018-01-31 19:14:15 +00:00
|
|
|
base::StringAppendF(&content, "#EXTM3U\n%s%s%s%s", version_line.c_str(),
|
|
|
|
audio_output.c_str(), subtitle_output.c_str(),
|
|
|
|
video_output.c_str());
|
2017-06-03 00:05:47 +00:00
|
|
|
|
|
|
|
// 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();
|
2017-07-10 18:26:22 +00:00
|
|
|
if (!File::WriteFileAtomically(file_path.c_str(), content)) {
|
2017-06-15 20:00:28 +00:00
|
|
|
LOG(ERROR) << "Failed to write master playlist to: " << file_path;
|
2016-03-25 08:39:07 +00:00
|
|
|
return false;
|
|
|
|
}
|
2017-06-03 00:05:47 +00:00
|
|
|
written_playlist_ = content;
|
2016-03-25 08:39:07 +00:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
} // namespace hls
|
2016-05-20 21:19:33 +00:00
|
|
|
} // namespace shaka
|