 Chromium Code Reviews
 Chromium Code Reviews Issue 2808583002:
  RELAND: Media Remoting end to end integration tests.  (Closed)
    
  
    Issue 2808583002:
  RELAND: Media Remoting end to end integration tests.  (Closed) 
  | Index: media/test/pipeline_integration_test_base.cc | 
| diff --git a/media/test/pipeline_integration_test_base.cc b/media/test/pipeline_integration_test_base.cc | 
| index b2fd6f608808f6a898df374bf93c36ccbcb7152d..60b4eec159f62181498f6270f3590552fbaaaff0 100644 | 
| --- a/media/test/pipeline_integration_test_base.cc | 
| +++ b/media/test/pipeline_integration_test_base.cc | 
| @@ -12,11 +12,13 @@ | 
| #include "base/memory/scoped_vector.h" | 
| #include "base/run_loop.h" | 
| #include "base/single_thread_task_runner.h" | 
| -#include "media/base/cdm_context.h" | 
| +#include "base/threading/thread_task_runner_handle.h" | 
| +#include "media/base/cdm_key_information.h" | 
| #include "media/base/media_log.h" | 
| #include "media/base/media_tracks.h" | 
| #include "media/base/test_data_util.h" | 
| -#include "media/filters/chunk_demuxer.h" | 
| +#include "media/base/timestamp_constants.h" | 
| +#include "media/cdm/aes_decryptor.h" | 
| #if !defined(MEDIA_DISABLE_FFMPEG) | 
| #include "media/filters/ffmpeg_audio_decoder.h" | 
| #include "media/filters/ffmpeg_demuxer.h" | 
| @@ -82,6 +84,275 @@ static ScopedVector<AudioDecoder> CreateAudioDecodersForTest( | 
| const char kNullVideoHash[] = "d41d8cd98f00b204e9800998ecf8427e"; | 
| const char kNullAudioHash[] = "0.00,0.00,0.00,0.00,0.00,0.00,"; | 
| +const size_t kAppendWholeFile = std::numeric_limits<size_t>::max(); | 
| + | 
| +namespace { | 
| 
DaleCurtis
2017/04/20 18:24:51
Again drop.
 
xjz
2017/04/20 21:26:10
Done.
 | 
| + | 
| +constexpr char kSourceId[] = "SourceId"; | 
| + | 
| +class RendererFactoryImpl final : public PipelineTestRendererFactory { | 
| + public: | 
| + explicit RendererFactoryImpl(PipelineIntegrationTestBase* integration_test) | 
| + : integration_test_(integration_test) {} | 
| + ~RendererFactoryImpl() override {} | 
| + | 
| + // PipelineTestRendererFactory implementation. | 
| + std::unique_ptr<Renderer> CreateRenderer( | 
| + CreateVideoDecodersCB prepend_video_decoders_cb = CreateVideoDecodersCB(), | 
| + CreateAudioDecodersCB prepend_audio_decoders_cb = | 
| + CreateAudioDecodersCB()) override { | 
| + return integration_test_->CreateRenderer(prepend_video_decoders_cb, | 
| + prepend_audio_decoders_cb); | 
| + } | 
| + | 
| + private: | 
| + PipelineIntegrationTestBase* integration_test_; | 
| + | 
| + DISALLOW_COPY_AND_ASSIGN(RendererFactoryImpl); | 
| +}; | 
| + | 
| +} // namespace | 
| + | 
| +MockMediaSource::MockMediaSource(const std::string& filename, | 
| + const std::string& mimetype, | 
| + size_t initial_append_size) | 
| + : current_position_(0), | 
| + initial_append_size_(initial_append_size), | 
| + mimetype_(mimetype), | 
| + chunk_demuxer_(new ChunkDemuxer( | 
| + base::Bind(&MockMediaSource::DemuxerOpened, base::Unretained(this)), | 
| + base::Bind(&MockMediaSource::OnEncryptedMediaInitData, | 
| + base::Unretained(this)), | 
| + &media_log_)), | 
| + owned_chunk_demuxer_(chunk_demuxer_) { | 
| + file_data_ = ReadTestDataFile(filename); | 
| + | 
| + if (initial_append_size_ == kAppendWholeFile) | 
| + initial_append_size_ = file_data_->data_size(); | 
| + | 
| + DCHECK_GT(initial_append_size_, 0u); | 
| + DCHECK_LE(initial_append_size_, file_data_->data_size()); | 
| +} | 
| + | 
| +MockMediaSource::~MockMediaSource() {} | 
| + | 
| +std::unique_ptr<Demuxer> MockMediaSource::GetDemuxer() { | 
| + return std::move(owned_chunk_demuxer_); | 
| +} | 
| + | 
| +void MockMediaSource::set_encrypted_media_init_data_cb( | 
| + const Demuxer::EncryptedMediaInitDataCB& encrypted_media_init_data_cb) { | 
| + encrypted_media_init_data_cb_ = encrypted_media_init_data_cb; | 
| +} | 
| + | 
| +void MockMediaSource::set_demuxer_failure_cb( | 
| + const PipelineStatusCB& demuxer_failure_cb) { | 
| + demuxer_failure_cb_ = demuxer_failure_cb; | 
| +} | 
| + | 
| +void MockMediaSource::Seek(base::TimeDelta seek_time, | 
| + size_t new_position, | 
| + size_t seek_append_size) { | 
| + chunk_demuxer_->StartWaitingForSeek(seek_time); | 
| + | 
| + chunk_demuxer_->ResetParserState(kSourceId, base::TimeDelta(), | 
| + kInfiniteDuration, &last_timestamp_offset_); | 
| + | 
| + DCHECK_LT(new_position, file_data_->data_size()); | 
| + current_position_ = new_position; | 
| + | 
| + AppendData(seek_append_size); | 
| +} | 
| + | 
| +void MockMediaSource::Seek(base::TimeDelta seek_time) { | 
| + chunk_demuxer_->StartWaitingForSeek(seek_time); | 
| +} | 
| + | 
| +void MockMediaSource::AppendData(size_t size) { | 
| + DCHECK(chunk_demuxer_); | 
| + DCHECK_LT(current_position_, file_data_->data_size()); | 
| + DCHECK_LE(current_position_ + size, file_data_->data_size()); | 
| + | 
| + ASSERT_TRUE(chunk_demuxer_->AppendData( | 
| + kSourceId, file_data_->data() + current_position_, size, | 
| + base::TimeDelta(), kInfiniteDuration, &last_timestamp_offset_)); | 
| + current_position_ += size; | 
| +} | 
| + | 
| +bool MockMediaSource::AppendAtTime(base::TimeDelta timestamp_offset, | 
| + const uint8_t* pData, | 
| + int size) { | 
| + CHECK(!chunk_demuxer_->IsParsingMediaSegment(kSourceId)); | 
| + bool success = | 
| + chunk_demuxer_->AppendData(kSourceId, pData, size, base::TimeDelta(), | 
| + kInfiniteDuration, ×tamp_offset); | 
| + last_timestamp_offset_ = timestamp_offset; | 
| + return success; | 
| +} | 
| + | 
| +void MockMediaSource::AppendAtTimeWithWindow( | 
| + base::TimeDelta timestamp_offset, | 
| + base::TimeDelta append_window_start, | 
| + base::TimeDelta append_window_end, | 
| + const uint8_t* pData, | 
| + int size) { | 
| + CHECK(!chunk_demuxer_->IsParsingMediaSegment(kSourceId)); | 
| + ASSERT_TRUE(chunk_demuxer_->AppendData(kSourceId, pData, size, | 
| + append_window_start, append_window_end, | 
| + ×tamp_offset)); | 
| + last_timestamp_offset_ = timestamp_offset; | 
| +} | 
| + | 
| +void MockMediaSource::SetMemoryLimits(size_t limit_bytes) { | 
| + chunk_demuxer_->SetMemoryLimitsForTest(DemuxerStream::AUDIO, limit_bytes); | 
| + chunk_demuxer_->SetMemoryLimitsForTest(DemuxerStream::VIDEO, limit_bytes); | 
| +} | 
| + | 
| +void MockMediaSource::EvictCodedFrames(base::TimeDelta currentMediaTime, | 
| + size_t newDataSize) { | 
| + chunk_demuxer_->EvictCodedFrames(kSourceId, currentMediaTime, newDataSize); | 
| +} | 
| + | 
| +void MockMediaSource::RemoveRange(base::TimeDelta start, base::TimeDelta end) { | 
| + chunk_demuxer_->Remove(kSourceId, start, end); | 
| +} | 
| + | 
| +void MockMediaSource::EndOfStream() { | 
| + chunk_demuxer_->MarkEndOfStream(PIPELINE_OK); | 
| +} | 
| + | 
| +void MockMediaSource::Shutdown() { | 
| + if (!chunk_demuxer_) | 
| + return; | 
| + chunk_demuxer_->ResetParserState(kSourceId, base::TimeDelta(), | 
| + kInfiniteDuration, &last_timestamp_offset_); | 
| + chunk_demuxer_->Shutdown(); | 
| + chunk_demuxer_ = NULL; | 
| +} | 
| + | 
| +void MockMediaSource::DemuxerOpened() { | 
| + base::ThreadTaskRunnerHandle::Get()->PostTask( | 
| + FROM_HERE, | 
| + base::Bind(&MockMediaSource::DemuxerOpenedTask, base::Unretained(this))); | 
| +} | 
| + | 
| +void MockMediaSource::DemuxerOpenedTask() { | 
| + ChunkDemuxer::Status status = AddId(); | 
| + if (status != ChunkDemuxer::kOk) { | 
| + CHECK(!demuxer_failure_cb_.is_null()); | 
| + demuxer_failure_cb_.Run(DEMUXER_ERROR_COULD_NOT_OPEN); | 
| + return; | 
| + } | 
| + chunk_demuxer_->SetTracksWatcher( | 
| + kSourceId, base::Bind(&MockMediaSource::InitSegmentReceived, | 
| + base::Unretained(this))); | 
| + | 
| + AppendData(initial_append_size_); | 
| +} | 
| + | 
| +ChunkDemuxer::Status MockMediaSource::AddId() { | 
| + // This code assumes that |mimetype_| is one of the following forms. | 
| + // 1. audio/mpeg | 
| + // 2. video/webm;codec="vorbis,vp8". | 
| + size_t semicolon = mimetype_.find(";"); | 
| + std::string type = mimetype_; | 
| + std::string codecs_param = ""; | 
| + if (semicolon != std::string::npos) { | 
| + type = mimetype_.substr(0, semicolon); | 
| + size_t codecs_param_start = mimetype_.find("codecs=\"", semicolon); | 
| + | 
| + CHECK_NE(codecs_param_start, std::string::npos); | 
| + | 
| + codecs_param_start += 8; // Skip over the codecs=". | 
| + | 
| + size_t codecs_param_end = mimetype_.find("\"", codecs_param_start); | 
| + | 
| + CHECK_NE(codecs_param_end, std::string::npos); | 
| + | 
| + codecs_param = mimetype_.substr(codecs_param_start, | 
| + codecs_param_end - codecs_param_start); | 
| + } | 
| + | 
| + return chunk_demuxer_->AddId(kSourceId, type, codecs_param); | 
| +} | 
| + | 
| +void MockMediaSource::OnEncryptedMediaInitData( | 
| + EmeInitDataType init_data_type, | 
| + const std::vector<uint8_t>& init_data) { | 
| + DCHECK(!init_data.empty()); | 
| + CHECK(!encrypted_media_init_data_cb_.is_null()); | 
| + encrypted_media_init_data_cb_.Run(init_data_type, init_data); | 
| +} | 
| + | 
| +base::TimeDelta MockMediaSource::last_timestamp_offset() const { | 
| + return last_timestamp_offset_; | 
| +} | 
| + | 
| +void MockMediaSource::InitSegmentReceived(std::unique_ptr<MediaTracks> tracks) { | 
| + CHECK(tracks.get()); | 
| + EXPECT_GT(tracks->tracks().size(), 0u); | 
| + CHECK(chunk_demuxer_); | 
| + // Verify that track ids are unique. | 
| + std::set<MediaTrack::Id> track_ids; | 
| + for (const auto& track : tracks->tracks()) { | 
| + EXPECT_EQ(track_ids.end(), track_ids.find(track->id())); | 
| + track_ids.insert(track->id()); | 
| + } | 
| + InitSegmentReceivedMock(tracks); | 
| +} | 
| + | 
| +FakeEncryptedMedia::TestCdmContext::TestCdmContext(Decryptor* decryptor) | 
| + : decryptor_(decryptor) {} | 
| + | 
| +Decryptor* FakeEncryptedMedia::TestCdmContext::GetDecryptor() { | 
| + return decryptor_; | 
| +} | 
| +int FakeEncryptedMedia::TestCdmContext::GetCdmId() const { | 
| + return kInvalidCdmId; | 
| +} | 
| + | 
| +FakeEncryptedMedia::FakeEncryptedMedia(AppBase* app) | 
| + : decryptor_( | 
| + new AesDecryptor(GURL::EmptyGURL(), | 
| + base::Bind(&FakeEncryptedMedia::OnSessionMessage, | 
| + base::Unretained(this)), | 
| + base::Bind(&FakeEncryptedMedia::OnSessionClosed, | 
| + base::Unretained(this)), | 
| + base::Bind(&FakeEncryptedMedia::OnSessionKeysChange, | 
| + base::Unretained(this)))), | 
| + cdm_context_(decryptor_.get()), | 
| + app_(app) {} | 
| + | 
| +FakeEncryptedMedia::~FakeEncryptedMedia() {} | 
| + | 
| +CdmContext* FakeEncryptedMedia::GetCdmContext() { | 
| + return &cdm_context_; | 
| +} | 
| + | 
| +// Callbacks for firing session events. Delegate to |app_|. | 
| +void FakeEncryptedMedia::OnSessionMessage( | 
| + const std::string& session_id, | 
| + ContentDecryptionModule::MessageType message_type, | 
| + const std::vector<uint8_t>& message) { | 
| + app_->OnSessionMessage(session_id, message_type, message, decryptor_.get()); | 
| +} | 
| + | 
| +void FakeEncryptedMedia::OnSessionClosed(const std::string& session_id) { | 
| + app_->OnSessionClosed(session_id); | 
| +} | 
| + | 
| +void FakeEncryptedMedia::OnSessionKeysChange(const std::string& session_id, | 
| + bool has_additional_usable_key, | 
| + CdmKeysInfo keys_info) { | 
| + app_->OnSessionKeysChange(session_id, has_additional_usable_key, | 
| + std::move(keys_info)); | 
| +} | 
| + | 
| +void FakeEncryptedMedia::OnEncryptedMediaInitData( | 
| + EmeInitDataType init_data_type, | 
| + const std::vector<uint8_t>& init_data) { | 
| + app_->OnEncryptedMediaInitData(init_data_type, init_data, decryptor_.get()); | 
| +} | 
| PipelineIntegrationTestBase::PipelineIntegrationTestBase() | 
| : hashing_enabled_(false), | 
| @@ -91,7 +362,8 @@ PipelineIntegrationTestBase::PipelineIntegrationTestBase() | 
| pipeline_status_(PIPELINE_OK), | 
| last_video_frame_format_(PIXEL_FORMAT_UNKNOWN), | 
| last_video_frame_color_space_(COLOR_SPACE_UNSPECIFIED), | 
| - current_duration_(kInfiniteDuration) { | 
| + current_duration_(kInfiniteDuration), | 
| + renderer_factory_(new RendererFactoryImpl(this)) { | 
| ResetVideoHash(); | 
| EXPECT_CALL(*this, OnVideoAverageKeyframeDistanceUpdate()).Times(AnyNumber()); | 
| } | 
| @@ -217,12 +489,12 @@ PipelineStatus PipelineIntegrationTestBase::StartInternal( | 
| // media files are provided in advance. | 
| EXPECT_CALL(*this, OnWaitingForDecryptionKey()).Times(0); | 
| - pipeline_->Start( | 
| - demuxer_.get(), | 
| - CreateRenderer(prepend_video_decoders_cb, prepend_audio_decoders_cb), | 
| - this, | 
| - base::Bind(&PipelineIntegrationTestBase::OnStatusCallback, | 
| - base::Unretained(this))); | 
| + pipeline_->Start(demuxer_.get(), | 
| + renderer_factory_->CreateRenderer(prepend_video_decoders_cb, | 
| + prepend_audio_decoders_cb), | 
| + this, | 
| + base::Bind(&PipelineIntegrationTestBase::OnStatusCallback, | 
| + base::Unretained(this))); | 
| base::RunLoop().Run(); | 
| return pipeline_status_; | 
| } | 
| @@ -298,7 +570,7 @@ bool PipelineIntegrationTestBase::Resume(base::TimeDelta seek_time) { | 
| EXPECT_CALL(*this, OnBufferingStateChange(BUFFERING_HAVE_ENOUGH)) | 
| .WillOnce(InvokeWithoutArgs(&message_loop_, &base::MessageLoop::QuitNow)); | 
| - pipeline_->Resume(CreateRenderer(), seek_time, | 
| + pipeline_->Resume(renderer_factory_->CreateRenderer(), seek_time, | 
| base::Bind(&PipelineIntegrationTestBase::OnSeeked, | 
| base::Unretained(this), seek_time)); | 
| base::RunLoop().Run(); | 
| @@ -472,6 +744,101 @@ base::TimeDelta PipelineIntegrationTestBase::GetAudioTime() { | 
| return clockless_audio_sink_->render_time(); | 
| } | 
| +PipelineStatus PipelineIntegrationTestBase::StartPipelineWithMediaSource( | 
| + MockMediaSource* source) { | 
| + return StartPipelineWithMediaSource(source, kNormal, nullptr); | 
| +} | 
| + | 
| +PipelineStatus PipelineIntegrationTestBase::StartPipelineWithEncryptedMedia( | 
| + MockMediaSource* source, | 
| + FakeEncryptedMedia* encrypted_media) { | 
| + return StartPipelineWithMediaSource(source, kNormal, encrypted_media); | 
| +} | 
| + | 
| +PipelineStatus PipelineIntegrationTestBase::StartPipelineWithMediaSource( | 
| + MockMediaSource* source, | 
| + uint8_t test_type, | 
| + FakeEncryptedMedia* encrypted_media) { | 
| + hashing_enabled_ = test_type & kHashed; | 
| + clockless_playback_ = test_type & kClockless; | 
| + | 
| + if (!(test_type & kExpectDemuxerFailure)) | 
| + EXPECT_CALL(*source, InitSegmentReceivedMock(_)).Times(AtLeast(1)); | 
| + | 
| + EXPECT_CALL(*this, OnMetadata(_)) | 
| + .Times(AtMost(1)) | 
| + .WillRepeatedly(SaveArg<0>(&metadata_)); | 
| + EXPECT_CALL(*this, OnBufferingStateChange(BUFFERING_HAVE_ENOUGH)) | 
| + .Times(AnyNumber()); | 
| + EXPECT_CALL(*this, OnBufferingStateChange(BUFFERING_HAVE_NOTHING)) | 
| + .Times(AnyNumber()); | 
| + EXPECT_CALL(*this, OnDurationChange()).Times(AnyNumber()); | 
| + EXPECT_CALL(*this, OnVideoNaturalSizeChange(_)).Times(AtMost(1)); | 
| + EXPECT_CALL(*this, OnVideoOpacityChange(_)).Times(AtMost(1)); | 
| + | 
| + source->set_demuxer_failure_cb(base::Bind( | 
| + &PipelineIntegrationTestBase::OnStatusCallback, base::Unretained(this))); | 
| + demuxer_ = source->GetDemuxer(); | 
| + | 
| + if (encrypted_media) { | 
| + EXPECT_CALL(*this, DecryptorAttached(true)); | 
| + | 
| + // Encrypted content used but keys provided in advance, so this is | 
| + // never called. | 
| + EXPECT_CALL(*this, OnWaitingForDecryptionKey()).Times(0); | 
| + pipeline_->SetCdm( | 
| + encrypted_media->GetCdmContext(), | 
| + base::Bind(&PipelineIntegrationTestBase::DecryptorAttached, | 
| + base::Unretained(this))); | 
| + } else { | 
| + // Encrypted content not used, so this is never called. | 
| + EXPECT_CALL(*this, OnWaitingForDecryptionKey()).Times(0); | 
| + } | 
| + | 
| + pipeline_->Start(demuxer_.get(), renderer_factory_->CreateRenderer(), this, | 
| + base::Bind(&PipelineIntegrationTestBase::OnStatusCallback, | 
| + base::Unretained(this))); | 
| + | 
| + if (encrypted_media) { | 
| + source->set_encrypted_media_init_data_cb( | 
| + base::Bind(&FakeEncryptedMedia::OnEncryptedMediaInitData, | 
| + base::Unretained(encrypted_media))); | 
| + } | 
| + base::RunLoop().Run(); | 
| + return pipeline_status_; | 
| +} | 
| + | 
| +// Verifies that seeking works properly for ChunkDemuxer when the | 
| +// seek happens while there is a pending read on the ChunkDemuxer | 
| +// and no data is available. | 
| +bool PipelineIntegrationTestBase::TestSeekDuringRead( | 
| + const std::string& filename, | 
| + const std::string& mimetype, | 
| + int initial_append_size, | 
| + base::TimeDelta start_seek_time, | 
| + base::TimeDelta seek_time, | 
| + int seek_file_position, | 
| + int seek_append_size) { | 
| + MockMediaSource source(filename, mimetype, initial_append_size); | 
| + | 
| + if (StartPipelineWithMediaSource(&source) != PIPELINE_OK) | 
| + return false; | 
| + | 
| + Play(); | 
| + if (!WaitUntilCurrentTimeIsAfter(start_seek_time)) | 
| + return false; | 
| + | 
| + source.Seek(seek_time, seek_file_position, seek_append_size); | 
| + if (!Seek(seek_time)) | 
| + return false; | 
| + | 
| + source.EndOfStream(); | 
| + | 
| + source.Shutdown(); | 
| + Stop(); | 
| + return true; | 
| +} | 
| + | 
| base::TimeTicks DummyTickClock::NowTicks() { | 
| now_ += base::TimeDelta::FromSeconds(60); | 
| return now_; |