1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
5 * You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "MediaController.h"
9 #include "MediaControlService.h"
10 #include "MediaControlUtils.h"
11 #include "MediaControlKeySource.h"
12 #include "mozilla/AsyncEventDispatcher.h"
13 #include "mozilla/StaticPrefs_media.h"
14 #include "mozilla/dom/BrowsingContext.h"
15 #include "mozilla/dom/CanonicalBrowsingContext.h"
16 #include "mozilla/dom/MediaSession.h"
17 #include "mozilla/dom/PositionStateEvent.h"
19 // avoid redefined macro in unified build
21 #define LOG(msg, ...) \
22 MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
23 ("MediaController=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
26 namespace mozilla::dom
{
28 NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaController
, DOMEventTargetHelper
)
29 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(MediaController
,
31 nsITimerCallback
, nsINamed
)
32 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaController
,
34 NS_IMPL_CYCLE_COLLECTION_TRACE_END
36 nsISupports
* MediaController::GetParentObject() const {
37 RefPtr
<BrowsingContext
> bc
= BrowsingContext::Get(Id());
41 JSObject
* MediaController::WrapObject(JSContext
* aCx
,
42 JS::Handle
<JSObject
*> aGivenProto
) {
43 return MediaController_Binding::Wrap(aCx
, this, aGivenProto
);
46 void MediaController::GetSupportedKeys(
47 nsTArray
<MediaControlKey
>& aRetVal
) const {
49 for (const auto& key
: mSupportedKeys
) {
50 aRetVal
.AppendElement(key
);
54 void MediaController::GetMetadata(MediaMetadataInit
& aMetadata
,
56 if (!IsActive() || mShutdown
) {
57 aRv
.Throw(NS_ERROR_NOT_AVAILABLE
);
61 const MediaMetadataBase metadata
= GetCurrentMediaMetadata();
62 aMetadata
.mTitle
= metadata
.mTitle
;
63 aMetadata
.mArtist
= metadata
.mArtist
;
64 aMetadata
.mAlbum
= metadata
.mAlbum
;
65 for (const auto& artwork
: metadata
.mArtwork
) {
66 if (MediaImage
* image
= aMetadata
.mArtwork
.AppendElement(fallible
)) {
67 image
->mSrc
= artwork
.mSrc
;
68 image
->mSizes
= artwork
.mSizes
;
69 image
->mType
= artwork
.mType
;
71 aRv
.Throw(NS_ERROR_OUT_OF_MEMORY
);
77 static const MediaControlKey sDefaultSupportedKeys
[] = {
78 MediaControlKey::Focus
, MediaControlKey::Play
,
79 MediaControlKey::Pause
, MediaControlKey::Playpause
,
80 MediaControlKey::Stop
, MediaControlKey::Seekto
,
81 MediaControlKey::Seekforward
, MediaControlKey::Seekbackward
};
83 static void GetDefaultSupportedKeys(nsTArray
<MediaControlKey
>& aKeys
) {
84 for (const auto& key
: sDefaultSupportedKeys
) {
85 aKeys
.AppendElement(key
);
89 MediaController::MediaController(uint64_t aBrowsingContextId
)
90 : MediaStatusManager(aBrowsingContextId
) {
91 MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
92 "MediaController only runs on Chrome process!");
93 LOG("Create controller %" PRId64
, Id());
94 GetDefaultSupportedKeys(mSupportedKeys
);
95 mSupportedActionsChangedListener
= SupportedActionsChangedEvent().Connect(
96 AbstractThread::MainThread(), this,
97 &MediaController::HandleSupportedMediaSessionActionsChanged
);
98 mPlaybackChangedListener
= PlaybackChangedEvent().Connect(
99 AbstractThread::MainThread(), this,
100 &MediaController::HandleActualPlaybackStateChanged
);
101 mPositionStateChangedListener
= PositionChangedEvent().Connect(
102 AbstractThread::MainThread(), this,
103 &MediaController::HandlePositionStateChanged
);
104 mMetadataChangedListener
=
105 MetadataChangedEvent().Connect(AbstractThread::MainThread(), this,
106 &MediaController::HandleMetadataChanged
);
109 MediaController::~MediaController() {
110 LOG("Destroy controller %" PRId64
, Id());
116 void MediaController::Focus() {
118 UpdateMediaControlActionToContentMediaIfNeeded(
119 MediaControlAction(MediaControlKey::Focus
));
122 void MediaController::Play() {
124 UpdateMediaControlActionToContentMediaIfNeeded(
125 MediaControlAction(MediaControlKey::Play
));
128 void MediaController::Pause() {
130 UpdateMediaControlActionToContentMediaIfNeeded(
131 MediaControlAction(MediaControlKey::Pause
));
134 void MediaController::PrevTrack() {
136 UpdateMediaControlActionToContentMediaIfNeeded(
137 MediaControlAction(MediaControlKey::Previoustrack
));
140 void MediaController::NextTrack() {
142 UpdateMediaControlActionToContentMediaIfNeeded(
143 MediaControlAction(MediaControlKey::Nexttrack
));
146 void MediaController::SeekBackward(double aSeekOffset
) {
147 LOG("Seek Backward");
148 UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction(
149 MediaControlKey::Seekbackward
, SeekDetails(aSeekOffset
)));
152 void MediaController::SeekForward(double aSeekOffset
) {
154 UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction(
155 MediaControlKey::Seekforward
, SeekDetails(aSeekOffset
)));
158 void MediaController::SkipAd() {
160 UpdateMediaControlActionToContentMediaIfNeeded(
161 MediaControlAction(MediaControlKey::Skipad
));
164 void MediaController::SeekTo(double aSeekTime
, bool aFastSeek
) {
166 UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction(
167 MediaControlKey::Seekto
, SeekDetails(aSeekTime
, aFastSeek
)));
170 void MediaController::Stop() {
172 UpdateMediaControlActionToContentMediaIfNeeded(
173 MediaControlAction(MediaControlKey::Stop
));
174 MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded();
177 uint64_t MediaController::Id() const { return mTopLevelBrowsingContextId
; }
179 bool MediaController::IsAudible() const { return IsMediaAudible(); }
181 bool MediaController::IsPlaying() const { return IsMediaPlaying(); }
183 bool MediaController::IsActive() const { return mIsActive
; };
185 bool MediaController::ShouldPropagateActionToAllContexts(
186 const MediaControlAction
& aAction
) const {
187 // These actions have default action handler for each frame, so we
188 // need to propagate to all contexts. We would handle default handlers in
189 // `ContentMediaController::HandleMediaKey`.
190 if (aAction
.mKey
.isSome()) {
191 switch (aAction
.mKey
.value()) {
192 case MediaControlKey::Play
:
193 case MediaControlKey::Pause
:
194 case MediaControlKey::Stop
:
195 case MediaControlKey::Seekto
:
196 case MediaControlKey::Seekforward
:
197 case MediaControlKey::Seekbackward
:
206 void MediaController::UpdateMediaControlActionToContentMediaIfNeeded(
207 const MediaControlAction
& aAction
) {
208 // If the controller isn't active or it has been shutdown, we don't need to
209 // update media action to the content process.
210 if (!mIsActive
|| mShutdown
) {
214 // For some actions which have default action handler, we want to propagate
215 // them on all contexts in order to trigger the default handler on each
216 // context separately. Otherwise, other action should only be propagated to
217 // the context where active media session exists.
218 const bool propateToAll
= ShouldPropagateActionToAllContexts(aAction
);
219 const uint64_t targetContextId
= propateToAll
|| !mActiveMediaSessionContextId
221 : *mActiveMediaSessionContextId
;
222 RefPtr
<BrowsingContext
> context
= BrowsingContext::Get(targetContextId
);
223 if (!context
|| context
->IsDiscarded()) {
228 context
->PreOrderWalk([&](BrowsingContext
* bc
) {
229 bc
->Canonical()->UpdateMediaControlAction(aAction
);
232 context
->Canonical()->UpdateMediaControlAction(aAction
);
236 void MediaController::Shutdown() {
237 MOZ_ASSERT(!mShutdown
, "Do not call shutdown twice!");
238 // The media controller would be removed from the service when we receive a
239 // notification from the content process about all controlled media has been
240 // stoppped. However, if controlled media is stopped after detaching
241 // browsing context, then sending the notification from the content process
242 // would fail so that we are not able to notify the chrome process to remove
243 // the corresponding controller. Therefore, we should manually remove the
244 // controller from the service.
247 mSupportedActionsChangedListener
.DisconnectIfExists();
248 mPlaybackChangedListener
.DisconnectIfExists();
249 mPositionStateChangedListener
.DisconnectIfExists();
250 mMetadataChangedListener
.DisconnectIfExists();
253 void MediaController::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId
,
254 MediaPlaybackState aState
) {
258 MediaStatusManager::NotifyMediaPlaybackChanged(aBrowsingContextId
, aState
);
259 UpdateDeactivationTimerIfNeeded();
260 UpdateActivatedStateIfNeeded();
263 void MediaController::UpdateDeactivationTimerIfNeeded() {
264 if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) {
268 bool shouldBeAlwaysActive
= IsPlaying() || IsBeingUsedInPIPModeOrFullscreen();
269 if (shouldBeAlwaysActive
&& mDeactivationTimer
) {
270 LOG("Cancel deactivation timer");
271 mDeactivationTimer
->Cancel();
272 mDeactivationTimer
= nullptr;
273 } else if (!shouldBeAlwaysActive
&& !mDeactivationTimer
) {
274 nsresult rv
= NS_NewTimerWithCallback(
275 getter_AddRefs(mDeactivationTimer
), this,
276 StaticPrefs::media_mediacontrol_stopcontrol_timer_ms(),
277 nsITimer::TYPE_ONE_SHOT
, AbstractThread::MainThread());
278 if (NS_SUCCEEDED(rv
)) {
279 LOG("Create a deactivation timer");
281 LOG("Failed to create a deactivation timer");
286 bool MediaController::IsBeingUsedInPIPModeOrFullscreen() const {
287 return mIsInPictureInPictureMode
|| mIsInFullScreenMode
;
290 NS_IMETHODIMP
MediaController::Notify(nsITimer
* aTimer
) {
291 mDeactivationTimer
= nullptr;
292 if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) {
297 LOG("Cancel deactivation timer because controller has been shutdown");
301 // As the media being used in the PIP mode or fullscreen would always display
302 // on the screen, users would have high chance to interact with it again, so
303 // we don't want to stop media control.
304 if (IsBeingUsedInPIPModeOrFullscreen()) {
305 LOG("Cancel deactivation timer because controller is in PIP mode");
310 LOG("Cancel deactivation timer because controller is still playing");
315 LOG("Cancel deactivation timer because controller has been deactivated");
322 NS_IMETHODIMP
MediaController::GetName(nsACString
& aName
) {
323 aName
.AssignLiteral("MediaController");
327 void MediaController::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId
,
328 MediaAudibleState aState
) {
333 bool oldAudible
= IsAudible();
334 MediaStatusManager::NotifyMediaAudibleChanged(aBrowsingContextId
, aState
);
335 if (IsAudible() == oldAudible
) {
338 UpdateActivatedStateIfNeeded();
340 // Request the audio focus amongs different controllers that could cause
341 // pausing other audible controllers if we enable the audio focus management.
342 RefPtr
<MediaControlService
> service
= MediaControlService::GetService();
345 service
->GetAudioFocusManager().RequestAudioFocus(this);
347 service
->GetAudioFocusManager().RevokeAudioFocus(this);
351 bool MediaController::ShouldActivateController() const {
352 MOZ_ASSERT(!mShutdown
);
353 // After media is successfully loaded and match our critiera, such as its
354 // duration is longer enough, which is used to exclude the notification-ish
355 // sound, then it would be able to be controlled once the controll gets
358 // Activating a controller means that we would start to intercept the media
359 // keys on the platform and show the virtual control interface (if needed).
360 // The controller would be activated when (1) controllable media starts in the
361 // browsing context that controller belongs to (2) controllable media enters
362 // fullscreen or PIP mode.
363 return IsAnyMediaBeingControlled() &&
364 (IsPlaying() || IsBeingUsedInPIPModeOrFullscreen()) && !mIsActive
;
367 bool MediaController::ShouldDeactivateController() const {
368 MOZ_ASSERT(!mShutdown
);
369 // If we don't have an active media session and no controlled media exists,
370 // then we don't need to keep controller active, because there is nothing to
371 // control. However, if we still have an active media session, then we should
372 // keep controller active in order to receive media keys even if we don't have
373 // any controlled media existing, because a website might start other media
374 // when media session receives media keys.
375 return !IsAnyMediaBeingControlled() && mIsActive
&&
376 !mActiveMediaSessionContextId
;
379 void MediaController::Activate() {
380 MOZ_ASSERT(!mShutdown
);
381 RefPtr
<MediaControlService
> service
= MediaControlService::GetService();
382 if (service
&& !mIsActive
) {
384 mIsActive
= service
->RegisterActiveMediaController(this);
385 MOZ_ASSERT(mIsActive
, "Fail to register controller!");
386 DispatchAsyncEvent(u
"activated"_ns
);
390 void MediaController::Deactivate() {
391 MOZ_ASSERT(!mShutdown
);
392 RefPtr
<MediaControlService
> service
= MediaControlService::GetService();
394 service
->GetAudioFocusManager().RevokeAudioFocus(this);
397 mIsActive
= !service
->UnregisterActiveMediaController(this);
398 MOZ_ASSERT(!mIsActive
, "Fail to unregister controller!");
399 DispatchAsyncEvent(u
"deactivated"_ns
);
404 void MediaController::SetIsInPictureInPictureMode(
405 uint64_t aBrowsingContextId
, bool aIsInPictureInPictureMode
) {
406 if (mIsInPictureInPictureMode
== aIsInPictureInPictureMode
) {
409 LOG("Set IsInPictureInPictureMode to %s",
410 aIsInPictureInPictureMode
? "true" : "false");
411 mIsInPictureInPictureMode
= aIsInPictureInPictureMode
;
412 ForceToBecomeMainControllerIfNeeded();
413 UpdateDeactivationTimerIfNeeded();
414 mPictureInPictureModeChangedEvent
.Notify(mIsInPictureInPictureMode
);
417 void MediaController::NotifyMediaFullScreenState(uint64_t aBrowsingContextId
,
418 bool aIsInFullScreen
) {
419 if (mIsInFullScreenMode
== aIsInFullScreen
) {
422 LOG("%s fullscreen", aIsInFullScreen
? "Entered" : "Left");
423 mIsInFullScreenMode
= aIsInFullScreen
;
424 ForceToBecomeMainControllerIfNeeded();
425 mFullScreenChangedEvent
.Notify(mIsInFullScreenMode
);
428 bool MediaController::IsMainController() const {
429 RefPtr
<MediaControlService
> service
= MediaControlService::GetService();
430 return service
? service
->GetMainController() == this : false;
433 bool MediaController::ShouldRequestForMainController() const {
434 // This controller is already the main controller.
435 if (IsMainController()) {
438 // We would only force controller to become main controller if it's in the
439 // PIP mode or fullscreen, otherwise it should follow the general rule.
440 // In addition, do nothing if the controller has been shutdowned.
441 return IsBeingUsedInPIPModeOrFullscreen() && !mShutdown
;
444 void MediaController::ForceToBecomeMainControllerIfNeeded() {
445 if (!ShouldRequestForMainController()) {
448 RefPtr
<MediaControlService
> service
= MediaControlService::GetService();
449 MOZ_ASSERT(service
, "service was shutdown before shutting down controller?");
450 // If the controller hasn't been activated and it's ready to be activated,
451 // then activating it should also make it become a main controller. If it's
452 // already activated but isn't a main controller yet, then explicitly request
454 if (!IsActive() && ShouldActivateController()) {
456 } else if (IsActive()) {
457 service
->RequestUpdateMainController(this);
461 void MediaController::HandleActualPlaybackStateChanged() {
462 // Media control service would like to know all controllers' playback state
463 // in order to decide which controller should be the main controller that is
464 // usually the last tab which plays media.
465 if (RefPtr
<MediaControlService
> service
= MediaControlService::GetService()) {
466 service
->NotifyControllerPlaybackStateChanged(this);
468 DispatchAsyncEvent(u
"playbackstatechange"_ns
);
471 void MediaController::UpdateActivatedStateIfNeeded() {
472 if (ShouldActivateController()) {
474 } else if (ShouldDeactivateController()) {
479 void MediaController::HandleSupportedMediaSessionActionsChanged(
480 const nsTArray
<MediaSessionAction
>& aSupportedAction
) {
481 // Convert actions to keys, some of them have been included in the supported
482 // keys, such as "play", "pause" and "stop".
483 nsTArray
<MediaControlKey
> newSupportedKeys
;
484 GetDefaultSupportedKeys(newSupportedKeys
);
485 for (const auto& action
: aSupportedAction
) {
486 MediaControlKey key
= ConvertMediaSessionActionToControlKey(action
);
487 if (!newSupportedKeys
.Contains(key
)) {
488 newSupportedKeys
.AppendElement(key
);
491 // As the supported key event should only be notified when supported keys
492 // change, so abort following steps if they don't change.
493 if (newSupportedKeys
== mSupportedKeys
) {
496 LOG("Supported keys changes");
497 mSupportedKeys
= newSupportedKeys
;
498 mSupportedKeysChangedEvent
.Notify(mSupportedKeys
);
499 RefPtr
<AsyncEventDispatcher
> asyncDispatcher
= new AsyncEventDispatcher(
500 this, u
"supportedkeyschange"_ns
, CanBubble::eYes
);
501 asyncDispatcher
->PostDOMEvent();
502 MediaController_Binding::ClearCachedSupportedKeysValue(this);
505 void MediaController::HandlePositionStateChanged(
506 const Maybe
<PositionState
>& aState
) {
511 PositionStateEventInit init
;
512 init
.mDuration
= aState
->mDuration
;
513 init
.mPlaybackRate
= aState
->mPlaybackRate
;
514 init
.mPosition
= aState
->mLastReportedPlaybackPosition
;
515 RefPtr
<PositionStateEvent
> event
=
516 PositionStateEvent::Constructor(this, u
"positionstatechange"_ns
, init
);
517 DispatchAsyncEvent(event
.forget());
520 void MediaController::HandleMetadataChanged(
521 const MediaMetadataBase
& aMetadata
) {
522 // The reason we don't append metadata with `metadatachange` event is that
523 // allocating artwork might fail if the memory is not enough, but for the
524 // event we are not able to throw an error. Therefore, we want to the listener
525 // to use `getMetadata()` to get metadata, because it would throw an error if
526 // we fail to allocate artwork.
527 DispatchAsyncEvent(u
"metadatachange"_ns
);
528 // If metadata change is because of resetting active media session, then we
529 // should check if controller needs to be deactivated.
530 if (ShouldDeactivateController()) {
535 void MediaController::DispatchAsyncEvent(const nsAString
& aName
) {
536 RefPtr
<Event
> event
= NS_NewDOMEvent(this, nullptr, nullptr);
537 event
->InitEvent(aName
, false, false);
538 event
->SetTrusted(true);
539 DispatchAsyncEvent(event
.forget());
542 void MediaController::DispatchAsyncEvent(already_AddRefed
<Event
> aEvent
) {
543 RefPtr
<Event
> event
= aEvent
;
545 nsAutoString eventType
;
546 event
->GetType(eventType
);
547 if (!mIsActive
&& !eventType
.EqualsLiteral("deactivated")) {
548 LOG("Only 'deactivated' can be dispatched on a deactivated controller, not "
550 NS_ConvertUTF16toUTF8(eventType
).get());
553 LOG("Dispatch event %s", NS_ConvertUTF16toUTF8(eventType
).get());
554 RefPtr
<AsyncEventDispatcher
> asyncDispatcher
=
555 new AsyncEventDispatcher(this, event
.forget());
556 asyncDispatcher
->PostDOMEvent();
559 CopyableTArray
<MediaControlKey
> MediaController::GetSupportedMediaKeys() const {
560 return mSupportedKeys
;
563 void MediaController::Select() const {
564 if (RefPtr
<BrowsingContext
> bc
= BrowsingContext::Get(Id())) {
565 bc
->Canonical()->AddPageAwakeRequest();
569 void MediaController::Unselect() const {
570 if (RefPtr
<BrowsingContext
> bc
= BrowsingContext::Get(Id())) {
571 bc
->Canonical()->RemovePageAwakeRequest();
575 } // namespace mozilla::dom