@@ -43,19 +43,23 @@ import io.getstream.video.android.core.utils.buildAudioConstraints
4343import io.getstream.video.android.core.utils.mapState
4444import io.getstream.video.android.core.utils.safeCall
4545import kotlinx.coroutines.CoroutineScope
46+ import kotlinx.coroutines.Job
4647import kotlinx.coroutines.delay
4748import kotlinx.coroutines.flow.MutableStateFlow
4849import kotlinx.coroutines.flow.StateFlow
4950import kotlinx.coroutines.launch
51+ import kotlinx.coroutines.suspendCancellableCoroutine
5052import org.webrtc.Camera2Capturer
5153import org.webrtc.Camera2Enumerator
5254import org.webrtc.CameraEnumerationAndroid
55+ import org.webrtc.CameraVideoCapturer
5356import org.webrtc.EglBase
5457import org.webrtc.MediaStreamTrack
5558import org.webrtc.ScreenCapturerAndroid
5659import org.webrtc.SurfaceTextureHelper
5760import stream.video.sfu.models.VideoDimension
5861import java.util.UUID
62+ import kotlin.coroutines.resumeWithException
5963
6064sealed class DeviceStatus {
6165 data object NotSelected : DeviceStatus ()
@@ -169,6 +173,7 @@ class SpeakerManager(
169173 selectedBeforeSpeaker != null &&
170174 selectedBeforeSpeaker !is StreamAudioDevice .Speakerphone &&
171175 devices.contains(selectedBeforeSpeaker) -> selectedBeforeSpeaker
176+
172177 else -> firstNonSpeaker
173178 }
174179
@@ -659,20 +664,76 @@ class CameraManager(
659664 }
660665 }
661666
662- /* *
663- * Flips the camera
664- */
665- fun flip () {
666- if (isCapturingVideo) {
667- setup()
668- val newDirection = when (_direction .value) {
669- CameraDirection .Front -> CameraDirection .Back
670- CameraDirection .Back -> CameraDirection .Front
667+ private var job: Job ? = null
668+
669+ private suspend fun flipInternal (): Boolean =
670+ suspendCancellableCoroutine { cont ->
671+ videoCapturer.switchCamera(object : CameraVideoCapturer .CameraSwitchHandler {
672+ override fun onCameraSwitchDone (isFrontCamera : Boolean ) {
673+ if (cont.isActive) cont.resumeWith(Result .success(isFrontCamera))
674+ }
675+
676+ override fun onCameraSwitchError (error : String? ) {
677+ if (cont.isActive) {
678+ cont.resumeWithException(
679+ RuntimeException (
680+ error ? : " Unknown" ,
681+ ),
682+ )
683+ }
684+ }
685+ })
686+ cont.invokeOnCancellation {
687+ // No direct cancel for switchCamera; nothing to do here.
671688 }
672- val device = devices.firstOrNull { it.direction == newDirection }
673- device?.let { select(it.id, triggeredByFlip = true ) }
689+ }
690+
691+ /* * Flips the camera */
692+ fun flip () {
693+ logger.v { " [flip] no args" }
694+ if (! isCapturingVideo) {
695+ return
696+ }
674697
675- videoCapturer.switchCamera(null )
698+ if (job != null ) {
699+ logger.v { " [flip] job already running" }
700+ return
701+ }
702+
703+ job = mediaManager.scope.launch {
704+ logger.v { " [flip] launched" }
705+ try {
706+ val isFront = flipInternal()
707+ logger.v { " [flip] isFront: $isFront " }
708+
709+ // 3) Update state ONLY after success; no select()/restart.
710+ val newDir = if (isFront) CameraDirection .Front else CameraDirection .Back
711+ _direction .value = newDir
712+
713+ val dev = devices.firstOrNull { it.direction == newDir }
714+ _selectedDevice .value = dev
715+ _availableResolutions .value = dev?.supportedFormats?.toList().orEmpty()
716+
717+ _resolution .value = selectDesiredResolution(
718+ dev?.supportedFormats,
719+ mediaManager.call.state.settings.value?.video,
720+ )
721+ if (_resolution .value != null ) {
722+ videoCapturer.changeCaptureFormat(
723+ _resolution .value!! .width,
724+ _resolution .value!! .height,
725+ _resolution .value!! .framerate.max,
726+ )
727+ }
728+
729+ // preview.mirror = (newDir == CameraDirection.Front)
730+ } catch (t: Throwable ) {
731+ logger.e(t) { " [flip] failed" }
732+ } finally {
733+ logger.v { " [flip] finally, wait to settle" }
734+ delay(500 )
735+ job = null
736+ }
676737 }
677738 }
678739
@@ -752,9 +813,9 @@ class CameraManager(
752813 * Handle the setup of the camera manager and enumerator
753814 * You should only call this once the permissions have been granted
754815 */
755- internal fun setup (force : Boolean = false) {
816+ internal fun setup (force : Boolean = false): Result < Unit > = runCatching {
756817 if (setupCompleted && ! force) {
757- return
818+ return @runCatching
758819 }
759820
760821 initDeviceList()
0 commit comments