MediaControlService.cpp (18702B)
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 "MediaControlUtils.h" 8 #include "MediaController.h" 9 #include "mozilla/AppShutdown.h" 10 #include "mozilla/Assertions.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/intl/Localization.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 38 /* static */ 39 RefPtr<MediaControlService> MediaControlService::GetService() { 40 MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(), 41 "MediaControlService only runs on Chrome process!"); 42 if (AppShutdown::IsInOrBeyond(ShutdownPhase::XPCOMShutdown)) { 43 return nullptr; 44 } 45 if (!gMediaControlService) { 46 gMediaControlService = new MediaControlService(); 47 gMediaControlService->Init(); 48 } 49 RefPtr<MediaControlService> service = gMediaControlService.get(); 50 return service; 51 } 52 53 /* static */ 54 void MediaControlService::GenerateMediaControlKey(const GlobalObject& global, 55 MediaControlKey aKey, 56 double aSeekTime) { 57 RefPtr<MediaControlService> service = MediaControlService::GetService(); 58 if (service) { 59 service->GenerateTestMediaControlKey(aKey, aSeekTime); 60 } 61 } 62 63 /* static */ 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 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) 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 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 nsTArray<nsCString> resIds{ 118 "branding/brand.ftl"_ns, 119 "dom/media.ftl"_ns, 120 }; 121 RefPtr<Localization> l10n = Localization::Create(resIds, true); 122 { 123 nsAutoCString translation; 124 IgnoredErrorResult rv; 125 l10n->FormatValueSync("mediastatus-fallback-title"_ns, {}, translation, rv); 126 if (!rv.Failed()) { 127 mFallbackTitle = NS_ConvertUTF8toUTF16(translation); 128 } 129 } 130 } 131 132 MediaControlService::~MediaControlService() { 133 LOG("destroy media control service"); 134 Shutdown(); 135 } 136 137 NS_IMETHODIMP 138 MediaControlService::Observe(nsISupports* aSubject, const char* aTopic, 139 const char16_t* aData) { 140 if (!strcmp(aTopic, "xpcom-shutdown")) { 141 LOG("XPCOM shutdown"); 142 MOZ_ASSERT(gMediaControlService); 143 RefPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); 144 if (obs) { 145 obs->RemoveObserver(this, "xpcom-shutdown"); 146 } 147 Shutdown(); 148 gMediaControlService = nullptr; 149 } 150 return NS_OK; 151 } 152 153 void MediaControlService::Shutdown() { 154 mControllerManager->Shutdown(); 155 mMediaControlKeyManager->RemoveListener(mMediaKeysHandler.get()); 156 } 157 158 bool MediaControlService::RegisterActiveMediaController( 159 MediaController* aController) { 160 MOZ_DIAGNOSTIC_ASSERT(mControllerManager, 161 "Register controller before initializing service"); 162 if (!mControllerManager->AddController(aController)) { 163 LOG("Fail to register controller %" PRId64, aController->Id()); 164 return false; 165 } 166 LOG("Register media controller %" PRId64 ", currentNum=%" PRId64, 167 aController->Id(), GetActiveControllersNum()); 168 if (StaticPrefs::media_mediacontrol_testingevents_enabled()) { 169 if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) { 170 obs->NotifyObservers(nullptr, "media-controller-amount-changed", nullptr); 171 } 172 } 173 return true; 174 } 175 176 bool MediaControlService::UnregisterActiveMediaController( 177 MediaController* aController) { 178 MOZ_DIAGNOSTIC_ASSERT(mControllerManager, 179 "Unregister controller before initializing service"); 180 if (!mControllerManager->RemoveController(aController)) { 181 LOG("Fail to unregister controller %" PRId64, aController->Id()); 182 return false; 183 } 184 LOG("Unregister media controller %" PRId64 ", currentNum=%" PRId64, 185 aController->Id(), GetActiveControllersNum()); 186 if (StaticPrefs::media_mediacontrol_testingevents_enabled()) { 187 if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) { 188 obs->NotifyObservers(nullptr, "media-controller-amount-changed", nullptr); 189 } 190 } 191 return true; 192 } 193 194 void MediaControlService::NotifyControllerPlaybackStateChanged( 195 MediaController* aController) { 196 MOZ_DIAGNOSTIC_ASSERT( 197 mControllerManager, 198 "controller state change happens before initializing service"); 199 MOZ_DIAGNOSTIC_ASSERT(aController); 200 // The controller is not an active controller. 201 if (!mControllerManager->Contains(aController)) { 202 return; 203 } 204 205 // The controller is the main controller, propagate its playback state. 206 if (GetMainController() == aController) { 207 mControllerManager->MainControllerPlaybackStateChanged( 208 aController->PlaybackState()); 209 return; 210 } 211 212 // The controller is not the main controller, but will become a new main 213 // controller. As the service can contains multiple controllers and only one 214 // controller can be controlled by media control keys. Therefore, when 215 // controller's state becomes `playing`, then we would like to let that 216 // controller being controlled, rather than other controller which might not 217 // be playing at the time. 218 if (GetMainController() != aController && 219 aController->PlaybackState() == MediaSessionPlaybackState::Playing) { 220 mControllerManager->UpdateMainControllerIfNeeded(aController); 221 } 222 } 223 224 void MediaControlService::RequestUpdateMainController( 225 MediaController* aController) { 226 MOZ_DIAGNOSTIC_ASSERT(aController); 227 MOZ_DIAGNOSTIC_ASSERT( 228 mControllerManager, 229 "using controller in PIP mode before initializing service"); 230 // The controller is not an active controller. 231 if (!mControllerManager->Contains(aController)) { 232 return; 233 } 234 mControllerManager->UpdateMainControllerIfNeeded(aController); 235 } 236 237 uint64_t MediaControlService::GetActiveControllersNum() const { 238 MOZ_DIAGNOSTIC_ASSERT(mControllerManager); 239 return mControllerManager->GetControllersNum(); 240 } 241 242 MediaController* MediaControlService::GetMainController() const { 243 MOZ_DIAGNOSTIC_ASSERT(mControllerManager); 244 return mControllerManager->GetMainController(); 245 } 246 247 void MediaControlService::GenerateTestMediaControlKey(MediaControlKey aKey, 248 double aSeekValue) { 249 if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) { 250 return; 251 } 252 // Generate seek details when necessary 253 switch (aKey) { 254 case MediaControlKey::Seekto: 255 mMediaKeysHandler->OnActionPerformed(MediaControlAction( 256 aKey, SeekDetails(aSeekValue, false /* fast seek */))); 257 break; 258 case MediaControlKey::Seekbackward: 259 case MediaControlKey::Seekforward: 260 mMediaKeysHandler->OnActionPerformed( 261 MediaControlAction(aKey, SeekDetails(aSeekValue))); 262 break; 263 default: 264 mMediaKeysHandler->OnActionPerformed(MediaControlAction(aKey)); 265 } 266 } 267 268 MediaMetadataBase MediaControlService::GetMainControllerMediaMetadata() const { 269 MediaMetadataBase metadata; 270 if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) { 271 return metadata; 272 } 273 return GetMainController() ? GetMainController()->GetCurrentMediaMetadata() 274 : metadata; 275 } 276 277 MediaSessionPlaybackState MediaControlService::GetMainControllerPlaybackState() 278 const { 279 if (!StaticPrefs::media_mediacontrol_testingevents_enabled()) { 280 return MediaSessionPlaybackState::None; 281 } 282 return GetMainController() ? GetMainController()->PlaybackState() 283 : MediaSessionPlaybackState::None; 284 } 285 286 nsString MediaControlService::GetFallbackTitle() const { 287 return mFallbackTitle; 288 } 289 290 // Following functions belong to ControllerManager 291 MediaControlService::ControllerManager::ControllerManager( 292 MediaControlService* aService) 293 : mSource(aService->GetMediaControlKeySource()) { 294 MOZ_ASSERT(mSource); 295 } 296 297 bool MediaControlService::ControllerManager::AddController( 298 MediaController* aController) { 299 MOZ_DIAGNOSTIC_ASSERT(aController); 300 if (mControllers.contains(aController)) { 301 return false; 302 } 303 mControllers.insertBack(aController); 304 UpdateMainControllerIfNeeded(aController); 305 return true; 306 } 307 308 bool MediaControlService::ControllerManager::RemoveController( 309 MediaController* aController) { 310 MOZ_DIAGNOSTIC_ASSERT(aController); 311 if (!mControllers.contains(aController)) { 312 return false; 313 } 314 // This is LinkedListElement's method which will remove controller from 315 // `mController`. 316 static_cast<LinkedListControllerPtr>(aController)->remove(); 317 // If main controller is removed from the list, the last controller in the 318 // list would become the main controller. Or reset the main controller when 319 // the list is already empty. 320 if (GetMainController() == aController) { 321 UpdateMainControllerInternal( 322 mControllers.isEmpty() ? nullptr : mControllers.getLast()); 323 } 324 return true; 325 } 326 327 void MediaControlService::ControllerManager::UpdateMainControllerIfNeeded( 328 MediaController* aController) { 329 MOZ_DIAGNOSTIC_ASSERT(aController); 330 331 if (GetMainController() == aController) { 332 LOG_MAINCONTROLLER("This controller is alreay the main controller"); 333 return; 334 } 335 336 if (GetMainController() && 337 GetMainController()->IsBeingUsedInPIPModeOrFullscreen() && 338 !aController->IsBeingUsedInPIPModeOrFullscreen()) { 339 LOG_MAINCONTROLLER( 340 "Normal media controller can't replace the controller being used in " 341 "PIP mode or fullscreen"); 342 return ReorderGivenController(aController, 343 InsertOptions::eInsertAsNormalController); 344 } 345 ReorderGivenController(aController, InsertOptions::eInsertAsMainController); 346 UpdateMainControllerInternal(aController); 347 } 348 349 void MediaControlService::ControllerManager::ReorderGivenController( 350 MediaController* aController, InsertOptions aOption) { 351 MOZ_DIAGNOSTIC_ASSERT(aController); 352 MOZ_DIAGNOSTIC_ASSERT(mControllers.contains(aController)); 353 // Reset the controller's position and make it not in any list. 354 static_cast<LinkedListControllerPtr>(aController)->remove(); 355 356 if (aOption == InsertOptions::eInsertAsMainController) { 357 // Make the main controller as the last element in the list to maintain the 358 // order of controllers because we always use the last controller in the 359 // list as the next main controller when removing current main controller 360 // from the list. Eg. If the list contains [A, B, C], and now the last 361 // element C is the main controller. When B becomes main controller later, 362 // the list would become [A, C, B]. And if A becomes main controller, list 363 // would become [C, B, A]. Then, if we remove A from the list, the next main 364 // controller would be B. But if we don't maintain the controller order when 365 // main controller changes, we would pick C as the main controller because 366 // the list is still [A, B, C]. 367 return mControllers.insertBack(aController); 368 } 369 370 MOZ_ASSERT(aOption == InsertOptions::eInsertAsNormalController); 371 MOZ_ASSERT(GetMainController() != aController); 372 // We might have multiple controllers which have higher priority (being used 373 // in PIP or fullscreen) from the head, the normal controller should be 374 // inserted before them. Therefore, search a higher priority controller from 375 // the head and insert new controller before it. 376 // Eg. a list [A, B, C, D, E] and D and E have higher priority, if we want 377 // to insert F, then the final result would be [A, B, C, F, D, E] 378 auto* current = static_cast<LinkedListControllerPtr>(mControllers.getFirst()); 379 while (!static_cast<MediaController*>(current) 380 ->IsBeingUsedInPIPModeOrFullscreen()) { 381 current = current->getNext(); 382 } 383 MOZ_ASSERT(current, "Should have at least one higher priority controller!"); 384 current->setPrevious(aController); 385 } 386 387 void MediaControlService::ControllerManager::Shutdown() { 388 mControllers.clear(); 389 DisconnectMainControllerEvents(); 390 } 391 392 void MediaControlService::ControllerManager::MainControllerPlaybackStateChanged( 393 MediaSessionPlaybackState aState) { 394 MOZ_ASSERT(NS_IsMainThread()); 395 mSource->SetPlaybackState(aState); 396 } 397 398 void MediaControlService::ControllerManager::MainControllerMetadataChanged( 399 const MediaMetadataBase& aMetadata) { 400 MOZ_ASSERT(NS_IsMainThread()); 401 mSource->SetMediaMetadata(aMetadata); 402 } 403 404 void MediaControlService::ControllerManager::UpdateMainControllerInternal( 405 MediaController* aController) { 406 MOZ_ASSERT(NS_IsMainThread()); 407 if (aController) { 408 aController->Select(); 409 } 410 if (mMainController) { 411 mMainController->Unselect(); 412 } 413 mMainController = aController; 414 415 if (!mMainController) { 416 LOG_MAINCONTROLLER_INFO("Clear main controller"); 417 mSource->Close(); 418 DisconnectMainControllerEvents(); 419 } else { 420 LOG_MAINCONTROLLER_INFO("Set controller %" PRId64 " as main controller", 421 mMainController->Id()); 422 if (!mSource->Open()) { 423 LOG("Failed to open source for monitoring media keys"); 424 } 425 // We would still update those status to the event source even if it failed 426 // to open, because it would save the result and set them to the real 427 // source when it opens. In addition, another benefit to do that is to 428 // prevent testing from affecting by platform specific issues, because our 429 // testing events rely on those status changes and they are all platform 430 // independent. 431 mSource->SetPlaybackState(mMainController->PlaybackState()); 432 mSource->SetMediaMetadata(mMainController->GetCurrentMediaMetadata()); 433 mSource->SetSupportedMediaKeys(mMainController->GetSupportedMediaKeys()); 434 mSource->SetPositionState(mMainController->GetCurrentPositionState()); 435 ConnectMainControllerEvents(); 436 } 437 438 if (StaticPrefs::media_mediacontrol_testingevents_enabled()) { 439 if (nsCOMPtr<nsIObserverService> obs = services::GetObserverService()) { 440 obs->NotifyObservers(nullptr, "main-media-controller-changed", nullptr); 441 } 442 } 443 } 444 445 void MediaControlService::ControllerManager::ConnectMainControllerEvents() { 446 // As main controller has been changed, we should disconnect listeners from 447 // the previous controller and reconnect them to the new controller. 448 DisconnectMainControllerEvents(); 449 // Listen to main controller's event in order to propagate the content that 450 // might be displayed on the virtual control interface created by the source. 451 mMetadataChangedListener = mMainController->MetadataChangedEvent().Connect( 452 AbstractThread::MainThread(), this, 453 &ControllerManager::MainControllerMetadataChanged); 454 mSupportedKeysChangedListener = 455 mMainController->SupportedKeysChangedEvent().Connect( 456 AbstractThread::MainThread(), 457 [this](const MediaKeysArray& aSupportedKeys) { 458 mSource->SetSupportedMediaKeys(aSupportedKeys); 459 }); 460 mFullScreenChangedListener = 461 mMainController->FullScreenChangedEvent().Connect( 462 AbstractThread::MainThread(), [this](bool aIsEnabled) { 463 mSource->SetEnableFullScreen(aIsEnabled); 464 }); 465 mPictureInPictureModeChangedListener = 466 mMainController->PictureInPictureModeChangedEvent().Connect( 467 AbstractThread::MainThread(), [this](bool aIsEnabled) { 468 mSource->SetEnablePictureInPictureMode(aIsEnabled); 469 }); 470 mPositionChangedListener = mMainController->PositionChangedEvent().Connect( 471 AbstractThread::MainThread(), [this](const Maybe<PositionState>& aState) { 472 mSource->SetPositionState(aState); 473 }); 474 } 475 476 void MediaControlService::ControllerManager::DisconnectMainControllerEvents() { 477 mMetadataChangedListener.DisconnectIfExists(); 478 mSupportedKeysChangedListener.DisconnectIfExists(); 479 mFullScreenChangedListener.DisconnectIfExists(); 480 mPictureInPictureModeChangedListener.DisconnectIfExists(); 481 mPositionChangedListener.DisconnectIfExists(); 482 } 483 484 MediaController* MediaControlService::ControllerManager::GetMainController() 485 const { 486 return mMainController.get(); 487 } 488 489 uint64_t MediaControlService::ControllerManager::GetControllersNum() const { 490 return mControllers.length(); 491 } 492 493 bool MediaControlService::ControllerManager::Contains( 494 MediaController* aController) const { 495 return mControllers.contains(aController); 496 } 497 498 } // namespace mozilla::dom