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"
20 # include "nsIFaviconService.h"
23 extern mozilla::LazyLogModule gMediaControlLog
;
25 // avoid redefined macro in unified build
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.
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());
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
);
98 if (IsMetadataEmpty(aMetadata
)) {
99 LOG("Reset metadata for session %" PRIu64
, aBrowsingContextId
);
100 info
->mMetadata
.reset();
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
110 if (mActiveMediaSessionContextId
&&
111 *mActiveMediaSessionContextId
== aBrowsingContextId
) {
112 LOG("Notify metadata change for active session %" PRIu64
,
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",
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
);
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
) {
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
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());
211 nsString
MediaStatusManager::GetDefaultTitle() const {
212 RefPtr
<MediaControlService
> service
= MediaControlService::GetService();
213 nsString defaultTitle
= service
->GetFallbackTitle();
215 RefPtr
<CanonicalBrowsingContext
> bc
=
216 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId
);
221 RefPtr
<WindowGlobalParent
> globalParent
= bc
->GetCurrentWindowGlobal();
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
);
249 RefPtr
<WindowGlobalParent
> globalParent
= bc
->GetCurrentWindowGlobal();
254 if (IsInPrivateBrowsing()) {
258 nsIURI
* documentURI
= globalParent
->GetDocumentURI();
263 return documentURI
->GetSpecOrDefault();
266 nsString
MediaStatusManager::GetDefaultFaviconURL() const {
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();
280 nsCOMPtr
<nsIURI
> processedURI
;
281 regService
->ConvertChromeURL(faviconURI
, getter_AddRefs(processedURI
));
284 if (NS_FAILED(processedURI
->GetSpec(spec
))) {
287 return NS_ConvertUTF8toUTF16(spec
);
293 void MediaStatusManager::SetDeclaredPlaybackState(
294 uint64_t aBrowsingContextId
, MediaSessionPlaybackState aState
) {
295 auto info
= mMediaSessionInfoMap
.Lookup(aBrowsingContextId
);
299 LOG("SetDeclaredPlaybackState from %s to %s",
300 ToMediaSessionPlaybackStateStr(info
->mDeclaredPlaybackState
),
301 ToMediaSessionPlaybackStateStr(aState
));
302 info
->mDeclaredPlaybackState
= aState
;
303 UpdateActualPlaybackState();
306 MediaSessionPlaybackState
MediaStatusManager::GetCurrentDeclaredPlaybackState()
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
) {
328 if (mPlaybackStatusDelegate
.IsPlaying()) {
329 SetGuessedPlayState(MediaSessionPlaybackState::Playing
);
331 SetGuessedPlayState(MediaSessionPlaybackState::Paused
);
335 void MediaStatusManager::SetGuessedPlayState(MediaSessionPlaybackState aState
) {
336 if (aState
== mGuessedPlaybackState
) {
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
) {
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
);
366 if (info
->IsActionSupported(aAction
)) {
367 LOG("Action '%s' has already been enabled for context %" PRIu64
,
368 GetEnumString(aAction
).get(), aBrowsingContextId
);
371 LOG("Enable action %s for context %" PRIu64
, GetEnumString(aAction
).get(),
373 info
->EnableAction(aAction
);
374 NotifySupportedKeysChangedIfNeeded(aBrowsingContextId
);
377 void MediaStatusManager::DisableAction(uint64_t aBrowsingContextId
,
378 MediaSessionAction aAction
) {
379 auto info
= mMediaSessionInfoMap
.Lookup(aBrowsingContextId
);
383 if (!info
->IsActionSupported(aAction
)) {
384 LOG("Action '%s' hasn't been enabled yet for context %" PRIu64
,
385 GetEnumString(aAction
).get(), aBrowsingContextId
);
388 LOG("Disable action %s for context %" PRIu64
, GetEnumString(aAction
).get(),
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
);
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
) {
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
) {
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
) {
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
) {
442 mSupportedActionsChangedEvent
.Notify(GetSupportedActions());
445 CopyableTArray
<MediaSessionAction
> MediaStatusManager::GetSupportedActions()
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
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();
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
);
513 RefPtr
<Element
> element
= bc
->GetEmbedderElement();
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
) {
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()) {
548 LOG("page title changed, update default metadata");
549 mMetadataChangedEvent
.Notify(GetCurrentMediaMetadata());
552 } // namespace mozilla::dom