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