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