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