diff --git a/docs/source/options/dash_options.rst b/docs/source/options/dash_options.rst index b7e94d3bd7..3402dea526 100644 --- a/docs/source/options/dash_options.rst +++ b/docs/source/options/dash_options.rst @@ -64,6 +64,14 @@ DASH options in the manifest. This allows the player to choose the correct default language for the content. + This applies to both audio and text tracks. The default language for text + tracks can be overriden by 'default_text_language'. + +--default_text_language + + Same as above, but this applies to text tracks only, and overrides the + default language for text tracks. + --allow_approximate_segment_timeline For live profile only. diff --git a/docs/source/options/hls_options.rst b/docs/source/options/hls_options.rst index 6a82b8d355..476f354627 100644 --- a/docs/source/options/hls_options.rst +++ b/docs/source/options/hls_options.rst @@ -43,3 +43,11 @@ HLS options The first audio/text rendition in a group tagged with this language will have 'DEFAULT' attribute set to 'YES'. This allows the player to choose the correct default language for the content. + + This applies to both audio and text tracks. The default language for text + tracks can be overriden by 'default_text_language'. + +--default_text_language + + Same as above, but this applies to text tracks only, and overrides the + default language for text tracks. diff --git a/packager/app/manifest_flags.cc b/packager/app/manifest_flags.cc index 4f7d8e582f..337ac93d99 100644 --- a/packager/app/manifest_flags.cc +++ b/packager/app/manifest_flags.cc @@ -25,4 +25,11 @@ DEFINE_string(default_language, "have in the manifest; For HLS, the " "first audio/text rendition in a group tagged with this language " "will have 'DEFAULT' attribute set to 'YES'. This allows the " - "player to choose the correct default language for the content."); + "player to choose the correct default language for the content." + "This applies to both audio and text tracks. The default " + "language for text tracks can be overriden by " + "'--default_text_language'."); +DEFINE_string(default_text_language, + "", + "Same as above, but this applies to text tracks only, and " + "overrides the default language for text tracks."); diff --git a/packager/app/manifest_flags.h b/packager/app/manifest_flags.h index 7acecf3da9..36a5c5df2e 100644 --- a/packager/app/manifest_flags.h +++ b/packager/app/manifest_flags.h @@ -14,5 +14,6 @@ DECLARE_double(time_shift_buffer_depth); DECLARE_uint64(preserved_segments_outside_live_window); DECLARE_string(default_language); +DECLARE_string(default_text_language); #endif // PACKAGER_APP_MANIFEST_FLAGS_H_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index 45f44aa758..ff853bf746 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -443,6 +443,7 @@ base::Optional GetPackagingParams() { } mpd_params.default_language = FLAGS_default_language; + mpd_params.default_text_language = FLAGS_default_text_language; mpd_params.generate_static_live_mpd = FLAGS_generate_static_mpd; mpd_params.generate_dash_if_iop_compliant_mpd = FLAGS_generate_dash_if_iop_compliant_mpd; @@ -460,6 +461,7 @@ base::Optional GetPackagingParams() { hls_params.preserved_segments_outside_live_window = FLAGS_preserved_segments_outside_live_window; hls_params.default_language = FLAGS_default_language; + hls_params.default_text_language = FLAGS_default_text_language; TestParams& test_params = packaging_params.test_params; test_params.dump_stream_info = FLAGS_dump_stream_info; diff --git a/packager/hls/base/master_playlist.cc b/packager/hls/base/master_playlist.cc index 1819d0ea18..a5f479cad1 100644 --- a/packager/hls/base/master_playlist.cc +++ b/packager/hls/base/master_playlist.cc @@ -343,7 +343,8 @@ void BuildMediaTags( } } -void AppendPlaylists(const std::string& default_language, +void AppendPlaylists(const std::string& default_audio_language, + const std::string& default_text_language, const std::string& base_url, const std::list& playlists, std::string* content) { @@ -374,12 +375,13 @@ void AppendPlaylists(const std::string& default_language, if (!audio_playlist_groups.empty()) { content->append("\n"); - BuildMediaTags(audio_playlist_groups, default_language, base_url, content); + BuildMediaTags(audio_playlist_groups, default_audio_language, base_url, + content); } if (!subtitle_playlist_groups.empty()) { content->append("\n"); - BuildMediaTags(subtitle_playlist_groups, default_language, base_url, + BuildMediaTags(subtitle_playlist_groups, default_text_language, base_url, content); } @@ -406,8 +408,12 @@ void AppendPlaylists(const std::string& default_language, } // namespace MasterPlaylist::MasterPlaylist(const std::string& file_name, - const std::string& default_language) - : file_name_(file_name), default_language_(default_language) {} + const std::string& default_audio_language, + const std::string& default_text_language) + : file_name_(file_name), + default_audio_language_(default_audio_language), + default_text_language_(default_text_language) {} + MasterPlaylist::~MasterPlaylist() {} bool MasterPlaylist::WriteMasterPlaylist( @@ -416,7 +422,8 @@ bool MasterPlaylist::WriteMasterPlaylist( const std::list& playlists) { std::string content = "#EXTM3U\n"; AppendVersionString(&content); - AppendPlaylists(default_language_, base_url, playlists, &content); + AppendPlaylists(default_audio_language_, default_text_language_, base_url, + playlists, &content); // Skip if the playlist is already written. if (content == written_playlist_) diff --git a/packager/hls/base/master_playlist.h b/packager/hls/base/master_playlist.h index 18590f73a1..a758806f03 100644 --- a/packager/hls/base/master_playlist.h +++ b/packager/hls/base/master_playlist.h @@ -20,10 +20,13 @@ class MediaPlaylist; class MasterPlaylist { public: /// @param file_name is the file name of the master playlist. - /// @param default_language determines the rendition that should be tagged - /// with 'DEFAULT'. + /// @param default_audio_language determines the audio rendition that should + /// be tagged with 'DEFAULT'. + /// @param default_text_language determines the text rendition that should be + /// tagged with 'DEFAULT'. MasterPlaylist(const std::string& file_name, - const std::string& default_language); + const std::string& default_audio_language, + const std::string& default_text_language); virtual ~MasterPlaylist(); /// Writes Master Playlist to output_dir + . @@ -45,7 +48,8 @@ class MasterPlaylist { std::string written_playlist_; const std::string file_name_; - const std::string default_language_; + const std::string default_audio_language_; + const std::string default_text_language_; }; } // namespace hls diff --git a/packager/hls/base/master_playlist_unittest.cc b/packager/hls/base/master_playlist_unittest.cc index 0e1bb7daff..2d3e043e12 100644 --- a/packager/hls/base/master_playlist_unittest.cc +++ b/packager/hls/base/master_playlist_unittest.cc @@ -28,7 +28,8 @@ using ::testing::StrEq; namespace { const char kDefaultMasterPlaylistName[] = "playlist.m3u8"; -const char kDefaultLanguage[] = "en"; +const char kDefaultAudioLanguage[] = "en"; +const char kDefaultTextLanguage[] = "fr"; const uint32_t kWidth = 800; const uint32_t kHeight = 600; @@ -123,7 +124,9 @@ std::unique_ptr CreateTextPlaylist( class MasterPlaylistTest : public ::testing::Test { protected: MasterPlaylistTest() - : master_playlist_(kDefaultMasterPlaylistName, kDefaultLanguage), + : master_playlist_(kDefaultMasterPlaylistName, + kDefaultAudioLanguage, + kDefaultTextLanguage), test_output_dir_("memory://test_dir"), master_playlist_path_( FilePath::FromUTF8Unsafe(test_output_dir_) @@ -382,10 +385,11 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideosAndTexts) { "test\n" "\n" "#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/eng.m3u8\"," - "GROUP-ID=\"textgroup\",LANGUAGE=\"en\",NAME=\"english\",DEFAULT=YES," + "GROUP-ID=\"textgroup\",LANGUAGE=\"en\",NAME=\"english\"," "AUTOSELECT=YES\n" "#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/fr.m3u8\"," - "GROUP-ID=\"textgroup\",LANGUAGE=\"fr\",NAME=\"french\",AUTOSELECT=YES\n" + "GROUP-ID=\"textgroup\",LANGUAGE=\"fr\",NAME=\"french\",DEFAULT=YES," + "AUTOSELECT=YES\n" "\n" "#EXT-X-STREAM-INF:BANDWIDTH=300000,AVERAGE-BANDWIDTH=200000," "CODECS=\"sdvideocodec,textcodec\",RESOLUTION=800x600," @@ -423,8 +427,8 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndTextWithCharacteritics) { "test\n" "\n" "#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/eng.m3u8\"," - "GROUP-ID=\"textgroup\",LANGUAGE=\"en\",NAME=\"english\",DEFAULT=YES," - "AUTOSELECT=YES,CHARACTERISTICS=\"" + "GROUP-ID=\"textgroup\",LANGUAGE=\"en\",NAME=\"english\",AUTOSELECT=YES," + "CHARACTERISTICS=\"" "public.accessibility.transcribes-spoken-dialog,public.easy-to-read\"\n" "\n" "#EXT-X-STREAM-INF:BANDWIDTH=300000,AVERAGE-BANDWIDTH=200000," @@ -463,10 +467,10 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndTextGroups) { "\n" "#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/eng.m3u8\"," "GROUP-ID=\"en-text-group\",LANGUAGE=\"en\",NAME=\"english\"," - "DEFAULT=YES,AUTOSELECT=YES\n" + "AUTOSELECT=YES\n" "#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/fr.m3u8\"," "GROUP-ID=\"fr-text-group\",LANGUAGE=\"fr\",NAME=\"french\"," - "AUTOSELECT=YES\n" + "DEFAULT=YES,AUTOSELECT=YES\n" "\n" "#EXT-X-STREAM-INF:BANDWIDTH=300000,AVERAGE-BANDWIDTH=200000," "CODECS=\"sdvideocodec,textcodec\",RESOLUTION=800x600," @@ -511,7 +515,7 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistVideoAndAudioAndText) { "DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\"\n" "\n" "#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/eng.m3u8\"," - "GROUP-ID=\"textgroup\",LANGUAGE=\"en\",NAME=\"english\",DEFAULT=YES," + "GROUP-ID=\"textgroup\",LANGUAGE=\"en\",NAME=\"english\"," "AUTOSELECT=YES\n" "\n" "#EXT-X-STREAM-INF:BANDWIDTH=350000,AVERAGE-BANDWIDTH=230000," @@ -537,14 +541,14 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistMixedPlaylistsDifferentGroups) { "audiocodec", "en", kAudioChannels, kAudioMaxBitrate, kAudioAvgBitrate), CreateAudioPlaylist("audio-2.m3u8", "audio 2", "audio-group-2", - "audiocodec", "en", kAudioChannels, kAudioMaxBitrate, + "audiocodec", "fr", kAudioChannels, kAudioMaxBitrate, kAudioAvgBitrate), // SUBTITLES CreateTextPlaylist("text-1.m3u8", "text 1", "text-group-1", "textcodec", "en"), CreateTextPlaylist("text-2.m3u8", "text 2", "text-group-2", "textcodec", - "en"), + "fr"), // VIDEO CreateVideoPlaylist("video-1.m3u8", "sdvideocodec", kVideoMaxBitrate, @@ -581,14 +585,14 @@ TEST_F(MasterPlaylistTest, WriteMasterPlaylistMixedPlaylistsDifferentGroups) { "GROUP-ID=\"audio-group-1\",LANGUAGE=\"en\",NAME=\"audio 1\"," "DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\"\n" "#EXT-X-MEDIA:TYPE=AUDIO,URI=\"http://playlists.org/audio-2.m3u8\"," - "GROUP-ID=\"audio-group-2\",LANGUAGE=\"en\",NAME=\"audio 2\"," - "DEFAULT=YES,AUTOSELECT=YES,CHANNELS=\"2\"\n" + "GROUP-ID=\"audio-group-2\",LANGUAGE=\"fr\",NAME=\"audio 2\"," + "AUTOSELECT=YES,CHANNELS=\"2\"\n" "\n" "#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/text-1.m3u8\"," "GROUP-ID=\"text-group-1\",LANGUAGE=\"en\",NAME=\"text 1\"," - "DEFAULT=YES,AUTOSELECT=YES\n" + "AUTOSELECT=YES\n" "#EXT-X-MEDIA:TYPE=SUBTITLES,URI=\"http://playlists.org/text-2.m3u8\"," - "GROUP-ID=\"text-group-2\",LANGUAGE=\"en\",NAME=\"text 2\"," + "GROUP-ID=\"text-group-2\",LANGUAGE=\"fr\",NAME=\"text 2\"," "DEFAULT=YES,AUTOSELECT=YES\n" "\n" "#EXT-X-STREAM-INF:BANDWIDTH=350000,AVERAGE-BANDWIDTH=130000," diff --git a/packager/hls/base/simple_hls_notifier.cc b/packager/hls/base/simple_hls_notifier.cc index 11dde8e31e..6b8c11caa9 100644 --- a/packager/hls/base/simple_hls_notifier.cc +++ b/packager/hls/base/simple_hls_notifier.cc @@ -282,9 +282,14 @@ SimpleHlsNotifier::SimpleHlsNotifier(const HlsParams& hls_params) const base::FilePath master_playlist_path( base::FilePath::FromUTF8Unsafe(hls_params.master_playlist_output)); output_dir_ = master_playlist_path.DirName().AsUTF8Unsafe(); + const std::string& default_audio_langauge = hls_params.default_language; + const std::string& default_text_language = + hls_params.default_text_language.empty() + ? hls_params.default_language + : hls_params.default_text_language; master_playlist_.reset( new MasterPlaylist(master_playlist_path.BaseName().AsUTF8Unsafe(), - hls_params.default_language)); + default_audio_langauge, default_text_language)); } SimpleHlsNotifier::~SimpleHlsNotifier() {} diff --git a/packager/hls/base/simple_hls_notifier_unittest.cc b/packager/hls/base/simple_hls_notifier_unittest.cc index 17f6943c88..6443deb9f1 100644 --- a/packager/hls/base/simple_hls_notifier_unittest.cc +++ b/packager/hls/base/simple_hls_notifier_unittest.cc @@ -35,7 +35,8 @@ using ::testing::WithParamInterface; namespace { const char kMasterPlaylistName[] = "master.m3u8"; -const char kDefaultLanguage[] = "en"; +const char kDefaultAudioLanguage[] = "en"; +const char kDefaultTextLanguage[] = "fr"; const char kEmptyKeyUri[] = ""; const char kFairPlayKeyUri[] = "skd://www.license.com/getkey?key_id=testing"; const char kIdentityKeyUri[] = "https://www.license.com/getkey?key_id=testing"; @@ -45,7 +46,9 @@ const HlsPlaylistType kLivePlaylist = HlsPlaylistType::kLive; class MockMasterPlaylist : public MasterPlaylist { public: MockMasterPlaylist() - : MasterPlaylist(kMasterPlaylistName, kDefaultLanguage) {} + : MasterPlaylist(kMasterPlaylistName, + kDefaultAudioLanguage, + kDefaultTextLanguage) {} MOCK_METHOD3(WriteMasterPlaylist, bool(const std::string& prefix, diff --git a/packager/hls/public/hls_params.h b/packager/hls/public/hls_params.h index 22c7d882c1..f35618db4b 100644 --- a/packager/hls/public/hls_params.h +++ b/packager/hls/public/hls_params.h @@ -45,7 +45,12 @@ struct HlsParams { /// The renditions tagged with this language will have 'DEFAULT' set to 'YES' /// in 'EXT-X-MEDIA' tag. This allows the player to choose the correct default /// language for the content. + /// This applies to both audio and text tracks. The default language for text + /// tracks can be overriden by 'default_text_language'. std::string default_language; + /// Same as above, but this overrides the default language for text tracks, + /// i.e. subtitles or close-captions. + std::string default_text_language; /// This is the target segment duration requested by the user. The actual /// segment duration may be different to the target segment duration. /// This parameter is included here to for bandwidth estimator to exclude the diff --git a/packager/mpd/base/period.cc b/packager/mpd/base/period.cc index fa41da2d67..06c85ed9d0 100644 --- a/packager/mpd/base/period.cc +++ b/packager/mpd/base/period.cc @@ -32,6 +32,16 @@ std::set GetUUIDs( return uuids; } +const std::string& GetDefaultAudioLanguage(const MpdOptions& mpd_options) { + return mpd_options.mpd_params.default_language; +} + +const std::string& GetDefaultTextLanguage(const MpdOptions& mpd_options) { + return mpd_options.mpd_params.default_text_language.empty() + ? mpd_options.mpd_params.default_language + : mpd_options.mpd_params.default_text_language; +} + } // namespace Period::Period(uint32_t period_id, @@ -149,8 +159,14 @@ bool Period::SetNewAdaptationSetAttributes( const MediaInfo& media_info, const std::list& adaptation_sets, AdaptationSet* new_adaptation_set) { - if (!language.empty() && language == mpd_options_.mpd_params.default_language) - new_adaptation_set->AddRole(AdaptationSet::kRoleMain); + if (!language.empty()) { + const bool is_main_role = + language == (media_info.has_audio_info() + ? GetDefaultAudioLanguage(mpd_options_) + : GetDefaultTextLanguage(mpd_options_)); + if (is_main_role) + new_adaptation_set->AddRole(AdaptationSet::kRoleMain); + } if (media_info.has_video_info()) { // Because 'language' is ignored for videos, |adaptation_sets| must have diff --git a/packager/mpd/base/period_unittest.cc b/packager/mpd/base/period_unittest.cc index 0c5afa5f75..59622a5662 100644 --- a/packager/mpd/base/period_unittest.cc +++ b/packager/mpd/base/period_unittest.cc @@ -93,15 +93,13 @@ class TestablePeriod : public Period { } // namespace -class PeriodTest : public ::testing::TestWithParam { +class PeriodTest : public ::testing::Test { public: PeriodTest() : testable_period_(mpd_options_), default_adaptation_set_(new StrictMock()), default_adaptation_set_ptr_(default_adaptation_set_.get()) {} - void SetUp() override { content_protection_in_adaptation_set_ = GetParam(); } - protected: MpdOptions mpd_options_; TestablePeriod testable_period_; @@ -112,7 +110,7 @@ class PeriodTest : public ::testing::TestWithParam { StrictMock* default_adaptation_set_ptr_; }; -TEST_P(PeriodTest, GetXml) { +TEST_F(PeriodTest, GetXml) { const char kVideoMediaInfo[] = "video_info {\n" " codec: 'avc1'\n" @@ -143,7 +141,7 @@ TEST_P(PeriodTest, GetXml) { XmlNodeEqual(kExpectedXml)); } -TEST_P(PeriodTest, DynamicMpdGetXml) { +TEST_F(PeriodTest, DynamicMpdGetXml) { const char kVideoMediaInfo[] = "video_info {\n" " codec: 'avc1'\n" @@ -175,7 +173,7 @@ TEST_P(PeriodTest, DynamicMpdGetXml) { XmlNodeEqual(kExpectedXml)); } -TEST_P(PeriodTest, SetDurationAndGetXml) { +TEST_F(PeriodTest, SetDurationAndGetXml) { const char kVideoMediaInfo[] = "video_info {\n" " codec: 'avc1'\n" @@ -217,7 +215,7 @@ TEST_P(PeriodTest, SetDurationAndGetXml) { } // Verify ForceSetSegmentAlignment is called. -TEST_P(PeriodTest, Text) { +TEST_F(PeriodTest, Text) { const char kTextMediaInfo[] = "text_info {\n" " codec: 'ttml'\n" @@ -235,7 +233,7 @@ TEST_P(PeriodTest, Text) { content_protection_in_adaptation_set_)); } -TEST_P(PeriodTest, TrickPlayWithMatchingAdaptationSet) { +TEST_F(PeriodTest, TrickPlayWithMatchingAdaptationSet) { const char kVideoMediaInfo[] = "video_info {\n" " codec: 'avc1'\n" @@ -282,7 +280,7 @@ TEST_P(PeriodTest, TrickPlayWithMatchingAdaptationSet) { } // Verify no AdaptationSet is returned on trickplay media info. -TEST_P(PeriodTest, TrickPlayWithNoMatchingAdaptationSet) { +TEST_F(PeriodTest, TrickPlayWithNoMatchingAdaptationSet) { const char kVideoMediaInfo[] = "video_info {\n" " codec: 'avc1'\n" @@ -324,11 +322,338 @@ TEST_P(PeriodTest, TrickPlayWithNoMatchingAdaptationSet) { content_protection_in_adaptation_set_)); } +// Don't put different audio languages or codecs in the same AdaptationSet. +TEST_F(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) { + const char kAacEnglishAudioContent[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'eng'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + const char kAacGermanAudioContent[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'ger'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + const char kVorbisGermanAudioContent1[] = + "audio_info {\n" + " codec: 'vorbis'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'ger'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_WEBM\n" + "media_duration_seconds: 10.5\n"; + const char kVorbisGermanAudioContent2[] = + "audio_info {\n" + " codec: 'vorbis'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'ger'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_WEBM\n" + "media_duration_seconds: 10.5\n"; + + std::unique_ptr> aac_eng_adaptation_set( + new StrictMock()); + auto* aac_eng_adaptation_set_ptr = aac_eng_adaptation_set.get(); + std::unique_ptr> aac_ger_adaptation_set( + new StrictMock()); + auto* aac_ger_adaptation_set_ptr = aac_ger_adaptation_set.get(); + std::unique_ptr> vorbis_german_adaptation_set( + new StrictMock()); + auto* vorbis_german_adaptation_set_ptr = vorbis_german_adaptation_set.get(); + + // We expect three AdaptationSets. + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(aac_eng_adaptation_set)))) + .WillOnce(Return(ByMove(std::move(aac_ger_adaptation_set)))) + .WillOnce(Return(ByMove(std::move(vorbis_german_adaptation_set)))); + + ASSERT_EQ(aac_eng_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kAacEnglishAudioContent), + content_protection_in_adaptation_set_)); + ASSERT_EQ(aac_ger_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kAacGermanAudioContent), + content_protection_in_adaptation_set_)); + ASSERT_EQ(vorbis_german_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVorbisGermanAudioContent1), + content_protection_in_adaptation_set_)); + // The same AdaptationSet is returned. + ASSERT_EQ(vorbis_german_adaptation_set_ptr, + testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kVorbisGermanAudioContent2), + content_protection_in_adaptation_set_)); +} + +TEST_F(PeriodTest, GetAdaptationSets) { + const char kContent1[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'eng'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + const char kContent2[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'ger'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + + std::unique_ptr> adaptation_set_1( + new StrictMock()); + auto* adaptation_set_1_ptr = adaptation_set_1.get(); + std::unique_ptr> adaptation_set_2( + new StrictMock()); + auto* adaptation_set_2_ptr = adaptation_set_2.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(adaptation_set_1)))) + .WillOnce(Return(ByMove(std::move(adaptation_set_2)))); + + ASSERT_EQ(adaptation_set_1_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kContent1), + content_protection_in_adaptation_set_)); + EXPECT_THAT(testable_period_.GetAdaptationSets(), + ElementsAre(adaptation_set_1_ptr)); + + ASSERT_EQ(adaptation_set_2_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kContent2), + content_protection_in_adaptation_set_)); + EXPECT_THAT(testable_period_.GetAdaptationSets(), + ElementsAre(adaptation_set_1_ptr, adaptation_set_2_ptr)); +} + +TEST_F(PeriodTest, OrderedByAdaptationSetId) { + const char kContent1[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'eng'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + const char kContent2[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'ger'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + + std::unique_ptr> adaptation_set_1( + new StrictMock()); + auto* adaptation_set_1_ptr = adaptation_set_1.get(); + std::unique_ptr> adaptation_set_2( + new StrictMock()); + auto* adaptation_set_2_ptr = adaptation_set_2.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(adaptation_set_1)))) + .WillOnce(Return(ByMove(std::move(adaptation_set_2)))); + + ASSERT_EQ(adaptation_set_1_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kContent1), + content_protection_in_adaptation_set_)); + ASSERT_EQ(adaptation_set_2_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kContent2), + content_protection_in_adaptation_set_)); + + adaptation_set_1_ptr->set_id(2); + adaptation_set_2_ptr->set_id(1); + const char kExpectedXml[] = + R"()" + // ContentType and Representation elements are populated after + // Representation::Init() is called. + R"( )" + R"( )" + R"()"; + EXPECT_THAT(testable_period_.GetXml(!kOutputPeriodDuration).get(), + XmlNodeEqual(kExpectedXml)); +} + +TEST_F(PeriodTest, AudioAdaptationSetDefaultLanguage) { + mpd_options_.mpd_params.default_language = "en"; + const char kEnglishAudioContent[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'en'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + std::unique_ptr> adaptation_set( + new StrictMock()); + auto* adaptation_set_ptr = adaptation_set.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(adaptation_set)))); + EXPECT_CALL(*adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)); + ASSERT_EQ(adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kEnglishAudioContent), + content_protection_in_adaptation_set_)); +} + +TEST_F(PeriodTest, AudioAdaptationSetNonDefaultLanguage) { + mpd_options_.mpd_params.default_language = "fr"; + const char kEnglishAudioContent[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 1200\n" + " num_channels: 2\n" + " language: 'en'\n" + "}\n" + "reference_time_scale: 50\n" + "container_type: CONTAINER_MP4\n" + "media_duration_seconds: 10.5\n"; + std::unique_ptr> adaptation_set( + new StrictMock()); + auto* adaptation_set_ptr = adaptation_set.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(adaptation_set)))); + EXPECT_CALL(*adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)).Times(0); + ASSERT_EQ(adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kEnglishAudioContent), + content_protection_in_adaptation_set_)); +} + +TEST_F(PeriodTest, TextAdaptationSetDefaultLanguage) { + mpd_options_.mpd_params.default_language = "en"; + const char kEnglishTextContent[] = + "text_info {\n" + " codec: 'webvtt'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}"; + std::unique_ptr> adaptation_set( + new StrictMock()); + auto* adaptation_set_ptr = adaptation_set.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(adaptation_set)))); + EXPECT_CALL(*adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)); + EXPECT_CALL(*adaptation_set_ptr, ForceSetSegmentAlignment(true)); + ASSERT_EQ(adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kEnglishTextContent), + content_protection_in_adaptation_set_)); +} + +TEST_F(PeriodTest, TextAdaptationSetNonDefaultLanguage) { + mpd_options_.mpd_params.default_language = "fr"; + const char kEnglishTextContent[] = + "text_info {\n" + " codec: 'webvtt'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}"; + std::unique_ptr> adaptation_set( + new StrictMock()); + auto* adaptation_set_ptr = adaptation_set.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(adaptation_set)))); + EXPECT_CALL(*adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)).Times(0); + EXPECT_CALL(*adaptation_set_ptr, ForceSetSegmentAlignment(true)); + ASSERT_EQ(adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kEnglishTextContent), + content_protection_in_adaptation_set_)); +} + +TEST_F(PeriodTest, TextAdaptationSetNonDefaultLanguageButDefaultTextLanguage) { + mpd_options_.mpd_params.default_language = "fr"; + mpd_options_.mpd_params.default_text_language = "en"; + const char kEnglishTextContent[] = + "text_info {\n" + " codec: 'webvtt'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}"; + std::unique_ptr> adaptation_set( + new StrictMock()); + auto* adaptation_set_ptr = adaptation_set.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(adaptation_set)))); + EXPECT_CALL(*adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)); + EXPECT_CALL(*adaptation_set_ptr, ForceSetSegmentAlignment(true)); + ASSERT_EQ(adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kEnglishTextContent), + content_protection_in_adaptation_set_)); +} + +TEST_F(PeriodTest, TextAdaptationSetDefaultLanguageButNonDefaultTextLanguage) { + mpd_options_.mpd_params.default_language = "en"; + mpd_options_.mpd_params.default_text_language = "fr"; + const char kEnglishTextContent[] = + "text_info {\n" + " codec: 'webvtt'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}"; + std::unique_ptr> adaptation_set( + new StrictMock()); + auto* adaptation_set_ptr = adaptation_set.get(); + + EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) + .WillOnce(Return(ByMove(std::move(adaptation_set)))); + EXPECT_CALL(*adaptation_set_ptr, AddRole(AdaptationSet::kRoleMain)).Times(0); + EXPECT_CALL(*adaptation_set_ptr, ForceSetSegmentAlignment(true)); + ASSERT_EQ(adaptation_set_ptr, testable_period_.GetOrCreateAdaptationSet( + ConvertToMediaInfo(kEnglishTextContent), + content_protection_in_adaptation_set_)); +} + +class PeriodTestWithContentProtection + : public PeriodTest, + public ::testing::WithParamInterface { + void SetUp() override { content_protection_in_adaptation_set_ = GetParam(); } +}; + // With content_protection_adaptation_set_ == true, verify with different // MediaInfo::ProtectedContent, two AdaptationSets should be created. // AdaptationSets with different DRM won't be switchable. // Otherwise, only one AdaptationSet is created. -TEST_P(PeriodTest, DifferentProtectedContent) { +TEST_P(PeriodTestWithContentProtection, DifferentProtectedContent) { // Note they both have different (bogus) pssh, like real use case. // default Key ID = _default_key_id_ const char kSdProtectedContent[] = @@ -435,7 +760,7 @@ TEST_P(PeriodTest, DifferentProtectedContent) { // Verify with the same MediaInfo::ProtectedContent, only one AdaptationSets // should be created regardless of the value of // content_protection_in_adaptation_set_. -TEST_P(PeriodTest, SameProtectedContent) { +TEST_P(PeriodTestWithContentProtection, SameProtectedContent) { // These have the same default key ID and PSSH. const char kSdProtectedContent[] = "video_info {\n" @@ -527,7 +852,7 @@ TEST_P(PeriodTest, SameProtectedContent) { // 3. Add a 4k protected content. This should also make a new AdaptationSet. // It should be switchable with SD/HD AdaptationSet. // Otherwise only one AdaptationSet is created. -TEST_P(PeriodTest, SetAdaptationSetSwitching) { +TEST_P(PeriodTestWithContentProtection, SetAdaptationSetSwitching) { // These have the same default key ID and PSSH. const char kSdProtectedContent[] = "video_info {\n" @@ -668,7 +993,8 @@ TEST_P(PeriodTest, SetAdaptationSetSwitching) { // Even if the UUIDs match, video and audio AdaptationSets should not be // switchable. -TEST_P(PeriodTest, DoNotSetAdaptationSetSwitchingIfContentTypesDifferent) { +TEST_P(PeriodTestWithContentProtection, + DoNotSetAdaptationSetSwitchingIfContentTypesDifferent) { // These have the same default key ID and PSSH. const char kVideoContent[] = "video_info {\n" @@ -745,192 +1071,8 @@ TEST_P(PeriodTest, DoNotSetAdaptationSetSwitchingIfContentTypesDifferent) { content_protection_in_adaptation_set_)); } -// Don't put different audio languages or codecs in the same AdaptationSet. -TEST_P(PeriodTest, SplitAdaptationSetsByLanguageAndCodec) { - const char kAacEnglishAudioContent[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'eng'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - const char kAacGermanAudioContent[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - const char kVorbisGermanAudioContent1[] = - "audio_info {\n" - " codec: 'vorbis'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_WEBM\n" - "media_duration_seconds: 10.5\n"; - const char kVorbisGermanAudioContent2[] = - "audio_info {\n" - " codec: 'vorbis'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_WEBM\n" - "media_duration_seconds: 10.5\n"; - - std::unique_ptr> aac_eng_adaptation_set( - new StrictMock()); - auto* aac_eng_adaptation_set_ptr = aac_eng_adaptation_set.get(); - std::unique_ptr> aac_ger_adaptation_set( - new StrictMock()); - auto* aac_ger_adaptation_set_ptr = aac_ger_adaptation_set.get(); - std::unique_ptr> vorbis_german_adaptation_set( - new StrictMock()); - auto* vorbis_german_adaptation_set_ptr = vorbis_german_adaptation_set.get(); - - // We expect three AdaptationSets. - EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) - .WillOnce(Return(ByMove(std::move(aac_eng_adaptation_set)))) - .WillOnce(Return(ByMove(std::move(aac_ger_adaptation_set)))) - .WillOnce(Return(ByMove(std::move(vorbis_german_adaptation_set)))); - - ASSERT_EQ(aac_eng_adaptation_set_ptr, - testable_period_.GetOrCreateAdaptationSet( - ConvertToMediaInfo(kAacEnglishAudioContent), - content_protection_in_adaptation_set_)); - ASSERT_EQ(aac_ger_adaptation_set_ptr, - testable_period_.GetOrCreateAdaptationSet( - ConvertToMediaInfo(kAacGermanAudioContent), - content_protection_in_adaptation_set_)); - ASSERT_EQ(vorbis_german_adaptation_set_ptr, - testable_period_.GetOrCreateAdaptationSet( - ConvertToMediaInfo(kVorbisGermanAudioContent1), - content_protection_in_adaptation_set_)); - // The same AdaptationSet is returned. - ASSERT_EQ(vorbis_german_adaptation_set_ptr, - testable_period_.GetOrCreateAdaptationSet( - ConvertToMediaInfo(kVorbisGermanAudioContent2), - content_protection_in_adaptation_set_)); -} - -TEST_P(PeriodTest, GetAdaptationSets) { - const char kContent1[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'eng'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - const char kContent2[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - - std::unique_ptr> adaptation_set_1( - new StrictMock()); - auto* adaptation_set_1_ptr = adaptation_set_1.get(); - std::unique_ptr> adaptation_set_2( - new StrictMock()); - auto* adaptation_set_2_ptr = adaptation_set_2.get(); - - EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) - .WillOnce(Return(ByMove(std::move(adaptation_set_1)))) - .WillOnce(Return(ByMove(std::move(adaptation_set_2)))); - - ASSERT_EQ(adaptation_set_1_ptr, testable_period_.GetOrCreateAdaptationSet( - ConvertToMediaInfo(kContent1), - content_protection_in_adaptation_set_)); - EXPECT_THAT(testable_period_.GetAdaptationSets(), - ElementsAre(adaptation_set_1_ptr)); - - ASSERT_EQ(adaptation_set_2_ptr, testable_period_.GetOrCreateAdaptationSet( - ConvertToMediaInfo(kContent2), - content_protection_in_adaptation_set_)); - EXPECT_THAT(testable_period_.GetAdaptationSets(), - ElementsAre(adaptation_set_1_ptr, adaptation_set_2_ptr)); -} - -TEST_P(PeriodTest, OrderedByAdaptationSetId) { - const char kContent1[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'eng'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - const char kContent2[] = - "audio_info {\n" - " codec: 'mp4a.40.2'\n" - " sampling_frequency: 44100\n" - " time_scale: 1200\n" - " num_channels: 2\n" - " language: 'ger'\n" - "}\n" - "reference_time_scale: 50\n" - "container_type: CONTAINER_MP4\n" - "media_duration_seconds: 10.5\n"; - - std::unique_ptr> adaptation_set_1( - new StrictMock()); - auto* adaptation_set_1_ptr = adaptation_set_1.get(); - std::unique_ptr> adaptation_set_2( - new StrictMock()); - auto* adaptation_set_2_ptr = adaptation_set_2.get(); - - EXPECT_CALL(testable_period_, NewAdaptationSet(_, _, _)) - .WillOnce(Return(ByMove(std::move(adaptation_set_1)))) - .WillOnce(Return(ByMove(std::move(adaptation_set_2)))); - - ASSERT_EQ(adaptation_set_1_ptr, testable_period_.GetOrCreateAdaptationSet( - ConvertToMediaInfo(kContent1), - content_protection_in_adaptation_set_)); - ASSERT_EQ(adaptation_set_2_ptr, testable_period_.GetOrCreateAdaptationSet( - ConvertToMediaInfo(kContent2), - content_protection_in_adaptation_set_)); - - adaptation_set_1_ptr->set_id(2); - adaptation_set_2_ptr->set_id(1); - const char kExpectedXml[] = - R"()" - // ContentType and Representation elements are populated after - // Representation::Init() is called. - R"( )" - R"( )" - R"()"; - EXPECT_THAT(testable_period_.GetXml(!kOutputPeriodDuration).get(), - XmlNodeEqual(kExpectedXml)); -} INSTANTIATE_TEST_CASE_P(ContentProtectionInAdaptationSet, - PeriodTest, + PeriodTestWithContentProtection, ::testing::Bool()); } // namespace shaka diff --git a/packager/mpd/public/mpd_params.h b/packager/mpd/public/mpd_params.h index 1b2bfab61e..2b249cc412 100644 --- a/packager/mpd/public/mpd_params.h +++ b/packager/mpd/public/mpd_params.h @@ -53,7 +53,12 @@ struct MpdParams { /// The tracks tagged with this language will have /// in the manifest. This allows the player to choose the correct default /// language for the content. + /// This applies to both audio and text tracks. The default language for text + /// tracks can be overriden by 'default_text_language'. std::string default_language; + /// Same as above, but this overrides the default language for text tracks, + /// i.e. subtitles or close-captions. + std::string default_text_language; /// Generate static MPD for live profile. Note that this flag has no effect /// for on-demand profile, in which case static MPD is always used. bool generate_static_live_mpd = false; diff --git a/packager/packager.cc b/packager/packager.cc index 335919997c..953d1c8d25 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -895,8 +895,12 @@ Status Packager::Initialize( // in the shortest form. mpd_params.default_language = LanguageToShortestForm(mpd_params.default_language); + mpd_params.default_text_language = + LanguageToShortestForm(mpd_params.default_text_language); hls_params.default_language = LanguageToShortestForm(hls_params.default_language); + hls_params.default_text_language = + LanguageToShortestForm(hls_params.default_text_language); if (!mpd_params.mpd_output.empty()) { const bool on_demand_dash_profile =