CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/maven-androidx-media3--media3-exoplayer

ExoPlayer module that provides the core ExoPlayer implementation for local media playback on Android, supporting various media formats and streaming protocols

Pending
Overview
Eval results
Files

analytics.mddocs/

Analytics

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.

AnalyticsListener Interface

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) {}
}

AnalyticsCollector Interface

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);
}

DefaultAnalyticsCollector

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();
}

EventLogger

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
}

Media Load Data

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);
}

Load Event Info

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);
}

Usage Examples

Basic Analytics Setup

// 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();

Custom Analytics Listener

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);

Performance Monitoring

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();
    }
}

Network Analytics

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
    }
}

Debug Text View Helper

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

docs

analytics.md

audio-rendering.md

core-player.md

drm-support.md

index.md

media-sources.md

offline-support.md

track-selection.md

video-rendering.md

tile.json