Bug 1085356 - Better handling of OSX audio output devices switching when SourceMediaStream are present in the MSG. r=jesup a=lmandel
authorPaul Adenot <paul@paul.cx>
Wed, 22 Oct 2014 16:12:29 +0200
changeset 225911 80b1fc2042df
parent 225910 1ca39da5df9d
child 225912 ddc951a77894
push id4064
push userrjesup@wgate.com
push date2014-11-03 01:27 +0000
treeherdermozilla-beta@ddc951a77894 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjesup, lmandel
bugs1085356
milestone34.0
Bug 1085356 - Better handling of OSX audio output devices switching when SourceMediaStream are present in the MSG. r=jesup a=lmandel On OSX, when the audio output device changes, the OS will call the audio callbacks in weird patterns, if at all, during a period of ~1s. If real-time SourceMediaStreams are present in the MediaStreamGraph, this means buffering will occur, and the overall latency between the MediaStreamGraph insertion time, and the actual output time will grow. To fix this, we detect when the output device changes, and we switch temporarily to a SystemClockDriver, that will pull from the SourceMediaStream, and simply discard all input data. Then, when we get audio callbacks called reliably (basically, when OSX is done switching to the other output), we switch back to the previous AudioCallbackDriver. We keep the previous AudioCallbackDriver alive using a self-reference. If an AudioCallbackDriver has a self-reference, that means it's in a state when a device is switching, so it's not linked to an MSG per se.
content/media/GraphDriver.cpp
content/media/GraphDriver.h
content/media/SelfRef.h
content/media/moz.build
content/media/webaudio/AudioNode.h
--- a/content/media/GraphDriver.cpp
+++ b/content/media/GraphDriver.cpp
@@ -75,17 +75,28 @@ void GraphDriver::SetGraphTime(GraphDriv
 
   STREAM_LOG(PR_LOG_DEBUG, ("Setting previous driver: %p (%s)", aPreviousDriver, aPreviousDriver->AsAudioCallbackDriver() ? "AudioCallbackDriver" : "SystemClockDriver"));
   MOZ_ASSERT(!mPreviousDriver);
   mPreviousDriver = aPreviousDriver;
 }
 
 void GraphDriver::SwitchAtNextIteration(GraphDriver* aNextDriver)
 {
-
+  // This is the situation where `mPreviousDriver` is an AudioCallbackDriver
+  // that is switching device, and the graph has found the current driver is not
+  // an AudioCallbackDriver, but tries to switch to a _new_ AudioCallbackDriver
+  // because it found audio has to be output. In this case, simply ignore the
+  // request to switch, since we know we will switch back to the old
+  // AudioCallbackDriver when it has recovered from the device switching.
+  if (aNextDriver->AsAudioCallbackDriver() &&
+      mPreviousDriver &&
+      mPreviousDriver->AsAudioCallbackDriver()->IsSwitchingDevice() &&
+      mPreviousDriver != aNextDriver) {
+    return;
+  }
   LIFECYCLE_LOG("Switching to new driver: %p (%s)",
       aNextDriver, aNextDriver->AsAudioCallbackDriver() ?
       "AudioCallbackDriver" : "SystemClockDriver");
   // Sometimes we switch twice to a new driver per iteration, this is probably a
   // bug.
   MOZ_ASSERT(!mNextDriver || mNextDriver->AsAudioCallbackDriver());
   mNextDriver = aNextDriver;
 }
@@ -183,21 +194,25 @@ public:
     LIFECYCLE_LOG("Starting a new system driver for graph %p\n",
                   mDriver->mGraphImpl);
     if (mDriver->mPreviousDriver) {
       LIFECYCLE_LOG("%p releasing an AudioCallbackDriver(%p), for graph %p\n",
                     mDriver,
                     mDriver->mPreviousDriver.get(),
                     mDriver->GraphImpl());
       MOZ_ASSERT(!mDriver->AsAudioCallbackDriver());
-      // Stop and release the previous driver off-main-thread.
-      nsRefPtr<AsyncCubebTask> releaseEvent =
-        new AsyncCubebTask(mDriver->mPreviousDriver->AsAudioCallbackDriver(), AsyncCubebTask::SHUTDOWN);
-      mDriver->mPreviousDriver = nullptr;
-      releaseEvent->Dispatch();
+      // Stop and release the previous driver off-main-thread, but only if we're
+      // not in the situation where we've fallen back to a system clock driver
+      // because the osx audio stack is currently switching output device.
+      if (!mDriver->mPreviousDriver->AsAudioCallbackDriver()->IsSwitchingDevice()) {
+        nsRefPtr<AsyncCubebTask> releaseEvent =
+          new AsyncCubebTask(mDriver->mPreviousDriver->AsAudioCallbackDriver(), AsyncCubebTask::SHUTDOWN);
+        mDriver->mPreviousDriver = nullptr;
+        releaseEvent->Dispatch();
+      }
     } else {
       MonitorAutoLock mon(mDriver->mGraphImpl->GetMonitor());
       MOZ_ASSERT(mDriver->mGraphImpl->MessagesQueued(), "Don't start a graph without messages queued.");
       mDriver->mGraphImpl->SwapMessageQueues();
     }
     mDriver->RunThread();
     return NS_OK;
   }
@@ -530,16 +545,19 @@ AsyncCubebTask::Run()
 }
 
 AudioCallbackDriver::AudioCallbackDriver(MediaStreamGraphImpl* aGraphImpl, dom::AudioChannel aChannel)
   : GraphDriver(aGraphImpl)
   , mStarted(false)
   , mAudioChannel(aChannel)
   , mInCallback(false)
   , mPauseRequested(false)
+#ifdef XP_MACOSX
+  , mCallbackReceivedWhileSwitching(0)
+#endif
 {
   STREAM_LOG(PR_LOG_DEBUG, ("AudioCallbackDriver ctor for graph %p", aGraphImpl));
 }
 
 AudioCallbackDriver::~AudioCallbackDriver()
 {}
 
 void
@@ -762,26 +780,65 @@ AudioCallbackDriver::AutoInCallback::Aut
 {
   mDriver->mInCallback = true;
 }
 
 AudioCallbackDriver::AutoInCallback::~AutoInCallback() {
   mDriver->mInCallback = false;
 }
 
+#ifdef XP_MACOSX
+bool
+AudioCallbackDriver::OSXDeviceSwitchingWorkaround()
+{
+  MonitorAutoLock mon(GraphImpl()->GetMonitor());
+  if (mSelfReference) {
+    // Apparently, depending on the osx version, on device switch, the
+    // callback is called "some" number of times, and then stops being called,
+    // and then gets called again. 10 is to be safe, it's a low-enough number
+    // of milliseconds anyways (< 100ms)
+    if (mCallbackReceivedWhileSwitching++ >= 10) {
+      // If we have a self reference, we have fallen back temporarily on a
+      // system clock driver, but we just got called back, that means the osx
+      // audio backend has switched to the new device.
+      // Ask the graph to switch back to the previous AudioCallbackDriver
+      // (`this`), and when the graph has effectively switched, we can drop
+      // the self reference and unref the SystemClockDriver we fallen back on.
+      if (GraphImpl()->CurrentDriver() == this) {
+        mSelfReference.Drop(this);
+        mNextDriver = nullptr;
+      } else {
+        GraphImpl()->CurrentDriver()->SwitchAtNextIteration(this);
+      }
+
+    }
+    return true;
+  }
+
+  return false;
+}
+#endif // XP_MACOSX
+
 long
 AudioCallbackDriver::DataCallback(AudioDataValue* aBuffer, long aFrames)
 {
   bool stillProcessing;
 
   if (mPauseRequested) {
     PodZero(aBuffer, aFrames * mGraphImpl->AudioChannelCount());
     return aFrames;
   }
 
+#ifdef XP_MACOSX
+  if (OSXDeviceSwitchingWorkaround()) {
+    PodZero(aBuffer, aFrames * mGraphImpl->AudioChannelCount());
+    return aFrames;
+  }
+#endif
+
 #ifdef DEBUG
   // DebugOnly<> doesn't work here... it forces an initialization that will cause
   // mInCallback to be set back to false before we exit the statement.  Do it by
   // hand instead.
   AutoInCallback aic(this);
 #endif
 
   if (mStateComputedTime == 0) {
@@ -952,16 +1009,41 @@ void AudioCallbackDriver::PanOutputIfNee
   }
 #endif
 }
 
 void
 AudioCallbackDriver::DeviceChangedCallback() {
   MonitorAutoLock mon(mGraphImpl->GetMonitor());
   PanOutputIfNeeded(mMicrophoneActive);
+  // On OSX, changing the output device causes the audio thread to no call the
+  // audio callback, so we're unable to process real-time input data, and this
+  // results in latency building up.
+  // We switch to a system driver until audio callbacks are called again, so we
+  // still pull from the input stream, so that everything works apart from the
+  // audio output.
+#ifdef XP_MACOSX
+  // Don't bother doing the device switching dance if the graph is not RUNNING
+  // (starting up, shutting down), because we haven't started pulling from the
+  // SourceMediaStream.
+  if (!GraphImpl()->Running()) {
+    return;
+  }
+
+  if (mSelfReference) {
+    return;
+  }
+  mSelfReference.Take(this);
+  mCallbackReceivedWhileSwitching = 0;
+  mNextDriver = new SystemClockDriver(GraphImpl());
+  mNextDriver->SetGraphTime(this, mIterationStart, mIterationEnd,
+                            mStateComputedTime, mNextStateComputedTime);
+  mGraphImpl->SetCurrentDriver(mNextDriver);
+  mNextDriver->Start();
+#endif
 }
 
 void
 AudioCallbackDriver::SetMicrophoneActive(bool aActive)
 {
   MonitorAutoLock mon(mGraphImpl->GetMonitor());
 
   mMicrophoneActive = aActive;
--- a/content/media/GraphDriver.h
+++ b/content/media/GraphDriver.h
@@ -6,16 +6,17 @@
 #ifndef GRAPHDRIVER_H_
 #define GRAPHDRIVER_H_
 
 #include "nsAutoPtr.h"
 #include "nsAutoRef.h"
 #include "AudioBufferUtils.h"
 #include "AudioMixer.h"
 #include "AudioSegment.h"
+#include "SelfRef.h"
 #include "mozilla/Atomics.h"
 
 struct cubeb_stream;
 
 namespace mozilla {
 
 
 /**
@@ -380,16 +381,24 @@ public:
                              uint32_t aChannels,
                              uint32_t aFrames,
                              uint32_t aSampleRate) MOZ_OVERRIDE;
 
   virtual AudioCallbackDriver* AsAudioCallbackDriver() {
     return this;
   }
 
+  bool IsSwitchingDevice() {
+#ifdef XP_MACOSX
+    return mSelfReference;
+#else
+    return false;
+#endif
+  }
+
   /**
    * Whether the audio callback is processing. This is for asserting only.
    */
   bool InCallback();
 
   virtual bool OnThread() MOZ_OVERRIDE { return !mStarted || InCallback(); }
 
   /* Whether the underlying cubeb stream has been started. See comment for
@@ -461,16 +470,28 @@ private:
   /* A thread has been created to be able to pause and restart the audio thread,
    * but has not done so yet. This indicates that the callback should return
    * early */
   bool mPauseRequested;
   /**
    * True if microphone is being used by this process. This is synchronized by
    * the graph's monitor. */
   bool mMicrophoneActive;
+
+#ifdef XP_MACOSX
+  /* Implements the workaround for the osx audio stack when changing output
+   * devices. See comments in .cpp */
+  bool OSXDeviceSwitchingWorkaround();
+  /* Self-reference that keep this driver alive when switching output audio
+   * device and making the graph running temporarily off a SystemClockDriver.  */
+  SelfReference<AudioCallbackDriver> mSelfReference;
+  /* While switching devices, we keep track of the number of callbacks received,
+   * since OSX seems to still call us _sometimes_. */
+  uint32_t mCallbackReceivedWhileSwitching;
+#endif
 };
 
 class AsyncCubebTask : public nsRunnable
 {
 public:
   enum AsyncCubebOperation {
     INIT,
     SHUTDOWN,
new file mode 100644
--- /dev/null
+++ b/content/media/SelfRef.h
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* 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/. */
+
+#ifndef SELF_REF_H
+#define SELF_REF_H
+
+#include "mozilla/Attributes.h"
+
+namespace mozilla {
+
+template<class T>
+class SelfReference {
+public:
+  SelfReference() : mHeld(false) {}
+  ~SelfReference()
+  {
+    NS_ASSERTION(!mHeld, "Forgot to drop the self reference?");
+  }
+
+  void Take(T* t)
+  {
+    if (!mHeld) {
+      mHeld = true;
+      t->AddRef();
+    }
+  }
+  void Drop(T* t)
+  {
+    if (mHeld) {
+      mHeld = false;
+      t->Release();
+    }
+  }
+
+  operator bool() const { return mHeld; }
+
+  SelfReference(const SelfReference& aOther) MOZ_DELETE;
+  void operator=(const SelfReference& aOther) MOZ_DELETE;
+private:
+  bool mHeld;
+};
+} // mozilla
+
+#endif // SELF_REF_H
--- a/content/media/moz.build
+++ b/content/media/moz.build
@@ -93,16 +93,17 @@ EXPORTS += [
     'MediaSegment.h',
     'MediaStreamGraph.h',
     'MediaTaskQueue.h',
     'MediaTrack.h',
     'MediaTrackList.h',
     'MP3FrameParser.h',
     'nsIDocumentActivity.h',
     'RtspMediaResource.h',
+    'SelfRef.h',
     'SharedBuffer.h',
     'SharedThreadPool.h',
     'StreamBuffer.h',
     'ThreadPoolCOMListener.h',
     'TimeVarying.h',
     'TrackUnionStream.h',
     'VideoFrameContainer.h',
     'VideoSegment.h',
--- a/content/media/webaudio/AudioNode.h
+++ b/content/media/webaudio/AudioNode.h
@@ -12,57 +12,28 @@
 #include "nsCycleCollectionParticipant.h"
 #include "nsAutoPtr.h"
 #include "nsTArray.h"
 #include "AudioContext.h"
 #include "MediaStreamGraph.h"
 #include "WebAudioUtils.h"
 #include "mozilla/MemoryReporting.h"
 #include "nsWeakReference.h"
+#include "SelfRef.h"
 
 namespace mozilla {
 
 namespace dom {
 
 class AudioContext;
 class AudioBufferSourceNode;
 class AudioParam;
 class AudioParamTimeline;
 struct ThreeDPoint;
 
-template<class T>
-class SelfReference {
-public:
-  SelfReference() : mHeld(false) {}
-  ~SelfReference()
-  {
-    NS_ASSERTION(!mHeld, "Forgot to drop the self reference?");
-  }
-
-  void Take(T* t)
-  {
-    if (!mHeld) {
-      mHeld = true;
-      t->AddRef();
-    }
-  }
-  void Drop(T* t)
-  {
-    if (mHeld) {
-      mHeld = false;
-      t->Release();
-    }
-  }
-
-  operator bool() const { return mHeld; }
-
-private:
-  bool mHeld;
-};
-
 /**
  * The DOM object representing a Web Audio AudioNode.
  *
  * Each AudioNode has a MediaStream representing the actual
  * real-time processing and output of this AudioNode.
  *
  * We track the incoming and outgoing connections to other AudioNodes.
  * Outgoing connections have strong ownership.  Also, AudioNodes that will