From ff777a046912bda6fc6e65811f49851ba57e028f Mon Sep 17 00:00:00 2001
From: Lyubomir Marinov <lyubomir.marinov@jitsi.org>
Date: Mon, 17 Jun 2013 11:34:11 +0300
Subject: [PATCH] Attempts to unblock the media flow/codec chain when a Windows
 Audio Session API (WASAPI) render device appears to be
 malfunctioning/blocked.

---
 .../jmfext/media/protocol/wasapi/WASAPI.java  |  39 ++++++
 .../media/protocol/wasapi/WASAPIStream.java   |   7 +-
 .../renderer/audio/PortAudioRenderer.java     |  30 ++--
 .../media/renderer/audio/WASAPIRenderer.java  | 129 +++++++++++++++++-
 4 files changed, 181 insertions(+), 24 deletions(-)

diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPI.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPI.java
index cab87859..a947a9c0 100644
--- a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPI.java
+++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPI.java
@@ -76,12 +76,34 @@ public class WASAPI
 
     public static final int STGM_READ = 0x0;
 
+    /**
+     * The return value of {@link #WaitForSingleObject(long, long)} which
+     * indicates that the specified object is a mutex that was not released by
+     * the thread that owned the mutex before the owning thread terminated.
+     * Ownership of the mutex is granted to the calling thread and the mutex
+     * state is set to non-signaled.
+     */
     public static final int WAIT_ABANDONED = 0x00000080;
 
+    /**
+     * The return value of {@link #WaitForSingleObject(long, long)} which
+     * indicates that the function has failed. Normally, the function will throw
+     * an {@link HResultException} in the case and
+     * {@link HResultException#getHResult()} will return <tt>WAIT_FAILED</tt>.
+     */
     public static final int WAIT_FAILED = 0xffffffff;
 
+    /**
+     * The return value of {@link #WaitForSingleObject(long, long)} which
+     * indicates that the specified object is signaled.
+     */
     public static final int WAIT_OBJECT_0 = 0x00000000;
 
+    /**
+     * The return value of {@link #WaitForSingleObject(long, long)} which
+     * indicates that the specified time-out interval has elapsed and the state
+     * of the specified object is non-signaled.
+     */
     public static final int WAIT_TIMEOUT = 0x00000102;
 
     public static final char WAVE_FORMAT_PCM = 1;
@@ -302,6 +324,23 @@ public static native long PSPropertyKeyFromString(String pszString)
     public static native void ResetEvent(long hEvent)
         throws HResultException;
 
+    /**
+     * Waits until the specified object is in the signaled state or the
+     * specified time-out interval elapses.
+     *
+     * @param hHandle a <tt>HANDLE</tt> to the object to wait for
+     * @param dwMilliseconds the time-out interval in milliseconds to wait. If a
+     * nonzero value is specified, the function waits until the specified object
+     * is signaled or the specified time-out interval elapses. If
+     * <tt>dwMilliseconds</tt> is zero, the function does not enter a wait state
+     * if the specified object is not signaled; it always returns immediately.
+     * If <tt>dwMilliseconds</tt> is <tt>INFINITE</tt>, the function will return
+     * only when the specified object is signaled.
+     * @return one of the <tt>WAIT_XXX</tt> constant values defined by the
+     * <tt>WASAPI</tt> class to indicate the event that caused the function to
+     * return
+     * @throws HResultException if the return value is {@link #WAIT_FAILED}
+     */
     public static native int WaitForSingleObject(
             long hHandle,
             long dwMilliseconds)
diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPIStream.java b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPIStream.java
index 8f995a82..3321155b 100644
--- a/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPIStream.java
+++ b/src/org/jitsi/impl/neomedia/jmfext/media/protocol/wasapi/WASAPIStream.java
@@ -816,6 +816,11 @@ private void runInEventHandleCmd(Runnable eventHandleCmd)
                 }
                 catch (HResultException hre)
                 {
+                    /*
+                     * WaitForSingleObject will throw HResultException only in
+                     * the case of WAIT_FAILED. Event if it didn't, it would
+                     * still be a failure from our point of view.
+                     */
                     wfso = WAIT_FAILED;
                     logger.error("WaitForSingleObject", hre);
                 }
@@ -823,7 +828,7 @@ private void runInEventHandleCmd(Runnable eventHandleCmd)
                  * If the function WaitForSingleObject fails once, it will very
                  * likely fail forever. Bail out of a possible busy wait.
                  */
-                if (wfso == WAIT_FAILED)
+                if ((wfso == WAIT_FAILED) || (wfso == WAIT_ABANDONED))
                     break;
             }
             while (true);
diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/PortAudioRenderer.java b/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/PortAudioRenderer.java
index f0a6126a..18f2d08a 100644
--- a/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/PortAudioRenderer.java
+++ b/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/PortAudioRenderer.java
@@ -60,12 +60,6 @@ public class PortAudioRenderer
      */
     private static final byte FLAG_STARTED = 2;
 
-    /**
-     * The constant which expresses a non-existent time in milliseconds for the
-     * purposes of {@link #writeIsMalfunctioningSince}.
-     */
-    private static final long NEVER = DiagnosticsControl.NEVER;
-
     /**
      * The human-readable name of the <tt>PortAudioRenderer</tt> JMF plug-in.
      */
@@ -311,7 +305,7 @@ public void willPaUpdateAvailableDeviceList()
      * <tt>paTimedOut</tt> and/or Windows Multimedia reporting
      * <tt>MMSYSERR_NODRIVER</tt> (may) indicate abnormal functioning.
      */
-    private long writeIsMalfunctioningSince = -1;
+    private long writeIsMalfunctioningSince = DiagnosticsControl.NEVER;
 
     /**
      * Initializes a new <tt>PortAudioRenderer</tt> instance.
@@ -366,7 +360,7 @@ public synchronized void close()
                     started = false;
                     flags &= ~(FLAG_OPEN | FLAG_STARTED);
 
-                    if (writeIsMalfunctioningSince != NEVER)
+                    if (writeIsMalfunctioningSince != DiagnosticsControl.NEVER)
                         setWriteIsMalfunctioning(false);
                 }
                 catch (PortAudioException paex)
@@ -668,7 +662,7 @@ private void doOpen()
                     * framesPerBuffer;
 
             // Pa_WriteStream has not been invoked yet.
-            if (writeIsMalfunctioningSince != NEVER)
+            if (writeIsMalfunctioningSince != DiagnosticsControl.NEVER)
                 setWriteIsMalfunctioning(false);
         }
     }
@@ -743,7 +737,7 @@ public int process(Buffer buffer)
                  * The execution is somewhat abnormal but it is not because of a
                  * malfunction in Pa_WriteStream.
                  */
-                if (writeIsMalfunctioningSince != NEVER)
+                if (writeIsMalfunctioningSince != DiagnosticsControl.NEVER)
                     setWriteIsMalfunctioning(false);
 
                 return BUFFER_PROCESSED_OK;
@@ -788,14 +782,14 @@ public int process(Buffer buffer)
                 if (errorCode == Pa.paNoError)
                 {
                     // Pa_WriteStream appears to function normally.
-                    if (writeIsMalfunctioningSince != NEVER)
+                    if (writeIsMalfunctioningSince != DiagnosticsControl.NEVER)
                         setWriteIsMalfunctioning(false);
                 }
                 else if ((Pa.paTimedOut == errorCode)
                         || (Pa.HostApiTypeId.paMME.equals(hostApiType)
                                 && (Pa.MMSYSERR_NODRIVER == errorCode)))
                 {
-                    if (writeIsMalfunctioningSince == NEVER)
+                    if (writeIsMalfunctioningSince == DiagnosticsControl.NEVER)
                         setWriteIsMalfunctioning(true);
                     yield = true;
                 }
@@ -899,21 +893,21 @@ public void setLocator(MediaLocator locator)
     /**
      * Indicates whether <tt>Pa_WriteStream</tt> is malfunctioning.
      *
-     * @param malfunctioning <tt>true</tt> if <tt>Pa_WriteStream</tt> is
+     * @param writeIsMalfunctioning <tt>true</tt> if <tt>Pa_WriteStream</tt> is
      * malfunctioning; otherwise, <tt>false</tt>
      */
-    private void setWriteIsMalfunctioning(boolean malfunctioning)
+    private void setWriteIsMalfunctioning(boolean writeIsMalfunctioning)
     {
-        if (malfunctioning)
+        if (writeIsMalfunctioning)
         {
-            if (writeIsMalfunctioningSince == NEVER)
+            if (writeIsMalfunctioningSince == DiagnosticsControl.NEVER)
             {
                 writeIsMalfunctioningSince = System.currentTimeMillis();
                 PortAudioSystem.monitorFunctionalHealth(diagnosticsControl);
             }
         }
         else
-            writeIsMalfunctioningSince = NEVER;
+            writeIsMalfunctioningSince = DiagnosticsControl.NEVER;
     }
 
     /**
@@ -953,7 +947,7 @@ public synchronized void stop()
                 flags &= ~FLAG_STARTED;
 
                 bufferLeft = null;
-                if (writeIsMalfunctioningSince != NEVER)
+                if (writeIsMalfunctioningSince != DiagnosticsControl.NEVER)
                     setWriteIsMalfunctioning(false);
             }
             catch (PortAudioException paex)
diff --git a/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/WASAPIRenderer.java b/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/WASAPIRenderer.java
index 3b6894c6..de3dd9ef 100644
--- a/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/WASAPIRenderer.java
+++ b/src/org/jitsi/impl/neomedia/jmfext/media/renderer/audio/WASAPIRenderer.java
@@ -16,6 +16,7 @@
 import javax.media.*;
 import javax.media.format.*;
 
+import org.jitsi.impl.neomedia.control.*;
 import org.jitsi.impl.neomedia.device.*;
 import org.jitsi.impl.neomedia.jmfext.media.protocol.wasapi.*;
 import org.jitsi.service.neomedia.*;
@@ -50,6 +51,11 @@ public class WASAPIRenderer
     private static final String PLUGIN_NAME
         = "Windows Audio Session API (WASAPI) Renderer";
 
+    /**
+     * The duration in milliseconds of the endpoint buffer.
+     */
+    private long bufferDuration;
+
     /**
      * The indicator which determines whether the audio stream represented by
      * this instance, {@link #iAudioClient} and {@link #iAudioRenderClient} is
@@ -162,6 +168,23 @@ public class WASAPIRenderer
      */
     private boolean started;
 
+    /**
+     * The time in milliseconds at which the writing to the render endpoint
+     * buffer has started malfunctioning. For example, {@link #remainder} being
+     * full from the point of view of {@link #process(Buffer)} for an extended
+     * period of time may indicate abnormal functioning.
+     */
+    private long writeIsMalfunctioningSince = DiagnosticsControl.NEVER;
+
+    /**
+     * The maximum interval of time in milliseconds that the writing to the
+     * render endpoint buffer is allowed to be under suspicion that it is
+     * malfunctioning. If it remains under suspicion after the maximum interval
+     * of time has elapsed, the writing to the render endpoing buffer is to be
+     * considered malfunctioning for real. 
+     */
+    private long writeIsMalfunctioningTimeout;
+
     /**
      * Initializes a new <tt>WASAPIRenderer</tt> instance which is to perform
      * playback (as opposed to sound a notification).
@@ -352,6 +375,7 @@ public synchronized void open()
 
                         int sampleRate = (int) format.getSampleRate();
 
+                        bufferDuration = numBufferFrames * 1000L / sampleRate;
                         /*
                          * We will very likely be inefficient if we fail to
                          * synchronize with the scheduling period of the audio
@@ -359,9 +383,6 @@ public synchronized void open()
                          */
                         if (devicePeriod <= 1)
                         {
-                            long bufferDuration
-                                = numBufferFrames * 1000L / sampleRate;
-
                             devicePeriod = bufferDuration / 2;
                             if ((devicePeriod
                                         > WASAPISystem.DEFAULT_DEVICE_PERIOD)
@@ -388,6 +409,10 @@ public synchronized void open()
                          */
                         remainderLength = remainder.length;
 
+                        writeIsMalfunctioningSince = DiagnosticsControl.NEVER;
+                        writeIsMalfunctioningTimeout
+                            = 2 * Math.max(bufferDuration, devicePeriod);
+
                         this.eventHandle = eventHandle;
                         eventHandle = 0;
                         this.iAudioClient = iAudioClient;
@@ -572,6 +597,14 @@ public int process(Buffer buffer)
                          */
                         ret |= INPUT_BUFFER_NOT_CONSUMED;
                         sleep = devicePeriod;
+                        /*
+                         * The writing to the render endpoint buffer may or may
+                         * not be malfunctioning, it depends on the interval of
+                         * time that the state remains unchanged.
+                         */
+                        if (writeIsMalfunctioningSince
+                                == DiagnosticsControl.NEVER)
+                            setWriteIsMalfunctioning(true);
                     }
                     else
                     {
@@ -598,11 +631,28 @@ public int process(Buffer buffer)
                                 buffer.setOffset(offset + toCopy);
                                 ret |= INPUT_BUFFER_NOT_CONSUMED;
                             }
+
+                            /*
+                             * Writing from the input Buffer into remainder has
+                             * occurred so it does not look like the writing to
+                             * the render endpoint buffer is malfunctioning.
+                             */
+                            if (writeIsMalfunctioningSince
+                                    != DiagnosticsControl.NEVER)
+                                setWriteIsMalfunctioning(false);
                         }
                         else
                         {
                             ret |= INPUT_BUFFER_NOT_CONSUMED;
                             sleep = devicePeriod;
+                            /*
+                             * No writing from the input Buffer into remainder
+                             * has occurred so it is possible that the writing
+                             * to the render endpoint buffer is malfunctioning.
+                             */
+                            if (writeIsMalfunctioningSince
+                                    == DiagnosticsControl.NEVER)
+                                setWriteIsMalfunctioning(true);
                         }
                     }
                 }
@@ -733,6 +783,41 @@ else if (written > 0)
                             // We have consumed frames from remainder.
                             popFromRemainder(written);
                         }
+
+                        if (writeIsMalfunctioningSince
+                                != DiagnosticsControl.NEVER)
+                            setWriteIsMalfunctioning(false);
+                    }
+                }
+
+                /*
+                 * If the writing to the render endpoint buffer is
+                 * malfunctioning, fail the processing of the input Buffer in
+                 * order to avoid blocking of the Codec chain.
+                 */
+                if (((ret & INPUT_BUFFER_NOT_CONSUMED)
+                            == INPUT_BUFFER_NOT_CONSUMED)
+                        && (writeIsMalfunctioningSince
+                                != DiagnosticsControl.NEVER))
+                {
+                    long writeIsMalfunctioningDuration
+                        = System.currentTimeMillis()
+                            - writeIsMalfunctioningSince;
+
+                    if (writeIsMalfunctioningDuration
+                            > writeIsMalfunctioningTimeout)
+                    {
+                        /*
+                         * The writing to the render endpoint buffer has taken
+                         * too long so whatever is in remainder is surely
+                         * out-of-date.
+                         */
+                        remainderLength = 0;
+                        ret = BUFFER_PROCESSED_FAILED;
+                        logger.warn(
+                                "Audio endpoint device appears to be"
+                                    + " malfunctioning: "
+                                    + getLocator());
                     }
                 }
             }
@@ -899,7 +984,14 @@ private void runInEventHandleCmd(Runnable eventHandleCmd)
                             written = 0;
                             logger.error("IAudioRenderClient_Write", hre);
                         }
-                        popFromRemainder(written);
+                        if (written != 0)
+                        {
+                            popFromRemainder(written);
+
+                            if (writeIsMalfunctioningSince
+                                    != DiagnosticsControl.NEVER)
+                                setWriteIsMalfunctioning(false);
+                        }
                     }
                 }
                 finally
@@ -919,6 +1011,11 @@ private void runInEventHandleCmd(Runnable eventHandleCmd)
                 }
                 catch (HResultException hre)
                 {
+                    /*
+                     * WaitForSingleObject will throw HResultException only in
+                     * the case of WAIT_FAILED. Event if it didn't, it would
+                     * still be a failure from our point of view.
+                     */
                     wfso = WAIT_FAILED;
                     logger.error("WaitForSingleObject", hre);
                 }
@@ -926,7 +1023,7 @@ private void runInEventHandleCmd(Runnable eventHandleCmd)
                  * If the function WaitForSingleObject fails once, it will very
                  * likely fail forever. Bail out of a possible busy wait.
                  */
-                if (wfso == WAIT_FAILED)
+                if ((wfso == WAIT_FAILED) || (wfso == WAIT_ABANDONED))
                     break;
             }
             while (true);
@@ -963,6 +1060,26 @@ public Format setInputFormat(Format format)
         return setInputFormat;
     }
 
+    /**
+     * Indicates whether the writing to the render endpoint buffer is
+     * malfunctioning. Keeps track of the time at which the malfunction has
+     * started.
+     *
+     * @param writeIsMalfunctioning <tt>true</tt> if the writing to the render
+     * endpoint buffer is (believed to be) malfunctioning; otherwise,
+     * <tt>false</tt>
+     */
+    private void setWriteIsMalfunctioning(boolean writeIsMalfunctioning)
+    {
+        if (writeIsMalfunctioning)
+        {
+            if (writeIsMalfunctioningSince == DiagnosticsControl.NEVER)
+                writeIsMalfunctioningSince = System.currentTimeMillis();
+        }
+        else
+            writeIsMalfunctioningSince = DiagnosticsControl.NEVER;
+    }
+
     /**
      * {@inheritDoc}
      */
@@ -1076,6 +1193,8 @@ public synchronized void stop()
                 started = false;
 
                 waitWhileEventHandleCmd();
+
+                writeIsMalfunctioningSince = DiagnosticsControl.NEVER;
             }
             catch (HResultException hre)
             {
-- 
GitLab