diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index b49892a..bc06b81 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -10,4 +10,10 @@ + + + diff --git a/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoder.kt b/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoder.kt index 982c0ad..54de4c4 100644 --- a/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoder.kt +++ b/android/src/main/kotlin/top/skychip/video_decode_plugin/VideoDecoder.kt @@ -17,6 +17,7 @@ import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock import java.util.Collections import java.util.concurrent.ConcurrentHashMap +import android.os.SystemClock /** * 视频解码器核心类。 @@ -41,6 +42,11 @@ import java.util.concurrent.ConcurrentHashMap * - inputFrameQueue: 输入帧队列,支持并发 * - running: 解码器运行状态 * - frameSeqSet: 用于去重的线程安全Set,防止重复帧入队 + * - outputFrameQueue: 解码输出缓冲区,容量为10帧 + * - renderThreadRunning: 渲染线程控制 + * - renderThread: 渲染线程 + * - mainHandler: 主线程Handler,用于安全切换onFrameAvailable到主线程 + * - renderFps: 渲染帧率(fps),可由外部控制,默认15 * * 主要方法: * - decodeFrame: 向解码器输入一帧数据 @@ -57,7 +63,7 @@ class VideoDecoder( companion object { private const val TAG = "VideoDecoder" private const val TIMEOUT_US = 10000L - private const val INPUT_BUFFER_QUEUE_CAPACITY = 20 + private const val INPUT_BUFFER_QUEUE_CAPACITY = 50 } private val surfaceTexture: SurfaceTexture = textureEntry.surfaceTexture() @@ -67,6 +73,25 @@ class VideoDecoder( private var running = true private val frameSeqSet = Collections.newSetFromMap(ConcurrentHashMap()) + // 解码输出缓冲区,容量为10帧 + private val outputFrameQueue = LinkedBlockingQueue(50) + // 渲染线程控制 + @Volatile private var renderThreadRunning = true + private var renderThread: Thread? = null + + // 主线程Handler,用于安全切换onFrameAvailable到主线程 + private val mainHandler = Handler(Looper.getMainLooper()) + + // 首帧原始时间戳(微秒),用于归零 + private var firstTimestampUs: Long? = null + // 上一帧归一化时间戳(微秒),用于误差兼容 + private var lastTimestampUs: Long = 0L + // 容忍误差区间(15fps一帧时长,单位微秒) + private val toleranceUs = 66000L + + // 渲染帧率(fps),可由外部控制,默认15 + @Volatile var renderFps: Int = 20 + private data class FrameData( val data: ByteArray, val frameType: Int, @@ -75,6 +100,14 @@ class VideoDecoder( val refIFrameSeq: Int? ) + // 解码后帧结构,显式携带时间戳(单位:微秒) + private data class DecodedFrame( + val codec: MediaCodec, + val bufferIndex: Int, + val info: MediaCodec.BufferInfo, + val timestampUs: Long // 帧时间戳,单位微秒 + ) + init { surfaceTexture.setDefaultBufferSize(width, height) val mime = when (codecType) { @@ -96,15 +129,16 @@ class VideoDecoder( inputBuffer.clear() inputBuffer.put(frame.data) val start = System.nanoTime() + val ptsUs = frame.timestamp * 1000L codec.queueInputBuffer( index, 0, frame.data.size, - 0, + ptsUs, if (frame.frameType == 0) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0 ) val end = System.nanoTime() - Log.d(TAG, "queueInputBuffer cost: ${end - start} ns, frameSeq=${frame.frameSeq}, type=${frame.frameType}") + Log.d(TAG, "queueInputBuffer cost: "+ (end - start) + " ns, frameSeq="+frame.frameSeq+", type="+frame.frameType+", ptsUs="+ptsUs) } else { codec.queueInputBuffer(index, 0, 0, 0, 0) } @@ -114,11 +148,16 @@ class VideoDecoder( } override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) { if (!running) return - val start = System.nanoTime() - codec.releaseOutputBuffer(index, true) - val end = System.nanoTime() - Log.d(TAG, "releaseOutputBuffer cost: ${end - start} ns") - onFrameAvailable() + // 将解码后帧放入输出缓冲区,由渲染线程处理 + val frame = DecodedFrame(codec, index, MediaCodec.BufferInfo().apply { + set(0, info.size, info.presentationTimeUs, info.flags) + }, info.presentationTimeUs) + if (!outputFrameQueue.offer(frame)) { + // 缓冲区满,丢弃最旧帧再插入 + outputFrameQueue.poll() + outputFrameQueue.offer(frame) + Log.w(TAG, "outputFrameQueue full, drop oldest frame") + } } override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) { Log.e(TAG, "MediaCodec error", e) @@ -128,12 +167,50 @@ class VideoDecoder( decoder.configure(format, surface, null, 0) decoder.start() mediaCodec = decoder + + // 启动渲染线程 + renderThreadRunning = true + renderThread = Thread { + while (renderThreadRunning) { + val frameIntervalMs = if (renderFps > 0) 1000L / renderFps else 66L + val loopStart = SystemClock.elapsedRealtime() + try { + val frame = outputFrameQueue.poll() + if (frame != null) { + Log.i(TAG, "[RenderThread] 定时渲染帧: frame.timestampUs=${frame.timestampUs}") + val start = System.nanoTime() + frame.codec.releaseOutputBuffer(frame.bufferIndex, true) + val end = System.nanoTime() + Log.d(TAG, "[RenderThread] releaseOutputBuffer cost: "+ (end - start) + " ns, frame.timestampUs=${frame.timestampUs}") + // 确保onFrameAvailable在主线程执行,避免FlutterJNI线程异常 + mainHandler.post { onFrameAvailable() } + } else { + Log.d(TAG, "[RenderThread] 定时渲染无帧可用") + } + } catch (e: Exception) { + Log.e(TAG, "[RenderThread] Exception", e) + } + val loopCost = SystemClock.elapsedRealtime() - loopStart + val sleepMs = frameIntervalMs - loopCost + if (sleepMs > 0) { + try { Thread.sleep(sleepMs) } catch (_: Exception) {} + } + } + // 清理剩余帧 + while (true) { + val frame = outputFrameQueue.poll() ?: break + try { + frame.codec.releaseOutputBuffer(frame.bufferIndex, false) + } catch (_: Exception) {} + } + } + renderThread?.start() } fun decodeFrame( frameData: ByteArray, frameType: Int, - timestamp: Long, + timestamp: Long, // 单位:毫秒,要求外部递增 frameSeq: Int, refIFrameSeq: Int? ): Boolean { @@ -141,12 +218,19 @@ class VideoDecoder( if (!frameSeqSet.add(frameSeq)) { return false } + // 直接使用外部传入的递增时间戳,无需归零和对齐 return inputFrameQueue.offer(FrameData(frameData, frameType, timestamp, frameSeq, refIFrameSeq), 50, TimeUnit.MILLISECONDS) } fun release() { running = false inputFrameQueue.clear() + // 停止渲染线程 + renderThreadRunning = false + try { + renderThread?.join(200) + } catch (_: Exception) {} + outputFrameQueue.clear() try { mediaCodec?.stop() mediaCodec?.release() diff --git a/example/android/gradle.properties b/example/android/gradle.properties index bc1ce5b..9be070b 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx4G android.useAndroidX=true android.enableJetifier=true -#org.gradle.java.home=C:/Users/liyi/other/jdk-17.0.1 \ No newline at end of file +org.gradle.java.home=C:/Users/liyi/other/jdk-17.0.1 \ No newline at end of file