From b6718a37c5345b6efede0edc1ae8ae4ef75cfa24 Mon Sep 17 00:00:00 2001 From: Joey Parrish Date: Tue, 8 Mar 2016 11:15:12 -0800 Subject: [PATCH] Accept language subtags When normalizing language tags to their shortest form, we need to split off subtags and treat them as separate components that do not get changed. Furthermore, MP4 can only store language tags without subtags. So only store the main language in MP4 output. Fixes b/27533973 Change-Id: I96049e274aae9841e321c53039ef6464a2e61574 --- packager/media/formats/mp4/mp4_muxer.cc | 15 ++-- packager/media/test/packager_test.cc | 84 +++++++++++++++++++---- packager/mpd/base/language_utils.cc | 42 +++++++++--- packager/mpd/base/mpd_builder_unittest.cc | 22 ++++++ 4 files changed, 133 insertions(+), 30 deletions(-) diff --git a/packager/media/formats/mp4/mp4_muxer.cc b/packager/media/formats/mp4/mp4_muxer.cc index a6a6fcb976..cc744becc9 100644 --- a/packager/media/formats/mp4/mp4_muxer.cc +++ b/packager/media/formats/mp4/mp4_muxer.cc @@ -191,12 +191,19 @@ void MP4Muxer::InitializeTrak(const StreamInfo* info, Track* trak) { trak->media.header.timescale = info->time_scale(); trak->media.header.duration = 0; if (!info->language().empty()) { - // ISO-639-2/T language code should be 3 characters.. - if (info->language().size() != 3) { - LOG(WARNING) << "'" << info->language() << "' is not a valid ISO-639-2 " + // Strip off the subtag, if any. + std::string main_language = info->language(); + size_t dash = main_language.find('-'); + if (dash != std::string::npos) { + main_language.erase(dash); + } + + // ISO-639-2/T main language code should be 3 characters. + if (main_language.size() != 3) { + LOG(WARNING) << "'" << main_language << "' is not a valid ISO-639-2 " << "language code, ignoring."; } else { - trak->media.header.language.code = info->language(); + trak->media.header.language.code = main_language; } } } diff --git a/packager/media/test/packager_test.cc b/packager/media/test/packager_test.cc index 3f92521f91..28989e9866 100644 --- a/packager/media/test/packager_test.cc +++ b/packager/media/test/packager_test.cc @@ -48,6 +48,7 @@ const bool kSingleSegment = true; const bool kMultipleSegments = false; const bool kEnableEncryption = true; const bool kDisableEncryption = false; +const char kNoLanguageOverride[] = ""; // Encryption constants. const char kKeyIdHex[] = "e5007e6e9dcd5ac095202ed3758382cd"; @@ -107,7 +108,8 @@ class PackagerTestBasic : public ::testing::TestWithParam { const std::string& video_output, const std::string& audio_output, bool single_segment, - bool enable_encryption); + bool enable_encryption, + const std::string& override_language); void Decrypt(const std::string& input, const std::string& video_output, @@ -149,7 +151,8 @@ void PackagerTestBasic::Remux(const std::string& input, const std::string& video_output, const std::string& audio_output, bool single_segment, - bool enable_encryption) { + bool enable_encryption, + const std::string& language_override) { CHECK(!video_output.empty() || !audio_output.empty()); Demuxer demuxer(GetFullPath(input)); @@ -166,7 +169,12 @@ void PackagerTestBasic::Remux(const std::string& input, new mp4::MP4Muxer(SetupOptions(video_output, single_segment))); muxer_video->set_clock(&fake_clock_); - muxer_video->AddStream(FindFirstVideoStream(demuxer.streams())); + MediaStream* stream = FindFirstVideoStream(demuxer.streams()); + if (!language_override.empty()) { + stream->info()->set_language(language_override); + ASSERT_EQ(language_override, stream->info()->language()); + } + muxer_video->AddStream(stream); if (enable_encryption) { muxer_video->SetKeySource(encryption_key_source.get(), @@ -182,7 +190,12 @@ void PackagerTestBasic::Remux(const std::string& input, new mp4::MP4Muxer(SetupOptions(audio_output, single_segment))); muxer_audio->set_clock(&fake_clock_); - muxer_audio->AddStream(FindFirstAudioStream(demuxer.streams())); + MediaStream* stream = FindFirstAudioStream(demuxer.streams()); + if (!language_override.empty()) { + stream->info()->set_language(language_override); + ASSERT_EQ(language_override, stream->info()->language()); + } + muxer_audio->AddStream(stream); if (enable_encryption) { muxer_audio->SetKeySource(encryption_key_source.get(), @@ -234,7 +247,8 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedVideo) { kOutputVideo, kOutputNone, kSingleSegment, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); } TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedAudio) { @@ -242,7 +256,8 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentUnencryptedAudio) { kOutputNone, kOutputAudio, kSingleSegment, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); } TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedVideo) { @@ -250,7 +265,8 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedVideo) { kOutputVideo, kOutputNone, kSingleSegment, - kEnableEncryption)); + kEnableEncryption, + kNoLanguageOverride)); ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputVideo, kOutputVideo2, @@ -262,13 +278,44 @@ TEST_P(PackagerTestBasic, MP4MuxerSingleSegmentEncryptedAudio) { kOutputNone, kOutputAudio, kSingleSegment, - kEnableEncryption)); + kEnableEncryption, + kNoLanguageOverride)); ASSERT_NO_FATAL_FAILURE(Decrypt(kOutputAudio, kOutputNone, kOutputAudio2)); } +TEST_P(PackagerTestBasic, MP4MuxerLanguageWithoutSubtag) { + ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), + kOutputNone, + kOutputAudio, + kSingleSegment, + kDisableEncryption, + "por")); + + Demuxer demuxer(GetFullPath(kOutputAudio)); + ASSERT_OK(demuxer.Initialize()); + + MediaStream* stream = FindFirstAudioStream(demuxer.streams()); + ASSERT_EQ("por", stream->info()->language()); +} + +TEST_P(PackagerTestBasic, MP4MuxerLanguageWithSubtag) { + ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), + kOutputNone, + kOutputAudio, + kSingleSegment, + kDisableEncryption, + "por-BR")); + + Demuxer demuxer(GetFullPath(kOutputAudio)); + ASSERT_OK(demuxer.Initialize()); + + MediaStream* stream = FindFirstAudioStream(demuxer.streams()); + ASSERT_EQ("por", stream->info()->language()); +} + class PackagerTest : public PackagerTestBasic { public: void SetUp() override { @@ -278,13 +325,15 @@ class PackagerTest : public PackagerTestBasic { kOutputVideo, kOutputNone, kSingleSegment, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); ASSERT_NO_FATAL_FAILURE(Remux(GetParam(), kOutputNone, kOutputAudio, kSingleSegment, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); } }; @@ -295,7 +344,8 @@ TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedVideoAgain) { kOutputVideo2, kOutputNone, kSingleSegment, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); } @@ -306,7 +356,8 @@ TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedAudioAgain) { kOutputNone, kOutputAudio2, kSingleSegment, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); EXPECT_TRUE(ContentsEqual(kOutputAudio, kOutputAudio2)); } @@ -315,7 +366,8 @@ TEST_P(PackagerTest, MP4MuxerSingleSegmentUnencryptedSeparateAudioVideo) { kOutputVideo2, kOutputAudio2, kSingleSegment, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); // Compare the output with single muxer output. They should match. EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); @@ -327,7 +379,8 @@ TEST_P(PackagerTest, MP4MuxerMultiSegmentsUnencryptedVideo) { kOutputVideo2, kOutputNone, kMultipleSegments, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); // Find and concatenates the segments. const std::string kOutputVideoSegmentsCombined = @@ -361,7 +414,8 @@ TEST_P(PackagerTest, MP4MuxerMultiSegmentsUnencryptedVideo) { kOutputVideo2, kOutputNone, kSingleSegment, - kDisableEncryption)); + kDisableEncryption, + kNoLanguageOverride)); EXPECT_TRUE(ContentsEqual(kOutputVideo, kOutputVideo2)); } diff --git a/packager/mpd/base/language_utils.cc b/packager/mpd/base/language_utils.cc index 8c3fefd10b..eaeef91804 100644 --- a/packager/mpd/base/language_utils.cc +++ b/packager/mpd/base/language_utils.cc @@ -70,42 +70,62 @@ const LanguageMapPairType kLanguageMap[] = { { "yor", "yo" }, { "zha", "za" }, { "zho", "zh" }, { "zul", "zu" }, }; +void SplitLanguageTag(const std::string& tag, + std::string* main_language, std::string* subtag) { + // Split the main language from its subtag (if any). + *main_language = tag; + subtag->clear(); + size_t dash = main_language->find('-'); + if (dash != std::string::npos) { + *subtag = main_language->substr(dash); + main_language->erase(dash); + } +} + } // namespace namespace edash_packager { std::string LanguageToShortestForm(const std::string& language) { - if (language.size() == 2) { + std::string main_language; + std::string subtag; + SplitLanguageTag(language, &main_language, &subtag); + + if (main_language.size() == 2) { // Presumably already a valid ISO-639-1 code, and therefore conforms to // BCP-47's requirement to use the shortest possible code. - return language; + return main_language + subtag; } for (size_t i = 0; i < arraysize(kLanguageMap); ++i) { - if (language == kLanguageMap[i].iso_639_2) { - return kLanguageMap[i].iso_639_1; + if (main_language == kLanguageMap[i].iso_639_2) { + return kLanguageMap[i].iso_639_1 + subtag; } } // This could happen legitimately for languages which have no 2-letter code, // but that would imply that the input language code is a 3-letter code. - DCHECK_EQ(3u, language.size()); - return language; + DCHECK_EQ(3u, main_language.size()); + return main_language + subtag; } std::string LanguageToISO_639_2(const std::string& language) { - if (language.size() == 3) { + std::string main_language; + std::string subtag; + SplitLanguageTag(language, &main_language, &subtag); + + if (main_language.size() == 3) { // Presumably already a valid ISO-639-2 code. - return language; + return main_language + subtag; } for (size_t i = 0; i < arraysize(kLanguageMap); ++i) { - if (language == kLanguageMap[i].iso_639_1) { - return kLanguageMap[i].iso_639_2; + if (main_language == kLanguageMap[i].iso_639_1) { + return kLanguageMap[i].iso_639_2 + subtag; } } - LOG(WARNING) << "No equivalent 3-letter language code for " << language; + LOG(WARNING) << "No equivalent 3-letter language code for " << main_language; // This is probably a mistake on the part of the user and should be treated // as invalid input. return "und"; diff --git a/packager/mpd/base/mpd_builder_unittest.cc b/packager/mpd/base/mpd_builder_unittest.cc index 39326fcc3c..1fc694594d 100644 --- a/packager/mpd/base/mpd_builder_unittest.cc +++ b/packager/mpd/base/mpd_builder_unittest.cc @@ -788,6 +788,28 @@ TEST_F(CommonMpdBuilderTest, CheckLanguageAttributeSet) { ExpectAttributeEqString("lang", "en", node_xml.get())); } +// Verify that language tags with subtags can still be converted. +TEST_F(CommonMpdBuilderTest, CheckConvertLanguageWithSubtag) { + 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"; + + // "por-BR" is the long tag for Brazillian Portuguese. The short tag + // is "pt-BR", which is what should appear in the manifest. + auto adaptation_set = + CreateAdaptationSet(kAnyAdaptationSetId, "por-BR", MpdOptions(), + MpdBuilder::kStatic, &sequence_counter); + adaptation_set->AddRepresentation(ConvertToMediaInfo(kTextMediaInfo)); + + xml::scoped_xml_ptr node_xml(adaptation_set->GetXml()); + EXPECT_NO_FATAL_FAILURE( + ExpectAttributeEqString("lang", "pt-BR", node_xml.get())); +} + TEST_F(CommonMpdBuilderTest, CheckAdaptationSetId) { base::AtomicSequenceNumber sequence_counter; const uint32_t kAdaptationSetId = 42;