feat:v1版本实现

This commit is contained in:
liyi 2025-04-21 15:11:23 +08:00
parent 3381ef72fc
commit f8bfa2b229
6 changed files with 1191 additions and 1344 deletions

252
README.md
View File

@ -1,150 +1,160 @@
# video_decode_plugin
# Video Decode Plugin
一个高性能的 Flutter 插件,用于在 Android 原生层解码 H.264 裸流数据,并支持两种渲染模式
基于MediaCodec/VideoToolbox的跨平台H.264/H.265视频解码Flutter插件专为低延迟实时视频流解码设计
## 功能特点
[![pub package](https://img.shields.io/pub/v/video_decode_plugin.svg)](https://pub.dev/packages/video_decode_plugin)
- 支持 H.264 Annex B 格式裸流解码(含 NALU 单元)
- 使用 Android MediaCodec 硬解码,提供高性能解码能力
- 支持两种渲染模式:
- Flutter 纹理渲染:将解码后的帧通过 Flutter Texture 传递到 Flutter UI
- 原生 SurfaceView 渲染:在原生 Android 层直接渲染
- 提供完善的配置管理和性能监控
- 支持动态丢帧策略,优化内存使用
- 适配低端设备的性能优化措施
## 特性
- 🔄 基于原生解码器的高性能视频解码Android使用MediaCodeciOS使用VideoToolbox
- 🖼️ 支持H.264和H.265HEVC视频格式
- ⏱️ 低延迟解码,适用于实时视频流应用
- 📱 跨平台支持Android和iOS
- 🔧 高度可配置的解码参数
- 📊 详细的解码统计和诊断信息
- 💡 支持I帧和P帧的单独传入和处理
- 🎞️ 使用Flutter Texture进行高效渲染
## 安装
`pubspec.yaml` 文件中添加依赖:
```yaml
dependencies:
video_decode_plugin: ^0.0.1
video_decode_plugin: ^1.0.0
```
## 使用方法
## 快速开始
### 基本用法
### 初始化解码器
```dart
import 'package:video_decode_plugin/video_decode_plugin.dart';
// 创建解码器Flutter 渲染模式)
final decoder = H264Decoder(renderMode: RenderMode.flutter);
// 初始化解码器
await decoder.init(
const H264DecoderConfig(
bufferSize: 10,
maxWidth: 1280,
maxHeight: 720,
useDropFrameStrategy: true,
debugMode: true,
),
// 创建解码器配置
final config = VideoDecoderConfig(
width: 640, // 视频宽度
height: 480, // 视频高度
codecType: CodecType.h264, // 编解码类型h264或h265
frameRate: 30, // 目标帧率(可选)
bufferSize: 30, // 缓冲区大小
isDebug: true, // 是否启用详细日志
);
// 开始解码
await decoder.start();
// 初始化解码器获取纹理ID
final textureId = await VideoDecodePlugin.initDecoder(config);
// 输入 H.264 数据
await decoder.feedData(h264Data);
// 暂停解码
await decoder.pause();
// 恢复解码
await decoder.resume();
// 释放资源
await decoder.release();
```
### 渲染视图
#### Flutter 渲染模式
```dart
// 使用 Flutter 渲染模式显示视频
H264VideoPlayerWidget(
decoder: decoder,
width: 640,
height: 360,
backgroundColor: Colors.black,
)
```
#### 原生渲染模式
```dart
// 使用原生渲染模式显示视频
const H264NativePlayerWidget(
width: 640,
height: 360,
backgroundColor: Colors.black,
)
```
### 监听事件
```dart
// 订阅解码器事件
decoder.eventStream.listen((event) {
switch (event.type) {
case H264DecoderEventType.frameAvailable:
// 新帧可用
break;
case H264DecoderEventType.stats:
// 性能统计信息
final stats = event.data as Map<String, dynamic>;
print('总帧数: ${stats['totalFrames']}');
print('丢弃帧数: ${stats['droppedFrames']}');
print('缓冲区使用: ${stats['bufferUsage']}');
print('解码耗时: ${stats['lastDecodingTimeMs']}ms');
break;
case H264DecoderEventType.error:
// 解码错误
break;
}
// 设置帧回调
VideoDecodePlugin.setFrameCallback((textureId) {
// 当新帧可用时被调用
setState(() {
// 更新UI
});
});
```
## 配置选项
### 渲染视频
`H264DecoderConfig` 类提供以下配置选项:
| 参数 | 类型 | 默认值 | 说明 |
|-----|------|-------|-----|
| bufferSize | int | 5 | 缓冲区大小(帧数) |
| maxWidth | int | 1280 | 最大解码宽度 |
| maxHeight | int | 720 | 最大解码高度 |
| useDropFrameStrategy | bool | true | 是否启用丢帧策略 |
| debugMode | bool | false | 是否启用调试模式 |
## 性能优化
本插件提供多项性能优化措施:
1. **动态丢帧策略**:缓冲区满时,优先保留 I 帧,丢弃 P 帧,确保解码连续性。
2. **零拷贝传输**:使用 Surface 和 SurfaceTexture 直接渲染,避免内存拷贝。
3. **异步处理**:解码和渲染在独立线程进行,不阻塞主线程。
4. **低端设备适配**:可设置最大解码分辨率,避免低端设备性能问题。
## 示例应用
本项目自带一个完整的示例应用,演示如何使用这个插件播放 H.264 视频流。
运行示例:
```shell
cd example
flutter run
```dart
// 使用Flutter的Texture组件显示视频
Texture(
textureId: textureId,
filterQuality: FilterQuality.low,
)
```
### 解码视频帧
```dart
// 解码I帧
await VideoDecodePlugin.decodeFrame(
frameData, // Uint8List类型的H.264/H.265帧数据
FrameType.iFrame
);
// 解码P帧
await VideoDecodePlugin.decodeFrame(
frameData, // Uint8List类型的H.264/H.265帧数据
FrameType.pFrame
);
```
### 获取解码统计信息
```dart
final stats = await VideoDecodePlugin.getDecoderStats(textureId);
print('已渲染帧数: ${stats['renderedFrames']}');
print('丢弃帧数: ${stats['droppedFrames']}');
```
### 释放资源
```dart
await VideoDecodePlugin.releaseDecoder();
```
## 高级用法
### 多实例支持
插件支持同时创建和管理多个解码器实例:
```dart
// 创建第一个解码器
final textureId1 = await VideoDecodePlugin.createDecoder(config1);
// 创建第二个解码器
final textureId2 = await VideoDecodePlugin.createDecoder(config2);
// 为特定纹理ID设置回调
VideoDecodePlugin.setFrameCallbackForTexture(textureId1, (id) {
// 处理第一个解码器的帧
});
// 为特定纹理ID解码帧
await VideoDecodePlugin.decodeFrameForTexture(textureId2, frameData, frameType);
// 释放特定解码器
await VideoDecodePlugin.releaseDecoderForTexture(textureId1);
```
### 优化I帧和SPS/PPS处理
对于H.264视频流,建议按照以下顺序处理帧:
1. 首先发送SPS序列参数集NAL类型7
2. 其次发送PPS图像参数集NAL类型8
3. 然后发送IDR帧即I帧NAL类型5
4. 最后发送P帧NAL类型1
```dart
// 发送SPS和PPS数据
await VideoDecodePlugin.decodeFrame(spsData, FrameType.iFrame);
await VideoDecodePlugin.decodeFrame(ppsData, FrameType.iFrame);
// 发送IDR帧
await VideoDecodePlugin.decodeFrame(idrData, FrameType.iFrame);
// 发送P帧
await VideoDecodePlugin.decodeFrame(pFrameData, FrameType.pFrame);
```
## 完整示例
请参考示例应用,了解如何:
- 从文件或网络流加载H.264视频
- 正确解析和处理NAL单元
- 高效地解码和渲染视频帧
- 监控解码性能并进行故障排除
## 支持
- Android 5.0 (API级别21)及以上
- iOS 11.0及以上
## 注意事项
- 目前仅支持 Android 平台
- 需要确保输入的 H.264 数据是有效的 Annex B 格式(含 NALU 开始码)
- 建议在实际项目中根据设备性能动态调整解码配置
- 视频分辨率受设备硬件限制,较旧设备可能无法支持高分辨率视频
- 硬件解码器可能在某些设备上不可用,插件会自动回退到软件解码
- 对于最佳性能,建议在实际硬件设备上测试,而不仅仅是模拟器
## 许可证

View File

@ -136,12 +136,17 @@ class VideoDecodePlugin : FlutterPlugin, MethodCallHandler {
// 通知Flutter刷新纹理
runOnMainThread {
try {
Log.d(TAG, "发送帧可用通知给Flutter纹理ID: $textureId")
channel.invokeMethod("onFrameAvailable", mapOf("textureId" to textureId))
} catch (e: Exception) {
Log.e(TAG, "通知Flutter更新纹理失败", e)
}
}
}
override fun onError(error: String) {
Log.e(TAG, "解码器错误: $error")
}
}
// 保存解码器

View File

@ -4,23 +4,16 @@ import android.content.Context
import android.graphics.SurfaceTexture
import android.media.MediaCodec
import android.media.MediaFormat
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.view.Surface
import io.flutter.view.TextureRegistry
import java.nio.ByteBuffer
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.atomic.AtomicLong
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.math.max
/**
* 视频解码器
* 简化版视频解码器
* 负责解码H264/H265视频数据并将其渲染到Surface上
*/
class VideoDecoder(
@ -30,52 +23,35 @@ class VideoDecoder(
) {
companion object {
private const val TAG = "VideoDecoder"
private const val TIMEOUT_US = 10000L // 10ms
private const val MAX_FRAME_AGE_MS = 100L // 丢弃过旧的帧
private const val TIMEOUT_US = 10000L // 使用更短的超时时间10毫秒
// Mime types
private const val MIME_H264 = "video/avc"
private const val MIME_H265 = "video/hevc"
// H264相关常量
private const val NAL_UNIT_TYPE_SPS = 7
private const val NAL_UNIT_TYPE_PPS = 8
private const val NAL_UNIT_TYPE_IDR = 5
private const val NAL_UNIT_TYPE_NON_IDR = 1 // P帧
// 最大允许连续P帧数
private const val MAX_CONSECUTIVE_P_FRAMES = 30
// 异步模式参数
private const val LOW_LATENCY_MODE = true
private const val OPERATING_RATE = 90 // 解码速率提高到90FPS
}
// 回调接口
interface DecoderCallback {
fun onFrameAvailable()
fun onError(error: String)
}
// 回调实例
var callback: DecoderCallback? = null
// 帧类型枚举
enum class FrameType {
I_FRAME, P_FRAME, UNKNOWN
}
// 帧结构体
private data class Frame(
val data: ByteArray,
val type: FrameType,
val timestamp: Long = System.currentTimeMillis()
) {
// 检查帧是否过期
fun isExpired(): Boolean {
return System.currentTimeMillis() - timestamp > MAX_FRAME_AGE_MS
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as Frame
if (!data.contentEquals(other.data)) return false
if (type != other.type) return false
return true
}
override fun hashCode(): Int {
var result = data.contentHashCode()
result = 31 * result + type.hashCode()
return result
}
}
// SurfaceTexture 和 Surface 用于显示解码后的帧
private val surfaceTexture: SurfaceTexture = textureEntry.surfaceTexture()
private val surface: Surface = Surface(surfaceTexture)
@ -83,254 +59,326 @@ class VideoDecoder(
// MediaCodec 解码器
private var mediaCodec: MediaCodec? = null
// 待解码的帧队列
private val frameQueue = LinkedBlockingQueue<Frame>(config.bufferSize)
// 解码线程
private var decodeThread: Thread? = null
// 解码状态
private val isRunning = AtomicBoolean(false)
private val isDecoderConfigured = AtomicBoolean(false)
// 当前解码的帧计数
// 帧计数
private var frameCount = 0
// 解码流状态跟踪
private val hasReceivedIFrame = AtomicBoolean(false)
private val lastIFrameTimestamp = AtomicLong(0)
private var droppedFrameCount = 0
private var renderedFrameCount = 0
private var droppedFrameCount = 0
// 跟踪I帧状态
private var hasSentSPS = false
private var hasSentPPS = false
private var hasSentIDR = false
// 跟踪上一个关键帧时间
private var lastIFrameTimeMs = 0L
// 连续P帧计数
private var consecutivePFrameCount = 0
// 用于避免重复处理相同SPS/PPS的缓存
private var lastSPSHash: Int? = null
private var lastPPSHash: Int? = null
// 最后有效输出时间戳,用于检测解码器卡住的情况
private var lastOutputTimeMs = 0L
// 主线程Handler用于在主线程上更新纹理
private val mainHandler = Handler(Looper.getMainLooper())
// 初始化解码器
// 日志控制
private var logVerbose = false
private var frameLogThreshold = 30 // 每30帧输出一次详细日志
// 解码器初始化
init {
try {
logVerbose = config.isDebug
// 捕获所有日志
Log.d(TAG, "初始化解码器: ${config.width}x${config.height}, 编码: ${config.codecType}")
// 在主线程上设置SurfaceTexture
mainHandler.post {
try {
// 设置SurfaceTexture的默认缓冲区大小
surfaceTexture.setDefaultBufferSize(config.width, config.height)
Log.d(TAG, "SurfaceTexture缓冲区大小设置为: ${config.width}x${config.height}")
// 初始化解码器
setupDecoder()
startDecodeThread()
// 输出解码器状态
mediaCodec?.let {
Log.d(TAG, "解码器已启动: ${it.codecInfo.name}")
}
// 延迟100ms通知一个空帧确保Surface已准备好
mainHandler.postDelayed({
Log.d(TAG, "发送初始化完成通知")
callback?.onFrameAvailable()
}, 100)
} catch (e: Exception) {
Log.e(TAG, "初始化解码器失败", e)
release()
// 设置SurfaceTexture的默认缓冲区大小
surfaceTexture.setDefaultBufferSize(config.width, config.height)
// 初始化解码器
if (setupDecoder()) {
isRunning.set(true)
// 通知初始帧可用让Flutter创建Texture View
Log.d(TAG, "解码器初始化成功,发送初始帧通知")
mainHandler.post {
notifyFrameAvailable()
}
} else {
Log.e(TAG, "解码器初始化失败")
callback?.onError("解码器初始化失败")
}
} catch (e: Exception) {
Log.e(TAG, "创建解码器实例失败", e)
callback?.onError("创建解码器实例失败: ${e.message}")
release()
throw e
}
}
/**
* 设置解码器
*/
private fun setupDecoder() {
private fun setupDecoder(): Boolean {
try {
Log.d(TAG, "开始设置解码器")
// 确定MIME类型
val mimeType = if (config.codecType.lowercase() == "h265") {
MediaFormat.MIMETYPE_VIDEO_HEVC
} else {
MediaFormat.MIMETYPE_VIDEO_AVC // 默认H.264
// 选择适合的MIME类型
val mime = when (config.codecType) {
"h264" -> MIME_H264
"h265" -> MIME_H265
else -> MIME_H264 // 默认使用H.264
}
// 创建格式
val format = MediaFormat.createVideoFormat(mimeType, config.width, config.height)
// 创建MediaFormat
val format = MediaFormat.createVideoFormat(mime, config.width, config.height)
// 配置基本参数
// 配置参数
format.setInteger(MediaFormat.KEY_MAX_INPUT_SIZE, config.width * config.height)
// 创建解码器
val decoderInstance = if (config.enableHardwareDecoder) {
try {
MediaCodec.createDecoderByType(mimeType)
} catch (e: Exception) {
Log.w(TAG, "硬件解码器创建失败,尝试使用软件解码器", e)
MediaCodec.createDecoderByType(mimeType)
}
} else {
MediaCodec.createDecoderByType(mimeType)
// 如果指定了帧率,设置帧率
config.frameRate?.let { fps ->
format.setInteger(MediaFormat.KEY_FRAME_RATE, fps)
}
// 创建解码器实例
val decoder = MediaCodec.createDecoderByType(mime)
// 配置解码器
decoderInstance.configure(format, surface, null, 0)
decoderInstance.start()
decoder.configure(format, surface, null, 0)
decoder.start()
// 保存解码器实例
mediaCodec = decoderInstance
// 标记为运行中
isRunning.set(true)
Log.d(TAG, "解码器设置完成: ${decoderInstance.codecInfo.name}")
mediaCodec = decoder
isDecoderConfigured.set(true)
Log.d(TAG, "解码器设置完成: ${decoder.codecInfo.name}")
return true
} catch (e: Exception) {
Log.e(TAG, "设置解码器失败", e)
throw e
}
}
/**
* 启动解码线程
*/
private fun startDecodeThread() {
decodeThread = Thread({
try {
Log.d(TAG, "解码线程已启动")
decodeLoop()
} catch (e: Exception) {
if (isRunning.get()) {
Log.e(TAG, "解码线程异常退出", e)
}
} finally {
Log.d(TAG, "解码线程已结束")
}
}, "VideoDecoderThread")
decodeThread?.start()
}
/**
* 解码主循环
*/
private fun decodeLoop() {
val codec = mediaCodec ?: return
Log.d(TAG, "开始解码循环,解码器: ${codec.codecInfo.name}")
while (isRunning.get()) {
try {
// 从队列取出一帧
val frame = frameQueue.poll(100, TimeUnit.MILLISECONDS)
if (frame == null) {
continue // 没有帧可解码,继续等待
}
// 处理I帧标志
if (frame.type == FrameType.I_FRAME) {
hasReceivedIFrame.set(true)
lastIFrameTimestamp.set(System.currentTimeMillis())
Log.d(TAG, "收到I帧: 大小=${frame.data.size}字节")
} else if (!hasReceivedIFrame.get()) {
// 如果还没有收到I帧丢弃P帧
droppedFrameCount++
continue
}
// 获取输入缓冲区
val inputBufferId = codec.dequeueInputBuffer(TIMEOUT_US)
if (inputBufferId >= 0) {
val inputBuffer = codec.getInputBuffer(inputBufferId)
if (inputBuffer != null) {
// 将数据复制到缓冲区
inputBuffer.clear()
inputBuffer.put(frame.data)
// 提交缓冲区进行解码
codec.queueInputBuffer(
inputBufferId,
0,
frame.data.size,
System.nanoTime() / 1000,
if (frame.type == FrameType.I_FRAME) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0
)
}
}
// 处理输出缓冲区
val bufferInfo = MediaCodec.BufferInfo()
var outputBufferId = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
while (outputBufferId >= 0) {
// 将帧渲染到Surface
val shouldRender = true // 始终渲染
if (shouldRender) {
codec.releaseOutputBuffer(outputBufferId, true)
frameCount++
renderedFrameCount++
// 强制通知
if (frameCount % 1 == 0) { // 每帧都通知
mainHandler.post {
Log.d(TAG, "通知帧可用: 第$frameCount帧")
callback?.onFrameAvailable()
}
}
} else {
codec.releaseOutputBuffer(outputBufferId, false)
droppedFrameCount++
}
// 获取下一个输出缓冲区
outputBufferId = codec.dequeueOutputBuffer(bufferInfo, TIMEOUT_US)
}
} catch (e: InterruptedException) {
Log.d(TAG, "解码线程被中断")
break
} catch (e: Exception) {
Log.e(TAG, "解码循环异常", e)
}
}
}
/**
* 解码视频帧
*
* @param frameData 帧数据
* @param isIFrame 是否为I帧
* @return 是否成功添加到解码队列
*/
fun decodeFrame(frameData: ByteArray, isIFrame: Boolean): Boolean {
if (!isRunning.get() || frameData.isEmpty()) {
Log.w(TAG, "解码器未运行或帧数据为空")
isDecoderConfigured.set(false)
callback?.onError("设置解码器失败: ${e.message}")
return false
}
}
/**
* 通知帧可用
*/
private fun notifyFrameAvailable() {
if (!isRunning.get()) {
Log.d(TAG, "解码器已停止,跳过帧可用通知")
return
}
try {
// 创建帧对象
val frameType = if (isIFrame) FrameType.I_FRAME else FrameType.P_FRAME
val frame = Frame(frameData, frameType)
// 对于I帧记录日志
if (isIFrame) {
Log.d(TAG, "添加I帧到队列: 大小=${frameData.size}字节")
Log.d(TAG, "发送帧可用通知,当前渲染帧数: $renderedFrameCount")
callback?.onFrameAvailable()
} catch (e: Exception) {
Log.e(TAG, "通知帧可用时出错: ${e.message}", e)
}
}
/**
* 快速检查NAL类型
*/
private fun checkNalType(frame: ByteArray): Int {
try {
// 快速检查常见的4字节起始码
if (frame.size > 4 && frame[0] == 0.toByte() && frame[1] == 0.toByte() &&
frame[2] == 0.toByte() && frame[3] == 1.toByte()) {
return frame[4].toInt() and 0x1F
}
// 将帧添加到队列
return if (frameQueue.offer(frame)) {
true
} else {
// 队列已满,移除一帧后再添加
frameQueue.poll()
droppedFrameCount++
frameQueue.offer(frame)
// 快速检查常见的3字节起始码
if (frame.size > 3 && frame[0] == 0.toByte() && frame[1] == 0.toByte() &&
frame[2] == 1.toByte()) {
return frame[3].toInt() and 0x1F
}
// 尝试搜索起始码
for (i in 0 until frame.size - 4) {
if (frame[i] == 0.toByte() && frame[i+1] == 0.toByte() &&
frame[i+2] == 0.toByte() && frame[i+3] == 1.toByte()) {
return frame[i+4].toInt() and 0x1F
}
if (i < frame.size - 3 && frame[i] == 0.toByte() &&
frame[i+1] == 0.toByte() && frame[i+2] == 1.toByte()) {
return frame[i+3].toInt() and 0x1F
}
}
} catch (e: Exception) {
Log.e(TAG, "添加帧到解码队列失败", e)
Log.e(TAG, "检查NAL类型出错", e)
}
// 无法识别,使用传入的参数
return -1
}
/**
* 解码视频帧 - 简化但严格
*/
fun decodeFrame(frameData: ByteArray, isIFrame: Boolean): Boolean {
if (!isRunning.get() || !isDecoderConfigured.get() || frameData.isEmpty()) {
Log.w(TAG, "解码器未运行或未配置或帧数据为空")
return false
}
val codec = mediaCodec ?: return false
try {
// 检查NAL类型
val nalType = checkNalType(frameData)
// 实际使用的NAL类型
val effectiveType = if (nalType != -1) nalType else if (isIFrame) NAL_UNIT_TYPE_IDR else NAL_UNIT_TYPE_NON_IDR
// 如果是SPS或PPS且在缓存中已有相同内容跳过
if (effectiveType == NAL_UNIT_TYPE_SPS) {
val hash = frameData.hashCode()
if (lastSPSHash == hash) return true
lastSPSHash = hash
hasSentSPS = true
} else if (effectiveType == NAL_UNIT_TYPE_PPS) {
val hash = frameData.hashCode()
if (lastPPSHash == hash) return true
lastPPSHash = hash
hasSentPPS = true
} else if (effectiveType == NAL_UNIT_TYPE_IDR) {
hasSentIDR = true
lastIFrameTimeMs = System.currentTimeMillis()
consecutivePFrameCount = 0
} else {
// P帧处理
if (!hasSentIDR) {
Log.w(TAG, "丢弃P帧因为尚未收到I帧")
return false
}
consecutivePFrameCount++
}
// 记录帧信息
frameCount++
// 解码帧
val inputBufferIndex = codec.dequeueInputBuffer(TIMEOUT_US)
if (inputBufferIndex < 0) {
Log.w(TAG, "无法获取输入缓冲区,可能需要等待")
return false
}
// 获取输入缓冲区
val inputBuffer = codec.getInputBuffer(inputBufferIndex)
if (inputBuffer == null) {
Log.e(TAG, "获取输入缓冲区失败")
return false
}
// 填充数据
inputBuffer.clear()
inputBuffer.put(frameData)
// 提交缓冲区
val flags = if (isIFrame) MediaCodec.BUFFER_FLAG_KEY_FRAME else 0
codec.queueInputBuffer(
inputBufferIndex,
0,
frameData.size,
System.nanoTime() / 1000L,
flags
)
// 处理输出
processOutputBuffers()
return true
} catch (e: Exception) {
Log.e(TAG, "解码帧失败", e)
return false
}
}
/**
* 处理所有可用的输出缓冲区
*/
private fun processOutputBuffers() {
val codec = mediaCodec ?: return
val bufferInfo = MediaCodec.BufferInfo()
var outputDone = false
while (!outputDone) {
val outputBufferIndex = codec.dequeueOutputBuffer(bufferInfo, 0) // 不等待,只检查当前可用的
when {
outputBufferIndex >= 0 -> {
val render = bufferInfo.size > 0
codec.releaseOutputBuffer(outputBufferIndex, render)
if (render) {
renderedFrameCount++
lastOutputTimeMs = System.currentTimeMillis()
Log.d(TAG, "成功渲染帧 #$renderedFrameCount")
// 通知Flutter刷新纹理
notifyFrameAvailable()
}
}
outputBufferIndex == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED -> {
Log.d(TAG, "输出格式变更: ${codec.outputFormat}")
}
outputBufferIndex == MediaCodec.INFO_TRY_AGAIN_LATER -> {
outputDone = true
}
}
}
}
/**
* 释放所有资源
*/
fun release() {
Log.d(TAG, "释放解码器资源")
isRunning.set(false)
isDecoderConfigured.set(false)
try {
mediaCodec?.let { codec ->
try {
codec.stop()
codec.release()
} catch (e: Exception) {
Log.e(TAG, "释放MediaCodec失败", e)
}
}
mediaCodec = null
try {
surface.release()
} catch (e: Exception) {
Log.e(TAG, "释放Surface失败", e)
}
try {
textureEntry.release()
} catch (e: Exception) {
Log.e(TAG, "释放TextureEntry失败", e)
}
callback = null
Log.d(TAG, "所有资源已释放")
} catch (e: Exception) {
Log.e(TAG, "释放资源时出错", e)
}
}
/**
@ -341,71 +389,13 @@ class VideoDecoder(
"totalFrames" to frameCount,
"renderedFrames" to renderedFrameCount,
"droppedFrames" to droppedFrameCount,
"queueSize" to frameQueue.size,
"hasIFrame" to hasReceivedIFrame.get(),
"lastIFrameAgeMs" to (System.currentTimeMillis() - lastIFrameTimestamp.get())
"hasSentSPS" to hasSentSPS,
"hasSentPPS" to hasSentPPS,
"hasSentIDR" to hasSentIDR,
"consecutivePFrames" to consecutivePFrameCount,
"targetWidth" to config.width,
"targetHeight" to config.height,
"frameRate" to (config.frameRate ?: 0)
)
}
/**
* 释放资源
*/
fun release() {
Log.d(TAG, "开始释放解码器资源")
// 标记为停止运行
isRunning.set(false)
// 清除回调
callback = null
try {
// 停止解码线程
decodeThread?.let { thread ->
thread.interrupt()
try {
thread.join(500) // 等待最多500ms
} catch (e: Exception) {
Log.w(TAG, "等待解码线程结束超时", e)
}
}
decodeThread = null
// 释放MediaCodec
mediaCodec?.let { codec ->
try {
codec.stop()
codec.release()
Log.d(TAG, "MediaCodec已释放")
} catch (e: Exception) {
Log.e(TAG, "释放MediaCodec失败", e)
}
}
mediaCodec = null
// 清空队列
frameQueue.clear()
// 释放Surface
try {
surface.release()
Log.d(TAG, "Surface已释放")
} catch (e: Exception) {
Log.e(TAG, "释放Surface失败", e)
}
// 释放纹理
try {
textureEntry.release()
Log.d(TAG, "TextureEntry已释放")
} catch (e: Exception) {
Log.e(TAG, "释放TextureEntry失败", e)
}
Log.d(TAG, "所有资源释放完成")
} catch (e: Exception) {
Log.e(TAG, "释放资源失败", e)
}
}
}

View File

@ -1,137 +0,0 @@
import 'dart:typed_data';
/// H.264
/// H.264
class H264FrameGenerator {
//
final int width;
//
final int height;
// (SPS) -
// SPS
final List<int> _spsData = [
0x00,
0x00,
0x00,
0x01,
0x67,
0x42,
0x00,
0x0A,
0xF8,
0x41,
0xA2,
0x00,
0x00,
0x03,
0x00,
0x01,
0x00,
0x00,
0x03,
0x00,
0x32,
0x0F,
0x18,
0x31,
0x8C
];
// (PPS) -
// PPS
final List<int> _ppsData = [0x00, 0x00, 0x00, 0x01, 0x68, 0xCE, 0x38, 0x80];
// I帧的起始数据示例 -
final List<int> _iFrameHeader = [
0x00,
0x00,
0x00,
0x01,
0x65,
0x88,
0x80,
0x00,
0x00,
0x03,
0x00,
0x02,
0x00
];
// P帧的起始数据示例 -
final List<int> _pFrameHeader = [
0x00,
0x00,
0x00,
0x01,
0x41,
0x9A,
0x1C,
0x0D,
0x3E,
0x04
];
//
int _frameCount = 0;
// I帧之间的P帧数量
final int _pFramesPerIFrame = 9;
///
H264FrameGenerator({
required this.width,
required this.height,
});
/// SPS和PPS数据
Uint8List getConfigurationData() {
// SPS和PPS
List<int> data = [];
data.addAll(_spsData);
data.addAll(_ppsData);
return Uint8List.fromList(data);
}
///
/// (, I帧)
(Uint8List, bool) generateNextFrame() {
_frameCount++;
// 10I帧
bool isIFrame = _frameCount % (_pFramesPerIFrame + 1) == 1;
List<int> frameData = [];
if (isIFrame) {
// I帧: SPS和PPSI帧数据
frameData.addAll(_spsData);
frameData.addAll(_ppsData);
frameData.addAll(_iFrameHeader);
// I帧数据
//
for (int i = 0; i < 1000; i++) {
frameData.add((i * 13) % 256);
}
} else {
// P帧: P帧数据
frameData.addAll(_pFrameHeader);
// P帧数据
//
for (int i = 0; i < 300; i++) {
frameData.add((i * 7 + _frameCount) % 256);
}
}
return (Uint8List.fromList(frameData), isIFrame);
}
///
void reset() {
_frameCount = 0;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -30,7 +30,7 @@ dependencies:
# permission_handler: ^12.0.0+1
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.6
cupertino_icons: ^1.0.2
dev_dependencies:
integration_test:
@ -56,7 +56,7 @@ flutter:
# the material Icons class.
uses-material-design: true
# 添加示例资源
# 添加assets资源
assets:
- assets/demo.h264