[Ad Insertion] Support one file per Representation per Period

Allow the output file to contain template identifiers like $Number$,
$Time$ etc. Unlike the actual template used in SegmentTemplate, the
identifiers in output will be populated before pushing to the DASH
manifest.

Issue: #384

Change-Id: Ife1caadb6fccd32167fa1bc83fe2afcb2d2ad087
This commit is contained in:
KongQun Yang 2018-05-22 17:26:18 -07:00
parent 9feb2bf503
commit 6e4820eedc
14 changed files with 227 additions and 26 deletions

View File

@ -151,6 +151,7 @@ class PackagerAppTest(unittest.TestCase):
drm_label=None, drm_label=None,
skip_encryption=None, skip_encryption=None,
bandwidth=None, bandwidth=None,
split_content_on_ad_cues=False,
test_file=None): test_file=None):
"""Get a stream descriptor as a string. """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. skip_encryption: If set to true, the stream will not be encrypted.
bandwidth: The expected bandwidth value that should be listed in the bandwidth: The expected bandwidth value that should be listed in the
manifest. 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 test_file: Specify the input file to use. If the input file is not
specify, a default file will be used. 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) seg_template = '%s-$Number$.%s' % (output_file_path, segment_ext)
stream.Append('segment_template', seg_template) stream.Append('segment_template', seg_template)
else: 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) stream.Append('output', output_file_path)
if bandwidth: if bandwidth:
@ -1016,6 +1022,14 @@ class PackagerFunctionalTest(PackagerAppTest):
self._VerifyDecryption(self.output[0], 'bear-640x360-a-demuxed-golden.mp4') self._VerifyDecryption(self.output[0], 'bear-640x360-a-demuxed-golden.mp4')
self._VerifyDecryption(self.output[1], 'bear-640x360-v-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): def testHlsAudioVideoTextWithAdCues(self):
streams = [ streams = [
self._GetStream('audio', self._GetStream('audio',

View File

@ -0,0 +1,22 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#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

View File

@ -0,0 +1,20 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#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

View File

@ -0,0 +1,19 @@
#EXTM3U
#EXT-X-VERSION:6
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#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

View File

@ -0,0 +1,9 @@
#EXTM3U
## Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>
#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"

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--Generated with https://github.com/google/shaka-packager version <tag>-<hash>-<test>-->
<MPD xmlns="urn:mpeg:dash:schema:mpd:2011" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xlink="http://www.w3.org/1999/xlink" xsi:schemaLocation="urn:mpeg:dash:schema:mpd:2011 DASH-MPD.xsd" xmlns:cenc="urn:mpeg:cenc:2013" profiles="urn:mpeg:dash:profile:isoff-on-demand:2011" minBufferTime="PT2S" type="static" mediaPresentationDuration="PT2.8695333003997803S">
<Period id="0" duration="PT2.0687333333333333S">
<AdaptationSet id="0" contentType="video" width="640" height="360" frameRate="30000/1001" subsegmentAlignment="true" par="16:9">
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="31323334-3536-3738-3930-313233343536"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA==</cenc:pssh>
</ContentProtection>
<Representation id="0" bandwidth="977743" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1">
<BaseURL>bear-640x360-video1.mp4</BaseURL>
<SegmentBase indexRange="1091-1146" timescale="30000">
<Initialization range="0-1090"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="audio" subsegmentAlignment="true">
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="31323334-3536-3738-3930-313233343536"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA==</cenc:pssh>
</ContentProtection>
<Representation id="1" bandwidth="174678" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>bear-640x360-audio1.mp4</BaseURL>
<SegmentBase indexRange="967-1034" timescale="44100">
<Initialization range="0-966"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
<Period id="1" duration="PT.8008000000000002S">
<AdaptationSet id="0" contentType="video" width="640" height="360" frameRate="30000/1001" subsegmentAlignment="true" par="16:9">
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="31323334-3536-3738-3930-313233343536"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA==</cenc:pssh>
</ContentProtection>
<Representation id="0" bandwidth="799871" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1">
<BaseURL>bear-640x360-video2.mp4</BaseURL>
<SegmentBase indexRange="1091-1134" timescale="30000" presentationTimeOffset="60059">
<Initialization range="0-1090"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="audio" subsegmentAlignment="true">
<ContentProtection value="cenc" schemeIdUri="urn:mpeg:dash:mp4protection:2011" cenc:default_KID="31323334-3536-3738-3930-313233343536"/>
<ContentProtection schemeIdUri="urn:uuid:1077efec-c0b2-4d02-ace3-3c1e52e2fb4b">
<cenc:pssh>AAAANHBzc2gBAAAAEHfv7MCyTQKs4zweUuL7SwAAAAExMjM0NTY3ODkwMTIzNDU2AAAAAA==</cenc:pssh>
</ContentProtection>
<Representation id="1" bandwidth="108126" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>bear-640x360-audio2.mp4</BaseURL>
<SegmentBase indexRange="967-1010" timescale="44100" presentationTimeOffset="88288">
<Initialization range="0-966"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -9,15 +9,22 @@
#include <algorithm> #include <algorithm>
#include "packager/media/base/media_sample.h" #include "packager/media/base/media_sample.h"
#include "packager/media/base/muxer_util.h"
#include "packager/status_macros.h"
namespace shaka { namespace shaka {
namespace media { namespace media {
namespace { namespace {
const bool kInitialEncryptionInfo = true; const bool kInitialEncryptionInfo = true;
const int64_t kStartTime = 0;
} // namespace } // namespace
Muxer::Muxer(const MuxerOptions& options) Muxer::Muxer(const MuxerOptions& options) : options_(options) {
: options_(options), cancelled_(false), clock_(NULL) {} // "$" 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() {} Muxer::~Muxer() {}
@ -39,16 +46,7 @@ Status Muxer::Process(std::unique_ptr<StreamData> stream_data) {
switch (stream_data->stream_data_type) { switch (stream_data->stream_data_type) {
case StreamDataType::kStreamInfo: case StreamDataType::kStreamInfo:
streams_.push_back(std::move(stream_data->stream_info)); streams_.push_back(std::move(stream_data->stream_info));
if (muxer_listener_ && streams_.back()->is_encrypted()) { return ReinitializeMuxer(kStartTime);
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();
case StreamDataType::kSegmentInfo: { case StreamDataType::kSegmentInfo: {
const auto& segment_info = *stream_data->segment_info; const auto& segment_info = *stream_data->segment_info;
if (muxer_listener_ && segment_info.is_encrypted) { if (muxer_listener_ && segment_info.is_encrypted) {
@ -80,6 +78,12 @@ Status Muxer::Process(std::unique_ptr<StreamData> stream_data) {
static_cast<int64_t>(time_in_seconds * time_scale); static_cast<int64_t>(time_in_seconds * time_scale);
muxer_listener_->OnCueEvent(scaled_time, muxer_listener_->OnCueEvent(scaled_time,
stream_data->cue_event->cue_data); 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; break;
default: default:
@ -91,5 +95,29 @@ Status Muxer::Process(std::unique_ptr<StreamData> stream_data) {
return Status::OK; 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 media
} // namespace shaka } // namespace shaka

View File

@ -63,7 +63,7 @@ class Muxer : public MediaHandler {
/// @{ /// @{
Status InitializeInternal() override { return Status::OK; } Status InitializeInternal() override { return Status::OK; }
Status Process(std::unique_ptr<StreamData> stream_data) override; Status Process(std::unique_ptr<StreamData> 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_; } const MuxerOptions& options() const { return options_; }
@ -72,6 +72,13 @@ class Muxer : public MediaHandler {
base::Clock* clock() { return clock_; } base::Clock* clock() { return clock_; }
private: 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. // Initialize the muxer.
virtual Status InitializeMuxer() = 0; virtual Status InitializeMuxer() = 0;
@ -92,14 +99,17 @@ class Muxer : public MediaHandler {
std::vector<std::shared_ptr<const StreamInfo>> streams_; std::vector<std::shared_ptr<const StreamInfo>> streams_;
std::vector<uint8_t> current_key_id_; std::vector<uint8_t> current_key_id_;
bool encryption_started_ = false; bool encryption_started_ = false;
bool cancelled_; bool cancelled_ = false;
std::unique_ptr<MuxerListener> muxer_listener_; std::unique_ptr<MuxerListener> muxer_listener_;
std::unique_ptr<ProgressListener> progress_listener_; std::unique_ptr<ProgressListener> progress_listener_;
// An external injected clock, can be NULL. // 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 } // namespace media

View File

@ -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 } // namespace
bool GenerateMediaInfo(const MuxerOptions& muxer_options, bool GenerateMediaInfo(const MuxerOptions& muxer_options,
@ -181,15 +201,9 @@ bool GenerateMediaInfo(const MuxerOptions& muxer_options,
bool IsMediaInfoCompatible(const MediaInfo& media_info1, bool IsMediaInfoCompatible(const MediaInfo& media_info1,
const MediaInfo& media_info2) { const MediaInfo& media_info2) {
return media_info1.reference_time_scale() == return MessageDifferencer::Equals(
media_info2.reference_time_scale() && GetCompatibleComparisonMediaInfo(media_info1),
media_info1.container_type() == media_info2.container_type() && GetCompatibleComparisonMediaInfo(media_info2));
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());
} }
bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges, bool SetVodInformation(const MuxerListener::MediaRanges& media_ranges,

View File

@ -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 // There are some specifics that must be checked based on which format
// we are writing to. // we are writing to.
const MediaContainerName output_format = GetOutputFormat(stream); const MediaContainerName output_format = GetOutputFormat(stream);