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 #include "nsIUUIDGenerator.h"
14
15 //#include "AudioSession.h"
16 #include "nsCOMPtr.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 nsCOMPtr<nsIUUIDGenerator> uuidgen =
197 do_GetService("@mozilla.org/uuid-generator;1");
198 NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
199 uuidgen->GenerateUUIDInPlace(&mSessionGroupingParameter);
200 }
201
202 mState = FAILED;
203
204 MOZ_ASSERT(!mDisplayName.IsEmpty() || !mIconPath.IsEmpty(),
205 "Should never happen ...");
206
207 RefPtr<IMMDeviceEnumerator> enumerator;
208 hr = ::CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL,
209 IID_IMMDeviceEnumerator, getter_AddRefs(enumerator));
210 if (FAILED(hr)) return NS_ERROR_NOT_AVAILABLE;
211
212 RefPtr<IMMDevice> device;
213 hr = enumerator->GetDefaultAudioEndpoint(
214 EDataFlow::eRender, ERole::eMultimedia, getter_AddRefs(device));
215 if (FAILED(hr)) {
216 if (hr == E_NOTFOUND) return NS_ERROR_NOT_AVAILABLE;
217 return NS_ERROR_FAILURE;
218 }
219
220 RefPtr<IAudioSessionManager> manager;
221 hr = device->Activate(IID_IAudioSessionManager, CLSCTX_ALL, nullptr,
222 getter_AddRefs(manager));
223 if (FAILED(hr)) {
224 return NS_ERROR_FAILURE;
225 }
226
227 MutexAutoLock lock(mMutex);
228 hr = manager->GetAudioSessionControl(&GUID_NULL, 0,
229 getter_AddRefs(mAudioSessionControl));
230
231 if (FAILED(hr)) {
232 return NS_ERROR_FAILURE;
233 }
234
235 // Increments refcount of 'this'.
236 hr = mAudioSessionControl->RegisterAudioSessionNotification(this);
237 if (FAILED(hr)) {
238 StopInternal();
239 return NS_ERROR_FAILURE;
240 }
241
242 nsCOMPtr<nsIRunnable> runnable =
243 NewRunnableMethod("AudioSession::CommitAudioSessionData", this,
244 &AudioSession::CommitAudioSessionData);
245 NS_DispatchToMainThread(runnable);
246
247 mState = STARTED;
248
249 return NS_OK;
250 }
251
SpawnASCReleaseThread(RefPtr<IAudioSessionControl> && aASC)252 void SpawnASCReleaseThread(RefPtr<IAudioSessionControl>&& aASC) {
253 // Fake moving to the other thread by circumventing the ref count.
254 // (RefPtrs don't play well with C++11 lambdas and we don't want to use
255 // XPCOM here.)
256 IAudioSessionControl* rawPtr = nullptr;
257 aASC.forget(&rawPtr);
258 MOZ_ASSERT(rawPtr);
259 PRThread* thread = PR_CreateThread(
260 PR_USER_THREAD,
261 [](void* aRawPtr) {
262 NS_SetCurrentThreadName("AudioASCReleaser");
263 static_cast<IAudioSessionControl*>(aRawPtr)->Release();
264 },
265 rawPtr, PR_PRIORITY_NORMAL, PR_LOCAL_THREAD, PR_UNJOINABLE_THREAD, 0);
266 if (!thread) {
267 // We can't make a thread so just destroy the IAudioSessionControl here.
268 rawPtr->Release();
269 }
270 }
271
StopInternal()272 void AudioSession::StopInternal() {
273 mMutex.AssertCurrentThreadOwns();
274
275 if (mAudioSessionControl && (mState == STARTED || mState == STOPPED)) {
276 // Decrement refcount of 'this'
277 mAudioSessionControl->UnregisterAudioSessionNotification(this);
278 }
279
280 if (mAudioSessionControl) {
281 // Avoid hanging when destroying AudioSessionControl. We do that by
282 // moving the AudioSessionControl to a worker thread (that we never
283 // 'join') for destruction.
284 SpawnASCReleaseThread(std::move(mAudioSessionControl));
285 }
286 }
287
Stop()288 nsresult AudioSession::Stop() {
289 MOZ_ASSERT(mState == STARTED || mState == UNINITIALIZED || // XXXremove this
290 mState == FAILED,
291 "State invariants violated");
292 SessionState state = mState;
293 mState = STOPPED;
294
295 {
296 RefPtr<AudioSession> kungFuDeathGrip;
297 kungFuDeathGrip.swap(sService);
298
299 MutexAutoLock lock(mMutex);
300 StopInternal();
301 }
302
303 if (state != UNINITIALIZED) {
304 ::CoUninitialize();
305 }
306 return NS_OK;
307 }
308
CopynsID(nsID & lhs,const nsID & rhs)309 void CopynsID(nsID& lhs, const nsID& rhs) {
310 lhs.m0 = rhs.m0;
311 lhs.m1 = rhs.m1;
312 lhs.m2 = rhs.m2;
313 for (int i = 0; i < 8; i++) {
314 lhs.m3[i] = rhs.m3[i];
315 }
316 }
317
GetSessionData(nsID & aID,nsString & aSessionName,nsString & aIconPath)318 nsresult AudioSession::GetSessionData(nsID& aID, nsString& aSessionName,
319 nsString& aIconPath) {
320 MOZ_ASSERT(mState == FAILED || mState == STARTED || mState == CLONED,
321 "State invariants violated");
322
323 CopynsID(aID, mSessionGroupingParameter);
324 aSessionName = mDisplayName;
325 aIconPath = mIconPath;
326
327 if (mState == FAILED) return NS_ERROR_FAILURE;
328
329 return NS_OK;
330 }
331
SetSessionData(const nsID & aID,const nsString & aSessionName,const nsString & aIconPath)332 nsresult AudioSession::SetSessionData(const nsID& aID,
333 const nsString& aSessionName,
334 const nsString& aIconPath) {
335 MOZ_ASSERT(mState == UNINITIALIZED, "State invariants violated");
336 MOZ_ASSERT(!XRE_IsParentProcess(),
337 "Should never get here in a chrome process!");
338 mState = CLONED;
339
340 CopynsID(mSessionGroupingParameter, aID);
341 mDisplayName = aSessionName;
342 mIconPath = aIconPath;
343 return NS_OK;
344 }
345
CommitAudioSessionData()346 nsresult AudioSession::CommitAudioSessionData() {
347 MutexAutoLock lock(mMutex);
348
349 if (!mAudioSessionControl) {
350 // Stop() was called before we had a chance to do this.
351 return NS_OK;
352 }
353
354 HRESULT hr = mAudioSessionControl->SetGroupingParam(
355 (LPGUID)&mSessionGroupingParameter, nullptr);
356 if (FAILED(hr)) {
357 StopInternal();
358 return NS_ERROR_FAILURE;
359 }
360
361 hr = mAudioSessionControl->SetDisplayName(mDisplayName.get(), nullptr);
362 if (FAILED(hr)) {
363 StopInternal();
364 return NS_ERROR_FAILURE;
365 }
366
367 hr = mAudioSessionControl->SetIconPath(mIconPath.get(), nullptr);
368 if (FAILED(hr)) {
369 StopInternal();
370 return NS_ERROR_FAILURE;
371 }
372
373 return NS_OK;
374 }
375
376 STDMETHODIMP
OnChannelVolumeChanged(DWORD aChannelCount,float aChannelVolumeArray[],DWORD aChangedChannel,LPCGUID aContext)377 AudioSession::OnChannelVolumeChanged(DWORD aChannelCount,
378 float aChannelVolumeArray[],
379 DWORD aChangedChannel, LPCGUID aContext) {
380 return S_OK; // NOOP
381 }
382
383 STDMETHODIMP
OnDisplayNameChanged(LPCWSTR aDisplayName,LPCGUID aContext)384 AudioSession::OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext) {
385 return S_OK; // NOOP
386 }
387
388 STDMETHODIMP
OnGroupingParamChanged(LPCGUID aGroupingParam,LPCGUID aContext)389 AudioSession::OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext) {
390 return S_OK; // NOOP
391 }
392
393 STDMETHODIMP
OnIconPathChanged(LPCWSTR aIconPath,LPCGUID aContext)394 AudioSession::OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext) {
395 return S_OK; // NOOP
396 }
397
398 STDMETHODIMP
OnSessionDisconnected(AudioSessionDisconnectReason aReason)399 AudioSession::OnSessionDisconnected(AudioSessionDisconnectReason aReason) {
400 // Run our code asynchronously. Per MSDN we can't do anything interesting
401 // in this callback.
402 nsCOMPtr<nsIRunnable> runnable =
403 NewRunnableMethod("widget::AudioSession::OnSessionDisconnectedInternal",
404 this, &AudioSession::OnSessionDisconnectedInternal);
405 NS_DispatchToMainThread(runnable);
406 return S_OK;
407 }
408
OnSessionDisconnectedInternal()409 nsresult AudioSession::OnSessionDisconnectedInternal() {
410 // When successful, UnregisterAudioSessionNotification will decrement the
411 // refcount of 'this'. Start will re-increment it. In the interim,
412 // we'll need to reference ourselves.
413 RefPtr<AudioSession> kungFuDeathGrip(this);
414
415 {
416 // We need to release the mutex before we call Start().
417 MutexAutoLock lock(mMutex);
418
419 if (!mAudioSessionControl) return NS_OK;
420
421 mAudioSessionControl->UnregisterAudioSessionNotification(this);
422 mAudioSessionControl = nullptr;
423 }
424
425 mState = AUDIO_SESSION_DISCONNECTED;
426 CoUninitialize();
427 Start(); // If it fails there's not much we can do.
428 return NS_OK;
429 }
430
431 STDMETHODIMP
OnSimpleVolumeChanged(float aVolume,BOOL aMute,LPCGUID aContext)432 AudioSession::OnSimpleVolumeChanged(float aVolume, BOOL aMute,
433 LPCGUID aContext) {
434 return S_OK; // NOOP
435 }
436
437 STDMETHODIMP
OnStateChanged(AudioSessionState aState)438 AudioSession::OnStateChanged(AudioSessionState aState) {
439 return S_OK; // NOOP
440 }
441
442 } // namespace widget
443 } // namespace mozilla
444