summaryrefslogtreecommitdiffstats
path: root/dom/media/mediasession/MediaSession.cpp
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--dom/media/mediasession/MediaSession.cpp335
1 files changed, 335 insertions, 0 deletions
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<JSObject*> 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<MediaSessionActionHandler>& 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<MediaSessionActionHandler> handler =
+ mSession->GetActionHandler(mDetails.mAction)) {
+ handler->Call(mDetails);
+ }
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<const MediaSession> mSession;
+ MediaSessionActionDetails mDetails;
+ };
+
+ RefPtr<nsIRunnable> 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<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC);
+ RefPtr<WindowContext> wc = currentBC->GetTopWindowContext();
+ if (!wc) {
+ return false;
+ }
+ Maybe<uint64_t> 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<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update session status after context destroyed!");
+
+ RefPtr<IMediaInfoUpdater> 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<MediaSessionAction>(idx);
+ if (mActionHandlers[action]) {
+ NotifyEnableSupportedAction(action);
+ }
+ }
+ if (mPositionState) {
+ NotifyPositionStateChanged();
+ }
+}
+
+void MediaSession::NotifyPlaybackStateUpdated() {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC,
+ "Update session playback state after context destroyed!");
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->SetDeclaredPlaybackState(currentBC->Id(), mDeclaredPlaybackState);
+ }
+}
+
+void MediaSession::NotifyMetadataUpdated() {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update session metadata after context destroyed!");
+
+ Maybe<MediaMetadataBase> metadata;
+ if (GetMetadata()) {
+ metadata.emplace(*(GetMetadata()->AsMetadataBase()));
+ }
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->UpdateMetadata(currentBC->Id(), metadata);
+ }
+}
+
+void MediaSession::NotifyEnableSupportedAction(MediaSessionAction aAction) {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->EnableAction(currentBC->Id(), aAction);
+ }
+}
+
+void MediaSession::NotifyDisableSupportedAction(MediaSessionAction aAction) {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->DisableAction(currentBC->Id(), aAction);
+ }
+}
+
+void MediaSession::NotifyPositionStateChanged() {
+ if (mSessionDocState != SessionDocStatus::eActive) {
+ return;
+ }
+ RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
+ MOZ_ASSERT(currentBC, "Update action after context destroyed!");
+ if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
+ updater->UpdatePositionState(currentBC->Id(), *mPositionState);
+ }
+}
+
+} // namespace mozilla::dom