Add missing events to AnalyticsListener.

And also add a test that all Player.Listener events are forwarded
to AnalyticsListener.

The AnalyticsCollector also needlessly implemented
Video/AudioRendererEventListener, which is not needed because all of
the equivalent methods are called directly and never through the
interface.

#minor-release

PiperOrigin-RevId: 427478000
This commit is contained in:
tonihei 2022-02-09 16:27:00 +00:00 committed by Ian Baker
parent 018631320b
commit c3cb2f7cfb
5 changed files with 376 additions and 111 deletions

View File

@ -106,12 +106,12 @@ public class ForwardingPlayerTest {
public void forwardingPlayer_overridesAllPlayerMethods() throws Exception { public void forwardingPlayer_overridesAllPlayerMethods() throws Exception {
// Check with reflection that ForwardingPlayer overrides all Player methods. // Check with reflection that ForwardingPlayer overrides all Player methods.
List<Method> methods = getPublicMethods(Player.class); List<Method> methods = getPublicMethods(Player.class);
for (int i = 0; i < methods.size(); i++) { for (Method method : methods) {
Method method = methods.get(i);
assertThat( assertThat(
ForwardingPlayer.class.getDeclaredMethod( ForwardingPlayer.class
method.getName(), method.getParameterTypes())) .getDeclaredMethod(method.getName(), method.getParameterTypes())
.isNotNull(); .getDeclaringClass())
.isEqualTo(ForwardingPlayer.class);
} }
} }
@ -120,10 +120,12 @@ public class ForwardingPlayerTest {
// Check with reflection that ForwardingListener overrides all Listener methods. // Check with reflection that ForwardingListener overrides all Listener methods.
Class<?> forwardingListenerClass = getInnerClass("ForwardingListener"); Class<?> forwardingListenerClass = getInnerClass("ForwardingListener");
List<Method> methods = getPublicMethods(Player.Listener.class); List<Method> methods = getPublicMethods(Player.Listener.class);
for (int i = 0; i < methods.size(); i++) { for (Method method : methods) {
Method method = methods.get(i); assertThat(
assertThat(forwardingListenerClass.getMethod(method.getName(), method.getParameterTypes())) forwardingListenerClass
.isNotNull(); .getMethod(method.getName(), method.getParameterTypes())
.getDeclaringClass())
.isEqualTo(forwardingListenerClass);
} }
} }

View File

@ -19,12 +19,18 @@ import static androidx.media3.common.util.Assertions.checkNotNull;
import static androidx.media3.common.util.Assertions.checkState; import static androidx.media3.common.util.Assertions.checkState;
import static androidx.media3.common.util.Assertions.checkStateNotNull; import static androidx.media3.common.util.Assertions.checkStateNotNull;
import android.media.AudioTrack;
import android.media.MediaCodec;
import android.media.MediaCodec.CodecException;
import android.os.Looper; import android.os.Looper;
import android.os.SystemClock;
import android.util.SparseArray; import android.util.SparseArray;
import android.view.Surface;
import androidx.annotation.CallSuper; import androidx.annotation.CallSuper;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.AudioAttributes; import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata; import androidx.media3.common.MediaMetadata;
@ -32,24 +38,28 @@ import androidx.media3.common.Metadata;
import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackException;
import androidx.media3.common.PlaybackParameters; import androidx.media3.common.PlaybackParameters;
import androidx.media3.common.Player; import androidx.media3.common.Player;
import androidx.media3.common.Player.DiscontinuityReason;
import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Player.PlaybackSuppressionReason;
import androidx.media3.common.Timeline; import androidx.media3.common.Timeline;
import androidx.media3.common.Timeline.Period; import androidx.media3.common.Timeline.Period;
import androidx.media3.common.Timeline.Window; import androidx.media3.common.Timeline.Window;
import androidx.media3.common.TrackGroupArray; import androidx.media3.common.TrackGroupArray;
import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionArray;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo; import androidx.media3.common.TracksInfo;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.Clock; import androidx.media3.common.util.Clock;
import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.HandlerWrapper;
import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.ListenerSet;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.common.util.Util; import androidx.media3.common.util.Util;
import androidx.media3.decoder.DecoderException;
import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.DecoderCounters;
import androidx.media3.exoplayer.DecoderReuseEvaluation; import androidx.media3.exoplayer.DecoderReuseEvaluation;
import androidx.media3.exoplayer.ExoPlaybackException; import androidx.media3.exoplayer.ExoPlaybackException;
import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime; import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime;
import androidx.media3.exoplayer.audio.AudioRendererEventListener; import androidx.media3.exoplayer.audio.AudioSink;
import androidx.media3.exoplayer.drm.DrmSession; import androidx.media3.exoplayer.drm.DrmSession;
import androidx.media3.exoplayer.drm.DrmSessionEventListener; import androidx.media3.exoplayer.drm.DrmSessionEventListener;
import androidx.media3.exoplayer.source.LoadEventInfo; import androidx.media3.exoplayer.source.LoadEventInfo;
@ -57,7 +67,7 @@ import androidx.media3.exoplayer.source.MediaLoadData;
import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId; import androidx.media3.exoplayer.source.MediaSource.MediaPeriodId;
import androidx.media3.exoplayer.source.MediaSourceEventListener; import androidx.media3.exoplayer.source.MediaSourceEventListener;
import androidx.media3.exoplayer.upstream.BandwidthMeter; import androidx.media3.exoplayer.upstream.BandwidthMeter;
import androidx.media3.exoplayer.video.VideoRendererEventListener; import androidx.media3.exoplayer.video.VideoDecoderOutputBufferRenderer;
import com.google.common.base.Objects; import com.google.common.base.Objects;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
@ -73,8 +83,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull;
@UnstableApi @UnstableApi
public class AnalyticsCollector public class AnalyticsCollector
implements Player.Listener, implements Player.Listener,
AudioRendererEventListener,
VideoRendererEventListener,
MediaSourceEventListener, MediaSourceEventListener,
BandwidthMeter.EventListener, BandwidthMeter.EventListener,
DrmSessionEventListener { DrmSessionEventListener {
@ -185,10 +193,15 @@ public class AnalyticsCollector
} }
} }
// AudioRendererEventListener implementation. // Audio events.
/**
* Called when the audio renderer is enabled.
*
* @param counters {@link DecoderCounters} that will be updated by the audio renderer for as long
* as it remains enabled.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onAudioEnabled(DecoderCounters counters) { public final void onAudioEnabled(DecoderCounters counters) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -200,8 +213,15 @@ public class AnalyticsCollector
}); });
} }
/**
* Called when a audio decoder is created.
*
* @param decoderName The audio decoder that was created.
* @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
* finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onAudioDecoderInitialized( public final void onAudioDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) { String decoderName, long initializedTimestampMs, long initializationDurationMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -217,8 +237,15 @@ public class AnalyticsCollector
}); });
} }
/**
* Called when the format of the media being consumed by the audio renderer changes.
*
* @param format The new format.
* @param decoderReuseEvaluation The result of the evaluation to determine whether an existing
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onAudioInputFormatChanged( public final void onAudioInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -232,7 +259,13 @@ public class AnalyticsCollector
}); });
} }
@Override /**
* Called when the audio position has increased for the first time since the last pause or
* position reset.
*
* @param playoutStartSystemTimeMs The approximate derived {@link System#currentTimeMillis()} at
* which playout started.
*/
public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) { public final void onAudioPositionAdvancing(long playoutStartSystemTimeMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -241,7 +274,14 @@ public class AnalyticsCollector
listener -> listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs)); listener -> listener.onAudioPositionAdvancing(eventTime, playoutStartSystemTimeMs));
} }
@Override /**
* Called when an audio underrun occurs.
*
* @param bufferSize The size of the audio output buffer, in bytes.
* @param bufferSizeMs The size of the audio output buffer, in milliseconds, if it contains PCM
* encoded audio. {@link C#TIME_UNSET} if the output buffer contains non-PCM encoded audio.
* @param elapsedSinceLastFeedMs The time since audio was last written to the output buffer.
*/
public final void onAudioUnderrun( public final void onAudioUnderrun(
int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -252,7 +292,11 @@ public class AnalyticsCollector
listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs)); listener.onAudioUnderrun(eventTime, bufferSize, bufferSizeMs, elapsedSinceLastFeedMs));
} }
@Override /**
* Called when a audio decoder is released.
*
* @param decoderName The audio decoder that was released.
*/
public final void onAudioDecoderReleased(String decoderName) { public final void onAudioDecoderReleased(String decoderName) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -261,8 +305,12 @@ public class AnalyticsCollector
listener -> listener.onAudioDecoderReleased(eventTime, decoderName)); listener -> listener.onAudioDecoderReleased(eventTime, decoderName));
} }
/**
* Called when the audio renderer is disabled.
*
* @param counters {@link DecoderCounters} that were updated by the audio renderer.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onAudioDisabled(DecoderCounters counters) { public final void onAudioDisabled(DecoderCounters counters) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -274,16 +322,16 @@ public class AnalyticsCollector
}); });
} }
@Override /**
public final void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { * Called when {@link AudioSink} has encountered an error.
EventTime eventTime = generateReadingMediaPeriodEventTime(); *
sendEvent( * <p>If the sink writes to a platform {@link AudioTrack}, this will be called for all {@link
eventTime, * AudioTrack} errors.
AnalyticsListener.EVENT_SKIP_SILENCE_ENABLED_CHANGED, *
listener -> listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled)); * @param audioSinkError The error that occurred. Typically an {@link
} * AudioSink.InitializationException}, a {@link AudioSink.WriteException}, or an {@link
* AudioSink.UnexpectedDiscontinuityException}.
@Override */
public final void onAudioSinkError(Exception audioSinkError) { public final void onAudioSinkError(Exception audioSinkError) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -292,7 +340,12 @@ public class AnalyticsCollector
listener -> listener.onAudioSinkError(eventTime, audioSinkError)); listener -> listener.onAudioSinkError(eventTime, audioSinkError));
} }
@Override /**
* Called when an audio decoder encounters an error.
*
* @param audioCodecError The error. Typically a {@link CodecException} if the renderer uses
* {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder.
*/
public final void onAudioCodecError(Exception audioCodecError) { public final void onAudioCodecError(Exception audioCodecError) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -301,34 +354,6 @@ public class AnalyticsCollector
listener -> listener.onAudioCodecError(eventTime, audioCodecError)); listener -> listener.onAudioCodecError(eventTime, audioCodecError));
} }
// Additional audio events.
/**
* Called when the audio session ID changes.
*
* @param audioSessionId The audio session ID.
*/
public final void onAudioSessionIdChanged(int audioSessionId) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_AUDIO_SESSION_ID,
listener -> listener.onAudioSessionIdChanged(eventTime, audioSessionId));
}
/**
* Called when the audio attributes change.
*
* @param audioAttributes The audio attributes.
*/
public final void onAudioAttributesChanged(AudioAttributes audioAttributes) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_AUDIO_ATTRIBUTES_CHANGED,
listener -> listener.onAudioAttributesChanged(eventTime, audioAttributes));
}
/** /**
* Called when the volume changes. * Called when the volume changes.
* *
@ -342,10 +367,15 @@ public class AnalyticsCollector
listener -> listener.onVolumeChanged(eventTime, volume)); listener -> listener.onVolumeChanged(eventTime, volume));
} }
// VideoRendererEventListener implementation. // Video events.
/**
* Called when the video renderer is enabled.
*
* @param counters {@link DecoderCounters} that will be updated by the video renderer for as long
* as it remains enabled.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoEnabled(DecoderCounters counters) { public final void onVideoEnabled(DecoderCounters counters) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -357,8 +387,15 @@ public class AnalyticsCollector
}); });
} }
/**
* Called when a video decoder is created.
*
* @param decoderName The decoder that was created.
* @param initializedTimestampMs {@link SystemClock#elapsedRealtime()} when initialization
* finished.
* @param initializationDurationMs The time taken to initialize the decoder in milliseconds.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoDecoderInitialized( public final void onVideoDecoderInitialized(
String decoderName, long initializedTimestampMs, long initializationDurationMs) { String decoderName, long initializedTimestampMs, long initializationDurationMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -374,8 +411,15 @@ public class AnalyticsCollector
}); });
} }
/**
* Called when the format of the media being consumed by the video renderer changes.
*
* @param format The new format.
* @param decoderReuseEvaluation The result of the evaluation to determine whether an existing
* decoder instance can be reused for the new format, or {@code null} if the renderer did not
* have a decoder.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoInputFormatChanged( public final void onVideoInputFormatChanged(
Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
@ -389,7 +433,16 @@ public class AnalyticsCollector
}); });
} }
@Override /**
* Called to report the number of frames dropped by the video renderer. Dropped frames are
* reported whenever the renderer is stopped having dropped frames, and optionally, whenever the
* count reaches a specified threshold whilst the renderer is started.
*
* @param count The number of dropped frames.
* @param elapsedMs The duration in milliseconds over which the frames were dropped. This duration
* is timed from when the renderer was started or from when dropped frames were last reported
* (whichever was more recent), and not from when the first of the reported drops occurred.
*/
public final void onDroppedFrames(int count, long elapsedMs) { public final void onDroppedFrames(int count, long elapsedMs) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -398,7 +451,11 @@ public class AnalyticsCollector
listener -> listener.onDroppedVideoFrames(eventTime, count, elapsedMs)); listener -> listener.onDroppedVideoFrames(eventTime, count, elapsedMs));
} }
@Override /**
* Called when a video decoder is released.
*
* @param decoderName The video decoder that was released.
*/
public final void onVideoDecoderReleased(String decoderName) { public final void onVideoDecoderReleased(String decoderName) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -407,8 +464,12 @@ public class AnalyticsCollector
listener -> listener.onVideoDecoderReleased(eventTime, decoderName)); listener -> listener.onVideoDecoderReleased(eventTime, decoderName));
} }
/**
* Called when the video renderer is disabled.
*
* @param counters {@link DecoderCounters} that were updated by the video renderer.
*/
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoDisabled(DecoderCounters counters) { public final void onVideoDisabled(DecoderCounters counters) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -420,25 +481,14 @@ public class AnalyticsCollector
}); });
} }
@SuppressWarnings("deprecation") // Calling deprecated listener method. /**
@Override * Called when a frame is rendered for the first time since setting the output, or since the
public final void onVideoSizeChanged(VideoSize videoSize) { * renderer was reset, or since the stream being rendered was changed.
EventTime eventTime = generateReadingMediaPeriodEventTime(); *
sendEvent( * @param output The output of the video renderer. Normally a {@link Surface}, however some video
eventTime, * renderers may have other output types (e.g., a {@link VideoDecoderOutputBufferRenderer}).
AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED, * @param renderTimeMs The {@link SystemClock#elapsedRealtime()} when the frame was rendered.
listener -> { */
listener.onVideoSizeChanged(eventTime, videoSize);
listener.onVideoSizeChanged(
eventTime,
videoSize.width,
videoSize.height,
videoSize.unappliedRotationDegrees,
videoSize.pixelWidthHeightRatio);
});
}
@Override
public final void onRenderedFirstFrame(Object output, long renderTimeMs) { public final void onRenderedFirstFrame(Object output, long renderTimeMs) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -447,7 +497,24 @@ public class AnalyticsCollector
listener -> listener.onRenderedFirstFrame(eventTime, output, renderTimeMs)); listener -> listener.onRenderedFirstFrame(eventTime, output, renderTimeMs));
} }
@Override /**
* Called to report the video processing offset of video frames processed by the video renderer.
*
* <p>Video processing offset represents how early a video frame is processed compared to the
* player's current position. For each video frame, the offset is calculated as <em>P<sub>vf</sub>
* - P<sub>pl</sub></em> where <em>P<sub>vf</sub></em> is the presentation timestamp of the video
* frame and <em>P<sub>pl</sub></em> is the current position of the player. Positive values
* indicate the frame was processed early enough whereas negative values indicate that the
* player's position had progressed beyond the frame's timestamp when the frame was processed (and
* the frame was probably dropped).
*
* <p>The renderer reports the sum of video processing offset samples (one sample per processed
* video frame: dropped, skipped or rendered) and the total number of samples.
*
* @param totalProcessingOffsetUs The sum of all video frame processing offset samples for the
* video frames processed by the renderer in microseconds.
* @param frameCount The number of samples included in the {@code totalProcessingOffsetUs}.
*/
public final void onVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount) { public final void onVideoFrameProcessingOffset(long totalProcessingOffsetUs, int frameCount) {
EventTime eventTime = generatePlayingMediaPeriodEventTime(); EventTime eventTime = generatePlayingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -457,7 +524,19 @@ public class AnalyticsCollector
listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount)); listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount));
} }
@Override /**
* Called when a video decoder encounters an error.
*
* <p>This method being called does not indicate that playback has failed, or that it will fail.
* The player may be able to recover from the error. Hence applications should <em>not</em>
* implement this method to display a user visible error or initiate an application level retry.
* {@link Player.Listener#onPlayerError} is the appropriate place to implement such behavior. This
* method is called to provide the application with an opportunity to log the error if it wishes
* to do so.
*
* @param videoCodecError The error. Typically a {@link CodecException} if the renderer uses
* {@link MediaCodec}, or a {@link DecoderException} if the renderer uses a software decoder.
*/
public final void onVideoCodecError(Exception videoCodecError) { public final void onVideoCodecError(Exception videoCodecError) {
EventTime eventTime = generateReadingMediaPeriodEventTime(); EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent( sendEvent(
@ -466,8 +545,6 @@ public class AnalyticsCollector
listener -> listener.onVideoCodecError(eventTime, videoCodecError)); listener -> listener.onVideoCodecError(eventTime, videoCodecError));
} }
// Additional video events.
/** /**
* Called each time there's a change in the size of the surface onto which the video is being * Called each time there's a change in the size of the surface onto which the video is being
* rendered. * rendered.
@ -608,6 +685,12 @@ public class AnalyticsCollector
listener -> listener.onTracksInfoChanged(eventTime, tracksInfo)); listener -> listener.onTracksInfoChanged(eventTime, tracksInfo));
} }
@SuppressWarnings("deprecation") // Implementing deprecated method.
@Override
public void onLoadingChanged(boolean isLoading) {
// Do nothing. Handled by non-deprecated onIsLoadingChanged.
}
@SuppressWarnings("deprecation") // Calling deprecated listener method. @SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override @Override
public final void onIsLoadingChanged(boolean isLoading) { public final void onIsLoadingChanged(boolean isLoading) {
@ -699,21 +782,26 @@ public class AnalyticsCollector
@Override @Override
public final void onPlayerError(PlaybackException error) { public final void onPlayerError(PlaybackException error) {
@Nullable EventTime eventTime = null; EventTime eventTime = getEventTimeForErrorEvent(error);
if (error instanceof ExoPlaybackException) {
ExoPlaybackException exoError = (ExoPlaybackException) error;
if (exoError.mediaPeriodId != null) {
eventTime = generateEventTime(new MediaPeriodId(exoError.mediaPeriodId));
}
}
if (eventTime == null) {
eventTime = generateCurrentPlayerMediaPeriodEventTime();
}
EventTime finalEventTime = eventTime;
sendEvent( sendEvent(
eventTime, eventTime,
AnalyticsListener.EVENT_PLAYER_ERROR, AnalyticsListener.EVENT_PLAYER_ERROR,
listener -> listener.onPlayerError(finalEventTime, error)); listener -> listener.onPlayerError(eventTime, error));
}
@Override
public void onPlayerErrorChanged(@Nullable PlaybackException error) {
EventTime eventTime = getEventTimeForErrorEvent(error);
sendEvent(
eventTime,
AnalyticsListener.EVENT_PLAYER_ERROR,
listener -> listener.onPlayerErrorChanged(eventTime, error));
}
@SuppressWarnings("deprecation") // Implementing deprecated method.
@Override
public void onPositionDiscontinuity(@DiscontinuityReason int reason) {
// Do nothing. Handled by non-deprecated onPositionDiscontinuity.
} }
// Calling deprecated callback. // Calling deprecated callback.
@ -801,6 +889,13 @@ public class AnalyticsCollector
listener -> listener.onMetadata(eventTime, metadata)); listener -> listener.onMetadata(eventTime, metadata));
} }
@Override
public void onCues(List<Cue> cues) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
sendEvent(
eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cues));
}
@SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method.
@Override @Override
public final void onSeekProcessed() { public final void onSeekProcessed() {
@ -809,6 +904,89 @@ public class AnalyticsCollector
eventTime, /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekProcessed(eventTime)); eventTime, /* eventFlag= */ C.INDEX_UNSET, listener -> listener.onSeekProcessed(eventTime));
} }
@Override
public final void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_SKIP_SILENCE_ENABLED_CHANGED,
listener -> listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled));
}
@Override
public final void onAudioSessionIdChanged(int audioSessionId) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_AUDIO_SESSION_ID,
listener -> listener.onAudioSessionIdChanged(eventTime, audioSessionId));
}
@Override
public final void onAudioAttributesChanged(AudioAttributes audioAttributes) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_AUDIO_ATTRIBUTES_CHANGED,
listener -> listener.onAudioAttributesChanged(eventTime, audioAttributes));
}
@SuppressWarnings("deprecation") // Calling deprecated listener method.
@Override
public final void onVideoSizeChanged(VideoSize videoSize) {
EventTime eventTime = generateReadingMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_VIDEO_SIZE_CHANGED,
listener -> {
listener.onVideoSizeChanged(eventTime, videoSize);
listener.onVideoSizeChanged(
eventTime,
videoSize.width,
videoSize.height,
videoSize.unappliedRotationDegrees,
videoSize.pixelWidthHeightRatio);
});
}
@Override
public void onTrackSelectionParametersChanged(TrackSelectionParameters parameters) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
listener -> listener.onTrackSelectionParametersChanged(eventTime, parameters));
}
@Override
public void onDeviceInfoChanged(DeviceInfo deviceInfo) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_DEVICE_INFO_CHANGED,
listener -> listener.onDeviceInfoChanged(eventTime, deviceInfo));
}
@Override
public void onDeviceVolumeChanged(int volume, boolean muted) {
EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime();
sendEvent(
eventTime,
AnalyticsListener.EVENT_DEVICE_VOLUME_CHANGED,
listener -> listener.onDeviceVolumeChanged(eventTime, volume, muted));
}
@SuppressWarnings("UngroupedOverloads") // Grouped by interface.
@Override
public void onRenderedFirstFrame() {
// Do nothing. Handled by onRenderedFirstFrame call with additional parameters.
}
@Override
public void onEvents(Player player, Player.Events events) {
// Do nothing. AnalyticsCollector issues its own onEvents.
}
// BandwidthMeter.EventListener implementation. // BandwidthMeter.EventListener implementation.
@Override @Override
@ -1002,6 +1180,16 @@ public class AnalyticsCollector
windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null); windowIsInTimeline ? timeline : Timeline.EMPTY, windowIndex, /* mediaPeriodId= */ null);
} }
private EventTime getEventTimeForErrorEvent(@Nullable PlaybackException error) {
if (error instanceof ExoPlaybackException) {
ExoPlaybackException exoError = (ExoPlaybackException) error;
if (exoError.mediaPeriodId != null) {
return generateEventTime(new MediaPeriodId(exoError.mediaPeriodId));
}
}
return generateCurrentPlayerMediaPeriodEventTime();
}
/** Keeps track of the active media periods and currently playing and reading media period. */ /** Keeps track of the active media periods and currently playing and reading media period. */
private static final class MediaPeriodQueueTracker { private static final class MediaPeriodQueueTracker {

View File

@ -32,6 +32,7 @@ import androidx.annotation.IntDef;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.media3.common.AudioAttributes; import androidx.media3.common.AudioAttributes;
import androidx.media3.common.C; import androidx.media3.common.C;
import androidx.media3.common.DeviceInfo;
import androidx.media3.common.FlagSet; import androidx.media3.common.FlagSet;
import androidx.media3.common.Format; import androidx.media3.common.Format;
import androidx.media3.common.MediaItem; import androidx.media3.common.MediaItem;
@ -47,8 +48,10 @@ import androidx.media3.common.Timeline;
import androidx.media3.common.TrackGroupArray; import androidx.media3.common.TrackGroupArray;
import androidx.media3.common.TrackSelection; import androidx.media3.common.TrackSelection;
import androidx.media3.common.TrackSelectionArray; import androidx.media3.common.TrackSelectionArray;
import androidx.media3.common.TrackSelectionParameters;
import androidx.media3.common.TracksInfo; import androidx.media3.common.TracksInfo;
import androidx.media3.common.VideoSize; import androidx.media3.common.VideoSize;
import androidx.media3.common.text.Cue;
import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.UnstableApi;
import androidx.media3.decoder.DecoderException; import androidx.media3.decoder.DecoderException;
import androidx.media3.exoplayer.DecoderCounters; import androidx.media3.exoplayer.DecoderCounters;
@ -66,6 +69,7 @@ import java.lang.annotation.Documented;
import java.lang.annotation.Retention; import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy; import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; import java.lang.annotation.Target;
import java.util.List;
/** /**
* A listener for analytics events. * A listener for analytics events.
@ -184,6 +188,10 @@ public interface AnalyticsListener {
EVENT_PLAYLIST_METADATA_CHANGED, EVENT_PLAYLIST_METADATA_CHANGED,
EVENT_SEEK_BACK_INCREMENT_CHANGED, EVENT_SEEK_BACK_INCREMENT_CHANGED,
EVENT_SEEK_FORWARD_INCREMENT_CHANGED, EVENT_SEEK_FORWARD_INCREMENT_CHANGED,
EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED,
EVENT_TRACK_SELECTION_PARAMETERS_CHANGED,
EVENT_DEVICE_INFO_CHANGED,
EVENT_DEVICE_VOLUME_CHANGED,
EVENT_LOAD_STARTED, EVENT_LOAD_STARTED,
EVENT_LOAD_COMPLETED, EVENT_LOAD_COMPLETED,
EVENT_LOAD_CANCELED, EVENT_LOAD_CANCELED,
@ -192,6 +200,7 @@ public interface AnalyticsListener {
EVENT_UPSTREAM_DISCARDED, EVENT_UPSTREAM_DISCARDED,
EVENT_BANDWIDTH_ESTIMATE, EVENT_BANDWIDTH_ESTIMATE,
EVENT_METADATA, EVENT_METADATA,
EVENT_CUES,
EVENT_AUDIO_ENABLED, EVENT_AUDIO_ENABLED,
EVENT_AUDIO_DECODER_INITIALIZED, EVENT_AUDIO_DECODER_INITIALIZED,
EVENT_AUDIO_INPUT_FORMAT_CHANGED, EVENT_AUDIO_INPUT_FORMAT_CHANGED,
@ -272,6 +281,8 @@ public interface AnalyticsListener {
/** {@link Player#getMaxSeekToPreviousPosition()} changed. */ /** {@link Player#getMaxSeekToPreviousPosition()} changed. */
int EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED = int EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED =
Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED; Player.EVENT_MAX_SEEK_TO_PREVIOUS_POSITION_CHANGED;
/** {@link Player#getTrackSelectionParameters()} changed. */
int EVENT_TRACK_SELECTION_PARAMETERS_CHANGED = Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED;
/** Audio attributes changed. */ /** Audio attributes changed. */
int EVENT_AUDIO_ATTRIBUTES_CHANGED = Player.EVENT_AUDIO_ATTRIBUTES_CHANGED; int EVENT_AUDIO_ATTRIBUTES_CHANGED = Player.EVENT_AUDIO_ATTRIBUTES_CHANGED;
/** An audio session id was set. */ /** An audio session id was set. */
@ -291,9 +302,12 @@ public interface AnalyticsListener {
int EVENT_RENDERED_FIRST_FRAME = Player.EVENT_RENDERED_FIRST_FRAME; int EVENT_RENDERED_FIRST_FRAME = Player.EVENT_RENDERED_FIRST_FRAME;
/** Metadata associated with the current playback time was reported. */ /** Metadata associated with the current playback time was reported. */
int EVENT_METADATA = Player.EVENT_METADATA; int EVENT_METADATA = Player.EVENT_METADATA;
/** {@link Player#getCurrentCues()} changed. */
// TODO: Forward EVENT_CUES, EVENT_DEVICE_INFO_CHANGED and EVENT_DEVICE_VOLUME_CHANGED. int EVENT_CUES = Player.EVENT_CUES;
/** {@link Player#getDeviceInfo()} changed. */
int EVENT_DEVICE_INFO_CHANGED = Player.EVENT_DEVICE_INFO_CHANGED;
/** {@link Player#getDeviceVolume()} changed. */
int EVENT_DEVICE_VOLUME_CHANGED = Player.EVENT_DEVICE_VOLUME_CHANGED;
/** A source started loading data. */ /** A source started loading data. */
int EVENT_LOAD_STARTED = 1000; // Intentional gap to leave space for new Player events int EVENT_LOAD_STARTED = 1000; // Intentional gap to leave space for new Player events
/** A source started completed loading data. */ /** A source started completed loading data. */
@ -683,6 +697,17 @@ public interface AnalyticsListener {
*/ */
default void onPlayerError(EventTime eventTime, PlaybackException error) {} default void onPlayerError(EventTime eventTime, PlaybackException error) {}
/**
* Called when the {@link PlaybackException} returned by {@link Player#getPlayerError()} changes.
*
* <p>Implementations of Player may pass an instance of a subclass of {@link PlaybackException} to
* this method in order to include more information about the error.
*
* @param eventTime The event time.
* @param error The new error, or null if the error is being cleared.
*/
default void onPlayerErrorChanged(EventTime eventTime, @Nullable PlaybackException error) {}
/** /**
* Called when the available or selected tracks for the renderers changed. * Called when the available or selected tracks for the renderers changed.
* *
@ -703,6 +728,15 @@ public interface AnalyticsListener {
*/ */
default void onTracksInfoChanged(EventTime eventTime, TracksInfo tracksInfo) {} default void onTracksInfoChanged(EventTime eventTime, TracksInfo tracksInfo) {}
/**
* Called when track selection parameters change.
*
* @param eventTime The event time.
* @param trackSelectionParameters The new {@link TrackSelectionParameters}.
*/
default void onTrackSelectionParametersChanged(
EventTime eventTime, TrackSelectionParameters trackSelectionParameters) {}
/** /**
* Called when the combined {@link MediaMetadata} changes. * Called when the combined {@link MediaMetadata} changes.
* *
@ -812,6 +846,17 @@ public interface AnalyticsListener {
*/ */
default void onMetadata(EventTime eventTime, Metadata metadata) {} default void onMetadata(EventTime eventTime, Metadata metadata) {}
/**
* Called when there is a change in the {@link Cue Cues}.
*
* <p>{@code cues} is in ascending order of priority. If any of the cue boxes overlap when
* displayed, the {@link Cue} nearer the end of the list should be shown on top.
*
* @param eventTime The event time.
* @param cues The {@link Cue Cues}. May be empty.
*/
default void onCues(EventTime eventTime, List<Cue> cues) {}
/** @deprecated Use {@link #onAudioEnabled} and {@link #onVideoEnabled} instead. */ /** @deprecated Use {@link #onAudioEnabled} and {@link #onVideoEnabled} instead. */
@Deprecated @Deprecated
default void onDecoderEnabled( default void onDecoderEnabled(
@ -989,6 +1034,23 @@ public interface AnalyticsListener {
*/ */
default void onVolumeChanged(EventTime eventTime, float volume) {} default void onVolumeChanged(EventTime eventTime, float volume) {}
/**
* Called when the device information changes
*
* @param eventTime The event time.
* @param deviceInfo The new {@link DeviceInfo}.
*/
default void onDeviceInfoChanged(EventTime eventTime, DeviceInfo deviceInfo) {}
/**
* Called when the device volume or mute state changes.
*
* @param eventTime The event time.
* @param volume The new device volume, with 0 being silence and 1 being unity gain.
* @param muted Whether the device is muted.
*/
default void onDeviceVolumeChanged(EventTime eventTime, int volume, boolean muted) {}
/** /**
* Called when a video renderer is enabled. * Called when a video renderer is enabled.
* *

View File

@ -132,7 +132,7 @@ public interface AudioRendererEventListener {
/** /**
* Called when {@link AudioSink} has encountered an error. * Called when {@link AudioSink} has encountered an error.
* *
* <p>If the sink writes to a platform {@link AudioTrack}, this will called for all {@link * <p>If the sink writes to a platform {@link AudioTrack}, this will be called for all {@link
* AudioTrack} errors. * AudioTrack} errors.
* *
* <p>This method being called does not indicate that playback has failed, or that it will fail. * <p>This method being called does not indicate that playback has failed, or that it will fail.

View File

@ -121,6 +121,7 @@ import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableList;
import java.io.IOException; import java.io.IOException;
import java.lang.reflect.Method;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
@ -191,6 +192,18 @@ public final class AnalyticsCollectorTest {
private EventWindowAndPeriodId window0Period1Seq0; private EventWindowAndPeriodId window0Period1Seq0;
private EventWindowAndPeriodId window1Period0Seq1; private EventWindowAndPeriodId window1Period0Seq1;
@Test
public void analyticsCollector_overridesAllPlayerListenerMethods() throws Exception {
// Verify that AnalyticsCollector forwards all Player.Listener methods to AnalyticsListener.
for (Method method : Player.Listener.class.getDeclaredMethods()) {
assertThat(
AnalyticsCollector.class
.getMethod(method.getName(), method.getParameterTypes())
.getDeclaringClass())
.isEqualTo(AnalyticsCollector.class);
}
}
@Test @Test
public void emptyTimeline() throws Exception { public void emptyTimeline() throws Exception {
FakeMediaSource mediaSource = FakeMediaSource mediaSource =
@ -434,7 +447,7 @@ public final class AnalyticsCollectorTest {
// Wait until second period has fully loaded to assert loading events without flakiness. // Wait until second period has fully loaded to assert loading events without flakiness.
.waitForIsLoading(true) .waitForIsLoading(true)
.waitForIsLoading(false) .waitForIsLoading(false)
.seek(/* windowIndex= */ 1, /* positionMs= */ 0) .seek(/* mediaItemIndex= */ 1, /* positionMs= */ 0)
.play() .play()
.build(); .build();
TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule);
@ -527,7 +540,7 @@ public final class AnalyticsCollectorTest {
new ActionSchedule.Builder(TAG) new ActionSchedule.Builder(TAG)
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
.playUntilPosition(/* windowIndex= */ 0, periodDurationMs) .playUntilPosition(/* mediaItemIndex= */ 0, periodDurationMs)
.seekAndWait(/* positionMs= */ 0) .seekAndWait(/* positionMs= */ 0)
.play() .play()
.build(); .build();
@ -821,7 +834,7 @@ public final class AnalyticsCollectorTest {
.pause() .pause()
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
// Ensure second period is already being read from. // Ensure second period is already being read from.
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ periodDurationMs) .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ periodDurationMs)
.executeRunnable( .executeRunnable(
() -> () ->
concatenatedMediaSource.moveMediaSource( concatenatedMediaSource.moveMediaSource(
@ -1086,9 +1099,9 @@ public final class AnalyticsCollectorTest {
.waitForPlaybackState(Player.STATE_READY) .waitForPlaybackState(Player.STATE_READY)
// Wait in each content part to ensure previously triggered events get a chance to be // Wait in each content part to ensure previously triggered events get a chance to be
// delivered. This prevents flakiness caused by playback progressing too fast. // delivered. This prevents flakiness caused by playback progressing too fast.
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 3_000) .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 3_000)
.waitForPendingPlayerCommands() .waitForPendingPlayerCommands()
.playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 8_000) .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 8_000)
.waitForPendingPlayerCommands() .waitForPendingPlayerCommands()
.play() .play()
.waitForPlaybackState(Player.STATE_ENDED) .waitForPlaybackState(Player.STATE_ENDED)