This repository has no description
0

Configure Feed

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

fix: ios camera hanging

+121 -87
+105 -79
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraEngine.kt
··· 221 221 cameraController.switchCamera() 222 222 } 223 223 224 + /** 225 + * Deterministically select which lens to use. 226 + * This is idempotent (won't switch if already on the requested lens), 227 + * which makes it safe to call from Compose recomposition. 228 + */ 229 + fun setUseFrontCamera(useFront: Boolean) { 230 + MemoryManager.updateMemoryStatus() 231 + if (MemoryManager.isUnderMemoryPressure()) MemoryManager.clearBufferPools() 232 + cameraController.setUseFrontCamera(useFront) 233 + } 234 + 224 235 fun startSession() { 225 236 MemoryManager.clearBufferPools() 226 237 cameraController.startSession() ··· 288 299 private var videoOutput: AVCaptureVideoDataOutput? = null 289 300 private var movieFileOutput: AVCaptureMovieFileOutput? = null 290 301 var cameraPreviewLayer: AVCaptureVideoPreviewLayer? = null 291 - var isUsingFrontCamera = true 302 + var isUsingFrontCamera = false 292 303 var skeletonRepository: SkeletonRepository? = null 293 304 var customObjectRepository: CustomObjectRespository? = null 294 305 var cameraViewController: CameraViewController? = null ··· 302 313 303 314 // Serial queue to serialize session configuration and start/stop calls 304 315 private val sessionQueue = dispatch_queue_create("com.performancecoachlab.captureSessionQueue", null) 316 + 317 + // Prevent overlapping switch operations (which can deadlock/jank the UI). 318 + private var isSwitchingCamera: Boolean = false 319 + 320 + // Reuse a single queue for frame processing work. 321 + private val frameProcessingQueue = dispatch_queue_create("com.performancecoachlab.frameProcessing", null) 305 322 306 323 sealed class CameraException : Exception() { 307 324 class DeviceNotAvailable : CameraException() ··· 365 382 frameProcessor.setObjectModel(objectModel) 366 383 } 367 384 385 + /** 386 + * Deterministically select which lens to use. 387 + * No-ops if the requested lens is already active. 388 + */ 389 + fun setUseFrontCamera(useFront: Boolean) { 390 + if (isUsingFrontCamera == useFront) return 391 + switchCamera() 392 + } 393 + 368 394 fun setupSession() { 369 395 // Run synchronously on sessionQueue so callers (like setupCamera) can safely call startSession afterwards 370 396 dispatch_sync(sessionQueue) { ··· 406 432 407 433 private fun setupVideoOutput() { 408 434 videoOutput = AVCaptureVideoDataOutput() 409 - videoOutput?.setSampleBufferDelegate(this, dispatch_get_main_queue()) 435 + // Deliver frames off the main thread; toggling/config and UI should not compete with capture callbacks. 436 + videoOutput?.setSampleBufferDelegate(this, sessionQueue) 410 437 videoOutput?.videoSettings = mapOf(AVVideoCodecKey to AVVideoCodecJPEG) 411 438 412 439 if (captureSession?.canAddOutput(videoOutput!!) == true) { ··· 516 543 fun switchCamera() { 517 544 guard(captureSession != null) { return@guard } 518 545 519 - // Run camera switch on sessionQueue to serialize with setupSession/startSession 520 - dispatch_sync(sessionQueue) { 546 + // Never block the caller thread (often the main thread). Serialize on sessionQueue. 547 + dispatch_async(sessionQueue) { 548 + if (isSwitchingCamera) return@dispatch_async 549 + isSwitchingCamera = true 550 + 521 551 var configurationBegan = false 522 552 try { 523 553 captureSession?.beginConfiguration() ··· 533 563 val newCamera = currentCamera ?: throw CameraException.DeviceNotAvailable() 534 564 535 565 val newInput = AVCaptureDeviceInput.deviceInputWithDevice( 536 - newCamera, null 566 + newCamera, 567 + null 537 568 ) ?: throw CameraException.ConfigurationError("Failed to create input") 538 569 539 570 if (captureSession?.canAddInput(newInput) == true) { ··· 548 579 connection.setVideoMirrored(isUsingFrontCamera) 549 580 } 550 581 } 551 - 552 582 } catch (e: CameraException) { 553 583 onError?.invoke(e) 554 584 } catch (e: Exception) { ··· 557 587 if (configurationBegan) { 558 588 captureSession?.commitConfiguration() 559 589 } 590 + isSwitchingCamera = false 560 591 } 561 592 } 562 593 } ··· 571 602 ) { 572 603 timeStamp().also { timestamp -> 573 604 runCatching { 574 - dispatch_queue_create( 575 - "com.performancecoachlab.frameProcessing", null 576 - ).also { processingQueue -> 577 - dispatch_async(processingQueue) { 578 - try { 579 - var detectedSkeleton: Skeleton? = null 580 - var detectedObjects: List<AnalysisObject> = emptyList() 581 - frameProcessor.analyseBufferForAll( 582 - CMSampleBufferGetImageBuffer(didOutputSampleBuffer), 583 - timestamp, 584 - preview = cameraPreviewLayer, 585 - onSkeletonProcessed = { skeleton -> 586 - detectedSkeleton = skeleton 587 - }, 588 - onObjectsProcessed = { objects -> 589 - detectedObjects = objects 590 - }) 591 - cameraPreviewLayer?.also { preview -> 592 - val previewSkeleton = detectedSkeleton?.let { 593 - mapSkeletonToPreview( 594 - skeleton = it, 595 - previewLayer = preview, 605 + dispatch_async(frameProcessingQueue) { 606 + try { 607 + var detectedSkeleton: Skeleton? = null 608 + var detectedObjects: List<AnalysisObject> = emptyList() 609 + frameProcessor.analyseBufferForAll( 610 + CMSampleBufferGetImageBuffer(didOutputSampleBuffer), 611 + timestamp, 612 + preview = cameraPreviewLayer, 613 + onSkeletonProcessed = { skeleton -> 614 + detectedSkeleton = skeleton 615 + }, 616 + onObjectsProcessed = { objects -> 617 + detectedObjects = objects 618 + } 619 + ) 620 + cameraPreviewLayer?.also { preview -> 621 + val previewSkeleton = detectedSkeleton?.let { 622 + mapSkeletonToPreview( 623 + skeleton = it, 624 + previewLayer = preview, 625 + width = 480f, 626 + height = 360f 627 + ) 628 + } 629 + previewSkeleton?.also { 630 + skeletonRepository?.updateSkeleton(it) 631 + } 632 + val previewObjects = detectedObjects.map { 633 + it.copy( 634 + boundingBox = mapBoxToPreview( 635 + it.boundingBox, 636 + preview, 596 637 width = 480f, 597 638 height = 360f 598 - ) 599 - } 600 - previewSkeleton?.also { 601 - skeletonRepository?.updateSkeleton( 602 - it 603 - ) 604 - } 605 - val previewObjects = detectedObjects.map { 606 - it.copy( 607 - boundingBox = mapBoxToPreview( 608 - it.boundingBox, preview, width = 480f, height = 360f 609 - ), frameSize = it.frameSize.let { 610 - mapBoxToPreview( 611 - Rect(Offset.Zero, Size(480f, 360f)), 612 - preview, 613 - width = 480f, 614 - height = 360f 615 - ).let { FrameSize(it.width.toInt().absoluteValue, it.height.toInt().absoluteValue) } 616 - }) 617 - } 618 - previewObjects.also { objects -> 619 - customObjectRepository?.updateCustomObject(objects) 620 - } 621 - preview.bounds.useContents { 622 - Pair( 623 - size.width.toInt(), size.height.toInt() 624 - ) 625 - }.also { bo -> 626 - ImageBitmap( 627 - bo.first, bo.second 628 - ).drawResults( 629 - if (drawSkeleton) previewSkeleton else null, 630 - drawObjects?.invoke(previewObjects) ?: emptyList(), 631 - textMeasurer = textMeasurer 632 - ).also { drawn -> 633 - cameraViewController?.setRequestDataProvider { 634 - CameraViewData( 635 - width = bo.first.toFloat(), 636 - height = bo.second.toFloat(), 637 - rotation = when (currentVideoOrientation()) { 638 - AVCaptureVideoOrientationPortrait -> SensorRotation.ROTATION_90 639 - AVCaptureVideoOrientationLandscapeRight -> SensorRotation.ROTATION_0 640 - AVCaptureVideoOrientationPortraitUpsideDown -> SensorRotation.ROTATION_270 641 - AVCaptureVideoOrientationLandscapeLeft -> SensorRotation.ROTATION_180 642 - else -> SensorRotation.ROTATION_0 643 - } 644 - ) 639 + ), 640 + frameSize = it.frameSize.let { 641 + mapBoxToPreview( 642 + Rect(Offset.Zero, Size(480f, 360f)), 643 + preview, 644 + width = 480f, 645 + height = 360f 646 + ).let { r -> 647 + FrameSize(r.width.toInt().absoluteValue, r.height.toInt().absoluteValue) 645 648 } 646 - frameListener?.updateFrame( 647 - drawn 649 + } 650 + ) 651 + } 652 + customObjectRepository?.updateCustomObject(previewObjects) 653 + 654 + preview.bounds.useContents { 655 + Pair(size.width.toInt(), size.height.toInt()) 656 + }.also { bo -> 657 + ImageBitmap(bo.first, bo.second).drawResults( 658 + if (drawSkeleton) previewSkeleton else null, 659 + drawObjects?.invoke(previewObjects) ?: emptyList(), 660 + textMeasurer = textMeasurer 661 + ).also { drawn -> 662 + cameraViewController?.setRequestDataProvider { 663 + CameraViewData( 664 + width = bo.first.toFloat(), 665 + height = bo.second.toFloat(), 666 + rotation = when (currentVideoOrientation()) { 667 + AVCaptureVideoOrientationPortrait -> SensorRotation.ROTATION_90 668 + AVCaptureVideoOrientationLandscapeRight -> SensorRotation.ROTATION_0 669 + AVCaptureVideoOrientationPortraitUpsideDown -> SensorRotation.ROTATION_270 670 + AVCaptureVideoOrientationLandscapeLeft -> SensorRotation.ROTATION_180 671 + else -> SensorRotation.ROTATION_0 672 + } 648 673 ) 649 674 } 675 + frameListener?.updateFrame(drawn) 650 676 } 651 677 } 652 - } catch (e: Exception) { 653 - // ignore frame processing errors 654 678 } 679 + } catch (_: Exception) { 680 + // ignore frame processing errors 655 681 } 656 682 } 657 683 }
+13 -3
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.ios.kt
··· 50 50 LaunchedEffect(detectMode) { 51 51 cameraEngine.value?.setDetectMode(detectMode) 52 52 } 53 + 54 + // Ensure lens selection is deterministic and recomposition-safe. 55 + LaunchedEffect(frontCamera) { 56 + cameraEngine.value?.setUseFrontCamera(frontCamera) 57 + } 58 + 53 59 val recordingDone = {path: String -> 54 60 val id = idMap.entries.firstOrNull { it.value == path }?.key 55 61 if (id != null) { ··· 98 104 } 99 105 Box(modifier = Modifier.fillMaxSize()) { 100 106 CameraPreview( 101 - modifier = Modifier.fillMaxSize(), onCameraControllerReady = { engine -> 102 - cameraEngine.value = engine.also { if (!frontCamera) it.toggleCameraLens() } 103 - },) 107 + modifier = Modifier.fillMaxSize(), 108 + onCameraControllerReady = { engine -> 109 + cameraEngine.value = engine 110 + // Apply immediately as well for the first composition. 111 + engine.setUseFrontCamera(frontCamera) 112 + }, 113 + ) 104 114 if (drawSkeleton || drawObjects!= null) { 105 115 kotlin.native.runtime.GC.collect() 106 116 frameBitmap?.also {
+3 -5
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/App.kt
··· 316 316 "YOLOv3FP16" 317 317 ) 318 318 ) 319 + var frontCamera by remember { mutableStateOf(true) } 319 320 val controller = remember { CameraViewControllerImpl() } 320 321 PermissionProvider().apply { 321 322 if (!hasCameraPermission()) RequestCameraPermission(onGranted = { ··· 369 370 objectModel = generalModel, 370 371 modifier = Modifier.weight(1f), 371 372 focusArea = Rect(0f,0f,0.1f,1f), 372 - frontCamera = false, 373 + frontCamera = frontCamera, 373 374 recordingId = recordingId, 374 375 controller = controller, 375 376 onVideoSaved = {id,url -> path = url }, ··· 377 378 } 378 379 Button( 379 380 onClick = { 380 - //recordingId = "${Clock.System.now().epochSeconds}" 381 - controller.requestData { data -> 382 - Logger.d("CameraViewData: $data") 383 - } 381 + frontCamera = !frontCamera 384 382 }, 385 383 modifier = Modifier.imePadding().padding(16.dp).align(Alignment.TopStart) 386 384 ) {