diff options
-rw-r--r-- | media/base/demuxer_stream.h | 6 | ||||
-rw-r--r-- | media/base/filters.h | 9 | ||||
-rw-r--r-- | media/filters/audio_renderer_base.cc | 72 | ||||
-rw-r--r-- | media/filters/audio_renderer_base.h | 6 | ||||
-rw-r--r-- | media/filters/audio_renderer_base_unittest.cc | 64 | ||||
-rw-r--r-- | media/filters/chunk_demuxer.cc | 219 | ||||
-rw-r--r-- | media/filters/ffmpeg_audio_decoder.cc | 6 | ||||
-rw-r--r-- | media/filters/ffmpeg_audio_decoder_unittest.cc | 16 | ||||
-rw-r--r-- | media/filters/ffmpeg_video_decoder.cc | 5 | ||||
-rw-r--r-- | media/filters/ffmpeg_video_decoder_unittest.cc | 12 | ||||
-rw-r--r-- | media/filters/gpu_video_decoder.cc | 10 | ||||
-rw-r--r-- | media/filters/pipeline_integration_test.cc | 140 | ||||
-rw-r--r-- | media/filters/video_renderer_base.cc | 14 | ||||
-rw-r--r-- | media/filters/video_renderer_base_unittest.cc | 99 | ||||
-rw-r--r-- | media/webm/webm_cluster_parser.cc | 8 |
15 files changed, 557 insertions, 129 deletions
diff --git a/media/base/demuxer_stream.h b/media/base/demuxer_stream.h index fb0739b..d793d49 100644 --- a/media/base/demuxer_stream.h +++ b/media/base/demuxer_stream.h @@ -1,4 +1,4 @@ -// Copyright (c) 2011 The Chromium Authors. All rights reserved. +// Copyright (c) 2012 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. @@ -27,7 +27,9 @@ class MEDIA_EXPORT DemuxerStream // Request a buffer to returned via the provided callback. // - // Buffers will be non-NULL yet may be end of stream buffers. + // Non-NULL buffer pointers will contain media data or signal the end of the + // stream. A NULL pointer indicates an aborted Read(). This can happen if the + // DemuxerStream gets flushed and doesn't have any more data to return. typedef base::Callback<void(const scoped_refptr<Buffer>&)> ReadCallback; virtual void Read(const ReadCallback& read_callback) = 0; diff --git a/media/base/filters.h b/media/base/filters.h index 8f186d6..cf8bcea 100644 --- a/media/base/filters.h +++ b/media/base/filters.h @@ -135,7 +135,9 @@ class MEDIA_EXPORT VideoDecoder : public Filter { // Implementations guarantee that the callback will not be called from within // this method. // - // Frames will be non-NULL yet may be end of stream frames. + // Non-NULL frames contain decoded video data or may indicate the end of + // the stream. NULL video frames indicate an aborted read. This can happen if + // the DemuxerStream gets flushed and doesn't have any more data to return. typedef base::Callback<void(scoped_refptr<VideoFrame>)> ReadCB; virtual void Read(const ReadCB& callback) = 0; @@ -183,7 +185,10 @@ class MEDIA_EXPORT AudioDecoder : public Filter { // Implementations guarantee that the callback will not be called from within // this method. // - // Sample buffers will be non-NULL yet may be end of stream buffers. + // Non-NULL sample buffer pointers will contain decoded audio data or may + // indicate the end of the stream. A NULL buffer pointer indicates an aborted + // Read(). This can happen if the DemuxerStream gets flushed and doesn't have + // any more data to return. typedef base::Callback<void(scoped_refptr<Buffer>)> ReadCB; virtual void Read(const ReadCB& callback) = 0; diff --git a/media/filters/audio_renderer_base.cc b/media/filters/audio_renderer_base.cc index b80536a..23c17a9 100644 --- a/media/filters/audio_renderer_base.cc +++ b/media/filters/audio_renderer_base.cc @@ -67,6 +67,7 @@ void AudioRendererBase::Seek(base::TimeDelta time, const FilterStatusCB& cb) { base::AutoLock auto_lock(lock_); DCHECK_EQ(kPaused, state_); DCHECK(!pending_read_) << "Pending read must complete before seeking"; + DCHECK(pause_callback_.is_null()); DCHECK(seek_cb_.is_null()); state_ = kSeeking; seek_cb_ = cb; @@ -144,46 +145,46 @@ void AudioRendererBase::DecodedAudioReady(scoped_refptr<Buffer> buffer) { CHECK(pending_read_); pending_read_ = false; - // TODO(scherkus): this happens due to a race, primarily because Stop() is a - // synchronous call when it should be asynchronous and accept a callback. - // Refer to http://crbug.com/16059 - if (state_ == kStopped) { - return; - } - - // Don't enqueue an end-of-stream buffer because it has no data, otherwise - // discard decoded audio data until we reach our desired seek timestamp. - if (buffer->IsEndOfStream()) { + if (buffer && buffer->IsEndOfStream()) { recieved_end_of_stream_ = true; - // Transition to kPlaying if we are currently handling an underflow since no - // more data will be arriving. + // Transition to kPlaying if we are currently handling an underflow since + // no more data will be arriving. if (state_ == kUnderflow || state_ == kRebuffering) state_ = kPlaying; - } else if (state_ == kSeeking && !buffer->IsEndOfStream() && - (buffer->GetTimestamp() + buffer->GetDuration()) < - seek_timestamp_) { - ScheduleRead_Locked(); - } else { - // Note: Calling this may schedule more reads. - algorithm_->EnqueueBuffer(buffer); } - // Check for our preroll complete condition. - if (state_ == kSeeking) { - DCHECK(!seek_cb_.is_null()); - if (algorithm_->IsQueueFull() || recieved_end_of_stream_) { - // Transition into paused whether we have data in |algorithm_| or not. - // FillBuffer() will play silence if there's nothing to fill. + switch (state_) { + case kUninitialized: + NOTREACHED(); + return; + case kPaused: + if (buffer && !buffer->IsEndOfStream()) + algorithm_->EnqueueBuffer(buffer); + DCHECK(!pending_read_); + ResetAndRunCB(&pause_callback_); + return; + case kSeeking: + if (IsBeforeSeekTime(buffer)) { + ScheduleRead_Locked(); + return; + } + if (buffer && !buffer->IsEndOfStream()) { + algorithm_->EnqueueBuffer(buffer); + if (!algorithm_->IsQueueFull()) + return; + } state_ = kPaused; ResetAndRunCB(&seek_cb_, PIPELINE_OK); - } - } else if (state_ == kPaused && !pending_read_) { - // No more pending read! We're now officially "paused". - if (!pause_callback_.is_null()) { - pause_callback_.Run(); - pause_callback_.Reset(); - } + return; + case kPlaying: + case kUnderflow: + case kRebuffering: + if (buffer && !buffer->IsEndOfStream()) + algorithm_->EnqueueBuffer(buffer); + return; + case kStopped: + return; } } @@ -278,7 +279,7 @@ void AudioRendererBase::SignalEndOfStream() { void AudioRendererBase::ScheduleRead_Locked() { lock_.AssertAcquired(); - if (pending_read_) + if (pending_read_ || state_ == kPaused) return; pending_read_ = true; decoder_->Read(read_cb_); @@ -294,4 +295,9 @@ float AudioRendererBase::GetPlaybackRate() { return algorithm_->playback_rate(); } +bool AudioRendererBase::IsBeforeSeekTime(const scoped_refptr<Buffer>& buffer) { + return (state_ == kSeeking) && buffer && !buffer->IsEndOfStream() && + (buffer->GetTimestamp() + buffer->GetDuration()) < seek_timestamp_; +} + } // namespace media diff --git a/media/filters/audio_renderer_base.h b/media/filters/audio_renderer_base.h index f6218c7..4326901 100644 --- a/media/filters/audio_renderer_base.h +++ b/media/filters/audio_renderer_base.h @@ -113,6 +113,11 @@ class MEDIA_EXPORT AudioRendererBase : public AudioRenderer { // Safe to call from any thread. void ScheduleRead_Locked(); + // Returns true if the data in the buffer is all before + // |seek_timestamp_|. This can only return true while + // in the kSeeking state. + bool IsBeforeSeekTime(const scoped_refptr<Buffer>& buffer); + // Audio decoder. scoped_refptr<AudioDecoder> decoder_; @@ -128,7 +133,6 @@ class MEDIA_EXPORT AudioRendererBase : public AudioRenderer { kSeeking, kPlaying, kStopped, - kError, kUnderflow, kRebuffering, }; diff --git a/media/filters/audio_renderer_base_unittest.cc b/media/filters/audio_renderer_base_unittest.cc index e07d524..976743c 100644 --- a/media/filters/audio_renderer_base_unittest.cc +++ b/media/filters/audio_renderer_base_unittest.cc @@ -16,6 +16,8 @@ using ::testing::_; using ::testing::AnyNumber; using ::testing::Invoke; using ::testing::Return; +using ::testing::ReturnPointee; +using ::testing::SaveArg; using ::testing::StrictMock; namespace media { @@ -111,6 +113,18 @@ class AudioRendererBaseTest : public ::testing::Test { renderer_->SetPlaybackRate(1.0f); } + void Seek(base::TimeDelta seek_time) { + next_timestamp_ = seek_time; + + // Seek to trigger prerolling. + EXPECT_CALL(*decoder_, Read(_)); + renderer_->Seek(seek_time, NewSeekCB()); + + // Fill entire buffer to complete prerolling. + EXPECT_CALL(*this, OnSeekComplete(PIPELINE_OK)); + DeliverRemainingAudio(); + } + // Delivers |size| bytes with value kPlayingAudio to |renderer_|. // // There must be a pending read callback. @@ -120,11 +134,22 @@ class AudioRendererBaseTest : public ::testing::Test { buffer->SetDataSize(size); memset(buffer->GetWritableData(), kPlayingAudio, buffer->GetDataSize()); + buffer->SetTimestamp(next_timestamp_); + int64 bps = decoder_->bits_per_channel() * decoder_->samples_per_second(); + buffer->SetDuration(base::TimeDelta::FromMilliseconds(8000 * size / bps)); + next_timestamp_ += buffer->GetDuration(); + AudioDecoder::ReadCB read_cb; std::swap(read_cb, read_cb_); read_cb.Run(buffer); } + void AbortPendingRead() { + AudioDecoder::ReadCB read_cb; + std::swap(read_cb, read_cb_); + read_cb.Run(NULL); + } + // Delivers an end of stream buffer to |renderer_|. // // There must be a pending read callback. @@ -182,6 +207,7 @@ class AudioRendererBaseTest : public ::testing::Test { scoped_refptr<MockAudioDecoder> decoder_; StrictMock<MockFilterHost> host_; AudioDecoder::ReadCB read_cb_; + base::TimeDelta next_timestamp_; private: void SaveReadCallback(const AudioDecoder::ReadCB& callback) { @@ -326,6 +352,9 @@ TEST_F(AudioRendererBaseTest, Underflow_EndOfStream) { EXPECT_CALL(*renderer_, OnRenderEndOfStream()) .WillOnce(Invoke(renderer_.get(), &AudioRendererBase::SignalEndOfStream)); EXPECT_CALL(host_, NotifyEnded()); + + EXPECT_CALL(host_, GetTime()).WillOnce(Return(base::TimeDelta())); + EXPECT_CALL(host_, SetTime(_)); EXPECT_FALSE(ConsumeBufferedData(kDataSize, &muted)); EXPECT_FALSE(muted); } @@ -359,4 +388,39 @@ TEST_F(AudioRendererBaseTest, Underflow_ResumeFromCallback) { EXPECT_FALSE(muted); } +TEST_F(AudioRendererBaseTest, AbortPendingRead_Preroll) { + Initialize(); + + // Seek to trigger prerolling. + EXPECT_CALL(*decoder_, Read(_)); + renderer_->Seek(base::TimeDelta(), NewSeekCB()); + + // Simulate the decoder aborting the pending read. + EXPECT_CALL(*this, OnSeekComplete(PIPELINE_OK)); + AbortPendingRead(); + + // Seek to trigger another preroll and verify it completes + // normally. + Seek(base::TimeDelta::FromSeconds(1)); + + ASSERT_TRUE(read_cb_.is_null()); +} + +TEST_F(AudioRendererBaseTest, AbortPendingRead_Pause) { + Initialize(); + + Preroll(); + Play(); + + // Partially drain internal buffer so we get a pending read. + EXPECT_CALL(*decoder_, Read(_)); + EXPECT_TRUE(ConsumeBufferedData(bytes_buffered() / 2, NULL)); + + renderer_->Pause(NewExpectedClosure()); + + AbortPendingRead(); + + Seek(base::TimeDelta::FromSeconds(1)); +} + } // namespace media diff --git a/media/filters/chunk_demuxer.cc b/media/filters/chunk_demuxer.cc index 0c27733..95cd9e7 100644 --- a/media/filters/chunk_demuxer.cc +++ b/media/filters/chunk_demuxer.cc @@ -24,12 +24,14 @@ class ChunkDemuxerStream : public DemuxerStream { public: typedef std::deque<scoped_refptr<Buffer> > BufferQueue; typedef std::deque<ReadCallback> ReadCBQueue; + typedef std::deque<base::Closure> ClosureQueue; explicit ChunkDemuxerStream(const AudioDecoderConfig& audio_config); explicit ChunkDemuxerStream(const VideoDecoderConfig& video_config); virtual ~ChunkDemuxerStream(); void Flush(); + void Seek(base::TimeDelta time); // Checks if it is ok to add the |buffers| to the stream. bool CanAddBuffers(const BufferQueue& buffers) const; @@ -40,22 +42,41 @@ class ChunkDemuxerStream : public DemuxerStream { bool GetLastBufferTimestamp(base::TimeDelta* timestamp) const; // DemuxerStream methods. - virtual void Read(const ReadCallback& read_callback); - virtual Type type(); - virtual void EnableBitstreamConverter(); - virtual const AudioDecoderConfig& audio_decoder_config(); - virtual const VideoDecoderConfig& video_decoder_config(); + virtual void Read(const ReadCallback& read_callback) OVERRIDE; + virtual Type type() OVERRIDE; + virtual void EnableBitstreamConverter() OVERRIDE; + virtual const AudioDecoderConfig& audio_decoder_config() OVERRIDE; + virtual const VideoDecoderConfig& video_decoder_config() OVERRIDE; private: + enum State { + RETURNING_DATA_FOR_READS, + WAITING_FOR_SEEK, + RECEIVED_EOS_WHILE_WAITING_FOR_SEEK, // EOS = End of stream. + RECEIVED_EOS, + RETURNING_EOS_FOR_READS, + SHUTDOWN, + }; + + // Assigns |state_| to |state| + void ChangeState_Locked(State state); + + // Adds the callback to |read_cbs_| so it can be called later when we + // have data. + void DeferRead_Locked(const ReadCallback& read_cb); + + // Creates closures that bind ReadCallbacks in |read_cbs_| to data in + // |buffers_| and pops the callbacks & buffers from the respecive queues. + void CreateReadDoneClosures_Locked(ClosureQueue* closures); + Type type_; AudioDecoderConfig audio_config_; VideoDecoderConfig video_config_; mutable base::Lock lock_; + State state_; ReadCBQueue read_cbs_; BufferQueue buffers_; - bool shutdown_called_; - bool received_end_of_stream_; // Keeps track of the timestamp of the last buffer we have // added to |buffers_|. This is used to enforce buffers with strictly @@ -67,8 +88,7 @@ class ChunkDemuxerStream : public DemuxerStream { ChunkDemuxerStream::ChunkDemuxerStream(const AudioDecoderConfig& audio_config) : type_(AUDIO), - shutdown_called_(false), - received_end_of_stream_(false), + state_(RETURNING_DATA_FOR_READS), last_buffer_timestamp_(kNoTimestamp()) { audio_config_.CopyFrom(audio_config); } @@ -76,8 +96,7 @@ ChunkDemuxerStream::ChunkDemuxerStream(const AudioDecoderConfig& audio_config) ChunkDemuxerStream::ChunkDemuxerStream(const VideoDecoderConfig& video_config) : type_(VIDEO), - shutdown_called_(false), - received_end_of_stream_(false), + state_(RETURNING_DATA_FOR_READS), last_buffer_timestamp_(kNoTimestamp()) { video_config_.CopyFrom(video_config); } @@ -86,10 +105,34 @@ ChunkDemuxerStream::~ChunkDemuxerStream() {} void ChunkDemuxerStream::Flush() { DVLOG(1) << "Flush()"; + ReadCBQueue read_cbs; + { + base::AutoLock auto_lock(lock_); + buffers_.clear(); + ChangeState_Locked(WAITING_FOR_SEEK); + last_buffer_timestamp_ = kNoTimestamp(); + + std::swap(read_cbs_, read_cbs); + } + + for (ReadCBQueue::iterator it = read_cbs.begin(); it != read_cbs.end(); ++it) + it->Run(scoped_refptr<Buffer>()); +} + +void ChunkDemuxerStream::Seek(base::TimeDelta time) { base::AutoLock auto_lock(lock_); - buffers_.clear(); - received_end_of_stream_ = false; - last_buffer_timestamp_ = kNoTimestamp(); + + DCHECK(read_cbs_.empty()); + + if (state_ == WAITING_FOR_SEEK) { + ChangeState_Locked(RETURNING_DATA_FOR_READS); + return; + } + + if (state_ == RECEIVED_EOS_WHILE_WAITING_FOR_SEEK) { + ChangeState_Locked(RECEIVED_EOS); + return; + } } bool ChunkDemuxerStream::CanAddBuffers(const BufferQueue& buffers) const { @@ -109,7 +152,7 @@ void ChunkDemuxerStream::AddBuffers(const BufferQueue& buffers) { if (buffers.empty()) return; - std::deque<base::Closure> callbacks; + ClosureQueue closures; { base::AutoLock auto_lock(lock_); @@ -117,17 +160,15 @@ void ChunkDemuxerStream::AddBuffers(const BufferQueue& buffers) { itr != buffers.end(); itr++) { // Make sure we aren't trying to add a buffer after we have received and // "end of stream" buffer. - DCHECK(!received_end_of_stream_); + DCHECK_NE(state_, RECEIVED_EOS_WHILE_WAITING_FOR_SEEK); + DCHECK_NE(state_, RECEIVED_EOS); + DCHECK_NE(state_, RETURNING_EOS_FOR_READS); if ((*itr)->IsEndOfStream()) { - received_end_of_stream_ = true; - - // Push enough EOS buffers to satisfy outstanding Read() requests. - if (read_cbs_.size() > buffers_.size()) { - size_t pending_read_without_data = read_cbs_.size() - buffers_.size(); - for (size_t i = 0; i <= pending_read_without_data; ++i) { - buffers_.push_back(*itr); - } + if (state_ == WAITING_FOR_SEEK) { + ChangeState_Locked(RECEIVED_EOS_WHILE_WAITING_FOR_SEEK); + } else { + ChangeState_Locked(RECEIVED_EOS); } } else { base::TimeDelta current_ts = (*itr)->GetTimestamp(); @@ -141,38 +182,27 @@ void ChunkDemuxerStream::AddBuffers(const BufferQueue& buffers) { } } - while (!buffers_.empty() && !read_cbs_.empty()) { - callbacks.push_back(base::Bind(read_cbs_.front(), buffers_.front())); - buffers_.pop_front(); - read_cbs_.pop_front(); - } + CreateReadDoneClosures_Locked(&closures); } - while (!callbacks.empty()) { - callbacks.front().Run(); - callbacks.pop_front(); - } + for (ClosureQueue::iterator it = closures.begin(); it != closures.end(); ++it) + it->Run(); } void ChunkDemuxerStream::Shutdown() { - std::deque<ReadCallback> callbacks; + ReadCBQueue read_cbs; { base::AutoLock auto_lock(lock_); - shutdown_called_ = true; + ChangeState_Locked(SHUTDOWN); - // Collect all the pending Read() callbacks. - while (!read_cbs_.empty()) { - callbacks.push_back(read_cbs_.front()); - read_cbs_.pop_front(); - } + std::swap(read_cbs_, read_cbs); + buffers_.clear(); } // Pass end of stream buffers to all callbacks to signal that no more data // will be sent. - while (!callbacks.empty()) { - callbacks.front().Run(CreateEOSBuffer()); - callbacks.pop_front(); - } + for (ReadCBQueue::iterator it = read_cbs.begin(); it != read_cbs.end(); ++it) + it->Run(CreateEOSBuffer()); } bool ChunkDemuxerStream::GetLastBufferTimestamp( @@ -206,33 +236,46 @@ void ChunkDemuxerStream::Read(const ReadCallback& read_callback) { { base::AutoLock auto_lock(lock_); - if (shutdown_called_ || (received_end_of_stream_ && buffers_.empty())) { - buffer = CreateEOSBuffer(); - } else { - if (buffers_.empty()) { - // Wrap & store |read_callback| so that it will - // get called on the current MessageLoop. - read_cbs_.push_back(base::Bind(&RunOnMessageLoop, - read_callback, - MessageLoop::current())); - return; - } + switch(state_) { + case RETURNING_DATA_FOR_READS: + // If we don't have any buffers ready or already have + // pending reads, then defer this read. + if (buffers_.empty() || !read_cbs_.empty()) { + DeferRead_Locked(read_callback); + return; + } - if (!read_cbs_.empty()) { - // Wrap & store |read_callback| so that it will - // get called on the current MessageLoop. - read_cbs_.push_back(base::Bind(&RunOnMessageLoop, - read_callback, - MessageLoop::current())); - return; - } + buffer = buffers_.front(); + buffers_.pop_front(); + break; + + case WAITING_FOR_SEEK: + case RECEIVED_EOS_WHILE_WAITING_FOR_SEEK: + // Null buffers should be returned in this state since we are waiting + // for a seek. Any buffers in |buffers_| should NOT be returned because + // they are associated with the seek. + DCHECK(read_cbs_.empty()); + break; + case RECEIVED_EOS: + DCHECK(read_cbs_.empty()); + + if (buffers_.empty()) { + ChangeState_Locked(RETURNING_EOS_FOR_READS); + buffer = CreateEOSBuffer(); + } else { + buffer = buffers_.front(); + buffers_.pop_front(); + } + break; - buffer = buffers_.front(); - buffers_.pop_front(); + case RETURNING_EOS_FOR_READS: + case SHUTDOWN: + DCHECK(buffers_.empty()); + DCHECK(read_cbs_.empty()); + buffer = CreateEOSBuffer(); } } - DCHECK(buffer.get()); read_callback.Run(buffer); } @@ -250,6 +293,44 @@ const VideoDecoderConfig& ChunkDemuxerStream::video_decoder_config() { return video_config_; } +void ChunkDemuxerStream::ChangeState_Locked(State state) { + lock_.AssertAcquired(); + state_ = state; +} + +void ChunkDemuxerStream::DeferRead_Locked(const ReadCallback& read_cb) { + lock_.AssertAcquired(); + // Wrap & store |read_callback| so that it will + // get called on the current MessageLoop. + read_cbs_.push_back(base::Bind(&RunOnMessageLoop, read_cb, + MessageLoop::current())); +} + +void ChunkDemuxerStream::CreateReadDoneClosures_Locked(ClosureQueue* closures) { + lock_.AssertAcquired(); + + if (state_ != RETURNING_DATA_FOR_READS && state_ != RECEIVED_EOS) + return; + + while (!buffers_.empty() && !read_cbs_.empty()) { + closures->push_back(base::Bind(read_cbs_.front(), buffers_.front())); + buffers_.pop_front(); + read_cbs_.pop_front(); + } + + if (state_ != RECEIVED_EOS || !buffers_.empty() || read_cbs_.empty()) + return; + + // Push enough EOS buffers to satisfy outstanding Read() requests. + scoped_refptr<Buffer> end_of_stream_buffer = CreateEOSBuffer(); + while (!read_cbs_.empty()) { + closures->push_back(base::Bind(read_cbs_.front(), end_of_stream_buffer)); + read_cbs_.pop_front(); + } + + ChangeState_Locked(RETURNING_EOS_FOR_READS); +} + ChunkDemuxer::ChunkDemuxer(ChunkDemuxerClient* client) : state_(WAITING_FOR_INIT), client_(client), @@ -306,6 +387,12 @@ void ChunkDemuxer::Seek(base::TimeDelta time, const PipelineStatusCB& cb) { base::AutoLock auto_lock(lock_); if (state_ == INITIALIZED || state_ == ENDED) { + if (audio_) + audio_->Seek(time); + + if (video_) + video_->Seek(time); + if (seek_waits_for_data_) { DVLOG(1) << "Seek() : waiting for more data to arrive."; seek_cb_ = cb; diff --git a/media/filters/ffmpeg_audio_decoder.cc b/media/filters/ffmpeg_audio_decoder.cc index 71b32e5..a3ad564 100644 --- a/media/filters/ffmpeg_audio_decoder.cc +++ b/media/filters/ffmpeg_audio_decoder.cc @@ -169,6 +169,12 @@ void FFmpegAudioDecoder::DoDecodeBuffer(const scoped_refptr<Buffer>& input) { DCHECK_EQ(MessageLoop::current(), message_loop_); DCHECK(!read_cb_.is_null()); + if (!input) { + // DemuxeStream::Read() was aborted so we abort the decoder's pending read. + DeliverSamples(NULL); + return; + } + // FFmpeg tends to seek Ogg audio streams in the middle of nowhere, giving us // a whole bunch of AV_NOPTS_VALUE packets. Discard them until we find // something valid. Refer to http://crbug.com/49709 diff --git a/media/filters/ffmpeg_audio_decoder_unittest.cc b/media/filters/ffmpeg_audio_decoder_unittest.cc index 12e2ce6..0efe91e 100644 --- a/media/filters/ffmpeg_audio_decoder_unittest.cc +++ b/media/filters/ffmpeg_audio_decoder_unittest.cc @@ -180,4 +180,20 @@ TEST_F(FFmpegAudioDecoderTest, ProduceAudioSamples) { Stop(); } +TEST_F(FFmpegAudioDecoderTest, ReadAbort) { + Initialize(); + + encoded_audio_.clear(); + encoded_audio_.push_back(NULL); + + EXPECT_CALL(*demuxer_, Read(_)) + .WillOnce(InvokeReadPacket(this)); + Read(); + + EXPECT_EQ(decoded_audio_.size(), 1u); + EXPECT_TRUE(decoded_audio_[0].get() == NULL); + + Stop(); +} + } // namespace media diff --git a/media/filters/ffmpeg_video_decoder.cc b/media/filters/ffmpeg_video_decoder.cc index 048cbe5..7ca0d1d 100644 --- a/media/filters/ffmpeg_video_decoder.cc +++ b/media/filters/ffmpeg_video_decoder.cc @@ -221,6 +221,11 @@ void FFmpegVideoDecoder::DoDecodeBuffer(const scoped_refptr<Buffer>& buffer) { DCHECK_NE(state_, kDecodeFinished); DCHECK(!read_cb_.is_null()); + if (!buffer) { + DeliverFrame(NULL); + return; + } + // During decode, because reads are issued asynchronously, it is possible to // receive multiple end of stream buffers since each read is acked. When the // first end of stream buffer is read, FFmpeg may still have frames queued diff --git a/media/filters/ffmpeg_video_decoder_unittest.cc b/media/filters/ffmpeg_video_decoder_unittest.cc index 2f285c3..83a754e 100644 --- a/media/filters/ffmpeg_video_decoder_unittest.cc +++ b/media/filters/ffmpeg_video_decoder_unittest.cc @@ -438,4 +438,16 @@ TEST_F(FFmpegVideoDecoderTest, Stop_EndOfStream) { Stop(); } +// Test aborted read on the demuxer stream. +TEST_F(FFmpegVideoDecoderTest, AbortPendingRead) { + Initialize(); + + EXPECT_CALL(*demuxer_, Read(_)) + .WillOnce(ReturnBuffer(scoped_refptr<Buffer>())); + + scoped_refptr<VideoFrame> video_frame; + Read(&video_frame); + EXPECT_FALSE(video_frame); +} + } // namespace media diff --git a/media/filters/gpu_video_decoder.cc b/media/filters/gpu_video_decoder.cc index 193ccd8..c839d04 100644 --- a/media/filters/gpu_video_decoder.cc +++ b/media/filters/gpu_video_decoder.cc @@ -224,6 +224,16 @@ void GpuVideoDecoder::RequestBufferDecode(const scoped_refptr<Buffer>& buffer) { } demuxer_read_in_progress_ = false; + if (!buffer) { + if (pending_read_cb_.is_null()) + return; + + gvd_loop_proxy_->PostTask(FROM_HERE, base::Bind( + pending_read_cb_, scoped_refptr<VideoFrame>())); + pending_read_cb_.Reset(); + return; + } + if (!vda_) { EnqueueFrameAndTriggerFrameDelivery(VideoFrame::CreateEmptyFrame()); return; diff --git a/media/filters/pipeline_integration_test.cc b/media/filters/pipeline_integration_test.cc index 37d92c1..dbdbbc8 100644 --- a/media/filters/pipeline_integration_test.cc +++ b/media/filters/pipeline_integration_test.cc @@ -9,6 +9,9 @@ #include "media/base/message_loop_factory_impl.h" #include "media/base/pipeline.h" #include "media/base/test_data_util.h" +#include "media/filters/chunk_demuxer.h" +#include "media/filters/chunk_demuxer_client.h" +#include "media/filters/chunk_demuxer_factory.h" #include "media/filters/ffmpeg_audio_decoder.h" #include "media/filters/ffmpeg_demuxer_factory.h" #include "media/filters/ffmpeg_video_decoder.h" @@ -22,6 +25,71 @@ using ::testing::AnyNumber; namespace media { +// Helper class that emulates calls made on the ChunkDemuxer by the +// Media Source API. +class MockMediaSource : public ChunkDemuxerClient { + public: + MockMediaSource(const std::string& filename, int initial_append_size) + : url_(GetTestDataURL(filename)), + current_position_(0), + initial_append_size_(initial_append_size) { + ReadTestDataFile(filename, &file_data_, &file_data_size_); + + DCHECK_GT(initial_append_size_, 0); + DCHECK_LE(initial_append_size_, file_data_size_); + } + + virtual ~MockMediaSource() {} + + const std::string& url() { return url_; } + + void Seek(int new_position, int seek_append_size) { + chunk_demuxer_->FlushData(); + + DCHECK_GE(new_position, 0); + DCHECK_LT(new_position, file_data_size_); + current_position_ = new_position; + + AppendData(seek_append_size); + } + + void AppendData(int size) { + DCHECK(chunk_demuxer_.get()); + DCHECK_LT(current_position_, file_data_size_); + DCHECK_LE(current_position_ + size, file_data_size_); + chunk_demuxer_->AppendData(file_data_.get() + current_position_, size); + current_position_ += size; + } + + void EndOfStream() { + chunk_demuxer_->EndOfStream(PIPELINE_OK); + } + + void Abort() { + if (!chunk_demuxer_.get()) + return; + chunk_demuxer_->Shutdown(); + } + + // ChunkDemuxerClient methods. + virtual void DemuxerOpened(ChunkDemuxer* demuxer) { + chunk_demuxer_ = demuxer; + AppendData(initial_append_size_); + } + + virtual void DemuxerClosed() { + chunk_demuxer_ = NULL; + } + + private: + std::string url_; + scoped_array<uint8> file_data_; + int file_data_size_; + int current_position_; + int initial_append_size_; + scoped_refptr<ChunkDemuxer> chunk_demuxer_; +}; + // Integration tests for Pipeline. Real demuxers, real decoders, and // base renderer implementations are used to verify pipeline functionality. The // renderers used in these tests rely heavily on the AudioRendererBase & @@ -38,7 +106,7 @@ class PipelineIntegrationTest : public testing::Test { pipeline_(new Pipeline(&message_loop_, new MediaLog())), ended_(false) { EXPECT_CALL(*this, OnVideoRendererPaint()).Times(AnyNumber()); - EXPECT_CALL(*this, OnSetOpaque(true)); + EXPECT_CALL(*this, OnSetOpaque(true)).Times(AnyNumber()); } virtual ~PipelineIntegrationTest() { @@ -50,7 +118,7 @@ class PipelineIntegrationTest : public testing::Test { void OnStatusCallback(PipelineStatus expected_status, PipelineStatus status) { - DCHECK_EQ(status, expected_status); + EXPECT_EQ(status, expected_status); message_loop_.PostTask(FROM_HERE, MessageLoop::QuitClosure()); } @@ -138,11 +206,20 @@ class PipelineIntegrationTest : public testing::Test { scoped_ptr<FilterCollection> CreateFilterCollection(const std::string& url) { scoped_refptr<FileDataSource> data_source = new FileDataSource(); CHECK_EQ(PIPELINE_OK, data_source->Initialize(url)); - - scoped_ptr<FilterCollection> collection( - new FilterCollection()); - collection->SetDemuxerFactory(scoped_ptr<DemuxerFactory>( + return CreateFilterCollection(scoped_ptr<DemuxerFactory>( new FFmpegDemuxerFactory(data_source, &message_loop_))); + } + + scoped_ptr<FilterCollection> CreateFilterCollection( + ChunkDemuxerClient* client) { + return CreateFilterCollection(scoped_ptr<DemuxerFactory>( + new ChunkDemuxerFactory(client))); + } + + scoped_ptr<FilterCollection> CreateFilterCollection( + scoped_ptr<DemuxerFactory> demuxer_factory) { + scoped_ptr<FilterCollection> collection(new FilterCollection()); + collection->SetDemuxerFactory(demuxer_factory.Pass()); collection->AddAudioDecoder(new FFmpegAudioDecoder( message_loop_factory_->GetMessageLoop("AudioDecoderThread"))); collection->AddVideoDecoder(new FFmpegVideoDecoder( @@ -156,6 +233,38 @@ class PipelineIntegrationTest : public testing::Test { return collection.Pass(); } + // 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. + void TestSeekDuringRead(const std::string& filename, + int initial_append_size, + base::TimeDelta start_seek_time, + base::TimeDelta seek_time, + int seek_file_position, + int seek_append_size) { + MockMediaSource source(filename, initial_append_size); + + pipeline_->Start(CreateFilterCollection(&source), source.url(), + base::Bind(&PipelineIntegrationTest::OnEnded, + base::Unretained(this)), + base::Bind(&PipelineIntegrationTest::OnError, + base::Unretained(this)), + NetworkEventCB(), + QuitOnStatusCB(PIPELINE_OK)); + message_loop_.Run(); + + Play(); + WaitUntilCurrentTimeIsAfter(start_seek_time); + + source.Seek(seek_file_position, seek_append_size); + Seek(seek_time); + + source.EndOfStream(); + + source.Abort(); + Stop(); + } + protected: MessageLoop message_loop_; scoped_ptr<MessageLoopFactory> message_loop_factory_; @@ -199,7 +308,8 @@ TEST_F(PipelineIntegrationTest, SeekWhilePaused) { WaitUntilOnEnded(); } -TEST_F(PipelineIntegrationTest, SeekWhilePlaying) { +// TODO(acolwell): Fix flakiness http://crbug.com/109875 +TEST_F(PipelineIntegrationTest, FLAKY_SeekWhilePlaying) { Start(GetTestDataURL("bear-320x240.webm"), PIPELINE_OK); base::TimeDelta duration(pipeline_->GetMediaDuration()); @@ -218,4 +328,20 @@ TEST_F(PipelineIntegrationTest, SeekWhilePlaying) { WaitUntilOnEnded(); } +// Verify audio decoder & renderer can handle aborted demuxer reads. +TEST_F(PipelineIntegrationTest, ChunkDemuxerAbortRead_AudioOnly) { + TestSeekDuringRead("bear-320x240-audio-only.webm", 8192, + base::TimeDelta::FromMilliseconds(477), + base::TimeDelta::FromMilliseconds(617), + 0x10CA, 19730); +} + +// Verify video decoder & renderer can handle aborted demuxer reads. +TEST_F(PipelineIntegrationTest, ChunkDemuxerAbortRead_VideoOnly) { + TestSeekDuringRead("bear-320x240-video-only.webm", 32768, + base::TimeDelta::FromMilliseconds(200), + base::TimeDelta::FromMilliseconds(1668), + 0x1C896, 65536); +} + } // namespace media diff --git a/media/filters/video_renderer_base.cc b/media/filters/video_renderer_base.cc index df7350c..69a6dac 100644 --- a/media/filters/video_renderer_base.cc +++ b/media/filters/video_renderer_base.cc @@ -336,8 +336,6 @@ void VideoRendererBase::PutCurrentFrame(scoped_refptr<VideoFrame> frame) { } void VideoRendererBase::FrameReady(scoped_refptr<VideoFrame> frame) { - DCHECK(frame); - base::AutoLock auto_lock(lock_); DCHECK_NE(state_, kUninitialized); DCHECK_NE(state_, kStopped); @@ -352,6 +350,18 @@ void VideoRendererBase::FrameReady(scoped_refptr<VideoFrame> frame) { return; } + if (!frame) { + if (state_ != kSeeking) + return; + + // Abort seek early for a NULL frame because we won't get more frames. + // A new seek will be requested after this one completes so there is no + // point trying to collect more frames. + state_ = kPrerolled; + ResetAndRunCB(&seek_cb_, PIPELINE_OK); + return; + } + // Discard frames until we reach our desired seek timestamp. if (state_ == kSeeking && !frame->IsEndOfStream() && (frame->GetTimestamp() + frame->GetDuration()) <= seek_timestamp_) { diff --git a/media/filters/video_renderer_base_unittest.cc b/media/filters/video_renderer_base_unittest.cc index 88a1b1a..7ff3f41 100644 --- a/media/filters/video_renderer_base_unittest.cc +++ b/media/filters/video_renderer_base_unittest.cc @@ -48,10 +48,11 @@ class VideoRendererBaseTest : public ::testing::Test { event_(false, false), timeout_(base::TimeDelta::FromMilliseconds( TestTimeouts::action_timeout_ms())), - seeking_(false) { + seeking_(false), + paint_cv_(&lock_), + paint_was_called_(false) { renderer_ = new VideoRendererBase( - base::Bind(&VideoRendererBaseTest::PaintCBWasCalled, - base::Unretained(this)), + base::Bind(&VideoRendererBaseTest::Paint, base::Unretained(this)), base::Bind(&VideoRendererBaseTest::SetOpaqueCBWasCalled, base::Unretained(this))); renderer_->set_host(&host_); @@ -74,8 +75,6 @@ class VideoRendererBaseTest : public ::testing::Test { } } - MOCK_METHOD0(PaintCBWasCalled, void()); - MOCK_CONST_METHOD1(SetOpaqueCBWasCalled, void(bool)); void Initialize() { @@ -178,6 +177,18 @@ class VideoRendererBaseTest : public ::testing::Test { } } + void AbortRead() { + // Lock+swap to avoid re-entrancy issues. + VideoDecoder::ReadCB read_cb; + { + base::AutoLock l(lock_); + CHECK(!read_cb_.is_null()) << "Can't deliver a frame without a callback"; + std::swap(read_cb, read_cb_); + } + + read_cb.Run(NULL); + } + void ExpectCurrentFrame(bool present) { scoped_refptr<VideoFrame> frame; renderer_->GetCurrentFrame(&frame); @@ -220,10 +231,12 @@ class VideoRendererBaseTest : public ::testing::Test { void RenderFrame(int64 timestamp) { base::AutoLock l(lock_); time_ = base::TimeDelta::FromMicroseconds(timestamp); + paint_was_called_ = false; if (read_cb_.is_null()) { cv_.TimedWait(timeout_); CHECK(!read_cb_.is_null()) << "Timed out waiting for read to occur."; } + WaitForPaint_Locked(); } // Advances clock to |timestamp| (which should be the timestamp of the last @@ -241,6 +254,11 @@ class VideoRendererBaseTest : public ::testing::Test { base::WaitableEvent* event() { return &event_; } const base::TimeDelta& timeout() { return timeout_; } + void VerifyNotSeeking() { + base::AutoLock l(lock_); + ASSERT_FALSE(seeking_); + } + protected: StatisticsCallback NewStatisticsCallback() { return base::Bind(&MockStatisticsCallback::OnStatistics, @@ -280,13 +298,12 @@ class VideoRendererBaseTest : public ::testing::Test { } void FinishSeeking(int64 timestamp) { - EXPECT_CALL(*this, PaintCBWasCalled()); - EXPECT_TRUE(seeking_); - // Satisfy the read requests. The callback must be executed in order // to exit the loop since VideoRendererBase can read a few extra frames // after |timestamp| in order to preroll. base::AutoLock l(lock_); + EXPECT_TRUE(seeking_); + paint_was_called_ = false; int i = 0; while (seeking_) { if (!read_cb_.is_null()) { @@ -310,6 +327,21 @@ class VideoRendererBaseTest : public ::testing::Test { } EXPECT_TRUE(read_cb_.is_null()); + WaitForPaint_Locked(); + } + + void Paint() { + base::AutoLock l(lock_); + paint_was_called_ = true; + paint_cv_.Signal(); + } + + void WaitForPaint_Locked() { + lock_.AssertAcquired(); + if (paint_was_called_) + return; + paint_cv_.TimedWait(timeout_); + EXPECT_TRUE(paint_was_called_); } base::Lock lock_; @@ -322,6 +354,10 @@ class VideoRendererBaseTest : public ::testing::Test { VideoDecoder::ReadCB read_cb_; base::TimeDelta time_; + // Used in conjunction with |lock_| to wait for Paint() calls. + base::ConditionVariable paint_cv_; + bool paint_was_called_; + DISALLOW_COPY_AND_ASSIGN(VideoRendererBaseTest); }; @@ -342,13 +378,8 @@ TEST_F(VideoRendererBaseTest, EndOfStream) { Play(); // Finish rendering up to the next-to-last frame. - // - // Put the gmock expectation here to avoid racing with the rendering thread. - EXPECT_CALL(*this, PaintCBWasCalled()) - .Times(limits::kMaxVideoFrames - 1); - for (int i = 1; i < limits::kMaxVideoFrames; ++i) { + for (int i = 1; i < limits::kMaxVideoFrames; ++i) RenderFrame(kFrameDuration * i); - } // Finish rendering the last frame, we should NOT get a new frame but instead // get notified of end of stream. @@ -475,4 +506,44 @@ TEST_F(VideoRendererBaseTest, Shutdown_DuringPaint) { Stop(); } +TEST_F(VideoRendererBaseTest, AbortPendingRead_Playing) { + Initialize(); + Play(); + + // Render a frame to trigger a Read(). + RenderFrame(kFrameDuration); + + AbortRead(); + + Pause(); + Flush(); + Seek(kFrameDuration * 6); + ExpectCurrentTimestamp(kFrameDuration * 6); + Shutdown(); +} + +TEST_F(VideoRendererBaseTest, AbortPendingRead_Flush) { + Initialize(); + Play(); + + // Render a frame to trigger a Read(). + RenderFrame(kFrameDuration); + + Pause(); + renderer_->Flush(NewWaitableClosure()); + AbortRead(); + WaitForClosure(); + Shutdown(); +} + +TEST_F(VideoRendererBaseTest, AbortPendingRead_Seek) { + Initialize(); + Pause(); + Flush(); + StartSeeking(kFrameDuration * 6); + AbortRead(); + VerifyNotSeeking(); + Shutdown(); +} + } // namespace media diff --git a/media/webm/webm_cluster_parser.cc b/media/webm/webm_cluster_parser.cc index 8151ec6..92f1dc7 100644 --- a/media/webm/webm_cluster_parser.cc +++ b/media/webm/webm_cluster_parser.cc @@ -6,13 +6,17 @@ #include "base/logging.h" #include "media/base/data_buffer.h" +#include "media/ffmpeg/ffmpeg_common.h" #include "media/webm/webm_constants.h" namespace media { static Buffer* CreateBuffer(const uint8* data, size_t size) { - scoped_array<uint8> buf(new uint8[size]); + // Why FF_INPUT_BUFFER_PADDING_SIZE? FFmpeg assumes all input buffers are + // padded with this value. + scoped_array<uint8> buf(new uint8[size + FF_INPUT_BUFFER_PADDING_SIZE]); memcpy(buf.get(), data, size); + memset(buf.get() + size, 0, FF_INPUT_BUFFER_PADDING_SIZE); return new DataBuffer(buf.Pass(), size); } @@ -129,7 +133,7 @@ bool WebMClusterParser::OnSimpleBlock(int track_num, int timecode, if (!queue->empty() && buffer->GetTimestamp() == queue->back()->GetTimestamp()) { DVLOG(1) << "Got SimpleBlock timecode is not strictly monotonically " - << "increasing for track " << track_num; + << "increasing for track " << track_num; return false; } |