From 9990524f98c3cfc62b1560e6a2db4678b4375144 Mon Sep 17 00:00:00 2001 From: Kongqun Yang Date: Tue, 7 Feb 2017 10:58:47 -0800 Subject: [PATCH] Implement ChunkingHandler This handler is a multi-in multi-out handler. If more than one input is provided, there should be one and only one video stream; also, all inputs should come from the same thread and are synchronized. There can be multiple chunking handler running in different threads or even different processes, we use the "consistent chunking algorithm" to make sure the chunks in different streams are aligned without explicit communcating with each other - which is not efficient and often difficult. Consistent Chunking Algorithm: 1. Find the consistent chunkable boundary Let the timestamps for video frames be (t1, t2, t3, ...). Then a consistent chunkable boundary is simply the first chunkable boundary after (tk / N) != (tk-1 / N), where '/' denotes integer division, and N is the intended chunk duration. 2. Chunk only at the consistent chunkable boundary This algorithm will make sure the chunks from different video streams are aligned if they have aligned GoPs. However, this algorithm will only work for video streams. To be able to chunk non video streams at similar positions as video streams, ChunkingHandler is designed to accept one video input and multiple non video inputs, the non video inputs are chunked when the video input is chunked. If the inputs are synchronized - which is true if the inputs come from the same demuxer, the video and non video chunks are aligned. Change-Id: Id3bad51ab14f311efdb8713b6cd36d36cf9e4639 --- packager/media/base/media_base.gyp | 12 + packager/media/base/media_handler.h | 14 +- .../media/base/media_handler_test_base.cc | 154 +++++++++++ packager/media/base/media_handler_test_base.h | 98 +++++++ packager/media/base/status.cc | 8 + packager/media/base/status.h | 3 + packager/media/base/stream_info.h | 14 +- packager/media/chunking/chunking.gyp | 39 +++ packager/media/chunking/chunking_handler.cc | 252 ++++++++++++++++++ packager/media/chunking/chunking_handler.h | 123 +++++++++ .../chunking/chunking_handler_unittest.cc | 237 ++++++++++++++++ packager/media/crypto/crypto.gyp | 1 + packager/media/crypto/encryption_handler.cc | 13 +- packager/media/crypto/encryption_handler.h | 5 +- .../crypto/encryption_handler_unittest.cc | 118 ++------ packager/packager.gyp | 1 + 16 files changed, 973 insertions(+), 119 deletions(-) create mode 100644 packager/media/base/media_handler_test_base.cc create mode 100644 packager/media/base/media_handler_test_base.h create mode 100644 packager/media/chunking/chunking.gyp create mode 100644 packager/media/chunking/chunking_handler.cc create mode 100644 packager/media/chunking/chunking_handler.h create mode 100644 packager/media/chunking/chunking_handler_unittest.cc diff --git a/packager/media/base/media_base.gyp b/packager/media/base/media_base.gyp index 86f5b114a8..608d46bb9e 100644 --- a/packager/media/base/media_base.gyp +++ b/packager/media/base/media_base.gyp @@ -118,6 +118,18 @@ '../../third_party/protobuf/protobuf.gyp:protobuf_lite', ], }, + { + 'target_name': 'media_handler_test_base', + 'type': '<(component)', + 'sources': [ + 'media_handler_test_base.cc', + 'media_handler_test_base.h', + ], + 'dependencies': [ + '../../testing/gmock.gyp:gmock', + '../../testing/gtest.gyp:gtest', + ], + }, { 'target_name': 'media_base_unittest', 'type': '<(gtest_target_type)', diff --git a/packager/media/base/media_handler.h b/packager/media/base/media_handler.h index 799a227e18..b74fa65b0b 100644 --- a/packager/media/base/media_handler.h +++ b/packager/media/base/media_handler.h @@ -35,14 +35,14 @@ struct MediaEvent {}; struct SegmentInfo { bool is_subsegment = false; bool is_encrypted = false; - uint64_t start_timestamp = 0; - uint64_t duration = 0; + int64_t start_timestamp = -1; + int64_t duration = 0; }; // TODO(kqyang): Should we use protobuf? struct StreamData { - int stream_index; - StreamDataType stream_data_type; + int stream_index = -1; + StreamDataType stream_data_type = StreamDataType::kUnknown; std::unique_ptr period_info; std::unique_ptr stream_info; @@ -114,6 +114,7 @@ class MediaHandler { std::unique_ptr period_info) { std::unique_ptr stream_data(new StreamData); stream_data->stream_index = stream_index; + stream_data->stream_data_type = StreamDataType::kPeriodInfo; stream_data->period_info = std::move(period_info); return Dispatch(std::move(stream_data)); } @@ -123,6 +124,7 @@ class MediaHandler { std::unique_ptr stream_info) { std::unique_ptr stream_data(new StreamData); stream_data->stream_index = stream_index; + stream_data->stream_data_type = StreamDataType::kStreamInfo; stream_data->stream_info = std::move(stream_info); return Dispatch(std::move(stream_data)); } @@ -133,6 +135,7 @@ class MediaHandler { std::unique_ptr encryption_config) { std::unique_ptr stream_data(new StreamData); stream_data->stream_index = stream_index; + stream_data->stream_data_type = StreamDataType::kEncryptionConfig; stream_data->encryption_config = std::move(encryption_config); return Dispatch(std::move(stream_data)); } @@ -142,6 +145,7 @@ class MediaHandler { std::unique_ptr media_sample) { std::unique_ptr stream_data(new StreamData); stream_data->stream_index = stream_index; + stream_data->stream_data_type = StreamDataType::kMediaSample; stream_data->media_sample = std::move(media_sample); return Dispatch(std::move(stream_data)); } @@ -151,6 +155,7 @@ class MediaHandler { std::unique_ptr media_event) { std::unique_ptr stream_data(new StreamData); stream_data->stream_index = stream_index; + stream_data->stream_data_type = StreamDataType::kMediaEvent; stream_data->media_event = std::move(media_event); return Dispatch(std::move(stream_data)); } @@ -160,6 +165,7 @@ class MediaHandler { std::unique_ptr segment_info) { std::unique_ptr stream_data(new StreamData); stream_data->stream_index = stream_index; + stream_data->stream_data_type = StreamDataType::kSegmentInfo; stream_data->segment_info = std::move(segment_info); return Dispatch(std::move(stream_data)); } diff --git a/packager/media/base/media_handler_test_base.cc b/packager/media/base/media_handler_test_base.cc new file mode 100644 index 0000000000..d4630a7f7c --- /dev/null +++ b/packager/media/base/media_handler_test_base.cc @@ -0,0 +1,154 @@ +// 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/base/media_handler_test_base.h" + +#include "packager/media/base/audio_stream_info.h" +#include "packager/media/base/test/status_test_util.h" +#include "packager/media/base/video_stream_info.h" + +namespace { + +const int kTrackId = 1; +const uint64_t kDuration = 10000; +const char kCodecString[] = "codec string"; +const uint8_t kSampleBits = 1; +const uint8_t kNumChannels = 2; +const uint32_t kSamplingFrequency = 48000; +const uint64_t kSeekPrerollNs = 12345; +const uint64_t kCodecDelayNs = 56789; +const uint32_t kMaxBitrate = 13579; +const uint32_t kAvgBitrate = 13000; +const char kLanguage[] = "eng"; +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 uint8_t kNaluLengthSize = 1u; +const bool kEncrypted = true; + +// Use H264 code config. +const uint8_t kCodecConfig[]{ + // Header + 0x01, 0x64, 0x00, 0x1e, 0xff, + // SPS count (ignore top three bits) + 0xe1, + // SPS + 0x00, 0x19, // Size + 0x67, 0x64, 0x00, 0x1e, 0xac, 0xd9, 0x40, 0xa0, 0x2f, 0xf9, 0x70, 0x11, + 0x00, 0x00, 0x03, 0x03, 0xe9, 0x00, 0x00, 0xea, 0x60, 0x0f, 0x16, 0x2d, + 0x96, + // PPS count + 0x01, + // PPS + 0x00, 0x06, // Size + 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, +}; + +// Mock data, we don't really care about what is inside. +const uint8_t kData[]{ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, +}; + +} // namespace + +namespace shaka { +namespace media { + +// A fake media handler definition used for testing. +class FakeMediaHandler : public MediaHandler { + public: + const std::vector>& stream_data_vector() const { + return stream_data_vector_; + } + void clear_stream_data_vector() { stream_data_vector_.clear(); } + + protected: + Status InitializeInternal() override { return Status::OK; } + Status Process(std::unique_ptr stream_data) override { + stream_data_vector_.push_back(std::move(stream_data)); + return Status::OK; + } + Status FlushStream(int input_stream_index) override { return Status::OK; } + bool ValidateOutputStreamIndex(int stream_index) const override { + return true; + } + + std::vector> stream_data_vector_; +}; + +MediaHandlerTestBase::MediaHandlerTestBase() + : next_handler_(new FakeMediaHandler), + some_handler_(new FakeMediaHandler) {} + +std::unique_ptr MediaHandlerTestBase::GetStreamInfoStreamData( + int stream_index, + Codec codec, + uint32_t time_scale) { + std::unique_ptr stream_data(new StreamData); + stream_data->stream_index = stream_index; + stream_data->stream_data_type = StreamDataType::kStreamInfo; + stream_data->stream_info = GetMockStreamInfo(codec, time_scale); + return stream_data; +} + +std::unique_ptr MediaHandlerTestBase::GetMediaSampleStreamData( + int stream_index, + int64_t timestamp, + int64_t duration, + bool is_keyframe) { + std::unique_ptr stream_data(new StreamData); + stream_data->stream_index = stream_index; + stream_data->stream_data_type = StreamDataType::kMediaSample; + stream_data->media_sample.reset( + new MediaSample(kData, sizeof(kData), nullptr, 0, is_keyframe)); + stream_data->media_sample->set_dts(timestamp); + stream_data->media_sample->set_duration(duration); + return stream_data; +} + +void MediaHandlerTestBase::SetUpGraph(int num_inputs, + int num_outputs, + std::shared_ptr handler) { + // Input handler is not really used anywhere but just to satisfy one input + // one output restriction for the encryption handler. + auto input_handler = std::make_shared(); + for (int i = 0; i < num_inputs; ++i) + ASSERT_OK(input_handler->SetHandler(i, handler)); + // All outputs are routed to |next_handler_|. + for (int i = 0; i < num_outputs; ++i) + ASSERT_OK(handler->SetHandler(i, next_handler_)); +} + +const std::vector>& +MediaHandlerTestBase::GetOutputStreamDataVector() const { + return next_handler_->stream_data_vector(); +} + +void MediaHandlerTestBase::ClearOutputStreamDataVector() { + next_handler_->clear_stream_data_vector(); +} + +std::unique_ptr MediaHandlerTestBase::GetMockStreamInfo( + Codec codec, uint32_t time_scale) { + if (codec >= kCodecAudio && codec < kCodecAudioMaxPlusOne) { + return std::unique_ptr(new AudioStreamInfo( + kTrackId, time_scale, kDuration, codec, kCodecString, kCodecConfig, + sizeof(kCodecConfig), kSampleBits, kNumChannels, kSamplingFrequency, + kSeekPrerollNs, kCodecDelayNs, kMaxBitrate, kAvgBitrate, kLanguage, + !kEncrypted)); + } else if (codec >= kCodecVideo && codec < kCodecVideoMaxPlusOne) { + return std::unique_ptr(new VideoStreamInfo( + kTrackId, time_scale, kDuration, codec, kCodecString, kCodecConfig, + sizeof(kCodecConfig), kWidth, kHeight, kPixelWidth, kPixelHeight, + kTrickPlayRate, kNaluLengthSize, kLanguage, !kEncrypted)); + } + return nullptr; +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/base/media_handler_test_base.h b/packager/media/base/media_handler_test_base.h new file mode 100644 index 0000000000..e6286e29f0 --- /dev/null +++ b/packager/media/base/media_handler_test_base.h @@ -0,0 +1,98 @@ +// 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 +#include + +#include "packager/media/base/media_handler.h" + +namespace shaka { +namespace media { + +class FakeMediaHandler; + +MATCHER_P3(IsStreamInfo, stream_index, time_scale, encrypted, "") { + 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; +} + +MATCHER_P4(IsSegmentInfo, stream_index, timestamp, duration, subsegment, "") { + return arg->stream_index == stream_index && + arg->stream_data_type == StreamDataType::kSegmentInfo && + arg->segment_info->start_timestamp == timestamp && + arg->segment_info->duration == duration && + arg->segment_info->is_subsegment == subsegment; +} + +MATCHER_P3(IsMediaSample, 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; +} + +class MediaHandlerTestBase : public ::testing::Test { + public: + MediaHandlerTestBase(); + + /// Return a stream data with mock stream info. + std::unique_ptr GetStreamInfoStreamData(int stream_index, + Codec codec, + uint32_t time_scale); + + /// Return a stream data with mock video stream info. + std::unique_ptr GetVideoStreamInfoStreamData( + int stream_index, + uint32_t time_scale) { + return GetStreamInfoStreamData(stream_index, kCodecVP9, time_scale); + } + + /// Return a stream data with mock audio stream info. + std::unique_ptr GetAudioStreamInfoStreamData( + int stream_index, + uint32_t time_scale) { + return GetStreamInfoStreamData(stream_index, kCodecAAC, time_scale); + } + + /// Return a stream data with mock media sample. + std::unique_ptr GetMediaSampleStreamData(int stream_index, + int64_t timestamp, + int64_t duration, + bool is_keyframe); + + /// Setup a graph using |handler| with |num_inputs| and |num_outputs|. + void SetUpGraph(int num_inputs, + int num_outputs, + std::shared_ptr handler); + + /// Return the output stream data vector from handler. + const std::vector>& GetOutputStreamDataVector() + const; + + /// Clear the output stream data vector. + void ClearOutputStreamDataVector(); + + /// @return some random handler that can be used for testing. + std::shared_ptr some_handler() { return some_handler_; } + + private: + MediaHandlerTestBase(const MediaHandlerTestBase&) = delete; + MediaHandlerTestBase& operator=(const MediaHandlerTestBase&) = delete; + + // Get a mock stream info for testing. + std::unique_ptr GetMockStreamInfo(Codec codec, + uint32_t time_scale); + + // Downstream handler used in testing graph. + std::shared_ptr next_handler_; + // Some random handler which can be used for testing. + std::shared_ptr some_handler_; +}; + +} // namespace media +} // namespace shaka diff --git a/packager/media/base/status.cc b/packager/media/base/status.cc index df06a97aad..aa5c3802d9 100644 --- a/packager/media/base/status.cc +++ b/packager/media/base/status.cc @@ -34,6 +34,10 @@ std::string ErrorCodeToString(Code error_code) { return "HTTP_FAILURE"; case PARSER_FAILURE: return "PARSER_FAILURE"; + case ENCRYPTION_FAILURE: + return "ENCRYPTION_FAILURE"; + case CHUNKING_ERROR: + return "CHUNKING_ERROR"; case MUXER_FAILURE: return "MUXER_FAILURE"; case FRAGMENT_FINALIZED: @@ -46,6 +50,10 @@ std::string ErrorCodeToString(Code error_code) { return "STOPPED"; case TIME_OUT: return "TIME_OUT"; + case NOT_FOUND: + return "NOT_FOUND"; + case ALREADY_EXISTS: + return "ALREADY_EXISTS"; default: NOTIMPLEMENTED() << "Unknown Status Code: " << error_code; return "UNKNOWN_STATUS"; diff --git a/packager/media/base/status.h b/packager/media/base/status.h index 6316ea1f29..ed695b7086 100644 --- a/packager/media/base/status.h +++ b/packager/media/base/status.h @@ -51,6 +51,9 @@ enum Code { // Failed to do the encryption. ENCRYPTION_FAILURE, + // Error when trying to do chunking. + CHUNKING_ERROR, + // Fail to mux the media file. MUXER_FAILURE, diff --git a/packager/media/base/stream_info.h b/packager/media/base/stream_info.h index cd7a8e1039..76b7b178ba 100644 --- a/packager/media/base/stream_info.h +++ b/packager/media/base/stream_info.h @@ -22,7 +22,9 @@ enum StreamType { enum Codec { kUnknownCodec = 0, - kCodecH264, + + kCodecVideo = 100, + kCodecH264 = kCodecVideo, kCodecHEV1, kCodecHVC1, kCodecVC1, @@ -32,7 +34,10 @@ enum Codec { kCodecVP8, kCodecVP9, kCodecVP10, - kCodecAAC, + kCodecVideoMaxPlusOne, + + kCodecAudio = 200, + kCodecAAC = kCodecAudio, kCodecAC3, kCodecDTSC, kCodecDTSE, @@ -43,8 +48,9 @@ enum Codec { kCodecEAC3, kCodecOpus, kCodecVorbis, - kCodecText, - kNumCodec + kCodecAudioMaxPlusOne, + + kCodecText = 300, }; /// Abstract class holds stream information. diff --git a/packager/media/chunking/chunking.gyp b/packager/media/chunking/chunking.gyp new file mode 100644 index 0000000000..89c9ffbfe2 --- /dev/null +++ b/packager/media/chunking/chunking.gyp @@ -0,0 +1,39 @@ +# 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': 'chunking', + 'type': '<(component)', + 'sources': [ + 'chunking_handler.cc', + 'chunking_handler.h', + ], + 'dependencies': [ + '../base/media_base.gyp:media_base', + ], + }, + { + 'target_name': 'chunking_unittest', + 'type': '<(gtest_target_type)', + 'sources': [ + 'chunking_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', + 'chunking', + ] + }, + ], +} + diff --git a/packager/media/chunking/chunking_handler.cc b/packager/media/chunking/chunking_handler.cc new file mode 100644 index 0000000000..c173ddb291 --- /dev/null +++ b/packager/media/chunking/chunking_handler.cc @@ -0,0 +1,252 @@ +// 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/chunking/chunking_handler.h" + +#include "packager/base/logging.h" +#include "packager/base/threading/platform_thread.h" +#include "packager/media/base/media_sample.h" + +namespace { +int64_t kThreadIdUnset = -1; +int64_t kTimeStampToDispatchAllSamples = -1; +} // namespace + +namespace shaka { +namespace media { + +ChunkingHandler::ChunkingHandler(const ChunkingOptions& chunking_options) + : chunking_options_(chunking_options), thread_id_(kThreadIdUnset) { + CHECK_NE(chunking_options.segment_duration_in_seconds, 0u); +} + +ChunkingHandler::~ChunkingHandler() {} + +Status ChunkingHandler::InitializeInternal() { + segment_info_.resize(num_input_streams()); + subsegment_info_.resize(num_input_streams()); + time_scales_.resize(num_input_streams()); + last_sample_end_timestamps_.resize(num_input_streams()); + return Status::OK; +} + +Status ChunkingHandler::Process(std::unique_ptr stream_data) { + switch (stream_data->stream_data_type) { + case StreamDataType::kStreamInfo: { + // Make sure the inputs come from the same thread. + const int64_t thread_id = + static_cast(base::PlatformThread::CurrentId()); + int64_t expected = kThreadIdUnset; + if (!thread_id_.compare_exchange_strong(expected, thread_id) && + expected != thread_id) { + return Status(error::CHUNKING_ERROR, + "Inputs should come from the same thread."); + } + + const auto time_scale = stream_data->stream_info->time_scale(); + // The video stream is treated as the main stream. If there is only one + // stream, it is the main stream. + const bool is_main_stream = + main_stream_index_ == -1 && + (stream_data->stream_info->stream_type() == kStreamVideo || + num_input_streams() == 1); + if (is_main_stream) { + main_stream_index_ = stream_data->stream_index; + segment_duration_ = + chunking_options_.segment_duration_in_seconds * time_scale; + subsegment_duration_ = + chunking_options_.subsegment_duration_in_seconds * time_scale; + } else if (stream_data->stream_info->stream_type() == kStreamVideo) { + return Status(error::CHUNKING_ERROR, + "Only one video stream is allowed per chunking handler."); + } + time_scales_[stream_data->stream_index] = time_scale; + break; + } + case StreamDataType::kSegmentInfo: + VLOG(3) << "Drop existing segment info."; + return Status::OK; + case StreamDataType::kMediaSample: { + const int stream_index = stream_data->stream_index; + DCHECK_NE(time_scales_[stream_index], 0u) + << "kStreamInfo should arrive before kMediaSample"; + if (stream_index != main_stream_index_) { + if (!stream_data->media_sample->is_key_frame()) { + return Status(error::CHUNKING_ERROR, + "All non video samples should be key frames."); + } + // Cache non main stream samples, since we don't know yet whether these + // samples belong to the current or next segment. + non_main_samples_.push_back(std::move(stream_data)); + // The streams are expected to be synchronized, so we don't expect to + // see a lot of samples before seeing video samples. + const size_t kMaxSamplesPerStreamBeforeVideoSample = 5u; + if (non_main_samples_.size() > + num_input_streams() * kMaxSamplesPerStreamBeforeVideoSample) { + return Status(error::CHUNKING_ERROR, + "Too many non video samples before video sample."); + } + return Status::OK; + } + + const MediaSample* sample = stream_data->media_sample.get(); + Status status = ProcessMediaSample(sample); + if (!status.ok()) + return status; + // Discard samples before segment start. + if (!segment_info_[stream_index]) + return Status::OK; + last_sample_end_timestamps_[stream_index] = + sample->dts() + sample->duration(); + break; + } + default: + VLOG(3) << "Stream data type " + << static_cast(stream_data->stream_data_type) << " ignored."; + break; + } + return Dispatch(std::move(stream_data)); +} + +Status ChunkingHandler::FlushStream(int input_stream_index) { + if (segment_info_[input_stream_index]) { + Status status; + if (input_stream_index != main_stream_index_) { + status = DispatchNonMainSamples(kTimeStampToDispatchAllSamples); + if (!status.ok()) + return status; + } + auto& segment_info = segment_info_[input_stream_index]; + if (segment_info->start_timestamp != -1) { + segment_info->duration = last_sample_end_timestamps_[input_stream_index] - + segment_info->start_timestamp; + status = DispatchSegmentInfo(input_stream_index, std::move(segment_info)); + if (!status.ok()) + return status; + } + } + return MediaHandler::FlushStream(input_stream_index); +} + +Status ChunkingHandler::ProcessMediaSample(const MediaSample* sample) { + const bool is_key_frame = sample->is_key_frame(); + const int64_t timestamp = sample->dts(); + // Check if we need to terminate the current (sub)segment. + bool new_segment = false; + bool new_subsegment = false; + if (is_key_frame || !chunking_options_.segment_sap_aligned) { + const int64_t segment_index = timestamp / segment_duration_; + if (segment_index != current_segment_index_) { + current_segment_index_ = segment_index; + new_segment = true; + } + } + if (!new_segment && subsegment_duration_ > 0 && + (is_key_frame || !chunking_options_.subsegment_sap_aligned)) { + const int64_t subsegment_index = + (timestamp - segment_info_[main_stream_index_]->start_timestamp) / + subsegment_duration_; + if (subsegment_index != current_subsegment_index_) { + current_subsegment_index_ = subsegment_index; + new_subsegment = true; + } + } + + Status status; + if (new_segment || new_subsegment) { + // Dispatch the samples before |timestamp| - See the implemention on how we + // determine if a sample is before |timestamp|.. + status.Update(DispatchNonMainSamples(timestamp)); + } + + if (new_segment) { + status.Update(DispatchSegmentInfoForAllStreams()); + segment_info_[main_stream_index_]->start_timestamp = timestamp; + } + if (subsegment_duration_ > 0 && (new_segment || new_subsegment)) { + status.Update(DispatchSubsegmentInfoForAllStreams()); + subsegment_info_[main_stream_index_]->start_timestamp = timestamp; + } + if (!status.ok()) + return status; + + // Dispatch non-main samples for the next segment. + return DispatchNonMainSamples(kTimeStampToDispatchAllSamples); +} + +Status ChunkingHandler::DispatchNonMainSamples(int64_t timestamp_threshold) { + Status status; + while (status.ok() && !non_main_samples_.empty()) { + DCHECK_EQ(non_main_samples_.front()->stream_data_type, + StreamDataType::kMediaSample); + const int stream_index = non_main_samples_.front()->stream_index; + const MediaSample* sample = non_main_samples_.front()->media_sample.get(); + // If the portion of the sample before |timestamp_threshold| is bigger than + // the other portion, we consider it part of the current segment. + const int64_t timestamp = sample->dts() + sample->duration() / 2; + const bool stop = + (timestamp_threshold != kTimeStampToDispatchAllSamples && + (static_cast(timestamp) / time_scales_[stream_index]) > + (static_cast(timestamp_threshold) / + time_scales_[main_stream_index_])); + VLOG(3) << "Sample ts: " << sample->dts() << " " + << " duration: " << sample->duration() + << " scale: " << time_scales_[stream_index] << "\n" + << " threshold: " << timestamp_threshold + << " scale: " << time_scales_[main_stream_index_] + << (stop ? " stop " + : (segment_info_[stream_index] ? " dispatch " + : " discard ")); + if (stop) + break; + // Only dispatch samples if the segment has started, otherwise discard + // them. + if (segment_info_[stream_index]) { + if (segment_info_[stream_index]->start_timestamp == -1) + segment_info_[stream_index]->start_timestamp = sample->dts(); + if (subsegment_info_[stream_index] && + subsegment_info_[stream_index]->start_timestamp == -1) { + subsegment_info_[stream_index]->start_timestamp = sample->dts(); + } + last_sample_end_timestamps_[stream_index] = + sample->dts() + sample->duration(); + status.Update(Dispatch(std::move(non_main_samples_.front()))); + } + non_main_samples_.pop_front(); + } + return status; +} + +Status ChunkingHandler::DispatchSegmentInfoForAllStreams() { + Status status; + for (size_t i = 0; i < segment_info_.size() && status.ok(); ++i) { + if (segment_info_[i] && segment_info_[i]->start_timestamp != -1) { + segment_info_[i]->duration = + last_sample_end_timestamps_[i] - segment_info_[i]->start_timestamp; + status.Update(DispatchSegmentInfo(i, std::move(segment_info_[i]))); + } + segment_info_[i].reset(new SegmentInfo); + subsegment_info_[i].reset(); + } + return status; +} + +Status ChunkingHandler::DispatchSubsegmentInfoForAllStreams() { + Status status; + for (size_t i = 0; i < subsegment_info_.size() && status.ok(); ++i) { + if (subsegment_info_[i] && subsegment_info_[i]->start_timestamp != -1) { + subsegment_info_[i]->duration = + last_sample_end_timestamps_[i] - subsegment_info_[i]->start_timestamp; + status.Update(DispatchSegmentInfo(i, std::move(subsegment_info_[i]))); + } + subsegment_info_[i].reset(new SegmentInfo); + subsegment_info_[i]->is_subsegment = true; + } + return status; +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/chunking/chunking_handler.h b/packager/media/chunking/chunking_handler.h new file mode 100644 index 0000000000..67f876a747 --- /dev/null +++ b/packager/media/chunking/chunking_handler.h @@ -0,0 +1,123 @@ +// 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_CHUNKING_CHUNKING_HANDLER_ +#define PACKAGER_MEDIA_CHUNKING_CHUNKING_HANDLER_ + +#include + +#include "packager/media/base/media_handler.h" + +namespace shaka { +namespace media { + +struct ChunkingOptions { + /// Segment duration in seconds. + double segment_duration_in_seconds = 0; + + /// Subsegment duration in seconds. Should not be larger than the segment + /// duration. + double subsegment_duration_in_seconds = 0; + + /// Force segments to begin with stream access points. Actual segment duration + /// may not be exactly what is specified by segment_duration. + bool segment_sap_aligned = true; + + /// Force subsegments to begin with stream access points. Actual subsegment + /// duration may not be exactly what is specified by subsegment_duration. + /// Setting to true implies that segment_sap_aligned is true as well. + bool subsegment_sap_aligned = true; +}; + +/// ChunkingHandler splits the samples into segments / subsegments based on the +/// specified chunking options. +/// This handler is a multi-in multi-out handler. If more than one input is +/// provided, there should be one and only one video stream; also, all inputs +/// should come from the same thread and are synchronized. +/// There can be multiple chunking handler running in different threads or even +/// different processes, we use the "consistent chunking algorithm" to make sure +/// the chunks in different streams are aligned without explicit communcating +/// with each other - which is not efficient and often difficult. +/// +/// Consistent Chunking Algorithm: +/// 1. Find the consistent chunkable boundary +/// Let the timestamps for video frames be (t1, t2, t3, ...). Then a +/// consistent chunkable boundary is simply the first chunkable boundary after +/// (tk / N) != (tk-1 / N), where '/' denotes integer division, and N is the +/// intended chunk duration. +/// 2. Chunk only at the consistent chunkable boundary +/// +/// This algorithm will make sure the chunks from different video streams are +/// aligned if they have aligned GoPs. However, this algorithm will only work +/// for video streams. To be able to chunk non video streams at similar +/// positions as video streams, ChunkingHandler is designed to accept one video +/// input and multiple non video inputs, the non video inputs are chunked when +/// the video input is chunked. If the inputs are synchronized - which is true +/// if the inputs come from the same demuxer, the video and non video chunks +/// are aligned. +class ChunkingHandler : public MediaHandler { + public: + explicit ChunkingHandler(const ChunkingOptions& chunking_options); + ~ChunkingHandler() override; + + protected: + /// @name MediaHandler implementation overrides. + /// @{ + Status InitializeInternal() override; + Status Process(std::unique_ptr stream_data) override; + Status FlushStream(int input_stream_index) override; + /// @} + + private: + friend class ChunkingHandlerTest; + + ChunkingHandler(const ChunkingHandler&) = delete; + ChunkingHandler& operator=(const ChunkingHandler&) = delete; + + // Processes media sample and apply chunking if needed. + Status ProcessMediaSample(const MediaSample* sample); + + // Dispatch cached non main stream samples before |timestamp_threshold|. + Status DispatchNonMainSamples(int64_t timestamp_threshold); + + // The (sub)segments are aligned and dispatched together. + Status DispatchSegmentInfoForAllStreams(); + Status DispatchSubsegmentInfoForAllStreams(); + + const ChunkingOptions chunking_options_; + + // The inputs are expected to come from the same thread. + std::atomic thread_id_; + + // The video stream is the main stream; if there is only one stream, it is the + // main stream. The chunking is based on the main stream. + int main_stream_index_ = -1; + // Segment and subsegment duration in main stream's time scale. + int64_t segment_duration_ = 0; + int64_t subsegment_duration_ = 0; + + // The streams are expected to be synchronized. Cache non main (video) stream + // samples so we can determine whether the next segment should include these + // samples. The samples will be dispatched after seeing the next main stream + // sample. + std::deque> non_main_samples_; + + // Current segment index, useful to determine where to do chunking. + int64_t current_segment_index_ = -1; + // Current subsegment index, useful to determine where to do chunking. + int64_t current_subsegment_index_ = -1; + + std::vector> segment_info_; + std::vector> subsegment_info_; + std::vector time_scales_; + // The end timestamp of the last dispatched sample. + std::vector last_sample_end_timestamps_; +}; + +} // namespace media +} // namespace shaka + +#endif // PACKAGER_MEDIA_CHUNKING_CHUNKING_HANDLER_ diff --git a/packager/media/chunking/chunking_handler_unittest.cc b/packager/media/chunking/chunking_handler_unittest.cc new file mode 100644 index 0000000000..67d92c6977 --- /dev/null +++ b/packager/media/chunking/chunking_handler_unittest.cc @@ -0,0 +1,237 @@ +// 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/chunking/chunking_handler.h" + +#include +#include + +#include "packager/media/base/media_handler_test_base.h" +#include "packager/media/base/test/status_test_util.h" + +using ::testing::ElementsAre; +using ::testing::IsEmpty; + +namespace shaka { +namespace media { +namespace { +const int kStreamIndex0 = 0; +const int kStreamIndex1 = 1; +const uint32_t kTimeScale0 = 800; +const uint32_t kTimeScale1 = 1000; +const int64_t kDuration0 = 200; +const int64_t kDuration1 = 300; +const bool kKeyFrame = true; +const bool kIsSubsegment = true; +const bool kEncrypted = true; + +} // namespace + +class ChunkingHandlerTest : public MediaHandlerTestBase { + public: + void SetUpChunkingHandler(int num_inputs, + const ChunkingOptions& chunking_options) { + chunking_handler_.reset(new ChunkingHandler(chunking_options)); + SetUpGraph(num_inputs, num_inputs, chunking_handler_); + ASSERT_OK(chunking_handler_->Initialize()); + } + + Status Process(std::unique_ptr stream_data) { + return chunking_handler_->Process(std::move(stream_data)); + } + + Status FlushStream(int stream_index) { + return chunking_handler_->FlushStream(stream_index); + } + + protected: + std::shared_ptr chunking_handler_; +}; + +TEST_F(ChunkingHandlerTest, AudioNoSubsegmentsThenFlush) { + ChunkingOptions chunking_options; + chunking_options.segment_duration_in_seconds = 1; + SetUpChunkingHandler(1, chunking_options); + + ASSERT_OK(Process(GetAudioStreamInfoStreamData(kStreamIndex0, kTimeScale0))); + EXPECT_THAT( + GetOutputStreamDataVector(), + ElementsAre(IsStreamInfo(kStreamIndex0, kTimeScale0, !kEncrypted))); + + for (int i = 0; i < 5; ++i) { + ClearOutputStreamDataVector(); + ASSERT_OK(Process(GetMediaSampleStreamData(kStreamIndex0, i * kDuration1, + kDuration1, kKeyFrame))); + // One output stream_data except when i == 3, which also has SegmentInfo. + if (i == 3) { + EXPECT_THAT( + GetOutputStreamDataVector(), + ElementsAre( + IsSegmentInfo(kStreamIndex0, 0, kDuration1 * 3, !kIsSubsegment), + IsMediaSample(kStreamIndex0, i * kDuration1, kDuration1))); + } else { + EXPECT_THAT(GetOutputStreamDataVector(), + ElementsAre(IsMediaSample(kStreamIndex0, i * kDuration1, + kDuration1))); + } + } + + ClearOutputStreamDataVector(); + ASSERT_OK(FlushStream(kStreamIndex0)); + EXPECT_THAT(GetOutputStreamDataVector(), + ElementsAre(IsSegmentInfo(kStreamIndex0, kDuration1 * 3, + kDuration1 * 2, !kIsSubsegment))); +} + +TEST_F(ChunkingHandlerTest, VideoAndSubsegmentAndNonzeroStart) { + ChunkingOptions chunking_options; + chunking_options.segment_duration_in_seconds = 1; + chunking_options.subsegment_duration_in_seconds = 0.3; + SetUpChunkingHandler(1, chunking_options); + + ASSERT_OK(Process(GetVideoStreamInfoStreamData(kStreamIndex0, kTimeScale1))); + const int64_t kVideoStartTimestamp = 12345; + for (int i = 0; i < 6; ++i) { + // Alternate key frame. + const bool is_key_frame = (i % 2) == 1; + ASSERT_OK(Process(GetMediaSampleStreamData( + kStreamIndex0, kVideoStartTimestamp + i * kDuration1, kDuration1, + is_key_frame))); + } + EXPECT_THAT( + GetOutputStreamDataVector(), + ElementsAre( + IsStreamInfo(kStreamIndex0, kTimeScale1, !kEncrypted), + // The first samples @ kStartTimestamp is discarded - not key frame. + IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration1, + kDuration1), + IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration1 * 2, + kDuration1), + // The next segment boundary 13245 / 1000 != 12645 / 1000. + IsSegmentInfo(kStreamIndex0, kVideoStartTimestamp + kDuration1, + kDuration1 * 2, !kIsSubsegment), + IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration1 * 3, + kDuration1), + IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration1 * 4, + kDuration1), + // The subsegment has duration kDuration1 * 2 since it can only + // terminate before key frame. + IsSegmentInfo(kStreamIndex0, kVideoStartTimestamp + kDuration1 * 3, + kDuration1 * 2, kIsSubsegment), + IsMediaSample(kStreamIndex0, kVideoStartTimestamp + kDuration1 * 5, + kDuration1))); +} + +TEST_F(ChunkingHandlerTest, AudioAndVideo) { + ChunkingOptions chunking_options; + chunking_options.segment_duration_in_seconds = 1; + chunking_options.subsegment_duration_in_seconds = 0.3; + SetUpChunkingHandler(2, chunking_options); + + ASSERT_OK(Process(GetAudioStreamInfoStreamData(kStreamIndex0, kTimeScale0))); + ASSERT_OK(Process(GetVideoStreamInfoStreamData(kStreamIndex1, kTimeScale1))); + EXPECT_THAT( + GetOutputStreamDataVector(), + ElementsAre(IsStreamInfo(kStreamIndex0, kTimeScale0, !kEncrypted), + IsStreamInfo(kStreamIndex1, kTimeScale1, !kEncrypted))); + ClearOutputStreamDataVector(); + + // Equivalent to 12345 in video timescale. + const int64_t kAudioStartTimestamp = 9876; + const int64_t kVideoStartTimestamp = 12345; + for (int i = 0; i < 5; ++i) { + ASSERT_OK(Process(GetMediaSampleStreamData( + kStreamIndex0, kAudioStartTimestamp + kDuration0 * i, kDuration0, + true))); + // Alternate key frame. + const bool is_key_frame = (i % 2) == 1; + ASSERT_OK(Process(GetMediaSampleStreamData( + kStreamIndex1, kVideoStartTimestamp + kDuration1 * i, kDuration1, + is_key_frame))); + } + + EXPECT_THAT( + GetOutputStreamDataVector(), + ElementsAre( + // The first samples @ kStartTimestamp is discarded - not key frame. + IsMediaSample(kStreamIndex0, kAudioStartTimestamp + kDuration0, + kDuration0), + IsMediaSample(kStreamIndex1, kVideoStartTimestamp + kDuration1, + kDuration1), + IsMediaSample(kStreamIndex0, kAudioStartTimestamp + kDuration0 * 2, + kDuration0), + IsMediaSample(kStreamIndex1, kVideoStartTimestamp + kDuration1 * 2, + kDuration1), + IsMediaSample(kStreamIndex0, kAudioStartTimestamp + kDuration0 * 3, + kDuration0), + // The audio segment is terminated together with video stream. + IsSegmentInfo(kStreamIndex0, kAudioStartTimestamp + kDuration0, + kDuration0 * 3, !kIsSubsegment), + // The next segment boundary 13245 / 1000 != 12645 / 1000. + IsSegmentInfo(kStreamIndex1, kVideoStartTimestamp + kDuration1, + kDuration1 * 2, !kIsSubsegment), + IsMediaSample(kStreamIndex1, kVideoStartTimestamp + kDuration1 * 3, + kDuration1), + IsMediaSample(kStreamIndex0, kAudioStartTimestamp + kDuration0 * 4, + kDuration0), + IsMediaSample(kStreamIndex1, kVideoStartTimestamp + kDuration1 * 4, + kDuration1))); + ClearOutputStreamDataVector(); + + // The side comments below show the equivalent timestamp in video timescale. + // The audio and video are made ~aligned. + ASSERT_OK(Process(GetMediaSampleStreamData(kStreamIndex0, + kAudioStartTimestamp + kDuration0 * 5, + kDuration0, true))); // 13595 + ASSERT_OK(Process(GetMediaSampleStreamData(kStreamIndex1, + kVideoStartTimestamp + kDuration1 * 5, + kDuration1, true))); // 13845 + ASSERT_OK(Process(GetMediaSampleStreamData(kStreamIndex0, + kAudioStartTimestamp + kDuration0 * 6, + kDuration0, true))); // 13845 + // This expectation are separated from the expectation above because + // ElementsAre supports at most 10 elements. + EXPECT_THAT( + GetOutputStreamDataVector(), + ElementsAre( + IsMediaSample(kStreamIndex0, kAudioStartTimestamp + kDuration0 * 5, + kDuration0), + // Audio is terminated along with video below. + IsSegmentInfo(kStreamIndex0, kAudioStartTimestamp + kDuration0 * 4, + kDuration0 * 2, kIsSubsegment), + // The subsegment has duration kDuration1 * 2 since it can only + // terminate before key frame. + IsSegmentInfo(kStreamIndex1, kVideoStartTimestamp + kDuration1 * 3, + kDuration1 * 2, kIsSubsegment), + IsMediaSample(kStreamIndex1, kVideoStartTimestamp + kDuration1 * 5, + kDuration1))); + + ClearOutputStreamDataVector(); + ASSERT_OK(FlushStream(kStreamIndex0)); + EXPECT_THAT( + GetOutputStreamDataVector(), + ElementsAre( + IsMediaSample(kStreamIndex0, kAudioStartTimestamp + kDuration0 * 6, + kDuration0), + IsSegmentInfo(kStreamIndex0, kAudioStartTimestamp + kDuration0 * 4, + kDuration0 * 3, !kIsSubsegment))); + + ClearOutputStreamDataVector(); + ASSERT_OK(FlushStream(kStreamIndex1)); + EXPECT_THAT(GetOutputStreamDataVector(), + ElementsAre(IsSegmentInfo(kStreamIndex1, + kVideoStartTimestamp + kDuration1 * 3, + kDuration1 * 3, !kIsSubsegment))); + + // Flush again will do nothing. + ClearOutputStreamDataVector(); + ASSERT_OK(FlushStream(kStreamIndex0)); + ASSERT_OK(FlushStream(kStreamIndex1)); + EXPECT_THAT(GetOutputStreamDataVector(), IsEmpty()); +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/crypto/crypto.gyp b/packager/media/crypto/crypto.gyp index 12a004ae3c..a05e895d28 100644 --- a/packager/media/crypto/crypto.gyp +++ b/packager/media/crypto/crypto.gyp @@ -30,6 +30,7 @@ 'dependencies': [ '../../testing/gtest.gyp:gtest', '../../testing/gmock.gyp:gmock', + '../base/media_base.gyp:media_handler_test_base', '../test/media_test.gyp:media_test_support', 'crypto', ] diff --git a/packager/media/crypto/encryption_handler.cc b/packager/media/crypto/encryption_handler.cc index 7e36ae2c1f..138ad13bc2 100644 --- a/packager/media/crypto/encryption_handler.cc +++ b/packager/media/crypto/encryption_handler.cc @@ -14,6 +14,7 @@ #include "packager/media/base/aes_encryptor.h" #include "packager/media/base/aes_pattern_cryptor.h" #include "packager/media/base/key_source.h" +#include "packager/media/base/media_sample.h" #include "packager/media/base/video_stream_info.h" #include "packager/media/codecs/video_slice_header_parser.h" #include "packager/media/codecs/vp8_parser.h" @@ -104,11 +105,13 @@ Status EncryptionHandler::Process(std::unique_ptr stream_data) { status = ProcessStreamInfo(stream_data->stream_info.get()); break; case StreamDataType::kSegmentInfo: - new_segment_ = true; - if (remaining_clear_lead_ > 0) - remaining_clear_lead_ -= stream_data->segment_info->duration; - else - stream_data->segment_info->is_encrypted = true; + if (!stream_data->segment_info->is_subsegment) { + new_segment_ = true; + if (remaining_clear_lead_ > 0) + remaining_clear_lead_ -= stream_data->segment_info->duration; + else + stream_data->segment_info->is_encrypted = true; + } break; case StreamDataType::kMediaSample: status = ProcessMediaSample(stream_data->media_sample.get()); diff --git a/packager/media/crypto/encryption_handler.h b/packager/media/crypto/encryption_handler.h index fb1341ea25..95c327f123 100644 --- a/packager/media/crypto/encryption_handler.h +++ b/packager/media/crypto/encryption_handler.h @@ -9,7 +9,6 @@ #include "packager/media/base/key_source.h" #include "packager/media/base/media_handler.h" -#include "packager/media/base/stream_info.h" namespace shaka { namespace media { @@ -66,7 +65,7 @@ class EncryptionHandler : public MediaHandler { // Processes |stream_info| and sets up stream specific variables. Status ProcessStreamInfo(StreamInfo* stream_info); - // Processes media sample end encrypts it if needed. + // Processes media sample and encrypts it if needed. Status ProcessMediaSample(MediaSample* sample); bool CreateEncryptor(EncryptionKey* encryption_key); @@ -81,7 +80,7 @@ class EncryptionHandler : public MediaHandler { void InjectVideoSliceHeaderParserForTesting( std::unique_ptr header_parser); - EncryptionOptions encryption_options_; + const EncryptionOptions encryption_options_; KeySource* key_source_ = nullptr; KeySource::TrackType track_type_ = KeySource::TRACK_TYPE_UNKNOWN; std::unique_ptr encryptor_; diff --git a/packager/media/crypto/encryption_handler_unittest.cc b/packager/media/crypto/encryption_handler_unittest.cc index 2c1f729282..fcf3a64ef5 100644 --- a/packager/media/crypto/encryption_handler_unittest.cc +++ b/packager/media/crypto/encryption_handler_unittest.cc @@ -11,10 +11,9 @@ #include "packager/media/base/aes_decryptor.h" #include "packager/media/base/aes_pattern_cryptor.h" -#include "packager/media/base/audio_stream_info.h" #include "packager/media/base/fixed_key_source.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" #include "packager/media/codecs/video_slice_header_parser.h" #include "packager/media/codecs/vpx_parser.h" @@ -40,26 +39,6 @@ class MockKeySource : public FixedKeySource { EncryptionKey* key)); }; -class FakeMediaHandler : public MediaHandler { - public: - const std::vector>& stream_data_vector() const { - return stream_data_vector_; - } - void clear_stream_data_vector() { stream_data_vector_.clear(); } - - protected: - Status InitializeInternal() override { return Status::OK; } - Status Process(std::unique_ptr stream_data) override { - stream_data_vector_.push_back(std::move(stream_data)); - return Status::OK; - } - bool ValidateOutputStreamIndex(int stream_index) const override { - return stream_index == 0; - } - - std::vector> stream_data_vector_; -}; - class MockVpxParser : public VPxParser { public: MOCK_METHOD3(Parse, @@ -77,20 +56,14 @@ class MockVideoSliceHeaderParser : public VideoSliceHeaderParser { } // namespace -class EncryptionHandlerTest : public ::testing::Test { +class EncryptionHandlerTest : public MediaHandlerTestBase { public: void SetUp() override { SetUpEncryptionHandler(EncryptionOptions()); } void SetUpEncryptionHandler(const EncryptionOptions& encryption_options) { encryption_handler_.reset( new EncryptionHandler(encryption_options, &mock_key_source_)); - next_handler_.reset(new FakeMediaHandler); - - // Input handler is not really used anywhere but just to satisfy one input - // one output restriction for the encryption handler. - auto input_handler = std::make_shared(); - ASSERT_OK(input_handler->AddHandler(encryption_handler_)); - ASSERT_OK(encryption_handler_->AddHandler(next_handler_)); + SetUpGraph(1 /* one input */, 1 /* one output */, encryption_handler_); } Status Process(std::unique_ptr stream_data) { @@ -109,7 +82,6 @@ class EncryptionHandlerTest : public ::testing::Test { protected: std::shared_ptr encryption_handler_; - std::shared_ptr next_handler_; MockKeySource mock_key_source_; }; @@ -118,61 +90,26 @@ TEST_F(EncryptionHandlerTest, Initialize) { } TEST_F(EncryptionHandlerTest, OnlyOneOutput) { - auto another_handler = std::make_shared(); // Connecting another handler will fail. ASSERT_EQ(error::INVALID_ARGUMENT, - encryption_handler_->AddHandler(another_handler).error_code()); + encryption_handler_->AddHandler(some_handler()).error_code()); } TEST_F(EncryptionHandlerTest, OnlyOneInput) { - auto another_handler = std::make_shared(); - ASSERT_OK(another_handler->AddHandler(encryption_handler_)); + ASSERT_OK(some_handler()->AddHandler(encryption_handler_)); ASSERT_EQ(error::INVALID_ARGUMENT, encryption_handler_->Initialize().error_code()); } namespace { -const int kTrackId = 1; -const uint32_t kTimeScale = 1000; -const uint64_t kDuration = 10000; -const char kCodecString[] = "codec string"; -const uint8_t kSampleBits = 1; -const uint8_t kNumChannels = 2; -const uint32_t kSamplingFrequency = 48000; -const uint64_t kSeekPrerollNs = 12345; -const uint64_t kCodecDelayNs = 56789; -const uint32_t kMaxBitrate = 13579; -const uint32_t kAvgBitrate = 13000; -const char kLanguage[] = "eng"; -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 uint8_t kNaluLengthSize = 1u; +const int kStreamIndex = 0; const bool kEncrypted = true; +const uint32_t kTimeScale = 1000; const uint32_t kMaxSdPixels = 100u; const uint32_t kMaxHdPixels = 200u; const uint32_t kMaxUhd1Pixels = 300u; -// Use H264 code config. -const uint8_t kCodecConfig[]{ - // Header - 0x01, 0x64, 0x00, 0x1e, 0xff, - // SPS count (ignore top three bits) - 0xe1, - // SPS - 0x00, 0x19, // Size - 0x67, 0x64, 0x00, 0x1e, 0xac, 0xd9, 0x40, 0xa0, 0x2f, 0xf9, 0x70, 0x11, - 0x00, 0x00, 0x03, 0x03, 0xe9, 0x00, 0x00, 0xea, 0x60, 0x0f, 0x16, 0x2d, - 0x96, - // PPS count - 0x01, - // PPS - 0x00, 0x06, // Size - 0x68, 0xeb, 0xe3, 0xcb, 0x22, 0xc0, -}; // The data is based on H264. The same data is also used to test audio, which // does not care the underlying data, and VP9, for which we will mock the // parser. @@ -222,22 +159,6 @@ class EncryptionHandlerEncryptionTest SetUpEncryptionHandler(encryption_options); } - std::unique_ptr GetMockStreamInfo() { - if (codec_ == kCodecAAC) { - return std::unique_ptr(new AudioStreamInfo( - kTrackId, kTimeScale, kDuration, codec_, kCodecString, kCodecConfig, - sizeof(kCodecConfig), kSampleBits, kNumChannels, kSamplingFrequency, - kSeekPrerollNs, kCodecDelayNs, kMaxBitrate, kAvgBitrate, kLanguage, - !kEncrypted)); - - } else { - return std::unique_ptr(new VideoStreamInfo( - kTrackId, kTimeScale, kDuration, codec_, kCodecString, kCodecConfig, - sizeof(kCodecConfig), kWidth, kHeight, kPixelWidth, kPixelHeight, - kTrickPlayRate, kNaluLengthSize, kLanguage, !kEncrypted)); - } - } - std::vector GetMockVpxFrameInfo() { std::vector vpx_frames; vpx_frames.resize(2); @@ -373,17 +294,9 @@ class EncryptionHandlerEncryptionTest }; TEST_P(EncryptionHandlerEncryptionTest, Encrypt) { - std::unique_ptr stream_data(new StreamData); - stream_data->stream_index = 0; - stream_data->stream_data_type = StreamDataType::kStreamInfo; - stream_data->stream_info = GetMockStreamInfo(); - ASSERT_OK(Process(std::move(stream_data))); - ASSERT_EQ(1u, next_handler_->stream_data_vector().size()); - ASSERT_EQ(0, next_handler_->stream_data_vector().back()->stream_index); - ASSERT_EQ(StreamDataType::kStreamInfo, - next_handler_->stream_data_vector().back()->stream_data_type); - ASSERT_TRUE( - next_handler_->stream_data_vector().back()->stream_info->is_encrypted()); + ASSERT_OK(Process(GetStreamInfoStreamData(kStreamIndex, codec_, kTimeScale))); + EXPECT_THAT(GetOutputStreamDataVector(), + ElementsAre(IsStreamInfo(kStreamIndex, kTimeScale, kEncrypted))); // Inject vpx parser / video slice header parser if needed. switch (codec_) { @@ -410,7 +323,7 @@ TEST_P(EncryptionHandlerEncryptionTest, Encrypt) { break; } - stream_data.reset(new StreamData); + std::unique_ptr stream_data(new StreamData); stream_data->stream_index = 0; stream_data->stream_data_type = StreamDataType::kMediaSample; stream_data->media_sample.reset( @@ -420,13 +333,12 @@ TEST_P(EncryptionHandlerEncryptionTest, Encrypt) { .WillOnce( DoAll(SetArgPointee<1>(GetMockEncryptionKey()), Return(Status::OK))); ASSERT_OK(Process(std::move(stream_data))); - ASSERT_EQ(2u, next_handler_->stream_data_vector().size()); - ASSERT_EQ(0, next_handler_->stream_data_vector().back()->stream_index); + ASSERT_EQ(2u, GetOutputStreamDataVector().size()); + ASSERT_EQ(0, GetOutputStreamDataVector().back()->stream_index); ASSERT_EQ(StreamDataType::kMediaSample, - next_handler_->stream_data_vector().back()->stream_data_type); + GetOutputStreamDataVector().back()->stream_data_type); - auto* media_sample = - next_handler_->stream_data_vector().back()->media_sample.get(); + auto* media_sample = GetOutputStreamDataVector().back()->media_sample.get(); auto* decrypt_config = media_sample->decrypt_config(); EXPECT_EQ(std::vector(kKeyId, kKeyId + sizeof(kKeyId)), decrypt_config->key_id()); diff --git a/packager/packager.gyp b/packager/packager.gyp index b4ca5b745c..859217bf57 100644 --- a/packager/packager.gyp +++ b/packager/packager.gyp @@ -115,6 +115,7 @@ 'dependencies': [ 'hls/hls.gyp:hls_unittest', 'media/base/media_base.gyp:media_base_unittest', + 'media/chunking/chunking.gyp:chunking_unittest', 'media/codecs/codecs.gyp:codecs_unittest', 'media/crypto/crypto.gyp:crypto_unittest', 'media/event/media_event.gyp:media_event_unittest',