summaryrefslogtreecommitdiffstats
path: root/dom/media/mediacontrol/MediaStatusManager.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'dom/media/mediacontrol/MediaStatusManager.cpp')
-rw-r--r--dom/media/mediacontrol/MediaStatusManager.cpp482
1 files changed, 482 insertions, 0 deletions
diff --git a/dom/media/mediacontrol/MediaStatusManager.cpp b/dom/media/mediacontrol/MediaStatusManager.cpp
new file mode 100644
index 0000000000..4365e6b531
--- /dev/null
+++ b/dom/media/mediacontrol/MediaStatusManager.cpp
@@ -0,0 +1,482 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "MediaStatusManager.h"
+
+#include "MediaControlService.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/Element.h"
+#include "mozilla/dom/MediaControlUtils.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "nsContentUtils.h"
+#include "nsIChromeRegistry.h"
+#include "nsIObserverService.h"
+#include "nsIXULAppInfo.h"
+#include "nsNetUtil.h"
+
+#ifdef MOZ_PLACES
+# include "nsIFaviconService.h"
+#endif // MOZ_PLACES
+
+extern mozilla::LazyLogModule gMediaControlLog;
+
+// avoid redefined macro in unified build
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaStatusManager=%p, " msg, this, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+static bool IsMetadataEmpty(const Maybe<MediaMetadataBase>& aMetadata) {
+ // Media session's metadata is null.
+ if (!aMetadata) {
+ return true;
+ }
+
+ // All attirbutes in metadata are empty.
+ // https://w3c.github.io/mediasession/#empty-metadata
+ const MediaMetadataBase& metadata = *aMetadata;
+ return metadata.mTitle.IsEmpty() && metadata.mArtist.IsEmpty() &&
+ metadata.mAlbum.IsEmpty() && metadata.mArtwork.IsEmpty();
+}
+
+MediaStatusManager::MediaStatusManager(uint64_t aBrowsingContextId)
+ : mTopLevelBrowsingContextId(aBrowsingContextId) {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
+ "MediaStatusManager only runs on Chrome process!");
+}
+
+void MediaStatusManager::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId,
+ MediaAudibleState aState) {
+ Maybe<uint64_t> oldAudioFocusOwnerId =
+ mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
+ mPlaybackStatusDelegate.UpdateMediaAudibleState(aBrowsingContextId, aState);
+ Maybe<uint64_t> newAudioFocusOwnerId =
+ mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
+ if (oldAudioFocusOwnerId != newAudioFocusOwnerId) {
+ HandleAudioFocusOwnerChanged(newAudioFocusOwnerId);
+ }
+}
+
+void MediaStatusManager::NotifySessionCreated(uint64_t aBrowsingContextId) {
+ const bool created = mMediaSessionInfoMap.WithEntryHandle(
+ aBrowsingContextId, [&](auto&& entry) {
+ if (entry) return false;
+
+ LOG("Session %" PRIu64 " has been created", aBrowsingContextId);
+ entry.Insert(MediaSessionInfo::EmptyInfo());
+ return true;
+ });
+
+ if (created && IsSessionOwningAudioFocus(aBrowsingContextId)) {
+ // This can't be done from within the WithEntryHandle functor, since it
+ // accesses mMediaSessionInfoMap.
+ SetActiveMediaSessionContextId(aBrowsingContextId);
+ }
+}
+
+void MediaStatusManager::NotifySessionDestroyed(uint64_t aBrowsingContextId) {
+ if (mMediaSessionInfoMap.Remove(aBrowsingContextId)) {
+ LOG("Session %" PRIu64 " has been destroyed", aBrowsingContextId);
+
+ if (mActiveMediaSessionContextId &&
+ *mActiveMediaSessionContextId == aBrowsingContextId) {
+ ClearActiveMediaSessionContextIdIfNeeded();
+ }
+ }
+}
+
+void MediaStatusManager::UpdateMetadata(
+ uint64_t aBrowsingContextId, const Maybe<MediaMetadataBase>& aMetadata) {
+ auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
+ if (!info) {
+ return;
+ }
+ if (IsMetadataEmpty(aMetadata)) {
+ LOG("Reset metadata for session %" PRIu64, aBrowsingContextId);
+ info->mMetadata.reset();
+ } else {
+ LOG("Update metadata for session %" PRIu64 " title=%s artist=%s album=%s",
+ aBrowsingContextId, NS_ConvertUTF16toUTF8((*aMetadata).mTitle).get(),
+ NS_ConvertUTF16toUTF8(aMetadata->mArtist).get(),
+ NS_ConvertUTF16toUTF8(aMetadata->mAlbum).get());
+ info->mMetadata = aMetadata;
+ }
+ // Only notify the event if the changed metadata belongs to the active media
+ // session.
+ if (mActiveMediaSessionContextId &&
+ *mActiveMediaSessionContextId == aBrowsingContextId) {
+ LOG("Notify metadata change for active session %" PRIu64,
+ aBrowsingContextId);
+ mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
+ }
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-session-controller-metadata-changed",
+ nullptr);
+ }
+ }
+}
+
+void MediaStatusManager::HandleAudioFocusOwnerChanged(
+ Maybe<uint64_t>& aBrowsingContextId) {
+ // No one is holding the audio focus.
+ if (!aBrowsingContextId) {
+ LOG("No one is owning audio focus");
+ return ClearActiveMediaSessionContextIdIfNeeded();
+ }
+
+ // This owner of audio focus doesn't have media session, so we should deactive
+ // the active session because the active session must own the audio focus.
+ if (!mMediaSessionInfoMap.Contains(*aBrowsingContextId)) {
+ LOG("The owner of audio focus doesn't have media session");
+ return ClearActiveMediaSessionContextIdIfNeeded();
+ }
+
+ // This owner has media session so it should become an active session context.
+ SetActiveMediaSessionContextId(*aBrowsingContextId);
+}
+
+void MediaStatusManager::SetActiveMediaSessionContextId(
+ uint64_t aBrowsingContextId) {
+ if (mActiveMediaSessionContextId &&
+ *mActiveMediaSessionContextId == aBrowsingContextId) {
+ LOG("Active session context %" PRIu64 " keeps unchanged",
+ *mActiveMediaSessionContextId);
+ return;
+ }
+ mActiveMediaSessionContextId = Some(aBrowsingContextId);
+ StoreMediaSessionContextIdOnWindowContext();
+ LOG("context %" PRIu64 " becomes active session context",
+ *mActiveMediaSessionContextId);
+ mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
+ mSupportedActionsChangedEvent.Notify(GetSupportedActions());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "active-media-session-changed", nullptr);
+ }
+ }
+}
+
+void MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded() {
+ if (!mActiveMediaSessionContextId) {
+ return;
+ }
+ LOG("Clear active session context");
+ mActiveMediaSessionContextId.reset();
+ StoreMediaSessionContextIdOnWindowContext();
+ mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
+ mSupportedActionsChangedEvent.Notify(GetSupportedActions());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "active-media-session-changed", nullptr);
+ }
+ }
+}
+
+void MediaStatusManager::StoreMediaSessionContextIdOnWindowContext() {
+ RefPtr<CanonicalBrowsingContext> bc =
+ CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
+ if (bc && bc->GetTopWindowContext()) {
+ Unused << bc->GetTopWindowContext()->SetActiveMediaSessionContextId(
+ mActiveMediaSessionContextId);
+ }
+}
+
+bool MediaStatusManager::IsSessionOwningAudioFocus(
+ uint64_t aBrowsingContextId) const {
+ Maybe<uint64_t> audioFocusContextId =
+ mPlaybackStatusDelegate.GetAudioFocusOwnerContextId();
+ return audioFocusContextId ? *audioFocusContextId == aBrowsingContextId
+ : false;
+}
+
+MediaMetadataBase MediaStatusManager::CreateDefaultMetadata() const {
+ MediaMetadataBase metadata;
+ metadata.mTitle = GetDefaultTitle();
+ metadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL();
+
+ LOG("Default media metadata, title=%s, album src=%s",
+ NS_ConvertUTF16toUTF8(metadata.mTitle).get(),
+ NS_ConvertUTF16toUTF8(metadata.mArtwork[0].mSrc).get());
+ return metadata;
+}
+
+nsString MediaStatusManager::GetDefaultTitle() const {
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ nsString defaultTitle = service->GetFallbackTitle();
+
+ RefPtr<CanonicalBrowsingContext> bc =
+ CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
+ if (!bc) {
+ return defaultTitle;
+ }
+
+ RefPtr<WindowGlobalParent> globalParent = bc->GetCurrentWindowGlobal();
+ if (!globalParent) {
+ return defaultTitle;
+ }
+
+ // The media metadata would be shown on the virtual controller interface. For
+ // example, on Android, the interface would be shown on both notification bar
+ // and lockscreen. Therefore, what information we provide via metadata is
+ // quite important, because if we're in private browsing, we don't want to
+ // expose details about what website the user is browsing on the lockscreen.
+ // Therefore, using the default title when in the private browsing or the
+ // document title is empty. Otherwise, use the document title.
+ nsString documentTitle;
+ if (!IsInPrivateBrowsing()) {
+ globalParent->GetDocumentTitle(documentTitle);
+ }
+ return documentTitle.IsEmpty() ? defaultTitle : documentTitle;
+}
+
+nsString MediaStatusManager::GetDefaultFaviconURL() const {
+#ifdef MOZ_PLACES
+ nsCOMPtr<nsIURI> faviconURI;
+ nsresult rv = NS_NewURI(getter_AddRefs(faviconURI),
+ nsLiteralCString(FAVICON_DEFAULT_URL));
+ NS_ENSURE_SUCCESS(rv, u""_ns);
+
+ // Convert URI from `chrome://XXX` to `file://XXX` because we would like to
+ // let OS related frameworks, such as SMTC and MPRIS, handle this URL in order
+ // to show the icon on virtual controller interface.
+ nsCOMPtr<nsIChromeRegistry> regService = services::GetChromeRegistry();
+ if (!regService) {
+ return u""_ns;
+ }
+ nsCOMPtr<nsIURI> processedURI;
+ regService->ConvertChromeURL(faviconURI, getter_AddRefs(processedURI));
+
+ nsAutoCString spec;
+ if (NS_FAILED(processedURI->GetSpec(spec))) {
+ return u""_ns;
+ }
+ return NS_ConvertUTF8toUTF16(spec);
+#else
+ return u""_ns;
+#endif
+}
+
+void MediaStatusManager::SetDeclaredPlaybackState(
+ uint64_t aBrowsingContextId, MediaSessionPlaybackState aState) {
+ auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
+ if (!info) {
+ return;
+ }
+ LOG("SetDeclaredPlaybackState from %s to %s",
+ ToMediaSessionPlaybackStateStr(info->mDeclaredPlaybackState),
+ ToMediaSessionPlaybackStateStr(aState));
+ info->mDeclaredPlaybackState = aState;
+ UpdateActualPlaybackState();
+}
+
+MediaSessionPlaybackState MediaStatusManager::GetCurrentDeclaredPlaybackState()
+ const {
+ if (!mActiveMediaSessionContextId) {
+ return MediaSessionPlaybackState::None;
+ }
+ return mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId)
+ .mDeclaredPlaybackState;
+}
+
+void MediaStatusManager::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId,
+ MediaPlaybackState aState) {
+ LOG("UpdateMediaPlaybackState %s for context %" PRIu64,
+ ToMediaPlaybackStateStr(aState), aBrowsingContextId);
+ const bool oldPlaying = mPlaybackStatusDelegate.IsPlaying();
+ mPlaybackStatusDelegate.UpdateMediaPlaybackState(aBrowsingContextId, aState);
+
+ // Playback state doesn't change, we don't need to update the guessed playback
+ // state. This is used to prevent the state from changing from `none` to
+ // `paused` when receiving `MediaPlaybackState::eStarted`.
+ if (mPlaybackStatusDelegate.IsPlaying() == oldPlaying) {
+ return;
+ }
+ if (mPlaybackStatusDelegate.IsPlaying()) {
+ SetGuessedPlayState(MediaSessionPlaybackState::Playing);
+ } else {
+ SetGuessedPlayState(MediaSessionPlaybackState::Paused);
+ }
+}
+
+void MediaStatusManager::SetGuessedPlayState(MediaSessionPlaybackState aState) {
+ if (aState == mGuessedPlaybackState) {
+ return;
+ }
+ LOG("SetGuessedPlayState : '%s'", ToMediaSessionPlaybackStateStr(aState));
+ mGuessedPlaybackState = aState;
+ UpdateActualPlaybackState();
+}
+
+void MediaStatusManager::UpdateActualPlaybackState() {
+ // The way to compute the actual playback state is based on the spec.
+ // https://w3c.github.io/mediasession/#actual-playback-state
+ MediaSessionPlaybackState newState =
+ GetCurrentDeclaredPlaybackState() == MediaSessionPlaybackState::Playing
+ ? MediaSessionPlaybackState::Playing
+ : mGuessedPlaybackState;
+ if (mActualPlaybackState == newState) {
+ return;
+ }
+ mActualPlaybackState = newState;
+ LOG("UpdateActualPlaybackState : '%s'",
+ ToMediaSessionPlaybackStateStr(mActualPlaybackState));
+ mPlaybackStateChangedEvent.Notify(mActualPlaybackState);
+}
+
+void MediaStatusManager::EnableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) {
+ auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
+ if (!info) {
+ return;
+ }
+ if (info->IsActionSupported(aAction)) {
+ LOG("Action '%s' has already been enabled for context %" PRIu64,
+ ToMediaSessionActionStr(aAction), aBrowsingContextId);
+ return;
+ }
+ LOG("Enable action %s for context %" PRIu64, ToMediaSessionActionStr(aAction),
+ aBrowsingContextId);
+ info->EnableAction(aAction);
+ NotifySupportedKeysChangedIfNeeded(aBrowsingContextId);
+}
+
+void MediaStatusManager::DisableAction(uint64_t aBrowsingContextId,
+ MediaSessionAction aAction) {
+ auto info = mMediaSessionInfoMap.Lookup(aBrowsingContextId);
+ if (!info) {
+ return;
+ }
+ if (!info->IsActionSupported(aAction)) {
+ LOG("Action '%s' hasn't been enabled yet for context %" PRIu64,
+ ToMediaSessionActionStr(aAction), aBrowsingContextId);
+ return;
+ }
+ LOG("Disable action %s for context %" PRIu64,
+ ToMediaSessionActionStr(aAction), aBrowsingContextId);
+ info->DisableAction(aAction);
+ NotifySupportedKeysChangedIfNeeded(aBrowsingContextId);
+}
+
+void MediaStatusManager::UpdatePositionState(uint64_t aBrowsingContextId,
+ const PositionState& aState) {
+ // The position state comes from non-active media session which we don't care.
+ if (!mActiveMediaSessionContextId ||
+ *mActiveMediaSessionContextId != aBrowsingContextId) {
+ return;
+ }
+ mPositionStateChangedEvent.Notify(aState);
+}
+
+void MediaStatusManager::NotifySupportedKeysChangedIfNeeded(
+ uint64_t aBrowsingContextId) {
+ // Only the active media session's supported actions would be shown in virtual
+ // control interface, so we only notify the event when supported actions
+ // change happens on the active media session.
+ if (!mActiveMediaSessionContextId ||
+ *mActiveMediaSessionContextId != aBrowsingContextId) {
+ return;
+ }
+ mSupportedActionsChangedEvent.Notify(GetSupportedActions());
+}
+
+CopyableTArray<MediaSessionAction> MediaStatusManager::GetSupportedActions()
+ const {
+ CopyableTArray<MediaSessionAction> supportedActions;
+ if (!mActiveMediaSessionContextId) {
+ return supportedActions;
+ }
+
+ MediaSessionInfo info =
+ mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId);
+ const uint8_t actionNums = uint8_t(MediaSessionAction::EndGuard_);
+ for (uint8_t actionValue = 0; actionValue < actionNums; actionValue++) {
+ MediaSessionAction action = ConvertToMediaSessionAction(actionValue);
+ if (info.IsActionSupported(action)) {
+ supportedActions.AppendElement(action);
+ }
+ }
+ return supportedActions;
+}
+
+MediaMetadataBase MediaStatusManager::GetCurrentMediaMetadata() const {
+ // If we don't have active media session, active media session doesn't have
+ // media metadata, or we're in private browsing mode, then we should create a
+ // default metadata which is using website's title and favicon as title and
+ // artwork.
+ if (mActiveMediaSessionContextId && !IsInPrivateBrowsing()) {
+ MediaSessionInfo info =
+ mMediaSessionInfoMap.Get(*mActiveMediaSessionContextId);
+ if (!info.mMetadata) {
+ return CreateDefaultMetadata();
+ }
+ MediaMetadataBase& metadata = *(info.mMetadata);
+ FillMissingTitleAndArtworkIfNeeded(metadata);
+ return metadata;
+ }
+ return CreateDefaultMetadata();
+}
+
+void MediaStatusManager::FillMissingTitleAndArtworkIfNeeded(
+ MediaMetadataBase& aMetadata) const {
+ // If the metadata doesn't set its title and artwork properly, we would like
+ // to use default title and favicon instead in order to prevent showing
+ // nothing on the virtual control interface.
+ if (aMetadata.mTitle.IsEmpty()) {
+ aMetadata.mTitle = GetDefaultTitle();
+ }
+ if (aMetadata.mArtwork.IsEmpty()) {
+ aMetadata.mArtwork.AppendElement()->mSrc = GetDefaultFaviconURL();
+ }
+}
+
+bool MediaStatusManager::IsInPrivateBrowsing() const {
+ RefPtr<CanonicalBrowsingContext> bc =
+ CanonicalBrowsingContext::Get(mTopLevelBrowsingContextId);
+ if (!bc) {
+ return false;
+ }
+ RefPtr<Element> element = bc->GetEmbedderElement();
+ if (!element) {
+ return false;
+ }
+ return nsContentUtils::IsInPrivateBrowsing(element->OwnerDoc());
+}
+
+MediaSessionPlaybackState MediaStatusManager::PlaybackState() const {
+ return mActualPlaybackState;
+}
+
+bool MediaStatusManager::IsMediaAudible() const {
+ return mPlaybackStatusDelegate.IsAudible();
+}
+
+bool MediaStatusManager::IsMediaPlaying() const {
+ return mActualPlaybackState == MediaSessionPlaybackState::Playing;
+}
+
+bool MediaStatusManager::IsAnyMediaBeingControlled() const {
+ return mPlaybackStatusDelegate.IsAnyMediaBeingControlled();
+}
+
+void MediaStatusManager::NotifyPageTitleChanged() {
+ // If active media session has set non-empty metadata, then we would use that
+ // instead of using default metadata.
+ if (mActiveMediaSessionContextId &&
+ mMediaSessionInfoMap.Lookup(*mActiveMediaSessionContextId)->mMetadata) {
+ return;
+ }
+ // In private browsing mode, we won't show page title on default metadata so
+ // we don't need to update that.
+ if (IsInPrivateBrowsing()) {
+ return;
+ }
+ LOG("page title changed, update default metadata");
+ mMetadataChangedEvent.Notify(GetCurrentMediaMetadata());
+}
+
+} // namespace mozilla::dom