From 8c5399533547e28fa22c7b07500817a79b4def23 Mon Sep 17 00:00:00 2001 From: Rintaro Kuroiwa Date: Thu, 29 Oct 2015 13:58:36 -0700 Subject: [PATCH] Text support for MpdBuilder Change-Id: I75e1da391356e9edfcb520941029341941c31332 --- packager/mpd/base/dash_iop_mpd_notifier.cc | 8 + .../base/dash_iop_mpd_notifier_unittest.cc | 36 +++- packager/mpd/base/media_info.proto | 7 + packager/mpd/base/mock_mpd_builder.h | 1 + packager/mpd/base/mpd_builder.cc | 91 ++++++++-- packager/mpd/base/mpd_builder.h | 8 +- packager/mpd/base/mpd_builder_unittest.cc | 171 ++++++++++++++++-- packager/mpd/base/mpd_utils.cc | 37 ++-- packager/mpd/base/simple_mpd_notifier.cc | 2 + 9 files changed, 316 insertions(+), 45 deletions(-) diff --git a/packager/mpd/base/dash_iop_mpd_notifier.cc b/packager/mpd/base/dash_iop_mpd_notifier.cc index c9814cbcb1..d48b13e36e 100644 --- a/packager/mpd/base/dash_iop_mpd_notifier.cc +++ b/packager/mpd/base/dash_iop_mpd_notifier.cc @@ -77,11 +77,19 @@ bool DashIopMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, std::string lang; if (media_info.has_audio_info()) { lang = media_info.audio_info().language(); + } else if (media_info.has_text_info()) { + lang = media_info.text_info().language(); } AdaptationSet* adaptation_set = GetAdaptationSetForMediaInfo(media_info, content_type, lang); DCHECK(adaptation_set); + if (media_info.has_text_info()) { + // IOP requires all AdaptationSets to have (sub)segmentAlignment set to + // true, so carelessly set it to true. + // In practice it doesn't really make sense to adapt between text tracks. + adaptation_set->ForceSetSegmentAlignment(true); + } MediaInfo adjusted_media_info(media_info); MpdBuilder::MakePathsRelativeToMpd(output_path_, &adjusted_media_info); diff --git a/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc b/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc index 2e32338e10..dd3b2b2e59 100644 --- a/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc +++ b/packager/mpd/base/dash_iop_mpd_notifier_unittest.cc @@ -135,7 +135,9 @@ class DashIopMpdNotifierTest const MpdOptions empty_mpd_option_; const std::vector empty_base_urls_; - // Default AdaptationSet mock. + // Default mocks that can be used for the tests. + // IOW, if a test only requires one instance of + // Mock{AdaptationSet,Representation}, these can be used. scoped_ptr default_mock_adaptation_set_; scoped_ptr default_mock_representation_; @@ -181,6 +183,38 @@ TEST_P(DashIopMpdNotifierTest, NotifyNewContainer) { EXPECT_TRUE(notifier.Flush()); } +// Verify that if the MediaInfo contains text information, then +// MpdBuilder::ForceSetSegmentAlignment() is called. +TEST_P(DashIopMpdNotifierTest, NotifyNewTextContainer) { + const char kTextMediaInfo[] = + "text_info {\n" + " format: 'ttml'\n" + " language: 'en'\n" + "}\n" + "container_type: CONTAINER_TEXT\n"; + DashIopMpdNotifier notifier(dash_profile(), empty_mpd_option_, + empty_base_urls_, output_path_); + + scoped_ptr mock_mpd_builder(new MockMpdBuilder(mpd_type())); + + EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(StrEq("en"))) + .WillOnce(Return(default_mock_adaptation_set_.get())); + EXPECT_CALL(*default_mock_adaptation_set_, AddRole(_)).Times(0); + EXPECT_CALL(*default_mock_adaptation_set_, ForceSetSegmentAlignment(true)); + EXPECT_CALL(*default_mock_adaptation_set_, AddRepresentation(_)) + .WillOnce(Return(default_mock_representation_.get())); + + // This is for the Flush() below but adding expectation here because the next + // lines Pass() the pointer. + EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true)); + + uint32_t unused_container_id; + SetMpdBuilder(¬ifier, mock_mpd_builder.Pass()); + EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kTextMediaInfo), + &unused_container_id)); + EXPECT_TRUE(notifier.Flush()); +} + // Verify VOD NotifyNewContainer() operation works with different // MediaInfo::ProtectedContent. // Two AdaptationSets should be created. diff --git a/packager/mpd/base/media_info.proto b/packager/mpd/base/media_info.proto index 894633e9fc..be2aa8065f 100644 --- a/packager/mpd/base/media_info.proto +++ b/packager/mpd/base/media_info.proto @@ -16,6 +16,7 @@ message MediaInfo { CONTAINER_MP4 = 1; CONTAINER_MPEG2_TS= 2; CONTAINER_WEBM = 3; + CONTAINER_TEXT = 4; } message VideoInfo { @@ -51,8 +52,14 @@ message MediaInfo { } message TextInfo { + enum TextType { + UNKNOWN = 0; + CAPTION = 1; + SUBTITLE = 2; + } optional string format = 1; optional string language = 2; + optional TextType type = 3; } message ProtectedContent { diff --git a/packager/mpd/base/mock_mpd_builder.h b/packager/mpd/base/mock_mpd_builder.h index 23c9c7016a..50f123ab3f 100644 --- a/packager/mpd/base/mock_mpd_builder.h +++ b/packager/mpd/base/mock_mpd_builder.h @@ -38,6 +38,7 @@ class MockAdaptationSet : public AdaptationSet { MOCK_METHOD2(UpdateContentProtectionPssh, void(const std::string& drm_uuid, const std::string& pssh)); MOCK_METHOD1(AddRole, void(AdaptationSet::Role role)); + MOCK_METHOD1(ForceSetSegmentAlignment, void(bool segment_alignment)); MOCK_METHOD1(SetGroup, void(int group_number)); MOCK_CONST_METHOD0(Group, int()); diff --git a/packager/mpd/base/mpd_builder.cc b/packager/mpd/base/mpd_builder.cc index 23eff7777d..795bb709a1 100644 --- a/packager/mpd/base/mpd_builder.cc +++ b/packager/mpd/base/mpd_builder.cc @@ -39,6 +39,22 @@ namespace { const int kAdaptationSetGroupNotSet = -1; +AdaptationSet::Role MediaInfoTextTypeToRole(MediaInfo::TextInfo::TextType type) { + switch (type) { + case MediaInfo::TextInfo::UNKNOWN: + LOG(WARNING) << "Unknown text type, assuming subtitle."; + return AdaptationSet::kRoleSubtitle; + case MediaInfo::TextInfo::CAPTION: + return AdaptationSet::kRoleCaption; + case MediaInfo::TextInfo::SUBTITLE: + return AdaptationSet::kRoleSubtitle; + default: + NOTREACHED() << "Unknown MediaInfo TextType: " << type + << " assuming subtitle."; + return AdaptationSet::kRoleSubtitle; + } +} + std::string GetMimeType(const std::string& prefix, MediaInfo::ContainerType container_type) { switch (container_type) { @@ -54,7 +70,7 @@ std::string GetMimeType(const std::string& prefix, } // Unsupported container types should be rejected/handled by the caller. - NOTREACHED() << "Unrecognized container type: " << container_type; + LOG(ERROR) << "Unrecognized container type: " << container_type; return std::string(); } @@ -684,6 +700,13 @@ Representation* AdaptationSet::AddRepresentation(const MediaInfo& media_info) { content_type_ = "video"; } else if (media_info.has_audio_info()) { content_type_ = "audio"; + } else if (media_info.has_text_info()) { + content_type_ = "text"; + + if (media_info.text_info().has_type() && + (media_info.text_info().type() != MediaInfo::TextInfo::UNKNOWN)) { + roles_.insert(MediaInfoTextTypeToRole(media_info.text_info().type())); + } } representations_.push_back(representation.get()); @@ -998,20 +1021,20 @@ Representation::Representation( Representation::~Representation() {} bool Representation::Init() { - codecs_ = GetCodecs(media_info_); - if (codecs_.empty()) { - LOG(ERROR) << "Missing codec info in MediaInfo."; - return false; - } - - const bool has_video_info = media_info_.has_video_info(); - const bool has_audio_info = media_info_.has_audio_info(); - - if (!has_video_info && !has_audio_info) { + if (!AtLeastOneTrue(media_info_.has_video_info(), + media_info_.has_audio_info(), + media_info_.has_text_info())) { // This is an error. Segment information can be in AdaptationSet, Period, or // MPD but the interface does not provide a way to set them. // See 5.3.9.1 ISO 23009-1:2012 for segment info. - LOG(ERROR) << "Representation needs video or audio."; + LOG(ERROR) << "Representation needs one of video, audio, or text."; + return false; + } + + if (MoreThanOneTrue(media_info_.has_video_info(), + media_info_.has_audio_info(), + media_info_.has_text_info())) { + LOG(ERROR) << "Only one of VideoInfo, AudioInfo, or TextInfo can be set."; return false; } @@ -1020,18 +1043,22 @@ bool Representation::Init() { return false; } - // For mimetypes, this checks the video and then audio. Usually when there is - // audio + video, we take video/. - if (has_video_info) { + if (media_info_.has_video_info()) { mime_type_ = GetVideoMimeType(); if (!HasRequiredVideoFields(media_info_.video_info())) { LOG(ERROR) << "Missing required fields to create a video Representation."; return false; } - } else if (has_audio_info) { + } else if (media_info_.has_audio_info()) { mime_type_ = GetAudioMimeType(); + } else if (media_info_.has_text_info()) { + mime_type_ = GetTextMimeType(); } + if (mime_type_.empty()) + return false; + + codecs_ = GetCodecs(media_info_); return true; } @@ -1110,7 +1137,8 @@ xml::ScopedXmlPtr::type Representation::GetXml() { // Mandatory fields for Representation. representation.SetId(id_); representation.SetIntegerAttribute("bandwidth", bandwidth); - representation.SetStringAttribute("codecs", codecs_); + if (!codecs_.empty()) + representation.SetStringAttribute("codecs", codecs_); representation.SetStringAttribute("mimeType", mime_type_); const bool has_video_info = media_info_.has_video_info(); @@ -1285,6 +1313,35 @@ std::string Representation::GetAudioMimeType() const { return GetMimeType("audio", media_info_.container_type()); } +std::string Representation::GetTextMimeType() const { + CHECK(media_info_.has_text_info()); + if (media_info_.text_info().format() == "ttml") { + switch (media_info_.container_type()) { + case MediaInfo::CONTAINER_TEXT: + return "application/ttml+xml"; + case MediaInfo::CONTAINER_MP4: + return "application/mp4"; + default: + LOG(ERROR) << "Failed to determine MIME type for TTML container: " + << media_info_.container_type(); + return ""; + } + } + if (media_info_.text_info().format() == "vtt") { + if (media_info_.container_type() == MediaInfo::CONTAINER_TEXT) { + return "text/vtt"; + } + LOG(ERROR) << "Failed to determine MIME type for VTT container: " + << media_info_.container_type(); + return ""; + } + + LOG(ERROR) << "Cannot determine MIME type for format: " + << media_info_.text_info().format() + << " container: " << media_info_.container_type(); + return ""; +} + bool Representation::GetEarliestTimestamp(double* timestamp_seconds) { DCHECK(timestamp_seconds); diff --git a/packager/mpd/base/mpd_builder.h b/packager/mpd/base/mpd_builder.h index 30a5c15f4e..f96c718249 100644 --- a/packager/mpd/base/mpd_builder.h +++ b/packager/mpd/base/mpd_builder.h @@ -172,7 +172,8 @@ class AdaptationSet { /// Create a Representation instance using @a media_info. /// @param media_info is a MediaInfo object used to initialize the returned - /// Representation instance. + /// Representation instance. It may contain only one of VideoInfo, + /// AudioInfo, or TextInfo, i.e. VideoInfo XOR AudioInfo XOR TextInfo. /// @return On success, returns a pointer to Representation. Otherwise returns /// NULL. The returned pointer is owned by the AdaptationSet instance. virtual Representation* AddRepresentation(const MediaInfo& media_info); @@ -220,7 +221,7 @@ class AdaptationSet { /// for the AdaptationSet. /// @param segment_alignment is the value used for (sub)segmentAlignment /// attribute. - void ForceSetSegmentAlignment(bool segment_alignment); + virtual void ForceSetSegmentAlignment(bool segment_alignment); /// Sets the AdaptationSet@group attribute. /// Passing a negative value to this method will unset the attribute. @@ -517,11 +518,14 @@ class Representation { // strings. std::string GetVideoMimeType() const; std::string GetAudioMimeType() const; + std::string GetTextMimeType() const; // Gets the earliest, normalized segment timestamp. Returns true if // successful, false otherwise. bool GetEarliestTimestamp(double* timestamp_seconds); + // Init() checks that only one of VideoInfo, AudioInfo, or TextInfo is set. So + // any logic using this can assume only one set. MediaInfo media_info_; std::list content_protection_elements_; std::list segment_infos_; diff --git a/packager/mpd/base/mpd_builder_unittest.cc b/packager/mpd/base/mpd_builder_unittest.cc index 64b88e14b5..b4f0d7a740 100644 --- a/packager/mpd/base/mpd_builder_unittest.cc +++ b/packager/mpd/base/mpd_builder_unittest.cc @@ -414,6 +414,42 @@ TEST_F(CommonMpdBuilderTest, ValidMediaInfo) { EXPECT_TRUE(representation->Init()); } +// Verify that if VideoInfo, AudioInfo, or TextInfo is not set, Init() fails. +TEST_F(CommonMpdBuilderTest, VideoAudioTextInfoNotSet) { + const char kTestMediaInfo[] = "container_type: 1"; + + auto representation = + CreateRepresentation(ConvertToMediaInfo(kTestMediaInfo), MpdOptions(), + kAnyRepresentationId, NoListener()); + EXPECT_FALSE(representation->Init()); +} + +// Verify that if more than one of VideoInfo, AudioInfo, or TextInfo is set, +// then Init() fails. +TEST_F(CommonMpdBuilderTest, VideoAndAudioInfoSet) { + const char kTestMediaInfo[] = + "video_info {\n" + " codec: 'avc1'\n" + " height: 480\n" + " time_scale: 10\n" + " frame_duration: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + "}\n" + "container_type: CONTAINER_MP4\n"; + + auto representation = + CreateRepresentation(ConvertToMediaInfo(kTestMediaInfo), MpdOptions(), + kAnyRepresentationId, NoListener()); + EXPECT_FALSE(representation->Init()); +} + // Verify that Representation::Init() fails if a required field is missing. TEST_F(CommonMpdBuilderTest, InvalidMediaInfo) { // Missing width. @@ -538,16 +574,15 @@ TEST_F(CommonMpdBuilderTest, CheckAdaptationSetVideoContentType) { " pixel_width: 1\n" " pixel_height: 1\n" "}\n" - "container_type: 1\n"; + "container_type: CONTAINER_MP4\n"; auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(), MpdBuilder::kStatic, &sequence_counter); adaptation_set->AddRepresentation(ConvertToMediaInfo(kVideoMediaInfo)); - xml::ScopedXmlPtr::type node_xml(adaptation_set->GetXml()); - EXPECT_NO_FATAL_FAILURE( - ExpectAttributeEqString("contentType", "video", node_xml.get())); + EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString( + "contentType", "video", adaptation_set->GetXml().get())); } // Verify that content type is set correctly if audio info is present in @@ -561,40 +596,100 @@ TEST_F(CommonMpdBuilderTest, CheckAdaptationSetAudioContentType) { " time_scale: 1200\n" " num_channels: 2\n" "}\n" - "container_type: 1\n"; + "container_type: CONTAINER_MP4\n"; auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(), MpdBuilder::kStatic, &sequence_counter); adaptation_set->AddRepresentation(ConvertToMediaInfo(kAudioMediaInfo)); - xml::ScopedXmlPtr::type node_xml(adaptation_set->GetXml()); - EXPECT_NO_FATAL_FAILURE( - ExpectAttributeEqString("contentType", "audio", node_xml.get())); + EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString( + "contentType", "audio", adaptation_set->GetXml().get())); } // Verify that content type is set correctly if text info is present in // MediaInfo. -// TODO(rkuroiwa): Enable this once text support is implemented. -// This fails because it fails to get the codec, therefore Representation -// creation fails. -TEST_F(CommonMpdBuilderTest, DISABLED_CheckAdaptationSetTextContentType) { +TEST_F(CommonMpdBuilderTest, CheckAdaptationSetTextContentType) { base::AtomicSequenceNumber sequence_counter; const char kTextMediaInfo[] = "text_info {\n" " format: 'ttml'\n" " language: 'en'\n" "}\n" - "container_type: 1\n"; + "container_type: CONTAINER_TEXT\n"; auto adaptation_set = CreateAdaptationSet(kAnyAdaptationSetId, "", MpdOptions(), MpdBuilder::kStatic, &sequence_counter); adaptation_set->AddRepresentation(ConvertToMediaInfo(kTextMediaInfo)); + EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString( + "contentType", "text", adaptation_set->GetXml().get())); +} + +TEST_F(CommonMpdBuilderTest, TtmlXmlMimeType) { + const char kTtmlXmlMediaInfo[] = + "text_info {\n" + " format: 'ttml'\n" + "}\n" + "container_type: CONTAINER_TEXT\n"; + + auto representation = + CreateRepresentation(ConvertToMediaInfo(kTtmlXmlMediaInfo), MpdOptions(), + kAnyRepresentationId, NoListener()); + EXPECT_TRUE(representation->Init()); + EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString( + "mimeType", "application/ttml+xml", representation->GetXml().get())); +} + +TEST_F(CommonMpdBuilderTest, TtmlMp4MimeType) { + const char kTtmlMp4MediaInfo[] = + "text_info {\n" + " format: 'ttml'\n" + "}\n" + "container_type: CONTAINER_MP4\n"; + + auto representation = + CreateRepresentation(ConvertToMediaInfo(kTtmlMp4MediaInfo), MpdOptions(), + kAnyRepresentationId, NoListener()).Pass(); + EXPECT_TRUE(representation->Init()); + EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString( + "mimeType", "application/mp4", representation->GetXml().get())); +} + +TEST_F(CommonMpdBuilderTest, WebVttMimeType) { + const char kWebVttMediaInfo[] = + "text_info {\n" + " format: 'vtt'\n" + "}\n" + "container_type: CONTAINER_TEXT\n"; + + auto representation = + CreateRepresentation(ConvertToMediaInfo(kWebVttMediaInfo), MpdOptions(), + kAnyRepresentationId, NoListener()).Pass(); + EXPECT_TRUE(representation->Init()); + EXPECT_NO_FATAL_FAILURE(ExpectAttributeEqString( + "mimeType", "text/vtt", representation->GetXml().get())); +} + +// Verify that language passed to the constructor sets the @lang field is set. +TEST_F(CommonMpdBuilderTest, CheckLanguageAttributeSet) { + base::AtomicSequenceNumber sequence_counter; + // The media info doesn't really matter as long as it is valid. + const char kTextMediaInfo[] = + "text_info {\n" + " format: 'ttml'\n" + "}\n" + "container_type: CONTAINER_TEXT\n"; + + auto adaptation_set = + CreateAdaptationSet(kAnyAdaptationSetId, "en", MpdOptions(), + MpdBuilder::kStatic, &sequence_counter); + adaptation_set->AddRepresentation(ConvertToMediaInfo(kTextMediaInfo)); + xml::ScopedXmlPtr::type node_xml(adaptation_set->GetXml()); EXPECT_NO_FATAL_FAILURE( - ExpectAttributeEqString("contentType", "text", node_xml.get())); + ExpectAttributeEqString("lang", "en", node_xml.get())); } TEST_F(CommonMpdBuilderTest, CheckAdaptationSetId) { @@ -1621,6 +1716,54 @@ TEST_F(StaticMpdBuilderTest, WriteToFile) { EXPECT_TRUE(DeleteFile(file_path, kNonRecursive)); } +// Verify that a text path works. +TEST_F(StaticMpdBuilderTest, Text) { + const char kTextMediaInfo[] = + "text_info {\n" + " format: 'ttml'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}\n" + "media_duration_seconds: 35\n" + "bandwidth: 1000\n" + "media_file_name: 'subtitle.xml'\n" + "container_type: CONTAINER_TEXT\n"; + + const char kExpectedOutput[] = + "\n" + "" + " " + " " + " \n" + " " + " subtitle.xml" + " " + " " + " " + ""; + + AdaptationSet* text_adaptation_set = mpd_.AddAdaptationSet("en"); + ASSERT_TRUE(text_adaptation_set); + + Representation* text_representation = text_adaptation_set->AddRepresentation( + ConvertToMediaInfo(kTextMediaInfo)); + ASSERT_TRUE(text_representation); + + std::string mpd_output; + ASSERT_TRUE(mpd_.ToString(&mpd_output)); + ASSERT_TRUE(ValidateMpdSchema(mpd_output)); + EXPECT_TRUE(XmlEqual(kExpectedOutput, mpd_output)); +} + // Check whether the attributes are set correctly for dynamic element. // This test must use ASSERT_EQ for comparison because XmlEqual() cannot // handle namespaces correctly yet. diff --git a/packager/mpd/base/mpd_utils.cc b/packager/mpd/base/mpd_utils.cc index 1c70fedc63..ce1e6e14a2 100644 --- a/packager/mpd/base/mpd_utils.cc +++ b/packager/mpd/base/mpd_utils.cc @@ -13,6 +13,24 @@ #include "packager/mpd/base/xml/scoped_xml_ptr.h" namespace edash_packager { +namespace { + +std::string TextCodecString( + const edash_packager::MediaInfo& media_info) { + CHECK(media_info.has_text_info()); + const std::string& format = media_info.text_info().format(); + // DASH IOP mentions that the codec for ttml in mp4 is stpp. + if (format == "ttml" && + (media_info.container_type() == MediaInfo::CONTAINER_MP4)) { + return "stpp"; + } + + // Otherwise codec doesn't need to be specified, e.g. vtt and ttml+xml are + // obvious from the mime type. + return ""; +} + +} // namespace bool HasVODOnlyFields(const MediaInfo& media_info) { return media_info.has_init_range() || media_info.has_index_range() || @@ -40,22 +58,19 @@ void RemoveDuplicateAttributes( } std::string GetCodecs(const MediaInfo& media_info) { - std::string video_codec; + CHECK(OnlyOneTrue(media_info.has_video_info(), media_info.has_audio_info(), + media_info.has_text_info())); + if (media_info.has_video_info()) - video_codec = media_info.video_info().codec(); + return media_info.video_info().codec(); - std::string audio_codec; if (media_info.has_audio_info()) - audio_codec = media_info.audio_info().codec(); + return media_info.audio_info().codec(); - if (!video_codec.empty() && !audio_codec.empty()) { - return video_codec + "," + audio_codec; - } else if (!video_codec.empty()) { - return video_codec; - } else if (!audio_codec.empty()) { - return audio_codec; - } + if (media_info.has_text_info()) + return TextCodecString(media_info); + NOTREACHED(); return ""; } diff --git a/packager/mpd/base/simple_mpd_notifier.cc b/packager/mpd/base/simple_mpd_notifier.cc index 42a4ec8863..55202b6158 100644 --- a/packager/mpd/base/simple_mpd_notifier.cc +++ b/packager/mpd/base/simple_mpd_notifier.cc @@ -49,6 +49,8 @@ bool SimpleMpdNotifier::NotifyNewContainer(const MediaInfo& media_info, std::string lang; if (media_info.has_audio_info()) { lang = media_info.audio_info().language(); + } else if (media_info.has_text_info()) { + lang = media_info.text_info().language(); } AdaptationSet** adaptation_set = &adaptation_set_map_[content_type][lang]; if (*adaptation_set == NULL)