diff --git a/RELEASENOTES.md b/RELEASENOTES.md
index 537079fd23..cd22c42b36 100644
--- a/RELEASENOTES.md
+++ b/RELEASENOTES.md
@@ -122,6 +122,9 @@
* Add floating point PCM output capability in `MediaCodecAudioRenderer`,
and `LibopusAudioRenderer`.
* Do not use a MediaCodec for PCM formats if AudioTrack supports it.
+ * Add optional support for using framework audio speed adjustment instead
+ of application-level audio speed adjustment
+ ([#7502](https://github.com/google/ExoPlayer/issues/7502)).
* Text:
* Recreate the decoder when handling and swallowing decode errors in
`TextRenderer`. This fixes a case where playback would never end when
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
index 1de327fbd2..51e94c2155 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/DefaultRenderersFactory.java
@@ -17,6 +17,7 @@ package com.google.android.exoplayer2;
import android.content.Context;
import android.media.MediaCodec;
+import android.media.PlaybackParams;
import android.os.Handler;
import android.os.Looper;
import androidx.annotation.IntDef;
@@ -94,6 +95,7 @@ public class DefaultRenderersFactory implements RenderersFactory {
private @MediaCodecRenderer.MediaCodecOperationMode int audioMediaCodecOperationMode;
private @MediaCodecRenderer.MediaCodecOperationMode int videoMediaCodecOperationMode;
private boolean enableFloatOutput;
+ private boolean enableAudioTrackPlaybackParams;
private boolean enableOffload;
/** @param context A {@link Context}. */
@@ -258,6 +260,30 @@ public class DefaultRenderersFactory implements RenderersFactory {
return this;
}
+ /**
+ * Sets whether to enable setting playback speed using {@link
+ * android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, which is supported from API level
+ * 23, rather than using application-level audio speed adjustment. This setting has no effect on
+ * builds before API level 23 (application-level speed adjustment will be used in all cases).
+ *
+ *
If enabled and supported, new playback speed settings will take effect more quickly because
+ * they are applied at the audio mixer, rather than at the point of writing data to the track.
+ *
+ *
When using this mode, the maximum supported playback speed is limited by the size of the
+ * audio track's buffer. If the requested speed is not supported the player's event listener will
+ * be notified twice on setting playback speed, once with the requested speed, then again with the
+ * old playback speed reflecting the fact that the requested speed was not supported.
+ *
+ * @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link
+ * android.media.AudioTrack#setPlaybackParams(PlaybackParams)}.
+ * @return This factory, for convenience.
+ */
+ public DefaultRenderersFactory setEnableAudioTrackPlaybackParams(
+ boolean enableAudioTrackPlaybackParams) {
+ this.enableAudioTrackPlaybackParams = enableAudioTrackPlaybackParams;
+ return this;
+ }
+
/**
* Sets the maximum duration for which video renderers can attempt to seamlessly join an ongoing
* playback.
@@ -290,7 +316,9 @@ public class DefaultRenderersFactory implements RenderersFactory {
videoRendererEventListener,
allowedVideoJoiningTimeMs,
renderersList);
- @Nullable AudioSink audioSink = buildAudioSink(context, enableFloatOutput, enableOffload);
+ @Nullable
+ AudioSink audioSink =
+ buildAudioSink(context, enableFloatOutput, enableAudioTrackPlaybackParams, enableOffload);
if (audioSink != null) {
buildAudioRenderers(
context,
@@ -611,6 +639,8 @@ public class DefaultRenderersFactory implements RenderersFactory {
*
* @param context The {@link Context} associated with the player.
* @param enableFloatOutput Whether to enable use of floating point audio output, if available.
+ * @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link
+ * android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported.
* @param enableOffload Whether to enable use of audio offload for supported formats, if
* available.
* @return The {@link AudioSink} to which the audio renderers will output. May be {@code null} if
@@ -619,11 +649,15 @@ public class DefaultRenderersFactory implements RenderersFactory {
*/
@Nullable
protected AudioSink buildAudioSink(
- Context context, boolean enableFloatOutput, boolean enableOffload) {
+ Context context,
+ boolean enableFloatOutput,
+ boolean enableAudioTrackPlaybackParams,
+ boolean enableOffload) {
return new DefaultAudioSink(
AudioCapabilities.getCapabilities(context),
new DefaultAudioProcessorChain(),
enableFloatOutput,
+ enableAudioTrackPlaybackParams,
enableOffload);
}
}
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
index 0f2f48b284..e70c3ef7dc 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/ExoPlayer.java
@@ -625,7 +625,7 @@ public interface ExoPlayer extends Player {
*
audio offload rendering is enabled in {@link
* DefaultRenderersFactory#setEnableAudioOffload} or the equivalent option passed to {@link
* com.google.android.exoplayer2.audio.DefaultAudioSink#DefaultAudioSink(AudioCapabilities,
- * DefaultAudioSink.AudioProcessorChain, boolean, boolean)}.
+ * DefaultAudioSink.AudioProcessorChain, boolean, boolean, boolean)}.
* an audio track is playing in a format which the device supports offloading (for example
* MP3 or AAC).
* The {@link com.google.android.exoplayer2.audio.AudioSink} is playing with an offload
diff --git a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
index 88f4f93ffc..c1d8df5c75 100644
--- a/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
+++ b/library/core/src/main/java/com/google/android/exoplayer2/audio/AudioTrackPositionTracker.java
@@ -144,6 +144,7 @@ import java.lang.reflect.Method;
private int outputSampleRate;
private boolean needsPassthroughWorkarounds;
private long bufferSizeUs;
+ private float audioTrackPlaybackSpeed;
private long smoothedPlayheadOffsetUs;
private long lastPlayheadSampleTimeUs;
@@ -223,6 +224,16 @@ import java.lang.reflect.Method;
forceResetWorkaroundTimeMs = C.TIME_UNSET;
lastLatencySampleTimeUs = 0;
latencyUs = 0;
+ audioTrackPlaybackSpeed = 1f;
+ }
+
+ public void setAudioTrackPlaybackSpeed(float audioTrackPlaybackSpeed) {
+ this.audioTrackPlaybackSpeed = audioTrackPlaybackSpeed;
+ // Extrapolation from the last audio timestamp relies on the audio rate being constant, so we
+ // reset audio timestamp tracking and wait for a new timestamp.
+ if (audioTimestampPoller != null) {
+ audioTimestampPoller.reset();
+ }
}
public long getCurrentPositionUs(boolean sourceEnded) {
@@ -241,6 +252,8 @@ import java.lang.reflect.Method;
long timestampPositionFrames = audioTimestampPoller.getTimestampPositionFrames();
long timestampPositionUs = framesToDurationUs(timestampPositionFrames);
long elapsedSinceTimestampUs = systemTimeUs - audioTimestampPoller.getTimestampSystemTimeUs();
+ elapsedSinceTimestampUs =
+ Util.getMediaDurationForPlayoutDuration(elapsedSinceTimestampUs, audioTrackPlaybackSpeed);
positionUs = timestampPositionUs + elapsedSinceTimestampUs;
} else {
if (playheadOffsetCount == 0) {
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 86369ff33d..491a0b0e11 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
@@ -22,6 +22,7 @@ import android.annotation.SuppressLint;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioTrack;
+import android.media.PlaybackParams;
import android.os.ConditionVariable;
import android.os.Handler;
import android.os.SystemClock;
@@ -273,6 +274,7 @@ public final class DefaultAudioSink implements AudioSink {
private final ConditionVariable releasingConditionVariable;
private final AudioTrackPositionTracker audioTrackPositionTracker;
private final ArrayDeque mediaPositionParametersCheckpoints;
+ private final boolean enableAudioTrackPlaybackParams;
private final boolean enableOffload;
@MonotonicNonNull private StreamEventCallbackV29 offloadStreamEventCallbackV29;
@@ -287,6 +289,7 @@ public final class DefaultAudioSink implements AudioSink {
private AudioAttributes audioAttributes;
@Nullable private MediaPositionParameters afterDrainParameters;
private MediaPositionParameters mediaPositionParameters;
+ private float audioTrackPlaybackSpeed;
@Nullable private ByteBuffer avSyncHeader;
private int bytesUntilNextAvSync;
@@ -359,6 +362,7 @@ public final class DefaultAudioSink implements AudioSink {
audioCapabilities,
new DefaultAudioProcessorChain(audioProcessors),
enableFloatOutput,
+ /* enableAudioTrackPlaybackParams= */ false,
/* enableOffload= */ false);
}
@@ -375,6 +379,8 @@ public final class DefaultAudioSink implements AudioSink {
* (24-bit or 32-bit) integer PCM. Float output is supported from API level 21. Audio
* processing (for example, speed adjustment) will not be available when float output is in
* use.
+ * @param enableAudioTrackPlaybackParams Whether to enable setting playback speed using {@link
+ * android.media.AudioTrack#setPlaybackParams(PlaybackParams)}, if supported.
* @param enableOffload Whether to enable audio offload. If an audio format can be both played
* with offload and encoded audio passthrough, it will be played in offload. Audio offload is
* supported from API level 29. Most Android devices can only support one offload {@link
@@ -386,10 +392,12 @@ public final class DefaultAudioSink implements AudioSink {
@Nullable AudioCapabilities audioCapabilities,
AudioProcessorChain audioProcessorChain,
boolean enableFloatOutput,
+ boolean enableAudioTrackPlaybackParams,
boolean enableOffload) {
this.audioCapabilities = audioCapabilities;
this.audioProcessorChain = Assertions.checkNotNull(audioProcessorChain);
this.enableFloatOutput = Util.SDK_INT >= 21 && enableFloatOutput;
+ this.enableAudioTrackPlaybackParams = Util.SDK_INT >= 23 && enableAudioTrackPlaybackParams;
this.enableOffload = Util.SDK_INT >= 29 && enableOffload;
releasingConditionVariable = new ConditionVariable(true);
audioTrackPositionTracker = new AudioTrackPositionTracker(new PositionTrackerListener());
@@ -414,6 +422,7 @@ public final class DefaultAudioSink implements AudioSink {
DEFAULT_SKIP_SILENCE,
/* mediaTimeUs= */ 0,
/* audioTrackPositionUs= */ 0);
+ audioTrackPlaybackSpeed = 1f;
drainingAudioProcessorIndex = C.INDEX_UNSET;
activeAudioProcessors = new AudioProcessor[0];
outputBuffers = new ByteBuffer[0];
@@ -641,7 +650,10 @@ public final class DefaultAudioSink implements AudioSink {
startMediaTimeUs = max(0, presentationTimeUs);
startMediaTimeUsNeedsSync = false;
- applyPlaybackSpeedAndSkipSilence(presentationTimeUs);
+ if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) {
+ setAudioTrackPlaybackSpeedV23(audioTrackPlaybackSpeed);
+ }
+ applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
audioTrackPositionTracker.setAudioTrack(
audioTrack,
@@ -701,7 +713,7 @@ public final class DefaultAudioSink implements AudioSink {
}
}
// Re-apply playback parameters.
- applyPlaybackSpeedAndSkipSilence(presentationTimeUs);
+ applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
}
if (!isInitialized()) {
@@ -740,7 +752,7 @@ public final class DefaultAudioSink implements AudioSink {
// Don't process any more input until draining completes.
return false;
}
- applyPlaybackSpeedAndSkipSilence(presentationTimeUs);
+ applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
afterDrainParameters = null;
}
@@ -771,7 +783,7 @@ public final class DefaultAudioSink implements AudioSink {
startMediaTimeUs += adjustmentUs;
startMediaTimeUsNeedsSync = false;
// Re-apply playback parameters because the startMediaTimeUs changed.
- applyPlaybackSpeedAndSkipSilence(presentationTimeUs);
+ applyAudioProcessorPlaybackSpeedAndSkipSilence(presentationTimeUs);
if (listener != null && adjustmentUs != 0) {
listener.onPositionDiscontinuity();
}
@@ -985,17 +997,24 @@ public final class DefaultAudioSink implements AudioSink {
@Override
public void setPlaybackSpeed(float playbackSpeed) {
- setPlaybackSpeedAndSkipSilence(playbackSpeed, getSkipSilenceEnabled());
+ if (enableAudioTrackPlaybackParams && Util.SDK_INT >= 23) {
+ setAudioTrackPlaybackSpeedV23(playbackSpeed);
+ } else {
+ setAudioProcessorPlaybackSpeedAndSkipSilence(playbackSpeed, getSkipSilenceEnabled());
+ }
}
@Override
public float getPlaybackSpeed() {
- return getMediaPositionParameters().playbackSpeed;
+ // We use either audio processor speed adjustment or AudioTrack playback parameters, so one of
+ // the operands is always 1f.
+ return getAudioProcessorPlaybackSpeed() * audioTrackPlaybackSpeed;
}
@Override
public void setSkipSilenceEnabled(boolean skipSilenceEnabled) {
- setPlaybackSpeedAndSkipSilence(getPlaybackSpeed(), skipSilenceEnabled);
+ setAudioProcessorPlaybackSpeedAndSkipSilence(
+ getAudioProcessorPlaybackSpeed(), skipSilenceEnabled);
}
@Override
@@ -1147,7 +1166,7 @@ public final class DefaultAudioSink implements AudioSink {
framesPerEncodedSample = 0;
mediaPositionParameters =
new MediaPositionParameters(
- getPlaybackSpeed(),
+ getAudioProcessorPlaybackSpeed(),
getSkipSilenceEnabled(),
/* mediaTimeUs= */ 0,
/* audioTrackPositionUs= */ 0);
@@ -1183,7 +1202,28 @@ public final class DefaultAudioSink implements AudioSink {
}.start();
}
- private void setPlaybackSpeedAndSkipSilence(float playbackSpeed, boolean skipSilence) {
+ @RequiresApi(23)
+ private void setAudioTrackPlaybackSpeedV23(float audioTrackPlaybackSpeed) {
+ if (isInitialized()) {
+ PlaybackParams playbackParams =
+ new PlaybackParams()
+ .allowDefaults()
+ .setSpeed(audioTrackPlaybackSpeed)
+ .setAudioFallbackMode(PlaybackParams.AUDIO_FALLBACK_MODE_FAIL);
+ try {
+ audioTrack.setPlaybackParams(playbackParams);
+ } catch (IllegalArgumentException e) {
+ Log.w(TAG, "Failed to set playback params", e);
+ }
+ // Update the speed using the actual effective speed from the audio track.
+ audioTrackPlaybackSpeed = audioTrack.getPlaybackParams().getSpeed();
+ audioTrackPositionTracker.setAudioTrackPlaybackSpeed(audioTrackPlaybackSpeed);
+ }
+ this.audioTrackPlaybackSpeed = audioTrackPlaybackSpeed;
+ }
+
+ private void setAudioProcessorPlaybackSpeedAndSkipSilence(
+ float playbackSpeed, boolean skipSilence) {
MediaPositionParameters currentMediaPositionParameters = getMediaPositionParameters();
if (playbackSpeed != currentMediaPositionParameters.playbackSpeed
|| skipSilence != currentMediaPositionParameters.skipSilence) {
@@ -1205,6 +1245,10 @@ public final class DefaultAudioSink implements AudioSink {
}
}
+ private float getAudioProcessorPlaybackSpeed() {
+ return getMediaPositionParameters().playbackSpeed;
+ }
+
private MediaPositionParameters getMediaPositionParameters() {
// Mask the already set parameters.
return afterDrainParameters != null
@@ -1214,10 +1258,10 @@ public final class DefaultAudioSink implements AudioSink {
: mediaPositionParameters;
}
- private void applyPlaybackSpeedAndSkipSilence(long presentationTimeUs) {
+ private void applyAudioProcessorPlaybackSpeedAndSkipSilence(long presentationTimeUs) {
float playbackSpeed =
configuration.canApplyPlaybackParameters
- ? audioProcessorChain.applyPlaybackSpeed(getPlaybackSpeed())
+ ? audioProcessorChain.applyPlaybackSpeed(getAudioProcessorPlaybackSpeed())
: DEFAULT_PLAYBACK_SPEED;
boolean skipSilenceEnabled =
configuration.canApplyPlaybackParameters
diff --git a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java
index c3c1046186..4c00e672cf 100644
--- a/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java
+++ b/library/core/src/test/java/com/google/android/exoplayer2/audio/DefaultAudioSinkTest.java
@@ -62,6 +62,7 @@ public final class DefaultAudioSinkTest {
AudioCapabilities.DEFAULT_AUDIO_CAPABILITIES,
new DefaultAudioSink.DefaultAudioProcessorChain(teeAudioProcessor),
/* enableFloatOutput= */ false,
+ /* enableAudioTrackPlaybackParams= */ false,
/* enableOffload= */ false);
}