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