mirror of
https://github.com/androidx/media.git
synced 2025-05-03 21:57:46 +08:00
Automated g4 rollforward of commit ffa4ad0e77a24168430fb3ac4c2afd13b68a701a.
*** Reason for rollforward *** Rollforward after making sure the handler is created, and that a test is written preventing a similar regression. *** Original change description *** Rollback ofb6f5a263f7
*** Original commit *** Rollforward of commit 5612ac50a332e425dc130c3c13a139b9e6fce9ec. *** Reason for rollforward *** Rollforward after making sure the handler is created from the playback thread and not from an app thread. *** Original change description *** Rollback ofe1beb1d194
*** Original commit *** PiperOrigin-RevId: 318274400
This commit is contained in:
parent
a8ae98b1bf
commit
e386b0b00a
@ -166,6 +166,7 @@
|
|||||||
* No longer use a `MediaCodec` in audio passthrough mode.
|
* No longer use a `MediaCodec` in audio passthrough mode.
|
||||||
* Check `DefaultAudioSink` supports passthrough, in addition to checking
|
* Check `DefaultAudioSink` supports passthrough, in addition to checking
|
||||||
the `AudioCapabilities`
|
the `AudioCapabilities`
|
||||||
|
* Add an experimental scheduling mode to save power in offload.
|
||||||
([#7404](https://github.com/google/ExoPlayer/issues/7404)).
|
([#7404](https://github.com/google/ExoPlayer/issues/7404)).
|
||||||
* Adjust input timestamps in `MediaCodecRenderer` to account for the
|
* Adjust input timestamps in `MediaCodecRenderer` to account for the
|
||||||
Codec2 MP3 decoder having lower timestamps on the output side.
|
Codec2 MP3 decoder having lower timestamps on the output side.
|
||||||
|
@ -219,12 +219,20 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets whether audio should be played using the offload path. Audio offload disables audio
|
* Sets whether audio should be played using the offload path.
|
||||||
* processors (for example speed adjustment).
|
*
|
||||||
|
* <p>Audio offload disables ExoPlayer audio processing, but significantly reduces the energy
|
||||||
|
* consumption of the playback when {@link
|
||||||
|
* ExoPlayer#experimental_enableOffloadScheduling(boolean)} is enabled.
|
||||||
|
*
|
||||||
|
* <p>Most Android devices can only support one offload {@link android.media.AudioTrack} at a time
|
||||||
|
* and can invalidate it at any time. Thus an app can never be guaranteed that it will be able to
|
||||||
|
* play in offload.
|
||||||
*
|
*
|
||||||
* <p>The default value is {@code false}.
|
* <p>The default value is {@code false}.
|
||||||
*
|
*
|
||||||
* @param enableOffload If audio offload should be used.
|
* @param enableOffload Whether to enable use of audio offload for supported formats, if
|
||||||
|
* available.
|
||||||
* @return This factory, for convenience.
|
* @return This factory, for convenience.
|
||||||
*/
|
*/
|
||||||
public DefaultRenderersFactory setEnableAudioOffload(boolean enableOffload) {
|
public DefaultRenderersFactory setEnableAudioOffload(boolean enableOffload) {
|
||||||
@ -423,7 +431,8 @@ public class DefaultRenderersFactory implements RenderersFactory {
|
|||||||
* before output. May be empty.
|
* before output. May be empty.
|
||||||
* @param eventHandler A handler to use when invoking event listeners and outputs.
|
* @param eventHandler A handler to use when invoking event listeners and outputs.
|
||||||
* @param eventListener An event listener.
|
* @param eventListener An event listener.
|
||||||
* @param enableOffload If the renderer should use audio offload for all supported formats.
|
* @param enableOffload Whether to enable use of audio offload for supported formats, if
|
||||||
|
* available.
|
||||||
* @param out An array to which the built renderers should be appended.
|
* @param out An array to which the built renderers should be appended.
|
||||||
*/
|
*/
|
||||||
protected void buildAudioRenderers(
|
protected void buildAudioRenderers(
|
||||||
|
@ -20,6 +20,8 @@ import android.os.Looper;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
import com.google.android.exoplayer2.analytics.AnalyticsCollector;
|
||||||
|
import com.google.android.exoplayer2.audio.AudioCapabilities;
|
||||||
|
import com.google.android.exoplayer2.audio.DefaultAudioSink;
|
||||||
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
import com.google.android.exoplayer2.audio.MediaCodecAudioRenderer;
|
||||||
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
import com.google.android.exoplayer2.metadata.MetadataRenderer;
|
||||||
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
import com.google.android.exoplayer2.source.ClippingMediaSource;
|
||||||
@ -597,4 +599,39 @@ public interface ExoPlayer extends Player {
|
|||||||
* @see #setPauseAtEndOfMediaItems(boolean)
|
* @see #setPauseAtEndOfMediaItems(boolean)
|
||||||
*/
|
*/
|
||||||
boolean getPauseAtEndOfMediaItems();
|
boolean getPauseAtEndOfMediaItems();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enables audio offload scheduling, which runs ExoPlayer's main loop as rarely as possible when
|
||||||
|
* playing an audio stream using audio offload.
|
||||||
|
*
|
||||||
|
* <p>Only use this scheduling mode if the player is not displaying anything to the user. For
|
||||||
|
* example when the application is in the background, or the screen is off. The player state
|
||||||
|
* (including position) is rarely updated (between 10s and 1min).
|
||||||
|
*
|
||||||
|
* <p>While offload scheduling is enabled, player events may be delivered severely delayed and
|
||||||
|
* apps should not interact with the player. When returning to the foreground, disable offload
|
||||||
|
* scheduling before interacting with the player
|
||||||
|
*
|
||||||
|
* <p>This mode should save significant power when the phone is playing offload audio with the
|
||||||
|
* screen off.
|
||||||
|
*
|
||||||
|
* <p>This mode only has an effect when playing an audio track in offload mode, which requires all
|
||||||
|
* the following:
|
||||||
|
*
|
||||||
|
* <ul>
|
||||||
|
* <li>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)}.
|
||||||
|
* <li>an audio track is playing in a format which the device supports offloading (for example
|
||||||
|
* MP3 or AAC).
|
||||||
|
* <li>The {@link com.google.android.exoplayer2.audio.AudioSink} is playing with an offload
|
||||||
|
* {@link android.media.AudioTrack}.
|
||||||
|
* </ul>
|
||||||
|
*
|
||||||
|
* <p>This method is experimental, and will be renamed or removed in a future release.
|
||||||
|
*
|
||||||
|
* @param enableOffloadScheduling Whether to enable offload scheduling.
|
||||||
|
*/
|
||||||
|
void experimental_enableOffloadScheduling(boolean enableOffloadScheduling);
|
||||||
}
|
}
|
||||||
|
@ -202,6 +202,11 @@ import java.util.concurrent.TimeoutException;
|
|||||||
internalPlayer.experimental_throwWhenStuckBuffering();
|
internalPlayer.experimental_throwWhenStuckBuffering();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void experimental_enableOffloadScheduling(boolean enableOffloadScheduling) {
|
||||||
|
internalPlayer.experimental_enableOffloadScheduling(enableOffloadScheduling);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public AudioComponent getAudioComponent() {
|
public AudioComponent getAudioComponent() {
|
||||||
|
@ -95,6 +95,15 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
|
|
||||||
private static final int ACTIVE_INTERVAL_MS = 10;
|
private static final int ACTIVE_INTERVAL_MS = 10;
|
||||||
private static final int IDLE_INTERVAL_MS = 1000;
|
private static final int IDLE_INTERVAL_MS = 1000;
|
||||||
|
/**
|
||||||
|
* Duration under which pausing the main DO_SOME_WORK loop is not expected to yield significant
|
||||||
|
* power saving.
|
||||||
|
*
|
||||||
|
* <p>This value is probably too high, power measurements are needed adjust it, but as renderer
|
||||||
|
* sleep is currently only implemented for audio offload, which uses buffer much bigger than 2s,
|
||||||
|
* this does not matter for now.
|
||||||
|
*/
|
||||||
|
private static final long MIN_RENDERER_SLEEP_DURATION_MS = 2000;
|
||||||
|
|
||||||
private final Renderer[] renderers;
|
private final Renderer[] renderers;
|
||||||
private final RendererCapabilities[] rendererCapabilities;
|
private final RendererCapabilities[] rendererCapabilities;
|
||||||
@ -128,6 +137,8 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
@Player.RepeatMode private int repeatMode;
|
@Player.RepeatMode private int repeatMode;
|
||||||
private boolean shuffleModeEnabled;
|
private boolean shuffleModeEnabled;
|
||||||
private boolean foregroundMode;
|
private boolean foregroundMode;
|
||||||
|
private boolean requestForRendererSleep;
|
||||||
|
private boolean offloadSchedulingEnabled;
|
||||||
|
|
||||||
private int enabledRendererCount;
|
private int enabledRendererCount;
|
||||||
@Nullable private SeekPosition pendingInitialSeekPosition;
|
@Nullable private SeekPosition pendingInitialSeekPosition;
|
||||||
@ -197,6 +208,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
throwWhenStuckBuffering = true;
|
throwWhenStuckBuffering = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void experimental_enableOffloadScheduling(boolean enableOffloadScheduling) {
|
||||||
|
offloadSchedulingEnabled = enableOffloadScheduling;
|
||||||
|
if (!enableOffloadScheduling) {
|
||||||
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void prepare() {
|
public void prepare() {
|
||||||
handler.obtainMessage(MSG_PREPARE).sendToTarget();
|
handler.obtainMessage(MSG_PREPARE).sendToTarget();
|
||||||
}
|
}
|
||||||
@ -868,12 +886,13 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
|
|
||||||
if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY)
|
if ((shouldPlayWhenReady() && playbackInfo.playbackState == Player.STATE_READY)
|
||||||
|| playbackInfo.playbackState == Player.STATE_BUFFERING) {
|
|| playbackInfo.playbackState == Player.STATE_BUFFERING) {
|
||||||
scheduleNextWork(operationStartTimeMs, ACTIVE_INTERVAL_MS);
|
maybeScheduleWakeup(operationStartTimeMs, ACTIVE_INTERVAL_MS);
|
||||||
} else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
|
} else if (enabledRendererCount != 0 && playbackInfo.playbackState != Player.STATE_ENDED) {
|
||||||
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
|
scheduleNextWork(operationStartTimeMs, IDLE_INTERVAL_MS);
|
||||||
} else {
|
} else {
|
||||||
handler.removeMessages(MSG_DO_SOME_WORK);
|
handler.removeMessages(MSG_DO_SOME_WORK);
|
||||||
}
|
}
|
||||||
|
requestForRendererSleep = false; // A sleep request is only valid for the current doSomeWork.
|
||||||
|
|
||||||
TraceUtil.endSection();
|
TraceUtil.endSection();
|
||||||
}
|
}
|
||||||
@ -883,6 +902,14 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
|
handler.sendEmptyMessageAtTime(MSG_DO_SOME_WORK, thisOperationStartTimeMs + intervalMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void maybeScheduleWakeup(long operationStartTimeMs, long intervalMs) {
|
||||||
|
if (offloadSchedulingEnabled && requestForRendererSleep) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
scheduleNextWork(operationStartTimeMs, intervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
|
private void seekToInternal(SeekPosition seekPosition) throws ExoPlaybackException {
|
||||||
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
playbackInfoUpdate.incrementPendingOperationAcks(/* operationAcks= */ 1);
|
||||||
|
|
||||||
@ -2051,6 +2078,24 @@ import java.util.concurrent.atomic.AtomicBoolean;
|
|||||||
joining,
|
joining,
|
||||||
mayRenderStartOfStream,
|
mayRenderStartOfStream,
|
||||||
periodHolder.getRendererOffset());
|
periodHolder.getRendererOffset());
|
||||||
|
|
||||||
|
renderer.handleMessage(
|
||||||
|
Renderer.MSG_SET_WAKEUP_LISTENER,
|
||||||
|
new Renderer.WakeupListener() {
|
||||||
|
@Override
|
||||||
|
public void onSleep(long wakeupDeadlineMs) {
|
||||||
|
// Do not sleep if the expected sleep time is not long enough to save significant power.
|
||||||
|
if (wakeupDeadlineMs >= MIN_RENDERER_SLEEP_DURATION_MS) {
|
||||||
|
requestForRendererSleep = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onWakeup() {
|
||||||
|
handler.sendEmptyMessage(MSG_DO_SOME_WORK);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
mediaClock.onRendererEnabled(renderer);
|
mediaClock.onRendererEnabled(renderer);
|
||||||
// Start the renderer if playing.
|
// Start the renderer if playing.
|
||||||
if (playing) {
|
if (playing) {
|
||||||
|
@ -46,6 +46,30 @@ import java.lang.annotation.RetentionPolicy;
|
|||||||
*/
|
*/
|
||||||
public interface Renderer extends PlayerMessage.Target {
|
public interface Renderer extends PlayerMessage.Target {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some renderers can signal when {@link #render(long, long)} should be called.
|
||||||
|
*
|
||||||
|
* <p>That allows the player to sleep until the next wakeup, instead of calling {@link
|
||||||
|
* #render(long, long)} in a tight loop. The aim of this interrupt based scheduling is to save
|
||||||
|
* power.
|
||||||
|
*/
|
||||||
|
interface WakeupListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The renderer no longer needs to render until the next wakeup.
|
||||||
|
*
|
||||||
|
* @param wakeupDeadlineMs Maximum time in milliseconds until {@link #onWakeup()} will be
|
||||||
|
* called.
|
||||||
|
*/
|
||||||
|
void onSleep(long wakeupDeadlineMs);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The renderer needs to render some frames. The client should call {@link #render(long, long)}
|
||||||
|
* at its earliest convenience.
|
||||||
|
*/
|
||||||
|
void onWakeup();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of a message that can be passed to a video renderer via {@link
|
* The type of a message that can be passed to a video renderer via {@link
|
||||||
* ExoPlayer#createMessage(Target)}. The message payload should be the target {@link Surface}, or
|
* ExoPlayer#createMessage(Target)}. The message payload should be the target {@link Surface}, or
|
||||||
@ -137,6 +161,14 @@ public interface Renderer extends PlayerMessage.Target {
|
|||||||
* representing the audio session ID that will be attached to the underlying audio track.
|
* representing the audio session ID that will be attached to the underlying audio track.
|
||||||
*/
|
*/
|
||||||
int MSG_SET_AUDIO_SESSION_ID = 102;
|
int MSG_SET_AUDIO_SESSION_ID = 102;
|
||||||
|
/**
|
||||||
|
* A type of a message that can be passed to a {@link Renderer} via {@link
|
||||||
|
* ExoPlayer#createMessage(Target)}, to inform the renderer that it can schedule waking up another
|
||||||
|
* component.
|
||||||
|
*
|
||||||
|
* <p>The message payload must be a {@link WakeupListener} instance.
|
||||||
|
*/
|
||||||
|
int MSG_SET_WAKEUP_LISTENER = 103;
|
||||||
/**
|
/**
|
||||||
* Applications or extensions may define custom {@code MSG_*} constants that can be passed to
|
* Applications or extensions may define custom {@code MSG_*} constants that can be passed to
|
||||||
* renderers. These custom constants must be greater than or equal to this value.
|
* renderers. These custom constants must be greater than or equal to this value.
|
||||||
|
@ -633,6 +633,11 @@ public class SimpleExoPlayer extends BasePlayer
|
|||||||
C.TRACK_TYPE_AUDIO, Renderer.MSG_SET_SKIP_SILENCE_ENABLED, skipSilenceEnabled);
|
C.TRACK_TYPE_AUDIO, Renderer.MSG_SET_SKIP_SILENCE_ENABLED, skipSilenceEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void experimental_enableOffloadScheduling(boolean enableOffloadScheduling) {
|
||||||
|
player.experimental_enableOffloadScheduling(enableOffloadScheduling);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nullable
|
@Nullable
|
||||||
public AudioComponent getAudioComponent() {
|
public AudioComponent getAudioComponent() {
|
||||||
|
@ -90,6 +90,17 @@ public interface AudioSink {
|
|||||||
* @param skipSilenceEnabled Whether skipping silences is enabled.
|
* @param skipSilenceEnabled Whether skipping silences is enabled.
|
||||||
*/
|
*/
|
||||||
void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled);
|
void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled);
|
||||||
|
|
||||||
|
/** Called when the offload buffer has been partially emptied. */
|
||||||
|
default void onOffloadBufferEmptying() {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the offload buffer has been filled completely.
|
||||||
|
*
|
||||||
|
* @param bufferEmptyingDeadlineMs Maximum time in milliseconds until {@link
|
||||||
|
* #onOffloadBufferEmptying()} will be called.
|
||||||
|
*/
|
||||||
|
default void onOffloadBufferFull(long bufferEmptyingDeadlineMs) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -335,6 +335,11 @@ import java.lang.reflect.Method;
|
|||||||
return bufferSize - bytesPending;
|
return bufferSize - bytesPending;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns the duration of audio that is buffered but unplayed. */
|
||||||
|
public long getPendingBufferDurationMs(long writtenFrames) {
|
||||||
|
return C.usToMs(framesToDurationUs(writtenFrames - getPlaybackHeadPosition()));
|
||||||
|
}
|
||||||
|
|
||||||
/** Returns whether the track is in an invalid state and must be recreated. */
|
/** Returns whether the track is in an invalid state and must be recreated. */
|
||||||
public boolean isStalled(long writtenFrames) {
|
public boolean isStalled(long writtenFrames) {
|
||||||
return forceResetWorkaroundTimeMs != C.TIME_UNSET
|
return forceResetWorkaroundTimeMs != C.TIME_UNSET
|
||||||
|
@ -20,6 +20,7 @@ import android.media.AudioFormat;
|
|||||||
import android.media.AudioManager;
|
import android.media.AudioManager;
|
||||||
import android.media.AudioTrack;
|
import android.media.AudioTrack;
|
||||||
import android.os.ConditionVariable;
|
import android.os.ConditionVariable;
|
||||||
|
import android.os.Handler;
|
||||||
import android.os.SystemClock;
|
import android.os.SystemClock;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
@ -274,6 +275,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
private final AudioTrackPositionTracker audioTrackPositionTracker;
|
private final AudioTrackPositionTracker audioTrackPositionTracker;
|
||||||
private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints;
|
private final ArrayDeque<MediaPositionParameters> mediaPositionParametersCheckpoints;
|
||||||
private final boolean enableOffload;
|
private final boolean enableOffload;
|
||||||
|
@MonotonicNonNull private StreamEventCallbackV29 offloadStreamEventCallbackV29;
|
||||||
|
|
||||||
@Nullable private Listener listener;
|
@Nullable private Listener listener;
|
||||||
/** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */
|
/** Used to keep the audio session active on pre-V21 builds (see {@link #initialize(long)}). */
|
||||||
@ -304,7 +306,7 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
@Nullable private ByteBuffer inputBuffer;
|
@Nullable private ByteBuffer inputBuffer;
|
||||||
private int inputBufferAccessUnitCount;
|
private int inputBufferAccessUnitCount;
|
||||||
@Nullable private ByteBuffer outputBuffer;
|
@Nullable private ByteBuffer outputBuffer;
|
||||||
private byte[] preV21OutputBuffer;
|
@MonotonicNonNull private byte[] preV21OutputBuffer;
|
||||||
private int preV21OutputBufferOffset;
|
private int preV21OutputBufferOffset;
|
||||||
private int drainingAudioProcessorIndex;
|
private int drainingAudioProcessorIndex;
|
||||||
private boolean handledEndOfStream;
|
private boolean handledEndOfStream;
|
||||||
@ -366,7 +368,10 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
* be available when float output is in use.
|
* be available when float output is in use.
|
||||||
* @param enableOffload Whether audio offloading is enabled. If an audio format can be both played
|
* @param enableOffload Whether audio offloading is enabled. If an audio format can be both played
|
||||||
* with offload and encoded audio passthrough, it will be played in offload. Audio offload is
|
* with offload and encoded audio passthrough, it will be played in offload. Audio offload is
|
||||||
* supported starting with API 29 ({@link android.os.Build.VERSION_CODES#Q}).
|
* supported starting with API 29 ({@link android.os.Build.VERSION_CODES#Q}). Most Android
|
||||||
|
* devices can only support one offload {@link android.media.AudioTrack} at a time and can
|
||||||
|
* invalidate it at any time. Thus an app can never be guaranteed that it will be able to play
|
||||||
|
* in offload.
|
||||||
*/
|
*/
|
||||||
public DefaultAudioSink(
|
public DefaultAudioSink(
|
||||||
@Nullable AudioCapabilities audioCapabilities,
|
@Nullable AudioCapabilities audioCapabilities,
|
||||||
@ -563,6 +568,9 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
audioTrack =
|
audioTrack =
|
||||||
Assertions.checkNotNull(configuration)
|
Assertions.checkNotNull(configuration)
|
||||||
.buildAudioTrack(tunneling, audioAttributes, audioSessionId);
|
.buildAudioTrack(tunneling, audioAttributes, audioSessionId);
|
||||||
|
if (isOffloadedPlayback(audioTrack)) {
|
||||||
|
registerStreamEventCallbackV29(audioTrack);
|
||||||
|
}
|
||||||
int audioSessionId = audioTrack.getAudioSessionId();
|
int audioSessionId = audioTrack.getAudioSessionId();
|
||||||
if (enablePreV21AudioSessionWorkaround) {
|
if (enablePreV21AudioSessionWorkaround) {
|
||||||
if (Util.SDK_INT < 21) {
|
if (Util.SDK_INT < 21) {
|
||||||
@ -744,6 +752,16 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiresApi(29)
|
||||||
|
private void registerStreamEventCallbackV29(AudioTrack audioTrack) {
|
||||||
|
if (offloadStreamEventCallbackV29 == null) {
|
||||||
|
// Must be lazily initialized to receive stream event callbacks on the current (playback)
|
||||||
|
// thread as the constructor is not called in the playback thread.
|
||||||
|
offloadStreamEventCallbackV29 = new StreamEventCallbackV29();
|
||||||
|
}
|
||||||
|
offloadStreamEventCallbackV29.register(audioTrack);
|
||||||
|
}
|
||||||
|
|
||||||
private void processBuffers(long avSyncPresentationTimeUs) throws WriteException {
|
private void processBuffers(long avSyncPresentationTimeUs) throws WriteException {
|
||||||
int count = activeAudioProcessors.length;
|
int count = activeAudioProcessors.length;
|
||||||
int index = count;
|
int index = count;
|
||||||
@ -822,6 +840,15 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
throw new WriteException(bytesWritten);
|
throw new WriteException(bytesWritten);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (playing
|
||||||
|
&& listener != null
|
||||||
|
&& bytesWritten < bytesRemaining
|
||||||
|
&& isOffloadedPlayback(audioTrack)) {
|
||||||
|
long pendingDurationMs =
|
||||||
|
audioTrackPositionTracker.getPendingBufferDurationMs(writtenEncodedFrames);
|
||||||
|
listener.onOffloadBufferFull(pendingDurationMs);
|
||||||
|
}
|
||||||
|
|
||||||
if (configuration.isInputPcm) {
|
if (configuration.isInputPcm) {
|
||||||
writtenPcmBytes += bytesWritten;
|
writtenPcmBytes += bytesWritten;
|
||||||
}
|
}
|
||||||
@ -1040,6 +1067,9 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
if (audioTrackPositionTracker.isPlaying()) {
|
if (audioTrackPositionTracker.isPlaying()) {
|
||||||
audioTrack.pause();
|
audioTrack.pause();
|
||||||
}
|
}
|
||||||
|
if (isOffloadedPlayback(audioTrack)) {
|
||||||
|
Assertions.checkNotNull(offloadStreamEventCallbackV29).unregister(audioTrack);
|
||||||
|
}
|
||||||
// AudioTrack.release can take some time, so we call it on a background thread.
|
// AudioTrack.release can take some time, so we call it on a background thread.
|
||||||
final AudioTrack toRelease = audioTrack;
|
final AudioTrack toRelease = audioTrack;
|
||||||
audioTrack = null;
|
audioTrack = null;
|
||||||
@ -1229,6 +1259,36 @@ public final class DefaultAudioSink implements AudioSink {
|
|||||||
audioFormat, audioAttributes.getAudioAttributesV21());
|
audioFormat, audioAttributes.getAudioAttributesV21());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static boolean isOffloadedPlayback(AudioTrack audioTrack) {
|
||||||
|
return Util.SDK_INT >= 29 && audioTrack.isOffloadedPlayback();
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(29)
|
||||||
|
private final class StreamEventCallbackV29 extends AudioTrack.StreamEventCallback {
|
||||||
|
private final Handler handler;
|
||||||
|
|
||||||
|
public StreamEventCallbackV29() {
|
||||||
|
handler = new Handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDataRequest(AudioTrack track, int size) {
|
||||||
|
Assertions.checkState(track == DefaultAudioSink.this.audioTrack);
|
||||||
|
if (listener != null) {
|
||||||
|
listener.onOffloadBufferEmptying();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void register(AudioTrack audioTrack) {
|
||||||
|
audioTrack.registerStreamEventCallback(handler::post, this);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unregister(AudioTrack audioTrack) {
|
||||||
|
audioTrack.unregisterStreamEventCallback(this);
|
||||||
|
handler.removeCallbacksAndMessages(/* token= */ null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) {
|
private static AudioTrack initializeKeepSessionIdAudioTrack(int audioSessionId) {
|
||||||
int sampleRate = 4000; // Equal to private AudioTrack.MIN_SAMPLE_RATE.
|
int sampleRate = 4000; // Equal to private AudioTrack.MIN_SAMPLE_RATE.
|
||||||
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
int channelConfig = AudioFormat.CHANNEL_OUT_MONO;
|
||||||
|
@ -92,6 +92,8 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
private boolean allowFirstBufferPositionDiscontinuity;
|
private boolean allowFirstBufferPositionDiscontinuity;
|
||||||
private boolean allowPositionDiscontinuity;
|
private boolean allowPositionDiscontinuity;
|
||||||
|
|
||||||
|
@Nullable private WakeupListener wakeupListener;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param context A context.
|
* @param context A context.
|
||||||
* @param mediaCodecSelector A decoder selector.
|
* @param mediaCodecSelector A decoder selector.
|
||||||
@ -696,6 +698,9 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
case MSG_SET_AUDIO_SESSION_ID:
|
case MSG_SET_AUDIO_SESSION_ID:
|
||||||
audioSink.setAudioSessionId((Integer) message);
|
audioSink.setAudioSessionId((Integer) message);
|
||||||
break;
|
break;
|
||||||
|
case MSG_SET_WAKEUP_LISTENER:
|
||||||
|
this.wakeupListener = (WakeupListener) message;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
super.handleMessage(messageType, message);
|
super.handleMessage(messageType, message);
|
||||||
break;
|
break;
|
||||||
@ -875,5 +880,19 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
|
|||||||
eventDispatcher.skipSilenceEnabledChanged(skipSilenceEnabled);
|
eventDispatcher.skipSilenceEnabledChanged(skipSilenceEnabled);
|
||||||
onAudioTrackSkipSilenceEnabledChanged(skipSilenceEnabled);
|
onAudioTrackSkipSilenceEnabledChanged(skipSilenceEnabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOffloadBufferEmptying() {
|
||||||
|
if (wakeupListener != null) {
|
||||||
|
wakeupListener.onWakeup();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onOffloadBufferFull(long bufferEmptyingDeadlineMs) {
|
||||||
|
if (wakeupListener != null) {
|
||||||
|
wakeupListener.onSleep(bufferEmptyingDeadlineMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -465,4 +465,9 @@ public abstract class StubExoPlayer extends BasePlayer implements ExoPlayer {
|
|||||||
public boolean getPauseAtEndOfMediaItems() {
|
public boolean getPauseAtEndOfMediaItems() {
|
||||||
throw new UnsupportedOperationException();
|
throw new UnsupportedOperationException();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void experimental_enableOffloadScheduling(boolean enableOffloadScheduling) {
|
||||||
|
throw new UnsupportedOperationException();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user