When disconnected, the player transitions to STATE_READY instead STATE_IDLE and no error is reported.
Nobody has claimed this yet.
- Dominant language
- Java
- Stars
- 3k
- Forks
- 955
- Avg merge
- 12d 14h
- Merged PRs (30d)
- 2
Description
Version
Media3 1.2.0
More version details
No response
Devices that reproduce the issue
Emulator running Android 13 API 33 x86_64
MicroTouch IC-215P-AA2 running Android 10
Devices that do not reproduce the issue
No response
Reproducible in the demo app?
Not tested
Reproduction steps
- Play an RTSP stream.
- Turn on "Airplane Mode" to cause a disconnect.
- The ExoPlayer transitions to STATE_BUFFERING, and the buffering indicator appears on the player. Then after several seconds, probably the RTSP timeout, the buffering indicator is removed and the player transitions to STATE_READY. No error is reported to
onPlayerError, and playback never recovers. There's no indication that playback has failed.
I'm working around this by polling. I've discovered that if the player disconnects, the buffer will be C.TIME_UNSET, which can be used to detect a stalled state.
// Start a watcher job to restart the player if it stops or stalls. This is necessary because the
// player won't report any errors.
stoppedCheckJob = scope.launch(mainDispatcher) {
while(true) {
delay(3000L)
if (restartJob == null) {
if (player.playbackState in listOf(
Player.STATE_IDLE,
Player.STATE_ENDED
)) {
Timber.w("$TAG: stopped player detected, restarting. card.id='${cardId}', config.name='${curConfig?.name}'")
restartAfterDelay()
} else if (isPlayerStalled()) {
Timber.w("$TAG: stalled player detected, restarting. card.id='${cardId}', config.name='${curConfig?.name}'")
restartAfterDelay()
}
}
}
}
/**
* The player says it's playing, but it's actually stalled because the buffer isn't loading.
* For some reason the RTSP media source or the player doesn't go into an error state here,
* it just acts like it's still working.
*/
private fun isPlayerStalled(): Boolean {
return player.playbackState == Player.STATE_READY && player.contentBufferedPosition == C.TIME_UNSET
}
Here's my full code for initializing the player:
// Start playback immediately instead of buffering for 5 seconds, and reduce the buffer size
@UnstableApi
private val loadControl = DefaultLoadControl.Builder()
.setBackBuffer(0, false)
// Min size must be over 500 to prevent low buffer warning.
.setBufferDurationsMs(1000, 5000, 0, 0)
// This defaults to 125Mb, which seems way too big.
.setTargetBufferBytes(2 * 1024 * 1024)
.build()
// Keep playback near the live edge to avoid falling behind and running out of memory for the buffer.
private val liveConfiguration = MediaItem.LiveConfiguration.Builder()
.setTargetOffsetMs(200)
.setMinOffsetMs(200)
.setMaxOffsetMs(200)
.setMinPlaybackSpeed(0.8f)
.setMaxPlaybackSpeed(10f)
.build()
@UnstableApi
private val livePlaybackSpeedControl = DefaultLivePlaybackSpeedControl.Builder().build().apply {
setLiveConfiguration(liveConfiguration)
}
// Use our custom media source so that liveConfig isn't ignored.
@UnstableApi
private val rtspMediaSourceFactory = CfaRtspMediaSource.Factory()
val player: ExoPlayer = ExoPlayer.Builder(ctx)
.setLivePlaybackSpeedControl(livePlaybackSpeedControl)
.setLoadControl(loadControl)
// Turn off audio because we don't need it, and it reduces the surface area for things that
// can throw errors and crash the app.
.setRenderersFactory(ExoPlayerNoAudioRenderersFactory(ctx))
.build()
val eventsListener = object : Player.Listener {
override fun onPlayerError(e: PlaybackException) {
Timber.e(e, "$TAG: ExoPlayer error. card.id='${cardId}', config.name='${curConfig?.name}'")
}
override fun onPlaybackStateChanged(playbackState: Int) {
// If we were previously playing and now we're not, then try to restart. We have to do
// this manually because RtspMediaSource doesn't implement setLoadErrorHandlingPolicy.
if (prevPlaybackState in listOf(Player.STATE_BUFFERING, Player.STATE_READY) &&
playbackState in listOf(Player.STATE_IDLE, Player.STATE_ENDED)
) {
restartAfterDelay()
}
prevPlaybackState = playbackState
prevPlaybackStateChangedAt = Instant.now()
}
}
init {
player.addListener(eventsListener)
}
fun syncMediaItem(/* args */) {
// ...
curMediaItem = MediaItem.Builder()
.setUri(newUrl)
.setLiveConfiguration(liveConfiguration)
.build()
val mediaSource = createMediaSource(curMediaItem!!)
player.setMediaSource(mediaSource)
player.volume = 0f
player.prepare()
player.play()
}
Expected result
The player should follow it's normal error behavior, which I think is to transition to STATE_IDLE and fire an error to the onPlayerError callback.
Actual result
The player transitions to STATE_READY, even though it's not playing. This, combined with the lack of an error being reported, makes it very difficult to implement error handling at all.
Overall I'm very frustrated with ExoPlayer, stuff like correct error handling is necessary for an app to work correctly, and all the bugs and memory leaks in ExoPlayer have made it almost impossible to use. It's taken me 2 weeks and a lot of dirty hacks just to get it to reliably play an RTSP stream without running out of memory or disconnecting.
Media
Needs an RTSP stream, I'm not sure of a public one.
Bug Report
- You will email the zip file produced by
adb bugreportto android-media-github@google.com after filing this issue.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.