1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim:set ts=2 sw=2 sts=2 et cindent: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "mozilla/dom/BrowsingContext.h"
8 #include "mozilla/dom/ContentMediaController.h"
9 #include "mozilla/dom/Document.h"
10 #include "mozilla/dom/MediaSession.h"
11 #include "mozilla/dom/MediaControlUtils.h"
12 #include "mozilla/dom/WindowContext.h"
13 #include "mozilla/EnumeratedArrayCycleCollection.h"
14 
15 // avoid redefined macro in unified build
16 #undef LOG
17 #define LOG(msg, ...)                        \
18   MOZ_LOG(gMediaControlLog, LogLevel::Debug, \
19           ("MediaSession=%p, " msg, this, ##__VA_ARGS__))
20 
21 namespace mozilla::dom {
22 
23 // We don't use NS_IMPL_CYCLE_COLLECTION_WRAPPERCACHE because we need to
24 // unregister MediaSession from document's activity listeners.
25 NS_IMPL_CYCLE_COLLECTION_CLASS(MediaSession)
26 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(MediaSession)
27   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mParent)
28   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMediaMetadata)
29   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mActionHandlers)
30   NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDoc)
31 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
32 NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(MediaSession)
33 
34 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(MediaSession)
35   tmp->Shutdown();
36   NS_IMPL_CYCLE_COLLECTION_UNLINK(mParent)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaMetadata)37   NS_IMPL_CYCLE_COLLECTION_UNLINK(mMediaMetadata)
38   NS_IMPL_CYCLE_COLLECTION_UNLINK(mActionHandlers)
39   NS_IMPL_CYCLE_COLLECTION_UNLINK(mDoc)
40   NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER
41 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
42 NS_IMPL_CYCLE_COLLECTING_ADDREF(MediaSession)
43 NS_IMPL_CYCLE_COLLECTING_RELEASE(MediaSession)
44 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(MediaSession)
45   NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY
46   NS_INTERFACE_MAP_ENTRY(nsIDocumentActivity)
47 NS_INTERFACE_MAP_END
48 
49 MediaSession::MediaSession(nsPIDOMWindowInner* aParent)
50     : mParent(aParent), mDoc(mParent->GetExtantDoc()) {
51   MOZ_ASSERT(mParent);
52   MOZ_ASSERT(mDoc);
53   mDoc->RegisterActivityObserver(this);
54   if (mDoc->IsCurrentActiveDocument()) {
55     SetMediaSessionDocStatus(SessionDocStatus::eActive);
56   }
57 }
58 
Shutdown()59 void MediaSession::Shutdown() {
60   if (mDoc) {
61     mDoc->UnregisterActivityObserver(this);
62   }
63   if (mParent) {
64     SetMediaSessionDocStatus(SessionDocStatus::eInactive);
65   }
66 }
67 
NotifyOwnerDocumentActivityChanged()68 void MediaSession::NotifyOwnerDocumentActivityChanged() {
69   const bool isDocActive = mDoc->IsCurrentActiveDocument();
70   LOG("Document activity changed, isActive=%d", isDocActive);
71   if (isDocActive) {
72     SetMediaSessionDocStatus(SessionDocStatus::eActive);
73   } else {
74     SetMediaSessionDocStatus(SessionDocStatus::eInactive);
75   }
76 }
77 
SetMediaSessionDocStatus(SessionDocStatus aState)78 void MediaSession::SetMediaSessionDocStatus(SessionDocStatus aState) {
79   if (mSessionDocState == aState) {
80     return;
81   }
82   mSessionDocState = aState;
83   NotifyMediaSessionDocStatus(mSessionDocState);
84 }
85 
GetParentObject() const86 nsPIDOMWindowInner* MediaSession::GetParentObject() const { return mParent; }
87 
WrapObject(JSContext * aCx,JS::Handle<JSObject * > aGivenProto)88 JSObject* MediaSession::WrapObject(JSContext* aCx,
89                                    JS::Handle<JSObject*> aGivenProto) {
90   return MediaSession_Binding::Wrap(aCx, this, aGivenProto);
91 }
92 
GetMetadata() const93 MediaMetadata* MediaSession::GetMetadata() const { return mMediaMetadata; }
94 
SetMetadata(MediaMetadata * aMetadata)95 void MediaSession::SetMetadata(MediaMetadata* aMetadata) {
96   mMediaMetadata = aMetadata;
97   NotifyMetadataUpdated();
98 }
99 
SetPlaybackState(const MediaSessionPlaybackState & aPlaybackState)100 void MediaSession::SetPlaybackState(
101     const MediaSessionPlaybackState& aPlaybackState) {
102   if (mDeclaredPlaybackState == aPlaybackState) {
103     return;
104   }
105   mDeclaredPlaybackState = aPlaybackState;
106   NotifyPlaybackStateUpdated();
107 }
108 
PlaybackState() const109 MediaSessionPlaybackState MediaSession::PlaybackState() const {
110   return mDeclaredPlaybackState;
111 }
112 
SetActionHandler(MediaSessionAction aAction,MediaSessionActionHandler * aHandler)113 void MediaSession::SetActionHandler(MediaSessionAction aAction,
114                                     MediaSessionActionHandler* aHandler) {
115   MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
116   // If the media session changes its supported action, then we would propagate
117   // this information to the chrome process in order to run the media session
118   // actions update algorithm.
119   // https://w3c.github.io/mediasession/#supported-media-session-actions
120   RefPtr<MediaSessionActionHandler>& hanlder = mActionHandlers[aAction];
121   if (!hanlder && aHandler) {
122     NotifyEnableSupportedAction(aAction);
123   } else if (hanlder && !aHandler) {
124     NotifyDisableSupportedAction(aAction);
125   }
126   mActionHandlers[aAction] = aHandler;
127 }
128 
GetActionHandler(MediaSessionAction aAction) const129 MediaSessionActionHandler* MediaSession::GetActionHandler(
130     MediaSessionAction aAction) const {
131   MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
132   return mActionHandlers[aAction];
133 }
134 
SetPositionState(const MediaPositionState & aState,ErrorResult & aRv)135 void MediaSession::SetPositionState(const MediaPositionState& aState,
136                                     ErrorResult& aRv) {
137   // https://w3c.github.io/mediasession/#dom-mediasession-setpositionstate
138   // If the state is an empty dictionary then clear the position state.
139   if (!aState.IsAnyMemberPresent()) {
140     mPositionState.reset();
141     return;
142   }
143 
144   // If the duration is not present, throw a TypeError.
145   if (!aState.mDuration.WasPassed()) {
146     return aRv.ThrowTypeError("Duration is not present");
147   }
148 
149   // If the duration is negative, throw a TypeError.
150   if (aState.mDuration.WasPassed() && aState.mDuration.Value() < 0.0) {
151     return aRv.ThrowTypeError(nsPrintfCString(
152         "Invalid duration %f, it can't be negative", aState.mDuration.Value()));
153   }
154 
155   // If the position is negative or greater than duration, throw a TypeError.
156   if (aState.mPosition.WasPassed() &&
157       (aState.mPosition.Value() < 0.0 ||
158        aState.mPosition.Value() > aState.mDuration.Value())) {
159     return aRv.ThrowTypeError(nsPrintfCString(
160         "Invalid position %f, it can't be negative or greater than duration",
161         aState.mPosition.Value()));
162   }
163 
164   // If the playbackRate is zero, throw a TypeError.
165   if (aState.mPlaybackRate.WasPassed() && aState.mPlaybackRate.Value() == 0.0) {
166     return aRv.ThrowTypeError("The playbackRate is zero");
167   }
168 
169   // If the position is not present, set it to zero.
170   double position = aState.mPosition.WasPassed() ? aState.mPosition.Value() : 0;
171 
172   // If the playbackRate is not present, set it to 1.0.
173   double playbackRate =
174       aState.mPlaybackRate.WasPassed() ? aState.mPlaybackRate.Value() : 1.0;
175 
176   // Update the position state and last position updated time.
177   MOZ_ASSERT(aState.mDuration.WasPassed());
178   mPositionState =
179       Some(PositionState(aState.mDuration.Value(), playbackRate, position));
180   NotifyPositionStateChanged();
181 }
182 
NotifyHandler(const MediaSessionActionDetails & aDetails)183 void MediaSession::NotifyHandler(const MediaSessionActionDetails& aDetails) {
184   DispatchNotifyHandler(aDetails);
185 }
186 
DispatchNotifyHandler(const MediaSessionActionDetails & aDetails)187 void MediaSession::DispatchNotifyHandler(
188     const MediaSessionActionDetails& aDetails) {
189   class Runnable final : public mozilla::Runnable {
190    public:
191     Runnable(const MediaSession* aSession,
192              const MediaSessionActionDetails& aDetails)
193         : mozilla::Runnable("MediaSession::DispatchNotifyHandler"),
194           mSession(aSession),
195           mDetails(aDetails) {}
196 
197     MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHOD Run() override {
198       if (RefPtr<MediaSessionActionHandler> handler =
199               mSession->GetActionHandler(mDetails.mAction)) {
200         handler->Call(mDetails);
201       }
202       return NS_OK;
203     }
204 
205    private:
206     RefPtr<const MediaSession> mSession;
207     MediaSessionActionDetails mDetails;
208   };
209 
210   RefPtr<nsIRunnable> runnable = new Runnable(this, aDetails);
211   NS_DispatchToMainThread(runnable);
212 }
213 
IsSupportedAction(MediaSessionAction aAction) const214 bool MediaSession::IsSupportedAction(MediaSessionAction aAction) const {
215   MOZ_ASSERT(size_t(aAction) < ArrayLength(mActionHandlers));
216   return mActionHandlers[aAction] != nullptr;
217 }
218 
IsActive() const219 bool MediaSession::IsActive() const {
220   RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
221   MOZ_ASSERT(currentBC);
222   RefPtr<WindowContext> wc = currentBC->GetTopWindowContext();
223   if (!wc) {
224     return false;
225   }
226   Maybe<uint64_t> activeSessionContextId = wc->GetActiveMediaSessionContextId();
227   if (!activeSessionContextId) {
228     return false;
229   }
230   LOG("session context Id=%" PRIu64 ", active session context Id=%" PRIu64,
231       currentBC->Id(), *activeSessionContextId);
232   return *activeSessionContextId == currentBC->Id();
233 }
234 
NotifyMediaSessionDocStatus(SessionDocStatus aState)235 void MediaSession::NotifyMediaSessionDocStatus(SessionDocStatus aState) {
236   RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
237   MOZ_ASSERT(currentBC, "Update session status after context destroyed!");
238 
239   RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC);
240   if (!updater) {
241     return;
242   }
243   if (aState == SessionDocStatus::eActive) {
244     updater->NotifySessionCreated(currentBC->Id());
245     // If media session set its attributes before its document becomes active,
246     // then we would notify those attributes which hasn't been notified as well
247     // because attributes update would only happen if its document is already
248     // active.
249     NotifyMediaSessionAttributes();
250   } else {
251     updater->NotifySessionDestroyed(currentBC->Id());
252   }
253 }
254 
NotifyMediaSessionAttributes()255 void MediaSession::NotifyMediaSessionAttributes() {
256   MOZ_ASSERT(mSessionDocState == SessionDocStatus::eActive);
257   if (mDeclaredPlaybackState != MediaSessionPlaybackState::None) {
258     NotifyPlaybackStateUpdated();
259   }
260   if (mMediaMetadata) {
261     NotifyMetadataUpdated();
262   }
263   for (size_t idx = 0; idx < ArrayLength(mActionHandlers); idx++) {
264     MediaSessionAction action = static_cast<MediaSessionAction>(idx);
265     if (mActionHandlers[action]) {
266       NotifyEnableSupportedAction(action);
267     }
268   }
269   if (mPositionState) {
270     NotifyPositionStateChanged();
271   }
272 }
273 
NotifyPlaybackStateUpdated()274 void MediaSession::NotifyPlaybackStateUpdated() {
275   if (mSessionDocState != SessionDocStatus::eActive) {
276     return;
277   }
278   RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
279   MOZ_ASSERT(currentBC,
280              "Update session playback state after context destroyed!");
281   if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
282     updater->SetDeclaredPlaybackState(currentBC->Id(), mDeclaredPlaybackState);
283   }
284 }
285 
NotifyMetadataUpdated()286 void MediaSession::NotifyMetadataUpdated() {
287   if (mSessionDocState != SessionDocStatus::eActive) {
288     return;
289   }
290   RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
291   MOZ_ASSERT(currentBC, "Update session metadata after context destroyed!");
292 
293   Maybe<MediaMetadataBase> metadata;
294   if (GetMetadata()) {
295     metadata.emplace(*(GetMetadata()->AsMetadataBase()));
296   }
297   if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
298     updater->UpdateMetadata(currentBC->Id(), metadata);
299   }
300 }
301 
NotifyEnableSupportedAction(MediaSessionAction aAction)302 void MediaSession::NotifyEnableSupportedAction(MediaSessionAction aAction) {
303   if (mSessionDocState != SessionDocStatus::eActive) {
304     return;
305   }
306   RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
307   MOZ_ASSERT(currentBC, "Update action after context destroyed!");
308   if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
309     updater->EnableAction(currentBC->Id(), aAction);
310   }
311 }
312 
NotifyDisableSupportedAction(MediaSessionAction aAction)313 void MediaSession::NotifyDisableSupportedAction(MediaSessionAction aAction) {
314   if (mSessionDocState != SessionDocStatus::eActive) {
315     return;
316   }
317   RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
318   MOZ_ASSERT(currentBC, "Update action after context destroyed!");
319   if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
320     updater->DisableAction(currentBC->Id(), aAction);
321   }
322 }
323 
NotifyPositionStateChanged()324 void MediaSession::NotifyPositionStateChanged() {
325   if (mSessionDocState != SessionDocStatus::eActive) {
326     return;
327   }
328   RefPtr<BrowsingContext> currentBC = GetParentObject()->GetBrowsingContext();
329   MOZ_ASSERT(currentBC, "Update action after context destroyed!");
330   if (RefPtr<IMediaInfoUpdater> updater = ContentMediaAgent::Get(currentBC)) {
331     updater->UpdatePositionState(currentBC->Id(), *mPositionState);
332   }
333 }
334 
335 }  // namespace mozilla::dom
336