alpha
Login
or
Join now
nateholland.bsky.social
/
PoseDetection
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
This repository has no description
Star
0
Fork
0
Atom
Configure Feed
Issues
Pull Requests
Commits
Tags
Feed URL
Select the types of activity you want to include in your feed.
Overview
Issues
Pulls
Pipelines
fix: handle ios model parsing error
author
nate
date
7 months ago
(Oct 30, 2025, 6:19 PM +0200)
commit
9fafb791
9fafb791e8bb85caedba7cd59773eeee7eb61a95
parent
5ca48483
5ca4848305333adbe1112ca4f870879fc9d28749
+92
-58
1 changed file
Expand all
Collapse all
Unified
Split
posedetection
src
iosMain
kotlin
com
performancecoachlab
posedetection
camera
CameraEngine.kt
+92
-58
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraEngine.kt
Reviewed
···
89
89
import platform.darwin.dispatch_get_global_queue
90
90
import platform.darwin.dispatch_get_main_queue
91
91
import platform.darwin.dispatch_queue_create
92
92
+
import platform.darwin.dispatch_sync
93
93
+
import platform.darwin.dispatch_time
92
94
import platform.posix.memcpy
93
95
import kotlin.math.abs
94
96
import kotlin.native.runtime.NativeRuntimeApi
···
160
162
161
163
@OptIn(ExperimentalForeignApi::class)
162
164
private fun setupCamera() {
165
165
+
// Run session setup synchronously so callers can safely start the session afterwards
163
166
cameraController.setupSession()
164
167
cameraController.setupPreviewLayer(view)
165
168
startSession()
···
289
292
var drawSkeleton: Boolean = true
290
293
var drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)? = null
291
294
295
295
+
// Serial queue to serialize session configuration and start/stop calls
296
296
+
private val sessionQueue = dispatch_queue_create("com.performancecoachlab.captureSessionQueue", null)
297
297
+
292
298
sealed class CameraException : Exception() {
293
299
class DeviceNotAvailable : CameraException()
294
300
class ConfigurationError(message: String) : CameraException()
···
296
302
}
297
303
298
304
fun startRecording(): String? {
299
299
-
if (movieFileOutput == null) {
300
300
-
movieFileOutput = AVCaptureMovieFileOutput()
301
301
-
captureSession?.addOutput(movieFileOutput!!)
305
305
+
// Ensure we add outputs on the session queue to avoid races
306
306
+
dispatch_sync(sessionQueue) {
307
307
+
if (movieFileOutput == null) {
308
308
+
val candidate = AVCaptureMovieFileOutput()
309
309
+
movieFileOutput = candidate
310
310
+
if (captureSession?.canAddOutput(candidate) == true) {
311
311
+
captureSession?.addOutput(movieFileOutput!!)
312
312
+
}
313
313
+
}
302
314
}
315
315
+
303
316
val outputURL = generateSegmentURL()
304
317
movieFileOutput?.connections?.firstOrNull()?.let { connection ->
305
318
(connection as AVCaptureConnection).let { avConnection ->
···
345
358
}
346
359
347
360
fun setupSession() {
348
348
-
try {
349
349
-
captureSession = AVCaptureSession()
350
350
-
captureSession?.beginConfiguration()
351
351
-
captureSession?.sessionPreset = AVCaptureSessionPresetMedium
361
361
+
// Run synchronously on sessionQueue so callers (like setupCamera) can safely call startSession afterwards
362
362
+
dispatch_sync(sessionQueue) {
363
363
+
var configurationBegan = false
364
364
+
try {
365
365
+
captureSession = AVCaptureSession()
366
366
+
captureSession?.beginConfiguration()
367
367
+
configurationBegan = true
368
368
+
captureSession?.sessionPreset = AVCaptureSessionPresetMedium
369
369
+
370
370
+
if (!setupInputs()) {
371
371
+
throw CameraException.DeviceNotAvailable()
372
372
+
}
373
373
+
setupVideoOutput()
352
374
353
353
-
if (!setupInputs()) {
354
354
-
throw CameraException.DeviceNotAvailable()
375
375
+
// Add movie file output for recording only if it can be added
376
376
+
if (movieFileOutput == null) {
377
377
+
val candidate = AVCaptureMovieFileOutput()
378
378
+
if (captureSession?.canAddOutput(candidate) == true) {
379
379
+
movieFileOutput = candidate
380
380
+
captureSession?.addOutput(movieFileOutput!!)
381
381
+
} else {
382
382
+
// keep the instance (not added) so it can be added later when appropriate
383
383
+
movieFileOutput = candidate
384
384
+
}
385
385
+
}
386
386
+
} catch (e: CameraException) {
387
387
+
cleanupSession()
388
388
+
onError?.invoke(e)
389
389
+
return@dispatch_sync
390
390
+
} finally {
391
391
+
if (configurationBegan) {
392
392
+
captureSession?.commitConfiguration()
393
393
+
}
355
394
}
356
356
-
setupVideoOutput()
357
357
-
// Add movie file output for recording
358
358
-
if (movieFileOutput == null) {
359
359
-
movieFileOutput = AVCaptureMovieFileOutput()
360
360
-
captureSession?.addOutput(movieFileOutput!!)
361
361
-
}
362
362
-
captureSession?.commitConfiguration()
363
363
-
startRecording()
364
364
-
} catch (e: CameraException) {
365
365
-
cleanupSession()
366
366
-
onError?.invoke(e)
367
395
}
396
396
+
// Do not start recording here; startRecording() should be called explicitly after session is running
368
397
}
369
398
370
399
private fun setupVideoOutput() {
···
416
445
}
417
446
418
447
fun startSession() {
419
419
-
if (captureSession?.isRunning() == false) {
420
420
-
dispatch_async(
421
421
-
dispatch_get_global_queue(
422
422
-
DISPATCH_QUEUE_PRIORITY_HIGH.toLong(), 0u
423
423
-
)
424
424
-
) {
448
448
+
// Run on sessionQueue to avoid overlapping with configuration
449
449
+
dispatch_async(sessionQueue) {
450
450
+
if (captureSession?.isRunning() == false) {
425
451
captureSession?.startRunning()
426
452
}
427
453
}
428
454
}
429
455
430
456
fun stopSession() {
431
431
-
if (captureSession?.isRunning() == true) {
432
432
-
captureSession?.stopRunning()
457
457
+
dispatch_async(sessionQueue) {
458
458
+
if (captureSession?.isRunning() == true) {
459
459
+
captureSession?.stopRunning()
460
460
+
}
433
461
}
434
462
}
435
463
···
480
508
fun switchCamera() {
481
509
guard(captureSession != null) { return@guard }
482
510
483
483
-
captureSession?.beginConfiguration()
511
511
+
// Run camera switch on sessionQueue to serialize with setupSession/startSession
512
512
+
dispatch_sync(sessionQueue) {
513
513
+
var configurationBegan = false
514
514
+
try {
515
515
+
captureSession?.beginConfiguration()
516
516
+
configurationBegan = true
517
517
+
518
518
+
captureSession?.inputs?.firstOrNull()?.let { input ->
519
519
+
captureSession?.removeInput(input as AVCaptureInput)
520
520
+
}
484
521
485
485
-
try {
486
486
-
captureSession?.inputs?.firstOrNull()?.let { input ->
487
487
-
captureSession?.removeInput(input as AVCaptureInput)
488
488
-
}
522
522
+
isUsingFrontCamera = !isUsingFrontCamera
523
523
+
currentCamera = if (isUsingFrontCamera) frontCamera else backCamera
489
524
490
490
-
isUsingFrontCamera = !isUsingFrontCamera
491
491
-
currentCamera = if (isUsingFrontCamera) frontCamera else backCamera
525
525
+
val newCamera = currentCamera ?: throw CameraException.DeviceNotAvailable()
492
526
493
493
-
val newCamera = currentCamera ?: throw CameraException.DeviceNotAvailable()
527
527
+
val newInput = AVCaptureDeviceInput.deviceInputWithDevice(
528
528
+
newCamera, null
529
529
+
) ?: throw CameraException.ConfigurationError("Failed to create input")
494
530
495
495
-
val newInput = AVCaptureDeviceInput.deviceInputWithDevice(
496
496
-
newCamera, null
497
497
-
) ?: throw CameraException.ConfigurationError("Failed to create input")
531
531
+
if (captureSession?.canAddInput(newInput) == true) {
532
532
+
captureSession?.addInput(newInput)
533
533
+
} else {
534
534
+
throw CameraException.ConfigurationError("Cannot add input")
535
535
+
}
498
536
499
499
-
if (captureSession?.canAddInput(newInput) == true) {
500
500
-
captureSession?.addInput(newInput)
501
501
-
} else {
502
502
-
throw CameraException.ConfigurationError("Cannot add input")
503
503
-
}
537
537
+
cameraPreviewLayer?.connection?.let { connection ->
538
538
+
if (connection.isVideoMirroringSupported()) {
539
539
+
connection.automaticallyAdjustsVideoMirroring = false
540
540
+
connection.setVideoMirrored(isUsingFrontCamera)
541
541
+
}
542
542
+
}
504
543
505
505
-
cameraPreviewLayer?.connection?.let { connection ->
506
506
-
if (connection.isVideoMirroringSupported()) {
507
507
-
connection.automaticallyAdjustsVideoMirroring = false
508
508
-
connection.setVideoMirrored(isUsingFrontCamera)
544
544
+
} catch (e: CameraException) {
545
545
+
onError?.invoke(e)
546
546
+
} catch (e: Exception) {
547
547
+
onError?.invoke(CameraException.ConfigurationError(e.message ?: "Unknown error"))
548
548
+
} finally {
549
549
+
if (configurationBegan) {
550
550
+
captureSession?.commitConfiguration()
509
551
}
510
552
}
511
511
-
512
512
-
captureSession?.commitConfiguration()
513
513
-
} catch (e: CameraException) {
514
514
-
captureSession?.commitConfiguration()
515
515
-
onError?.invoke(e)
516
516
-
} catch (e: Exception) {
517
517
-
captureSession?.commitConfiguration()
518
518
-
onError?.invoke(CameraException.ConfigurationError(e.message ?: "Unknown error"))
519
553
}
520
554
}
521
555
···
573
607
).let { FrameSize(it.width.toInt(), it.height.toInt()) }
574
608
})
575
609
}
576
576
-
previewObjects?.also { objects ->
610
610
+
previewObjects.also { objects ->
577
611
customObjectRepository?.updateCustomObject(objects)
578
612
}
579
613
preview.bounds.useContents {
···
607
641
}
608
642
}
609
643
} catch (e: Exception) {
610
610
-
//println(e.message ?: "Unknown error in frame processing")
644
644
+
// ignore frame processing errors
611
645
}
612
646
}
613
647
}