diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java index 821bd3b679..c16d046721 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/EventLogger.java @@ -159,6 +159,12 @@ public class EventLogger implements DemoPlayer.Listener, DemoPlayer.InfoListener printInternalError("audioTrackWriteError", e); } + @Override + public void onAudioTrackUnderrun(long audioTrackBufferSizeMs, long elapsedSinceLastFeedMs) { + printInternalError("audioTrackUnderrun [" + audioTrackBufferSizeMs + ", " + + elapsedSinceLastFeedMs + "]", null); + } + @Override public void onCryptoError(CryptoException e) { printInternalError("cryptoError", e); diff --git a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java index 982ef6c790..283daaff41 100644 --- a/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java +++ b/demo/src/main/java/com/google/android/exoplayer/demo/player/DemoPlayer.java @@ -105,6 +105,7 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi void onRendererInitializationError(Exception e); void onAudioTrackInitializationError(AudioTrack.InitializationException e); void onAudioTrackWriteError(AudioTrack.WriteException e); + void onAudioTrackUnderrun(long audioTrackBufferSizeMs, long elapsedSinceLastFeedMs); void onDecoderInitializationError(DecoderInitializationException e); void onCryptoError(CryptoException e); void onLoadError(int sourceId, IOException e); @@ -481,6 +482,13 @@ public class DemoPlayer implements ExoPlayer.Listener, ChunkSampleSource.EventLi } } + @Override + public void onAudioTrackUnderrun(long audioTrackBufferSizeMs, long elapsedSinceLastFeedMs) { + if (internalErrorListener != null) { + internalErrorListener.onAudioTrackUnderrun(audioTrackBufferSizeMs, elapsedSinceLastFeedMs); + } + } + @Override public void onCryptoError(CryptoException e) { if (internalErrorListener != null) { diff --git a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java index 8a9f2881ba..df881b8712 100644 --- a/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java +++ b/library/src/main/java/com/google/android/exoplayer/MediaCodecAudioTrackRenderer.java @@ -26,6 +26,7 @@ import android.media.AudioManager; import android.media.MediaCodec; import android.media.audiofx.Virtualizer; import android.os.Handler; +import android.os.SystemClock; import java.nio.ByteBuffer; @@ -55,6 +56,14 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem */ void onAudioTrackWriteError(AudioTrack.WriteException e); + /** + * Invoked when an {@link AudioTrack} underrun occurs. + * + * @param audioTrackBufferSizeMs The size of the {@link AudioTrack}'s buffer, in milliseconds. + * @param elapsedSinceLastFeedMs The time since the {@link AudioTrack} was last fed data. + */ + void onAudioTrackUnderrun(long audioTrackBufferSizeMs, long elapsedSinceLastFeedMs); + } /** @@ -77,6 +86,9 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem private long currentPositionUs; private boolean allowPositionDiscontinuity; + private boolean audioTrackHasData; + private long lastFeedElapsedRealtimeMs; + /** * @param source The upstream source from which the renderer obtains samples. */ @@ -272,8 +284,7 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem @Override protected boolean isReady() { - return audioTrack.hasPendingData() - || (super.isReady() && getSourceState() == SOURCE_STATE_READY_READ_MAY_FAIL); + return audioTrack.hasPendingData() || super.isReady(); } @Override @@ -321,8 +332,8 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem return true; } - // Initialize and start the audio track now. if (!audioTrack.isInitialized()) { + // Initialize the AudioTrack now. try { if (audioSessionId != AudioTrack.SESSION_ID_NOT_SET) { audioTrack.initialize(audioSessionId); @@ -330,20 +341,29 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem audioSessionId = audioTrack.initialize(); onAudioSessionId(audioSessionId); } + audioTrackHasData = false; } catch (AudioTrack.InitializationException e) { notifyAudioTrackInitializationError(e); throw new ExoPlaybackException(e); } - if (getState() == TrackRenderer.STATE_STARTED) { audioTrack.play(); } + } else { + // Check for AudioTrack underrun. + boolean audioTrackHadData = audioTrackHasData; + audioTrackHasData = audioTrack.hasPendingData(); + if (audioTrackHadData && !audioTrackHasData && getState() == TrackRenderer.STATE_STARTED) { + long elapsedSinceLastFeedMs = SystemClock.elapsedRealtime() - lastFeedElapsedRealtimeMs; + notifyAudioTrackUnderrun(audioTrack.getBufferSizeUs() / 1000, elapsedSinceLastFeedMs); + } } int handleBufferResult; try { handleBufferResult = audioTrack.handleBuffer( buffer, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs); + lastFeedElapsedRealtimeMs = SystemClock.elapsedRealtime(); } catch (AudioTrack.WriteException e) { notifyAudioTrackWriteError(e); throw new ExoPlaybackException(e); @@ -405,4 +425,16 @@ public class MediaCodecAudioTrackRenderer extends MediaCodecTrackRenderer implem } } + private void notifyAudioTrackUnderrun(final long audioTrackBufferSizeMs, + final long elapsedSinceLastFeedMs) { + if (eventHandler != null && eventListener != null) { + eventHandler.post(new Runnable() { + @Override + public void run() { + eventListener.onAudioTrackUnderrun(audioTrackBufferSizeMs, elapsedSinceLastFeedMs); + } + }); + } + } + } diff --git a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java index 74c12cb17c..93882aa60a 100644 --- a/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java +++ b/library/src/main/java/com/google/android/exoplayer/audio/AudioTrack.java @@ -181,6 +181,7 @@ public final class AudioTrack { private int frameSize; private int minBufferSize; private int bufferSize; + private long bufferSizeUs; private int nextPlayheadOffsetIndex; private int playheadOffsetCount; @@ -433,9 +434,25 @@ public final class AudioTrack { : multipliedBufferSize > maxAppBufferSize ? maxAppBufferSize : multipliedBufferSize; } + bufferSizeUs = framesToDurationUs(bytesToFrames(bufferSize)); } - /** Starts/resumes playing audio if the audio track has been initialized. */ + /** + * Returns the size of this {@link AudioTrack}'s buffer in microseconds, given its current + * configuration. + *
+ * The duration returned from this method may change as a result of calling one of the + * {@link #reconfigure} methods. + * + * @return The size of the buffer in microseconds. + */ + public long getBufferSizeUs() { + return bufferSizeUs; + } + + /** + * Starts or resumes playing audio if the audio track has been initialized. + */ public void play() { if (isInitialized()) { resumeSystemTimeUs = System.nanoTime() / 1000; @@ -443,7 +460,9 @@ public final class AudioTrack { } } - /** Signals to the audio track that the next buffer is discontinuous with the previous buffer. */ + /** + * Signals to the audio track that the next buffer is discontinuous with the previous buffer. + */ public void handleDiscontinuity() { // Force resynchronization after a skipped buffer. if (startMediaTimeState == START_IN_SYNC) { @@ -584,14 +603,18 @@ public final class AudioTrack { return audioTrack.write(buffer, size, android.media.AudioTrack.WRITE_NON_BLOCKING); } - /** Returns whether the audio track has more data pending that will be played back. */ + /** + * Returns whether the audio track has more data pending that will be played back. + */ public boolean hasPendingData() { return isInitialized() && (bytesToFrames(submittedBytes) > audioTrackUtil.getPlaybackHeadPosition() || overrideHasPendingData()); } - /** Sets the playback volume. */ + /** + * Sets the playback volume. + */ public void setVolume(float volume) { if (this.volume != volume) { this.volume = volume; @@ -619,7 +642,9 @@ public final class AudioTrack { audioTrack.setStereoVolume(volume, volume); } - /** Pauses playback. */ + /** + * Pauses playback. + */ public void pause() { if (isInitialized()) { resetSyncParams(); @@ -662,13 +687,17 @@ public final class AudioTrack { } } - /** Releases all resources associated with this instance. */ + /** + * Releases all resources associated with this instance. + */ public void release() { reset(); releaseKeepSessionIdAudioTrack(); } - /** Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. */ + /** + * Releases {@link #keepSessionIdAudioTrack} asynchronously, if it is non-{@code null}. + */ private void releaseKeepSessionIdAudioTrack() { if (keepSessionIdAudioTrack == null) { return; @@ -685,7 +714,9 @@ public final class AudioTrack { }.start(); } - /** Returns whether {@link #getCurrentPositionUs} can return the current playback position. */ + /** + * Returns whether {@link #getCurrentPositionUs} can return the current playback position. + */ private boolean hasCurrentPositionUs() { return isInitialized() && startMediaTimeState != START_NOT_SET; } @@ -757,7 +788,7 @@ public final class AudioTrack { // Compute the audio track latency, excluding the latency due to the buffer (leaving // latency due to the mixer and audio hardware driver). latencyUs = (Integer) getLatencyMethod.invoke(audioTrack, (Object[]) null) * 1000L - - framesToDurationUs(bytesToFrames(bufferSize)); + - bufferSizeUs; // Sanity check that the latency is non-negative. latencyUs = Math.max(latencyUs, 0); // Sanity check that the latency isn't too large. diff --git a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java index 320af8e924..49970d0423 100644 --- a/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java +++ b/playbacktests/src/main/java/com/google/android/exoplayer/playbacktests/util/LogcatLogger.java @@ -104,6 +104,12 @@ public final class LogcatLogger implements ExoPlayer.Listener, Log.e(tag, "Audio track write error", e); } + @Override + public void onAudioTrackUnderrun(long audioTrackBufferSizeMs, long elapsedSinceLastFeedMs) { + Log.e(tag, "Audio track underrun (" + audioTrackBufferSizeMs + ", " + elapsedSinceLastFeedMs + + ")"); + } + @Override public void onDroppedFrames(int count, long elapsed) { Log.w(tag, "Dropped frames (" + count + ")");