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