From 5c4d930465875dfa45ee39fa4f6f328eff85b53f Mon Sep 17 00:00:00 2001 From: KongQun Yang Date: Tue, 2 Oct 2018 16:08:32 -0700 Subject: [PATCH] Move subsample extraction out of EncryptionHandler Created SubsampleGenerator to generate subsamples. This is part of the EncryptionHandler clean up to make it more modular and testable. Change-Id: I6f4076b057027c72335beb3cbf1965341eb18031 --- packager/media/crypto/crypto.gyp | 3 + packager/media/crypto/encryption_handler.cc | 299 ++---------- packager/media/crypto/encryption_handler.h | 41 +- .../crypto/encryption_handler_unittest.cc | 298 +++++------- packager/media/crypto/subsample_generator.cc | 303 ++++++++++++ packager/media/crypto/subsample_generator.h | 103 +++++ .../crypto/subsample_generator_unittest.cc | 435 ++++++++++++++++++ 7 files changed, 1017 insertions(+), 465 deletions(-) create mode 100644 packager/media/crypto/subsample_generator.cc create mode 100644 packager/media/crypto/subsample_generator.h create mode 100644 packager/media/crypto/subsample_generator_unittest.cc diff --git a/packager/media/crypto/crypto.gyp b/packager/media/crypto/crypto.gyp index 1106ec7934..221660d5be 100644 --- a/packager/media/crypto/crypto.gyp +++ b/packager/media/crypto/crypto.gyp @@ -19,6 +19,8 @@ 'encryption_handler.h', 'sample_aes_ec3_cryptor.cc', 'sample_aes_ec3_cryptor.h', + 'subsample_generator.cc', + 'subsample_generator.h', ], 'dependencies': [ '../base/media_base.gyp:media_base', @@ -31,6 +33,7 @@ 'sources': [ 'encryption_handler_unittest.cc', 'sample_aes_ec3_cryptor_unittest.cc', + 'subsample_generator_unittest.cc', ], 'dependencies': [ '../../testing/gtest.gyp:gtest', diff --git a/packager/media/crypto/encryption_handler.cc b/packager/media/crypto/encryption_handler.cc index 4ad7a1a263..be4a9e9eb8 100644 --- a/packager/media/crypto/encryption_handler.cc +++ b/packager/media/crypto/encryption_handler.cc @@ -10,25 +10,21 @@ #include #include -#include #include "packager/media/base/aes_encryptor.h" #include "packager/media/base/audio_stream_info.h" #include "packager/media/base/key_source.h" +#include "packager/media/base/macros.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" -#include "packager/media/codecs/vp9_parser.h" #include "packager/media/crypto/aes_encryptor_factory.h" +#include "packager/media/crypto/subsample_generator.h" #include "packager/status_macros.h" namespace shaka { namespace media { namespace { -const size_t kCencBlockSize = 16u; - // The encryption handler only supports a single output. const size_t kStreamIndex = 0; @@ -44,31 +40,6 @@ const uint8_t kKeyRotationDefaultIv[] = { 0, 0, 0, 0, 0, 0, 0, 0, }; -// Adds one or more subsamples to |*decrypt_config|. This may add more than one -// if one of the values overflows the integer in the subsample. -void AddSubsample(uint64_t clear_bytes, - uint64_t cipher_bytes, - DecryptConfig* decrypt_config) { - CHECK_LT(cipher_bytes, std::numeric_limits::max()); - const uint64_t kUInt16Max = std::numeric_limits::max(); - while (clear_bytes > kUInt16Max) { - decrypt_config->AddSubsample(kUInt16Max, 0); - clear_bytes -= kUInt16Max; - } - - if (clear_bytes > 0 || cipher_bytes > 0) - decrypt_config->AddSubsample(clear_bytes, cipher_bytes); -} - -uint8_t GetNaluLengthSize(const StreamInfo& stream_info) { - if (stream_info.stream_type() != kStreamVideo) - return 0; - - const VideoStreamInfo& video_stream_info = - static_cast(stream_info); - return video_stream_info.nalu_length_size(); -} - std::string GetStreamLabelForEncryption( const StreamInfo& stream_info, const std::function(encryption_params.protection_scheme)), key_source_(key_source), + subsample_generator_( + new SubsampleGenerator(encryption_params.vp9_subsample_encryption)), encryptor_factory_(new AesEncryptorFactory) {} EncryptionHandler::~EncryptionHandler() = default; @@ -157,6 +130,8 @@ Status EncryptionHandler::ProcessStreamInfo(const StreamInfo& clear_info) { DCHECK_NE(kStreamUnknown, clear_info.stream_type()); DCHECK_NE(kStreamText, clear_info.stream_type()); std::shared_ptr stream_info = clear_info.Clone(); + RETURN_IF_ERROR( + subsample_generator_->Initialize(protection_scheme_, *stream_info)); remaining_clear_lead_ = encryption_params_.clear_lead_in_seconds * stream_info->time_scale(); @@ -164,36 +139,10 @@ Status EncryptionHandler::ProcessStreamInfo(const StreamInfo& clear_info) { encryption_params_.crypto_period_duration_in_seconds * stream_info->time_scale(); codec_ = stream_info->codec(); - nalu_length_size_ = GetNaluLengthSize(*stream_info); stream_label_ = GetStreamLabelForEncryption( *stream_info, encryption_params_.stream_label_func); - switch (codec_) { - case kCodecVP9: - if (encryption_params_.vp9_subsample_encryption) - vpx_parser_.reset(new VP9Parser); - break; - case kCodecH264: - header_parser_.reset(new H264VideoSliceHeaderParser); - break; - case kCodecH265: - header_parser_.reset(new H265VideoSliceHeaderParser); - break; - default: - // Other codecs should have nalu length size == 0. - if (nalu_length_size_ > 0) { - LOG(WARNING) << "Unknown video codec '" << codec_ << "'"; - return Status(error::ENCRYPTION_FAILURE, "Unknown video codec."); - } - } - if (header_parser_) { - CHECK_NE(nalu_length_size_, 0u) << "AnnexB stream is not supported yet"; - if (!header_parser_->Initialize(stream_info->codec_config())) { - return Status(error::ENCRYPTION_FAILURE, - "Fail to read SPS and PPS data."); - } - } - RETURN_IF_ERROR(SetupProtectionPattern(stream_info->stream_type())); + SetupProtectionPattern(stream_info->stream_type()); EncryptionKey encryption_key; const bool key_rotation_enabled = crypto_period_duration_ != 0; @@ -223,15 +172,11 @@ Status EncryptionHandler::ProcessMediaSample( std::shared_ptr clear_sample) { DCHECK(clear_sample); - // We need to parse the frame (which also updates the vpx parser) even if the - // frame is not encrypted as the next (encrypted) frame may be dependent on - // this clear frame. - std::vector vpx_frames; - if (vpx_parser_ && !vpx_parser_->Parse(clear_sample->data(), - clear_sample->data_size(), - &vpx_frames)) { - return Status(error::ENCRYPTION_FAILURE, "Failed to parse vpx frame."); - } + // Process the frame even if the frame is not encrypted as the next + // (encrypted) frame may be dependent on this clear frame. + std::vector subsamples; + RETURN_IF_ERROR(subsample_generator_->GenerateSubsamples( + clear_sample->data(), clear_sample->data_size(), &subsamples)); // Need to setup the encryptor for new segments even if this segment does not // need to be encrypted, so we can signal encryption metadata earlier to @@ -258,57 +203,43 @@ Status EncryptionHandler::ProcessMediaSample( return DispatchMediaSample(kStreamIndex, std::move(clear_sample)); } - std::unique_ptr decrypt_config(new DecryptConfig( - encryption_config_->key_id, - encryptor_->iv(), - std::vector(), - protection_scheme_, - crypt_byte_block_, - skip_byte_block_)); - - // Now that we know that this sample must be encrypted, make a copy of - // the sample first so that all the encryption operations can be done - // in-place. - std::shared_ptr cipher_sample(clear_sample->Clone()); - // |cipher_sample| above still contains the old clear sample data. We will - // use |cipher_sample_data| to hold cipher sample data then transfer it to - // |cipher_sample| after encryption. std::shared_ptr cipher_sample_data( new uint8_t[clear_sample->data_size()], std::default_delete()); - if (vpx_parser_) { - if (!EncryptVpxFrame(vpx_frames, clear_sample->data(), - clear_sample->data_size(), - &cipher_sample_data.get()[0], decrypt_config.get())) { - return Status(error::ENCRYPTION_FAILURE, "Failed to encrypt VPX frame."); + const uint8_t* source = clear_sample->data(); + uint8_t* dest = cipher_sample_data.get(); + if (!subsamples.empty()) { + size_t total_size = 0; + for (const SubsampleEntry& subsample : subsamples) { + if (subsample.clear_bytes > 0) { + memcpy(dest, source, subsample.clear_bytes); + source += subsample.clear_bytes; + dest += subsample.clear_bytes; + total_size += subsample.clear_bytes; + } + if (subsample.cipher_bytes > 0) { + EncryptBytes(source, subsample.cipher_bytes, dest); + source += subsample.cipher_bytes; + dest += subsample.cipher_bytes; + total_size += subsample.cipher_bytes; + } } - DCHECK_EQ(decrypt_config->GetTotalSizeOfSubsamples(), - clear_sample->data_size()); - } else if (header_parser_) { - if (!EncryptNalFrame(clear_sample->data(), clear_sample->data_size(), - &cipher_sample_data.get()[0], decrypt_config.get())) { - return Status(error::ENCRYPTION_FAILURE, "Failed to encrypt NAL frame."); - } - DCHECK_EQ(decrypt_config->GetTotalSizeOfSubsamples(), - clear_sample->data_size()); + DCHECK_EQ(total_size, clear_sample->data_size()); } else { - memcpy(&cipher_sample_data.get()[0], clear_sample->data(), - std::min(clear_sample->data_size(), leading_clear_bytes_size_)); - if (clear_sample->data_size() > leading_clear_bytes_size_) { - // The residual block is left unecrypted (copied without encryption). No - // need to do special handling here. - EncryptBytes(clear_sample->data() + leading_clear_bytes_size_, - clear_sample->data_size() - leading_clear_bytes_size_, - &cipher_sample_data.get()[leading_clear_bytes_size_]); - } + EncryptBytes(source, clear_sample->data_size(), dest); } + std::shared_ptr cipher_sample(clear_sample->Clone()); cipher_sample->TransferData(std::move(cipher_sample_data), clear_sample->data_size()); + // Finish initializing the sample before sending it downstream. We must // wait until now to finish the initialization as we will lose access to // |decrypt_config| once we set it. cipher_sample->set_is_encrypted(true); + std::unique_ptr decrypt_config(new DecryptConfig( + encryption_config_->key_id, encryptor_->iv(), subsamples, + protection_scheme_, crypt_byte_block_, skip_byte_block_)); cipher_sample->set_decrypt_config(std::move(decrypt_config)); encryptor_->UpdateIv(); @@ -316,33 +247,7 @@ Status EncryptionHandler::ProcessMediaSample( return DispatchMediaSample(kStreamIndex, std::move(cipher_sample)); } -Status EncryptionHandler::SetupProtectionPattern(StreamType stream_type) { - if (protection_scheme_ == kAppleSampleAesProtectionScheme) { - const size_t kH264LeadingClearBytesSize = 32u; - const size_t kSmallNalUnitSize = 32u + 16u; - const size_t kAudioLeadingClearBytesSize = 16u; - switch (codec_) { - case kCodecH264: - leading_clear_bytes_size_ = kH264LeadingClearBytesSize; - min_protected_data_size_ = kSmallNalUnitSize + 1u; - break; - case kCodecAAC: - FALLTHROUGH_INTENDED; - case kCodecAC3: - FALLTHROUGH_INTENDED; - case kCodecEAC3: - // E-AC3 encryption is handled by SampleAesEc3Cryptor, which also - // manages leading clear bytes. - leading_clear_bytes_size_ = - codec_ == kCodecEAC3 ? 0 : kAudioLeadingClearBytesSize; - min_protected_data_size_ = leading_clear_bytes_size_ + 15u; - break; - default: - return Status( - error::ENCRYPTION_FAILURE, - "Only AAC/AC3/EAC3 and H264 are supported in Sample AES."); - } - } +void EncryptionHandler::SetupProtectionPattern(StreamType stream_type) { if (stream_type == kStreamVideo && IsPatternEncryptionScheme(protection_scheme_)) { // Use 1:9 pattern. @@ -355,7 +260,6 @@ Status EncryptionHandler::SetupProtectionPattern(StreamType stream_type) { crypt_byte_block_ = 0u; skip_byte_block_ = 0u; } - return Status::OK; } bool EncryptionHandler::CreateEncryptor(const EncryptionKey& encryption_key) { @@ -384,124 +288,6 @@ bool EncryptionHandler::CreateEncryptor(const EncryptionKey& encryption_key) { return true; } -bool EncryptionHandler::EncryptVpxFrame( - const std::vector& vpx_frames, - const uint8_t* source, - size_t source_size, - uint8_t* dest, - DecryptConfig* decrypt_config) { - const uint8_t* data = source; - for (const VPxFrameInfo& frame : vpx_frames) { - uint16_t clear_bytes = - static_cast(frame.uncompressed_header_size); - uint32_t cipher_bytes = static_cast( - frame.frame_size - frame.uncompressed_header_size); - - // "VP Codec ISO Media File Format Binding" document requires that the - // encrypted bytes of each frame within the superframe must be block - // aligned so that the counter state can be computed for each frame - // within the superframe. - // ISO/IEC 23001-7:2016 10.2 'cbc1' 10.3 'cens' - // The BytesOfProtectedData size SHALL be a multiple of 16 bytes to - // avoid partial blocks in Subsamples. - // For consistency, apply block alignment to all frames. - const uint16_t misalign_bytes = cipher_bytes % kCencBlockSize; - clear_bytes += misalign_bytes; - cipher_bytes -= misalign_bytes; - - decrypt_config->AddSubsample(clear_bytes, cipher_bytes); - memcpy(dest, data, clear_bytes); - if (cipher_bytes > 0) - EncryptBytes(data + clear_bytes, cipher_bytes, dest + clear_bytes); - data += frame.frame_size; - dest += frame.frame_size; - } - // Add subsample for the superframe index if exists. - const bool is_superframe = vpx_frames.size() > 1; - if (is_superframe) { - size_t index_size = source + source_size - data; - DCHECK_LE(index_size, 2 + vpx_frames.size() * 4); - DCHECK_GE(index_size, 2 + vpx_frames.size() * 1); - uint16_t clear_bytes = static_cast(index_size); - uint32_t cipher_bytes = 0; - decrypt_config->AddSubsample(clear_bytes, cipher_bytes); - memcpy(dest, data, clear_bytes); - } - return true; -} - -bool EncryptionHandler::EncryptNalFrame(const uint8_t* source, - size_t source_size, - uint8_t* dest, - DecryptConfig* decrypt_config) { - DCHECK_NE(nalu_length_size_, 0u); - DCHECK(header_parser_); - const Nalu::CodecType nalu_type = - (codec_ == kCodecH265) ? Nalu::kH265 : Nalu::kH264; - NaluReader reader(nalu_type, nalu_length_size_, source, source_size); - - // Store the current length of clear data. This is used to squash - // multiple unencrypted NAL units into fewer subsample entries. - uint64_t accumulated_clear_bytes = 0; - - Nalu nalu; - NaluReader::Result result; - while ((result = reader.Advance(&nalu)) == NaluReader::kOk) { - const uint64_t nalu_total_size = nalu.header_size() + nalu.payload_size(); - if (nalu.is_video_slice() && nalu_total_size >= min_protected_data_size_) { - uint64_t current_clear_bytes = leading_clear_bytes_size_; - if (current_clear_bytes == 0) { - // For video-slice NAL units, encrypt the video slice. This skips - // the frame header. - const int64_t video_slice_header_size = - header_parser_->GetHeaderSize(nalu); - if (video_slice_header_size < 0) { - LOG(ERROR) << "Failed to read slice header."; - return false; - } - current_clear_bytes = nalu.header_size() + video_slice_header_size; - } - uint64_t cipher_bytes = nalu_total_size - current_clear_bytes; - - // ISO/IEC 23001-7:2016 10.2 'cbc1' 10.3 'cens' - // The BytesOfProtectedData size SHALL be a multiple of 16 bytes to - // avoid partial blocks in Subsamples. - // CMAF requires 'cenc' scheme BytesOfProtectedData SHALL be a multiple - // of 16 bytes; while 'cbcs' scheme BytesOfProtectedData SHALL start on - // the first byte of video data following the slice header. - if (protection_scheme_ == FOURCC_cbc1 || - protection_scheme_ == FOURCC_cens || - protection_scheme_ == FOURCC_cenc) { - const uint16_t misalign_bytes = cipher_bytes % kCencBlockSize; - current_clear_bytes += misalign_bytes; - cipher_bytes -= misalign_bytes; - } - - accumulated_clear_bytes += nalu_length_size_ + current_clear_bytes; - AddSubsample(accumulated_clear_bytes, cipher_bytes, decrypt_config); - memcpy(dest, source, accumulated_clear_bytes); - source += accumulated_clear_bytes; - dest += accumulated_clear_bytes; - accumulated_clear_bytes = 0; - - DCHECK_EQ(nalu.data() + current_clear_bytes, source); - EncryptBytes(source, cipher_bytes, dest); - source += cipher_bytes; - dest += cipher_bytes; - } else { - // For non-video-slice or small NAL units, don't encrypt. - accumulated_clear_bytes += nalu_length_size_ + nalu_total_size; - } - } - if (result != NaluReader::kEOStream) { - LOG(ERROR) << "Failed to parse NAL units."; - return false; - } - AddSubsample(accumulated_clear_bytes, 0, decrypt_config); - memcpy(dest, source, accumulated_clear_bytes); - return true; -} - void EncryptionHandler::EncryptBytes(const uint8_t* source, size_t source_size, uint8_t* dest) { @@ -511,14 +297,9 @@ void EncryptionHandler::EncryptBytes(const uint8_t* source, CHECK(encryptor_->Crypt(source, source_size, dest)); } -void EncryptionHandler::InjectVpxParserForTesting( - std::unique_ptr vpx_parser) { - vpx_parser_ = std::move(vpx_parser); -} - -void EncryptionHandler::InjectVideoSliceHeaderParserForTesting( - std::unique_ptr header_parser) { - header_parser_ = std::move(header_parser); +void EncryptionHandler::InjectSubsampleGeneratorForTesting( + std::unique_ptr generator) { + subsample_generator_ = std::move(generator); } void EncryptionHandler::InjectEncryptorFactoryForTesting( diff --git a/packager/media/crypto/encryption_handler.h b/packager/media/crypto/encryption_handler.h index cc0ebb81d9..3f100b390c 100644 --- a/packager/media/crypto/encryption_handler.h +++ b/packager/media/crypto/encryption_handler.h @@ -16,10 +16,8 @@ namespace media { class AesCryptor; class AesEncryptorFactory; -class VideoSliceHeaderParser; -class VPxParser; +class SubsampleGenerator; struct EncryptionKey; -struct VPxFrameInfo; class EncryptionHandler : public MediaHandler { public: @@ -46,21 +44,8 @@ class EncryptionHandler : public MediaHandler { // Processes media sample and encrypts it if needed. Status ProcessMediaSample(std::shared_ptr clear_sample); - Status SetupProtectionPattern(StreamType stream_type); + void SetupProtectionPattern(StreamType stream_type); bool CreateEncryptor(const EncryptionKey& encryption_key); - // Encrypt a VPx frame with size |source_size|. |dest| should have at least - // |source_size| bytes. - bool EncryptVpxFrame(const std::vector& vpx_frames, - const uint8_t* source, - size_t source_size, - uint8_t* dest, - DecryptConfig* decrypt_config); - // Encrypt a NAL unit frame with size |source_size|. |dest| should have at - // least |source_size| bytes. - bool EncryptNalFrame(const uint8_t* source, - size_t source_size, - uint8_t* dest, - DecryptConfig* decrypt_config); // Encrypt an E-AC3 frame with size |source_size| according to SAMPLE-AES // specification. |dest| should have at least |source_size| bytes. bool SampleAesEncryptEac3Frame(const uint8_t* source, @@ -78,9 +63,8 @@ class EncryptionHandler : public MediaHandler { std::vector* syncframe_sizes); // Testing injections. - void InjectVpxParserForTesting(std::unique_ptr vpx_parser); - void InjectVideoSliceHeaderParserForTesting( - std::unique_ptr header_parser); + void InjectSubsampleGeneratorForTesting( + std::unique_ptr generator); void InjectEncryptorFactoryForTesting( std::unique_ptr encryptor_factory); @@ -92,15 +76,6 @@ class EncryptionHandler : public MediaHandler { std::shared_ptr encryption_config_; std::unique_ptr encryptor_; Codec codec_ = kUnknownCodec; - // Specifies the size of NAL unit length in bytes. Can be 1, 2 or 4 bytes. 0 - // if it is not a NAL structured video. - uint8_t nalu_length_size_ = 0; - // For Sample AES, 32 bytes for Video and 16 bytes for audio. - size_t leading_clear_bytes_size_ = 0; - // For Sample AES, if the data size is less than this value, none of the bytes - // are encrypted. The size is 48+1 bytes for video NAL and 16+15 bytes for - // audio according to MPEG-2 Stream Encryption Format for HTTP Live Streaming. - size_t min_protected_data_size_ = 0; // Remaining clear lead in the stream's time scale. int64_t remaining_clear_lead_ = 0; // Crypto period duration in the stream's time scale. @@ -109,16 +84,12 @@ class EncryptionHandler : public MediaHandler { int64_t prev_crypto_period_index_ = -1; bool check_new_crypto_period_ = false; + std::unique_ptr subsample_generator_; + std::unique_ptr encryptor_factory_; // Number of encrypted blocks (16-byte-block) in pattern based encryption. uint8_t crypt_byte_block_ = 0; /// Number of unencrypted blocks (16-byte-block) in pattern based encryption. uint8_t skip_byte_block_ = 0; - - std::unique_ptr encryptor_factory_; - // VPx parser for VPx streams. - std::unique_ptr vpx_parser_; - // Video slice header parser for NAL strucutred streams. - std::unique_ptr header_parser_; }; } // namespace media diff --git a/packager/media/crypto/encryption_handler_unittest.cc b/packager/media/crypto/encryption_handler_unittest.cc index f5e99c7935..272ecf866f 100644 --- a/packager/media/crypto/encryption_handler_unittest.cc +++ b/packager/media/crypto/encryption_handler_unittest.cc @@ -13,9 +13,8 @@ #include "packager/media/base/media_handler_test_base.h" #include "packager/media/base/mock_aes_cryptor.h" #include "packager/media/base/raw_key_source.h" -#include "packager/media/codecs/video_slice_header_parser.h" -#include "packager/media/codecs/vpx_parser.h" #include "packager/media/crypto/aes_encryptor_factory.h" +#include "packager/media/crypto/subsample_generator.h" #include "packager/status_test_util.h" namespace shaka { @@ -69,19 +68,16 @@ class MockKeySource : public RawKeySource { EncryptionKey* key)); }; -class MockVpxParser : public VPxParser { +class MockSubsampleGenerator : public SubsampleGenerator { public: - MOCK_METHOD3(Parse, - bool(const uint8_t* data, - size_t data_size, - std::vector* vpx_frames)); -}; + MockSubsampleGenerator() : SubsampleGenerator(true) {} -class MockVideoSliceHeaderParser : public VideoSliceHeaderParser { - public: - MOCK_METHOD1(Initialize, - bool(const std::vector& decoder_configuration)); - MOCK_METHOD1(GetHeaderSize, int64_t(const Nalu& nalu)); + MOCK_METHOD2(Initialize, + Status(FourCC protection_scheme, const StreamInfo& stream_info)); + MOCK_METHOD3(GenerateSubsamples, + Status(const uint8_t* frame, + size_t frame_size, + std::vector* subsamples)); }; class MockAesEncryptorFactory : public AesEncryptorFactory { @@ -118,6 +114,9 @@ class EncryptionHandlerTest : public MediaHandlerGraphTestBase { encryption_handler_.reset( new EncryptionHandler(new_encryption_params, &mock_key_source_)); SetUpGraph(1 /* one input */, 1 /* one output */, encryption_handler_); + // Inject default subsamples to avoid parsing problems. + const std::vector empty_subsamples; + InjectSubsamples(empty_subsamples); } Status Process(std::unique_ptr stream_data) { @@ -132,14 +131,15 @@ class EncryptionHandlerTest : public MediaHandlerGraphTestBase { return encryption_key; } - void InjectVpxParserForTesting(std::unique_ptr vpx_parser) { - encryption_handler_->InjectVpxParserForTesting(std::move(vpx_parser)); - } + void InjectSubsamples(const std::vector& subsamples) { + std::unique_ptr mock_generator( + new MockSubsampleGenerator); + EXPECT_CALL(*mock_generator, GenerateSubsamples(_, _, _)) + .WillRepeatedly( + DoAll(SetArgPointee<2>(subsamples), Return(Status::OK))); - void InjectVideoSliceHeaderParserForTesting( - std::unique_ptr header_parser) { - encryption_handler_->InjectVideoSliceHeaderParserForTesting( - std::move(header_parser)); + encryption_handler_->InjectSubsampleGeneratorForTesting( + std::move(mock_generator)); } void InjectEncryptorFactoryForTesting( @@ -199,154 +199,25 @@ TEST_F(EncryptionHandlerTest, CreateEncryptorFailed) { namespace { -const bool kVp9SubsampleEncryption = true; const bool kIsKeyFrame = true; const bool kIsSubsegment = true; const bool kEncrypted = true; const int64_t kSegmentDuration = 1000; -// 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. -const uint8_t kData[]{ - // First NALU - 0x30, 0x01, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, 0x21, - 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30, 0x31, 0x32, 0x33, - 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, - 0x46, - // Second NALU - 0x31, 0x25, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, - 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x20, 0x21, - 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x30, 0x31, 0x32, 0x33, - 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, - 0x46, 0x47, - // Third non-video-slice NALU for H264 or superframe index for VP9. - 0x06, 0x67, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, -}; +// The contents of the data does not matter. +const uint8_t kData[] = {0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09}; const size_t kDataSize = sizeof(kData); -// H264 subsample information for the the above data. -const size_t kNaluLengthSize = 1u; -const size_t kNaluHeaderSize = 1u; -const size_t kSubsampleSize1 = 49u; -const size_t kSliceHeaderSize1 = 1u; -const size_t kSubsampleSize2 = 50u; -const size_t kSliceHeaderSize2 = 16u; -const size_t kSubsampleSize3 = 7u; -// VP9 frame information for the above data. It should match H264 subsample -// information. -const size_t kVpxFrameSize1 = kSubsampleSize1; -const size_t kUncompressedHeaderSize1 = - kNaluLengthSize + kNaluHeaderSize + kSliceHeaderSize1; -const size_t kVpxFrameSize2 = kSubsampleSize2; -const size_t kUncompressedHeaderSize2 = - kNaluLengthSize + kNaluHeaderSize + kSliceHeaderSize2; -// Subsample pairs for the above data. -const size_t kClearSize1 = kUncompressedHeaderSize1; -const size_t kCipherSize1 = kVpxFrameSize1 - kUncompressedHeaderSize1; -const size_t kClearSize2 = kUncompressedHeaderSize2; -const size_t kCipherSize2 = kVpxFrameSize2 - kUncompressedHeaderSize2; -// Align cipher bytes for some protection schemes. -const size_t kAesBlockSize = 16u; -const size_t kAlignedClearSize1 = kClearSize1 + kCipherSize1 % kAesBlockSize; -static_assert(kAlignedClearSize1 != kClearSize1, - "Clearsize 1 should not be aligned"); -const size_t kAlignedCipherSize1 = kCipherSize1 - kCipherSize1 % kAesBlockSize; -// Apple Sample AES. -const size_t kVideoLeadingClearBytesSize = 32u + kNaluLengthSize; -// Subsample 1 is <= 48 bytes, so not encrypted and merged with subsample2. -const size_t kSampleAesClearSize1 = - kSubsampleSize1 + kVideoLeadingClearBytesSize; -const size_t kSampleAesCipherSize1 = - kSubsampleSize2 - kVideoLeadingClearBytesSize; - } // namespace -inline bool operator==(const SubsampleEntry& lhs, const SubsampleEntry& rhs) { - return lhs.clear_bytes == rhs.clear_bytes && - lhs.cipher_bytes == rhs.cipher_bytes; -} - class EncryptionHandlerEncryptionTest : public EncryptionHandlerTest, - public WithParamInterface> { + public WithParamInterface> { public: void SetUp() override { protection_scheme_ = std::tr1::get<0>(GetParam()); codec_ = std::tr1::get<1>(GetParam()); - vp9_subsample_encryption_ = std::tr1::get<2>(GetParam()); - } - - std::vector GetMockVpxFrameInfo() { - std::vector vpx_frames; - vpx_frames.resize(2); - vpx_frames[0].frame_size = kVpxFrameSize1; - vpx_frames[0].uncompressed_header_size = kUncompressedHeaderSize1; - vpx_frames[1].frame_size = kVpxFrameSize2; - vpx_frames[1].uncompressed_header_size = kUncompressedHeaderSize2; - return vpx_frames; - } - - // The subsamples values should match |GetMockVpxFrameInfo| above. - std::vector GetExpectedSubsamples() { - std::vector subsamples; - if (codec_ == kCodecAAC || - (codec_ == kCodecVP9 && !vp9_subsample_encryption_)) { - return subsamples; - } - if (protection_scheme_ == kAppleSampleAesProtectionScheme) { - subsamples.emplace_back(static_cast(kSampleAesClearSize1), - static_cast(kSampleAesCipherSize1)); - subsamples.emplace_back(static_cast(kSubsampleSize3), 0u); - } else { - if (codec_ == kCodecVP9 || protection_scheme_ == FOURCC_cbc1 || - protection_scheme_ == FOURCC_cens || - protection_scheme_ == FOURCC_cenc) { - // Align the encrypted bytes to multiple of 16 bytes. - subsamples.emplace_back(static_cast(kAlignedClearSize1), - static_cast(kAlignedCipherSize1)); - // Subsample 2 is already aligned. - } else { - subsamples.emplace_back(static_cast(kClearSize1), - static_cast(kCipherSize1)); - } - subsamples.emplace_back(static_cast(kClearSize2), - static_cast(kCipherSize2)); - subsamples.emplace_back(static_cast(kSubsampleSize3), 0u); - } - return subsamples; - } - - // Inject vpx parser / video slice header parser if needed. - void InjectCodecParser() { - switch (codec_) { - case kCodecVP9: - if (vp9_subsample_encryption_) { - std::unique_ptr mock_vpx_parser(new MockVpxParser); - EXPECT_CALL(*mock_vpx_parser, Parse(_, kDataSize, _)) - .WillRepeatedly( - DoAll(SetArgPointee<2>(GetMockVpxFrameInfo()), Return(true))); - InjectVpxParserForTesting(std::move(mock_vpx_parser)); - } - break; - case kCodecH264: { - std::unique_ptr mock_header_parser( - new MockVideoSliceHeaderParser); - if (protection_scheme_ == kAppleSampleAesProtectionScheme) { - EXPECT_CALL(*mock_header_parser, GetHeaderSize(_)).Times(0); - } else { - EXPECT_CALL(*mock_header_parser, GetHeaderSize(_)) - .WillOnce(Return(kSliceHeaderSize1)) - .WillOnce(Return(kSliceHeaderSize2)) - .WillRepeatedly(Return(kSliceHeaderSize2)); - } - InjectVideoSliceHeaderParserForTesting(std::move(mock_header_parser)); - break; - } - default: - break; - } } uint8_t GetExpectedCryptByteBlock() { @@ -407,7 +278,6 @@ class EncryptionHandlerEncryptionTest protected: FourCC protection_scheme_; Codec codec_; - bool vp9_subsample_encryption_; }; TEST_P(EncryptionHandlerEncryptionTest, VerifyEncryptorFactoryParams) { @@ -444,7 +314,6 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithNoKeyRotation) { EncryptionParams encryption_params; encryption_params.protection_scheme = protection_scheme_; encryption_params.clear_lead_in_seconds = kClearLeadInSeconds; - encryption_params.vp9_subsample_encryption = vp9_subsample_encryption_; SetUpEncryptionHandler(encryption_params); const EncryptionKey mock_encryption_key = GetMockEncryptionKey(); @@ -475,8 +344,6 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithNoKeyRotation) { ClearOutputStreamDataVector(); Mock::VerifyAndClearExpectations(&mock_key_source_); - InjectCodecParser(); - // There are three segments. Only the third segment is encrypted. for (int i = 0; i < 3; ++i) { // Use single-frame segment for testing. @@ -501,7 +368,7 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithNoKeyRotation) { decrypt_config->key_id()); EXPECT_EQ(std::vector(kIv, kIv + sizeof(kIv)), decrypt_config->iv()); - EXPECT_EQ(GetExpectedSubsamples(), decrypt_config->subsamples()); + EXPECT_TRUE(decrypt_config->subsamples().empty()); EXPECT_EQ(protection_scheme_, decrypt_config->protection_scheme()); EXPECT_EQ(GetExpectedCryptByteBlock(), decrypt_config->crypt_byte_block()); @@ -523,7 +390,6 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) { encryption_params.clear_lead_in_seconds = kClearLeadInSeconds; encryption_params.crypto_period_duration_in_seconds = kCryptoPeriodDurationInSeconds; - encryption_params.vp9_subsample_encryption = vp9_subsample_encryption_; SetUpEncryptionHandler(encryption_params); if (IsVideoCodec(codec_)) { @@ -550,8 +416,6 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) { encryption_config.key_id); ClearOutputStreamDataVector(); - InjectCodecParser(); - // There are five segments with the first two not encrypted. for (int i = 0; i < 5; ++i) { if ((i % kSegmentsPerCryptoPeriod) == 0) { @@ -586,17 +450,109 @@ TEST_P(EncryptionHandlerEncryptionTest, ClearLeadWithKeyRotation) { } } -INSTANTIATE_TEST_CASE_P( - CencProtectionSchemes, - EncryptionHandlerEncryptionTest, - Combine(Values(FOURCC_cenc, FOURCC_cens, FOURCC_cbc1, FOURCC_cbcs), - Values(kCodecAAC, kCodecH264, kCodecVP9), - Values(kVp9SubsampleEncryption, !kVp9SubsampleEncryption))); -INSTANTIATE_TEST_CASE_P(AppleSampleAes, +INSTANTIATE_TEST_CASE_P(ProtectionSchemes, EncryptionHandlerEncryptionTest, - Combine(Values(kAppleSampleAesProtectionScheme), - Values(kCodecAAC, kCodecH264), - Values(kVp9SubsampleEncryption))); + Combine(Values(kAppleSampleAesProtectionScheme, + FOURCC_cenc, + FOURCC_cens, + FOURCC_cbc1, + FOURCC_cbcs), + Values(kCodecAAC, kCodecH264))); + +struct SubsampleTestCase { + std::vector subsamples; + std::vector expected_output; +}; + +inline bool operator==(const SubsampleEntry& lhs, const SubsampleEntry& rhs) { + return lhs.clear_bytes == rhs.clear_bytes && + lhs.cipher_bytes == rhs.cipher_bytes; +} + +namespace { + +const int64_t kSampleDuration = 1000; + +// This mock encryption increases every byte by 0x10. See the function below. +const SubsampleTestCase kSubsampleTestCases[] = { + { + std::vector(), // No subsamples, i.e. full sample + // encrypted. + {0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19}, + }, + { + {{8, 2}}, // One subsample. + {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x18, 0x19}, + }, + { + {{6, 2}, {2, 0}}, // Two subsamples. + {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x16, 0x17, 0x08, 0x09}, + }, + { + {{6, 2}, {0, 2}}, // Two subsamples. + {0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x16, 0x17, 0x18, 0x19}, + }, +}; + +bool MockEncrypt(const uint8_t* text, + size_t text_size, + uint8_t* crypt_text, + size_t* crypt_text_size) { + *crypt_text_size = text_size; + for (size_t i = 0; i < text_size; i++) + crypt_text[i] = text[i] + 0x10; + return true; +} + +} // namespace + +class EncryptionHandlerSubsampleTest + : public EncryptionHandlerTest, + public WithParamInterface {}; + +INSTANTIATE_TEST_CASE_P(SubsampleTestCases, + EncryptionHandlerSubsampleTest, + ValuesIn(kSubsampleTestCases)); + +TEST_P(EncryptionHandlerSubsampleTest, SubsampleTest) { + std::unique_ptr mock_encryptor(new MockAesCryptor); + EXPECT_CALL(*mock_encryptor, CryptInternal(_, _, _, _)) + .WillRepeatedly(Invoke(MockEncrypt)); + ASSERT_TRUE(mock_encryptor->SetIv( + std::vector(std::begin(kIv), std::end(kIv)))); + + std::unique_ptr mock_encryptor_factory( + new MockAesEncryptorFactory); + EXPECT_CALL(*mock_encryptor_factory, CreateEncryptor(_, _, _, _, _, _)) + .WillOnce(Return(ByMove(std::move(mock_encryptor)))); + InjectEncryptorFactoryForTesting(std::move(mock_encryptor_factory)); + + InjectSubsamples(GetParam().subsamples); + + EXPECT_CALL(mock_key_source_, GetKey(_, _)) + .WillOnce( + DoAll(SetArgPointee<1>(GetMockEncryptionKey()), Return(Status::OK))); + + ASSERT_OK(Process(StreamData::FromStreamInfo( + kStreamIndex, GetVideoStreamInfo(kTimeScale, kCodecH264)))); + ASSERT_OK(Process(StreamData::FromMediaSample( + kStreamIndex, + GetMediaSample(0, kSampleDuration, kIsKeyFrame, kData, kDataSize)))); + + const auto& output_stream_data = GetOutputStreamDataVector(); + EXPECT_THAT(output_stream_data, + ElementsAre(IsStreamInfo(kStreamIndex, kTimeScale, kEncrypted, _), + IsMediaSample(kStreamIndex, 0, kSampleDuration, + kEncrypted, _))); + + const MediaSample& sample = *output_stream_data.back()->media_sample; + EXPECT_EQ( + GetParam().expected_output, + std::vector(sample.data(), sample.data() + sample.data_size())); + + const DecryptConfig& decrypt_config = *sample.decrypt_config(); + EXPECT_EQ(GetParam().subsamples, decrypt_config.subsamples()); +} class EncryptionHandlerTrackTypeTest : public EncryptionHandlerTest {}; diff --git a/packager/media/crypto/subsample_generator.cc b/packager/media/crypto/subsample_generator.cc new file mode 100644 index 0000000000..48d4e706da --- /dev/null +++ b/packager/media/crypto/subsample_generator.cc @@ -0,0 +1,303 @@ +// Copyright 2018 Google LLC. 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/crypto/subsample_generator.h" + +#include +#include + +#include "packager/media/base/decrypt_config.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" +#include "packager/media/codecs/vp9_parser.h" + +namespace shaka { +namespace media { +namespace { + +const size_t kAesBlockSize = 16u; + +uint8_t GetNaluLengthSize(const StreamInfo& stream_info) { + if (stream_info.stream_type() != kStreamVideo) + return 0; + + const VideoStreamInfo& video_stream_info = + static_cast(stream_info); + return video_stream_info.nalu_length_size(); +} + +// A convenient util class to organize subsamples, e.g. combine consecutive +// subsamples with only clear bytes, split subsamples if the clear bytes exceeds +// 2^16 etc. +class SubsampleOrganizer { + public: + SubsampleOrganizer(bool align_protected_data, + std::vector* subsamples) + : align_protected_data_(align_protected_data), subsamples_(subsamples) {} + + ~SubsampleOrganizer() { + if (accumulated_clear_bytes_ > 0) { + PushSubsample(accumulated_clear_bytes_, 0); + accumulated_clear_bytes_ = 0; + } + } + + void AddSubsample(size_t clear_bytes, size_t cipher_bytes) { + DCHECK_LT(clear_bytes, std::numeric_limits::max()); + DCHECK_LT(cipher_bytes, std::numeric_limits::max()); + + if (align_protected_data_ && cipher_bytes != 0) { + const size_t misalign_bytes = cipher_bytes % kAesBlockSize; + clear_bytes += misalign_bytes; + cipher_bytes -= misalign_bytes; + } + + accumulated_clear_bytes_ += clear_bytes; + // Accumulated clear bytes are handled later. + if (cipher_bytes == 0) + return; + + PushSubsample(accumulated_clear_bytes_, cipher_bytes); + accumulated_clear_bytes_ = 0; + } + + private: + SubsampleOrganizer(const SubsampleOrganizer&) = delete; + SubsampleOrganizer& operator=(const SubsampleOrganizer&) = delete; + + void PushSubsample(size_t clear_bytes, size_t cipher_bytes) { + const uint16_t kUInt16Max = std::numeric_limits::max(); + while (clear_bytes > kUInt16Max) { + subsamples_->emplace_back(kUInt16Max, 0); + clear_bytes -= kUInt16Max; + } + subsamples_->emplace_back(static_cast(clear_bytes), + static_cast(cipher_bytes)); + } + + const bool align_protected_data_ = false; + std::vector* const subsamples_ = nullptr; + size_t accumulated_clear_bytes_ = 0; +}; + +} // namespace + +SubsampleGenerator::SubsampleGenerator(bool vp9_subsample_encryption) + : vp9_subsample_encryption_(vp9_subsample_encryption) {} + +SubsampleGenerator::~SubsampleGenerator() {} + +Status SubsampleGenerator::Initialize(FourCC protection_scheme, + const StreamInfo& stream_info) { + codec_ = stream_info.codec(); + nalu_length_size_ = GetNaluLengthSize(stream_info); + + switch (codec_) { + case kCodecVP9: + if (vp9_subsample_encryption_) + vpx_parser_.reset(new VP9Parser); + break; + case kCodecH264: + header_parser_.reset(new H264VideoSliceHeaderParser); + break; + case kCodecH265: + header_parser_.reset(new H265VideoSliceHeaderParser); + break; + default: + // Other codecs should have nalu length size == 0. + if (nalu_length_size_ > 0) { + LOG(WARNING) << "Unknown video codec '" << codec_ << "'"; + return Status(error::ENCRYPTION_FAILURE, "Unknown video codec."); + } + } + if (header_parser_) { + CHECK_NE(nalu_length_size_, 0u) << "AnnexB stream is not supported yet"; + if (!header_parser_->Initialize(stream_info.codec_config())) { + return Status(error::ENCRYPTION_FAILURE, + "Fail to read SPS and PPS data."); + } + } + + switch (codec_) { + case kCodecVP9: + // "VP Codec ISO Media File Format Binding" document requires that the + // encrypted bytes of each frame within the superframe must be block + // aligned so that the counter state can be computed for each frame + // within the superframe. + // ISO/IEC 23001-7:2016 10.2 'cbc1' 10.3 'cens' + // The BytesOfProtectedData size SHALL be a multiple of 16 bytes to + // avoid partial blocks in Subsamples. + // For consistency, apply block alignment to all frames when VP9 subsample + // encryption is enabled. + align_protected_data_ = vp9_subsample_encryption_; + break; + default: + // ISO/IEC 23001-7:2016 10.2 'cbc1' 10.3 'cens' + // The BytesOfProtectedData size SHALL be a multiple of 16 bytes to avoid + // partial blocks in Subsamples. + // CMAF requires 'cenc' scheme BytesOfProtectedData SHALL be a multiple of + // 16 bytes; while 'cbcs' scheme BytesOfProtectedData SHALL start on the + // first byte of video data following the slice header. + align_protected_data_ = protection_scheme == FOURCC_cbc1 || + protection_scheme == FOURCC_cens || + protection_scheme == FOURCC_cenc; + break; + } + + if (protection_scheme == kAppleSampleAesProtectionScheme) { + const size_t kH264LeadingClearBytesSize = 32u; + const size_t kAudioLeadingClearBytesSize = 16u; + switch (codec_) { + case kCodecH264: + leading_clear_bytes_size_ = kH264LeadingClearBytesSize; + min_protected_data_size_ = + leading_clear_bytes_size_ + kAesBlockSize + 1u; + break; + case kCodecAAC: + FALLTHROUGH_INTENDED; + case kCodecAC3: + leading_clear_bytes_size_ = kAudioLeadingClearBytesSize; + min_protected_data_size_ = leading_clear_bytes_size_ + kAesBlockSize; + break; + case kCodecEAC3: + // E-AC3 encryption is handled by SampleAesEc3Cryptor, which also + // manages leading clear bytes. + leading_clear_bytes_size_ = 0; + min_protected_data_size_ = leading_clear_bytes_size_ + kAesBlockSize; + break; + default: + LOG(ERROR) << "Unexpected codec for SAMPLE-AES " << codec_; + return Status(error::ENCRYPTION_FAILURE, + "Unexpected codec for SAMPLE-AES."); + } + } + return Status::OK; +} + +Status SubsampleGenerator::GenerateSubsamples( + const uint8_t* frame, + size_t frame_size, + std::vector* subsamples) { + subsamples->clear(); + switch (codec_) { + case kCodecH264: + FALLTHROUGH_INTENDED; + case kCodecH265: + return GenerateSubsamplesFromH26xFrame(frame, frame_size, subsamples); + case kCodecVP9: + if (vp9_subsample_encryption_) + return GenerateSubsamplesFromVPxFrame(frame, frame_size, subsamples); + // Full sample encrypted so no subsamples. + break; + default: + // Other codecs are full sample encrypted unless there are clear leading + // bytes. + if (leading_clear_bytes_size_ > 0) { + SubsampleOrganizer subsample_organizer(align_protected_data_, + subsamples); + const size_t clear_bytes = + std::min(frame_size, leading_clear_bytes_size_); + const size_t cipher_bytes = frame_size - clear_bytes; + subsample_organizer.AddSubsample(clear_bytes, cipher_bytes); + } else { + // Full sample encrypted so no subsamples. + } + break; + } + return Status::OK; +} + +void SubsampleGenerator::InjectVpxParserForTesting( + std::unique_ptr vpx_parser) { + vpx_parser_ = std::move(vpx_parser); +} + +void SubsampleGenerator::InjectVideoSliceHeaderParserForTesting( + std::unique_ptr header_parser) { + header_parser_ = std::move(header_parser); +} + +Status SubsampleGenerator::GenerateSubsamplesFromVPxFrame( + const uint8_t* frame, + size_t frame_size, + std::vector* subsamples) { + DCHECK(vpx_parser_); + std::vector vpx_frames; + if (!vpx_parser_->Parse(frame, frame_size, &vpx_frames)) + return Status(error::ENCRYPTION_FAILURE, "Failed to parse vpx frame."); + + SubsampleOrganizer subsample_organizer(align_protected_data_, subsamples); + + size_t total_size = 0; + for (const VPxFrameInfo& frame : vpx_frames) { + subsample_organizer.AddSubsample( + frame.uncompressed_header_size, + frame.frame_size - frame.uncompressed_header_size); + total_size += frame.frame_size; + } + // Add subsample for the superframe index if exists. + const bool is_superframe = vpx_frames.size() > 1; + if (is_superframe) { + const size_t index_size = frame_size - total_size; + DCHECK_LE(index_size, 2 + vpx_frames.size() * 4); + DCHECK_GE(index_size, 2 + vpx_frames.size() * 1); + subsample_organizer.AddSubsample(index_size, 0); + } else { + DCHECK_EQ(total_size, frame_size); + } + return Status::OK; +} + +Status SubsampleGenerator::GenerateSubsamplesFromH26xFrame( + const uint8_t* frame, + size_t frame_size, + std::vector* subsamples) { + DCHECK_NE(nalu_length_size_, 0u); + DCHECK(header_parser_); + + SubsampleOrganizer subsample_organizer(align_protected_data_, subsamples); + + const Nalu::CodecType nalu_type = + (codec_ == kCodecH265) ? Nalu::kH265 : Nalu::kH264; + NaluReader reader(nalu_type, nalu_length_size_, frame, frame_size); + + Nalu nalu; + NaluReader::Result result; + while ((result = reader.Advance(&nalu)) == NaluReader::kOk) { + const size_t nalu_total_size = nalu.header_size() + nalu.payload_size(); + size_t clear_bytes = 0; + if (nalu.is_video_slice() && nalu_total_size >= min_protected_data_size_) { + clear_bytes = leading_clear_bytes_size_; + if (clear_bytes == 0) { + // For video-slice NAL units, encrypt the video slice. This skips + // the frame header. + const int64_t video_slice_header_size = + header_parser_->GetHeaderSize(nalu); + if (video_slice_header_size < 0) { + LOG(ERROR) << "Failed to read slice header."; + return Status(error::ENCRYPTION_FAILURE, + "Failed to read slice header."); + } + clear_bytes = nalu.header_size() + video_slice_header_size; + } + } else { + // For non-video-slice or small NAL units, don't encrypt. + clear_bytes = nalu_total_size; + } + const size_t cipher_bytes = nalu_total_size - clear_bytes; + subsample_organizer.AddSubsample(nalu_length_size_ + clear_bytes, + cipher_bytes); + } + if (result != NaluReader::kEOStream) { + LOG(ERROR) << "Failed to parse NAL units."; + return Status(error::ENCRYPTION_FAILURE, "Failed to parse NAL units."); + } + return Status::OK; +} + +} // namespace media +} // namespace shaka diff --git a/packager/media/crypto/subsample_generator.h b/packager/media/crypto/subsample_generator.h new file mode 100644 index 0000000000..5a5ae5a589 --- /dev/null +++ b/packager/media/crypto/subsample_generator.h @@ -0,0 +1,103 @@ +// Copyright 2018 Google LLC. 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_CRYPTO_SUBSAMPLE_GENERATOR_H_ +#define PACKAGER_MEDIA_CRYPTO_SUBSAMPLE_GENERATOR_H_ + +#include +#include + +#include "packager/media/base/fourccs.h" +#include "packager/media/base/stream_info.h" +#include "packager/status.h" + +namespace shaka { +namespace media { + +class VideoSliceHeaderParser; +class VPxParser; +struct SubsampleEntry; + +/// Parsing and generating encryption subsamples from bitstreams. Note that the +/// class can be used to generate subsamples from both audio and video +/// bitstreams according to relevant specifications. For example, for video +/// streams, the most notable specifications are Common Encryption v3 spec [1] +/// and Apple SAMPLE AES spec [2]; for audio streams, they are full sample +/// encrypted except for Apple SAMPLE AES spec, which usually contains leading +/// clear bytes. +/// [1] https://www.iso.org/standard/68042.html +/// [2] https://apple.co/2Noi43q +class SubsampleGenerator { + public: + /// @param vp9_subsample_encryption determines if subsample encryption or full + /// sample encryption is used for VP9. Only relevant for VP9 codec. + explicit SubsampleGenerator(bool vp9_subsample_encryption); + + virtual ~SubsampleGenerator(); + + /// Initialize the generator. + /// @param protection_scheme is the protection scheme to be used for the + /// encryption. It is used to determine if the protected data should be + /// block aligned. + /// @param stream_info contains stream information. + /// @returns OK on success, an error status otherwise. + virtual Status Initialize(FourCC protection_scheme, + const StreamInfo& stream_info); + + /// Generates subsamples from the bitstream. Note that all frames should be + /// processed by this function even if it is not encrypted as the next + /// (encrypted) frame may be dependent on the previous clear frames. + /// @param frame points to the start of the frame. + /// @param frame_size is the size of the frame. + /// @param[out] subsamples will contain the output subsamples on success. It + /// will be empty if the frame should be full sample encrypted. + /// @returns OK on success, an error status otherwise. + virtual Status GenerateSubsamples(const uint8_t* frame, + size_t frame_size, + std::vector* subsamples); + + // Testing injections. + void InjectVpxParserForTesting(std::unique_ptr vpx_parser); + void InjectVideoSliceHeaderParserForTesting( + std::unique_ptr header_parser); + + private: + SubsampleGenerator(const SubsampleGenerator&) = delete; + SubsampleGenerator& operator=(const SubsampleGenerator&) = delete; + + Status GenerateSubsamplesFromVPxFrame( + const uint8_t* frame, + size_t frame_size, + std::vector* subsamples); + Status GenerateSubsamplesFromH26xFrame( + const uint8_t* frame, + size_t frame_size, + std::vector* subsamples); + + const bool vp9_subsample_encryption_ = false; + // Whether the protected portion should be AES block (16 bytes) aligned. + bool align_protected_data_ = false; + Codec codec_ = kUnknownCodec; + // For NAL structured video only, the size of NAL unit length in bytes. Can be + // 1, 2 or 4 bytes. + uint8_t nalu_length_size_ = 0; + // For SAMPLE AES only, 32 bytes for Video and 16 bytes for audio. + size_t leading_clear_bytes_size_ = 0; + // For SAMPLE AES only, if the data size is less than this value, none of the + // bytes are encrypted. The size is 48+1 bytes for video NAL and 32 bytes for + // audio according to MPEG-2 Stream Encryption Format for HTTP Live Streaming. + size_t min_protected_data_size_ = 0; + + // VPx parser for VPx streams. + std::unique_ptr vpx_parser_; + // Video slice header parser for NAL strucutred streams. + std::unique_ptr header_parser_; +}; + +} // namespace media +} // namespace shaka + +#endif // PACKAGER_MEDIA_CRYPTO_SUBSAMPLE_GENERATOR_H_ diff --git a/packager/media/crypto/subsample_generator_unittest.cc b/packager/media/crypto/subsample_generator_unittest.cc new file mode 100644 index 0000000000..a0c44cb4c7 --- /dev/null +++ b/packager/media/crypto/subsample_generator_unittest.cc @@ -0,0 +1,435 @@ +// Copyright 2018 Google LLC. 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/crypto/subsample_generator.h" + +#include +#include + +#include "packager/media/base/audio_stream_info.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" +#include "packager/status_test_util.h" + +namespace shaka { +namespace media { +namespace { + +using ::testing::_; +using ::testing::DoAll; +using ::testing::ElementsAre; +using ::testing::ElementsAreArray; +using ::testing::Return; +using ::testing::SetArgPointee; +using ::testing::Test; +using ::testing::Values; +using ::testing::WithParamInterface; + +const bool kVP9SubsampleEncryption = true; +// Use H264 code config. +const uint8_t kH264CodecConfig[]{ + // clang-format off + // 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, + // clang-format on +}; +const int kTrackId = 1; +const uint32_t kTimeScale = 1000; +const uint64_t kDuration = 10000; +const char kCodecString[] = "codec string"; +const char kLanguage[] = "eng"; +const bool kEncrypted = true; + +VideoStreamInfo GetVideoStreamInfo(Codec codec) { + const uint16_t kWidth = 10u; + const uint16_t kHeight = 20u; + const uint32_t kPixelWidth = 2u; + const uint32_t kPixelHeight = 3u; + const int16_t kTrickPlayFactor = 0; + const uint8_t kNaluLengthSize = 1u; + + const uint8_t* codec_config = nullptr; + size_t codec_config_size = 0; + switch (codec) { + case kCodecH264: + codec_config = kH264CodecConfig; + codec_config_size = sizeof(kH264CodecConfig); + break; + default: + // We do not care about the codec configs for other codecs in this file. + break; + } + return VideoStreamInfo(kTrackId, kTimeScale, kDuration, codec, + H26xStreamFormat::kUnSpecified, kCodecString, + codec_config, codec_config_size, kWidth, kHeight, + kPixelWidth, kPixelHeight, kTrickPlayFactor, + kNaluLengthSize, kLanguage, !kEncrypted); +} + +AudioStreamInfo GetAudioStreamInfo(Codec codec) { + 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 uint8_t kCodecConfig[] = {0x00}; + + return AudioStreamInfo(kTrackId, kTimeScale, kDuration, codec, kCodecString, + kCodecConfig, sizeof(kCodecConfig), kSampleBits, + kNumChannels, kSamplingFrequency, kSeekPrerollNs, + kCodecDelayNs, kMaxBitrate, kAvgBitrate, kLanguage, + !kEncrypted); +} + +} // namespace + +inline bool operator==(const SubsampleEntry& lhs, const SubsampleEntry& rhs) { + return lhs.clear_bytes == rhs.clear_bytes && + lhs.cipher_bytes == rhs.cipher_bytes; +} + +class MockVPxParser : public VPxParser { + public: + MOCK_METHOD3(Parse, + bool(const uint8_t* data, + size_t data_size, + std::vector* vpx_frames)); +}; + +class MockVideoSliceHeaderParser : public VideoSliceHeaderParser { + public: + MOCK_METHOD1(Initialize, + bool(const std::vector& decoder_configuration)); + MOCK_METHOD1(GetHeaderSize, int64_t(const Nalu& nalu)); +}; + +class SubsampleGeneratorTest : public Test, public WithParamInterface { + public: + SubsampleGeneratorTest() : protection_scheme_(GetParam()) {} + + protected: + FourCC protection_scheme_; +}; + +TEST_P(SubsampleGeneratorTest, VP9FullSampleEncryption) { + SubsampleGenerator generator(!kVP9SubsampleEncryption); + ASSERT_OK( + generator.Initialize(protection_scheme_, GetVideoStreamInfo(kCodecVP9))); + + constexpr size_t kFrameSize = 50; + constexpr uint8_t kFrame[kFrameSize] = {}; + + std::vector subsamples; + ASSERT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); + EXPECT_THAT(subsamples, ElementsAre()); +} + +TEST_P(SubsampleGeneratorTest, VP9ParseFailed) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK( + generator.Initialize(protection_scheme_, GetVideoStreamInfo(kCodecVP9))); + + constexpr size_t kFrameSize = 50; + constexpr uint8_t kFrame[kFrameSize] = {}; + + std::unique_ptr mock_vpx_parser(new MockVPxParser); + EXPECT_CALL(*mock_vpx_parser, Parse(kFrame, kFrameSize, _)) + .WillOnce(Return(false)); + + generator.InjectVpxParserForTesting(std::move(mock_vpx_parser)); + + std::vector subsamples; + ASSERT_NOT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); +} + +TEST_P(SubsampleGeneratorTest, VP9SubsampleEncryption) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK( + generator.Initialize(protection_scheme_, GetVideoStreamInfo(kCodecVP9))); + + constexpr size_t kFrameSize = 50; + constexpr uint8_t kFrame[kFrameSize] = {}; + constexpr size_t kUncompressedHeaderSize = 20; + // VP9 block align protected data for all protection schemes. + const SubsampleEntry kExpectedSubsamples[] = { + // {20,30} block aligned. + {34, 16}, + }; + + std::vector vpx_frame_info(1); + vpx_frame_info[0].frame_size = kFrameSize; + vpx_frame_info[0].uncompressed_header_size = kUncompressedHeaderSize; + + std::unique_ptr mock_vpx_parser(new MockVPxParser); + EXPECT_CALL(*mock_vpx_parser, Parse(kFrame, kFrameSize, _)) + .WillOnce(DoAll(SetArgPointee<2>(vpx_frame_info), Return(true))); + + generator.InjectVpxParserForTesting(std::move(mock_vpx_parser)); + + std::vector subsamples; + ASSERT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); + EXPECT_THAT(subsamples, ElementsAreArray(kExpectedSubsamples)); +} + +TEST_P(SubsampleGeneratorTest, VP9SubsampleEncryptionWithSuperFrame) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK( + generator.Initialize(protection_scheme_, GetVideoStreamInfo(kCodecVP9))); + + constexpr size_t kFrameSize = 50; + constexpr uint8_t kFrame[kFrameSize] = {}; + // Super frame with two subframes. + constexpr size_t kSubFrameSizes[] = {10, 34}; + constexpr size_t kUncompressedHeaderSizes[] = {4, 1}; + // VP9 block align protected data for all protection schemes. + const SubsampleEntry kExpectedSubsamples[] = { + // {4,6},{1,33} block aligned => {10,0},{2,32} + // Then merge consecutive clear-only subsamples. + {12, 32}, + // Superframe index (50 - 10 - 34). + {6, 0}, + }; + + std::vector vpx_frame_info(2); + for (int i = 0; i < 2; i++) { + vpx_frame_info[i].frame_size = kSubFrameSizes[i]; + vpx_frame_info[i].uncompressed_header_size = kUncompressedHeaderSizes[i]; + } + + std::unique_ptr mock_vpx_parser(new MockVPxParser); + EXPECT_CALL(*mock_vpx_parser, Parse(kFrame, kFrameSize, _)) + .WillOnce(DoAll(SetArgPointee<2>(vpx_frame_info), Return(true))); + + generator.InjectVpxParserForTesting(std::move(mock_vpx_parser)); + + std::vector subsamples; + ASSERT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); + EXPECT_THAT(subsamples, ElementsAreArray(kExpectedSubsamples)); +} + +TEST_P(SubsampleGeneratorTest, VP9SubsampleEncryptionWithLargeSuperFrame) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK( + generator.Initialize(protection_scheme_, GetVideoStreamInfo(kCodecVP9))); + + constexpr size_t kFrameSize = 0x23456; + constexpr uint8_t kFrame[kFrameSize] = {}; + // Super frame with two subframes. + constexpr size_t kSubFrameSizes[] = {0x10, 0x23000, 0x440}; + constexpr size_t kUncompressedHeaderSizes[] = {4, 0x21000, 2}; + // VP9 block align protected data for all protection schemes. + const SubsampleEntry kExpectedSubsamples[] = { + // {4,12},{1,0x23000-1} block aligned => {16,0},{0x21000,0x2000} + // Then split big clear_bytes, merge consecutive clear-only subsamples. + {0xffff, 0}, + {0xffff, 0}, + {0x1012, 0x2000}, + // {2,0x440-2} block aligned. + {0x10, 0x430}, + // Superframe index. + {6, 0}, + }; + + std::vector vpx_frame_info(3); + for (int i = 0; i < 3; i++) { + vpx_frame_info[i].frame_size = kSubFrameSizes[i]; + vpx_frame_info[i].uncompressed_header_size = kUncompressedHeaderSizes[i]; + } + + std::unique_ptr mock_vpx_parser(new MockVPxParser); + EXPECT_CALL(*mock_vpx_parser, Parse(kFrame, kFrameSize, _)) + .WillOnce(DoAll(SetArgPointee<2>(vpx_frame_info), Return(true))); + + generator.InjectVpxParserForTesting(std::move(mock_vpx_parser)); + + std::vector subsamples; + ASSERT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); + EXPECT_THAT(subsamples, ElementsAreArray(kExpectedSubsamples)); +} + +TEST_P(SubsampleGeneratorTest, H264ParseFailed) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK( + generator.Initialize(protection_scheme_, GetVideoStreamInfo(kCodecH264))); + + constexpr uint8_t kFrame[] = { + // First NALU (nalu_size = 9). + 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09}; + constexpr size_t kFrameSize = sizeof(kFrame); + + std::unique_ptr mock_video_slice_header_parser( + new MockVideoSliceHeaderParser); + EXPECT_CALL(*mock_video_slice_header_parser, GetHeaderSize(_)) + .WillOnce(Return(-1)); + + generator.InjectVideoSliceHeaderParserForTesting( + std::move(mock_video_slice_header_parser)); + + std::vector subsamples; + ASSERT_NOT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); +} + +TEST_P(SubsampleGeneratorTest, H264SubsampleEncryption) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK( + generator.Initialize(protection_scheme_, GetVideoStreamInfo(kCodecH264))); + + constexpr uint8_t kFrame[] = { + // First NALU (nalu_size = 9). + 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + // Second NALU (nalu_size = 0x25). + 0x27, 0x25, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, + // Third non-video-slice NALU (nalu_size = 0x32). + 0x32, 0x67, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32}; + constexpr size_t kFrameSize = sizeof(kFrame); + // There are two video slices. + const size_t kSliceHeaderSize[] = {4, 5}; + const SubsampleEntry kExpectedUnalignedSubsamples[] = { + // clear_bytes = nalu_length_size (1) + type_size (1) + header_size (4). + // encrypted_bytes = nalu_size (9) - type_size (1) - header_size (4). + {6, 4}, + // clear_bytes = nalu_length_size (1) + type_size (1) + header_size (5). + // encrypted_bytes = nalu_size (0x27) - type_size (1) - header_size (5). + {7, 0x21}, + // Non-video slice, clear_bytes = nalu_length_size (1) + nalu_size (0x32). + // encrypted_bytes = 0. + {0x33, 0}, + }; + const SubsampleEntry kExpectedAlignedSubsamples[] = { + // {6,4},{7,0x21} block aligned => {10,0},{8,0x20} + // Then merge consecutive clear-only subsamples. + {18, 0x20}, + {0x33, 0}, + }; + + std::unique_ptr mock_video_slice_header_parser( + new MockVideoSliceHeaderParser); + EXPECT_CALL(*mock_video_slice_header_parser, GetHeaderSize(_)) + .WillOnce(Return(kSliceHeaderSize[0])) + .WillOnce(Return(kSliceHeaderSize[1])); + + generator.InjectVideoSliceHeaderParserForTesting( + std::move(mock_video_slice_header_parser)); + + std::vector subsamples; + ASSERT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); + // Align subsamples for all CENC protection schemes except for cbcs. + if (protection_scheme_ == FOURCC_cbcs) + EXPECT_THAT(subsamples, ElementsAreArray(kExpectedUnalignedSubsamples)); + else + EXPECT_THAT(subsamples, ElementsAreArray(kExpectedAlignedSubsamples)); +} + +TEST_P(SubsampleGeneratorTest, AACIsFullSampleEncrypted) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK( + generator.Initialize(protection_scheme_, GetAudioStreamInfo(kCodecAAC))); + + constexpr size_t kFrameSize = 50; + constexpr uint8_t kFrame[kFrameSize] = {}; + + std::vector subsamples; + ASSERT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); + EXPECT_THAT(subsamples, ElementsAre()); +} + +INSTANTIATE_TEST_CASE_P( + CencProtectionSchemes, + SubsampleGeneratorTest, + Values(FOURCC_cenc, FOURCC_cens, FOURCC_cbc1, FOURCC_cbcs)); + +TEST(SampleAesSubsampleGeneratorTest, AAC) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK(generator.Initialize(kAppleSampleAesProtectionScheme, + GetAudioStreamInfo(kCodecAAC))); + + constexpr size_t kNumFrames = 4; + constexpr size_t kMaxFrameSize = 100; + constexpr size_t kFrameSizes[] = {6, 16, 17, 50}; + constexpr uint8_t kFrames[kNumFrames][kMaxFrameSize] = {}; + // 16 bytes clear lead. + const SubsampleEntry kExpectedSubsamples[] = { + {6, 0}, + {16, 0}, + {16, 1}, + {16, 34}, + }; + + for (int i = 0; i < 4; i++) { + std::vector subsamples; + ASSERT_OK( + generator.GenerateSubsamples(kFrames[i], kFrameSizes[i], &subsamples)); + EXPECT_THAT(subsamples, ElementsAre(kExpectedSubsamples[i])); + } +} + +TEST(SampleAesSubsampleGeneratorTest, H264) { + SubsampleGenerator generator(kVP9SubsampleEncryption); + ASSERT_OK(generator.Initialize(kAppleSampleAesProtectionScheme, + GetVideoStreamInfo(kCodecH264))); + + constexpr uint8_t kFrame[] = { + // First NALU (nalu_size = 9). + 0x09, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, + // Second NALU (nalu_size = 0x30). + 0x30, 0x25, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, + // Third NALU (nalu_size = 0x31). + 0x31, 0x25, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, + // Fourth non-video-slice NALU (nalu_size = 6). + 0x32, 0x67, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, + 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, + 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, + 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, + 0x30, 0x31, 0x32}; + constexpr size_t kFrameSize = sizeof(kFrame); + const SubsampleEntry kExpectedSubsamples[] = { + // NAL units with nalu_size <= 32+16 is not encrypted, so + // the first two NALUs are left in clear {1+9,0},{1+48,0}. + // The third NALUs has a fixed 32 bytes clear lead, +1 byte NALU length + // size, so it is {1+32, 17}. + // Then merge consecutive clear-only subsamples. + {1 + 9 + 1 + 48 + 1 + 32, 17}, + // Non video slice is not encrypted. + {0x33, 0}, + }; + + std::vector subsamples; + ASSERT_OK(generator.GenerateSubsamples(kFrame, kFrameSize, &subsamples)); + EXPECT_THAT(subsamples, ElementsAreArray(kExpectedSubsamples)); +} + +} // namespace media +} // namespace shaka