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