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