Implement trick play handler.
Change-Id: I3d288dc7c4b77fd3aca1bad2ce96ad733384bcc8
This commit is contained in:
parent
0a3ef49c11
commit
cc04698460
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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',
|
||||||
|
]
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
|
@ -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
|
|
@ -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_
|
|
@ -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
|
|
@ -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',
|
||||||
],
|
],
|
||||||
|
|
Loading…
Reference in New Issue