feat:v1版本实现
This commit is contained in:
parent
3381ef72fc
commit
f8bfa2b229
252
README.md
252
README.md
@ -1,150 +1,160 @@
|
||||
# video_decode_plugin
|
||||
# Video Decode Plugin
|
||||
|
||||
一个高性能的 Flutter 插件,用于在 Android 原生层解码 H.264 裸流数据,并支持两种渲染模式。
|
||||
基于MediaCodec/VideoToolbox的跨平台H.264/H.265视频解码Flutter插件,专为低延迟实时视频流解码设计。
|
||||
|
||||
## 功能特点
|
||||
[](https://pub.dev/packages/video_decode_plugin)
|
||||
|
||||
- 支持 H.264 Annex B 格式裸流解码(含 NALU 单元)
|
||||
- 使用 Android MediaCodec 硬解码,提供高性能解码能力
|
||||
- 支持两种渲染模式:
|
||||
- Flutter 纹理渲染:将解码后的帧通过 Flutter Texture 传递到 Flutter UI
|
||||
- 原生 SurfaceView 渲染:在原生 Android 层直接渲染
|
||||
- 提供完善的配置管理和性能监控
|
||||
- 支持动态丢帧策略,优化内存使用
|
||||
- 适配低端设备的性能优化措施
|
||||
## 特性
|
||||
|
||||
- 🔄 基于原生解码器的高性能视频解码(Android使用MediaCodec,iOS使用VideoToolbox)
|
||||
- 🖼️ 支持H.264和H.265(HEVC)视频格式
|
||||
- ⏱️ 低延迟解码,适用于实时视频流应用
|
||||
- 📱 跨平台支持(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 开始码)
|
||||
- 建议在实际项目中根据设备性能动态调整解码配置
|
||||
- 视频分辨率受设备硬件限制,较旧设备可能无法支持高分辨率视频
|
||||
- 硬件解码器可能在某些设备上不可用,插件会自动回退到软件解码
|
||||
- 对于最佳性能,建议在实际硬件设备上测试,而不仅仅是模拟器
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
// 保存解码器
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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++;
|
||||
|
||||
// 每10帧插入一个I帧
|
||||
bool isIFrame = _frameCount % (_pFramesPerIFrame + 1) == 1;
|
||||
|
||||
List<int> frameData = [];
|
||||
|
||||
if (isIFrame) {
|
||||
// I帧: 添加SPS和PPS,并添加I帧数据
|
||||
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
@ -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
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user