1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
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/. */
6
7 #include <windows.h>
8 #include <audiopolicy.h>
9 #include <mmdeviceapi.h>
10
11 #include "mozilla/RefPtr.h"
12 #include "nsIStringBundle.h"
13
14 //#include "AudioSession.h"
15 #include "nsCOMPtr.h"
16 #include "nsID.h"
17 #include "nsServiceManagerUtils.h"
18 #include "nsString.h"
19 #include "nsThreadUtils.h"
20 #include "nsXULAppAPI.h"
21 #include "mozilla/Attributes.h"
22 #include "mozilla/Mutex.h"
23 #include "mozilla/WindowsVersion.h"
24
25 #include <objbase.h>
26
27 namespace mozilla {
28 namespace widget {
29
30 /*
31 * To take advantage of what Vista+ have to offer with respect to audio,
32 * we need to maintain an audio session. This class wraps IAudioSessionControl
33 * and implements IAudioSessionEvents (for callbacks from Windows)
34 */
35 class AudioSession final : public IAudioSessionEvents {
36 private:
37 AudioSession();
38 ~AudioSession();
39
40 public:
41 static AudioSession* GetSingleton();
42
43 // COM IUnknown
44 STDMETHODIMP_(ULONG) AddRef();
45 STDMETHODIMP QueryInterface(REFIID, void**);
46 STDMETHODIMP_(ULONG) Release();
47
48 // IAudioSessionEvents
49 STDMETHODIMP OnChannelVolumeChanged(DWORD aChannelCount,
50 float aChannelVolumeArray[],
51 DWORD aChangedChannel, LPCGUID aContext);
52 STDMETHODIMP OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext);
53 STDMETHODIMP OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext);
54 STDMETHODIMP OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext);
55 STDMETHODIMP OnSessionDisconnected(AudioSessionDisconnectReason aReason);
56
57 private:
58 nsresult OnSessionDisconnectedInternal();
59 nsresult CommitAudioSessionData();
60
61 public:
62 STDMETHODIMP OnSimpleVolumeChanged(float aVolume, BOOL aMute,
63 LPCGUID aContext);
64 STDMETHODIMP OnStateChanged(AudioSessionState aState);
65
66 nsresult Start();
67 nsresult Stop();
68 void StopInternal();
69
70 nsresult GetSessionData(nsID& aID, nsString& aSessionName,
71 nsString& aIconPath);
72
73 nsresult SetSessionData(const nsID& aID, const nsString& aSessionName,
74 const nsString& aIconPath);
75
76 enum SessionState {
77 UNINITIALIZED, // Has not been initialized yet
78 STARTED, // Started
79 CLONED, // SetSessionInfoCalled, Start not called
80 FAILED, // The audio session failed to start
81 STOPPED, // Stop called
82 AUDIO_SESSION_DISCONNECTED // Audio session disconnected
83 };
84
85 protected:
86 RefPtr<IAudioSessionControl> mAudioSessionControl;
87 nsString mDisplayName;
88 nsString mIconPath;
89 nsID mSessionGroupingParameter;
90 SessionState mState;
91 // Guards the IAudioSessionControl
92 mozilla::Mutex mMutex;
93
94 ThreadSafeAutoRefCnt mRefCnt;
95 NS_DECL_OWNINGTHREAD
96
97 static AudioSession* sService;
98 };
99
StartAudioSession()100 nsresult StartAudioSession() { return AudioSession::GetSingleton()->Start(); }
101
StopAudioSession()102 nsresult StopAudioSession() { return AudioSession::GetSingleton()->Stop(); }
103
GetAudioSessionData(nsID & aID,nsString & aSessionName,nsString & aIconPath)104 nsresult GetAudioSessionData(nsID& aID, nsString& aSessionName,
105 nsString& aIconPath) {
106 return AudioSession::GetSingleton()->GetSessionData(aID, aSessionName,
107 aIconPath);
108 }
109
RecvAudioSessionData(const nsID & aID,const nsString & aSessionName,const nsString & aIconPath)110 nsresult RecvAudioSessionData(const nsID& aID, const nsString& aSessionName,
111 const nsString& aIconPath) {
112 return AudioSession::GetSingleton()->SetSessionData(aID, aSessionName,
113 aIconPath);
114 }
115
116 AudioSession* AudioSession::sService = nullptr;
117
AudioSession()118 AudioSession::AudioSession() : mMutex("AudioSessionControl") {
119 mState = UNINITIALIZED;
120 }
121
~AudioSession()122 AudioSession::~AudioSession() {}
123
GetSingleton()124 AudioSession* AudioSession::GetSingleton() {
125 if (!(AudioSession::sService)) {
126 RefPtr<AudioSession> service = new AudioSession();
127 service.forget(&AudioSession::sService);
128 }
129
130 // We don't refcount AudioSession on the Gecko side, we hold one single ref
131 // as long as the appshell is running.
132 return AudioSession::sService;
133 }
134
135 // It appears Windows will use us on a background thread ...
136 NS_IMPL_ADDREF(AudioSession)
NS_IMPL_RELEASE(AudioSession)137 NS_IMPL_RELEASE(AudioSession)
138
139 STDMETHODIMP
140 AudioSession::QueryInterface(REFIID iid, void** ppv) {
141 const IID IID_IAudioSessionEvents = __uuidof(IAudioSessionEvents);
142 if ((IID_IUnknown == iid) || (IID_IAudioSessionEvents == iid)) {
143 *ppv = static_cast<IAudioSessionEvents*>(this);
144 AddRef();
145 return S_OK;
146 }
147
148 return E_NOINTERFACE;
149 }
150
151 // Once we are started Windows will hold a reference to us through our
152 // IAudioSessionEvents interface that will keep us alive until the appshell
153 // calls Stop.
Start()154 nsresult AudioSession::Start() {
155 MOZ_ASSERT(mState == UNINITIALIZED || mState == CLONED ||
156 mState == AUDIO_SESSION_DISCONNECTED,
157 "State invariants violated");
158
159 const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
160 const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
161 const IID IID_IAudioSessionManager = __uuidof(IAudioSessionManager);
162
163 HRESULT hr;
164
165 // There's a matching CoUninit in Stop() for this tied to a state of
166 // UNINITIALIZED.
167 hr = CoInitialize(nullptr);
168 MOZ_ASSERT(SUCCEEDED(hr),
169 "CoInitialize failure in audio session control, unexpected");
170
171 if (mState == UNINITIALIZED) {
172 mState = FAILED;
173
174 // Content processes should be CLONED
175 if (XRE_IsContentProcess()) {
176 return NS_ERROR_FAILURE;
177 }
178
179 MOZ_ASSERT(XRE_IsParentProcess(),
180 "Should only get here in a chrome process!");
181
182 nsCOMPtr<nsIStringBundleService> bundleService =
183 do_GetService(NS_STRINGBUNDLE_CONTRACTID);
184 NS_ENSURE_TRUE(bundleService, NS_ERROR_FAILURE);
185 nsCOMPtr<nsIStringBundle> bundle;
186 bundleService->CreateBundle("chrome://branding/locale/brand.properties",
187 getter_AddRefs(bundle));
188 NS_ENSURE_TRUE(bundle, NS_ERROR_FAILURE);
189
190 bundle->GetStringFromName("brandFullName", mDisplayName);
191
192 wchar_t* buffer;
193 mIconPath.GetMutableData(&buffer, MAX_PATH);
194 ::GetModuleFileNameW(nullptr, buffer, MAX_PATH);
195
196 nsresult rv = nsID::GenerateUUIDInPlace(mSessionGroupingParameter);
197 NS_ENSURE_SUCCESS(rv, rv);
198 }
199
200 mState = FAILED;
201
202 MOZ_ASSERT(!mDisplayName.IsEmpty() || !mIconPath.IsEmpty(),
203 "Should never happen ...");
204
205 RefPtr<IMMDeviceEnumerator> enumerator;
206 hr = ::CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL,
207 IID_IMMDeviceEnumerator, getter_AddRefs(enumerator));
208 if (FAILED(hr)) return NS_ERROR_NOT_AVAILABLE;
209
210 RefPtr<IMMDevice> device;
211 hr = enumerator->GetDefaultAudioEndpoint(
212 EDataFlow::eRender, ERole::eMultimedia, getter_AddRefs(device));
213 if (FAILED(hr)) {
214 if (hr == E_NOTFOUND) return NS_ERROR_NOT_AVAILABLE;
215 return NS_ERROR_FAILURE;
216 }
217
218 RefPtr<IAudioSessionManager> manager;
219 hr = device->Activate(IID_IAudioSessionManager, CLSCTX_ALL, nullptr,
220 getter_AddRefs(manager));
221 if (FAILED(hr)) {
222 return NS_ERROR_FAILURE;
223 }
224
225 MutexAutoLock lock(mMutex);
226 hr = manager->GetAudioSessionControl(&GUID_NULL, 0,
227 getter_AddRefs(mAudioSessionControl));
228
229 if (FAILED(hr)) {
230 return NS_ERROR_FAILURE;
231 }
232
233 // Increments refcount of 'this'.
234 hr = mAudioSessionControl->RegisterAudioSessionNotification(this);
235 if (FAILED(hr)) {
236 StopInternal();
237 return NS_ERROR_FAILURE;
238 }
239
240 nsCOMPtr<nsIRunnable> runnable =
241 NewRunnableMethod("AudioSession::CommitAudioSessionData", this,
242 &AudioSession::CommitAudioSessionData);
243 NS_DispatchToMainThread(runnable);
244
245 mState = STARTED;
246
247 return NS_OK;
248 }
249
SpawnASCReleaseThread(RefPtr<IAudioSessionControl> && aASC)250 void SpawnASCReleaseThread(RefPtr<IAudioSessionControl>&& aASC) {
251 // Fake moving to the other thread by circumventing the ref count.
252 // (RefPtrs don't play well with C++11 lambdas and we don't want to use
253 // XPCOM here.)
254 IAudioSessionControl* rawPtr = nullptr;
255 aASC.forget(&rawPtr);
256 MOZ_ASSERT(rawPtr);
257 PRThread* thread = PR_CreateThread(
258 PR_USER_THREAD,
259 [](void* aRawPtr) {
260 NS_SetCurrentThreadName("AudioASCReleaser");
261 static_cast<IAudioSessionControl*>(aRawPtr)->Release();
262 },
263 rawPtr, PR_PRIORITY_NORMAL, PR_LOCAL_THREAD, PR_UNJOINABLE_THREAD, 0);
264 if (!thread) {
265 // We can't make a thread so just destroy the IAudioSessionControl here.
266 rawPtr->Release();
267 }
268 }
269
StopInternal()270 void AudioSession::StopInternal() {
271 mMutex.AssertCurrentThreadOwns();
272
273 if (mAudioSessionControl && (mState == STARTED || mState == STOPPED)) {
274 // Decrement refcount of 'this'
275 mAudioSessionControl->UnregisterAudioSessionNotification(this);
276 }
277
278 if (mAudioSessionControl) {
279 // Avoid hanging when destroying AudioSessionControl. We do that by
280 // moving the AudioSessionControl to a worker thread (that we never
281 // 'join') for destruction.
282 SpawnASCReleaseThread(std::move(mAudioSessionControl));
283 }
284 }
285
Stop()286 nsresult AudioSession::Stop() {
287 MOZ_ASSERT(mState == STARTED || mState == UNINITIALIZED || // XXXremove this
288 mState == FAILED,
289 "State invariants violated");
290 SessionState state = mState;
291 mState = STOPPED;
292
293 {
294 RefPtr<AudioSession> kungFuDeathGrip;
295 kungFuDeathGrip.swap(sService);
296
297 MutexAutoLock lock(mMutex);
298 StopInternal();
299 }
300
301 if (state != UNINITIALIZED) {
302 ::CoUninitialize();
303 }
304 return NS_OK;
305 }
306
CopynsID(nsID & lhs,const nsID & rhs)307 void CopynsID(nsID& lhs, const nsID& rhs) {
308 lhs.m0 = rhs.m0;
309 lhs.m1 = rhs.m1;
310 lhs.m2 = rhs.m2;
311 for (int i = 0; i < 8; i++) {
312 lhs.m3[i] = rhs.m3[i];
313 }
314 }
315
GetSessionData(nsID & aID,nsString & aSessionName,nsString & aIconPath)316 nsresult AudioSession::GetSessionData(nsID& aID, nsString& aSessionName,
317 nsString& aIconPath) {
318 MOZ_ASSERT(mState == FAILED || mState == STARTED || mState == CLONED,
319 "State invariants violated");
320
321 CopynsID(aID, mSessionGroupingParameter);
322 aSessionName = mDisplayName;
323 aIconPath = mIconPath;
324
325 if (mState == FAILED) return NS_ERROR_FAILURE;
326
327 return NS_OK;
328 }
329
SetSessionData(const nsID & aID,const nsString & aSessionName,const nsString & aIconPath)330 nsresult AudioSession::SetSessionData(const nsID& aID,
331 const nsString& aSessionName,
332 const nsString& aIconPath) {
333 MOZ_ASSERT(mState == UNINITIALIZED, "State invariants violated");
334 MOZ_ASSERT(!XRE_IsParentProcess(),
335 "Should never get here in a chrome process!");
336 mState = CLONED;
337
338 CopynsID(mSessionGroupingParameter, aID);
339 mDisplayName = aSessionName;
340 mIconPath = aIconPath;
341 return NS_OK;
342 }
343
CommitAudioSessionData()344 nsresult AudioSession::CommitAudioSessionData() {
345 MutexAutoLock lock(mMutex);
346
347 if (!mAudioSessionControl) {
348 // Stop() was called before we had a chance to do this.
349 return NS_OK;
350 }
351
352 HRESULT hr = mAudioSessionControl->SetGroupingParam(
353 (LPGUID)&mSessionGroupingParameter, nullptr);
354 if (FAILED(hr)) {
355 StopInternal();
356 return NS_ERROR_FAILURE;
357 }
358
359 hr = mAudioSessionControl->SetDisplayName(mDisplayName.get(), nullptr);
360 if (FAILED(hr)) {
361 StopInternal();
362 return NS_ERROR_FAILURE;
363 }
364
365 hr = mAudioSessionControl->SetIconPath(mIconPath.get(), nullptr);
366 if (FAILED(hr)) {
367 StopInternal();
368 return NS_ERROR_FAILURE;
369 }
370
371 return NS_OK;
372 }
373
374 STDMETHODIMP
OnChannelVolumeChanged(DWORD aChannelCount,float aChannelVolumeArray[],DWORD aChangedChannel,LPCGUID aContext)375 AudioSession::OnChannelVolumeChanged(DWORD aChannelCount,
376 float aChannelVolumeArray[],
377 DWORD aChangedChannel, LPCGUID aContext) {
378 return S_OK; // NOOP
379 }
380
381 STDMETHODIMP
OnDisplayNameChanged(LPCWSTR aDisplayName,LPCGUID aContext)382 AudioSession::OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext) {
383 return S_OK; // NOOP
384 }
385
386 STDMETHODIMP
OnGroupingParamChanged(LPCGUID aGroupingParam,LPCGUID aContext)387 AudioSession::OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext) {
388 return S_OK; // NOOP
389 }
390
391 STDMETHODIMP
OnIconPathChanged(LPCWSTR aIconPath,LPCGUID aContext)392 AudioSession::OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext) {
393 return S_OK; // NOOP
394 }
395
396 STDMETHODIMP
OnSessionDisconnected(AudioSessionDisconnectReason aReason)397 AudioSession::OnSessionDisconnected(AudioSessionDisconnectReason aReason) {
398 // Run our code asynchronously. Per MSDN we can't do anything interesting
399 // in this callback.
400 nsCOMPtr<nsIRunnable> runnable =
401 NewRunnableMethod("widget::AudioSession::OnSessionDisconnectedInternal",
402 this, &AudioSession::OnSessionDisconnectedInternal);
403 NS_DispatchToMainThread(runnable);
404 return S_OK;
405 }
406
OnSessionDisconnectedInternal()407 nsresult AudioSession::OnSessionDisconnectedInternal() {
408 // When successful, UnregisterAudioSessionNotification will decrement the
409 // refcount of 'this'. Start will re-increment it. In the interim,
410 // we'll need to reference ourselves.
411 RefPtr<AudioSession> kungFuDeathGrip(this);
412
413 {
414 // We need to release the mutex before we call Start().
415 MutexAutoLock lock(mMutex);
416
417 if (!mAudioSessionControl) return NS_OK;
418
419 mAudioSessionControl->UnregisterAudioSessionNotification(this);
420 mAudioSessionControl = nullptr;
421 }
422
423 mState = AUDIO_SESSION_DISCONNECTED;
424 CoUninitialize();
425 Start(); // If it fails there's not much we can do.
426 return NS_OK;
427 }
428
429 STDMETHODIMP
OnSimpleVolumeChanged(float aVolume,BOOL aMute,LPCGUID aContext)430 AudioSession::OnSimpleVolumeChanged(float aVolume, BOOL aMute,
431 LPCGUID aContext) {
432 return S_OK; // NOOP
433 }
434
435 STDMETHODIMP
OnStateChanged(AudioSessionState aState)436 AudioSession::OnStateChanged(AudioSessionState aState) {
437 return S_OK; // NOOP
438 }
439
440 } // namespace widget
441 } // namespace mozilla
442