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/. */
7 #include "AudioChannelService.h"
9 #include "base/basictypes.h"
11 #include "mozilla/Services.h"
12 #include "mozilla/StaticPtr.h"
13 #include "mozilla/Unused.h"
14 #include "mozilla/dom/Document.h"
16 #include "nsContentUtils.h"
17 #include "nsISupportsPrimitives.h"
18 #include "nsThreadUtils.h"
19 #include "nsHashPropertyBag.h"
20 #include "nsComponentManagerUtils.h"
21 #include "nsGlobalWindow.h"
22 #include "nsPIDOMWindow.h"
23 #include "nsServiceManagerUtils.h"
25 #include "mozilla/Preferences.h"
27 using namespace mozilla
;
28 using namespace mozilla::dom
;
30 mozilla::LazyLogModule
gAudioChannelLog("AudioChannel");
34 bool sXPCOMShuttingDown
= false;
36 class AudioPlaybackRunnable final
: public Runnable
{
38 AudioPlaybackRunnable(nsPIDOMWindowOuter
* aWindow
, bool aActive
,
39 AudioChannelService::AudibleChangedReasons aReason
)
40 : mozilla::Runnable("AudioPlaybackRunnable"),
45 NS_IMETHOD
Run() override
{
46 nsCOMPtr
<nsIObserverService
> observerService
=
47 services::GetObserverService();
48 if (NS_WARN_IF(!observerService
)) {
49 return NS_ERROR_FAILURE
;
53 GetActiveState(state
);
55 observerService
->NotifyObservers(ToSupports(mWindow
), "audio-playback",
58 MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug
,
59 ("AudioPlaybackRunnable, active = %s, reason = %s\n",
60 mActive
? "true" : "false", AudibleChangedReasonToStr(mReason
)));
66 void GetActiveState(nsAString
& aState
) {
68 aState
.AssignLiteral("active");
71 AudioChannelService::AudibleChangedReasons::ePauseStateChanged
) {
72 aState
.AssignLiteral("inactive-pause");
74 aState
.AssignLiteral("inactive-nonaudible");
79 nsCOMPtr
<nsPIDOMWindowOuter
> mWindow
;
81 AudioChannelService::AudibleChangedReasons mReason
;
84 } // anonymous namespace
86 namespace mozilla::dom
{
88 const char* SuspendTypeToStr(const nsSuspendedTypes
& aSuspend
) {
89 MOZ_ASSERT(aSuspend
== nsISuspendedTypes::NONE_SUSPENDED
||
90 aSuspend
== nsISuspendedTypes::SUSPENDED_BLOCK
);
93 case nsISuspendedTypes::NONE_SUSPENDED
:
95 case nsISuspendedTypes::SUSPENDED_BLOCK
:
102 const char* AudibleStateToStr(
103 const AudioChannelService::AudibleState
& aAudible
) {
104 MOZ_ASSERT(aAudible
== AudioChannelService::AudibleState::eNotAudible
||
105 aAudible
== AudioChannelService::AudibleState::eMaybeAudible
||
106 aAudible
== AudioChannelService::AudibleState::eAudible
);
109 case AudioChannelService::AudibleState::eNotAudible
:
110 return "not-audible";
111 case AudioChannelService::AudibleState::eMaybeAudible
:
112 return "maybe-audible";
113 case AudioChannelService::AudibleState::eAudible
:
120 const char* AudibleChangedReasonToStr(
121 const AudioChannelService::AudibleChangedReasons
& aReason
) {
123 aReason
== AudioChannelService::AudibleChangedReasons::eVolumeChanged
||
125 AudioChannelService::AudibleChangedReasons::eDataAudibleChanged
||
127 AudioChannelService::AudibleChangedReasons::ePauseStateChanged
);
130 case AudioChannelService::AudibleChangedReasons::eVolumeChanged
:
132 case AudioChannelService::AudibleChangedReasons::eDataAudibleChanged
:
133 return "data-audible";
134 case AudioChannelService::AudibleChangedReasons::ePauseStateChanged
:
135 return "pause-state";
141 StaticRefPtr
<AudioChannelService
> gAudioChannelService
;
144 void AudioChannelService::CreateServiceIfNeeded() {
145 MOZ_ASSERT(NS_IsMainThread());
147 if (!gAudioChannelService
) {
148 gAudioChannelService
= new AudioChannelService();
153 already_AddRefed
<AudioChannelService
> AudioChannelService::GetOrCreate() {
154 if (sXPCOMShuttingDown
) {
158 CreateServiceIfNeeded();
159 RefPtr
<AudioChannelService
> service
= gAudioChannelService
.get();
160 return service
.forget();
164 already_AddRefed
<AudioChannelService
> AudioChannelService::Get() {
165 if (sXPCOMShuttingDown
) {
169 RefPtr
<AudioChannelService
> service
= gAudioChannelService
.get();
170 return service
.forget();
174 LogModule
* AudioChannelService::GetAudioChannelLog() {
175 return gAudioChannelLog
;
179 void AudioChannelService::Shutdown() {
180 if (gAudioChannelService
) {
181 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
183 obs
->RemoveObserver(gAudioChannelService
, "xpcom-shutdown");
184 obs
->RemoveObserver(gAudioChannelService
, "outer-window-destroyed");
187 gAudioChannelService
->mWindows
.Clear();
189 gAudioChannelService
= nullptr;
193 NS_INTERFACE_MAP_BEGIN(AudioChannelService
)
194 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports
, nsIObserver
)
195 NS_INTERFACE_MAP_ENTRY(nsIObserver
)
198 NS_IMPL_ADDREF(AudioChannelService
)
199 NS_IMPL_RELEASE(AudioChannelService
)
201 AudioChannelService::AudioChannelService() {
202 nsCOMPtr
<nsIObserverService
> obs
= mozilla::services::GetObserverService();
204 obs
->AddObserver(this, "xpcom-shutdown", false);
205 obs
->AddObserver(this, "outer-window-destroyed", false);
209 AudioChannelService::~AudioChannelService() = default;
211 void AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent
* aAgent
,
212 AudibleState aAudible
) {
215 uint64_t windowID
= aAgent
->WindowID();
216 AudioChannelWindow
* winData
= GetWindowData(windowID
);
218 winData
= new AudioChannelWindow(windowID
);
219 mWindows
.AppendElement(WrapUnique(winData
));
222 // To make sure agent would be alive because AppendAgent() would trigger the
223 // callback function of AudioChannelAgentOwner that means the agent might be
224 // released in their callback.
225 RefPtr
<AudioChannelAgent
> kungFuDeathGrip(aAgent
);
226 winData
->AppendAgent(aAgent
, aAudible
);
229 void AudioChannelService::UnregisterAudioChannelAgent(
230 AudioChannelAgent
* aAgent
) {
233 AudioChannelWindow
* winData
= GetWindowData(aAgent
->WindowID());
238 // To make sure agent would be alive because AppendAgent() would trigger the
239 // callback function of AudioChannelAgentOwner that means the agent might be
240 // released in their callback.
241 RefPtr
<AudioChannelAgent
> kungFuDeathGrip(aAgent
);
242 winData
->RemoveAgent(aAgent
);
245 AudioPlaybackConfig
AudioChannelService::GetMediaConfig(
246 nsPIDOMWindowOuter
* aWindow
) const {
247 AudioPlaybackConfig
config(1.0, false, nsISuspendedTypes::NONE_SUSPENDED
);
250 config
.mVolume
= 0.0;
251 config
.mMuted
= true;
252 config
.mSuspend
= nsISuspendedTypes::SUSPENDED_BLOCK
;
256 AudioChannelWindow
* winData
= nullptr;
257 nsCOMPtr
<nsPIDOMWindowOuter
> window
= aWindow
;
259 // The volume must be calculated based on the window hierarchy. Here we go up
260 // to the top window and we calculate the volume and the muted flag.
262 winData
= GetWindowData(window
->WindowID());
264 config
.mVolume
*= winData
->mConfig
.mVolume
;
265 config
.mMuted
= config
.mMuted
|| winData
->mConfig
.mMuted
;
266 config
.mCapturedAudio
= winData
->mIsAudioCaptured
;
269 config
.mMuted
= config
.mMuted
|| window
->GetAudioMuted();
270 if (window
->ShouldDelayMediaFromStart()) {
271 config
.mSuspend
= nsISuspendedTypes::SUSPENDED_BLOCK
;
274 nsCOMPtr
<nsPIDOMWindowOuter
> win
=
275 window
->GetInProcessScriptableParentOrNull();
282 // If there is no parent, or we are the toplevel we don't continue.
283 } while (window
&& window
!= aWindow
);
288 void AudioChannelService::AudioAudibleChanged(AudioChannelAgent
* aAgent
,
289 AudibleState aAudible
,
290 AudibleChangedReasons aReason
) {
293 uint64_t windowID
= aAgent
->WindowID();
294 AudioChannelWindow
* winData
= GetWindowData(windowID
);
296 winData
->AudioAudibleChanged(aAgent
, aAudible
, aReason
);
301 AudioChannelService::Observe(nsISupports
* aSubject
, const char* aTopic
,
302 const char16_t
* aData
) {
303 if (!strcmp(aTopic
, "xpcom-shutdown")) {
304 sXPCOMShuttingDown
= true;
306 } else if (!strcmp(aTopic
, "outer-window-destroyed")) {
307 nsCOMPtr
<nsISupportsPRUint64
> wrapper
= do_QueryInterface(aSubject
);
308 NS_ENSURE_TRUE(wrapper
, NS_ERROR_FAILURE
);
311 nsresult rv
= wrapper
->GetData(&outerID
);
312 if (NS_WARN_IF(NS_FAILED(rv
))) {
316 UniquePtr
<AudioChannelWindow
> winData
;
318 nsTObserverArray
<UniquePtr
<AudioChannelWindow
>>::ForwardIterator
iter(
320 while (iter
.HasMore()) {
321 auto& next
= iter
.GetNext();
322 if (next
->mWindowID
== outerID
) {
323 winData
= std::move(next
);
331 for (AudioChannelAgent
* agent
: winData
->mAgents
.ForwardRange()) {
332 agent
->WindowVolumeChanged(winData
->mConfig
.mVolume
,
333 winData
->mConfig
.mMuted
);
341 void AudioChannelService::RefreshAgents(
342 nsPIDOMWindowOuter
* aWindow
,
343 const std::function
<void(AudioChannelAgent
*)>& aFunc
) {
346 nsCOMPtr
<nsPIDOMWindowOuter
> topWindow
= aWindow
->GetInProcessScriptableTop();
351 AudioChannelWindow
* winData
= GetWindowData(topWindow
->WindowID());
356 for (AudioChannelAgent
* agent
: winData
->mAgents
.ForwardRange()) {
361 void AudioChannelService::RefreshAgentsVolume(nsPIDOMWindowOuter
* aWindow
,
362 float aVolume
, bool aMuted
) {
363 RefreshAgents(aWindow
, [aVolume
, aMuted
](AudioChannelAgent
* agent
) {
364 agent
->WindowVolumeChanged(aVolume
, aMuted
);
368 void AudioChannelService::RefreshAgentsSuspend(nsPIDOMWindowOuter
* aWindow
,
369 nsSuspendedTypes aSuspend
) {
370 RefreshAgents(aWindow
, [aSuspend
](AudioChannelAgent
* agent
) {
371 agent
->WindowSuspendChanged(aSuspend
);
375 void AudioChannelService::SetWindowAudioCaptured(nsPIDOMWindowOuter
* aWindow
,
376 uint64_t aInnerWindowID
,
378 MOZ_ASSERT(NS_IsMainThread());
381 MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug
,
382 ("AudioChannelService, SetWindowAudioCaptured, window = %p, "
386 nsCOMPtr
<nsPIDOMWindowOuter
> topWindow
= aWindow
->GetInProcessScriptableTop();
391 AudioChannelWindow
* winData
= GetWindowData(topWindow
->WindowID());
393 // This can happen, but only during shutdown, because the the outer window
394 // changes ScriptableTop, so that its ID is different.
395 // In this case either we are capturing, and it's too late because the window
396 // has been closed anyways, or we are un-capturing, and everything has already
397 // been cleaned up by the HTMLMediaElements or the AudioContexts.
402 if (aCapture
!= winData
->mIsAudioCaptured
) {
403 winData
->mIsAudioCaptured
= aCapture
;
404 for (AudioChannelAgent
* agent
: winData
->mAgents
.ForwardRange()) {
405 agent
->WindowAudioCaptureChanged(aInnerWindowID
, aCapture
);
410 AudioChannelService::AudioChannelWindow
*
411 AudioChannelService::GetOrCreateWindowData(nsPIDOMWindowOuter
* aWindow
) {
412 MOZ_ASSERT(NS_IsMainThread());
415 AudioChannelWindow
* winData
= GetWindowData(aWindow
->WindowID());
417 winData
= new AudioChannelWindow(aWindow
->WindowID());
418 mWindows
.AppendElement(WrapUnique(winData
));
424 AudioChannelService::AudioChannelWindow
* AudioChannelService::GetWindowData(
425 uint64_t aWindowID
) const {
426 const auto [begin
, end
] = mWindows
.NonObservingRange();
427 const auto foundIt
= std::find_if(begin
, end
, [aWindowID
](const auto& next
) {
428 return next
->mWindowID
== aWindowID
;
430 return foundIt
!= end
? foundIt
->get() : nullptr;
433 bool AudioChannelService::IsWindowActive(nsPIDOMWindowOuter
* aWindow
) {
434 MOZ_ASSERT(NS_IsMainThread());
436 auto* window
= nsPIDOMWindowOuter::From(aWindow
)->GetInProcessScriptableTop();
441 AudioChannelWindow
* winData
= GetWindowData(window
->WindowID());
446 return !winData
->mAudibleAgents
.IsEmpty();
449 void AudioChannelService::NotifyResumingDelayedMedia(
450 nsPIDOMWindowOuter
* aWindow
) {
453 nsCOMPtr
<nsPIDOMWindowOuter
> topWindow
= aWindow
->GetInProcessScriptableTop();
458 AudioChannelWindow
* winData
= GetWindowData(topWindow
->WindowID());
463 winData
->NotifyMediaBlockStop(aWindow
);
464 RefreshAgentsSuspend(aWindow
, nsISuspendedTypes::NONE_SUSPENDED
);
467 void AudioChannelService::AudioChannelWindow::AppendAgent(
468 AudioChannelAgent
* aAgent
, AudibleState aAudible
) {
471 AppendAgentAndIncreaseAgentsNum(aAgent
);
472 AudioAudibleChanged(aAgent
, aAudible
,
473 AudibleChangedReasons::eDataAudibleChanged
);
476 void AudioChannelService::AudioChannelWindow::RemoveAgent(
477 AudioChannelAgent
* aAgent
) {
480 RemoveAgentAndReduceAgentsNum(aAgent
);
481 AudioAudibleChanged(aAgent
, AudibleState::eNotAudible
,
482 AudibleChangedReasons::ePauseStateChanged
);
485 void AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop(
486 nsPIDOMWindowOuter
* aWindow
) {
487 if (mShouldSendActiveMediaBlockStopEvent
) {
488 mShouldSendActiveMediaBlockStopEvent
= false;
489 nsCOMPtr
<nsPIDOMWindowOuter
> window
= aWindow
;
490 NS_DispatchToCurrentThread(NS_NewRunnableFunction(
491 "dom::AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop",
493 nsCOMPtr
<nsIObserverService
> observerService
=
494 services::GetObserverService();
495 if (NS_WARN_IF(!observerService
)) {
499 observerService
->NotifyObservers(ToSupports(window
), "audio-playback",
500 u
"activeMediaBlockStop");
505 void AudioChannelService::AudioChannelWindow::AppendAgentAndIncreaseAgentsNum(
506 AudioChannelAgent
* aAgent
) {
508 MOZ_ASSERT(!mAgents
.Contains(aAgent
));
510 mAgents
.AppendElement(aAgent
);
512 ++mConfig
.mNumberOfAgents
;
515 void AudioChannelService::AudioChannelWindow::RemoveAgentAndReduceAgentsNum(
516 AudioChannelAgent
* aAgent
) {
518 MOZ_ASSERT(mAgents
.Contains(aAgent
));
520 mAgents
.RemoveElement(aAgent
);
522 MOZ_ASSERT(mConfig
.mNumberOfAgents
> 0);
523 --mConfig
.mNumberOfAgents
;
526 void AudioChannelService::AudioChannelWindow::AudioAudibleChanged(
527 AudioChannelAgent
* aAgent
, AudibleState aAudible
,
528 AudibleChangedReasons aReason
) {
531 if (aAudible
== AudibleState::eAudible
) {
532 AppendAudibleAgentIfNotContained(aAgent
, aReason
);
534 RemoveAudibleAgentIfContained(aAgent
, aReason
);
537 if (aAudible
!= AudibleState::eNotAudible
) {
538 MaybeNotifyMediaBlockStart(aAgent
);
542 void AudioChannelService::AudioChannelWindow::AppendAudibleAgentIfNotContained(
543 AudioChannelAgent
* aAgent
, AudibleChangedReasons aReason
) {
545 MOZ_ASSERT(mAgents
.Contains(aAgent
));
547 if (!mAudibleAgents
.Contains(aAgent
)) {
548 mAudibleAgents
.AppendElement(aAgent
);
549 if (IsFirstAudibleAgent()) {
550 NotifyAudioAudibleChanged(aAgent
->Window(), AudibleState::eAudible
,
556 void AudioChannelService::AudioChannelWindow::RemoveAudibleAgentIfContained(
557 AudioChannelAgent
* aAgent
, AudibleChangedReasons aReason
) {
560 if (mAudibleAgents
.Contains(aAgent
)) {
561 mAudibleAgents
.RemoveElement(aAgent
);
562 if (IsLastAudibleAgent()) {
563 NotifyAudioAudibleChanged(aAgent
->Window(), AudibleState::eNotAudible
,
569 bool AudioChannelService::AudioChannelWindow::IsFirstAudibleAgent() const {
570 return (mAudibleAgents
.Length() == 1);
573 bool AudioChannelService::AudioChannelWindow::IsLastAudibleAgent() const {
574 return mAudibleAgents
.IsEmpty();
577 void AudioChannelService::AudioChannelWindow::NotifyAudioAudibleChanged(
578 nsPIDOMWindowOuter
* aWindow
, AudibleState aAudible
,
579 AudibleChangedReasons aReason
) {
580 RefPtr
<AudioPlaybackRunnable
> runnable
= new AudioPlaybackRunnable(
581 aWindow
, aAudible
== AudibleState::eAudible
, aReason
);
582 DebugOnly
<nsresult
> rv
= NS_DispatchToCurrentThread(runnable
);
583 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv
), "NS_DispatchToCurrentThread failed");
586 void AudioChannelService::AudioChannelWindow::MaybeNotifyMediaBlockStart(
587 AudioChannelAgent
* aAgent
) {
588 nsCOMPtr
<nsPIDOMWindowOuter
> window
= aAgent
->Window();
593 nsCOMPtr
<nsPIDOMWindowInner
> inner
= window
->GetCurrentInnerWindow();
598 nsCOMPtr
<Document
> doc
= inner
->GetExtantDoc();
603 if (!window
->ShouldDelayMediaFromStart() || !doc
->Hidden()) {
607 if (!mShouldSendActiveMediaBlockStopEvent
) {
608 mShouldSendActiveMediaBlockStopEvent
= true;
609 NS_DispatchToCurrentThread(NS_NewRunnableFunction(
610 "dom::AudioChannelService::AudioChannelWindow::"
611 "MaybeNotifyMediaBlockStart",
613 nsCOMPtr
<nsIObserverService
> observerService
=
614 services::GetObserverService();
615 if (NS_WARN_IF(!observerService
)) {
619 observerService
->NotifyObservers(ToSupports(window
), "audio-playback",
620 u
"activeMediaBlockStart");
625 } // namespace mozilla::dom