This repository has no description
0

Configure Feed

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

feat: record image sequence to video

+490 -19
+1 -1
README.md
··· 6 6 Import the Compose library 7 7 8 8 ```kotlin 9 - implementation("com.performancecoachlab.posedetection:posedetection-compose:1.2.2") 9 + implementation("com.performancecoachlab.posedetection:posedetection-compose:1.3.0") 10 10 ``` 11 11 12 12 Add camera use to your android manifest
+1 -1
posedetection/build.gradle.kts
··· 6 6 7 7 mavenPublishing { 8 8 publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 9 - coordinates("com.performancecoachlab.posedetection", "posedetection-compose", "1.2.2") 9 + coordinates("com.performancecoachlab.posedetection", "posedetection-compose", "1.3.0") 10 10 11 11 pom { 12 12 name.set("Pose Detection")
+1
posedetection/src/androidMain/kotlin/com/performancecoachlab/posedetection/encoding/VideoBuilder.android.kt
··· 7 7 import android.media.MediaCodecInfo 8 8 import android.media.MediaFormat 9 9 import android.media.MediaMuxer 10 + import com.performancecoachlab.posedetection.recording.InputFrame 10 11 import kotlinx.coroutines.Dispatchers 11 12 import kotlinx.coroutines.withContext 12 13 import java.io.File
+3 -3
posedetection/src/androidMain/kotlin/com/performancecoachlab/posedetection/recording/InputFrame.android.kt
··· 48 48 } 49 49 } 50 50 51 - objectDetector.process(img).addOnSuccessListener { 51 + /*objectDetector.process(img).addOnSuccessListener { 52 52 objectResults = it.map { detectedObject -> 53 53 val boundingBox = detectedObject.boundingBox.let{ bound-> 54 54 Rect( ··· 73 73 }.addOnFailureListener { 74 74 println(it.message) 75 75 tryResume() 76 - } 77 - 76 + }*/ 77 + tryResume() 78 78 poseDetector.process(img).addOnSuccessListener { pose -> 79 79 poseResult = skeleton(pose, inputFrame.timestamp, img.width, img.height) 80 80 tryResume()
+1
posedetection/src/commonMain/kotlin/com/performancecoachlab/posedetection/encoding/VideoBuilder.kt
··· 1 1 package com.performancecoachlab.posedetection.encoding 2 2 3 3 import androidx.compose.ui.graphics.ImageBitmap 4 + import com.performancecoachlab.posedetection.recording.InputFrame 4 5 5 6 /** 6 7 * Builder for creating videos from a sequence of ImageBitmap frames
+1 -1
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/camera/FrameProcessor.kt
··· 278 278 return 279 279 } 280 280 memScoped { 281 - 281 + onProccessed(emptyList()) 282 282 } 283 283 } 284 284
+445 -10
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/encoding/VideoBuilder.ios.kt
··· 1 1 package com.performancecoachlab.posedetection.encoding 2 2 3 - /** 4 - * Creates a new VideoBuilder instance 5 - */ 3 + import androidx.compose.ui.graphics.ImageBitmap 4 + import androidx.compose.ui.graphics.PixelMap 5 + import androidx.compose.ui.graphics.toPixelMap 6 + import com.performancecoachlab.posedetection.recording.InputFrame 7 + import kotlinx.cinterop.ByteVar 8 + import kotlinx.cinterop.CValue 9 + import kotlinx.cinterop.ExperimentalForeignApi 10 + import kotlinx.cinterop.addressOf 11 + import kotlinx.cinterop.alloc 12 + import kotlinx.cinterop.convert 13 + import kotlinx.cinterop.memScoped 14 + import kotlinx.cinterop.plus 15 + import kotlinx.cinterop.ptr 16 + import kotlinx.cinterop.reinterpret 17 + import kotlinx.cinterop.useContents 18 + import kotlinx.cinterop.usePinned 19 + import kotlinx.cinterop.value 20 + import kotlinx.coroutines.Dispatchers 21 + import kotlinx.coroutines.withContext 22 + import platform.AVFoundation.AVAssetWriter 23 + import platform.AVFoundation.AVAssetWriterInput 24 + import platform.AVFoundation.AVAssetWriterInputPixelBufferAdaptor 25 + import platform.AVFoundation.AVAssetWriterStatusWriting 26 + import platform.AVFoundation.AVFileTypeMPEG4 27 + import platform.AVFoundation.AVMediaTypeVideo 28 + import platform.AVFoundation.AVVideoAverageBitRateKey 29 + import platform.AVFoundation.AVVideoCodecH264 30 + import platform.AVFoundation.AVVideoCodecKey 31 + import platform.AVFoundation.AVVideoCompressionPropertiesKey 32 + import platform.AVFoundation.AVVideoHeightKey 33 + import platform.AVFoundation.AVVideoProfileLevelH264HighAutoLevel 34 + import platform.AVFoundation.AVVideoProfileLevelKey 35 + import platform.AVFoundation.AVVideoWidthKey 36 + import platform.CoreFoundation.kCFAllocatorDefault 37 + import platform.CoreGraphics.CGBitmapContextCreate 38 + import platform.CoreGraphics.CGColorSpaceCreateDeviceRGB 39 + import platform.CoreGraphics.CGColorSpaceRelease 40 + import platform.CoreGraphics.CGContextClearRect 41 + import platform.CoreGraphics.CGContextDrawImage 42 + import platform.CoreGraphics.CGContextRelease 43 + import platform.CoreGraphics.CGContextScaleCTM 44 + import platform.CoreGraphics.CGContextTranslateCTM 45 + import platform.CoreGraphics.CGImageAlphaInfo 46 + import platform.CoreGraphics.CGImageRef 47 + import platform.CoreGraphics.CGRectMake 48 + import platform.CoreGraphics.CGSize 49 + import platform.CoreGraphics.CGSizeMake 50 + import platform.CoreGraphics.kCGBitmapByteOrder32Little 51 + import platform.CoreMedia.CMTimeAdd 52 + import platform.CoreMedia.CMTimeMake 53 + import platform.CoreVideo.CVPixelBufferGetBaseAddress 54 + import platform.CoreVideo.CVPixelBufferGetBytesPerRow 55 + import platform.CoreVideo.CVPixelBufferLockBaseAddress 56 + import platform.CoreVideo.CVPixelBufferPoolCreatePixelBuffer 57 + import platform.CoreVideo.CVPixelBufferPoolRef 58 + import platform.CoreVideo.CVPixelBufferRef 59 + import platform.CoreVideo.CVPixelBufferRefVar 60 + import platform.CoreVideo.CVPixelBufferRelease 61 + import platform.CoreVideo.CVPixelBufferUnlockBaseAddress 62 + import platform.CoreVideo.kCVPixelBufferCGBitmapContextCompatibilityKey 63 + import platform.CoreVideo.kCVPixelBufferCGImageCompatibilityKey 64 + import platform.CoreVideo.kCVPixelBufferHeightKey 65 + import platform.CoreVideo.kCVPixelBufferPixelFormatTypeKey 66 + import platform.CoreVideo.kCVPixelBufferWidthKey 67 + import platform.CoreVideo.kCVPixelFormatType_32BGRA 68 + import platform.CoreVideo.kCVReturnSuccess 69 + import platform.Foundation.CFBridgingRelease 70 + import platform.Foundation.NSDocumentDirectory 71 + import platform.Foundation.NSFileManager 72 + import platform.Foundation.NSURL 73 + import platform.Foundation.NSUserDomainMask 74 + import platform.darwin.dispatch_queue_create 75 + import kotlin.native.runtime.GC 76 + import kotlin.native.runtime.NativeRuntimeApi 77 + 78 + private class IOSVideoBuilder( 79 + private val outputPath: String, 80 + private val fps: Int, 81 + private val width: Int, 82 + private val height: Int 83 + ) : VideoBuilder { 84 + private var writer: AVAssetWriter? = null 85 + private var input: AVAssetWriterInput? = null 86 + private var adaptor: AVAssetWriterInputPixelBufferAdaptor? = null 87 + private var frameCount = 0L 88 + private var started = false 89 + private val timeScale = 600L // Standard timescale for video 90 + private val images: MutableList<ImageBitmap> = 91 + mutableListOf() // Placeholder for images, if needed 92 + 93 + @OptIn(ExperimentalForeignApi::class) 94 + private fun initWriter() { 95 + // Initialize writer and session 96 + val fileManager = NSFileManager.defaultManager 97 + if (fileManager.fileExistsAtPath(outputPath)) { 98 + fileManager.removeItemAtPath(outputPath, null) 99 + } 100 + val videoWriter = AVAssetWriter(NSURL.fileURLWithPath(outputPath), AVFileTypeMPEG4, null) 101 + val outputSettings = mapOf( 102 + AVVideoCodecKey.toString() to AVVideoCodecH264, 103 + AVVideoWidthKey.toString() to width, 104 + AVVideoHeightKey.toString() to height, 105 + AVVideoCompressionPropertiesKey.toString() to mapOf( 106 + AVVideoAverageBitRateKey.toString() to 6000000, 107 + AVVideoProfileLevelKey.toString() to AVVideoProfileLevelH264HighAutoLevel 108 + ) 109 + ) 110 + val videoWriterInput = AVAssetWriterInput( 111 + mediaType = AVMediaTypeVideo, outputSettings = outputSettings as Map<Any?, *>? 112 + ) 113 + val sourcePixelBufferAttributes = mapOf( 114 + CFBridgingRelease(kCVPixelBufferPixelFormatTypeKey) as String to kCVPixelFormatType_32BGRA, 115 + CFBridgingRelease(kCVPixelBufferWidthKey) as String to width, 116 + CFBridgingRelease(kCVPixelBufferHeightKey) as String to height, 117 + CFBridgingRelease(kCVPixelBufferCGImageCompatibilityKey) as String to true, 118 + CFBridgingRelease(kCVPixelBufferCGBitmapContextCompatibilityKey) as String to true 119 + ) 120 + val pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor( 121 + assetWriterInput = videoWriterInput, 122 + sourcePixelBufferAttributes = sourcePixelBufferAttributes as Map<Any?, *>? 123 + ) 124 + if (videoWriter.canAddInput(videoWriterInput)) { 125 + videoWriter.addInput(videoWriterInput) 126 + } else { 127 + throw IllegalStateException("Cannot add input to writer") 128 + } 129 + videoWriter.startWriting() 130 + videoWriter.startSessionAtSourceTime(CMTimeMake(0, 1)) 131 + writer = videoWriter 132 + input = videoWriterInput 133 + adaptor = pixelBufferAdaptor 134 + started = true 135 + frameCount = 0L 136 + } 137 + @OptIn(ExperimentalForeignApi::class, NativeRuntimeApi::class) 138 + override suspend fun addFrame(frame: ImageBitmap) { 139 + if (!started) initWriter() 140 + val videoWriter = writer!! 141 + val videoWriterInput = input!! 142 + val pixelBufferAdaptor = adaptor!! 143 + val pool = pixelBufferAdaptor.pixelBufferPool ?: throw IllegalStateException("Pixel buffer pool is null") 144 + val fps = 30 145 + val frameDuration = CMTimeMake(1, fps) 146 + val presentationTime = if (frameCount == 0L) CMTimeMake(0, fps) else CMTimeAdd(CMTimeMake(frameCount, fps), frameDuration) 147 + memScoped { 148 + val pixelBufferPtr = alloc<CVPixelBufferRefVar>() 149 + val status = CVPixelBufferPoolCreatePixelBuffer(kCFAllocatorDefault, pool, pixelBufferPtr.ptr) 150 + if (status == kCVReturnSuccess) { 151 + val pixelBuffer = pixelBufferPtr.value 152 + CVPixelBufferLockBaseAddress(pixelBuffer, 0u) 153 + val baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) 154 + val bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) 155 + val colorSpace = CGColorSpaceCreateDeviceRGB() 156 + val bitmapInfo = CGImageAlphaInfo.kCGImageAlphaPremultipliedFirst.value 157 + val context = CGBitmapContextCreate( 158 + baseAddress, 159 + width.convert(), 160 + height.convert(), 161 + 8u, 162 + bytesPerRow.convert(), 163 + colorSpace, 164 + bitmapInfo 165 + ) 166 + if (context != null) { 167 + CGContextClearRect(context, CGRectMake(0.0, 0.0, width.toDouble(), height.toDouble())) 168 + val pixelMap = frame.toPixelMap() 169 + val buffer = ensureFourChannelBuffer(pixelMap) 170 + val contextData = baseAddress?.reinterpret<ByteVar>() 171 + if (contextData != null) { 172 + val srcRowBytes = width * 4 173 + val dstRowBytes = bytesPerRow.toInt() 174 + for (row in 0 until height) { 175 + buffer.usePinned { pinned -> 176 + val srcOffset = row * srcRowBytes 177 + val dstOffset = row * dstRowBytes 178 + val dstPtr = contextData.plus(dstOffset) 179 + platform.posix.memcpy( 180 + dstPtr, 181 + pinned.addressOf(srcOffset), 182 + srcRowBytes.toULong() 183 + ) 184 + } 185 + } 186 + } 187 + CGContextRelease(context) 188 + } 189 + CGColorSpaceRelease(colorSpace) 190 + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0u) 191 + // Wait for input to be ready 192 + while (!videoWriterInput.readyForMoreMediaData) {} 193 + pixelBufferAdaptor.appendPixelBuffer(pixelBuffer, withPresentationTime = presentationTime) 194 + CVPixelBufferRelease(pixelBuffer) 195 + } else { 196 + println("Failed to allocate pixel buffer: $status") 197 + } 198 + } 199 + frameCount++ 200 + } 201 + 202 + @OptIn(ExperimentalForeignApi::class) 203 + override suspend fun finalize(): String = withContext(Dispatchers.Default) { 204 + if (started) { 205 + input?.markAsFinished() 206 + writer?.finishWritingWithCompletionHandler { 207 + println("[VideoBuilder] Finished writing video!") 208 + } 209 + // Wait for writing to finish 210 + while (writer?.status == AVAssetWriterStatusWriting) {} 211 + cleanup() 212 + } 213 + outputPath 214 + } 215 + 216 + override suspend fun cancel() = withContext(Dispatchers.Default) { 217 + writer?.cancelWriting() 218 + cleanup() 219 + } 220 + 221 + private fun cleanup() { 222 + writer = null 223 + input = null 224 + adaptor = null 225 + started = false 226 + frameCount = 0L 227 + } 228 + 229 + @OptIn(ExperimentalForeignApi::class, NativeRuntimeApi::class) 230 + fun buildVideoFromImages( 231 + images: List<ImageBitmap>, 232 + outputSize: CValue<CGSize> = CGSizeMake(width.toDouble(), height.toDouble()) 233 + ): String { 234 + CGSizeMake(1.0, 1.0) 235 + // Create output file path 236 + val fileManager = NSFileManager.defaultManager 237 + 238 + // Delete existing file if needed 239 + if (fileManager.fileExistsAtPath(outputPath)) { 240 + val success = fileManager.removeItemAtPath(outputPath, null) 241 + if (!success) { 242 + throw IllegalStateException("Unable to delete existing file") 243 + } 244 + } 245 + 246 + // Create asset writer 247 + val videoWriter = AVAssetWriter(NSURL.fileURLWithPath(outputPath), AVFileTypeMPEG4, null) 248 + ?: throw IllegalStateException("AVAssetWriter error") 249 + 250 + // Configure video settings 251 + val width = outputSize.useContents { width.toInt() } 252 + val height = outputSize.useContents { height.toInt() } 253 + 254 + val outputSettings = mapOf( 255 + AVVideoCodecKey.toString() to AVVideoCodecH264, 256 + AVVideoWidthKey.toString() to width, 257 + AVVideoHeightKey.toString() to height, 258 + AVVideoCompressionPropertiesKey.toString() to mapOf( 259 + AVVideoAverageBitRateKey.toString() to 6000000, // 6 Mbps 260 + AVVideoProfileLevelKey.toString() to AVVideoProfileLevelH264HighAutoLevel 261 + ) 262 + ) 263 + val sourcePixelBufferAttributes = mapOf( 264 + CFBridgingRelease(kCVPixelBufferPixelFormatTypeKey) as String to kCVPixelFormatType_32BGRA, 265 + CFBridgingRelease(kCVPixelBufferWidthKey) as String to width, 266 + CFBridgingRelease(kCVPixelBufferHeightKey) as String to height, 267 + CFBridgingRelease(kCVPixelBufferCGImageCompatibilityKey) as String to true, 268 + CFBridgingRelease(kCVPixelBufferCGBitmapContextCompatibilityKey) as String to true 269 + ) 270 + val videoWriterInput = AVAssetWriterInput( 271 + mediaType = AVMediaTypeVideo, outputSettings = outputSettings as Map<Any?, *>? 272 + ) 273 + // Add input to writer and start session 274 + if (videoWriter.canAddInput(videoWriterInput)) { 275 + println("[VideoBuilder] Adding input to writer.") 276 + videoWriter.addInput(videoWriterInput) 277 + } else { 278 + println("[VideoBuilder] Cannot add input to writer. canAddInput returned false.") 279 + throw IllegalStateException("Cannot add input to writer") 280 + } 281 + println("[VideoBuilder] videoWriterInput.outputSettings: $outputSettings") 282 + println("[VideoBuilder] sourcePixelBufferAttributes: $sourcePixelBufferAttributes") 283 + println("[VideoBuilder] videoWriter status before startWriting: ${videoWriter.status}") 284 + // Create pixel buffer adaptor AFTER input is added 285 + val pixelBufferAdaptor = AVAssetWriterInputPixelBufferAdaptor( 286 + assetWriterInput = videoWriterInput, 287 + sourcePixelBufferAttributes = sourcePixelBufferAttributes as Map<Any?, *>? 288 + ) 289 + 290 + videoWriter.startWriting() 291 + println("[VideoBuilder] videoWriter status after startWriting: ${videoWriter.status}") 292 + videoWriter.startSessionAtSourceTime(CMTimeMake(0, 1)) 293 + println("[VideoBuilder] videoWriter status after startSessionAtSourceTime: ${videoWriter.status}") 294 + 295 + val fps: Int = 30 296 + val frameDuration = CMTimeMake(1, fps) 297 + var frameCount: Long = 0 298 + val remainingImages = images.toMutableList() 299 + // Only access pixelBufferPool after writing has started 300 + val pool = pixelBufferAdaptor.pixelBufferPool 301 + if (pool == null) { 302 + println("[VideoBuilder] pixelBufferAdaptor.pixelBufferPool is STILL null after startWriting/startSessionAtSourceTime!") 303 + println("[VideoBuilder] videoWriter status: ${videoWriter.status}") 304 + println("[VideoBuilder] videoWriterInput isReadyForMoreMediaData: ${videoWriterInput.readyForMoreMediaData}") 305 + throw IllegalStateException("Pixel buffer pool is null after startWriting/startSessionAtSourceTime") 306 + } 307 + 308 + // Process each image 309 + videoWriterInput.requestMediaDataWhenReadyOnQueue( 310 + dispatch_queue_create( 311 + "mediaInputQueue", null 312 + ) 313 + ) { 314 + var appendSucceeded = true 315 + 316 + while (remainingImages.isNotEmpty() && appendSucceeded) { 317 + if (videoWriterInput.readyForMoreMediaData) { 318 + val nextPhoto = remainingImages.removeAt(0) 319 + val lastFrameTime = CMTimeMake(frameCount, fps) 320 + val presentationTime = if (frameCount == 0L) lastFrameTime else CMTimeAdd( 321 + lastFrameTime, frameDuration 322 + ) 323 + 324 + memScoped { 325 + val pixelBufferPtr = alloc<CVPixelBufferRefVar>() 326 + val status = CVPixelBufferPoolCreatePixelBuffer( 327 + kCFAllocatorDefault, pool, pixelBufferPtr.ptr 328 + ) 329 + 330 + if (status == kCVReturnSuccess) { 331 + val pixelBuffer = pixelBufferPtr.value 332 + CVPixelBufferLockBaseAddress(pixelBuffer, 0u) 333 + 334 + val baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) 335 + val bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer) 336 + val colorSpace = CGColorSpaceCreateDeviceRGB() 337 + val bitmapInfo = CGImageAlphaInfo.kCGImageAlphaPremultipliedFirst.value 338 + 339 + val context = CGBitmapContextCreate( 340 + baseAddress, 341 + width.convert(), 342 + height.convert(), 343 + 8u, 344 + bytesPerRow.convert(), 345 + colorSpace, 346 + bitmapInfo 347 + ) 348 + 349 + if (context != null) { 350 + // Clear context 351 + CGContextClearRect( 352 + context, 353 + CGRectMake(0.0, 0.0, width.toDouble(), height.toDouble()) 354 + ) 355 + 356 + // Use ImageBitmap pixel data directly 357 + println("image processed") 358 + val pixelMap = nextPhoto.toPixelMap() 359 + val buffer = ensureFourChannelBuffer(pixelMap) 360 + val contextData = baseAddress?.reinterpret<ByteVar>() 361 + // Debug prints 362 + println("[VideoBuilder] ensureFourChannelBuffer: buffer.size=${buffer.size}, expected=${width * height * 4}") 363 + println("[VideoBuilder] CGContext bytesPerRow: $bytesPerRow, expected=${width * 4}") 364 + if (buffer.size != width * height * 4) { 365 + println("[VideoBuilder] ERROR: Buffer size does not match expected size for BGRA data!") 366 + } 367 + if (contextData != null) { 368 + // Copy row by row to handle bytesPerRow alignment 369 + val srcRowBytes = width * 4 370 + val dstRowBytes = bytesPerRow.toInt() 371 + for (row in 0 until height) { 372 + buffer.usePinned { pinned -> 373 + val srcOffset = row * srcRowBytes 374 + val dstOffset = row * dstRowBytes 375 + val dstPtr = contextData.plus(dstOffset) 376 + platform.posix.memcpy( 377 + dstPtr, 378 + pinned.addressOf(srcOffset), 379 + srcRowBytes.toULong() 380 + ) 381 + } 382 + } 383 + } 384 + CGContextRelease(context) 385 + } 386 + 387 + CGColorSpaceRelease(colorSpace) 388 + CVPixelBufferUnlockBaseAddress(pixelBuffer, 0u) 389 + 390 + // Append frame to video 391 + appendSucceeded = pixelBufferAdaptor.appendPixelBuffer( 392 + pixelBuffer, withPresentationTime = presentationTime 393 + ) 394 + CVPixelBufferRelease(pixelBuffer) 395 + } else { 396 + println("Failed to allocate pixel buffer: $status") 397 + appendSucceeded = false 398 + } 399 + } 400 + 401 + frameCount++ 402 + } 403 + } 404 + 405 + videoWriterInput.markAsFinished() 406 + videoWriter.finishWritingWithCompletionHandler { 407 + println("[VideoBuilder] Finished writing video!") 408 + } 409 + } 410 + 411 + // Wait for writing to finish (this is simplified) 412 + // In a real app, you would handle this asynchronously 413 + while (videoWriter.status == AVAssetWriterStatusWriting) { 414 + // Wait for completion 415 + } 416 + 417 + return outputPath 418 + } 419 + 420 + private fun ensureFourChannelBuffer(pixelMap: PixelMap): ByteArray { 421 + val width = pixelMap.width 422 + val height = pixelMap.height 423 + val buffer = pixelMap.buffer 424 + // Compose PixelMap's buffer is an IntArray (ARGB for each pixel) 425 + // If buffer size matches width*height, treat as 4-channel ARGB and convert to BGRA bytes 426 + if (buffer.size == width * height) { 427 + val dst = ByteArray(width * height * 4) 428 + for (i in 0 until width * height) { 429 + val argb = buffer[i] 430 + val a = (argb shr 24) and 0xFF 431 + val r = (argb shr 16) and 0xFF 432 + val g = (argb shr 8) and 0xFF 433 + val b = argb and 0xFF 434 + dst[i * 4] = b.toByte() // B 435 + dst[i * 4 + 1] = g.toByte() // G 436 + dst[i * 4 + 2] = r.toByte() // R 437 + dst[i * 4 + 3] = a.toByte() // A 438 + } 439 + return dst 440 + } 441 + // If buffer is not ARGB IntArray, fallback (should not happen) 442 + throw IllegalArgumentException("Unsupported pixel buffer format for ImageBitmap/PixelMap") 443 + } 444 + } 445 + 6 446 actual fun createVideoBuilder( 7 - outputPath: String, 8 - fps: Int, 9 - width: Int, 10 - height: Int 11 - ): VideoBuilder { 12 - TODO("Not yet implemented") 13 - } 447 + outputPath: String, fps: Int, width: Int, height: Int 448 + ): VideoBuilder = IOSVideoBuilder(outputPath, fps, width, height)
+4
posedetection/src/iosMain/kotlin/com/performancecoachlab/posedetection/recording/InputFrame.ios.kt
··· 22 22 actual fun drawSkeleton(skel: Skeleton): ImageBitmap { 23 23 return toImageBitmap().drawSkeleton(skel) 24 24 } 25 + 26 + actual fun drawAnalysisResults(analysisResults: AnalysisResult): ImageBitmap { 27 + return toImageBitmap().drawSkeleton(analysisResults.skeleton) 28 + } 25 29 } 26 30 27 31
+33 -3
sample/composeApp/src/commonMain/kotlin/com/nate/posedetection/App.kt
··· 26 26 import androidx.compose.ui.layout.ContentScale 27 27 import androidx.compose.ui.unit.dp 28 28 import androidx.compose.ui.unit.sp 29 + import chaintech.videoplayer.host.MediaPlayerHost 30 + import chaintech.videoplayer.model.ScreenResize 31 + import chaintech.videoplayer.model.VideoPlayerConfig 32 + import chaintech.videoplayer.ui.video.VideoPlayerComposable 29 33 import chaintech.videoplayer.util.RetrieveMediaDuration 30 34 import com.nate.posedetection.theme.AppTheme 31 35 import com.performancecoachlab.posedetection.camera.CameraView ··· 133 137 var isRecording by remember { mutableStateOf(false) } 134 138 val launcher = rememberShareFileLauncher() 135 139 val file = PlatformFile(FileKit.filesDir, "video.mp4") 140 + var savedPath:String? by remember { mutableStateOf(null) } 136 141 lateinit var size: Pair<Int,Int> 137 142 // Function to start recording 138 143 fun startRecording() { ··· 158 163 coroutineScope.launch { 159 164 try { 160 165 videoBuilder.value?.let { builder -> 161 - val videoPath = builder.finalize() 162 - launcher.launch(file) 166 + savedPath = builder.finalize() 167 + println(savedPath) 168 + //launcher.launch(file) 163 169 videoBuilder.value = null 164 170 } 165 171 } catch (e: Exception) { ··· 209 215 } 210 216 211 217 Box(modifier = modifier.fillMaxSize()) { 212 - if (bitmap != null) { 218 + if(savedPath != null){ 219 + val playerHost = remember { 220 + MediaPlayerHost( 221 + mediaUrl = savedPath?:"", 222 + isLooping = true, 223 + isPaused = false, 224 + isMuted = true, 225 + initialVideoFitMode = ScreenResize.FIT, ) 226 + } 227 + VideoPlayerComposable( 228 + modifier = Modifier.padding(16.dp).fillMaxSize(), 229 + playerHost = playerHost, 230 + playerConfig = VideoPlayerConfig( 231 + isSeekBarVisible = true, 232 + isDurationVisible = false, 233 + isFastForwardBackwardEnabled = false, 234 + isMuteControlEnabled = false, 235 + isSpeedControlEnabled = false, 236 + isScreenLockEnabled = false, 237 + isScreenResizeEnabled = false, 238 + isFullScreenEnabled = false, 239 + isPauseResumeEnabled = true, 240 + ) 241 + ) 242 + }else if (bitmap != null) { 213 243 bitmap?.also { 214 244 Image( 215 245 bitmap = it,