feat:v1版本完成

This commit is contained in:
liyi 2025-04-30 18:00:54 +08:00
parent 338d24570d
commit 8ed55f1bb3
3 changed files with 79 additions and 9 deletions

View File

@ -189,4 +189,55 @@ await VideoDecodePlugin.decodeFrame(pFrameData, FrameType.pFrame);
- [ ] 支持更多编解码格式
- [ ] 优化内存管理
## 插件架构与职责
本插件为Flutter平台下的高性能、可扩展、易调试的视频解码渲染插件专注于Android端H.264/H.265实时流解码。
- **Flutter端职责**
- 负责NALU分割、SPS/PPS/I帧依赖链管理、滑动窗口丢帧、业务层帧流控制。
- 依赖链完整性校验P帧仅在依赖I帧已解码时才推送。
- 通过MethodChannel将帧数据、类型、序号等元数据传递到原生端。
- **Android端职责**
- 极简高效解码与渲染,主线程安全回调。
- 兜底依赖链校验,解码器生命周期管理,异常自愈。
## 文件结构与说明
### Dart端lib/
- **video_decode_plugin.dart**:插件主入口,负责解码器初始化、帧推送、回调注册、依赖链管理、与原生通信等。
- **frame_dependency_manager.dart**SPS/PPS/I帧依赖链滑动窗口管理支持I帧序号窗口、依赖校验、SPS/PPS缓存。
- **nalu_utils.dart**NALU分割与类型识别工具支持H.264/H.265帧的NALU单元分离、类型提取。
- **video_decode_plugin_platform_interface.dart**插件平台接口定义支持多平台扩展默认实现为MethodChannel。
- **video_decode_plugin_method_channel.dart**插件MethodChannel实现负责与原生端通信。
### Android端android/src/main/kotlin/top/skychip/video_decode_plugin/
- **VideoDecodePlugin.kt**插件原生入口注册MethodChannel管理解码器生命周期处理Flutter端方法调用。
- **VideoDecoder.kt**解码器核心实现负责MediaCodec解码、输入/输出队列、渲染线程、依赖链兜底校验、主线程回调、资源释放。
- **VideoDecoderConfig.kt**:解码器配置参数定义,支持分辨率、编解码类型、帧率、调试模式等。
## H.264解码注意事项
### 1. 马赛克/花屏出现的根因
- **依赖链断裂**P/B帧依赖的I帧未被解码成功或I帧本身丢失/损坏后续P/B帧全部解码失败必然花屏。
- **SPS/PPS/I帧推送顺序错误**未按SPS→PPS→I帧顺序推送或I帧前未收到最新SPS/PPS解码器无法正确解码I帧。
- **解码链断裂后未及时reset解码器**解码器内部状态异常后续即使收到新I帧也无法恢复需reset解码器。
### 2. 外部调用者(业务方)必须做好的前置操作
- **推送顺序**务必保证每个I帧前都已推送最新SPSNAL类型7、PPSNAL类型8再推送I帧NAL类型5最后推送P/B帧。
- **依赖链完整性校验**业务端需实现滑动窗口I帧序号管理P帧推送前校验refIFrameSeq是否在窗口内否则丢弃。
- **丢帧策略**:强烈建议业务端实现丢帧与依赖链管理,避免无效帧流入原生端。
- **异常自愈**:检测到解码失败/花屏/长时间无I帧时建议主动reset解码器等待下一个I帧恢复链路。
- **日志监控**:建议业务端和原生端均输出详细日志,便于端到端排查依赖链断裂、丢包、乱序等问题。
### 3. 推荐推送流程(伪代码)
```dart
// 发送SPS和PPS
await VideoDecodePlugin.sendFrame(frameData: sps, frameType: 0, ...);
await VideoDecodePlugin.sendFrame(frameData: pps, frameType: 0, ...);
// 发送I帧
await VideoDecodePlugin.sendFrame(frameData: iFrame, frameType: 0, ...);
// 发送P帧
await VideoDecodePlugin.sendFrame(frameData: pFrame, frameType: 1, refIFrameSeq: iFrameSeq, ...);
```

View File

@ -18,6 +18,7 @@ import kotlin.concurrent.withLock
import java.util.Collections
import java.util.concurrent.ConcurrentHashMap
import android.os.SystemClock
import java.util.ArrayDeque
/**
* 视频解码器核心类
@ -75,6 +76,9 @@ class VideoDecoder(
// 渲染帧率fps可由外部控制默认18
@Volatile var renderFps: Int = 18
// 兜底记录最近一次I帧的frameSeqP/B帧依赖校验
@Volatile private var lastIFrameSeq: Int? = null
// 输入帧结构体
private data class FrameData(
val data: ByteArray,
@ -214,6 +218,22 @@ class VideoDecoder(
): Boolean {
if (!running || mediaCodec == null) return false
if (!frameSeqSet.add(frameSeq)) return false // 防止重复帧
var allow = false
if (frameType == 0) { // I帧
lastIFrameSeq = frameSeq
allow = true
} else {
val lastI = lastIFrameSeq
if (lastI == null) {
Log.w(TAG, "[decodeFrame] Drop P/B frame: no I-frame yet, frameSeq=$frameSeq, refIFrameSeq=$refIFrameSeq")
return false
}
allow = (refIFrameSeq == lastI)
if (!allow) {
Log.w(TAG, "[decodeFrame] Drop P/B frame: refIFrameSeq=$refIFrameSeq != lastIFrameSeq=$lastI, frameSeq=$frameSeq")
return false
}
}
return inputFrameQueue.offer(FrameData(frameData, frameType, timestamp, frameSeq, refIFrameSeq), 50, TimeUnit.MILLISECONDS)
}

View File

@ -202,6 +202,14 @@ class VideoDecodePlugin {
return;
}
// SPS/PPS/I帧/P帧等场景
// P帧依赖链完整性校验
if (frameType == 1) {
if (!_depManager.isIFrameDecoded(refIFrameSeq)) {
print(
'[丢帧] P帧依赖的I帧未解码丢弃 frameSeq=$frameSeq, refIFrameSeq=$refIFrameSeq');
return;
}
}
await _decodeFrame(
frameData: Uint8List.fromList(frameData),
frameType: frameType,
@ -211,14 +219,5 @@ class VideoDecodePlugin {
);
// I帧
if (frameType == 0) _depManager.updateIFrameSeq(frameSeq);
// P帧依赖链完整性校验
if (frameType == 1) {
if (!_depManager.isIFrameDecoded(refIFrameSeq)) {
print(
'[丢帧] P帧依赖的I帧未解码丢弃 frameSeq=$frameSeq, refIFrameSeq=$refIFrameSeq');
return;
}
}
}
}