Skip to content

Commit 2729dbb

Browse files
microkatzcopybara-github
authored andcommitted
Limit dynamic scheduling interval by the audio track buffer size
In certain bluetooth playback scenarios, it was found that the delta of audio duration written by the AudioSink from the current playback position was greater than the size of the audio track buffer, causing underruns. The solution is to utilize the audio track buffer size as an upper limit for an audio renderer's getDurationToProgress. PiperOrigin-RevId: 735761604
1 parent a7c727e commit 2729dbb

File tree

8 files changed

+520
-30
lines changed

8 files changed

+520
-30
lines changed

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioSink.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,6 +591,15 @@ default void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {}
591591
*/
592592
default void setOutputStreamOffsetUs(long outputStreamOffsetUs) {}
593593

594+
/**
595+
* Returns the size of the underlying {@link AudioTrack} buffer in microseconds. If unsupported or
596+
* the {@link AudioTrack} is not initialized then return {@link C#TIME_UNSET};
597+
*
598+
* <p>If the {@link AudioTrack} is configured with a compressed encoding, then the returned
599+
* duration is an estimated minimum based on the encoding's maximum encoded byte rate.
600+
*/
601+
long getAudioTrackBufferSizeUs();
602+
594603
/**
595604
* Enables tunneling, if possible. The sink is reset if tunneling was previously disabled.
596605
* Enabling tunneling is only possible if the sink is based on a platform {@link AudioTrack}, and

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DecoderAudioRenderer.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import static androidx.media3.exoplayer.source.SampleStream.FLAG_REQUIRE_FORMAT;
2323
import static com.google.common.base.MoreObjects.firstNonNull;
2424
import static java.lang.Math.max;
25+
import static java.lang.Math.min;
2526
import static java.lang.annotation.ElementType.TYPE_USE;
2627

2728
import android.media.AudioDeviceInfo;
@@ -246,16 +247,23 @@ public long getDurationToProgressUs(long positionUs, long elapsedRealtimeUs) {
246247
if (nextBufferToWritePresentationTimeUs == C.TIME_UNSET) {
247248
return super.getDurationToProgressUs(positionUs, elapsedRealtimeUs);
248249
}
249-
long durationUs =
250+
// Compare written, yet-to-play content duration against the audio track buffer size.
251+
long writtenDurationUs = (nextBufferToWritePresentationTimeUs - positionUs);
252+
long audioTrackBufferDurationUs = audioSink.getAudioTrackBufferSizeUs();
253+
long bufferedDurationUs =
254+
audioTrackBufferDurationUs != C.TIME_UNSET
255+
? min(audioTrackBufferDurationUs, writtenDurationUs)
256+
: writtenDurationUs;
257+
bufferedDurationUs =
250258
(long)
251-
((nextBufferToWritePresentationTimeUs - positionUs)
259+
(bufferedDurationUs
252260
/ (getPlaybackParameters() != null ? getPlaybackParameters().speed : 1.0f)
253261
/ 2);
254262
if (isStarted) {
255263
// Account for the elapsed time since the start of this iteration of the rendering loop.
256-
durationUs -= Util.msToUs(getClock().elapsedRealtime()) - elapsedRealtimeUs;
264+
bufferedDurationUs -= Util.msToUs(getClock().elapsedRealtime()) - elapsedRealtimeUs;
257265
}
258-
return max(DEFAULT_DURATION_TO_PROGRESS_US, durationUs);
266+
return max(DEFAULT_DURATION_TO_PROGRESS_US, bufferedDurationUs);
259267
}
260268

261269
@Override

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/DefaultAudioSink.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,7 @@
7171
import java.lang.annotation.Retention;
7272
import java.lang.annotation.RetentionPolicy;
7373
import java.lang.annotation.Target;
74+
import java.math.RoundingMode;
7475
import java.nio.ByteBuffer;
7576
import java.nio.ByteOrder;
7677
import java.util.ArrayDeque;
@@ -1454,6 +1455,23 @@ public void setPreferredDevice(@Nullable AudioDeviceInfo audioDeviceInfo) {
14541455
}
14551456
}
14561457

1458+
@Override
1459+
public long getAudioTrackBufferSizeUs() {
1460+
if (!isAudioTrackInitialized()) {
1461+
return C.TIME_UNSET;
1462+
}
1463+
if (Util.SDK_INT >= 23) {
1464+
return Api23.getAudioTrackBufferSizeUs(audioTrack, configuration);
1465+
}
1466+
long byteRate =
1467+
configuration.outputMode == OUTPUT_MODE_PCM
1468+
? (long) configuration.outputSampleRate * configuration.outputPcmFrameSize
1469+
: DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond(
1470+
configuration.outputEncoding);
1471+
return Util.scaleLargeValue(
1472+
configuration.bufferSize, C.MICROS_PER_SECOND, byteRate, RoundingMode.DOWN);
1473+
}
1474+
14571475
@Override
14581476
public void enableTunnelingV21() {
14591477
Assertions.checkState(externalAudioSessionIdProvided);
@@ -2365,6 +2383,18 @@ public static void setPreferredDeviceOnAudioTrack(
23652383
audioTrack.setPreferredDevice(
23662384
audioDeviceInfo == null ? null : audioDeviceInfo.audioDeviceInfo);
23672385
}
2386+
2387+
public static long getAudioTrackBufferSizeUs(
2388+
AudioTrack audioTrack, Configuration configuration) {
2389+
return configuration.outputMode == OUTPUT_MODE_PCM
2390+
? configuration.framesToDurationUs(audioTrack.getBufferSizeInFrames())
2391+
: Util.scaleLargeValue(
2392+
audioTrack.getBufferSizeInFrames(),
2393+
C.MICROS_PER_SECOND,
2394+
DefaultAudioTrackBufferSizeProvider.getMaximumEncodedRateBytesPerSecond(
2395+
configuration.outputEncoding),
2396+
RoundingMode.DOWN);
2397+
}
23682398
}
23692399

23702400
@RequiresApi(31)

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/ForwardingAudioSink.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ public void setOutputStreamOffsetUs(long outputStreamOffsetUs) {
162162
sink.setOutputStreamOffsetUs(outputStreamOffsetUs);
163163
}
164164

165+
@Override
166+
public long getAudioTrackBufferSizeUs() {
167+
return sink.getAudioTrackBufferSizeUs();
168+
}
169+
165170
@Override
166171
public void enableTunnelingV21() {
167172
sink.enableTunnelingV21();

libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/MediaCodecAudioRenderer.java

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import static androidx.media3.exoplayer.DecoderReuseEvaluation.REUSE_RESULT_NO;
2121
import static com.google.common.base.MoreObjects.firstNonNull;
2222
import static java.lang.Math.max;
23+
import static java.lang.Math.min;
2324

2425
import android.annotation.SuppressLint;
2526
import android.content.Context;
@@ -518,20 +519,27 @@ public MediaClock getMediaClock() {
518519
@Override
519520
protected long getDurationToProgressUs(
520521
long positionUs, long elapsedRealtimeUs, boolean isOnBufferAvailableListenerRegistered) {
521-
if (nextBufferToWritePresentationTimeUs != C.TIME_UNSET) {
522-
long durationUs =
523-
(long)
524-
((nextBufferToWritePresentationTimeUs - positionUs)
525-
/ (getPlaybackParameters() != null ? getPlaybackParameters().speed : 1.0f)
526-
/ 2);
527-
if (isStarted) {
528-
// Account for the elapsed time since the start of this iteration of the rendering loop.
529-
durationUs -= Util.msToUs(getClock().elapsedRealtime()) - elapsedRealtimeUs;
530-
}
531-
return max(DEFAULT_DURATION_TO_PROGRESS_US, durationUs);
522+
if (nextBufferToWritePresentationTimeUs == C.TIME_UNSET) {
523+
return super.getDurationToProgressUs(
524+
positionUs, elapsedRealtimeUs, isOnBufferAvailableListenerRegistered);
525+
}
526+
// Compare written, yet-to-play content duration against the audio track buffer size.
527+
long writtenDurationUs = (nextBufferToWritePresentationTimeUs - positionUs);
528+
long audioTrackBufferDurationUs = audioSink.getAudioTrackBufferSizeUs();
529+
long bufferedDurationUs =
530+
audioTrackBufferDurationUs != C.TIME_UNSET
531+
? min(audioTrackBufferDurationUs, writtenDurationUs)
532+
: writtenDurationUs;
533+
bufferedDurationUs =
534+
(long)
535+
(bufferedDurationUs
536+
/ (getPlaybackParameters() != null ? getPlaybackParameters().speed : 1.0f)
537+
/ 2);
538+
if (isStarted) {
539+
// Account for the elapsed time since the start of this iteration of the rendering loop.
540+
bufferedDurationUs -= Util.msToUs(getClock().elapsedRealtime()) - elapsedRealtimeUs;
532541
}
533-
return super.getDurationToProgressUs(
534-
positionUs, elapsedRealtimeUs, isOnBufferAvailableListenerRegistered);
542+
return max(DEFAULT_DURATION_TO_PROGRESS_US, bufferedDurationUs);
535543
}
536544

537545
@Override

0 commit comments

Comments
 (0)