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/. */
4 
5 #include "MediaStatusManager.h"
6 
7 #include "mozilla/dom/CanonicalBrowsingContext.h"
8 #include "mozilla/dom/MediaControlUtils.h"
9 #include "mozilla/dom/WindowGlobalParent.h"
10 #include "mozilla/StaticPrefs_media.h"
11 #include "nsIChromeRegistry.h"
12 #include "nsIObserverService.h"
13 #include "nsIXULAppInfo.h"
14 
15 #ifdef MOZ_PLACES
16 #  include "nsIFaviconService.h"
17 #endif  // MOZ_PLACES
18 
19 extern mozilla::LazyLogModule gMediaControlLog;
20 
21 // avoid redefined macro in unified build
22 #undef LOG
23 #define LOG(msg, ...)                        \
24   MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
25           ("MediaStatusManager=%p, " msg, this, ##__VA_ARGS__))
26 
27 namespace mozilla {
28 namespace dom {
29 
IsMetadataEmpty(const Maybe<MediaMetadataBase> & aMetadata)30 static bool IsMetadataEmpty(const Maybe<MediaMetadataBase>& aMetadata) {
31   // Media session's metadata is null.
32   if (!aMetadata) {
33     return true;
34   }
35 
36   // All attirbutes in metadata are empty.
37   // https://w3c.github.io/mediasession/#empty-metadata
38   const MediaMetadataBase& metadata = *aMetadata;
39   return metadata.mTitle.IsEmpty() && metadata.mArtist.IsEmpty() &&
40          metadata.mAlbum.IsEmpty() && metadata.mArtwork.IsEmpty();
41 }
42 
MediaStatusManager(uint64_t aBrowsingContextId)43 MediaStatusManager::MediaStatusManager(uint64_t aBrowsingContextId)
44     : mTopLevelBrowsingContextId(aBrowsingContextId) {
45   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
46                         "MediaStatusManager only runs on Chrome process!");
47 }
48 
NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,MediaAudibleState aState)49 void MediaStatusManager::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
50                                                    MediaAudibleState aState) {
51   Maybe<uint64_t> oldAudioFocusOwnerId =
52       mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
53   mPlaybackStatusDelegate.UpdateMediaAudibleState(aBrowsingContextId, aState);
54   Maybe<uint64_t> newAudioFocusOwnerId =
55       mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
56   if (oldAudioFocusOwnerId != newAudioFocusOwnerId) {
57     HandleAudioFocusOwnerChanged(newAudioFocusOwnerId);
58   }
59 }
60 
NotifySessionCreated(uint64_t aBrowsingContextId)61 void MediaStatusManager::NotifySessionCreated(uint64_t aBrowsingContextId) {
62   if (mMediaSessionInfoMap.Contains(aBrowsingContextId)) {
63     return;
64   }
65 
66   LOG("Session %" PRIu64 " has been created", aBrowsingContextId);
67   mMediaSessionInfoMap.Put(aBrowsingContextId, MediaSessionInfo::EmptyInfo());
68   if (IsSessionOwningAudioFocus(aBrowsingContextId)) {
69     SetActiveMediaSessionContextId(aBrowsingContextId);
70   }
71 }
72 
NotifySessionDestroyed(uint64_t aBrowsingContextId)73 void MediaStatusManager::NotifySessionDestroyed(uint64_t aBrowsingContextId) {
74   if (!mMediaSessionInfoMap.Contains(aBrowsingContextId)) {
75     return;
76   }
77   LOG("Session %" PRIu64 " has been destroyed", aBrowsingContextId);
78   mMediaSessionInfoMap.Remove(aBrowsingContextId);
79   if (mActiveMediaSessionContextId &&
80       *mActiveMediaSessionContextId == aBrowsingContextId) {
81     ClearActiveMediaSessionContextIdIfNeeded();
82   }
83 }
84 
UpdateMetadata(uint64_t aBrowsingContextId,const Maybe<MediaMetadataBase> & aMetadata)85 void MediaStatusManager::UpdateMetadata(
86     uint64_t aBrowsingContextId, const Maybe<MediaMetadataBase>& aMetadata) {
87   if (!mMediaSessionInfoMap.Contains(aBrowsingContextId)) {
88     return;
89   }
90 
91   MediaSessionInfo* info = mMediaSessionInfoMap.GetValue(aBrowsingContextId);
92   if (IsMetadataEmpty(aMetadata)) {
93     LOG("Reset metadata for session %" PRIu64, aBrowsingContextId);
94     info->mMetadata.reset();
95   } else {
96     LOG("Update metadata for session %" PRIu64 " title=%s artist=%s album=%s",
97         aBrowsingContextId, NS_ConvertUTF16toUTF8((*aMetadata).mTitle).get(),
98         NS_ConvertUTF16toUTF8(aMetadata->mArtist).get(),
99         NS_ConvertUTF16toUTF8(aMetadata->mAlbum).get());
100     info->mMetadata = aMetadata;
101   }
102   mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
103   if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
104     if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
105       obs->NotifyObservers(nullptr, "media-session-controller-metadata-changed",
106                            nullptr);
107     }
108   }
109 }
110 
HandleAudioFocusOwnerChanged(Maybe<uint64_t> & aBrowsingContextId)111 void MediaStatusManager::HandleAudioFocusOwnerChanged(
112     Maybe<uint64_t>& aBrowsingContextId) {
113   // No one is holding the audio focus.
114   if (!aBrowsingContextId) {
115     LOG("No one is owning audio focus");
116     return ClearActiveMediaSessionContextIdIfNeeded();
117   }
118 
119   // This owner of audio focus doesn't have media session, so we should deactive
120   // the active session because the active session must own the audio focus.
121   if (!mMediaSessionInfoMap.Contains(*aBrowsingContextId)) {
122     LOG("The owner of audio focus doesn't have media session");
123     return ClearActiveMediaSessionContextIdIfNeeded();
124   }
125 
126   // This owner has media session so it should become an active session context.
127   SetActiveMediaSessionContextId(*aBrowsingContextId);
128 }
129 
SetActiveMediaSessionContextId(uint64_t aBrowsingContextId)130 void MediaStatusManager::SetActiveMediaSessionContextId(
131     uint64_t aBrowsingContextId) {
132   if (mActiveMediaSessionContextId &&
133       *mActiveMediaSessionContextId == aBrowsingContextId) {
134     LOG("Active session context %" PRIu64 " keeps unchanged",
135         *mActiveMediaSessionContextId);
136     return;
137   }
138   mActiveMediaSessionContextId = Some(aBrowsingContextId);
139   LOG("context %" PRIu64 " becomes active session context",
140       *mActiveMediaSessionContextId);
141   mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
142 }
143 
ClearActiveMediaSessionContextIdIfNeeded()144 void MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded() {
145   if (!mActiveMediaSessionContextId) {
146     return;
147   }
148   LOG("Clear active session context");
149   mActiveMediaSessionContextId.reset();
150   mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
151 }
152 
IsSessionOwningAudioFocus(uint64_t aBrowsingContextId) const153 bool MediaStatusManager::IsSessionOwningAudioFocus(
154     uint64_t aBrowsingContextId) const {
155   Maybe<uint64_t> audioFocusContextId =
156       mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
157   return audioFocusContextId ? *audioFocusContextId == aBrowsingContextId
158                              : false;
159 }
160 
CreateDefaultMetadata() const161 MediaMetadataBase MediaStatusManager::CreateDefaultMetadata() const {
162   MediaMetadataBase metadata;
163   metadata.mTitle = GetDefaultTitle();
164   metadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL();
165 
166   LOG("Default media metadata, title=%s, album src=%s",
167       NS_ConvertUTF16toUTF8(metadata.mTitle).get(),
168       NS_ConvertUTF16toUTF8(metadata.mArtwork[0].mSrc).get());
169   return metadata;
170 }
171 
GetDefaultTitle() const172 nsString MediaStatusManager::GetDefaultTitle() const {
173   RefPtr<CanonicalBrowsingContext> bc =
174       CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
175   if (!bc) {
176     return EmptyString();
177   }
178 
179   RefPtr<WindowGlobalParent> globalParent = bc->GetCurrentWindowGlobal();
180   if (!globalParent) {
181     return EmptyString();
182   }
183 
184   // The media metadata would be shown on the virtual controller interface. For
185   // example, on Android, the interface would be shown on both notification bar
186   // and lockscreen. Therefore, what information we provide via metadata is
187   // quite important, because if we're in private browsing, we don't want to
188   // expose details about what website the user is browsing on the lockscreen.
189   nsString defaultTitle;
190   if (IsInPrivateBrowsing()) {
191     // TODO : maybe need l10n?
192     if (nsCOMPtr<nsIXULAppInfo> appInfo =
193             do_GetService("@mozilla.org/xre/app-info;1")) {
194       nsCString appName;
195       appInfo->GetName(appName);
196       CopyUTF8toUTF16(appName, defaultTitle);
197     } else {
198       defaultTitle.AssignLiteral("Firefox");
199     }
200     defaultTitle.AppendLiteral(" is playing media");
201   } else {
202     globalParent->GetDocumentTitle(defaultTitle);
203   }
204   return defaultTitle;
205 }
206 
GetDefaultFaviconURL() const207 nsString MediaStatusManager::GetDefaultFaviconURL() const {
208 #ifdef MOZ_PLACES
209   nsCOMPtr<nsIURI> faviconURI;
210   nsresult rv = NS_NewURI(getter_AddRefs(faviconURI),
211                           NS_LITERAL_CSTRING(FAVICON_DEFAULT_URL));
212   NS_ENSURE_SUCCESS(rv, NS_LITERAL_STRING(""));
213 
214   // Convert URI from `chrome://XXX` to `file://XXX` because we would like to
215   // let OS related frameworks, such as SMTC and MPRIS, handle this URL in order
216   // to show the icon on virtual controller interface.
217   nsCOMPtr<nsIChromeRegistry> regService = services::GetChromeRegistryService();
218   if (!regService) {
219     return EmptyString();
220   }
221   nsCOMPtr<nsIURI> processedURI;
222   regService->ConvertChromeURL(faviconURI, getter_AddRefs(processedURI));
223 
224   nsAutoCString spec;
225   if (NS_FAILED(processedURI->GetSpec(spec))) {
226     return EmptyString();
227   }
228   return NS_ConvertUTF8toUTF16(spec);
229 #endif
230   return EmptyString();
231 }
232 
SetDeclaredPlaybackState(uint64_t aBrowsingContextId,MediaSessionPlaybackState aState)233 void MediaStatusManager::SetDeclaredPlaybackState(
234     uint64_t aBrowsingContextId, MediaSessionPlaybackState aState) {
235   if (!mMediaSessionInfoMap.Contains(aBrowsingContextId)) {
236     return;
237   }
238   MediaSessionInfo* info = mMediaSessionInfoMap.GetValue(aBrowsingContextId);
239   LOG("SetDeclaredPlaybackState from %s to %s",
240       ToMediaSessionPlaybackStateStr(info->mDeclaredPlaybackState),
241       ToMediaSessionPlaybackStateStr(aState));
242   info->mDeclaredPlaybackState = aState;
243   UpdateActualPlaybackState();
244 }
245 
GetCurrentDeclaredPlaybackState() const246 MediaSessionPlaybackState MediaStatusManager::GetCurrentDeclaredPlaybackState()
247     const {
248   if (!mActiveMediaSessionContextId) {
249     return MediaSessionPlaybackState::None;
250   }
251   return mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId)
252       .mDeclaredPlaybackState;
253 }
254 
NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,MediaPlaybackState aState)255 void MediaStatusManager::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
256                                                     MediaPlaybackState aState) {
257   LOG("UpdateMediaPlaybackState %s for context %" PRIu64,
258       ToMediaPlaybackStateStr(aState), aBrowsingContextId);
259   const bool oldPlaying = mPlaybackStatusDelegate.IsPlaying();
260   mPlaybackStatusDelegate.UpdateMediaPlaybackState(aBrowsingContextId, aState);
261 
262   // Playback state doesn't change, we don't need to update the guessed playback
263   // state. This is used to prevent the state from changing from `none` to
264   // `paused` when receiving `MediaPlaybackState::eStarted`.
265   if (mPlaybackStatusDelegate.IsPlaying() == oldPlaying) {
266     return;
267   }
268   if (mPlaybackStatusDelegate.IsPlaying()) {
269     SetGuessedPlayState(MediaSessionPlaybackState::Playing);
270   } else {
271     SetGuessedPlayState(MediaSessionPlaybackState::Paused);
272   }
273 }
274 
SetGuessedPlayState(MediaSessionPlaybackState aState)275 void MediaStatusManager::SetGuessedPlayState(MediaSessionPlaybackState aState) {
276   if (aState == mGuessedPlaybackState) {
277     return;
278   }
279   LOG("SetGuessedPlayState : '%s'", ToMediaSessionPlaybackStateStr(aState));
280   mGuessedPlaybackState = aState;
281   UpdateActualPlaybackState();
282 }
283 
UpdateActualPlaybackState()284 void MediaStatusManager::UpdateActualPlaybackState() {
285   // The way to compute the actual playback state is based on the spec.
286   // https://w3c.github.io/mediasession/#actual-playback-state
287   MediaSessionPlaybackState newState =
288       GetCurrentDeclaredPlaybackState() == MediaSessionPlaybackState::Playing
289           ? MediaSessionPlaybackState::Playing
290           : mGuessedPlaybackState;
291   if (mActualPlaybackState == newState) {
292     return;
293   }
294   mActualPlaybackState = newState;
295   LOG("UpdateActualPlaybackState : '%s'",
296       ToMediaSessionPlaybackStateStr(mActualPlaybackState));
297   HandleActualPlaybackStateChanged();
298 }
299 
GetCurrentMediaMetadata() const300 MediaMetadataBase MediaStatusManager::GetCurrentMediaMetadata() const {
301   // If we don't have active media session, active media session doesn't have
302   // media metadata, or we're in private browsing mode, then we should create a
303   // default metadata which is using website's title and favicon as title and
304   // artwork.
305   if (mActiveMediaSessionContextId && !IsInPrivateBrowsing()) {
306     MediaSessionInfo info =
307         mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId);
308     if (!info.mMetadata) {
309       return CreateDefaultMetadata();
310     }
311     MediaMetadataBase& metadata = *(info.mMetadata);
312     FillMissingTitleAndArtworkIfNeeded(metadata);
313     return metadata;
314   }
315   return CreateDefaultMetadata();
316 }
317 
FillMissingTitleAndArtworkIfNeeded(MediaMetadataBase & aMetadata) const318 void MediaStatusManager::FillMissingTitleAndArtworkIfNeeded(
319     MediaMetadataBase& aMetadata) const {
320   // If the metadata doesn't set its title and artwork properly, we would like
321   // to use default title and favicon instead in order to prevent showing
322   // nothing on the virtual control interface.
323   if (aMetadata.mTitle.IsEmpty()) {
324     aMetadata.mTitle = GetDefaultTitle();
325   }
326   if (aMetadata.mArtwork.IsEmpty()) {
327     aMetadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL();
328   }
329 }
330 
IsInPrivateBrowsing() const331 bool MediaStatusManager::IsInPrivateBrowsing() const {
332   RefPtr<CanonicalBrowsingContext> bc =
333       CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
334   if (!bc) {
335     return false;
336   }
337   RefPtr<Element> element = bc->GetEmbedderElement();
338   if (!element) {
339     return false;
340   }
341   return nsContentUtils::IsInPrivateBrowsing(element->OwnerDoc());
342 }
343 
GetState() const344 MediaSessionPlaybackState MediaStatusManager::GetState() const {
345   return mActualPlaybackState;
346 }
347 
IsMediaAudible() const348 bool MediaStatusManager::IsMediaAudible() const {
349   return mPlaybackStatusDelegate.IsAudible();
350 }
351 
IsMediaPlaying() const352 bool MediaStatusManager::IsMediaPlaying() const {
353   return mActualPlaybackState == MediaSessionPlaybackState::Playing;
354 }
355 
IsAnyMediaBeingControlled() const356 bool MediaStatusManager::IsAnyMediaBeingControlled() const {
357   return mPlaybackStatusDelegate.IsAnyMediaBeingControlled();
358 }
359 
360 }  // namespace dom
361 }  // namespace mozilla
362