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 "MediaControlService.h"
6 
7 #include "MediaController.h"
8 #include "MediaControlUtils.h"
9 #include "mozilla/Assertions.h"
10 #include "mozilla/intl/Localization.h"
11 #include "mozilla/Logging.h"
12 #include "mozilla/Services.h"
13 #include "mozilla/StaticPrefs_media.h"
14 #include "mozilla/StaticPtr.h"
15 #include "mozilla/Telemetry.h"
16 #include "nsIObserverService.h"
17 #include "nsXULAppAPI.h"
18 
19 using mozilla::intl::Localization;
20 
21 #undef LOG
22 #define LOG(msg, ...)                        \
23   MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
24           ("MediaControlService=%p, " msg, this, ##__VA_ARGS__))
25 
26 #undef LOG_MAINCONTROLLER
27 #define LOG_MAINCONTROLLER(msg, ...) \
28   MOZ_LOG(gMediaControlLog, LogLevel::Debug, (msg, ##__VA_ARGS__))
29 
30 #undef LOG_MAINCONTROLLER_INFO
31 #define LOG_MAINCONTROLLER_INFO(msg, ...) \
32   MOZ_LOG(gMediaControlLog, LogLevel::Info, (msg, ##__VA_ARGS__))
33 
34 namespace mozilla::dom {
35 
36 StaticRefPtr<MediaControlService> gMediaControlService;
37 static bool sIsXPCOMShutdown = false;
38 
39 /* static */
GetService()40 RefPtr<MediaControlService> MediaControlService::GetService() {
41   MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(),
42                         "MediaControlService only runs on Chrome process!");
43   if (sIsXPCOMShutdown) {
44     return nullptr;
45   }
46   if (!gMediaControlService) {
47     gMediaControlService = new MediaControlService();
48     gMediaControlService->Init();
49   }
50   RefPtr<MediaControlService> service = gMediaControlService.get();
51   return service;
52 }
53 
54 /* static */
GenerateMediaControlKey(const GlobalObject & global,MediaControlKey aKey)55 void MediaControlService::GenerateMediaControlKey(const GlobalObject& global,
56                                                   MediaControlKey aKey) {
57   RefPtr<MediaControlService> service = MediaControlService::GetService();
58   if (service) {
59     service->GenerateTestMediaControlKey(aKey);
60   }
61 }
62 
63 /* static */
GetCurrentActiveMediaMetadata(const GlobalObject & aGlobal,MediaMetadataInit & aMetadata)64 void MediaControlService::GetCurrentActiveMediaMetadata(
65     const GlobalObject& aGlobal, MediaMetadataInit& aMetadata) {
66   if (RefPtr<MediaControlService> service = MediaControlService::GetService()) {
67     MediaMetadataBase metadata = service->GetMainControllerMediaMetadata();
68     aMetadata.mTitle = metadata.mTitle;
69     aMetadata.mArtist = metadata.mArtist;
70     aMetadata.mAlbum = metadata.mAlbum;
71     for (const auto& artwork : metadata.mArtwork) {
72       // If OOM happens resulting in not able to append the element, then we
73       // would get incorrect result and fail on test, so we don't need to throw
74       // an error explicitly.
75       if (MediaImage* image = aMetadata.mArtwork.AppendElement(fallible)) {
76         image->mSrc = artwork.mSrc;
77         image->mSizes = artwork.mSizes;
78         image->mType = artwork.mType;
79       }
80     }
81   }
82 }
83 
84 /* static */
85 MediaSessionPlaybackState
GetCurrentMediaSessionPlaybackState(GlobalObject & aGlobal)86 MediaControlService::GetCurrentMediaSessionPlaybackState(
87     GlobalObject& aGlobal) {
88   if (RefPtr<MediaControlService> service = MediaControlService::GetService()) {
89     return service->GetMainControllerPlaybackState();
90   }
91   return MediaSessionPlaybackState::None;
92 }
93 
94 NS_INTERFACE_MAP_BEGIN(MediaControlService)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports,nsIObserver)95   NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
96   NS_INTERFACE_MAP_ENTRY(nsIObserver)
97 NS_INTERFACE_MAP_END
98 
99 NS_IMPL_ADDREF(MediaControlService)
100 NS_IMPL_RELEASE(MediaControlService)
101 
102 MediaControlService::MediaControlService() {
103   LOG("create media control service");
104   RefPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
105   if (obs) {
106     obs->AddObserver(this, "xpcom-shutdown", false);
107   }
108 }
109 
Init()110 void MediaControlService::Init() {
111   mMediaKeysHandler = new MediaControlKeyHandler();
112   mMediaControlKeyManager = new MediaControlKeyManager();
113   mMediaControlKeyManager->AddListener(mMediaKeysHandler.get());
114   mControllerManager = MakeUnique<ControllerManager>(this);
115 
116   // Initialize the fallback title
117   nsCOMPtr<nsIGlobalObject> global =
118       xpc::NativeGlobal(xpc::PrivilegedJunkScope());
119   RefPtr<Localization> l10n = Localization::Create(global, true, {});
120   l10n->AddResourceId(u"branding/brand.ftl"_ns);
121   l10n->AddResourceId(u"dom/media.ftl"_ns);
122   {
123     AutoSafeJSContext cx;
124 
125     nsAutoCString translation;
126     ErrorResult rv;
127     l10n->FormatValueSync(cx, "mediastatus-fallback-title"_ns, {}, translation,
128                           rv);
129     if (!rv.Failed()) {
130       mFallbackTitle = NS_ConvertUTF8toUTF16(translation);
131     }
132   }
133 }
134 
~MediaControlService()135 MediaControlService::~MediaControlService() {
136   LOG("destroy media control service");
137   Shutdown();
138 }
139 
NotifyMediaControlHasEverBeenUsed()140 void MediaControlService::NotifyMediaControlHasEverBeenUsed() {
141   // We've already updated the telemetry for using meida control.
142   if (mHasEverUsedMediaControl) {
143     return;
144   }
145   mHasEverUsedMediaControl = true;
146   const uint32_t usedOnMediaControl = 1;
147 #ifdef XP_WIN
148   Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
149                        u"Windows"_ns, usedOnMediaControl);
150 #endif
151 #ifdef XP_MACOSX
152   Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
153                        u"MacOS"_ns, usedOnMediaControl);
154 #endif
155 #ifdef MOZ_WIDGET_GTK
156   Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
157                        u"Linux"_ns, usedOnMediaControl);
158 #endif
159 #ifdef MOZ_WIDGET_ANDROID
160   Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
161                        u"Android"_ns, usedOnMediaControl);
162 #endif
163 }
164 
NotifyMediaControlHasEverBeenEnabled()165 void MediaControlService::NotifyMediaControlHasEverBeenEnabled() {
166   // We've already enabled the service and update the telemetry.
167   if (mHasEverEnabledMediaControl) {
168     return;
169   }
170   mHasEverEnabledMediaControl = true;
171   const uint32_t enableOnMediaControl = 0;
172 #ifdef XP_WIN
173   Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
174                        u"Windows"_ns, enableOnMediaControl);
175 #endif
176 #ifdef XP_MACOSX
177   Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
178                        u"MacOS"_ns, enableOnMediaControl);
179 #endif
180 #ifdef MOZ_WIDGET_GTK
181   Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
182                        u"Linux"_ns, enableOnMediaControl);
183 #endif
184 #ifdef MOZ_WIDGET_ANDROID
185   Telemetry::ScalarSet(Telemetry::ScalarID::MEDIA_CONTROL_PLATFORM_USAGE,
186                        u"Android"_ns, enableOnMediaControl);
187 #endif
188 }
189 
190 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)191 MediaControlService::Observe(nsISupports* aSubject, const char* aTopic,
192                              const char16_t* aData) {
193   if (!strcmp(aTopic, "xpcom-shutdown")) {
194     LOG("XPCOM shutdown");
195     MOZ_ASSERT(gMediaControlService);
196     RefPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
197     if (obs) {
198       obs->RemoveObserver(this, "xpcom-shutdown");
199     }
200     Shutdown();
201     sIsXPCOMShutdown = true;
202     gMediaControlService = nullptr;
203   }
204   return NS_OK;
205 }
206 
Shutdown()207 void MediaControlService::Shutdown() {
208   mControllerManager->Shutdown();
209   mMediaControlKeyManager->RemoveListener(mMediaKeysHandler.get());
210 }
211 
RegisterActiveMediaController(MediaController * aController)212 bool MediaControlService::RegisterActiveMediaController(
213     MediaController* aController) {
214   MOZ_DIAGNOSTIC_ASSERT(mControllerManager,
215                         "Register controller before initializing service");
216   if (!mControllerManager->AddController(aController)) {
217     LOG("Fail to register controller %" PRId64, aController->Id());
218     return false;
219   }
220   LOG("Register media controller %" PRId64 ", currentNum=%" PRId64,
221       aController->Id(), GetActiveControllersNum());
222   if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
223     if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
224       obs->NotifyObservers(nullptr, "media-controller-amount-changed", nullptr);
225     }
226   }
227   return true;
228 }
229 
UnregisterActiveMediaController(MediaController * aController)230 bool MediaControlService::UnregisterActiveMediaController(
231     MediaController* aController) {
232   MOZ_DIAGNOSTIC_ASSERT(mControllerManager,
233                         "Unregister controller before initializing service");
234   if (!mControllerManager->RemoveController(aController)) {
235     LOG("Fail to unregister controller %" PRId64, aController->Id());
236     return false;
237   }
238   LOG("Unregister media controller %" PRId64 ", currentNum=%" PRId64,
239       aController->Id(), GetActiveControllersNum());
240   if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
241     if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
242       obs->NotifyObservers(nullptr, "media-controller-amount-changed", nullptr);
243     }
244   }
245   return true;
246 }
247 
NotifyControllerPlaybackStateChanged(MediaController * aController)248 void MediaControlService::NotifyControllerPlaybackStateChanged(
249     MediaController* aController) {
250   MOZ_DIAGNOSTIC_ASSERT(
251       mControllerManager,
252       "controller state change happens before initializing service");
253   MOZ_DIAGNOSTIC_ASSERT(aController);
254   // The controller is not an active controller.
255   if (!mControllerManager->Contains(aController)) {
256     return;
257   }
258 
259   // The controller is the main controller, propagate its playback state.
260   if (GetMainController() == aController) {
261     mControllerManager->MainControllerPlaybackStateChanged(
262         aController->PlaybackState());
263     return;
264   }
265 
266   // The controller is not the main controller, but will become a new main
267   // controller. As the service can contains multiple controllers and only one
268   // controller can be controlled by media control keys. Therefore, when
269   // controller's state becomes `playing`, then we would like to let that
270   // controller being controlled, rather than other controller which might not
271   // be playing at the time.
272   if (GetMainController() != aController &&
273       aController->PlaybackState() == MediaSessionPlaybackState::Playing) {
274     mControllerManager->UpdateMainControllerIfNeeded(aController);
275   }
276 }
277 
RequestUpdateMainController(MediaController * aController)278 void MediaControlService::RequestUpdateMainController(
279     MediaController* aController) {
280   MOZ_DIAGNOSTIC_ASSERT(aController);
281   MOZ_DIAGNOSTIC_ASSERT(
282       mControllerManager,
283       "using controller in PIP mode before initializing service");
284   // The controller is not an active controller.
285   if (!mControllerManager->Contains(aController)) {
286     return;
287   }
288   mControllerManager->UpdateMainControllerIfNeeded(aController);
289 }
290 
GetActiveControllersNum() const291 uint64_t MediaControlService::GetActiveControllersNum() const {
292   MOZ_DIAGNOSTIC_ASSERT(mControllerManager);
293   return mControllerManager->GetControllersNum();
294 }
295 
GetMainController() const296 MediaController* MediaControlService::GetMainController() const {
297   MOZ_DIAGNOSTIC_ASSERT(mControllerManager);
298   return mControllerManager->GetMainController();
299 }
300 
GenerateTestMediaControlKey(MediaControlKey aKey)301 void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey) {
302   if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
303     return;
304   }
305   // Generate a seek details for `seekto`
306   if (aKey == MediaControlKey::Seekto) {
307     mMediaKeysHandler->OnActionPerformed(
308         MediaControlAction(aKey, SeekDetails()));
309   } else {
310     mMediaKeysHandler->OnActionPerformed(MediaControlAction(aKey));
311   }
312 }
313 
GetMainControllerMediaMetadata() const314 MediaMetadataBase MediaControlService::GetMainControllerMediaMetadata() const {
315   MediaMetadataBase metadata;
316   if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
317     return metadata;
318   }
319   return GetMainController() ? GetMainController()->GetCurrentMediaMetadata()
320                              : metadata;
321 }
322 
GetMainControllerPlaybackState() const323 MediaSessionPlaybackState MediaControlService::GetMainControllerPlaybackState()
324     const {
325   if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) {
326     return MediaSessionPlaybackState::None;
327   }
328   return GetMainController() ? GetMainController()->PlaybackState()
329                              : MediaSessionPlaybackState::None;
330 }
331 
GetFallbackTitle() const332 nsString MediaControlService::GetFallbackTitle() const {
333   return mFallbackTitle;
334 }
335 
336 // Following functions belong to ControllerManager
ControllerManager(MediaControlService * aService)337 MediaControlService::ControllerManager::ControllerManager(
338     MediaControlService* aService)
339     : mSource(aService->GetMediaControlKeySource()) {
340   MOZ_ASSERT(mSource);
341 }
342 
AddController(MediaController * aController)343 bool MediaControlService::ControllerManager::AddController(
344     MediaController* aController) {
345   MOZ_DIAGNOSTIC_ASSERT(aController);
346   if (mControllers.contains(aController)) {
347     return false;
348   }
349   mControllers.insertBack(aController);
350   UpdateMainControllerIfNeeded(aController);
351   return true;
352 }
353 
RemoveController(MediaController * aController)354 bool MediaControlService::ControllerManager::RemoveController(
355     MediaController* aController) {
356   MOZ_DIAGNOSTIC_ASSERT(aController);
357   if (!mControllers.contains(aController)) {
358     return false;
359   }
360   // This is LinkedListElement's method which will remove controller from
361   // `mController`.
362   static_cast<LinkedListControllerPtr>(aController)->remove();
363   // If main controller is removed from the list, the last controller in the
364   // list would become the main controller. Or reset the main controller when
365   // the list is already empty.
366   if (GetMainController() == aController) {
367     UpdateMainControllerInternal(
368         mControllers.isEmpty() ? nullptr : mControllers.getLast());
369   }
370   return true;
371 }
372 
UpdateMainControllerIfNeeded(MediaController * aController)373 void MediaControlService::ControllerManager::UpdateMainControllerIfNeeded(
374     MediaController* aController) {
375   MOZ_DIAGNOSTIC_ASSERT(aController);
376 
377   if (GetMainController() == aController) {
378     LOG_MAINCONTROLLER("This controller is alreay the main controller");
379     return;
380   }
381 
382   if (GetMainController() &&
383       GetMainController()->IsBeingUsedInPIPModeOrFullscreen() &&
384       !aController->IsBeingUsedInPIPModeOrFullscreen()) {
385     LOG_MAINCONTROLLER(
386         "Normal media controller can't replace the controller being used in "
387         "PIP mode or fullscreen");
388     return ReorderGivenController(aController,
389                                   InsertOptions::eInsertAsNormalController);
390   }
391   ReorderGivenController(aController, InsertOptions::eInsertAsMainController);
392   UpdateMainControllerInternal(aController);
393 }
394 
ReorderGivenController(MediaController * aController,InsertOptions aOption)395 void MediaControlService::ControllerManager::ReorderGivenController(
396     MediaController* aController, InsertOptions aOption) {
397   MOZ_DIAGNOSTIC_ASSERT(aController);
398   MOZ_DIAGNOSTIC_ASSERT(mControllers.contains(aController));
399   // Reset the controller's position and make it not in any list.
400   static_cast<LinkedListControllerPtr>(aController)->remove();
401 
402   if (aOption == InsertOptions::eInsertAsMainController) {
403     // Make the main controller as the last element in the list to maintain the
404     // order of controllers because we always use the last controller in the
405     // list as the next main controller when removing current main controller
406     // from the list. Eg. If the list contains [A, B, C], and now the last
407     // element C is the main controller. When B becomes main controller later,
408     // the list would become [A, C, B]. And if A becomes main controller, list
409     // would become [C, B, A]. Then, if we remove A from the list, the next main
410     // controller would be B. But if we don't maintain the controller order when
411     // main controller changes, we would pick C as the main controller because
412     // the list is still [A, B, C].
413     return mControllers.insertBack(aController);
414   }
415 
416   MOZ_ASSERT(aOption == InsertOptions::eInsertAsNormalController);
417   MOZ_ASSERT(GetMainController() != aController);
418   // We might have multiple controllers which have higher priority (being used
419   // in PIP or fullscreen) from the head, the normal controller should be
420   // inserted before them. Therefore, search a higher priority controller from
421   // the head and insert new controller before it.
422   // Eg. a list [A, B, C, D, E] and D and E have higher priority, if we want
423   // to insert F, then the final result would be [A, B, C, F, D, E]
424   auto* current = static_cast<LinkedListControllerPtr>(mControllers.getFirst());
425   while (!static_cast<MediaController*>(current)
426               ->IsBeingUsedInPIPModeOrFullscreen()) {
427     current = current->getNext();
428   }
429   MOZ_ASSERT(current, "Should have at least one higher priority controller!");
430   current->setPrevious(aController);
431 }
432 
Shutdown()433 void MediaControlService::ControllerManager::Shutdown() {
434   mControllers.clear();
435   DisconnectMainControllerEvents();
436 }
437 
MainControllerPlaybackStateChanged(MediaSessionPlaybackState aState)438 void MediaControlService::ControllerManager::MainControllerPlaybackStateChanged(
439     MediaSessionPlaybackState aState) {
440   MOZ_ASSERT(NS_IsMainThread());
441   mSource->SetPlaybackState(aState);
442 }
443 
MainControllerMetadataChanged(const MediaMetadataBase & aMetadata)444 void MediaControlService::ControllerManager::MainControllerMetadataChanged(
445     const MediaMetadataBase& aMetadata) {
446   MOZ_ASSERT(NS_IsMainThread());
447   mSource->SetMediaMetadata(aMetadata);
448 }
449 
UpdateMainControllerInternal(MediaController * aController)450 void MediaControlService::ControllerManager::UpdateMainControllerInternal(
451     MediaController* aController) {
452   MOZ_ASSERT(NS_IsMainThread());
453   if (aController) {
454     aController->Select();
455   }
456   if (mMainController) {
457     mMainController->Unselect();
458   }
459   mMainController = aController;
460 
461   if (!mMainController) {
462     LOG_MAINCONTROLLER_INFO("Clear main controller");
463     mSource->Close();
464     DisconnectMainControllerEvents();
465   } else {
466     LOG_MAINCONTROLLER_INFO("Set controller %" PRId64 " as main controller",
467                             mMainController->Id());
468     if (!mSource->Open()) {
469       LOG("Failed to open source for monitoring media keys");
470     }
471     // We would still update those status to the event source even if it failed
472     // to open, because it would save the result and set them to the real
473     // source when it opens. In addition, another benefit to do that is to
474     // prevent testing from affecting by platform specific issues, because our
475     // testing events rely on those status changes and they are all platform
476     // independent.
477     mSource->SetPlaybackState(mMainController->PlaybackState());
478     mSource->SetMediaMetadata(mMainController->GetCurrentMediaMetadata());
479     mSource->SetSupportedMediaKeys(mMainController->GetSupportedMediaKeys());
480     ConnectMainControllerEvents();
481   }
482 
483   if (StaticPrefs::media_mediacontrol_testingevents_enabled()) {
484     if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) {
485       obs->NotifyObservers(nullptr, "main-media-controller-changed", nullptr);
486     }
487   }
488 }
489 
ConnectMainControllerEvents()490 void MediaControlService::ControllerManager::ConnectMainControllerEvents() {
491   // As main controller has been changed, we should disconnect listeners from
492   // the previous controller and reconnect them to the new controller.
493   DisconnectMainControllerEvents();
494   // Listen to main controller's event in order to propagate the content that
495   // might be displayed on the virtual control interface created by the source.
496   mMetadataChangedListener = mMainController->MetadataChangedEvent().Connect(
497       AbstractThread::MainThread(), this,
498       &ControllerManager::MainControllerMetadataChanged);
499   mSupportedKeysChangedListener =
500       mMainController->SupportedKeysChangedEvent().Connect(
501           AbstractThread::MainThread(),
502           [this](const MediaKeysArray& aSupportedKeys) {
503             mSource->SetSupportedMediaKeys(aSupportedKeys);
504           });
505   mFullScreenChangedListener =
506       mMainController->FullScreenChangedEvent().Connect(
507           AbstractThread::MainThread(), [this](bool aIsEnabled) {
508             mSource->SetEnableFullScreen(aIsEnabled);
509           });
510   mPictureInPictureModeChangedListener =
511       mMainController->PictureInPictureModeChangedEvent().Connect(
512           AbstractThread::MainThread(), [this](bool aIsEnabled) {
513             mSource->SetEnablePictureInPictureMode(aIsEnabled);
514           });
515   mPositionChangedListener = mMainController->PositionChangedEvent().Connect(
516       AbstractThread::MainThread(), [this](const PositionState& aState) {
517         mSource->SetPositionState(aState);
518       });
519 }
520 
DisconnectMainControllerEvents()521 void MediaControlService::ControllerManager::DisconnectMainControllerEvents() {
522   mMetadataChangedListener.DisconnectIfExists();
523   mSupportedKeysChangedListener.DisconnectIfExists();
524   mFullScreenChangedListener.DisconnectIfExists();
525   mPictureInPictureModeChangedListener.DisconnectIfExists();
526   mPositionChangedListener.DisconnectIfExists();
527 }
528 
GetMainController() const529 MediaController* MediaControlService::ControllerManager::GetMainController()
530     const {
531   return mMainController.get();
532 }
533 
GetControllersNum() const534 uint64_t MediaControlService::ControllerManager::GetControllersNum() const {
535   return mControllers.length();
536 }
537 
Contains(MediaController * aController) const538 bool MediaControlService::ControllerManager::Contains(
539     MediaController* aController) const {
540   return mControllers.contains(aController);
541 }
542 
543 }  // namespace mozilla::dom
544