AudioChannelService.cpp (17523B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 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 file, 5 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 6 7 #include "AudioChannelService.h" 8 9 #include "base/basictypes.h" 10 #include "mozilla/Preferences.h" 11 #include "mozilla/Services.h" 12 #include "mozilla/StaticPtr.h" 13 #include "mozilla/dom/Document.h" 14 #include "nsComponentManagerUtils.h" 15 #include "nsContentUtils.h" 16 #include "nsHashPropertyBag.h" 17 #include "nsIObserverService.h" 18 #include "nsISupportsPrimitives.h" 19 #include "nsPIDOMWindow.h" 20 #include "nsServiceManagerUtils.h" 21 #include "nsThreadUtils.h" 22 23 using namespace mozilla; 24 using namespace mozilla::dom; 25 26 mozilla::LazyLogModule gAudioChannelLog("AudioChannel"); 27 28 namespace { 29 30 bool sXPCOMShuttingDown = false; 31 32 class AudioPlaybackRunnable final : public Runnable { 33 public: 34 AudioPlaybackRunnable(nsPIDOMWindowOuter* aWindow, bool aActive, 35 AudioChannelService::AudibleChangedReasons aReason) 36 : mozilla::Runnable("AudioPlaybackRunnable"), 37 mWindow(aWindow), 38 mActive(aActive), 39 mReason(aReason) {} 40 41 NS_IMETHOD Run() override { 42 nsCOMPtr<nsIObserverService> observerService = 43 services::GetObserverService(); 44 if (NS_WARN_IF(!observerService)) { 45 return NS_ERROR_FAILURE; 46 } 47 48 nsAutoString state; 49 GetActiveState(state); 50 51 observerService->NotifyObservers(ToSupports(mWindow), "audio-playback", 52 state.get()); 53 54 MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug, 55 ("AudioPlaybackRunnable, active = %s, reason = %s\n", 56 mActive ? "true" : "false", 57 AudioChannelService::EnumValueToString(mReason))); 58 59 return NS_OK; 60 } 61 62 private: 63 void GetActiveState(nsAString& aState) { 64 if (mActive) { 65 aState.AssignLiteral("active"); 66 } else { 67 if (mReason == 68 AudioChannelService::AudibleChangedReasons::ePauseStateChanged) { 69 aState.AssignLiteral("inactive-pause"); 70 } else { 71 aState.AssignLiteral("inactive-nonaudible"); 72 } 73 } 74 } 75 76 nsCOMPtr<nsPIDOMWindowOuter> mWindow; 77 bool mActive; 78 AudioChannelService::AudibleChangedReasons mReason; 79 }; 80 81 } // anonymous namespace 82 83 namespace mozilla::dom { 84 85 const char* SuspendTypeToStr(const nsSuspendedTypes& aSuspend) { 86 MOZ_ASSERT(aSuspend == nsISuspendedTypes::NONE_SUSPENDED || 87 aSuspend == nsISuspendedTypes::SUSPENDED_BLOCK); 88 89 switch (aSuspend) { 90 case nsISuspendedTypes::NONE_SUSPENDED: 91 return "none"; 92 case nsISuspendedTypes::SUSPENDED_BLOCK: 93 return "block"; 94 default: 95 return "unknown"; 96 } 97 } 98 99 StaticRefPtr<AudioChannelService> gAudioChannelService; 100 101 /* static */ 102 void AudioChannelService::CreateServiceIfNeeded() { 103 MOZ_ASSERT(NS_IsMainThread()); 104 105 if (!gAudioChannelService) { 106 gAudioChannelService = new AudioChannelService(); 107 } 108 } 109 110 /* static */ 111 already_AddRefed<AudioChannelService> AudioChannelService::GetOrCreate() { 112 if (sXPCOMShuttingDown) { 113 return nullptr; 114 } 115 116 CreateServiceIfNeeded(); 117 RefPtr<AudioChannelService> service = gAudioChannelService.get(); 118 return service.forget(); 119 } 120 121 /* static */ 122 already_AddRefed<AudioChannelService> AudioChannelService::Get() { 123 if (sXPCOMShuttingDown) { 124 return nullptr; 125 } 126 127 RefPtr<AudioChannelService> service = gAudioChannelService.get(); 128 return service.forget(); 129 } 130 131 /* static */ 132 LogModule* AudioChannelService::GetAudioChannelLog() { 133 return gAudioChannelLog; 134 } 135 136 /* static */ 137 void AudioChannelService::Shutdown() { 138 if (gAudioChannelService) { 139 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); 140 if (obs) { 141 obs->RemoveObserver(gAudioChannelService, "xpcom-shutdown"); 142 obs->RemoveObserver(gAudioChannelService, "outer-window-destroyed"); 143 } 144 145 gAudioChannelService->mWindows.Clear(); 146 147 gAudioChannelService = nullptr; 148 } 149 } 150 151 NS_INTERFACE_MAP_BEGIN(AudioChannelService) 152 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver) 153 NS_INTERFACE_MAP_ENTRY(nsIObserver) 154 NS_INTERFACE_MAP_END 155 156 NS_IMPL_ADDREF(AudioChannelService) 157 NS_IMPL_RELEASE(AudioChannelService) 158 159 AudioChannelService::AudioChannelService() { 160 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService(); 161 if (obs) { 162 obs->AddObserver(this, "xpcom-shutdown", false); 163 obs->AddObserver(this, "outer-window-destroyed", false); 164 } 165 } 166 167 AudioChannelService::~AudioChannelService() = default; 168 169 void AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent, 170 AudibleState aAudible) { 171 MOZ_ASSERT(aAgent); 172 173 uint64_t windowID = aAgent->WindowID(); 174 AudioChannelWindow* winData = GetWindowData(windowID); 175 if (!winData) { 176 winData = new AudioChannelWindow(windowID); 177 mWindows.AppendElement(WrapUnique(winData)); 178 } 179 180 // To make sure agent would be alive because AppendAgent() would trigger the 181 // callback function of AudioChannelAgentOwner that means the agent might be 182 // released in their callback. 183 RefPtr<AudioChannelAgent> kungFuDeathGrip(aAgent); 184 winData->AppendAgent(aAgent, aAudible); 185 } 186 187 void AudioChannelService::UnregisterAudioChannelAgent( 188 AudioChannelAgent* aAgent) { 189 MOZ_ASSERT(aAgent); 190 191 AudioChannelWindow* winData = GetWindowData(aAgent->WindowID()); 192 if (!winData) { 193 return; 194 } 195 196 // To make sure agent would be alive because AppendAgent() would trigger the 197 // callback function of AudioChannelAgentOwner that means the agent might be 198 // released in their callback. 199 RefPtr<AudioChannelAgent> kungFuDeathGrip(aAgent); 200 winData->RemoveAgent(aAgent); 201 } 202 203 AudioPlaybackConfig AudioChannelService::GetMediaConfig( 204 nsPIDOMWindowOuter* aWindow) const { 205 AudioPlaybackConfig config(1.0, false, nsISuspendedTypes::NONE_SUSPENDED); 206 207 if (!aWindow) { 208 config.mVolume = 0.0; 209 config.mMuted = true; 210 config.mSuspend = nsISuspendedTypes::SUSPENDED_BLOCK; 211 return config; 212 } 213 214 AudioChannelWindow* winData = nullptr; 215 nsCOMPtr<nsPIDOMWindowOuter> window = aWindow; 216 217 // The volume must be calculated based on the window hierarchy. Here we go up 218 // to the top window and we calculate the volume and the muted flag. 219 do { 220 winData = GetWindowData(window->WindowID()); 221 if (winData) { 222 config.mVolume *= winData->mConfig.mVolume; 223 config.mMuted = config.mMuted || winData->mConfig.mMuted; 224 config.mCapturedAudio = winData->mIsAudioCaptured; 225 } 226 227 config.mMuted = config.mMuted || window->GetAudioMuted(); 228 if (window->ShouldDelayMediaFromStart()) { 229 config.mSuspend = nsISuspendedTypes::SUSPENDED_BLOCK; 230 } 231 232 nsCOMPtr<nsPIDOMWindowOuter> win = 233 window->GetInProcessScriptableParentOrNull(); 234 if (!win) { 235 break; 236 } 237 238 window = win; 239 240 // If there is no parent, or we are the toplevel we don't continue. 241 } while (window && window != aWindow); 242 243 return config; 244 } 245 246 void AudioChannelService::AudioAudibleChanged(AudioChannelAgent* aAgent, 247 AudibleState aAudible, 248 AudibleChangedReasons aReason) { 249 MOZ_ASSERT(aAgent); 250 251 uint64_t windowID = aAgent->WindowID(); 252 AudioChannelWindow* winData = GetWindowData(windowID); 253 if (winData) { 254 winData->AudioAudibleChanged(aAgent, aAudible, aReason); 255 } 256 } 257 258 NS_IMETHODIMP 259 AudioChannelService::Observe(nsISupports* aSubject, const char* aTopic, 260 const char16_t* aData) { 261 if (!strcmp(aTopic, "xpcom-shutdown")) { 262 sXPCOMShuttingDown = true; 263 Shutdown(); 264 } else if (!strcmp(aTopic, "outer-window-destroyed")) { 265 nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject); 266 NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE); 267 268 uint64_t outerID; 269 nsresult rv = wrapper->GetData(&outerID); 270 if (NS_WARN_IF(NS_FAILED(rv))) { 271 return rv; 272 } 273 274 UniquePtr<AudioChannelWindow> winData; 275 { 276 nsTObserverArray<UniquePtr<AudioChannelWindow>>::ForwardIterator iter( 277 mWindows); 278 while (iter.HasMore()) { 279 auto& next = iter.GetNext(); 280 if (next->mWindowID == outerID) { 281 winData = std::move(next); 282 iter.Remove(); 283 break; 284 } 285 } 286 } 287 288 if (winData) { 289 for (AudioChannelAgent* agent : winData->mAgents.ForwardRange()) { 290 agent->WindowVolumeChanged(winData->mConfig.mVolume, 291 winData->mConfig.mMuted); 292 } 293 } 294 } 295 296 return NS_OK; 297 } 298 299 void AudioChannelService::RefreshAgents( 300 nsPIDOMWindowOuter* aWindow, 301 const std::function<void(AudioChannelAgent*)>& aFunc) { 302 MOZ_ASSERT(aWindow); 303 304 nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetInProcessScriptableTop(); 305 if (!topWindow) { 306 return; 307 } 308 309 AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); 310 if (!winData) { 311 return; 312 } 313 314 for (AudioChannelAgent* agent : winData->mAgents.ForwardRange()) { 315 aFunc(agent); 316 } 317 } 318 319 void AudioChannelService::RefreshAgentsVolume(nsPIDOMWindowOuter* aWindow, 320 float aVolume, bool aMuted) { 321 RefreshAgents(aWindow, [aVolume, aMuted](AudioChannelAgent* agent) { 322 agent->WindowVolumeChanged(aVolume, aMuted); 323 }); 324 } 325 326 void AudioChannelService::RefreshAgentsSuspend(nsPIDOMWindowOuter* aWindow, 327 nsSuspendedTypes aSuspend) { 328 RefreshAgents(aWindow, [aSuspend](AudioChannelAgent* agent) { 329 agent->WindowSuspendChanged(aSuspend); 330 }); 331 } 332 333 void AudioChannelService::SetWindowAudioCaptured(nsPIDOMWindowOuter* aWindow, 334 uint64_t aInnerWindowID, 335 bool aCapture) { 336 MOZ_ASSERT(NS_IsMainThread()); 337 MOZ_ASSERT(aWindow); 338 339 MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug, 340 ("AudioChannelService, SetWindowAudioCaptured, window = %p, " 341 "aCapture = %d\n", 342 aWindow, aCapture)); 343 344 nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetInProcessScriptableTop(); 345 if (!topWindow) { 346 return; 347 } 348 349 AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); 350 351 // This can happen, but only during shutdown, because the the outer window 352 // changes ScriptableTop, so that its ID is different. 353 // In this case either we are capturing, and it's too late because the window 354 // has been closed anyways, or we are un-capturing, and everything has already 355 // been cleaned up by the HTMLMediaElements or the AudioContexts. 356 if (!winData) { 357 return; 358 } 359 360 if (aCapture != winData->mIsAudioCaptured) { 361 winData->mIsAudioCaptured = aCapture; 362 for (AudioChannelAgent* agent : winData->mAgents.ForwardRange()) { 363 agent->WindowAudioCaptureChanged(aInnerWindowID, aCapture); 364 } 365 } 366 } 367 368 AudioChannelService::AudioChannelWindow* 369 AudioChannelService::GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow) { 370 MOZ_ASSERT(NS_IsMainThread()); 371 MOZ_ASSERT(aWindow); 372 373 AudioChannelWindow* winData = GetWindowData(aWindow->WindowID()); 374 if (!winData) { 375 winData = new AudioChannelWindow(aWindow->WindowID()); 376 mWindows.AppendElement(WrapUnique(winData)); 377 } 378 379 return winData; 380 } 381 382 AudioChannelService::AudioChannelWindow* AudioChannelService::GetWindowData( 383 uint64_t aWindowID) const { 384 const auto [begin, end] = mWindows.NonObservingRange(); 385 const auto foundIt = std::find_if(begin, end, [aWindowID](const auto& next) { 386 return next->mWindowID == aWindowID; 387 }); 388 return foundIt != end ? foundIt->get() : nullptr; 389 } 390 391 bool AudioChannelService::IsWindowActive(nsPIDOMWindowOuter* aWindow) { 392 MOZ_ASSERT(NS_IsMainThread()); 393 394 auto* window = nsPIDOMWindowOuter::From(aWindow)->GetInProcessScriptableTop(); 395 if (!window) { 396 return false; 397 } 398 399 AudioChannelWindow* winData = GetWindowData(window->WindowID()); 400 if (!winData) { 401 return false; 402 } 403 404 return !winData->mAudibleAgents.IsEmpty(); 405 } 406 407 void AudioChannelService::NotifyResumingDelayedMedia( 408 nsPIDOMWindowOuter* aWindow) { 409 MOZ_ASSERT(aWindow); 410 411 nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetInProcessScriptableTop(); 412 if (!topWindow) { 413 return; 414 } 415 416 AudioChannelWindow* winData = GetWindowData(topWindow->WindowID()); 417 if (!winData) { 418 return; 419 } 420 421 winData->NotifyMediaBlockStop(aWindow); 422 RefreshAgentsSuspend(aWindow, nsISuspendedTypes::NONE_SUSPENDED); 423 } 424 425 void AudioChannelService::AudioChannelWindow::AppendAgent( 426 AudioChannelAgent* aAgent, AudibleState aAudible) { 427 MOZ_ASSERT(aAgent); 428 429 AppendAgentAndIncreaseAgentsNum(aAgent); 430 AudioAudibleChanged(aAgent, aAudible, 431 AudibleChangedReasons::eDataAudibleChanged); 432 } 433 434 void AudioChannelService::AudioChannelWindow::RemoveAgent( 435 AudioChannelAgent* aAgent) { 436 MOZ_ASSERT(aAgent); 437 438 RemoveAgentAndReduceAgentsNum(aAgent); 439 AudioAudibleChanged(aAgent, AudibleState::eNotAudible, 440 AudibleChangedReasons::ePauseStateChanged); 441 } 442 443 void AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop( 444 nsPIDOMWindowOuter* aWindow) { 445 if (mShouldSendActiveMediaBlockStopEvent) { 446 mShouldSendActiveMediaBlockStopEvent = false; 447 nsCOMPtr<nsPIDOMWindowOuter> window = aWindow; 448 NS_DispatchToCurrentThread(NS_NewRunnableFunction( 449 "dom::AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop", 450 [window]() -> void { 451 nsCOMPtr<nsIObserverService> observerService = 452 services::GetObserverService(); 453 if (NS_WARN_IF(!observerService)) { 454 return; 455 } 456 457 observerService->NotifyObservers(ToSupports(window), "audio-playback", 458 u"activeMediaBlockStop"); 459 })); 460 } 461 } 462 463 void AudioChannelService::AudioChannelWindow::AppendAgentAndIncreaseAgentsNum( 464 AudioChannelAgent* aAgent) { 465 MOZ_ASSERT(aAgent); 466 MOZ_ASSERT(!mAgents.Contains(aAgent)); 467 468 mAgents.AppendElement(aAgent); 469 470 ++mConfig.mNumberOfAgents; 471 } 472 473 void AudioChannelService::AudioChannelWindow::RemoveAgentAndReduceAgentsNum( 474 AudioChannelAgent* aAgent) { 475 MOZ_ASSERT(aAgent); 476 MOZ_ASSERT(mAgents.Contains(aAgent)); 477 478 mAgents.RemoveElement(aAgent); 479 480 MOZ_ASSERT(mConfig.mNumberOfAgents > 0); 481 --mConfig.mNumberOfAgents; 482 } 483 484 void AudioChannelService::AudioChannelWindow::AudioAudibleChanged( 485 AudioChannelAgent* aAgent, AudibleState aAudible, 486 AudibleChangedReasons aReason) { 487 MOZ_ASSERT(aAgent); 488 489 if (aAudible == AudibleState::eAudible) { 490 AppendAudibleAgentIfNotContained(aAgent, aReason); 491 } else { 492 RemoveAudibleAgentIfContained(aAgent, aReason); 493 } 494 495 if (aAudible != AudibleState::eNotAudible) { 496 MaybeNotifyMediaBlockStart(aAgent); 497 } 498 } 499 500 void AudioChannelService::AudioChannelWindow::AppendAudibleAgentIfNotContained( 501 AudioChannelAgent* aAgent, AudibleChangedReasons aReason) { 502 MOZ_ASSERT(aAgent); 503 MOZ_ASSERT(mAgents.Contains(aAgent)); 504 505 if (!mAudibleAgents.Contains(aAgent)) { 506 mAudibleAgents.AppendElement(aAgent); 507 if (IsFirstAudibleAgent()) { 508 NotifyAudioAudibleChanged(aAgent->Window(), AudibleState::eAudible, 509 aReason); 510 } 511 } 512 } 513 514 void AudioChannelService::AudioChannelWindow::RemoveAudibleAgentIfContained( 515 AudioChannelAgent* aAgent, AudibleChangedReasons aReason) { 516 MOZ_ASSERT(aAgent); 517 518 if (mAudibleAgents.Contains(aAgent)) { 519 mAudibleAgents.RemoveElement(aAgent); 520 if (IsLastAudibleAgent()) { 521 NotifyAudioAudibleChanged(aAgent->Window(), AudibleState::eNotAudible, 522 aReason); 523 } 524 } 525 } 526 527 bool AudioChannelService::AudioChannelWindow::IsFirstAudibleAgent() const { 528 return (mAudibleAgents.Length() == 1); 529 } 530 531 bool AudioChannelService::AudioChannelWindow::IsLastAudibleAgent() const { 532 return mAudibleAgents.IsEmpty(); 533 } 534 535 void AudioChannelService::AudioChannelWindow::NotifyAudioAudibleChanged( 536 nsPIDOMWindowOuter* aWindow, AudibleState aAudible, 537 AudibleChangedReasons aReason) { 538 RefPtr<AudioPlaybackRunnable> runnable = new AudioPlaybackRunnable( 539 aWindow, aAudible == AudibleState::eAudible, aReason); 540 DebugOnly<nsresult> rv = NS_DispatchToCurrentThread(runnable); 541 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToCurrentThread failed"); 542 } 543 544 void AudioChannelService::AudioChannelWindow::MaybeNotifyMediaBlockStart( 545 AudioChannelAgent* aAgent) { 546 nsCOMPtr<nsPIDOMWindowOuter> window = aAgent->Window(); 547 if (!window) { 548 return; 549 } 550 551 nsCOMPtr<nsPIDOMWindowInner> inner = window->GetCurrentInnerWindow(); 552 if (!inner) { 553 return; 554 } 555 556 nsCOMPtr<Document> doc = inner->GetExtantDoc(); 557 if (!doc) { 558 return; 559 } 560 561 if (!window->ShouldDelayMediaFromStart() || !doc->Hidden()) { 562 return; 563 } 564 565 if (!mShouldSendActiveMediaBlockStopEvent) { 566 mShouldSendActiveMediaBlockStopEvent = true; 567 NS_DispatchToCurrentThread(NS_NewRunnableFunction( 568 "dom::AudioChannelService::AudioChannelWindow::" 569 "MaybeNotifyMediaBlockStart", 570 [window]() -> void { 571 nsCOMPtr<nsIObserverService> observerService = 572 services::GetObserverService(); 573 if (NS_WARN_IF(!observerService)) { 574 return; 575 } 576 577 observerService->NotifyObservers(ToSupports(window), "audio-playback", 578 u"activeMediaBlockStart"); 579 })); 580 } 581 } 582 583 } // namespace mozilla::dom