diff --git a/mpd/base/mpd_builder.cc b/mpd/base/mpd_builder.cc new file mode 100644 index 0000000000..5916a28f6a --- /dev/null +++ b/mpd/base/mpd_builder.cc @@ -0,0 +1,313 @@ +#include "mpd/base/mpd_builder.h" + +#include + +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/strings/string_number_conversions.h" +#include "mpd/base/content_protection_element.h" +#include "mpd/base/mpd_utils.h" +#include "mpd/base/xml/xml_node.h" +#include "third_party/libxml/src/include/libxml/tree.h" + +// TODO(rkuroiwa): If performance is a problem work on fine grained locking. +namespace dash_packager { + +using xml::XmlNode; +using xml::RepresentationXmlNode; +using xml::AdaptationSetXmlNode; + +MpdBuilder::MpdBuilder(MpdType type) + : type_(type), + adaptation_sets_deleter_(&adaptation_sets_) {} + +MpdBuilder::~MpdBuilder() {} + +void MpdBuilder::AddBaseUrl(const std::string& base_url) { + base::AutoLock scoped_lock(lock_); + base_urls_.push_back(base_url); +} + +AdaptationSet* MpdBuilder::AddAdaptationSet() { + base::AutoLock scoped_lock(lock_); + scoped_ptr adaptation_set(new AdaptationSet( + adaptation_set_counter_.GetNext(), &representation_counter_)); + + DCHECK(adaptation_set); + adaptation_sets_.push_back(adaptation_set.get()); + return adaptation_set.release(); +} + +bool MpdBuilder::WriteMpd() { + base::AutoLock scoped_lock(lock_); + std::string mpd; + bool result = ToStringImpl(&mpd); + + // TODO(rkuroiwa): Write to file, after interface change. + return result; +} + +bool MpdBuilder::ToString(std::string* output) { + base::AutoLock scoped_lock(lock_); + return ToStringImpl(output); +} + +bool MpdBuilder::ToStringImpl(std::string* output) { + xmlInitParser(); + xml::ScopedXmlPtr::type doc(GenerateMpd()); + if (!doc.get()) + return false; + + static const int kNiceFormat = 1; + int doc_str_size = 0; + xmlChar* doc_str = NULL; + xmlDocDumpFormatMemoryEnc( + doc.get(), &doc_str, &doc_str_size, "UTF-8", kNiceFormat); + + output->assign(doc_str, doc_str + doc_str_size); + xmlFree(doc_str); + + DLOG(INFO) << *output; + + // Cleanup, free the doc then cleanup parser. + doc.reset(); + xmlCleanupParser(); + return true; +} + +// TODO(rkuroiwa): This function is too big. +xmlDocPtr MpdBuilder::GenerateMpd() { + // Setup nodes. + static const char kXmlVersion[] = "1.0"; + xml::ScopedXmlPtr::type doc(xmlNewDoc(BAD_CAST kXmlVersion)); + XmlNode mpd("MPD"); + + static const char kXmlSchema[] = "http://www.w3.org/2001/XMLSchema-instance"; + mpd.SetStringAttribute("xmlns:xsi", kXmlSchema); + + static const char kDashSchemaMpd2011[] = + "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd"; + mpd.SetStringAttribute("xsi:schemaLocation", kDashSchemaMpd2011); + + static const char kMinBufferTimeTwoSeconds[] = "PT2S"; + mpd.SetStringAttribute("minBufferTime", kMinBufferTimeTwoSeconds); + + // Iterate thru AdaptationSets and add them to one big Period element. + XmlNode period("Period"); + std::list::iterator adaptation_sets_it = + adaptation_sets_.begin(); + for (; adaptation_sets_it != adaptation_sets_.end(); ++adaptation_sets_it) { + xml::ScopedXmlPtr::type child((*adaptation_sets_it)->GetXml()); + if (!child.get() || !period.AddChild(child.Pass())) + return NULL; + } + + // Add baseurls to MPD. + std::list::const_iterator base_urls_it = base_urls_.begin(); + for (; base_urls_it != base_urls_.end(); ++base_urls_it) { + XmlNode base_url("BaseURL"); + base_url.SetContent(*base_urls_it); + + if (!mpd.AddChild(base_url.PassScopedPtr())) + return NULL; + } + + if (!mpd.AddChild(period.PassScopedPtr())) + return NULL; + + switch (type_) { + case kStatic: + AddStaticMpdInfo(&mpd); + break; + case kDynamic: + NOTIMPLEMENTED() << "MPD for live is not implemented."; + break; + default: + NOTREACHED() << "Unknown MPD type: " << type_; + break; + } + + DCHECK(doc); + xmlDocSetRootElement(doc.get(), mpd.Release()); + return doc.release(); +} + +void MpdBuilder::AddStaticMpdInfo(XmlNode* mpd_node) { + DCHECK(mpd_node); + DCHECK_EQ(MpdBuilder::kStatic, type_); + + static const char kStaticMpdType[] = "static"; + static const char kStaticMpdProfile[] = + "urn:mpeg:dash:profile:isoff-on-demand:2011"; + mpd_node->SetStringAttribute("type", kStaticMpdType); + mpd_node->SetStringAttribute("profiles", kStaticMpdProfile); + mpd_node->SetStringAttribute( + "mediaPresentationDuration", + SecondsToXmlDuration(GetStaticMpdDuration(mpd_node))); +} + +uint32 MpdBuilder::GetStaticMpdDuration(XmlNode* mpd_node) { + DCHECK(mpd_node); + DCHECK_EQ(MpdBuilder::kStatic, type_); + + xmlNodePtr period_node = xmlFirstElementChild(mpd_node->GetRawPtr()); + DCHECK(period_node); + DCHECK_NE(strcmp(reinterpret_cast(period_node->name), "Period"), + 0); + + // TODO(rkuroiwa): Update this so that the duration for each Representation is + // (duration / timescale). + // Attribute mediaPresentationDuration must be present for 'static' MPD. So + // setting "P0S" is still required if none of the representaions had a + // duration attribute. + uint32 max_duration = 0; + for (xmlNodePtr adaptation_set = xmlFirstElementChild(period_node); + adaptation_set; + adaptation_set = xmlNextElementSibling(adaptation_set)) { + for (xmlNodePtr representation = xmlFirstElementChild(adaptation_set); + representation; + representation = xmlNextElementSibling(representation)) { + uint32 duration = 0; + if (GetDurationAttribute(representation, &duration)) { + max_duration = max_duration > duration ? max_duration : duration; + + // 'duration' attribute is there only to help generate MPD, not + // necessary for MPD, remove the attribute. + xmlUnsetProp(representation, BAD_CAST "duration"); + } + } + } + + return max_duration; +} + +AdaptationSet::AdaptationSet(uint32 adaptation_set_id, + base::AtomicSequenceNumber* counter) + : representations_deleter_(&representations_), + representation_counter_(counter), + id_(adaptation_set_id) { + DCHECK(counter); +} + +AdaptationSet::~AdaptationSet() {} + +Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) { + base::AutoLock scoped_lock(lock_); + if (HasVODOnlyFields(media_info) && HasLiveOnlyFields(media_info)) { + LOG(ERROR) << "MediaInfo cannot have both VOD and Live fields."; + return NULL; + } + + if (!media_info.has_bandwidth()) { + LOG(ERROR) << "MediaInfo missing required bandwidth field."; + return NULL; + } + + scoped_ptr representation( + new Representation(media_info, representation_counter_->GetNext())); + + DCHECK(representation); + representations_.push_back(representation.get()); + return representation.release(); +} + +void AdaptationSet::AddContentProtectionElement( + const ContentProtectionElement& content_protection_element) { + base::AutoLock scoped_lock(lock_); + content_protection_elements_.push_back(content_protection_element); + RemoveDuplicateAttributes(&content_protection_elements_.back()); +} + +// Creates a copy of xml element, iterate thru all the +// (child) elements and add them to the copy. +xml::ScopedXmlPtr::type AdaptationSet::GetXml() { + base::AutoLock scoped_lock(lock_); + AdaptationSetXmlNode adaptation_set; + + if (!adaptation_set.AddContentProtectionElements( + content_protection_elements_)) { + return xml::ScopedXmlPtr::type(); + } + + std::list::iterator representation_it = + representations_.begin(); + + for (; representation_it != representations_.end(); ++representation_it) { + xml::ScopedXmlPtr::type child((*representation_it)->GetXml()); + if (!child.get() || !adaptation_set.AddChild(child.Pass())) + return xml::ScopedXmlPtr::type(); + } + + adaptation_set.SetId(id_); + return adaptation_set.PassScopedPtr(); +} + +Representation::Representation(const MediaInfo& media_info, uint32 id) + : media_info_(media_info), id_(id) {} + +Representation::~Representation() {} + +void Representation::AddContentProtectionElement( + const ContentProtectionElement& content_protection_element) { + base::AutoLock scoped_lock(lock_); + content_protection_elements_.push_back(content_protection_element); + RemoveDuplicateAttributes(&content_protection_elements_.back()); +} + +bool Representation::AddNewSegment(uint64 start_time, uint64 duration) { + base::AutoLock scoped_lock(lock_); + segment_starttime_duration_pairs_.push_back( + std::pair(start_time, duration)); + return true; +} + +// TODO(rkuroiwa): We don't need to create a node every single time. Make an +// internal copy of this element. +// Uses info in |media_info_| and |content_protection_elements_| to create a +// "Representation" node. +xml::ScopedXmlPtr::type Representation::GetXml() { + base::AutoLock scoped_lock(lock_); + DCHECK(!(HasVODOnlyFields(media_info_) && HasLiveOnlyFields(media_info_))); + DCHECK(media_info_.has_bandwidth()); + + RepresentationXmlNode representation; + if (!representation.AddContentProtectionElements( + content_protection_elements_)) { + return xml::ScopedXmlPtr::type(); + } + + // Two 'Mandatory' fields for Representation. + representation.SetId(id_); + representation.SetNumberAttribute("bandwidth", media_info_.bandwidth()); + + const bool has_video_info = media_info_.video_info_size() > 0; + const bool has_audio_info = media_info_.audio_info_size() > 0; + if (has_video_info || has_audio_info) { + const std::string codecs = GetCodecs(media_info_); + if (!codecs.empty()) + representation.SetStringAttribute("codecs", codecs); + + if (has_video_info) { + if (!representation.AddVideoInfo(media_info_.video_info())) + return xml::ScopedXmlPtr::type(); + } + + if (has_audio_info) { + if (!representation.AddAudioInfo(media_info_.audio_info())) + return xml::ScopedXmlPtr::type(); + } + } + + // TODO(rkuroiwa): Add TextInfo. + // TODO(rkuroiwa): Add ContentProtection info. + if (HasVODOnlyFields(media_info_)) { + if (!representation.AddVODOnlyInfo(media_info_)) + return xml::ScopedXmlPtr::type(); + } + + // TODO(rkuroiwa): Handle Live case. Handle data in + // segment_starttime_duration_pairs_. + return representation.PassScopedPtr(); +} + +} // namespace dash_packager diff --git a/mpd/base/mpd_builder.h b/mpd/base/mpd_builder.h new file mode 100644 index 0000000000..61e7caa65b --- /dev/null +++ b/mpd/base/mpd_builder.h @@ -0,0 +1,156 @@ +// This file contains the MpdBuilder, AdaptationSet, and Representation class +// declarations. +// http://goo.gl/UrsSlF + +#ifndef MPD_BASE_MPD_BUILDER_H_ +#define MPD_BASE_MPD_BUILDER_H_ + +#include +#include + +#include "base/atomic_sequence_num.h" +#include "base/basictypes.h" +#include "base/stl_util.h" +#include "base/synchronization/lock.h" +#include "mpd/base/content_protection_element.h" +#include "mpd/base/media_info.pb.h" +#include "mpd/base/mpd_utils.h" +#include "mpd/base/xml/scoped_xml_ptr.h" + +// TODO(rkuroiwa): For classes with |id_|, consider removing the field and let +// the MPD (XML) generation functions take care of assigning an ID to each +// element. +namespace dash_packager { + +class AdaptationSet; +class Representation; + +namespace xml { + +class XmlNode; + +} // namespace xml + +class MpdBuilder { + public: + enum MpdType { + kStatic = 0, + kDynamic + }; + + explicit MpdBuilder(MpdType type); + ~MpdBuilder(); + + void AddBaseUrl(const std::string& base_url); + + // The returned pointer is owned by this object. + AdaptationSet* AddAdaptationSet(); + + // TODO(rkuroiwa): Once File interface is defined, make this method take a + // pointer to a File. + // This will write to stdout until File interface is defined. + bool WriteMpd(); + bool ToString(std::string* output); + + private: + bool ToStringImpl(std::string* output); + + // Returns the document pointer to the MPD. This must be freed by the caller + // using appropriate xmlDocPtr freeing function. + // On failure, this returns NULL. + xmlDocPtr GenerateMpd(); + + // Adds 'static' MPD attributes and elements to |mpd_node|. This assumes that + // the first child element is a Period element. + void AddStaticMpdInfo(xml::XmlNode* mpd_node); + uint32 GetStaticMpdDuration(xml::XmlNode* mpd_node); + + MpdType type_; + std::list adaptation_sets_; + ::STLElementDeleter > adaptation_sets_deleter_; + + std::list base_urls_; + + // TODO(rkuroiwa): I don't like locks. + base::Lock lock_; + base::AtomicSequenceNumber adaptation_set_counter_; + base::AtomicSequenceNumber representation_counter_; + + DISALLOW_COPY_AND_ASSIGN(MpdBuilder); +}; + +class AdaptationSet { + public: + AdaptationSet(uint32 adaptation_set_id, + base::AtomicSequenceNumber* representation_counter); + ~AdaptationSet(); + + // The returned pointer is owned by this object. + Representation* AddRepresentation(const MediaInfo& media_info); + + // If |element| has {value, schemeIdUri} set and has + // {“value”, “schemeIdUri”} as key for additional_attributes, + // then the former is used. + void AddContentProtectionElement(const ContentProtectionElement& element); + + // Makes a copy of AdaptationSet xml element with its child elements, which + // are Representation elements. On success this returns non-NULL ScopedXmlPtr, + // otherwise returns NULL ScopedXmlPtr. + xml::ScopedXmlPtr::type GetXml(); + + // Must be unique in the Period. + uint32 id() const { + return id_; + } + + private: + std::list content_protection_elements_; + std::list representations_; + ::STLElementDeleter > representations_deleter_; + + base::Lock lock_; + + base::AtomicSequenceNumber* const representation_counter_; + + const uint32 id_; + + DISALLOW_COPY_AND_ASSIGN(AdaptationSet); +}; + +class Representation { + public: + Representation(const MediaInfo& media_info, uint32 representation_id); + ~Representation(); + + // If |element| has {value, schemeIdUri} set and has + // {“value”, “schemeIdUri”} as key for additional_attributes, + // then the former is used. + void AddContentProtectionElement(const ContentProtectionElement& element); + + bool AddNewSegment(uint64 start_time, uint64 duration); + + // Makes a copy of the current XML. Note that this is a copy. The caller is + // responsible for cleaning up the allocated resource. + xml::ScopedXmlPtr::type GetXml(); + + // Must be unique amongst other Representations in the MPD file. + // As the MPD spec says. + uint32 id() const { + return id_; + } + + private: + MediaInfo media_info_; + std::list content_protection_elements_; + std::list > segment_starttime_duration_pairs_; + + base::Lock lock_; + + const uint32 id_; + + DISALLOW_COPY_AND_ASSIGN(Representation); +}; + +} // namespace dash_packager + +#endif // MPD_BASE_MPD_BUILDER_H_ diff --git a/mpd/mpd.gyp b/mpd/mpd.gyp index e5d2cff6e1..4f61d99884 100644 --- a/mpd/mpd.gyp +++ b/mpd/mpd.gyp @@ -15,5 +15,27 @@ }, 'includes': ['../build/protoc.gypi'], }, + { + 'target_name': 'mpd_builder', + 'type': 'static_library', + 'sources': [ + 'base/content_protection_element.h', + 'base/mpd_builder.cc', + 'base/mpd_builder.h', + 'base/mpd_utils.cc', + 'base/mpd_utils.h', + 'base/xml/scoped_xml_ptr.h', + 'base/xml/xml_node.cc', + 'base/xml/xml_node.h', + ], + 'include_dirs': [ + '..', + ], + 'dependencies': [ + '../base/base.gyp:base', + '../third_party/libxml/libxml.gyp:libxml', + 'media_info_proto', + ], + }, ], }