Implement trick play handler.

Change-Id: I3d288dc7c4b77fd3aca1bad2ce96ad733384bcc8
This commit is contained in:
Haoming Chen 2017-02-27 09:22:25 -08:00
parent 0a3ef49c11
commit cc04698460
13 changed files with 552 additions and 4 deletions

View File

@ -30,6 +30,7 @@ enum FieldType {
kHlsNameField, kHlsNameField,
kHlsGroupIdField, kHlsGroupIdField,
kHlsPlaylistNameField, kHlsPlaylistNameField,
kTrickPlayRateField,
}; };
struct FieldNameToTypeMapping { struct FieldNameToTypeMapping {
@ -57,6 +58,7 @@ const FieldNameToTypeMapping kFieldNameTypeMappings[] = {
{"hls_name", kHlsNameField}, {"hls_name", kHlsNameField},
{"hls_group_id", kHlsGroupIdField}, {"hls_group_id", kHlsGroupIdField},
{"playlist_name", kHlsPlaylistNameField}, {"playlist_name", kHlsPlaylistNameField},
{"trick_play_rate", kTrickPlayRateField},
}; };
FieldType GetFieldType(const std::string& field_name) { FieldType GetFieldType(const std::string& field_name) {

View File

@ -33,6 +33,7 @@ struct StreamDescriptor {
std::string hls_name; std::string hls_name;
std::string hls_group_id; std::string hls_group_id;
std::string hls_playlist_name; std::string hls_playlist_name;
int16_t trick_play_rate;
}; };
class StreamDescriptorCompareFn { class StreamDescriptorCompareFn {

View File

@ -27,7 +27,7 @@ const uint16_t kWidth = 10u;
const uint16_t kHeight = 20u; const uint16_t kHeight = 20u;
const uint32_t kPixelWidth = 2u; const uint32_t kPixelWidth = 2u;
const uint32_t kPixelHeight = 3u; const uint32_t kPixelHeight = 3u;
const int16_t kTrickPlayRate = 4; const int16_t kTrickPlayRate = 0;
const uint8_t kNaluLengthSize = 1u; const uint8_t kNaluLengthSize = 1u;
const bool kEncrypted = true; const bool kEncrypted = true;

View File

@ -63,6 +63,32 @@ std::shared_ptr<MediaSample> MediaSample::CopyFrom(const uint8_t* data,
side_data_size, is_key_frame); side_data_size, is_key_frame);
} }
// static
std::shared_ptr<MediaSample> MediaSample::CopyFrom(
const MediaSample& media_sample) {
std::shared_ptr<MediaSample> new_media_sample = CopyFrom(
media_sample.data(), media_sample.data_size(), media_sample.side_data(),
media_sample.side_data_size(), media_sample.is_key_frame());
new_media_sample->set_dts(media_sample.dts());
new_media_sample->set_pts(media_sample.pts());
new_media_sample->set_is_encrypted(media_sample.is_encrypted());
new_media_sample->set_config_id(media_sample.config_id());
new_media_sample->set_duration(media_sample.duration());
if (media_sample.decrypt_config()) {
std::unique_ptr<DecryptConfig> decrypt_config(
new DecryptConfig(media_sample.decrypt_config()->key_id(),
media_sample.decrypt_config()->iv(),
media_sample.decrypt_config()->subsamples(),
media_sample.decrypt_config()->protection_scheme(),
media_sample.decrypt_config()->crypt_byte_block(),
media_sample.decrypt_config()->skip_byte_block()));
new_media_sample->set_decrypt_config(std::move(decrypt_config));
}
return new_media_sample;
}
// static // static
std::shared_ptr<MediaSample> MediaSample::FromMetadata(const uint8_t* metadata, std::shared_ptr<MediaSample> MediaSample::FromMetadata(const uint8_t* metadata,
size_t metadata_size) { size_t metadata_size) {

View File

@ -45,6 +45,10 @@ class MediaSample {
size_t side_data_size, size_t side_data_size,
bool is_key_frame); bool is_key_frame);
/// Make a copy of MediaSample.
/// @param media_sample is the source MediaSample to copy from.
static std::shared_ptr<MediaSample> CopyFrom(const MediaSample& media_sample);
/// Create a MediaSample object from metadata. /// Create a MediaSample object from metadata.
/// Unlike other factory methods, this cannot be a key frame. It must be only /// Unlike other factory methods, this cannot be a key frame. It must be only
/// for metadata. /// for metadata.
@ -108,12 +112,12 @@ class MediaSample {
} }
const uint8_t* data() const { const uint8_t* data() const {
DCHECK(!end_of_stream()); DCHECK(!end_of_stream());
return &data_[0]; return data_.data();
} }
uint8_t* writable_data() { uint8_t* writable_data() {
DCHECK(!end_of_stream()); DCHECK(!end_of_stream());
return &data_[0]; return data_.data();
} }
size_t data_size() const { size_t data_size() const {
@ -122,7 +126,7 @@ class MediaSample {
} }
const uint8_t* side_data() const { const uint8_t* side_data() const {
return &side_data_[0]; return side_data_.data();
} }
size_t side_data_size() const { size_t side_data_size() const {

View File

@ -78,6 +78,9 @@ enum Code {
// The entity that a client attempted to create (e.g., file or directory) // The entity that a client attempted to create (e.g., file or directory)
// already exists. // already exists.
ALREADY_EXISTS, ALREADY_EXISTS,
// Error when trying to generate trick play stream.
TRICK_PLAY_ERROR,
}; };
} // namespace error } // namespace error

View File

@ -56,6 +56,8 @@ enum Codec {
/// Abstract class holds stream information. /// Abstract class holds stream information.
class StreamInfo { class StreamInfo {
public: public:
StreamInfo() = default;
StreamInfo(StreamType stream_type, int track_id, uint32_t time_scale, StreamInfo(StreamType stream_type, int track_id, uint32_t time_scale,
uint64_t duration, Codec codec, const std::string& codec_string, uint64_t duration, Codec codec, const std::string& codec_string,
const uint8_t* codec_config, size_t codec_config_size, const uint8_t* codec_config, size_t codec_config_size,

View File

@ -22,6 +22,8 @@ enum class H26xStreamFormat {
/// Holds video stream information. /// Holds video stream information.
class VideoStreamInfo : public StreamInfo { class VideoStreamInfo : public StreamInfo {
public: public:
VideoStreamInfo() = default;
/// Construct an initialized video stream info object. /// Construct an initialized video stream info object.
/// @param pixel_width is the width of the pixel. 0 if unknown. /// @param pixel_width is the width of the pixel. 0 if unknown.
/// @param pixel_height is the height of the pixels. 0 if unknown. /// @param pixel_height is the height of the pixels. 0 if unknown.
@ -67,6 +69,9 @@ class VideoStreamInfo : public StreamInfo {
void set_height(uint32_t height) { height_ = height; } void set_height(uint32_t height) { height_ = height; }
void set_pixel_width(uint32_t pixel_width) { pixel_width_ = pixel_width; } 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_pixel_height(uint32_t pixel_height) { pixel_height_ = pixel_height; }
void set_trick_play_rate(int16_t trick_play_rate) {
trick_play_rate_ = trick_play_rate;
}
void set_eme_init_data(const uint8_t* eme_init_data, void set_eme_init_data(const uint8_t* eme_init_data,
size_t eme_init_data_size) { size_t eme_init_data_size) {
eme_init_data_.assign(eme_init_data, eme_init_data + eme_init_data_size); eme_init_data_.assign(eme_init_data, eme_init_data + eme_init_data_size);

View File

@ -0,0 +1,38 @@
# Copyright 2017 Google Inc. All rights reserved.
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd
{
'includes': [
'../../common.gypi',
],
'targets': [
{
'target_name': 'trick_play',
'type': '<(component)',
'sources': [
'trick_play_handler.cc',
'trick_play_handler.h',
],
'dependencies': [
'../base/media_base.gyp:media_base',
],
},
{
'target_name': 'trick_play_unittest',
'type': '<(gtest_target_type)',
'sources': [
'trick_play_handler_unittest.cc',
],
'dependencies': [
'../../testing/gtest.gyp:gtest',
'../../testing/gmock.gyp:gmock',
'../base/media_base.gyp:media_handler_test_base',
'../test/media_test.gyp:media_test_support',
'trick_play',
]
},
],
}

View File

@ -0,0 +1,161 @@
// Copyright 2017 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/trick_play/trick_play_handler.h"
#include "packager/base/logging.h"
#include "packager/media/base/video_stream_info.h"
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);
}
}
TrickPlayHandler::~TrickPlayHandler() {}
Status TrickPlayHandler::InitializeInternal() {
return Status::OK;
}
Status TrickPlayHandler::Process(
std::unique_ptr<StreamData> input_stream_data) {
// The non-trick play stream is dispatched at index 0.
// The trick-play streams are dispatched to index 1, index 2 and so on.
DCHECK_EQ(input_stream_data->stream_index, 0u);
std::unique_ptr<StreamData> output_stream_data(new StreamData());
*output_stream_data = *input_stream_data;
Status status = Dispatch(std::move(output_stream_data));
if (!status.ok()) {
return status;
}
std::shared_ptr<StreamData> stream_data(std::move(input_stream_data));
if (stream_data->stream_data_type == StreamDataType::kStreamInfo) {
if (stream_data->stream_info->stream_type() != kStreamVideo) {
status.SetError(error::TRICK_PLAY_ERROR,
"Trick play does not support non-video stream");
return status;
}
const VideoStreamInfo& video_stream_info =
static_cast<const VideoStreamInfo&>(*stream_data->stream_info);
if (video_stream_info.trick_play_rate() > 0) {
status.SetError(error::TRICK_PLAY_ERROR,
"This stream is alreay a trick play stream.");
return status;
}
}
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
// the stream is flushed.
for (size_t i = 0; i < cached_stream_data_.size(); ++i)
cached_stream_data_[i].push_back(stream_data);
return Status::OK;
}
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());
for (size_t i = 0; i < cached_stream_data_.size(); ++i) {
int16_t rate = trick_play_options_.trick_play_rates[i];
if (total_key_frames_ % rate == 0) {
if (!cached_stream_data_[i].empty()) {
Status status =
ProcessCachedStreamData(i + 1, &cached_stream_data_[i]);
if (!status.ok())
return status;
}
cached_stream_data_[i].push_back(stream_data);
}
}
total_key_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();
};
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) {
ProcessCachedStreamData(i + 1, &cached_stream_data_[i]);
}
return MediaHandler::FlushDownstream(input_stream_index);
}
Status TrickPlayHandler::ProcessCachedStreamData(
int output_stream_index,
std::deque<std::shared_ptr<StreamData>>* cached_stream_data) {
while (!cached_stream_data->empty()) {
Status status =
ProcessOneStreamData(output_stream_index, cached_stream_data->front());
if (!status.ok()) {
return status;
}
cached_stream_data->pop_front();
}
return Status::OK;
}
Status TrickPlayHandler::ProcessOneStreamData(
int 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];
Status status;
switch (stream_data->stream_data_type) {
// trick_play_rate in StreamInfo should be modified.
case StreamDataType::kStreamInfo: {
const VideoStreamInfo& video_stream_info =
static_cast<const VideoStreamInfo&>(*stream_data->stream_info);
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);
status =
DispatchStreamInfo(output_stream_index, trick_play_video_stream_info);
break;
}
case StreamDataType::kMediaSample: {
if (stream_data->media_sample->is_key_frame()) {
std::shared_ptr<MediaSample> trick_play_media_sample =
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);
}
break;
}
default:
std::unique_ptr<StreamData> new_stream_data(new StreamData(*stream_data));
new_stream_data->stream_index = output_stream_index;
status = Dispatch(std::move(new_stream_data));
break;
}
return status;
}
} // namespace media
} // namespace shaka

View File

@ -0,0 +1,94 @@
// Copyright 2017 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#ifndef PACKAGER_MEDIA_BASE_TRICK_PLAY_HANDLER_H_
#define PACKAGER_MEDIA_BASE_TRICK_PLAY_HANDLER_H_
#include "packager/media/base/media_handler.h"
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
// information need to be updated, including trick_play_rate in
// VideoStreamInfo, the duration in MediaSample (which makes sure there is no
// gap between the media sample dts). Since the duration information can be
// determined after getting the next media sample, a queue is used to cache the
// input stream data before the next key frame.
class TrickPlayHandler : public MediaHandler {
public:
explicit TrickPlayHandler(const TrickPlayOptions& trick_play_options);
~TrickPlayHandler() override;
protected:
/// @name MediaHandler implementation overrides.
/// @{
Status InitializeInternal() override;
Status Process(std::unique_ptr<StreamData> stream_data) override;
bool ValidateOutputStreamIndex(size_t stream_index) const override;
Status OnFlushRequest(size_t input_stream_index) override;
/// @}
private:
friend class TrickPlayHandlerTest;
// Process the cached stream data for one trick play stream.
// The cached data is dispatched to the |output_stream_index|.
// The |current_dts| is for updating the duration of key frames,
// so that there is no gap between consecutive key frames. When
// |current_dts| = -1, the original duration of the key frame is used.
Status ProcessCachedStreamData(
int output_stream_index,
std::deque<std::shared_ptr<StreamData>>* cached_stream_data);
// Process a single stream data. Depending on the stream data type, some
// information needs to be updated.
// Decoding timestamp for current key media sample. It is used for calculating
// the duration of previous key media sample, to make sure there is no gap
// between two key media samples.
Status ProcessOneStreamData(int output_stream_index,
const std::shared_ptr<StreamData>& stream_data);
const TrickPlayOptions trick_play_options_;
TrickPlayHandler(const TrickPlayHandler&) = delete;
TrickPlayHandler& operator=(const TrickPlayHandler&) = delete;
/// Num of key frames received.
uint32_t total_key_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;
// 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
// be dispatched to different trick play stream, each trick play stream need
// its own queue to handle the synchronization.
// TODO(hmchen): Use one queue and multiple iterators, instead of multiple
// queues.
std::vector<std::deque<std::shared_ptr<StreamData>>> cached_stream_data_;
};
} // namespace media
} // namespace shaka
#endif // PACKAGER_MEDIA_BASE_TRICK_PLAY_HANDLER_H_

View File

@ -0,0 +1,211 @@
// Copyright 2017 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd
#include "packager/media/trick_play/trick_play_handler.h"
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "packager/media/base/media_handler_test_base.h"
#include "packager/media/base/test/status_test_util.h"
#include "packager/media/base/video_stream_info.h"
using ::testing::ElementsAre;
using ::testing::IsEmpty;
namespace shaka {
namespace media {
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 bool kEncrypted = true;
} // namespace
MATCHER_P4(IsTrickPlayVideoStreamInfo,
stream_index,
time_scale,
encrypted,
trick_play_rate,
"") {
return arg->stream_index == stream_index &&
arg->stream_data_type == StreamDataType::kStreamInfo &&
arg->stream_info->time_scale() == time_scale &&
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;
}
MATCHER_P3(IsKeyFrameMediaSample, stream_index, timestamp, duration, "") {
return arg->stream_index == stream_index &&
arg->stream_data_type == StreamDataType::kMediaSample &&
arg->media_sample->dts() == timestamp &&
arg->media_sample->duration() == duration &&
arg->media_sample->is_key_frame() == true;
}
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_);
ASSERT_OK(trick_play_handler_->Initialize());
}
Status Process(std::unique_ptr<StreamData> stream_data) {
return trick_play_handler_->Process(std::move(stream_data));
}
Status FlushStream(size_t stream_index) {
return trick_play_handler_->OnFlushRequest(stream_index);
}
protected:
std::shared_ptr<TrickPlayHandler> trick_play_handler_;
};
// 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),
std::end(kTrickPlayRates));
SetUpTrickPlayHandler(trick_play_options);
Status status =
Process(GetAudioStreamInfoStreamData(kStreamIndex0, kTimeScale));
Status kExpectStatus(error::TRICK_PLAY_ERROR, "Some Messages");
EXPECT_TRUE(status.Matches(kExpectStatus));
}
// 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),
std::end(kTrickPlayRates));
SetUpTrickPlayHandler(trick_play_options);
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),
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),
// 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),
// Frame 0, TrickPlayRate = 1.
IsKeyFrameMediaSample(kStreamIndex1, 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),
// Frame 3, TrickPlayRate = 1.
IsKeyFrameMediaSample(kStreamIndex1,
kVideoStartTimestamp + kDuration * 3,
kDuration * 3),
// Frame 0, TrickPlayRate = 2.
IsKeyFrameMediaSample(kStreamIndex2, kVideoStartTimestamp,
kDuration * 6),
// Frame 7.
IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration * 7,
kDuration, !kEncrypted)));
ClearOutputStreamDataVector();
ASSERT_OK(FlushStream(0));
EXPECT_THAT(GetOutputStreamDataVector(),
ElementsAre(
// Frame 6, TrickPlayRate = 1.
IsKeyFrameMediaSample(kStreamIndex1,
kVideoStartTimestamp + kDuration * 6,
kDuration * 2),
// Frame 6, TrickPlayRate = 2.
IsKeyFrameMediaSample(kStreamIndex2,
kVideoStartTimestamp + kDuration * 6,
kDuration * 2),
// Frame 0, TrickPlayRate = 4.
IsKeyFrameMediaSample(kStreamIndex3, kVideoStartTimestamp,
kDuration * 8)));
ClearOutputStreamDataVector();
// Flush again, get nothing.
ASSERT_OK(FlushStream(0));
EXPECT_THAT(GetOutputStreamDataVector(), IsEmpty());
}
} // namespace media
} // namespace shaka

View File

@ -127,6 +127,7 @@
'media/formats/webm/webm.gyp:webm_unittest', 'media/formats/webm/webm.gyp:webm_unittest',
'media/formats/webvtt/webvtt.gyp:webvtt_unittest', 'media/formats/webvtt/webvtt.gyp:webvtt_unittest',
'media/formats/wvm/wvm.gyp:wvm_unittest', 'media/formats/wvm/wvm.gyp:wvm_unittest',
'media/trick_play/trick_play.gyp:trick_play_unittest',
'mpd/mpd.gyp:mpd_unittest', 'mpd/mpd.gyp:mpd_unittest',
'packager_test', 'packager_test',
], ],