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