// Copyright 2016 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/remoting/renderer_controller.h" #include "base/bind.h" #include "base/logging.h" #include "base/threading/thread_checker.h" #include "base/time/time.h" #include "media/remoting/remoting_cdm.h" #include "media/remoting/remoting_cdm_context.h" namespace media { namespace remoting { RendererController::RendererController(scoped_refptr session) : session_(std::move(session)), weak_factory_(this) { session_->AddClient(this); } RendererController::~RendererController() { DCHECK(thread_checker_.CalledOnValidThread()); metrics_recorder_.WillStopSession(MEDIA_ELEMENT_DESTROYED); session_->RemoveClient(this); } void RendererController::OnStarted(bool success) { DCHECK(thread_checker_.CalledOnValidThread()); if (success) { VLOG(1) << "Remoting started successively."; if (remote_rendering_started_) { metrics_recorder_.DidStartSession(); DCHECK(client_); client_->SwitchRenderer(true); } else { session_->StopRemoting(this); } } else { VLOG(1) << "Failed to start remoting."; remote_rendering_started_ = false; metrics_recorder_.WillStopSession(START_RACE); } } void RendererController::OnSessionStateChanged() { DCHECK(thread_checker_.CalledOnValidThread()); UpdateFromSessionState(SINK_AVAILABLE, ROUTE_TERMINATED); } void RendererController::UpdateFromSessionState(StartTrigger start_trigger, StopTrigger stop_trigger) { VLOG(1) << "UpdateFromSessionState: " << session_->state(); if (client_) client_->ActivateViewportIntersectionMonitoring(IsRemoteSinkAvailable()); UpdateAndMaybeSwitch(start_trigger, stop_trigger); } bool RendererController::IsRemoteSinkAvailable() { DCHECK(thread_checker_.CalledOnValidThread()); switch (session_->state()) { case SharedSession::SESSION_CAN_START: case SharedSession::SESSION_STARTING: case SharedSession::SESSION_STARTED: return true; case SharedSession::SESSION_UNAVAILABLE: case SharedSession::SESSION_STOPPING: case SharedSession::SESSION_PERMANENTLY_STOPPED: return false; } NOTREACHED(); return false; // To suppress compiler warning on Windows. } void RendererController::OnEnteredFullscreen() { DCHECK(thread_checker_.CalledOnValidThread()); is_fullscreen_ = true; // See notes in OnBecameDominantVisibleContent() for why this is forced: is_dominant_content_ = true; UpdateAndMaybeSwitch(ENTERED_FULLSCREEN, UNKNOWN_STOP_TRIGGER); } void RendererController::OnExitedFullscreen() { DCHECK(thread_checker_.CalledOnValidThread()); is_fullscreen_ = false; // See notes in OnBecameDominantVisibleContent() for why this is forced: is_dominant_content_ = false; UpdateAndMaybeSwitch(UNKNOWN_START_TRIGGER, EXITED_FULLSCREEN); } void RendererController::OnBecameDominantVisibleContent(bool is_dominant) { DCHECK(thread_checker_.CalledOnValidThread()); // Two scenarios where "dominance" status mixes with fullscreen transitions: // // 1. Just before/after entering fullscreen, the element will, of course, // become the dominant on-screen content via automatic page layout. // 2. Just before/after exiting fullscreen, the element may or may not // shrink in size enough to become non-dominant. However, exiting // fullscreen was caused by a user action that explicitly indicates a // desire to exit remoting, so even if the element is still dominant, // remoting should be shut down. // // Thus, to achieve the desired behaviors, |is_dominant_content_| is force-set // in OnEnteredFullscreen() and OnExitedFullscreen(), and changes to it here // are ignored while in fullscreen. if (is_fullscreen_) return; is_dominant_content_ = is_dominant; UpdateAndMaybeSwitch(BECAME_DOMINANT_CONTENT, BECAME_AUXILIARY_CONTENT); } void RendererController::OnSetCdm(CdmContext* cdm_context) { DCHECK(thread_checker_.CalledOnValidThread()); auto* remoting_cdm_context = RemotingCdmContext::From(cdm_context); if (!remoting_cdm_context) return; session_->RemoveClient(this); session_ = remoting_cdm_context->GetSharedSession(); session_->AddClient(this); UpdateFromSessionState(CDM_READY, DECRYPTION_ERROR); } void RendererController::OnRemotePlaybackDisabled(bool disabled) { DCHECK(thread_checker_.CalledOnValidThread()); is_remote_playback_disabled_ = disabled; metrics_recorder_.OnRemotePlaybackDisabled(disabled); UpdateAndMaybeSwitch(ENABLED_BY_PAGE, DISABLED_BY_PAGE); } base::WeakPtr RendererController::GetRpcBroker() const { DCHECK(thread_checker_.CalledOnValidThread()); return session_->rpc_broker()->GetWeakPtr(); } void RendererController::StartDataPipe( std::unique_ptr audio_data_pipe, std::unique_ptr video_data_pipe, const SharedSession::DataPipeStartCallback& done_callback) { DCHECK(thread_checker_.CalledOnValidThread()); session_->StartDataPipe(std::move(audio_data_pipe), std::move(video_data_pipe), done_callback); } void RendererController::OnMetadataChanged(const PipelineMetadata& metadata) { DCHECK(thread_checker_.CalledOnValidThread()); const bool was_audio_codec_supported = has_audio() && IsAudioCodecSupported(); const bool was_video_codec_supported = has_video() && IsVideoCodecSupported(); pipeline_metadata_ = metadata; const bool is_audio_codec_supported = has_audio() && IsAudioCodecSupported(); const bool is_video_codec_supported = has_video() && IsVideoCodecSupported(); metrics_recorder_.OnPipelineMetadataChanged(metadata); is_encrypted_ = false; if (has_video()) is_encrypted_ |= metadata.video_decoder_config.is_encrypted(); if (has_audio()) is_encrypted_ |= metadata.audio_decoder_config.is_encrypted(); StartTrigger start_trigger = UNKNOWN_START_TRIGGER; if (!was_audio_codec_supported && is_audio_codec_supported) start_trigger = SUPPORTED_AUDIO_CODEC; if (!was_video_codec_supported && is_video_codec_supported) { start_trigger = start_trigger == SUPPORTED_AUDIO_CODEC ? SUPPORTED_AUDIO_AND_VIDEO_CODECS : SUPPORTED_VIDEO_CODEC; } StopTrigger stop_trigger = UNKNOWN_STOP_TRIGGER; if (was_audio_codec_supported && !is_audio_codec_supported) stop_trigger = UNSUPPORTED_AUDIO_CODEC; if (was_video_codec_supported && !is_video_codec_supported) { stop_trigger = stop_trigger == UNSUPPORTED_AUDIO_CODEC ? UNSUPPORTED_AUDIO_AND_VIDEO_CODECS : UNSUPPORTED_VIDEO_CODEC; } UpdateAndMaybeSwitch(start_trigger, stop_trigger); } bool RendererController::IsVideoCodecSupported() { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(has_video()); switch (pipeline_metadata_.video_decoder_config.codec()) { case VideoCodec::kCodecH264: case VideoCodec::kCodecVP8: return true; default: VLOG(2) << "Remoting does not support video codec: " << pipeline_metadata_.video_decoder_config.codec(); return false; } } bool RendererController::IsAudioCodecSupported() { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(has_audio()); switch (pipeline_metadata_.audio_decoder_config.codec()) { case AudioCodec::kCodecAAC: case AudioCodec::kCodecMP3: case AudioCodec::kCodecPCM: case AudioCodec::kCodecVorbis: case AudioCodec::kCodecFLAC: case AudioCodec::kCodecAMR_NB: case AudioCodec::kCodecAMR_WB: case AudioCodec::kCodecPCM_MULAW: case AudioCodec::kCodecGSM_MS: case AudioCodec::kCodecPCM_S16BE: case AudioCodec::kCodecPCM_S24BE: case AudioCodec::kCodecOpus: case AudioCodec::kCodecEAC3: case AudioCodec::kCodecPCM_ALAW: case AudioCodec::kCodecALAC: case AudioCodec::kCodecAC3: return true; default: VLOG(2) << "Remoting does not support audio codec: " << pipeline_metadata_.audio_decoder_config.codec(); return false; } } void RendererController::OnPlaying() { DCHECK(thread_checker_.CalledOnValidThread()); is_paused_ = false; UpdateAndMaybeSwitch(PLAY_COMMAND, UNKNOWN_STOP_TRIGGER); } void RendererController::OnPaused() { DCHECK(thread_checker_.CalledOnValidThread()); is_paused_ = true; } bool RendererController::ShouldBeRemoting() { DCHECK(thread_checker_.CalledOnValidThread()); if (!client_) { DCHECK(!remote_rendering_started_); return false; // No way to switch to the remoting renderer. } const SharedSession::SessionState state = session_->state(); if (is_encrypted_) { // Due to technical limitations when playing encrypted content, once a // remoting session has been started, playback cannot be resumed locally // without reloading the page, so leave the CourierRenderer in-place to // avoid having the default renderer attempt and fail to play the content. // // TODO(miu): Revisit this once more of the encrypted-remoting impl is // in-place. For example, this will prevent metrics from recording session // stop reasons. return state == SharedSession::SESSION_STARTED || state == SharedSession::SESSION_STOPPING || state == SharedSession::SESSION_PERMANENTLY_STOPPED; } if (encountered_renderer_fatal_error_) return false; switch (state) { case SharedSession::SESSION_UNAVAILABLE: return false; // Cannot remote media without a remote sink. case SharedSession::SESSION_CAN_START: case SharedSession::SESSION_STARTING: case SharedSession::SESSION_STARTED: break; // Media remoting is possible, assuming other requirments are met. case SharedSession::SESSION_STOPPING: case SharedSession::SESSION_PERMANENTLY_STOPPED: return false; // Use local rendering after stopping remoting. } switch (session_->sink_capabilities()) { case mojom::RemotingSinkCapabilities::NONE: return false; case mojom::RemotingSinkCapabilities::RENDERING_ONLY: case mojom::RemotingSinkCapabilities::CONTENT_DECRYPTION_AND_RENDERING: break; // The sink is capable of remote rendering. } if ((!has_audio() && !has_video()) || (has_video() && !IsVideoCodecSupported()) || (has_audio() && !IsAudioCodecSupported())) { return false; } if (is_remote_playback_disabled_) return false; // Normally, entering fullscreen or being the dominant visible content is the // signal that starts remote rendering. However, current technical limitations // require encrypted content be remoted without waiting for a user signal. return is_fullscreen_ || is_dominant_content_; } void RendererController::UpdateAndMaybeSwitch(StartTrigger start_trigger, StopTrigger stop_trigger) { DCHECK(thread_checker_.CalledOnValidThread()); bool should_be_remoting = ShouldBeRemoting(); if (remote_rendering_started_ == should_be_remoting) return; // Only switch to remoting when media is playing. Since the renderer is // created when video starts loading/playing, receiver will display a black // screen before video starts playing if switching to remoting when paused. // Thus, the user experience is improved by not starting remoting until // playback resumes. if (should_be_remoting && is_paused_) return; // Switch between local renderer and remoting renderer. remote_rendering_started_ = should_be_remoting; DCHECK(client_); if (remote_rendering_started_) { if (session_->state() == SharedSession::SESSION_PERMANENTLY_STOPPED) { client_->SwitchRenderer(true); return; } DCHECK_NE(start_trigger, UNKNOWN_START_TRIGGER); metrics_recorder_.WillStartSession(start_trigger); // |MediaObserverClient::SwitchRenderer()| will be called after remoting is // started successfully. session_->StartRemoting(this); } else { // For encrypted content, it's only valid to switch to remoting renderer, // and never back to the local renderer. The RemotingCdmController will // force-stop the session when remoting has ended; so no need to call // StopRemoting() from here. DCHECK(!is_encrypted_); DCHECK_NE(stop_trigger, UNKNOWN_STOP_TRIGGER); metrics_recorder_.WillStopSession(stop_trigger); client_->SwitchRenderer(false); session_->StopRemoting(this); } } void RendererController::OnRendererFatalError(StopTrigger stop_trigger) { DCHECK(thread_checker_.CalledOnValidThread()); // Do not act on errors caused by things like Mojo pipes being closed during // shutdown. if (!remote_rendering_started_) return; encountered_renderer_fatal_error_ = true; UpdateAndMaybeSwitch(UNKNOWN_START_TRIGGER, stop_trigger); } void RendererController::SetClient(MediaObserverClient* client) { DCHECK(thread_checker_.CalledOnValidThread()); DCHECK(client); DCHECK(!client_); client_ = client; client_->ActivateViewportIntersectionMonitoring(IsRemoteSinkAvailable()); } } // namespace remoting } // namespace media