Add onAudioTrackInitialized/Released events

This is useful for analytics and understanding player behavior
during transitions.

#minor-release

PiperOrigin-RevId: 570623227
(cherry picked from commit 8e2bf21011c63e2ca2fc58c4353cd66930b621e3)
This commit is contained in:
tonihei 2023-10-04 01:47:28 -07:00 committed by oceanjules
parent 55633658c6
commit 6d2bf513fb
11 changed files with 262 additions and 19 deletions

View File

@ -8,6 +8,9 @@
* Track Selection:
* Extractors:
* Audio:
* Add `onAudioTrackInitialized` and `onAudioTrackReleased` callbacks to
`AnalyticsListener`, `AudioRendererEventListener` and
`AudioSink.Listener`.
* Video:
* Text:
* Metadata:

View File

@ -97,6 +97,7 @@ import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector;
import androidx.media3.exoplayer.analytics.MediaMetricsListener;
import androidx.media3.exoplayer.analytics.PlayerId;
import androidx.media3.exoplayer.audio.AudioRendererEventListener;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.metadata.MetadataOutput;
import androidx.media3.exoplayer.source.MaskingMediaSource;
import androidx.media3.exoplayer.source.MediaSource;
@ -3120,6 +3121,16 @@ import java.util.concurrent.TimeoutException;
analyticsCollector.onAudioCodecError(audioCodecError);
}
@Override
public void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig) {
analyticsCollector.onAudioTrackInitialized(audioTrackConfig);
}
@Override
public void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig) {
analyticsCollector.onAudioTrackReleased(audioTrackConfig);
}
// TextOutput implementation
@Override
public void onCues(List<Cue> cues) {

View File

@ -179,6 +179,22 @@ public interface AnalyticsCollector
*/
void onAudioCodecError(Exception audioCodecError);
/**
* Called when an {@link AudioTrack} has been initialized.
*
* @param audioTrackConfig The {@link AudioSink.AudioTrackConfig} of the initialized {@link
* AudioTrack}.
*/
void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig);
/**
* Called when an {@link AudioTrack} has been released.
*
* @param audioTrackConfig The {@link AudioSink.AudioTrackConfig} of the released {@link
* AudioTrack}.
*/
void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig);
// Video events.
/**

View File

@ -22,6 +22,7 @@ import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.PARAMETER;
import static java.lang.annotation.ElementType.TYPE_USE;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.os.Looper;
@ -232,6 +233,8 @@ public interface AnalyticsListener {
EVENT_PLAYER_RELEASED,
EVENT_AUDIO_CODEC_ERROR,
EVENT_VIDEO_CODEC_ERROR,
EVENT_AUDIO_TRACK_INITIALIZED,
EVENT_AUDIO_TRACK_RELEASED
})
@interface EventFlags {}
@ -435,6 +438,12 @@ public interface AnalyticsListener {
/** The video codec encountered an error. */
@UnstableApi int EVENT_VIDEO_CODEC_ERROR = 1030;
/** An audio track has been initialized. */
@UnstableApi int EVENT_AUDIO_TRACK_INITIALIZED = 1031;
/** An audio track has been released. */
@UnstableApi int EVENT_AUDIO_TRACK_RELEASED = 1032;
/** Time information of an event. */
@UnstableApi
final class EventTime {
@ -1113,6 +1122,28 @@ public interface AnalyticsListener {
@UnstableApi
default void onAudioCodecError(EventTime eventTime, Exception audioCodecError) {}
/**
* Called when an {@link AudioTrack} has been initialized.
*
* @param eventTime The event time.
* @param audioTrackConfig The {@link AudioSink.AudioTrackConfig} of the initialized {@link
* AudioTrack}.
*/
@UnstableApi
default void onAudioTrackInitialized(
EventTime eventTime, AudioSink.AudioTrackConfig audioTrackConfig) {}
/**
* Called when an {@link AudioTrack} has been released.
*
* @param eventTime The event time.
* @param audioTrackConfig The {@link AudioSink.AudioTrackConfig} of the released {@link
* AudioTrack}.
*/
@UnstableApi
default void onAudioTrackReleased(
EventTime eventTime, AudioSink.AudioTrackConfig audioTrackConfig) {}
/**
* Called when the volume changes.
*

View File

@ -52,6 +52,7 @@ import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.DecoderReuseEvaluation;
import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
@ -263,6 +264,24 @@ public class DefaultAnalyticsCollector implements AnalyticsCollector {
listener -> listener.onAudioCodecError(eventTime, audioCodecError));
}
@Override
public void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_AUDIO_TRACK_INITIALIZED,
listener -> listener.onAudioTrackInitialized(eventTime, audioTrackConfig));
}
@Override
public void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_AUDIO_TRACK_RELEASED,
listener -> listener.onAudioTrackReleased(eventTime, audioTrackConfig));
}
@Override
public final void onVolumeChanged(float volume) {
EventTime eventTime = generateReadingMediaPeriodEventTime();

View File

@ -150,6 +150,22 @@ public interface AudioRendererEventListener {
*/
default void onAudioSinkError(Exception audioSinkError) {}
/**
* Called when an {@link AudioTrack} has been initialized.
*
* @param audioTrackConfig The {@link AudioSink.AudioTrackConfig} of the initialized {@link
* AudioTrack}.
*/
default void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig) {}
/**
* Called when an {@link AudioTrack} has been released.
*
* @param audioTrackConfig The {@link AudioSink.AudioTrackConfig} of the released {@link
* AudioTrack}.
*/
default void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig) {}
/** Dispatches events to an {@link AudioRendererEventListener}. */
final class EventDispatcher {
@ -256,5 +272,19 @@ public interface AudioRendererEventListener {
handler.post(() -> castNonNull(listener).onAudioCodecError(audioCodecError));
}
}
/** Invokes {@link AudioRendererEventListener#onAudioTrackInitialized}. */
public void audioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onAudioTrackInitialized(audioTrackConfig));
}
}
/** Invokes {@link AudioRendererEventListener#onAudioTrackReleased}. */
public void audioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig) {
if (handler != null) {
handler.post(() -> castNonNull(listener).onAudioTrackReleased(audioTrackConfig));
}
}
}
}

View File

@ -138,6 +138,68 @@ public interface AudioSink {
/** Called when audio capabilities changed. */
default void onAudioCapabilitiesChanged() {}
/**
* Called when an {@link AudioTrack} has been initialized.
*
* @param audioTrackConfig The {@link AudioTrackConfig} of the initialized {@link AudioTrack}.
*/
default void onAudioTrackInitialized(AudioTrackConfig audioTrackConfig) {}
/**
* Called when an {@link AudioTrack} has been released.
*
* @param audioTrackConfig The {@link AudioTrackConfig} of the released {@link AudioTrack}.
*/
default void onAudioTrackReleased(AudioTrackConfig audioTrackConfig) {}
}
/** Configuration parameters used for an {@link AudioTrack}. */
final class AudioTrackConfig {
/* The {@link C.Encoding} of the audio data. */
public final @C.Encoding int encoding;
/** The sample rate of the audio data. */
public final int sampleRate;
/** The channel configuration of the track. See {@code AudioTrack.CHANNEL_OUT_XXX} constants. */
public final int channelConfig;
/** Whether tunneling is enabled for this track. */
public final boolean tunneling;
/** Whether offload is enabled for this track. */
public final boolean offload;
/** The buffer size of the track in bytes. */
public final int bufferSize;
/**
* Creates the audio track configuration parameters.
*
* @param encoding The {@link C.Encoding} of the audio data
* @param sampleRate The sample rate of the audio data.
* @param channelConfig The channel configuration of the track. See {@code
* AudioTrack.CHANNEL_OUT_XXX} constants.
* @param tunneling Whether tunneling is enabled for this track.
* @param offload Whether offload is enabled for this track.
* @param bufferSize The buffer size of the track in bytes.
*/
public AudioTrackConfig(
@C.Encoding int encoding,
int sampleRate,
int channelConfig,
boolean tunneling,
boolean offload,
int bufferSize) {
this.encoding = encoding;
this.sampleRate = sampleRate;
this.channelConfig = channelConfig;
this.tunneling = tunneling;
this.offload = offload;
this.bufferSize = bufferSize;
}
}
/** Thrown when a failure occurs configuring the sink. */

View File

@ -856,6 +856,16 @@ public abstract class DecoderAudioRenderer<
Log.e(TAG, "Audio sink error", audioSinkError);
eventDispatcher.audioSinkError(audioSinkError);
}
@Override
public void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig) {
eventDispatcher.audioTrackInitialized(audioTrackConfig);
}
@Override
public void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig) {
eventDispatcher.audioTrackReleased(audioTrackConfig);
}
}
@RequiresApi(23)

View File

@ -767,7 +767,8 @@ public final class DefaultAudioSink implements AudioSink {
bufferSize,
audioProcessingPipeline,
enableAudioTrackPlaybackParams,
enableOffloadGapless);
enableOffloadGapless,
tunneling);
if (isAudioTrackInitialized()) {
this.pendingConfiguration = pendingConfiguration;
} else {
@ -817,8 +818,12 @@ public final class DefaultAudioSink implements AudioSink {
if (preferredDevice != null && Util.SDK_INT >= 23) {
Api23.setPreferredDeviceOnAudioTrack(audioTrack, preferredDevice);
}
startMediaTimeUsNeedsInit = true;
if (listener != null) {
listener.onAudioTrackInitialized(configuration.buildAudioTrackConfig());
}
return true;
}
@ -1021,8 +1026,7 @@ public final class DefaultAudioSink implements AudioSink {
private AudioTrack buildAudioTrack(Configuration configuration) throws InitializationException {
try {
AudioTrack audioTrack =
configuration.buildAudioTrack(tunneling, audioAttributes, audioSessionId);
AudioTrack audioTrack = configuration.buildAudioTrack(audioAttributes, audioSessionId);
if (audioOffloadListener != null) {
audioOffloadListener.onOffloadedPlayback(isOffloadedPlayback(audioTrack));
}
@ -1427,12 +1431,13 @@ public final class DefaultAudioSink implements AudioSink {
// we next create an audio track.
audioSessionId = C.AUDIO_SESSION_ID_UNSET;
}
AudioTrackConfig oldAudioTrackConfig = configuration.buildAudioTrackConfig();
if (pendingConfiguration != null) {
configuration = pendingConfiguration;
pendingConfiguration = null;
}
audioTrackPositionTracker.reset();
releaseAudioTrackAsync(audioTrack, releasingConditionVariable);
releaseAudioTrackAsync(audioTrack, releasingConditionVariable, listener, oldAudioTrackConfig);
audioTrack = null;
}
writeExceptionPendingExceptionHolder.clear();
@ -1823,11 +1828,15 @@ public final class DefaultAudioSink implements AudioSink {
}
private static void releaseAudioTrackAsync(
AudioTrack audioTrack, ConditionVariable releasedConditionVariable) {
AudioTrack audioTrack,
ConditionVariable releasedConditionVariable,
@Nullable Listener listener,
AudioTrackConfig audioTrackConfig) {
// AudioTrack.release can take some time, so we call it on a background thread. The background
// thread is shared statically to avoid creating many threads when multiple players are released
// at the same time.
releasedConditionVariable.close();
Handler audioTrackThreadHandler = new Handler(Looper.myLooper());
synchronized (releaseExecutorLock) {
if (releaseExecutor == null) {
releaseExecutor = Util.newSingleThreadExecutor("ExoPlayer:AudioTrackReleaseThread");
@ -1839,6 +1848,9 @@ public final class DefaultAudioSink implements AudioSink {
audioTrack.flush();
audioTrack.release();
} finally {
if (listener != null && audioTrackThreadHandler.getLooper().getThread().isAlive()) {
audioTrackThreadHandler.post(() -> listener.onAudioTrackReleased(audioTrackConfig));
}
releasedConditionVariable.open();
synchronized (releaseExecutorLock) {
pendingReleaseCount--;
@ -2018,6 +2030,7 @@ public final class DefaultAudioSink implements AudioSink {
public final AudioProcessingPipeline audioProcessingPipeline;
public final boolean enableAudioTrackPlaybackParams;
public final boolean enableOffloadGapless;
public final boolean tunneling;
public Configuration(
Format inputFormat,
@ -2030,7 +2043,8 @@ public final class DefaultAudioSink implements AudioSink {
int bufferSize,
AudioProcessingPipeline audioProcessingPipeline,
boolean enableAudioTrackPlaybackParams,
boolean enableOffloadGapless) {
boolean enableOffloadGapless,
boolean tunneling) {
this.inputFormat = inputFormat;
this.inputPcmFrameSize = inputPcmFrameSize;
this.outputMode = outputMode;
@ -2042,6 +2056,7 @@ public final class DefaultAudioSink implements AudioSink {
this.audioProcessingPipeline = audioProcessingPipeline;
this.enableAudioTrackPlaybackParams = enableAudioTrackPlaybackParams;
this.enableOffloadGapless = enableOffloadGapless;
this.tunneling = tunneling;
}
public Configuration copyWithBufferSize(int bufferSize) {
@ -2056,7 +2071,8 @@ public final class DefaultAudioSink implements AudioSink {
bufferSize,
audioProcessingPipeline,
enableAudioTrackPlaybackParams,
enableOffloadGapless);
enableOffloadGapless,
tunneling);
}
/** Returns if the configurations are sufficiently compatible to reuse the audio track. */
@ -2078,12 +2094,21 @@ public final class DefaultAudioSink implements AudioSink {
return Util.sampleCountToDurationUs(frameCount, outputSampleRate);
}
public AudioTrack buildAudioTrack(
boolean tunneling, AudioAttributes audioAttributes, int audioSessionId)
public AudioTrackConfig buildAudioTrackConfig() {
return new AudioTrackConfig(
outputEncoding,
outputSampleRate,
outputChannelConfig,
tunneling,
outputMode == OUTPUT_MODE_OFFLOAD,
bufferSize);
}
public AudioTrack buildAudioTrack(AudioAttributes audioAttributes, int audioSessionId)
throws InitializationException {
AudioTrack audioTrack;
try {
audioTrack = createAudioTrack(tunneling, audioAttributes, audioSessionId);
audioTrack = createAudioTrack(audioAttributes, audioSessionId);
} catch (UnsupportedOperationException | IllegalArgumentException e) {
throw new InitializationException(
AudioTrack.STATE_UNINITIALIZED,
@ -2115,20 +2140,18 @@ public final class DefaultAudioSink implements AudioSink {
return audioTrack;
}
private AudioTrack createAudioTrack(
boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) {
private AudioTrack createAudioTrack(AudioAttributes audioAttributes, int audioSessionId) {
if (Util.SDK_INT >= 29) {
return createAudioTrackV29(tunneling, audioAttributes, audioSessionId);
return createAudioTrackV29(audioAttributes, audioSessionId);
} else if (Util.SDK_INT >= 21) {
return createAudioTrackV21(tunneling, audioAttributes, audioSessionId);
return createAudioTrackV21(audioAttributes, audioSessionId);
} else {
return createAudioTrackV9(audioAttributes, audioSessionId);
}
}
@RequiresApi(29)
private AudioTrack createAudioTrackV29(
boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) {
private AudioTrack createAudioTrackV29(AudioAttributes audioAttributes, int audioSessionId) {
AudioFormat audioFormat =
Util.getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding);
android.media.AudioAttributes audioTrackAttributes =
@ -2144,8 +2167,7 @@ public final class DefaultAudioSink implements AudioSink {
}
@RequiresApi(21)
private AudioTrack createAudioTrackV21(
boolean tunneling, AudioAttributes audioAttributes, int audioSessionId) {
private AudioTrack createAudioTrackV21(AudioAttributes audioAttributes, int audioSessionId) {
return new AudioTrack(
getAudioTrackAttributesV21(audioAttributes, tunneling),
Util.getAudioFormat(outputSampleRate, outputChannelConfig, outputEncoding),

View File

@ -1022,6 +1022,16 @@ public class MediaCodecAudioRenderer extends MediaCodecRenderer implements Media
public void onAudioCapabilitiesChanged() {
MediaCodecAudioRenderer.this.onRendererCapabilitiesChanged();
}
@Override
public void onAudioTrackInitialized(AudioSink.AudioTrackConfig audioTrackConfig) {
eventDispatcher.audioTrackInitialized(audioTrackConfig);
}
@Override
public void onAudioTrackReleased(AudioSink.AudioTrackConfig audioTrackConfig) {
eventDispatcher.audioTrackReleased(audioTrackConfig);
}
}
@RequiresApi(23)

View File

@ -38,6 +38,7 @@ import androidx.media3.common.util.UnstableApi;
import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.DecoderReuseEvaluation;
import androidx.media3.exoplayer.analytics.AnalyticsListener;
import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.source.LoadEventInfo;
import androidx.media3.exoplayer.source.MediaLoadData;
@ -401,6 +402,20 @@ public class EventLogger implements AnalyticsListener {
logd(eventTime, "volume", Float.toString(volume));
}
@UnstableApi
@Override
public void onAudioTrackInitialized(
EventTime eventTime, AudioSink.AudioTrackConfig audioTrackConfig) {
logd(eventTime, "audioTrackInit", getAudioTrackConfigString(audioTrackConfig));
}
@UnstableApi
@Override
public void onAudioTrackReleased(
EventTime eventTime, AudioSink.AudioTrackConfig audioTrackConfig) {
logd(eventTime, "audioTrackReleased", getAudioTrackConfigString(audioTrackConfig));
}
@UnstableApi
@Override
public void onVideoEnabled(EventTime eventTime, DecoderCounters decoderCounters) {
@ -745,4 +760,18 @@ public class EventLogger implements AnalyticsListener {
return "?";
}
}
private static String getAudioTrackConfigString(AudioSink.AudioTrackConfig audioTrackConfig) {
return audioTrackConfig.encoding
+ ","
+ audioTrackConfig.channelConfig
+ ","
+ audioTrackConfig.sampleRate
+ ","
+ audioTrackConfig.tunneling
+ ","
+ audioTrackConfig.offload
+ ","
+ audioTrackConfig.bufferSize;
}
}