Bug 1943650 - Command-line --help output misformatted after --dbus-service. r=emilio
[gecko.git] / dom / media / mediacontrol / MediaStatusManager.cpp
blob9d62b302fb519df9c00e26e8876dc6adcbc0b448
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 "MediaStatusManager.h"
7 #include "MediaControlService.h"
8 #include "mozilla/StaticPrefs_media.h"
9 #include "mozilla/dom/CanonicalBrowsingContext.h"
10 #include "mozilla/dom/Element.h"
11 #include "mozilla/dom/MediaControlUtils.h"
12 #include "mozilla/dom/WindowGlobalParent.h"
13 #include "nsContentUtils.h"
14 #include "nsIChromeRegistry.h"
15 #include "nsIObserverService.h"
16 #include "nsIXULAppInfo.h"
17 #include "nsNetUtil.h"
19 #ifdef MOZ_PLACES
20 # include "nsIFaviconService.h"
21 #endif // MOZ_PLACES
23 extern mozilla::LazyLogModule gMediaControlLog;
25 // avoid redefined macro in unified build
26 #undef LOG
27 #define LOG(msg, ...) \
28 MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
29 ("MediaStatusManager=%p, " msg, this, ##__VA_ARGS__))
31 namespace mozilla::dom {
33 static bool IsMetadataEmpty(const Maybe<MediaMetadataBase>& aMetadata) {
34 // Media session's metadata is null.
35 if (!aMetadata) {
36 return true;
39 // All attirbutes in metadata are empty.
40 // https://w3c.github.io/mediasession/#empty-metadata
41 const MediaMetadataBase& metadata = *aMetadata;
42 return metadata.mTitle.IsEmpty() && metadata.mArtist.IsEmpty() &&
43 metadata.mAlbum.IsEmpty() && metadata.mArtwork.IsEmpty();
46 MediaStatusManager::MediaStatusManager(uint64_t aBrowsingContextId)
47 : mTopLevelBrowsingContextId(aBrowsingContextId) {
48 MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
49 "MediaStatusManager only runs on Chrome process!");
52 void MediaStatusManager::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
53 MediaAudibleState aState) {
54 Maybe<uint64_t> oldAudioFocusOwnerId =
55 mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
56 mPlaybackStatusDelegate.UpdateMediaAudibleState(aBrowsingContextId, aState);
57 Maybe<uint64_t> newAudioFocusOwnerId =
58 mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
59 if (oldAudioFocusOwnerId != newAudioFocusOwnerId) {
60 HandleAudioFocusOwnerChanged(newAudioFocusOwnerId);
64 void MediaStatusManager::NotifySessionCreated(uint64_t aBrowsingContextId) {
65 const bool created = mMediaSessionInfoMap.WithEntryHandle(
66 aBrowsingContextId, [&](auto&& entry) {
67 if (entry) return false;
69 LOG("Session %" PRIu64 " has been created", aBrowsingContextId);
70 entry.Insert(MediaSessionInfo::EmptyInfo());
71 return true;
72 });
74 if (created && IsSessionOwningAudioFocus(aBrowsingContextId)) {
75 // This can't be done from within the WithEntryHandle functor, since it
76 // accesses mMediaSessionInfoMap.
77 SetActiveMediaSessionContextId(aBrowsingContextId);
81 void MediaStatusManager::NotifySessionDestroyed(uint64_t aBrowsingContextId) {
82 if (mMediaSessionInfoMap.Remove(aBrowsingContextId)) {
83 LOG("Session %" PRIu64 " has been destroyed", aBrowsingContextId);
85 if (mActiveMediaSessionContextId &&
86 *mActiveMediaSessionContextId == aBrowsingContextId) {
87 ClearActiveMediaSessionContextIdIfNeeded();
92 void MediaStatusManager::UpdateMetadata(
93 uint64_t aBrowsingContextId, const Maybe<MediaMetadataBase>& aMetadata) {
94 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
95 if (!info) {
96 return;
98 if (IsMetadataEmpty(aMetadata)) {
99 LOG("Reset metadata for session %" PRIu64, aBrowsingContextId);
100 info->mMetadata.reset();
101 } else {
102 LOG("Update metadata for session %" PRIu64 " title=%s artist=%s album=%s",
103 aBrowsingContextId, NS_ConvertUTF16toUTF8((*aMetadata).mTitle).get(),
104 NS_ConvertUTF16toUTF8(aMetadata->mArtist).get(),
105 NS_ConvertUTF16toUTF8(aMetadata->mAlbum).get());
106 info->mMetadata = aMetadata;
108 // Only notify the event if the changed metadata belongs to the active media
109 // session.
110 if (mActiveMediaSessionContextId &&
111 *mActiveMediaSessionContextId == aBrowsingContextId) {
112 LOG("Notify metadata change for active session %" PRIu64,
113 aBrowsingContextId);
114 mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
116 if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
117 if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
118 obs->NotifyObservers(nullptr, "media-session-controller-metadata-changed",
119 nullptr);
124 void MediaStatusManager::HandleAudioFocusOwnerChanged(
125 Maybe<uint64_t>& aBrowsingContextId) {
126 // No one is holding the audio focus.
127 if (!aBrowsingContextId) {
128 LOG("No one is owning audio focus");
129 return ClearActiveMediaSessionContextIdIfNeeded();
132 // This owner of audio focus doesn't have media session, so we should deactive
133 // the active session because the active session must own the audio focus.
134 if (!mMediaSessionInfoMap.Contains(*aBrowsingContextId)) {
135 LOG("The owner of audio focus doesn't have media session");
136 return ClearActiveMediaSessionContextIdIfNeeded();
139 // This owner has media session so it should become an active session context.
140 SetActiveMediaSessionContextId(*aBrowsingContextId);
143 void MediaStatusManager::SetActiveMediaSessionContextId(
144 uint64_t aBrowsingContextId) {
145 if (mActiveMediaSessionContextId &&
146 *mActiveMediaSessionContextId == aBrowsingContextId) {
147 LOG("Active session context %" PRIu64 " keeps unchanged",
148 *mActiveMediaSessionContextId);
149 return;
151 mActiveMediaSessionContextId = Some(aBrowsingContextId);
152 StoreMediaSessionContextIdOnWindowContext();
153 LOG("context %" PRIu64 " becomes active session context",
154 *mActiveMediaSessionContextId);
155 mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
156 mSupportedActionsChangedEvent.Notify(GetSupportedActions());
157 mPositionStateChangedEvent.Notify(GetCurrentPositionState());
158 if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
159 if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
160 obs->NotifyObservers(nullptr, "active-media-session-changed", nullptr);
165 void MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded() {
166 if (!mActiveMediaSessionContextId) {
167 return;
169 LOG("Clear active session context");
170 mActiveMediaSessionContextId.reset();
171 StoreMediaSessionContextIdOnWindowContext();
172 mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
173 mSupportedActionsChangedEvent.Notify(GetSupportedActions());
174 mPositionStateChangedEvent.Notify(GetCurrentPositionState());
175 if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
176 if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
177 obs->NotifyObservers(nullptr, "active-media-session-changed", nullptr);
182 void MediaStatusManager::StoreMediaSessionContextIdOnWindowContext() {
183 RefPtr<CanonicalBrowsingContext> bc =
184 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
185 if (bc && bc->GetTopWindowContext()) {
186 Unused << bc->GetTopWindowContext()->SetActiveMediaSessionContextId(
187 mActiveMediaSessionContextId);
191 bool MediaStatusManager::IsSessionOwningAudioFocus(
192 uint64_t aBrowsingContextId) const {
193 Maybe<uint64_t> audioFocusContextId =
194 mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
195 return audioFocusContextId ? *audioFocusContextId == aBrowsingContextId
196 : false;
199 MediaMetadataBase MediaStatusManager::CreateDefaultMetadata() const {
200 MediaMetadataBase metadata;
201 metadata.mTitle = GetDefaultTitle();
202 metadata.mUrl = GetUrl();
203 metadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL();
205 LOG("Default media metadata, title=%s, album src=%s",
206 NS_ConvertUTF16toUTF8(metadata.mTitle).get(),
207 NS_ConvertUTF16toUTF8(metadata.mArtwork[0].mSrc).get());
208 return metadata;
211 nsString MediaStatusManager::GetDefaultTitle() const {
212 RefPtr<MediaControlService> service = MediaControlService::GetService();
213 nsString defaultTitle = service->GetFallbackTitle();
215 RefPtr<CanonicalBrowsingContext> bc =
216 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
217 if (!bc) {
218 return defaultTitle;
221 RefPtr<WindowGlobalParent> globalParent = bc->GetCurrentWindowGlobal();
222 if (!globalParent) {
223 return defaultTitle;
226 // The media metadata would be shown on the virtual controller interface. For
227 // example, on Android, the interface would be shown on both notification bar
228 // and lockscreen. Therefore, what information we provide via metadata is
229 // quite important, because if we're in private browsing, we don't want to
230 // expose details about what website the user is browsing on the lockscreen.
231 // Therefore, using the default title when in the private browsing or the
232 // document title is empty. Otherwise, use the document title.
233 nsString documentTitle;
234 if (!IsInPrivateBrowsing()) {
235 globalParent->GetDocumentTitle(documentTitle);
237 return documentTitle.IsEmpty() ? defaultTitle : documentTitle;
240 nsCString MediaStatusManager::GetUrl() const {
241 nsCString defaultUrl;
243 RefPtr<CanonicalBrowsingContext> bc =
244 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
245 if (!bc) {
246 return defaultUrl;
249 RefPtr<WindowGlobalParent> globalParent = bc->GetCurrentWindowGlobal();
250 if (!globalParent) {
251 return defaultUrl;
254 if (IsInPrivateBrowsing()) {
255 return defaultUrl;
258 nsIURI* documentURI = globalParent->GetDocumentURI();
259 if (!documentURI) {
260 return defaultUrl;
263 return documentURI->GetSpecOrDefault();
266 nsString MediaStatusManager::GetDefaultFaviconURL() const {
267 #ifdef MOZ_PLACES
268 nsCOMPtr<nsIURI> faviconURI;
269 nsresult rv = NS_NewURI(getter_AddRefs(faviconURI),
270 nsLiteralCString(FAVICON_DEFAULT_URL));
271 NS_ENSURE_SUCCESS(rv, u""_ns);
273 // Convert URI from `chrome://XXX` to `file://XXX` because we would like to
274 // let OS related frameworks, such as SMTC and MPRIS, handle this URL in order
275 // to show the icon on virtual controller interface.
276 nsCOMPtr<nsIChromeRegistry> regService = services::GetChromeRegistry();
277 if (!regService) {
278 return u""_ns;
280 nsCOMPtr<nsIURI> processedURI;
281 regService->ConvertChromeURL(faviconURI, getter_AddRefs(processedURI));
283 nsAutoCString spec;
284 if (NS_FAILED(processedURI->GetSpec(spec))) {
285 return u""_ns;
287 return NS_ConvertUTF8toUTF16(spec);
288 #else
289 return u""_ns;
290 #endif
293 void MediaStatusManager::SetDeclaredPlaybackState(
294 uint64_t aBrowsingContextId, MediaSessionPlaybackState aState) {
295 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
296 if (!info) {
297 return;
299 LOG("SetDeclaredPlaybackState from %s to %s",
300 ToMediaSessionPlaybackStateStr(info->mDeclaredPlaybackState),
301 ToMediaSessionPlaybackStateStr(aState));
302 info->mDeclaredPlaybackState = aState;
303 UpdateActualPlaybackState();
306 MediaSessionPlaybackState MediaStatusManager::GetCurrentDeclaredPlaybackState()
307 const {
308 if (!mActiveMediaSessionContextId) {
309 return MediaSessionPlaybackState::None;
311 return mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId)
312 .mDeclaredPlaybackState;
315 void MediaStatusManager::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
316 MediaPlaybackState aState) {
317 LOG("UpdateMediaPlaybackState %s for context %" PRIu64,
318 EnumValueToString(aState), aBrowsingContextId);
319 const bool oldPlaying = mPlaybackStatusDelegate.IsPlaying();
320 mPlaybackStatusDelegate.UpdateMediaPlaybackState(aBrowsingContextId, aState);
322 // Playback state doesn't change, we don't need to update the guessed playback
323 // state. This is used to prevent the state from changing from `none` to
324 // `paused` when receiving `MediaPlaybackState::eStarted`.
325 if (mPlaybackStatusDelegate.IsPlaying() == oldPlaying) {
326 return;
328 if (mPlaybackStatusDelegate.IsPlaying()) {
329 SetGuessedPlayState(MediaSessionPlaybackState::Playing);
330 } else {
331 SetGuessedPlayState(MediaSessionPlaybackState::Paused);
335 void MediaStatusManager::SetGuessedPlayState(MediaSessionPlaybackState aState) {
336 if (aState == mGuessedPlaybackState) {
337 return;
339 LOG("SetGuessedPlayState : '%s'", ToMediaSessionPlaybackStateStr(aState));
340 mGuessedPlaybackState = aState;
341 UpdateActualPlaybackState();
344 void MediaStatusManager::UpdateActualPlaybackState() {
345 // The way to compute the actual playback state is based on the spec.
346 // https://w3c.github.io/mediasession/#actual-playback-state
347 MediaSessionPlaybackState newState =
348 GetCurrentDeclaredPlaybackState() == MediaSessionPlaybackState::Playing
349 ? MediaSessionPlaybackState::Playing
350 : mGuessedPlaybackState;
351 if (mActualPlaybackState == newState) {
352 return;
354 mActualPlaybackState = newState;
355 LOG("UpdateActualPlaybackState : '%s'",
356 ToMediaSessionPlaybackStateStr(mActualPlaybackState));
357 mPlaybackStateChangedEvent.Notify(mActualPlaybackState);
360 void MediaStatusManager::EnableAction(uint64_t aBrowsingContextId,
361 MediaSessionAction aAction) {
362 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
363 if (!info) {
364 return;
366 if (info->IsActionSupported(aAction)) {
367 LOG("Action '%s' has already been enabled for context %" PRIu64,
368 GetEnumString(aAction).get(), aBrowsingContextId);
369 return;
371 LOG("Enable action %s for context %" PRIu64, GetEnumString(aAction).get(),
372 aBrowsingContextId);
373 info->EnableAction(aAction);
374 NotifySupportedKeysChangedIfNeeded(aBrowsingContextId);
377 void MediaStatusManager::DisableAction(uint64_t aBrowsingContextId,
378 MediaSessionAction aAction) {
379 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
380 if (!info) {
381 return;
383 if (!info->IsActionSupported(aAction)) {
384 LOG("Action '%s' hasn't been enabled yet for context %" PRIu64,
385 GetEnumString(aAction).get(), aBrowsingContextId);
386 return;
388 LOG("Disable action %s for context %" PRIu64, GetEnumString(aAction).get(),
389 aBrowsingContextId);
390 info->DisableAction(aAction);
391 NotifySupportedKeysChangedIfNeeded(aBrowsingContextId);
394 void MediaStatusManager::UpdatePositionState(
395 uint64_t aBrowsingContextId, const Maybe<PositionState>& aState) {
396 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
397 if (info) {
398 LOG("Update position state for context %" PRIu64, aBrowsingContextId);
399 info->mPositionState = aState;
402 // The position state comes from non-active media session which we don't care.
403 if (!mActiveMediaSessionContextId ||
404 *mActiveMediaSessionContextId != aBrowsingContextId) {
405 return;
407 mPositionStateChangedEvent.Notify(aState);
410 void MediaStatusManager::UpdateGuessedPositionState(
411 uint64_t aBrowsingContextId, const nsID& aMediaId,
412 const Maybe<PositionState>& aGuessedState) {
413 mPlaybackStatusDelegate.UpdateGuessedPositionState(aBrowsingContextId,
414 aMediaId, aGuessedState);
416 // The position state comes from a non-active media session and
417 // there is another one active (with some metadata).
418 if (mActiveMediaSessionContextId &&
419 *mActiveMediaSessionContextId != aBrowsingContextId) {
420 return;
423 // media session is declared for the updated session, but there's no active
424 // session - it will get emitted once the session becomes active
425 if (mMediaSessionInfoMap.Contains(aBrowsingContextId) &&
426 !mActiveMediaSessionContextId) {
427 return;
430 mPositionStateChangedEvent.Notify(GetCurrentPositionState());
433 void MediaStatusManager::NotifySupportedKeysChangedIfNeeded(
434 uint64_t aBrowsingContextId) {
435 // Only the active media session's supported actions would be shown in virtual
436 // control interface, so we only notify the event when supported actions
437 // change happens on the active media session.
438 if (!mActiveMediaSessionContextId ||
439 *mActiveMediaSessionContextId != aBrowsingContextId) {
440 return;
442 mSupportedActionsChangedEvent.Notify(GetSupportedActions());
445 CopyableTArray<MediaSessionAction> MediaStatusManager::GetSupportedActions()
446 const {
447 CopyableTArray<MediaSessionAction> supportedActions;
448 if (!mActiveMediaSessionContextId) {
449 return supportedActions;
452 MediaSessionInfo info =
453 mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId);
454 for (MediaSessionAction action :
455 MakeWebIDLEnumeratedRange<MediaSessionAction>()) {
456 if (info.IsActionSupported(action)) {
457 supportedActions.AppendElement(action);
460 return supportedActions;
463 MediaMetadataBase MediaStatusManager::GetCurrentMediaMetadata() const {
464 // If we don't have active media session, active media session doesn't have
465 // media metadata, or we're in private browsing mode, then we should create a
466 // default metadata which is using website's title and favicon as title and
467 // artwork.
468 if (mActiveMediaSessionContextId && !IsInPrivateBrowsing()) {
469 MediaSessionInfo info =
470 mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId);
471 if (!info.mMetadata) {
472 return CreateDefaultMetadata();
474 MediaMetadataBase& metadata = *(info.mMetadata);
475 FillMissingTitleAndArtworkIfNeeded(metadata);
476 metadata.mUrl = GetUrl();
477 return metadata;
479 return CreateDefaultMetadata();
482 Maybe<PositionState> MediaStatusManager::GetCurrentPositionState() const {
483 if (mActiveMediaSessionContextId) {
484 auto info = mMediaSessionInfoMap.Lookup(*mActiveMediaSessionContextId);
485 if (info && info->mPositionState) {
486 return info->mPositionState;
490 return mPlaybackStatusDelegate.GuessedMediaPositionState(
491 mActiveMediaSessionContextId);
494 void MediaStatusManager::FillMissingTitleAndArtworkIfNeeded(
495 MediaMetadataBase& aMetadata) const {
496 // If the metadata doesn't set its title and artwork properly, we would like
497 // to use default title and favicon instead in order to prevent showing
498 // nothing on the virtual control interface.
499 if (aMetadata.mTitle.IsEmpty()) {
500 aMetadata.mTitle = GetDefaultTitle();
502 if (aMetadata.mArtwork.IsEmpty()) {
503 aMetadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL();
507 bool MediaStatusManager::IsInPrivateBrowsing() const {
508 RefPtr<CanonicalBrowsingContext> bc =
509 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
510 if (!bc) {
511 return false;
513 RefPtr<Element> element = bc->GetEmbedderElement();
514 if (!element) {
515 return false;
517 return element->OwnerDoc()->IsInPrivateBrowsing();
520 MediaSessionPlaybackState MediaStatusManager::PlaybackState() const {
521 return mActualPlaybackState;
524 bool MediaStatusManager::IsMediaAudible() const {
525 return mPlaybackStatusDelegate.IsAudible();
528 bool MediaStatusManager::IsMediaPlaying() const {
529 return mActualPlaybackState == MediaSessionPlaybackState::Playing;
532 bool MediaStatusManager::IsAnyMediaBeingControlled() const {
533 return mPlaybackStatusDelegate.IsAnyMediaBeingControlled();
536 void MediaStatusManager::NotifyPageTitleChanged() {
537 // If active media session has set non-empty metadata, then we would use that
538 // instead of using default metadata.
539 if (mActiveMediaSessionContextId &&
540 mMediaSessionInfoMap.Lookup(*mActiveMediaSessionContextId)->mMetadata) {
541 return;
543 // In private browsing mode, we won't show page title on default metadata so
544 // we don't need to update that.
545 if (IsInPrivateBrowsing()) {
546 return;
548 LOG("page title changed, update default metadata");
549 mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
552 } // namespace mozilla::dom