diff --git a/AUTHORS b/AUTHORS index 30ed4f3c72..fa5959d1e7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -16,6 +16,7 @@ 3Q GmbH <*@3qsdn.com> Alen Vrecko Anders Hasselqvist +Audible <*@audible.com> Chun-da Chen Daniel CantarĂ­n Dolby Laboratories <*@dolby.com> diff --git a/CONTRIBUTORS b/CONTRIBUTORS index e5010bdb0b..bc24fa1476 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -43,6 +43,7 @@ Piotr Srebrny Qingquan Wang Richard Eklycke Rintaro Kuroiwa +Samidh Talsania Sanil Raut Sergio Ammirata Thomas Inskip diff --git a/packager/app/mpd_flags.cc b/packager/app/mpd_flags.cc index 99e3b4e38a..5a881a0e6d 100644 --- a/packager/app/mpd_flags.cc +++ b/packager/app/mpd_flags.cc @@ -69,3 +69,9 @@ DEFINE_bool(include_mspr_pro_for_playready, "If enabled, PlayReady Object will be inserted into " " element alongside with " "when using PlayReady protection system."); +DEFINE_bool(dash_force_segment_list, + false, + "Uses SegmentList instead of SegmentBase. Use this if the " + "content is huge and the total number of (sub)segment references " + "is greater than what the sidx atom allows (65535). Currently " + "this flag is only supported in DASH ondemand profile."); diff --git a/packager/app/mpd_flags.h b/packager/app/mpd_flags.h index 337f4452fb..ccb4192947 100644 --- a/packager/app/mpd_flags.h +++ b/packager/app/mpd_flags.h @@ -23,5 +23,6 @@ DECLARE_bool(generate_dash_if_iop_compliant_mpd); DECLARE_bool(allow_approximate_segment_timeline); DECLARE_bool(allow_codec_switching); DECLARE_bool(include_mspr_pro_for_playready); +DECLARE_bool(dash_force_segment_list); #endif // APP_MPD_FLAGS_H_ diff --git a/packager/app/packager_main.cc b/packager/app/packager_main.cc index c8653ce7bc..179ecfd926 100644 --- a/packager/app/packager_main.cc +++ b/packager/app/packager_main.cc @@ -451,6 +451,7 @@ base::Optional GetPackagingParams() { mpd_params.time_shift_buffer_depth = FLAGS_time_shift_buffer_depth; mpd_params.preserved_segments_outside_live_window = FLAGS_preserved_segments_outside_live_window; + mpd_params.use_segment_list = FLAGS_dash_force_segment_list; if (!FLAGS_utc_timings.empty()) { base::StringPairs pairs; diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index 4605bbd658..0399ed1aa5 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -478,7 +478,8 @@ class PackagerAppTest(unittest.TestCase): default_language=None, segment_duration=1.0, use_fake_clock=True, - allow_codec_switching=False): + allow_codec_switching=False, + dash_force_segment_list=False): flags = ['--single_threaded'] if not strip_parameter_set_nalus: @@ -568,6 +569,10 @@ class PackagerAppTest(unittest.TestCase): if default_language: flags += ['--default_language', default_language] + if dash_force_segment_list: + flags += ['--dash_force_segment_list'] + flags += ['--generate_sidx_in_media_segments=false'] + flags.append('--segment_duration={0}'.format(segment_duration)) # Use fake clock, so output can be compared. @@ -1518,7 +1523,7 @@ class PackagerFunctionalTest(PackagerAppTest): def testEncryptionAndOutputMediaInfoAndMpdFromMediaInfo(self): self.assertPackageSuccess( - # The order is not determinstic if there are more than one + # The order is not deterministic if there are more than one # AdaptationSets, so only one is included here. self._GetStreams(['video']), self._GetFlags(encryption=True, output_media_info=True)) @@ -1526,6 +1531,15 @@ class PackagerFunctionalTest(PackagerAppTest): self._CheckTestResults( 'encryption-and-output-media-info-and-mpd-from-media-info') + def testEncryptionAndOutputMediaInfoAndMpdFromMediaInfoSegmentList(self): + self.assertPackageSuccess( + # The order is not deterministic if there are more than one + # AdaptationSets, so only one is included here. + self._GetStreams(['audio']), + self._GetFlags(encryption=True, output_media_info=True, dash_force_segment_list=True, output_dash=True)) + self._CheckTestResults( + 'encryption-and-output-media-info-and-mpd-from-media-info-segmentlist') + def testHlsSingleSegmentMp4Encrypted(self): self.assertPackageSuccess( self._GetStreams(['audio', 'video'], hls=True), diff --git a/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4 b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4 new file mode 100644 index 0000000000..81d1a3dc56 Binary files /dev/null and b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4 differ diff --git a/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4.media_info b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4.media_info new file mode 100644 index 0000000000..1aa25b4e6f --- /dev/null +++ b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/bear-640x360-audio.mp4.media_info @@ -0,0 +1,40 @@ +bandwidth: 133663 +audio_info { + codec: "mp4a.40.2" + sampling_frequency: 44100 + time_scale: 44100 + num_channels: 2 + decoder_config: "\022\020V\345\000" +} +init_range { + begin: 0 + end: 1006 +} +index_range { + begin: 1007 + end: 1006 +} +media_file_name: "bear-640x360-audio.mp4" +media_duration_seconds: 2.76317453 +reference_time_scale: 44100 +container_type: CONTAINER_MP4 +protected_content { + default_key_id: "1234567890123456" + content_protection_entry { + uuid: "1077efec-c0b2-4d02-ace3-3c1e52e2fb4b" + pssh: "\000\000\0004pssh\001\000\000\000\020w\357\354\300\262M\002\254\343<\036R\342\373K\000\000\000\0011234567890123456\000\000\000\000" + } + protection_scheme: "cenc" +} +subsegment_ranges { + begin: 1007 + end: 18034 +} +subsegment_ranges { + begin: 18035 + end: 34716 +} +subsegment_ranges { + begin: 34717 + end: 44575 +} diff --git a/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/output.mpd b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/output.mpd new file mode 100644 index 0000000000..866f0cb8b8 --- /dev/null +++ b/packager/app/test/testdata/encryption-and-output-media-info-and-mpd-from-media-info-segmentlist/output.mpd @@ -0,0 +1,22 @@ + + + + + + + + AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== + + + + 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 421f0454dd..40d1b08ccb 100644 --- a/packager/media/event/mpd_notify_muxer_listener.cc +++ b/packager/media/event/mpd_notify_muxer_listener.cc @@ -142,6 +142,7 @@ void MpdNotifyMuxerListener::OnMediaEnd(const MediaRanges& media_ranges, DCHECK(media_info_); if (!internal::SetVodInformation(media_ranges, duration_seconds, + mpd_notifier_->use_segment_list(), media_info_.get())) { LOG(ERROR) << "Failed to generate VOD information from input."; return; diff --git a/packager/media/event/mpd_notify_muxer_listener_unittest.cc b/packager/media/event/mpd_notify_muxer_listener_unittest.cc index 219fac1826..5ef272e02b 100644 --- a/packager/media/event/mpd_notify_muxer_listener_unittest.cc +++ b/packager/media/event/mpd_notify_muxer_listener_unittest.cc @@ -68,6 +68,19 @@ class MpdNotifyMuxerListenerTest : public ::testing::TestWithParam { mpd_options.dash_profile = DashProfile::kOnDemand; // On-demand profile should be static. mpd_options.mpd_type = MpdType::kStatic; + mpd_options.mpd_params.use_segment_list = false; + notifier_.reset(new MockMpdNotifier(mpd_options)); + listener_.reset( + new MpdNotifyMuxerListener(notifier_.get())); + } + + + void SetupForVodSegmentList() { + MpdOptions mpd_options; + mpd_options.dash_profile = DashProfile::kOnDemand; + // On-demand profile should be static. + mpd_options.mpd_type = MpdType::kStatic; + mpd_options.mpd_params.use_segment_list = true; notifier_.reset(new MockMpdNotifier(mpd_options)); listener_.reset( new MpdNotifyMuxerListener(notifier_.get())); @@ -78,6 +91,7 @@ class MpdNotifyMuxerListenerTest : public ::testing::TestWithParam { mpd_options.dash_profile = DashProfile::kLive; // Live profile can be static or dynamic. mpd_options.mpd_type = GetParam(); + mpd_options.mpd_params.use_segment_list = false; notifier_.reset(new MockMpdNotifier(mpd_options)); listener_.reset(new MpdNotifyMuxerListener(notifier_.get())); } @@ -123,6 +137,28 @@ TEST_F(MpdNotifyMuxerListenerTest, VodClearContent) { FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); } + +TEST_F(MpdNotifyMuxerListenerTest, VodClearContentSegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options; + SetDefaultMuxerOptions(&muxer_options); + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + listener_->OnMediaStart(muxer_options, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + EXPECT_CALL(*notifier_, NotifyNewContainer( + ExpectMediaInfoEq(kExpectedDefaultMediaInfoSubsegmentRange), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + // default_key_id and pssh are converted to string because when std::equal // compares a negative char and uint8_t > 127, it considers them not equal. MATCHER_P4(ProtectedContentEq, uuid, name, default_key_id, pssh, "") { @@ -190,6 +226,49 @@ TEST_F(MpdNotifyMuxerListenerTest, VodEncryptedContent) { FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); } + +TEST_F(MpdNotifyMuxerListenerTest, VodEncryptedContentSegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options; + SetDefaultMuxerOptions(&muxer_options); + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + + const std::vector default_key_id( + kDefaultKeyId, kDefaultKeyId + arraysize(kDefaultKeyId) - 1); + + const std::string kExpectedMediaInfo = + std::string(kExpectedDefaultMediaInfoSubsegmentRange) + + "protected_content {\n" + " protection_scheme: 'cenc'\n" + " content_protection_entry {\n" + " uuid: '00010203-0405-0607-0809-0a0b0c0d0e0f'\n" + " pssh: '" + std::string(kExpectedDefaultPsshBox) + "'\n" + " }\n" + " default_key_id: 'defaultkeyid'\n" + " include_mspr_pro: 1\n" + "}\n"; + + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + + std::vector iv(kBogusIv, kBogusIv + arraysize(kBogusIv)); + listener_->OnEncryptionInfoReady(kInitialEncryptionInfo, FOURCC_cenc, + default_key_id, iv, + GetDefaultKeySystemInfo()); + + listener_->OnMediaStart(muxer_options, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + EXPECT_CALL(*notifier_, + NotifyNewContainer(ExpectMediaInfoEq(kExpectedMediaInfo), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + // Verify that calling OnSampleDurationReady() sets the frame duration in the // media info, and the media info gets passed to NotifyNewContainer() with // frame_duration == sample_duration. @@ -223,6 +302,57 @@ TEST_F(MpdNotifyMuxerListenerTest, VodOnSampleDurationReady) { "container_type: 1\n" "media_file_name: 'test_output_file_name.mp4'\n" "media_duration_seconds: 10.5\n"; + + const uint32_t kReferenceTimeScale = 1111u; // Should match the protobuf. + + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + listener_->OnMediaStart(muxer_options, *video_stream_info, + kReferenceTimeScale, MuxerListener::kContainerMp4); + listener_->OnSampleDurationReady(kSampleDuration); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + EXPECT_CALL(*notifier_, + NotifyNewContainer(ExpectMediaInfoEq(kExpectedMediaInfo), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + + +TEST_F(MpdNotifyMuxerListenerTest, VodOnSampleDurationReadySegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options; + SetDefaultMuxerOptions(&muxer_options); + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + const uint32_t kSampleDuration = 1234u; + const char kExpectedMediaInfo[] = + "video_info {\n" + " frame_duration: 1234\n" // Should match the constant above. + " codec: 'avc1.010101'\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 120\n" + "}\n" + "index_range {\n" + " begin: 121\n" + " end: 221\n" + "}\n" + "reference_time_scale: 1111\n" + "container_type: 1\n" + "media_file_name: 'test_output_file_name.mp4'\n" + "media_duration_seconds: 10.5\n" + "subsegment_ranges {\n" + " begin: 222\n" + " end: 9999\n" + "}\n"; const uint32_t kReferenceTimeScale = 1111u; // Should match the protobuf. EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); @@ -278,6 +408,44 @@ TEST_F(MpdNotifyMuxerListenerTest, VodOnNewSegment) { FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); } +TEST_F(MpdNotifyMuxerListenerTest, VodOnNewSegmentSegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options; + SetDefaultMuxerOptions(&muxer_options); + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + + const uint64_t kStartTime1 = 0u; + const uint64_t kDuration1 = 1000u; + const uint64_t kSegmentFileSize1 = 29812u; + const uint64_t kStartTime2 = 1001u; + const uint64_t kDuration2 = 3787u; + const uint64_t kSegmentFileSize2 = 83743u; + + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + EXPECT_CALL(*notifier_, NotifyNewSegment(_, _, _, _)).Times(0); + listener_->OnMediaStart(muxer_options, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + listener_->OnNewSegment("", kStartTime1, kDuration1, kSegmentFileSize1); + listener_->OnCueEvent(kStartTime2, "dummy cue data"); + listener_->OnNewSegment("", kStartTime2, kDuration2, kSegmentFileSize2); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + InSequence s; + EXPECT_CALL(*notifier_, NotifyNewContainer( + ExpectMediaInfoEq(kExpectedDefaultMediaInfoSubsegmentRange), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, + NotifyNewSegment(_, kStartTime1, kDuration1, kSegmentFileSize1)); + EXPECT_CALL(*notifier_, NotifyCueEvent(_, kStartTime2)); + EXPECT_CALL(*notifier_, + NotifyNewSegment(_, kStartTime2, kDuration2, kSegmentFileSize2)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + // Verify the event handling with multiple files, i.e. multiple OnMediaStart and // OnMediaEnd calls. TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFiles) { @@ -341,6 +509,68 @@ TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFiles) { FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); } + +TEST_F(MpdNotifyMuxerListenerTest, VodMultipleFilesSegmentList) { + SetupForVodSegmentList(); + MuxerOptions muxer_options1; + SetDefaultMuxerOptions(&muxer_options1); + muxer_options1.output_file_name = "test_output1.mp4"; + MuxerOptions muxer_options2 = muxer_options1; + muxer_options2.output_file_name = "test_output2.mp4"; + + MediaInfo expected_media_info1 = + ConvertToMediaInfo(kExpectedDefaultMediaInfoSubsegmentRange); + expected_media_info1.set_media_file_name("test_output1.mp4"); + MediaInfo expected_media_info2 = expected_media_info1; + expected_media_info2.set_media_file_name("test_output2.mp4"); + + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + std::shared_ptr video_stream_info = + CreateVideoStreamInfo(video_params); + + const uint64_t kStartTime1 = 0u; + const uint64_t kDuration1 = 1000u; + const uint64_t kSegmentFileSize1 = 29812u; + const uint64_t kStartTime2 = 1001u; + const uint64_t kDuration2 = 3787u; + const uint64_t kSegmentFileSize2 = 83743u; + + // Expectation for first file before OnMediaEnd. + EXPECT_CALL(*notifier_, NotifyNewContainer(_, _)).Times(0); + EXPECT_CALL(*notifier_, NotifyNewSegment(_, _, _, _)).Times(0); + listener_->OnMediaStart(muxer_options1, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + listener_->OnNewSegment("", kStartTime1, kDuration1, kSegmentFileSize1); + listener_->OnCueEvent(kStartTime2, "dummy cue data"); + ::testing::Mock::VerifyAndClearExpectations(notifier_.get()); + + // Expectation for first file OnMediaEnd. + InSequence s; + EXPECT_CALL(*notifier_, + NotifyNewContainer(EqualsProto(expected_media_info1), _)) + .WillOnce(Return(true)); + EXPECT_CALL(*notifier_, + NotifyNewSegment(_, kStartTime1, kDuration1, kSegmentFileSize1)); + EXPECT_CALL(*notifier_, NotifyCueEvent(_, kStartTime2)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); + + // Expectation for second file before OnMediaEnd. + listener_->OnMediaStart(muxer_options2, *video_stream_info, + kDefaultReferenceTimeScale, + MuxerListener::kContainerMp4); + listener_->OnNewSegment("", kStartTime2, kDuration2, kSegmentFileSize2); + + // Expectation for second file OnMediaEnd. + EXPECT_CALL(*notifier_, + NotifyMediaInfoUpdate(_, EqualsProto(expected_media_info2))); + EXPECT_CALL(*notifier_, + NotifyNewSegment(_, kStartTime2, kDuration2, kSegmentFileSize2)); + EXPECT_CALL(*notifier_, Flush()); + FireOnMediaEndWithParams(GetDefaultOnMediaEndParams()); +} + // Live without key rotation. Note that OnEncryptionInfoReady() is called before // OnMediaStart() but no more calls. TEST_P(MpdNotifyMuxerListenerTest, LiveNoKeyRotation) { diff --git a/packager/media/event/muxer_listener_factory.cc b/packager/media/event/muxer_listener_factory.cc index 820c540858..287a936f96 100644 --- a/packager/media/event/muxer_listener_factory.cc +++ b/packager/media/event/muxer_listener_factory.cc @@ -25,11 +25,12 @@ namespace { const char kMediaInfoSuffix[] = ".media_info"; std::unique_ptr CreateMediaInfoDumpListenerInternal( - const std::string& output) { + const std::string& output, + bool use_segment_list) { DCHECK(!output.empty()); std::unique_ptr listener( - new VodMediaInfoDumpMuxerListener(output + kMediaInfoSuffix)); + new VodMediaInfoDumpMuxerListener(output + kMediaInfoSuffix, use_segment_list)); return listener; } @@ -80,11 +81,13 @@ std::list> CreateHlsListenersInternal( } // namespace MuxerListenerFactory::MuxerListenerFactory(bool output_media_info, + bool use_segment_list, MpdNotifier* mpd_notifier, hls::HlsNotifier* hls_notifier) : output_media_info_(output_media_info), mpd_notifier_(mpd_notifier), - hls_notifier_(hls_notifier) {} + hls_notifier_(hls_notifier), + use_segment_list_(use_segment_list) {} std::unique_ptr MuxerListenerFactory::CreateListener( const StreamData& stream) { @@ -103,7 +106,8 @@ std::unique_ptr MuxerListenerFactory::CreateListener( new CombinedMuxerListener); if (output_media_info_) { combined_listener->AddListener( - CreateMediaInfoDumpListenerInternal(stream.media_info_output)); + CreateMediaInfoDumpListenerInternal(stream.media_info_output, + use_segment_list_)); } if (mpd_notifier_ && !stream.hls_only) { diff --git a/packager/media/event/muxer_listener_factory.h b/packager/media/event/muxer_listener_factory.h index 841d42e995..fd7e384fdc 100644 --- a/packager/media/event/muxer_listener_factory.h +++ b/packager/media/event/muxer_listener_factory.h @@ -58,11 +58,15 @@ class MuxerListenerFactory { /// Create a new muxer listener. /// @param output_media_info must be true for the combined listener to include /// a media info dump listener. + /// @param use_segment_list is set when mpd_notifier_ is null and + /// --output_media_info is set. If mpd_notifer is non-null, this value + /// is the same as mpd_notifier->use_segment_list(). /// @param mpd_notifer must be non-null for the combined listener to include a /// mpd listener. /// @param hls_notifier must be non-null for the combined listener to include /// an HLS listener. MuxerListenerFactory(bool output_media_info, + bool use_segment_list, MpdNotifier* mpd_notifier, hls::HlsNotifier* hls_notifier); @@ -81,6 +85,9 @@ class MuxerListenerFactory { MpdNotifier* mpd_notifier_; hls::HlsNotifier* hls_notifier_; + /// This is set when mpd_notifier_ is NULL and --output_media_info is set. + bool use_segment_list_; + // A counter to track which stream we are on. int stream_index_ = 0; }; diff --git a/packager/media/event/muxer_listener_internal.cc b/packager/media/event/muxer_listener_internal.cc index 0ef27cff27..336737c1c3 100644 --- a/packager/media/event/muxer_listener_internal.cc +++ b/packager/media/event/muxer_listener_internal.cc @@ -251,6 +251,7 @@ bool IsMediaInfoCompatible(const MediaInfo& media_info1, bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges, float duration_seconds, + bool use_segment_lists, MediaInfo* media_info) { DCHECK(media_info); @@ -270,6 +271,12 @@ bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges, media_info->mutable_index_range()); } + if (use_segment_lists) { + for (const auto& range : media_ranges.subsegment_ranges) { + SetRange(range.start, range.end, media_info->add_subsegment_ranges()); + } + } + media_info->set_media_duration_seconds(duration_seconds); return true; diff --git a/packager/media/event/muxer_listener_internal.h b/packager/media/event/muxer_listener_internal.h index a803928b3e..ab250904a9 100644 --- a/packager/media/event/muxer_listener_internal.h +++ b/packager/media/event/muxer_listener_internal.h @@ -42,6 +42,7 @@ bool IsMediaInfoCompatible(const MediaInfo& media_info1, /// @return true on success, false otherwise. bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges, float duration_seconds, + bool use_segment_list, MediaInfo* media_info); /// @param protection_scheme specifies the protection scheme: 'cenc', 'cens', diff --git a/packager/media/event/muxer_listener_test_helper.h b/packager/media/event/muxer_listener_test_helper.h index ca3a15d872..d1c49a4d16 100644 --- a/packager/media/event/muxer_listener_test_helper.h +++ b/packager/media/event/muxer_listener_test_helper.h @@ -43,6 +43,33 @@ const char kExpectedDefaultMediaInfo[] = "container_type: 1\n" "media_file_name: 'test_output_file_name.mp4'\n" "media_duration_seconds: 10.5\n"; + +const char kExpectedDefaultMediaInfoSubsegmentRange[] = + "video_info {\n" + " codec: 'avc1.010101'\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 120\n" + "}\n" + "index_range {\n" + " begin: 121\n" + " end: 221\n" + "}\n" + "reference_time_scale: 1000\n" + "container_type: 1\n" + "media_file_name: 'test_output_file_name.mp4'\n" + "media_duration_seconds: 10.5\n" + "subsegment_ranges {\n" + " begin: 222\n" + " end: 9999\n" + "}\n"; + const uint32_t kDefaultReferenceTimeScale = 1000u; // Struct that gets passed for to CreateVideoStreamInfo() to create a diff --git a/packager/media/event/vod_media_info_dump_muxer_listener.cc b/packager/media/event/vod_media_info_dump_muxer_listener.cc index e1f59c4948..2259408f7b 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener.cc +++ b/packager/media/event/vod_media_info_dump_muxer_listener.cc @@ -22,8 +22,9 @@ namespace shaka { namespace media { VodMediaInfoDumpMuxerListener::VodMediaInfoDumpMuxerListener( - const std::string& output_file_path) - : output_file_name_(output_file_path) {} + const std::string& output_file_path, bool use_segment_list) + : output_file_name_(output_file_path), + use_segment_list_(use_segment_list) {} VodMediaInfoDumpMuxerListener::~VodMediaInfoDumpMuxerListener() {} @@ -78,7 +79,7 @@ void VodMediaInfoDumpMuxerListener::OnMediaEnd(const MediaRanges& media_ranges, float duration_seconds) { DCHECK(media_info_); if (!internal::SetVodInformation(media_ranges, duration_seconds, - media_info_.get())) { + use_segment_list_, media_info_.get())) { LOG(ERROR) << "Failed to generate VOD information from input."; return; } diff --git a/packager/media/event/vod_media_info_dump_muxer_listener.h b/packager/media/event/vod_media_info_dump_muxer_listener.h index 19044d472b..eea91e483c 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener.h +++ b/packager/media/event/vod_media_info_dump_muxer_listener.h @@ -27,7 +27,7 @@ namespace media { class VodMediaInfoDumpMuxerListener : public MuxerListener { public: - VodMediaInfoDumpMuxerListener(const std::string& output_file_name); + VodMediaInfoDumpMuxerListener(const std::string& output_file_name, bool use_segment_list); ~VodMediaInfoDumpMuxerListener() override; /// @name MuxerListener implementation overrides. @@ -63,6 +63,8 @@ class VodMediaInfoDumpMuxerListener : public MuxerListener { static bool WriteMediaInfoToFile(const MediaInfo& media_info, const std::string& output_file_path); + void set_use_segment_list(bool value) {use_segment_list_ = value;} + private: std::string output_file_name_; std::unique_ptr media_info_; @@ -74,6 +76,8 @@ class VodMediaInfoDumpMuxerListener : public MuxerListener { std::vector default_key_id_; std::vector key_system_info_; + bool use_segment_list_ = false; + DISALLOW_COPY_AND_ASSIGN(VodMediaInfoDumpMuxerListener); }; diff --git a/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc b/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc index 9e74b19113..a284dd1786 100644 --- a/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc +++ b/packager/media/event/vod_media_info_dump_muxer_listener_unittest.cc @@ -71,7 +71,12 @@ class VodMediaInfoDumpMuxerListenerTest : public ::testing::Test { DLOG(INFO) << "Created temp file: " << temp_file_path_.value(); listener_.reset(new VodMediaInfoDumpMuxerListener(temp_file_path_ - .AsUTF8Unsafe())); + .AsUTF8Unsafe(),false)); + + } + + void SetSegmentListFlag() { + listener_->set_use_segment_list(true); } void TearDown() override { @@ -228,6 +233,7 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, CheckPixelWidthAndHeightSet) { "container_type: 1\n" "media_file_name: 'test_output_file_name.mp4'\n" "media_duration_seconds: 10.5\n"; + EXPECT_THAT(temp_file_path_.AsUTF8Unsafe(), FileContentEqualsProto(kExpectedProtobufOutput)); } @@ -274,5 +280,47 @@ TEST_F(VodMediaInfoDumpMuxerListenerTest, CheckBandwidth) { FileContentEqualsProto(kExpectedProtobufOutput)); } +// Equivalent tests with segment list flag on which writes subsegment ranges +// to media info files + +TEST_F(VodMediaInfoDumpMuxerListenerTest, UnencryptedStream_Normal_SegmentList) { + SetSegmentListFlag(); + std::shared_ptr stream_info = + CreateVideoStreamInfo(GetDefaultVideoStreamInfoParams()); + + FireOnMediaStartWithDefaultMuxerOptions(*stream_info, !kEnableEncryption); + OnMediaEndParameters media_end_param = GetDefaultOnMediaEndParams(); + FireOnMediaEndWithParams(media_end_param); + + const char kExpectedProtobufOutput[] = + "bandwidth: 0\n" + "video_info {\n" + " codec: 'avc1.010101'\n" + " width: 720\n" + " height: 480\n" + " time_scale: 10\n" + " pixel_width: 1\n" + " pixel_height: 1\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 120\n" + "}\n" + "index_range {\n" + " begin: 121\n" + " end: 221\n" + "}\n" + "reference_time_scale: 1000\n" + "container_type: 1\n" + "media_file_name: 'test_output_file_name.mp4'\n" + "media_duration_seconds: 10.5\n" + "subsegment_ranges {\n" + " begin: 222\n" + " end: 9999\n" + "}\n"; + EXPECT_THAT(temp_file_path_.AsUTF8Unsafe(), + FileContentEqualsProto(kExpectedProtobufOutput)); +} + } // namespace media } // namespace shaka diff --git a/packager/mpd/base/media_info.proto b/packager/mpd/base/media_info.proto index 5e2f8619ff..d449461b2a 100644 --- a/packager/mpd/base/media_info.proto +++ b/packager/mpd/base/media_info.proto @@ -172,6 +172,7 @@ message MediaInfo { optional Range init_range = 6; optional Range index_range = 7; optional string media_file_name = 8; + repeated Range subsegment_ranges = 23; // END VOD only. // VOD and static LIVE. diff --git a/packager/mpd/base/mpd_notifier.h b/packager/mpd/base/mpd_notifier.h index 6576b7c625..22f3c80b9f 100644 --- a/packager/mpd/base/mpd_notifier.h +++ b/packager/mpd/base/mpd_notifier.h @@ -113,6 +113,11 @@ class MpdNotifier { /// @return The mpd type for this object. MpdType mpd_type() const { return mpd_options_.mpd_type; } + /// @return The value of dash_force_segment_list flag + bool use_segment_list() const { + return mpd_options_.mpd_params.use_segment_list; + } + private: const MpdOptions mpd_options_; diff --git a/packager/mpd/base/representation.cc b/packager/mpd/base/representation.cc index 3d17472fcb..0616540530 100644 --- a/packager/mpd/base/representation.cc +++ b/packager/mpd/base/representation.cc @@ -265,7 +265,9 @@ base::Optional Representation::GetXml() { } if (HasVODOnlyFields(media_info_) && - !representation.AddVODOnlyInfo(media_info_)) { + !representation.AddVODOnlyInfo( + media_info_, mpd_options_.mpd_params.use_segment_list, + mpd_options_.mpd_params.target_segment_duration)) { LOG(ERROR) << "Failed to add VOD info."; return base::nullopt; } diff --git a/packager/mpd/base/xml/xml_node.cc b/packager/mpd/base/xml/xml_node.cc index ff504f0d2c..c8ebb1a74b 100644 --- a/packager/mpd/base/xml/xml_node.cc +++ b/packager/mpd/base/xml/xml_node.cc @@ -9,6 +9,7 @@ #include #include +#include #include #include @@ -376,57 +377,83 @@ bool RepresentationXmlNode::AddAudioInfo(const AudioInfo& audio_info) { AddAudioSamplingRateInfo(audio_info); } -bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info) { - const bool use_segment_list_text = +bool RepresentationXmlNode::AddVODOnlyInfo(const MediaInfo& media_info, + bool use_segment_list, + double target_segment_duration) { + const bool use_single_segment_url_with_media = media_info.has_text_info() && media_info.has_presentation_time_offset(); - if (media_info.has_media_file_url() && !use_segment_list_text) { + if (media_info.has_media_file_url() && !use_single_segment_url_with_media) { XmlNode base_url("BaseURL"); base_url.SetContent(media_info.media_file_url()); RCHECK(AddChild(std::move(base_url))); } - const bool need_segment_base = - media_info.has_index_range() || media_info.has_init_range() || - (media_info.has_reference_time_scale() && !media_info.has_text_info()); - DCHECK(!need_segment_base || !use_segment_list_text); + const bool need_segment_base_or_list = + use_segment_list || media_info.has_index_range() || + media_info.has_init_range() || + (media_info.has_reference_time_scale() && !media_info.has_text_info()) || + use_single_segment_url_with_media; - if (need_segment_base || use_segment_list_text) { - XmlNode child(need_segment_base ? "SegmentBase" : "SegmentList"); - if (media_info.has_index_range()) { - RCHECK(child.SetStringAttribute("indexRange", - RangeToString(media_info.index_range()))); - } - - if (media_info.has_reference_time_scale()) { - RCHECK(child.SetIntegerAttribute("timescale", - media_info.reference_time_scale())); - } - - if (media_info.has_presentation_time_offset()) { - RCHECK(child.SetIntegerAttribute("presentationTimeOffset", - media_info.presentation_time_offset())); - } - - if (media_info.has_init_range()) { - XmlNode initialization("Initialization"); - RCHECK(initialization.SetStringAttribute( - "range", RangeToString(media_info.init_range()))); - - RCHECK(child.AddChild(std::move(initialization))); - } - - if (use_segment_list_text) { - XmlNode media_url("SegmentURL"); - RCHECK( - media_url.SetStringAttribute("media", media_info.media_file_url())); - RCHECK(child.AddChild(std::move(media_url))); - } - - RCHECK(AddChild(std::move(child))); + if (!need_segment_base_or_list) { + return true; } + XmlNode child(use_segment_list || use_single_segment_url_with_media + ? "SegmentList" + : "SegmentBase"); + + // Forcing SegmentList for longer audio causes sidx atom to not be + // generated, therefore indexRange is not added to MPD if flag is set. + if (media_info.has_index_range() && !use_segment_list) { + RCHECK(child.SetStringAttribute("indexRange", + RangeToString(media_info.index_range()))); + } + + if (media_info.has_reference_time_scale()) { + RCHECK(child.SetIntegerAttribute("timescale", + media_info.reference_time_scale())); + + if (use_segment_list && !use_single_segment_url_with_media) { + const uint64_t duration_seconds = static_cast( + floor(target_segment_duration * media_info.reference_time_scale())); + RCHECK(child.SetIntegerAttribute("duration", duration_seconds)); + } + } + + if (media_info.has_presentation_time_offset()) { + RCHECK(child.SetIntegerAttribute("presentationTimeOffset", + media_info.presentation_time_offset())); + } + + if (media_info.has_init_range()) { + XmlNode initialization("Initialization"); + RCHECK(initialization.SetStringAttribute( + "range", RangeToString(media_info.init_range()))); + + RCHECK(child.AddChild(std::move(initialization))); + } + + if (use_single_segment_url_with_media) { + XmlNode media_url("SegmentURL"); + RCHECK(media_url.SetStringAttribute("media", media_info.media_file_url())); + RCHECK(child.AddChild(std::move(media_url))); + } + + // Since the SegmentURLs here do not have a @media element, + // BaseURL element is mapped to the @media attribute. + if (use_segment_list) { + for (const Range& subsegment_range : media_info.subsegment_ranges()) { + XmlNode subsegment("SegmentURL"); + RCHECK(subsegment.SetStringAttribute("mediaRange", + RangeToString(subsegment_range))); + + RCHECK(child.AddChild(std::move(subsegment))); + } + } + + RCHECK(AddChild(std::move(child))); return true; } diff --git a/packager/mpd/base/xml/xml_node.h b/packager/mpd/base/xml/xml_node.h index a306fdfed7..b048d39627 100644 --- a/packager/mpd/base/xml/xml_node.h +++ b/packager/mpd/base/xml/xml_node.h @@ -205,8 +205,15 @@ class RepresentationXmlNode : public RepresentationBaseXmlNode { /// Adds fields that are specific to VOD. This ignores @a media_info fields /// for Live. /// @param media_info is a MediaInfo with VOD information. + /// @param use_segment_list is a param that instructs the xml writer to + /// use SegmentList instead of SegmentBase. + /// @param target_segment_duration is a param that specifies the target + // duration of media segments. This is only used when use_segment_list + // is true. /// @return true on success, false otherwise. - bool AddVODOnlyInfo(const MediaInfo& media_info) WARN_UNUSED_RESULT; + bool AddVODOnlyInfo(const MediaInfo& media_info, + bool use_segment_list, + double target_segment_duration) WARN_UNUSED_RESULT; /// @param segment_infos is a set of SegmentInfos. This method assumes that /// SegmentInfos are sorted by its start time. diff --git a/packager/mpd/base/xml/xml_node_unittest.cc b/packager/mpd/base/xml/xml_node_unittest.cc index c24aac35b4..b323ec5434 100644 --- a/packager/mpd/base/xml/xml_node_unittest.cc +++ b/packager/mpd/base/xml/xml_node_unittest.cc @@ -15,11 +15,13 @@ #include "packager/base/strings/string_util.h" #include "packager/mpd/base/segment_info.h" #include "packager/mpd/base/xml/xml_node.h" +#include "packager/mpd/test/mpd_builder_test_helper.h" #include "packager/mpd/test/xml_compare.h" DECLARE_bool(segment_template_constant_duration); DECLARE_bool(dash_add_last_segment_number_when_needed); + using ::testing::ElementsAre; namespace shaka { @@ -543,5 +545,175 @@ TEST_F(LiveSegmentTimelineTest, LastSegmentNumberSupplementalProperty) { FLAGS_dash_add_last_segment_number_when_needed = false; } +// Creating a separate Test Suite for RepresentationXmlNode::AddVODOnlyInfo +class OnDemandVODSegmentTest : public ::testing::Test { +}; + +TEST_F(OnDemandVODSegmentTest, SegmentBase) { + const char kTestMediaInfo[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 44100\n" + " num_channels: 2\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 863\n" + "}\n" + "index_range {\n" + " begin: 864\n" + " end: 931\n" + "}\n" + "media_file_url: 'encrypted_audio.mp4'\n" + "media_duration_seconds: 24.009434\n" + "reference_time_scale: 44100\n" + "presentation_time_offset: 100\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTestMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, false, 100)); + EXPECT_THAT(representation, + XmlNodeEqual("" + "encrypted_audio.mp4" + "" + "" + "" + "")); +} + +TEST_F(OnDemandVODSegmentTest, TextInfoBaseUrl) { + const char kTextMediaInfo[] = + "text_info {\n" + " codec: 'ttml'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}\n" + "media_duration_seconds: 35\n" + "bandwidth: 1000\n" + "media_file_url: 'subtitle.xml'\n" + "container_type: CONTAINER_TEXT\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTextMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, false, 100)); + EXPECT_THAT(representation, XmlNodeEqual("" + "subtitle.xml" + "")); +} + +TEST_F(OnDemandVODSegmentTest, TextInfoWithPresentationOffset) { + const char kTextMediaInfo[] = + "text_info {\n" + " codec: 'ttml'\n" + " language: 'en'\n" + " type: SUBTITLE\n" + "}\n" + "media_duration_seconds: 35\n" + "bandwidth: 1000\n" + "media_file_url: 'subtitle.xml'\n" + "container_type: CONTAINER_TEXT\n" + "presentation_time_offset: 100\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTextMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, false, 100)); + + EXPECT_THAT(representation, + XmlNodeEqual("" + "" + "" + "" + "")); +} + +TEST_F(OnDemandVODSegmentTest, SegmentListWithoutUrls) { + const char kTestMediaInfo[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 44100\n" + " num_channels: 2\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 863\n" + "}\n" + "index_range {\n" + " begin: 864\n" + " end: 931\n" + "}\n" + "media_file_url: 'encrypted_audio.mp4'\n" + "media_duration_seconds: 24.009434\n" + "reference_time_scale: 44100\n" + "presentation_time_offset: 100\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTestMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, true, 100)); + + EXPECT_THAT( + representation, + XmlNodeEqual("" + "encrypted_audio.mp4" + "" + "" + "" + "")); +} + +TEST_F(OnDemandVODSegmentTest, SegmentUrlWithMediaRanges) { + const char kTextMediaInfo[] = + "audio_info {\n" + " codec: 'mp4a.40.2'\n" + " sampling_frequency: 44100\n" + " time_scale: 44100\n" + " num_channels: 2\n" + "}\n" + "init_range {\n" + " begin: 0\n" + " end: 863\n" + "}\n" + "index_range {\n" + " begin: 864\n" + " end: 931\n" + "}\n" + "media_file_url: 'encrypted_audio.mp4'\n" + "media_duration_seconds: 24.009434\n" + "reference_time_scale: 44100\n" + "presentation_time_offset: 100\n" + "subsegment_ranges {\n" + " begin: 932\n" + " end: 9999\n" + "}\n" + "subsegment_ranges {\n" + " begin: 10000\n" + " end: 11000\n" + "}\n"; + + const MediaInfo media_info = ConvertToMediaInfo(kTextMediaInfo); + + RepresentationXmlNode representation; + ASSERT_TRUE(representation.AddVODOnlyInfo(media_info, true, 100)); + + EXPECT_THAT( + representation, + XmlNodeEqual("" + "encrypted_audio.mp4" + "" + "" + "" + "" + "" + "")); +} + } // namespace xml } // namespace shaka diff --git a/packager/mpd/public/mpd_params.h b/packager/mpd/public/mpd_params.h index 002e0fcc7e..7f2faff290 100644 --- a/packager/mpd/public/mpd_params.h +++ b/packager/mpd/public/mpd_params.h @@ -87,6 +87,10 @@ struct MpdParams { /// element alongside with /// when using PlayReady protection system. bool include_mspr_pro = true; + /// Uses SegmentList instead of SegmentBase. Use this if the + /// content is huge and the total number of (sub)segment references + /// is greater than what the sidx atom allows (65535). + bool use_segment_list = false; }; } // namespace shaka diff --git a/packager/packager.cc b/packager/packager.cc index 16c7d855a9..a81624c592 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -367,10 +367,11 @@ Status ValidateParams(const PackagingParams& packaging_params, if (on_demand_dash_profile && !packaging_params.mpd_params.mpd_output.empty() && - !packaging_params.mp4_output_params.generate_sidx_in_media_segments) { + !packaging_params.mp4_output_params.generate_sidx_in_media_segments && + !packaging_params.mpd_params.use_segment_list) { return Status(error::UNIMPLEMENTED, "--generate_sidx_in_media_segments is required for DASH " - "on-demand profile (not using segment_template)."); + "on-demand profile (not using segment_template or segment list)."); } return Status::OK; @@ -937,8 +938,9 @@ Status Packager::Initialize( } media::MuxerListenerFactory muxer_listener_factory( - packaging_params.output_media_info, internal->mpd_notifier.get(), - internal->hls_notifier.get()); + packaging_params.output_media_info, + packaging_params.mpd_params.use_segment_list, + internal->mpd_notifier.get(), internal->hls_notifier.get()); RETURN_IF_ERROR(media::CreateAllJobs( streams_for_jobs, packaging_params, internal->mpd_notifier.get(),