From 0ebf5bdf043a27fd3dfb7f92e0cb63d88954c44d Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:47:29 +0200 Subject: Adding upstream version 115.8.0esr. Signed-off-by: Daniel Baumann --- dom/media/mediasession/MediaMetadata.cpp | 154 ++++++++++ dom/media/mediasession/MediaMetadata.h | 97 ++++++ dom/media/mediasession/MediaSession.cpp | 335 +++++++++++++++++++++ dom/media/mediasession/MediaSession.h | 132 ++++++++ dom/media/mediasession/MediaSessionIPCUtils.h | 102 +++++++ dom/media/mediasession/moz.build | 24 ++ .../mediasession/test/MediaSessionTestUtils.js | 30 ++ dom/media/mediasession/test/browser.ini | 9 + .../test/browser_active_mediasession_among_tabs.js | 201 +++++++++++++ .../mediasession/test/crashtests/crashtests.list | 1 + .../test/crashtests/inactive-mediasession.html | 16 + .../mediasession/test/file_media_session.html | 31 ++ .../test/file_trigger_actionhanlder_frame.html | 40 +++ .../test/file_trigger_actionhanlder_window.html | 107 +++++++ dom/media/mediasession/test/mochitest.ini | 12 + .../mediasession/test/test_setactionhandler.html | 92 ++++++ .../test/test_trigger_actionhanlder.html | 57 ++++ 17 files changed, 1440 insertions(+) create mode 100644 dom/media/mediasession/MediaMetadata.cpp create mode 100644 dom/media/mediasession/MediaMetadata.h create mode 100644 dom/media/mediasession/MediaSession.cpp create mode 100644 dom/media/mediasession/MediaSession.h create mode 100644 dom/media/mediasession/MediaSessionIPCUtils.h create mode 100644 dom/media/mediasession/moz.build create mode 100644 dom/media/mediasession/test/MediaSessionTestUtils.js create mode 100644 dom/media/mediasession/test/browser.ini create mode 100644 dom/media/mediasession/test/browser_active_mediasession_among_tabs.js create mode 100644 dom/media/mediasession/test/crashtests/crashtests.list create mode 100644 dom/media/mediasession/test/crashtests/inactive-mediasession.html create mode 100644 dom/media/mediasession/test/file_media_session.html create mode 100644 dom/media/mediasession/test/file_trigger_actionhanlder_frame.html create mode 100644 dom/media/mediasession/test/file_trigger_actionhanlder_window.html create mode 100644 dom/media/mediasession/test/mochitest.ini create mode 100644 dom/media/mediasession/test/test_setactionhandler.html create mode 100644 dom/media/mediasession/test/test_trigger_actionhanlder.html (limited to 'dom/media/mediasession') diff --git a/dom/media/mediasession/MediaMetadata.cpp b/dom/media/mediasession/MediaMetadata.cpp new file mode 100644 index 0000000000..8ee2c217ac --- /dev/null +++ b/dom/media/mediasession/MediaMetadata.cpp @@ -0,0 +1,154 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/MediaMetadata.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MediaSessionBinding.h" +#include "mozilla/dom/ScriptSettings.h" +#include "mozilla/dom/ToJSValue.h" +#include "nsNetUtil.h" + +namespace mozilla::dom { + +// Only needed for refcounted objects. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE(MediaMetadata, mParent) +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaMetadata) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaMetadata) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaMetadata) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +MediaMetadata::MediaMetadata(nsIGlobalObject* aParent, const nsString& aTitle, + const nsString& aArtist, const nsString& aAlbum) + : MediaMetadataBase(aTitle, aArtist, aAlbum), mParent(aParent) { + MOZ_ASSERT(mParent); +} + +nsIGlobalObject* MediaMetadata::GetParentObject() const { return mParent; } + +JSObject* MediaMetadata::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return MediaMetadata_Binding::Wrap(aCx, this, aGivenProto); +} + +already_AddRefed MediaMetadata::Constructor( + const GlobalObject& aGlobal, const MediaMetadataInit& aInit, + ErrorResult& aRv) { + nsCOMPtr global = do_QueryInterface(aGlobal.GetAsSupports()); + if (!global) { + aRv.Throw(NS_ERROR_FAILURE); + return nullptr; + } + + RefPtr mediaMetadata = + new MediaMetadata(global, aInit.mTitle, aInit.mArtist, aInit.mAlbum); + mediaMetadata->SetArtworkInternal(aInit.mArtwork, aRv); + return aRv.Failed() ? nullptr : mediaMetadata.forget(); +} + +void MediaMetadata::GetTitle(nsString& aRetVal) const { aRetVal = mTitle; } + +void MediaMetadata::SetTitle(const nsAString& aTitle) { mTitle = aTitle; } + +void MediaMetadata::GetArtist(nsString& aRetVal) const { aRetVal = mArtist; } + +void MediaMetadata::SetArtist(const nsAString& aArtist) { mArtist = aArtist; } + +void MediaMetadata::GetAlbum(nsString& aRetVal) const { aRetVal = mAlbum; } + +void MediaMetadata::SetAlbum(const nsAString& aAlbum) { mAlbum = aAlbum; } + +void MediaMetadata::GetArtwork(JSContext* aCx, nsTArray& aRetVal, + ErrorResult& aRv) const { + // Convert the MediaImages to JS Objects + if (!aRetVal.SetCapacity(mArtwork.Length(), fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (size_t i = 0; i < mArtwork.Length(); ++i) { + JS::Rooted value(aCx); + if (!ToJSValue(aCx, mArtwork[i], &value)) { + aRv.NoteJSContextException(aCx); + return; + } + + JS::Rooted object(aCx, &value.toObject()); + if (!JS_FreezeObject(aCx, object)) { + aRv.NoteJSContextException(aCx); + return; + } + + aRetVal.AppendElement(object); + } +} + +void MediaMetadata::SetArtwork(JSContext* aCx, + const Sequence& aArtwork, + ErrorResult& aRv) { + // Convert the JS Objects to MediaImages + Sequence artwork; + if (!artwork.SetCapacity(aArtwork.Length(), fallible)) { + aRv.Throw(NS_ERROR_OUT_OF_MEMORY); + return; + } + + for (JSObject* object : aArtwork) { + JS::Rooted value(aCx, JS::ObjectValue(*object)); + + MediaImage* image = artwork.AppendElement(fallible); + MOZ_ASSERT(image, "The capacity is preallocated"); + if (!image->Init(aCx, value)) { + aRv.NoteJSContextException(aCx); + return; + } + } + + SetArtworkInternal(artwork, aRv); +}; + +static nsIURI* GetEntryBaseURL() { + nsCOMPtr doc = GetEntryDocument(); + return doc ? doc->GetDocBaseURI() : nullptr; +} + +// `aURL` is an inout parameter. +static nsresult ResolveURL(nsString& aURL, nsIURI* aBaseURI) { + nsCOMPtr uri; + nsresult rv = NS_NewURI(getter_AddRefs(uri), aURL, + /* UTF-8 for charset */ nullptr, aBaseURI); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString spec; + rv = uri->GetSpec(spec); + if (NS_FAILED(rv)) { + return rv; + } + + CopyUTF8toUTF16(spec, aURL); + return NS_OK; +} + +void MediaMetadata::SetArtworkInternal(const Sequence& aArtwork, + ErrorResult& aRv) { + nsTArray artwork; + artwork.Assign(aArtwork); + + nsCOMPtr baseURI = GetEntryBaseURL(); + for (MediaImage& image : artwork) { + nsresult rv = ResolveURL(image.mSrc, baseURI); + if (NS_WARN_IF(NS_FAILED(rv))) { + aRv.ThrowTypeError(NS_ConvertUTF16toUTF8(image.mSrc)); + return; + } + } + mArtwork = std::move(artwork); +} + +} // namespace mozilla::dom diff --git a/dom/media/mediasession/MediaMetadata.h b/dom/media/mediasession/MediaMetadata.h new file mode 100644 index 0000000000..6c552e6465 --- /dev/null +++ b/dom/media/mediasession/MediaMetadata.h @@ -0,0 +1,97 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef mozilla_dom_MediaMetadata_h +#define mozilla_dom_MediaMetadata_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/BindingDeclarations.h" +#include "mozilla/dom/MediaSessionBinding.h" +#include "nsCycleCollectionParticipant.h" +#include "nsWrapperCache.h" + +class nsIGlobalObject; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class MediaMetadataBase { + public: + MediaMetadataBase() = default; + MediaMetadataBase(const nsString& aTitle, const nsString& aArtist, + const nsString& aAlbum) + : mTitle(aTitle), mArtist(aArtist), mAlbum(aAlbum) {} + + static MediaMetadataBase EmptyData() { return MediaMetadataBase(); } + + nsString mTitle; + nsString mArtist; + nsString mAlbum; + CopyableTArray mArtwork; +}; + +class MediaMetadata final : public nsISupports, + public nsWrapperCache, + private MediaMetadataBase { + public: + // Ref counting and cycle collection + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaMetadata) + + // WebIDL methods + nsIGlobalObject* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + static already_AddRefed Constructor( + const GlobalObject& aGlobal, const MediaMetadataInit& aInit, + ErrorResult& aRv); + + void GetTitle(nsString& aRetVal) const; + + void SetTitle(const nsAString& aTitle); + + void GetArtist(nsString& aRetVal) const; + + void SetArtist(const nsAString& aArtist); + + void GetAlbum(nsString& aRetVal) const; + + void SetAlbum(const nsAString& aAlbum); + + void GetArtwork(JSContext* aCx, nsTArray& aRetVal, + ErrorResult& aRv) const; + + void SetArtwork(JSContext* aCx, const Sequence& aArtwork, + ErrorResult& aRv); + + // This would expose MediaMetadataBase's members as public, so use this method + // carefully. Now we only use this when we want to update the metadata to the + // media session controller in the chrome process. + MediaMetadataBase* AsMetadataBase() { return this; } + + private: + MediaMetadata(nsIGlobalObject* aParent, const nsString& aTitle, + const nsString& aArtist, const nsString& aAlbum); + + ~MediaMetadata() = default; + + // Perform `convert artwork algorithm`. Set `mArtwork` to a converted + // `aArtwork` if the conversion works, otherwise throw a type error in `aRv`. + void SetArtworkInternal(const Sequence& aArtwork, + ErrorResult& aRv); + + nsCOMPtr mParent; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MediaMetadata_h diff --git a/dom/media/mediasession/MediaSession.cpp b/dom/media/mediasession/MediaSession.cpp new file mode 100644 index 0000000000..e55fa28d96 --- /dev/null +++ b/dom/media/mediasession/MediaSession.cpp @@ -0,0 +1,335 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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 "mozilla/dom/BrowsingContext.h" +#include "mozilla/dom/ContentMediaController.h" +#include "mozilla/dom/Document.h" +#include "mozilla/dom/MediaSession.h" +#include "mozilla/dom/MediaControlUtils.h" +#include "mozilla/dom/WindowContext.h" +#include "mozilla/EnumeratedArrayCycleCollection.h" + +// avoid redefined macro in unified build +#undef LOG +#define LOG(msg, ...) \ + MOZ_LOG(gMediaControlLog, LogLevel::Debug, \ + ("MediaSession=%p, " msg, this, ##__VA_ARGS__)) + +namespace mozilla::dom { + +// We don't use NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE because we need to +// unregister MediaSession from document's activity listeners. +NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaSession) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaSession) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaMetadata) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionHandlers) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDoc) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaSession) + tmp->Shutdown(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaMetadata) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionHandlers) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDoc) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaSession) +NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaSession) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaSession) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity) +NS_INTERFACE_MAP_END + +MediaSession::MediaSession(nsPIDOMWindowInner* aParent) + : mParent(aParent), mDoc(mParent->GetExtantDoc()) { + MOZ_ASSERT(mParent); + MOZ_ASSERT(mDoc); + mDoc->RegisterActivityObserver(this); + if (mDoc->IsCurrentActiveDocument()) { + SetMediaSessionDocStatus(SessionDocStatus::eActive); + } +} + +void MediaSession::Shutdown() { + if (mDoc) { + mDoc->UnregisterActivityObserver(this); + } + if (mParent) { + SetMediaSessionDocStatus(SessionDocStatus::eInactive); + } +} + +void MediaSession::NotifyOwnerDocumentActivityChanged() { + const bool isDocActive = mDoc->IsCurrentActiveDocument(); + LOG("Document activity changed, isActive=%d", isDocActive); + if (isDocActive) { + SetMediaSessionDocStatus(SessionDocStatus::eActive); + } else { + SetMediaSessionDocStatus(SessionDocStatus::eInactive); + } +} + +void MediaSession::SetMediaSessionDocStatus(SessionDocStatus aState) { + if (mSessionDocState == aState) { + return; + } + mSessionDocState = aState; + NotifyMediaSessionDocStatus(mSessionDocState); +} + +nsPIDOMWindowInner* MediaSession::GetParentObject() const { return mParent; } + +JSObject* MediaSession::WrapObject(JSContext* aCx, + JS::Handle aGivenProto) { + return MediaSession_Binding::Wrap(aCx, this, aGivenProto); +} + +MediaMetadata* MediaSession::GetMetadata() const { return mMediaMetadata; } + +void MediaSession::SetMetadata(MediaMetadata* aMetadata) { + mMediaMetadata = aMetadata; + NotifyMetadataUpdated(); +} + +void MediaSession::SetPlaybackState( + const MediaSessionPlaybackState& aPlaybackState) { + if (mDeclaredPlaybackState == aPlaybackState) { + return; + } + mDeclaredPlaybackState = aPlaybackState; + NotifyPlaybackStateUpdated(); +} + +MediaSessionPlaybackState MediaSession::PlaybackState() const { + return mDeclaredPlaybackState; +} + +void MediaSession::SetActionHandler(MediaSessionAction aAction, + MediaSessionActionHandler* aHandler) { + MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers)); + // If the media session changes its supported action, then we would propagate + // this information to the chrome process in order to run the media session + // actions update algorithm. + // https://w3c.github.io/mediasession/#supported-media-session-actions + RefPtr& hanlder = mActionHandlers[aAction]; + if (!hanlder && aHandler) { + NotifyEnableSupportedAction(aAction); + } else if (hanlder && !aHandler) { + NotifyDisableSupportedAction(aAction); + } + mActionHandlers[aAction] = aHandler; +} + +MediaSessionActionHandler* MediaSession::GetActionHandler( + MediaSessionAction aAction) const { + MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers)); + return mActionHandlers[aAction]; +} + +void MediaSession::SetPositionState(const MediaPositionState& aState, + ErrorResult& aRv) { + // https://w3c.github.io/mediasession/#dom-mediasession-setpositionstate + // If the state is an empty dictionary then clear the position state. + if (!aState.IsAnyMemberPresent()) { + mPositionState.reset(); + return; + } + + // If the duration is not present, throw a TypeError. + if (!aState.mDuration.WasPassed()) { + return aRv.ThrowTypeError("Duration is not present"); + } + + // If the duration is negative, throw a TypeError. + if (aState.mDuration.WasPassed() && aState.mDuration.Value() < 0.0) { + return aRv.ThrowTypeError(nsPrintfCString( + "Invalid duration %f, it can't be negative", aState.mDuration.Value())); + } + + // If the position is negative or greater than duration, throw a TypeError. + if (aState.mPosition.WasPassed() && + (aState.mPosition.Value() < 0.0 || + aState.mPosition.Value() > aState.mDuration.Value())) { + return aRv.ThrowTypeError(nsPrintfCString( + "Invalid position %f, it can't be negative or greater than duration", + aState.mPosition.Value())); + } + + // If the playbackRate is zero, throw a TypeError. + if (aState.mPlaybackRate.WasPassed() && aState.mPlaybackRate.Value() == 0.0) { + return aRv.ThrowTypeError("The playbackRate is zero"); + } + + // If the position is not present, set it to zero. + double position = aState.mPosition.WasPassed() ? aState.mPosition.Value() : 0; + + // If the playbackRate is not present, set it to 1.0. + double playbackRate = + aState.mPlaybackRate.WasPassed() ? aState.mPlaybackRate.Value() : 1.0; + + // Update the position state and last position updated time. + MOZ_ASSERT(aState.mDuration.WasPassed()); + mPositionState = + Some(PositionState(aState.mDuration.Value(), playbackRate, position)); + NotifyPositionStateChanged(); +} + +void MediaSession::NotifyHandler(const MediaSessionActionDetails& aDetails) { + DispatchNotifyHandler(aDetails); +} + +void MediaSession::DispatchNotifyHandler( + const MediaSessionActionDetails& aDetails) { + class Runnable final : public mozilla::Runnable { + public: + Runnable(const MediaSession* aSession, + const MediaSessionActionDetails& aDetails) + : mozilla::Runnable("MediaSession::DispatchNotifyHandler"), + mSession(aSession), + mDetails(aDetails) {} + + MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override { + if (RefPtr handler = + mSession->GetActionHandler(mDetails.mAction)) { + handler->Call(mDetails); + } + return NS_OK; + } + + private: + RefPtr mSession; + MediaSessionActionDetails mDetails; + }; + + RefPtr runnable = new Runnable(this, aDetails); + NS_DispatchToMainThread(runnable); +} + +bool MediaSession::IsSupportedAction(MediaSessionAction aAction) const { + MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers)); + return mActionHandlers[aAction] != nullptr; +} + +bool MediaSession::IsActive() const { + RefPtr currentBC = GetParentObject()->GetBrowsingContext(); + MOZ_ASSERT(currentBC); + RefPtr wc = currentBC->GetTopWindowContext(); + if (!wc) { + return false; + } + Maybe activeSessionContextId = wc->GetActiveMediaSessionContextId(); + if (!activeSessionContextId) { + return false; + } + LOG("session context Id=%" PRIu64 ", active session context Id=%" PRIu64, + currentBC->Id(), *activeSessionContextId); + return *activeSessionContextId == currentBC->Id(); +} + +void MediaSession::NotifyMediaSessionDocStatus(SessionDocStatus aState) { + RefPtr currentBC = GetParentObject()->GetBrowsingContext(); + MOZ_ASSERT(currentBC, "Update session status after context destroyed!"); + + RefPtr updater = ContentMediaAgent::Get(currentBC); + if (!updater) { + return; + } + if (aState == SessionDocStatus::eActive) { + updater->NotifySessionCreated(currentBC->Id()); + // If media session set its attributes before its document becomes active, + // then we would notify those attributes which hasn't been notified as well + // because attributes update would only happen if its document is already + // active. + NotifyMediaSessionAttributes(); + } else { + updater->NotifySessionDestroyed(currentBC->Id()); + } +} + +void MediaSession::NotifyMediaSessionAttributes() { + MOZ_ASSERT(mSessionDocState == SessionDocStatus::eActive); + if (mDeclaredPlaybackState != MediaSessionPlaybackState::None) { + NotifyPlaybackStateUpdated(); + } + if (mMediaMetadata) { + NotifyMetadataUpdated(); + } + for (size_t idx = 0; idx < ArrayLength(mActionHandlers); idx++) { + MediaSessionAction action = static_cast(idx); + if (mActionHandlers[action]) { + NotifyEnableSupportedAction(action); + } + } + if (mPositionState) { + NotifyPositionStateChanged(); + } +} + +void MediaSession::NotifyPlaybackStateUpdated() { + if (mSessionDocState != SessionDocStatus::eActive) { + return; + } + RefPtr currentBC = GetParentObject()->GetBrowsingContext(); + MOZ_ASSERT(currentBC, + "Update session playback state after context destroyed!"); + if (RefPtr updater = ContentMediaAgent::Get(currentBC)) { + updater->SetDeclaredPlaybackState(currentBC->Id(), mDeclaredPlaybackState); + } +} + +void MediaSession::NotifyMetadataUpdated() { + if (mSessionDocState != SessionDocStatus::eActive) { + return; + } + RefPtr currentBC = GetParentObject()->GetBrowsingContext(); + MOZ_ASSERT(currentBC, "Update session metadata after context destroyed!"); + + Maybe metadata; + if (GetMetadata()) { + metadata.emplace(*(GetMetadata()->AsMetadataBase())); + } + if (RefPtr updater = ContentMediaAgent::Get(currentBC)) { + updater->UpdateMetadata(currentBC->Id(), metadata); + } +} + +void MediaSession::NotifyEnableSupportedAction(MediaSessionAction aAction) { + if (mSessionDocState != SessionDocStatus::eActive) { + return; + } + RefPtr currentBC = GetParentObject()->GetBrowsingContext(); + MOZ_ASSERT(currentBC, "Update action after context destroyed!"); + if (RefPtr updater = ContentMediaAgent::Get(currentBC)) { + updater->EnableAction(currentBC->Id(), aAction); + } +} + +void MediaSession::NotifyDisableSupportedAction(MediaSessionAction aAction) { + if (mSessionDocState != SessionDocStatus::eActive) { + return; + } + RefPtr currentBC = GetParentObject()->GetBrowsingContext(); + MOZ_ASSERT(currentBC, "Update action after context destroyed!"); + if (RefPtr updater = ContentMediaAgent::Get(currentBC)) { + updater->DisableAction(currentBC->Id(), aAction); + } +} + +void MediaSession::NotifyPositionStateChanged() { + if (mSessionDocState != SessionDocStatus::eActive) { + return; + } + RefPtr currentBC = GetParentObject()->GetBrowsingContext(); + MOZ_ASSERT(currentBC, "Update action after context destroyed!"); + if (RefPtr updater = ContentMediaAgent::Get(currentBC)) { + updater->UpdatePositionState(currentBC->Id(), *mPositionState); + } +} + +} // namespace mozilla::dom diff --git a/dom/media/mediasession/MediaSession.h b/dom/media/mediasession/MediaSession.h new file mode 100644 index 0000000000..db6864c842 --- /dev/null +++ b/dom/media/mediasession/MediaSession.h @@ -0,0 +1,132 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim:set ts=2 sw=2 sts=2 et cindent: */ +/* 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/. */ + +#ifndef mozilla_dom_MediaSession_h +#define mozilla_dom_MediaSession_h + +#include "js/TypeDecls.h" +#include "mozilla/Attributes.h" +#include "mozilla/dom/MediaSessionBinding.h" +#include "mozilla/EnumeratedArray.h" +#include "nsCycleCollectionParticipant.h" +#include "nsIDocumentActivity.h" +#include "nsWrapperCache.h" + +class nsPIDOMWindowInner; + +namespace mozilla { +class ErrorResult; + +namespace dom { + +class Document; +class MediaMetadata; + +// https://w3c.github.io/mediasession/#position-state +struct PositionState { + PositionState() = default; + PositionState(double aDuration, double aPlaybackRate, + double aLastReportedTime) + : mDuration(aDuration), + mPlaybackRate(aPlaybackRate), + mLastReportedPlaybackPosition(aLastReportedTime) {} + double mDuration; + double mPlaybackRate; + double mLastReportedPlaybackPosition; +}; + +class MediaSession final : public nsIDocumentActivity, public nsWrapperCache { + public: + // Ref counting and cycle collection + NS_DECL_CYCLE_COLLECTING_ISUPPORTS + NS_DECL_CYCLE_COLLECTION_WRAPPERCACHE_CLASS(MediaSession) + NS_DECL_NSIDOCUMENTACTIVITY + + explicit MediaSession(nsPIDOMWindowInner* aParent); + + // WebIDL methods + nsPIDOMWindowInner* GetParentObject() const; + + JSObject* WrapObject(JSContext* aCx, + JS::Handle aGivenProto) override; + + MediaMetadata* GetMetadata() const; + + void SetMetadata(MediaMetadata* aMetadata); + + void SetPlaybackState(const MediaSessionPlaybackState& aPlaybackState); + + MediaSessionPlaybackState PlaybackState() const; + + void SetActionHandler(MediaSessionAction aAction, + MediaSessionActionHandler* aHandler); + + void SetPositionState(const MediaPositionState& aState, ErrorResult& aRv); + + bool IsSupportedAction(MediaSessionAction aAction) const; + + // Use this method to trigger media session action handler asynchronously. + void NotifyHandler(const MediaSessionActionDetails& aDetails); + + void Shutdown(); + + // `MediaStatusManager` would determine which media session is an active media + // session and update it from the chrome process. This active session is not + // 100% equal to the active media session in the spec, which is a globally + // active media session *among all tabs*. The active session here is *among + // different windows but in same tab*, so each tab can have at most one + // active media session. + bool IsActive() const; + + private: + // When the document which media session belongs to is going to be destroyed, + // or is in the bfcache, then the session would be inactive. Otherwise, it's + // active all the time. + enum class SessionDocStatus : bool { + eInactive = false, + eActive = true, + }; + void SetMediaSessionDocStatus(SessionDocStatus aState); + + // These methods are used to propagate media session's status to the chrome + // process. + void NotifyMediaSessionDocStatus(SessionDocStatus aState); + void NotifyMediaSessionAttributes(); + void NotifyPlaybackStateUpdated(); + void NotifyMetadataUpdated(); + void NotifyEnableSupportedAction(MediaSessionAction aAction); + void NotifyDisableSupportedAction(MediaSessionAction aAction); + void NotifyPositionStateChanged(); + + void DispatchNotifyHandler(const MediaSessionActionDetails& aDetails); + + MediaSessionActionHandler* GetActionHandler(MediaSessionAction aAction) const; + + ~MediaSession() = default; + + nsCOMPtr mParent; + + RefPtr mMediaMetadata; + + EnumeratedArray> + mActionHandlers; + + // This is used as is a hint for the user agent to determine whether the + // browsing context is playing or paused. + // https://w3c.github.io/mediasession/#declared-playback-state + MediaSessionPlaybackState mDeclaredPlaybackState = + MediaSessionPlaybackState::None; + + Maybe mPositionState; + RefPtr mDoc; + SessionDocStatus mSessionDocState = SessionDocStatus::eInactive; +}; + +} // namespace dom +} // namespace mozilla + +#endif // mozilla_dom_MediaSession_h diff --git a/dom/media/mediasession/MediaSessionIPCUtils.h b/dom/media/mediasession/MediaSessionIPCUtils.h new file mode 100644 index 0000000000..c44f4b4553 --- /dev/null +++ b/dom/media/mediasession/MediaSessionIPCUtils.h @@ -0,0 +1,102 @@ +/* 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/. */ + +#ifndef DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_ +#define DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_ + +#include "ipc/EnumSerializer.h" +#include "MediaMetadata.h" +#include "mozilla/dom/MediaSession.h" +#include "mozilla/dom/MediaSessionBinding.h" +#include "mozilla/Maybe.h" + +namespace mozilla { +namespace dom { + +typedef Maybe MaybeMediaMetadataBase; + +} // namespace dom +} // namespace mozilla + +namespace IPC { + +template <> +struct ParamTraits { + typedef mozilla::dom::MediaImage paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mSizes); + WriteParam(aWriter, aParam.mSrc); + WriteParam(aWriter, aParam.mType); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &(aResult->mSizes)) || + !ReadParam(aReader, &(aResult->mSrc)) || + !ReadParam(aReader, &(aResult->mType))) { + return false; + } + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::dom::MediaMetadataBase paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mTitle); + WriteParam(aWriter, aParam.mArtist); + WriteParam(aWriter, aParam.mAlbum); + WriteParam(aWriter, aParam.mArtwork); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &(aResult->mTitle)) || + !ReadParam(aReader, &(aResult->mArtist)) || + !ReadParam(aReader, &(aResult->mAlbum)) || + !ReadParam(aReader, &(aResult->mArtwork))) { + return false; + } + return true; + } +}; + +template <> +struct ParamTraits { + typedef mozilla::dom::PositionState paramType; + + static void Write(MessageWriter* aWriter, const paramType& aParam) { + WriteParam(aWriter, aParam.mDuration); + WriteParam(aWriter, aParam.mPlaybackRate); + WriteParam(aWriter, aParam.mLastReportedPlaybackPosition); + } + + static bool Read(MessageReader* aReader, paramType* aResult) { + if (!ReadParam(aReader, &(aResult->mDuration)) || + !ReadParam(aReader, &(aResult->mPlaybackRate)) || + !ReadParam(aReader, &(aResult->mLastReportedPlaybackPosition))) { + return false; + } + return true; + } +}; + +template <> +struct ParamTraits + : public ContiguousEnumSerializer< + mozilla::dom::MediaSessionPlaybackState, + mozilla::dom::MediaSessionPlaybackState::None, + mozilla::dom::MediaSessionPlaybackState::EndGuard_> {}; + +template <> +struct ParamTraits + : public ContiguousEnumSerializer< + mozilla::dom::MediaSessionAction, + mozilla::dom::MediaSessionAction::Play, + mozilla::dom::MediaSessionAction::EndGuard_> {}; + +} // namespace IPC + +#endif // DOM_MEDIA_MEDIASESSION_MEDIASESSIONIPCUTILS_H_ diff --git a/dom/media/mediasession/moz.build b/dom/media/mediasession/moz.build new file mode 100644 index 0000000000..afd208b0ca --- /dev/null +++ b/dom/media/mediasession/moz.build @@ -0,0 +1,24 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +MOCHITEST_MANIFESTS += ["test/mochitest.ini"] + +EXPORTS.mozilla.dom += [ + "MediaMetadata.h", + "MediaSession.h", + "MediaSessionIPCUtils.h", +] + +UNIFIED_SOURCES += [ + "MediaMetadata.cpp", + "MediaSession.cpp", +] + +CRASHTEST_MANIFESTS += ["test/crashtests/crashtests.list"] + +include("/ipc/chromium/chromium-config.mozbuild") + +FINAL_LIBRARY = "xul" diff --git a/dom/media/mediasession/test/MediaSessionTestUtils.js b/dom/media/mediasession/test/MediaSessionTestUtils.js new file mode 100644 index 0000000000..1ab0e1fe9b --- /dev/null +++ b/dom/media/mediasession/test/MediaSessionTestUtils.js @@ -0,0 +1,30 @@ +const gMediaSessionActions = [ + "play", + "pause", + "seekbackward", + "seekforward", + "previoustrack", + "nexttrack", + "skipad", + "seekto", + "stop", +]; + +// gCommands and gResults are used in `test_active_mediasession_within_page.html` +const gCommands = { + createMainFrameSession: "create-main-frame-session", + createChildFrameSession: "create-child-frame-session", + destroyChildFrameSessions: "destroy-child-frame-sessions", + destroyActiveChildFrameSession: "destroy-active-child-frame-session", + destroyInactiveChildFrameSession: "destroy-inactive-child-frame-session", +}; + +const gResults = { + mainFrameSession: "main-frame-session", + childFrameSession: "child-session-unchanged", + childFrameSessionUpdated: "child-session-changed", +}; + +function nextWindowMessage() { + return new Promise(r => (window.onmessage = event => r(event))); +} diff --git a/dom/media/mediasession/test/browser.ini b/dom/media/mediasession/test/browser.ini new file mode 100644 index 0000000000..b3cd8300cc --- /dev/null +++ b/dom/media/mediasession/test/browser.ini @@ -0,0 +1,9 @@ +[DEFAULT] +subsuite = media-bc +tags = mediacontrol +support-files = + file_media_session.html + ../../test/gizmo.mp4 + +[browser_active_mediasession_among_tabs.js] + diff --git a/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js b/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js new file mode 100644 index 0000000000..a6a1dbee68 --- /dev/null +++ b/dom/media/mediasession/test/browser_active_mediasession_among_tabs.js @@ -0,0 +1,201 @@ +/* eslint-disable no-undef */ +"use strict"; + +const PAGE = + "https://example.com/browser/dom/media/mediasession/test/file_media_session.html"; + +const ACTION = "previoustrack"; + +add_task(async function setupTestingPref() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["dom.media.mediasession.enabled", true], + ["media.mediacontrol.testingevents.enabled", true], + ], + }); +}); + +/** + * When multiple tabs are all having media session, the latest created one would + * become an active session. When the active media session is destroyed via + * closing the tab, the previous active session would become current active + * session again. + */ +add_task(async function testActiveSessionWhenClosingTab() { + info(`open tab1 and load media session test page`); + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session`); + await checkIfActionReceived(tab1, ACTION); + + info(`open tab2 and load media session test page`); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab2); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab2 should become active session`); + await checkIfActionReceived(tab2, ACTION); + await checkIfActionNotReceived(tab1, ACTION); + + info(`remove tab2`); + const controllerChanged = waitUntilMainMediaControllerChanged(); + BrowserTestUtils.removeTab(tab2); + await controllerChanged; + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session again`); + await checkIfActionReceived(tab1, ACTION); + + info(`remove tab1`); + BrowserTestUtils.removeTab(tab1); +}); + +/** + * This test is similar with `testActiveSessionWhenClosingTab`, the difference + * is that the way we use to destroy active session is via naviagation, not + * closing tab. + */ +add_task(async function testActiveSessionWhenNavigatingTab() { + info(`open tab1 and load media session test page`); + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session`); + await checkIfActionReceived(tab1, ACTION); + + info(`open tab2 and load media session test page`); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab2); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab2 should become active session`); + await checkIfActionReceived(tab2, ACTION); + await checkIfActionNotReceived(tab1, ACTION); + + info(`navigate tab2 to blank page`); + const controllerChanged = waitUntilMainMediaControllerChanged(); + BrowserTestUtils.loadURIString(tab2.linkedBrowser, "about:blank"); + await controllerChanged; + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session`); + await checkIfActionReceived(tab1, ACTION); + + info(`remove tabs`); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +/** + * If we create a media session in a tab where no any playing media exists, then + * that session would not involve in global active media session selection. The + * current active media session would remain unchanged. + */ +add_task(async function testCreatingSessionWithoutPlayingMedia() { + info(`open tab1 and load media session test page`); + const tab1 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + await startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab1); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info(`session in tab1 should become active session`); + await checkIfActionReceived(tab1, ACTION); + + info(`open tab2 and load media session test page`); + const tab2 = await BrowserTestUtils.openNewForegroundTab(gBrowser, PAGE); + + info(`pressing '${ACTION}' key`); + MediaControlService.generateMediaControlKey(ACTION); + + info( + `session in tab1 is still an active session because there is no media playing in tab2` + ); + await checkIfActionReceived(tab1, ACTION); + await checkIfActionNotReceived(tab2, ACTION); + + info(`remove tabs`); + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); +}); + +/** + * The following are helper functions + */ +async function startMediaPlaybackAndWaitMedisSessionBecomeActiveSession(tab) { + await Promise.all([ + BrowserUtils.promiseObserved("active-media-session-changed"), + SpecialPowers.spawn(tab.linkedBrowser, [], () => { + const video = content.document.getElementById("testVideo"); + if (!video) { + ok(false, `can't get the media element!`); + } + video.play(); + }), + ]); +} + +async function checkIfActionReceived(tab, action) { + await SpecialPowers.spawn(tab.linkedBrowser, [action], expectedAction => { + return new Promise(resolve => { + const result = content.document.getElementById("result"); + if (!result) { + ok(false, `can't get the element for showing result!`); + } + + function checkAction() { + is( + result.innerHTML, + expectedAction, + `received '${expectedAction}' correctly` + ); + // Reset the result after finishing checking result, then we can dispatch + // same action again without worrying about previous result. + result.innerHTML = ""; + resolve(); + } + + if (result.innerHTML == "") { + info(`wait until receiving action`); + result.addEventListener("actionChanged", () => checkAction(), { + once: true, + }); + } else { + checkAction(); + } + }); + }); +} + +async function checkIfActionNotReceived(tab, action) { + await SpecialPowers.spawn(tab.linkedBrowser, [action], expectedAction => { + return new Promise(resolve => { + const result = content.document.getElementById("result"); + if (!result) { + ok(false, `can't get the element for showing result!`); + } + is(result.innerHTML, "", `should not receive any action`); + ok(result.innerHTML != expectedAction, `not receive '${expectedAction}'`); + resolve(); + }); + }); +} + +function waitUntilMainMediaControllerChanged() { + return BrowserUtils.promiseObserved("main-media-controller-changed"); +} diff --git a/dom/media/mediasession/test/crashtests/crashtests.list b/dom/media/mediasession/test/crashtests/crashtests.list new file mode 100644 index 0000000000..9ca4956ab6 --- /dev/null +++ b/dom/media/mediasession/test/crashtests/crashtests.list @@ -0,0 +1 @@ +load inactive-mediasession.html diff --git a/dom/media/mediasession/test/crashtests/inactive-mediasession.html b/dom/media/mediasession/test/crashtests/inactive-mediasession.html new file mode 100644 index 0000000000..b24fb887ff --- /dev/null +++ b/dom/media/mediasession/test/crashtests/inactive-mediasession.html @@ -0,0 +1,16 @@ + + + + diff --git a/dom/media/mediasession/test/file_media_session.html b/dom/media/mediasession/test/file_media_session.html new file mode 100644 index 0000000000..b6680fb46b --- /dev/null +++ b/dom/media/mediasession/test/file_media_session.html @@ -0,0 +1,31 @@ + + + + Media Session and non-autoplay media + + + +

+ + + diff --git a/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html b/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html new file mode 100644 index 0000000000..4d55db2189 --- /dev/null +++ b/dom/media/mediasession/test/file_trigger_actionhanlder_frame.html @@ -0,0 +1,40 @@ + + + + Test frame for triggering media session's action handler + + + + + + + diff --git a/dom/media/mediasession/test/file_trigger_actionhanlder_window.html b/dom/media/mediasession/test/file_trigger_actionhanlder_window.html new file mode 100644 index 0000000000..f3316d6dad --- /dev/null +++ b/dom/media/mediasession/test/file_trigger_actionhanlder_window.html @@ -0,0 +1,107 @@ + + + + Test window for triggering media session's action handler + + + + + + + + + diff --git a/dom/media/mediasession/test/mochitest.ini b/dom/media/mediasession/test/mochitest.ini new file mode 100644 index 0000000000..146f1afea8 --- /dev/null +++ b/dom/media/mediasession/test/mochitest.ini @@ -0,0 +1,12 @@ +[DEFAULT] +subsuite = media +tags = mediasession mediacontrol + +support-files = + ../../test/gizmo.mp4 + file_trigger_actionhanlder_frame.html + file_trigger_actionhanlder_window.html + MediaSessionTestUtils.js + +[test_setactionhandler.html] +[test_trigger_actionhanlder.html] diff --git a/dom/media/mediasession/test/test_setactionhandler.html b/dom/media/mediasession/test/test_setactionhandler.html new file mode 100644 index 0000000000..e0fba77d80 --- /dev/null +++ b/dom/media/mediasession/test/test_setactionhandler.html @@ -0,0 +1,92 @@ + + + + + + + + + + + diff --git a/dom/media/mediasession/test/test_trigger_actionhanlder.html b/dom/media/mediasession/test/test_trigger_actionhanlder.html new file mode 100644 index 0000000000..72e8eaf7b5 --- /dev/null +++ b/dom/media/mediasession/test/test_trigger_actionhanlder.html @@ -0,0 +1,57 @@ + + + + Test of triggering media session's action handlers + + + + + + + + -- cgit v1.2.3