ExoPlayer stuck in buffering after re-adding the surface view a few time
See original GitHub issueExoPlayer gets stuck in buffering after re-adding the surface view a few time.
I created a minimal example that demonstrates that problem. I used ExoPlayer 2.3.1 and tested it on a Nexus 5X with Android 7.1.2 and on a Moto G4 Play with Android 6.
I found this issue because I am removing the surface from ExoPlayer in a recyclerview when the view gets recycled but while trying to find the cause of this I created this demo without all the complexity.
It just prepares an ExoPlayer and when the user clicks on the screen it attaches / detaches the SurfaceView.
I logged all events and it gets stuck in buffering state:
04-19 08:11:47.199 D/player: onPlayerStateChanged with playWhenRead=true and playbackState=idle
04-19 08:11:47.513 D/player: onPlayerStateChanged with playWhenRead=true and playbackState=buffering
04-19 08:11:47.888 D/player: onTimelineChanged
04-19 08:11:47.941 D/player: onLoadingChanged
04-19 08:11:49.023 D/player: onTracksChanged
04-19 08:11:50.001 D/player: onPlayerStateChanged with playWhenRead=true and playbackState=ready
04-19 08:11:55.792 D/player: onLoadingChanged
04-19 08:12:05.599 D/player: onPlayerStateChanged with playWhenRead=true and playbackState=buffering
04-19 08:12:05.664 D/player: onPlayerStateChanged with playWhenRead=true and playbackState=ready
04-19 08:12:09.579 D/player: onPlayerStateChanged with playWhenRead=true and playbackState=buffering
04-19 08:12:09.623 D/player: onPlayerStateChanged with playWhenRead=true and playbackState=ready
04-19 08:12:12.021 D/player: onPlayerStateChanged with playWhenRead=true and playbackState=buffering
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val container = findViewById(android.R.id.content) as ViewGroup
val surface = SurfaceView(this)
container.addView(surface)
val videoTrackSelectionFactory = AdaptiveTrackSelection.Factory(DefaultBandwidthMeter())
val trackSelector = DefaultTrackSelector(videoTrackSelectionFactory)
val player = ExoPlayerFactory.newSimpleInstance(this, trackSelector, DefaultLoadControl())
player.addListener(object : ExoPlayer.EventListener {
override fun onTracksChanged(trackGroups: TrackGroupArray?, trackSelections: TrackSelectionArray?) {
Log.d("player", "onTracksChanged")
}
override fun onPlayerError(error: ExoPlaybackException?) {
Log.d("player", "onPlayerError")
}
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
val state = when (playbackState) {
ExoPlayer.STATE_BUFFERING -> "buffering"
ExoPlayer.STATE_ENDED -> "ended"
ExoPlayer.STATE_READY -> "ready"
ExoPlayer.STATE_IDLE -> "idle"
else -> "unknownState$playbackState"
}
Log.d("player", "onPlayerStateChanged with playWhenRead=$playWhenReady and playbackState=$state")
}
override fun onLoadingChanged(isLoading: Boolean) {
Log.d("player", "onLoadingChanged")
}
override fun onPositionDiscontinuity() {
Log.d("player", "onPositionDiscontinuity")
}
override fun onTimelineChanged(timeline: Timeline?, manifest: Any?) {
Log.d("player", "onTimelineChanged")
}
})
val dataSourceFactory = DefaultDataSourceFactory(this, packageName)
val dashUri = Uri.parse("http://www-itec.uni-klu.ac.at/ftp/datasets/DASHDataset2014/BigBuckBunny/15sec/BigBuckBunny_15s_simple_2014_05_09.mpd")
val mediaSource = DashMediaSource(dashUri, dataSourceFactory, DefaultDashChunkSource.Factory(dataSourceFactory), null, null)
player.prepare(mediaSource)
player.setVideoSurfaceView(surface)
player.playWhenReady = true
container.setOnClickListener {
if (container.childCount == 0) container.addView(surface)
else container.removeView(surface)
}
}
}
Issue Analytics
- State:
- Created 6 years ago
- Comments:11 (6 by maintainers)
Top Results From Across the Web
A brand new website interface for an even better experience!
ExoPlayer stuck in buffering after re-adding the surface view a few time.
Read more >华为移动服务/hms-wiseplay-demo-exoplayer - Gitee.com
Fix case where another app spuriously holding transient audio focus could prevent ExoPlayer from acquiring audio focus for an indefinite period of time...
Read more >SurfaceView - Android Developers
android:accessibilityTraversalAfter, Sets the id of a view after which this one ... If false, no state will be saved for this view when...
Read more >Preloading and Buffering Videos in Android with ExoPlayer
This tutorial will show the reader how to pre-load and buffer videos in Android using ExoPlayer.
Read more >RELEASENOTES.md · master · Lahlouh, Ishak / RFC_Player
Fix gradle config to allow specifying a relative path for exoplayerRoot when depending on ExoPlayer locally (#8927). Update MediaItem.Builder ...
Read more >
Top Related Medium Post
No results found
Top Related StackOverflow Question
No results found
Troubleshoot Live Code
Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free
Top Related Reddit Thread
No results found
Top Related Hackernoon Post
No results found
Top Related Tweet
No results found
Top Related Dev.to Post
No results found
Top Related Hashnode Post
No results found
I took a look at this and was able to reproduce, thanks. It’s a fundamental limitation of MediaCodec (in the Android platform) that it needs a Surface attached to it at all times. When the SurfaceView is removed from the view the underlying Surface is destroyed. This in turn forces ExoPlayer to destroy its MediaCodec instance, because there’s no longer a Surface for it to be attached to.
When the SurfaceView is added back to the view hierarchy its underlying Surface is created again. ExoPlayer creates a new MediaCodec instance, however since it’s a new instance it can only start decoding from the next key-frame. It does this and displays the next key-frame immediately, then waits for the playback position to reach the position of that key-frame before rendering subsequent frames. This is why even after a single remove/add cycle, you observe that the first frame that’s displayed is from the future and frozen for a while.
If you remove/add multiple times the player skips ahead 1 key-frame each time. If you do this enough, the player gets stuck in a weird state where it’s buffered a long way into the future compared to the correct playback position, but by successively rendering all of the key-frames it actually doesn’t have any frame to render. The player doesn’t think it needs to buffer, but it’s also in a state where it cannot transition to the playing state because it hasn’t rendered a frame.
The fundamental limitation of MediaCodec is a real pain, unfortunately. There are however things we can do in the library, and things you can do in your application:
On the library side:
On the application side, there are a bunch of things you can do to try and provide a better user experience.
DummySurface, which we recently added to the library. This gives proper seamless re-join. It wont work prior to API level 23, because the approach relies onMediaCodec.setOutputSurface. The basic idea is to use theDummySurfaceas a means of ensuring there’s always a surface attached to theMediaCodec. Rather than callingplayer.setVideoSurfaceView, manage the Surface lifecycle yourself. Something like this should work well from your activity:With:
In the future, we hope to be able to hook
DummySurfaceup automatically inside of the player for cases where it helps.For pre-API-23 there are complicated solutions involving off-screen rendering into GL textures. It would take quite a bit of time and effort to put together sample code, however.
One easier option is to disable and re-enable the video renderer, but this causes re-buffering and so isn’t ideal. Nevertheless, you can do it something like this:
Thanks. I cannot reproduce the issue with the latest
dev-v2. Could you give that a try to see if you see the same behavior there? I just pushed some recent changes todev-v2to make sure you have all of the same changes I tested with, so be sure to pull them if you give it a try.