···
package com.performancecoachlab.posedetection.recording
3
-
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
5
-
import kotlinx.cinterop.ObjCObjectVar
6
-
import kotlinx.cinterop.alloc
7
-
import kotlinx.cinterop.memScoped
8
-
import kotlinx.cinterop.ptr
9
-
import kotlinx.cinterop.value
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import kotlinx.coroutines.withContext
import platform.AVFoundation.AVAsset
import platform.AVFoundation.AVAssetImageGenerator
15
-
import platform.AVFoundation.AVAssetReader
16
-
import platform.AVFoundation.AVAssetReaderTrackOutput
17
-
import platform.AVFoundation.AVAssetTrack
18
-
import platform.AVFoundation.AVMediaTypeVideo
import platform.AVFoundation.CMTimeValue
20
-
import platform.AVFoundation.tracksWithMediaType
import platform.AVFoundation.valueWithCMTime
import platform.CoreMedia.CMTimeMake
import platform.CoreMedia.CMTimeMakeWithSeconds
24
-
import platform.CoreMedia.CMTimeGetSeconds
25
-
import platform.CoreMedia.CMSampleBufferGetPresentationTimeStamp
26
-
import platform.CoreFoundation.CFRelease
27
-
import platform.Foundation.NSError
import platform.Foundation.NSURL
import platform.Foundation.NSValue
30
-
import platform.Foundation.NSFileManager
@OptIn(ExperimentalForeignApi::class)
actual suspend fun extractFrame(
···
58
-
@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
59
-
actual suspend fun listVideoFrameTimestamps(
61
-
): List<Long> = withContext(Dispatchers.IO) {
62
-
val out = mutableListOf<Long>()
65
-
val resolvedUrl = createUrl(url = videoPath)
66
-
println("[iOS] listVideoFrameTimestamps using URL: ${'$'}{resolvedUrl.absoluteString}")
68
-
// AVAssetReader requires a local file URL. Bail out early for remote URLs.
69
-
if (!resolvedUrl.isFileURL()) {
70
-
println("AVAssetReader requires a local file URL; got: ${'$'}{resolvedUrl.scheme}:// ...")
71
-
return@withContext out
74
-
// Verify the file actually exists at path
75
-
val path = resolvedUrl.path
76
-
if (path == null || !NSFileManager.defaultManager.fileExistsAtPath(path)) {
77
-
println("File does not exist at path: ${'$'}path")
78
-
return@withContext out
81
-
val asset = AVAsset.assetWithURL(resolvedUrl)
83
-
// Use synchronous approach - get tracks directly using tracksWithMediaType
84
-
val videoTracks = asset.tracksWithMediaType(AVMediaTypeVideo)
86
-
if (videoTracks.isEmpty()) {
87
-
println("No video tracks found in video: ${'$'}videoPath")
88
-
return@withContext out
91
-
val videoTrack = videoTracks.first() as AVAssetTrack
92
-
println("Found video track: ${'$'}{videoTrack}")
95
-
val errorPtr = alloc<ObjCObjectVar<NSError?>>()
96
-
val reader = AVAssetReader.assetReaderWithAsset(asset, error = errorPtr.ptr)
98
-
if (reader == null) {
99
-
val err = errorPtr.value
100
-
println("Failed to create AVAssetReader: ${'$'}{err?.localizedDescription}")
101
-
return@withContext out
104
-
val trackOutput = AVAssetReaderTrackOutput.assetReaderTrackOutputWithTrack(
105
-
track = videoTrack,
106
-
outputSettings = null
109
-
if (!reader.canAddOutput(trackOutput)) {
110
-
println("Cannot add track output to reader")
111
-
return@withContext out
114
-
reader.addOutput(trackOutput)
116
-
if (!reader.startReading()) {
117
-
println("Failed to start reading: ${'$'}{reader.error?.localizedDescription}")
118
-
return@withContext out
121
-
println("Started reading video frames...")
124
-
val sampleBuffer = trackOutput.copyNextSampleBuffer()
125
-
if (sampleBuffer == null) {
129
-
val presentationTime = CMSampleBufferGetPresentationTimeStamp(sampleBuffer)
130
-
val timeInSeconds = CMTimeGetSeconds(presentationTime)
131
-
val timeInMillis = (timeInSeconds * 1000.0).toLong()
133
-
out.add(timeInMillis)
136
-
// Release the sample buffer
137
-
CFRelease(sampleBuffer)
139
-
if (out.size >= Int.MAX_VALUE) break
142
-
println("Extracted ${'$'}frameCount frame timestamps")
145
-
} catch (e: Exception) {
146
-
println("Exception in listVideoFrameTimestamps: ${'$'}{e.message}")
147
-
e.printStackTrace()
150
-
println("Returning ${'$'}{out.size} timestamps")
fun createUrl(url: String): NSURL {
43
+
return NSURL.fileURLWithPath(url)
155
-
val trimmed = url.trim()
157
-
// Remote URLs: return as-is
158
-
if (trimmed.startsWith("http://") || trimmed.startsWith("https://")) {
159
-
return requireNotNull(NSURL.URLWithString(trimmed)) { "Invalid http(s) URL: ${'$'}trimmed" }
162
-
// Well-formed file URLs
163
-
if (trimmed.startsWith("file://")) {
164
-
// Normalize to a proper file URL via its path component to avoid odd host parts
165
-
NSURL.URLWithString(trimmed)?.let { fileUrl ->
166
-
fileUrl.path?.let { path ->
167
-
return NSURL.fileURLWithPath(path)
173
-
// Malformed file: prefix (e.g., "file:/var/..."), coerce to path
174
-
if (trimmed.startsWith("file:")) {
175
-
var path = trimmed.removePrefix("file:")
176
-
// Remove duplicate leading slashes
177
-
while (path.startsWith("//")) path = path.removePrefix("//")
178
-
if (!path.startsWith("/")) path = "/${'$'}path"
179
-
return NSURL.fileURLWithPath(path)
182
-
// Otherwise treat as a filesystem path
183
-
return NSURL.fileURLWithPath(trimmed)
actual object VideoExtractionContext