/* 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 gMediaControlService; static bool sIsXPCOMShutdown = false; /* static */ RefPtr 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 service = gMediaControlService.get(); return service; } /* static */ void MediaControlService::GenerateMediaControlKey(const GlobalObject& global, MediaControlKey aKey) { RefPtr service = MediaControlService::GetService(); if (service) { service->GenerateTestMediaControlKey(aKey); } } /* static */ void MediaControlService::GetCurrentActiveMediaMetadata( const GlobalObject& aGlobal, MediaMetadataInit& aMetadata) { if (RefPtr 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 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 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(this); // Initialize the fallback title nsTArray resIds{ "branding/brand.ftl"_ns, "dom/media.ftl"_ns, }; RefPtr 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 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 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 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(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(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(mControllers.getFirst()); while (!static_cast(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 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