···
package com.performancecoachlab.posedetection.recording
+
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
+
import kotlinx.cinterop.ObjCObjectVar
+
import kotlinx.cinterop.alloc
+
import kotlinx.cinterop.memScoped
+
import kotlinx.cinterop.ptr
+
import kotlinx.cinterop.value
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import platform.AVFoundation.AVAsset
import platform.AVFoundation.AVAssetImageGenerator
+
import platform.AVFoundation.AVAssetReader
+
import platform.AVFoundation.AVAssetReaderTrackOutput
+
import platform.AVFoundation.AVAssetTrack
+
import platform.AVFoundation.AVMediaTypeVideo
import platform.AVFoundation.CMTimeValue
+
import platform.AVFoundation.tracksWithMediaType
import platform.AVFoundation.valueWithCMTime
import platform.CoreMedia.CMTimeMake
import platform.CoreMedia.CMTimeMakeWithSeconds
+
import platform.CoreMedia.CMTimeGetSeconds
+
import platform.CoreMedia.CMSampleBufferGetPresentationTimeStamp
+
import platform.CoreFoundation.CFRelease
+
import platform.Foundation.NSError
import platform.Foundation.NSURL
import platform.Foundation.NSValue
+
import platform.Foundation.NSFileManager
@OptIn(ExperimentalForeignApi::class)
actual suspend fun extractFrame(
···
+
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
+
actual suspend fun listVideoFrameTimestamps(
+
): List<Long> = withContext(Dispatchers.IO) {
+
val out = mutableListOf<Long>()
+
val resolvedUrl = createUrl(url = videoPath)
+
println("[iOS] listVideoFrameTimestamps using URL: ${'$'}{resolvedUrl.absoluteString}")
+
// AVAssetReader requires a local file URL. Bail out early for remote URLs.
+
if (!resolvedUrl.isFileURL()) {
+
println("AVAssetReader requires a local file URL; got: ${'$'}{resolvedUrl.scheme}:// ...")
+
// Verify the file actually exists at path
+
val path = resolvedUrl.path
+
if (path == null || !NSFileManager.defaultManager.fileExistsAtPath(path)) {
+
println("File does not exist at path: ${'$'}path")
+
val asset = AVAsset.assetWithURL(resolvedUrl)
+
// Use synchronous approach - get tracks directly using tracksWithMediaType
+
val videoTracks = asset.tracksWithMediaType(AVMediaTypeVideo)
+
if (videoTracks.isEmpty()) {
+
println("No video tracks found in video: ${'$'}videoPath")
+
val videoTrack = videoTracks.first() as AVAssetTrack
+
println("Found video track: ${'$'}{videoTrack}")
+
val errorPtr = alloc<ObjCObjectVar<NSError?>>()
+
val reader = AVAssetReader.assetReaderWithAsset(asset, error = errorPtr.ptr)
+
val err = errorPtr.value
+
println("Failed to create AVAssetReader: ${'$'}{err?.localizedDescription}")
+
val trackOutput = AVAssetReaderTrackOutput.assetReaderTrackOutputWithTrack(
+
if (!reader.canAddOutput(trackOutput)) {
+
println("Cannot add track output to reader")
+
reader.addOutput(trackOutput)
+
if (!reader.startReading()) {
+
println("Failed to start reading: ${'$'}{reader.error?.localizedDescription}")
+
println("Started reading video frames...")
+
val sampleBuffer = trackOutput.copyNextSampleBuffer()
+
if (sampleBuffer == null) {
+
val presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
+
val timeInSeconds = CMTimeGetSeconds(presentationTime)
+
val timeInMillis = (timeInSeconds * 1000.0).toLong()
+
// Release the sample buffer
+
CFRelease(sampleBuffer)
+
if (out.size >= Int.MAX_VALUE) break
+
println("Extracted ${'$'}frameCount frame timestamps")
+
} catch (e: Exception) {
+
println("Exception in listVideoFrameTimestamps: ${'$'}{e.message}")
+
println("Returning ${'$'}{out.size} timestamps")
fun createUrl(url: String): NSURL {
+
val trimmed = url.trim()
+
// Remote URLs: return as-is
+
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
+
return requireNotNull(NSURL.URLWithString(trimmed)) { "Invalid http(s) URL: ${'$'}trimmed" }
+
// Well-formed file URLs
+
if (trimmed.startsWith("file://")) {
+
// Normalize to a proper file URL via its path component to avoid odd host parts
+
NSURL.URLWithString(trimmed)?.let { fileUrl ->
+
fileUrl.path?.let { path ->
+
return NSURL.fileURLWithPath(path)
+
// Malformed file: prefix (e.g., "file:/var/..."), coerce to path
+
if (trimmed.startsWith("file:")) {
+
var path = trimmed.removePrefix("file:")
+
// Remove duplicate leading slashes
+
while (path.startsWith("//")) path = path.removePrefix("//")
+
if (!path.startsWith("/")) path = "/${'$'}path"
+
return NSURL.fileURLWithPath(path)
+
// Otherwise treat as a filesystem path
+
return NSURL.fileURLWithPath(trimmed)
actual object VideoExtractionContext