androidx / media

Jetpack Media3 support libraries for media use cases, including ExoPlayer, an extensible media player for Android
https://developer.android.com/media/media3
Apache License 2.0
1.76k stars 423 forks source link

java.io.IOException: bt socket closed, read return #965

Open akrulec opened 10 months ago

akrulec commented 10 months ago

Version

Media3 1.1.1

More version details

We are using media3 library to send voice commands to the media outlet. We noticed that sending voice commands to exo player works great if there are not bluetooth devices connected to the phone. However, if the user is trying to use a bluetooth speaker (or headphones) while using our app, the bluetooth seems to crash occasionally, and pause the background music. For example, user is using Spotify to listen to the podcast. Every once in a while, they receive a spoken voice command from our app (via exoplayer). Each time they receive a command the system successfully ducks the podcast, and then gives it focus once our app is done. However, after 10-25 mins of back and forth, the bluetooth seems to crash, and the Spotify podcast fails to resume once our app gives away focus. I see the following error in the logs:

Error reading stream, exiting loop
                                                                                                    java.io.IOException: bt socket closed, read return: -1
                                                                                                        at android.bluetooth.BluetoothSocket.read(BluetoothSocket.java:797)
                                                                                                        at android.bluetooth.BluetoothInputStream.read(BluetoothInputStream.java:62)
                                                                                                        at java.io.DataInputStream.readUnsignedByte(DataInputStream.java:296)
                                                                                                        at csb.c(PG:2)
                                                                                                        at bom.run(PG:7)
                                                                                                        at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
                                                                                                        at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:644)
                                                                                                        at java.lang.Thread.run(Thread.java:1012)

Is there a way to figure out why is this crashing only when user has bluetooth connected?

Devices that reproduce the issue

Devices that do not reproduce the issue

No response

Reproducible in the demo app?

Yes

Reproduction steps

Connect bluetooth speaker or headphones to the device. Start playing podcast in YouTube Music, or Spotify. Play and pause

Expected result

The background media resumes successfully.

Actual result

Every once in a while the background media fails to resume, and there is a bluetooth error in the logs.

Media

Follow up in email

Bug Report

marcbaechinger commented 10 months ago

Thanks for your report.

We are using media3 library to send voice commands to the media outlet.

Can you clarify a bit what Media3 API you are using to send a voice command from your app to another app? I don't know how this works I'm afraid.

Best would probably be to extend the the repro steps to make them tell what API is called to pause/play another app with Media3.

google-oss-bot commented 10 months ago

Hey @akrulec. We need more information to resolve this issue but there hasn't been an update in 14 weekdays. I'm marking the issue as stale and if there are no new updates in the next 7 days I will close it automatically.

If you have more information that will help us get to the bottom of this, just add a comment!

akrulec commented 10 months ago

We are using ExoPlayer from media3 to play voice commands to the user while they are going through a routine. Each time we speak a voice command, we request focus, play our mp4, and then give away focus when the audio is done (usually short 1-8sec). If the user connects to the phone via bluetooth, and they are listening to anything in the background, the background music ducks or stops, but after a while, there is a bluetooth crash, and the background music never resumes.

import androidx.media3.common.AudioAttributes
import androidx.media3.common.C
import androidx.media3.common.PlaybackException
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.ExoPlaybackException
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.session.MediaSession
import androidx.media3.session.MediaSessionService

AudioPlayerService : MediaSessionService()
    private var exoPlayer: ExoPlayer? = null
    private var mediaSession: MediaSession? = null

    private var currentItem: AudioQueueItem? = null

    private val audioAttributes =
        AudioAttributes.Builder()
            // Cannot use C.USAGE_ASSISTANT here because it connects to Bixby on Samsung phones, and
            // it makes it impossible to adjust when the media is playing in the background. I
            // suspect it connects to AudioSystem.STREAM_ASSISTANT which is not publicly available.
            .setUsage(C.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
            .setContentType(C.AUDIO_CONTENT_TYPE_SPEECH)
            .build()

    // Lock for all adding and removing media operations. This seems to have fixed the issue when
    // sometimes the media is not ducking.
    private val focusLock = Any()
    // Check whether the playback is delayed, because audio focus was not granted.
    private var playbackDelayed = false
    // Variable to keep track on whether the playback should be continued when the focus is
    // regained.
    private var resumeOnFocusGain = false
    // Audio focus listener with the help from here:
    // https://developer.android.com/reference/android/media/AudioFocusRequest
    private val onAudioFocusChangeListener =
        AudioManager.OnAudioFocusChangeListener { focusChange ->
            when (focusChange) {
                AudioManager.AUDIOFOCUS_GAIN -> {
                    if (playbackDelayed || resumeOnFocusGain) {
                        synchronized(focusLock) {
                            playbackDelayed = false
                            resumeOnFocusGain = false
                        }
                        playbackNow()
                    }
                }
                AudioManager.AUDIOFOCUS_LOSS -> {
                    synchronized(focusLock) {
                        // This is not a transient loss, we shouldn't automatically resume for now.
                        resumeOnFocusGain = false
                        playbackDelayed = false
                    }
                    duckPlayback()
                }
                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT,
                AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
                    // Handle all transient losses the same way because we never duck.
                    synchronized(focusLock) {
                        // We should only resume if playback was interrupted.
                        resumeOnFocusGain = exoPlayer?.isPlaying ?: false
                        playbackDelayed = false
                    }
                    duckPlayback()
                }
            }
        }

        @OptIn(UnstableApi::class)
    override fun onCreate() {
        super.onCreate()

        exoPlayer =
            ExoPlayer.Builder(this)
                .setAudioAttributes(audioAttributes, false)
                .setMediaSourceFactory(
                    DefaultMediaSourceFactory(coPilotDownloadManager.cachedMediaSourceFactory))
                .build()
        exoPlayer?.also {
            it.addListener(playerListener)
            it.playWhenReady = true
            it.setWakeMode(PowerManager.PARTIAL_WAKE_LOCK)
            it.prepare()
            setVolume()
        }

        audioManager = applicationContext.getSystemService(Context.AUDIO_SERVICE) as AudioManager
        val audioAttributes =
            android.media.AudioAttributes.Builder()
                .setUsage(android.media.AudioAttributes.USAGE_ASSISTANCE_NAVIGATION_GUIDANCE)
                .setContentType(android.media.AudioAttributes.CONTENT_TYPE_SPEECH)
                .build()
        focusRequest =
            AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK)
                .setAcceptsDelayedFocusGain(false)
                .setAudioAttributes(audioAttributes)
                .setForceDucking(true)
                .setOnAudioFocusChangeListener(onAudioFocusChangeListener)
                .build()
        exoPlayer?.let { mediaSession = MediaSession.Builder(applicationContext, it).build() }

        acquireWifiLock()
    }
    ....
                currentItem?.let { item ->
                if (audioManager != null && focusRequest != null) {
                    // Requesting audio focus.
                    val res = audioManager!!.requestAudioFocus(focusRequest!!)
                    synchronized(focusLock) {
                        focusRequested = true

                        when (res) {
                            AudioManager.AUDIOFOCUS_REQUEST_FAILED -> {
                                playbackDelayed = false
                            }
                            AudioManager.AUDIOFOCUS_REQUEST_GRANTED -> {
                                playbackDelayed = false
                                playbackNow(item.url)
                            }
                            AudioManager.AUDIOFOCUS_REQUEST_DELAYED -> {
                                playbackDelayed = true
                            }
                        }
                    }
                }
            }
...
    /** Clears media from the player and abandons focus. */
    fun clearMedia() {
        synchronized(focusLock) {
            // First abandon focus.
            if (audioManager != null && focusRequest != null && focusRequested) {
                audioManager!!.abandonAudioFocusRequest(focusRequest!!)
                synchronized(focusLock) { focusRequested = false }
            }
            // Then clear the media.
            resumeOnFocusGain = false
            exoPlayer?.clearMediaItems()
        }
    }

    /** Plays the given URL or resumes the exo player if there is anything to play. */
    @OptIn(UnstableApi::class)
    private fun playbackNow(url: String? = null) {
        // Play new item or continue playing.
        url?.let {
            exoPlayer?.addMediaSource(coPilotDownloadManager.buildMediaSourceFromUrl(it))
        }
        setVolume()
        exoPlayer?.play()
    }

    /** Pauses the audio. Used when other apps request audio duck. */
    private fun duckPlayback() {
        val volume = SharedPreferencesUtil.getVoiceInstructionsVolume(applicationContext)
        // Volume comes in 1-100, and ExoPlayer accepts 0-1. Multiply with duck to talk in the back.
        exoPlayer?.volume = (volume / MAX_VOLUME) * DUCK_VOLUME
    }

    private fun acquireWifiLock() {
        if (wifiLock == null) {
            val wifiManager = getSystemService(Context.WIFI_SERVICE) as WifiManager
            wifiLock =
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    wifiManager.createWifiLock(
                        WifiManager.WIFI_MODE_FULL_LOW_LATENCY, "coPilot_lock")
                } else {
                    wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL, "coPilot_lock")
                }
        }

        wifiLock?.acquire()
    }
    private fun releaseWifiLock() {
        wifiLock?.release()
    }

    private inner class PlayerEventListener : Player.Listener {
        override fun onPlaybackStateChanged(playbackState: Int) {
            when (playbackState) {
                Player.STATE_ENDED -> {
                    // Only clear focus when no new items to play.
                    if (audioManager != null &&
                        focusRequest != null &&
                        focusRequested &&
                        (listener?.isQueueEmpty() == true)) {
                        audioManager!!.abandonAudioFocusRequest(focusRequest!!)
                        synchronized(focusLock) { focusRequested = false }
                    }
                    // Clear the media items, and reset the player.
                    // Unsure if this needs to happen here or in the STATE_READY.
                    exoPlayer?.clearMediaItems()
                    exoPlayer?.seekToDefaultPosition()
                    mediaReady()
                }
                Player.STATE_BUFFERING,
                Player.STATE_IDLE,
                Player.STATE_READY -> {
                    // Don't do anything for now.
                }
            }
        }

Dependencies

    version_media = "1.2.0"
    implementation "androidx.media3:media3-exoplayer:$version_media"
    implementation "androidx.media3:media3-session:$version_media"