From 8ed55f1bb38d1fdf9db3c6db37f4d19fbd3a557e Mon Sep 17 00:00:00 2001 From: liyi Date: Wed, 30 Apr 2025 18:00:54 +0800 Subject: [PATCH] =?UTF-8?q?feat:v1=E7=89=88=E6=9C=AC=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 51 +++++++++++++++++++ .../video_decode_plugin/VideoDecoder.kt | 20 ++++++++ lib/video_decode_plugin.dart | 17 +++---- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 42a6a09..37ee4a4 100644 --- a/README.md +++ b/README.md @@ -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帧前都已推送最新SPS(NAL类型7)、PPS(NAL类型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, ...); +``` + 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 9565448..ade4ab9 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 @@ -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帧的frameSeq,P/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) } diff --git a/lib/video_decode_plugin.dart b/lib/video_decode_plugin.dart index 46cae30..426e060 100644 --- a/lib/video_decode_plugin.dart +++ b/lib/video_decode_plugin.dart @@ -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; - } - } } }