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
11 #include "mozilla/Services.h"
12 #include "mozilla/StaticPtr.h"
13 #include "mozilla/Unused.h"
14
15 #include "nsContentUtils.h"
16 #include "nsIScriptSecurityManager.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"
24
25 #include "mozilla/Preferences.h"
26
27 using namespace mozilla;
28 using namespace mozilla::dom;
29
30 static mozilla::LazyLogModule gAudioChannelLog("AudioChannel");
31
32 namespace {
33
34 bool sAudioChannelCompeting = false;
35 bool sAudioChannelCompetingAllAgents = false;
36 bool sXPCOMShuttingDown = false;
37
38 class NotifyChannelActiveRunnable final : public Runnable {
39 public:
NotifyChannelActiveRunnable(uint64_t aWindowID,bool aActive)40 NotifyChannelActiveRunnable(uint64_t aWindowID, bool aActive)
41 : Runnable("NotifyChannelActiveRunnable"),
42 mWindowID(aWindowID),
43 mActive(aActive) {}
44
Run()45 NS_IMETHOD Run() override {
46 nsCOMPtr<nsIObserverService> observerService =
47 services::GetObserverService();
48 if (NS_WARN_IF(!observerService)) {
49 return NS_ERROR_FAILURE;
50 }
51
52 nsCOMPtr<nsISupportsPRUint64> wrapper =
53 do_CreateInstance(NS_SUPPORTS_PRUINT64_CONTRACTID);
54 if (NS_WARN_IF(!wrapper)) {
55 return NS_ERROR_FAILURE;
56 }
57
58 wrapper->SetData(mWindowID);
59
60 observerService->NotifyObservers(wrapper, "media-playback",
61 mActive ? u"active" : u"inactive");
62
63 MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
64 ("NotifyChannelActiveRunnable, active = %s\n",
65 mActive ? "true" : "false"));
66
67 return NS_OK;
68 }
69
70 private:
71 const uint64_t mWindowID;
72 const bool mActive;
73 };
74
75 class AudioPlaybackRunnable final : public Runnable {
76 public:
AudioPlaybackRunnable(nsPIDOMWindowOuter * aWindow,bool aActive,AudioChannelService::AudibleChangedReasons aReason)77 AudioPlaybackRunnable(nsPIDOMWindowOuter* aWindow, bool aActive,
78 AudioChannelService::AudibleChangedReasons aReason)
79 : mozilla::Runnable("AudioPlaybackRunnable"),
80 mWindow(aWindow),
81 mActive(aActive),
82 mReason(aReason) {}
83
Run()84 NS_IMETHOD Run() override {
85 nsCOMPtr<nsIObserverService> observerService =
86 services::GetObserverService();
87 if (NS_WARN_IF(!observerService)) {
88 return NS_ERROR_FAILURE;
89 }
90
91 nsAutoString state;
92 GetActiveState(state);
93
94 observerService->NotifyObservers(ToSupports(mWindow), "audio-playback",
95 state.get());
96
97 MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
98 ("AudioPlaybackRunnable, active = %s, reason = %s\n",
99 mActive ? "true" : "false", AudibleChangedReasonToStr(mReason)));
100
101 return NS_OK;
102 }
103
104 private:
GetActiveState(nsAString & astate)105 void GetActiveState(nsAString& astate) {
106 if (mActive) {
107 CopyASCIItoUTF16("active", astate);
108 } else {
109 if (mReason ==
110 AudioChannelService::AudibleChangedReasons::ePauseStateChanged) {
111 CopyASCIItoUTF16("inactive-pause", astate);
112 } else {
113 CopyASCIItoUTF16("inactive-nonaudible", astate);
114 }
115 }
116 }
117
118 nsCOMPtr<nsPIDOMWindowOuter> mWindow;
119 bool mActive;
120 AudioChannelService::AudibleChangedReasons mReason;
121 };
122
IsEnableAudioCompetingForAllAgents()123 bool IsEnableAudioCompetingForAllAgents() {
124 // In general, the audio competing should only be for audible media and it
125 // helps user can focus on one media at the same time. However, we hope to
126 // treat all media as the same in the mobile device. First reason is we have
127 // media control on fennec and we just want to control one media at once time.
128 // Second reason is to reduce the bandwidth, avoiding to play any non-audible
129 // media in background which user doesn't notice about.
130 #ifdef MOZ_WIDGET_ANDROID
131 return true;
132 #else
133 return sAudioChannelCompetingAllAgents;
134 #endif
135 }
136
137 } // anonymous namespace
138
139 namespace mozilla {
140 namespace dom {
141
SuspendTypeToStr(const nsSuspendedTypes & aSuspend)142 const char* SuspendTypeToStr(const nsSuspendedTypes& aSuspend) {
143 MOZ_ASSERT(aSuspend == nsISuspendedTypes::NONE_SUSPENDED ||
144 aSuspend == nsISuspendedTypes::SUSPENDED_PAUSE ||
145 aSuspend == nsISuspendedTypes::SUSPENDED_BLOCK ||
146 aSuspend == nsISuspendedTypes::SUSPENDED_PAUSE_DISPOSABLE ||
147 aSuspend == nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE);
148
149 switch (aSuspend) {
150 case nsISuspendedTypes::NONE_SUSPENDED:
151 return "none";
152 case nsISuspendedTypes::SUSPENDED_PAUSE:
153 return "pause";
154 case nsISuspendedTypes::SUSPENDED_BLOCK:
155 return "block";
156 case nsISuspendedTypes::SUSPENDED_PAUSE_DISPOSABLE:
157 return "disposable-pause";
158 case nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE:
159 return "disposable-stop";
160 default:
161 return "unknown";
162 }
163 }
164
AudibleStateToStr(const AudioChannelService::AudibleState & aAudible)165 const char* AudibleStateToStr(
166 const AudioChannelService::AudibleState& aAudible) {
167 MOZ_ASSERT(aAudible == AudioChannelService::AudibleState::eNotAudible ||
168 aAudible == AudioChannelService::AudibleState::eMaybeAudible ||
169 aAudible == AudioChannelService::AudibleState::eAudible);
170
171 switch (aAudible) {
172 case AudioChannelService::AudibleState::eNotAudible:
173 return "not-audible";
174 case AudioChannelService::AudibleState::eMaybeAudible:
175 return "maybe-audible";
176 case AudioChannelService::AudibleState::eAudible:
177 return "audible";
178 default:
179 return "unknown";
180 }
181 }
182
AudibleChangedReasonToStr(const AudioChannelService::AudibleChangedReasons & aReason)183 const char* AudibleChangedReasonToStr(
184 const AudioChannelService::AudibleChangedReasons& aReason) {
185 MOZ_ASSERT(
186 aReason == AudioChannelService::AudibleChangedReasons::eVolumeChanged ||
187 aReason ==
188 AudioChannelService::AudibleChangedReasons::eDataAudibleChanged ||
189 aReason ==
190 AudioChannelService::AudibleChangedReasons::ePauseStateChanged);
191
192 switch (aReason) {
193 case AudioChannelService::AudibleChangedReasons::eVolumeChanged:
194 return "volume";
195 case AudioChannelService::AudibleChangedReasons::eDataAudibleChanged:
196 return "data-audible";
197 case AudioChannelService::AudibleChangedReasons::ePauseStateChanged:
198 return "pause-state";
199 default:
200 return "unknown";
201 }
202 }
203
204 StaticRefPtr<AudioChannelService> gAudioChannelService;
205
CreateServiceIfNeeded()206 /* static */ void AudioChannelService::CreateServiceIfNeeded() {
207 MOZ_ASSERT(NS_IsMainThread());
208
209 if (!gAudioChannelService) {
210 gAudioChannelService = new AudioChannelService();
211 }
212 }
213
214 /* static */ already_AddRefed<AudioChannelService>
GetOrCreate()215 AudioChannelService::GetOrCreate() {
216 if (sXPCOMShuttingDown) {
217 return nullptr;
218 }
219
220 CreateServiceIfNeeded();
221 RefPtr<AudioChannelService> service = gAudioChannelService.get();
222 return service.forget();
223 }
224
Get()225 /* static */ already_AddRefed<AudioChannelService> AudioChannelService::Get() {
226 if (sXPCOMShuttingDown) {
227 return nullptr;
228 }
229
230 RefPtr<AudioChannelService> service = gAudioChannelService.get();
231 return service.forget();
232 }
233
GetAudioChannelLog()234 /* static */ LogModule* AudioChannelService::GetAudioChannelLog() {
235 return gAudioChannelLog;
236 }
237
Shutdown()238 /* static */ void AudioChannelService::Shutdown() {
239 if (gAudioChannelService) {
240 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
241 if (obs) {
242 obs->RemoveObserver(gAudioChannelService, "xpcom-shutdown");
243 obs->RemoveObserver(gAudioChannelService, "outer-window-destroyed");
244 }
245
246 gAudioChannelService->mWindows.Clear();
247
248 gAudioChannelService = nullptr;
249 }
250 }
251
IsEnableAudioCompeting()252 /* static */ bool AudioChannelService::IsEnableAudioCompeting() {
253 CreateServiceIfNeeded();
254 return sAudioChannelCompeting;
255 }
256
257 NS_INTERFACE_MAP_BEGIN(AudioChannelService)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports,nsIObserver)258 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIObserver)
259 NS_INTERFACE_MAP_ENTRY(nsIObserver)
260 NS_INTERFACE_MAP_END
261
262 NS_IMPL_ADDREF(AudioChannelService)
263 NS_IMPL_RELEASE(AudioChannelService)
264
265 AudioChannelService::AudioChannelService() {
266 nsCOMPtr<nsIObserverService> obs = mozilla::services::GetObserverService();
267 if (obs) {
268 obs->AddObserver(this, "xpcom-shutdown", false);
269 obs->AddObserver(this, "outer-window-destroyed", false);
270 }
271
272 Preferences::AddBoolVarCache(&sAudioChannelCompeting,
273 "dom.audiochannel.audioCompeting");
274 Preferences::AddBoolVarCache(&sAudioChannelCompetingAllAgents,
275 "dom.audiochannel.audioCompeting.allAgents");
276 }
277
~AudioChannelService()278 AudioChannelService::~AudioChannelService() {}
279
RegisterAudioChannelAgent(AudioChannelAgent * aAgent,AudibleState aAudible)280 void AudioChannelService::RegisterAudioChannelAgent(AudioChannelAgent* aAgent,
281 AudibleState aAudible) {
282 MOZ_ASSERT(aAgent);
283
284 uint64_t windowID = aAgent->WindowID();
285 AudioChannelWindow* winData = GetWindowData(windowID);
286 if (!winData) {
287 winData = new AudioChannelWindow(windowID);
288 mWindows.AppendElement(winData);
289 }
290
291 // To make sure agent would be alive because AppendAgent() would trigger the
292 // callback function of AudioChannelAgentOwner that means the agent might be
293 // released in their callback.
294 RefPtr<AudioChannelAgent> kungFuDeathGrip(aAgent);
295 winData->AppendAgent(aAgent, aAudible);
296 }
297
UnregisterAudioChannelAgent(AudioChannelAgent * aAgent)298 void AudioChannelService::UnregisterAudioChannelAgent(
299 AudioChannelAgent* aAgent) {
300 MOZ_ASSERT(aAgent);
301
302 AudioChannelWindow* winData = GetWindowData(aAgent->WindowID());
303 if (!winData) {
304 return;
305 }
306
307 // To make sure agent would be alive because AppendAgent() would trigger the
308 // callback function of AudioChannelAgentOwner that means the agent might be
309 // released in their callback.
310 RefPtr<AudioChannelAgent> kungFuDeathGrip(aAgent);
311 winData->RemoveAgent(aAgent);
312 }
313
GetMediaConfig(nsPIDOMWindowOuter * aWindow) const314 AudioPlaybackConfig AudioChannelService::GetMediaConfig(
315 nsPIDOMWindowOuter* aWindow) const {
316 AudioPlaybackConfig config(1.0, false, nsISuspendedTypes::NONE_SUSPENDED);
317
318 if (!aWindow) {
319 config.SetConfig(0.0, true, nsISuspendedTypes::SUSPENDED_BLOCK);
320 return config;
321 }
322
323 AudioChannelWindow* winData = nullptr;
324 nsCOMPtr<nsPIDOMWindowOuter> window = aWindow;
325
326 // The volume must be calculated based on the window hierarchy. Here we go up
327 // to the top window and we calculate the volume and the muted flag.
328 do {
329 winData = GetWindowData(window->WindowID());
330 if (winData) {
331 config.mVolume *= winData->mConfig.mVolume;
332 config.mMuted = config.mMuted || winData->mConfig.mMuted;
333 config.mSuspend = winData->mOwningAudioFocus
334 ? config.mSuspend
335 : nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE;
336 }
337
338 config.mVolume *= window->GetAudioVolume();
339 config.mMuted = config.mMuted || window->GetAudioMuted();
340 if (window->GetMediaSuspend() != nsISuspendedTypes::NONE_SUSPENDED) {
341 config.mSuspend = window->GetMediaSuspend();
342 }
343
344 nsCOMPtr<nsPIDOMWindowOuter> win = window->GetScriptableParentOrNull();
345 if (!win) {
346 break;
347 }
348
349 window = do_QueryInterface(win);
350
351 // If there is no parent, or we are the toplevel we don't continue.
352 } while (window && window != aWindow);
353
354 return config;
355 }
356
AudioAudibleChanged(AudioChannelAgent * aAgent,AudibleState aAudible,AudibleChangedReasons aReason)357 void AudioChannelService::AudioAudibleChanged(AudioChannelAgent* aAgent,
358 AudibleState aAudible,
359 AudibleChangedReasons aReason) {
360 MOZ_ASSERT(aAgent);
361
362 uint64_t windowID = aAgent->WindowID();
363 AudioChannelWindow* winData = GetWindowData(windowID);
364 if (winData) {
365 winData->AudioAudibleChanged(aAgent, aAudible, aReason);
366 }
367 }
368
369 NS_IMETHODIMP
Observe(nsISupports * aSubject,const char * aTopic,const char16_t * aData)370 AudioChannelService::Observe(nsISupports* aSubject, const char* aTopic,
371 const char16_t* aData) {
372 if (!strcmp(aTopic, "xpcom-shutdown")) {
373 sXPCOMShuttingDown = true;
374 Shutdown();
375 } else if (!strcmp(aTopic, "outer-window-destroyed")) {
376 nsCOMPtr<nsISupportsPRUint64> wrapper = do_QueryInterface(aSubject);
377 NS_ENSURE_TRUE(wrapper, NS_ERROR_FAILURE);
378
379 uint64_t outerID;
380 nsresult rv = wrapper->GetData(&outerID);
381 if (NS_WARN_IF(NS_FAILED(rv))) {
382 return rv;
383 }
384
385 nsAutoPtr<AudioChannelWindow> winData;
386 {
387 nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator iter(
388 mWindows);
389 while (iter.HasMore()) {
390 nsAutoPtr<AudioChannelWindow>& next = iter.GetNext();
391 if (next->mWindowID == outerID) {
392 uint32_t pos = mWindows.IndexOf(next);
393 winData = next.forget();
394 mWindows.RemoveElementAt(pos);
395 break;
396 }
397 }
398 }
399
400 if (winData) {
401 nsTObserverArray<AudioChannelAgent*>::ForwardIterator iter(
402 winData->mAgents);
403 while (iter.HasMore()) {
404 iter.GetNext()->WindowVolumeChanged();
405 }
406 }
407 }
408
409 return NS_OK;
410 }
411
RefreshAgents(nsPIDOMWindowOuter * aWindow,const std::function<void (AudioChannelAgent *)> & aFunc)412 void AudioChannelService::RefreshAgents(
413 nsPIDOMWindowOuter* aWindow,
414 const std::function<void(AudioChannelAgent*)>& aFunc) {
415 MOZ_ASSERT(aWindow);
416
417 nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop();
418 if (!topWindow) {
419 return;
420 }
421
422 AudioChannelWindow* winData = GetWindowData(topWindow->WindowID());
423 if (!winData) {
424 return;
425 }
426
427 nsTObserverArray<AudioChannelAgent*>::ForwardIterator iter(winData->mAgents);
428 while (iter.HasMore()) {
429 aFunc(iter.GetNext());
430 }
431 }
432
RefreshAgentsVolume(nsPIDOMWindowOuter * aWindow)433 void AudioChannelService::RefreshAgentsVolume(nsPIDOMWindowOuter* aWindow) {
434 RefreshAgents(aWindow,
435 [](AudioChannelAgent* agent) { agent->WindowVolumeChanged(); });
436 }
437
RefreshAgentsSuspend(nsPIDOMWindowOuter * aWindow,nsSuspendedTypes aSuspend)438 void AudioChannelService::RefreshAgentsSuspend(nsPIDOMWindowOuter* aWindow,
439 nsSuspendedTypes aSuspend) {
440 RefreshAgents(aWindow, [aSuspend](AudioChannelAgent* agent) {
441 agent->WindowSuspendChanged(aSuspend);
442 });
443 }
444
SetWindowAudioCaptured(nsPIDOMWindowOuter * aWindow,uint64_t aInnerWindowID,bool aCapture)445 void AudioChannelService::SetWindowAudioCaptured(nsPIDOMWindowOuter* aWindow,
446 uint64_t aInnerWindowID,
447 bool aCapture) {
448 MOZ_ASSERT(NS_IsMainThread());
449 MOZ_ASSERT(aWindow);
450
451 MOZ_LOG(GetAudioChannelLog(), LogLevel::Debug,
452 ("AudioChannelService, SetWindowAudioCaptured, window = %p, "
453 "aCapture = %d\n",
454 aWindow, aCapture));
455
456 nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop();
457 if (!topWindow) {
458 return;
459 }
460
461 AudioChannelWindow* winData = GetWindowData(topWindow->WindowID());
462
463 // This can happen, but only during shutdown, because the the outer window
464 // changes ScriptableTop, so that its ID is different.
465 // In this case either we are capturing, and it's too late because the window
466 // has been closed anyways, or we are un-capturing, and everything has already
467 // been cleaned up by the HTMLMediaElements or the AudioContexts.
468 if (!winData) {
469 return;
470 }
471
472 if (aCapture != winData->mIsAudioCaptured) {
473 winData->mIsAudioCaptured = aCapture;
474 nsTObserverArray<AudioChannelAgent*>::ForwardIterator iter(
475 winData->mAgents);
476 while (iter.HasMore()) {
477 iter.GetNext()->WindowAudioCaptureChanged(aInnerWindowID, aCapture);
478 }
479 }
480 }
481
482 AudioChannelService::AudioChannelWindow*
GetOrCreateWindowData(nsPIDOMWindowOuter * aWindow)483 AudioChannelService::GetOrCreateWindowData(nsPIDOMWindowOuter* aWindow) {
484 MOZ_ASSERT(NS_IsMainThread());
485 MOZ_ASSERT(aWindow);
486
487 AudioChannelWindow* winData = GetWindowData(aWindow->WindowID());
488 if (!winData) {
489 winData = new AudioChannelWindow(aWindow->WindowID());
490 mWindows.AppendElement(winData);
491 }
492
493 return winData;
494 }
495
GetWindowData(uint64_t aWindowID) const496 AudioChannelService::AudioChannelWindow* AudioChannelService::GetWindowData(
497 uint64_t aWindowID) const {
498 nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator iter(
499 mWindows);
500 while (iter.HasMore()) {
501 AudioChannelWindow* next = iter.GetNext();
502 if (next->mWindowID == aWindowID) {
503 return next;
504 }
505 }
506
507 return nullptr;
508 }
509
IsWindowActive(nsPIDOMWindowOuter * aWindow)510 bool AudioChannelService::IsWindowActive(nsPIDOMWindowOuter* aWindow) {
511 MOZ_ASSERT(NS_IsMainThread());
512
513 auto* window = nsPIDOMWindowOuter::From(aWindow)->GetScriptableTop();
514 if (!window) {
515 return false;
516 }
517
518 AudioChannelWindow* winData = GetWindowData(window->WindowID());
519 if (!winData) {
520 return false;
521 }
522
523 return !winData->mAudibleAgents.IsEmpty();
524 }
525
RefreshAgentsAudioFocusChanged(AudioChannelAgent * aAgent)526 void AudioChannelService::RefreshAgentsAudioFocusChanged(
527 AudioChannelAgent* aAgent) {
528 MOZ_ASSERT(aAgent);
529
530 nsTObserverArray<nsAutoPtr<AudioChannelWindow>>::ForwardIterator iter(
531 mWindows);
532 while (iter.HasMore()) {
533 AudioChannelWindow* winData = iter.GetNext();
534 if (winData->mOwningAudioFocus) {
535 winData->AudioFocusChanged(aAgent);
536 }
537 }
538 }
539
NotifyMediaResumedFromBlock(nsPIDOMWindowOuter * aWindow)540 void AudioChannelService::NotifyMediaResumedFromBlock(
541 nsPIDOMWindowOuter* aWindow) {
542 MOZ_ASSERT(aWindow);
543
544 nsCOMPtr<nsPIDOMWindowOuter> topWindow = aWindow->GetScriptableTop();
545 if (!topWindow) {
546 return;
547 }
548
549 AudioChannelWindow* winData = GetWindowData(topWindow->WindowID());
550 if (!winData) {
551 return;
552 }
553
554 winData->NotifyMediaBlockStop(aWindow);
555 }
556
RequestAudioFocus(AudioChannelAgent * aAgent)557 void AudioChannelService::AudioChannelWindow::RequestAudioFocus(
558 AudioChannelAgent* aAgent) {
559 MOZ_ASSERT(aAgent);
560
561 // Don't need to check audio focus for window-less agent.
562 if (!aAgent->Window()) {
563 return;
564 }
565
566 // We already have the audio focus. No operation is needed.
567 if (mOwningAudioFocus) {
568 return;
569 }
570
571 // Only foreground window can request audio focus, but it would still own the
572 // audio focus even it goes to background. Audio focus would be abandoned
573 // only when other foreground window starts audio competing.
574 // One exception is if the pref "media.block-autoplay-until-in-foreground"
575 // is on and the background page is the non-visited before. Because the media
576 // in that page would be blocked until the page is going to foreground.
577 mOwningAudioFocus = (!(aAgent->Window()->IsBackground()) ||
578 aAgent->Window()->GetMediaSuspend() ==
579 nsISuspendedTypes::SUSPENDED_BLOCK);
580
581 MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
582 ("AudioChannelWindow, RequestAudioFocus, this = %p, "
583 "agent = %p, owning audio focus = %s\n",
584 this, aAgent, mOwningAudioFocus ? "true" : "false"));
585 }
586
NotifyAudioCompetingChanged(AudioChannelAgent * aAgent)587 void AudioChannelService::AudioChannelWindow::NotifyAudioCompetingChanged(
588 AudioChannelAgent* aAgent) {
589 // This function may be called after RemoveAgentAndReduceAgentsNum(), so the
590 // agent may be not contained in mAgent. In addition, the agent would still
591 // be alive because we have kungFuDeathGrip in UnregisterAudioChannelAgent().
592 MOZ_ASSERT(aAgent);
593
594 RefPtr<AudioChannelService> service = AudioChannelService::GetOrCreate();
595 MOZ_ASSERT(service);
596
597 if (!service->IsEnableAudioCompeting()) {
598 return;
599 }
600
601 if (!IsAgentInvolvingInAudioCompeting(aAgent)) {
602 return;
603 }
604
605 MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
606 ("AudioChannelWindow, NotifyAudioCompetingChanged, this = %p, "
607 "agent = %p\n",
608 this, aAgent));
609
610 service->RefreshAgentsAudioFocusChanged(aAgent);
611 }
612
IsAgentInvolvingInAudioCompeting(AudioChannelAgent * aAgent) const613 bool AudioChannelService::AudioChannelWindow::IsAgentInvolvingInAudioCompeting(
614 AudioChannelAgent* aAgent) const {
615 MOZ_ASSERT(aAgent);
616
617 if (!mOwningAudioFocus) {
618 return false;
619 }
620
621 if (IsAudioCompetingInSameTab()) {
622 return false;
623 }
624
625 // TODO : add MediaSession::ambient kind, because it doens't interact with
626 // other kinds.
627 return true;
628 }
629
IsAudioCompetingInSameTab() const630 bool AudioChannelService::AudioChannelWindow::IsAudioCompetingInSameTab()
631 const {
632 bool hasMultipleActiveAgents = IsEnableAudioCompetingForAllAgents()
633 ? mAgents.Length() > 1
634 : mAudibleAgents.Length() > 1;
635 return mOwningAudioFocus && hasMultipleActiveAgents;
636 }
637
AudioFocusChanged(AudioChannelAgent * aNewPlayingAgent)638 void AudioChannelService::AudioChannelWindow::AudioFocusChanged(
639 AudioChannelAgent* aNewPlayingAgent) {
640 // This agent isn't always known for the current window, because it can comes
641 // from other window.
642 MOZ_ASSERT(aNewPlayingAgent);
643
644 if (IsInactiveWindow()) {
645 // These would happen in two situations,
646 // (1) Audio in page A was ended, and another page B want to play audio.
647 // Page A should abandon its focus.
648 // (2) Audio was paused by remote-control, page should still own the focus.
649 mOwningAudioFocus = IsContainingPlayingAgent(aNewPlayingAgent);
650 } else {
651 nsTObserverArray<AudioChannelAgent*>::ForwardIterator iter(
652 IsEnableAudioCompetingForAllAgents() ? mAgents : mAudibleAgents);
653 while (iter.HasMore()) {
654 AudioChannelAgent* agent = iter.GetNext();
655 MOZ_ASSERT(agent);
656
657 // Don't need to update the playing state of new playing agent.
658 if (agent == aNewPlayingAgent) {
659 continue;
660 }
661
662 uint32_t type = GetCompetingBehavior(agent);
663
664 // If window will be suspended, it needs to abandon the audio focus
665 // because only one window can own audio focus at a time. However, we
666 // would support multiple audio focus at the same time in the future.
667 mOwningAudioFocus = (type == nsISuspendedTypes::NONE_SUSPENDED);
668
669 // TODO : support other behaviors which are definded in MediaSession API.
670 switch (type) {
671 case nsISuspendedTypes::NONE_SUSPENDED:
672 case nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE:
673 agent->WindowSuspendChanged(type);
674 break;
675 }
676 }
677 }
678
679 MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
680 ("AudioChannelWindow, AudioFocusChanged, this = %p, "
681 "OwningAudioFocus = %s\n",
682 this, mOwningAudioFocus ? "true" : "false"));
683 }
684
IsContainingPlayingAgent(AudioChannelAgent * aAgent) const685 bool AudioChannelService::AudioChannelWindow::IsContainingPlayingAgent(
686 AudioChannelAgent* aAgent) const {
687 return (aAgent->WindowID() == mWindowID);
688 }
689
GetCompetingBehavior(AudioChannelAgent * aAgent) const690 uint32_t AudioChannelService::AudioChannelWindow::GetCompetingBehavior(
691 AudioChannelAgent* aAgent) const {
692 MOZ_ASSERT(aAgent);
693 MOZ_ASSERT(IsEnableAudioCompetingForAllAgents()
694 ? mAgents.Contains(aAgent)
695 : mAudibleAgents.Contains(aAgent));
696
697 uint32_t competingBehavior = nsISuspendedTypes::SUSPENDED_STOP_DISPOSABLE;
698
699 MOZ_LOG(AudioChannelService::GetAudioChannelLog(), LogLevel::Debug,
700 ("AudioChannelWindow, GetCompetingBehavior, this = %p, "
701 "behavior = %s\n",
702 this, SuspendTypeToStr(competingBehavior)));
703
704 return competingBehavior;
705 }
706
AppendAgent(AudioChannelAgent * aAgent,AudibleState aAudible)707 void AudioChannelService::AudioChannelWindow::AppendAgent(
708 AudioChannelAgent* aAgent, AudibleState aAudible) {
709 MOZ_ASSERT(aAgent);
710
711 RequestAudioFocus(aAgent);
712 AppendAgentAndIncreaseAgentsNum(aAgent);
713 AudioCapturedChanged(aAgent, AudioCaptureState::eCapturing);
714 if (aAudible == AudibleState::eAudible) {
715 AudioAudibleChanged(aAgent, AudibleState::eAudible,
716 AudibleChangedReasons::eDataAudibleChanged);
717 } else if (IsEnableAudioCompetingForAllAgents() &&
718 aAudible != AudibleState::eAudible) {
719 NotifyAudioCompetingChanged(aAgent);
720 }
721 }
722
RemoveAgent(AudioChannelAgent * aAgent)723 void AudioChannelService::AudioChannelWindow::RemoveAgent(
724 AudioChannelAgent* aAgent) {
725 MOZ_ASSERT(aAgent);
726
727 RemoveAgentAndReduceAgentsNum(aAgent);
728 AudioCapturedChanged(aAgent, AudioCaptureState::eNotCapturing);
729 AudioAudibleChanged(aAgent, AudibleState::eNotAudible,
730 AudibleChangedReasons::ePauseStateChanged);
731 }
732
NotifyMediaBlockStop(nsPIDOMWindowOuter * aWindow)733 void AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop(
734 nsPIDOMWindowOuter* aWindow) {
735 // Can't use raw pointer for lamba variable capturing, use smart ptr.
736 nsCOMPtr<nsPIDOMWindowOuter> window = aWindow;
737 NS_DispatchToCurrentThread(NS_NewRunnableFunction(
738 "dom::AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop",
739 [window]() -> void {
740 nsCOMPtr<nsIObserverService> observerService =
741 services::GetObserverService();
742 if (NS_WARN_IF(!observerService)) {
743 return;
744 }
745
746 observerService->NotifyObservers(ToSupports(window), "audio-playback",
747 u"mediaBlockStop");
748 }));
749
750 if (mShouldSendActiveMediaBlockStopEvent) {
751 mShouldSendActiveMediaBlockStopEvent = false;
752 NS_DispatchToCurrentThread(NS_NewRunnableFunction(
753 "dom::AudioChannelService::AudioChannelWindow::NotifyMediaBlockStop",
754 [window]() -> void {
755 nsCOMPtr<nsIObserverService> observerService =
756 services::GetObserverService();
757 if (NS_WARN_IF(!observerService)) {
758 return;
759 }
760
761 observerService->NotifyObservers(ToSupports(window), "audio-playback",
762 u"activeMediaBlockStop");
763 }));
764 }
765 }
766
AppendAgentAndIncreaseAgentsNum(AudioChannelAgent * aAgent)767 void AudioChannelService::AudioChannelWindow::AppendAgentAndIncreaseAgentsNum(
768 AudioChannelAgent* aAgent) {
769 MOZ_ASSERT(aAgent);
770 MOZ_ASSERT(!mAgents.Contains(aAgent));
771
772 mAgents.AppendElement(aAgent);
773
774 ++mConfig.mNumberOfAgents;
775
776 // TODO: Make NotifyChannelActiveRunnable irrelevant to
777 // BrowserElementAudioChannel
778 if (mConfig.mNumberOfAgents == 1) {
779 NotifyChannelActive(aAgent->WindowID(), true);
780 }
781 }
782
RemoveAgentAndReduceAgentsNum(AudioChannelAgent * aAgent)783 void AudioChannelService::AudioChannelWindow::RemoveAgentAndReduceAgentsNum(
784 AudioChannelAgent* aAgent) {
785 MOZ_ASSERT(aAgent);
786 MOZ_ASSERT(mAgents.Contains(aAgent));
787
788 mAgents.RemoveElement(aAgent);
789
790 MOZ_ASSERT(mConfig.mNumberOfAgents > 0);
791 --mConfig.mNumberOfAgents;
792
793 if (mConfig.mNumberOfAgents == 0) {
794 NotifyChannelActive(aAgent->WindowID(), false);
795 }
796 }
797
AudioCapturedChanged(AudioChannelAgent * aAgent,AudioCaptureState aCapture)798 void AudioChannelService::AudioChannelWindow::AudioCapturedChanged(
799 AudioChannelAgent* aAgent, AudioCaptureState aCapture) {
800 MOZ_ASSERT(aAgent);
801
802 if (mIsAudioCaptured) {
803 aAgent->WindowAudioCaptureChanged(aAgent->InnerWindowID(), aCapture);
804 }
805 }
806
AudioAudibleChanged(AudioChannelAgent * aAgent,AudibleState aAudible,AudibleChangedReasons aReason)807 void AudioChannelService::AudioChannelWindow::AudioAudibleChanged(
808 AudioChannelAgent* aAgent, AudibleState aAudible,
809 AudibleChangedReasons aReason) {
810 MOZ_ASSERT(aAgent);
811
812 if (aAudible == AudibleState::eAudible) {
813 AppendAudibleAgentIfNotContained(aAgent, aReason);
814 NotifyAudioCompetingChanged(aAgent);
815 } else {
816 RemoveAudibleAgentIfContained(aAgent, aReason);
817 }
818
819 if (aAudible != AudibleState::eNotAudible) {
820 MaybeNotifyMediaBlockStart(aAgent);
821 }
822 }
823
AppendAudibleAgentIfNotContained(AudioChannelAgent * aAgent,AudibleChangedReasons aReason)824 void AudioChannelService::AudioChannelWindow::AppendAudibleAgentIfNotContained(
825 AudioChannelAgent* aAgent, AudibleChangedReasons aReason) {
826 MOZ_ASSERT(aAgent);
827 MOZ_ASSERT(mAgents.Contains(aAgent));
828
829 if (!mAudibleAgents.Contains(aAgent)) {
830 mAudibleAgents.AppendElement(aAgent);
831 if (IsFirstAudibleAgent()) {
832 NotifyAudioAudibleChanged(aAgent->Window(), AudibleState::eAudible,
833 aReason);
834 }
835 }
836 }
837
RemoveAudibleAgentIfContained(AudioChannelAgent * aAgent,AudibleChangedReasons aReason)838 void AudioChannelService::AudioChannelWindow::RemoveAudibleAgentIfContained(
839 AudioChannelAgent* aAgent, AudibleChangedReasons aReason) {
840 MOZ_ASSERT(aAgent);
841
842 if (mAudibleAgents.Contains(aAgent)) {
843 mAudibleAgents.RemoveElement(aAgent);
844 if (IsLastAudibleAgent()) {
845 NotifyAudioAudibleChanged(aAgent->Window(), AudibleState::eNotAudible,
846 aReason);
847 }
848 }
849 }
850
IsFirstAudibleAgent() const851 bool AudioChannelService::AudioChannelWindow::IsFirstAudibleAgent() const {
852 return (mAudibleAgents.Length() == 1);
853 }
854
IsLastAudibleAgent() const855 bool AudioChannelService::AudioChannelWindow::IsLastAudibleAgent() const {
856 return mAudibleAgents.IsEmpty();
857 }
858
IsInactiveWindow() const859 bool AudioChannelService::AudioChannelWindow::IsInactiveWindow() const {
860 return IsEnableAudioCompetingForAllAgents()
861 ? mAudibleAgents.IsEmpty() && mAgents.IsEmpty()
862 : mAudibleAgents.IsEmpty();
863 }
864
NotifyAudioAudibleChanged(nsPIDOMWindowOuter * aWindow,AudibleState aAudible,AudibleChangedReasons aReason)865 void AudioChannelService::AudioChannelWindow::NotifyAudioAudibleChanged(
866 nsPIDOMWindowOuter* aWindow, AudibleState aAudible,
867 AudibleChangedReasons aReason) {
868 RefPtr<AudioPlaybackRunnable> runnable = new AudioPlaybackRunnable(
869 aWindow, aAudible == AudibleState::eAudible, aReason);
870 DebugOnly<nsresult> rv = NS_DispatchToCurrentThread(runnable);
871 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToCurrentThread failed");
872 }
873
NotifyChannelActive(uint64_t aWindowID,bool aActive)874 void AudioChannelService::AudioChannelWindow::NotifyChannelActive(
875 uint64_t aWindowID, bool aActive) {
876 RefPtr<NotifyChannelActiveRunnable> runnable =
877 new NotifyChannelActiveRunnable(aWindowID, aActive);
878 DebugOnly<nsresult> rv = NS_DispatchToCurrentThread(runnable);
879 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "NS_DispatchToCurrentThread failed");
880 }
881
MaybeNotifyMediaBlockStart(AudioChannelAgent * aAgent)882 void AudioChannelService::AudioChannelWindow::MaybeNotifyMediaBlockStart(
883 AudioChannelAgent* aAgent) {
884 nsCOMPtr<nsPIDOMWindowOuter> window = aAgent->Window();
885 if (!window) {
886 return;
887 }
888
889 nsCOMPtr<nsPIDOMWindowInner> inner = window->GetCurrentInnerWindow();
890 if (!inner) {
891 return;
892 }
893
894 nsCOMPtr<nsIDocument> doc = inner->GetExtantDoc();
895 if (!doc) {
896 return;
897 }
898
899 if (window->GetMediaSuspend() != nsISuspendedTypes::SUSPENDED_BLOCK ||
900 !doc->Hidden()) {
901 return;
902 }
903
904 if (!mShouldSendActiveMediaBlockStopEvent) {
905 mShouldSendActiveMediaBlockStopEvent = true;
906 NS_DispatchToCurrentThread(NS_NewRunnableFunction(
907 "dom::AudioChannelService::AudioChannelWindow::"
908 "MaybeNotifyMediaBlockStart",
909 [window]() -> void {
910 nsCOMPtr<nsIObserverService> observerService =
911 services::GetObserverService();
912 if (NS_WARN_IF(!observerService)) {
913 return;
914 }
915
916 observerService->NotifyObservers(ToSupports(window), "audio-playback",
917 u"activeMediaBlockStart");
918 }));
919 }
920 }
921
922 } // namespace dom
923 } // namespace mozilla
924