-
Notifications
You must be signed in to change notification settings - Fork 690
Description
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:
- Smooth playback without frame rate timing issues (choppiness)
- 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
syncFramePresentationTimeNsand 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():
- Old sync point remains (calibrated for 60fps)
- New frames arrive at 25fps timing
- Frame presentation timing calculations use stale baseline
- Results in incorrect frame display timing → choppiness
Why stop() Fixes It
player.stop():
- Calls
onDisabled()on all renderers - Releases MediaCodec decoder
- Clears
VideoFrameReleaseHelperstate 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
VideoFrameReleaseHelpertiming 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
VideoFrameReleaseHelpersync 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:
- A recommended approach for handling frame rate changes without surface clearing
- Consideration of adding explicit renderer flush API
- Improved
setKeepContentOnPlayerReset()behavior for built-in TVs
Devices that reproduce the issue
Sony Android TV
Google Chromecast, Streamer etc.