// Copyright 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/renderers/video_renderer_impl.h" #include #include "base/bind.h" #include "base/callback.h" #include "base/callback_helpers.h" #include "base/location.h" #include "base/metrics/histogram_macros.h" #include "base/single_thread_task_runner.h" #include "base/strings/string_util.h" #include "base/time/default_tick_clock.h" #include "base/trace_event/trace_event.h" #include "media/base/bind_to_current_loop.h" #include "media/base/limits.h" #include "media/base/media_log.h" #include "media/base/media_switches.h" #include "media/base/pipeline_status.h" #include "media/base/renderer_client.h" #include "media/base/video_frame.h" #include "media/renderers/gpu_video_accelerator_factories.h" #include "media/video/gpu_memory_buffer_video_frame_pool.h" namespace media { VideoRendererImpl::VideoRendererImpl( const scoped_refptr& media_task_runner, const scoped_refptr& worker_task_runner, VideoRendererSink* sink, ScopedVector decoders, bool drop_frames, GpuVideoAcceleratorFactories* gpu_factories, const scoped_refptr& media_log) : task_runner_(media_task_runner), sink_(sink), sink_started_(false), client_(nullptr), video_frame_stream_(new VideoFrameStream(media_task_runner, std::move(decoders), media_log)), gpu_memory_buffer_pool_(nullptr), media_log_(media_log), low_delay_(false), received_end_of_stream_(false), rendered_end_of_stream_(false), state_(kUninitialized), pending_read_(false), drop_frames_(drop_frames), buffering_state_(BUFFERING_HAVE_NOTHING), frames_decoded_(0), frames_dropped_(0), tick_clock_(new base::DefaultTickClock()), was_background_rendering_(false), time_progressing_(false), last_video_memory_usage_(0), have_renderered_frames_(false), last_frame_opaque_(false), painted_first_frame_(false), max_buffered_frames_(limits::kMaxVideoFrames), weak_factory_(this), frame_callback_weak_factory_(this) { if (gpu_factories && gpu_factories->ShouldUseGpuMemoryBuffersForVideoFrames()) { gpu_memory_buffer_pool_.reset(new GpuMemoryBufferVideoFramePool( media_task_runner, worker_task_runner, gpu_factories)); } } VideoRendererImpl::~VideoRendererImpl() { DCHECK(task_runner_->BelongsToCurrentThread()); if (!init_cb_.is_null()) base::ResetAndReturn(&init_cb_).Run(PIPELINE_ERROR_ABORT); if (!flush_cb_.is_null()) base::ResetAndReturn(&flush_cb_).Run(); if (sink_started_) StopSink(); } void VideoRendererImpl::Flush(const base::Closure& callback) { DVLOG(1) << __func__; DCHECK(task_runner_->BelongsToCurrentThread()); if (sink_started_) StopSink(); base::AutoLock auto_lock(lock_); DCHECK_EQ(state_, kPlaying); flush_cb_ = callback; state_ = kFlushing; if (buffering_state_ != BUFFERING_HAVE_NOTHING) { buffering_state_ = BUFFERING_HAVE_NOTHING; task_runner_->PostTask( FROM_HERE, base::Bind(&VideoRendererImpl::OnBufferingStateChange, weak_factory_.GetWeakPtr(), buffering_state_)); } received_end_of_stream_ = false; rendered_end_of_stream_ = false; // Reset |video_frame_stream_| and drop any pending read callbacks from it. pending_read_ = false; frame_callback_weak_factory_.InvalidateWeakPtrs(); video_frame_stream_->Reset( base::Bind(&VideoRendererImpl::OnVideoFrameStreamResetDone, weak_factory_.GetWeakPtr())); // To avoid unnecessary work by VDAs, only delete queued frames after // resetting |video_frame_stream_|. If this is done in the opposite order VDAs // will get a bunch of ReusePictureBuffer() calls before the Reset(), which // they may use to output more frames that won't be used. algorithm_->Reset(); painted_first_frame_ = false; // Reset preroll capacity so seek time is not penalized. // TODO(dalecurtis): Not sure if this is the right decision, but it's what we // do for audio, so carry over that behavior for now. max_buffered_frames_ = limits::kMaxVideoFrames; } void VideoRendererImpl::StartPlayingFrom(base::TimeDelta timestamp) { DVLOG(1) << __func__ << "(" << timestamp.InMicroseconds() << ")"; DCHECK(task_runner_->BelongsToCurrentThread()); base::AutoLock auto_lock(lock_); DCHECK_EQ(state_, kFlushed); DCHECK(!pending_read_); DCHECK_EQ(buffering_state_, BUFFERING_HAVE_NOTHING); state_ = kPlaying; start_timestamp_ = timestamp; painted_first_frame_ = false; AttemptRead_Locked(); } void VideoRendererImpl::Initialize( DemuxerStream* stream, CdmContext* cdm_context, RendererClient* client, const TimeSource::WallClockTimeCB& wall_clock_time_cb, const PipelineStatusCB& init_cb) { DCHECK(task_runner_->BelongsToCurrentThread()); base::AutoLock auto_lock(lock_); DCHECK(stream); DCHECK_EQ(stream->type(), DemuxerStream::VIDEO); DCHECK(!init_cb.is_null()); DCHECK(!wall_clock_time_cb.is_null()); DCHECK_EQ(kUninitialized, state_); DCHECK(!was_background_rendering_); DCHECK(!time_progressing_); DCHECK(!have_renderered_frames_); low_delay_ = (stream->liveness() == DemuxerStream::LIVENESS_LIVE); UMA_HISTOGRAM_BOOLEAN("Media.VideoRenderer.LowDelay", low_delay_); if (low_delay_) MEDIA_LOG(DEBUG, media_log_) << "Video rendering in low delay mode."; // Always post |init_cb_| because |this| could be destroyed if initialization // failed. init_cb_ = BindToCurrentLoop(init_cb); client_ = client; wall_clock_time_cb_ = wall_clock_time_cb; state_ = kInitializing; video_frame_stream_->Initialize( stream, base::Bind(&VideoRendererImpl::OnVideoFrameStreamInitialized, weak_factory_.GetWeakPtr()), cdm_context, base::Bind(&VideoRendererImpl::OnStatisticsUpdate, weak_factory_.GetWeakPtr()), base::Bind(&VideoRendererImpl::OnWaitingForDecryptionKey, weak_factory_.GetWeakPtr())); } scoped_refptr VideoRendererImpl::Render( base::TimeTicks deadline_min, base::TimeTicks deadline_max, bool background_rendering) { base::AutoLock auto_lock(lock_); DCHECK_EQ(state_, kPlaying); size_t frames_dropped = 0; scoped_refptr result = algorithm_->Render(deadline_min, deadline_max, &frames_dropped); // Due to how the |algorithm_| holds frames, this should never be null if // we've had a proper startup sequence. DCHECK(result); // Declare HAVE_NOTHING if we reach a state where we can't progress playback // any further. We don't want to do this if we've already done so, reached // end of stream, or have frames available. We also don't want to do this in // background rendering mode, as the frames aren't visible anyways. MaybeFireEndedCallback_Locked(true); if (buffering_state_ == BUFFERING_HAVE_ENOUGH && !received_end_of_stream_ && !algorithm_->effective_frames_queued() && !background_rendering && !was_background_rendering_) { // Do not set |buffering_state_| here as the lock in FrameReady() may be // held already and it fire the state changes in the wrong order. DVLOG(3) << __func__ << " posted TransitionToHaveNothing."; task_runner_->PostTask( FROM_HERE, base::Bind(&VideoRendererImpl::TransitionToHaveNothing, weak_factory_.GetWeakPtr())); } // We don't count dropped frames in the background to avoid skewing the count // and impacting JavaScript visible metrics used by web developers. // // Just after resuming from background rendering, we also don't count the // dropped frames since they are likely just dropped due to being too old. if (!background_rendering && !was_background_rendering_) frames_dropped_ += frames_dropped; UpdateStats_Locked(); was_background_rendering_ = background_rendering; // Always post this task, it will acquire new frames if necessary and since it // happens on another thread, even if we don't have room in the queue now, by // the time it runs (may be delayed up to 50ms for complex decodes!) we might. task_runner_->PostTask( FROM_HERE, base::Bind(&VideoRendererImpl::AttemptReadAndCheckForMetadataChanges, weak_factory_.GetWeakPtr(), result->format(), result->natural_size())); return result; } void VideoRendererImpl::OnFrameDropped() { base::AutoLock auto_lock(lock_); algorithm_->OnLastFrameDropped(); } void VideoRendererImpl::OnVideoFrameStreamInitialized(bool success) { DCHECK(task_runner_->BelongsToCurrentThread()); base::AutoLock auto_lock(lock_); DCHECK_EQ(state_, kInitializing); if (!success) { state_ = kUninitialized; base::ResetAndReturn(&init_cb_).Run(DECODER_ERROR_NOT_SUPPORTED); return; } // We're all good! Consider ourselves flushed because we have not read any // frames yet. state_ = kFlushed; algorithm_.reset(new VideoRendererAlgorithm(wall_clock_time_cb_)); if (!drop_frames_) algorithm_->disable_frame_dropping(); base::ResetAndReturn(&init_cb_).Run(PIPELINE_OK); } void VideoRendererImpl::OnPlaybackError(PipelineStatus error) { DCHECK(task_runner_->BelongsToCurrentThread()); client_->OnError(error); } void VideoRendererImpl::OnPlaybackEnded() { DCHECK(task_runner_->BelongsToCurrentThread()); client_->OnEnded(); } void VideoRendererImpl::OnStatisticsUpdate(const PipelineStatistics& stats) { DCHECK(task_runner_->BelongsToCurrentThread()); client_->OnStatisticsUpdate(stats); } void VideoRendererImpl::OnBufferingStateChange(BufferingState state) { DCHECK(task_runner_->BelongsToCurrentThread()); media_log_->AddEvent(media_log_->CreateBufferingStateChangedEvent( "video_buffering_state", state)); client_->OnBufferingStateChange(state); } void VideoRendererImpl::OnWaitingForDecryptionKey() { DCHECK(task_runner_->BelongsToCurrentThread()); client_->OnWaitingForDecryptionKey(); } void VideoRendererImpl::SetTickClockForTesting( std::unique_ptr tick_clock) { tick_clock_.swap(tick_clock); } void VideoRendererImpl::SetGpuMemoryBufferVideoForTesting( std::unique_ptr gpu_memory_buffer_pool) { gpu_memory_buffer_pool_.swap(gpu_memory_buffer_pool); } void VideoRendererImpl::OnTimeProgressing() { DCHECK(task_runner_->BelongsToCurrentThread()); // WARNING: Do not attempt to use |lock_| here as StartSink() may cause a // reentrant call. time_progressing_ = true; if (sink_started_) return; // If only an EOS frame came in after a seek, the renderer may not have // received the ended event yet though we've posted it. if (rendered_end_of_stream_) return; // If we have no frames queued, there is a pending buffering state change in // flight and we should ignore the start attempt. if (!algorithm_->frames_queued()) { DCHECK_EQ(buffering_state_, BUFFERING_HAVE_NOTHING); return; } StartSink(); } void VideoRendererImpl::OnTimeStopped() { DCHECK(task_runner_->BelongsToCurrentThread()); // WARNING: Do not attempt to use |lock_| here as StopSink() may cause a // reentrant call. time_progressing_ = false; if (!sink_started_) return; StopSink(); // Make sure we expire everything we can if we can't read any more currently, // otherwise playback may hang indefinitely. Note: There are no effective // frames queued at this point, otherwise FrameReady() would have canceled // the underflow state before reaching this point. if (buffering_state_ == BUFFERING_HAVE_NOTHING) { base::AutoLock al(lock_); RemoveFramesForUnderflowOrBackgroundRendering(); // If we've underflowed, increase the number of frames required to reach // BUFFERING_HAVE_ENOUGH upon resume; this will help prevent us from // repeatedly underflowing. const size_t kMaxBufferedFrames = 2 * limits::kMaxVideoFrames; if (max_buffered_frames_ < kMaxBufferedFrames) ++max_buffered_frames_; } } void VideoRendererImpl::FrameReadyForCopyingToGpuMemoryBuffers( VideoFrameStream::Status status, const scoped_refptr& frame) { if (status != VideoFrameStream::OK || IsBeforeStartTime(frame->timestamp())) { VideoRendererImpl::FrameReady(status, frame); return; } DCHECK(frame); gpu_memory_buffer_pool_->MaybeCreateHardwareFrame( frame, base::Bind(&VideoRendererImpl::FrameReady, frame_callback_weak_factory_.GetWeakPtr(), status)); } void VideoRendererImpl::FrameReady(VideoFrameStream::Status status, const scoped_refptr& frame) { DCHECK(task_runner_->BelongsToCurrentThread()); base::AutoLock auto_lock(lock_); DCHECK_EQ(state_, kPlaying); CHECK(pending_read_); pending_read_ = false; if (status == VideoFrameStream::DECODE_ERROR) { DCHECK(!frame); task_runner_->PostTask( FROM_HERE, base::Bind(&VideoRendererImpl::OnPlaybackError, weak_factory_.GetWeakPtr(), PIPELINE_ERROR_DECODE)); return; } // Can happen when demuxers are preparing for a new Seek(). if (!frame) { DCHECK_EQ(status, VideoFrameStream::DEMUXER_READ_ABORTED); return; } if (frame->metadata()->IsTrue(VideoFrameMetadata::END_OF_STREAM)) { DCHECK(!received_end_of_stream_); received_end_of_stream_ = true; } else if ((low_delay_ || !video_frame_stream_->CanReadWithoutStalling()) && IsBeforeStartTime(frame->timestamp())) { // Don't accumulate frames that are earlier than the start time if we // won't have a chance for a better frame, otherwise we could declare // HAVE_ENOUGH_DATA and start playback prematurely. AttemptRead_Locked(); return; } else { // If the sink hasn't been started, we still have time to release less // than ideal frames prior to startup. We don't use IsBeforeStartTime() // here since it's based on a duration estimate and we can be exact here. if (!sink_started_ && frame->timestamp() <= start_timestamp_) algorithm_->Reset(); AddReadyFrame_Locked(frame); } // Attempt to purge bad frames in case of underflow or backgrounding. RemoveFramesForUnderflowOrBackgroundRendering(); // We may have removed all frames above and have reached end of stream. MaybeFireEndedCallback_Locked(time_progressing_); // Update statistics here instead of during Render() when the sink is stopped. if (!sink_started_) UpdateStats_Locked(); // Paint the first frame if possible and necessary. Paint ahead of // HAVE_ENOUGH_DATA to ensure the user sees the frame as early as possible. if (!sink_started_ && algorithm_->frames_queued() && !painted_first_frame_) { // We want to paint the first frame under two conditions: Either (1) we have // enough frames to know it's definitely the first frame or (2) there may be // no more frames coming (sometimes unless we paint one of them). // // For the first condition, we need at least two frames or the first frame // must have a timestamp >= |start_timestamp_|, since otherwise we may be // prerolling frames before the actual start time that will be dropped. if (algorithm_->frames_queued() > 1 || received_end_of_stream_ || frame->timestamp() >= start_timestamp_ || low_delay_ || !video_frame_stream_->CanReadWithoutStalling()) { scoped_refptr first_frame = algorithm_->Render(base::TimeTicks(), base::TimeTicks(), nullptr); CheckForMetadataChanges(first_frame->format(), first_frame->natural_size()); sink_->PaintSingleFrame(first_frame); painted_first_frame_ = true; } } // Signal buffering state if we've met our conditions. if (buffering_state_ == BUFFERING_HAVE_NOTHING && HaveEnoughData_Locked()) TransitionToHaveEnough_Locked(); // Always request more decoded video if we have capacity. AttemptRead_Locked(); } bool VideoRendererImpl::HaveEnoughData_Locked() { DCHECK_EQ(state_, kPlaying); lock_.AssertAcquired(); if (received_end_of_stream_) return true; if (HaveReachedBufferingCap()) return true; if (was_background_rendering_ && frames_decoded_) return true; if (!low_delay_ && video_frame_stream_->CanReadWithoutStalling()) return false; return algorithm_->effective_frames_queued() > 0; } void VideoRendererImpl::TransitionToHaveEnough_Locked() { DVLOG(3) << __func__; DCHECK(task_runner_->BelongsToCurrentThread()); DCHECK_EQ(buffering_state_, BUFFERING_HAVE_NOTHING); lock_.AssertAcquired(); buffering_state_ = BUFFERING_HAVE_ENOUGH; task_runner_->PostTask( FROM_HERE, base::Bind(&VideoRendererImpl::OnBufferingStateChange, weak_factory_.GetWeakPtr(), buffering_state_)); } void VideoRendererImpl::TransitionToHaveNothing() { DVLOG(3) << __func__; DCHECK(task_runner_->BelongsToCurrentThread()); base::AutoLock auto_lock(lock_); TransitionToHaveNothing_Locked(); } void VideoRendererImpl::TransitionToHaveNothing_Locked() { DVLOG(3) << __func__; DCHECK(task_runner_->BelongsToCurrentThread()); lock_.AssertAcquired(); if (buffering_state_ != BUFFERING_HAVE_ENOUGH || HaveEnoughData_Locked()) return; buffering_state_ = BUFFERING_HAVE_NOTHING; task_runner_->PostTask( FROM_HERE, base::Bind(&VideoRendererImpl::OnBufferingStateChange, weak_factory_.GetWeakPtr(), buffering_state_)); } void VideoRendererImpl::AddReadyFrame_Locked( const scoped_refptr& frame) { DCHECK(task_runner_->BelongsToCurrentThread()); lock_.AssertAcquired(); DCHECK(!frame->metadata()->IsTrue(VideoFrameMetadata::END_OF_STREAM)); frames_decoded_++; algorithm_->EnqueueFrame(frame); } void VideoRendererImpl::AttemptRead_Locked() { DCHECK(task_runner_->BelongsToCurrentThread()); lock_.AssertAcquired(); if (pending_read_ || received_end_of_stream_) return; if (HaveReachedBufferingCap()) return; switch (state_) { case kPlaying: pending_read_ = true; if (gpu_memory_buffer_pool_) { video_frame_stream_->Read(base::Bind( &VideoRendererImpl::FrameReadyForCopyingToGpuMemoryBuffers, frame_callback_weak_factory_.GetWeakPtr())); } else { video_frame_stream_->Read( base::Bind(&VideoRendererImpl::FrameReady, frame_callback_weak_factory_.GetWeakPtr())); } return; case kUninitialized: case kInitializing: case kFlushing: case kFlushed: return; } } void VideoRendererImpl::OnVideoFrameStreamResetDone() { // We don't need to acquire the |lock_| here, because we can only get here // when Flush is in progress, so rendering and video sink must be stopped. DCHECK(task_runner_->BelongsToCurrentThread()); DCHECK(!sink_started_); DCHECK_EQ(kFlushing, state_); DCHECK(!received_end_of_stream_); DCHECK(!rendered_end_of_stream_); DCHECK_EQ(buffering_state_, BUFFERING_HAVE_NOTHING); state_ = kFlushed; base::ResetAndReturn(&flush_cb_).Run(); } void VideoRendererImpl::UpdateStats_Locked() { lock_.AssertAcquired(); DCHECK_GE(frames_decoded_, 0); DCHECK_GE(frames_dropped_, 0); if (frames_decoded_ || frames_dropped_) { PipelineStatistics statistics; statistics.video_frames_decoded = frames_decoded_; statistics.video_frames_dropped = frames_dropped_; const size_t memory_usage = algorithm_->GetMemoryUsage(); statistics.video_memory_usage = memory_usage - last_video_memory_usage_; task_runner_->PostTask(FROM_HERE, base::Bind(&VideoRendererImpl::OnStatisticsUpdate, weak_factory_.GetWeakPtr(), statistics)); frames_decoded_ = 0; frames_dropped_ = 0; last_video_memory_usage_ = memory_usage; } } bool VideoRendererImpl::HaveReachedBufferingCap() { DCHECK(task_runner_->BelongsToCurrentThread()); // When the display rate is less than the frame rate, the effective frames // queued may be much smaller than the actual number of frames queued. Here // we ensure that frames_queued() doesn't get excessive. return algorithm_->effective_frames_queued() >= max_buffered_frames_ || algorithm_->frames_queued() >= 3 * max_buffered_frames_; } void VideoRendererImpl::StartSink() { DCHECK(task_runner_->BelongsToCurrentThread()); DCHECK_GT(algorithm_->frames_queued(), 0u); sink_started_ = true; was_background_rendering_ = false; sink_->Start(this); } void VideoRendererImpl::StopSink() { DCHECK(task_runner_->BelongsToCurrentThread()); sink_->Stop(); algorithm_->set_time_stopped(); sink_started_ = false; was_background_rendering_ = false; } void VideoRendererImpl::MaybeFireEndedCallback_Locked(bool time_progressing) { lock_.AssertAcquired(); // If there's only one frame in the video or Render() was never called, the // algorithm will have one frame linger indefinitely. So in cases where the // frame duration is unknown and we've received EOS, fire it once we get down // to a single frame. // Don't fire ended if we haven't received EOS or have already done so. if (!received_end_of_stream_ || rendered_end_of_stream_) return; // Don't fire ended if time isn't moving and we have frames. if (!time_progressing && algorithm_->frames_queued()) return; // Fire ended if we have no more effective frames or only ever had one frame. if (!algorithm_->effective_frames_queued() || (algorithm_->frames_queued() == 1u && algorithm_->average_frame_duration().is_zero())) { rendered_end_of_stream_ = true; task_runner_->PostTask(FROM_HERE, base::Bind(&VideoRendererImpl::OnPlaybackEnded, weak_factory_.GetWeakPtr())); } } base::TimeTicks VideoRendererImpl::ConvertMediaTimestamp( base::TimeDelta media_time) { std::vector media_times(1, media_time); std::vector wall_clock_times; if (!wall_clock_time_cb_.Run(media_times, &wall_clock_times)) return base::TimeTicks(); return wall_clock_times[0]; } base::TimeTicks VideoRendererImpl::GetCurrentMediaTimeAsWallClockTime() { std::vector current_time; wall_clock_time_cb_.Run(std::vector(), ¤t_time); return current_time[0]; } bool VideoRendererImpl::IsBeforeStartTime(base::TimeDelta timestamp) { return timestamp + video_frame_stream_->AverageDuration() < start_timestamp_; } void VideoRendererImpl::RemoveFramesForUnderflowOrBackgroundRendering() { // Nothing to do if frame dropping is disabled for testing or we have nothing. if (!drop_frames_ || !algorithm_->frames_queued()) return; // If we're paused for prerolling (current time is 0), don't expire any // frames. It's possible that during preroll |have_nothing| is false while // |was_background_rendering_| is true. We differentiate this from actual // background rendering by checking if current time is 0. const base::TimeTicks current_time = GetCurrentMediaTimeAsWallClockTime(); if (current_time.is_null()) return; // Background rendering updates may not be ticking fast enough to remove // expired frames, so provide a boost here by ensuring we don't exit the // decoding cycle too early. Dropped frames are not counted in this case. if (was_background_rendering_) { algorithm_->RemoveExpiredFrames(tick_clock_->NowTicks()); return; } // If we've paused for underflow, and still have no effective frames, clear // the entire queue. Note: this may cause slight inaccuracies in the number // of dropped frames since the frame may have been rendered before. if (!sink_started_ && !algorithm_->effective_frames_queued()) { frames_dropped_ += algorithm_->frames_queued(); algorithm_->Reset( VideoRendererAlgorithm::ResetFlag::kPreserveNextFrameEstimates); painted_first_frame_ = false; // It's possible in the background rendering case for us to expire enough // frames that we need to transition from HAVE_ENOUGH => HAVE_NOTHING. Just // calling this function will check if we need to transition or not. if (buffering_state_ == BUFFERING_HAVE_ENOUGH) TransitionToHaveNothing_Locked(); return; } // Use the current media wall clock time plus the frame duration since // RemoveExpiredFrames() is expecting the end point of an interval (it will // subtract from the given value). It's important to always call this so // that frame statistics are updated correctly. if (buffering_state_ == BUFFERING_HAVE_NOTHING) { frames_dropped_ += algorithm_->RemoveExpiredFrames( current_time + algorithm_->average_frame_duration()); return; } // If we reach this point, the normal rendering process will take care of // removing any expired frames. } void VideoRendererImpl::CheckForMetadataChanges(VideoPixelFormat pixel_format, const gfx::Size& natural_size) { DCHECK(task_runner_->BelongsToCurrentThread()); // Notify client of size and opacity changes if this is the first frame // or if those have changed from the last frame. if (!have_renderered_frames_ || last_frame_natural_size_ != natural_size) { last_frame_natural_size_ = natural_size; client_->OnVideoNaturalSizeChange(last_frame_natural_size_); } const bool is_opaque = IsOpaque(pixel_format); if (!have_renderered_frames_ || last_frame_opaque_ != is_opaque) { last_frame_opaque_ = is_opaque; client_->OnVideoOpacityChange(last_frame_opaque_); } have_renderered_frames_ = true; } void VideoRendererImpl::AttemptReadAndCheckForMetadataChanges( VideoPixelFormat pixel_format, const gfx::Size& natural_size) { base::AutoLock auto_lock(lock_); CheckForMetadataChanges(pixel_format, natural_size); AttemptRead_Locked(); } } // namespace media