Source code
Revision control
Copy as Markdown
Other Tools
/* -*- 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,
#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, MediaControlKey::Seekto,
MediaControlKey::Seekforward, MediaControlKey::Seekbackward};
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(double aSeekOffset) {
LOG("Seek Backward");
UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction(
MediaControlKey::Seekbackward, SeekDetails(aSeekOffset)));
}
void MediaController::SeekForward(double aSeekOffset) {
LOG("Seek Forward");
UpdateMediaControlActionToContentMediaIfNeeded(MediaControlAction(
MediaControlKey::Seekforward, SeekDetails(aSeekOffset)));
}
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 actions have default action handler for each frame, so we
// need to propagate to all contexts. We would handle default handlers in
// `ContentMediaController::HandleMediaKey`.
if (aAction.mKey.isSome()) {
switch (aAction.mKey.value()) {
case MediaControlKey::Play:
case MediaControlKey::Pause:
case MediaControlKey::Stop:
case MediaControlKey::Seekto:
case MediaControlKey::Seekforward:
case MediaControlKey::Seekbackward:
return true;
default:
return false;
}
}
return false;
}
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);
}
}
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 Maybe<PositionState>& aState) {
if (!aState) {
return;
}
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.forget());
}
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.forget());
}
void MediaController::DispatchAsyncEvent(already_AddRefed<Event> aEvent) {
RefPtr<Event> event = aEvent;
MOZ_ASSERT(event);
nsAutoString eventType;
event->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, event.forget());
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