diff options
author | fgalligan@chromium.org <fgalligan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-14 23:52:41 +0000 |
---|---|---|
committer | fgalligan@chromium.org <fgalligan@chromium.org@0039d316-1c4b-4281-b951-d872f2087c98> | 2013-03-14 23:52:41 +0000 |
commit | 4d98e445c98768bb5eb44f8947bea55c1da8334b (patch) | |
tree | b6cf720b8d7ac51b47ab9196bbcaa7c5349977f9 | |
parent | 76e5e6d404fa7d608a9da91e3903a9a2c9165a77 (diff) | |
download | chromium_src-4d98e445c98768bb5eb44f8947bea55c1da8334b.zip chromium_src-4d98e445c98768bb5eb44f8947bea55c1da8334b.tar.gz chromium_src-4d98e445c98768bb5eb44f8947bea55c1da8334b.tar.bz2 |
Support for parsing encrypted WebM streams by src.
- Note: Only looking for comments on direction. A lot of work
still needs to be done before committing.
- Added support to FFmpegDemuxer to decrypt encrypted WebM streams.
- Added support to FFmpegDemuxer to handle the needKey and keyAdded
messages.
- Added support to WebMediaPlayerImpl to handle the needKey and
keyAdded messages.
BUG=123426
TEST=All media_unittests pass
Review URL: https://chromiumcodereview.appspot.com/10829470
git-svn-id: svn://svn.chromium.org/chrome/trunk/src@188228 0039d316-1c4b-4281-b951-d872f2087c98
-rw-r--r-- | media/ffmpeg/ffmpeg_common.cc | 23 | ||||
-rw-r--r-- | media/ffmpeg/ffmpeg_common.h | 4 | ||||
-rw-r--r-- | media/filters/chunk_demuxer_unittest.cc | 7 | ||||
-rw-r--r-- | media/filters/ffmpeg_demuxer.cc | 53 | ||||
-rw-r--r-- | media/filters/ffmpeg_demuxer.h | 20 | ||||
-rw-r--r-- | media/filters/ffmpeg_demuxer_unittest.cc | 32 | ||||
-rw-r--r-- | media/filters/pipeline_integration_test.cc | 17 | ||||
-rw-r--r-- | media/filters/pipeline_integration_test_base.cc | 31 | ||||
-rw-r--r-- | media/filters/pipeline_integration_test_base.h | 10 | ||||
-rw-r--r-- | media/media.gyp | 2 | ||||
-rw-r--r-- | media/tools/demuxer_bench/demuxer_bench.cc | 9 | ||||
-rw-r--r-- | media/tools/player_x11/player_x11.cc | 9 | ||||
-rw-r--r-- | media/tools/seek_tester/seek_tester.cc | 9 | ||||
-rw-r--r-- | media/webm/webm_cluster_parser.cc | 49 | ||||
-rw-r--r-- | media/webm/webm_cluster_parser_unittest.cc | 57 | ||||
-rw-r--r-- | media/webm/webm_crypto_helpers.cc | 60 | ||||
-rw-r--r-- | media/webm/webm_crypto_helpers.h | 32 | ||||
-rw-r--r-- | media/webm/webm_stream_parser.cc | 11 | ||||
-rw-r--r-- | webkit/media/filter_helpers.cc | 5 | ||||
-rw-r--r-- | webkit/media/filter_helpers.h | 5 | ||||
-rw-r--r-- | webkit/media/webmediaplayer_impl.cc | 8 |
21 files changed, 375 insertions, 78 deletions
diff --git a/media/ffmpeg/ffmpeg_common.cc b/media/ffmpeg/ffmpeg_common.cc index a2c4efd..8a69a86 100644 --- a/media/ffmpeg/ffmpeg_common.cc +++ b/media/ffmpeg/ffmpeg_common.cc @@ -269,8 +269,9 @@ static AVSampleFormat SampleFormatToAVSampleFormat(SampleFormat sample_format) { return AV_SAMPLE_FMT_NONE; } -void AVCodecContextToAudioDecoderConfig( +static void AVCodecContextToAudioDecoderConfig( const AVCodecContext* codec_context, + bool is_encrypted, AudioDecoderConfig* config) { DCHECK_EQ(codec_context->codec_type, AVMEDIA_TYPE_AUDIO); @@ -294,7 +295,7 @@ void AVCodecContextToAudioDecoderConfig( codec_context->sample_rate, codec_context->extradata, codec_context->extradata_size, - false, // Not encrypted. + is_encrypted, true); if (codec != kCodecOpus) { DCHECK_EQ(av_get_bytes_per_sample(codec_context->sample_fmt) * 8, @@ -302,6 +303,17 @@ void AVCodecContextToAudioDecoderConfig( } } +void AVStreamToAudioDecoderConfig( + const AVStream* stream, + AudioDecoderConfig* config) { + bool is_encrypted = false; + AVDictionaryEntry* key = av_dict_get(stream->metadata, "enc_key_id", NULL, 0); + if (key) + is_encrypted = true; + return AVCodecContextToAudioDecoderConfig(stream->codec, + is_encrypted, config); +} + void AudioDecoderConfigToAVCodecContext(const AudioDecoderConfig& config, AVCodecContext* codec_context) { codec_context->codec_type = AVMEDIA_TYPE_AUDIO; @@ -365,12 +377,17 @@ void AVStreamToVideoDecoderConfig( coded_size = natural_size; } + bool is_encrypted = false; + AVDictionaryEntry* key = av_dict_get(stream->metadata, "enc_key_id", NULL, 0); + if (key) + is_encrypted = true; + config->Initialize(codec, profile, format, coded_size, visible_rect, natural_size, stream->codec->extradata, stream->codec->extradata_size, - false, // Not encrypted. + is_encrypted, true); } diff --git a/media/ffmpeg/ffmpeg_common.h b/media/ffmpeg/ffmpeg_common.h index 38f70d0..62c6385 100644 --- a/media/ffmpeg/ffmpeg_common.h +++ b/media/ffmpeg/ffmpeg_common.h @@ -72,8 +72,8 @@ MEDIA_EXPORT base::TimeDelta ConvertFromTimeBase(const AVRational& time_base, MEDIA_EXPORT int64 ConvertToTimeBase(const AVRational& time_base, const base::TimeDelta& timestamp); -void AVCodecContextToAudioDecoderConfig( - const AVCodecContext* codec_context, +void AVStreamToAudioDecoderConfig( + const AVStream* stream, AudioDecoderConfig* config); void AudioDecoderConfigToAVCodecContext( const AudioDecoderConfig& config, diff --git a/media/filters/chunk_demuxer_unittest.cc b/media/filters/chunk_demuxer_unittest.cc index 2200e4b..2a32ca6 100644 --- a/media/filters/chunk_demuxer_unittest.cc +++ b/media/filters/chunk_demuxer_unittest.cc @@ -6,12 +6,14 @@ #include "base/message_loop.h" #include "media/base/audio_decoder_config.h" #include "media/base/decoder_buffer.h" +#include "media/base/decrypt_config.h" #include "media/base/mock_demuxer_host.h" #include "media/base/test_data_util.h" #include "media/base/test_helpers.h" #include "media/filters/chunk_demuxer.h" #include "media/webm/cluster_builder.h" #include "media/webm/webm_constants.h" +#include "media/webm/webm_crypto_helpers.h" #include "testing/gtest/include/gtest/gtest.h" using ::testing::AnyNumber; @@ -66,8 +68,6 @@ static const char kDefaultFirstClusterRange[] = "{ [0,46) }"; static const int kDefaultFirstClusterEndTimestamp = 66; static const int kDefaultSecondClusterEndTimestamp = 132; -static const char kWebMInitDataType[] = "video/webm"; - base::TimeDelta kDefaultDuration() { return base::TimeDelta::FromMilliseconds(201224); } @@ -784,7 +784,8 @@ TEST_F(ChunkDemuxerTest, TestInit) { if (is_audio_encrypted || is_video_encrypted) { int need_key_count = (is_audio_encrypted ? 1 : 0) + (is_video_encrypted ? 1 : 0); - EXPECT_CALL(*this, NeedKeyMock(kWebMInitDataType, NotNull(), 16)) + EXPECT_CALL(*this, NeedKeyMock(kWebMEncryptInitDataType, NotNull(), + DecryptConfig::kDecryptionKeySize)) .Times(Exactly(need_key_count)); } diff --git a/media/filters/ffmpeg_demuxer.cc b/media/filters/ffmpeg_demuxer.cc index f1b0385..ee899f0 100644 --- a/media/filters/ffmpeg_demuxer.cc +++ b/media/filters/ffmpeg_demuxer.cc @@ -7,6 +7,7 @@ #include <algorithm> #include <string> +#include "base/base64.h" #include "base/bind.h" #include "base/callback.h" #include "base/callback_helpers.h" @@ -20,12 +21,14 @@ #include "media/base/audio_decoder_config.h" #include "media/base/bind_to_loop.h" #include "media/base/decoder_buffer.h" +#include "media/base/decrypt_config.h" #include "media/base/limits.h" #include "media/base/media_switches.h" #include "media/base/video_decoder_config.h" #include "media/ffmpeg/ffmpeg_common.h" #include "media/filters/ffmpeg_glue.h" #include "media/filters/ffmpeg_h264_to_annex_b_bitstream_converter.h" +#include "media/webm/webm_crypto_helpers.h" namespace media { @@ -45,15 +48,19 @@ FFmpegDemuxerStream::FFmpegDemuxerStream( bitstream_converter_enabled_(false) { DCHECK(demuxer_); + bool is_encrypted = false; + // Determine our media format. switch (stream->codec->codec_type) { case AVMEDIA_TYPE_AUDIO: type_ = AUDIO; - AVCodecContextToAudioDecoderConfig(stream->codec, &audio_config_); + AVStreamToAudioDecoderConfig(stream, &audio_config_); + is_encrypted = audio_config_.is_encrypted(); break; case AVMEDIA_TYPE_VIDEO: type_ = VIDEO; AVStreamToVideoDecoderConfig(stream, &video_config_); + is_encrypted = video_config_.is_encrypted(); break; default: NOTREACHED(); @@ -67,6 +74,24 @@ FFmpegDemuxerStream::FFmpegDemuxerStream( bitstream_converter_.reset( new FFmpegH264ToAnnexBBitstreamConverter(stream_->codec)); } + + if (is_encrypted) { + AVDictionaryEntry* key = av_dict_get(stream->metadata, "enc_key_id", NULL, + 0); + DCHECK(key); + DCHECK(key->value); + if (!key || !key->value) + return; + base::StringPiece base64_key_id(key->value); + std::string enc_key_id; + base::Base64Decode(base64_key_id, &enc_key_id); + DCHECK(!enc_key_id.empty()); + if (enc_key_id.empty()) + return; + + encryption_key_id_.assign(enc_key_id); + demuxer_->FireNeedKey(kWebMEncryptInitDataType, enc_key_id); + } } void FFmpegDemuxerStream::EnqueuePacket(ScopedAVPacket packet) { @@ -88,6 +113,18 @@ void FFmpegDemuxerStream::EnqueuePacket(ScopedAVPacket packet) { // into memory we control. scoped_refptr<DecoderBuffer> buffer; buffer = DecoderBuffer::CopyFrom(packet->data, packet->size); + + if ((type() == DemuxerStream::AUDIO && audio_config_.is_encrypted()) || + (type() == DemuxerStream::VIDEO && video_config_.is_encrypted())) { + scoped_ptr<DecryptConfig> config(WebMCreateDecryptConfig( + packet->data, packet->size, + reinterpret_cast<const uint8*>(encryption_key_id_.data()), + encryption_key_id_.size())); + if (!config) + LOG(ERROR) << "Creation of DecryptConfig failed."; + buffer->SetDecryptConfig(config.Pass()); + } + buffer->SetTimestamp(ConvertStreamTimestamp( stream_->time_base, packet->pts)); buffer->SetDuration(ConvertStreamTimestamp( @@ -231,7 +268,8 @@ base::TimeDelta FFmpegDemuxerStream::ConvertStreamTimestamp( // FFmpegDemuxer::FFmpegDemuxer( const scoped_refptr<base::MessageLoopProxy>& message_loop, - const scoped_refptr<DataSource>& data_source) + const scoped_refptr<DataSource>& data_source, + const FFmpegNeedKeyCB& need_key_cb) : host_(NULL), message_loop_(message_loop), blocking_thread_("FFmpegDemuxer"), @@ -243,7 +281,8 @@ FFmpegDemuxer::FFmpegDemuxer( audio_disabled_(false), duration_known_(false), url_protocol_(data_source, BindToLoop(message_loop_, base::Bind( - &FFmpegDemuxer::OnDataSourceError, base::Unretained(this)))) { + &FFmpegDemuxer::OnDataSourceError, base::Unretained(this)))), + need_key_cb_(need_key_cb) { DCHECK(message_loop_); DCHECK(data_source_); } @@ -661,6 +700,14 @@ void FFmpegDemuxer::StreamHasEnded() { } } +void FFmpegDemuxer::FireNeedKey(const std::string& init_data_type, + const std::string& encryption_key_id) { + int key_id_size = encryption_key_id.size(); + scoped_array<uint8> key_id_local(new uint8[key_id_size]); + memcpy(key_id_local.get(), encryption_key_id.data(), key_id_size); + need_key_cb_.Run(init_data_type, key_id_local.Pass(), key_id_size); +} + void FFmpegDemuxer::NotifyCapacityAvailable() { DCHECK(message_loop_->BelongsToCurrentThread()); ReadFrameIfNeeded(); diff --git a/media/filters/ffmpeg_demuxer.h b/media/filters/ffmpeg_demuxer.h index 1056d07..8a340ef 100644 --- a/media/filters/ffmpeg_demuxer.h +++ b/media/filters/ffmpeg_demuxer.h @@ -22,6 +22,7 @@ #ifndef MEDIA_FILTERS_FFMPEG_DEMUXER_H_ #define MEDIA_FILTERS_FFMPEG_DEMUXER_H_ +#include <string> #include <vector> #include "base/callback.h" @@ -42,6 +43,14 @@ struct AVStream; namespace media { +// A new potentially encrypted stream has been parsed. +// First parameter - The type of initialization data. +// Second parameter - The initialization data associated with the stream. +// Third parameter - Number of bytes of the initialization data. +typedef base::Callback<void(const std::string& type, + scoped_array<uint8> init_data, + int init_data_size)> FFmpegNeedKeyCB; + class FFmpegDemuxer; class FFmpegGlue; class FFmpegH264ToAnnexBBitstreamConverter; @@ -121,13 +130,16 @@ class FFmpegDemuxerStream : public DemuxerStream { scoped_ptr<FFmpegH264ToAnnexBBitstreamConverter> bitstream_converter_; bool bitstream_converter_enabled_; + std::string encryption_key_id_; + DISALLOW_COPY_AND_ASSIGN(FFmpegDemuxerStream); }; class MEDIA_EXPORT FFmpegDemuxer : public Demuxer { public: FFmpegDemuxer(const scoped_refptr<base::MessageLoopProxy>& message_loop, - const scoped_refptr<DataSource>& data_source); + const scoped_refptr<DataSource>& data_source, + const FFmpegNeedKeyCB& need_key_cb); // Demuxer implementation. virtual void Initialize(DemuxerHost* host, @@ -140,6 +152,10 @@ class MEDIA_EXPORT FFmpegDemuxer : public Demuxer { DemuxerStream::Type type) OVERRIDE; virtual base::TimeDelta GetStartTime() const OVERRIDE; + // Calls |need_key_cb_| with the initialization data encountered in the file. + void FireNeedKey(const std::string& init_data_type, + const std::string& encryption_key_id); + // Allow FFmpegDemuxerStream to notify us when there is updated information // about capacity and what buffered data is available. void NotifyCapacityAvailable(); @@ -233,6 +249,8 @@ class MEDIA_EXPORT FFmpegDemuxer : public Demuxer { BlockingUrlProtocol url_protocol_; scoped_ptr<FFmpegGlue> glue_; + const FFmpegNeedKeyCB need_key_cb_; + DISALLOW_COPY_AND_ASSIGN(FFmpegDemuxer); }; diff --git a/media/filters/ffmpeg_demuxer_unittest.cc b/media/filters/ffmpeg_demuxer_unittest.cc index af2cd92..d2f02ae 100644 --- a/media/filters/ffmpeg_demuxer_unittest.cc +++ b/media/filters/ffmpeg_demuxer_unittest.cc @@ -11,17 +11,21 @@ #include "base/files/file_path.h" #include "base/path_service.h" #include "base/threading/thread.h" +#include "media/base/decrypt_config.h" #include "media/base/mock_demuxer_host.h" #include "media/base/test_helpers.h" #include "media/ffmpeg/ffmpeg_common.h" #include "media/filters/ffmpeg_demuxer.h" #include "media/filters/file_data_source.h" +#include "media/webm/webm_crypto_helpers.h" #include "testing/gtest/include/gtest/gtest.h" using ::testing::AnyNumber; using ::testing::DoAll; +using ::testing::Exactly; using ::testing::InSequence; using ::testing::Invoke; +using ::testing::NotNull; using ::testing::Return; using ::testing::SaveArg; using ::testing::SetArgPointee; @@ -77,8 +81,12 @@ class FFmpegDemuxerTest : public testing::Test { EXPECT_CALL(host_, AddBufferedTimeRange(_, _)).Times(AnyNumber()); CreateDataSource(name); + + media::FFmpegNeedKeyCB need_key_cb = + base::Bind(&FFmpegDemuxerTest::NeedKeyCB, base::Unretained(this)); demuxer_ = new FFmpegDemuxer(message_loop_.message_loop_proxy(), - data_source_); + data_source_, + need_key_cb); } MOCK_METHOD1(CheckPoint, void(int v)); @@ -121,6 +129,17 @@ class FFmpegDemuxerTest : public testing::Test { location, size, timestampInMicroseconds); } + // TODO(xhwang): This is a workaround of the issue that move-only parameters + // are not supported in mocked methods. Remove this when the issue is fixed + // (http://code.google.com/p/googletest/issues/detail?id=395) or when we use + // std::string instead of scoped_array<uint8> (http://crbug.com/130689). + MOCK_METHOD3(NeedKeyCBMock, void(const std::string& type, + const uint8* init_data, int init_data_size)); + void NeedKeyCB(const std::string& type, + scoped_array<uint8> init_data, int init_data_size) { + NeedKeyCBMock(type, init_data.get(), init_data_size); + } + // Accessor to demuxer internals. void set_duration_known(bool duration_known) { demuxer_->duration_known_ = duration_known; @@ -281,6 +300,17 @@ TEST_F(FFmpegDemuxerTest, Initialize_Multitrack) { EXPECT_FALSE(demuxer_->GetStream(DemuxerStream::UNKNOWN)); } +// TODO(fgalligan): Enable test when code to parse encrypted WebM files lands +// in Chromium's FFmpeg. crbug.com/189221 +TEST_F(FFmpegDemuxerTest, DISABLED_Initialize_Encrypted) { + EXPECT_CALL(*this, NeedKeyCBMock(kWebMEncryptInitDataType, NotNull(), + DecryptConfig::kDecryptionKeySize)) + .Times(Exactly(2)); + + CreateDemuxer("bear-320x240-av_enc-av.webm"); + InitializeDemuxer(); +} + TEST_F(FFmpegDemuxerTest, Read_Audio) { // We test that on a successful audio packet read. CreateDemuxer("bear-320x240.webm"); diff --git a/media/filters/pipeline_integration_test.cc b/media/filters/pipeline_integration_test.cc index 3d6c16b..d1720c1 100644 --- a/media/filters/pipeline_integration_test.cc +++ b/media/filters/pipeline_integration_test.cc @@ -313,6 +313,7 @@ class MockMediaSource { scoped_array<uint8> init_data, int init_data_size) { DCHECK(init_data.get()); DCHECK_GT(init_data_size, 0); + CHECK(!need_key_cb_.is_null()); need_key_cb_.Run("", "", type, init_data.Pass(), init_data_size); } @@ -424,6 +425,22 @@ TEST_F(PipelineIntegrationTest, BasicPlaybackHashed) { // EXPECT_EQ(GetAudioHash(), ""); } +// TODO(fgalligan): Enable test when code to parse encrypted WebM files lands +// in Chromium's FFmpeg. crbug.com/189221 +TEST_F(PipelineIntegrationTest, DISABLED_BasicPlaybackEncrypted) { + FakeEncryptedMedia encrypted_media(new KeyProvidingApp()); + set_need_key_cb(base::Bind(&FakeEncryptedMedia::NeedKey, + base::Unretained(&encrypted_media))); + + ASSERT_TRUE(Start(GetTestDataFilePath("bear-320x240-av_enc-av.webm"), + encrypted_media.decryptor())); + + Play(); + + ASSERT_TRUE(WaitUntilOnEnded()); + Stop(); +} + TEST_F(PipelineIntegrationTest, BasicPlayback_MediaSource) { MockMediaSource source("bear-320x240.webm", kWebM, 219229); StartPipelineWithMediaSource(&source); diff --git a/media/filters/pipeline_integration_test_base.cc b/media/filters/pipeline_integration_test_base.cc index c43cc92..ed47c41 100644 --- a/media/filters/pipeline_integration_test_base.cc +++ b/media/filters/pipeline_integration_test_base.cc @@ -59,6 +59,16 @@ PipelineStatusCB PipelineIntegrationTestBase::QuitOnStatusCB( expected_status); } +void PipelineIntegrationTestBase::DemuxerNeedKeyCB( + const std::string& type, + scoped_array<uint8> init_data, + int init_data_size) { + DCHECK(init_data.get()); + DCHECK_GT(init_data_size, 0); + CHECK(!need_key_cb_.is_null()); + need_key_cb_.Run("", "", type, init_data.Pass(), init_data_size); +} + void PipelineIntegrationTestBase::OnEnded() { DCHECK(!ended_); ended_ = true; @@ -94,7 +104,7 @@ bool PipelineIntegrationTestBase::Start(const base::FilePath& file_path, EXPECT_CALL(*this, OnBufferingState(Pipeline::kPrerollCompleted)) .Times(AtMost(1)); pipeline_->Start( - CreateFilterCollection(file_path), + CreateFilterCollection(file_path, NULL), base::Bind(&PipelineIntegrationTestBase::OnEnded, base::Unretained(this)), base::Bind(&PipelineIntegrationTestBase::OnError, base::Unretained(this)), QuitOnStatusCB(expected_status), @@ -113,12 +123,17 @@ bool PipelineIntegrationTestBase::Start(const base::FilePath& file_path, } bool PipelineIntegrationTestBase::Start(const base::FilePath& file_path) { + return Start(file_path, NULL); +} + +bool PipelineIntegrationTestBase::Start(const base::FilePath& file_path, + Decryptor* decryptor) { EXPECT_CALL(*this, OnBufferingState(Pipeline::kHaveMetadata)) .Times(AtMost(1)); EXPECT_CALL(*this, OnBufferingState(Pipeline::kPrerollCompleted)) .Times(AtMost(1)); pipeline_->Start( - CreateFilterCollection(file_path), + CreateFilterCollection(file_path, decryptor), base::Bind(&PipelineIntegrationTestBase::OnEnded, base::Unretained(this)), base::Bind(&PipelineIntegrationTestBase::OnError, base::Unretained(this)), base::Bind(&PipelineIntegrationTestBase::OnStatusCallback, @@ -186,12 +201,18 @@ bool PipelineIntegrationTestBase::WaitUntilCurrentTimeIsAfter( scoped_ptr<FilterCollection> PipelineIntegrationTestBase::CreateFilterCollection( - const base::FilePath& file_path) { + const base::FilePath& file_path, + Decryptor* decryptor) { scoped_refptr<FileDataSource> data_source = new FileDataSource(); CHECK(data_source->Initialize(file_path)); + media::FFmpegNeedKeyCB need_key_cb = + base::Bind(&PipelineIntegrationTestBase::DemuxerNeedKeyCB, + base::Unretained(this)); return CreateFilterCollection( - new FFmpegDemuxer(message_loop_.message_loop_proxy(), data_source), - NULL); + new FFmpegDemuxer(message_loop_.message_loop_proxy(), + data_source, + need_key_cb), + decryptor); } scoped_ptr<FilterCollection> diff --git a/media/filters/pipeline_integration_test_base.h b/media/filters/pipeline_integration_test_base.h index 23c0339..05e2e7e 100644 --- a/media/filters/pipeline_integration_test_base.h +++ b/media/filters/pipeline_integration_test_base.h @@ -49,6 +49,7 @@ class PipelineIntegrationTestBase { // Initialize the pipeline and ignore any status updates. Useful for testing // invalid audio/video clips which don't have deterministic results. bool Start(const base::FilePath& file_path); + bool Start(const base::FilePath& file_path, Decryptor* decryptor); void Play(); void Pause(); @@ -56,7 +57,7 @@ class PipelineIntegrationTestBase { void Stop(); bool WaitUntilCurrentTimeIsAfter(const base::TimeDelta& wait_time); scoped_ptr<FilterCollection> CreateFilterCollection( - const base::FilePath& file_path); + const base::FilePath& file_path, Decryptor* decryptor); // Returns the MD5 hash of all video frames seen. Should only be called once // after playback completes. First time hashes should be generated with @@ -77,11 +78,18 @@ class PipelineIntegrationTestBase { scoped_refptr<NullAudioSink> audio_sink_; bool ended_; PipelineStatus pipeline_status_; + NeedKeyCB need_key_cb_; void OnStatusCallbackChecked(PipelineStatus expected_status, PipelineStatus status); void OnStatusCallback(PipelineStatus status); PipelineStatusCB QuitOnStatusCB(PipelineStatus expected_status); + void DemuxerNeedKeyCB(const std::string& type, + scoped_array<uint8> init_data, int init_data_size); + void set_need_key_cb(const NeedKeyCB& need_key_cb) { + need_key_cb_ = need_key_cb; + } + void OnEnded(); void OnError(PipelineStatus status); void QuitAfterCurrentTimeTask(const base::TimeDelta& quit_time); diff --git a/media/media.gyp b/media/media.gyp index d506272..2b64447 100644 --- a/media/media.gyp +++ b/media/media.gyp @@ -408,6 +408,8 @@ 'webm/webm_content_encodings.h', 'webm/webm_content_encodings_client.cc', 'webm/webm_content_encodings_client.h', + 'webm/webm_crypto_helpers.cc', + 'webm/webm_crypto_helpers.h', 'webm/webm_info_parser.cc', 'webm/webm_info_parser.h', 'webm/webm_parser.cc', diff --git a/media/tools/demuxer_bench/demuxer_bench.cc b/media/tools/demuxer_bench/demuxer_bench.cc index c874c88..6d49743 100644 --- a/media/tools/demuxer_bench/demuxer_bench.cc +++ b/media/tools/demuxer_bench/demuxer_bench.cc @@ -43,6 +43,11 @@ void QuitLoopWithStatus(MessageLoop* message_loop, message_loop->PostTask(FROM_HERE, MessageLoop::QuitWhenIdleClosure()); } +static void NeedKey(const std::string& type, scoped_array<uint8> init_data, + int init_data_size) { + LOG(INFO) << "File is encrypted."; +} + typedef std::vector<scoped_refptr<media::DemuxerStream> > Streams; // Simulates playback reading requirements by reading from each stream @@ -177,8 +182,10 @@ int main(int argc, char** argv) { new media::FileDataSource(); CHECK(data_source->Initialize(file_path)); + media::FFmpegNeedKeyCB need_key_cb = base::Bind(&NeedKey); scoped_refptr<media::FFmpegDemuxer> demuxer = - new media::FFmpegDemuxer(message_loop.message_loop_proxy(), data_source); + new media::FFmpegDemuxer(message_loop.message_loop_proxy(), data_source, + need_key_cb); demuxer->Initialize(&demuxer_host, base::Bind( &QuitLoopWithStatus, &message_loop)); diff --git a/media/tools/player_x11/player_x11.cc b/media/tools/player_x11/player_x11.cc index 96f07b1..6fc4e47 100644 --- a/media/tools/player_x11/player_x11.cc +++ b/media/tools/player_x11/player_x11.cc @@ -94,6 +94,11 @@ void Paint(MessageLoop* message_loop, const PaintCB& paint_cb, static void OnBufferingState(media::Pipeline::BufferingState buffering_state) {} +static void NeedKey(const std::string& type, scoped_array<uint8> init_data, + int init_data_size) { + std::cout << "File is encrypted." << std::endl; +} + // TODO(vrk): Re-enabled audio. (crbug.com/112159) bool InitPipeline(const scoped_refptr<base::MessageLoopProxy>& message_loop, const scoped_refptr<media::DataSource>& data_source, @@ -104,7 +109,9 @@ bool InitPipeline(const scoped_refptr<base::MessageLoopProxy>& message_loop, // Create our filter factories. scoped_ptr<media::FilterCollection> collection( new media::FilterCollection()); - collection->SetDemuxer(new media::FFmpegDemuxer(message_loop, data_source)); + media::FFmpegNeedKeyCB need_key_cb = base::Bind(&NeedKey); + collection->SetDemuxer(new media::FFmpegDemuxer(message_loop, data_source, + need_key_cb)); collection->GetAudioDecoders()->push_back(new media::FFmpegAudioDecoder( message_loop)); collection->GetVideoDecoders()->push_back(new media::FFmpegVideoDecoder( diff --git a/media/tools/seek_tester/seek_tester.cc b/media/tools/seek_tester/seek_tester.cc index ba9f6fb..29e7ef8 100644 --- a/media/tools/seek_tester/seek_tester.cc +++ b/media/tools/seek_tester/seek_tester.cc @@ -53,6 +53,11 @@ void TimestampExtractor(uint64* timestamp_ms, loop->PostTask(FROM_HERE, MessageLoop::QuitClosure()); } +static void NeedKey(const std::string& type, scoped_array<uint8> init_data, + int init_data_size) { + LOG(INFO) << "File is encrypted."; +} + int main(int argc, char** argv) { base::AtExitManager at_exit; media::InitializeMediaLibraryForTesting(); @@ -67,8 +72,10 @@ int main(int argc, char** argv) { DemuxerHostImpl host; MessageLoop loop; media::PipelineStatusCB quitter = base::Bind(&QuitMessageLoop, &loop); + media::FFmpegNeedKeyCB need_key_cb = base::Bind(&NeedKey); scoped_refptr<media::FFmpegDemuxer> demuxer( - new media::FFmpegDemuxer(loop.message_loop_proxy(), file_data_source)); + new media::FFmpegDemuxer(loop.message_loop_proxy(), file_data_source, + need_key_cb)); demuxer->Initialize(&host, quitter); loop.Run(); diff --git a/media/webm/webm_cluster_parser.cc b/media/webm/webm_cluster_parser.cc index 8a984c8..a0b95a8 100644 --- a/media/webm/webm_cluster_parser.cc +++ b/media/webm/webm_cluster_parser.cc @@ -10,19 +10,10 @@ #include "media/base/buffers.h" #include "media/base/decrypt_config.h" #include "media/webm/webm_constants.h" +#include "media/webm/webm_crypto_helpers.h" namespace media { -// Generates a 16 byte CTR counter block. The CTR counter block format is a -// CTR IV appended with a CTR block counter. |iv| is an 8 byte CTR IV. -// |iv_size| is the size of |iv| in btyes. Returns a string of -// kDecryptionKeySize bytes. -static std::string GenerateCounterBlock(const uint8* iv, int iv_size) { - std::string counter_block(reinterpret_cast<const char*>(iv), iv_size); - counter_block.append(DecryptConfig::kDecryptionKeySize - iv_size, 0); - return counter_block; -} - WebMClusterParser::TextTrackIterator::TextTrackIterator( const TextTrackMap& text_track_map) : iterator_(text_track_map.begin()), @@ -294,39 +285,13 @@ bool WebMClusterParser::OnBlock(bool is_simple_block, int track_num, // encrypted WebM request for comments specification is here // http://wiki.webmproject.org/encryption/webm-encryption-rfc if (!encryption_key_id.empty()) { - DCHECK_EQ(kWebMSignalByteSize, 1); - if (size < kWebMSignalByteSize) { - MEDIA_LOG(log_cb_) - << "Got a block from an encrypted stream with no data."; + scoped_ptr<DecryptConfig> config(WebMCreateDecryptConfig( + data, size, + reinterpret_cast<const uint8*>(encryption_key_id.data()), + encryption_key_id.size())); + if (!config) return false; - } - uint8 signal_byte = data[0]; - int data_offset = sizeof(signal_byte); - - // Setting the DecryptConfig object of the buffer while leaving the - // initialization vector empty will tell the decryptor that the frame is - // unencrypted. - std::string counter_block; - - if (signal_byte & kWebMFlagEncryptedFrame) { - if (size < kWebMSignalByteSize + kWebMIvSize) { - MEDIA_LOG(log_cb_) << "Got an encrypted block with not enough data " - << size; - return false; - } - counter_block = GenerateCounterBlock(data + data_offset, kWebMIvSize); - data_offset += kWebMIvSize; - } - - // TODO(fgalligan): Revisit if DecryptConfig needs to be set on unencrypted - // frames after the CDM API is finalized. - // Unencrypted frames of potentially encrypted streams currently set - // DecryptConfig. - buffer->SetDecryptConfig(scoped_ptr<DecryptConfig>(new DecryptConfig( - encryption_key_id, - counter_block, - data_offset, - std::vector<SubsampleEntry>()))); + buffer->SetDecryptConfig(config.Pass()); } buffer->SetTimestamp(timestamp); diff --git a/media/webm/webm_cluster_parser_unittest.cc b/media/webm/webm_cluster_parser_unittest.cc index 07deb05..dc73329 100644 --- a/media/webm/webm_cluster_parser_unittest.cc +++ b/media/webm/webm_cluster_parser_unittest.cc @@ -6,6 +6,7 @@ #include "base/bind.h" #include "base/logging.h" +#include "media/base/decrypt_config.h" #include "media/webm/cluster_builder.h" #include "media/webm/webm_cluster_parser.h" #include "media/webm/webm_constants.h" @@ -32,7 +33,7 @@ struct BlockInfo { bool use_simple_block; }; -const BlockInfo kDefaultBlockInfo[] = { +static const BlockInfo kDefaultBlockInfo[] = { { kAudioTrackNum, 0, 23, true }, { kAudioTrackNum, 23, 23, true }, { kVideoTrackNum, 33, 34, true }, @@ -42,6 +43,11 @@ const BlockInfo kDefaultBlockInfo[] = { { kVideoTrackNum, 100, 33, false }, }; +static const uint8 kEncryptedFrame[] = { + 0x01, // Block is encrypted + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08 // IV +}; + static scoped_ptr<Cluster> CreateCluster(int timecode, const BlockInfo* block_info, int block_count) { @@ -67,6 +73,18 @@ static scoped_ptr<Cluster> CreateCluster(int timecode, return cb.Finish(); } +// Creates a Cluster with one encrypted Block. |bytes_to_write| is number of +// bytes of the encrypted frame to write. +static scoped_ptr<Cluster> CreateEncryptedCluster(int bytes_to_write) { + CHECK_GT(bytes_to_write, 0); + CHECK_LE(bytes_to_write, static_cast<int>(sizeof(kEncryptedFrame))); + + ClusterBuilder cb; + cb.SetClusterTimecode(0); + cb.AddSimpleBlock(kVideoTrackNum, 0, 0, kEncryptedFrame, bytes_to_write); + return cb.Finish(); +} + static bool VerifyBuffers(const WebMClusterParser::BufferQueue& audio_buffers, const WebMClusterParser::BufferQueue& video_buffers, const WebMClusterParser::BufferQueue& text_buffers, @@ -165,6 +183,15 @@ static bool VerifyTextBuffers( return true; } +static bool VerifyEncryptedBuffer( + scoped_refptr<StreamParserBuffer> buffer) { + EXPECT_TRUE(buffer->GetDecryptConfig()); + EXPECT_EQ(static_cast<unsigned long>(DecryptConfig::kDecryptionKeySize), + buffer->GetDecryptConfig()->iv().length()); + const uint8* data = buffer->GetData(); + return data[0] & kWebMFlagEncryptedFrame; +} + static void AppendToEnd(const WebMClusterParser::BufferQueue& src, WebMClusterParser::BufferQueue* dest) { for (WebMClusterParser::BufferQueue::const_iterator itr = src.begin(); @@ -440,4 +467,32 @@ TEST_F(WebMClusterParserTest, ParseMultipleTextTracks) { } } +TEST_F(WebMClusterParserTest, ParseEncryptedBlock) { + scoped_ptr<Cluster> cluster(CreateEncryptedCluster(sizeof(kEncryptedFrame))); + + parser_.reset(new WebMClusterParser( + kTimecodeScale, kAudioTrackNum, kVideoTrackNum, + std::set<int>(), + std::set<int64>(), "", "video_key_id", + LogCB())); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(cluster->size(), result); + ASSERT_EQ(1UL, parser_->video_buffers().size()); + scoped_refptr<StreamParserBuffer> buffer = parser_->video_buffers()[0]; + EXPECT_TRUE(VerifyEncryptedBuffer(buffer)); +} + +TEST_F(WebMClusterParserTest, ParseBadEncryptedBlock) { + scoped_ptr<Cluster> cluster( + CreateEncryptedCluster(sizeof(kEncryptedFrame) - 1)); + + parser_.reset(new WebMClusterParser( + kTimecodeScale, kAudioTrackNum, kVideoTrackNum, + std::set<int>(), + std::set<int64>(), "", "video_key_id", + LogCB())); + int result = parser_->Parse(cluster->data(), cluster->size()); + EXPECT_EQ(-1, result); +} + } // namespace media diff --git a/media/webm/webm_crypto_helpers.cc b/media/webm/webm_crypto_helpers.cc new file mode 100644 index 0000000..ea63f87 --- /dev/null +++ b/media/webm/webm_crypto_helpers.cc @@ -0,0 +1,60 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include "media/webm/webm_crypto_helpers.h" + +#include "base/logging.h" +#include "base/sys_byteorder.h" +#include "media/base/decrypt_config.h" +#include "media/webm/webm_constants.h" + +namespace media { +namespace { + +// Generates a 16 byte CTR counter block. The CTR counter block format is a +// CTR IV appended with a CTR block counter. |iv| is an 8 byte CTR IV. +// |iv_size| is the size of |iv| in btyes. Returns a string of +// kDecryptionKeySize bytes. +std::string GenerateWebMCounterBlock(const uint8* iv, int iv_size) { + std::string counter_block(reinterpret_cast<const char*>(iv), iv_size); + counter_block.append(DecryptConfig::kDecryptionKeySize - iv_size, 0); + return counter_block; +} + +} // namespace anonymous + +scoped_ptr<DecryptConfig> WebMCreateDecryptConfig( + const uint8* data, int data_size, + const uint8* key_id, int key_id_size) { + if (data_size < kWebMSignalByteSize) { + DVLOG(1) << "Got a block from an encrypted stream with no data."; + return scoped_ptr<DecryptConfig>(NULL); + } + + uint8 signal_byte = data[0]; + int frame_offset = sizeof(signal_byte); + + // Setting the DecryptConfig object of the buffer while leaving the + // initialization vector empty will tell the decryptor that the frame is + // unencrypted. + std::string counter_block; + + if (signal_byte & kWebMFlagEncryptedFrame) { + if (data_size < kWebMSignalByteSize + kWebMIvSize) { + DVLOG(1) << "Got an encrypted block with not enough data " << data_size; + return scoped_ptr<DecryptConfig>(NULL); + } + counter_block = GenerateWebMCounterBlock(data + frame_offset, kWebMIvSize); + frame_offset += kWebMIvSize; + } + + scoped_ptr<DecryptConfig> config(new DecryptConfig( + std::string(reinterpret_cast<const char*>(key_id), key_id_size), + counter_block, + frame_offset, + std::vector<SubsampleEntry>())); + return config.Pass(); +} + +} // namespace media diff --git a/media/webm/webm_crypto_helpers.h b/media/webm/webm_crypto_helpers.h new file mode 100644 index 0000000..c5f1f15 --- /dev/null +++ b/media/webm/webm_crypto_helpers.h @@ -0,0 +1,32 @@ +// Copyright (c) 2013 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#ifndef MEDIA_WEBM_WEBM_CRYPTO_HELPERS_H_ +#define MEDIA_WEBM_WEBM_CRYPTO_HELPERS_H_ + +#include "base/basictypes.h" +#include "base/memory/scoped_ptr.h" +#include "media/base/decoder_buffer.h" + +namespace media { + +// TODO(xhwang): Figure out the init data type appropriately once it's spec'ed. +// See https://www.w3.org/Bugs/Public/show_bug.cgi?id=19096 for more +// information. +const char kWebMEncryptInitDataType[] = "video/webm"; + +// Returns an initialized DecryptConfig, which can be sent to the Decryptor if +// the stream has potentially encrypted frames. Every encrypted Block has a +// signal byte, and if the frame is encrypted, an initialization vector +// prepended to the frame. Leaving the IV empty will tell the decryptor that the +// frame is unencrypted. Returns NULL if |data| is invalid. Current encrypted +// WebM request for comments specification is here +// http://wiki.webmproject.org/encryption/webm-encryption-rfc +scoped_ptr<DecryptConfig> WebMCreateDecryptConfig( + const uint8* data, int data_size, + const uint8* key_id, int key_id_size); + +} // namespace media + +#endif // MEDIA_WEBM_WEBM_CRYPT_HELPERS_H_ diff --git a/media/webm/webm_stream_parser.cc b/media/webm/webm_stream_parser.cc index b60de5d..f47fa41 100644 --- a/media/webm/webm_stream_parser.cc +++ b/media/webm/webm_stream_parser.cc @@ -14,14 +14,12 @@ #include "media/webm/webm_cluster_parser.h" #include "media/webm/webm_constants.h" #include "media/webm/webm_content_encodings.h" +#include "media/webm/webm_crypto_helpers.h" #include "media/webm/webm_info_parser.h" #include "media/webm/webm_tracks_parser.h" namespace media { -// TODO(xhwang): Figure out the init data type appropriately once it's spec'ed. -static const char kWebMInitDataType[] = "video/webm"; - // Helper class that uses FFmpeg to create AudioDecoderConfig & // VideoDecoderConfig objects. // @@ -150,13 +148,12 @@ bool FFmpegConfigHelper::SetupStreamConfigs() { bool no_supported_streams = true; for (size_t i = 0; i < format_context->nb_streams; ++i) { AVStream* stream = format_context->streams[i]; - AVCodecContext* codec_context = stream->codec; - AVMediaType codec_type = codec_context->codec_type; + AVMediaType codec_type = stream->codec->codec_type; if (codec_type == AVMEDIA_TYPE_AUDIO && stream->codec->codec_id == CODEC_ID_VORBIS && !audio_config_.IsValidConfig()) { - AVCodecContextToAudioDecoderConfig(stream->codec, &audio_config_); + AVStreamToAudioDecoderConfig(stream, &audio_config_); no_supported_streams = false; continue; } @@ -473,7 +470,7 @@ void WebMStreamParser::FireNeedKey(const std::string& key_id) { DCHECK_GT(key_id_size, 0); scoped_array<uint8> key_id_array(new uint8[key_id_size]); memcpy(key_id_array.get(), key_id.data(), key_id_size); - need_key_cb_.Run(kWebMInitDataType, key_id_array.Pass(), key_id_size); + need_key_cb_.Run(kWebMEncryptInitDataType, key_id_array.Pass(), key_id_size); } } // namespace media diff --git a/webkit/media/filter_helpers.cc b/webkit/media/filter_helpers.cc index 4308f9a..df584a4 100644 --- a/webkit/media/filter_helpers.cc +++ b/webkit/media/filter_helpers.cc @@ -72,9 +72,10 @@ void BuildMediaSourceCollection( void BuildDefaultCollection( const scoped_refptr<media::DataSource>& data_source, const scoped_refptr<base::MessageLoopProxy>& message_loop, - media::FilterCollection* filter_collection) { + media::FilterCollection* filter_collection, + const media::FFmpegNeedKeyCB& need_key_cb) { filter_collection->SetDemuxer(new media::FFmpegDemuxer( - message_loop, data_source)); + message_loop, data_source, need_key_cb)); AddDefaultDecodersToCollection(message_loop, filter_collection); } diff --git a/webkit/media/filter_helpers.h b/webkit/media/filter_helpers.h index bfa8b89..b30ecd2 100644 --- a/webkit/media/filter_helpers.h +++ b/webkit/media/filter_helpers.h @@ -7,6 +7,8 @@ #include "base/basictypes.h" #include "base/memory/ref_counted.h" +// TODO(fgalligan): Remove the dependency on FFmpeg. +#include "media/filters/ffmpeg_demuxer.h" namespace base { class MessageLoopProxy; @@ -33,7 +35,8 @@ void BuildMediaSourceCollection( void BuildDefaultCollection( const scoped_refptr<media::DataSource>& data_source, const scoped_refptr<base::MessageLoopProxy>& message_loop, - media::FilterCollection* filter_collection); + media::FilterCollection* filter_collection, + const media::FFmpegNeedKeyCB& need_key_cb); } // webkit_media diff --git a/webkit/media/webmediaplayer_impl.cc b/webkit/media/webmediaplayer_impl.cc index a4bfe39..981ed30 100644 --- a/webkit/media/webmediaplayer_impl.cc +++ b/webkit/media/webmediaplayer_impl.cc @@ -277,9 +277,11 @@ void WebMediaPlayerImpl::load(const WebKit::WebURL& url, CORSMode cors_mode) { is_local_source_ = !gurl.SchemeIs("http") && !gurl.SchemeIs("https"); - BuildDefaultCollection(data_source_, - media_thread_.message_loop_proxy(), - filter_collection_.get()); + BuildDefaultCollection( + data_source_, + media_thread_.message_loop_proxy(), + filter_collection_.get(), + BIND_TO_RENDER_LOOP_2(&WebMediaPlayerImpl::OnNeedKey, "", "")); } void WebMediaPlayerImpl::load(const WebKit::WebURL& url, |