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
feat: split recordings seamlessly
author
nathan holland
date
10 months ago
(Aug 24, 2025, 10:33 AM +0300)
commit
43f3e2e6
43f3e2e6206b2fb7c0ca9c4c1512e9ca7a69945a
parent
e268830c
e268830c37747aee3493b43fa161565101cc6839
+128
-250
6 changed files
Expand all
Collapse all
Unified
Split
posedetection
src
androidMain
kotlin
com.performancecoachlab
posedetection
camera
CameraView.android.kt
commonMain
kotlin
com
performancecoachlab
posedetection
camera
CameraView.kt
iosMain
kotlin
com
performancecoachlab
posedetection
camera
CameraEngine.kt
CameraView.ios.kt
encoding
VideoBuilder.ios.kt
sample
composeApp
src
commonMain
kotlin
com
nate
posedetection
App.kt
+86
-36
posedetection/src/androidMain/kotlin/com.performancecoachlab/posedetection/camera/CameraView.android.kt
Reviewed
···
55
55
import org.tensorflow.lite.support.image.TensorImage
56
56
import org.tensorflow.lite.task.vision.detector.ObjectDetector
57
57
58
58
+
// Data class to hold recording state for each recording ID
59
59
+
data class RecordingSlot(
60
60
+
var builder: com.performancecoachlab.posedetection.encoding.VideoBuilder? = null,
61
61
+
var firstTimestampMs: Long? = null,
62
62
+
val outputPath: String
63
63
+
)
64
64
+
58
65
@OptIn(ExperimentalGetImage::class)
59
66
@Composable
60
67
actual fun CameraView(
···
66
73
drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)?,
67
74
modifier: Modifier,
68
75
frontCamera: Boolean,
69
69
-
isRecording: Boolean,
76
76
+
recordingId: String?,
70
77
focusArea: Rect?,
71
78
onRecordToggled: (Boolean) -> Unit,
72
72
-
onVideoSaved: (String) -> Unit,
79
79
+
onVideoSaved: (String, String) -> Unit,
73
80
) {
74
81
var bitmap by remember { mutableStateOf<ImageBitmap?>(null) }
75
82
val options =
···
80
87
val previewView: PreviewView = remember { PreviewView(context) }
81
88
val executor = remember { Executors.newSingleThreadExecutor() }
82
89
val scope = rememberCoroutineScope()
83
83
-
var firstFrameTimestamp: Long? = null
84
90
var focus by remember { mutableStateOf(focusArea) }
85
91
var objectDetector by remember { mutableStateOf(objectModel) }
86
92
var currentDetectMode by remember { mutableStateOf(detectMode) }
···
96
102
objectDetector = objectModel
97
103
}
98
104
99
99
-
// Video recording state
100
100
-
var videoBuilder by remember { mutableStateOf<com.performancecoachlab.posedetection.encoding.VideoBuilder?>(null) }
105
105
+
// Multi-recording state - map of recording ID to recording slot
106
106
+
var activeRecordings by remember { mutableStateOf<Map<String, RecordingSlot>>(emptyMap()) }
101
107
102
102
-
var recordingActive by remember { mutableStateOf(false) }
103
103
-
var videoSavedPath by remember { mutableStateOf<String?>(null) }
104
104
-
105
105
-
suspend fun startRecording(width: Int, height: Int) {
106
106
-
videoBuilder = com.performancecoachlab.posedetection.encoding.createVideoBuilder(
107
107
-
outputPath = File(context.cacheDir, "camera_recording_${System.currentTimeMillis()}.mp4").absolutePath,
108
108
-
fps = 30,
109
109
-
width = width,
110
110
-
height = height
108
108
+
suspend fun startRecording(id: String, width: Int, height: Int) {
109
109
+
val outputPath = File(context.cacheDir, "camera_recording_${id}_${System.currentTimeMillis()}.mp4").absolutePath
110
110
+
val slot = RecordingSlot(
111
111
+
builder = null, // Lazily created on first frame
112
112
+
firstTimestampMs = null,
113
113
+
outputPath = outputPath
111
114
)
112
112
-
recordingActive = true
115
115
+
activeRecordings = activeRecordings + (id to slot)
113
116
}
114
117
115
115
-
suspend fun stopRecording() {
116
116
-
videoBuilder?.let { builder ->
117
117
-
val path = builder.finalize()
118
118
-
videoSavedPath = path
119
119
-
onVideoSaved(path)
120
120
-
videoBuilder = null
121
121
-
firstFrameTimestamp = null
118
118
+
suspend fun stopRecording(id: String) {
119
119
+
activeRecordings[id]?.let { slot ->
120
120
+
slot.builder?.let { builder ->
121
121
+
try {
122
122
+
val path = builder.finalize()
123
123
+
onVideoSaved(id, path)
124
124
+
} catch (e: Exception) {
125
125
+
println("Error finalizing recording $id: ${e.message}")
126
126
+
}
127
127
+
}
128
128
+
activeRecordings = activeRecordings - id
122
129
}
123
123
-
recordingActive = false
124
130
}
125
125
-
fun addFrame(it: ImageBitmap, timestamp: Long) {
126
126
-
if (recordingActive && videoBuilder != null) {
131
131
+
132
132
+
fun addFrameToActiveRecordings(frame: ImageBitmap, timestampMs: Long) {
133
133
+
if (activeRecordings.isNotEmpty()) {
127
134
scope.launch {
128
128
-
if (firstFrameTimestamp == null) firstFrameTimestamp = timestamp
129
129
-
val relativeTimestamp = timestamp - (firstFrameTimestamp ?: timestamp)
130
130
-
videoBuilder?.addFrame(it, relativeTimestamp)
135
135
+
activeRecordings.forEach { (id, slot) ->
136
136
+
try {
137
137
+
// Lazily create builder on first frame to lock width/height
138
138
+
if (slot.builder == null) {
139
139
+
slot.builder = com.performancecoachlab.posedetection.encoding.createVideoBuilder(
140
140
+
outputPath = slot.outputPath,
141
141
+
fps = 30,
142
142
+
width = frame.width,
143
143
+
height = frame.height
144
144
+
)
145
145
+
slot.firstTimestampMs = timestampMs
146
146
+
}
147
147
+
148
148
+
slot.builder?.let { builder ->
149
149
+
val relativeTimestamp = timestampMs - (slot.firstTimestampMs ?: timestampMs)
150
150
+
builder.addFrame(frame, relativeTimestamp)
151
151
+
}
152
152
+
} catch (e: Exception) {
153
153
+
// Handle recording errors gracefully
154
154
+
println("Error recording frame for ID $id: ${e.message}")
155
155
+
}
156
156
+
}
131
157
}
132
158
}
133
159
}
134
160
135
135
-
// React to isRecording changes
136
136
-
LaunchedEffect(isRecording) {
137
137
-
if (isRecording && !recordingActive && bitmap != null) {
138
138
-
// Start recording with current frame size
139
139
-
startRecording(bitmap!!.width, bitmap!!.height)
140
140
-
} else if (!isRecording && recordingActive) {
141
141
-
stopRecording()
161
161
+
// React to recordingIds changes - diff and start/stop recordings
162
162
+
LaunchedEffect(recordingId) {
163
163
+
val currentIds = activeRecordings.keys
164
164
+
recordingId?.also { id ->
165
165
+
currentIds.forEach {
166
166
+
if(!it.contentEquals(id)){
167
167
+
stopRecording(it)
168
168
+
}
169
169
+
}
170
170
+
if(!currentIds.contains(id)){
171
171
+
if (bitmap != null) {
172
172
+
startRecording(id, bitmap!!.width, bitmap!!.height)
173
173
+
} else {
174
174
+
// If no frame yet, create empty slot that will be initialized on first frame
175
175
+
val outputPath = File(context.cacheDir, "camera_recording_${id}_${System.currentTimeMillis()}.mp4").absolutePath
176
176
+
val slot = RecordingSlot(
177
177
+
builder = null,
178
178
+
firstTimestampMs = null,
179
179
+
outputPath = outputPath
180
180
+
)
181
181
+
activeRecordings = activeRecordings + (id to slot)
182
182
+
}
183
183
+
}
184
184
+
}?: run {
185
185
+
// No recordingId - stop all
186
186
+
currentIds.forEach {
187
187
+
stopRecording(it)
188
188
+
}
142
189
}
190
190
+
191
191
+
143
192
}
144
193
145
194
LaunchedEffect(lifecycleOwner) {
···
165
214
bitmap =
166
215
imageProxy.toBitmap().rotate(imageProxy.imageInfo.rotationDegrees.toFloat())
167
216
.asImageBitmap().let { inbmp ->
168
168
-
addFrame(inbmp,timestamp)
217
217
+
addFrameToActiveRecordings(inbmp, timestamp)
169
218
inbmp.drawResults(if(drawSkeleton) it.skeleton else null, drawObjects?.invoke(it.objects)?: emptyList())
170
219
}
171
220
imageProxy.close()
···
180
229
)
181
230
previewView.scaleType = PreviewView.ScaleType.FIT_CENTER
182
231
}
232
232
+
183
233
Box(
184
234
modifier = modifier
185
235
) {
+2
-2
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.kt
Reviewed
···
31
31
},
32
32
modifier: Modifier = Modifier.fillMaxSize(),
33
33
frontCamera: Boolean = true,
34
34
-
isRecording: Boolean = false,
34
34
+
recordingId: String? = null,
35
35
focusArea: Rect? = null,
36
36
onRecordToggled: (Boolean) -> Unit = {},
37
37
-
onVideoSaved: (String) -> Unit = {},
37
37
+
onVideoSaved: (String, String) -> Unit
38
38
)
39
39
40
40
data class DrawableObject(
+6
-8
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraEngine.kt
Reviewed
···
204
204
cameraController.frameListener = listener
205
205
}
206
206
207
207
-
fun startRecording() {
208
208
-
cameraController.startRecording()
209
209
-
}
207
207
+
fun startRecording() = cameraController.startRecording()
210
208
211
209
fun stopRecording() {
212
210
cameraController.stopRecording()
213
211
}
214
212
215
215
-
fun splitRecording() {
216
216
-
cameraController.splitRecording()
217
217
-
}
213
213
+
fun splitRecording() = cameraController.splitRecording()
218
214
219
215
fun setOnVideoSavedCallback(callback: (String) -> Unit) {
220
216
onVideoSaved = callback
···
262
258
class CaptureError(message: String) : CameraException()
263
259
}
264
260
265
265
-
fun startRecording() {
261
261
+
fun startRecording(): String? {
266
262
if (movieFileOutput == null) {
267
263
movieFileOutput = AVCaptureMovieFileOutput()
268
264
captureSession?.addOutput(movieFileOutput!!)
···
277
273
278
274
}
279
275
movieFileOutput?.startRecordingToOutputFileURL(outputURL, this)
276
276
+
return outputURL.path
280
277
}
281
278
282
279
fun stopRecording() {
283
280
movieFileOutput?.stopRecording()
284
281
}
285
282
286
286
-
fun splitRecording() {
283
283
+
fun splitRecording(): String? {
287
284
movieFileOutput?.stopRecording()
288
285
val outputURL = generateSegmentURL()
289
286
movieFileOutput?.startRecordingToOutputFileURL(outputURL, this)
287
287
+
return outputURL.path
290
288
}
291
289
292
290
private fun generateSegmentURL(): platform.Foundation.NSURL {
+25
-7
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/CameraView.ios.kt
Reviewed
···
34
34
drawObjects: ((List<AnalysisObject>) -> List<DrawableObject>)?,
35
35
modifier: Modifier,
36
36
frontCamera: Boolean,
37
37
-
isRecording: Boolean,
37
37
+
recordingId: String?,
38
38
focusArea: Rect?,
39
39
onRecordToggled: (Boolean) -> Unit,
40
40
-
onVideoSaved: (String) -> Unit,
40
40
+
onVideoSaved: (String, String) -> Unit,
41
41
) {
42
42
val cameraEngine = remember { mutableStateOf<CameraEngine?>(null) }
43
43
val frameListener = remember { FrameRepository() }
44
44
val frameBitmap by frameListener.frameFlow.collectAsState()
45
45
var lastRecordingState by remember { mutableStateOf(false) }
46
46
+
var idMap by remember { mutableStateOf<Map<String, String>>(emptyMap()) }
46
47
LaunchedEffect(detectMode) {
47
48
cameraEngine.value?.setDetectMode(detectMode)
48
49
}
49
49
-
LaunchedEffect(cameraEngine.value, isRecording) {
50
50
+
val recordingDone = {path: String ->
51
51
+
val id = idMap.entries.firstOrNull { it.value == path }?.key
52
52
+
if (id != null) {
53
53
+
onVideoSaved(id, path)
54
54
+
idMap = idMap - id
55
55
+
}
56
56
+
}
57
57
+
LaunchedEffect(cameraEngine.value, recordingId) {
50
58
cameraEngine.value?.apply {
51
59
addSkeletonRepository(skeletonRepository)
52
60
addCustomObjectRepository(customObjectRepository)
53
61
addFrameListener(frameListener)
54
54
-
setOnVideoSavedCallback(onVideoSaved)
62
62
+
setOnVideoSavedCallback(recordingDone)
55
63
setDrawOptions(
56
64
drawSkeleton = drawSkeleton,
57
65
drawObjects = drawObjects,
58
66
)
59
59
-
if (isRecording && !lastRecordingState) startRecording()
60
60
-
if (!isRecording && lastRecordingState) stopRecording()
61
61
-
lastRecordingState = isRecording
67
67
+
68
68
+
if (recordingId != null) {
69
69
+
(if(lastRecordingState) splitRecording()
70
70
+
else startRecording())?.also{
71
71
+
idMap = idMap + (recordingId to it)
72
72
+
lastRecordingState = true
73
73
+
}
74
74
+
} else {
75
75
+
if(lastRecordingState){
76
76
+
stopRecording()
77
77
+
}
78
78
+
lastRecordingState = false
79
79
+
}
62
80
}
63
81
}
64
82
LaunchedEffect(focusArea){
-191
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/encoding/VideoBuilder.ios.kt
Reviewed
···
222
222
frameCount = 0L
223
223
}
224
224
225
225
-
@OptIn(ExperimentalForeignApi::class, NativeRuntimeApi::class)
226
226
-
fun buildVideoFromImages(
227
227
-
images: List<ImageBitmap>,
228
228
-
outputSize: CValue<CGSize> = CGSizeMake(width.toDouble(), height.toDouble())
229
229
-
): String {
230
230
-
CGSizeMake(1.0, 1.0)
231
231
-
// Create output file path
232
232
-
val fileManager = NSFileManager.defaultManager
233
233
-
234
234
-
// Delete existing file if needed
235
235
-
if (fileManager.fileExistsAtPath(outputPath)) {
236
236
-
val success = fileManager.removeItemAtPath(outputPath, null)
237
237
-
if (!success) {
238
238
-
throw IllegalStateException("Unable to delete existing file")
239
239
-
}
240
240
-
}
241
241
-
242
242
-
// Create asset writer
243
243
-
val videoWriter = AVAssetWriter(NSURL.fileURLWithPath(outputPath), AVFileTypeMPEG4, null)
244
244
-
?: throw IllegalStateException("AVAssetWriter error")
245
245
-
246
246
-
// Configure video settings
247
247
-
val width = outputSize.useContents { width.toInt() }
248
248
-
val height = outputSize.useContents { height.toInt() }
249
249
-
250
250
-
val outputSettings = mapOf(
251
251
-
AVVideoCodecKey.toString() to AVVideoCodecH264,
252
252
-
AVVideoWidthKey.toString() to width,
253
253
-
AVVideoHeightKey.toString() to height,
254
254
-
AVVideoCompressionPropertiesKey.toString() to mapOf(
255
255
-
AVVideoAverageBitRateKey.toString() to 6000000, // 6 Mbps
256
256
-
AVVideoProfileLevelKey.toString() to AVVideoProfileLevelH264HighAutoLevel
257
257
-
)
258
258
-
)
259
259
-
val sourcePixelBufferAttributes = mapOf(
260
260
-
CFBridgingRelease(kCVPixelBufferPixelFormatTypeKey) as String to kCVPixelFormatType_32BGRA,
261
261
-
CFBridgingRelease(kCVPixelBufferWidthKey) as String to width,
262
262
-
CFBridgingRelease(kCVPixelBufferHeightKey) as String to height,
263
263
-
CFBridgingRelease(kCVPixelBufferCGImageCompatibilityKey) as String to true,
264
264
-
CFBridgingRelease(kCVPixelBufferCGBitmapContextCompatibilityKey) as String to true
265
265
-
)
266
266
-
val videoWriterInput = AVAssetWriterInput(
267
267
-
mediaType = AVMediaTypeVideo, outputSettings = outputSettings as Map<Any?, *>?
268
268
-
)
269
269
-
// Add input to writer and start session
270
270
-
if (videoWriter.canAddInput(videoWriterInput)) {
271
271
-
println("[VideoBuilder] Adding input to writer.")
272
272
-
videoWriter.addInput(videoWriterInput)
273
273
-
} else {
274
274
-
println("[VideoBuilder] Cannot add input to writer. canAddInput returned false.")
275
275
-
throw IllegalStateException("Cannot add input to writer")
276
276
-
}
277
277
-
println("[VideoBuilder] videoWriterInput.outputSettings: $outputSettings")
278
278
-
println("[VideoBuilder] sourcePixelBufferAttributes: $sourcePixelBufferAttributes")
279
279
-
println("[VideoBuilder] videoWriter status before startWriting: ${videoWriter.status}")
280
280
-
// Create pixel buffer adaptor AFTER input is added
281
281
-
val pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor(
282
282
-
assetWriterInput = videoWriterInput,
283
283
-
sourcePixelBufferAttributes = sourcePixelBufferAttributes as Map<Any?, *>?
284
284
-
)
285
285
-
286
286
-
videoWriter.startWriting()
287
287
-
println("[VideoBuilder] videoWriter status after startWriting: ${videoWriter.status}")
288
288
-
videoWriter.startSessionAtSourceTime(CMTimeMake(0, 1))
289
289
-
println("[VideoBuilder] videoWriter status after startSessionAtSourceTime: ${videoWriter.status}")
290
290
-
291
291
-
val fps: Int = 30
292
292
-
val frameDuration = CMTimeMake(1, fps)
293
293
-
var frameCount: Long = 0
294
294
-
val remainingImages = images.toMutableList()
295
295
-
// Only access pixelBufferPool after writing has started
296
296
-
val pool = pixelBufferAdaptor.pixelBufferPool
297
297
-
if (pool == null) {
298
298
-
println("[VideoBuilder] pixelBufferAdaptor.pixelBufferPool is STILL null after startWriting/startSessionAtSourceTime!")
299
299
-
println("[VideoBuilder] videoWriter status: ${videoWriter.status}")
300
300
-
println("[VideoBuilder] videoWriterInput isReadyForMoreMediaData: ${videoWriterInput.readyForMoreMediaData}")
301
301
-
throw IllegalStateException("Pixel buffer pool is null after startWriting/startSessionAtSourceTime")
302
302
-
}
303
303
-
304
304
-
// Process each image
305
305
-
videoWriterInput.requestMediaDataWhenReadyOnQueue(
306
306
-
dispatch_queue_create(
307
307
-
"mediaInputQueue", null
308
308
-
)
309
309
-
) {
310
310
-
var appendSucceeded = true
311
311
-
312
312
-
while (remainingImages.isNotEmpty() && appendSucceeded) {
313
313
-
if (videoWriterInput.readyForMoreMediaData) {
314
314
-
val nextPhoto = remainingImages.removeAt(0)
315
315
-
val lastFrameTime = CMTimeMake(frameCount, fps)
316
316
-
val presentationTime = if (frameCount == 0L) lastFrameTime else CMTimeAdd(
317
317
-
lastFrameTime, frameDuration
318
318
-
)
319
319
-
320
320
-
memScoped {
321
321
-
val pixelBufferPtr = alloc<CVPixelBufferRefVar>()
322
322
-
val status = CVPixelBufferPoolCreatePixelBuffer(
323
323
-
kCFAllocatorDefault, pool, pixelBufferPtr.ptr
324
324
-
)
325
325
-
326
326
-
if (status == kCVReturnSuccess) {
327
327
-
val pixelBuffer = pixelBufferPtr.value
328
328
-
CVPixelBufferLockBaseAddress(pixelBuffer, 0u)
329
329
-
330
330
-
val baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer)
331
331
-
val bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
332
332
-
val colorSpace = CGColorSpaceCreateDeviceRGB()
333
333
-
val bitmapInfo = CGImageAlphaInfo.kCGImageAlphaPremultipliedFirst.value
334
334
-
335
335
-
val context = CGBitmapContextCreate(
336
336
-
baseAddress,
337
337
-
width.convert(),
338
338
-
height.convert(),
339
339
-
8u,
340
340
-
bytesPerRow.convert(),
341
341
-
colorSpace,
342
342
-
bitmapInfo
343
343
-
)
344
344
-
345
345
-
if (context != null) {
346
346
-
// Clear context
347
347
-
CGContextClearRect(
348
348
-
context,
349
349
-
CGRectMake(0.0, 0.0, width.toDouble(), height.toDouble())
350
350
-
)
351
351
-
352
352
-
// Use ImageBitmap pixel data directly
353
353
-
println("image processed")
354
354
-
val pixelMap = nextPhoto.toPixelMap()
355
355
-
val buffer = ensureFourChannelBuffer(pixelMap)
356
356
-
val contextData = baseAddress?.reinterpret<ByteVar>()
357
357
-
// Debug prints
358
358
-
println("[VideoBuilder] ensureFourChannelBuffer: buffer.size=${buffer.size}, expected=${width * height * 4}")
359
359
-
println("[VideoBuilder] CGContext bytesPerRow: $bytesPerRow, expected=${width * 4}")
360
360
-
if (buffer.size != width * height * 4) {
361
361
-
println("[VideoBuilder] ERROR: Buffer size does not match expected size for BGRA data!")
362
362
-
}
363
363
-
if (contextData != null) {
364
364
-
// Copy row by row to handle bytesPerRow alignment
365
365
-
val srcRowBytes = width * 4
366
366
-
val dstRowBytes = bytesPerRow.toInt()
367
367
-
for (row in 0 until height) {
368
368
-
buffer.usePinned { pinned ->
369
369
-
val srcOffset = row * srcRowBytes
370
370
-
val dstOffset = row * dstRowBytes
371
371
-
val dstPtr = contextData.plus(dstOffset)
372
372
-
platform.posix.memcpy(
373
373
-
dstPtr,
374
374
-
pinned.addressOf(srcOffset),
375
375
-
srcRowBytes.toULong()
376
376
-
)
377
377
-
}
378
378
-
}
379
379
-
}
380
380
-
CGContextRelease(context)
381
381
-
}
382
382
-
383
383
-
CGColorSpaceRelease(colorSpace)
384
384
-
CVPixelBufferUnlockBaseAddress(pixelBuffer, 0u)
385
385
-
386
386
-
// Append frame to video
387
387
-
appendSucceeded = pixelBufferAdaptor.appendPixelBuffer(
388
388
-
pixelBuffer, withPresentationTime = presentationTime
389
389
-
)
390
390
-
CVPixelBufferRelease(pixelBuffer)
391
391
-
} else {
392
392
-
println("Failed to allocate pixel buffer: $status")
393
393
-
appendSucceeded = false
394
394
-
}
395
395
-
}
396
396
-
397
397
-
frameCount++
398
398
-
}
399
399
-
}
400
400
-
401
401
-
videoWriterInput.markAsFinished()
402
402
-
videoWriter.finishWritingWithCompletionHandler {
403
403
-
println("[VideoBuilder] Finished writing video!")
404
404
-
}
405
405
-
}
406
406
-
407
407
-
// Wait for writing to finish (this is simplified)
408
408
-
// In a real app, you would handle this asynchronously
409
409
-
while (videoWriter.status == AVAssetWriterStatusWriting) {
410
410
-
// Wait for completion
411
411
-
}
412
412
-
413
413
-
return outputPath
414
414
-
}
415
415
-
416
225
private fun ensureFourChannelBuffer(pixelMap: PixelMap): ByteArray {
417
226
val width = pixelMap.width
418
227
val height = pixelMap.height
+9
-6
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/App.kt
Reviewed
···
63
63
import kotlinx.coroutines.Job
64
64
import kotlinx.coroutines.launch
65
65
import kotlin.math.roundToLong
66
66
+
import kotlin.time.Clock
67
67
+
import kotlin.time.ExperimentalTime
66
68
67
69
@Composable
68
70
internal fun App() = AppTheme {
···
276
278
}
277
279
}
278
280
281
281
+
@OptIn(ExperimentalTime::class)
279
282
@Composable
280
283
fun CameraSample() {
281
284
val skeletonRepository = remember { SkeletonRepository() }
···
283
286
val skeleton by skeletonRepository.skeletonFlow.collectAsState()
284
287
val customObjects by customObjectRespository.customObjectFlow.collectAsState()
285
288
var permissionGranted by remember { mutableStateOf(false) }
286
286
-
var isRecording by remember { mutableStateOf(false) }
289
289
+
var recordingId : String? by remember { mutableStateOf(null) }
287
290
var path by remember { mutableStateOf("") }
288
291
val generalModel = initialiseObjectModel(
289
292
ModelPath(
···
328
331
CameraView(
329
332
skeletonRepository = skeletonRepository,
330
333
customObjectRepository = customObjectRespository,
334
334
+
detectMode = DetectMode.NONE,
331
335
drawSkeleton = true,
332
336
objectModel = generalModel,
333
337
modifier = Modifier.weight(1f),
334
338
frontCamera = true,
335
335
-
isRecording = isRecording,
336
336
-
onRecordToggled = { isRecording = it },
337
337
-
onVideoSaved = { path = it },
339
339
+
recordingId = recordingId,
340
340
+
onVideoSaved = {id,url -> path = url },
338
341
)
339
342
}
340
343
Button(
341
344
onClick = {
342
342
-
isRecording = !isRecording
345
345
+
recordingId = "${Clock.System.now().epochSeconds}"
343
346
},
344
347
modifier = Modifier.imePadding().padding(16.dp).align(Alignment.TopStart)
345
348
) {
346
346
-
Text(if (isRecording) "Stop Recording" else "Start Recording")
349
349
+
Text("Start Recording")
347
350
}
348
351
}
349
352
} else Text("Camera permission not granted")