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