Integrate trick play.

- Add trick play adaptation in mpd. This addresses #178.

Change-Id: Id6cc08c788eda074c261d0274768051837b84541
This commit is contained in:
Haoming Chen 2017-03-21 16:14:46 -07:00
parent 1e2da22c88
commit 4ba5bec660
36 changed files with 875 additions and 121 deletions

View File

@ -43,6 +43,7 @@
#include "packager/media/formats/mp2t/ts_muxer.h"
#include "packager/media/formats/mp4/mp4_muxer.h"
#include "packager/media/formats/webm/webm_muxer.h"
#include "packager/media/trick_play/trick_play_handler.h"
#include "packager/mpd/base/dash_iop_mpd_notifier.h"
#include "packager/mpd/base/media_info.pb.h"
#include "packager/mpd/base/mpd_builder.h"
@ -234,7 +235,10 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
DCHECK(!(mpd_notifier && hls_notifier));
DCHECK(remux_jobs);
std::shared_ptr<TrickPlayHandler> trick_play_handler;
std::string previous_input;
std::string previous_stream_selector;
int stream_number = 0;
for (StreamDescriptorList::const_iterator
stream_iter = stream_descriptors.begin();
@ -294,6 +298,7 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
demuxer->SetKeySource(std::move(decryption_key_source));
}
remux_jobs->emplace_back(new RemuxJob(std::move(demuxer)));
trick_play_handler.reset();
previous_input = stream_iter->input;
// Skip setting up muxers if output is not needed.
if (stream_iter->output.empty() && stream_iter->segment_template.empty())
@ -301,6 +306,15 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
}
DCHECK(!remux_jobs->empty());
// Each stream selector requires an individual trick play handler.
// E.g., an input with two video streams needs two trick play handlers.
// TODO(hmchen): add a test case in packager_test.py for two video streams
// input.
if (stream_iter->stream_selector != previous_stream_selector) {
previous_stream_selector = stream_iter->stream_selector;
trick_play_handler.reset();
}
std::shared_ptr<Muxer> muxer(
CreateOutputMuxer(stream_muxer_options, stream_iter->output_format));
if (FLAGS_use_fake_clock_for_muxer) muxer->set_clock(fake_clock);
@ -341,8 +355,29 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
if (muxer_listener)
muxer->SetMuxerListener(std::move(muxer_listener));
Status status;
// Create a new trick_play_handler. Note that the stream_decriptors
// are sorted so that for the same input and stream_selector, the main
// stream is always the last one following the trick play streams.
if (stream_iter->trick_play_rate > 0) {
if (!trick_play_handler) {
trick_play_handler.reset(new TrickPlayHandler());
}
trick_play_handler->SetHandlerForTrickPlay(stream_iter->trick_play_rate,
std::move(muxer));
if (trick_play_handler->IsConnected())
continue;
} else if (trick_play_handler) {
trick_play_handler->SetHandlerForMainStream(std::move(muxer));
DCHECK(trick_play_handler->IsConnected());
continue;
}
std::vector<std::shared_ptr<MediaHandler>> handlers;
auto chunking_handler = std::make_shared<ChunkingHandler>(chunking_options);
handlers.push_back(chunking_handler);
Status status;
if (encryption_key_source) {
auto new_encryption_options = encryption_options;
// Use Sample AES in MPEG2TS.
@ -353,18 +388,22 @@ bool CreateRemuxJobs(const StreamDescriptorList& stream_descriptors,
new_encryption_options.protection_scheme =
kAppleSampleAesProtectionScheme;
}
auto encryption_handler = std::make_shared<EncryptionHandler>(
new_encryption_options, encryption_key_source);
status.Update(encryption_handler->SetHandler(0, std::move(muxer)));
status.Update(
chunking_handler->SetHandler(0, std::move(encryption_handler)));
handlers.emplace_back(
new EncryptionHandler(new_encryption_options, encryption_key_source));
}
// If trick_play_handler is available, muxer should already be connected to
// trick_play_handler.
if (trick_play_handler) {
handlers.push_back(trick_play_handler);
} else {
status.Update(chunking_handler->SetHandler(0, std::move(muxer)));
handlers.push_back(std::move(muxer));
}
auto* demuxer = remux_jobs->back()->demuxer();
const std::string& stream_selector = stream_iter->stream_selector;
status.Update(demuxer->SetHandler(stream_selector, chunking_handler));
status.Update(ConnectHandlers(handlers));
if (!status.ok()) {
LOG(ERROR) << "Failed to setup graph: " << status;

View File

@ -11,16 +11,18 @@
#include "packager/app/crypto_flags.h"
#include "packager/app/fixed_key_encryption_flags.h"
#include "packager/app/playready_key_encryption_flags.h"
#include "packager/app/mpd_flags.h"
#include "packager/app/muxer_flags.h"
#include "packager/app/playready_key_encryption_flags.h"
#include "packager/app/widevine_encryption_flags.h"
#include "packager/base/logging.h"
#include "packager/base/strings/string_number_conversions.h"
#include "packager/media/base/fixed_key_source.h"
#include "packager/media/base/media_handler.h"
#include "packager/media/base/muxer_options.h"
#include "packager/media/base/playready_key_source.h"
#include "packager/media/base/request_signer.h"
#include "packager/media/base/status.h"
#include "packager/media/base/widevine_key_source.h"
#include "packager/media/chunking/chunking_handler.h"
#include "packager/media/crypto/encryption_handler.h"
@ -222,5 +224,14 @@ MpdOptions GetMpdOptions(bool on_demand_profile) {
return mpd_options;
}
Status ConnectHandlers(std::vector<std::shared_ptr<MediaHandler>>& handlers) {
size_t num_handlers = handlers.size();
Status status;
for (size_t i = 1; i < num_handlers; ++i) {
status.Update(handlers[i - 1]->AddHandler(handlers[i]));
}
return status;
}
} // namespace media
} // namespace shaka

View File

@ -21,7 +21,9 @@ struct MpdOptions;
namespace media {
class MediaHandler;
class KeySource;
class Status;
struct ChunkingOptions;
struct EncryptionOptions;
struct MuxerOptions;
@ -50,6 +52,12 @@ MuxerOptions GetMuxerOptions();
/// @return MpdOptions from provided command line options.
MpdOptions GetMpdOptions(bool on_demand_profile);
/// Connect handlers in the vector.
/// @param handlers A vector of media handlers to be conncected. the handlers
/// are chained from front() to back().
/// @return OK on success.
Status ConnectHandlers(std::vector<std::shared_ptr<MediaHandler>>& handlers);
} // namespace media
} // namespace shaka

View File

@ -71,8 +71,7 @@ FieldType GetFieldType(const std::string& field_name) {
} // anonymous namespace
StreamDescriptor::StreamDescriptor()
: bandwidth(0), output_format(CONTAINER_UNKNOWN) {}
StreamDescriptor::StreamDescriptor() {}
StreamDescriptor::~StreamDescriptor() {}
@ -144,6 +143,20 @@ bool InsertStreamDescriptor(const std::string& descriptor_string,
descriptor.hls_playlist_name = iter->second;
break;
}
case kTrickPlayRateField: {
unsigned rate;
if (!base::StringToUint(iter->second, &rate)) {
LOG(ERROR) << "Non-numeric trick play rate " << iter->second
<< " specified.";
return false;
}
if (rate == 0) {
LOG(ERROR) << "Stream trick_play_rate should be > 0.";
return false;
}
descriptor.trick_play_rate = rate;
break;
}
default:
LOG(ERROR) << "Unknown field in stream descriptor (\"" << iter->first
<< "\").";

View File

@ -27,18 +27,26 @@ struct StreamDescriptor {
std::string input;
std::string output;
std::string segment_template;
uint32_t bandwidth;
uint32_t bandwidth = 0;
std::string language;
MediaContainerName output_format;
MediaContainerName output_format = CONTAINER_UNKNOWN;
std::string hls_name;
std::string hls_group_id;
std::string hls_playlist_name;
int16_t trick_play_rate;
uint32_t trick_play_rate = 0;
};
class StreamDescriptorCompareFn {
public:
bool operator()(const StreamDescriptor& a, const StreamDescriptor& b) {
if (a.input == b.input) {
if (a.stream_selector == b.stream_selector)
// Stream with high trick_play_rate is at the beginning.
return a.trick_play_rate > b.trick_play_rate;
else
return a.stream_selector < b.stream_selector;
}
return a.input < b.input;
}
};

View File

@ -101,6 +101,41 @@ class PackagerAppTest(unittest.TestCase):
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
self._DiffGold(self.mpd_output, 'bear-640x360-av-golden.mpd')
def testPackageAudioVideoWithTrickPlay(self):
self.packager.Package(
self._GetStreams(['audio', 'video', 'video,trick_play_rate=1']),
self._GetFlags())
self._DiffGold(self.output[0], 'bear-640x360-a-golden.mp4')
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
self._DiffGold(self.output[2], 'bear-640x360-v-trick-1-golden.mp4')
self._DiffGold(self.mpd_output, 'bear-640x360-av-trick-1-golden.mpd')
def testPackageAudioVideoWithTwoTrickPlay(self):
self.packager.Package(
self._GetStreams(['audio', 'video', 'video,trick_play_rate=1',
'video,trick_play_rate=2']),
self._GetFlags())
self._DiffGold(self.output[0], 'bear-640x360-a-golden.mp4')
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
self._DiffGold(self.output[2], 'bear-640x360-v-trick-1-golden.mp4')
self._DiffGold(self.output[3], 'bear-640x360-v-trick-2-golden.mp4')
self._DiffGold(self.mpd_output,
'bear-640x360-av-trick-1-trick-2-golden.mpd')
def testPackageAudioVideoWithTwoTrickPlayDecreasingRate(self):
self.packager.Package(
self._GetStreams(['audio', 'video', 'video,trick_play_rate=2',
'video,trick_play_rate=1']),
self._GetFlags())
self._DiffGold(self.output[0], 'bear-640x360-a-golden.mp4')
self._DiffGold(self.output[1], 'bear-640x360-v-golden.mp4')
self._DiffGold(self.output[2], 'bear-640x360-v-trick-2-golden.mp4')
self._DiffGold(self.output[3], 'bear-640x360-v-trick-1-golden.mp4')
# Since the stream descriptors are sorted in packager app, a different
# order of trick play rates gets the same mpd.
self._DiffGold(self.mpd_output,
'bear-640x360-av-trick-1-trick-2-golden.mpd')
def testPackageAudioVideoWithLanguageOverride(self):
self.packager.Package(
self._GetStreams(['audio', 'video'], language_override='por-BR'),
@ -205,6 +240,36 @@ class PackagerAppTest(unittest.TestCase):
self._VerifyDecryption(self.output[0], 'bear-640x360-a-golden.mp4')
self._VerifyDecryption(self.output[1], 'bear-640x360-v-golden.mp4')
def testPackageWithEncryptionAndTrickPlay(self):
self.packager.Package(
self._GetStreams(['audio', 'video', 'video,trick_play_rate=1']),
self._GetFlags(encryption=True))
self._DiffGold(self.output[0], 'bear-640x360-a-cenc-golden.mp4')
self._DiffGold(self.output[1], 'bear-640x360-v-cenc-golden.mp4')
self._DiffGold(self.output[2], 'bear-640x360-v-trick-1-cenc-golden.mp4')
self._DiffGold(self.mpd_output, 'bear-640x360-av-trick-1-cenc-golden.mpd')
self._VerifyDecryption(self.output[0], 'bear-640x360-a-golden.mp4')
self._VerifyDecryption(self.output[1], 'bear-640x360-v-golden.mp4')
self._VerifyDecryption(self.output[2], 'bear-640x360-v-trick-1-golden.mp4')
# TODO(hmchen): Add a test case that SD and HD AdapatationSet share one trick
# play stream.
def testPackageWithEncryptionAndTwoTrickPlays(self):
self.packager.Package(
self._GetStreams(['audio', 'video', 'video,trick_play_rate=1',
'video,trick_play_rate=2']),
self._GetFlags(encryption=True))
self._DiffGold(self.output[0], 'bear-640x360-a-cenc-golden.mp4')
self._DiffGold(self.output[1], 'bear-640x360-v-cenc-golden.mp4')
self._DiffGold(self.output[2], 'bear-640x360-v-trick-1-cenc-golden.mp4')
self._DiffGold(self.output[3], 'bear-640x360-v-trick-2-cenc-golden.mp4')
self._DiffGold(self.mpd_output,
'bear-640x360-av-trick-1-trick-2-cenc-golden.mpd')
self._VerifyDecryption(self.output[0], 'bear-640x360-a-golden.mp4')
self._VerifyDecryption(self.output[1], 'bear-640x360-v-golden.mp4')
self._VerifyDecryption(self.output[2], 'bear-640x360-v-trick-1-golden.mp4')
self._VerifyDecryption(self.output[3], 'bear-640x360-v-trick-2-golden.mp4')
def testPackageWithEncryptionAndNoPsshInStream(self):
self.packager.Package(
self._GetStreams(['audio', 'video']),
@ -574,6 +639,11 @@ class PackagerAppTest(unittest.TestCase):
else:
output_prefix = '%s_%d_%s' % (self.output_prefix, test_file_index,
stream_descriptor)
# Replace ',', '=' with '_' to make it more like a filename, also
# avoid potential illegal charactors for a filename.
for ch in [',', '=']:
output_prefix = output_prefix.replace(ch, '_')
if live:
if output_format == 'ts':
stream = ('input=%s,stream=%s,segment_template=%s-$Number$.ts,'

View File

@ -0,0 +1,44 @@
<?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.76317S">
<Period id="0">
<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="885590" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1">
<BaseURL>output_video.mp4</BaseURL>
<SegmentBase indexRange="1091-1158" timescale="30000">
<Initialization range="0-1090"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="video" width="640" height="360" frameRate="30000/30030" 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>
<EssentialProperty schemeIdUri="http://dashif.org/guidelines/trickmode" value="0"/>
<Representation id="1" bandwidth="157348" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" maxPlayoutRate="30" codingDependency="false">
<BaseURL>output_video_trick_play_rate_1.mp4</BaseURL>
<SegmentBase indexRange="1091-1158" timescale="30000">
<Initialization range="0-1090"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" 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="2" bandwidth="129162" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>output_audio.mp4</BaseURL>
<SegmentBase indexRange="967-1034" timescale="44100">
<Initialization range="0-966"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -0,0 +1,32 @@
<?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.76317S">
<Period id="0">
<AdaptationSet id="0" contentType="video" width="640" height="360" frameRate="30000/1001" subsegmentAlignment="true" par="16:9">
<Representation id="0" bandwidth="882064" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1">
<BaseURL>output_video.mp4</BaseURL>
<SegmentBase indexRange="823-890" timescale="30000">
<Initialization range="0-822"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="video" width="640" height="360" frameRate="30000/30030" subsegmentAlignment="true" par="16:9">
<EssentialProperty schemeIdUri="http://dashif.org/guidelines/trickmode" value="0"/>
<Representation id="1" bandwidth="156160" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" maxPlayoutRate="30" codingDependency="false">
<BaseURL>output_video_trick_play_rate_1.mp4</BaseURL>
<SegmentBase indexRange="823-890" timescale="30000">
<Initialization range="0-822"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="audio" subsegmentAlignment="true">
<Representation id="2" bandwidth="126510" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>output_audio.mp4</BaseURL>
<SegmentBase indexRange="757-824" timescale="44100">
<Initialization range="0-756"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -0,0 +1,50 @@
<?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.76317S">
<Period id="0">
<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="885590" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1">
<BaseURL>output_video.mp4</BaseURL>
<SegmentBase indexRange="1091-1158" timescale="30000">
<Initialization range="0-1090"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="video" width="640" height="360" maxFrameRate="30000/30030" 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>
<EssentialProperty schemeIdUri="http://dashif.org/guidelines/trickmode" value="0"/>
<Representation id="1" bandwidth="104852" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" frameRate="30000/60060" maxPlayoutRate="60" codingDependency="false">
<BaseURL>output_video_trick_play_rate_2.mp4</BaseURL>
<SegmentBase indexRange="1091-1146" timescale="30000">
<Initialization range="0-1090"/>
</SegmentBase>
</Representation>
<Representation id="2" bandwidth="157348" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" frameRate="30000/30030" maxPlayoutRate="30" codingDependency="false">
<BaseURL>output_video_trick_play_rate_1.mp4</BaseURL>
<SegmentBase indexRange="1091-1158" timescale="30000">
<Initialization range="0-1090"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" 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="3" bandwidth="129162" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>output_audio.mp4</BaseURL>
<SegmentBase indexRange="967-1034" timescale="44100">
<Initialization range="0-966"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

View File

@ -0,0 +1,38 @@
<?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.76317S">
<Period id="0">
<AdaptationSet id="0" contentType="video" width="640" height="360" frameRate="30000/1001" subsegmentAlignment="true" par="16:9">
<Representation id="0" bandwidth="882064" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1">
<BaseURL>output_video.mp4</BaseURL>
<SegmentBase indexRange="823-890" timescale="30000">
<Initialization range="0-822"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="1" contentType="video" width="640" height="360" maxFrameRate="30000/30030" par="16:9">
<EssentialProperty schemeIdUri="http://dashif.org/guidelines/trickmode" value="0"/>
<Representation id="1" bandwidth="103866" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" frameRate="30000/60060" maxPlayoutRate="60" codingDependency="false">
<BaseURL>output_video_trick_play_rate_2.mp4</BaseURL>
<SegmentBase indexRange="823-878" timescale="30000">
<Initialization range="0-822"/>
</SegmentBase>
</Representation>
<Representation id="2" bandwidth="156160" codecs="avc1.64001e" mimeType="video/mp4" sar="1:1" frameRate="30000/30030" maxPlayoutRate="30" codingDependency="false">
<BaseURL>output_video_trick_play_rate_1.mp4</BaseURL>
<SegmentBase indexRange="823-890" timescale="30000">
<Initialization range="0-822"/>
</SegmentBase>
</Representation>
</AdaptationSet>
<AdaptationSet id="2" contentType="audio" subsegmentAlignment="true">
<Representation id="3" bandwidth="126510" codecs="mp4a.40.2" mimeType="audio/mp4" audioSamplingRate="44100">
<AudioChannelConfiguration schemeIdUri="urn:mpeg:dash:23003:3:audio_channel_configuration:2011" value="2"/>
<BaseURL>output_audio.mp4</BaseURL>
<SegmentBase indexRange="757-824" timescale="44100">
<Initialization range="0-756"/>
</SegmentBase>
</Representation>
</AdaptationSet>
</Period>
</MPD>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -69,5 +69,15 @@ Status MediaHandler::FlushDownstream(size_t output_stream_index) {
return handler_it->second.first->OnFlushRequest(handler_it->second.second);
}
Status MediaHandler::FlushAllDownstreams() {
for (const auto& pair : output_handlers_) {
Status status = pair.second.first->OnFlushRequest(pair.second.second);
if (!status.ok()) {
return status;
}
}
return Status::OK;
}
} // namespace media
} // namespace shaka

View File

@ -86,6 +86,9 @@ class MediaHandler {
/// called after setting up the graph before running the graph.
Status Initialize();
/// Validate if the handler is connected to its upstream handler.
bool IsConnected() { return num_input_streams_ > 0; }
protected:
/// Internal implementation of initialize. Note that it should only initialize
/// the MediaHandler itself. Downstream handlers are handled in Initialize().
@ -160,6 +163,9 @@ class MediaHandler {
/// Flush the downstream connected at the specified output stream index.
Status FlushDownstream(size_t output_stream_index);
/// Flush all connected downstreams.
Status FlushAllDownstreams();
bool initialized() { return initialized_; }
size_t num_input_streams() const { return num_input_streams_; }
size_t next_output_stream_index() const { return next_output_stream_index_; }

View File

@ -59,27 +59,22 @@ const uint8_t kData[]{
namespace shaka {
namespace media {
// A fake media handler definition used for testing.
class FakeMediaHandler : public MediaHandler {
public:
const std::vector<std::unique_ptr<StreamData>>& stream_data_vector() const {
return stream_data_vector_;
Status FakeMediaHandler::InitializeInternal() {
return Status::OK;
}
void clear_stream_data_vector() { stream_data_vector_.clear(); }
protected:
Status InitializeInternal() override { return Status::OK; }
Status Process(std::unique_ptr<StreamData> stream_data) override {
Status FakeMediaHandler::Process(std::unique_ptr<StreamData> stream_data) {
stream_data_vector_.push_back(std::move(stream_data));
return Status::OK;
}
Status OnFlushRequest(size_t input_stream_index) override { return Status::OK; }
bool ValidateOutputStreamIndex(size_t stream_index) const override {
return true;
Status FakeMediaHandler::OnFlushRequest(size_t input_stream_index) {
return Status::OK;
}
std::vector<std::unique_ptr<StreamData>> stream_data_vector_;
};
bool FakeMediaHandler::ValidateOutputStreamIndex(size_t stream_index) const {
return true;
}
MediaHandlerTestBase::MediaHandlerTestBase()
: next_handler_(new FakeMediaHandler),

View File

@ -13,8 +13,6 @@
namespace shaka {
namespace media {
class FakeMediaHandler;
MATCHER_P3(IsStreamInfo, stream_index, time_scale, encrypted, "") {
*result_listener << "which is (" << stream_index << "," << time_scale << ","
<< (encrypted ? "encrypted" : "not encrypted") << ")";
@ -76,6 +74,23 @@ MATCHER_P4(IsMediaSample, stream_index, timestamp, duration, encrypted, "") {
arg->media_sample->is_encrypted() == encrypted;
}
// A fake media handler definition used for testing.
class FakeMediaHandler : public MediaHandler {
public:
const std::vector<std::unique_ptr<StreamData>>& stream_data_vector() const {
return stream_data_vector_;
}
void clear_stream_data_vector() { stream_data_vector_.clear(); }
protected:
Status InitializeInternal() override;
Status Process(std::unique_ptr<StreamData> stream_data) override;
Status OnFlushRequest(size_t input_stream_index) override;
bool ValidateOutputStreamIndex(size_t stream_index) const override;
std::vector<std::unique_ptr<StreamData>> stream_data_vector_;
};
class MediaHandlerTestBase : public ::testing::Test {
public:
MediaHandlerTestBase();
@ -126,6 +141,9 @@ class MediaHandlerTestBase : public ::testing::Test {
/// @return some random handler that can be used for testing.
std::shared_ptr<MediaHandler> some_handler() { return some_handler_; }
/// @return some a downstream handler that can be used for connecting.
std::shared_ptr<FakeMediaHandler> next_handler() { return next_handler_; }
private:
MediaHandlerTestBase(const MediaHandlerTestBase&) = delete;
MediaHandlerTestBase& operator=(const MediaHandlerTestBase&) = delete;

View File

@ -48,7 +48,7 @@ VideoStreamInfo::VideoStreamInfo(int track_id,
uint16_t height,
uint32_t pixel_width,
uint32_t pixel_height,
int16_t trick_play_rate,
uint32_t trick_play_rate,
uint8_t nalu_length_size,
const std::string& language,
bool is_encrypted)

View File

@ -39,7 +39,7 @@ class VideoStreamInfo : public StreamInfo {
uint16_t height,
uint32_t pixel_width,
uint32_t pixel_height,
int16_t trick_play_rate,
uint32_t trick_play_rate,
uint8_t nalu_length_size,
const std::string& language,
bool is_encrypted);
@ -62,16 +62,20 @@ class VideoStreamInfo : public StreamInfo {
/// @return 0 if unknown.
uint32_t pixel_height() const { return pixel_height_; }
uint8_t nalu_length_size() const { return nalu_length_size_; }
int16_t trick_play_rate() const { return trick_play_rate_; }
uint32_t trick_play_rate() const { return trick_play_rate_; }
uint32_t playback_rate() const { return playback_rate_; }
const std::vector<uint8_t>& eme_init_data() const { return eme_init_data_; }
void set_width(uint32_t width) { width_ = width; }
void set_height(uint32_t height) { height_ = height; }
void set_pixel_width(uint32_t pixel_width) { pixel_width_ = pixel_width; }
void set_pixel_height(uint32_t pixel_height) { pixel_height_ = pixel_height; }
void set_trick_play_rate(int16_t trick_play_rate) {
void set_trick_play_rate(uint32_t trick_play_rate) {
trick_play_rate_ = trick_play_rate;
}
void set_playback_rate(uint32_t playback_rate) {
playback_rate_ = playback_rate;
}
void set_eme_init_data(const uint8_t* eme_init_data,
size_t eme_init_data_size) {
eme_init_data_.assign(eme_init_data, eme_init_data + eme_init_data_size);
@ -86,7 +90,19 @@ class VideoStreamInfo : public StreamInfo {
// 0 means unknown.
uint32_t pixel_width_;
uint32_t pixel_height_;
int16_t trick_play_rate_; // Non-zero for trick-play streams.
uint32_t trick_play_rate_ = 0; // Non-zero for trick-play streams.
// Playback rate is the attribute for trick play stream, which signals the
// playout capabilities
// (http://dashif.org/wp-content/uploads/2016/12/DASH-IF-IOP-v4.0-clean.pdf,
// page 18, line 1). It is the ratio of main frame rate to the trick play
// frame rate. If the time scale and frame duration are not modified after
// trick play handler processing, the playback_rate equals to the number of
// frames between consecutive key frames selected for trick play stream. For
// example, if the video stream has GOP size of 10 and the trick play rate is
// 3, the key frames are in this trick play stream are [frame_0, frame_30,
// frame_60, ...]. Then the playback_rate is 30.
uint32_t playback_rate_;
// Specifies the size of the NAL unit length field. Can be 1, 2 or 4 bytes, or
// 0 if the stream is not a NAL structured video stream or if it is an AnnexB

View File

@ -101,6 +101,13 @@ void AddVideoInfo(const VideoStreamInfo* video_stream_info,
if (!codec_config.empty()) {
video_info->set_decoder_config(&codec_config[0], codec_config.size());
}
if (video_stream_info->trick_play_rate() > 0) {
video_info->set_trick_play_rate(video_stream_info->trick_play_rate());
CHECK_GT(video_stream_info->playback_rate(), 0u)
<< "Max playout rate should be > 0 for trick play streams.";
video_info->set_playback_rate(video_stream_info->playback_rate());
}
}
void AddAudioInfo(const AudioStreamInfo* audio_stream_info,

View File

@ -574,7 +574,7 @@ bool WvmMediaParser::ParseIndexEntry() {
}
uint64_t track_duration = 0;
int16_t trick_play_rate = 0;
uint32_t trick_play_rate = 0;
uint32_t sampling_frequency = kDefaultSamplingFrequency;
uint32_t time_scale = kMpeg2ClockRate;
uint16_t video_width = 0;

View File

@ -12,17 +12,40 @@
namespace shaka {
namespace media {
TrickPlayHandler::TrickPlayHandler(const TrickPlayOptions& trick_play_option)
: trick_play_options_(trick_play_option),
cached_stream_data_(trick_play_option.trick_play_rates.size()) {
for (auto rate : trick_play_option.trick_play_rates) {
CHECK_GT(rate, 0);
}
namespace {
const size_t kMainStreamIndex = 0;
}
TrickPlayHandler::TrickPlayHandler() {}
TrickPlayHandler::~TrickPlayHandler() {}
void TrickPlayHandler::SetHandlerForMainStream(
std::shared_ptr<MediaHandler> handler) {
SetHandler(kMainStreamIndex, std::move(handler));
}
void TrickPlayHandler::SetHandlerForTrickPlay(
uint32_t trick_play_rate,
std::shared_ptr<MediaHandler> handler) {
trick_play_rates_.push_back(trick_play_rate);
// Trick play streams start from index 1.
SetHandler(trick_play_rates_.size(), std::move(handler));
}
Status TrickPlayHandler::InitializeInternal() {
if (!HasMainStream()) {
return Status(error::TRICK_PLAY_ERROR,
"Trick play does not have main stream");
}
if (trick_play_rates_.empty()) {
return Status(error::TRICK_PLAY_ERROR,
"Trick play rates are not specified.");
}
size_t num_trick_play_rates = trick_play_rates_.size();
cached_stream_data_.resize(num_trick_play_rates);
playback_rates_.resize(num_trick_play_rates, 0);
return Status::OK;
}
@ -54,6 +77,18 @@ Status TrickPlayHandler::Process(
}
}
if (stream_data->stream_data_type == StreamDataType::kSegmentInfo) {
for (auto& cached_data : cached_stream_data_) {
// It is possible that trick play stream has large frame duration that
// some segments in the main stream are skipped. To avoid empty segments,
// only cache SegementInfo with MediaSample before it.
if (!cached_data.empty() &&
cached_data.back()->stream_data_type == StreamDataType::kMediaSample)
cached_data.push_back(stream_data);
}
return Status::OK;
}
if (stream_data->stream_data_type != StreamDataType::kMediaSample) {
// Non media sample stream data needs to be dispatched to every output
// stream. It is just cached in every queue until a new key frame comes or
@ -66,12 +101,18 @@ Status TrickPlayHandler::Process(
if (stream_data->media_sample->is_key_frame()) {
// For a new key frame, some of the trick play streams may include it.
// The cached data in those trick play streams will be processed.
DCHECK_EQ(trick_play_options_.trick_play_rates.size(),
cached_stream_data_.size());
DCHECK_EQ(trick_play_rates_.size(), cached_stream_data_.size());
for (size_t i = 0; i < cached_stream_data_.size(); ++i) {
int16_t rate = trick_play_options_.trick_play_rates[i];
uint32_t rate = trick_play_rates_[i];
if (total_key_frames_ % rate == 0) {
if (!cached_stream_data_[i].empty()) {
// Delay processing cached stream data until receiving the second key
// frame so that the GOP size could be derived.
if (!cached_stream_data_[i].empty() && total_key_frames_ > 0) {
// Num of frames between first two key frames in the trick play
// streams. Use this as the playback_rate.
if (playback_rates_[i] == 0)
playback_rates_[i] = total_frames_;
Status status =
ProcessCachedStreamData(i + 1, &cached_stream_data_[i]);
if (!status.ok())
@ -84,24 +125,40 @@ Status TrickPlayHandler::Process(
total_key_frames_++;
}
total_frames_++;
prev_sample_end_timestamp_ =
stream_data->media_sample->dts() + stream_data->media_sample->duration();
return Status::OK;
}
bool TrickPlayHandler::ValidateOutputStreamIndex(size_t stream_index) const {
// Output stream index should be less than the number of trick play
// streams + one original stream.
return stream_index <= trick_play_options_.trick_play_rates.size();
return stream_index <= trick_play_rates_.size();
};
Status TrickPlayHandler::OnFlushRequest(size_t input_stream_index) {
DCHECK_EQ(input_stream_index, 0u)
<< "Trick Play Handler should only have single input.";
for (size_t i = 0; i < cached_stream_data_.size(); ++i) {
LOG_IF(WARNING, playback_rates_[i] == 0)
<< "Max playout rate for trick play rate " << trick_play_rates_[i]
<< " is not determined. "
<< "Specify it as total number of frames: " << total_frames_ << ".";
playback_rates_[i] = total_frames_;
ProcessCachedStreamData(i + 1, &cached_stream_data_[i]);
}
return MediaHandler::FlushDownstream(input_stream_index);
return MediaHandler::FlushAllDownstreams();
}
bool TrickPlayHandler::HasMainStream() {
const auto& handlers = output_handlers();
const auto& main_stream_handler = handlers.find(kMainStreamIndex);
if (main_stream_handler == handlers.end()) {
return false;
}
return main_stream_handler->second.first != nullptr;
}
Status TrickPlayHandler::ProcessCachedStreamData(
@ -121,8 +178,8 @@ Status TrickPlayHandler::ProcessCachedStreamData(
Status TrickPlayHandler::ProcessOneStreamData(
size_t output_stream_index,
const std::shared_ptr<StreamData>& stream_data) {
uint32_t trick_play_rate =
trick_play_options_.trick_play_rates[output_stream_index - 1];
size_t trick_play_index = output_stream_index - 1;
uint32_t trick_play_rate = trick_play_rates_[trick_play_index];
Status status;
switch (stream_data->stream_data_type) {
// trick_play_rate in StreamInfo should be modified.
@ -132,6 +189,9 @@ Status TrickPlayHandler::ProcessOneStreamData(
std::shared_ptr<VideoStreamInfo> trick_play_video_stream_info(
new VideoStreamInfo(video_stream_info));
trick_play_video_stream_info->set_trick_play_rate(trick_play_rate);
DCHECK_GT(playback_rates_[trick_play_index], 0u);
trick_play_video_stream_info->set_playback_rate(
playback_rates_[trick_play_index]);
status =
DispatchStreamInfo(output_stream_index, trick_play_video_stream_info);
break;
@ -142,7 +202,6 @@ Status TrickPlayHandler::ProcessOneStreamData(
MediaSample::CopyFrom(*(stream_data->media_sample));
trick_play_media_sample->set_duration(prev_sample_end_timestamp_ -
stream_data->media_sample->dts());
status =
DispatchMediaSample(output_stream_index, trick_play_media_sample);
}

View File

@ -12,17 +12,6 @@
namespace shaka {
namespace media {
struct TrickPlayOptions {
/// Trick play rates. Note that there can be multiple trick play rates,
/// e.g., 2, 4 and 8. That means, one input video stream will generate 3
/// output trick play streams and original stream. Three trick play streams
/// are:
/// [key_frame_0, key_frame_2, key_frame_4, ...]
/// [key_frame_0, key_frame_4, key_frame_8,...]
/// [key_frame_0, key_frame_8, key_frame_16, ...].
std::vector<int16_t> trick_play_rates;
};
/// TrickPlayHandler is a single-input-multiple-output media handler. It creates
/// trick play streams from the input.
// The stream data in trick play stream is not a simple duplicate. Some
@ -33,9 +22,13 @@ struct TrickPlayOptions {
// input stream data before the next key frame.
class TrickPlayHandler : public MediaHandler {
public:
explicit TrickPlayHandler(const TrickPlayOptions& trick_play_options);
TrickPlayHandler();
~TrickPlayHandler() override;
void SetHandlerForMainStream(std::shared_ptr<MediaHandler> handler);
void SetHandlerForTrickPlay(uint32_t trick_play_rate,
std::shared_ptr<MediaHandler> handler);
protected:
/// @name MediaHandler implementation overrides.
/// @{
@ -48,6 +41,10 @@ class TrickPlayHandler : public MediaHandler {
private:
friend class TrickPlayHandlerTest;
// Returns true if the trick play handler has main stream output handler
// connected, otherwise returns false.
bool HasMainStream();
// Process the cached stream data for one trick play stream.
// The cached data is dispatched to the |output_stream_index|.
Status ProcessCachedStreamData(
@ -62,7 +59,14 @@ class TrickPlayHandler : public MediaHandler {
Status ProcessOneStreamData(size_t output_stream_index,
const std::shared_ptr<StreamData>& stream_data);
const TrickPlayOptions trick_play_options_;
// Trick play rates. Note that there can be multiple trick play rates,
// e.g., 2, 4 and 8. That means, one input video stream will generate 3
// output trick play streams and original stream. Three trick play streams
// are:
// [key_frame_0, key_frame_2, key_frame_4, ...]
// [key_frame_0, key_frame_4, key_frame_8,...]
// [key_frame_0, key_frame_8, key_frame_16, ...].
std::vector<uint32_t> trick_play_rates_;
TrickPlayHandler(const TrickPlayHandler&) = delete;
TrickPlayHandler& operator=(const TrickPlayHandler&) = delete;
@ -70,11 +74,17 @@ class TrickPlayHandler : public MediaHandler {
/// Num of key frames received.
uint32_t total_key_frames_ = 0;
// Num of frames received.
uint32_t total_frames_ = 0;
// End timestamp of the previous processed media_sample, which is |dts| +
// |duration|. The duration of key frame in trick play stream is updated based
// on this timestamp.
int64_t prev_sample_end_timestamp_ = 0;
// Record playback_rate for each trick play stream.
std::vector<uint32_t> playback_rates_;
// The data in output streams should be in the same order as in the input
// stream. Cache the stream data before next key frame so that we can
// determine the duration for the current key frame. Since one key frame may

View File

@ -22,18 +22,19 @@ namespace {
const size_t kStreamIndex0 = 0;
const size_t kStreamIndex1 = 1;
const size_t kStreamIndex2 = 2;
const size_t kStreamIndex3 = 3;
const uint32_t kTimeScale = 800;
const uint32_t kDuration = 200;
const int16_t kTrickPlayRates[]{1, 2, 4};
const uint32_t kTrickPlayRates[]{1, 2};
const uint32_t kTrickPlayRatesDecreasing[]{2, 1};
const bool kEncrypted = true;
} // namespace
MATCHER_P4(IsTrickPlayVideoStreamInfo,
MATCHER_P5(IsTrickPlayVideoStreamInfo,
stream_index,
time_scale,
encrypted,
trick_play_rate,
playback_rate,
"") {
return arg->stream_index == stream_index &&
arg->stream_data_type == StreamDataType::kStreamInfo &&
@ -41,7 +42,9 @@ MATCHER_P4(IsTrickPlayVideoStreamInfo,
arg->stream_info->is_encrypted() == encrypted &&
arg->stream_info->stream_type() == kStreamVideo &&
static_cast<const VideoStreamInfo*>(arg->stream_info.get())
->trick_play_rate() == trick_play_rate;
->trick_play_rate() == trick_play_rate &&
static_cast<const VideoStreamInfo*>(arg->stream_info.get())
->playback_rate() == playback_rate;
}
MATCHER_P3(IsKeyFrameMediaSample, stream_index, timestamp, duration, "") {
@ -54,12 +57,16 @@ MATCHER_P3(IsKeyFrameMediaSample, stream_index, timestamp, duration, "") {
class TrickPlayHandlerTest : public MediaHandlerTestBase {
public:
void SetUpTrickPlayHandler(const TrickPlayOptions& trick_play_options) {
trick_play_handler_.reset(new TrickPlayHandler(trick_play_options));
// The output stream size is number of trick play stream + one
// non-trick-play stream.
SetUpGraph(1, trick_play_options.trick_play_rates.size() + 1,
trick_play_handler_);
void SetUpTrickPlayHandler(const std::vector<uint32_t>& trick_play_rates) {
trick_play_handler_.reset(new TrickPlayHandler());
// Use SetUpGraph to set only input handler, and use
// SetHandlerForMainStream and SetHandlerForTrickPlay for the output
// handlers.
SetUpGraph(1, 0, trick_play_handler_);
trick_play_handler_->SetHandlerForMainStream(next_handler());
for (uint32_t rate : trick_play_rates) {
trick_play_handler_->SetHandlerForTrickPlay(rate, next_handler());
}
ASSERT_OK(trick_play_handler_->Initialize());
}
@ -77,10 +84,9 @@ class TrickPlayHandlerTest : public MediaHandlerTestBase {
// This test makes sure the audio stream is rejected by the trick play handler.
TEST_F(TrickPlayHandlerTest, AudioStream) {
TrickPlayOptions trick_play_options;
trick_play_options.trick_play_rates.assign(std::begin(kTrickPlayRates),
const std::vector<uint32_t> trick_play_rates(std::begin(kTrickPlayRates),
std::end(kTrickPlayRates));
SetUpTrickPlayHandler(trick_play_options);
SetUpTrickPlayHandler(trick_play_rates);
Status status =
Process(GetAudioStreamInfoStreamData(kStreamIndex0, kTimeScale));
@ -90,11 +96,10 @@ TEST_F(TrickPlayHandlerTest, AudioStream) {
// This test makes sure the trick play handler can process stream data
// correctly.
TEST_F(TrickPlayHandlerTest, VideoStream) {
TrickPlayOptions trick_play_options;
trick_play_options.trick_play_rates.assign(std::begin(kTrickPlayRates),
TEST_F(TrickPlayHandlerTest, VideoStreamWithTrickPlay) {
const std::vector<uint32_t> trick_play_rates(std::begin(kTrickPlayRates),
std::end(kTrickPlayRates));
SetUpTrickPlayHandler(trick_play_options);
SetUpTrickPlayHandler(trick_play_rates);
ASSERT_OK(Process(GetVideoStreamInfoStreamData(kStreamIndex0, kTimeScale)));
// The stream info is cached, so the output is empty.
@ -119,12 +124,6 @@ TEST_F(TrickPlayHandlerTest, VideoStream) {
// Frame 0, key frame.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp, kDuration,
!kEncrypted),
IsTrickPlayVideoStreamInfo(kStreamIndex1, kTimeScale, !kEncrypted,
kTrickPlayRates[0]),
IsTrickPlayVideoStreamInfo(kStreamIndex2, kTimeScale, !kEncrypted,
kTrickPlayRates[1]),
IsTrickPlayVideoStreamInfo(kStreamIndex3, kTimeScale, !kEncrypted,
kTrickPlayRates[2]),
// Frame 1.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration,
kDuration, !kEncrypted),
@ -148,6 +147,10 @@ TEST_F(TrickPlayHandlerTest, VideoStream) {
// Frame 3, key frame.
IsKeyFrameMediaSample(
kStreamIndex0, kVideoStartTimestamp + kDuration * 3, kDuration),
// Stream info, TrickPlayRate = 1.
IsTrickPlayVideoStreamInfo(
kStreamIndex1, kTimeScale, !kEncrypted, kTrickPlayRates[0],
static_cast<uint32_t>(kGOPSize) * kTrickPlayRates[0]),
// Frame 0, TrickPlayRate = 1.
IsKeyFrameMediaSample(kStreamIndex1, kVideoStartTimestamp,
kDuration * 3),
@ -178,6 +181,10 @@ TEST_F(TrickPlayHandlerTest, VideoStream) {
IsKeyFrameMediaSample(kStreamIndex1,
kVideoStartTimestamp + kDuration * 3,
kDuration * 3),
// Stream info, TrickPlayRate = 2.
IsTrickPlayVideoStreamInfo(
kStreamIndex2, kTimeScale, !kEncrypted, kTrickPlayRates[1],
static_cast<uint32_t>(kGOPSize) * kTrickPlayRates[1]),
// Frame 0, TrickPlayRate = 2.
IsKeyFrameMediaSample(kStreamIndex2, kVideoStartTimestamp,
kDuration * 6),
@ -195,11 +202,128 @@ TEST_F(TrickPlayHandlerTest, VideoStream) {
kDuration * 2),
// Frame 6, TrickPlayRate = 2.
IsKeyFrameMediaSample(kStreamIndex2,
kVideoStartTimestamp + kDuration * 6,
kDuration * 2)));
ClearOutputStreamDataVector();
// Flush again, get nothing.
ASSERT_OK(FlushStream(0));
EXPECT_THAT(GetOutputStreamDataVector(), IsEmpty());
}
// This test makes sure the trick play handler can process stream data
// correctly with a decreasing order of trick play rates.
TEST_F(TrickPlayHandlerTest, VideoStreamWithDecreasingTrickPlayRates) {
const std::vector<uint32_t> trick_play_rates(
std::begin(kTrickPlayRatesDecreasing),
std::end(kTrickPlayRatesDecreasing));
SetUpTrickPlayHandler(trick_play_rates);
ASSERT_OK(Process(GetVideoStreamInfoStreamData(kStreamIndex0, kTimeScale)));
// The stream info is cached, so the output is empty.
EXPECT_THAT(
GetOutputStreamDataVector(),
ElementsAre(IsStreamInfo(kStreamIndex0, kTimeScale, !kEncrypted)));
ClearOutputStreamDataVector();
const int kVideoStartTimestamp = 12345;
// Group of Picture size, the frequency of key frames.
const int kGOPSize = 3;
for (int i = 0; i < 3; ++i) {
const bool is_key_frame = (i % kGOPSize == 0);
ASSERT_OK(Process(GetMediaSampleStreamData(
kStreamIndex0, kVideoStartTimestamp + kDuration * i, kDuration,
is_key_frame)));
}
EXPECT_THAT(
GetOutputStreamDataVector(),
ElementsAre(
// Frame 0, key frame.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp, kDuration,
!kEncrypted),
// Frame 1.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration,
kDuration, !kEncrypted),
// Frame 2.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration * 2,
kDuration, !kEncrypted)));
ClearOutputStreamDataVector();
// This expectation are separated from the expectation above because
// ElementsAre supports at most 10 elements.
for (int i = 3; i < 6; ++i) {
const bool is_key_frame = (i % kGOPSize == 0);
ASSERT_OK(Process(GetMediaSampleStreamData(
kStreamIndex0, kVideoStartTimestamp + kDuration * i, kDuration,
is_key_frame)));
}
EXPECT_THAT(
GetOutputStreamDataVector(),
ElementsAre(
// Frame 3, key frame.
IsKeyFrameMediaSample(
kStreamIndex0, kVideoStartTimestamp + kDuration * 3, kDuration),
// Stream info, TrickPlayRate = 1.
IsTrickPlayVideoStreamInfo(
kStreamIndex2, kTimeScale, !kEncrypted,
kTrickPlayRatesDecreasing[1],
static_cast<uint32_t>(kGOPSize) * kTrickPlayRatesDecreasing[1]),
// Frame 0, TrickPlayRate = 1.
IsKeyFrameMediaSample(kStreamIndex2, kVideoStartTimestamp,
kDuration * 3),
// Frame 4.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration * 4,
kDuration, !kEncrypted),
// Frame 5.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration * 5,
kDuration, !kEncrypted)));
ClearOutputStreamDataVector();
// This expectation are separated from the expectation above because
// ElementsAre supports at most 10 elements.
for (int i = 6; i < 8; ++i) {
const bool is_key_frame = (i % kGOPSize == 0);
ASSERT_OK(Process(GetMediaSampleStreamData(
kStreamIndex0, kVideoStartTimestamp + kDuration * i, kDuration,
is_key_frame)));
}
EXPECT_THAT(
GetOutputStreamDataVector(),
ElementsAre(
// Frame 6, key frame.
IsKeyFrameMediaSample(
kStreamIndex0, kVideoStartTimestamp + kDuration * 6, kDuration),
// Stream info, TrickPlayRate = 2.
IsTrickPlayVideoStreamInfo(
kStreamIndex1, kTimeScale, !kEncrypted,
kTrickPlayRatesDecreasing[0],
static_cast<uint32_t>(kGOPSize) * kTrickPlayRatesDecreasing[0]),
// Frame 0, TrickPlayRate = 2.
IsKeyFrameMediaSample(kStreamIndex1, kVideoStartTimestamp,
kDuration * 6),
// Frame 3, TrickPlayRate = 1.
IsKeyFrameMediaSample(kStreamIndex2,
kVideoStartTimestamp + kDuration * 3,
kDuration * 3),
// Frame 7.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration * 7,
kDuration, !kEncrypted)));
ClearOutputStreamDataVector();
ASSERT_OK(FlushStream(0));
EXPECT_THAT(GetOutputStreamDataVector(),
ElementsAre(
// Frame 6, TrickPlayRate = 2.
IsKeyFrameMediaSample(kStreamIndex1,
kVideoStartTimestamp + kDuration * 6,
kDuration * 2),
// Frame 0, TrickPlayRate = 4.
IsKeyFrameMediaSample(kStreamIndex3, kVideoStartTimestamp,
kDuration * 8)));
// Frame 6, TrickPlayRate = 1.
IsKeyFrameMediaSample(kStreamIndex2,
kVideoStartTimestamp + kDuration * 6,
kDuration * 2)));
ClearOutputStreamDataVector();
// Flush again, get nothing.

View File

@ -151,6 +151,34 @@ bool DashIopMpdNotifier::Flush() {
return WriteMpdToFile(output_path_, mpd_builder_.get());
}
AdaptationSet* DashIopMpdNotifier::ReuseAdaptationSet(
const std::list<AdaptationSet*>& adaptation_sets,
const MediaInfo& media_info) {
const bool has_protected_content = media_info.has_protected_content();
for (AdaptationSet* adaptation_set : adaptation_sets) {
ProtectedContentMap::const_iterator protected_content_it =
protected_content_map_.find(adaptation_set->id());
// If the AdaptationSet ID is not registered in the map, then it is clear
// content.
if (protected_content_it == protected_content_map_.end()) {
// Can reuse the AdaptationSet without content protection.
if (!has_protected_content) {
return adaptation_set;
}
continue;
}
if (ProtectedContentEq(protected_content_it->second,
media_info.protected_content())) {
// Content protection info matches. Reuse the AdaptationSet.
return adaptation_set;
}
}
return nullptr;
}
AdaptationSet* DashIopMpdNotifier::GetAdaptationSetForMediaInfo(
const std::string& key,
const MediaInfo& media_info) {
@ -158,30 +186,10 @@ AdaptationSet* DashIopMpdNotifier::GetAdaptationSetForMediaInfo(
if (adaptation_sets.empty())
return NewAdaptationSet(media_info, &adaptation_sets);
const bool has_protected_content = media_info.has_protected_content();
for (std::list<AdaptationSet*>::const_iterator adaptation_set_it =
adaptation_sets.begin();
adaptation_set_it != adaptation_sets.end(); ++adaptation_set_it) {
ProtectedContentMap::const_iterator protected_content_it =
protected_content_map_.find((*adaptation_set_it)->id());
// If the AdaptationSet ID is not registered in the map, then it is clear
// content (or encrypted but doesn't need <ContentProtection> element
// possibly because the player knows how to handle it).
if (protected_content_it == protected_content_map_.end()) {
// Can reuse the AdaptationSet without content protection.
if (!has_protected_content)
return *adaptation_set_it;
continue;
}
if (ProtectedContentEq(protected_content_it->second,
media_info.protected_content())) {
// Content protection info matches. Reuse the AdaptationSet.
return *adaptation_set_it;
}
}
AdaptationSet* reuse_adaptation_set =
ReuseAdaptationSet(adaptation_sets, media_info);
if (reuse_adaptation_set)
return reuse_adaptation_set;
// None of the adaptation sets match with the new content protection.
// Need a new one.
@ -267,8 +275,42 @@ AdaptationSet* DashIopMpdNotifier::NewAdaptationSet(
(*adaptation_sets->begin())->AddRole(AdaptationSet::kRoleMain);
new_adaptation_set->AddRole(AdaptationSet::kRoleMain);
}
if (media_info.video_info().trick_play_rate() > 0) {
uint32_t trick_play_reference_id = 0;
if (!FindOriginalAdaptationSetForTrickPlay(media_info,
&trick_play_reference_id)) {
LOG(ERROR) << "Failed to find main adaptation set for trick play.";
return nullptr;
}
DCHECK_NE(new_adaptation_set->id(), trick_play_reference_id);
new_adaptation_set->AddTrickPlayReferenceId(trick_play_reference_id);
}
}
return new_adaptation_set;
}
bool DashIopMpdNotifier::FindOriginalAdaptationSetForTrickPlay(
const MediaInfo& media_info,
uint32_t* main_adaptation_set_id) {
MediaInfo media_info_no_trickplay = media_info;
media_info_no_trickplay.mutable_video_info()->clear_trick_play_rate();
std::string key = GetAdaptationSetKey(media_info_no_trickplay);
const std::list<AdaptationSet*>& adaptation_sets =
adaptation_set_list_map_[key];
if (adaptation_sets.empty()) {
return false;
}
AdaptationSet* reuse_adaptation_set =
ReuseAdaptationSet(adaptation_sets, media_info);
if (!reuse_adaptation_set) {
return false;
}
*main_adaptation_set_id = reuse_adaptation_set->id();
return true;
}
} // namespace shaka

View File

@ -64,6 +64,19 @@ class DashIopMpdNotifier : public MpdNotifier {
// Maps AdaptationSet ID to ProtectedContent.
typedef std::map<uint32_t, MediaInfo::ProtectedContent> ProtectedContentMap;
// Find reusable AdaptationSet, instead of creating a new AdaptationSet for
// the |media_info|. There are two cases that an |existing_adaptation_set|
// can be used:
// 1) The media info does not have protected content and there is an existing
// unprotected content AdapationSet.
// 2) The media info has protected content and there is an exisiting
// AdaptationSet, which has same MediaInfo::ProtectedContent protobuf.
// Returns the reusable AdaptationSet pointer if found, otherwise returns
// nullptr.
AdaptationSet* ReuseAdaptationSet(
const std::list<AdaptationSet*>& adaptation_sets,
const MediaInfo& media_info);
// Checks the protected_content field of media_info and returns a non-null
// AdaptationSet* for a new Representation.
// This does not necessarily return a new AdaptationSet. If
@ -83,6 +96,15 @@ class DashIopMpdNotifier : public MpdNotifier {
AdaptationSet* NewAdaptationSet(const MediaInfo& media_info,
std::list<AdaptationSet*>* adaptation_sets);
// Gets the original AdaptationSet which the trick play video belongs
// to and returns the id of the original adapatation set.
// It is assumed that the corresponding AdaptationSet has been created before
// the trick play AdaptationSet.
// Returns true if main_adaptation_id is found, otherwise false;
bool FindOriginalAdaptationSetForTrickPlay(
const MediaInfo& media_info,
uint32_t* original_adaptation_set_id);
// Testing only method. Returns a pointer to MpdBuilder.
MpdBuilder* MpdBuilderForTesting() const {
return mpd_builder_.get();

View File

@ -154,6 +154,72 @@ TEST_F(DashIopMpdNotifierTest, NotifyNewContainer) {
EXPECT_TRUE(notifier.Flush());
}
// Verify that basic VOD NotifyNewContainer() operation works on trick play
// streams.
// No encrypted contents.
TEST_F(DashIopMpdNotifierTest, NotifyNewContainerForTrickPlay) {
const char kTrickPlayMediaInfo[] =
"video_info {\n"
" codec: 'avc1'\n"
" width: 1280\n"
" height: 720\n"
" time_scale: 10\n"
" frame_duration: 100\n"
" pixel_width: 1\n"
" pixel_height: 1\n"
" trick_play_rate: 2\n"
" playback_rate: 10\n"
"}\n"
"container_type: 1\n";
DashIopMpdNotifier notifier(empty_mpd_option_, empty_base_urls_,
output_path_);
std::unique_ptr<MockMpdBuilder> mock_mpd_builder(new MockMpdBuilder());
// Not using default mocks in this test so that we can keep track of
// mocks by named mocks.
const uint32_t kAdaptationSetId = 2u;
const uint32_t kTrickPlayAdaptationSetId = 3u;
std::unique_ptr<MockAdaptationSet> mock_adaptation_set(
new MockAdaptationSet(kAdaptationSetId));
std::unique_ptr<MockAdaptationSet> mock_tp_adaptation_set(
new MockAdaptationSet(kTrickPlayAdaptationSetId));
const uint32_t kRepresentationId = 4u;
const uint32_t kTrickPlayRepresentationId = 5u;
std::unique_ptr<MockRepresentation> mock_representation(
new MockRepresentation(kRepresentationId));
std::unique_ptr<MockRepresentation> mock_tp_representation(
new MockRepresentation(kTrickPlayRepresentationId));
InSequence in_sequence;
EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_))
.WillOnce(Return(mock_adaptation_set.get()));
EXPECT_CALL(*mock_adaptation_set, AddRole(_)).Times(0);
EXPECT_CALL(*mock_adaptation_set, AddRepresentation(_))
.WillOnce(Return(mock_representation.get()));
// Calls for trick-play stream.
EXPECT_CALL(*mock_mpd_builder, AddAdaptationSet(_))
.WillOnce(Return(mock_tp_adaptation_set.get()));
EXPECT_CALL(*mock_tp_adaptation_set, AddRole(_)).Times(0);
EXPECT_CALL(*mock_tp_adaptation_set,
AddTrickPlayReferenceId(kAdaptationSetId))
.Times(1);
EXPECT_CALL(*mock_tp_adaptation_set, AddRepresentation(_))
.WillOnce(Return(mock_tp_representation.get()));
EXPECT_CALL(*mock_mpd_builder, ToString(_)).WillOnce(Return(true));
uint32_t unused_container_id;
SetMpdBuilder(&notifier, std::move(mock_mpd_builder));
EXPECT_TRUE(notifier.NotifyNewContainer(ConvertToMediaInfo(kValidMediaInfo),
&unused_container_id));
EXPECT_TRUE(notifier.NotifyNewContainer(
ConvertToMediaInfo(kTrickPlayMediaInfo), &unused_container_id));
EXPECT_TRUE(notifier.Flush());
}
// Verify that if the MediaInfo contains text information, then
// MpdBuilder::ForceSetSegmentAlignment() is called.
TEST_F(DashIopMpdNotifierTest, NotifyNewTextContainer) {

View File

@ -40,6 +40,13 @@ message MediaInfo {
// aspect ratio, or the @par attribute set on AdaptationSet element.
optional uint32 pixel_width = 7;
optional uint32 pixel_height = 8;
// trick_play_rate: sample rate of the key frame from the original stream.
// e.g., 1 means every key frame, 2 means every two key frames.
optional uint32 trick_play_rate = 9;
// playback_rate: the playout capability (e.g., 4x, 8x, 16x fast foward) of
// the trick play stream.
optional uint32 playback_rate = 10;
}
message AudioInfo {

View File

@ -38,6 +38,7 @@ class MockAdaptationSet : public AdaptationSet {
void(const std::string& drm_uuid, const std::string& pssh));
MOCK_METHOD1(AddRole, void(AdaptationSet::Role role));
MOCK_METHOD1(ForceSetSegmentAlignment, void(bool segment_alignment));
MOCK_METHOD1(AddTrickPlayReferenceId, void(uint32_t id));
private:
// Only for constructing the super class. Not used for testing.

View File

@ -824,6 +824,17 @@ xml::scoped_xml_ptr<xmlNode> AdaptationSet::GetXml() {
return xml::scoped_xml_ptr<xmlNode>();
}
if (!trick_play_reference_ids_.empty()) {
std::string id_string;
for (uint32_t id : trick_play_reference_ids_) {
id_string += std::to_string(id) + ",";
}
DCHECK(!id_string.empty());
id_string.resize(id_string.size() - 1);
adaptation_set.AddEssentialProperty(
"http://dashif.org/guidelines/trickmode", id_string);
}
std::string switching_ids;
for (uint32_t id : adaptation_set_switching_ids_) {
if (!switching_ids.empty())
@ -889,6 +900,10 @@ void AdaptationSet::OnSetFrameRateForRepresentation(
RecordFrameRate(frame_duration, timescale);
}
void AdaptationSet::AddTrickPlayReferenceId(uint32_t id) {
trick_play_reference_ids_.insert(id);
}
bool AdaptationSet::GetEarliestTimestamp(double* timestamp_seconds) {
DCHECK(timestamp_seconds);
@ -1279,7 +1294,7 @@ bool Representation::IsContiguous(uint64_t start_time,
LOG(ERROR) << "Segments should not be out of order segment. Adding segment "
"with start_time == "
<< start_time << " but the previous segment starts at "
<< previous.start_time << ".";
<< previous_segment_start_time << ".";
return false;
}

View File

@ -267,6 +267,11 @@ class AdaptationSet {
uint32_t frame_duration,
uint32_t timescale);
/// Add the id of the adaptation set this trick play adaptation set belongs
/// to.
/// @param id the id of the reference (or main) adapation set.
virtual void AddTrickPlayReferenceId(uint32_t id);
protected:
/// @param adaptation_set_id is an ID number for this AdaptationSet.
/// @param lang is the language of this AdaptationSet. Mainly relevant for
@ -384,6 +389,12 @@ class AdaptationSet {
// reasonable and may cause an out-of-memory problem.
RepresentationTimeline representation_segment_start_times_;
// Record the reference id for the original adaptation sets the trick play
// stream belongs to. This is a set because the trick play streams may be for
// multiple AdaptationSets (e.g. SD and HD videos in different AdaptationSets
// can share the same trick play stream.)
std::set<uint32_t> trick_play_reference_ids_;
DISALLOW_COPY_AND_ASSIGN(AdaptationSet);
};

View File

@ -136,6 +136,13 @@ std::string GetAdaptationSetKey(const MediaInfo& media_info) {
key.append(":");
key.append(GetLanguage(media_info));
// Trick play streams of the same original stream, but possibly with
// different trick_play_rates, belong to the same trick play AdaptationSet.
if (media_info.has_video_info() &&
media_info.video_info().trick_play_rate() > 0) {
key.append(":trick_play");
}
return key;
}

View File

@ -175,6 +175,15 @@ void RepresentationBaseXmlNode::AddSupplementalProperty(
AddChild(supplemental_property.PassScopedPtr());
}
void RepresentationBaseXmlNode::AddEssentialProperty(
const std::string& scheme_id_uri,
const std::string& value) {
XmlNode essential_property("EssentialProperty");
essential_property.SetStringAttribute("schemeIdUri", scheme_id_uri);
essential_property.SetStringAttribute("value", value);
AddChild(essential_property.PassScopedPtr());
}
bool RepresentationBaseXmlNode::AddContentProtectionElement(
const ContentProtectionElement& content_protection_element) {
XmlNode content_protection_node("ContentProtection");
@ -245,6 +254,16 @@ bool RepresentationXmlNode::AddVideoInfo(const VideoInfo& video_info,
base::IntToString(video_info.time_scale()) + "/" +
base::IntToString(video_info.frame_duration()));
}
if (video_info.has_playback_rate()) {
SetStringAttribute("maxPlayoutRate",
base::IntToString(video_info.playback_rate()));
// Since the trick play stream contains only key frames, there is no coding
// dependency on the main stream. Simply set the codingDependency to false.
// TODO(hmchen): propagate this attribute up to the AdaptationSet, since
// all are set to false.
SetStringAttribute("codingDependency", "false");
}
return true;
}

View File

@ -105,6 +105,11 @@ class RepresentationBaseXmlNode : public XmlNode {
void AddSupplementalProperty(const std::string& scheme_id_uri,
const std::string& value);
/// @param scheme_id_uri is content of the schemeIdUri attribute.
/// @param value is the content of value attribute.
void AddEssentialProperty(const std::string& scheme_id_uri,
const std::string& value);
protected:
explicit RepresentationBaseXmlNode(const char* name);

View File

@ -54,6 +54,7 @@
'media/formats/webm/webm.gyp:webm',
'media/formats/webvtt/webvtt.gyp:webvtt',
'media/formats/wvm/wvm.gyp:wvm',
'media/trick_play/trick_play.gyp:trick_play',
'mpd/mpd.gyp:mpd_builder',
'third_party/boringssl/boringssl.gyp:boringssl',
'third_party/gflags/gflags.gyp:gflags',