1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
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
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 #include <audiopolicy.h>
10 #include <mmdeviceapi.h>
12 #include "mozilla/ClearOnShutdown.h"
13 #include "mozilla/RefPtr.h"
14 #include "mozilla/ScopeExit.h"
15 #include "mozilla/StaticPtr.h"
16 #include "nsIStringBundle.h"
20 #include "nsServiceManagerUtils.h"
22 #include "nsThreadUtils.h"
23 #include "nsXULAppAPI.h"
24 #include "mozilla/Attributes.h"
25 #include "mozilla/mscom/AgileReference.h"
26 #include "mozilla/mscom/Utils.h"
27 #include "mozilla/Mutex.h"
28 #include "mozilla/WindowsVersion.h"
34 * To take advantage of what Vista+ have to offer with respect to audio,
35 * we need to maintain an audio session. This class wraps IAudioSessionControl
36 * and implements IAudioSessionEvents (for callbacks from Windows)
38 class AudioSession final
: public IAudioSessionEvents
{
42 static AudioSession
* GetSingleton();
45 STDMETHODIMP_(ULONG
) AddRef();
46 STDMETHODIMP
QueryInterface(REFIID
, void**);
47 STDMETHODIMP_(ULONG
) Release();
49 // IAudioSessionEvents
50 STDMETHODIMP
OnChannelVolumeChanged(DWORD aChannelCount
,
51 float aChannelVolumeArray
[],
52 DWORD aChangedChannel
, LPCGUID aContext
);
53 STDMETHODIMP
OnDisplayNameChanged(LPCWSTR aDisplayName
, LPCGUID aContext
);
54 STDMETHODIMP
OnGroupingParamChanged(LPCGUID aGroupingParam
, LPCGUID aContext
);
55 STDMETHODIMP
OnIconPathChanged(LPCWSTR aIconPath
, LPCGUID aContext
);
56 STDMETHODIMP
OnSessionDisconnected(AudioSessionDisconnectReason aReason
);
57 STDMETHODIMP
OnSimpleVolumeChanged(float aVolume
, BOOL aMute
,
59 STDMETHODIMP
OnStateChanged(AudioSessionState aState
);
62 void Stop(bool shouldRestart
= false);
64 nsresult
GetSessionData(nsID
& aID
, nsString
& aSessionName
,
66 nsresult
SetSessionData(const nsID
& aID
, const nsString
& aSessionName
,
67 const nsString
& aIconPath
);
70 ~AudioSession() = default;
72 void StopInternal(const MutexAutoLock
& aProofOfLock
,
73 bool shouldRestart
= false);
76 RefPtr
<IAudioSessionControl
> mAudioSessionControl
;
77 nsString mDisplayName
;
79 nsID mSessionGroupingParameter
;
80 // Guards the IAudioSessionControl
81 mozilla::Mutex mMutex MOZ_UNANNOTATED
;
83 ThreadSafeAutoRefCnt mRefCnt
;
87 StaticRefPtr
<AudioSession
> sService
;
89 void StartAudioSession() {
90 MOZ_ASSERT(NS_IsMainThread());
91 MOZ_ASSERT(!sService
);
92 sService
= new AudioSession();
94 // Destroy AudioSession only after any background task threads have been
95 // stopped or abandoned.
96 ClearOnShutdown(&sService
, ShutdownPhase::XPCOMShutdownFinal
);
98 NS_DispatchBackgroundTask(
99 NS_NewCancelableRunnableFunction("StartAudioSession", []() -> void {
100 MOZ_ASSERT(AudioSession::GetSingleton(),
101 "AudioSession should outlive background threads");
102 AudioSession::GetSingleton()->Start();
106 void StopAudioSession() {
107 MOZ_ASSERT(NS_IsMainThread());
108 MOZ_ASSERT(sService
);
109 NS_DispatchBackgroundTask(
110 NS_NewRunnableFunction("StopAudioSession", []() -> void {
111 MOZ_ASSERT(AudioSession::GetSingleton(),
112 "AudioSession should outlive background threads");
113 AudioSession::GetSingleton()->Stop();
117 AudioSession
* AudioSession::GetSingleton() {
118 MOZ_ASSERT(mscom::IsCurrentThreadMTA());
122 // It appears Windows will use us on a background thread ...
123 NS_IMPL_ADDREF(AudioSession
)
124 NS_IMPL_RELEASE(AudioSession
)
127 AudioSession::QueryInterface(REFIID iid
, void** ppv
) {
128 const IID IID_IAudioSessionEvents
= __uuidof(IAudioSessionEvents
);
129 if ((IID_IUnknown
== iid
) || (IID_IAudioSessionEvents
== iid
)) {
130 *ppv
= static_cast<IAudioSessionEvents
*>(this);
135 return E_NOINTERFACE
;
138 AudioSession::AudioSession() : mMutex("AudioSessionControl") {
139 // This func must be run on the main thread as
140 // nsStringBundle is not thread safe otherwise
141 MOZ_ASSERT(NS_IsMainThread());
143 MOZ_ASSERT(XRE_IsParentProcess(),
144 "Should only get here in a chrome process!");
146 nsCOMPtr
<nsIStringBundleService
> bundleService
=
147 do_GetService(NS_STRINGBUNDLE_CONTRACTID
);
148 MOZ_ASSERT(bundleService
);
150 nsCOMPtr
<nsIStringBundle
> bundle
;
151 bundleService
->CreateBundle("chrome://branding/locale/brand.properties",
152 getter_AddRefs(bundle
));
154 bundle
->GetStringFromName("brandFullName", mDisplayName
);
157 mIconPath
.GetMutableData(&buffer
, MAX_PATH
);
158 ::GetModuleFileNameW(nullptr, buffer
, MAX_PATH
);
160 [[maybe_unused
]] nsresult rv
=
161 nsID::GenerateUUIDInPlace(mSessionGroupingParameter
);
162 MOZ_ASSERT(rv
== NS_OK
);
165 // Once we are started Windows will hold a reference to us through our
166 // IAudioSessionEvents interface that will keep us alive until the appshell
168 void AudioSession::Start() {
169 MOZ_ASSERT(mscom::IsCurrentThreadMTA());
171 const CLSID CLSID_MMDeviceEnumerator
= __uuidof(MMDeviceEnumerator
);
172 const IID IID_IMMDeviceEnumerator
= __uuidof(IMMDeviceEnumerator
);
173 const IID IID_IAudioSessionManager
= __uuidof(IAudioSessionManager
);
175 MutexAutoLock
lock(mMutex
);
176 MOZ_ASSERT(!mAudioSessionControl
);
177 MOZ_ASSERT(!mDisplayName
.IsEmpty() || !mIconPath
.IsEmpty(),
178 "Should never happen ...");
180 auto scopeExit
= MakeScopeExit([&] { StopInternal(lock
); });
182 RefPtr
<IMMDeviceEnumerator
> enumerator
;
184 ::CoCreateInstance(CLSID_MMDeviceEnumerator
, nullptr, CLSCTX_ALL
,
185 IID_IMMDeviceEnumerator
, getter_AddRefs(enumerator
));
190 RefPtr
<IMMDevice
> device
;
191 hr
= enumerator
->GetDefaultAudioEndpoint(
192 EDataFlow::eRender
, ERole::eMultimedia
, getter_AddRefs(device
));
197 RefPtr
<IAudioSessionManager
> manager
;
198 hr
= device
->Activate(IID_IAudioSessionManager
, CLSCTX_ALL
, nullptr,
199 getter_AddRefs(manager
));
204 hr
= manager
->GetAudioSessionControl(&GUID_NULL
, 0,
205 getter_AddRefs(mAudioSessionControl
));
207 if (FAILED(hr
) || !mAudioSessionControl
) {
211 // Increments refcount of 'this'.
212 hr
= mAudioSessionControl
->RegisterAudioSessionNotification(this);
217 hr
= mAudioSessionControl
->SetGroupingParam(
218 (LPGUID
) & (mSessionGroupingParameter
), nullptr);
223 hr
= mAudioSessionControl
->SetDisplayName(mDisplayName
.get(), nullptr);
228 hr
= mAudioSessionControl
->SetIconPath(mIconPath
.get(), nullptr);
236 void AudioSession::Stop(bool shouldRestart
) {
237 MOZ_ASSERT(mscom::IsCurrentThreadMTA());
239 MutexAutoLock
lock(mMutex
);
240 StopInternal(lock
, shouldRestart
);
243 void AudioSession::StopInternal(const MutexAutoLock
& aProofOfLock
,
244 bool shouldRestart
) {
245 if (!mAudioSessionControl
) {
249 // Decrement refcount of 'this'
250 mAudioSessionControl
->UnregisterAudioSessionNotification(this);
252 // Deleting the IAudioSessionControl COM object requires the STA/main thread.
253 // Audio code may concurrently be running on the main thread and it may
254 // block waiting for this to complete, creating deadlock. So we destroy the
255 // IAudioSessionControl on the main thread instead. In order to do that, we
256 // need to marshall the object to the main thread's apartment with an
258 const IID IID_IAudioSessionControl
= __uuidof(IAudioSessionControl
);
259 auto agileAsc
= MakeUnique
<mozilla::mscom::AgileReference
>(
260 IID_IAudioSessionControl
, mAudioSessionControl
);
261 mAudioSessionControl
= nullptr;
262 NS_DispatchToMainThread(NS_NewRunnableFunction(
263 "FreeAudioSession", [agileAsc
= std::move(agileAsc
),
264 IID_IAudioSessionControl
, shouldRestart
] {
265 RefPtr
<IAudioSessionControl
> toDelete
;
266 [[maybe_unused
]] HRESULT hr
= agileAsc
->Resolve(
267 IID_IAudioSessionControl
, getter_AddRefs(toDelete
));
268 MOZ_ASSERT(SUCCEEDED(hr
));
269 // Now release the AgileReference which holds our only reference to the
270 // IAudioSessionControl, then maybe restart.
272 NS_DispatchBackgroundTask(
273 NS_NewCancelableRunnableFunction("RestartAudioSession", [] {
274 AudioSession
* as
= AudioSession::GetSingleton();
282 void CopynsID(nsID
& lhs
, const nsID
& rhs
) {
286 for (int i
= 0; i
< 8; i
++) {
287 lhs
.m3
[i
] = rhs
.m3
[i
];
291 nsresult
AudioSession::GetSessionData(nsID
& aID
, nsString
& aSessionName
,
292 nsString
& aIconPath
) {
293 CopynsID(aID
, mSessionGroupingParameter
);
294 aSessionName
= mDisplayName
;
295 aIconPath
= mIconPath
;
300 nsresult
AudioSession::SetSessionData(const nsID
& aID
,
301 const nsString
& aSessionName
,
302 const nsString
& aIconPath
) {
303 MOZ_ASSERT(!XRE_IsParentProcess(),
304 "Should never get here in a chrome process!");
305 CopynsID(mSessionGroupingParameter
, aID
);
306 mDisplayName
= aSessionName
;
307 mIconPath
= aIconPath
;
312 AudioSession::OnChannelVolumeChanged(DWORD aChannelCount
,
313 float aChannelVolumeArray
[],
314 DWORD aChangedChannel
, LPCGUID aContext
) {
319 AudioSession::OnDisplayNameChanged(LPCWSTR aDisplayName
, LPCGUID aContext
) {
324 AudioSession::OnGroupingParamChanged(LPCGUID aGroupingParam
, LPCGUID aContext
) {
329 AudioSession::OnIconPathChanged(LPCWSTR aIconPath
, LPCGUID aContext
) {
334 AudioSession::OnSessionDisconnected(AudioSessionDisconnectReason aReason
) {
335 Stop(true /* shouldRestart */);
340 AudioSession::OnSimpleVolumeChanged(float aVolume
, BOOL aMute
,
346 AudioSession::OnStateChanged(AudioSessionState aState
) {
350 } // namespace widget
351 } // namespace mozilla