MediaStatusManager.cpp (19775B)
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 "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" 18 19 #ifdef MOZ_PLACES 20 # include "nsIFaviconService.h" 21 #endif // MOZ_PLACES 22 23 extern mozilla::LazyLogModule gMediaControlLog; 24 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__)) 30 31 namespace mozilla::dom { 32 33 static bool IsMetadataEmpty(const Maybe<MediaMetadataBase>& aMetadata) { 34 // Media session's metadata is null. 35 if (!aMetadata) { 36 return true; 37 } 38 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(); 44 } 45 46 MediaStatusManager::MediaStatusManager(uint64_t aBrowsingContextId) 47 : mTopLevelBrowsingContextId(aBrowsingContextId) { 48 MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(), 49 "MediaStatusManager only runs on Chrome process!"); 50 } 51 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); 61 } 62 } 63 64 void MediaStatusManager::NotifySessionCreated(uint64_t aBrowsingContextId) { 65 const bool created = mMediaSessionInfoMap.WithEntryHandle( 66 aBrowsingContextId, [&](auto&& entry) { 67 if (entry) return false; 68 69 LOG("Session %" PRIu64 " has been created", aBrowsingContextId); 70 entry.Insert(MediaSessionInfo::EmptyInfo()); 71 return true; 72 }); 73 74 if (created && IsSessionOwningAudioFocus(aBrowsingContextId)) { 75 // This can't be done from within the WithEntryHandle functor, since it 76 // accesses mMediaSessionInfoMap. 77 SetActiveMediaSessionContextId(aBrowsingContextId); 78 } 79 } 80 81 void MediaStatusManager::NotifySessionDestroyed(uint64_t aBrowsingContextId) { 82 if (mMediaSessionInfoMap.Remove(aBrowsingContextId)) { 83 LOG("Session %" PRIu64 " has been destroyed", aBrowsingContextId); 84 85 if (mActiveMediaSessionContextId && 86 *mActiveMediaSessionContextId == aBrowsingContextId) { 87 ClearActiveMediaSessionContextIdIfNeeded(); 88 } 89 } 90 } 91 92 void MediaStatusManager::UpdateMetadata( 93 uint64_t aBrowsingContextId, const Maybe<MediaMetadataBase>& aMetadata) { 94 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId); 95 if (!info) { 96 return; 97 } 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; 107 } 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()); 115 } 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); 120 } 121 } 122 } 123 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(); 130 } 131 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(); 137 } 138 139 // This owner has media session so it should become an active session context. 140 SetActiveMediaSessionContextId(*aBrowsingContextId); 141 } 142 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; 150 } 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); 161 } 162 } 163 } 164 165 void MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded() { 166 if (!mActiveMediaSessionContextId) { 167 return; 168 } 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); 178 } 179 } 180 } 181 182 void MediaStatusManager::StoreMediaSessionContextIdOnWindowContext() { 183 RefPtr<CanonicalBrowsingContext> bc = 184 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId); 185 if (bc && bc->GetTopWindowContext()) { 186 (void)bc->GetTopWindowContext()->SetActiveMediaSessionContextId( 187 mActiveMediaSessionContextId); 188 } 189 } 190 191 bool MediaStatusManager::IsSessionOwningAudioFocus( 192 uint64_t aBrowsingContextId) const { 193 Maybe<uint64_t> audioFocusContextId = 194 mPlaybackStatusDelegate.GetAudioFocusOwnerContextId(); 195 return audioFocusContextId ? *audioFocusContextId == aBrowsingContextId 196 : false; 197 } 198 199 MediaMetadataBase MediaStatusManager::CreateDefaultMetadata() const { 200 MediaMetadataBase metadata; 201 metadata.mTitle = GetDefaultTitle(); 202 metadata.mUrl = GetUrl(); 203 metadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL(); 204 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; 209 } 210 211 nsString MediaStatusManager::GetDefaultTitle() const { 212 RefPtr<MediaControlService> service = MediaControlService::GetService(); 213 nsString defaultTitle = service->GetFallbackTitle(); 214 215 RefPtr<CanonicalBrowsingContext> bc = 216 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId); 217 if (!bc) { 218 return defaultTitle; 219 } 220 221 RefPtr<WindowGlobalParent> globalParent = bc->GetCurrentWindowGlobal(); 222 if (!globalParent) { 223 return defaultTitle; 224 } 225 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); 236 } 237 return documentTitle.IsEmpty() ? defaultTitle : documentTitle; 238 } 239 240 nsCString MediaStatusManager::GetUrl() const { 241 nsCString defaultUrl; 242 243 RefPtr<CanonicalBrowsingContext> bc = 244 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId); 245 if (!bc) { 246 return defaultUrl; 247 } 248 249 RefPtr<WindowGlobalParent> globalParent = bc->GetCurrentWindowGlobal(); 250 if (!globalParent) { 251 return defaultUrl; 252 } 253 254 if (IsInPrivateBrowsing()) { 255 return defaultUrl; 256 } 257 258 nsIURI* documentURI = globalParent->GetDocumentURI(); 259 if (!documentURI) { 260 return defaultUrl; 261 } 262 263 return documentURI->GetSpecOrDefault(); 264 } 265 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); 272 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; 279 } 280 nsCOMPtr<nsIURI> processedURI; 281 regService->ConvertChromeURL(faviconURI, getter_AddRefs(processedURI)); 282 283 nsAutoCString spec; 284 if (NS_FAILED(processedURI->GetSpec(spec))) { 285 return u""_ns; 286 } 287 return NS_ConvertUTF8toUTF16(spec); 288 #else 289 return u""_ns; 290 #endif 291 } 292 293 void MediaStatusManager::SetDeclaredPlaybackState( 294 uint64_t aBrowsingContextId, MediaSessionPlaybackState aState) { 295 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId); 296 if (!info) { 297 return; 298 } 299 LOG("SetDeclaredPlaybackState from %s to %s", 300 ToMediaSessionPlaybackStateStr(info->mDeclaredPlaybackState), 301 ToMediaSessionPlaybackStateStr(aState)); 302 info->mDeclaredPlaybackState = aState; 303 UpdateActualPlaybackState(); 304 } 305 306 MediaSessionPlaybackState MediaStatusManager::GetCurrentDeclaredPlaybackState() 307 const { 308 if (!mActiveMediaSessionContextId) { 309 return MediaSessionPlaybackState::None; 310 } 311 return mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId) 312 .mDeclaredPlaybackState; 313 } 314 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); 321 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; 327 } 328 if (mPlaybackStatusDelegate.IsPlaying()) { 329 SetGuessedPlayState(MediaSessionPlaybackState::Playing); 330 } else { 331 SetGuessedPlayState(MediaSessionPlaybackState::Paused); 332 } 333 } 334 335 void MediaStatusManager::SetGuessedPlayState(MediaSessionPlaybackState aState) { 336 if (aState == mGuessedPlaybackState) { 337 return; 338 } 339 LOG("SetGuessedPlayState : '%s'", ToMediaSessionPlaybackStateStr(aState)); 340 mGuessedPlaybackState = aState; 341 UpdateActualPlaybackState(); 342 } 343 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; 353 } 354 mActualPlaybackState = newState; 355 LOG("UpdateActualPlaybackState : '%s'", 356 ToMediaSessionPlaybackStateStr(mActualPlaybackState)); 357 mPlaybackStateChangedEvent.Notify(mActualPlaybackState); 358 } 359 360 void MediaStatusManager::EnableAction(uint64_t aBrowsingContextId, 361 MediaSessionAction aAction) { 362 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId); 363 if (!info) { 364 return; 365 } 366 if (info->IsActionSupported(aAction)) { 367 LOG("Action '%s' has already been enabled for context %" PRIu64, 368 GetEnumString(aAction).get(), aBrowsingContextId); 369 return; 370 } 371 LOG("Enable action %s for context %" PRIu64, GetEnumString(aAction).get(), 372 aBrowsingContextId); 373 info->EnableAction(aAction); 374 NotifySupportedKeysChangedIfNeeded(aBrowsingContextId); 375 } 376 377 void MediaStatusManager::DisableAction(uint64_t aBrowsingContextId, 378 MediaSessionAction aAction) { 379 auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId); 380 if (!info) { 381 return; 382 } 383 if (!info->IsActionSupported(aAction)) { 384 LOG("Action '%s' hasn't been enabled yet for context %" PRIu64, 385 GetEnumString(aAction).get(), aBrowsingContextId); 386 return; 387 } 388 LOG("Disable action %s for context %" PRIu64, GetEnumString(aAction).get(), 389 aBrowsingContextId); 390 info->DisableAction(aAction); 391 NotifySupportedKeysChangedIfNeeded(aBrowsingContextId); 392 } 393 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; 400 } 401 402 // The position state comes from non-active media session which we don't care. 403 if (!mActiveMediaSessionContextId || 404 *mActiveMediaSessionContextId != aBrowsingContextId) { 405 return; 406 } 407 mPositionStateChangedEvent.Notify(aState); 408 } 409 410 void MediaStatusManager::UpdateGuessedPositionState( 411 uint64_t aBrowsingContextId, const nsID& aMediaId, 412 const Maybe<PositionState>& aGuessedState) { 413 mPlaybackStatusDelegate.UpdateGuessedPositionState(aBrowsingContextId, 414 aMediaId, aGuessedState); 415 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; 421 } 422 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; 428 } 429 430 mPositionStateChangedEvent.Notify(GetCurrentPositionState()); 431 } 432 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; 441 } 442 mSupportedActionsChangedEvent.Notify(GetSupportedActions()); 443 } 444 445 CopyableTArray<MediaSessionAction> MediaStatusManager::GetSupportedActions() 446 const { 447 CopyableTArray<MediaSessionAction> supportedActions; 448 if (!mActiveMediaSessionContextId) { 449 return supportedActions; 450 } 451 452 MediaSessionInfo info = 453 mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId); 454 for (MediaSessionAction action : 455 MakeWebIDLEnumeratedRange<MediaSessionAction>()) { 456 if (info.IsActionSupported(action)) { 457 supportedActions.AppendElement(action); 458 } 459 } 460 return supportedActions; 461 } 462 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(); 473 } 474 MediaMetadataBase& metadata = *(info.mMetadata); 475 FillMissingTitleAndArtworkIfNeeded(metadata); 476 metadata.mUrl = GetUrl(); 477 return metadata; 478 } 479 return CreateDefaultMetadata(); 480 } 481 482 Maybe<PositionState> MediaStatusManager::GetCurrentPositionState() const { 483 if (mActiveMediaSessionContextId) { 484 auto info = mMediaSessionInfoMap.Lookup(*mActiveMediaSessionContextId); 485 if (info && info->mPositionState) { 486 return info->mPositionState; 487 } 488 } 489 490 return mPlaybackStatusDelegate.GuessedMediaPositionState( 491 mActiveMediaSessionContextId); 492 } 493 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(); 501 } 502 if (aMetadata.mArtwork.IsEmpty()) { 503 aMetadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL(); 504 } 505 } 506 507 bool MediaStatusManager::IsInPrivateBrowsing() const { 508 RefPtr<CanonicalBrowsingContext> bc = 509 CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId); 510 if (!bc) { 511 return false; 512 } 513 RefPtr<Element> element = bc->GetEmbedderElement(); 514 if (!element) { 515 return false; 516 } 517 return element->OwnerDoc()->IsInPrivateBrowsing(); 518 } 519 520 MediaSessionPlaybackState MediaStatusManager::PlaybackState() const { 521 return mActualPlaybackState; 522 } 523 524 bool MediaStatusManager::IsMediaAudible() const { 525 return mPlaybackStatusDelegate.IsAudible(); 526 } 527 528 bool MediaStatusManager::IsMediaPlaying() const { 529 return mActualPlaybackState == MediaSessionPlaybackState::Playing; 530 } 531 532 bool MediaStatusManager::IsAnyMediaBeingControlled() const { 533 return mPlaybackStatusDelegate.IsAnyMediaBeingControlled(); 534 } 535 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; 542 } 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; 547 } 548 LOG("page title changed, update default metadata"); 549 mMetadataChangedEvent.Notify(GetCurrentMediaMetadata()); 550 } 551 552 } // namespace mozilla::dom