diff --git a/docs/source/options/dash_stream_descriptors.rst b/docs/source/options/dash_stream_descriptors.rst new file mode 100644 index 0000000000..6e1f901099 --- /dev/null +++ b/docs/source/options/dash_stream_descriptors.rst @@ -0,0 +1,16 @@ +DASH specific stream descriptor fields +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +:dash_accessibilities (accessibilities): + + Optional semicolon separated list of values for DASH Accessibility element. + The value should be in the format: scheme_id_uri=value, which propagates + to the Accessibility element in the result DASH manifest. See DASH + (ISO/IEC 23009-1) specification for details. + +:dash_roles (roles): + + Optional semicolon separated list of values for DASH Role element. The + value should be one of: **caption**, **subtitle**, **main**, **alternate**, + **supplementary**, **commentary** and **dub**. See DASH (ISO/IEC 23009-1) + specification for details. diff --git a/docs/source/tutorials/dash.rst b/docs/source/tutorials/dash.rst index f6773a6c0d..4e4c6980fb 100644 --- a/docs/source/tutorials/dash.rst +++ b/docs/source/tutorials/dash.rst @@ -82,5 +82,6 @@ which describes the streams. Configuration options --------------------- +.. include:: /options/dash_stream_descriptors.rst .. include:: /options/dash_options.rst .. include:: /options/segment_template_formatting.rst diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index ff853bf746..27c734b8fa 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -104,7 +104,14 @@ const char kUsage[] = " unspecified, no I-Frames only playlist is created.\n" " - hls_characteristics (charcs): Optional colon/semicolon separated\n" " list of values for the CHARACTERISTICS attribute for EXT-X-MEDIA.\n" - " See CHARACTERISTICS attribute in http://bit.ly/2OOUkdB for details.\n"; + " See CHARACTERISTICS attribute in http://bit.ly/2OOUkdB for details.\n" + " - dash_accessibilities (accessibilities): Optional semicolon separated\n" + " list of values for DASH Accessibility elements. The value should be\n" + " in the format: scheme_id_uri=value.\n" + " - dash_roles (roles): Optional semicolon separated list of values for\n" + " DASH Role elements. The value should be one of: caption, subtitle,\n" + " main, alternate, supplementary, commentary and dub. See DASH\n" + " (ISO/IEC 23009-1) specification for details.\n"; // Labels for parameters in RawKey key info. const char kDrmLabelLabel[] = "label"; diff --git a/packager/app/stream_descriptor.cc b/packager/app/stream_descriptor.cc index b02b3ae3f5..08cd670010 100644 --- a/packager/app/stream_descriptor.cc +++ b/packager/app/stream_descriptor.cc @@ -31,6 +31,8 @@ enum FieldType { kSkipEncryptionField, kDrmStreamLabelField, kHlsCharacteristicsField, + kDashAccessiblitiesField, + kDashRolesField, }; struct FieldNameToTypeMapping { @@ -67,6 +69,14 @@ const FieldNameToTypeMapping kFieldNameTypeMappings[] = { {"hls_characteristics", kHlsCharacteristicsField}, {"characteristics", kHlsCharacteristicsField}, {"charcs", kHlsCharacteristicsField}, + {"dash_accessibilities", kDashAccessiblitiesField}, + {"dash_accessibility", kDashAccessiblitiesField}, + {"accessibilities", kDashAccessiblitiesField}, + {"accessibility", kDashAccessiblitiesField}, + {"dash_roles", kDashRolesField}, + {"dash_role", kDashRolesField}, + {"roles", kDashRolesField}, + {"role", kDashRolesField}, }; FieldType GetFieldType(const std::string& field_name) { @@ -176,6 +186,26 @@ base::Optional ParseStreamDescriptor( base::SplitString(iter->second, ";:", base::TRIM_WHITESPACE, base::SPLIT_WANT_NONEMPTY); break; + case kDashAccessiblitiesField: + descriptor.dash_accessiblities = + base::SplitString(iter->second, ";", base::TRIM_WHITESPACE, + base::SPLIT_WANT_NONEMPTY); + for (const std::string& accessibility : + descriptor.dash_accessiblities) { + size_t pos = accessibility.find('='); + if (pos == std::string::npos) { + LOG(ERROR) + << "Accessibility should be in scheme=value format, but seeing " + << accessibility; + return base::nullopt; + } + } + break; + case kDashRolesField: + descriptor.dash_roles = + base::SplitString(iter->second, ";", base::TRIM_WHITESPACE, + base::SPLIT_WANT_NONEMPTY); + break; default: LOG(ERROR) << "Unknown field in stream descriptor (\"" << iter->first << "\")."; diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index 38c420404b..3100be2315 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -275,6 +275,8 @@ class PackagerAppTest(unittest.TestCase): using_time_specifier=False, hls=False, hls_characteristics=None, + dash_accessibilities=None, + dash_roles=None, trick_play_factor=None, drm_label=None, skip_encryption=None, @@ -301,6 +303,8 @@ class PackagerAppTest(unittest.TestCase): $Number$. This flag is only relevant if segmented is True. hls: Should the output be for an HLS manifest. hls_characteristics: CHARACTERISTICS attribute for the HLS stream. + dash_accessibilities: Accessibility element for the DASH stream. + dash_roles: Role element for the DASH stream. trick_play_factor: Signals the stream is to be used for a trick play stream and which key frames to use. A trick play factor of 0 is the same as not specifying a trick play factor. @@ -356,6 +360,11 @@ class PackagerAppTest(unittest.TestCase): if hls_characteristics: stream.Append('hls_characteristics', hls_characteristics) + if dash_accessibilities: + stream.Append('dash_accessibilities', dash_accessibilities) + if dash_roles: + stream.Append('dash_roles', dash_roles) + requires_init_segment = segmented and base_ext not in [ 'aac', 'ac3', 'ec3', 'ts', 'vtt' ] @@ -638,6 +647,18 @@ class PackagerFunctionalTest(PackagerAppTest): self._GetStreams(['audio', 'video']), self._GetFlags(output_dash=True)) self._CheckTestResults('audio-video') + def testAudioVideoWithAccessibilitiesAndRoles(self): + streams = [ + self._GetStream( + 'audio', + dash_accessibilities='urn:tva:metadata:cs:AudioPurposeCS:2007=1', + dash_roles='alternate'), + self._GetStream('video'), + ] + + self.assertPackageSuccess(streams, self._GetFlags(output_dash=True)) + self._CheckTestResults('audio-video-with-accessibilities-and-roles') + def testAudioVideoWithTrickPlay(self): streams = [ self._GetStream('audio'), diff --git a/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/bear-640x360-audio.mp4 b/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/bear-640x360-audio.mp4 new file mode 100644 index 0000000000..87f89a93c0 Binary files /dev/null and b/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/bear-640x360-audio.mp4 differ diff --git a/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/bear-640x360-video.mp4 b/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/bear-640x360-video.mp4 new file mode 100644 index 0000000000..9bc668f8f6 Binary files /dev/null and b/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/bear-640x360-video.mp4 differ diff --git a/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/output.mpd b/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/output.mpd new file mode 100644 index 0000000000..174a565c99 --- /dev/null +++ b/packager/app/test/testdata/audio-video-with-accessibilities-and-roles/output.mpd @@ -0,0 +1,25 @@ + + + + + + + bear-640x360-video.mp4 + + + + + + + + + + + bear-640x360-audio.mp4 + + + + + + + diff --git a/packager/media/event/mpd_notify_muxer_listener.cc b/packager/media/event/mpd_notify_muxer_listener.cc index 5fab8bf305..aee82b60ba 100644 --- a/packager/media/event/mpd_notify_muxer_listener.cc +++ b/packager/media/event/mpd_notify_muxer_listener.cc @@ -71,6 +71,10 @@ void MpdNotifyMuxerListener::OnMediaStart( LOG(ERROR) << "Failed to generate MediaInfo from input."; return; } + for (const std::string& accessibility : accessibilities_) + media_info->add_dash_accessibilities(accessibility); + for (const std::string& role : roles_) + media_info->add_dash_roles(role); if (is_encrypted_) { internal::SetContentProtectionFields(protection_scheme_, default_key_id_, diff --git a/packager/media/event/mpd_notify_muxer_listener.h b/packager/media/event/mpd_notify_muxer_listener.h index 11becce19f..ffa84c1f86 100644 --- a/packager/media/event/mpd_notify_muxer_listener.h +++ b/packager/media/event/mpd_notify_muxer_listener.h @@ -55,6 +55,12 @@ class MpdNotifyMuxerListener : public MuxerListener { void OnCueEvent(int64_t timestamp, const std::string& cue_data) override; /// @} + void set_accessibilities(const std::vector& accessiblities) { + accessibilities_ = accessiblities; + } + + void set_roles(const std::vector& roles) { roles_ = roles; } + private: MpdNotifyMuxerListener(const MpdNotifyMuxerListener&) = delete; MpdNotifyMuxerListener& operator=(const MpdNotifyMuxerListener&) = delete; @@ -65,6 +71,9 @@ class MpdNotifyMuxerListener : public MuxerListener { base::Optional notification_id_; std::unique_ptr media_info_; + std::vector accessibilities_; + std::vector roles_; + bool is_encrypted_ = false; // Storage for values passed to OnEncryptionInfoReady(). FourCC protection_scheme_ = FOURCC_NULL; diff --git a/packager/media/event/muxer_listener_factory.cc b/packager/media/event/muxer_listener_factory.cc index 704bfa7bae..2442998feb 100644 --- a/packager/media/event/muxer_listener_factory.cc +++ b/packager/media/event/muxer_listener_factory.cc @@ -6,6 +6,7 @@ #include "packager/media/event/muxer_listener_factory.h" +#include "packager/base/memory/ptr_util.h" #include "packager/base/strings/stringprintf.h" #include "packager/hls/base/hls_notifier.h" #include "packager/media/event/combined_muxer_listener.h" @@ -30,10 +31,13 @@ std::unique_ptr CreateMediaInfoDumpListenerInternal( } std::unique_ptr CreateMpdListenerInternal( + const MuxerListenerFactory::StreamData& stream, MpdNotifier* notifier) { DCHECK(notifier); - std::unique_ptr listener(new MpdNotifyMuxerListener(notifier)); + auto listener = base::MakeUnique(notifier); + listener->set_accessibilities(stream.dash_accessiblities); + listener->set_roles(stream.dash_roles); return listener; } @@ -91,7 +95,8 @@ std::unique_ptr MuxerListenerFactory::CreateListener( CreateMediaInfoDumpListenerInternal(stream.media_info_output)); } if (mpd_notifier_) { - combined_listener->AddListener(CreateMpdListenerInternal(mpd_notifier_)); + combined_listener->AddListener( + CreateMpdListenerInternal(stream, mpd_notifier_)); } if (hls_notifier_) { for (auto& listener : diff --git a/packager/media/event/muxer_listener_factory.h b/packager/media/event/muxer_listener_factory.h index 8e8f3f6941..a87bb746bd 100644 --- a/packager/media/event/muxer_listener_factory.h +++ b/packager/media/event/muxer_listener_factory.h @@ -46,6 +46,11 @@ class MuxerListenerFactory { std::string hls_playlist_name; std::string hls_iframe_playlist_name; std::vector hls_characteristics; + + // DASH specific values needed to write DASH mpd. Will only be used if an + // MpdNotifier is given to the factory. + std::vector dash_accessiblities; + std::vector dash_roles; }; /// Create a new muxer listener. diff --git a/packager/mpd/base/adaptation_set.cc b/packager/mpd/base/adaptation_set.cc index 782847237b..d0a0581309 100644 --- a/packager/mpd/base/adaptation_set.cc +++ b/packager/mpd/base/adaptation_set.cc @@ -55,11 +55,8 @@ std::string RoleToText(AdaptationSet::Role role) { case AdaptationSet::kRoleDub: return "dub"; default: - break; + return "unknown"; } - - NOTREACHED(); - return ""; } // Returns the picture aspect ratio string e.g. "16:9", "4:3". @@ -225,6 +222,11 @@ void AdaptationSet::UpdateContentProtectionPssh(const std::string& drm_uuid, &content_protection_elements_); } +void AdaptationSet::AddAccessibility(const std::string& scheme, + const std::string& value) { + accessibilities_.push_back(Accessibility{scheme, value}); +} + void AdaptationSet::AddRole(Role role) { roles_.insert(role); } @@ -318,6 +320,11 @@ xml::scoped_xml_ptr AdaptationSet::GetXml() { "urn:mpeg:dash:adaptation-set-switching:2016", switching_ids); } + for (const AdaptationSet::Accessibility& accessibility : accessibilities_) { + adaptation_set.AddAccessibilityElement(accessibility.scheme, + accessibility.value); + } + for (AdaptationSet::Role role : roles_) adaptation_set.AddRoleElement("urn:mpeg:dash:role:2011", RoleToText(role)); diff --git a/packager/mpd/base/adaptation_set.h b/packager/mpd/base/adaptation_set.h index 41911cfc6b..c3d7a89894 100644 --- a/packager/mpd/base/adaptation_set.h +++ b/packager/mpd/base/adaptation_set.h @@ -40,6 +40,7 @@ class AdaptationSet { // element to the AdaptationSet with schemeIdUri=urn:mpeg:dash:role:2011. // See ISO/IEC 23009-1:2012 section 5.8.5.5. enum Role { + kRoleUnknown, kRoleCaption, kRoleSubtitle, kRoleMain, @@ -94,6 +95,13 @@ class AdaptationSet { virtual void UpdateContentProtectionPssh(const std::string& drm_uuid, const std::string& pssh); + /// Set the Accessibility element for this AdaptationSet. + /// See ISO/IEC 23009-1:2012 section 5.8.4.3. + /// @param scheme is the schemeIdUri of the accessibility element. + /// @param value is the value of the accessibility element. + virtual void AddAccessibility(const std::string& scheme, + const std::string& value); + /// Set the Role element for this AdaptationSet. /// The Role element's is schemeIdUri='urn:mpeg:dash:role:2011'. /// See ISO/IEC 23009-1:2012 section 5.8.5.5. @@ -271,6 +279,13 @@ class AdaptationSet { // in this set. std::set picture_aspect_ratio_; + // accessibilities of this AdaptationSet. + struct Accessibility { + std::string scheme; + std::string value; + }; + std::vector accessibilities_; + // The roles of this AdaptationSet. std::set roles_; diff --git a/packager/mpd/base/adaptation_set_unittest.cc b/packager/mpd/base/adaptation_set_unittest.cc index b145e8df9e..0d6e37d3b8 100644 --- a/packager/mpd/base/adaptation_set_unittest.cc +++ b/packager/mpd/base/adaptation_set_unittest.cc @@ -163,6 +163,22 @@ TEST_F(AdaptationSetTest, CheckAdaptationSetId) { AttributeEqual("id", std::to_string(kAdaptationSetId))); } +// Verify AdaptationSet::AddAccessibilityElement() works. +TEST_F(AdaptationSetTest, AddAccessibilityElement) { + auto adaptation_set = CreateAdaptationSet(kNoLanguage); + adaptation_set->AddAccessibility("urn:tva:metadata:cs:AudioPurposeCS:2007", + "2"); + + // The empty contentType is sort of a side effect of being able to generate an + // MPD without adding any Representations. + const char kExpectedOutput[] = + "\n" + " \n" + ""; + EXPECT_THAT(adaptation_set->GetXml().get(), XmlNodeEqual(kExpectedOutput)); +} + // Verify AdaptationSet::AddRole() works for "main" role. TEST_F(AdaptationSetTest, AdaptationAddRoleElementMain) { auto adaptation_set = CreateAdaptationSet(kNoLanguage); diff --git a/packager/mpd/base/media_info.proto b/packager/mpd/base/media_info.proto index 7432c112da..b503606906 100644 --- a/packager/mpd/base/media_info.proto +++ b/packager/mpd/base/media_info.proto @@ -164,4 +164,12 @@ message MediaInfo { // HLS only. Defines CHARACTERISTICS attribute of the stream. repeated string hls_characteristics = 20; + + // DASH only. Defines Accessibility elements of the stream. It should be in + // the format: scheme_id_uri=value. + repeated string dash_accessibilities = 21; + // DASH only. Defines Role elements of the stream. The value can be a valid + // Role value defined in "urn:mpeg:dash:role:2011" scheme or in the format: + // scheme_id_uri=value (to be implemented). + repeated string dash_roles = 22; } diff --git a/packager/mpd/base/mpd_utils.cc b/packager/mpd/base/mpd_utils.cc index ff3f44a8b4..c8b99ee8a7 100644 --- a/packager/mpd/base/mpd_utils.cc +++ b/packager/mpd/base/mpd_utils.cc @@ -168,6 +168,18 @@ std::string GetAdaptationSetKey(const MediaInfo& media_info) { key.append(":trick_play"); } + if (!media_info.dash_accessibilities().empty()) { + key.append(":accessibility_"); + for (const std::string& accessibility : media_info.dash_accessibilities()) + key.append(accessibility); + } + + if (!media_info.dash_roles().empty()) { + key.append(":roles_"); + for (const std::string& role : media_info.dash_roles()) + key.append(role); + } + return key; } diff --git a/packager/mpd/base/period.cc b/packager/mpd/base/period.cc index 06c85ed9d0..aa9601a252 100644 --- a/packager/mpd/base/period.cc +++ b/packager/mpd/base/period.cc @@ -42,6 +42,24 @@ const std::string& GetDefaultTextLanguage(const MpdOptions& mpd_options) { : mpd_options.mpd_params.default_text_language; } +AdaptationSet::Role RoleFromString(const std::string& role_str) { + if (role_str == "caption") + return AdaptationSet::Role::kRoleCaption; + if (role_str == "subtitle") + return AdaptationSet::Role::kRoleSubtitle; + if (role_str == "main") + return AdaptationSet::Role::kRoleMain; + if (role_str == "alternate") + return AdaptationSet::Role::kRoleAlternate; + if (role_str == "supplementary") + return AdaptationSet::Role::kRoleSupplementary; + if (role_str == "commentary") + return AdaptationSet::Role::kRoleCommentary; + if (role_str == "dub") + return AdaptationSet::Role::kRoleDub; + return AdaptationSet::Role::kRoleUnknown; +} + } // namespace Period::Period(uint32_t period_id, @@ -159,7 +177,16 @@ bool Period::SetNewAdaptationSetAttributes( const MediaInfo& media_info, const std::list& adaptation_sets, AdaptationSet* new_adaptation_set) { - if (!language.empty()) { + if (!media_info.dash_roles().empty()) { + for (const std::string& role_str : media_info.dash_roles()) { + AdaptationSet::Role role = RoleFromString(role_str); + if (role == AdaptationSet::kRoleUnknown) { + LOG(ERROR) << "Unrecognized role '" << role_str << "'."; + return false; + } + new_adaptation_set->AddRole(role); + } + } else if (!language.empty()) { const bool is_main_role = language == (media_info.has_audio_info() ? GetDefaultAudioLanguage(mpd_options_) @@ -167,6 +194,17 @@ bool Period::SetNewAdaptationSetAttributes( if (is_main_role) new_adaptation_set->AddRole(AdaptationSet::kRoleMain); } + for (const std::string& accessibility : media_info.dash_accessibilities()) { + size_t pos = accessibility.find('='); + if (pos == std::string::npos) { + LOG(ERROR) + << "Accessibility should be in scheme=value format, but seeing " + << accessibility; + return false; + } + new_adaptation_set->AddAccessibility(accessibility.substr(0, pos), + accessibility.substr(pos + 1)); + } if (media_info.has_video_info()) { // Because 'language' is ignored for videos, |adaptation_sets| must have diff --git a/packager/mpd/base/xml/xml_node.cc b/packager/mpd/base/xml/xml_node.cc index 51df13fcc9..92f7d69162 100644 --- a/packager/mpd/base/xml/xml_node.cc +++ b/packager/mpd/base/xml/xml_node.cc @@ -286,6 +286,16 @@ AdaptationSetXmlNode::AdaptationSetXmlNode() : RepresentationBaseXmlNode("AdaptationSet") {} AdaptationSetXmlNode::~AdaptationSetXmlNode() {} +void AdaptationSetXmlNode::AddAccessibilityElement( + const std::string& scheme_id_uri, + const std::string& value) { + XmlNode accessibility("Accessibility"); + accessibility.SetStringAttribute("schemeIdUri", scheme_id_uri); + if (!value.empty()) + accessibility.SetStringAttribute("value", value); + AddChild(accessibility.PassScopedPtr()); +} + void AdaptationSetXmlNode::AddRoleElement(const std::string& scheme_id_uri, const std::string& value) { XmlNode role("Role"); diff --git a/packager/mpd/base/xml/xml_node.h b/packager/mpd/base/xml/xml_node.h index 1dc1f71ae0..364622f7ea 100644 --- a/packager/mpd/base/xml/xml_node.h +++ b/packager/mpd/base/xml/xml_node.h @@ -130,6 +130,11 @@ class AdaptationSetXmlNode : public RepresentationBaseXmlNode { AdaptationSetXmlNode(); ~AdaptationSetXmlNode() override; + /// @param scheme_id_uri is content of the schemeIdUri attribute. + /// @param value is the content of value attribute. + void AddAccessibilityElement(const std::string& scheme_id_uri, + const std::string& value); + /// @param scheme_id_uri is content of the schemeIdUri attribute. /// @param value is the content of value attribute. void AddRoleElement(const std::string& scheme_id_uri, diff --git a/packager/packager.cc b/packager/packager.cc index d7d4d42689..e8fb0b010e 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -87,11 +87,15 @@ MuxerListenerFactory::StreamData ToMuxerListenerData( const StreamDescriptor& stream) { MuxerListenerFactory::StreamData data; data.media_info_output = stream.output; + data.hls_group_id = stream.hls_group_id; data.hls_name = stream.hls_name; data.hls_playlist_name = stream.hls_playlist_name; data.hls_iframe_playlist_name = stream.hls_iframe_playlist_name; data.hls_characteristics = stream.hls_characteristics; + + data.dash_accessiblities = stream.dash_accessiblities; + data.dash_roles = stream.dash_roles; return data; }; diff --git a/packager/packager.h b/packager/packager.h index a70d2b5f40..b420189ae2 100644 --- a/packager/packager.h +++ b/packager/packager.h @@ -123,6 +123,11 @@ struct StreamDescriptor { /// Optional for HLS output. It defines the CHARACTERISTICS attribute of the /// stream. std::vector hls_characteristics; + + /// Optional for DASH output. It defines Accessibility elements of the stream. + std::vector dash_accessiblities; + /// Optional for DASH output. It defines Role elements of the stream. + std::vector dash_roles; }; class SHAKA_EXPORT Packager {