feat:完成20FPS渲染需求
This commit is contained in:
parent
d837a1206b
commit
369d35cd2e
@ -10,4 +10,10 @@
|
|||||||
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<activity
|
||||||
|
android:name="top.skychip.video_decode_plugin.NativeVideoPlayerActivity"
|
||||||
|
android:exported="false"
|
||||||
|
android:theme="@android:style/Theme.Black.NoTitleBar.Fullscreen"/>
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import java.util.concurrent.locks.ReentrantLock
|
|||||||
import kotlin.concurrent.withLock
|
import kotlin.concurrent.withLock
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
import android.os.SystemClock
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 视频解码器核心类。
|
* 视频解码器核心类。
|
||||||
@ -41,6 +42,11 @@ import java.util.concurrent.ConcurrentHashMap
|
|||||||
* - inputFrameQueue: 输入帧队列,支持并发
|
* - inputFrameQueue: 输入帧队列,支持并发
|
||||||
* - running: 解码器运行状态
|
* - running: 解码器运行状态
|
||||||
* - frameSeqSet: 用于去重的线程安全Set,防止重复帧入队
|
* - frameSeqSet: 用于去重的线程安全Set,防止重复帧入队
|
||||||
|
* - outputFrameQueue: 解码输出缓冲区,容量为10帧
|
||||||
|
* - renderThreadRunning: 渲染线程控制
|
||||||
|
* - renderThread: 渲染线程
|
||||||
|
* - mainHandler: 主线程Handler,用于安全切换onFrameAvailable到主线程
|
||||||
|
* - renderFps: 渲染帧率(fps),可由外部控制,默认15
|
||||||
*
|
*
|
||||||
* 主要方法:
|
* 主要方法:
|
||||||
* - decodeFrame: 向解码器输入一帧数据
|
* - decodeFrame: 向解码器输入一帧数据
|
||||||
@ -57,7 +63,7 @@ class VideoDecoder(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "VideoDecoder"
|
private const val TAG = "VideoDecoder"
|
||||||
private const val TIMEOUT_US = 10000L
|
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()
|
private val surfaceTexture: SurfaceTexture = textureEntry.surfaceTexture()
|
||||||
@ -67,6 +73,25 @@ class VideoDecoder(
|
|||||||
private var running = true
|
private var running = true
|
||||||
private val frameSeqSet = Collections.newSetFromMap(ConcurrentHashMap<Int, Boolean>())
|
private val frameSeqSet = Collections.newSetFromMap(ConcurrentHashMap<Int, Boolean>())
|
||||||
|
|
||||||
|
// 解码输出缓冲区,容量为10帧
|
||||||
|
private val outputFrameQueue = LinkedBlockingQueue<DecodedFrame>(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(
|
private data class FrameData(
|
||||||
val data: ByteArray,
|
val data: ByteArray,
|
||||||
val frameType: Int,
|
val frameType: Int,
|
||||||
@ -75,6 +100,14 @@ class VideoDecoder(
|
|||||||
val refIFrameSeq: Int?
|
val refIFrameSeq: Int?
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 解码后帧结构,显式携带时间戳(单位:微秒)
|
||||||
|
private data class DecodedFrame(
|
||||||
|
val codec: MediaCodec,
|
||||||
|
val bufferIndex: Int,
|
||||||
|
val info: MediaCodec.BufferInfo,
|
||||||
|
val timestampUs: Long // 帧时间戳,单位微秒
|
||||||
|
)
|
||||||
|
|
||||||
init {
|
init {
|
||||||
surfaceTexture.setDefaultBufferSize(width, height)
|
surfaceTexture.setDefaultBufferSize(width, height)
|
||||||
val mime = when (codecType) {
|
val mime = when (codecType) {
|
||||||
@ -96,15 +129,16 @@ class VideoDecoder(
|
|||||||
inputBuffer.clear()
|
inputBuffer.clear()
|
||||||
inputBuffer.put(frame.data)
|
inputBuffer.put(frame.data)
|
||||||
val start = System.nanoTime()
|
val start = System.nanoTime()
|
||||||
|
val ptsUs = frame.timestamp * 1000L
|
||||||
codec.queueInputBuffer(
|
codec.queueInputBuffer(
|
||||||
index,
|
index,
|
||||||
0,
|
0,
|
||||||
frame.data.size,
|
frame.data.size,
|
||||||
0,
|
ptsUs,
|
||||||
if (frame.frameType == 0) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0
|
if (frame.frameType == 0) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0
|
||||||
)
|
)
|
||||||
val end = System.nanoTime()
|
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 {
|
} else {
|
||||||
codec.queueInputBuffer(index, 0, 0, 0, 0)
|
codec.queueInputBuffer(index, 0, 0, 0, 0)
|
||||||
}
|
}
|
||||||
@ -114,11 +148,16 @@ class VideoDecoder(
|
|||||||
}
|
}
|
||||||
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
|
override fun onOutputBufferAvailable(codec: MediaCodec, index: Int, info: MediaCodec.BufferInfo) {
|
||||||
if (!running) return
|
if (!running) return
|
||||||
val start = System.nanoTime()
|
// 将解码后帧放入输出缓冲区,由渲染线程处理
|
||||||
codec.releaseOutputBuffer(index, true)
|
val frame = DecodedFrame(codec, index, MediaCodec.BufferInfo().apply {
|
||||||
val end = System.nanoTime()
|
set(0, info.size, info.presentationTimeUs, info.flags)
|
||||||
Log.d(TAG, "releaseOutputBuffer cost: ${end - start} ns")
|
}, info.presentationTimeUs)
|
||||||
onFrameAvailable()
|
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) {
|
override fun onError(codec: MediaCodec, e: MediaCodec.CodecException) {
|
||||||
Log.e(TAG, "MediaCodec error", e)
|
Log.e(TAG, "MediaCodec error", e)
|
||||||
@ -128,12 +167,50 @@ class VideoDecoder(
|
|||||||
decoder.configure(format, surface, null, 0)
|
decoder.configure(format, surface, null, 0)
|
||||||
decoder.start()
|
decoder.start()
|
||||||
mediaCodec = decoder
|
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(
|
fun decodeFrame(
|
||||||
frameData: ByteArray,
|
frameData: ByteArray,
|
||||||
frameType: Int,
|
frameType: Int,
|
||||||
timestamp: Long,
|
timestamp: Long, // 单位:毫秒,要求外部递增
|
||||||
frameSeq: Int,
|
frameSeq: Int,
|
||||||
refIFrameSeq: Int?
|
refIFrameSeq: Int?
|
||||||
): Boolean {
|
): Boolean {
|
||||||
@ -141,12 +218,19 @@ class VideoDecoder(
|
|||||||
if (!frameSeqSet.add(frameSeq)) {
|
if (!frameSeqSet.add(frameSeq)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
// 直接使用外部传入的递增时间戳,无需归零和对齐
|
||||||
return inputFrameQueue.offer(FrameData(frameData, frameType, timestamp, frameSeq, refIFrameSeq), 50, TimeUnit.MILLISECONDS)
|
return inputFrameQueue.offer(FrameData(frameData, frameType, timestamp, frameSeq, refIFrameSeq), 50, TimeUnit.MILLISECONDS)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun release() {
|
fun release() {
|
||||||
running = false
|
running = false
|
||||||
inputFrameQueue.clear()
|
inputFrameQueue.clear()
|
||||||
|
// 停止渲染线程
|
||||||
|
renderThreadRunning = false
|
||||||
|
try {
|
||||||
|
renderThread?.join(200)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
outputFrameQueue.clear()
|
||||||
try {
|
try {
|
||||||
mediaCodec?.stop()
|
mediaCodec?.stop()
|
||||||
mediaCodec?.release()
|
mediaCodec?.release()
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
org.gradle.jvmargs=-Xmx4G
|
org.gradle.jvmargs=-Xmx4G
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
#org.gradle.java.home=C:/Users/liyi/other/jdk-17.0.1
|
org.gradle.java.home=C:/Users/liyi/other/jdk-17.0.1
|
||||||
Loading…
x
Reference in New Issue
Block a user