527 lines
20 KiB
C++
527 lines
20 KiB
C++
// Copyright 2016 Google LLC. 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/simple_hls_notifier.h>
|
|
|
|
#include <cmath>
|
|
#include <filesystem>
|
|
#include <optional>
|
|
|
|
#include <absl/flags/flag.h>
|
|
#include <absl/log/check.h>
|
|
#include <absl/log/log.h>
|
|
#include <absl/strings/escaping.h>
|
|
#include <absl/strings/numbers.h>
|
|
|
|
#include <packager/file/file_util.h>
|
|
#include <packager/media/base/protection_system_ids.h>
|
|
#include <packager/media/base/protection_system_specific_info.h>
|
|
#include <packager/media/base/proto_json_util.h>
|
|
#include <packager/media/base/widevine_pssh_data.pb.h>
|
|
|
|
ABSL_FLAG(bool,
|
|
enable_legacy_widevine_hls_signaling,
|
|
false,
|
|
"Specifies whether Legacy Widevine HLS, i.e. v1 is signalled in "
|
|
"the media playlist. Applies to Widevine protection system in HLS "
|
|
"with SAMPLE-AES only.");
|
|
|
|
namespace shaka {
|
|
|
|
namespace hls {
|
|
|
|
namespace {
|
|
|
|
const char kUriBase64Prefix[] = "data:text/plain;base64,";
|
|
const char kUriBase64Utf16Prefix[] = "data:text/plain;charset=UTF-16;base64,";
|
|
const char kUriFairPlayPrefix[] = "skd://";
|
|
const char kWidevineDashIfIopUUID[] =
|
|
"urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";
|
|
|
|
bool IsWidevineSystemId(const std::vector<uint8_t>& system_id) {
|
|
return system_id.size() == std::size(media::kWidevineSystemId) &&
|
|
std::equal(system_id.begin(), system_id.end(),
|
|
media::kWidevineSystemId);
|
|
}
|
|
|
|
bool IsCommonSystemId(const std::vector<uint8_t>& system_id) {
|
|
return system_id.size() == std::size(media::kCommonSystemId) &&
|
|
std::equal(system_id.begin(), system_id.end(), media::kCommonSystemId);
|
|
}
|
|
|
|
bool IsFairPlaySystemId(const std::vector<uint8_t>& system_id) {
|
|
return system_id.size() == std::size(media::kFairPlaySystemId) &&
|
|
std::equal(system_id.begin(), system_id.end(),
|
|
media::kFairPlaySystemId);
|
|
}
|
|
|
|
bool IsLegacyFairPlaySystemId(const std::vector<uint8_t>& system_id) {
|
|
return system_id.size() == std::size(media::kLegacyFairPlaySystemId) &&
|
|
std::equal(system_id.begin(), system_id.end(),
|
|
media::kLegacyFairPlaySystemId);
|
|
}
|
|
|
|
bool IsPlayReadySystemId(const std::vector<uint8_t>& system_id) {
|
|
return system_id.size() == std::size(media::kPlayReadySystemId) &&
|
|
std::equal(system_id.begin(), system_id.end(),
|
|
media::kPlayReadySystemId);
|
|
}
|
|
|
|
std::string Base64EncodeData(const std::string& prefix,
|
|
const std::string& data) {
|
|
std::string data_base64;
|
|
absl::Base64Escape(data, &data_base64);
|
|
return prefix + data_base64;
|
|
}
|
|
|
|
std::string VectorToString(const std::vector<uint8_t>& v) {
|
|
return std::string(v.begin(), v.end());
|
|
}
|
|
|
|
// Segment URL is relative to either output directory or the directory
|
|
// containing the media playlist depends on whether base_url is set.
|
|
std::string GenerateSegmentUrl(const std::string& segment_name,
|
|
const std::string& base_url,
|
|
const std::string& output_dir,
|
|
const std::string& playlist_file_name) {
|
|
auto output_path = std::filesystem::u8path(output_dir);
|
|
if (!base_url.empty()) {
|
|
// Media segment URL is base_url + segment path relative to output
|
|
// directory.
|
|
return base_url + MakePathRelative(segment_name, output_path);
|
|
}
|
|
// Media segment URL is segment path relative to the directory containing the
|
|
// playlist.
|
|
const std::filesystem::path playlist_dir =
|
|
(output_path / playlist_file_name).parent_path() / "";
|
|
return MakePathRelative(segment_name, playlist_dir);
|
|
}
|
|
|
|
MediaInfo MakeMediaInfoPathsRelativeToPlaylist(
|
|
const MediaInfo& media_info,
|
|
const std::string& base_url,
|
|
const std::string& output_dir,
|
|
const std::string& playlist_name) {
|
|
MediaInfo media_info_copy = media_info;
|
|
if (media_info_copy.has_init_segment_name()) {
|
|
media_info_copy.set_init_segment_url(
|
|
GenerateSegmentUrl(media_info_copy.init_segment_name(), base_url,
|
|
output_dir, playlist_name));
|
|
}
|
|
if (media_info_copy.has_media_file_name()) {
|
|
media_info_copy.set_media_file_url(
|
|
GenerateSegmentUrl(media_info_copy.media_file_name(), base_url,
|
|
output_dir, playlist_name));
|
|
}
|
|
if (media_info_copy.has_segment_template()) {
|
|
media_info_copy.set_segment_template_url(
|
|
GenerateSegmentUrl(media_info_copy.segment_template(), base_url,
|
|
output_dir, playlist_name));
|
|
}
|
|
return media_info_copy;
|
|
}
|
|
|
|
bool WidevinePsshToJson(const std::vector<uint8_t>& pssh_box,
|
|
const std::vector<uint8_t>& key_id,
|
|
std::string* pssh_json) {
|
|
std::unique_ptr<media::PsshBoxBuilder> pssh_builder =
|
|
media::PsshBoxBuilder::ParseFromBox(pssh_box.data(), pssh_box.size());
|
|
if (!pssh_builder) {
|
|
LOG(ERROR) << "Failed to parse PSSH box.";
|
|
return false;
|
|
}
|
|
|
|
media::WidevinePsshData pssh_proto;
|
|
if (!pssh_proto.ParseFromArray(pssh_builder->pssh_data().data(),
|
|
pssh_builder->pssh_data().size())) {
|
|
LOG(ERROR) << "Failed to parse protection_system_specific_data.";
|
|
return false;
|
|
}
|
|
|
|
media::WidevineHeader widevine_header;
|
|
|
|
if (pssh_proto.has_provider()) {
|
|
widevine_header.set_provider(pssh_proto.provider());
|
|
} else {
|
|
LOG(WARNING) << "Missing provider in Widevine PSSH. The content may not "
|
|
"play in some devices.";
|
|
}
|
|
|
|
if (pssh_proto.has_content_id()) {
|
|
widevine_header.set_content_id(pssh_proto.content_id());
|
|
} else {
|
|
LOG(WARNING) << "Missing content_id in Widevine PSSH. The content may not "
|
|
"play in some devices.";
|
|
}
|
|
|
|
// Place the current |key_id| to the front and converts all key_id to hex
|
|
// format.
|
|
widevine_header.add_key_ids(absl::BytesToHexString(absl::string_view(
|
|
reinterpret_cast<const char*>(key_id.data()), key_id.size())));
|
|
for (const std::string& key_id_in_pssh : pssh_proto.key_id()) {
|
|
const std::string key_id_hex = absl::BytesToHexString(
|
|
absl::string_view(reinterpret_cast<const char*>(key_id_in_pssh.data()),
|
|
key_id_in_pssh.size()));
|
|
if (widevine_header.key_ids(0) != key_id_hex)
|
|
widevine_header.add_key_ids(key_id_hex);
|
|
}
|
|
|
|
*pssh_json = media::MessageToJsonString(widevine_header);
|
|
return true;
|
|
}
|
|
|
|
std::optional<MediaPlaylist::EncryptionMethod> StringToEncryptionMethod(
|
|
const std::string& method) {
|
|
if (method == "cenc") {
|
|
return MediaPlaylist::EncryptionMethod::kSampleAesCenc;
|
|
}
|
|
if (method == "cbcs") {
|
|
return MediaPlaylist::EncryptionMethod::kSampleAes;
|
|
}
|
|
if (method == "cbca") {
|
|
// cbca is a place holder for sample aes.
|
|
return MediaPlaylist::EncryptionMethod::kSampleAes;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
void NotifyEncryptionToMediaPlaylist(
|
|
MediaPlaylist::EncryptionMethod encryption_method,
|
|
const std::string& uri,
|
|
const std::vector<uint8_t>& key_id,
|
|
const std::vector<uint8_t>& iv,
|
|
const std::string& key_format,
|
|
const std::string& key_format_version,
|
|
MediaPlaylist* media_playlist) {
|
|
std::string iv_string;
|
|
if (!iv.empty()) {
|
|
iv_string =
|
|
"0x" + absl::BytesToHexString(absl::string_view(
|
|
reinterpret_cast<const char*>(iv.data()), iv.size()));
|
|
}
|
|
std::string key_id_string;
|
|
if (!key_id.empty()) {
|
|
key_id_string = "0x" + absl::BytesToHexString(absl::string_view(
|
|
reinterpret_cast<const char*>(key_id.data()),
|
|
key_id.size()));
|
|
}
|
|
|
|
media_playlist->AddEncryptionInfo(
|
|
encryption_method,
|
|
uri, key_id_string, iv_string,
|
|
key_format, key_format_version);
|
|
}
|
|
|
|
// Creates JSON format and the format similar to MPD.
|
|
bool HandleWidevineKeyFormats(
|
|
MediaPlaylist::EncryptionMethod encryption_method,
|
|
const std::vector<uint8_t>& key_id,
|
|
const std::vector<uint8_t>& iv,
|
|
const std::vector<uint8_t>& protection_system_specific_data,
|
|
MediaPlaylist* media_playlist) {
|
|
if (absl::GetFlag(FLAGS_enable_legacy_widevine_hls_signaling) &&
|
|
encryption_method == MediaPlaylist::EncryptionMethod::kSampleAes) {
|
|
// This format allows SAMPLE-AES only.
|
|
std::string key_uri_data;
|
|
if (!WidevinePsshToJson(protection_system_specific_data, key_id,
|
|
&key_uri_data)) {
|
|
return false;
|
|
}
|
|
std::string key_uri_data_base64 =
|
|
Base64EncodeData(kUriBase64Prefix, key_uri_data);
|
|
NotifyEncryptionToMediaPlaylist(encryption_method, key_uri_data_base64,
|
|
std::vector<uint8_t>(), iv, "com.widevine",
|
|
"1", media_playlist);
|
|
}
|
|
|
|
std::string pssh_as_string(
|
|
reinterpret_cast<const char*>(protection_system_specific_data.data()),
|
|
protection_system_specific_data.size());
|
|
std::string key_uri_data_base64 =
|
|
Base64EncodeData(kUriBase64Prefix, pssh_as_string);
|
|
NotifyEncryptionToMediaPlaylist(encryption_method, key_uri_data_base64,
|
|
key_id, iv, kWidevineDashIfIopUUID, "1",
|
|
media_playlist);
|
|
return true;
|
|
}
|
|
|
|
bool WriteMediaPlaylist(const std::string& output_dir,
|
|
MediaPlaylist* playlist) {
|
|
auto file_path = std::filesystem::u8path(output_dir) / playlist->file_name();
|
|
if (!playlist->WriteToFile(file_path)) {
|
|
LOG(ERROR) << "Failed to write playlist " << file_path.string();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
MediaPlaylistFactory::~MediaPlaylistFactory() {}
|
|
|
|
std::unique_ptr<MediaPlaylist> MediaPlaylistFactory::Create(
|
|
const HlsParams& hls_params,
|
|
const std::string& file_name,
|
|
const std::string& name,
|
|
const std::string& group_id) {
|
|
return std::unique_ptr<MediaPlaylist>(
|
|
new MediaPlaylist(hls_params, file_name, name, group_id));
|
|
}
|
|
|
|
SimpleHlsNotifier::SimpleHlsNotifier(const HlsParams& hls_params)
|
|
: HlsNotifier(hls_params),
|
|
media_playlist_factory_(new MediaPlaylistFactory()) {
|
|
const auto master_playlist_path =
|
|
std::filesystem::u8path(hls_params.master_playlist_output);
|
|
master_playlist_dir_ = master_playlist_path.parent_path().string();
|
|
const std::string& default_audio_langauge = hls_params.default_language;
|
|
const std::string& default_text_language =
|
|
hls_params.default_text_language.empty()
|
|
? hls_params.default_language
|
|
: hls_params.default_text_language;
|
|
master_playlist_.reset(new MasterPlaylist(
|
|
master_playlist_path.filename(), default_audio_langauge,
|
|
default_text_language, hls_params.is_independent_segments,
|
|
hls_params.create_session_keys));
|
|
}
|
|
|
|
SimpleHlsNotifier::~SimpleHlsNotifier() {}
|
|
|
|
bool SimpleHlsNotifier::Init() {
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHlsNotifier::NotifyNewStream(const MediaInfo& media_info,
|
|
const std::string& playlist_name,
|
|
const std::string& name,
|
|
const std::string& group_id,
|
|
uint32_t* stream_id) {
|
|
DCHECK(stream_id);
|
|
|
|
const std::string relative_playlist_path = MakePathRelative(
|
|
playlist_name, std::filesystem::u8path(master_playlist_dir_));
|
|
|
|
std::unique_ptr<MediaPlaylist> media_playlist =
|
|
media_playlist_factory_->Create(hls_params(), relative_playlist_path,
|
|
name, group_id);
|
|
MediaInfo adjusted_media_info = MakeMediaInfoPathsRelativeToPlaylist(
|
|
media_info, hls_params().base_url, master_playlist_dir_,
|
|
media_playlist->file_name());
|
|
if (!media_playlist->SetMediaInfo(adjusted_media_info)) {
|
|
LOG(ERROR) << "Failed to set media info for playlist " << playlist_name;
|
|
return false;
|
|
}
|
|
|
|
MediaPlaylist::EncryptionMethod encryption_method =
|
|
MediaPlaylist::EncryptionMethod::kNone;
|
|
if (media_info.protected_content().has_protection_scheme()) {
|
|
const std::string& protection_scheme =
|
|
media_info.protected_content().protection_scheme();
|
|
std::optional<MediaPlaylist::EncryptionMethod> enc_method =
|
|
StringToEncryptionMethod(protection_scheme);
|
|
if (!enc_method) {
|
|
LOG(ERROR) << "Failed to recognize protection scheme "
|
|
<< protection_scheme;
|
|
return false;
|
|
}
|
|
encryption_method = enc_method.value();
|
|
}
|
|
|
|
absl::MutexLock lock(&lock_);
|
|
*stream_id = sequence_number_++;
|
|
media_playlists_.push_back(media_playlist.get());
|
|
stream_map_[*stream_id].reset(
|
|
new StreamEntry{std::move(media_playlist), encryption_method});
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHlsNotifier::NotifySampleDuration(uint32_t stream_id,
|
|
int32_t sample_duration) {
|
|
absl::MutexLock lock(&lock_);
|
|
auto stream_iterator = stream_map_.find(stream_id);
|
|
if (stream_iterator == stream_map_.end()) {
|
|
LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
|
|
return false;
|
|
}
|
|
auto& media_playlist = stream_iterator->second->media_playlist;
|
|
media_playlist->SetSampleDuration(sample_duration);
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHlsNotifier::NotifyNewSegment(uint32_t stream_id,
|
|
const std::string& segment_name,
|
|
int64_t start_time,
|
|
int64_t duration,
|
|
uint64_t start_byte_offset,
|
|
uint64_t size) {
|
|
absl::MutexLock lock(&lock_);
|
|
auto stream_iterator = stream_map_.find(stream_id);
|
|
if (stream_iterator == stream_map_.end()) {
|
|
LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
|
|
return false;
|
|
}
|
|
auto& media_playlist = stream_iterator->second->media_playlist;
|
|
const std::string& segment_url =
|
|
GenerateSegmentUrl(segment_name, hls_params().base_url,
|
|
master_playlist_dir_, media_playlist->file_name());
|
|
media_playlist->AddSegment(segment_url, start_time, duration,
|
|
start_byte_offset, size);
|
|
|
|
// Update target duration.
|
|
int32_t longest_segment_duration =
|
|
static_cast<int32_t>(ceil(media_playlist->GetLongestSegmentDuration()));
|
|
bool target_duration_updated = false;
|
|
if (longest_segment_duration > target_duration_) {
|
|
target_duration_ = longest_segment_duration;
|
|
target_duration_updated = true;
|
|
}
|
|
|
|
// Update the playlists when there is new segments in live mode.
|
|
if (hls_params().playlist_type == HlsPlaylistType::kLive ||
|
|
hls_params().playlist_type == HlsPlaylistType::kEvent) {
|
|
// Update all playlists if target duration is updated.
|
|
if (target_duration_updated) {
|
|
for (MediaPlaylist* playlist : media_playlists_) {
|
|
playlist->SetTargetDuration(target_duration_);
|
|
if (!WriteMediaPlaylist(master_playlist_dir_, playlist))
|
|
return false;
|
|
}
|
|
} else {
|
|
if (!WriteMediaPlaylist(master_playlist_dir_, media_playlist.get()))
|
|
return false;
|
|
}
|
|
if (!master_playlist_->WriteMasterPlaylist(
|
|
hls_params().base_url, master_playlist_dir_, media_playlists_)) {
|
|
LOG(ERROR) << "Failed to write master playlist.";
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHlsNotifier::NotifyKeyFrame(uint32_t stream_id,
|
|
int64_t timestamp,
|
|
uint64_t start_byte_offset,
|
|
uint64_t size) {
|
|
absl::MutexLock lock(&lock_);
|
|
auto stream_iterator = stream_map_.find(stream_id);
|
|
if (stream_iterator == stream_map_.end()) {
|
|
LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
|
|
return false;
|
|
}
|
|
auto& media_playlist = stream_iterator->second->media_playlist;
|
|
media_playlist->AddKeyFrame(timestamp, start_byte_offset, size);
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHlsNotifier::NotifyCueEvent(uint32_t stream_id, int64_t timestamp) {
|
|
absl::MutexLock lock(&lock_);
|
|
auto stream_iterator = stream_map_.find(stream_id);
|
|
if (stream_iterator == stream_map_.end()) {
|
|
LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
|
|
return false;
|
|
}
|
|
auto& media_playlist = stream_iterator->second->media_playlist;
|
|
media_playlist->AddPlacementOpportunity();
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHlsNotifier::NotifyEncryptionUpdate(
|
|
uint32_t stream_id,
|
|
const std::vector<uint8_t>& key_id,
|
|
const std::vector<uint8_t>& system_id,
|
|
const std::vector<uint8_t>& iv,
|
|
const std::vector<uint8_t>& protection_system_specific_data) {
|
|
absl::MutexLock lock(&lock_);
|
|
auto stream_iterator = stream_map_.find(stream_id);
|
|
if (stream_iterator == stream_map_.end()) {
|
|
LOG(ERROR) << "Cannot find stream with ID: " << stream_id;
|
|
return false;
|
|
}
|
|
|
|
std::unique_ptr<MediaPlaylist>& media_playlist =
|
|
stream_iterator->second->media_playlist;
|
|
const MediaPlaylist::EncryptionMethod encryption_method =
|
|
stream_iterator->second->encryption_method;
|
|
LOG_IF(WARNING, encryption_method == MediaPlaylist::EncryptionMethod::kNone)
|
|
<< "Got encryption notification but the encryption method is NONE";
|
|
if (IsWidevineSystemId(system_id)) {
|
|
return HandleWidevineKeyFormats(encryption_method,
|
|
key_id, iv, protection_system_specific_data,
|
|
media_playlist.get());
|
|
}
|
|
|
|
// Key Id does not need to be specified with "identity" and "sdk".
|
|
const std::vector<uint8_t> empty_key_id;
|
|
|
|
if (IsCommonSystemId(system_id)) {
|
|
std::string key_uri = hls_params().key_uri;
|
|
if (key_uri.empty()) {
|
|
// Use key_id as the key_uri. The player needs to have custom logic to
|
|
// convert it to the actual key uri.
|
|
std::string key_uri_data = VectorToString(key_id);
|
|
key_uri = Base64EncodeData(kUriBase64Prefix, key_uri_data);
|
|
}
|
|
NotifyEncryptionToMediaPlaylist(encryption_method, key_uri, empty_key_id,
|
|
iv, "identity", "", media_playlist.get());
|
|
return true;
|
|
}
|
|
if (IsFairPlaySystemId(system_id) || IsLegacyFairPlaySystemId(system_id)) {
|
|
std::string key_uri = hls_params().key_uri;
|
|
if (key_uri.empty()) {
|
|
// Use key_id as the key_uri. The player needs to have custom logic to
|
|
// convert it to the actual key uri.
|
|
std::string key_uri_data = VectorToString(key_id);
|
|
key_uri = Base64EncodeData(kUriFairPlayPrefix, key_uri_data);
|
|
}
|
|
|
|
// FairPlay defines IV to be carried with the key, not the playlist.
|
|
const std::vector<uint8_t> empty_iv;
|
|
NotifyEncryptionToMediaPlaylist(encryption_method, key_uri, empty_key_id,
|
|
empty_iv, "com.apple.streamingkeydelivery",
|
|
"1", media_playlist.get());
|
|
return true;
|
|
}
|
|
if (IsPlayReadySystemId(system_id)) {
|
|
std::unique_ptr<media::PsshBoxBuilder> b =
|
|
media::PsshBoxBuilder::ParseFromBox(
|
|
protection_system_specific_data.data(),
|
|
protection_system_specific_data.size());
|
|
std::string pssh_data(reinterpret_cast<const char*>(b->pssh_data().data()),
|
|
b->pssh_data().size());
|
|
std::string key_uri_data_base64 =
|
|
Base64EncodeData(kUriBase64Utf16Prefix, pssh_data);
|
|
NotifyEncryptionToMediaPlaylist(encryption_method, key_uri_data_base64,
|
|
empty_key_id, iv, "com.microsoft.playready",
|
|
"1", media_playlist.get());
|
|
return true;
|
|
}
|
|
|
|
LOG(WARNING) << "HLS: Ignore unknown or unsupported system ID: "
|
|
<< absl::BytesToHexString(absl::string_view(
|
|
reinterpret_cast<const char*>(system_id.data()),
|
|
system_id.size()));
|
|
return true;
|
|
}
|
|
|
|
bool SimpleHlsNotifier::Flush() {
|
|
absl::MutexLock lock(&lock_);
|
|
for (MediaPlaylist* playlist : media_playlists_) {
|
|
playlist->SetTargetDuration(target_duration_);
|
|
if (!WriteMediaPlaylist(master_playlist_dir_, playlist))
|
|
return false;
|
|
}
|
|
if (!master_playlist_->WriteMasterPlaylist(
|
|
hls_params().base_url, master_playlist_dir_, media_playlists_)) {
|
|
LOG(ERROR) << "Failed to write master playlist.";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
} // namespace hls
|
|
} // namespace shaka
|