diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index d60c2b7fc7..b25f5a4e3a 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -1236,6 +1236,15 @@ class PackagerFunctionalTest(PackagerAppTest): self.assertPackageSuccess(streams, flags) self._CheckTestResults('dolby-vision-profile-5-with-encryption') + def testDolbyVisionProfile8WithEncryption(self): + streams = [ + self._GetStream('video', test_file='sparks_dovi_8.mp4') + ] + flags = self._GetFlags(encryption=True, output_dash=True, output_hls=True) + + self.assertPackageSuccess(streams, flags) + self._CheckTestResults('dolby-vision-profile-8-with-encryption') + def testVp8Mp4WithEncryption(self): streams = [ self._GetStream('video', diff --git a/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/output.m3u8 b/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/output.m3u8 new file mode 100644 index 0000000000..a101fe73f2 --- /dev/null +++ b/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/output.m3u8 @@ -0,0 +1,7 @@ +#EXTM3U +## Generated with https://github.com/google/shaka-packager version -- + +#EXT-X-STREAM-INF:BANDWIDTH=818598,AVERAGE-BANDWIDTH=755328,CODECS="hvc1.2.4.L90.90",RESOLUTION=640x360,FRAME-RATE=59.940,VIDEO-RANGE=PQ +stream_0.m3u8 +#EXT-X-STREAM-INF:BANDWIDTH=818598,AVERAGE-BANDWIDTH=755328,CODECS="dvh1.08.01",RESOLUTION=640x360,FRAME-RATE=59.940,VIDEO-RANGE=PQ +stream_0.m3u8 diff --git a/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/output.mpd b/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/output.mpd new file mode 100644 index 0000000000..9a9057dbb3 --- /dev/null +++ b/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/output.mpd @@ -0,0 +1,30 @@ + + + + + + + + AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== + + + sparks_dovi_8-video.mp4 + + + + + + + + + AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== + + + sparks_dovi_8-video.mp4 + + + + + + + diff --git a/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/sparks_dovi_8-video.mp4 b/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/sparks_dovi_8-video.mp4 new file mode 100644 index 0000000000..9d45d17ada Binary files /dev/null and b/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/sparks_dovi_8-video.mp4 differ diff --git a/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/stream_0.m3u8 b/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/stream_0.m3u8 new file mode 100644 index 0000000000..6e64c73711 --- /dev/null +++ b/packager/app/test/testdata/dolby-vision-profile-8-with-encryption/stream_0.m3u8 @@ -0,0 +1,20 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/google/shaka-packager version -- +#EXT-X-TARGETDURATION:3 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="sparks_dovi_8-video.mp4",BYTERANGE="1355@0" +#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity" +#EXTINF:2.002, +#EXT-X-BYTERANGE:172013@1435 +sparks_dovi_8-video.mp4 +#EXTINF:2.002, +#EXT-X-BYTERANGE:189474 +sparks_dovi_8-video.mp4 +#EXTINF:2.002, +#EXT-X-BYTERANGE:204854 +sparks_dovi_8-video.mp4 +#EXTINF:0.017, +#EXT-X-BYTERANGE:2296 +sparks_dovi_8-video.mp4 +#EXT-X-ENDLIST diff --git a/packager/media/event/combined_muxer_listener.h b/packager/media/event/combined_muxer_listener.h index a1a158a4c0..2528974ae7 100644 --- a/packager/media/event/combined_muxer_listener.h +++ b/packager/media/event/combined_muxer_listener.h @@ -7,20 +7,24 @@ #ifndef PACKAGER_MEDIA_EVENT_COMBINED_MUXER_LISTENER_H_ #define PACKAGER_MEDIA_EVENT_COMBINED_MUXER_LISTENER_H_ -#include #include +#include #include "packager/media/event/muxer_listener.h" namespace shaka { namespace media { +/// This class supports a group of MuxerListeners. All events are forwarded to +/// every individual MuxerListeners contained in this CombinedMuxerListener. class CombinedMuxerListener : public MuxerListener { public: CombinedMuxerListener() = default; void AddListener(std::unique_ptr listener); + /// @name MuxerListener implementation overrides. + /// @{ void OnEncryptionInfoReady(bool is_initial_encryption_info, FourCC protection_scheme, const std::vector& key_id, @@ -41,11 +45,29 @@ class CombinedMuxerListener : public MuxerListener { uint64_t segment_file_size) override; void OnKeyFrame(int64_t timestamp, uint64_t start_byte_offset, uint64_t size); void OnCueEvent(int64_t timestamp, const std::string& cue_data) override; + /// @} + + protected: + /// Limit the number of children MuxerListeners. It can only be used to reduce + /// the number of children. + /// @num is the number to set to. It is a no-op if `num` is equal or greater + /// than the existing number of children MuxerListeners. + void LimitNumOfMuxerListners(size_t num) { + if (num < muxer_listeners_.size()) + muxer_listeners_.resize(num); + } + /// @return MuxerListener at the specified index or nullptr if the index is + /// out of range. + MuxerListener* MuxerListenerAt(size_t index) { + return (index < muxer_listeners_.size()) ? muxer_listeners_[index].get() + : nullptr; + } private: - std::list> muxer_listeners_; + CombinedMuxerListener(const CombinedMuxerListener&) = delete; + CombinedMuxerListener& operator=(const CombinedMuxerListener&) = delete; - DISALLOW_COPY_AND_ASSIGN(CombinedMuxerListener); + std::vector> muxer_listeners_; }; } // namespace media diff --git a/packager/media/event/media_event.gyp b/packager/media/event/media_event.gyp index 5206c82ab4..62911b165c 100644 --- a/packager/media/event/media_event.gyp +++ b/packager/media/event/media_event.gyp @@ -20,6 +20,8 @@ 'hls_notify_muxer_listener.h', 'mpd_notify_muxer_listener.cc', 'mpd_notify_muxer_listener.h', + 'multi_codec_muxer_listener.cc', + 'multi_codec_muxer_listener.h', 'muxer_listener.h', 'muxer_listener_factory.cc', 'muxer_listener_factory.h', @@ -56,6 +58,7 @@ 'hls_notify_muxer_listener_unittest.cc', 'muxer_listener_internal_unittest.cc', 'mpd_notify_muxer_listener_unittest.cc', + 'multi_codec_muxer_listener_unittest.cc', 'muxer_listener_test_helper.cc', 'muxer_listener_test_helper.h', 'vod_media_info_dump_muxer_listener_unittest.cc', @@ -70,6 +73,7 @@ '../../third_party/protobuf/protobuf.gyp:protobuf_full_do_not_use', '../test/media_test.gyp:run_tests_with_atexit_manager', 'media_event', + 'mock_muxer_listener', ], }, ], diff --git a/packager/media/event/multi_codec_muxer_listener.cc b/packager/media/event/multi_codec_muxer_listener.cc new file mode 100644 index 0000000000..d9ec367099 --- /dev/null +++ b/packager/media/event/multi_codec_muxer_listener.cc @@ -0,0 +1,39 @@ +// Copyright 2019 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/event/multi_codec_muxer_listener.h" + +#include "packager/base/logging.h" +#include "packager/base/strings/string_split.h" +#include "packager/media/base/stream_info.h" + +namespace shaka { +namespace media { + +void MultiCodecMuxerListener::OnMediaStart(const MuxerOptions& muxer_options, + const StreamInfo& stream_info, + uint32_t time_scale, + ContainerType container_type) { + size_t num_codecs = 0; + for (const std::string& codec_string : + base::SplitString(stream_info.codec_string(), ";", base::KEEP_WHITESPACE, + base::SPLIT_WANT_NONEMPTY)) { + std::unique_ptr current_stream_info = stream_info.Clone(); + current_stream_info->set_codec_string(codec_string); + MuxerListener* current_muxer_listener = MuxerListenerAt(num_codecs++); + if (!current_muxer_listener) { + LOG(WARNING) << "'" << codec_string << "' is not handled."; + continue; + } + current_muxer_listener->OnMediaStart(muxer_options, *current_stream_info, + time_scale, container_type); + } + // We only need |num_codecs| MuxerListeners. + LimitNumOfMuxerListners(num_codecs); +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/event/multi_codec_muxer_listener.h b/packager/media/event/multi_codec_muxer_listener.h new file mode 100644 index 0000000000..1877f15f24 --- /dev/null +++ b/packager/media/event/multi_codec_muxer_listener.h @@ -0,0 +1,42 @@ +// Copyright 2019 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#ifndef PACKAGER_MEDIA_EVENT_MULTI_CODEC_MUXER_LISTENER_H_ +#define PACKAGER_MEDIA_EVENT_MULTI_CODEC_MUXER_LISTENER_H_ + +#include "packager/media/event/combined_muxer_listener.h" + +namespace shaka { +namespace media { + +/// MultiCodecMuxerListener is a variant of CombinedMuxerListener. It is +/// designed to handle the case that a stream can be signalled in multiple +/// different codecs. Like a normal CombinedMuxerListener, it contains multiple +/// child MuxerListeners, with one child per codec. If there are more child +/// MuxerListeners than the number of codecs, the extra child MuxerListeners are +/// removed; on the other hand, if there are more codecs than the number of +/// child MuxerListeners, the extra codecs are not handled. +class MultiCodecMuxerListener : public CombinedMuxerListener { + public: + MultiCodecMuxerListener() = default; + + /// @name CombinedMuxerListener implementation overrides. + /// @{ + void OnMediaStart(const MuxerOptions& muxer_options, + const StreamInfo& stream_info, + uint32_t time_scale, + ContainerType container_type) override; + /// @} + + private: + MultiCodecMuxerListener(const MultiCodecMuxerListener&) = delete; + MultiCodecMuxerListener& operator=(const MultiCodecMuxerListener&) = delete; +}; + +} // namespace media +} // namespace shaka + +#endif // PACKAGER_MEDIA_EVENT_MULTI_CODEC_MUXER_LISTENER_H_ diff --git a/packager/media/event/multi_codec_muxer_listener_unittest.cc b/packager/media/event/multi_codec_muxer_listener_unittest.cc new file mode 100644 index 0000000000..acce6036b9 --- /dev/null +++ b/packager/media/event/multi_codec_muxer_listener_unittest.cc @@ -0,0 +1,127 @@ +// Copyright 2019 Google LLC. All rights reserved. +// +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file or at +// https://developers.google.com/open-source/licenses/bsd + +#include "packager/media/event/multi_codec_muxer_listener.h" + +#include +#include + +#include "packager/media/base/muxer_options.h" +#include "packager/media/event/mock_muxer_listener.h" +#include "packager/media/event/muxer_listener_test_helper.h" + +namespace shaka { +namespace media { + +using ::testing::_; +using ::testing::Property; +using ::testing::StrEq; +using ::testing::StrictMock; + +namespace { + +const uint64_t kSegmentStartTime = 19283; +const uint64_t kSegmentDuration = 98028; +const uint64_t kSegmentSize = 756739; +const uint32_t kTimescale = 90000; +MuxerListener::ContainerType kContainer = MuxerListener::kContainerMpeg2ts; + +} // namespace + +class MultiCodecMuxerListenerTest : public ::testing::Test { + protected: + MultiCodecMuxerListenerTest() { + std::unique_ptr> listener_1( + new StrictMock); + listener_for_first_codec_ = listener_1.get(); + std::unique_ptr> listener_2( + new StrictMock); + listener_for_second_codec_ = listener_2.get(); + multi_codec_listener_.AddListener(std::move(listener_1)); + multi_codec_listener_.AddListener(std::move(listener_2)); + + muxer_options_.segment_template = "$Number$.ts"; + + VideoStreamInfoParameters video_params = GetDefaultVideoStreamInfoParams(); + video_stream_info_ = CreateVideoStreamInfo(video_params); + } + + MultiCodecMuxerListener multi_codec_listener_; + StrictMock* listener_for_first_codec_; + StrictMock* listener_for_second_codec_; + MuxerOptions muxer_options_; + std::shared_ptr video_stream_info_; +}; + +TEST_F(MultiCodecMuxerListenerTest, OnMediaStartSingleCodec) { + video_stream_info_->set_codec_string("codec_1"); + + EXPECT_CALL( + *listener_for_first_codec_, + OnMediaStart(_, Property(&StreamInfo::codec_string, StrEq("codec_1")), + kTimescale, kContainer)) + .Times(1); + + multi_codec_listener_.OnMediaStart(muxer_options_, *video_stream_info_, + kTimescale, kContainer); +} + +TEST_F(MultiCodecMuxerListenerTest, OnNewSegmentAfterOnMediaStartSingleCodec) { + video_stream_info_->set_codec_string("codec_1"); + + EXPECT_CALL(*listener_for_first_codec_, OnMediaStart(_, _, _, _)).Times(1); + + multi_codec_listener_.OnMediaStart(muxer_options_, *video_stream_info_, + kTimescale, kContainer); + + EXPECT_CALL(*listener_for_first_codec_, + OnNewSegment(StrEq("new_segment_name10.ts"), kSegmentStartTime, + kSegmentDuration, kSegmentSize)); + + multi_codec_listener_.OnNewSegment("new_segment_name10.ts", kSegmentStartTime, + kSegmentDuration, kSegmentSize); +} + +TEST_F(MultiCodecMuxerListenerTest, OnMediaStartTwoCodecs) { + video_stream_info_->set_codec_string("codec_1;codec_2"); + + EXPECT_CALL( + *listener_for_first_codec_, + OnMediaStart(_, Property(&StreamInfo::codec_string, StrEq("codec_1")), + kTimescale, kContainer)) + .Times(1); + EXPECT_CALL( + *listener_for_second_codec_, + OnMediaStart(_, Property(&StreamInfo::codec_string, StrEq("codec_2")), + kTimescale, kContainer)) + .Times(1); + + multi_codec_listener_.OnMediaStart(muxer_options_, *video_stream_info_, + kTimescale, kContainer); +} + +TEST_F(MultiCodecMuxerListenerTest, OnNewSegmentAfterOnMediaStartTwoCodecs) { + video_stream_info_->set_codec_string("codec_1;codec_2"); + + EXPECT_CALL(*listener_for_first_codec_, OnMediaStart(_, _, _, _)).Times(1); + EXPECT_CALL(*listener_for_second_codec_, OnMediaStart(_, _, _, _)).Times(1); + + multi_codec_listener_.OnMediaStart(muxer_options_, *video_stream_info_, + kTimescale, kContainer); + + EXPECT_CALL(*listener_for_first_codec_, + OnNewSegment(StrEq("new_segment_name10.ts"), kSegmentStartTime, + kSegmentDuration, kSegmentSize)); + EXPECT_CALL(*listener_for_second_codec_, + OnNewSegment(StrEq("new_segment_name10.ts"), kSegmentStartTime, + kSegmentDuration, kSegmentSize)); + + multi_codec_listener_.OnNewSegment("new_segment_name10.ts", kSegmentStartTime, + kSegmentDuration, kSegmentSize); +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/event/muxer_listener_factory.cc b/packager/media/event/muxer_listener_factory.cc index 2442998feb..add40a0038 100644 --- a/packager/media/event/muxer_listener_factory.cc +++ b/packager/media/event/muxer_listener_factory.cc @@ -6,12 +6,15 @@ #include "packager/media/event/muxer_listener_factory.h" +#include + #include "packager/base/memory/ptr_util.h" #include "packager/base/strings/stringprintf.h" #include "packager/hls/base/hls_notifier.h" #include "packager/media/event/combined_muxer_listener.h" #include "packager/media/event/hls_notify_muxer_listener.h" #include "packager/media/event/mpd_notify_muxer_listener.h" +#include "packager/media/event/multi_codec_muxer_listener.h" #include "packager/media/event/muxer_listener.h" #include "packager/media/event/vod_media_info_dump_muxer_listener.h" #include "packager/mpd/base/mpd_notifier.h" @@ -87,25 +90,36 @@ std::unique_ptr MuxerListenerFactory::CreateListener( const StreamData& stream) { const int stream_index = stream_index_++; - std::unique_ptr combined_listener( - new CombinedMuxerListener); - - if (output_media_info_) { - combined_listener->AddListener( - CreateMediaInfoDumpListenerInternal(stream.media_info_output)); - } - if (mpd_notifier_) { - combined_listener->AddListener( - CreateMpdListenerInternal(stream, mpd_notifier_)); - } - if (hls_notifier_) { - for (auto& listener : - CreateHlsListenersInternal(stream, stream_index, hls_notifier_)) { - combined_listener->AddListener(std::move(listener)); + // Use a MultiCodecMuxerListener to handle possible DolbyVision profile 8 + // stream which can be signalled as two different codecs. + std::unique_ptr multi_codec_listener( + new MultiCodecMuxerListener); + // Creates two child MuxerListeners. Both are used if the stream is a + // multi-codec stream (e.g. DolbyVision proifile 8); otherwise the second + // child is ignored. Right now the only use case is DolbyVision profile 8 + // which contains two codecs. + for (int i = 0; i < 2; i++) { + std::unique_ptr combined_listener( + new CombinedMuxerListener); + if (output_media_info_) { + combined_listener->AddListener( + CreateMediaInfoDumpListenerInternal(stream.media_info_output)); } + if (mpd_notifier_) { + combined_listener->AddListener( + CreateMpdListenerInternal(stream, mpd_notifier_)); + } + if (hls_notifier_) { + for (auto& listener : + CreateHlsListenersInternal(stream, stream_index, hls_notifier_)) { + combined_listener->AddListener(std::move(listener)); + } + } + + multi_codec_listener->AddListener(std::move(combined_listener)); } - return std::move(combined_listener); + return std::move(multi_codec_listener); } std::unique_ptr MuxerListenerFactory::CreateHlsListener( diff --git a/packager/media/formats/mp4/mp4_media_parser.cc b/packager/media/formats/mp4/mp4_media_parser.cc index 4480f4d112..0ddac82c05 100644 --- a/packager/media/formats/mp4/mp4_media_parser.cc +++ b/packager/media/formats/mp4/mp4_media_parser.cc @@ -139,12 +139,26 @@ bool UpdateCodecStringForDolbyVision( "configuration record."; return false; } - if (actual_format == FOURCC_dvh1 || actual_format == FOURCC_dvhe) { - // Non-Backward compatibility mode. Replace the code string with - // Dolby Vision only. - *codec_string = dovi_config.GetCodecString(actual_format); - } else { - // TODO(kqyang): Support backward compatible signaling. + switch (actual_format) { + case FOURCC_dvh1: + case FOURCC_dvhe: + // Non-Backward compatibility mode. Replace the code string with + // Dolby Vision only. + *codec_string = dovi_config.GetCodecString(actual_format); + break; + case FOURCC_hev1: + // Backward compatibility mode. Two codecs are signalled: base codec + // without Dolby Vision and HDR with Dolby Vision. + *codec_string += ";" + dovi_config.GetCodecString(FOURCC_dvhe); + break; + case FOURCC_hvc1: + // See above. + *codec_string += ";" + dovi_config.GetCodecString(FOURCC_dvh1); + break; + default: + LOG(ERROR) << "Unsupported format with extra codec " + << FourCCToString(actual_format); + return false; } return true; } @@ -629,6 +643,7 @@ bool MP4MediaParser::ParseMoov(BoxReader* reader) { transfer_characteristics = hevc_config.transfer_characteristics(); if (!entry.extra_codec_configs.empty()) { + // |extra_codec_configs| is present only for Dolby Vision. if (!UpdateCodecStringForDolbyVision( actual_format, entry.extra_codec_configs, &codec_string)) { return false; diff --git a/packager/media/test/data/README b/packager/media/test/data/README index bb29e4f4f5..a113480e24 100644 --- a/packager/media/test/data/README +++ b/packager/media/test/data/README @@ -111,3 +111,4 @@ sintel-1024x436.mp4 - First 6 seconds of Sintel stream. // First 6 seconds of Sparks, generated from Netflix open content: // http://download.opencontent.netflix.com/?prefix=TechblogAssets/Sparks/Sparks_DolbyVision_P3D65_PQ_5994fps_4096x2160_LtRt_IMF_20170214/ sparks_dovi_5.mp4 - Dolby Vision profile 5 +sparks_dovi_8.mp4 - Dolby Vision profile 8 diff --git a/packager/media/test/data/sparks_dovi_8.mp4 b/packager/media/test/data/sparks_dovi_8.mp4 new file mode 100644 index 0000000000..9956bc69ff Binary files /dev/null and b/packager/media/test/data/sparks_dovi_8.mp4 differ