MediaController.cpp (20842B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 3 /* This Source Code Form is subject to the terms of the Mozilla Public 4 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "MediaController.h" 8 9 #include "MediaControlKeySource.h" 10 #include "MediaControlService.h" 11 #include "MediaControlUtils.h" 12 #include "mozilla/AsyncEventDispatcher.h" 13 #include "mozilla/StaticPrefs_media.h" 14 #include "mozilla/dom/BrowsingContext.h" 15 #include "mozilla/dom/CanonicalBrowsingContext.h" 16 #include "mozilla/dom/MediaSession.h" 17 #include "mozilla/dom/PositionStateEvent.h" 18 19 // avoid redefined macro in unified build 20 #undef LOG 21 #define LOG(msg, ...) \ 22 MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ 23 ("MediaController=%p, Id=%" PRId64 ", " msg, this, this->Id(), \ 24 ##__VA_ARGS__)) 25 26 namespace mozilla::dom { 27 28 NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaController, DOMEventTargetHelper) 29 NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(MediaController, 30 DOMEventTargetHelper, 31 nsITimerCallback, nsINamed) 32 NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaController, 33 DOMEventTargetHelper) 34 NS_IMPL_CYCLE_COLLECTION_TRACE_END 35 36 nsISupports* MediaController::GetParentObject() const { 37 RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id()); 38 return bc; 39 } 40 41 JSObject* MediaController::WrapObject(JSContext* aCx, 42 JS::Handle<JSObject*> aGivenProto) { 43 return MediaController_Binding::Wrap(aCx, this, aGivenProto); 44 } 45 46 void MediaController::GetSupportedKeys( 47 nsTArray<MediaControlKey>& aRetVal) const { 48 aRetVal.Clear(); 49 for (const auto& key : mSupportedKeys) { 50 aRetVal.AppendElement(key); 51 } 52 } 53 54 void MediaController::GetMetadata(MediaMetadataInit& aMetadata, 55 ErrorResult& aRv) { 56 if (!IsActive() || mShutdown) { 57 aRv.Throw(NS_ERROR_NOT_AVAILABLE); 58 return; 59 } 60 61 const MediaMetadataBase metadata = GetCurrentMediaMetadata(); 62 aMetadata.mTitle = metadata.mTitle; 63 aMetadata.mArtist = metadata.mArtist; 64 aMetadata.mAlbum = metadata.mAlbum; 65 for (const auto& artwork : metadata.mArtwork) { 66 if (MediaImage* image = aMetadata.mArtwork.AppendElement(fallible)) { 67 image->mSrc = artwork.mSrc; 68 image->mSizes = artwork.mSizes; 69 image->mType = artwork.mType; 70 } else { 71 aRv.Throw(NS_ERROR_OUT_OF_MEMORY); 72 return; 73 } 74 } 75 } 76 77 static const MediaControlKey sDefaultSupportedKeys[] = { 78 MediaControlKey::Focus, MediaControlKey::Play, 79 MediaControlKey::Pause, MediaControlKey::Playpause, 80 MediaControlKey::Stop, MediaControlKey::Seekto, 81 MediaControlKey::Seekforward, MediaControlKey::Seekbackward}; 82 83 static void GetDefaultSupportedKeys(nsTArray<MediaControlKey>& aKeys) { 84 for (const auto& key : sDefaultSupportedKeys) { 85 aKeys.AppendElement(key); 86 } 87 } 88 89 MediaController::MediaController(uint64_t aBrowsingContextId) 90 : MediaStatusManager(aBrowsingContextId) { 91 MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(), 92 "MediaController only runs on Chrome process!"); 93 LOG("Create controller %" PRId64, Id()); 94 GetDefaultSupportedKeys(mSupportedKeys); 95 mSupportedActionsChangedListener = SupportedActionsChangedEvent().Connect( 96 AbstractThread::MainThread(), this, 97 &MediaController::HandleSupportedMediaSessionActionsChanged); 98 mPlaybackChangedListener = PlaybackChangedEvent().Connect( 99 AbstractThread::MainThread(), this, 100 &MediaController::HandleActualPlaybackStateChanged); 101 mPositionStateChangedListener = PositionChangedEvent().Connect( 102 AbstractThread::MainThread(), this, 103 &MediaController::HandlePositionStateChanged); 104 mMetadataChangedListener = 105 MetadataChangedEvent().Connect(AbstractThread::MainThread(), this, 106 &MediaController::HandleMetadataChanged); 107 } 108 109 MediaController::~MediaController() { 110 LOG("Destroy controller %" PRId64, Id()); 111 if (!mShutdown) { 112 Shutdown(); 113 } 114 }; 115 116 void MediaController::Focus() { 117 LOG("Focus"); 118 UpdateMediaControlActionToContentMediaIfNeeded( 119 MediaControlAction(MediaControlKey::Focus)); 120 } 121 122 void MediaController::Play() { 123 LOG("Play"); 124 UpdateMediaControlActionToContentMediaIfNeeded( 125 MediaControlAction(MediaControlKey::Play)); 126 } 127 128 void MediaController::Pause() { 129 LOG("Pause"); 130 UpdateMediaControlActionToContentMediaIfNeeded( 131 MediaControlAction(MediaControlKey::Pause)); 132 } 133 134 void MediaController::PrevTrack() { 135 LOG("Prev Track"); 136 UpdateMediaControlActionToContentMediaIfNeeded( 137 MediaControlAction(MediaControlKey::Previoustrack)); 138 } 139 140 void MediaController::NextTrack() { 141 LOG("Next Track"); 142 UpdateMediaControlActionToContentMediaIfNeeded( 143 MediaControlAction(MediaControlKey::Nexttrack)); 144 } 145 146 void MediaController::SeekBackward(double aSeekOffset) { 147 LOG("Seek Backward"); 148 UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction( 149 MediaControlKey::Seekbackward, SeekDetails(aSeekOffset))); 150 } 151 152 void MediaController::SeekForward(double aSeekOffset) { 153 LOG("Seek Forward"); 154 UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction( 155 MediaControlKey::Seekforward, SeekDetails(aSeekOffset))); 156 } 157 158 void MediaController::SkipAd() { 159 LOG("Skip Ad"); 160 UpdateMediaControlActionToContentMediaIfNeeded( 161 MediaControlAction(MediaControlKey::Skipad)); 162 } 163 164 void MediaController::SeekTo(double aSeekTime, bool aFastSeek) { 165 LOG("Seek To"); 166 UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction( 167 MediaControlKey::Seekto, SeekDetails(aSeekTime, aFastSeek))); 168 } 169 170 void MediaController::Stop() { 171 LOG("Stop"); 172 UpdateMediaControlActionToContentMediaIfNeeded( 173 MediaControlAction(MediaControlKey::Stop)); 174 MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded(); 175 } 176 177 uint64_t MediaController::Id() const { return mTopLevelBrowsingContextId; } 178 179 bool MediaController::IsAudible() const { return IsMediaAudible(); } 180 181 bool MediaController::IsPlaying() const { return IsMediaPlaying(); } 182 183 bool MediaController::IsActive() const { return mIsActive; }; 184 185 bool MediaController::ShouldPropagateActionToAllContexts( 186 const MediaControlAction& aAction) const { 187 // These actions have default action handler for each frame, so we 188 // need to propagate to all contexts. We would handle default handlers in 189 // `ContentMediaController::HandleMediaKey`. 190 if (aAction.mKey.isSome()) { 191 switch (aAction.mKey.value()) { 192 case MediaControlKey::Play: 193 case MediaControlKey::Pause: 194 case MediaControlKey::Stop: 195 case MediaControlKey::Seekto: 196 case MediaControlKey::Seekforward: 197 case MediaControlKey::Seekbackward: 198 return true; 199 default: 200 return false; 201 } 202 } 203 return false; 204 } 205 206 void MediaController::UpdateMediaControlActionToContentMediaIfNeeded( 207 const MediaControlAction& aAction) { 208 // If the controller isn't active or it has been shutdown, we don't need to 209 // update media action to the content process. 210 if (!mIsActive || mShutdown) { 211 return; 212 } 213 214 // For some actions which have default action handler, we want to propagate 215 // them on all contexts in order to trigger the default handler on each 216 // context separately. Otherwise, other action should only be propagated to 217 // the context where active media session exists. 218 const bool propateToAll = ShouldPropagateActionToAllContexts(aAction); 219 const uint64_t targetContextId = propateToAll || !mActiveMediaSessionContextId 220 ? Id() 221 : *mActiveMediaSessionContextId; 222 RefPtr<BrowsingContext> context = BrowsingContext::Get(targetContextId); 223 if (!context || context->IsDiscarded()) { 224 return; 225 } 226 227 if (propateToAll) { 228 context->PreOrderWalk([&](BrowsingContext* bc) { 229 bc->Canonical()->UpdateMediaControlAction(aAction); 230 }); 231 } else { 232 context->Canonical()->UpdateMediaControlAction(aAction); 233 } 234 } 235 236 void MediaController::Shutdown() { 237 MOZ_ASSERT(!mShutdown, "Do not call shutdown twice!"); 238 // The media controller would be removed from the service when we receive a 239 // notification from the content process about all controlled media has been 240 // stoppped. However, if controlled media is stopped after detaching 241 // browsing context, then sending the notification from the content process 242 // would fail so that we are not able to notify the chrome process to remove 243 // the corresponding controller. Therefore, we should manually remove the 244 // controller from the service. 245 Deactivate(); 246 mShutdown = true; 247 mSupportedActionsChangedListener.DisconnectIfExists(); 248 mPlaybackChangedListener.DisconnectIfExists(); 249 mPositionStateChangedListener.DisconnectIfExists(); 250 mMetadataChangedListener.DisconnectIfExists(); 251 } 252 253 void MediaController::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId, 254 MediaPlaybackState aState) { 255 if (mShutdown) { 256 return; 257 } 258 MediaStatusManager::NotifyMediaPlaybackChanged(aBrowsingContextId, aState); 259 UpdateDeactivationTimerIfNeeded(); 260 UpdateActivatedStateIfNeeded(); 261 } 262 263 void MediaController::UpdateDeactivationTimerIfNeeded() { 264 if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) { 265 return; 266 } 267 268 bool shouldBeAlwaysActive = IsPlaying() || IsBeingUsedInPIPModeOrFullscreen(); 269 if (shouldBeAlwaysActive && mDeactivationTimer) { 270 LOG("Cancel deactivation timer"); 271 mDeactivationTimer->Cancel(); 272 mDeactivationTimer = nullptr; 273 } else if (!shouldBeAlwaysActive && !mDeactivationTimer) { 274 nsresult rv = NS_NewTimerWithCallback( 275 getter_AddRefs(mDeactivationTimer), this, 276 StaticPrefs::media_mediacontrol_stopcontrol_timer_ms(), 277 nsITimer::TYPE_ONE_SHOT, AbstractThread::MainThread()); 278 if (NS_SUCCEEDED(rv)) { 279 LOG("Create a deactivation timer"); 280 } else { 281 LOG("Failed to create a deactivation timer"); 282 } 283 } 284 } 285 286 bool MediaController::IsBeingUsedInPIPModeOrFullscreen() const { 287 return mIsInPictureInPictureMode || mIsInFullScreenMode; 288 } 289 290 NS_IMETHODIMP MediaController::Notify(nsITimer* aTimer) { 291 mDeactivationTimer = nullptr; 292 if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) { 293 return NS_OK; 294 } 295 296 if (mShutdown) { 297 LOG("Cancel deactivation timer because controller has been shutdown"); 298 return NS_OK; 299 } 300 301 // As the media being used in the PIP mode or fullscreen would always display 302 // on the screen, users would have high chance to interact with it again, so 303 // we don't want to stop media control. 304 if (IsBeingUsedInPIPModeOrFullscreen()) { 305 LOG("Cancel deactivation timer because controller is in PIP mode"); 306 return NS_OK; 307 } 308 309 if (IsPlaying()) { 310 LOG("Cancel deactivation timer because controller is still playing"); 311 return NS_OK; 312 } 313 314 if (!mIsActive) { 315 LOG("Cancel deactivation timer because controller has been deactivated"); 316 return NS_OK; 317 } 318 Deactivate(); 319 return NS_OK; 320 } 321 322 NS_IMETHODIMP MediaController::GetName(nsACString& aName) { 323 aName.AssignLiteral("MediaController"); 324 return NS_OK; 325 } 326 327 void MediaController::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId, 328 MediaAudibleState aState) { 329 if (mShutdown) { 330 return; 331 } 332 333 bool oldAudible = IsAudible(); 334 MediaStatusManager::NotifyMediaAudibleChanged(aBrowsingContextId, aState); 335 if (IsAudible() == oldAudible) { 336 return; 337 } 338 UpdateActivatedStateIfNeeded(); 339 340 // Request the audio focus amongs different controllers that could cause 341 // pausing other audible controllers if we enable the audio focus management. 342 RefPtr<MediaControlService> service = MediaControlService::GetService(); 343 MOZ_ASSERT(service); 344 if (IsAudible()) { 345 service->GetAudioFocusManager().RequestAudioFocus(this); 346 } else { 347 service->GetAudioFocusManager().RevokeAudioFocus(this); 348 } 349 } 350 351 bool MediaController::ShouldActivateController() const { 352 MOZ_ASSERT(!mShutdown); 353 // After media is successfully loaded and match our critiera, such as its 354 // duration is longer enough, which is used to exclude the notification-ish 355 // sound, then it would be able to be controlled once the controll gets 356 // activated. 357 // 358 // Activating a controller means that we would start to intercept the media 359 // keys on the platform and show the virtual control interface (if needed). 360 // The controller would be activated when (1) controllable media starts in the 361 // browsing context that controller belongs to (2) controllable media enters 362 // fullscreen or PIP mode. 363 return IsAnyMediaBeingControlled() && 364 (IsPlaying() || IsBeingUsedInPIPModeOrFullscreen()) && !mIsActive; 365 } 366 367 bool MediaController::ShouldDeactivateController() const { 368 MOZ_ASSERT(!mShutdown); 369 // If we don't have an active media session and no controlled media exists, 370 // then we don't need to keep controller active, because there is nothing to 371 // control. However, if we still have an active media session, then we should 372 // keep controller active in order to receive media keys even if we don't have 373 // any controlled media existing, because a website might start other media 374 // when media session receives media keys. 375 return !IsAnyMediaBeingControlled() && mIsActive && 376 !mActiveMediaSessionContextId; 377 } 378 379 void MediaController::Activate() { 380 MOZ_ASSERT(!mShutdown); 381 RefPtr<MediaControlService> service = MediaControlService::GetService(); 382 if (service && !mIsActive) { 383 LOG("Activate"); 384 mIsActive = service->RegisterActiveMediaController(this); 385 MOZ_ASSERT(mIsActive, "Fail to register controller!"); 386 DispatchAsyncEvent(u"activated"_ns); 387 } 388 } 389 390 void MediaController::Deactivate() { 391 MOZ_ASSERT(!mShutdown); 392 RefPtr<MediaControlService> service = MediaControlService::GetService(); 393 if (service) { 394 service->GetAudioFocusManager().RevokeAudioFocus(this); 395 if (mIsActive) { 396 LOG("Deactivate"); 397 mIsActive = !service->UnregisterActiveMediaController(this); 398 MOZ_ASSERT(!mIsActive, "Fail to unregister controller!"); 399 DispatchAsyncEvent(u"deactivated"_ns); 400 } 401 } 402 } 403 404 void MediaController::SetIsInPictureInPictureMode( 405 uint64_t aBrowsingContextId, bool aIsInPictureInPictureMode) { 406 if (mIsInPictureInPictureMode == aIsInPictureInPictureMode) { 407 return; 408 } 409 LOG("Set IsInPictureInPictureMode to %s", 410 aIsInPictureInPictureMode ? "true" : "false"); 411 mIsInPictureInPictureMode = aIsInPictureInPictureMode; 412 ForceToBecomeMainControllerIfNeeded(); 413 UpdateDeactivationTimerIfNeeded(); 414 mPictureInPictureModeChangedEvent.Notify(mIsInPictureInPictureMode); 415 } 416 417 void MediaController::NotifyMediaFullScreenState(uint64_t aBrowsingContextId, 418 bool aIsInFullScreen) { 419 if (mIsInFullScreenMode == aIsInFullScreen) { 420 return; 421 } 422 LOG("%s fullscreen", aIsInFullScreen ? "Entered" : "Left"); 423 mIsInFullScreenMode = aIsInFullScreen; 424 ForceToBecomeMainControllerIfNeeded(); 425 mFullScreenChangedEvent.Notify(mIsInFullScreenMode); 426 } 427 428 bool MediaController::IsMainController() const { 429 RefPtr<MediaControlService> service = MediaControlService::GetService(); 430 return service ? service->GetMainController() == this : false; 431 } 432 433 bool MediaController::ShouldRequestForMainController() const { 434 // This controller is already the main controller. 435 if (IsMainController()) { 436 return false; 437 } 438 // We would only force controller to become main controller if it's in the 439 // PIP mode or fullscreen, otherwise it should follow the general rule. 440 // In addition, do nothing if the controller has been shutdowned. 441 return IsBeingUsedInPIPModeOrFullscreen() && !mShutdown; 442 } 443 444 void MediaController::ForceToBecomeMainControllerIfNeeded() { 445 if (!ShouldRequestForMainController()) { 446 return; 447 } 448 RefPtr<MediaControlService> service = MediaControlService::GetService(); 449 MOZ_ASSERT(service, "service was shutdown before shutting down controller?"); 450 // If the controller hasn't been activated and it's ready to be activated, 451 // then activating it should also make it become a main controller. If it's 452 // already activated but isn't a main controller yet, then explicitly request 453 // it. 454 if (!IsActive() && ShouldActivateController()) { 455 Activate(); 456 } else if (IsActive()) { 457 service->RequestUpdateMainController(this); 458 } 459 } 460 461 void MediaController::HandleActualPlaybackStateChanged() { 462 // Media control service would like to know all controllers' playback state 463 // in order to decide which controller should be the main controller that is 464 // usually the last tab which plays media. 465 if (RefPtr<MediaControlService> service = MediaControlService::GetService()) { 466 service->NotifyControllerPlaybackStateChanged(this); 467 } 468 DispatchAsyncEvent(u"playbackstatechange"_ns); 469 } 470 471 void MediaController::UpdateActivatedStateIfNeeded() { 472 if (ShouldActivateController()) { 473 Activate(); 474 } else if (ShouldDeactivateController()) { 475 Deactivate(); 476 } 477 } 478 479 void MediaController::HandleSupportedMediaSessionActionsChanged( 480 const nsTArray<MediaSessionAction>& aSupportedAction) { 481 // Convert actions to keys, some of them have been included in the supported 482 // keys, such as "play", "pause" and "stop". 483 nsTArray<MediaControlKey> newSupportedKeys; 484 GetDefaultSupportedKeys(newSupportedKeys); 485 for (const auto& action : aSupportedAction) { 486 MediaControlKey key = ConvertMediaSessionActionToControlKey(action); 487 if (!newSupportedKeys.Contains(key)) { 488 newSupportedKeys.AppendElement(key); 489 } 490 } 491 // As the supported key event should only be notified when supported keys 492 // change, so abort following steps if they don't change. 493 if (newSupportedKeys == mSupportedKeys) { 494 return; 495 } 496 LOG("Supported keys changes"); 497 mSupportedKeys = newSupportedKeys; 498 mSupportedKeysChangedEvent.Notify(mSupportedKeys); 499 RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( 500 this, u"supportedkeyschange"_ns, CanBubble::eYes); 501 asyncDispatcher->PostDOMEvent(); 502 MediaController_Binding::ClearCachedSupportedKeysValue(this); 503 } 504 505 void MediaController::HandlePositionStateChanged( 506 const Maybe<PositionState>& aState) { 507 if (!aState) { 508 return; 509 } 510 511 PositionStateEventInit init; 512 init.mDuration = aState->mDuration; 513 init.mPlaybackRate = aState->mPlaybackRate; 514 init.mPosition = aState->mLastReportedPlaybackPosition; 515 RefPtr<PositionStateEvent> event = 516 PositionStateEvent::Constructor(this, u"positionstatechange"_ns, init); 517 DispatchAsyncEvent(event.forget()); 518 } 519 520 void MediaController::HandleMetadataChanged( 521 const MediaMetadataBase& aMetadata) { 522 // The reason we don't append metadata with `metadatachange` event is that 523 // allocating artwork might fail if the memory is not enough, but for the 524 // event we are not able to throw an error. Therefore, we want to the listener 525 // to use `getMetadata()` to get metadata, because it would throw an error if 526 // we fail to allocate artwork. 527 DispatchAsyncEvent(u"metadatachange"_ns); 528 // If metadata change is because of resetting active media session, then we 529 // should check if controller needs to be deactivated. 530 if (ShouldDeactivateController()) { 531 Deactivate(); 532 } 533 } 534 535 void MediaController::DispatchAsyncEvent(const nsAString& aName) { 536 RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr); 537 event->InitEvent(aName, false, false); 538 event->SetTrusted(true); 539 DispatchAsyncEvent(event.forget()); 540 } 541 542 void MediaController::DispatchAsyncEvent(already_AddRefed<Event> aEvent) { 543 RefPtr<Event> event = aEvent; 544 MOZ_ASSERT(event); 545 nsAutoString eventType; 546 event->GetType(eventType); 547 if (!mIsActive && !eventType.EqualsLiteral("deactivated")) { 548 LOG("Only 'deactivated' can be dispatched on a deactivated controller, not " 549 "'%s'", 550 NS_ConvertUTF16toUTF8(eventType).get()); 551 return; 552 } 553 LOG("Dispatch event %s", NS_ConvertUTF16toUTF8(eventType).get()); 554 RefPtr<AsyncEventDispatcher> asyncDispatcher = 555 new AsyncEventDispatcher(this, event.forget()); 556 asyncDispatcher->PostDOMEvent(); 557 } 558 559 CopyableTArray<MediaControlKey> MediaController::GetSupportedMediaKeys() const { 560 return mSupportedKeys; 561 } 562 563 void MediaController::Select() const { 564 if (RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id())) { 565 bc->Canonical()->AddPageAwakeRequest(); 566 } 567 } 568 569 void MediaController::Unselect() const { 570 if (RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id())) { 571 bc->Canonical()->RemovePageAwakeRequest(); 572 } 573 } 574 575 } // namespace mozilla::dom