summaryrefslogtreecommitdiffstats
path: root/widget/windows/AudioSession.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'widget/windows/AudioSession.cpp')
-rw-r--r--widget/windows/AudioSession.cpp443
1 files changed, 443 insertions, 0 deletions
diff --git a/widget/windows/AudioSession.cpp b/widget/windows/AudioSession.cpp
new file mode 100644
index 0000000000..0975aa065e
--- /dev/null
+++ b/widget/windows/AudioSession.cpp
@@ -0,0 +1,443 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <windows.h>
+#include <audiopolicy.h>
+#include <mmdeviceapi.h>
+
+#include "mozilla/RefPtr.h"
+#include "nsIStringBundle.h"
+#include "nsIUUIDGenerator.h"
+
+//#include "AudioSession.h"
+#include "nsCOMPtr.h"
+#include "nsServiceManagerUtils.h"
+#include "nsString.h"
+#include "nsThreadUtils.h"
+#include "nsXULAppAPI.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/WindowsVersion.h"
+
+#include <objbase.h>
+
+namespace mozilla {
+namespace widget {
+
+/*
+ * To take advantage of what Vista+ have to offer with respect to audio,
+ * we need to maintain an audio session. This class wraps IAudioSessionControl
+ * and implements IAudioSessionEvents (for callbacks from Windows)
+ */
+class AudioSession final : public IAudioSessionEvents {
+ private:
+ AudioSession();
+ ~AudioSession();
+
+ public:
+ static AudioSession* GetSingleton();
+
+ // COM IUnknown
+ STDMETHODIMP_(ULONG) AddRef();
+ STDMETHODIMP QueryInterface(REFIID, void**);
+ STDMETHODIMP_(ULONG) Release();
+
+ // IAudioSessionEvents
+ STDMETHODIMP OnChannelVolumeChanged(DWORD aChannelCount,
+ float aChannelVolumeArray[],
+ DWORD aChangedChannel, LPCGUID aContext);
+ STDMETHODIMP OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext);
+ STDMETHODIMP OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext);
+ STDMETHODIMP OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext);
+ STDMETHODIMP OnSessionDisconnected(AudioSessionDisconnectReason aReason);
+
+ private:
+ nsresult OnSessionDisconnectedInternal();
+ nsresult CommitAudioSessionData();
+
+ public:
+ STDMETHODIMP OnSimpleVolumeChanged(float aVolume, BOOL aMute,
+ LPCGUID aContext);
+ STDMETHODIMP OnStateChanged(AudioSessionState aState);
+
+ nsresult Start();
+ nsresult Stop();
+ void StopInternal();
+
+ nsresult GetSessionData(nsID& aID, nsString& aSessionName,
+ nsString& aIconPath);
+
+ nsresult SetSessionData(const nsID& aID, const nsString& aSessionName,
+ const nsString& aIconPath);
+
+ enum SessionState {
+ UNINITIALIZED, // Has not been initialized yet
+ STARTED, // Started
+ CLONED, // SetSessionInfoCalled, Start not called
+ FAILED, // The audio session failed to start
+ STOPPED, // Stop called
+ AUDIO_SESSION_DISCONNECTED // Audio session disconnected
+ };
+
+ protected:
+ RefPtr<IAudioSessionControl> mAudioSessionControl;
+ nsString mDisplayName;
+ nsString mIconPath;
+ nsID mSessionGroupingParameter;
+ SessionState mState;
+ // Guards the IAudioSessionControl
+ mozilla::Mutex mMutex;
+
+ ThreadSafeAutoRefCnt mRefCnt;
+ NS_DECL_OWNINGTHREAD
+
+ static AudioSession* sService;
+};
+
+nsresult StartAudioSession() { return AudioSession::GetSingleton()->Start(); }
+
+nsresult StopAudioSession() { return AudioSession::GetSingleton()->Stop(); }
+
+nsresult GetAudioSessionData(nsID& aID, nsString& aSessionName,
+ nsString& aIconPath) {
+ return AudioSession::GetSingleton()->GetSessionData(aID, aSessionName,
+ aIconPath);
+}
+
+nsresult RecvAudioSessionData(const nsID& aID, const nsString& aSessionName,
+ const nsString& aIconPath) {
+ return AudioSession::GetSingleton()->SetSessionData(aID, aSessionName,
+ aIconPath);
+}
+
+AudioSession* AudioSession::sService = nullptr;
+
+AudioSession::AudioSession() : mMutex("AudioSessionControl") {
+ mState = UNINITIALIZED;
+}
+
+AudioSession::~AudioSession() {}
+
+AudioSession* AudioSession::GetSingleton() {
+ if (!(AudioSession::sService)) {
+ RefPtr<AudioSession> service = new AudioSession();
+ service.forget(&AudioSession::sService);
+ }
+
+ // We don't refcount AudioSession on the Gecko side, we hold one single ref
+ // as long as the appshell is running.
+ return AudioSession::sService;
+}
+
+// It appears Windows will use us on a background thread ...
+NS_IMPL_ADDREF(AudioSession)
+NS_IMPL_RELEASE(AudioSession)
+
+STDMETHODIMP
+AudioSession::QueryInterface(REFIID iid, void** ppv) {
+ const IID IID_IAudioSessionEvents = __uuidof(IAudioSessionEvents);
+ if ((IID_IUnknown == iid) || (IID_IAudioSessionEvents == iid)) {
+ *ppv = static_cast<IAudioSessionEvents*>(this);
+ AddRef();
+ return S_OK;
+ }
+
+ return E_NOINTERFACE;
+}
+
+// Once we are started Windows will hold a reference to us through our
+// IAudioSessionEvents interface that will keep us alive until the appshell
+// calls Stop.
+nsresult AudioSession::Start() {
+ MOZ_ASSERT(mState == UNINITIALIZED || mState == CLONED ||
+ mState == AUDIO_SESSION_DISCONNECTED,
+ "State invariants violated");
+
+ const CLSID CLSID_MMDeviceEnumerator = __uuidof(MMDeviceEnumerator);
+ const IID IID_IMMDeviceEnumerator = __uuidof(IMMDeviceEnumerator);
+ const IID IID_IAudioSessionManager = __uuidof(IAudioSessionManager);
+
+ HRESULT hr;
+
+ // There's a matching CoUninit in Stop() for this tied to a state of
+ // UNINITIALIZED.
+ hr = CoInitialize(nullptr);
+ MOZ_ASSERT(SUCCEEDED(hr),
+ "CoInitialize failure in audio session control, unexpected");
+
+ if (mState == UNINITIALIZED) {
+ mState = FAILED;
+
+ // Content processes should be CLONED
+ if (XRE_IsContentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Should only get here in a chrome process!");
+
+ nsCOMPtr<nsIStringBundleService> bundleService =
+ do_GetService(NS_STRINGBUNDLE_CONTRACTID);
+ NS_ENSURE_TRUE(bundleService, NS_ERROR_FAILURE);
+ nsCOMPtr<nsIStringBundle> bundle;
+ bundleService->CreateBundle("chrome://branding/locale/brand.properties",
+ getter_AddRefs(bundle));
+ NS_ENSURE_TRUE(bundle, NS_ERROR_FAILURE);
+
+ bundle->GetStringFromName("brandFullName", mDisplayName);
+
+ wchar_t* buffer;
+ mIconPath.GetMutableData(&buffer, MAX_PATH);
+ ::GetModuleFileNameW(nullptr, buffer, MAX_PATH);
+
+ nsCOMPtr<nsIUUIDGenerator> uuidgen =
+ do_GetService("@mozilla.org/uuid-generator;1");
+ NS_ENSURE_TRUE(uuidgen, NS_ERROR_FAILURE);
+ uuidgen->GenerateUUIDInPlace(&mSessionGroupingParameter);
+ }
+
+ mState = FAILED;
+
+ MOZ_ASSERT(!mDisplayName.IsEmpty() || !mIconPath.IsEmpty(),
+ "Should never happen ...");
+
+ RefPtr<IMMDeviceEnumerator> enumerator;
+ hr = ::CoCreateInstance(CLSID_MMDeviceEnumerator, nullptr, CLSCTX_ALL,
+ IID_IMMDeviceEnumerator, getter_AddRefs(enumerator));
+ if (FAILED(hr)) return NS_ERROR_NOT_AVAILABLE;
+
+ RefPtr<IMMDevice> device;
+ hr = enumerator->GetDefaultAudioEndpoint(
+ EDataFlow::eRender, ERole::eMultimedia, getter_AddRefs(device));
+ if (FAILED(hr)) {
+ if (hr == E_NOTFOUND) return NS_ERROR_NOT_AVAILABLE;
+ return NS_ERROR_FAILURE;
+ }
+
+ RefPtr<IAudioSessionManager> manager;
+ hr = device->Activate(IID_IAudioSessionManager, CLSCTX_ALL, nullptr,
+ getter_AddRefs(manager));
+ if (FAILED(hr)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ MutexAutoLock lock(mMutex);
+ hr = manager->GetAudioSessionControl(&GUID_NULL, 0,
+ getter_AddRefs(mAudioSessionControl));
+
+ if (FAILED(hr)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Increments refcount of 'this'.
+ hr = mAudioSessionControl->RegisterAudioSessionNotification(this);
+ if (FAILED(hr)) {
+ StopInternal();
+ return NS_ERROR_FAILURE;
+ }
+
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("AudioSession::CommitAudioSessionData", this,
+ &AudioSession::CommitAudioSessionData);
+ NS_DispatchToMainThread(runnable);
+
+ mState = STARTED;
+
+ return NS_OK;
+}
+
+void SpawnASCReleaseThread(RefPtr<IAudioSessionControl>&& aASC) {
+ // Fake moving to the other thread by circumventing the ref count.
+ // (RefPtrs don't play well with C++11 lambdas and we don't want to use
+ // XPCOM here.)
+ IAudioSessionControl* rawPtr = nullptr;
+ aASC.forget(&rawPtr);
+ MOZ_ASSERT(rawPtr);
+ PRThread* thread = PR_CreateThread(
+ PR_USER_THREAD,
+ [](void* aRawPtr) {
+ NS_SetCurrentThreadName("AudioASCReleaser");
+ static_cast<IAudioSessionControl*>(aRawPtr)->Release();
+ },
+ rawPtr, PR_PRIORITY_NORMAL, PR_LOCAL_THREAD, PR_UNJOINABLE_THREAD, 0);
+ if (!thread) {
+ // We can't make a thread so just destroy the IAudioSessionControl here.
+ rawPtr->Release();
+ }
+}
+
+void AudioSession::StopInternal() {
+ mMutex.AssertCurrentThreadOwns();
+
+ if (mAudioSessionControl && (mState == STARTED || mState == STOPPED)) {
+ // Decrement refcount of 'this'
+ mAudioSessionControl->UnregisterAudioSessionNotification(this);
+ }
+
+ if (mAudioSessionControl) {
+ // Avoid hanging when destroying AudioSessionControl. We do that by
+ // moving the AudioSessionControl to a worker thread (that we never
+ // 'join') for destruction.
+ SpawnASCReleaseThread(std::move(mAudioSessionControl));
+ }
+}
+
+nsresult AudioSession::Stop() {
+ MOZ_ASSERT(mState == STARTED || mState == UNINITIALIZED || // XXXremove this
+ mState == FAILED,
+ "State invariants violated");
+ SessionState state = mState;
+ mState = STOPPED;
+
+ {
+ RefPtr<AudioSession> kungFuDeathGrip;
+ kungFuDeathGrip.swap(sService);
+
+ MutexAutoLock lock(mMutex);
+ StopInternal();
+ }
+
+ if (state != UNINITIALIZED) {
+ ::CoUninitialize();
+ }
+ return NS_OK;
+}
+
+void CopynsID(nsID& lhs, const nsID& rhs) {
+ lhs.m0 = rhs.m0;
+ lhs.m1 = rhs.m1;
+ lhs.m2 = rhs.m2;
+ for (int i = 0; i < 8; i++) {
+ lhs.m3[i] = rhs.m3[i];
+ }
+}
+
+nsresult AudioSession::GetSessionData(nsID& aID, nsString& aSessionName,
+ nsString& aIconPath) {
+ MOZ_ASSERT(mState == FAILED || mState == STARTED || mState == CLONED,
+ "State invariants violated");
+
+ CopynsID(aID, mSessionGroupingParameter);
+ aSessionName = mDisplayName;
+ aIconPath = mIconPath;
+
+ if (mState == FAILED) return NS_ERROR_FAILURE;
+
+ return NS_OK;
+}
+
+nsresult AudioSession::SetSessionData(const nsID& aID,
+ const nsString& aSessionName,
+ const nsString& aIconPath) {
+ MOZ_ASSERT(mState == UNINITIALIZED, "State invariants violated");
+ MOZ_ASSERT(!XRE_IsParentProcess(),
+ "Should never get here in a chrome process!");
+ mState = CLONED;
+
+ CopynsID(mSessionGroupingParameter, aID);
+ mDisplayName = aSessionName;
+ mIconPath = aIconPath;
+ return NS_OK;
+}
+
+nsresult AudioSession::CommitAudioSessionData() {
+ MutexAutoLock lock(mMutex);
+
+ if (!mAudioSessionControl) {
+ // Stop() was called before we had a chance to do this.
+ return NS_OK;
+ }
+
+ HRESULT hr = mAudioSessionControl->SetGroupingParam(
+ (LPGUID)&mSessionGroupingParameter, nullptr);
+ if (FAILED(hr)) {
+ StopInternal();
+ return NS_ERROR_FAILURE;
+ }
+
+ hr = mAudioSessionControl->SetDisplayName(mDisplayName.get(), nullptr);
+ if (FAILED(hr)) {
+ StopInternal();
+ return NS_ERROR_FAILURE;
+ }
+
+ hr = mAudioSessionControl->SetIconPath(mIconPath.get(), nullptr);
+ if (FAILED(hr)) {
+ StopInternal();
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+STDMETHODIMP
+AudioSession::OnChannelVolumeChanged(DWORD aChannelCount,
+ float aChannelVolumeArray[],
+ DWORD aChangedChannel, LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnDisplayNameChanged(LPCWSTR aDisplayName, LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnGroupingParamChanged(LPCGUID aGroupingParam, LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnIconPathChanged(LPCWSTR aIconPath, LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnSessionDisconnected(AudioSessionDisconnectReason aReason) {
+ // Run our code asynchronously. Per MSDN we can't do anything interesting
+ // in this callback.
+ nsCOMPtr<nsIRunnable> runnable =
+ NewRunnableMethod("widget::AudioSession::OnSessionDisconnectedInternal",
+ this, &AudioSession::OnSessionDisconnectedInternal);
+ NS_DispatchToMainThread(runnable);
+ return S_OK;
+}
+
+nsresult AudioSession::OnSessionDisconnectedInternal() {
+ // When successful, UnregisterAudioSessionNotification will decrement the
+ // refcount of 'this'. Start will re-increment it. In the interim,
+ // we'll need to reference ourselves.
+ RefPtr<AudioSession> kungFuDeathGrip(this);
+
+ {
+ // We need to release the mutex before we call Start().
+ MutexAutoLock lock(mMutex);
+
+ if (!mAudioSessionControl) return NS_OK;
+
+ mAudioSessionControl->UnregisterAudioSessionNotification(this);
+ mAudioSessionControl = nullptr;
+ }
+
+ mState = AUDIO_SESSION_DISCONNECTED;
+ CoUninitialize();
+ Start(); // If it fails there's not much we can do.
+ return NS_OK;
+}
+
+STDMETHODIMP
+AudioSession::OnSimpleVolumeChanged(float aVolume, BOOL aMute,
+ LPCGUID aContext) {
+ return S_OK; // NOOP
+}
+
+STDMETHODIMP
+AudioSession::OnStateChanged(AudioSessionState aState) {
+ return S_OK; // NOOP
+}
+
+} // namespace widget
+} // namespace mozilla