diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/RetryableInitAudioSink.kt b/app/src/main/java/org/thoughtcrime/securesms/components/voice/RetryableInitAudioSink.kt new file mode 100644 index 0000000000..d06ce153ae --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/RetryableInitAudioSink.kt @@ -0,0 +1,52 @@ +package org.thoughtcrime.securesms.components.voice + +import android.content.Context +import com.google.android.exoplayer2.audio.AudioCapabilities +import com.google.android.exoplayer2.audio.AudioSink +import com.google.android.exoplayer2.audio.DefaultAudioSink +import org.signal.core.util.logging.Log +import java.nio.ByteBuffer + +/** + * Certain devices, including the incredibly popular Samsung A51, often fail to create a proper audio sink when switching from "media" mode to "communication" mode. + * It does eventually recover, but it needs to be given ample opportunity to. + * This class wraps the final DefaultAudioSink to provide exactly that functionality. + */ +class RetryableInitAudioSink( + context: Context, + enableFloatOutput: Boolean, + enableAudioTrackPlaybackParams: Boolean, + enableOffload: Boolean, + val delegate: AudioSink = DefaultAudioSink.Builder() + .setAudioCapabilities(AudioCapabilities.getCapabilities(context)) + .setEnableFloatOutput(enableFloatOutput) + .setEnableAudioTrackPlaybackParams(enableAudioTrackPlaybackParams) + .setOffloadMode(if (enableOffload) DefaultAudioSink.OFFLOAD_MODE_ENABLED_GAPLESS_REQUIRED else DefaultAudioSink.OFFLOAD_MODE_DISABLED) + .build() +) : AudioSink by delegate { + + private var retriesLeft = INITIAL_RETRY_COUNT + + override fun handleBuffer(buffer: ByteBuffer, presentationTimeUs: Long, encodedAccessUnitCount: Int): Boolean { + return try { + val bufferHandled = delegate.handleBuffer(buffer, presentationTimeUs, encodedAccessUnitCount) + if (bufferHandled) { + retriesLeft = INITIAL_RETRY_COUNT + } + bufferHandled + } catch (e: AudioSink.InitializationException) { + Log.w(TAG, "Could not handle this buffer due to an initialization exception. $retriesLeft retries remaining.", e) + if (retriesLeft > 0) { + retriesLeft-- + false + } else { + throw e + } + } + } + + companion object { + private val TAG = Log.tag(RetryableInitAudioSink::class.java) + const val INITIAL_RETRY_COUNT = 5 + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackController.kt b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackController.kt index 256e32a7f5..4a600a80a9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackController.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlaybackController.kt @@ -4,16 +4,16 @@ import android.media.AudioManager import android.os.Bundle import android.os.ResultReceiver import com.google.android.exoplayer2.C +import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.PlaybackParameters import com.google.android.exoplayer2.Player -import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector import com.google.android.exoplayer2.util.Util import org.signal.core.util.logging.Log class VoiceNotePlaybackController( - private val player: SimpleExoPlayer, + private val player: ExoPlayer, private val voiceNotePlaybackParameters: VoiceNotePlaybackParameters ) : MediaSessionConnector.CommandReceiver { @@ -42,7 +42,6 @@ class VoiceNotePlaybackController( player.playWhenReady = false player.setAudioAttributes(attributes, newStreamType == AudioManager.STREAM_MUSIC) - if (newStreamType == AudioManager.STREAM_VOICE_CALL) { player.playWhenReady = true } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlayer.kt b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlayer.kt index 52796c9b73..c3a91f3abc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlayer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/voice/VoiceNotePlayer.kt @@ -3,14 +3,17 @@ package org.thoughtcrime.securesms.components.voice import android.content.Context import com.google.android.exoplayer2.C import com.google.android.exoplayer2.DefaultLoadControl +import com.google.android.exoplayer2.DefaultRenderersFactory +import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.ForwardingPlayer -import com.google.android.exoplayer2.SimpleExoPlayer import com.google.android.exoplayer2.audio.AudioAttributes +import com.google.android.exoplayer2.audio.AudioSink import org.thoughtcrime.securesms.video.exo.SignalMediaSourceFactory class VoiceNotePlayer @JvmOverloads constructor( context: Context, - val internalPlayer: SimpleExoPlayer = SimpleExoPlayer.Builder(context) + val internalPlayer: ExoPlayer = ExoPlayer.Builder(context) + .setRenderersFactory(WorkaroundRenderersFactory(context)) .setMediaSourceFactory(SignalMediaSourceFactory(context)) .setLoadControl( DefaultLoadControl.Builder() @@ -39,3 +42,12 @@ class VoiceNotePlayer @JvmOverloads constructor( } } } + +/** + * @see RetryableInitAudioSink + */ +class WorkaroundRenderersFactory(val context: Context) : DefaultRenderersFactory(context) { + override fun buildAudioSink(context: Context, enableFloatOutput: Boolean, enableAudioTrackPlaybackParams: Boolean, enableOffload: Boolean): AudioSink? { + return RetryableInitAudioSink(context, enableFloatOutput, enableAudioTrackPlaybackParams, enableOffload) + } +}