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