androidx / androidx/media

When disconnected, the player transitions to STATE_READY instead STATE_IDLE and no error is reported.

Open
#869 1 comment 0 reactions 1 assignee View on GitHub

Nobody has claimed this yet.

bug needs triage
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
  1. Play an RTSP stream.
  2. Turn on "Airplane Mode" to cause a disconnect.
  3. 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 bugreport to android-media-github@google.com after filing this issue.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.