Committed by
GitHub
Make LocalVideoTrack options FlowObservable (#220)
* Change track options to be FlowObservable * View based sample update
正在显示
7 个修改的文件
包含
197 行增加
和
55 行删除
| @@ -5,17 +5,19 @@ import android.content.Context | @@ -5,17 +5,19 @@ import android.content.Context | ||
| 5 | import android.content.pm.PackageManager | 5 | import android.content.pm.PackageManager |
| 6 | import android.hardware.camera2.CameraManager | 6 | import android.hardware.camera2.CameraManager |
| 7 | import androidx.core.content.ContextCompat | 7 | import androidx.core.content.ContextCompat |
| 8 | +import com.github.ajalt.timberkt.Timber | ||
| 8 | import dagger.assisted.Assisted | 9 | import dagger.assisted.Assisted |
| 9 | import dagger.assisted.AssistedFactory | 10 | import dagger.assisted.AssistedFactory |
| 10 | import dagger.assisted.AssistedInject | 11 | import dagger.assisted.AssistedInject |
| 11 | import io.livekit.android.memory.CloseableManager | 12 | import io.livekit.android.memory.CloseableManager |
| 12 | import io.livekit.android.memory.SurfaceTextureHelperCloser | 13 | import io.livekit.android.memory.SurfaceTextureHelperCloser |
| 13 | import io.livekit.android.room.DefaultsManager | 14 | import io.livekit.android.room.DefaultsManager |
| 14 | -import io.livekit.android.room.track.video.Camera1CapturerWithSize | ||
| 15 | -import io.livekit.android.room.track.video.Camera2CapturerWithSize | ||
| 16 | -import io.livekit.android.room.track.video.VideoCapturerWithSize | 15 | +import io.livekit.android.room.track.video.* |
| 16 | +import io.livekit.android.util.FlowObservable | ||
| 17 | import io.livekit.android.util.LKLog | 17 | import io.livekit.android.util.LKLog |
| 18 | +import io.livekit.android.util.flowDelegate | ||
| 18 | import org.webrtc.* | 19 | import org.webrtc.* |
| 20 | +import org.webrtc.CameraVideoCapturer.CameraEventsHandler | ||
| 19 | import java.util.* | 21 | import java.util.* |
| 20 | 22 | ||
| 21 | 23 | ||
| @@ -30,7 +32,7 @@ constructor( | @@ -30,7 +32,7 @@ constructor( | ||
| 30 | @Assisted private var capturer: VideoCapturer, | 32 | @Assisted private var capturer: VideoCapturer, |
| 31 | @Assisted private var source: VideoSource, | 33 | @Assisted private var source: VideoSource, |
| 32 | @Assisted name: String, | 34 | @Assisted name: String, |
| 33 | - @Assisted var options: LocalVideoTrackOptions, | 35 | + @Assisted options: LocalVideoTrackOptions, |
| 34 | @Assisted rtcTrack: org.webrtc.VideoTrack, | 36 | @Assisted rtcTrack: org.webrtc.VideoTrack, |
| 35 | private val peerConnectionFactory: PeerConnectionFactory, | 37 | private val peerConnectionFactory: PeerConnectionFactory, |
| 36 | private val context: Context, | 38 | private val context: Context, |
| @@ -42,6 +44,10 @@ constructor( | @@ -42,6 +44,10 @@ constructor( | ||
| 42 | override var rtcTrack: org.webrtc.VideoTrack = rtcTrack | 44 | override var rtcTrack: org.webrtc.VideoTrack = rtcTrack |
| 43 | internal set | 45 | internal set |
| 44 | 46 | ||
| 47 | + @FlowObservable | ||
| 48 | + @get:FlowObservable | ||
| 49 | + var options: LocalVideoTrackOptions by flowDelegate(options) | ||
| 50 | + | ||
| 45 | val dimensions: Dimensions | 51 | val dimensions: Dimensions |
| 46 | get() { | 52 | get() { |
| 47 | (capturer as? VideoCapturerWithSize)?.let { capturerWithSize -> | 53 | (capturer as? VideoCapturerWithSize)?.let { capturerWithSize -> |
| @@ -116,13 +122,46 @@ constructor( | @@ -116,13 +122,46 @@ constructor( | ||
| 116 | targetDeviceId = deviceNames[(currentIndex + 1) % deviceNames.size] | 122 | targetDeviceId = deviceNames[(currentIndex + 1) % deviceNames.size] |
| 117 | } | 123 | } |
| 118 | 124 | ||
| 125 | + fun updateCameraOptions() { | ||
| 126 | + val newOptions = options.copy( | ||
| 127 | + deviceId = targetDeviceId, | ||
| 128 | + position = enumerator.getCameraPosition(targetDeviceId) | ||
| 129 | + ) | ||
| 130 | + options = newOptions | ||
| 131 | + } | ||
| 132 | + | ||
| 119 | val cameraSwitchHandler = object : CameraVideoCapturer.CameraSwitchHandler { | 133 | val cameraSwitchHandler = object : CameraVideoCapturer.CameraSwitchHandler { |
| 120 | override fun onCameraSwitchDone(isFrontFacing: Boolean) { | 134 | override fun onCameraSwitchDone(isFrontFacing: Boolean) { |
| 121 | - val newOptions = options.copy( | ||
| 122 | - deviceId = targetDeviceId, | ||
| 123 | - position = enumerator.getCameraPosition(targetDeviceId) | ||
| 124 | - ) | ||
| 125 | - options = newOptions | 135 | + // For cameras we control, wait until the first frame to ensure everything is okay. |
| 136 | + if (cameraCapturer is CameraCapturerWithSize) { | ||
| 137 | + cameraCapturer.cameraEventsDispatchHandler | ||
| 138 | + .registerHandler(object : CameraEventsHandler { | ||
| 139 | + override fun onFirstFrameAvailable() { | ||
| 140 | + updateCameraOptions() | ||
| 141 | + cameraCapturer.cameraEventsDispatchHandler.unregisterHandler(this) | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + override fun onCameraError(p0: String?) { | ||
| 145 | + cameraCapturer.cameraEventsDispatchHandler.unregisterHandler(this) | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + override fun onCameraDisconnected() { | ||
| 149 | + cameraCapturer.cameraEventsDispatchHandler.unregisterHandler(this) | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + override fun onCameraFreezed(p0: String?) { | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | + override fun onCameraOpening(p0: String?) { | ||
| 156 | + } | ||
| 157 | + | ||
| 158 | + override fun onCameraClosed() { | ||
| 159 | + cameraCapturer.cameraEventsDispatchHandler.unregisterHandler(this) | ||
| 160 | + } | ||
| 161 | + }) | ||
| 162 | + } else { | ||
| 163 | + updateCameraOptions() | ||
| 164 | + } | ||
| 126 | } | 165 | } |
| 127 | 166 | ||
| 128 | override fun onCameraSwitchError(errorDescription: String?) { | 167 | override fun onCameraSwitchError(errorDescription: String?) { |
| @@ -154,6 +193,7 @@ constructor( | @@ -154,6 +193,7 @@ constructor( | ||
| 154 | // sender owns rtcTrack, so it'll take care of disposing it. | 193 | // sender owns rtcTrack, so it'll take care of disposing it. |
| 155 | oldRtcTrack.setEnabled(false) | 194 | oldRtcTrack.setEnabled(false) |
| 156 | 195 | ||
| 196 | + // Close resources associated to the old track. new track resources is registered in createTrack. | ||
| 157 | val oldCloseable = closeableManager.unregisterResource(oldRtcTrack) | 197 | val oldCloseable = closeableManager.unregisterResource(oldRtcTrack) |
| 158 | oldCloseable?.close() | 198 | oldCloseable?.close() |
| 159 | 199 | ||
| @@ -298,8 +338,9 @@ constructor( | @@ -298,8 +338,9 @@ constructor( | ||
| 298 | enumerator: CameraEnumerator, | 338 | enumerator: CameraEnumerator, |
| 299 | options: LocalVideoTrackOptions | 339 | options: LocalVideoTrackOptions |
| 300 | ): Pair<VideoCapturer, LocalVideoTrackOptions>? { | 340 | ): Pair<VideoCapturer, LocalVideoTrackOptions>? { |
| 341 | + val cameraEventsDispatchHandler = CameraEventsDispatchHandler() | ||
| 301 | val targetDeviceName = enumerator.findCamera(options.deviceId, options.position) ?: return null | 342 | val targetDeviceName = enumerator.findCamera(options.deviceId, options.position) ?: return null |
| 302 | - val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, null) | 343 | + val targetVideoCapturer = enumerator.createCapturer(targetDeviceName, cameraEventsDispatchHandler) |
| 303 | 344 | ||
| 304 | // back fill any missing information | 345 | // back fill any missing information |
| 305 | val newOptions = options.copy( | 346 | val newOptions = options.copy( |
| @@ -310,7 +351,11 @@ constructor( | @@ -310,7 +351,11 @@ constructor( | ||
| 310 | // Cache supported capture formats ahead of time to avoid future camera locks. | 351 | // Cache supported capture formats ahead of time to avoid future camera locks. |
| 311 | Camera1Helper.getSupportedFormats(Camera1Helper.getCameraId(newOptions.deviceId)) | 352 | Camera1Helper.getSupportedFormats(Camera1Helper.getCameraId(newOptions.deviceId)) |
| 312 | return Pair( | 353 | return Pair( |
| 313 | - Camera1CapturerWithSize(targetVideoCapturer, targetDeviceName), | 354 | + Camera1CapturerWithSize( |
| 355 | + targetVideoCapturer, | ||
| 356 | + targetDeviceName, | ||
| 357 | + cameraEventsDispatchHandler | ||
| 358 | + ), | ||
| 314 | newOptions | 359 | newOptions |
| 315 | ) | 360 | ) |
| 316 | } | 361 | } |
| @@ -320,7 +365,8 @@ constructor( | @@ -320,7 +365,8 @@ constructor( | ||
| 320 | Camera2CapturerWithSize( | 365 | Camera2CapturerWithSize( |
| 321 | targetVideoCapturer, | 366 | targetVideoCapturer, |
| 322 | context.getSystemService(Context.CAMERA_SERVICE) as CameraManager, | 367 | context.getSystemService(Context.CAMERA_SERVICE) as CameraManager, |
| 323 | - targetDeviceName | 368 | + targetDeviceName, |
| 369 | + cameraEventsDispatchHandler | ||
| 324 | ), | 370 | ), |
| 325 | newOptions | 371 | newOptions |
| 326 | ) | 372 | ) |
| @@ -370,10 +416,7 @@ constructor( | @@ -370,10 +416,7 @@ constructor( | ||
| 370 | private fun CameraEnumerator.findCamera(predicate: (deviceName: String) -> Boolean): String? { | 416 | private fun CameraEnumerator.findCamera(predicate: (deviceName: String) -> Boolean): String? { |
| 371 | for (deviceName in deviceNames) { | 417 | for (deviceName in deviceNames) { |
| 372 | if (predicate(deviceName)) { | 418 | if (predicate(deviceName)) { |
| 373 | - val videoCapturer = createCapturer(deviceName, null) | ||
| 374 | - if (videoCapturer != null) { | ||
| 375 | - return deviceName | ||
| 376 | - } | 419 | + return deviceName |
| 377 | } | 420 | } |
| 378 | } | 421 | } |
| 379 | return null | 422 | return null |
livekit-android-sdk/src/main/java/io/livekit/android/room/track/video/CameraEventsDispatchHandler.kt
0 → 100644
| 1 | +package io.livekit.android.room.track.video | ||
| 2 | + | ||
| 3 | +import org.webrtc.CameraVideoCapturer.CameraEventsHandler | ||
| 4 | + | ||
| 5 | +/** | ||
| 6 | + * Dispatches CameraEventsHandler callbacks to registered handlers. | ||
| 7 | + * | ||
| 8 | + * @suppress | ||
| 9 | + */ | ||
| 10 | +internal class CameraEventsDispatchHandler : CameraEventsHandler { | ||
| 11 | + private val handlers = mutableSetOf<CameraEventsHandler>() | ||
| 12 | + | ||
| 13 | + @Synchronized | ||
| 14 | + fun registerHandler(handler: CameraEventsHandler) { | ||
| 15 | + handlers.add(handler) | ||
| 16 | + } | ||
| 17 | + | ||
| 18 | + @Synchronized | ||
| 19 | + fun unregisterHandler(handler: CameraEventsHandler) { | ||
| 20 | + handlers.remove(handler) | ||
| 21 | + } | ||
| 22 | + | ||
| 23 | + override fun onCameraError(errorDescription: String) { | ||
| 24 | + val handlersCopy = handlers.toMutableSet() | ||
| 25 | + for (handler in handlersCopy) { | ||
| 26 | + handler.onCameraError(errorDescription) | ||
| 27 | + } | ||
| 28 | + } | ||
| 29 | + | ||
| 30 | + override fun onCameraDisconnected() { | ||
| 31 | + val handlersCopy = handlers.toMutableSet() | ||
| 32 | + for (handler in handlersCopy) { | ||
| 33 | + handler.onCameraDisconnected() | ||
| 34 | + } | ||
| 35 | + } | ||
| 36 | + | ||
| 37 | + override fun onCameraFreezed(errorDescription: String) { | ||
| 38 | + val handlersCopy = handlers.toMutableSet() | ||
| 39 | + for (handler in handlersCopy) { | ||
| 40 | + handler.onCameraFreezed(errorDescription) | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + override fun onCameraOpening(cameraName: String) { | ||
| 45 | + val handlersCopy = handlers.toMutableSet() | ||
| 46 | + for (handler in handlersCopy) { | ||
| 47 | + handler.onCameraOpening(cameraName) | ||
| 48 | + } | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + override fun onFirstFrameAvailable() { | ||
| 52 | + val handlersCopy = handlers.toMutableSet() | ||
| 53 | + for (handler in handlersCopy) { | ||
| 54 | + handler.onFirstFrameAvailable() | ||
| 55 | + } | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + override fun onCameraClosed() { | ||
| 59 | + val handlersCopy = handlers.toMutableSet() | ||
| 60 | + for (handler in handlersCopy) { | ||
| 61 | + handler.onCameraClosed() | ||
| 62 | + } | ||
| 63 | + } | ||
| 64 | + | ||
| 65 | +} |
| @@ -13,10 +13,19 @@ internal interface VideoCapturerWithSize : VideoCapturer { | @@ -13,10 +13,19 @@ internal interface VideoCapturerWithSize : VideoCapturer { | ||
| 13 | /** | 13 | /** |
| 14 | * @suppress | 14 | * @suppress |
| 15 | */ | 15 | */ |
| 16 | + | ||
| 17 | +internal abstract class CameraCapturerWithSize( | ||
| 18 | + val cameraEventsDispatchHandler: CameraEventsDispatchHandler | ||
| 19 | +) : VideoCapturerWithSize | ||
| 20 | + | ||
| 21 | +/** | ||
| 22 | + * @suppress | ||
| 23 | + */ | ||
| 16 | internal class Camera1CapturerWithSize( | 24 | internal class Camera1CapturerWithSize( |
| 17 | private val capturer: Camera1Capturer, | 25 | private val capturer: Camera1Capturer, |
| 18 | - private val deviceName: String? | ||
| 19 | -) : CameraVideoCapturer by capturer, VideoCapturerWithSize { | 26 | + private val deviceName: String?, |
| 27 | + cameraEventsDispatchHandler: CameraEventsDispatchHandler, | ||
| 28 | +) : CameraCapturerWithSize(cameraEventsDispatchHandler), CameraVideoCapturer by capturer { | ||
| 20 | override fun findCaptureFormat(width: Int, height: Int): Size { | 29 | override fun findCaptureFormat(width: Int, height: Int): Size { |
| 21 | val cameraId = Camera1Helper.getCameraId(deviceName) | 30 | val cameraId = Camera1Helper.getCameraId(deviceName) |
| 22 | return Camera1Helper.findClosestCaptureFormat(cameraId, width, height) | 31 | return Camera1Helper.findClosestCaptureFormat(cameraId, width, height) |
| @@ -29,8 +38,9 @@ internal class Camera1CapturerWithSize( | @@ -29,8 +38,9 @@ internal class Camera1CapturerWithSize( | ||
| 29 | internal class Camera2CapturerWithSize( | 38 | internal class Camera2CapturerWithSize( |
| 30 | private val capturer: Camera2Capturer, | 39 | private val capturer: Camera2Capturer, |
| 31 | private val cameraManager: CameraManager, | 40 | private val cameraManager: CameraManager, |
| 32 | - private val deviceName: String? | ||
| 33 | -) : CameraVideoCapturer by capturer, VideoCapturerWithSize { | 41 | + private val deviceName: String?, |
| 42 | + cameraEventsDispatchHandler: CameraEventsDispatchHandler, | ||
| 43 | +) : CameraCapturerWithSize(cameraEventsDispatchHandler), CameraVideoCapturer by capturer { | ||
| 34 | override fun findCaptureFormat(width: Int, height: Int): Size { | 44 | override fun findCaptureFormat(width: Int, height: Int): Size { |
| 35 | return Camera2Helper.findClosestCaptureFormat(cameraManager, deviceName, width, height) | 45 | return Camera2Helper.findClosestCaptureFormat(cameraManager, deviceName, width, height) |
| 36 | } | 46 | } |
| @@ -69,9 +69,6 @@ class CallViewModel( | @@ -69,9 +69,6 @@ class CallViewModel( | ||
| 69 | private val mutableCameraEnabled = MutableLiveData(true) | 69 | private val mutableCameraEnabled = MutableLiveData(true) |
| 70 | val cameraEnabled = mutableCameraEnabled.hide() | 70 | val cameraEnabled = mutableCameraEnabled.hide() |
| 71 | 71 | ||
| 72 | - private val mutableFlipVideoButtonEnabled = MutableLiveData(true) | ||
| 73 | - val flipButtonVideoEnabled = mutableFlipVideoButtonEnabled.hide() | ||
| 74 | - | ||
| 75 | private val mutableScreencastEnabled = MutableLiveData(false) | 72 | private val mutableScreencastEnabled = MutableLiveData(false) |
| 76 | val screenshareEnabled = mutableScreencastEnabled.hide() | 73 | val screenshareEnabled = mutableScreencastEnabled.hide() |
| 77 | 74 | ||
| @@ -102,8 +99,8 @@ class CallViewModel( | @@ -102,8 +99,8 @@ class CallViewModel( | ||
| 102 | } | 99 | } |
| 103 | } | 100 | } |
| 104 | 101 | ||
| 102 | + // Handle room events. | ||
| 105 | launch { | 103 | launch { |
| 106 | - // Handle room events. | ||
| 107 | room.events.collect { | 104 | room.events.collect { |
| 108 | when (it) { | 105 | when (it) { |
| 109 | is RoomEvent.FailedToConnect -> mutableError.value = it.error | 106 | is RoomEvent.FailedToConnect -> mutableError.value = it.error |
| @@ -118,6 +115,7 @@ class CallViewModel( | @@ -118,6 +115,7 @@ class CallViewModel( | ||
| 118 | } | 115 | } |
| 119 | } | 116 | } |
| 120 | } | 117 | } |
| 118 | + | ||
| 121 | connectToRoom() | 119 | connectToRoom() |
| 122 | } | 120 | } |
| 123 | 121 |
| @@ -69,7 +69,6 @@ class CallActivity : AppCompatActivity() { | @@ -69,7 +69,6 @@ class CallActivity : AppCompatActivity() { | ||
| 69 | val activeSpeakers by viewModel.activeSpeakers.collectAsState(initial = emptyList()) | 69 | val activeSpeakers by viewModel.activeSpeakers.collectAsState(initial = emptyList()) |
| 70 | val micEnabled by viewModel.micEnabled.observeAsState(true) | 70 | val micEnabled by viewModel.micEnabled.observeAsState(true) |
| 71 | val videoEnabled by viewModel.cameraEnabled.observeAsState(true) | 71 | val videoEnabled by viewModel.cameraEnabled.observeAsState(true) |
| 72 | - val flipButtonEnabled by viewModel.flipButtonVideoEnabled.observeAsState(true) | ||
| 73 | val screencastEnabled by viewModel.screenshareEnabled.observeAsState(false) | 72 | val screencastEnabled by viewModel.screenshareEnabled.observeAsState(false) |
| 74 | val permissionAllowed by viewModel.permissionAllowed.collectAsState() | 73 | val permissionAllowed by viewModel.permissionAllowed.collectAsState() |
| 75 | Content( | 74 | Content( |
| @@ -79,7 +78,6 @@ class CallActivity : AppCompatActivity() { | @@ -79,7 +78,6 @@ class CallActivity : AppCompatActivity() { | ||
| 79 | activeSpeakers, | 78 | activeSpeakers, |
| 80 | micEnabled, | 79 | micEnabled, |
| 81 | videoEnabled, | 80 | videoEnabled, |
| 82 | - flipButtonEnabled, | ||
| 83 | screencastEnabled, | 81 | screencastEnabled, |
| 84 | audioSwitchHandler = viewModel.audioHandler, | 82 | audioSwitchHandler = viewModel.audioHandler, |
| 85 | permissionAllowed = permissionAllowed, | 83 | permissionAllowed = permissionAllowed, |
| @@ -128,7 +126,6 @@ class CallActivity : AppCompatActivity() { | @@ -128,7 +126,6 @@ class CallActivity : AppCompatActivity() { | ||
| 128 | activeSpeakers: List<Participant> = listOf(previewParticipant), | 126 | activeSpeakers: List<Participant> = listOf(previewParticipant), |
| 129 | micEnabled: Boolean = true, | 127 | micEnabled: Boolean = true, |
| 130 | videoEnabled: Boolean = true, | 128 | videoEnabled: Boolean = true, |
| 131 | - flipButtonEnabled: Boolean = true, | ||
| 132 | screencastEnabled: Boolean = false, | 129 | screencastEnabled: Boolean = false, |
| 133 | permissionAllowed: Boolean = true, | 130 | permissionAllowed: Boolean = true, |
| 134 | audioSwitchHandler: AudioSwitchHandler? = null, | 131 | audioSwitchHandler: AudioSwitchHandler? = null, |
| @@ -10,6 +10,8 @@ import androidx.compose.ui.res.painterResource | @@ -10,6 +10,8 @@ import androidx.compose.ui.res.painterResource | ||
| 10 | import io.livekit.android.compose.VideoRenderer | 10 | import io.livekit.android.compose.VideoRenderer |
| 11 | import io.livekit.android.room.Room | 11 | import io.livekit.android.room.Room |
| 12 | import io.livekit.android.room.participant.Participant | 12 | import io.livekit.android.room.participant.Participant |
| 13 | +import io.livekit.android.room.track.CameraPosition | ||
| 14 | +import io.livekit.android.room.track.LocalVideoTrack | ||
| 13 | import io.livekit.android.room.track.Track | 15 | import io.livekit.android.room.track.Track |
| 14 | import io.livekit.android.room.track.VideoTrack | 16 | import io.livekit.android.room.track.VideoTrack |
| 15 | import io.livekit.android.util.flow | 17 | import io.livekit.android.util.flow |
| @@ -22,7 +24,6 @@ fun VideoItemTrackSelector( | @@ -22,7 +24,6 @@ fun VideoItemTrackSelector( | ||
| 22 | room: Room, | 24 | room: Room, |
| 23 | participant: Participant, | 25 | participant: Participant, |
| 24 | modifier: Modifier = Modifier, | 26 | modifier: Modifier = Modifier, |
| 25 | - mirror: Boolean = false, | ||
| 26 | ) { | 27 | ) { |
| 27 | val videoTrackMap by participant::videoTracks.flow.collectAsState(initial = emptyList()) | 28 | val videoTrackMap by participant::videoTracks.flow.collectAsState(initial = emptyList()) |
| 28 | val videoPubs = videoTrackMap.filter { (pub) -> pub.subscribed } | 29 | val videoPubs = videoTrackMap.filter { (pub) -> pub.subscribed } |
| @@ -35,12 +36,22 @@ fun VideoItemTrackSelector( | @@ -35,12 +36,22 @@ fun VideoItemTrackSelector( | ||
| 35 | ?: videoPubs.firstOrNull() | 36 | ?: videoPubs.firstOrNull() |
| 36 | 37 | ||
| 37 | val videoTrack = videoPub?.track as? VideoTrack | 38 | val videoTrack = videoPub?.track as? VideoTrack |
| 38 | - val videoMuted by | ||
| 39 | - if (videoPub != null) { | ||
| 40 | - videoPub::muted.flow.collectAsState() | ||
| 41 | - } else { | ||
| 42 | - remember(videoPub) { | ||
| 43 | - derivedStateOf { false } | 39 | + var videoMuted by remember { mutableStateOf(false) } |
| 40 | + var cameraFacingFront by remember { mutableStateOf(false) } | ||
| 41 | + | ||
| 42 | + // monitor muted state | ||
| 43 | + LaunchedEffect(videoPub) { | ||
| 44 | + if (videoPub != null) { | ||
| 45 | + videoPub::muted.flow.collect { muted -> videoMuted = muted } | ||
| 46 | + } | ||
| 47 | + } | ||
| 48 | + | ||
| 49 | + // monitor camera facing for local participant | ||
| 50 | + LaunchedEffect(participant, videoTrack) { | ||
| 51 | + if (room.localParticipant == participant && videoTrack as? LocalVideoTrack != null) { | ||
| 52 | + videoTrack::options.flow.collect { options -> | ||
| 53 | + cameraFacingFront = options.position == CameraPosition.FRONT | ||
| 54 | + } | ||
| 44 | } | 55 | } |
| 45 | } | 56 | } |
| 46 | 57 | ||
| @@ -48,7 +59,7 @@ fun VideoItemTrackSelector( | @@ -48,7 +59,7 @@ fun VideoItemTrackSelector( | ||
| 48 | VideoRenderer( | 59 | VideoRenderer( |
| 49 | room = room, | 60 | room = room, |
| 50 | videoTrack = videoTrack, | 61 | videoTrack = videoTrack, |
| 51 | - mirror = mirror, | 62 | + mirror = room.localParticipant == participant && cameraFacingFront, |
| 52 | modifier = modifier | 63 | modifier = modifier |
| 53 | ) | 64 | ) |
| 54 | } else { | 65 | } else { |
| 1 | +@file:OptIn(ExperimentalCoroutinesApi::class) | ||
| 2 | + | ||
| 1 | package io.livekit.android.sample | 3 | package io.livekit.android.sample |
| 2 | 4 | ||
| 3 | 5 | ||
| 4 | -import android.graphics.Color | ||
| 5 | -import android.graphics.drawable.GradientDrawable | ||
| 6 | import android.view.View | 6 | import android.view.View |
| 7 | import com.github.ajalt.timberkt.Timber | 7 | import com.github.ajalt.timberkt.Timber |
| 8 | import com.xwray.groupie.viewbinding.BindableItem | 8 | import com.xwray.groupie.viewbinding.BindableItem |
| @@ -10,6 +10,8 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder | @@ -10,6 +10,8 @@ import com.xwray.groupie.viewbinding.GroupieViewHolder | ||
| 10 | import io.livekit.android.room.Room | 10 | import io.livekit.android.room.Room |
| 11 | import io.livekit.android.room.participant.ConnectionQuality | 11 | import io.livekit.android.room.participant.ConnectionQuality |
| 12 | import io.livekit.android.room.participant.Participant | 12 | import io.livekit.android.room.participant.Participant |
| 13 | +import io.livekit.android.room.track.CameraPosition | ||
| 14 | +import io.livekit.android.room.track.LocalVideoTrack | ||
| 13 | import io.livekit.android.room.track.Track | 15 | import io.livekit.android.room.track.Track |
| 14 | import io.livekit.android.room.track.VideoTrack | 16 | import io.livekit.android.room.track.VideoTrack |
| 15 | import io.livekit.android.sample.databinding.ParticipantItemBinding | 17 | import io.livekit.android.sample.databinding.ParticipantItemBinding |
| @@ -17,7 +19,6 @@ import io.livekit.android.util.flow | @@ -17,7 +19,6 @@ import io.livekit.android.util.flow | ||
| 17 | import kotlinx.coroutines.* | 19 | import kotlinx.coroutines.* |
| 18 | import kotlinx.coroutines.flow.* | 20 | import kotlinx.coroutines.flow.* |
| 19 | 21 | ||
| 20 | -@OptIn(ExperimentalCoroutinesApi::class) | ||
| 21 | class ParticipantItem( | 22 | class ParticipantItem( |
| 22 | private val room: Room, | 23 | private val room: Room, |
| 23 | private val participant: Participant, | 24 | private val participant: Participant, |
| @@ -92,29 +93,34 @@ class ParticipantItem( | @@ -92,29 +93,34 @@ class ParticipantItem( | ||
| 92 | } | 93 | } |
| 93 | 94 | ||
| 94 | coroutineScope?.launch { | 95 | coroutineScope?.launch { |
| 95 | - videoTrackPubFlow | ||
| 96 | - .flatMapLatest { pub -> | ||
| 97 | - if (pub != null) { | ||
| 98 | - pub::track.flow | ||
| 99 | - } else { | ||
| 100 | - flowOf(null) | ||
| 101 | - } | ||
| 102 | - } | ||
| 103 | - .collectLatest { videoTrack -> | 96 | + val videoTrackFlow = videoTrackPubFlow |
| 97 | + .flatMapLatestOrNull { pub -> pub::track.flow } | ||
| 98 | + | ||
| 99 | + // Configure video view with track | ||
| 100 | + launch { | ||
| 101 | + videoTrackFlow.collectLatest { videoTrack -> | ||
| 104 | setupVideoIfNeeded(videoTrack as? VideoTrack, viewBinding) | 102 | setupVideoIfNeeded(videoTrack as? VideoTrack, viewBinding) |
| 105 | } | 103 | } |
| 104 | + } | ||
| 105 | + | ||
| 106 | + // For local participants, mirror camera if using front camera. | ||
| 107 | + if (participant == room.localParticipant) { | ||
| 108 | + launch { | ||
| 109 | + videoTrackFlow | ||
| 110 | + .flatMapLatestOrNull { track -> (track as LocalVideoTrack)::options.flow } | ||
| 111 | + .collectLatest { options -> | ||
| 112 | + viewBinding.renderer.setMirror(options?.position == CameraPosition.FRONT) | ||
| 113 | + } | ||
| 114 | + } | ||
| 115 | + } | ||
| 106 | } | 116 | } |
| 117 | + | ||
| 118 | + // Handle muted changes | ||
| 107 | coroutineScope?.launch { | 119 | coroutineScope?.launch { |
| 108 | videoTrackPubFlow | 120 | videoTrackPubFlow |
| 109 | - .flatMapLatest { pub -> | ||
| 110 | - if (pub != null) { | ||
| 111 | - pub::muted.flow | ||
| 112 | - } else { | ||
| 113 | - flowOf(true) | ||
| 114 | - } | ||
| 115 | - } | 121 | + .flatMapLatestOrNull { pub -> pub::muted.flow } |
| 116 | .collectLatest { muted -> | 122 | .collectLatest { muted -> |
| 117 | - viewBinding.renderer.visibleOrInvisible(!muted) | 123 | + viewBinding.renderer.visibleOrInvisible(!(muted ?: true)) |
| 118 | } | 124 | } |
| 119 | } | 125 | } |
| 120 | val existingTrack = getVideoTrack() | 126 | val existingTrack = getVideoTrack() |
| @@ -175,3 +181,15 @@ private fun showFocus(binding: ParticipantItemBinding) { | @@ -175,3 +181,15 @@ private fun showFocus(binding: ParticipantItemBinding) { | ||
| 175 | private fun hideFocus(binding: ParticipantItemBinding) { | 181 | private fun hideFocus(binding: ParticipantItemBinding) { |
| 176 | binding.speakingIndicator.visibility = View.INVISIBLE | 182 | binding.speakingIndicator.visibility = View.INVISIBLE |
| 177 | } | 183 | } |
| 184 | + | ||
| 185 | +private inline fun <T, R> Flow<T?>.flatMapLatestOrNull( | ||
| 186 | + crossinline transform: suspend (value: T) -> Flow<R> | ||
| 187 | +): Flow<R?> { | ||
| 188 | + return flatMapLatest { | ||
| 189 | + if (it == null) { | ||
| 190 | + flowOf(null) | ||
| 191 | + } else { | ||
| 192 | + transform(it) | ||
| 193 | + } | ||
| 194 | + } | ||
| 195 | +} |
-
请 注册 或 登录 后发表评论