1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 #include "ContentMediaController.h"
7 #include "MediaControlUtils.h"
8 #include "mozilla/ClearOnShutdown.h"
9 #include "mozilla/StaticPtr.h"
10 #include "mozilla/Telemetry.h"
11 #include "mozilla/ToString.h"
12 #include "mozilla/dom/BrowsingContext.h"
13 #include "mozilla/dom/CanonicalBrowsingContext.h"
14 #include "mozilla/dom/ContentChild.h"
15 #include "nsGlobalWindowInner.h"
17 namespace mozilla::dom
{
20 #define LOG(msg, ...) \
21 MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
22 ("ContentMediaController=%p, " msg, this, ##__VA_ARGS__))
24 static Maybe
<bool> sXPCOMShutdown
;
26 static void InitXPCOMShutdownMonitor() {
30 sXPCOMShutdown
.emplace(false);
31 RunOnShutdown([&] { sXPCOMShutdown
= Some(true); });
34 static ContentMediaController
* GetContentMediaControllerFromBrowsingContext(
35 BrowsingContext
* aBrowsingContext
) {
36 MOZ_ASSERT(NS_IsMainThread());
37 InitXPCOMShutdownMonitor();
38 if (!aBrowsingContext
|| aBrowsingContext
->IsDiscarded()) {
42 nsPIDOMWindowOuter
* outer
= aBrowsingContext
->GetDOMWindow();
47 nsGlobalWindowInner
* inner
=
48 nsGlobalWindowInner::Cast(outer
->GetCurrentInnerWindow());
49 return inner
? inner
->GetContentMediaController() : nullptr;
52 static already_AddRefed
<BrowsingContext
> GetBrowsingContextForAgent(
53 uint64_t aBrowsingContextId
) {
54 // If XPCOM has been shutdown, then we're not able to access browsing context.
55 if (sXPCOMShutdown
&& *sXPCOMShutdown
) {
58 return BrowsingContext::Get(aBrowsingContextId
);
62 ContentMediaControlKeyReceiver
* ContentMediaControlKeyReceiver::Get(
63 BrowsingContext
* aBC
) {
64 MOZ_ASSERT(NS_IsMainThread());
65 return GetContentMediaControllerFromBrowsingContext(aBC
);
69 ContentMediaAgent
* ContentMediaAgent::Get(BrowsingContext
* aBC
) {
70 MOZ_ASSERT(NS_IsMainThread());
71 return GetContentMediaControllerFromBrowsingContext(aBC
);
74 void ContentMediaAgent::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId
,
75 MediaPlaybackState aState
) {
76 MOZ_ASSERT(NS_IsMainThread());
77 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
78 if (!bc
|| bc
->IsDiscarded()) {
82 LOG("Notify media %s in BC %" PRId64
, ToString(aState
).c_str(), bc
->Id());
83 if (XRE_IsContentProcess()) {
84 ContentChild
* contentChild
= ContentChild::GetSingleton();
85 Unused
<< contentChild
->SendNotifyMediaPlaybackChanged(bc
, aState
);
87 // Currently this only happen when we disable e10s, otherwise all controlled
88 // media would be run in the content process.
89 if (RefPtr
<IMediaInfoUpdater
> updater
=
90 bc
->Canonical()->GetMediaController()) {
91 updater
->NotifyMediaPlaybackChanged(bc
->Id(), aState
);
96 void ContentMediaAgent::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId
,
97 MediaAudibleState aState
) {
98 MOZ_ASSERT(NS_IsMainThread());
99 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
100 if (!bc
|| bc
->IsDiscarded()) {
104 LOG("Notify media became %s in BC %" PRId64
,
105 aState
== MediaAudibleState::eAudible
? "audible" : "inaudible",
107 if (XRE_IsContentProcess()) {
108 ContentChild
* contentChild
= ContentChild::GetSingleton();
109 Unused
<< contentChild
->SendNotifyMediaAudibleChanged(bc
, aState
);
111 // Currently this only happen when we disable e10s, otherwise all controlled
112 // media would be run in the content process.
113 if (RefPtr
<IMediaInfoUpdater
> updater
=
114 bc
->Canonical()->GetMediaController()) {
115 updater
->NotifyMediaAudibleChanged(bc
->Id(), aState
);
120 void ContentMediaAgent::SetIsInPictureInPictureMode(
121 uint64_t aBrowsingContextId
, bool aIsInPictureInPictureMode
) {
122 MOZ_ASSERT(NS_IsMainThread());
123 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
124 if (!bc
|| bc
->IsDiscarded()) {
128 LOG("Notify media Picture-in-Picture mode '%s' in BC %" PRId64
,
129 aIsInPictureInPictureMode
? "enabled" : "disabled", bc
->Id());
130 if (XRE_IsContentProcess()) {
131 ContentChild
* contentChild
= ContentChild::GetSingleton();
132 Unused
<< contentChild
->SendNotifyPictureInPictureModeChanged(
133 bc
, aIsInPictureInPictureMode
);
135 // Currently this only happen when we disable e10s, otherwise all controlled
136 // media would be run in the content process.
137 if (RefPtr
<IMediaInfoUpdater
> updater
=
138 bc
->Canonical()->GetMediaController()) {
139 updater
->SetIsInPictureInPictureMode(bc
->Id(), aIsInPictureInPictureMode
);
144 void ContentMediaAgent::SetDeclaredPlaybackState(
145 uint64_t aBrowsingContextId
, MediaSessionPlaybackState aState
) {
146 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
147 if (!bc
|| bc
->IsDiscarded()) {
151 LOG("Notify declared playback state '%s' in BC %" PRId64
,
152 ToMediaSessionPlaybackStateStr(aState
), bc
->Id());
153 if (XRE_IsContentProcess()) {
154 ContentChild
* contentChild
= ContentChild::GetSingleton();
155 Unused
<< contentChild
->SendNotifyMediaSessionPlaybackStateChanged(bc
,
159 // This would only happen when we disable e10s.
160 if (RefPtr
<IMediaInfoUpdater
> updater
=
161 bc
->Canonical()->GetMediaController()) {
162 updater
->SetDeclaredPlaybackState(bc
->Id(), aState
);
166 void ContentMediaAgent::NotifySessionCreated(uint64_t aBrowsingContextId
) {
167 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
168 if (!bc
|| bc
->IsDiscarded()) {
172 LOG("Notify media session being created in BC %" PRId64
, bc
->Id());
173 if (XRE_IsContentProcess()) {
174 ContentChild
* contentChild
= ContentChild::GetSingleton();
175 Unused
<< contentChild
->SendNotifyMediaSessionUpdated(bc
, true);
178 // This would only happen when we disable e10s.
179 if (RefPtr
<IMediaInfoUpdater
> updater
=
180 bc
->Canonical()->GetMediaController()) {
181 updater
->NotifySessionCreated(bc
->Id());
185 void ContentMediaAgent::NotifySessionDestroyed(uint64_t aBrowsingContextId
) {
186 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
187 if (!bc
|| bc
->IsDiscarded()) {
191 LOG("Notify media session being destroyed in BC %" PRId64
, bc
->Id());
192 if (XRE_IsContentProcess()) {
193 ContentChild
* contentChild
= ContentChild::GetSingleton();
194 Unused
<< contentChild
->SendNotifyMediaSessionUpdated(bc
, false);
197 // This would only happen when we disable e10s.
198 if (RefPtr
<IMediaInfoUpdater
> updater
=
199 bc
->Canonical()->GetMediaController()) {
200 updater
->NotifySessionDestroyed(bc
->Id());
204 void ContentMediaAgent::UpdateMetadata(
205 uint64_t aBrowsingContextId
, const Maybe
<MediaMetadataBase
>& aMetadata
) {
206 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
207 if (!bc
|| bc
->IsDiscarded()) {
211 LOG("Notify media session metadata change in BC %" PRId64
, bc
->Id());
212 if (XRE_IsContentProcess()) {
213 ContentChild
* contentChild
= ContentChild::GetSingleton();
214 Unused
<< contentChild
->SendNotifyUpdateMediaMetadata(bc
, aMetadata
);
217 // This would only happen when we disable e10s.
218 if (RefPtr
<IMediaInfoUpdater
> updater
=
219 bc
->Canonical()->GetMediaController()) {
220 updater
->UpdateMetadata(bc
->Id(), aMetadata
);
224 void ContentMediaAgent::EnableAction(uint64_t aBrowsingContextId
,
225 MediaSessionAction aAction
) {
226 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
227 if (!bc
|| bc
->IsDiscarded()) {
231 LOG("Notify to enable action '%s' in BC %" PRId64
,
232 GetEnumString(aAction
).get(), bc
->Id());
233 if (XRE_IsContentProcess()) {
234 ContentChild
* contentChild
= ContentChild::GetSingleton();
235 Unused
<< contentChild
->SendNotifyMediaSessionSupportedActionChanged(
239 // This would only happen when we disable e10s.
240 if (RefPtr
<IMediaInfoUpdater
> updater
=
241 bc
->Canonical()->GetMediaController()) {
242 updater
->EnableAction(bc
->Id(), aAction
);
246 void ContentMediaAgent::DisableAction(uint64_t aBrowsingContextId
,
247 MediaSessionAction aAction
) {
248 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
249 if (!bc
|| bc
->IsDiscarded()) {
253 LOG("Notify to disable action '%s' in BC %" PRId64
,
254 GetEnumString(aAction
).get(), bc
->Id());
255 if (XRE_IsContentProcess()) {
256 ContentChild
* contentChild
= ContentChild::GetSingleton();
257 Unused
<< contentChild
->SendNotifyMediaSessionSupportedActionChanged(
261 // This would only happen when we disable e10s.
262 if (RefPtr
<IMediaInfoUpdater
> updater
=
263 bc
->Canonical()->GetMediaController()) {
264 updater
->DisableAction(bc
->Id(), aAction
);
268 void ContentMediaAgent::NotifyMediaFullScreenState(uint64_t aBrowsingContextId
,
269 bool aIsInFullScreen
) {
270 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
271 if (!bc
|| bc
->IsDiscarded()) {
275 LOG("Notify %s fullscreen in BC %" PRId64
,
276 aIsInFullScreen
? "entered" : "left", bc
->Id());
277 if (XRE_IsContentProcess()) {
278 ContentChild
* contentChild
= ContentChild::GetSingleton();
279 Unused
<< contentChild
->SendNotifyMediaFullScreenState(bc
, aIsInFullScreen
);
282 // This would only happen when we disable e10s.
283 if (RefPtr
<IMediaInfoUpdater
> updater
=
284 bc
->Canonical()->GetMediaController()) {
285 updater
->NotifyMediaFullScreenState(bc
->Id(), aIsInFullScreen
);
289 void ContentMediaAgent::UpdatePositionState(
290 uint64_t aBrowsingContextId
, const Maybe
<PositionState
>& aState
) {
291 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
292 if (!bc
|| bc
->IsDiscarded()) {
295 if (XRE_IsContentProcess()) {
296 ContentChild
* contentChild
= ContentChild::GetSingleton();
297 Unused
<< contentChild
->SendNotifyPositionStateChanged(bc
, aState
);
300 // This would only happen when we disable e10s.
301 if (RefPtr
<IMediaInfoUpdater
> updater
=
302 bc
->Canonical()->GetMediaController()) {
303 updater
->UpdatePositionState(bc
->Id(), aState
);
307 void ContentMediaAgent::UpdateGuessedPositionState(
308 uint64_t aBrowsingContextId
, const nsID
& aMediaId
,
309 const Maybe
<PositionState
>& aState
) {
310 RefPtr
<BrowsingContext
> bc
= GetBrowsingContextForAgent(aBrowsingContextId
);
311 if (!bc
|| bc
->IsDiscarded()) {
316 LOG("Update guessed position state for BC %" PRId64
317 " media id %s (duration=%f, playbackRate=%f, position=%f)",
318 bc
->Id(), aMediaId
.ToString().get(), aState
->mDuration
,
319 aState
->mPlaybackRate
, aState
->mLastReportedPlaybackPosition
);
321 LOG("Clear guessed position state for BC %" PRId64
" media id %s", bc
->Id(),
322 aMediaId
.ToString().get());
325 if (XRE_IsContentProcess()) {
326 ContentChild
* contentChild
= ContentChild::GetSingleton();
327 Unused
<< contentChild
->SendNotifyGuessedPositionStateChanged(bc
, aMediaId
,
331 // This would only happen when we disable e10s.
332 if (RefPtr
<IMediaInfoUpdater
> updater
=
333 bc
->Canonical()->GetMediaController()) {
334 updater
->UpdateGuessedPositionState(bc
->Id(), aMediaId
, aState
);
338 ContentMediaController::ContentMediaController(uint64_t aId
) {
339 LOG("Create content media controller for BC %" PRId64
, aId
);
342 void ContentMediaController::AddReceiver(
343 ContentMediaControlKeyReceiver
* aListener
) {
344 MOZ_ASSERT(NS_IsMainThread());
345 mReceivers
.AppendElement(aListener
);
348 void ContentMediaController::RemoveReceiver(
349 ContentMediaControlKeyReceiver
* aListener
) {
350 MOZ_ASSERT(NS_IsMainThread());
351 mReceivers
.RemoveElement(aListener
);
354 void ContentMediaController::HandleMediaKey(MediaControlKey aKey
,
355 Maybe
<SeekDetails
> aDetails
) {
356 MOZ_ASSERT(NS_IsMainThread());
357 if (mReceivers
.IsEmpty()) {
360 LOG("Handle '%s' event, receiver num=%zu", GetEnumString(aKey
).get(),
361 mReceivers
.Length());
362 // We have default handlers for these actions
363 // https://w3c.github.io/mediasession/#ref-for-dom-mediasessionaction-play%E2%91%A3
365 case MediaControlKey::Pause
:
368 case MediaControlKey::Play
:
369 case MediaControlKey::Stop
:
370 case MediaControlKey::Seekto
:
371 case MediaControlKey::Seekforward
:
372 case MediaControlKey::Seekbackward
:
373 // When receiving `Stop`, the amount of receiver would vary during the
374 // iteration, so we use the backward iteration to avoid accessing the
375 // index which is over the array length.
376 for (auto& receiver
: Reversed(mReceivers
)) {
377 receiver
->HandleMediaKey(aKey
, aDetails
);
381 MOZ_ASSERT_UNREACHABLE("Not supported media key for default handler");
385 void ContentMediaController::PauseOrStopMedia() {
386 // When receiving `pause`, if a page contains playing media and paused media
387 // at that moment, that means a user intends to pause those playing
388 // media, not the already paused ones. Then, we're going to stop those already
389 // paused media and keep those latest paused media in `mReceivers`.
390 // The reason for doing that is, when resuming paused media, we only want to
391 // resume latest paused media, not all media, in order to get a better user
392 // experience, which matches Chrome's behavior.
393 bool isAnyMediaPlaying
= false;
394 for (const auto& receiver
: mReceivers
) {
395 if (receiver
->IsPlaying()) {
396 isAnyMediaPlaying
= true;
401 for (auto& receiver
: Reversed(mReceivers
)) {
402 if (isAnyMediaPlaying
&& !receiver
->IsPlaying()) {
403 receiver
->HandleMediaKey(MediaControlKey::Stop
);
405 receiver
->HandleMediaKey(MediaControlKey::Pause
);
410 } // namespace mozilla::dom