This repository has no description
0

Configure Feed

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

fix: race condition in VideoFrameDecoder causing SIGSEGV and IllegalStateException

Serialize access to the cached MediaCodec decoder with a Mutex to prevent
concurrent access from multiple IO threads. Add error recovery so a broken
decoder is released and recreated on the next call.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+66 -48
+1 -1
posedetection/build.gradle.kts
··· 4 4 5 5 mavenPublishing { 6 6 publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL) 7 - coordinates("com.performancecoachlab.posedetection", "posedetection-compose", "4.9.0") 7 + coordinates("com.performancecoachlab.posedetection", "posedetection-compose", "4.9.2") 8 8 9 9 pom { 10 10 name.set("Pose Detection")
+65 -47
posedetection/src/androidMain/kotlin/com/performancecoachlab/posedetection/recording/VideoUtils.android.kt
··· 11 11 import androidx.core.net.toUri 12 12 import co.touchlab.kermit.Logger 13 13 import kotlinx.coroutines.Dispatchers 14 + import kotlinx.coroutines.sync.Mutex 15 + import kotlinx.coroutines.sync.withLock 14 16 import kotlinx.coroutines.withContext 17 + import kotlin.coroutines.cancellation.CancellationException 15 18 16 19 /** 17 20 * Sequential video decoder using MediaCodec. Opens the video once and decodes ··· 81 84 val timeoutUs = 10_000L 82 85 val info = MediaCodec.BufferInfo() 83 86 84 - while (!outputEos) { 85 - // Feed input packets. 86 - if (!inputEos) { 87 - val inIdx = decoder.dequeueInputBuffer(0) 88 - if (inIdx >= 0) { 89 - val buf = decoder.getInputBuffer(inIdx)!! 90 - val size = ext.readSampleData(buf, 0) 91 - if (size < 0) { 92 - decoder.queueInputBuffer( 93 - inIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM 94 - ) 95 - inputEos = true 96 - } else { 97 - decoder.queueInputBuffer(inIdx, 0, size, ext.sampleTime, 0) 98 - ext.advance() 87 + try { 88 + while (!outputEos) { 89 + // Feed input packets. 90 + if (!inputEos) { 91 + val inIdx = decoder.dequeueInputBuffer(0) 92 + if (inIdx >= 0) { 93 + val buf = decoder.getInputBuffer(inIdx)!! 94 + val size = ext.readSampleData(buf, 0) 95 + if (size < 0) { 96 + decoder.queueInputBuffer( 97 + inIdx, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM 98 + ) 99 + inputEos = true 100 + } else { 101 + decoder.queueInputBuffer(inIdx, 0, size, ext.sampleTime, 0) 102 + ext.advance() 103 + } 99 104 } 100 105 } 101 - } 102 106 103 - // Drain output. 104 - val outIdx = decoder.dequeueOutputBuffer(info, timeoutUs) 105 - if (outIdx >= 0) { 106 - val ptsMs = info.presentationTimeUs / 1000L 107 + // Drain output. 108 + val outIdx = decoder.dequeueOutputBuffer(info, timeoutUs) 109 + if (outIdx >= 0) { 110 + val ptsMs = info.presentationTimeUs / 1000L 107 111 108 - if (info.size > 0) { 109 - val image = decoder.getOutputImage(outIdx) 110 - if (image != null) { 111 - // Recycle previous bitmap to limit memory. 112 - if (lastPtsMs != ptsMs) { 113 - // Don't recycle if we'd lose the only copy. 112 + if (info.size > 0) { 113 + val image = decoder.getOutputImage(outIdx) 114 + if (image != null) { 115 + val raw = yuvImageToBitmap(image) 116 + image.close() 117 + lastBitmap = applyRotation(raw) 118 + lastPtsMs = ptsMs 114 119 } 115 - val raw = yuvImageToBitmap(image) 116 - image.close() 117 - lastBitmap = applyRotation(raw) 118 - lastPtsMs = ptsMs 119 120 } 120 - } 121 121 122 - val eos = info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0 123 - decoder.releaseOutputBuffer(outIdx, false) 122 + val eos = info.flags and MediaCodec.BUFFER_FLAG_END_OF_STREAM != 0 123 + decoder.releaseOutputBuffer(outIdx, false) 124 124 125 - if (eos) { 126 - outputEos = true 127 - return lastBitmap 128 - } 125 + if (eos) { 126 + outputEos = true 127 + return lastBitmap 128 + } 129 129 130 - // We've reached or passed the target — return this frame. 131 - if (ptsMs >= targetMs) { 132 - return lastBitmap 130 + // We've reached or passed the target — return this frame. 131 + if (ptsMs >= targetMs) { 132 + return lastBitmap 133 + } 133 134 } 134 135 } 136 + } catch (e: Exception) { 137 + Logger.w { "decodeUpTo failed at targetMs=$targetMs: ${e.message}" } 138 + release() 139 + throw e 135 140 } 136 141 137 142 return lastBitmap ··· 198 203 // Cached decoder — persists across extractFrame() calls for the same video. 199 204 private var cachedDecoder: VideoFrameDecoder? = null 200 205 private var cachedDecoderPath: String? = null 206 + private val decoderMutex = Mutex() 201 207 202 208 actual suspend fun extractFrame( 203 209 videoPath: String, frameTimestamp: Long 204 210 ): InputFrame? = withContext(Dispatchers.IO) { 205 - if (cachedDecoderPath != videoPath) { 206 - cachedDecoder?.release() 207 - cachedDecoder = VideoFrameDecoder(videoPath) 208 - cachedDecoderPath = videoPath 209 - } 210 - cachedDecoder!!.decodeUpTo(frameTimestamp)?.let { 211 - InputFrame(bitmap = it, timestamp = frameTimestamp) 211 + decoderMutex.withLock { 212 + try { 213 + if (cachedDecoderPath != videoPath) { 214 + cachedDecoder?.release() 215 + cachedDecoder = VideoFrameDecoder(videoPath) 216 + cachedDecoderPath = videoPath 217 + } 218 + cachedDecoder!!.decodeUpTo(frameTimestamp)?.let { 219 + InputFrame(bitmap = it, timestamp = frameTimestamp) 220 + } 221 + } catch (ce: CancellationException) { 222 + throw ce 223 + } catch (e: Exception) { 224 + Logger.w { "extractFrame failed, releasing decoder: ${e.message}" } 225 + cachedDecoder?.release() 226 + cachedDecoder = null 227 + cachedDecoderPath = null 228 + null 229 + } 212 230 } 213 231 } 214 232