From cc04698460b1307d03b0e74bb8d3a6b068dc6506 Mon Sep 17 00:00:00 2001 From: Haoming Chen Date: Mon, 27 Feb 2017 09:22:25 -0800 Subject: [PATCH] Implement trick play handler. Change-Id: I3d288dc7c4b77fd3aca1bad2ce96ad733384bcc8 --- packager/app/stream_descriptor.cc | 2 + packager/app/stream_descriptor.h | 1 + .../media/base/media_handler_test_base.cc | 2 +- packager/media/base/media_sample.cc | 26 +++ packager/media/base/media_sample.h | 10 +- packager/media/base/status.h | 3 + packager/media/base/stream_info.h | 2 + packager/media/base/video_stream_info.h | 5 + packager/media/trick_play/trick_play.gyp | 38 ++++ .../media/trick_play/trick_play_handler.cc | 161 +++++++++++++ .../media/trick_play/trick_play_handler.h | 94 ++++++++ .../trick_play/trick_play_handler_unittest.cc | 211 ++++++++++++++++++ packager/packager.gyp | 1 + 13 files changed, 552 insertions(+), 4 deletions(-) create mode 100644 packager/media/trick_play/trick_play.gyp create mode 100644 packager/media/trick_play/trick_play_handler.cc create mode 100644 packager/media/trick_play/trick_play_handler.h create mode 100644 packager/media/trick_play/trick_play_handler_unittest.cc diff --git a/packager/app/stream_descriptor.cc b/packager/app/stream_descriptor.cc index d19b3daedb..e314316302 100644 --- a/packager/app/stream_descriptor.cc +++ b/packager/app/stream_descriptor.cc @@ -30,6 +30,7 @@ enum FieldType { kHlsNameField, kHlsGroupIdField, kHlsPlaylistNameField, + kTrickPlayRateField, }; struct FieldNameToTypeMapping { @@ -57,6 +58,7 @@ const FieldNameToTypeMapping kFieldNameTypeMappings[] = { {"hls_name", kHlsNameField}, {"hls_group_id", kHlsGroupIdField}, {"playlist_name", kHlsPlaylistNameField}, + {"trick_play_rate", kTrickPlayRateField}, }; FieldType GetFieldType(const std::string& field_name) { diff --git a/packager/app/stream_descriptor.h b/packager/app/stream_descriptor.h index c394cc3817..ab6ef368ee 100644 --- a/packager/app/stream_descriptor.h +++ b/packager/app/stream_descriptor.h @@ -33,6 +33,7 @@ struct StreamDescriptor { std::string hls_name; std::string hls_group_id; std::string hls_playlist_name; + int16_t trick_play_rate; }; class StreamDescriptorCompareFn { diff --git a/packager/media/base/media_handler_test_base.cc b/packager/media/base/media_handler_test_base.cc index c1b336441e..d4a92ae2fb 100644 --- a/packager/media/base/media_handler_test_base.cc +++ b/packager/media/base/media_handler_test_base.cc @@ -27,7 +27,7 @@ const uint16_t kWidth = 10u; const uint16_t kHeight = 20u; const uint32_t kPixelWidth = 2u; const uint32_t kPixelHeight = 3u; -const int16_t kTrickPlayRate = 4; +const int16_t kTrickPlayRate = 0; const uint8_t kNaluLengthSize = 1u; const bool kEncrypted = true; diff --git a/packager/media/base/media_sample.cc b/packager/media/base/media_sample.cc index f52f8f99d6..f055705ee7 100644 --- a/packager/media/base/media_sample.cc +++ b/packager/media/base/media_sample.cc @@ -63,6 +63,32 @@ std::shared_ptr MediaSample::CopyFrom(const uint8_t* data, side_data_size, is_key_frame); } +// static +std::shared_ptr MediaSample::CopyFrom( + const MediaSample& media_sample) { + std::shared_ptr 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 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 std::shared_ptr MediaSample::FromMetadata(const uint8_t* metadata, size_t metadata_size) { diff --git a/packager/media/base/media_sample.h b/packager/media/base/media_sample.h index f9c23433ed..601cab674a 100644 --- a/packager/media/base/media_sample.h +++ b/packager/media/base/media_sample.h @@ -45,6 +45,10 @@ class MediaSample { size_t side_data_size, bool is_key_frame); + /// Make a copy of MediaSample. + /// @param media_sample is the source MediaSample to copy from. + static std::shared_ptr CopyFrom(const MediaSample& media_sample); + /// Create a MediaSample object from metadata. /// Unlike other factory methods, this cannot be a key frame. It must be only /// for metadata. @@ -108,12 +112,12 @@ class MediaSample { } const uint8_t* data() const { DCHECK(!end_of_stream()); - return &data_[0]; + return data_.data(); } uint8_t* writable_data() { DCHECK(!end_of_stream()); - return &data_[0]; + return data_.data(); } size_t data_size() const { @@ -122,7 +126,7 @@ class MediaSample { } const uint8_t* side_data() const { - return &side_data_[0]; + return side_data_.data(); } size_t side_data_size() const { diff --git a/packager/media/base/status.h b/packager/media/base/status.h index ed695b7086..45f86d4880 100644 --- a/packager/media/base/status.h +++ b/packager/media/base/status.h @@ -78,6 +78,9 @@ enum Code { // The entity that a client attempted to create (e.g., file or directory) // already exists. ALREADY_EXISTS, + + // Error when trying to generate trick play stream. + TRICK_PLAY_ERROR, }; } // namespace error diff --git a/packager/media/base/stream_info.h b/packager/media/base/stream_info.h index 538f5e5248..742d05d396 100644 --- a/packager/media/base/stream_info.h +++ b/packager/media/base/stream_info.h @@ -56,6 +56,8 @@ enum Codec { /// Abstract class holds stream information. class StreamInfo { public: + StreamInfo() = default; + StreamInfo(StreamType stream_type, int track_id, uint32_t time_scale, uint64_t duration, Codec codec, const std::string& codec_string, const uint8_t* codec_config, size_t codec_config_size, diff --git a/packager/media/base/video_stream_info.h b/packager/media/base/video_stream_info.h index d5e26898b6..c462a5019b 100644 --- a/packager/media/base/video_stream_info.h +++ b/packager/media/base/video_stream_info.h @@ -22,6 +22,8 @@ enum class H26xStreamFormat { /// Holds video stream information. class VideoStreamInfo : public StreamInfo { public: + VideoStreamInfo() = default; + /// Construct an initialized video stream info object. /// @param pixel_width is the width of the pixel. 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_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) { + trick_play_rate_ = trick_play_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); diff --git a/packager/media/trick_play/trick_play.gyp b/packager/media/trick_play/trick_play.gyp new file mode 100644 index 0000000000..5263ccd68d --- /dev/null +++ b/packager/media/trick_play/trick_play.gyp @@ -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', + ] + }, + ], +} diff --git a/packager/media/trick_play/trick_play_handler.cc b/packager/media/trick_play/trick_play_handler.cc new file mode 100644 index 0000000000..af6d2f1b23 --- /dev/null +++ b/packager/media/trick_play/trick_play_handler.cc @@ -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 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 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 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(*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>* 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& 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(*stream_data->stream_info); + std::shared_ptr 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 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 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 diff --git a/packager/media/trick_play/trick_play_handler.h b/packager/media/trick_play/trick_play_handler.h new file mode 100644 index 0000000000..a3cf94d3f8 --- /dev/null +++ b/packager/media/trick_play/trick_play_handler.h @@ -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 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 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>* 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& 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>> cached_stream_data_; +}; + +} // namespace media +} // namespace shaka + +#endif // PACKAGER_MEDIA_BASE_TRICK_PLAY_HANDLER_H_ diff --git a/packager/media/trick_play/trick_play_handler_unittest.cc b/packager/media/trick_play/trick_play_handler_unittest.cc new file mode 100644 index 0000000000..0cf9795992 --- /dev/null +++ b/packager/media/trick_play/trick_play_handler_unittest.cc @@ -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 +#include + +#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(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 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 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 diff --git a/packager/packager.gyp b/packager/packager.gyp index 2232251ad6..304ad3af88 100644 --- a/packager/packager.gyp +++ b/packager/packager.gyp @@ -127,6 +127,7 @@ 'media/formats/webm/webm.gyp:webm_unittest', 'media/formats/webvtt/webvtt.gyp:webvtt_unittest', 'media/formats/wvm/wvm.gyp:wvm_unittest', + 'media/trick_play/trick_play.gyp:trick_play_unittest', 'mpd/mpd.gyp:mpd_unittest', 'packager_test', ],