blob: e144c94245cefe71488d30dab7fae142aaadea6a [file] [log] [blame]
// Copyright 2014 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 "content/browser/media/media_web_contents_observer.h"
#include <memory>
#include <tuple>
#include "base/bind.h"
#include "base/threading/sequenced_task_runner_handle.h"
#include "build/build_config.h"
#include "content/browser/media/audible_metrics.h"
#include "content/browser/media/audio_stream_monitor.h"
#include "content/browser/media/media_devices_util.h"
#include "content/browser/renderer_host/media/media_stream_manager.h"
#include "content/browser/web_contents/web_contents_impl.h"
#include "content/common/media/media_player_delegate_messages.h"
#include "content/public/browser/render_frame_host.h"
#include "content/public/browser/web_contents.h"
#include "ipc/ipc_message_macros.h"
#include "mojo/public/cpp/bindings/pending_receiver.h"
#include "services/device/public/mojom/wake_lock_context.mojom.h"
#include "services/media_session/public/cpp/media_position.h"
#include "third_party/blink/public/common/mediastream/media_devices.h"
#include "third_party/blink/public/platform/web_fullscreen_video_status.h"
#include "ui/gfx/geometry/size.h"
namespace content {
namespace {
AudibleMetrics* GetAudibleMetrics() {
static AudibleMetrics* metrics = new AudibleMetrics();
return metrics;
}
#if defined(OS_ANDROID)
static void SuspendAllMediaPlayersInRenderFrame(
RenderFrameHost* render_frame_host) {
render_frame_host->Send(new MediaPlayerDelegateMsg_SuspendAllMediaPlayers(
render_frame_host->GetRoutingID()));
}
#endif // defined(OS_ANDROID)
static void OnAudioOutputDeviceIdTranslated(
base::WeakPtr<MediaWebContentsObserver> observer,
RenderFrameHost* render_frame_host,
int delegate_id,
const base::Optional<std::string>& raw_device_id) {
if (!raw_device_id)
return;
content::GetUIThreadTaskRunner({})->PostTask(
FROM_HERE,
base::BindOnce(&MediaWebContentsObserver::OnReceivedTranslatedDeviceId,
std::move(observer), render_frame_host, delegate_id,
raw_device_id.value()));
}
} // anonymous namespace
// Maintains state for a single player. Issues WebContents and power-related
// notifications appropriate for state changes.
class MediaWebContentsObserver::PlayerInfo {
public:
PlayerInfo(const MediaPlayerId& id, MediaWebContentsObserver* observer)
: id_(id), observer_(observer) {}
~PlayerInfo() {
if (is_playing_) {
NotifyPlayerStopped(WebContentsObserver::MediaStoppedReason::kUnspecified,
MediaPowerExperimentManager::NotificationMode::kSkip);
}
}
PlayerInfo(const PlayerInfo&) = delete;
PlayerInfo& operator=(const PlayerInfo&) = delete;
void set_has_audio(bool has_audio) { has_audio_ = has_audio; }
bool has_video() const { return has_video_; }
void set_has_video(bool has_video) { has_video_ = has_video; }
bool is_playing() const { return is_playing_; }
void SetIsPlaying() {
DCHECK(!is_playing_);
is_playing_ = true;
NotifyPlayerStarted();
}
void SetIsStopped(bool reached_end_of_stream) {
DCHECK(is_playing_);
is_playing_ = false;
NotifyPlayerStopped(
reached_end_of_stream
? WebContentsObserver::MediaStoppedReason::kReachedEndOfStream
: WebContentsObserver::MediaStoppedReason::kUnspecified,
MediaPowerExperimentManager::NotificationMode::kNotify);
}
private:
void NotifyPlayerStarted() {
observer_->web_contents_impl()->MediaStartedPlaying(
WebContentsObserver::MediaPlayerInfo(has_video_, has_audio_), id_);
if (observer_->power_experiment_manager_) {
// Bind the callback to a WeakPtr for the frame, so that we won't try to
// notify the frame after it's been destroyed.
observer_->power_experiment_manager_->PlayerStarted(
id_, base::BindRepeating(
&MediaWebContentsObserver::OnExperimentStateChanged,
observer_->GetWeakPtrForFrame(id_.render_frame_host), id_));
}
}
void NotifyPlayerStopped(
WebContentsObserver::MediaStoppedReason stopped_reason,
MediaPowerExperimentManager::NotificationMode notification_mode) {
observer_->web_contents_impl()->MediaStoppedPlaying(
WebContentsObserver::MediaPlayerInfo(has_video_, has_audio_), id_,
stopped_reason);
if (observer_->power_experiment_manager_) {
observer_->power_experiment_manager_->PlayerStopped(id_,
notification_mode);
}
}
const MediaPlayerId id_;
MediaWebContentsObserver* const observer_;
bool has_audio_ = false;
bool has_video_ = false;
bool is_playing_ = false;
};
MediaWebContentsObserver::MediaWebContentsObserver(
WebContentsImpl* web_contents)
: WebContentsObserver(web_contents),
audible_metrics_(GetAudibleMetrics()),
session_controllers_manager_(web_contents),
power_experiment_manager_(MediaPowerExperimentManager::Instance()) {}
MediaWebContentsObserver::~MediaWebContentsObserver() = default;
void MediaWebContentsObserver::WebContentsDestroyed() {
use_after_free_checker_.check();
AudioStreamMonitor* audio_stream_monitor =
web_contents_impl()->audio_stream_monitor();
audible_metrics_->WebContentsDestroyed(
web_contents(), audio_stream_monitor->WasRecentlyAudible() &&
!web_contents()->IsAudioMuted());
// Remove all players so that the experiment manager is notified.
player_info_map_.clear();
// Remove all the mojo receivers and remotes associated to the media players
// handled by this WebContents to prevent from handling/sending any more
// messages after this point, plus properly cleaning things up.
media_player_hosts_.clear();
media_player_observer_hosts_.clear();
media_player_remotes_.clear();
}
void MediaWebContentsObserver::RenderFrameDeleted(
RenderFrameHost* render_frame_host) {
use_after_free_checker_.check();
base::EraseIf(
player_info_map_,
[render_frame_host](const PlayerInfoMap::value_type& id_and_player_info) {
return render_frame_host == id_and_player_info.first.render_frame_host;
});
base::EraseIf(media_player_hosts_,
[render_frame_host](const MediaPlayerHostImplMap::value_type&
media_player_hosts_value_type) {
return render_frame_host ==
media_player_hosts_value_type.first;
});
base::EraseIf(
media_player_observer_hosts_,
[render_frame_host](const MediaPlayerObserverHostImplMap::value_type&
media_player_observer_hosts_value_type) {
return render_frame_host ==
media_player_observer_hosts_value_type.first.render_frame_host;
});
base::EraseIf(
media_player_remotes_,
[render_frame_host](const MediaPlayerRemotesMap::value_type&
media_player_remotes_value_type) {
return render_frame_host ==
media_player_remotes_value_type.first.render_frame_host;
});
session_controllers_manager_.RenderFrameDeleted(render_frame_host);
if (fullscreen_player_ &&
fullscreen_player_->render_frame_host == render_frame_host) {
picture_in_picture_allowed_in_fullscreen_.reset();
fullscreen_player_.reset();
}
// Cancel any pending callbacks for players from this frame.
use_after_free_checker_.check();
per_frame_factory_.erase(render_frame_host);
}
void MediaWebContentsObserver::MaybeUpdateAudibleState() {
AudioStreamMonitor* audio_stream_monitor =
web_contents_impl()->audio_stream_monitor();
if (audio_stream_monitor->WasRecentlyAudible())
LockAudio();
else
CancelAudioLock();
audible_metrics_->UpdateAudibleWebContentsState(
web_contents(), audio_stream_monitor->IsCurrentlyAudible() &&
!web_contents()->IsAudioMuted());
}
bool MediaWebContentsObserver::HasActiveEffectivelyFullscreenVideo() const {
if (!web_contents()->IsFullscreen() || !fullscreen_player_)
return false;
// Check that the player is active.
if (const PlayerInfo* player_info = GetPlayerInfo(*fullscreen_player_))
return player_info->is_playing() && player_info->has_video();
return false;
}
bool MediaWebContentsObserver::IsPictureInPictureAllowedForFullscreenVideo()
const {
DCHECK(picture_in_picture_allowed_in_fullscreen_.has_value());
return *picture_in_picture_allowed_in_fullscreen_;
}
const base::Optional<MediaPlayerId>&
MediaWebContentsObserver::GetFullscreenVideoMediaPlayerId() const {
return fullscreen_player_;
}
bool MediaWebContentsObserver::OnMessageReceived(
const IPC::Message& msg,
RenderFrameHost* render_frame_host) {
bool handled = true;
IPC_BEGIN_MESSAGE_MAP_WITH_PARAM(MediaWebContentsObserver, msg,
render_frame_host)
IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaDestroyed,
OnMediaDestroyed)
IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaPaused, OnMediaPaused)
IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaMetadataChanged,
OnMediaMetadataChanged)
IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnMediaPlaying,
OnMediaPlaying)
IPC_MESSAGE_HANDLER(
MediaPlayerDelegateHostMsg_OnMediaEffectivelyFullscreenChanged,
OnMediaEffectivelyFullscreenChanged)
IPC_MESSAGE_HANDLER(MediaPlayerDelegateHostMsg_OnAudioOutputSinkChanged,
OnAudioOutputSinkChanged);
IPC_MESSAGE_UNHANDLED(handled = false)
IPC_END_MESSAGE_MAP()
return handled;
}
void MediaWebContentsObserver::MediaPictureInPictureChanged(
bool is_picture_in_picture) {
session_controllers_manager_.PictureInPictureStateChanged(
is_picture_in_picture);
}
void MediaWebContentsObserver::DidUpdateAudioMutingState(bool muted) {
session_controllers_manager_.WebContentsMutedStateChanged(muted);
}
void MediaWebContentsObserver::RequestPersistentVideo(bool value) {
if (!fullscreen_player_)
return;
// The message is sent to the renderer even though the video is already the
// fullscreen element itself. It will eventually be handled by Blink.
fullscreen_player_->render_frame_host->Send(
new MediaPlayerDelegateMsg_BecamePersistentVideo(
fullscreen_player_->render_frame_host->GetRoutingID(),
fullscreen_player_->delegate_id, value));
}
bool MediaWebContentsObserver::IsPlayerActive(
const MediaPlayerId& player_id) const {
if (const PlayerInfo* player_info = GetPlayerInfo(player_id))
return player_info->is_playing();
return false;
}
MediaWebContentsObserver::MediaPlayerHostImpl::MediaPlayerHostImpl(
RenderFrameHost* render_frame_host,
MediaWebContentsObserver* media_web_contents_observer)
: render_frame_host_(render_frame_host),
media_web_contents_observer_(media_web_contents_observer) {}
MediaWebContentsObserver::MediaPlayerHostImpl::~MediaPlayerHostImpl() = default;
void MediaWebContentsObserver::MediaPlayerHostImpl::BindMediaPlayerHostReceiver(
mojo::PendingReceiver<media::mojom::MediaPlayerHost> receiver) {
receiver_.reset();
receiver_.Bind(std::move(receiver));
// Both |media_web_contents_observer_| and |render_frame_host_| outlive
// MediaPlayerHostImpl, so it's safe to use base::Unretained().
receiver_.set_disconnect_handler(
base::BindOnce(&MediaWebContentsObserver::OnMediaPlayerHostDisconnected,
base::Unretained(media_web_contents_observer_),
base::Unretained(render_frame_host_)));
}
void MediaWebContentsObserver::MediaPlayerHostImpl::OnMediaPlayerAdded(
mojo::PendingRemote<media::mojom::MediaPlayer> media_player,
int32_t player_id) {
media_web_contents_observer_->OnMediaPlayerAdded(
std::move(media_player), MediaPlayerId(render_frame_host_, player_id));
}
MediaWebContentsObserver::MediaPlayerObserverHostImpl::
MediaPlayerObserverHostImpl(
const MediaPlayerId& media_player_id,
MediaWebContentsObserver* media_web_contents_observer)
: media_player_id_(media_player_id),
media_web_contents_observer_(media_web_contents_observer) {}
MediaWebContentsObserver::MediaPlayerObserverHostImpl::
~MediaPlayerObserverHostImpl() = default;
mojo::PendingRemote<media::mojom::MediaPlayerObserver>
MediaWebContentsObserver::MediaPlayerObserverHostImpl::
BindMediaPlayerObserverReceiverAndPassRemote() {
media_player_observer_receiver_.reset();
mojo::PendingRemote<media::mojom::MediaPlayerObserver> pending_remote =
media_player_observer_receiver_.BindNewPipeAndPassRemote();
// |media_web_contents_observer_| outlives MediaPlayerHostImpl, so it's safe
// to use base::Unretained().
media_player_observer_receiver_.set_disconnect_handler(base::BindOnce(
&MediaWebContentsObserver::OnMediaPlayerObserverDisconnected,
base::Unretained(media_web_contents_observer_), media_player_id_));
return pending_remote;
}
void MediaWebContentsObserver::MediaPlayerObserverHostImpl::
OnMutedStatusChanged(bool muted) {
media_web_contents_observer_->web_contents_impl()->MediaMutedStatusChanged(
media_player_id_, muted);
}
void MediaWebContentsObserver::MediaPlayerObserverHostImpl::
OnMediaPositionStateChanged(
const media_session::MediaPosition& media_position) {
media_web_contents_observer_->session_controllers_manager()
->OnMediaPositionStateChanged(media_player_id_, media_position);
}
void MediaWebContentsObserver::MediaPlayerObserverHostImpl::OnMediaSizeChanged(
const ::gfx::Size& size) {
media_web_contents_observer_->web_contents_impl()->MediaResized(
size, media_player_id_);
}
void MediaWebContentsObserver::MediaPlayerObserverHostImpl::
OnPictureInPictureAvailabilityChanged(bool available) {
media_web_contents_observer_->session_controllers_manager()
->OnPictureInPictureAvailabilityChanged(media_player_id_, available);
}
void MediaWebContentsObserver::MediaPlayerObserverHostImpl::
OnAudioOutputSinkChangingDisabled() {
media_web_contents_observer_->session_controllers_manager()
->OnAudioOutputSinkChangingDisabled(media_player_id_);
}
void MediaWebContentsObserver::MediaPlayerObserverHostImpl::
OnBufferUnderflow() {
media_web_contents_observer_->web_contents_impl()->MediaBufferUnderflow(
media_player_id_);
}
void MediaWebContentsObserver::MediaPlayerObserverHostImpl::OnSeek() {
media_web_contents_observer_->web_contents_impl()->MediaPlayerSeek(
media_player_id_);
}
MediaWebContentsObserver::PlayerInfo* MediaWebContentsObserver::GetPlayerInfo(
const MediaPlayerId& id) const {
const auto it = player_info_map_.find(id);
return it != player_info_map_.end() ? it->second.get() : nullptr;
}
void MediaWebContentsObserver::OnMediaDestroyed(
RenderFrameHost* render_frame_host,
int delegate_id) {
// TODO(liberato): Should we skip power manager notifications in this case?
const MediaPlayerId player_id(render_frame_host, delegate_id);
player_info_map_.erase(player_id);
session_controllers_manager_.OnEnd(player_id);
web_contents_impl()->MediaDestroyed(player_id);
}
void MediaWebContentsObserver::OnMediaPaused(RenderFrameHost* render_frame_host,
int delegate_id,
bool reached_end_of_stream) {
const MediaPlayerId player_id(render_frame_host, delegate_id);
PlayerInfo* player_info = GetPlayerInfo(player_id);
if (!player_info || !player_info->is_playing())
return;
player_info->SetIsStopped(reached_end_of_stream);
session_controllers_manager_.OnPause(player_id, reached_end_of_stream);
}
void MediaWebContentsObserver::OnMediaMetadataChanged(
RenderFrameHost* render_frame_host,
int delegate_id,
bool has_audio,
bool has_video,
media::MediaContentType media_content_type) {
const MediaPlayerId player_id(render_frame_host, delegate_id);
PlayerInfo* player_info = GetPlayerInfo(player_id);
if (!player_info) {
PlayerInfoMap::iterator it;
std::tie(it, std::ignore) = player_info_map_.emplace(
player_id, std::make_unique<PlayerInfo>(player_id, this));
player_info = it->second.get();
}
player_info->set_has_audio(has_audio);
player_info->set_has_video(has_video);
session_controllers_manager_.OnMetadata(player_id, has_audio, has_video,
media_content_type);
}
void MediaWebContentsObserver::OnMediaPlaying(
RenderFrameHost* render_frame_host,
int delegate_id) {
const MediaPlayerId player_id(render_frame_host, delegate_id);
PlayerInfo* player_info = GetPlayerInfo(player_id);
if (!player_info)
return;
if (!session_controllers_manager_.RequestPlay(player_id)) {
// Return early to avoid spamming WebContents with playing/stopped
// notifications. If RequestPlay() fails, media session will send a pause
// signal right away.
return;
}
if (!player_info->is_playing())
player_info->SetIsPlaying();
}
void MediaWebContentsObserver::OnMediaEffectivelyFullscreenChanged(
RenderFrameHost* render_frame_host,
int delegate_id,
blink::WebFullscreenVideoStatus fullscreen_status) {
const MediaPlayerId id(render_frame_host, delegate_id);
switch (fullscreen_status) {
case blink::WebFullscreenVideoStatus::kFullscreenAndPictureInPictureEnabled:
fullscreen_player_ = id;
picture_in_picture_allowed_in_fullscreen_ = true;
break;
case blink::WebFullscreenVideoStatus::
kFullscreenAndPictureInPictureDisabled:
fullscreen_player_ = id;
picture_in_picture_allowed_in_fullscreen_ = false;
break;
case blink::WebFullscreenVideoStatus::kNotEffectivelyFullscreen:
if (!fullscreen_player_ || *fullscreen_player_ != id)
return;
picture_in_picture_allowed_in_fullscreen_.reset();
fullscreen_player_.reset();
break;
}
bool is_fullscreen =
(fullscreen_status !=
blink::WebFullscreenVideoStatus::kNotEffectivelyFullscreen);
web_contents_impl()->MediaEffectivelyFullscreenChanged(is_fullscreen);
}
void MediaWebContentsObserver::OnAudioOutputSinkChanged(
RenderFrameHost* render_frame_host,
int delegate_id,
std::string hashed_device_id) {
auto salt_and_origin = content::GetMediaDeviceSaltAndOrigin(
render_frame_host->GetProcess()->GetID(),
render_frame_host->GetRoutingID());
auto callback_on_io_thread = base::BindOnce(
[](const std::string& salt, const url::Origin& origin,
const std::string& hashed_device_id,
base::OnceCallback<void(const base::Optional<std::string>&)>
callback) {
MediaStreamManager::GetMediaDeviceIDForHMAC(
blink::mojom::MediaDeviceType::MEDIA_AUDIO_OUTPUT, salt,
std::move(origin), hashed_device_id,
base::SequencedTaskRunnerHandle::Get(), std::move(callback));
},
salt_and_origin.device_id_salt, std::move(salt_and_origin.origin),
hashed_device_id,
base::BindOnce(&OnAudioOutputDeviceIdTranslated,
weak_ptr_factory_.GetWeakPtr(), render_frame_host,
delegate_id));
content::GetIOThreadTaskRunner({})->PostTask(
FROM_HERE, std::move(callback_on_io_thread));
}
void MediaWebContentsObserver::OnReceivedTranslatedDeviceId(
RenderFrameHost* render_frame_host,
int delegate_id,
const std::string& raw_device_id) {
session_controllers_manager_.OnAudioOutputSinkChanged(
MediaPlayerId(render_frame_host, delegate_id), raw_device_id);
}
mojo::Remote<media::mojom::MediaPlayer>&
MediaWebContentsObserver::GetMediaPlayerRemote(const MediaPlayerId& player_id) {
DCHECK(media_player_remotes_.contains(player_id));
DCHECK(media_player_remotes_[player_id].is_bound());
return media_player_remotes_[player_id];
}
void MediaWebContentsObserver::OnMediaPlayerHostDisconnected(
RenderFrameHost* host) {
DCHECK(media_player_hosts_.contains(host));
media_player_hosts_.erase(host);
}
void MediaWebContentsObserver::OnMediaPlayerObserverDisconnected(
const MediaPlayerId& player_id) {
DCHECK(media_player_observer_hosts_.contains(player_id));
media_player_observer_hosts_.erase(player_id);
}
device::mojom::WakeLock* MediaWebContentsObserver::GetAudioWakeLock() {
// Here is a lazy binding, and will not reconnect after connection error.
if (!audio_wake_lock_) {
mojo::PendingReceiver<device::mojom::WakeLock> receiver =
audio_wake_lock_.BindNewPipeAndPassReceiver();
device::mojom::WakeLockContext* wake_lock_context =
web_contents()->GetWakeLockContext();
if (wake_lock_context) {
wake_lock_context->GetWakeLock(
device::mojom::WakeLockType::kPreventAppSuspension,
device::mojom::WakeLockReason::kAudioPlayback, "Playing audio",
std::move(receiver));
}
}
return audio_wake_lock_.get();
}
void MediaWebContentsObserver::LockAudio() {
GetAudioWakeLock()->RequestWakeLock();
has_audio_wake_lock_for_testing_ = true;
}
void MediaWebContentsObserver::CancelAudioLock() {
GetAudioWakeLock()->CancelWakeLock();
has_audio_wake_lock_for_testing_ = false;
}
WebContentsImpl* MediaWebContentsObserver::web_contents_impl() const {
return static_cast<WebContentsImpl*>(web_contents());
}
void MediaWebContentsObserver::BindMediaPlayerHost(
RenderFrameHost* host,
mojo::PendingReceiver<media::mojom::MediaPlayerHost> player_receiver) {
if (!media_player_hosts_.contains(host)) {
media_player_hosts_[host] =
std::make_unique<MediaPlayerHostImpl>(host, this);
}
media_player_hosts_[host]->BindMediaPlayerHostReceiver(
std::move(player_receiver));
}
void MediaWebContentsObserver::SetMediaPlayerObserverForMediaPlayer(
const MediaPlayerId& player_id) {
if (!media_player_observer_hosts_.contains(player_id)) {
media_player_observer_hosts_[player_id] =
std::make_unique<MediaPlayerObserverHostImpl>(player_id, this);
}
DCHECK(media_player_remotes_[player_id]);
media_player_remotes_[player_id]->SetMediaPlayerObserver(
media_player_observer_hosts_[player_id]
->BindMediaPlayerObserverReceiverAndPassRemote());
}
void MediaWebContentsObserver::OnMediaPlayerAdded(
mojo::PendingRemote<media::mojom::MediaPlayer> player_remote,
MediaPlayerId player_id) {
if (!media_player_remotes_.contains(player_id)) {
media_player_remotes_[player_id] =
mojo::Remote<media::mojom::MediaPlayer>();
}
media_player_remotes_[player_id].reset();
media_player_remotes_[player_id].Bind(std::move(player_remote));
media_player_remotes_[player_id].set_disconnect_handler(base::BindOnce(
[](MediaWebContentsObserver* observer, const MediaPlayerId& player_id) {
observer->media_player_remotes_.erase(player_id);
},
base::Unretained(this), player_id));
// Create a new MediaPlayerObserverHostImpl to be able to receive messages
// from the renderer process via the MediaPlayerObserver mojo interface.
SetMediaPlayerObserverForMediaPlayer(player_id);
}
#if defined(OS_ANDROID)
void MediaWebContentsObserver::SuspendAllMediaPlayers() {
web_contents()->ForEachFrame(
base::BindRepeating(&SuspendAllMediaPlayersInRenderFrame));
}
#endif // defined(OS_ANDROID)
void MediaWebContentsObserver::OnExperimentStateChanged(MediaPlayerId id,
bool is_starting) {
use_after_free_checker_.check();
id.render_frame_host->Send(
new MediaPlayerDelegateMsg_NotifyPowerExperimentState(
id.render_frame_host->GetRoutingID(), id.delegate_id, is_starting));
}
base::WeakPtr<MediaWebContentsObserver>
MediaWebContentsObserver::GetWeakPtrForFrame(
RenderFrameHost* render_frame_host) {
auto iter = per_frame_factory_.find(render_frame_host);
if (iter != per_frame_factory_.end())
return iter->second->GetWeakPtr();
auto result = per_frame_factory_.emplace(std::make_pair(
render_frame_host,
std::make_unique<base::WeakPtrFactory<MediaWebContentsObserver>>(this)));
return result.first->second->GetWeakPtr();
}
} // namespace content