diff --git a/packager/app/test/packager_test.py b/packager/app/test/packager_test.py index cedf1944b7..dff004229e 100755 --- a/packager/app/test/packager_test.py +++ b/packager/app/test/packager_test.py @@ -23,7 +23,7 @@ class PackagerAppTest(unittest.TestCase): def setUp(self): self.packager = packager_app.PackagerApp() self.input = os.path.join( - test_env.SRC_DIR, 'media', 'test', 'data', 'bear-1280x720.mp4') + test_env.SRC_DIR, 'packager', 'media', 'test', 'data', 'bear-1280x720.mp4') self.tmpdir = tempfile.mkdtemp() fd, self.output = tempfile.mkstemp(dir=self.tmpdir) os.close(fd) diff --git a/packager/app/test/test_env.py b/packager/app/test/test_env.py index 84c537f7b4..21a1b58fd6 100644 --- a/packager/app/test/test_env.py +++ b/packager/app/test/test_env.py @@ -20,7 +20,7 @@ import sys # Define static global objects and attributes. -SRC_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../..') +SRC_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), '../../..') # Parse arguments and calculate dynamic global objects and attributes. diff --git a/packager/media/base/aes_encryptor.cc b/packager/media/base/aes_encryptor.cc index c7e096ef09..564ad3e36d 100644 --- a/packager/media/base/aes_encryptor.cc +++ b/packager/media/base/aes_encryptor.cc @@ -337,7 +337,7 @@ void AesCbcCtsEncryptor::Encrypt(const std::vector& plaintext, if (plaintext.empty()) return; - return Encrypt(plaintext.data(), plaintext.size(), &(*ciphertext)[0]); + Encrypt(plaintext.data(), plaintext.size(), &(*ciphertext)[0]); } bool AesCbcCtsEncryptor::SetIv(const std::vector& iv) { @@ -461,7 +461,7 @@ void AesCbcCtsDecryptor::Decrypt(const std::vector& ciphertext, if (ciphertext.empty()) return; - return Decrypt(ciphertext.data(), ciphertext.size(), &(*plaintext)[0]); + Decrypt(ciphertext.data(), ciphertext.size(), &(*plaintext)[0]); } bool AesCbcCtsDecryptor::SetIv(const std::vector& iv) { diff --git a/packager/media/base/demuxer.cc b/packager/media/base/demuxer.cc index 0d73dee2b5..85d60a4856 100644 --- a/packager/media/base/demuxer.cc +++ b/packager/media/base/demuxer.cc @@ -21,6 +21,8 @@ #include "packager/media/formats/wvm/wvm_media_parser.h" namespace { +const size_t kInitBufSize = 0x4000; // 16KB, sufficient to determine the + // container and likely all init data. const size_t kBufSize = 0x40000; // 256KB. } @@ -55,7 +57,7 @@ Status Demuxer::Initialize() { } // Determine media container. - int64_t bytes_read = media_file_->Read(buffer_.get(), kBufSize); + int64_t bytes_read = media_file_->Read(buffer_.get(), kInitBufSize); if (bytes_read <= 0) return Status(error::FILE_FAILURE, "Cannot read file " + file_name_); MediaContainerName container = DetermineContainer(buffer_.get(), bytes_read); @@ -80,16 +82,16 @@ Status Demuxer::Initialize() { base::Bind(&Demuxer::NewSampleEvent, base::Unretained(this)), key_source_.get()); - if (!parser_->Parse(buffer_.get(), bytes_read)) - return Status(error::PARSER_FAILURE, - "Cannot parse media file " + file_name_); - - Status status; - while (!init_event_received_) { - if (!(status = Parse()).ok()) - break; + if (!parser_->Parse(buffer_.get(), bytes_read)) { + init_parsing_status_ = + Status(error::PARSER_FAILURE, "Cannot parse media file " + file_name_); } - return status; + + // Parse until init event received or on error. + while (!init_event_received_ && init_parsing_status_.ok()) + init_parsing_status_ = Parse(); + // Defer error reporting if init completed successfully. + return init_event_received_ ? Status::OK : init_parsing_status_; } void Demuxer::ParserInitEvent( @@ -147,6 +149,11 @@ Status Demuxer::Parse() { DCHECK(parser_); DCHECK(buffer_); + // Return early and avoid call Parse(...) again if it has already failed at + // the initialization. + if (!init_parsing_status_.ok()) + return init_parsing_status_; + int64_t bytes_read = media_file_->Read(buffer_.get(), kBufSize); if (bytes_read <= 0) { if (media_file_->Eof()) { diff --git a/packager/media/base/demuxer.h b/packager/media/base/demuxer.h index 3e51cd992c..4315d5c26e 100644 --- a/packager/media/base/demuxer.h +++ b/packager/media/base/demuxer.h @@ -70,6 +70,7 @@ class Demuxer { std::string file_name_; File* media_file_; bool init_event_received_; + Status init_parsing_status_; scoped_ptr parser_; std::vector streams_; scoped_ptr buffer_; diff --git a/packager/media/formats/mp4/mp4_media_parser.cc b/packager/media/formats/mp4/mp4_media_parser.cc index 395a3eac44..f48990f38d 100644 --- a/packager/media/formats/mp4/mp4_media_parser.cc +++ b/packager/media/formats/mp4/mp4_media_parser.cc @@ -332,10 +332,9 @@ bool MP4MediaParser::FetchKeysIfNecessary( if (headers.empty()) return true; - if (!decryption_key_source_) { - LOG(ERROR) << "Content is encrypted, but content decryption not enabled."; - return false; - } + // An error will be returned later if the samples need to be decrypted. + if (!decryption_key_source_) + return true; // TODO(tinskip): Pass in raw 'pssh' boxes to FetchKeys. This will allow // supporting multiple keysystems. Move this to KeySource. @@ -408,14 +407,14 @@ bool MP4MediaParser::EnqueueSample(bool* err) { buf, runs_->sample_size(), runs_->is_keyframe())); if (runs_->is_encrypted()) { scoped_ptr decrypt_config = runs_->GetDecryptConfig(); - if (!decrypt_config) { + if (!decrypt_config || + !DecryptSampleBuffer(decrypt_config.get(), + stream_sample->writable_data(), + stream_sample->data_size())) { *err = true; + LOG(ERROR) << "Cannot decrypt samples."; return false; } - if (!DecryptSampleBuffer(decrypt_config.get(), - stream_sample->writable_data(), - stream_sample->data_size())) - return false; } stream_sample->set_dts(runs_->dts()); diff --git a/packager/media/formats/mp4/mp4_media_parser_unittest.cc b/packager/media/formats/mp4/mp4_media_parser_unittest.cc index c2b8954135..2859f4a7a7 100644 --- a/packager/media/formats/mp4/mp4_media_parser_unittest.cc +++ b/packager/media/formats/mp4/mp4_media_parser_unittest.cc @@ -4,27 +4,46 @@ // license that can be found in the LICENSE file or at // https://developers.google.com/open-source/licenses/bsd +#include #include #include "packager/base/bind.h" #include "packager/base/logging.h" +#include "packager/media/base/key_source.h" #include "packager/media/base/media_sample.h" +#include "packager/media/base/stream_info.h" #include "packager/media/formats/mp4/mp4_media_parser.h" #include "packager/media/test/test_data_util.h" +using ::testing::_; +using ::testing::DoAll; +using ::testing::Return; +using ::testing::SetArgPointee; + namespace edash_packager { namespace media { + +namespace { +class MockKeySource : public KeySource { + public: + MOCK_METHOD1(FetchKeys, Status(const std::vector& pssh_data)); + MOCK_METHOD2(GetKey, + Status(const std::vector& key_id, EncryptionKey* key)); +}; +} // namespace + namespace mp4 { class MP4MediaParserTest : public testing::Test { public: - MP4MediaParserTest() : configs_received_(false) { + MP4MediaParserTest() : num_streams_(0), num_samples_(0) { parser_.reset(new MP4MediaParser()); } protected: scoped_ptr parser_; - bool configs_received_; + size_t num_streams_; + size_t num_samples_; bool AppendData(const uint8_t* data, size_t length) { return parser_->Parse(data, length); @@ -46,76 +65,92 @@ class MP4MediaParserTest : public testing::Test { } void InitF(const std::vector >& streams) { - if (streams.size() > 0) - configs_received_ = true; + for (std::vector >::const_iterator iter = + streams.begin(); + iter != streams.end(); + ++iter) { + DVLOG(2) << (*iter)->ToString(); + } + num_streams_ = streams.size(); + num_samples_ = 0; } bool NewSampleF(uint32_t track_id, const scoped_refptr& sample) { DVLOG(2) << "Track Id: " << track_id << " " << sample->ToString(); + ++num_samples_; return true; } - void InitializeParser() { + void InitializeParser(KeySource* decryption_key_source) { parser_->Init( base::Bind(&MP4MediaParserTest::InitF, base::Unretained(this)), base::Bind(&MP4MediaParserTest::NewSampleF, base::Unretained(this)), - NULL); + decryption_key_source); } bool ParseMP4File(const std::string& filename, int append_bytes) { - InitializeParser(); + InitializeParser(NULL); std::vector buffer = ReadTestDataFile(filename); - EXPECT_TRUE(AppendDataInPieces(buffer.data(), - buffer.size(), - append_bytes)); - return true; + return AppendDataInPieces(buffer.data(), buffer.size(), append_bytes); } }; TEST_F(MP4MediaParserTest, UnalignedAppend) { // Test small, non-segment-aligned appends (small enough to exercise // incremental append system) - ParseMP4File("bear-1280x720-av_frag.mp4", 512); + EXPECT_TRUE(ParseMP4File("bear-1280x720-av_frag.mp4", 512)); + EXPECT_EQ(2u, num_streams_); + EXPECT_EQ(201u, num_samples_); } TEST_F(MP4MediaParserTest, BytewiseAppend) { // Ensure no incremental errors occur when parsing - ParseMP4File("bear-1280x720-av_frag.mp4", 1); + EXPECT_TRUE(ParseMP4File("bear-1280x720-av_frag.mp4", 1)); + EXPECT_EQ(2u, num_streams_); + EXPECT_EQ(201u, num_samples_); } TEST_F(MP4MediaParserTest, MultiFragmentAppend) { // Large size ensures multiple fragments are appended in one call (size is // larger than this particular test file) - ParseMP4File("bear-1280x720-av_frag.mp4", 768432); + EXPECT_TRUE(ParseMP4File("bear-1280x720-av_frag.mp4", 768432)); + EXPECT_EQ(2u, num_streams_); + EXPECT_EQ(201u, num_samples_); } TEST_F(MP4MediaParserTest, Flush) { // Flush while reading sample data, then start a new stream. - InitializeParser(); + InitializeParser(NULL); std::vector buffer = ReadTestDataFile("bear-1280x720-av_frag.mp4"); EXPECT_TRUE(AppendDataInPieces(buffer.data(), 65536, 512)); parser_->Flush(); EXPECT_TRUE(AppendDataInPieces(buffer.data(), buffer.size(), 512)); + EXPECT_EQ(2u, num_streams_); + EXPECT_EQ(201u, num_samples_); } TEST_F(MP4MediaParserTest, Reinitialization) { - InitializeParser(); + InitializeParser(NULL); std::vector buffer = ReadTestDataFile("bear-1280x720-av_frag.mp4"); EXPECT_TRUE(AppendDataInPieces(buffer.data(), buffer.size(), 512)); EXPECT_TRUE(AppendDataInPieces(buffer.data(), buffer.size(), 512)); + EXPECT_EQ(2u, num_streams_); + EXPECT_EQ(201u, num_samples_); } TEST_F(MP4MediaParserTest, MPEG2_AAC_LC) { - ParseMP4File("bear-mpeg2-aac-only_frag.mp4", 512); + EXPECT_TRUE(ParseMP4File("bear-mpeg2-aac-only_frag.mp4", 512)); + EXPECT_EQ(1u, num_streams_); + EXPECT_EQ(119u, num_samples_); } // Test that a moov box is not always required after Flush() is called. TEST_F(MP4MediaParserTest, NoMoovAfterFlush) { - InitializeParser(); + InitializeParser(NULL); std::vector buffer = ReadTestDataFile("bear-1280x720-av_frag.mp4"); EXPECT_TRUE(AppendDataInPieces(buffer.data(), buffer.size(), 512)); @@ -127,7 +162,48 @@ TEST_F(MP4MediaParserTest, NoMoovAfterFlush) { } TEST_F(MP4MediaParserTest, NON_FRAGMENTED_MP4) { - ParseMP4File("bear-1280x720.mp4", 512); + EXPECT_TRUE(ParseMP4File("bear-1280x720.mp4", 512)); + EXPECT_EQ(2u, num_streams_); + EXPECT_EQ(201u, num_samples_); +} + +TEST_F(MP4MediaParserTest, CencWithoutDecryptionSource) { + // Parsing should fail but it will get the streams successfully. + EXPECT_FALSE(ParseMP4File("bear-1280x720-v_frag-cenc.mp4", 512)); + EXPECT_EQ(1u, num_streams_); +} + +TEST_F(MP4MediaParserTest, CencInitWithoutDecryptionSource) { + InitializeParser(NULL); + + std::vector buffer = + ReadTestDataFile("bear-1280x720-v_frag-cenc.mp4"); + const int kFirstMoofOffset = 1646; + EXPECT_TRUE(AppendDataInPieces(buffer.data(), kFirstMoofOffset, 512)); + EXPECT_EQ(1u, num_streams_); +} + +TEST_F(MP4MediaParserTest, CencWithDecryptionSource) { + MockKeySource mock_key_source; + EXPECT_CALL(mock_key_source, FetchKeys(_)).WillOnce(Return(Status::OK)); + + const char kKey[] = + "\xeb\xdd\x62\xf1\x68\x14\xd2\x7b\x68\xef\x12\x2a\xfc\xe4\xae\x3c"; + const char kKeyId[] = "0123456789012345"; + + EncryptionKey encryption_key; + encryption_key.key.assign(kKey, kKey + strlen(kKey)); + EXPECT_CALL(mock_key_source, + GetKey(std::vector(kKeyId, kKeyId + strlen(kKeyId)), _)) + .WillOnce(DoAll(SetArgPointee<1>(encryption_key), Return(Status::OK))); + + InitializeParser(&mock_key_source); + + std::vector buffer = + ReadTestDataFile("bear-1280x720-v_frag-cenc.mp4"); + EXPECT_TRUE(AppendDataInPieces(buffer.data(), buffer.size(), 512)); + EXPECT_EQ(1u, num_streams_); + EXPECT_EQ(82u, num_samples_); } } // namespace mp4 diff --git a/packager/media/formats/wvm/wvm_media_parser.cc b/packager/media/formats/wvm/wvm_media_parser.cc index 6e37dc1cdd..9f30e66106 100644 --- a/packager/media/formats/wvm/wvm_media_parser.cc +++ b/packager/media/formats/wvm/wvm_media_parser.cc @@ -9,6 +9,7 @@ #include #include "packager/base/strings/string_number_conversions.h" +#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/media_sample.h" @@ -749,7 +750,11 @@ void WvmMediaParser::StartMediaSampleDemux(uint8_t* read_ptr) { bool WvmMediaParser::Output() { // Check decrypted sample data. if (prev_pes_flags_1_ & kScramblingBitsMask) { - content_decryptor_.Decrypt(sample_data_, &sample_data_); + if (!content_decryptor_) { + LOG(ERROR) << "Source content is encrypted, but decryption not enabled"; + return false; + } + content_decryptor_->Decrypt(sample_data_, &sample_data_); } if ((prev_pes_stream_id_ & kPesStreamIdVideoMask) == kPesStreamIdVideo) { // Set data on the video stream from the NalUnitStream. @@ -911,10 +916,7 @@ void WvmMediaParser::EmitSample(uint32_t parsed_audio_or_video_stream_id, bool WvmMediaParser::GetAssetKey(const uint32_t asset_id, EncryptionKey* encryption_key) { - if (decryption_key_source_ == NULL) { - LOG(ERROR) << "Source content is encrypted, but decryption not enabled"; - return false; - } + DCHECK(decryption_key_source_); Status status = decryption_key_source_->FetchKeys(asset_id); if (!status.ok()) { LOG(ERROR) << "Fetch Key(s) failed for AssetID = " << asset_id @@ -934,6 +936,10 @@ bool WvmMediaParser::GetAssetKey(const uint32_t asset_id, } bool WvmMediaParser::ProcessEcm() { + // An error will be returned later if the samples need to be decrypted. + if (!decryption_key_source_) + return true; + if (current_program_id_ > 0) { return true; } @@ -959,26 +965,29 @@ bool WvmMediaParser::ProcessEcm() { } std::vector iv(kInitializationVectorSizeBytes); AesCbcCtsDecryptor asset_decryptor; - asset_decryptor.InitializeWithIv(encryption_key.key, iv); - - std::vector content_key_buffer; // flags + contentKey + padding. - content_key_buffer.resize( - kEcmFlagsSizeBytes + kEcmContentKeySizeBytes + kEcmPaddingSizeBytes); - // Get content key + padding from ECM. - memcpy(&content_key_buffer[0], ecm_data, - kEcmFlagsSizeBytes + kEcmContentKeySizeBytes + kEcmPaddingSizeBytes); - asset_decryptor.Decrypt(content_key_buffer, &content_key_buffer); - if (content_key_buffer.empty()) { - LOG(ERROR) << "Decryption of content key failed for asset id = " - << asset_id; + if (!asset_decryptor.InitializeWithIv(encryption_key.key, iv)) { + LOG(ERROR) << "Failed to initialize asset_decryptor."; return false; } + const size_t content_key_buffer_size = + kEcmFlagsSizeBytes + kEcmContentKeySizeBytes + + kEcmPaddingSizeBytes; // flags + contentKey + padding. + std::vector content_key_buffer(content_key_buffer_size); + asset_decryptor.Decrypt( + ecm_data, content_key_buffer_size, &content_key_buffer[0]); + std::vector decrypted_content_key_vec( content_key_buffer.begin() + 4, content_key_buffer.begin() + 20); - content_decryptor_.InitializeWithIv(decrypted_content_key_vec, iv); - return(true); + scoped_ptr content_decryptor(new AesCbcCtsDecryptor); + if (!content_decryptor->InitializeWithIv(decrypted_content_key_vec, iv)) { + LOG(ERROR) << "Failed to initialize content decryptor."; + return false; + } + + content_decryptor_ = content_decryptor.Pass(); + return true; } DemuxStreamIdMediaSample::DemuxStreamIdMediaSample() : diff --git a/packager/media/formats/wvm/wvm_media_parser.h b/packager/media/formats/wvm/wvm_media_parser.h index 82f9214845..3ddb931f53 100644 --- a/packager/media/formats/wvm/wvm_media_parser.h +++ b/packager/media/formats/wvm/wvm_media_parser.h @@ -12,7 +12,6 @@ #include #include "packager/base/memory/scoped_ptr.h" -#include "packager/media/base/aes_encryptor.h" #include "packager/media/base/media_parser.h" #include "packager/media/base/network_util.h" #include "packager/media/filters/h264_byte_to_unit_stream_converter.h" @@ -20,6 +19,7 @@ namespace edash_packager { namespace media { +class AesCbcCtsDecryptor; class KeySource; struct EncryptionKey; @@ -248,7 +248,7 @@ class WvmMediaParser : public MediaParser { std::deque media_sample_queue_; std::vector sample_data_; KeySource* decryption_key_source_; - AesCbcCtsDecryptor content_decryptor_; + scoped_ptr content_decryptor_; DISALLOW_COPY_AND_ASSIGN(WvmMediaParser); }; diff --git a/packager/media/formats/wvm/wvm_media_parser_unittest.cc b/packager/media/formats/wvm/wvm_media_parser_unittest.cc index 7482f43c65..c97ace7ff2 100644 --- a/packager/media/formats/wvm/wvm_media_parser_unittest.cc +++ b/packager/media/formats/wvm/wvm_media_parser_unittest.cc @@ -30,6 +30,7 @@ const int kExpectedVideoFrameCount = 6665; const int kExpectedAudioFrameCount = 11964; const uint8_t kExpectedAssetKey[] = "\x06\x81\x7f\x48\x6b\xf2\x7f\x3e\xc7\x39\xa8\x3f\x12\x0a\xd2\xfc"; +const size_t kInitDataSize = 0x4000; } // namespace using ::testing::_; @@ -141,6 +142,23 @@ class WvmMediaParserTest : public testing::Test { } }; +TEST_F(WvmMediaParserTest, ParseWvmWithoutKeySource) { + // Parsing should fail but it will get the streams successfully. + key_source_.reset(); + InitializeParser(); + std::vector buffer = ReadTestDataFile(kWvmFile); + EXPECT_FALSE(parser_->Parse(buffer.data(), buffer.size())); + EXPECT_EQ(kExpectedStreams, stream_map_.size()); +} + +TEST_F(WvmMediaParserTest, ParseWvmInitWithoutKeySource) { + key_source_.reset(); + InitializeParser(); + std::vector buffer = ReadTestDataFile(kWvmFile); + EXPECT_TRUE(parser_->Parse(buffer.data(), kInitDataSize)); + EXPECT_EQ(kExpectedStreams, stream_map_.size()); +} + TEST_F(WvmMediaParserTest, ParseWvm) { EXPECT_CALL(*key_source_, FetchKeys(_)).WillOnce(Return(Status::OK)); EXPECT_CALL(*key_source_, GetKey(_, _))