diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index bfd78a7245..d62eb498dd 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -151,6 +151,7 @@ class PackagerAppTest(unittest.TestCase): drm_label=None, skip_encryption=None, bandwidth=None, + split_content_on_ad_cues=False, test_file=None): """Get a stream descriptor as a string. @@ -174,6 +175,8 @@ class PackagerAppTest(unittest.TestCase): skip_encryption: If set to true, the stream will not be encrypted. bandwidth: The expected bandwidth value that should be listed in the manifest. + split_content_on_ad_cues: If set to true, the output file will be split + into multiple files, with a total of NumAdCues + 1 files. test_file: Specify the input file to use. If the input file is not specify, a default file will be used. @@ -238,7 +241,10 @@ class PackagerAppTest(unittest.TestCase): seg_template = '%s-$Number$.%s' % (output_file_path, segment_ext) stream.Append('segment_template', seg_template) else: - output_file_path += '.' + base_ext + if split_content_on_ad_cues: + output_file_path += '$Number$.' + base_ext + else: + output_file_path += '.' + base_ext stream.Append('output', output_file_path) if bandwidth: @@ -1016,6 +1022,14 @@ class PackagerFunctionalTest(PackagerAppTest): self._VerifyDecryption(self.output[0], 'bear-640x360-a-demuxed-golden.mp4') self._VerifyDecryption(self.output[1], 'bear-640x360-v-golden.mp4') + def testEncryptionAndAdCuesSplitContent(self): + self.assertPackageSuccess( + self._GetStreams( + ['audio', 'video'], hls=True, split_content_on_ad_cues=True), + self._GetFlags( + encryption=True, output_dash=True, output_hls=True, ad_cues='1.5')) + self._CheckTestResults('encryption-and-ad-cues-split-content') + def testHlsAudioVideoTextWithAdCues(self): streams = [ self._GetStream('audio', diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio.m3u8 b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio.m3u8 new file mode 100644 index 0000000000..003ff27778 --- /dev/null +++ b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio.m3u8 @@ -0,0 +1,22 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/google/shaka-packager version -- +#EXT-X-TARGETDURATION:2 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="bear-640x360-audio1.mp4",BYTERANGE="967@0" +#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity" +#EXTINF:1.022, +#EXT-X-BYTERANGE:16655@1035 +bear-640x360-audio1.mp4 +#EXTINF:0.998, +#EXT-X-BYTERANGE:16650 +bear-640x360-audio1.mp4 +#EXTINF:0.046, +#EXT-X-BYTERANGE:1014 +bear-640x360-audio1.mp4 +#EXT-X-PLACEMENT-OPPORTUNITY +#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity" +#EXTINF:0.697, +#EXT-X-BYTERANGE:9415@1011 +bear-640x360-audio2.mp4 +#EXT-X-ENDLIST diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio1.mp4 b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio1.mp4 new file mode 100644 index 0000000000..eddee2bb72 Binary files /dev/null and b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio1.mp4 differ diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio2.mp4 b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio2.mp4 new file mode 100644 index 0000000000..5dd3d06aa1 Binary files /dev/null and b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-audio2.mp4 differ diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video-iframe.m3u8 b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video-iframe.m3u8 new file mode 100644 index 0000000000..86730bc97c --- /dev/null +++ b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video-iframe.m3u8 @@ -0,0 +1,20 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/google/shaka-packager version -- +#EXT-X-TARGETDURATION:2 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-I-FRAMES-ONLY +#EXT-X-MAP:URI="bear-640x360-video1.mp4",BYTERANGE="1091@0" +#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity" +#EXTINF:1.001, +#EXT-X-BYTERANGE:15581@1147 +bear-640x360-video1.mp4 +#EXTINF:1.001, +#EXT-X-BYTERANGE:18754@100460 +bear-640x360-video1.mp4 +#EXT-X-PLACEMENT-OPPORTUNITY +#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity" +#EXTINF:0.734, +#EXT-X-BYTERANGE:20068@1135 +bear-640x360-video2.mp4 +#EXT-X-ENDLIST diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video.m3u8 b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video.m3u8 new file mode 100644 index 0000000000..9c101d35f7 --- /dev/null +++ b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video.m3u8 @@ -0,0 +1,19 @@ +#EXTM3U +#EXT-X-VERSION:6 +## Generated with https://github.com/google/shaka-packager version -- +#EXT-X-TARGETDURATION:2 +#EXT-X-PLAYLIST-TYPE:VOD +#EXT-X-MAP:URI="bear-640x360-video1.mp4",BYTERANGE="1091@0" +#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity" +#EXTINF:1.068, +#EXT-X-BYTERANGE:99313@1147 +bear-640x360-video1.mp4 +#EXTINF:1.001, +#EXT-X-BYTERANGE:122340 +bear-640x360-video1.mp4 +#EXT-X-PLACEMENT-OPPORTUNITY +#EXT-X-KEY:METHOD=SAMPLE-AES-CTR,URI="data:text/plain;base64,MTIzNDU2Nzg5MDEyMzQ1Ng==",KEYFORMAT="identity" +#EXTINF:0.801, +#EXT-X-BYTERANGE:80067@1135 +bear-640x360-video2.mp4 +#EXT-X-ENDLIST diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video1.mp4 b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video1.mp4 new file mode 100644 index 0000000000..e76c07f45b Binary files /dev/null and b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video1.mp4 differ diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video2.mp4 b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video2.mp4 new file mode 100644 index 0000000000..f4dc57cdb5 Binary files /dev/null and b/packager/app/test/testdata/encryption-and-ad-cues-split-content/bear-640x360-video2.mp4 differ diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/output.m3u8 b/packager/app/test/testdata/encryption-and-ad-cues-split-content/output.m3u8 new file mode 100644 index 0000000000..ea8da59dce --- /dev/null +++ b/packager/app/test/testdata/encryption-and-ad-cues-split-content/output.m3u8 @@ -0,0 +1,9 @@ +#EXTM3U +## Generated with https://github.com/google/shaka-packager version -- + +#EXT-X-MEDIA:TYPE=AUDIO,URI="bear-640x360-audio.m3u8",GROUP-ID="default-audio-group",NAME="stream_0",AUTOSELECT=YES,CHANNELS="2" + +#EXT-X-STREAM-INF:BANDWIDTH=1152419,CODECS="avc1.64001e,mp4a.40.2",RESOLUTION=640x360,AUDIO="default-audio-group" +bear-640x360-video.m3u8 + +#EXT-X-I-FRAME-STREAM-INF:BANDWIDTH=218704,CODECS="avc1.64001e",RESOLUTION=640x360,URI="bear-640x360-video-iframe.m3u8" diff --git a/packager/app/test/testdata/encryption-and-ad-cues-split-content/output.mpd b/packager/app/test/testdata/encryption-and-ad-cues-split-content/output.mpd new file mode 100644 index 0000000000..2fda8f0e9d --- /dev/null +++ b/packager/app/test/testdata/encryption-and-ad-cues-split-content/output.mpd @@ -0,0 +1,58 @@ + + + + + + + + AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== + + + bear-640x360-video1.mp4 + + + + + + + + + AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== + + + + bear-640x360-audio1.mp4 + + + + + + + + + + + AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== + + + bear-640x360-video2.mp4 + + + + + + + + + AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA== + + + + bear-640x360-audio2.mp4 + + + + + + + diff --git a/packager/media/base/muxer.cc b/packager/media/base/muxer.cc index dad91c97e1..cf8212d75f 100644 --- a/packager/media/base/muxer.cc +++ b/packager/media/base/muxer.cc @@ -9,15 +9,22 @@ #include #include "packager/media/base/media_sample.h" +#include "packager/media/base/muxer_util.h" +#include "packager/status_macros.h" namespace shaka { namespace media { namespace { const bool kInitialEncryptionInfo = true; +const int64_t kStartTime = 0; } // namespace -Muxer::Muxer(const MuxerOptions& options) - : options_(options), cancelled_(false), clock_(NULL) {} +Muxer::Muxer(const MuxerOptions& options) : options_(options) { + // "$" is only allowed if the output file name is a template, which is used to + // support one file per Representation per Period when there are Ad Cues. + if (options_.output_file_name.find("$") != std::string::npos) + output_file_template_ = options_.output_file_name; +} Muxer::~Muxer() {} @@ -39,16 +46,7 @@ Status Muxer::Process(std::unique_ptr stream_data) { switch (stream_data->stream_data_type) { case StreamDataType::kStreamInfo: streams_.push_back(std::move(stream_data->stream_info)); - if (muxer_listener_ && streams_.back()->is_encrypted()) { - const EncryptionConfig& encryption_config = - streams_.back()->encryption_config(); - muxer_listener_->OnEncryptionInfoReady( - kInitialEncryptionInfo, encryption_config.protection_scheme, - encryption_config.key_id, encryption_config.constant_iv, - encryption_config.key_system_info); - current_key_id_ = encryption_config.key_id; - } - return InitializeMuxer(); + return ReinitializeMuxer(kStartTime); case StreamDataType::kSegmentInfo: { const auto& segment_info = *stream_data->segment_info; if (muxer_listener_ && segment_info.is_encrypted) { @@ -80,6 +78,12 @@ Status Muxer::Process(std::unique_ptr stream_data) { static_cast(time_in_seconds * time_scale); muxer_listener_->OnCueEvent(scaled_time, stream_data->cue_event->cue_data); + + // Finalize and re-initialize Muxer to generate different content files. + if (!output_file_template_.empty()) { + RETURN_IF_ERROR(Finalize()); + RETURN_IF_ERROR(ReinitializeMuxer(scaled_time)); + } } break; default: @@ -91,5 +95,29 @@ Status Muxer::Process(std::unique_ptr stream_data) { return Status::OK; } +Status Muxer::OnFlushRequest(size_t input_stream_index) { + return Finalize(); +} + +Status Muxer::ReinitializeMuxer(int64_t timestamp) { + if (muxer_listener_ && streams_.back()->is_encrypted()) { + const EncryptionConfig& encryption_config = + streams_.back()->encryption_config(); + muxer_listener_->OnEncryptionInfoReady( + kInitialEncryptionInfo, encryption_config.protection_scheme, + encryption_config.key_id, encryption_config.constant_iv, + encryption_config.key_system_info); + current_key_id_ = encryption_config.key_id; + } + if (!output_file_template_.empty()) { + // Update |output_file_name| with an actual file name, which will be used by + // the subclasses. + options_.output_file_name = + GetSegmentName(output_file_template_, timestamp, output_file_index_++, + options_.bandwidth); + } + return InitializeMuxer(); +} + } // namespace media } // namespace shaka diff --git a/packager/media/base/muxer.h b/packager/media/base/muxer.h index f3bcc87d86..d06fd21a36 100644 --- a/packager/media/base/muxer.h +++ b/packager/media/base/muxer.h @@ -63,7 +63,7 @@ class Muxer : public MediaHandler { /// @{ Status InitializeInternal() override { return Status::OK; } Status Process(std::unique_ptr stream_data) override; - Status OnFlushRequest(size_t input_stream_index) override { return Finalize(); } + Status OnFlushRequest(size_t input_stream_index) override; /// @} const MuxerOptions& options() const { return options_; } @@ -72,6 +72,13 @@ class Muxer : public MediaHandler { base::Clock* clock() { return clock_; } private: + Muxer(const Muxer&) = delete; + Muxer& operator=(const Muxer&) = delete; + + // Re-initialize Muxer. Could be called on StreamInfo or CueEvent. + // |timestamp| may be used to set the output file name. + Status ReinitializeMuxer(int64_t timestamp); + // Initialize the muxer. virtual Status InitializeMuxer() = 0; @@ -92,14 +99,17 @@ class Muxer : public MediaHandler { std::vector> streams_; std::vector current_key_id_; bool encryption_started_ = false; - bool cancelled_; + bool cancelled_ = false; std::unique_ptr muxer_listener_; std::unique_ptr progress_listener_; // An external injected clock, can be NULL. - base::Clock* clock_; + base::Clock* clock_ = nullptr; - DISALLOW_COPY_AND_ASSIGN(Muxer); + // In VOD single segment case with Ad Cues, |output_file_name| is allowed to + // be a template. In this case, there will be NumAdCues + 1 files generated. + std::string output_file_template_; + size_t output_file_index_ = 0; }; } // namespace media diff --git a/packager/media/event/muxer_listener_internal.cc b/packager/media/event/muxer_listener_internal.cc index d824886aaf..54928b306c 100644 --- a/packager/media/event/muxer_listener_internal.cc +++ b/packager/media/event/muxer_listener_internal.cc @@ -160,6 +160,26 @@ void SetMediaInfoMuxerOptions(const MuxerOptions& muxer_options, } } +// Adjust MediaInfo for compatibility comparison. MediaInfos are considered to +// be compatible if codec and container are the same. +MediaInfo GetCompatibleComparisonMediaInfo(const MediaInfo& media_info) { + MediaInfo adjusted_media_info; + adjusted_media_info.set_reference_time_scale( + media_info.reference_time_scale()); + adjusted_media_info.set_container_type(media_info.container_type()); + if (media_info.has_video_info()) { + *adjusted_media_info.mutable_video_info() = media_info.video_info(); + adjusted_media_info.mutable_video_info()->clear_frame_duration(); + } + if (media_info.has_audio_info()) { + *adjusted_media_info.mutable_audio_info() = media_info.audio_info(); + } + if (media_info.has_text_info()) { + *adjusted_media_info.mutable_text_info() = media_info.text_info(); + } + return adjusted_media_info; +} + } // namespace bool GenerateMediaInfo(const MuxerOptions& muxer_options, @@ -181,15 +201,9 @@ bool GenerateMediaInfo(const MuxerOptions& muxer_options, bool IsMediaInfoCompatible(const MediaInfo& media_info1, const MediaInfo& media_info2) { - return media_info1.reference_time_scale() == - media_info2.reference_time_scale() && - media_info1.container_type() == media_info2.container_type() && - MessageDifferencer::Equals(media_info1.video_info(), - media_info2.video_info()) && - MessageDifferencer::Equals(media_info1.audio_info(), - media_info2.audio_info()) && - MessageDifferencer::Equals(media_info1.text_info(), - media_info2.text_info()); + return MessageDifferencer::Equals( + GetCompatibleComparisonMediaInfo(media_info1), + GetCompatibleComparisonMediaInfo(media_info2)); } bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges, diff --git a/packager/packager.cc b/packager/packager.cc index 1d3a95f8db..27d751b825 100644 --- a/packager/packager.cc +++ b/packager/packager.cc @@ -219,6 +219,13 @@ Status ValidateStreamDescriptor(bool dump_stream_info, } } + if (stream.output.find('$') != std::string::npos) { + // "$" is only allowed if the output file name is a template, which is + // used to support one file per Representation per Period when there are + // Ad Cues. + RETURN_IF_ERROR(ValidateSegmentTemplate(stream.output)); + } + // There are some specifics that must be checked based on which format // we are writing to. const MediaContainerName output_format = GetOutputFormat(stream);