Skip to content

setKeepContentOnPlayerReset(true) doesn't preserve last frame on built-in Android TVs when switching between streams using player.stop() #2941

@needz

Description

@needz

Version

Media3 main branch

More version details

DESCRIPTION:

ExoPlayer/Media3 Version: Media3 (latest stable)
Android SDK: API 21+
Device Type: Built-in Android TV (not external devices like Chromecast/FireTV)
Content: Live IPTV streams (H.264, 1920x1080)
Frame Rates: 60fps and 25fps streams

Issue Description

When switching between video streams with different frame rates (e.g., 60fps → 25fps) on built-in Android TVs, there is no way to achieve both:

  1. Smooth playback without frame rate timing issues (choppiness)
  2. Preserving the last video frame during transition (avoiding black screen)

The issue does NOT occur on external devices (Chromecast, FireTV Stick) connected via HDMI.

Current Behavior

firstZap is just to check if the stream URL is different from the previous one

Scenario A: Using player.stop() before media switch

if (firstZap) {
    player.stop();
}
player.setMediaSource(newMediaSource);
player.prepare();
player.play();

Result:

  • ✅ Smooth playback (no choppiness on 60fps→25fps transition)
  • ❌ Black screen shown on built-in TVs during transition
  • ✅ Works correctly on external devices (Chromecast/FireTV) - last frame is preserved

Scenario B: NOT using player.stop()

playerView.setKeepContentOnPlayerReset(true);

if (firstZap) {
    // No stop() call
}
player.setMediaSource(newMediaSource, /* resetPosition= */ true);
player.prepare();
player.play();

Result:

  • ❌ Choppy/stuttering playback when transitioning from 60fps to 25fps
  • ✅ Last frame preserved on external devices (Chromecast/FireTV)
  • ❌ Black screen shown on built-in TVs anyway

Expected Behavior

setKeepContentOnPlayerReset(true) should preserve the last rendered frame on all device types (including built-in TVs) while properly resetting frame timing state when switching between streams with different frame rates.

Root Cause Analysis

Through extensive testing, we identified the following:

1. Frame Timing State Persistence

When switching from 60fps to 25fps without stop(), the VideoFrameReleaseHelper maintains timing state from the previous stream:

  • The syncFramePresentationTimeNs and frame duration calculations remain calibrated for 60fps (~16.67ms intervals)
  • New 25fps frames arrive with ~40ms intervals
  • The renderer attempts to display frames using the old 60fps timing baseline, causing judder

2. Surface Clearing Behavior Differences

External Devices (Chromecast/FireTV):

  • Use hardware overlays managed by dedicated video planes
  • SurfaceView buffer persists in HDMI output memory even after stop() is called
  • Surface composition happens at the HDMI layer, maintaining the last frame

Built-in TVs:

  • Render directly to the display compositor without HDMI buffering
  • When stop() is called, the surface is immediately cleared from the display pipeline
  • No intermediate buffering layer to preserve the last frame

3. Decoder State and Frame Timing

The choppiness occurs because:

  • setMediaSource() alone doesn't reset the internal renderer timing state
  • The video renderer needs decoder reinitialization to clear frame timing metadata
  • Only stop() fully releases and reinitializes the decoder, clearing timing state

Attempted Workarounds (All Failed)

1. Clear Video Surface

player.clearVideoSurface();
player.setMediaSource(newMediaSource, true);
player.prepare();

Result: No image shown after clearVideoSurface()

2. Seek to Reset Timing

player.seekTo(0);
player.setMediaSource(newMediaSource, true);
player.prepare();

Result: Still choppy on 60fps→25fps transition

3. Disable/Enable Video Track

player.setTrackSelectionParameters(
    params.buildUpon().setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, true).build()
);
player.setMediaSource(newMediaSource, true);
player.prepare();
player.setTrackSelectionParameters(
    params.buildUpon().setTrackTypeDisabled(C.TRACK_TYPE_VIDEO, false).build()
);

Result: Still choppy

4. Pause Before Media Switch

player.pause();
// Wait 50ms
player.setMediaSource(newMediaSource, true);
player.prepare();
player.play();

Result: Still choppy

5. Change Playback Speed

player.setPlaybackParameters(new PlaybackParameters(1.01f));
player.setPlaybackParameters(new PlaybackParameters(1.0f));
player.setMediaSource(newMediaSource, true);
player.prepare();

Result: Still choppy

6. Detach PlayerView Before stop()

playerView.setPlayer(null);
player.stop();
player.setMediaSource(newMediaSource, true);
player.prepare();
playerView.setPlayer(player);

Result: Still choppy (and defeats the purpose of preserving frame)

7. replaceMediaItem()

MediaItem newMediaItem = MediaItem.fromUri(newStreamUri);
player.replaceMediaItem(0, newMediaItem);

Result: Still choppy (doesn't reset frame timing for different frame rates)

8. setMediaSource with resetPosition=false + seekTo(0)

player.setMediaSource(newMediaSource, false);
player.prepare();
player.seekTo(0);

Result: Still choppy

Code to Reproduce

// Setup
ExoPlayer player = new ExoPlayer.Builder(context).build();
PlayerView playerView = findViewById(R.id.player_view);
playerView.setPlayer(player);

// Stream 1: 60fps H.264 content
MediaItem stream60fps = MediaItem.fromUri("https://example.com/stream_60fps.m3u8");
player.setMediaItem(stream60fps);
player.prepare();
player.play();

// Wait a few seconds, then switch to 25fps stream
Handler handler = new Handler(Looper.getMainLooper());
handler.postDelayed(() -> {
    // Stream 2: 25fps H.264 content
    MediaItem stream25fps = MediaItem.fromUri("https://example.com/stream_25fps.m3u8");

    playerView.setKeepContentOnPlayerReset(true);

    // Without stop() - CHOPPY on built-in TVs
    player.setMediaSource(
        new HlsMediaSource.Factory(dataSourceFactory)
            .createMediaSource(stream25fps),
        /* resetPosition= */ true
    );
    player.prepare();

    // Observe: Video playback is choppy/stuttering on built-in TV
    // Note: External devices (Chromecast/FireTV) may work fine
}, 5000);

Technical Details

VideoFrameReleaseHelper Frame Rate Inference

ExoPlayer calculates frame rate by analyzing presentation timestamps (PTS):

// From VideoFrameReleaseTimeHelper.java
long averageFrameDurationNs = (framePresentationTimeNs - syncFramePresentationTimeNs) / frameCount;

When switching streams without stop():

  1. Old sync point remains (calibrated for 60fps)
  2. New frames arrive at 25fps timing
  3. Frame presentation timing calculations use stale baseline
  4. Results in incorrect frame display timing → choppiness

Why stop() Fixes It

player.stop():

  • Calls onDisabled() on all renderers
  • Releases MediaCodec decoder
  • Clears VideoFrameReleaseHelper state completely
  • Forces full reinitialization with new frame rate timing

Proposed Solutions

Option 1: Add Explicit Renderer Flush API

Provide a public API to flush video renderer state without releasing the decoder:

player.flushVideoRenderer(); // New API
player.setMediaSource(newMediaSource, true);
player.prepare();

This would:

  • Reset VideoFrameReleaseHelper timing state
  • Flush MediaCodec buffers
  • Keep decoder alive (faster transition)
  • Not clear the surface

Option 2: Improve setMediaSource() to Detect Frame Rate Changes

Automatically reset frame timing state when setMediaSource() is called with content that has significantly different properties:

// Automatically detect frame rate change and reset timing
player.setMediaSource(newMediaSource, /* resetPosition= */ true);

Internal behavior should:

  • Compare new stream's frame rate metadata with current
  • If significantly different (>10% difference), trigger renderer flush
  • Reset VideoFrameReleaseHelper sync points
  • Keep surface attached

Option 3: Enhance setKeepContentOnPlayerReset for Built-in TVs

Make setKeepContentOnPlayerReset(true) work on built-in TVs by:

  • Using TextureView instead of SurfaceView when this flag is enabled
  • TextureView can preserve last frame in GPU memory even when decoder releases
  • Automatically switch surface type based on device hardware characteristics

Option 4: Add Frame Rate Change Strategy

Similar to setVideoChangeFrameRateStrategy(), add:

player.setFrameRateTransitionStrategy(
    C.FRAME_RATE_TRANSITION_STRATEGY_RESET_TIMING
);

This would automatically handle timing resets when frame rate changes are detected.

Impact

This issue affects:

  • IPTV/Live TV applications with channel zapping between different frame rate streams
  • Built-in Android TV users (major TV manufacturers: Samsung, Sony, LG Android TVs)
  • Any application needing seamless transitions between variable frame rate content

Current workaround (using stop()) results in poor UX with black screens during channel changes.

Device Testing Results

Device Type stop() Behavior Without stop()
Built-in Android TV Black screen + Smooth Last frame + Choppy
Chromecast with Google TV Last frame + Smooth Last frame + Choppy
Fire TV Stick Last frame + Smooth Last frame + Choppy

Additional Context

  • The issue is specifically with 60fps → 25fps transitions (and similar large frame rate differences)
  • Same codec (H.264 → H.264) transitions still exhibit the problem
  • The choppiness does NOT smooth out over time - it persists throughout playback
  • Switching between streams with the same frame rate works perfectly without stop()

Request

Could the ExoPlayer/Media3 team provide:

  1. A recommended approach for handling frame rate changes without surface clearing
  2. Consideration of adding explicit renderer flush API
  3. Improved setKeepContentOnPlayerReset() behavior for built-in TVs

Devices that reproduce the issue

Sony Android TV
Google Chromecast, Streamer etc.

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions