diff options
Diffstat (limited to 'dom/media/mediacontrol/MediaController.cpp')
-rw-r--r-- | dom/media/mediacontrol/MediaController.cpp | 560 |
1 files changed, 560 insertions, 0 deletions
diff --git a/dom/media/mediacontrol/MediaController.cpp b/dom/media/mediacontrol/MediaController.cpp new file mode 100644 index 0000000000..b06635091d --- /dev/null +++ b/dom/media/mediacontrol/MediaController.cpp @@ -0,0 +1,560 @@ +/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=8 sts=2 et sw=2 tw=80: */ +/* 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 "MediaController.h" + +#include "MediaControlService.h" +#include "MediaControlUtils.h" +#include "MediaControlKeySource.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/StaticPrefs_media.h" +#include "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/CanonicalBrowsingContext.h" +#include "mozilla/dom/MediaSession.h" +#include "mozilla/dom/PositionStateEvent.h" + +// avoid redefined macro in unified build +#undef LOG +#define LOG(msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("MediaController=%p, Id=%" PRId64 ", " msg, this, this->Id(), \ + ##__VA_ARGS__)) + +namespace mozilla::dom { + +NS_IMPL_CYCLE_COLLECTION_INHERITED(MediaController, DOMEventTargetHelper) +NS_IMPL_ISUPPORTS_CYCLE_COLLECTION_INHERITED(MediaController, + DOMEventTargetHelper, + nsITimerCallback, nsINamed) +NS_IMPL_CYCLE_COLLECTION_TRACE_BEGIN_INHERITED(MediaController, + DOMEventTargetHelper) +NS_IMPL_CYCLE_COLLECTION_TRACE_END + +nsISupports* MediaController::GetParentObject() const { + RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id()); + return bc; +} + +JSObject* MediaController::WrapObject(JSContext* aCx, + JS::Handle<JSObject*> aGivenProto) { + return MediaController_Binding::Wrap(aCx, this, aGivenProto); +} + +void MediaController::GetSupportedKeys( + nsTArray<MediaControlKey>& aRetVal) const { + aRetVal.Clear(); + for (const auto& key : mSupportedKeys) { + aRetVal.AppendElement(key); + } +} + +void MediaController::GetMetadata(MediaMetadataInit& aMetadata, + ErrorResult& aRv) { + if (!IsActive() || mShutdown) { + aRv.Throw(NS_ERROR_NOT_AVAILABLE); + return; + } + + const MediaMetadataBase metadata = GetCurrentMediaMetadata(); + aMetadata.mTitle = metadata.mTitle; + aMetadata.mArtist = metadata.mArtist; + aMetadata.mAlbum = metadata.mAlbum; + for (const auto& artwork : metadata.mArtwork) { + if (MediaImage* image = aMetadata.mArtwork.AppendElement(fallible)) { + image->mSrc = artwork.mSrc; + image->mSizes = artwork.mSizes; + image->mType = artwork.mType; + } else { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + } +} + +static const MediaControlKey sDefaultSupportedKeys[] = { + MediaControlKey::Focus, MediaControlKey::Play, MediaControlKey::Pause, + MediaControlKey::Playpause, MediaControlKey::Stop, +}; + +static void GetDefaultSupportedKeys(nsTArray<MediaControlKey>& aKeys) { + for (const auto& key : sDefaultSupportedKeys) { + aKeys.AppendElement(key); + } +} + +MediaController::MediaController(uint64_t aBrowsingContextId) + : MediaStatusManager(aBrowsingContextId) { + MOZ_DIAGNOSTIC_ASSERT(XRE_IsParentProcess(), + "MediaController only runs on Chrome process!"); + LOG("Create controller %" PRId64, Id()); + GetDefaultSupportedKeys(mSupportedKeys); + mSupportedActionsChangedListener = SupportedActionsChangedEvent().Connect( + AbstractThread::MainThread(), this, + &MediaController::HandleSupportedMediaSessionActionsChanged); + mPlaybackChangedListener = PlaybackChangedEvent().Connect( + AbstractThread::MainThread(), this, + &MediaController::HandleActualPlaybackStateChanged); + mPositionStateChangedListener = PositionChangedEvent().Connect( + AbstractThread::MainThread(), this, + &MediaController::HandlePositionStateChanged); + mMetadataChangedListener = + MetadataChangedEvent().Connect(AbstractThread::MainThread(), this, + &MediaController::HandleMetadataChanged); +} + +MediaController::~MediaController() { + LOG("Destroy controller %" PRId64, Id()); + if (!mShutdown) { + Shutdown(); + } +}; + +void MediaController::Focus() { + LOG("Focus"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Focus)); +} + +void MediaController::Play() { + LOG("Play"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Play)); +} + +void MediaController::Pause() { + LOG("Pause"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Pause)); +} + +void MediaController::PrevTrack() { + LOG("Prev Track"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Previoustrack)); +} + +void MediaController::NextTrack() { + LOG("Next Track"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Nexttrack)); +} + +void MediaController::SeekBackward() { + LOG("Seek Backward"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Seekbackward)); +} + +void MediaController::SeekForward() { + LOG("Seek Forward"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Seekforward)); +} + +void MediaController::SkipAd() { + LOG("Skip Ad"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Skipad)); +} + +void MediaController::SeekTo(double aSeekTime, bool aFastSeek) { + LOG("Seek To"); + UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction( + MediaControlKey::Seekto, SeekDetails(aSeekTime, aFastSeek))); +} + +void MediaController::Stop() { + LOG("Stop"); + UpdateMediaControlActionToContentMediaIfNeeded( + MediaControlAction(MediaControlKey::Stop)); + MediaStatusManager::ClearActiveMediaSessionContextIdIfNeeded(); +} + +uint64_t MediaController::Id() const { return mTopLevelBrowsingContextId; } + +bool MediaController::IsAudible() const { return IsMediaAudible(); } + +bool MediaController::IsPlaying() const { return IsMediaPlaying(); } + +bool MediaController::IsActive() const { return mIsActive; }; + +bool MediaController::ShouldPropagateActionToAllContexts( + const MediaControlAction& aAction) const { + // These three actions have default action handler for each frame, so we + // need to propagate to all contexts. We would handle default handlers in + // `ContentMediaController::HandleMediaKey`. + return aAction.mKey == MediaControlKey::Play || + aAction.mKey == MediaControlKey::Pause || + aAction.mKey == MediaControlKey::Stop; +} + +void MediaController::UpdateMediaControlActionToContentMediaIfNeeded( + const MediaControlAction& aAction) { + // If the controller isn't active or it has been shutdown, we don't need to + // update media action to the content process. + if (!mIsActive || mShutdown) { + return; + } + + // For some actions which have default action handler, we want to propagate + // them on all contexts in order to trigger the default handler on each + // context separately. Otherwise, other action should only be propagated to + // the context where active media session exists. + const bool propateToAll = ShouldPropagateActionToAllContexts(aAction); + const uint64_t targetContextId = propateToAll || !mActiveMediaSessionContextId + ? Id() + : *mActiveMediaSessionContextId; + RefPtr<BrowsingContext> context = BrowsingContext::Get(targetContextId); + if (!context || context->IsDiscarded()) { + return; + } + + if (propateToAll) { + context->PreOrderWalk([&](BrowsingContext* bc) { + bc->Canonical()->UpdateMediaControlAction(aAction); + }); + } else { + context->Canonical()->UpdateMediaControlAction(aAction); + } + RefPtr<MediaControlService> service = MediaControlService::GetService(); + MOZ_ASSERT(service); + service->NotifyMediaControlHasEverBeenUsed(); +} + +void MediaController::Shutdown() { + MOZ_ASSERT(!mShutdown, "Do not call shutdown twice!"); + // The media controller would be removed from the service when we receive a + // notification from the content process about all controlled media has been + // stoppped. However, if controlled media is stopped after detaching + // browsing context, then sending the notification from the content process + // would fail so that we are not able to notify the chrome process to remove + // the corresponding controller. Therefore, we should manually remove the + // controller from the service. + Deactivate(); + mShutdown = true; + mSupportedActionsChangedListener.DisconnectIfExists(); + mPlaybackChangedListener.DisconnectIfExists(); + mPositionStateChangedListener.DisconnectIfExists(); + mMetadataChangedListener.DisconnectIfExists(); +} + +void MediaController::NotifyMediaPlaybackChanged(uint64_t aBrowsingContextId, + MediaPlaybackState aState) { + if (mShutdown) { + return; + } + MediaStatusManager::NotifyMediaPlaybackChanged(aBrowsingContextId, aState); + UpdateDeactivationTimerIfNeeded(); + UpdateActivatedStateIfNeeded(); +} + +void MediaController::UpdateDeactivationTimerIfNeeded() { + if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) { + return; + } + + bool shouldBeAlwaysActive = IsPlaying() || IsBeingUsedInPIPModeOrFullscreen(); + if (shouldBeAlwaysActive && mDeactivationTimer) { + LOG("Cancel deactivation timer"); + mDeactivationTimer->Cancel(); + mDeactivationTimer = nullptr; + } else if (!shouldBeAlwaysActive && !mDeactivationTimer) { + nsresult rv = NS_NewTimerWithCallback( + getter_AddRefs(mDeactivationTimer), this, + StaticPrefs::media_mediacontrol_stopcontrol_timer_ms(), + nsITimer::TYPE_ONE_SHOT, AbstractThread::MainThread()); + if (NS_SUCCEEDED(rv)) { + LOG("Create a deactivation timer"); + } else { + LOG("Failed to create a deactivation timer"); + } + } +} + +bool MediaController::IsBeingUsedInPIPModeOrFullscreen() const { + return mIsInPictureInPictureMode || mIsInFullScreenMode; +} + +NS_IMETHODIMP MediaController::Notify(nsITimer* aTimer) { + mDeactivationTimer = nullptr; + if (!StaticPrefs::media_mediacontrol_stopcontrol_timer()) { + return NS_OK; + } + + if (mShutdown) { + LOG("Cancel deactivation timer because controller has been shutdown"); + return NS_OK; + } + + // As the media being used in the PIP mode or fullscreen would always display + // on the screen, users would have high chance to interact with it again, so + // we don't want to stop media control. + if (IsBeingUsedInPIPModeOrFullscreen()) { + LOG("Cancel deactivation timer because controller is in PIP mode"); + return NS_OK; + } + + if (IsPlaying()) { + LOG("Cancel deactivation timer because controller is still playing"); + return NS_OK; + } + + if (!mIsActive) { + LOG("Cancel deactivation timer because controller has been deactivated"); + return NS_OK; + } + Deactivate(); + return NS_OK; +} + +NS_IMETHODIMP MediaController::GetName(nsACString& aName) { + aName.AssignLiteral("MediaController"); + return NS_OK; +} + +void MediaController::NotifyMediaAudibleChanged(uint64_t aBrowsingContextId, + MediaAudibleState aState) { + if (mShutdown) { + return; + } + + bool oldAudible = IsAudible(); + MediaStatusManager::NotifyMediaAudibleChanged(aBrowsingContextId, aState); + if (IsAudible() == oldAudible) { + return; + } + UpdateActivatedStateIfNeeded(); + + // Request the audio focus amongs different controllers that could cause + // pausing other audible controllers if we enable the audio focus management. + RefPtr<MediaControlService> service = MediaControlService::GetService(); + MOZ_ASSERT(service); + if (IsAudible()) { + service->GetAudioFocusManager().RequestAudioFocus(this); + } else { + service->GetAudioFocusManager().RevokeAudioFocus(this); + } +} + +bool MediaController::ShouldActivateController() const { + MOZ_ASSERT(!mShutdown); + // After media is successfully loaded and match our critiera, such as its + // duration is longer enough, which is used to exclude the notification-ish + // sound, then it would be able to be controlled once the controll gets + // activated. + // + // Activating a controller means that we would start to intercept the media + // keys on the platform and show the virtual control interface (if needed). + // The controller would be activated when (1) controllable media starts in the + // browsing context that controller belongs to (2) controllable media enters + // fullscreen or PIP mode. + return IsAnyMediaBeingControlled() && + (IsPlaying() || IsBeingUsedInPIPModeOrFullscreen()) && !mIsActive; +} + +bool MediaController::ShouldDeactivateController() const { + MOZ_ASSERT(!mShutdown); + // If we don't have an active media session and no controlled media exists, + // then we don't need to keep controller active, because there is nothing to + // control. However, if we still have an active media session, then we should + // keep controller active in order to receive media keys even if we don't have + // any controlled media existing, because a website might start other media + // when media session receives media keys. + return !IsAnyMediaBeingControlled() && mIsActive && + !mActiveMediaSessionContextId; +} + +void MediaController::Activate() { + MOZ_ASSERT(!mShutdown); + RefPtr<MediaControlService> service = MediaControlService::GetService(); + if (service && !mIsActive) { + LOG("Activate"); + mIsActive = service->RegisterActiveMediaController(this); + MOZ_ASSERT(mIsActive, "Fail to register controller!"); + DispatchAsyncEvent(u"activated"_ns); + } +} + +void MediaController::Deactivate() { + MOZ_ASSERT(!mShutdown); + RefPtr<MediaControlService> service = MediaControlService::GetService(); + if (service) { + service->GetAudioFocusManager().RevokeAudioFocus(this); + if (mIsActive) { + LOG("Deactivate"); + mIsActive = !service->UnregisterActiveMediaController(this); + MOZ_ASSERT(!mIsActive, "Fail to unregister controller!"); + DispatchAsyncEvent(u"deactivated"_ns); + } + } +} + +void MediaController::SetIsInPictureInPictureMode( + uint64_t aBrowsingContextId, bool aIsInPictureInPictureMode) { + if (mIsInPictureInPictureMode == aIsInPictureInPictureMode) { + return; + } + LOG("Set IsInPictureInPictureMode to %s", + aIsInPictureInPictureMode ? "true" : "false"); + mIsInPictureInPictureMode = aIsInPictureInPictureMode; + ForceToBecomeMainControllerIfNeeded(); + UpdateDeactivationTimerIfNeeded(); + mPictureInPictureModeChangedEvent.Notify(mIsInPictureInPictureMode); +} + +void MediaController::NotifyMediaFullScreenState(uint64_t aBrowsingContextId, + bool aIsInFullScreen) { + if (mIsInFullScreenMode == aIsInFullScreen) { + return; + } + LOG("%s fullscreen", aIsInFullScreen ? "Entered" : "Left"); + mIsInFullScreenMode = aIsInFullScreen; + ForceToBecomeMainControllerIfNeeded(); + mFullScreenChangedEvent.Notify(mIsInFullScreenMode); +} + +bool MediaController::IsMainController() const { + RefPtr<MediaControlService> service = MediaControlService::GetService(); + return service ? service->GetMainController() == this : false; +} + +bool MediaController::ShouldRequestForMainController() const { + // This controller is already the main controller. + if (IsMainController()) { + return false; + } + // We would only force controller to become main controller if it's in the + // PIP mode or fullscreen, otherwise it should follow the general rule. + // In addition, do nothing if the controller has been shutdowned. + return IsBeingUsedInPIPModeOrFullscreen() && !mShutdown; +} + +void MediaController::ForceToBecomeMainControllerIfNeeded() { + if (!ShouldRequestForMainController()) { + return; + } + RefPtr<MediaControlService> service = MediaControlService::GetService(); + MOZ_ASSERT(service, "service was shutdown before shutting down controller?"); + // If the controller hasn't been activated and it's ready to be activated, + // then activating it should also make it become a main controller. If it's + // already activated but isn't a main controller yet, then explicitly request + // it. + if (!IsActive() && ShouldActivateController()) { + Activate(); + } else if (IsActive()) { + service->RequestUpdateMainController(this); + } +} + +void MediaController::HandleActualPlaybackStateChanged() { + // Media control service would like to know all controllers' playback state + // in order to decide which controller should be the main controller that is + // usually the last tab which plays media. + if (RefPtr<MediaControlService> service = MediaControlService::GetService()) { + service->NotifyControllerPlaybackStateChanged(this); + } + DispatchAsyncEvent(u"playbackstatechange"_ns); +} + +void MediaController::UpdateActivatedStateIfNeeded() { + if (ShouldActivateController()) { + Activate(); + } else if (ShouldDeactivateController()) { + Deactivate(); + } +} + +void MediaController::HandleSupportedMediaSessionActionsChanged( + const nsTArray<MediaSessionAction>& aSupportedAction) { + // Convert actions to keys, some of them have been included in the supported + // keys, such as "play", "pause" and "stop". + nsTArray<MediaControlKey> newSupportedKeys; + GetDefaultSupportedKeys(newSupportedKeys); + for (const auto& action : aSupportedAction) { + MediaControlKey key = ConvertMediaSessionActionToControlKey(action); + if (!newSupportedKeys.Contains(key)) { + newSupportedKeys.AppendElement(key); + } + } + // As the supported key event should only be notified when supported keys + // change, so abort following steps if they don't change. + if (newSupportedKeys == mSupportedKeys) { + return; + } + LOG("Supported keys changes"); + mSupportedKeys = newSupportedKeys; + mSupportedKeysChangedEvent.Notify(mSupportedKeys); + RefPtr<AsyncEventDispatcher> asyncDispatcher = new AsyncEventDispatcher( + this, u"supportedkeyschange"_ns, CanBubble::eYes); + asyncDispatcher->PostDOMEvent(); + MediaController_Binding::ClearCachedSupportedKeysValue(this); +} + +void MediaController::HandlePositionStateChanged(const PositionState& aState) { + PositionStateEventInit init; + init.mDuration = aState.mDuration; + init.mPlaybackRate = aState.mPlaybackRate; + init.mPosition = aState.mLastReportedPlaybackPosition; + RefPtr<PositionStateEvent> event = + PositionStateEvent::Constructor(this, u"positionstatechange"_ns, init); + DispatchAsyncEvent(event); +} + +void MediaController::HandleMetadataChanged( + const MediaMetadataBase& aMetadata) { + // The reason we don't append metadata with `metadatachange` event is that + // allocating artwork might fail if the memory is not enough, but for the + // event we are not able to throw an error. Therefore, we want to the listener + // to use `getMetadata()` to get metadata, because it would throw an error if + // we fail to allocate artwork. + DispatchAsyncEvent(u"metadatachange"_ns); + // If metadata change is because of resetting active media session, then we + // should check if controller needs to be deactivated. + if (ShouldDeactivateController()) { + Deactivate(); + } +} + +void MediaController::DispatchAsyncEvent(const nsAString& aName) { + RefPtr<Event> event = NS_NewDOMEvent(this, nullptr, nullptr); + event->InitEvent(aName, false, false); + event->SetTrusted(true); + DispatchAsyncEvent(event); +} + +void MediaController::DispatchAsyncEvent(Event* aEvent) { + MOZ_ASSERT(aEvent); + nsAutoString eventType; + aEvent->GetType(eventType); + if (!mIsActive && !eventType.EqualsLiteral("deactivated")) { + LOG("Only 'deactivated' can be dispatched on a deactivated controller, not " + "'%s'", + NS_ConvertUTF16toUTF8(eventType).get()); + return; + } + LOG("Dispatch event %s", NS_ConvertUTF16toUTF8(eventType).get()); + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(this, aEvent); + asyncDispatcher->PostDOMEvent(); +} + +CopyableTArray<MediaControlKey> MediaController::GetSupportedMediaKeys() const { + return mSupportedKeys; +} + +void MediaController::Select() const { + if (RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id())) { + bc->Canonical()->AddPageAwakeRequest(); + } +} + +void MediaController::Unselect() const { + if (RefPtr<BrowsingContext> bc = BrowsingContext::Get(Id())) { + bc->Canonical()->RemovePageAwakeRequest(); + } +} + +} // namespace mozilla::dom |