ExoPlayer module that provides the core ExoPlayer implementation for local media playback on Android, supporting various media formats and streaming protocols
—
ExoPlayer's analytics system provides comprehensive event tracking, performance monitoring, and debugging capabilities. The analytics framework allows you to monitor playback events, track performance metrics, and gather detailed information about player behavior.
The primary interface for receiving analytics events with detailed event timing and context information.
public interface AnalyticsListener {
/**
* Contains timing information for analytics events.
*/
final class EventTime {
/**
* The timeline at the time of the event.
*/
public final Timeline timeline;
/**
* The window index in the timeline.
*/
public final int windowIndex;
/**
* The media period id, or null if not applicable.
*/
@Nullable
public final MediaPeriodId mediaPeriodId;
/**
* The event timestamp in milliseconds since epoch.
*/
public final long realtimeMs;
/**
* The current timeline event time in milli
*/
public final long currentTimelineWindowSequenceNumber;
public EventTime(long realtimeMs, Timeline timeline, int windowIndex,
@Nullable MediaPeriodId mediaPeriodId, long currentPlaybackPositionMs,
Timeline currentTimeline, int currentWindowIndex);
}
// Playback state events
default void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {}
default void onPlayWhenReadyChanged(EventTime eventTime, boolean playWhenReady, @Player.PlayWhenReadyChangeReason int reason) {}
default void onIsPlayingChanged(EventTime eventTime, boolean isPlaying) {}
default void onTimelineChanged(EventTime eventTime, @Player.TimelineChangeReason int reason) {}
default void onTracksChanged(EventTime eventTime, Tracks tracks) {}
// Error events
default void onPlayerError(EventTime eventTime, PlaybackException error) {}
default void onPlayerErrorChanged(EventTime eventTime, @Nullable PlaybackException error) {}
// Seek events
default void onSeekStarted(EventTime eventTime) {}
default void onSeekProcessed(EventTime eventTime) {}
default void onPositionDiscontinuity(EventTime eventTime, Player.PositionInfo oldPosition, Player.PositionInfo newPosition, @Player.DiscontinuityReason int reason) {}
// Media events
default void onMediaItemTransition(EventTime eventTime, @Nullable MediaItem mediaItem, @Player.MediaItemTransitionReason int reason) {}
default void onPlaylistMetadataChanged(EventTime eventTime, MediaMetadata playlistMetadata) {}
default void onMediaMetadataChanged(EventTime eventTime, MediaMetadata mediaMetadata) {}
// Video events
default void onVideoSizeChanged(EventTime eventTime, VideoSize videoSize) {}
default void onSurfaceSizeChanged(EventTime eventTime, int width, int height) {}
default void onRenderedFirstFrame(EventTime eventTime, Object output, long renderTimeMs) {}
// Audio events
default void onAudioSessionIdChanged(EventTime eventTime, int audioSessionId) {}
default void onAudioAttributesChanged(EventTime eventTime, AudioAttributes audioAttributes) {}
default void onVolumeChanged(EventTime eventTime, float volume) {}
default void onSkipSilenceEnabledChanged(EventTime eventTime, boolean skipSilenceEnabled) {}
// Cue events (subtitles/captions)
default void onCues(EventTime eventTime, CueGroup cueGroup) {}
// Device events
default void onDeviceInfoChanged(EventTime eventTime, DeviceInfo deviceInfo) {}
default void onDeviceVolumeChanged(EventTime eventTime, int volume, boolean muted) {}
// Bandwidth and network events
default void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {}
// Loading events
default void onLoadStarted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
default void onLoadCompleted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
default void onLoadCanceled(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {}
default void onLoadError(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) {}
// Downstream format events
default void onDownstreamFormatChanged(EventTime eventTime, MediaLoadData mediaLoadData) {}
default void onUpstreamDiscarded(EventTime eventTime, MediaLoadData mediaLoadData) {}
// DRM events
default void onDrmSessionAcquired(EventTime eventTime, @C.TrackType int trackType) {}
default void onDrmKeysLoaded(EventTime eventTime, @C.TrackType int trackType) {}
default void onDrmSessionManagerError(EventTime eventTime, Exception error) {}
default void onDrmKeysRestored(EventTime eventTime, @C.TrackType int trackType) {}
default void onDrmKeysRemoved(EventTime eventTime, @C.TrackType int trackType) {}
default void onDrmSessionReleased(EventTime eventTime, @C.TrackType int trackType) {}
// Metadata events
default void onMetadata(EventTime eventTime, Metadata metadata) {}
// Decoder events
default void onAudioEnabled(EventTime eventTime, DecoderCounters counters) {}
default void onAudioDecoderInitialized(EventTime eventTime, String decoderName, long initializationDurationMs) {}
default void onAudioInputFormatChanged(EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
default void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {}
default void onAudioDecoderReleased(EventTime eventTime, String decoderName) {}
default void onAudioDisabled(EventTime eventTime, DecoderCounters counters) {}
default void onVideoEnabled(EventTime eventTime, DecoderCounters counters) {}
default void onVideoDecoderInitialized(EventTime eventTime, String decoderName, long initializationDurationMs) {}
default void onVideoInputFormatChanged(EventTime eventTime, Format format, @Nullable DecoderReuseEvaluation decoderReuseEvaluation) {}
default void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {}
default void onVideoFrameProcessingOffset(EventTime eventTime, long totalProcessingOffsetUs, int frameCount) {}
default void onVideoDecoderReleased(EventTime eventTime, String decoderName) {}
default void onVideoDisabled(EventTime eventTime, DecoderCounters counters) {}
}Collects and forwards analytics events from various ExoPlayer components.
public interface AnalyticsCollector extends Player.Listener, AudioRendererEventListener,
VideoRendererEventListener, MediaSourceEventListener,
BandwidthMeter.EventListener, DrmSessionEventListener {
/**
* Adds an analytics listener.
*
* @param listener The listener to add
*/
void addListener(AnalyticsListener listener);
/**
* Removes an analytics listener.
*
* @param listener The listener to remove
*/
void removeListener(AnalyticsListener listener);
/**
* Sets the player instance.
*
* @param player The player
* @param looper The looper
*/
void setPlayer(Player player, Looper looper);
/**
* Called when the player is released.
*/
void release();
/**
* Updates the media period queue info.
*
* @param queue The media period queue
* @param readingPeriod The reading period
*/
void updateMediaPeriodQueueInfo(List<MediaPeriodHolder> queue, @Nullable MediaPeriodHolder readingPeriod);
}The default implementation of AnalyticsCollector.
public class DefaultAnalyticsCollector implements AnalyticsCollector {
/**
* Creates a DefaultAnalyticsCollector.
*
* @param clock The clock for generating event timestamps
*/
public DefaultAnalyticsCollector(Clock clock);
/**
* Creates a DefaultAnalyticsCollector with the default clock.
*/
public DefaultAnalyticsCollector();
@Override
public void addListener(AnalyticsListener listener);
@Override
public void removeListener(AnalyticsListener listener);
@Override
public void setPlayer(Player player, Looper looper);
@Override
public void release();
}A utility class that logs analytics events for debugging purposes.
public final class EventLogger implements AnalyticsListener {
/**
* Creates an EventLogger.
*/
public EventLogger();
/**
* Creates an EventLogger with a custom tag.
*
* @param tag The log tag
*/
public EventLogger(@Nullable String tag);
/**
* Starts logging session with a session name.
*
* @param sessionName The session name
*/
public void startSession(String sessionName);
/**
* Stops the current logging session.
*/
public void stopSession();
// Implements all AnalyticsListener methods with logging
}Information about media loading operations.
public final class MediaLoadData {
/**
* The data type.
*/
@C.DataType public final int dataType;
/**
* The track type.
*/
@C.TrackType public final int trackType;
/**
* The track format, or null if not applicable.
*/
@Nullable public final Format trackFormat;
/**
* The track selection reason.
*/
@C.SelectionReason public final int trackSelectionReason;
/**
* The track selection data, or null if not applicable.
*/
@Nullable public final Object trackSelectionData;
/**
* The media start time in microseconds.
*/
public final long mediaStartTimeMs;
/**
* The media end time in microseconds.
*/
public final long mediaEndTimeMs;
public MediaLoadData(@C.DataType int dataType, @C.TrackType int trackType, @Nullable Format trackFormat,
@C.SelectionReason int trackSelectionReason, @Nullable Object trackSelectionData,
long mediaStartTimeMs, long mediaEndTimeMs);
}Information about loading events.
public final class LoadEventInfo {
/**
* The data specification for the load.
*/
public final DataSpec dataSpec;
/**
* The URI after any redirection.
*/
public final Uri uri;
/**
* The response headers, or empty if not applicable.
*/
public final Map<String, List<String>> responseHeaders;
/**
* The number of bytes loaded.
*/
public final long bytesLoaded;
/**
* The load duration in milliseconds.
*/
public final long loadDurationMs;
public LoadEventInfo(DataSpec dataSpec, Uri uri, Map<String, List<String>> responseHeaders,
long bytesLoaded, long loadDurationMs);
}// Create analytics collector
DefaultAnalyticsCollector analyticsCollector = new DefaultAnalyticsCollector();
// Add event logger for debugging
EventLogger eventLogger = new EventLogger("ExoPlayer");
analyticsCollector.addListener(eventLogger);
// Use with ExoPlayer
ExoPlayer player = new ExoPlayer.Builder(context)
.setAnalyticsCollector(analyticsCollector)
.build();public class CustomAnalyticsListener implements AnalyticsListener {
private static final String TAG = "CustomAnalytics";
@Override
public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
String stateName = getStateName(state);
Log.d(TAG, "Playback state changed to: " + stateName);
// Send analytics to your backend
sendAnalyticsEvent("playback_state_changed",
Map.of("state", stateName, "timestamp", eventTime.realtimeMs));
}
@Override
public void onPlayerError(EventTime eventTime, PlaybackException error) {
Log.e(TAG, "Player error occurred", error);
// Report error to crash reporting service
crashReporter.recordException(error);
// Send error analytics
sendAnalyticsEvent("player_error",
Map.of("error_code", error.errorCode,
"error_message", error.getMessage(),
"timestamp", eventTime.realtimeMs));
}
@Override
public void onVideoSizeChanged(EventTime eventTime, VideoSize videoSize) {
Log.d(TAG, String.format("Video size changed: %dx%d",
videoSize.width, videoSize.height));
sendAnalyticsEvent("video_size_changed",
Map.of("width", videoSize.width,
"height", videoSize.height,
"pixel_width_height_ratio", videoSize.pixelWidthHeightRatio));
}
@Override
public void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs,
long totalBytesLoaded, long bitrateEstimate) {
Log.d(TAG, String.format("Bandwidth estimate: %d kbps", bitrateEstimate / 1000));
sendAnalyticsEvent("bandwidth_estimate",
Map.of("bitrate_kbps", bitrateEstimate / 1000,
"total_load_time_ms", totalLoadTimeMs,
"total_bytes_loaded", totalBytesLoaded));
}
@Override
public void onLoadError(EventTime eventTime, LoadEventInfo loadEventInfo,
MediaLoadData mediaLoadData, IOException error, boolean wasCanceled) {
Log.w(TAG, "Load error occurred: " + error.getMessage());
sendAnalyticsEvent("load_error",
Map.of("error_message", error.getMessage(),
"was_canceled", wasCanceled,
"uri", loadEventInfo.uri.toString(),
"bytes_loaded", loadEventInfo.bytesLoaded));
}
private String getStateName(@Player.State int state) {
switch (state) {
case Player.STATE_IDLE: return "IDLE";
case Player.STATE_BUFFERING: return "BUFFERING";
case Player.STATE_READY: return "READY";
case Player.STATE_ENDED: return "ENDED";
default: return "UNKNOWN";
}
}
private void sendAnalyticsEvent(String eventName, Map<String, Object> parameters) {
// Implementation to send analytics to your backend
}
}
// Use the custom listener
CustomAnalyticsListener customListener = new CustomAnalyticsListener();
player.addAnalyticsListener(customListener);public class PerformanceAnalyticsListener implements AnalyticsListener {
private long sessionStartTime;
private int totalDroppedFrames;
private long totalVideoFrames;
private Map<String, Integer> decoderInitCounts = new HashMap<>();
@Override
public void onPlaybackStateChanged(EventTime eventTime, @Player.State int state) {
if (state == Player.STATE_READY && sessionStartTime == 0) {
sessionStartTime = eventTime.realtimeMs;
} else if (state == Player.STATE_ENDED || state == Player.STATE_IDLE) {
reportSessionMetrics(eventTime);
}
}
@Override
public void onDroppedVideoFrames(EventTime eventTime, int droppedFrames, long elapsedMs) {
totalDroppedFrames += droppedFrames;
// Calculate drop rate
if (elapsedMs > 0) {
double dropRate = (double) droppedFrames / (elapsedMs / 1000.0);
Log.d("Performance", String.format("Dropped %d frames in %dms (%.2f fps drop rate)",
droppedFrames, elapsedMs, dropRate));
}
}
@Override
public void onVideoDecoderInitialized(EventTime eventTime, String decoderName, long initializationDurationMs) {
decoderInitCounts.put(decoderName, decoderInitCounts.getOrDefault(decoderName, 0) + 1);
Log.d("Performance", String.format("Video decoder %s initialized in %dms",
decoderName, initializationDurationMs));
}
@Override
public void onAudioUnderrun(EventTime eventTime, int bufferSize, long bufferSizeMs, long elapsedSinceLastFeedMs) {
Log.w("Performance", String.format("Audio underrun: buffer=%d bytes (%dms), starved for %dms",
bufferSize, bufferSizeMs, elapsedSinceLastFeedMs));
}
private void reportSessionMetrics(EventTime eventTime) {
if (sessionStartTime == 0) return;
long sessionDuration = eventTime.realtimeMs - sessionStartTime;
double averageDropRate = totalVideoFrames > 0 ?
(double) totalDroppedFrames / totalVideoFrames * 100 : 0;
Log.i("Performance", String.format(
"Session metrics - Duration: %dms, Dropped frames: %d, Average drop rate: %.2f%%",
sessionDuration, totalDroppedFrames, averageDropRate));
// Reset for next session
sessionStartTime = 0;
totalDroppedFrames = 0;
totalVideoFrames = 0;
decoderInitCounts.clear();
}
}public class NetworkAnalyticsListener implements AnalyticsListener {
private long totalBytesLoaded = 0;
private long sessionStartTime;
private List<Long> bitrateEstimates = new ArrayList<>();
@Override
public void onLoadCompleted(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData) {
totalBytesLoaded += loadEventInfo.bytesLoaded;
// Calculate effective bitrate
if (loadEventInfo.loadDurationMs > 0) {
long effectiveBitrate = (loadEventInfo.bytesLoaded * 8 * 1000) / loadEventInfo.loadDurationMs;
Log.d("Network", String.format("Load completed: %d bytes in %dms (effective bitrate: %d bps)",
loadEventInfo.bytesLoaded, loadEventInfo.loadDurationMs, effectiveBitrate));
}
}
@Override
public void onBandwidthEstimate(EventTime eventTime, int totalLoadTimeMs, long totalBytesLoaded, long bitrateEstimate) {
bitrateEstimates.add(bitrateEstimate);
// Keep only recent estimates (last 10)
if (bitrateEstimates.size() > 10) {
bitrateEstimates.remove(0);
}
// Calculate average bitrate
long avgBitrate = bitrateEstimates.stream().mapToLong(Long::longValue).sum() / bitrateEstimates.size();
Log.d("Network", String.format("Bandwidth estimate: %d kbps (avg: %d kbps)",
bitrateEstimate / 1000, avgBitrate / 1000));
}
@Override
public void onLoadError(EventTime eventTime, LoadEventInfo loadEventInfo, MediaLoadData mediaLoadData,
IOException error, boolean wasCanceled) {
Log.w("Network", String.format("Load error for %s: %s (canceled: %s)",
loadEventInfo.uri, error.getMessage(), wasCanceled));
// Track error by type
String errorType = error.getClass().getSimpleName();
// Report to analytics backend
}
}public class DebugTextViewHelper {
private final ExoPlayer player;
private final TextView textView;
private final Runnable updateRunnable = this::updateAndPost;
private boolean started;
public DebugTextViewHelper(ExoPlayer player, TextView textView) {
this.player = player;
this.textView = textView;
}
/**
* Starts updating the text view with debug information.
*/
public void start() {
if (started) return;
started = true;
updateAndPost();
}
/**
* Stops updating the text view.
*/
public void stop() {
started = false;
textView.removeCallbacks(updateRunnable);
}
private void updateAndPost() {
updateDebugText();
if (started) {
textView.postDelayed(updateRunnable, 1000); // Update every second
}
}
private void updateDebugText() {
StringBuilder debug = new StringBuilder();
// Playback state
debug.append("State: ").append(getStateName(player.getPlaybackState())).append("\n");
debug.append("Playing: ").append(player.isPlaying()).append("\n");
// Position info
long position = player.getCurrentPosition();
long duration = player.getDuration();
long buffered = player.getBufferedPosition();
debug.append("Position: ").append(formatTime(position))
.append(" / ").append(formatTime(duration)).append("\n");
debug.append("Buffered: ").append(formatTime(buffered)).append("\n");
// Video info
VideoSize videoSize = player.getVideoSize();
if (videoSize.width > 0) {
debug.append("Video: ").append(videoSize.width).append("x").append(videoSize.height).append("\n");
}
// Audio info
Format audioFormat = player.getAudioFormat();
if (audioFormat != null) {
debug.append("Audio: ").append(audioFormat.sampleMimeType)
.append(", ").append(audioFormat.channelCount).append(" ch")
.append(", ").append(audioFormat.sampleRate).append(" Hz").append("\n");
}
textView.setText(debug.toString());
}
private String formatTime(long timeMs) {
if (timeMs == C.TIME_UNSET) return "--:--";
long seconds = timeMs / 1000;
return String.format("%d:%02d", seconds / 60, seconds % 60);
}
private String getStateName(@Player.State int state) {
switch (state) {
case Player.STATE_IDLE: return "IDLE";
case Player.STATE_BUFFERING: return "BUFFERING";
case Player.STATE_READY: return "READY";
case Player.STATE_ENDED: return "ENDED";
default: return "UNKNOWN";
}
}
}
// Usage
DebugTextViewHelper debugHelper = new DebugTextViewHelper(player, debugTextView);
debugHelper.start();
// ... later
debugHelper.stop();Install with Tessl CLI
npx tessl i tessl/maven-androidx-media3--media3-exoplayer