Bug 1943650 - Command-line --help output misformatted after --dbus-service. r=emilio
[gecko.git] / dom / media / mediacontrol / MediaController.cpp
blob3f08d24d4ed56bb72ed513ed602b2c8fa48afe7b
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
20 #undef LOG
21 #define LOG(msg, ...) \
22 MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
23 ("MediaController=%p, Id=%" PRId64 ", " msg, this, this->Id(), \
24 ##__VA_ARGS__))
26 namespace mozilla::dom {
28 NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaController, DOMEventTargetHelper)
29 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(MediaController,
30 DOMEventTargetHelper,
31 nsITimerCallback, nsINamed)
32 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaController,
33 DOMEventTargetHelper)
34 NS_IMPL_CYCLE_COLLECTION_TRACE_END
36 nsISupports* MediaController::GetParentObject() const {
37 RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id());
38 return bc;
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 {
48 aRetVal.Clear();
49 for (const auto& key : mSupportedKeys) {
50 aRetVal.AppendElement(key);
54 void MediaController::GetMetadata(MediaMetadataInit& aMetadata,
55 ErrorResult& aRv) {
56 if (!IsActive() || mShutdown) {
57 aRv.Throw(NS_ERROR_NOT_AVAILABLE);
58 return;
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;
70 } else {
71 aRv.Throw(NS_ERROR_OUT_OF_MEMORY);
72 return;
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());
111 if (!mShutdown) {
112 Shutdown();
116 void MediaController::Focus() {
117 LOG("Focus");
118 UpdateMediaControlActionToContentMediaIfNeeded(
119 MediaControlAction(MediaControlKey::Focus));
122 void MediaController::Play() {
123 LOG("Play");
124 UpdateMediaControlActionToContentMediaIfNeeded(
125 MediaControlAction(MediaControlKey::Play));
128 void MediaController::Pause() {
129 LOG("Pause");
130 UpdateMediaControlActionToContentMediaIfNeeded(
131 MediaControlAction(MediaControlKey::Pause));
134 void MediaController::PrevTrack() {
135 LOG("Prev Track");
136 UpdateMediaControlActionToContentMediaIfNeeded(
137 MediaControlAction(MediaControlKey::Previoustrack));
140 void MediaController::NextTrack() {
141 LOG("Next Track");
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) {
153 LOG("Seek Forward");
154 UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction(
155 MediaControlKey::Seekforward, SeekDetails(aSeekOffset)));
158 void MediaController::SkipAd() {
159 LOG("Skip Ad");
160 UpdateMediaControlActionToContentMediaIfNeeded(
161 MediaControlAction(MediaControlKey::Skipad));
164 void MediaController::SeekTo(double aSeekTime, bool aFastSeek) {
165 LOG("Seek To");
166 UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction(
167 MediaControlKey::Seekto, SeekDetails(aSeekTime, aFastSeek)));
170 void MediaController::Stop() {
171 LOG("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:
198 return true;
199 default:
200 return false;
203 return false;
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) {
211 return;
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
220 ? Id()
221 : *mActiveMediaSessionContextId;
222 RefPtr<BrowsingContext> context = BrowsingContext::Get(targetContextId);
223 if (!context || context->IsDiscarded()) {
224 return;
227 if (propateToAll) {
228 context->PreOrderWalk([&](BrowsingContext* bc) {
229 bc->Canonical()->UpdateMediaControlAction(aAction);
231 } else {
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.
245 Deactivate();
246 mShutdown = true;
247 mSupportedActionsChangedListener.DisconnectIfExists();
248 mPlaybackChangedListener.DisconnectIfExists();
249 mPositionStateChangedListener.DisconnectIfExists();
250 mMetadataChangedListener.DisconnectIfExists();
253 void MediaController::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
254 MediaPlaybackState aState) {
255 if (mShutdown) {
256 return;
258 MediaStatusManager::NotifyMediaPlaybackChanged(aBrowsingContextId, aState);
259 UpdateDeactivationTimerIfNeeded();
260 UpdateActivatedStateIfNeeded();
263 void MediaController::UpdateDeactivationTimerIfNeeded() {
264 if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) {
265 return;
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");
280 } else {
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()) {
293 return NS_OK;
296 if (mShutdown) {
297 LOG("Cancel deactivation timer because controller has been shutdown");
298 return NS_OK;
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");
306 return NS_OK;
309 if (IsPlaying()) {
310 LOG("Cancel deactivation timer because controller is still playing");
311 return NS_OK;
314 if (!mIsActive) {
315 LOG("Cancel deactivation timer because controller has been deactivated");
316 return NS_OK;
318 Deactivate();
319 return NS_OK;
322 NS_IMETHODIMP MediaController::GetName(nsACString& aName) {
323 aName.AssignLiteral("MediaController");
324 return NS_OK;
327 void MediaController::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
328 MediaAudibleState aState) {
329 if (mShutdown) {
330 return;
333 bool oldAudible = IsAudible();
334 MediaStatusManager::NotifyMediaAudibleChanged(aBrowsingContextId, aState);
335 if (IsAudible() == oldAudible) {
336 return;
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();
343 MOZ_ASSERT(service);
344 if (IsAudible()) {
345 service->GetAudioFocusManager().RequestAudioFocus(this);
346 } else {
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
356 // activated.
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) {
383 LOG("Activate");
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();
393 if (service) {
394 service->GetAudioFocusManager().RevokeAudioFocus(this);
395 if (mIsActive) {
396 LOG("Deactivate");
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) {
407 return;
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) {
420 return;
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()) {
436 return false;
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()) {
446 return;
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
453 // it.
454 if (!IsActive() && ShouldActivateController()) {
455 Activate();
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()) {
473 Activate();
474 } else if (ShouldDeactivateController()) {
475 Deactivate();
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) {
494 return;
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) {
507 if (!aState) {
508 return;
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()) {
531 Deactivate();
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;
544 MOZ_ASSERT(event);
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 "
549 "'%s'",
550 NS_ConvertUTF16toUTF8(eventType).get());
551 return;
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