summaryrefslogtreecommitdiffstats
path: root/dom/media/mediacontrol/MediaControlService.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/media/mediacontrol/MediaControlService.cpp540
1 files changed, 540 insertions, 0 deletions
diff --git a/dom/media/mediacontrol/MediaControlService.cpp b/dom/media/mediacontrol/MediaControlService.cpp
new file mode 100644
index 0000000000..c321e080d2
--- /dev/null
+++ b/dom/media/mediacontrol/MediaControlService.cpp
@@ -0,0 +1,540 @@
+/* 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 "MediaControlService.h"
+
+#include "MediaController.h"
+#include "MediaControlUtils.h"
+#include "mozilla/Assertions.h"
+#include "mozilla/intl/Localization.h"
+#include "mozilla/Logging.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticPrefs_media.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Telemetry.h"
+#include "nsIObserverService.h"
+#include "nsXULAppAPI.h"
+
+using mozilla::intl::Localization;
+
+#undef LOG
+#define LOG(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
+ ("MediaControlService=%p, " msg, this, ##__VA_ARGS__))
+
+#undef LOG_MAINCONTROLLER
+#define LOG_MAINCONTROLLER(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
+
+#undef LOG_MAINCONTROLLER_INFO
+#define LOG_MAINCONTROLLER_INFO(msg, ...) \
+ MOZ_LOG(gMediaControlLog, LogLevel::Info, (msg, ##__VA_ARGS__))
+
+namespace mozilla::dom {
+
+StaticRefPtr<MediaControlService> gMediaControlService;
+static bool sIsXPCOMShutdown = false;
+
+/* static */
+RefPtr<MediaControlService> MediaControlService::GetService() {
+ MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
+ "MediaControlService only runs on Chrome process!");
+ if (sIsXPCOMShutdown) {
+ return nullptr;
+ }
+ if (!gMediaControlService) {
+ gMediaControlService = new MediaControlService();
+ gMediaControlService->Init();
+ }
+ RefPtr<MediaControlService> service = gMediaControlService.get();
+ return service;
+}
+
+/* static */
+void MediaControlService::GenerateMediaControlKey(const GlobalObject& global,
+ MediaControlKey aKey) {
+ RefPtr<MediaControlService> service = MediaControlService::GetService();
+ if (service) {
+ service->GenerateTestMediaControlKey(aKey);
+ }
+}
+
+/* static */
+void MediaControlService::GetCurrentActiveMediaMetadata(
+ const GlobalObject& aGlobal, MediaMetadataInit& aMetadata) {
+ if (RefPtr<MediaControlService> service = MediaControlService::GetService()) {
+ MediaMetadataBase metadata = service->GetMainControllerMediaMetadata();
+ aMetadata.mTitle = metadata.mTitle;
+ aMetadata.mArtist = metadata.mArtist;
+ aMetadata.mAlbum = metadata.mAlbum;
+ for (const auto& artwork : metadata.mArtwork) {
+ // If OOM happens resulting in not able to append the element, then we
+ // would get incorrect result and fail on test, so we don't need to throw
+ // an error explicitly.
+ if (MediaImage* image = aMetadata.mArtwork.AppendElement(fallible)) {
+ image->mSrc = artwork.mSrc;
+ image->mSizes = artwork.mSizes;
+ image->mType = artwork.mType;
+ }
+ }
+ }
+}
+
+/* static */
+MediaSessionPlaybackState
+MediaControlService::GetCurrentMediaSessionPlaybackState(
+ GlobalObject& aGlobal) {
+ if (RefPtr<MediaControlService> service = MediaControlService::GetService()) {
+ return service->GetMainControllerPlaybackState();
+ }
+ return MediaSessionPlaybackState::None;
+}
+
+NS_INTERFACE_MAP_BEGIN(MediaControlService)
+ NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
+ NS_INTERFACE_MAP_ENTRY(nsIObserver)
+NS_INTERFACE_MAP_END
+
+NS_IMPL_ADDREF(MediaControlService)
+NS_IMPL_RELEASE(MediaControlService)
+
+MediaControlService::MediaControlService() {
+ LOG("create media control service");
+ RefPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->AddObserver(this, "xpcom-shutdown", false);
+ }
+}
+
+void MediaControlService::Init() {
+ mMediaKeysHandler = new MediaControlKeyHandler();
+ mMediaControlKeyManager = new MediaControlKeyManager();
+ mMediaControlKeyManager->AddListener(mMediaKeysHandler.get());
+ mControllerManager = MakeUnique<ControllerManager>(this);
+
+ // Initialize the fallback title
+ nsTArray<nsCString> resIds{
+ "branding/brand.ftl"_ns,
+ "dom/media.ftl"_ns,
+ };
+ RefPtr<Localization> l10n = Localization::Create(resIds, true);
+ {
+ nsAutoCString translation;
+ IgnoredErrorResult rv;
+ l10n->FormatValueSync("mediastatus-fallback-title"_ns, {}, translation, rv);
+ if (!rv.Failed()) {
+ mFallbackTitle = NS_ConvertUTF8toUTF16(translation);
+ }
+ }
+}
+
+MediaControlService::~MediaControlService() {
+ LOG("destroy media control service");
+ Shutdown();
+}
+
+void MediaControlService::NotifyMediaControlHasEverBeenUsed() {
+ // We've already updated the telemetry for using meida control.
+ if (mHasEverUsedMediaControl) {
+ return;
+ }
+ mHasEverUsedMediaControl = true;
+ const uint32_t usedOnMediaControl = 1;
+#ifdef XP_WIN
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Windows"_ns, usedOnMediaControl);
+#endif
+#ifdef XP_MACOSX
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"MacOS"_ns, usedOnMediaControl);
+#endif
+#ifdef MOZ_WIDGET_GTK
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Linux"_ns, usedOnMediaControl);
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Android"_ns, usedOnMediaControl);
+#endif
+}
+
+void MediaControlService::NotifyMediaControlHasEverBeenEnabled() {
+ // We've already enabled the service and update the telemetry.
+ if (mHasEverEnabledMediaControl) {
+ return;
+ }
+ mHasEverEnabledMediaControl = true;
+ const uint32_t enableOnMediaControl = 0;
+#ifdef XP_WIN
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Windows"_ns, enableOnMediaControl);
+#endif
+#ifdef XP_MACOSX
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"MacOS"_ns, enableOnMediaControl);
+#endif
+#ifdef MOZ_WIDGET_GTK
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Linux"_ns, enableOnMediaControl);
+#endif
+#ifdef MOZ_WIDGET_ANDROID
+ Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
+ u"Android"_ns, enableOnMediaControl);
+#endif
+}
+
+NS_IMETHODIMP
+MediaControlService::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, "xpcom-shutdown")) {
+ LOG("XPCOM shutdown");
+ MOZ_ASSERT(gMediaControlService);
+ RefPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
+ if (obs) {
+ obs->RemoveObserver(this, "xpcom-shutdown");
+ }
+ Shutdown();
+ sIsXPCOMShutdown = true;
+ gMediaControlService = nullptr;
+ }
+ return NS_OK;
+}
+
+void MediaControlService::Shutdown() {
+ mControllerManager->Shutdown();
+ mMediaControlKeyManager->RemoveListener(mMediaKeysHandler.get());
+}
+
+bool MediaControlService::RegisterActiveMediaController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(mControllerManager,
+ "Register controller before initializing service");
+ if (!mControllerManager->AddController(aController)) {
+ LOG("Fail to register controller %" PRId64, aController->Id());
+ return false;
+ }
+ LOG("Register media controller %" PRId64 ", currentNum=%" PRId64,
+ aController->Id(), GetActiveControllersNum());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-controller-amount-changed", nullptr);
+ }
+ }
+ return true;
+}
+
+bool MediaControlService::UnregisterActiveMediaController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(mControllerManager,
+ "Unregister controller before initializing service");
+ if (!mControllerManager->RemoveController(aController)) {
+ LOG("Fail to unregister controller %" PRId64, aController->Id());
+ return false;
+ }
+ LOG("Unregister media controller %" PRId64 ", currentNum=%" PRId64,
+ aController->Id(), GetActiveControllersNum());
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "media-controller-amount-changed", nullptr);
+ }
+ }
+ return true;
+}
+
+void MediaControlService::NotifyControllerPlaybackStateChanged(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(
+ mControllerManager,
+ "controller state change happens before initializing service");
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ // The controller is not an active controller.
+ if (!mControllerManager->Contains(aController)) {
+ return;
+ }
+
+ // The controller is the main controller, propagate its playback state.
+ if (GetMainController() == aController) {
+ mControllerManager->MainControllerPlaybackStateChanged(
+ aController->PlaybackState());
+ return;
+ }
+
+ // The controller is not the main controller, but will become a new main
+ // controller. As the service can contains multiple controllers and only one
+ // controller can be controlled by media control keys. Therefore, when
+ // controller's state becomes `playing`, then we would like to let that
+ // controller being controlled, rather than other controller which might not
+ // be playing at the time.
+ if (GetMainController() != aController &&
+ aController->PlaybackState() == MediaSessionPlaybackState::Playing) {
+ mControllerManager->UpdateMainControllerIfNeeded(aController);
+ }
+}
+
+void MediaControlService::RequestUpdateMainController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ MOZ_DIAGNOSTIC_ASSERT(
+ mControllerManager,
+ "using controller in PIP mode before initializing service");
+ // The controller is not an active controller.
+ if (!mControllerManager->Contains(aController)) {
+ return;
+ }
+ mControllerManager->UpdateMainControllerIfNeeded(aController);
+}
+
+uint64_t MediaControlService::GetActiveControllersNum() const {
+ MOZ_DIAGNOSTIC_ASSERT(mControllerManager);
+ return mControllerManager->GetControllersNum();
+}
+
+MediaController* MediaControlService::GetMainController() const {
+ MOZ_DIAGNOSTIC_ASSERT(mControllerManager);
+ return mControllerManager->GetMainController();
+}
+
+void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey) {
+ if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ return;
+ }
+ // Generate a seek details for `seekto`
+ if (aKey == MediaControlKey::Seekto) {
+ mMediaKeysHandler->OnActionPerformed(
+ MediaControlAction(aKey, SeekDetails()));
+ } else {
+ mMediaKeysHandler->OnActionPerformed(MediaControlAction(aKey));
+ }
+}
+
+MediaMetadataBase MediaControlService::GetMainControllerMediaMetadata() const {
+ MediaMetadataBase metadata;
+ if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ return metadata;
+ }
+ return GetMainController() ? GetMainController()->GetCurrentMediaMetadata()
+ : metadata;
+}
+
+MediaSessionPlaybackState MediaControlService::GetMainControllerPlaybackState()
+ const {
+ if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ return MediaSessionPlaybackState::None;
+ }
+ return GetMainController() ? GetMainController()->PlaybackState()
+ : MediaSessionPlaybackState::None;
+}
+
+nsString MediaControlService::GetFallbackTitle() const {
+ return mFallbackTitle;
+}
+
+// Following functions belong to ControllerManager
+MediaControlService::ControllerManager::ControllerManager(
+ MediaControlService* aService)
+ : mSource(aService->GetMediaControlKeySource()) {
+ MOZ_ASSERT(mSource);
+}
+
+bool MediaControlService::ControllerManager::AddController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ if (mControllers.contains(aController)) {
+ return false;
+ }
+ mControllers.insertBack(aController);
+ UpdateMainControllerIfNeeded(aController);
+ return true;
+}
+
+bool MediaControlService::ControllerManager::RemoveController(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ if (!mControllers.contains(aController)) {
+ return false;
+ }
+ // This is LinkedListElement's method which will remove controller from
+ // `mController`.
+ static_cast<LinkedListControllerPtr>(aController)->remove();
+ // If main controller is removed from the list, the last controller in the
+ // list would become the main controller. Or reset the main controller when
+ // the list is already empty.
+ if (GetMainController() == aController) {
+ UpdateMainControllerInternal(
+ mControllers.isEmpty() ? nullptr : mControllers.getLast());
+ }
+ return true;
+}
+
+void MediaControlService::ControllerManager::UpdateMainControllerIfNeeded(
+ MediaController* aController) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+
+ if (GetMainController() == aController) {
+ LOG_MAINCONTROLLER("This controller is alreay the main controller");
+ return;
+ }
+
+ if (GetMainController() &&
+ GetMainController()->IsBeingUsedInPIPModeOrFullscreen() &&
+ !aController->IsBeingUsedInPIPModeOrFullscreen()) {
+ LOG_MAINCONTROLLER(
+ "Normal media controller can't replace the controller being used in "
+ "PIP mode or fullscreen");
+ return ReorderGivenController(aController,
+ InsertOptions::eInsertAsNormalController);
+ }
+ ReorderGivenController(aController, InsertOptions::eInsertAsMainController);
+ UpdateMainControllerInternal(aController);
+}
+
+void MediaControlService::ControllerManager::ReorderGivenController(
+ MediaController* aController, InsertOptions aOption) {
+ MOZ_DIAGNOSTIC_ASSERT(aController);
+ MOZ_DIAGNOSTIC_ASSERT(mControllers.contains(aController));
+ // Reset the controller's position and make it not in any list.
+ static_cast<LinkedListControllerPtr>(aController)->remove();
+
+ if (aOption == InsertOptions::eInsertAsMainController) {
+ // Make the main controller as the last element in the list to maintain the
+ // order of controllers because we always use the last controller in the
+ // list as the next main controller when removing current main controller
+ // from the list. Eg. If the list contains [A, B, C], and now the last
+ // element C is the main controller. When B becomes main controller later,
+ // the list would become [A, C, B]. And if A becomes main controller, list
+ // would become [C, B, A]. Then, if we remove A from the list, the next main
+ // controller would be B. But if we don't maintain the controller order when
+ // main controller changes, we would pick C as the main controller because
+ // the list is still [A, B, C].
+ return mControllers.insertBack(aController);
+ }
+
+ MOZ_ASSERT(aOption == InsertOptions::eInsertAsNormalController);
+ MOZ_ASSERT(GetMainController() != aController);
+ // We might have multiple controllers which have higher priority (being used
+ // in PIP or fullscreen) from the head, the normal controller should be
+ // inserted before them. Therefore, search a higher priority controller from
+ // the head and insert new controller before it.
+ // Eg. a list [A, B, C, D, E] and D and E have higher priority, if we want
+ // to insert F, then the final result would be [A, B, C, F, D, E]
+ auto* current = static_cast<LinkedListControllerPtr>(mControllers.getFirst());
+ while (!static_cast<MediaController*>(current)
+ ->IsBeingUsedInPIPModeOrFullscreen()) {
+ current = current->getNext();
+ }
+ MOZ_ASSERT(current, "Should have at least one higher priority controller!");
+ current->setPrevious(aController);
+}
+
+void MediaControlService::ControllerManager::Shutdown() {
+ mControllers.clear();
+ DisconnectMainControllerEvents();
+}
+
+void MediaControlService::ControllerManager::MainControllerPlaybackStateChanged(
+ MediaSessionPlaybackState aState) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSource->SetPlaybackState(aState);
+}
+
+void MediaControlService::ControllerManager::MainControllerMetadataChanged(
+ const MediaMetadataBase& aMetadata) {
+ MOZ_ASSERT(NS_IsMainThread());
+ mSource->SetMediaMetadata(aMetadata);
+}
+
+void MediaControlService::ControllerManager::UpdateMainControllerInternal(
+ MediaController* aController) {
+ MOZ_ASSERT(NS_IsMainThread());
+ if (aController) {
+ aController->Select();
+ }
+ if (mMainController) {
+ mMainController->Unselect();
+ }
+ mMainController = aController;
+
+ if (!mMainController) {
+ LOG_MAINCONTROLLER_INFO("Clear main controller");
+ mSource->Close();
+ DisconnectMainControllerEvents();
+ } else {
+ LOG_MAINCONTROLLER_INFO("Set controller %" PRId64 " as main controller",
+ mMainController->Id());
+ if (!mSource->Open()) {
+ LOG("Failed to open source for monitoring media keys");
+ }
+ // We would still update those status to the event source even if it failed
+ // to open, because it would save the result and set them to the real
+ // source when it opens. In addition, another benefit to do that is to
+ // prevent testing from affecting by platform specific issues, because our
+ // testing events rely on those status changes and they are all platform
+ // independent.
+ mSource->SetPlaybackState(mMainController->PlaybackState());
+ mSource->SetMediaMetadata(mMainController->GetCurrentMediaMetadata());
+ mSource->SetSupportedMediaKeys(mMainController->GetSupportedMediaKeys());
+ ConnectMainControllerEvents();
+ }
+
+ if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
+ if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
+ obs->NotifyObservers(nullptr, "main-media-controller-changed", nullptr);
+ }
+ }
+}
+
+void MediaControlService::ControllerManager::ConnectMainControllerEvents() {
+ // As main controller has been changed, we should disconnect listeners from
+ // the previous controller and reconnect them to the new controller.
+ DisconnectMainControllerEvents();
+ // Listen to main controller's event in order to propagate the content that
+ // might be displayed on the virtual control interface created by the source.
+ mMetadataChangedListener = mMainController->MetadataChangedEvent().Connect(
+ AbstractThread::MainThread(), this,
+ &ControllerManager::MainControllerMetadataChanged);
+ mSupportedKeysChangedListener =
+ mMainController->SupportedKeysChangedEvent().Connect(
+ AbstractThread::MainThread(),
+ [this](const MediaKeysArray& aSupportedKeys) {
+ mSource->SetSupportedMediaKeys(aSupportedKeys);
+ });
+ mFullScreenChangedListener =
+ mMainController->FullScreenChangedEvent().Connect(
+ AbstractThread::MainThread(), [this](bool aIsEnabled) {
+ mSource->SetEnableFullScreen(aIsEnabled);
+ });
+ mPictureInPictureModeChangedListener =
+ mMainController->PictureInPictureModeChangedEvent().Connect(
+ AbstractThread::MainThread(), [this](bool aIsEnabled) {
+ mSource->SetEnablePictureInPictureMode(aIsEnabled);
+ });
+ mPositionChangedListener = mMainController->PositionChangedEvent().Connect(
+ AbstractThread::MainThread(), [this](const PositionState& aState) {
+ mSource->SetPositionState(aState);
+ });
+}
+
+void MediaControlService::ControllerManager::DisconnectMainControllerEvents() {
+ mMetadataChangedListener.DisconnectIfExists();
+ mSupportedKeysChangedListener.DisconnectIfExists();
+ mFullScreenChangedListener.DisconnectIfExists();
+ mPictureInPictureModeChangedListener.DisconnectIfExists();
+ mPositionChangedListener.DisconnectIfExists();
+}
+
+MediaController* MediaControlService::ControllerManager::GetMainController()
+ const {
+ return mMainController.get();
+}
+
+uint64_t MediaControlService::ControllerManager::GetControllersNum() const {
+ return mControllers.length();
+}
+
+bool MediaControlService::ControllerManager::Contains(
+ MediaController* aController) const {
+ return mControllers.contains(aController);
+}
+
+} // namespace mozilla::dom