Commit eb08e1a5 by andrewlewis Committed by Oliver Woodman

Work around AudioTrack Bluetooth connection issue

If connecting a Bluetooth audio device fails, the AudioTrack may be left in a
bad state, where it is not actually playing and its position has jumped back to
zero. Detect and work around this case by resetting the track.

-------------
Created by MOE: https://github.com/google/moe
MOE_MIGRATED_REVID=172600912
parent b8ef1dcc
...@@ -293,8 +293,6 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -293,8 +293,6 @@ public final class DefaultAudioSink implements AudioSink {
@Override @Override
public long getCurrentPositionUs(boolean sourceEnded) { public long getCurrentPositionUs(boolean sourceEnded) {
// If the device supports it, use the playback timestamp from AudioTrack.getTimestamp.
// Otherwise, derive a smoothed position by sampling the track's frame position.
if (!hasCurrentPositionUs()) { if (!hasCurrentPositionUs()) {
return CURRENT_POSITION_NOT_SET; return CURRENT_POSITION_NOT_SET;
} }
...@@ -303,6 +301,8 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -303,6 +301,8 @@ public final class DefaultAudioSink implements AudioSink {
maybeSampleSyncParams(); maybeSampleSyncParams();
} }
// If the device supports it, use the playback timestamp from AudioTrack.getTimestamp.
// Otherwise, derive a smoothed position by sampling the track's frame position.
long systemClockUs = System.nanoTime() / 1000; long systemClockUs = System.nanoTime() / 1000;
long positionUs; long positionUs;
if (audioTimestampSet) { if (audioTimestampSet) {
...@@ -638,6 +638,13 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -638,6 +638,13 @@ public final class DefaultAudioSink implements AudioSink {
inputBuffer = null; inputBuffer = null;
return true; return true;
} }
if (audioTrackUtil.needsReset(getWrittenFrames())) {
Log.w(TAG, "Resetting stalled audio track");
reset();
return true;
}
return false; return false;
} }
...@@ -1292,6 +1299,8 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1292,6 +1299,8 @@ public final class DefaultAudioSink implements AudioSink {
*/ */
private static class AudioTrackUtil { private static class AudioTrackUtil {
private static final long FORCE_RESET_WORKAROUND_TIMEOUT_MS = 200;
protected AudioTrack audioTrack; protected AudioTrack audioTrack;
private boolean needsPassthroughWorkaround; private boolean needsPassthroughWorkaround;
private int sampleRate; private int sampleRate;
...@@ -1300,6 +1309,7 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1300,6 +1309,7 @@ public final class DefaultAudioSink implements AudioSink {
private long passthroughWorkaroundPauseOffset; private long passthroughWorkaroundPauseOffset;
private long stopTimestampUs; private long stopTimestampUs;
private long forceResetWorkaroundTimeMs;
private long stopPlaybackHeadPosition; private long stopPlaybackHeadPosition;
private long endPlaybackHeadPosition; private long endPlaybackHeadPosition;
...@@ -1314,6 +1324,7 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1314,6 +1324,7 @@ public final class DefaultAudioSink implements AudioSink {
this.audioTrack = audioTrack; this.audioTrack = audioTrack;
this.needsPassthroughWorkaround = needsPassthroughWorkaround; this.needsPassthroughWorkaround = needsPassthroughWorkaround;
stopTimestampUs = C.TIME_UNSET; stopTimestampUs = C.TIME_UNSET;
forceResetWorkaroundTimeMs = C.TIME_UNSET;
lastRawPlaybackHeadPosition = 0; lastRawPlaybackHeadPosition = 0;
rawPlaybackHeadWrapCount = 0; rawPlaybackHeadWrapCount = 0;
passthroughWorkaroundPauseOffset = 0; passthroughWorkaroundPauseOffset = 0;
...@@ -1349,6 +1360,17 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1349,6 +1360,17 @@ public final class DefaultAudioSink implements AudioSink {
} }
/** /**
* Returns whether the track is in an invalid state and must be reset.
*
* @see #getPlaybackHeadPosition()
*/
public boolean needsReset(long writtenFrames) {
return forceResetWorkaroundTimeMs != C.TIME_UNSET && writtenFrames > 0
&& SystemClock.elapsedRealtime() - forceResetWorkaroundTimeMs
>= FORCE_RESET_WORKAROUND_TIMEOUT_MS;
}
/**
* {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an * {@link AudioTrack#getPlaybackHeadPosition()} returns a value intended to be interpreted as an
* unsigned 32 bit integer, which also wraps around periodically. This method returns the * unsigned 32 bit integer, which also wraps around periodically. This method returns the
* playback head position as a long that will only wrap around if the value exceeds * playback head position as a long that will only wrap around if the value exceeds
...@@ -1380,6 +1402,24 @@ public final class DefaultAudioSink implements AudioSink { ...@@ -1380,6 +1402,24 @@ public final class DefaultAudioSink implements AudioSink {
} }
rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset; rawPlaybackHeadPosition += passthroughWorkaroundPauseOffset;
} }
if (Util.SDK_INT <= 26) {
if (rawPlaybackHeadPosition == 0 && lastRawPlaybackHeadPosition > 0
&& state == PLAYSTATE_PLAYING) {
// If connecting a Bluetooth audio device fails, the AudioTrack may be left in a state
// where its Java API is in the playing state, but the native track is stopped. When this
// happens the playback head position gets stuck at zero. In this case, return the old
// playback head position and force the track to be reset after
// {@link #FORCE_RESET_WORKAROUND_TIMEOUT_MS} has elapsed.
if (forceResetWorkaroundTimeMs == C.TIME_UNSET) {
forceResetWorkaroundTimeMs = SystemClock.elapsedRealtime();
}
return lastRawPlaybackHeadPosition;
} else {
forceResetWorkaroundTimeMs = C.TIME_UNSET;
}
}
if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) { if (lastRawPlaybackHeadPosition > rawPlaybackHeadPosition) {
// The value must have wrapped around. // The value must have wrapped around.
rawPlaybackHeadWrapCount++; rawPlaybackHeadWrapCount++;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment