blob: 19675792ab5f3115622da1d31ada31bc1276613e [file] [log] [blame]
// 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/default_tick_clock.h"
#include "base/time/tick_clock.h"
#include "base/time/time.h"
#include "build/build_config.h"
#include "media/remoting/remoting_cdm.h"
#include "media/remoting/remoting_cdm_context.h"
#if defined(OS_ANDROID)
#include "media/base/android/media_codec_util.h"
#endif
namespace media {
namespace remoting {
namespace {
// The duration to delay the start of media remoting to ensure all preconditions
// are held stable before switching to media remoting.
constexpr base::TimeDelta kDelayedStart = base::TimeDelta::FromSeconds(5);
// The maximum fraction of the transmission capacity that can safely be used by
// Media Remoting to deliver the media contents.
constexpr double kMaxMediaBitrateCapacityFraction = 0.9;
} // namespace
RendererController::RendererController(scoped_refptr<SharedSession> session)
: session_(std::move(session)),
clock_(new base::DefaultTickClock()),
weak_factory_(this) {
session_->AddClient(this);
}
RendererController::~RendererController() {
DCHECK(thread_checker_.CalledOnValidThread());
if (remote_rendering_started_) {
DCHECK(client_);
client_->SwitchToLocalRenderer();
}
metrics_recorder_.WillStopSession(MEDIA_ELEMENT_DESTROYED);
session_->RemoveClient(this);
}
void RendererController::OnStarted(bool success) {
DCHECK(thread_checker_.CalledOnValidThread());
if (success) {
if (remote_rendering_started_) {
metrics_recorder_.DidStartSession();
DCHECK(client_);
client_->SwitchToRemoteRenderer(session_->sink_name());
} 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();
UpdateAndMaybeSwitch(start_trigger, stop_trigger);
}
bool RendererController::IsRemoteSinkAvailable() const {
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::OnBecameDominantVisibleContent(bool is_dominant) {
DCHECK(thread_checker_.CalledOnValidThread());
if (is_dominant_content_ == is_dominant)
return;
is_dominant_content_ = is_dominant;
UpdateAndMaybeSwitch(BECAME_DOMINANT_CONTENT, BECAME_AUXILIARY_CONTENT);
}
void RendererController::OnSetCdm(CdmContext* cdm_context) {
VLOG(2) << __func__;
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<RpcBroker> RendererController::GetRpcBroker() const {
DCHECK(thread_checker_.CalledOnValidThread());
return session_->rpc_broker()->GetWeakPtr();
}
void RendererController::StartDataPipe(
std::unique_ptr<mojo::DataPipe> audio_data_pipe,
std::unique_ptr<mojo::DataPipe> 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;
}
UpdateRemotePlaybackAvailabilityMonitoringState();
UpdateAndMaybeSwitch(start_trigger, stop_trigger);
}
void RendererController::OnDataSourceInitialized(
const GURL& url_after_redirects) {
DCHECK(thread_checker_.CalledOnValidThread());
if (url_after_redirects == url_after_redirects_)
return;
// TODO(avayvod): Does WMPI update MediaObserver when metadata becomes
// invalid or should we reset it here?
url_after_redirects_ = url_after_redirects;
UpdateRemotePlaybackAvailabilityMonitoringState();
}
void RendererController::UpdateRemotePlaybackAvailabilityMonitoringState() {
if (!client_)
return;
// Currently RemotePlayback-initated media remoting only supports URL flinging
// thus the source is supported when the URL is either http or https, video and
// audio codecs are supported by the remote playback device; HLS is playable by
// Chrome on Android (which is not detected by the pipeline metadata atm).
#if defined(OS_ANDROID)
// TODO(tguilbert): Detect the presence of HLS based on demuxing results,
// rather than the URL string. See crbug.com/663503.
bool is_hls = MediaCodecUtil::IsHLSURL(url_after_redirects_);
#else
bool is_hls = false;
#endif
// TODO(avayvod): add a check for CORS.
bool is_source_supported = url_after_redirects_.has_scheme() &&
(url_after_redirects_.SchemeIs("http") ||
url_after_redirects_.SchemeIs("https")) &&
(is_hls || IsAudioOrVideoSupported());
client_->UpdateRemotePlaybackCompatibility(is_source_supported);
}
bool RendererController::IsVideoCodecSupported() const {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(has_video());
switch (pipeline_metadata_.video_decoder_config.codec()) {
case VideoCodec::kCodecH264:
return session_->HasVideoCapability(
mojom::RemotingSinkVideoCapability::CODEC_H264);
case VideoCodec::kCodecVP8:
return session_->HasVideoCapability(
mojom::RemotingSinkVideoCapability::CODEC_VP8);
case VideoCodec::kCodecVP9:
return session_->HasVideoCapability(
mojom::RemotingSinkVideoCapability::CODEC_VP9);
case VideoCodec::kCodecHEVC:
return session_->HasVideoCapability(
mojom::RemotingSinkVideoCapability::CODEC_HEVC);
default:
VLOG(2) << "Remoting does not support video codec: "
<< pipeline_metadata_.video_decoder_config.codec();
return false;
}
}
bool RendererController::IsAudioCodecSupported() const {
DCHECK(thread_checker_.CalledOnValidThread());
DCHECK(has_audio());
switch (pipeline_metadata_.audio_decoder_config.codec()) {
case AudioCodec::kCodecAAC:
return session_->HasAudioCapability(
mojom::RemotingSinkAudioCapability::CODEC_AAC);
case AudioCodec::kCodecOpus:
return session_->HasAudioCapability(
mojom::RemotingSinkAudioCapability::CODEC_OPUS);
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::kCodecEAC3:
case AudioCodec::kCodecPCM_ALAW:
case AudioCodec::kCodecALAC:
case AudioCodec::kCodecAC3:
return session_->HasAudioCapability(
mojom::RemotingSinkAudioCapability::CODEC_BASELINE_SET);
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;
// Cancel the start if in the middle of delayed start.
CancelDelayedStart();
}
bool RendererController::CanBeRemoting() const {
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.
}
if (!IsAudioOrVideoSupported())
return false;
if (is_remote_playback_disabled_)
return false;
return true;
}
bool RendererController::IsAudioOrVideoSupported() const {
if ((!has_audio() && !has_video()) ||
(has_video() && !IsVideoCodecSupported()) ||
(has_audio() && !IsAudioCodecSupported())) {
return false;
}
return true;
}
void RendererController::UpdateAndMaybeSwitch(StartTrigger start_trigger,
StopTrigger stop_trigger) {
DCHECK(thread_checker_.CalledOnValidThread());
bool should_be_remoting = CanBeRemoting();
if (!is_encrypted_ && client_)
client_->ActivateViewportIntersectionMonitoring(should_be_remoting);
// Normally, 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.
if (!is_encrypted_)
should_be_remoting &= is_dominant_content_;
if ((remote_rendering_started_ ||
delayed_start_stability_timer_.IsRunning()) == should_be_remoting)
return;
DCHECK(client_);
if (is_encrypted_) {
DCHECK(should_be_remoting);
StartRemoting(start_trigger);
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;
if (should_be_remoting) {
WaitForStabilityBeforeStart(start_trigger);
} else if (delayed_start_stability_timer_.IsRunning()) {
DCHECK(!remote_rendering_started_);
CancelDelayedStart();
} else {
remote_rendering_started_ = false;
// 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_->SwitchToLocalRenderer();
VLOG(2) << "Request to stop remoting: stop_trigger=" << stop_trigger;
session_->StopRemoting(this);
}
}
void RendererController::WaitForStabilityBeforeStart(
StartTrigger start_trigger) {
DCHECK(!delayed_start_stability_timer_.IsRunning());
DCHECK(!remote_rendering_started_);
DCHECK(!is_encrypted_);
delayed_start_stability_timer_.Start(
FROM_HERE, kDelayedStart,
base::Bind(
&RendererController::OnDelayedStartTimerFired, base::Unretained(this),
start_trigger,
client_->AudioDecodedByteCount() + client_->VideoDecodedByteCount(),
clock_->NowTicks()));
session_->EstimateTransmissionCapacity(
base::BindOnce(&RendererController::OnReceivedTransmissionCapacity,
weak_factory_.GetWeakPtr()));
}
void RendererController::CancelDelayedStart() {
delayed_start_stability_timer_.Stop();
transmission_capacity_ = 0;
}
void RendererController::OnDelayedStartTimerFired(
StartTrigger start_trigger,
size_t decoded_bytes_before_delay,
base::TimeTicks delayed_start_time) {
DCHECK(is_dominant_content_);
DCHECK(!remote_rendering_started_);
DCHECK(!is_encrypted_);
base::TimeDelta elapsed = clock_->NowTicks() - delayed_start_time;
DCHECK(!elapsed.is_zero());
double kilobits_per_second =
(client_->AudioDecodedByteCount() + client_->VideoDecodedByteCount() -
decoded_bytes_before_delay) *
8.0 / elapsed.InSecondsF() / 1000.0;
DCHECK_GE(kilobits_per_second, 0);
const double capacity_kbps = transmission_capacity_ * 8.0 / 1000.0;
metrics_recorder_.RecordMediaBitrateVersusCapacity(kilobits_per_second,
capacity_kbps);
if (kilobits_per_second <= kMaxMediaBitrateCapacityFraction * capacity_kbps) {
StartRemoting(start_trigger);
} else {
VLOG(1) << "Media remoting is not supported: bitrate(kbps)="
<< kilobits_per_second
<< " transmission_capacity(kbps)=" << capacity_kbps;
encountered_renderer_fatal_error_ = true;
}
}
void RendererController::StartRemoting(StartTrigger start_trigger) {
DCHECK(client_);
remote_rendering_started_ = true;
if (session_->state() == SharedSession::SESSION_PERMANENTLY_STOPPED) {
client_->SwitchToRemoteRenderer(session_->sink_name());
return;
}
DCHECK_NE(start_trigger, UNKNOWN_START_TRIGGER);
metrics_recorder_.WillStartSession(start_trigger);
// |MediaObserverClient::SwitchToRemoteRenderer()| will be called after
// remoting is started successfully.
session_->StartRemoting(this);
}
void RendererController::OnReceivedTransmissionCapacity(double rate) {
DCHECK_GE(rate, 0);
transmission_capacity_ = rate;
}
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;
if (!is_encrypted_)
client_->ActivateViewportIntersectionMonitoring(CanBeRemoting());
}
} // namespace remoting
} // namespace media