This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

feat: switch between ultra wide and regular camera

+93 -24
+7 -2
posedetection/src/androidMain/kotlin/com.performancecoachlab/posedetection/camera/CameraView.android.kt
··· 126 126 drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)?, 127 127 modifier: Modifier, 128 128 frontCamera: Boolean, 129 + useUltraWide: Boolean, 129 130 recordingId: String?, 130 131 focusArea: Rect?, 131 132 controller: CameraViewController?, ··· 261 262 262 263 val gate = remember { FrameGate() } 263 264 264 - LaunchedEffect(lifecycleOwner, frontCamera) { 265 + LaunchedEffect(lifecycleOwner, frontCamera, useUltraWide) { 265 266 val cameraProvider = ProcessCameraProvider.getInstance(context).get() 266 267 cameraProvider.unbindAll() 267 268 268 269 val cameraSelector = if (frontCamera) { 269 270 DEFAULT_FRONT_CAMERA 270 271 } else { 271 - buildBackUltraWideSelectorOrNull(cameraProvider) ?: DEFAULT_BACK_CAMERA 272 + if (useUltraWide) { 273 + buildBackUltraWideSelectorOrNull(cameraProvider) ?: DEFAULT_BACK_CAMERA 274 + } else { 275 + DEFAULT_BACK_CAMERA 276 + } 272 277 } 273 278 274 279 val preview = Preview.Builder().build().also {
+1 -1
posedetection/src/androidMain/kotlin/com/performancecoachlab/posedetection/camera/Utils.android.kt
··· 508 508 } 509 509 Logger.d{ "ModelInfo.label: $cls not in labels" } 510 510 return "$cls" 511 - } 511 + }
+1
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.kt
··· 31 31 }, 32 32 modifier: Modifier = Modifier.fillMaxSize(), 33 33 frontCamera: Boolean = true, 34 + useUltraWide: Boolean = false, 34 35 recordingId: String? = null, 35 36 focusArea: Rect? = null, 36 37 controller: CameraViewController? = null,
+68 -17
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraEngine.kt
··· 235 235 cameraController.setUseFrontCamera(useFront) 236 236 } 237 237 238 + fun setUseUltraWide(useUltraWide: Boolean) { 239 + MemoryManager.updateMemoryStatus() 240 + if (MemoryManager.isUnderMemoryPressure()) MemoryManager.clearBufferPools() 241 + cameraController.setUseUltraWide(useUltraWide) 242 + } 243 + 238 244 fun startSession() { 239 245 MemoryManager.clearBufferPools() 240 246 cameraController.startSession() ··· 322 328 323 329 // Reuse a single queue for frame processing work. 324 330 private val frameProcessingQueue = dispatch_queue_create("com.performancecoachlab.frameProcessing", null) 331 + 332 + // iOS 14+ introduces a new way to access the ultra-wide camera, which we can use conditionally. 333 + private var backUltraWideCamera: AVCaptureDevice? = null 334 + private var backWideCamera: AVCaptureDevice? = null 335 + private var useUltraWideBack: Boolean = false 325 336 326 337 sealed class CameraException : Exception() { 327 338 class DeviceNotAvailable : CameraException() ··· 394 405 switchCamera() 395 406 } 396 407 408 + /** 409 + * Deterministically select ultra-wide (0.5x) vs wide (1.0x) when using the back camera. 410 + * No-ops if the requested lens is already active. 411 + */ 412 + fun setUseUltraWide(useUltraWide: Boolean) { 413 + if (useUltraWideBack == useUltraWide) return 414 + useUltraWideBack = useUltraWide 415 + if (!isUsingFrontCamera) { 416 + // Reconfigure session to switch back lens. 417 + switchCamera(forceReconfigure = true) 418 + } 419 + } 420 + 397 421 fun setupSession() { 398 422 // Run synchronously on sessionQueue so callers (like setupCamera) can safely call startSession afterwards 399 423 dispatch_sync(sessionQueue) { ··· 450 474 private fun setupInputs(): Boolean { 451 475 val availableDevices = AVCaptureDeviceDiscoverySession.discoverySessionWithDeviceTypes( 452 476 listOf( 453 - AVCaptureDeviceTypeBuiltInUltraWideCamera,AVCaptureDeviceTypeBuiltInWideAngleCamera 477 + AVCaptureDeviceTypeBuiltInUltraWideCamera, 478 + AVCaptureDeviceTypeBuiltInWideAngleCamera 454 479 ), 455 480 AVMediaTypeVideo, 456 481 AVCaptureDevicePositionUnspecified ··· 458 483 459 484 if (availableDevices.isEmpty()) return false 460 485 461 - for (device in availableDevices) { 462 - if((device as AVCaptureDevice).displayVideoZoomFactorMultiplier<1.0){ 463 - when (device.position) { 464 - AVCaptureDevicePositionBack -> backCamera = device 486 + // Reset cached devices 487 + backUltraWideCamera = null 488 + backWideCamera = null 489 + frontCamera = null 490 + 491 + for (deviceAny in availableDevices) { 492 + val device = deviceAny as AVCaptureDevice 493 + 494 + when (device.position) { 495 + AVCaptureDevicePositionBack -> { 496 + // Heuristic: ultra-wide cameras typically report a zoom multiplier < 1.0 497 + if (device.displayVideoZoomFactorMultiplier < 1.0) { 498 + backUltraWideCamera = device 499 + } else { 500 + backWideCamera = device 501 + } 465 502 } 466 - }else{ 467 - when (device.position) { 468 - AVCaptureDevicePositionFront -> frontCamera = device 503 + 504 + AVCaptureDevicePositionFront -> { 505 + // Front camera 506 + frontCamera = device 469 507 } 470 508 } 471 509 472 - Logger.d{"device: ${device.localizedName}, zoom: ${device.displayVideoZoomFactorMultiplier}, position: ${device.position}"} 510 + Logger.d { "device: ${device.localizedName}, zoom: ${device.displayVideoZoomFactorMultiplier}, position: ${device.position}" } 473 511 } 474 - currentCamera = if (isUsingFrontCamera) frontCamera ?: backCamera ?: return false 475 - else backCamera ?: frontCamera ?: return false 476 512 513 + // Keep existing backCamera/frontCamera fields used elsewhere. 514 + backCamera = if (useUltraWideBack) backUltraWideCamera ?: backWideCamera else backWideCamera ?: backUltraWideCamera 477 515 516 + currentCamera = if (isUsingFrontCamera) { 517 + frontCamera ?: backCamera ?: return false 518 + } else { 519 + backCamera ?: frontCamera ?: return false 520 + } 478 521 479 522 try { 480 - val input = AVCaptureDeviceInput.deviceInputWithDevice( 481 - currentCamera!!, null 482 - ) ?: return false 523 + val input = AVCaptureDeviceInput.deviceInputWithDevice(currentCamera!!, null) ?: return false 483 524 484 525 if (captureSession?.canAddInput(input) == true) { 485 526 captureSession?.addInput(input) ··· 552 593 } 553 594 554 595 @OptIn(ExperimentalForeignApi::class) 555 - fun switchCamera() { 596 + fun switchCamera(forceReconfigure: Boolean = false) { 556 597 guard(captureSession != null) { return@guard } 557 598 558 599 // Never block the caller thread (often the main thread). Serialize on sessionQueue. ··· 574 615 captureSession?.removeInput(input as AVCaptureInput) 575 616 } 576 617 577 - isUsingFrontCamera = !isUsingFrontCamera 578 - currentCamera = if (isUsingFrontCamera) frontCamera else backCamera 618 + if (!forceReconfigure) { 619 + isUsingFrontCamera = !isUsingFrontCamera 620 + } 621 + 622 + // Recompute which back camera we want. 623 + backCamera = if (useUltraWideBack) backUltraWideCamera ?: backWideCamera else backWideCamera ?: backUltraWideCamera 624 + 625 + currentCamera = if (isUsingFrontCamera) { 626 + frontCamera ?: backCamera 627 + } else { 628 + backCamera ?: frontCamera 629 + } 579 630 580 631 val newCamera = currentCamera ?: throw CameraException.DeviceNotAvailable() 581 632
+10 -2
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.ios.kt
··· 33 33 drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)?, 34 34 modifier: Modifier, 35 35 frontCamera: Boolean, 36 + useUltraWide: Boolean, 36 37 recordingId: String?, 37 38 focusArea: Rect?, 38 39 controller: CameraViewController?, ··· 50 51 } 51 52 52 53 // Ensure lens selection is deterministic and recomposition-safe. 53 - LaunchedEffect(frontCamera) { 54 + LaunchedEffect(frontCamera, useUltraWide) { 54 55 cameraEngine.value?.setUseFrontCamera(frontCamera) 56 + cameraEngine.value?.setUseUltraWideSafe(useUltraWide) 55 57 } 56 58 57 59 val recordingDone = {path: String -> ··· 107 109 cameraEngine.value = engine 108 110 // Apply immediately as well for the first composition. 109 111 engine.setUseFrontCamera(frontCamera) 112 + engine.setUseUltraWideSafe(useUltraWide) 110 113 }, 111 114 ) 112 115 if (drawSkeleton || drawObjects!= null) { ··· 123 126 } 124 127 125 128 } 126 - } 129 + } 130 + 131 + // Temporary forwarder to ensure call-sites compile. 132 + private fun CameraEngine.setUseUltraWideSafe(useUltraWide: Boolean) { 133 + this.setUseUltraWide(useUltraWide) 134 + }
+6 -2
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/App.kt
··· 27 27 import androidx.compose.runtime.setValue 28 28 import androidx.compose.ui.Alignment 29 29 import androidx.compose.ui.Modifier 30 + import androidx.compose.ui.geometry.Rect 30 31 import androidx.compose.ui.graphics.Color 31 32 import androidx.compose.ui.graphics.ImageBitmap 32 33 import androidx.compose.ui.graphics.drawscope.Stroke ··· 71 72 import kotlin.time.Clock 72 73 import kotlin.time.ExperimentalTime 73 74 74 - val androidPath = "basketballs_n1.tflite" 75 + val androidPath = "yolov10n_float16.tflite" 75 76 val iosPath = "basketballs_n1" 76 77 77 78 @Composable ··· 316 317 ModelPath(androidPath, iosPath) 317 318 ) 318 319 var frontCamera by remember { mutableStateOf(false) } 320 + var ultrawide by remember { mutableStateOf(false) } 319 321 val controller = remember { CameraViewControllerImpl() } 320 322 PermissionProvider().apply { 321 323 if (!hasCameraPermission()) RequestCameraPermission(onGranted = { ··· 378 380 // focusArea = Rect(0f,0f,0.1f,1f), 379 381 focusArea = null, 380 382 frontCamera = frontCamera, 383 + useUltraWide = ultrawide, 381 384 recordingId = recordingId, 382 385 controller = controller, 383 386 onVideoSaved = { id, url -> path = url }, ··· 385 388 } 386 389 Button( 387 390 onClick = { 388 - recordingId = "${Clock.System.now().epochSeconds}" 391 + //recordingId = "${Clock.System.now().epochSeconds}" 392 + ultrawide = !ultrawide 389 393 }, modifier = Modifier.imePadding().padding(16.dp).align(Alignment.TopStart) 390 394 ) { 391 395 Text("Start Recording")