From c3cb2f7cfb5fc456e1fe7ffaf9094a426fe845f0 Mon Sep 17 00:00:00 2001 From: tonihei Date: Wed, 9 Feb 2022 16:27:00 +0000 Subject: [PATCH] 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 --- .../media3/common/ForwardingPlayerTest.java | 20 +- .../analytics/AnalyticsCollector.java | 374 +++++++++++++----- .../analytics/AnalyticsListener.java | 68 +++- .../audio/AudioRendererEventListener.java | 2 +- .../analytics/AnalyticsCollectorTest.java | 23 +- 5 files changed, 376 insertions(+), 111 deletions(-) diff --git a/libraries/common/src/test/java/androidx/media3/common/ForwardingPlayerTest.java b/libraries/common/src/test/java/androidx/media3/common/ForwardingPlayerTest.java index 22b30a8b33..02b6a3e023 100644 --- a/libraries/common/src/test/java/androidx/media3/common/ForwardingPlayerTest.java +++ b/libraries/common/src/test/java/androidx/media3/common/ForwardingPlayerTest.java @@ -106,12 +106,12 @@ public class ForwardingPlayerTest { public void forwardingPlayer_overridesAllPlayerMethods() throws Exception { // Check with reflection that ForwardingPlayer overrides all Player methods. List methods = getPublicMethods(Player.class); - for (int i = 0; i < methods.size(); i++) { - Method method = methods.get(i); + for (Method method : methods) { assertThat( - ForwardingPlayer.class.getDeclaredMethod( - method.getName(), method.getParameterTypes())) - .isNotNull(); + ForwardingPlayer.class + .getDeclaredMethod(method.getName(), method.getParameterTypes()) + .getDeclaringClass()) + .isEqualTo(ForwardingPlayer.class); } } @@ -120,10 +120,12 @@ public class ForwardingPlayerTest { // Check with reflection that ForwardingListener overrides all Listener methods. Class forwardingListenerClass = getInnerClass("ForwardingListener"); List methods = getPublicMethods(Player.Listener.class); - for (int i = 0; i < methods.size(); i++) { - Method method = methods.get(i); - assertThat(forwardingListenerClass.getMethod(method.getName(), method.getParameterTypes())) - .isNotNull(); + for (Method method : methods) { + assertThat( + forwardingListenerClass + .getMethod(method.getName(), method.getParameterTypes()) + .getDeclaringClass()) + .isEqualTo(forwardingListenerClass); } } diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsCollector.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsCollector.java index 620d971217..72334349d7 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsCollector.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsCollector.java @@ -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.checkStateNotNull; +import android.media.AudioTrack; +import android.media.MediaCodec; +import android.media.MediaCodec.CodecException; import android.os.Looper; +import android.os.SystemClock; import android.util.SparseArray; +import android.view.Surface; import androidx.annotation.CallSuper; import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; +import androidx.media3.common.DeviceInfo; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; import androidx.media3.common.MediaMetadata; @@ -32,24 +38,28 @@ import androidx.media3.common.Metadata; import androidx.media3.common.PlaybackException; import androidx.media3.common.PlaybackParameters; import androidx.media3.common.Player; +import androidx.media3.common.Player.DiscontinuityReason; import androidx.media3.common.Player.PlaybackSuppressionReason; import androidx.media3.common.Timeline; import androidx.media3.common.Timeline.Period; import androidx.media3.common.Timeline.Window; import androidx.media3.common.TrackGroupArray; import androidx.media3.common.TrackSelectionArray; +import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; +import androidx.media3.common.text.Cue; import androidx.media3.common.util.Clock; import androidx.media3.common.util.HandlerWrapper; import androidx.media3.common.util.ListenerSet; import androidx.media3.common.util.UnstableApi; import androidx.media3.common.util.Util; +import androidx.media3.decoder.DecoderException; 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.AudioRendererEventListener; +import androidx.media3.exoplayer.audio.AudioSink; import androidx.media3.exoplayer.drm.DrmSession; import androidx.media3.exoplayer.drm.DrmSessionEventListener; 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.MediaSourceEventListener; 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.collect.ImmutableList; import com.google.common.collect.ImmutableMap; @@ -73,8 +83,6 @@ import org.checkerframework.checker.nullness.qual.RequiresNonNull; @UnstableApi public class AnalyticsCollector implements Player.Listener, - AudioRendererEventListener, - VideoRendererEventListener, MediaSourceEventListener, BandwidthMeter.EventListener, 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. - @Override public final void onAudioEnabled(DecoderCounters counters) { EventTime eventTime = generateReadingMediaPeriodEventTime(); 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. - @Override public final void onAudioDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { 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. - @Override public final void onAudioInputFormatChanged( Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { 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) { EventTime eventTime = generateReadingMediaPeriodEventTime(); sendEvent( @@ -241,7 +274,14 @@ public class AnalyticsCollector 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( int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); @@ -252,7 +292,11 @@ public class AnalyticsCollector 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) { EventTime eventTime = generateReadingMediaPeriodEventTime(); sendEvent( @@ -261,8 +305,12 @@ public class AnalyticsCollector 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. - @Override public final void onAudioDisabled(DecoderCounters counters) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); sendEvent( @@ -274,16 +322,16 @@ public class AnalyticsCollector }); } - @Override - public final void onSkipSilenceEnabledChanged(boolean skipSilenceEnabled) { - EventTime eventTime = generateReadingMediaPeriodEventTime(); - sendEvent( - eventTime, - AnalyticsListener.EVENT_SKIP_SILENCE_ENABLED_CHANGED, - listener -> listener.onSkipSilenceEnabledChanged(eventTime, skipSilenceEnabled)); - } - - @Override + /** + * Called when {@link AudioSink} has encountered an error. + * + *

If the sink writes to a platform {@link AudioTrack}, this will be called for all {@link + * AudioTrack} errors. + * + * @param audioSinkError The error that occurred. Typically an {@link + * AudioSink.InitializationException}, a {@link AudioSink.WriteException}, or an {@link + * AudioSink.UnexpectedDiscontinuityException}. + */ public final void onAudioSinkError(Exception audioSinkError) { EventTime eventTime = generateReadingMediaPeriodEventTime(); sendEvent( @@ -292,7 +340,12 @@ public class AnalyticsCollector 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) { EventTime eventTime = generateReadingMediaPeriodEventTime(); sendEvent( @@ -301,34 +354,6 @@ public class AnalyticsCollector 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. * @@ -342,10 +367,15 @@ public class AnalyticsCollector 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. - @Override public final void onVideoEnabled(DecoderCounters counters) { EventTime eventTime = generateReadingMediaPeriodEventTime(); 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. - @Override public final void onVideoDecoderInitialized( String decoderName, long initializedTimestampMs, long initializationDurationMs) { 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. - @Override public final void onVideoInputFormatChanged( Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) { 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) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); sendEvent( @@ -398,7 +451,11 @@ public class AnalyticsCollector 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) { EventTime eventTime = generateReadingMediaPeriodEventTime(); sendEvent( @@ -407,8 +464,12 @@ public class AnalyticsCollector 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. - @Override public final void onVideoDisabled(DecoderCounters counters) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); sendEvent( @@ -420,25 +481,14 @@ public class AnalyticsCollector }); } - @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 + /** + * Called when a frame is rendered for the first time since setting the output, or since the + * renderer was reset, or since the stream being rendered was changed. + * + * @param output The output of the video renderer. Normally a {@link Surface}, however some video + * renderers may have other output types (e.g., a {@link VideoDecoderOutputBufferRenderer}). + * @param renderTimeMs The {@link SystemClock#elapsedRealtime()} when the frame was rendered. + */ public final void onRenderedFirstFrame(Object output, long renderTimeMs) { EventTime eventTime = generateReadingMediaPeriodEventTime(); sendEvent( @@ -447,7 +497,24 @@ public class AnalyticsCollector listener -> listener.onRenderedFirstFrame(eventTime, output, renderTimeMs)); } - @Override + /** + * Called to report the video processing offset of video frames processed by the video renderer. + * + *

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 Pvf + * - Ppl where Pvf is the presentation timestamp of the video + * frame and Ppl 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). + * + *

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) { EventTime eventTime = generatePlayingMediaPeriodEventTime(); sendEvent( @@ -457,7 +524,19 @@ public class AnalyticsCollector listener.onVideoFrameProcessingOffset(eventTime, totalProcessingOffsetUs, frameCount)); } - @Override + /** + * Called when a video decoder encounters an error. + * + *

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 not + * 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) { EventTime eventTime = generateReadingMediaPeriodEventTime(); sendEvent( @@ -466,8 +545,6 @@ public class AnalyticsCollector 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 * rendered. @@ -608,6 +685,12 @@ public class AnalyticsCollector 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. @Override public final void onIsLoadingChanged(boolean isLoading) { @@ -699,21 +782,26 @@ public class AnalyticsCollector @Override public final void onPlayerError(PlaybackException error) { - @Nullable EventTime eventTime = null; - 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; + EventTime eventTime = getEventTimeForErrorEvent(error); sendEvent( eventTime, 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. @@ -801,6 +889,13 @@ public class AnalyticsCollector listener -> listener.onMetadata(eventTime, metadata)); } + @Override + public void onCues(List cues) { + EventTime eventTime = generateCurrentPlayerMediaPeriodEventTime(); + sendEvent( + eventTime, AnalyticsListener.EVENT_CUES, listener -> listener.onCues(eventTime, cues)); + } + @SuppressWarnings("deprecation") // Implementing and calling deprecated listener method. @Override public final void onSeekProcessed() { @@ -809,6 +904,89 @@ public class AnalyticsCollector 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. @Override @@ -1002,6 +1180,16 @@ public class AnalyticsCollector 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. */ private static final class MediaPeriodQueueTracker { diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsListener.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsListener.java index 30fa02b6e3..f99c2b6e4a 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsListener.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/analytics/AnalyticsListener.java @@ -32,6 +32,7 @@ import androidx.annotation.IntDef; import androidx.annotation.Nullable; import androidx.media3.common.AudioAttributes; import androidx.media3.common.C; +import androidx.media3.common.DeviceInfo; import androidx.media3.common.FlagSet; import androidx.media3.common.Format; import androidx.media3.common.MediaItem; @@ -47,8 +48,10 @@ import androidx.media3.common.Timeline; import androidx.media3.common.TrackGroupArray; import androidx.media3.common.TrackSelection; import androidx.media3.common.TrackSelectionArray; +import androidx.media3.common.TrackSelectionParameters; import androidx.media3.common.TracksInfo; import androidx.media3.common.VideoSize; +import androidx.media3.common.text.Cue; import androidx.media3.common.util.UnstableApi; import androidx.media3.decoder.DecoderException; import androidx.media3.exoplayer.DecoderCounters; @@ -66,6 +69,7 @@ import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import java.util.List; /** * A listener for analytics events. @@ -184,6 +188,10 @@ public interface AnalyticsListener { EVENT_PLAYLIST_METADATA_CHANGED, EVENT_SEEK_BACK_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_COMPLETED, EVENT_LOAD_CANCELED, @@ -192,6 +200,7 @@ public interface AnalyticsListener { EVENT_UPSTREAM_DISCARDED, EVENT_BANDWIDTH_ESTIMATE, EVENT_METADATA, + EVENT_CUES, EVENT_AUDIO_ENABLED, EVENT_AUDIO_DECODER_INITIALIZED, EVENT_AUDIO_INPUT_FORMAT_CHANGED, @@ -272,6 +281,8 @@ public interface AnalyticsListener { /** {@link Player#getMaxSeekToPreviousPosition()} changed. */ int 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. */ int EVENT_AUDIO_ATTRIBUTES_CHANGED = Player.EVENT_AUDIO_ATTRIBUTES_CHANGED; /** An audio session id was set. */ @@ -291,9 +302,12 @@ public interface AnalyticsListener { int EVENT_RENDERED_FIRST_FRAME = Player.EVENT_RENDERED_FIRST_FRAME; /** Metadata associated with the current playback time was reported. */ int EVENT_METADATA = Player.EVENT_METADATA; - - // TODO: Forward EVENT_CUES, EVENT_DEVICE_INFO_CHANGED and EVENT_DEVICE_VOLUME_CHANGED. - + /** {@link Player#getCurrentCues()} 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. */ int EVENT_LOAD_STARTED = 1000; // Intentional gap to leave space for new Player events /** A source started completed loading data. */ @@ -683,6 +697,17 @@ public interface AnalyticsListener { */ default void onPlayerError(EventTime eventTime, PlaybackException error) {} + /** + * Called when the {@link PlaybackException} returned by {@link Player#getPlayerError()} changes. + * + *

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. * @@ -703,6 +728,15 @@ public interface AnalyticsListener { */ 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. * @@ -812,6 +846,17 @@ public interface AnalyticsListener { */ default void onMetadata(EventTime eventTime, Metadata metadata) {} + /** + * Called when there is a change in the {@link Cue Cues}. + * + *

{@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 cues) {} + /** @deprecated Use {@link #onAudioEnabled} and {@link #onVideoEnabled} instead. */ @Deprecated default void onDecoderEnabled( @@ -989,6 +1034,23 @@ public interface AnalyticsListener { */ 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. * diff --git a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioRendererEventListener.java b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioRendererEventListener.java index 9002de6c6f..a01dcf614b 100644 --- a/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioRendererEventListener.java +++ b/libraries/exoplayer/src/main/java/androidx/media3/exoplayer/audio/AudioRendererEventListener.java @@ -132,7 +132,7 @@ public interface AudioRendererEventListener { /** * Called when {@link AudioSink} has encountered an error. * - *

If the sink writes to a platform {@link AudioTrack}, this will called for all {@link + *

If the sink writes to a platform {@link AudioTrack}, this will be called for all {@link * AudioTrack} errors. * *

This method being called does not indicate that playback has failed, or that it will fail. diff --git a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/AnalyticsCollectorTest.java b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/AnalyticsCollectorTest.java index b34c2b19a9..35e59c9740 100644 --- a/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/AnalyticsCollectorTest.java +++ b/libraries/exoplayer/src/test/java/androidx/media3/exoplayer/analytics/AnalyticsCollectorTest.java @@ -121,6 +121,7 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; import com.google.common.collect.ImmutableList; import java.io.IOException; +import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Iterator; import java.util.List; @@ -191,6 +192,18 @@ public final class AnalyticsCollectorTest { private EventWindowAndPeriodId window0Period1Seq0; 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 public void emptyTimeline() throws Exception { FakeMediaSource mediaSource = @@ -434,7 +447,7 @@ public final class AnalyticsCollectorTest { // Wait until second period has fully loaded to assert loading events without flakiness. .waitForIsLoading(true) .waitForIsLoading(false) - .seek(/* windowIndex= */ 1, /* positionMs= */ 0) + .seek(/* mediaItemIndex= */ 1, /* positionMs= */ 0) .play() .build(); TestAnalyticsListener listener = runAnalyticsTest(mediaSource, actionSchedule); @@ -527,7 +540,7 @@ public final class AnalyticsCollectorTest { new ActionSchedule.Builder(TAG) .pause() .waitForPlaybackState(Player.STATE_READY) - .playUntilPosition(/* windowIndex= */ 0, periodDurationMs) + .playUntilPosition(/* mediaItemIndex= */ 0, periodDurationMs) .seekAndWait(/* positionMs= */ 0) .play() .build(); @@ -821,7 +834,7 @@ public final class AnalyticsCollectorTest { .pause() .waitForPlaybackState(Player.STATE_READY) // Ensure second period is already being read from. - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ periodDurationMs) + .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ periodDurationMs) .executeRunnable( () -> concatenatedMediaSource.moveMediaSource( @@ -1086,9 +1099,9 @@ public final class AnalyticsCollectorTest { .waitForPlaybackState(Player.STATE_READY) // 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. - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 3_000) + .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 3_000) .waitForPendingPlayerCommands() - .playUntilPosition(/* windowIndex= */ 0, /* positionMs= */ 8_000) + .playUntilPosition(/* mediaItemIndex= */ 0, /* positionMs= */ 8_000) .waitForPendingPlayerCommands() .play() .waitForPlaybackState(Player.STATE_ENDED)