shaka-packager/packager/mpd/base/xml/xml_node.cc

635 lines
23 KiB
C++
Raw Normal View History

// Copyright 2014 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/mpd/base/xml/xml_node.h"
#include <absl/base/internal/endian.h>
#include <absl/flags/flag.h>
#include <absl/strings/numbers.h>
#include <glog/logging.h>
#include <libxml/tree.h>
#include <cmath>
#include <limits>
#include <set>
#include <absl/strings/escaping.h>
#include <absl/strings/str_format.h>
#include "packager/macros.h"
#include "packager/media/base/rcheck.h"
#include "packager/mpd/base/media_info.pb.h"
#include "packager/mpd/base/mpd_utils.h"
#include "packager/mpd/base/segment_info.h"
#include "packager/mpd/base/xml/scoped_xml_ptr.h"
ABSL_FLAG(bool,
segment_template_constant_duration,
false,
"Generates SegmentTemplate@duration if all segments except the "
"last one has the same duration if this flag is set to true.");
ABSL_FLAG(bool,
dash_add_last_segment_number_when_needed,
false,
"Adds a Supplemental Descriptor with @schemeIdUri "
"set to http://dashif.org/guidelines/last-segment-number with "
"the @value set to the last segment number.");
namespace shaka {
using xml::XmlNode;
typedef MediaInfo::AudioInfo AudioInfo;
typedef MediaInfo::VideoInfo VideoInfo;
namespace {
const char kEC3Codec[] = "ec-3";
const char kAC4Codec[] = "ac-4";
std::string RangeToString(const Range& range) {
return absl::StrFormat("%u-%u", range.begin(), range.end());
}
// Check if segments are continuous and all segments except the last one are of
// the same duration.
bool IsTimelineConstantDuration(const std::list<SegmentInfo>& segment_infos,
uint32_t start_number) {
if (!absl::GetFlag(FLAGS_segment_template_constant_duration))
return false;
DCHECK(!segment_infos.empty());
if (segment_infos.size() > 2)
return false;
const SegmentInfo& first_segment = segment_infos.front();
if (first_segment.start_time != first_segment.duration * (start_number - 1))
return false;
if (segment_infos.size() == 1)
return true;
const SegmentInfo& last_segment = segment_infos.back();
if (last_segment.repeat != 0)
return false;
const int64_t expected_last_segment_start_time =
first_segment.start_time +
first_segment.duration * (first_segment.repeat + 1);
return expected_last_segment_start_time == last_segment.start_time;
}
bool PopulateSegmentTimeline(const std::list<SegmentInfo>& segment_infos,
XmlNode* segment_timeline) {
for (const SegmentInfo& segment_info : segment_infos) {
XmlNode s_element("S");
RCHECK(s_element.SetIntegerAttribute("t", segment_info.start_time));
RCHECK(s_element.SetIntegerAttribute("d", segment_info.duration));
if (segment_info.repeat > 0)
RCHECK(s_element.SetIntegerAttribute("r", segment_info.repeat));
RCHECK(segment_timeline->AddChild(std::move(s_element)));
}
return true;
}
void CollectNamespaceFromName(const std::string& name,
std::set<std::string>* namespaces) {
const size_t pos = name.find(':');
if (pos != std::string::npos)
namespaces->insert(name.substr(0, pos));
}
void TraverseAttrsAndCollectNamespaces(const xmlAttr* attr,
std::set<std::string>* namespaces) {
for (const xmlAttr* cur_attr = attr; cur_attr; cur_attr = cur_attr->next) {
CollectNamespaceFromName(reinterpret_cast<const char*>(cur_attr->name),
namespaces);
}
}
void TraverseNodesAndCollectNamespaces(const xmlNode* node,
std::set<std::string>* namespaces) {
for (const xmlNode* cur_node = node; cur_node; cur_node = cur_node->next) {
CollectNamespaceFromName(reinterpret_cast<const char*>(cur_node->name),
namespaces);
TraverseNodesAndCollectNamespaces(cur_node->children, namespaces);
TraverseAttrsAndCollectNamespaces(cur_node->properties, namespaces);
}
}
} // namespace
namespace xml {
class XmlNode::Impl {
public:
scoped_xml_ptr<xmlNode> node;
};
XmlNode::XmlNode(const std::string& name) : impl_(new Impl) {
impl_->node.reset(xmlNewNode(NULL, BAD_CAST name.c_str()));
DCHECK(impl_->node);
}
XmlNode::XmlNode(XmlNode&&) = default;
XmlNode::~XmlNode() {}
XmlNode& XmlNode::operator=(XmlNode&&) = default;
bool XmlNode::AddChild(XmlNode child) {
DCHECK(impl_->node);
DCHECK(child.impl_->node);
RCHECK(xmlAddChild(impl_->node.get(), child.impl_->node.get()));
// Reaching here means the ownership of |child| transfered to |node|.
// Release the pointer so that it doesn't get destructed in this scope.
UNUSED(child.impl_->node.release());
return true;
}
bool XmlNode::AddElements(const std::vector<Element>& elements) {
for (size_t element_index = 0; element_index < elements.size();
++element_index) {
const Element& child_element = elements[element_index];
XmlNode child_node(child_element.name);
for (std::map<std::string, std::string>::const_iterator attribute_it =
child_element.attributes.begin();
attribute_it != child_element.attributes.end(); ++attribute_it) {
RCHECK(child_node.SetStringAttribute(attribute_it->first,
attribute_it->second));
}
// Note that somehow |SetContent| needs to be called before |AddElements|
// otherwise the added children will be overwritten by the content.
child_node.SetContent(child_element.content);
// Recursively set children for the child.
RCHECK(child_node.AddElements(child_element.subelements));
if (!xmlAddChild(impl_->node.get(), child_node.impl_->node.get())) {
LOG(ERROR) << "Failed to set child " << child_element.name
<< " to parent element "
<< reinterpret_cast<const char*>(impl_->node->name);
return false;
}
// Reaching here means the ownership of |child_node| transfered to |node|.
// Release the pointer so that it doesn't get destructed in this scope.
child_node.impl_->node.release();
}
return true;
}
bool XmlNode::SetStringAttribute(const std::string& attribute_name,
const std::string& attribute) {
DCHECK(impl_->node);
return xmlSetProp(impl_->node.get(), BAD_CAST attribute_name.c_str(),
BAD_CAST attribute.c_str()) != nullptr;
}
bool XmlNode::SetIntegerAttribute(const std::string& attribute_name,
uint64_t number) {
DCHECK(impl_->node);
return xmlSetProp(impl_->node.get(), BAD_CAST attribute_name.c_str(),
BAD_CAST(absl::StrFormat("%" PRIu64, number).c_str())) !=
nullptr;
}
bool XmlNode::SetFloatingPointAttribute(const std::string& attribute_name,
double number) {
DCHECK(impl_->node);
return xmlSetProp(impl_->node.get(), BAD_CAST attribute_name.c_str(),
BAD_CAST(FloatToXmlString(number).c_str())) != nullptr;
}
bool XmlNode::SetId(uint32_t id) {
return SetIntegerAttribute("id", id);
}
void XmlNode::AddContent(const std::string& content) {
DCHECK(impl_->node);
xmlNodeAddContent(impl_->node.get(), BAD_CAST content.c_str());
}
void XmlNode::SetContent(const std::string& content) {
DCHECK(impl_->node);
xmlNodeSetContent(impl_->node.get(), BAD_CAST content.c_str());
}
std::set<std::string> XmlNode::ExtractReferencedNamespaces() const {
std::set<std::string> namespaces;
TraverseNodesAndCollectNamespaces(impl_->node.get(), &namespaces);
return namespaces;
}
std::string XmlNode::ToString(const std::string& comment) const {
// Create an xmlDoc from xmlNodePtr. The node is copied so ownership does not
// transfer.
xml::scoped_xml_ptr<xmlDoc> doc(xmlNewDoc(BAD_CAST "1.0"));
if (comment.empty()) {
xmlDocSetRootElement(doc.get(), xmlCopyNode(impl_->node.get(), true));
} else {
xml::scoped_xml_ptr<xmlNode> comment_xml(
xmlNewDocComment(doc.get(), BAD_CAST comment.c_str()));
xmlDocSetRootElement(doc.get(), comment_xml.get());
xmlAddSibling(comment_xml.release(), xmlCopyNode(impl_->node.get(), true));
}
// Format the xmlDoc to string.
static const int kNiceFormat = 1;
int doc_str_size = 0;
xmlChar* doc_str = nullptr;
xmlDocDumpFormatMemoryEnc(doc.get(), &doc_str, &doc_str_size, "UTF-8",
kNiceFormat);
std::string output(doc_str, doc_str + doc_str_size);
xmlFree(doc_str);
return output;
}
bool XmlNode::GetAttribute(const std::string& name, std::string* value) const {
xml::scoped_xml_ptr<xmlChar> str(
xmlGetProp(impl_->node.get(), BAD_CAST name.c_str()));
if (!str)
return false;
*value = reinterpret_cast<const char*>(str.get());
return true;
}
xmlNode* XmlNode::GetRawPtr() const {
return impl_->node.get();
}
RepresentationBaseXmlNode::RepresentationBaseXmlNode(const std::string& name)
: XmlNode(name) {}
RepresentationBaseXmlNode::~RepresentationBaseXmlNode() {}
bool RepresentationBaseXmlNode::AddContentProtectionElements(
const std::list<ContentProtectionElement>& content_protection_elements) {
for (const auto& elem : content_protection_elements) {
RCHECK(AddContentProtectionElement(elem));
}
return true;
}
bool RepresentationBaseXmlNode::AddSupplementalProperty(
const std::string& scheme_id_uri,
const std::string& value) {
return AddDescriptor("SupplementalProperty", scheme_id_uri, value);
}
bool RepresentationBaseXmlNode::AddEssentialProperty(
const std::string& scheme_id_uri,
const std::string& value) {
return AddDescriptor("EssentialProperty", scheme_id_uri, value);
}
bool RepresentationBaseXmlNode::AddDescriptor(
const std::string& descriptor_name,
const std::string& scheme_id_uri,
const std::string& value) {
XmlNode descriptor(descriptor_name);
RCHECK(descriptor.SetStringAttribute("schemeIdUri", scheme_id_uri));
if (!value.empty())
RCHECK(descriptor.SetStringAttribute("value", value));
return AddChild(std::move(descriptor));
}
bool RepresentationBaseXmlNode::AddContentProtectionElement(
const ContentProtectionElement& content_protection_element) {
XmlNode content_protection_node("ContentProtection");
// @value is an optional attribute.
if (!content_protection_element.value.empty()) {
RCHECK(content_protection_node.SetStringAttribute(
"value", content_protection_element.value));
}
RCHECK(content_protection_node.SetStringAttribute(
"schemeIdUri", content_protection_element.scheme_id_uri));
for (const auto& pair : content_protection_element.additional_attributes) {
RCHECK(content_protection_node.SetStringAttribute(pair.first, pair.second));
}
RCHECK(content_protection_node.AddElements(
content_protection_element.subelements));
return AddChild(std::move(content_protection_node));
}
AdaptationSetXmlNode::AdaptationSetXmlNode()
: RepresentationBaseXmlNode("AdaptationSet") {}
AdaptationSetXmlNode::~AdaptationSetXmlNode() {}
bool AdaptationSetXmlNode::AddAccessibilityElement(
const std::string& scheme_id_uri,
const std::string& value) {
return AddDescriptor("Accessibility", scheme_id_uri, value);
}
bool AdaptationSetXmlNode::AddRoleElement(const std::string& scheme_id_uri,
const std::string& value) {
return AddDescriptor("Role", scheme_id_uri, value);
}
RepresentationXmlNode::RepresentationXmlNode()
: RepresentationBaseXmlNode("Representation") {}
RepresentationXmlNode::~RepresentationXmlNode() {}
bool RepresentationXmlNode::AddVideoInfo(const VideoInfo& video_info,
bool set_width,
bool set_height,
bool set_frame_rate) {
if (!video_info.has_width() || !video_info.has_height()) {
LOG(ERROR) << "Missing width or height for adding a video info.";
return false;
}
if (video_info.has_pixel_width() && video_info.has_pixel_height()) {
RCHECK(SetStringAttribute("sar",
absl::StrFormat("%d:%d", video_info.pixel_width(),
video_info.pixel_height())));
}
if (set_width)
RCHECK(SetIntegerAttribute("width", video_info.width()));
if (set_height)
RCHECK(SetIntegerAttribute("height", video_info.height()));
if (set_frame_rate) {
RCHECK(SetStringAttribute("frameRate",
absl::StrFormat("%d/%d", video_info.time_scale(),
video_info.frame_duration())));
}
if (video_info.has_playback_rate()) {
RCHECK(SetStringAttribute(
"maxPlayoutRate", absl::StrFormat("%d", video_info.playback_rate())));
// Since the trick play stream contains only key frames, there is no coding
// dependency on the main stream. Simply set the codingDependency to false.
// TODO(hmchen): propagate this attribute up to the AdaptationSet, since
// all are set to false.
RCHECK(SetStringAttribute("codingDependency", "false"));
}
return true;
}
bool RepresentationXmlNode::AddAudioInfo(const AudioInfo& audio_info) {
return AddAudioChannelInfo(audio_info) &&
AddAudioSamplingRateInfo(audio_info);
}
bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info,
bool use_segment_list,
double target_segment_duration) {
const bool use_single_segment_url_with_media =
media_info.has_text_info() && media_info.has_presentation_time_offset();
if (media_info.has_media_file_url() && !use_single_segment_url_with_media) {
XmlNode base_url("BaseURL");
base_url.SetContent(media_info.media_file_url());
RCHECK(AddChild(std::move(base_url)));
}
const bool need_segment_base_or_list =
use_segment_list || media_info.has_index_range() ||
media_info.has_init_range() ||
(media_info.has_reference_time_scale() && !media_info.has_text_info()) ||
use_single_segment_url_with_media;
if (!need_segment_base_or_list) {
return true;
}
XmlNode child(use_segment_list || use_single_segment_url_with_media
? "SegmentList"
: "SegmentBase");
// Forcing SegmentList for longer audio causes sidx atom to not be
// generated, therefore indexRange is not added to MPD if flag is set.
if (media_info.has_index_range() && !use_segment_list) {
RCHECK(child.SetStringAttribute("indexRange",
RangeToString(media_info.index_range())));
}
if (media_info.has_reference_time_scale()) {
RCHECK(child.SetIntegerAttribute("timescale",
media_info.reference_time_scale()));
if (use_segment_list && !use_single_segment_url_with_media) {
const auto duration_seconds = static_cast<int64_t>(
floor(target_segment_duration * media_info.reference_time_scale()));
RCHECK(child.SetIntegerAttribute("duration", duration_seconds));
}
}
if (media_info.has_presentation_time_offset()) {
RCHECK(child.SetIntegerAttribute("presentationTimeOffset",
media_info.presentation_time_offset()));
}
if (media_info.has_init_range()) {
XmlNode initialization("Initialization");
RCHECK(initialization.SetStringAttribute(
"range", RangeToString(media_info.init_range())));
RCHECK(child.AddChild(std::move(initialization)));
}
if (use_single_segment_url_with_media) {
XmlNode media_url("SegmentURL");
RCHECK(media_url.SetStringAttribute("media", media_info.media_file_url()));
RCHECK(child.AddChild(std::move(media_url)));
}
// Since the SegmentURLs here do not have a @media element,
// BaseURL element is mapped to the @media attribute.
if (use_segment_list) {
for (const Range& subsegment_range : media_info.subsegment_ranges()) {
XmlNode subsegment("SegmentURL");
RCHECK(subsegment.SetStringAttribute("mediaRange",
RangeToString(subsegment_range)));
RCHECK(child.AddChild(std::move(subsegment)));
}
}
RCHECK(AddChild(std::move(child)));
return true;
}
bool RepresentationXmlNode::AddLiveOnlyInfo(
const MediaInfo& media_info,
const std::list<SegmentInfo>& segment_infos,
uint32_t start_number,
bool low_latency_dash_mode) {
XmlNode segment_template("SegmentTemplate");
if (media_info.has_reference_time_scale()) {
RCHECK(segment_template.SetIntegerAttribute(
"timescale", media_info.reference_time_scale()));
}
if (media_info.has_segment_duration()) {
RCHECK(segment_template.SetIntegerAttribute("duration",
media_info.segment_duration()));
}
if (media_info.has_presentation_time_offset()) {
RCHECK(segment_template.SetIntegerAttribute(
"presentationTimeOffset", media_info.presentation_time_offset()));
}
if (media_info.has_availability_time_offset()) {
RCHECK(segment_template.SetFloatingPointAttribute(
"availabilityTimeOffset", media_info.availability_time_offset()));
}
fix: Low Latency DASH: include the "availabilityTimeComplete=false" attribute (#1198) # Low Latency DASH - `availabilityTimeComplete=false` Low Latency DASH manifests generated by Packager were missing the attribute `availabilityTimeComplete`. As per the [DASH specs](https://dashif.org/docs/CR-Low-Latency-Live-r8.pdf): **_the AdaptationSet@availabilityTimeCompleteshould be present and be set to 'FALSE'_** ## The Issue The missing attribute caused ULL streams from Shaka Packager to no longer be compatible with DASH.js. Previous versions of DASH.js allowed users to specify ULL mode when initializing the player. However, the most recent releases of DASH.js automatically detect ULL by scanning the manifest for ULL specific attributes. Although there are many attributes only associated with ULL, [DASH.js only greps for `availabilityTimeComplete` in its detection logic](https://github.com/Dash-Industry-Forum/dash.js/blob/development/src/streaming/controllers/PlaybackController.js#L792-L805). Because of the missing attribute in Packager and the limited ULL verification criteria by DASH.js, Packager streams were not being treated as low latency streams by DASH.js. ## Testing ### Unit Testing `./mpd_unittest --gtest_filter="SegmentTemplateTest.OneSegmentLowLatency"` ` ./mpd_unittest --gtest_filter="LowLatencySegmentTest.LowLatencySegmentTemplate"` ### Manual Testing - Created a low latency stream with Shaka Packager - Observed the expected `availabilityTimeComplete=false` attribute in the generated DASH manifest.
2023-07-05 21:33:51 +00:00
if (low_latency_dash_mode) {
RCHECK(segment_template.SetStringAttribute("availabilityTimeComplete",
"false"));
}
if (media_info.has_init_segment_url()) {
RCHECK(segment_template.SetStringAttribute("initialization",
media_info.init_segment_url()));
}
if (media_info.has_segment_template_url()) {
RCHECK(segment_template.SetStringAttribute(
"media", media_info.segment_template_url()));
RCHECK(segment_template.SetIntegerAttribute("startNumber", start_number));
}
if (!segment_infos.empty()) {
// Don't use SegmentTimeline if all segments except the last one are of
// the same duration.
if (IsTimelineConstantDuration(segment_infos, start_number)) {
RCHECK(segment_template.SetIntegerAttribute(
"duration", segment_infos.front().duration));
if (absl::GetFlag(FLAGS_dash_add_last_segment_number_when_needed)) {
uint32_t last_segment_number = start_number - 1;
for (const auto& segment_info_element : segment_infos)
last_segment_number += segment_info_element.repeat + 1;
RCHECK(AddSupplementalProperty(
"http://dashif.org/guidelines/last-segment-number",
std::to_string(last_segment_number)));
}
} else {
if (!low_latency_dash_mode) {
XmlNode segment_timeline("SegmentTimeline");
RCHECK(PopulateSegmentTimeline(segment_infos, &segment_timeline));
RCHECK(segment_template.AddChild(std::move(segment_timeline)));
}
}
}
return AddChild(std::move(segment_template));
}
bool RepresentationXmlNode::AddAudioChannelInfo(const AudioInfo& audio_info) {
std::string audio_channel_config_scheme;
std::string audio_channel_config_value;
if (audio_info.codec() == kEC3Codec) {
const auto& codec_data = audio_info.codec_specific_data();
// Use MPEG scheme if the mpeg value is available and valid, fallback to
// EC3 channel mapping otherwise.
// See https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/268
const uint32_t ec3_channel_mpeg_value = codec_data.channel_mpeg_value();
const uint32_t NO_MAPPING = 0xFFFFFFFF;
if (ec3_channel_mpeg_value == NO_MAPPING) {
// Convert EC3 channel map into string of hexadecimal digits. Spec:
// DASH-IF Interoperability Points v3.0 9.2.1.2.
audio_channel_config_value =
absl::StrFormat("%04X", codec_data.channel_mask());
audio_channel_config_scheme =
"tag:dolby.com,2014:dash:audio_channel_configuration:2011";
} else {
// Calculate EC3 channel configuration descriptor value with MPEG scheme.
// Spec: ETSI TS 102 366 V1.4.1 Digital Audio Compression
// (AC-3, Enhanced AC-3) I.1.2.
audio_channel_config_value =
absl::StrFormat("%u", ec3_channel_mpeg_value);
audio_channel_config_scheme = "urn:mpeg:mpegB:cicp:ChannelConfiguration";
}
bool ret = AddDescriptor("AudioChannelConfiguration",
audio_channel_config_scheme,
audio_channel_config_value);
// Dolby Digital Plus JOC descriptor. Spec: ETSI TS 103 420 v1.2.1
// Backwards-compatible object audio carriage using Enhanced AC-3 Standard
// D.2.2.
if (codec_data.ec3_joc_complexity() != 0) {
std::string ec3_joc_complexity =
absl::StrFormat("%u", codec_data.ec3_joc_complexity());
ret &= AddDescriptor("SupplementalProperty",
"tag:dolby.com,2018:dash:EC3_ExtensionType:2018",
"JOC");
ret &= AddDescriptor("SupplementalProperty",
"tag:dolby.com,2018:dash:"
"EC3_ExtensionComplexityIndex:2018",
ec3_joc_complexity);
}
return ret;
} else if (audio_info.codec().substr(0, 4) == kAC4Codec) {
const auto& codec_data = audio_info.codec_specific_data();
const bool ac4_ims_flag = codec_data.ac4_ims_flag();
// Use MPEG scheme if the mpeg value is available and valid, fallback to
// AC4 channel mask otherwise.
// See https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/268
const uint32_t ac4_channel_mpeg_value = codec_data.channel_mpeg_value();
const uint32_t NO_MAPPING = 0xFFFFFFFF;
if (ac4_channel_mpeg_value == NO_MAPPING) {
// Calculate AC-4 channel mask. Spec: ETSI TS 103 190-2 V1.2.1 Digital
// Audio Compression (AC-4) Standard; Part 2: Immersive and personalized
// audio G.3.1.
//
// this needs to print only 3 bytes of the 32-bit value
audio_channel_config_value =
absl::StrFormat("%06X", codec_data.channel_mask());
// Note that the channel config schemes for EC-3 and AC-4 are different.
// See https://github.com/Dash-Industry-Forum/DASH-IF-IOP/issues/268.
audio_channel_config_scheme =
"tag:dolby.com,2015:dash:audio_channel_configuration:2015";
} else {
// Calculate AC-4 channel configuration descriptor value with MPEG scheme.
// Spec: ETSI TS 103 190-2 V1.2.1 Digital Audio Compression (AC-4) Standard;
// Part 2: Immersive and personalized audio G.3.2.
audio_channel_config_value =
absl::StrFormat("%u", ac4_channel_mpeg_value);
audio_channel_config_scheme = "urn:mpeg:mpegB:cicp:ChannelConfiguration";
}
bool ret = AddDescriptor("AudioChannelConfiguration",
audio_channel_config_scheme,
audio_channel_config_value);
if (ac4_ims_flag) {
ret &= AddDescriptor("SupplementalProperty",
"tag:dolby.com,2016:dash:virtualized_content:2016",
"1");
}
return ret;
} else {
audio_channel_config_value =
absl::StrFormat("%u", audio_info.num_channels());
audio_channel_config_scheme =
"urn:mpeg:dash:23003:3:audio_channel_configuration:2011";
}
return AddDescriptor("AudioChannelConfiguration", audio_channel_config_scheme,
audio_channel_config_value);
}
// MPD expects one number for sampling frequency, or if it is a range it should
// be space separated.
bool RepresentationXmlNode::AddAudioSamplingRateInfo(
const AudioInfo& audio_info) {
return !audio_info.has_sampling_frequency() ||
SetIntegerAttribute("audioSamplingRate",
audio_info.sampling_frequency());
}
} // namespace xml
} // namespace shaka