-
Notifications
You must be signed in to change notification settings - Fork 38
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement camera fallback for iOS simulator
- Loading branch information
Showing
11 changed files
with
298 additions
and
176 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
171 changes: 171 additions & 0 deletions
171
...sArm64Main/kotlin/com/shepeliev/webrtckmp/video/CameraVideoCapturerController.iosArm64.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,171 @@ | ||
package com.shepeliev.webrtckmp.video | ||
|
||
import WebRTC.RTCCameraVideoCapturer | ||
import WebRTC.RTCVideoCapturerDelegateProtocol | ||
import com.shepeliev.webrtckmp.CameraVideoCapturerException | ||
import com.shepeliev.webrtckmp.DEFAULT_FRAME_RATE | ||
import com.shepeliev.webrtckmp.DEFAULT_VIDEO_HEIGHT | ||
import com.shepeliev.webrtckmp.DEFAULT_VIDEO_WIDTH | ||
import com.shepeliev.webrtckmp.FacingMode | ||
import com.shepeliev.webrtckmp.MediaTrackConstraints | ||
import com.shepeliev.webrtckmp.value | ||
import kotlinx.cinterop.ExperimentalForeignApi | ||
import kotlinx.cinterop.useContents | ||
import platform.AVFoundation.AVCaptureDevice | ||
import platform.AVFoundation.AVCaptureDeviceFormat | ||
import platform.AVFoundation.AVCaptureDevicePosition | ||
import platform.AVFoundation.AVCaptureDevicePositionBack | ||
import platform.AVFoundation.AVCaptureDevicePositionFront | ||
import platform.AVFoundation.AVFrameRateRange | ||
import platform.AVFoundation.position | ||
import platform.CoreMedia.CMFormatDescriptionGetMediaSubType | ||
import platform.CoreMedia.CMVideoFormatDescriptionGetDimensions | ||
import kotlin.math.abs | ||
|
||
@OptIn(ExperimentalForeignApi::class) | ||
internal actual class CameraVideoCapturerController actual constructor( | ||
private val constraints: MediaTrackConstraints, | ||
private val videoCapturerDelegate: RTCVideoCapturerDelegateProtocol | ||
) : VideoCapturerController() { | ||
private var videoCapturer: RTCCameraVideoCapturer? = null | ||
private var position: AVCaptureDevicePosition = AVCaptureDevicePositionBack | ||
private lateinit var device: AVCaptureDevice | ||
private lateinit var format: AVCaptureDeviceFormat | ||
private var fps: Long = -1 | ||
|
||
actual override fun startCapture() { | ||
if (videoCapturer != null) return | ||
videoCapturer = RTCCameraVideoCapturer(videoCapturerDelegate) | ||
if (!this::device.isInitialized) selectDevice() | ||
selectFormat() | ||
selectFps() | ||
|
||
var width: Int? = null | ||
var height: Int? = null | ||
CMVideoFormatDescriptionGetDimensions(format.formatDescription).useContents { | ||
width = this.width | ||
height = this.height | ||
} | ||
|
||
settings = settings.copy( | ||
deviceId = device.uniqueID, | ||
facingMode = device.position.toFacingMode(), | ||
width = width, | ||
height = height, | ||
frameRate = fps.toDouble() | ||
) | ||
|
||
videoCapturer?.startCaptureWithDevice(device, format, fps) | ||
} | ||
|
||
actual override fun stopCapture() { | ||
videoCapturer?.stopCapture() | ||
videoCapturer = null | ||
} | ||
|
||
private fun selectDevice() { | ||
position = when (constraints.facingMode?.value) { | ||
FacingMode.User -> AVCaptureDevicePositionFront | ||
FacingMode.Environment -> AVCaptureDevicePositionBack | ||
null -> AVCaptureDevicePositionFront | ||
} | ||
|
||
val searchCriteria: (Any?) -> Boolean = when { | ||
constraints.deviceId != null -> { | ||
{ (it as AVCaptureDevice).uniqueID == constraints.deviceId } | ||
} | ||
|
||
else -> { | ||
{ (it as AVCaptureDevice).position == position } | ||
} | ||
} | ||
|
||
device = RTCCameraVideoCapturer.captureDevices() | ||
.firstOrNull(searchCriteria) as? AVCaptureDevice | ||
?: throw CameraVideoCapturerException.notFound(constraints) | ||
|
||
settings = settings.copy( | ||
deviceId = device.uniqueID, | ||
facingMode = device.position.toFacingMode() | ||
) | ||
} | ||
|
||
private fun selectFormat() { | ||
val targetWidth = constraints.width?.value ?: DEFAULT_VIDEO_WIDTH | ||
val targetHeight = constraints.height?.value ?: DEFAULT_VIDEO_HEIGHT | ||
val formats = RTCCameraVideoCapturer.supportedFormatsForDevice(device) | ||
|
||
format = formats.fold(Pair(Int.MAX_VALUE, null as AVCaptureDeviceFormat?)) { acc, fmt -> | ||
val format = fmt as AVCaptureDeviceFormat | ||
val (currentDiff, currentFormat) = acc | ||
|
||
var diff = currentDiff | ||
CMVideoFormatDescriptionGetDimensions(format.formatDescription).useContents { | ||
diff = abs(targetWidth - width) + abs(targetHeight - height) | ||
} | ||
val pixelFormat = CMFormatDescriptionGetMediaSubType(format.formatDescription) | ||
if (diff < currentDiff) { | ||
return@fold Pair(diff, format) | ||
} else if (diff == currentDiff && pixelFormat == videoCapturer!!.preferredOutputPixelFormat()) { | ||
return@fold Pair(currentDiff, format) | ||
} | ||
Pair(0, currentFormat) | ||
}.second ?: throw CameraVideoCapturerException( | ||
"No valid video format for device $device. Requested video frame size: ${targetWidth}x$targetHeight" | ||
) | ||
} | ||
|
||
private fun selectFps() { | ||
val requestedFps = constraints.frameRate?.value ?: DEFAULT_FRAME_RATE | ||
|
||
val maxSupportedFrameRate = format.videoSupportedFrameRateRanges.fold(0.0) { acc, range -> | ||
val fpsRange = range as AVFrameRateRange | ||
maxOf(acc, fpsRange.maxFrameRate) | ||
} | ||
|
||
fps = minOf(maxSupportedFrameRate, requestedFps.toDouble()).toLong() | ||
} | ||
|
||
actual fun switchCamera() { | ||
checkNotNull(videoCapturer) { "Video capturing is not started." } | ||
val captureDevices = RTCCameraVideoCapturer.captureDevices() | ||
if (captureDevices.size < 2) { | ||
throw CameraVideoCapturerException("No other camera device found.") | ||
} | ||
|
||
stopCapture() | ||
val deviceIndex = captureDevices.indexOfFirst { | ||
(it as AVCaptureDevice).uniqueID == device.uniqueID | ||
} | ||
device = captureDevices[(deviceIndex + 1) % captureDevices.size] as AVCaptureDevice | ||
startCapture() | ||
|
||
settings = settings.copy( | ||
deviceId = device.uniqueID, | ||
facingMode = device.position.toFacingMode() | ||
) | ||
} | ||
|
||
actual fun switchCamera(deviceId: String) { | ||
checkNotNull(videoCapturer) { "Video capturing is not started." } | ||
|
||
stopCapture() | ||
device = RTCCameraVideoCapturer.captureDevices() | ||
.firstOrNull { (it as AVCaptureDevice).uniqueID == deviceId } as? AVCaptureDevice | ||
?: throw CameraVideoCapturerException.notFound(deviceId) | ||
startCapture() | ||
|
||
settings = settings.copy( | ||
deviceId = device.uniqueID, | ||
facingMode = device.position.toFacingMode() | ||
) | ||
} | ||
|
||
private fun AVCaptureDevicePosition.toFacingMode(): FacingMode? { | ||
return when (this) { | ||
AVCaptureDevicePositionFront -> FacingMode.User | ||
AVCaptureDevicePositionBack -> FacingMode.Environment | ||
else -> null | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.