diff --git a/RELEASENOTES.md b/RELEASENOTES.md index 0f1c314637..2a8f91ae20 100644 --- a/RELEASENOTES.md +++ b/RELEASENOTES.md @@ -122,6 +122,8 @@ ([#4661](https://github.com/google/ExoPlayer/issues/4661)). * Allow edit lists which do not start with a sync sample. ([#4774](https://github.com/google/ExoPlayer/issues/4774)). +* Fix issue with audio discontinuities at period transitions, e.g. when + looping ([#3829](https://github.com/google/ExoPlayer/issues/3829)). * IMA extension: * Refine the previous fix for empty ad groups to avoid discarding ad breaks unnecessarily ([#4030](https://github.com/google/ExoPlayer/issues/4030)), diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java index 0db52daa12..bb7ef22ef4 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioSink.java @@ -215,9 +215,7 @@ public interface AudioSink { */ void play(); - /** - * Signals to the sink that the next buffer is discontinuous with the previous buffer. - */ + /** Signals to the sink that the next buffer may be discontinuous with the previous buffer. */ void handleDiscontinuity(); /** diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java index aed4c63c75..f9a8c8ced5 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/DefaultAudioSink.java @@ -642,9 +642,10 @@ public final class DefaultAudioSink implements AudioSink { if (startMediaTimeState == START_NEED_SYNC) { // Adjust startMediaTimeUs to be consistent with the current buffer's start time and the // number of bytes submitted. - startMediaTimeUs += (presentationTimeUs - expectedPresentationTimeUs); + long adjustmentUs = presentationTimeUs - expectedPresentationTimeUs; + startMediaTimeUs += adjustmentUs; startMediaTimeState = START_IN_SYNC; - if (listener != null) { + if (listener != null && adjustmentUs != 0) { listener.onPositionDiscontinuity(); } } diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java index 1d3e65f7ff..23cf893b8a 100644 --- a/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java +++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/MediaCodecAudioRenderer.java @@ -25,6 +25,7 @@ import android.media.MediaFormat; import android.media.audiofx.Virtualizer; import android.os.Handler; import android.support.annotation.Nullable; +import android.util.Log; import com.google.android.exoplayer2.C; import com.google.android.exoplayer2.ExoPlaybackException; import com.google.android.exoplayer2.ExoPlayer; @@ -68,9 +69,19 @@ import java.util.List; @TargetApi(16) public class MediaCodecAudioRenderer extends MediaCodecRenderer implements MediaClock { + /** + * Maximum number of tracked pending stream change times. Generally there is zero or one pending + * stream change. We track more to allow for pending changes that have fewer samples than the + * codec latency. + */ + private static final int MAX_PENDING_STREAM_CHANGE_COUNT = 10; + + private static final String TAG = "MediaCodecAudioRenderer"; + private final Context context; private final EventDispatcher eventDispatcher; private final AudioSink audioSink; + private final long[] pendingStreamChangeTimesUs; private int codecMaxInputSize; private boolean passthroughEnabled; @@ -83,6 +94,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media private long currentPositionUs; private boolean allowFirstBufferPositionDiscontinuity; private boolean allowPositionDiscontinuity; + private long lastInputTimeUs; + private int pendingStreamChangeCount; /** * @param context A context. @@ -241,6 +254,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media /* assumedMinimumCodecOperatingRate= */ 44100); this.context = context.getApplicationContext(); this.audioSink = audioSink; + lastInputTimeUs = C.TIME_UNSET; + pendingStreamChangeTimesUs = new long[MAX_PENDING_STREAM_CHANGE_COUNT]; eventDispatcher = new EventDispatcher(eventHandler, eventListener); audioSink.setListener(new AudioSinkListener()); } @@ -469,6 +484,22 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } } + @Override + protected void onStreamChanged(Format[] formats, long offsetUs) throws ExoPlaybackException { + super.onStreamChanged(formats, offsetUs); + if (lastInputTimeUs != C.TIME_UNSET) { + if (pendingStreamChangeCount == pendingStreamChangeTimesUs.length) { + Log.w( + TAG, + "Too many stream changes, so dropping change at " + + pendingStreamChangeTimesUs[pendingStreamChangeCount - 1]); + } else { + pendingStreamChangeCount++; + } + pendingStreamChangeTimesUs[pendingStreamChangeCount - 1] = lastInputTimeUs; + } + } + @Override protected void onPositionReset(long positionUs, boolean joining) throws ExoPlaybackException { super.onPositionReset(positionUs, joining); @@ -476,6 +507,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media currentPositionUs = positionUs; allowFirstBufferPositionDiscontinuity = true; allowPositionDiscontinuity = true; + lastInputTimeUs = C.TIME_UNSET; + pendingStreamChangeCount = 0; } @Override @@ -494,6 +527,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media @Override protected void onDisabled() { try { + lastInputTimeUs = C.TIME_UNSET; + pendingStreamChangeCount = 0; audioSink.release(); } finally { try { @@ -544,6 +579,22 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media } allowFirstBufferPositionDiscontinuity = false; } + lastInputTimeUs = Math.max(buffer.timeUs, lastInputTimeUs); + } + + @Override + protected void onProcessedOutputBuffer(long presentationTimeUs) { + super.onProcessedOutputBuffer(presentationTimeUs); + while (pendingStreamChangeCount != 0 && presentationTimeUs >= pendingStreamChangeTimesUs[0]) { + audioSink.handleDiscontinuity(); + pendingStreamChangeCount--; + System.arraycopy( + pendingStreamChangeTimesUs, + /* srcPos= */ 1, + pendingStreamChangeTimesUs, + /* destPos= */ 0, + pendingStreamChangeCount); + } } @Override