339 lines
9.8 KiB
Dart
339 lines
9.8 KiB
Dart
import 'dart:async';
|
||
import 'dart:io';
|
||
import 'dart:typed_data';
|
||
|
||
import 'package:flutter/foundation.dart';
|
||
import 'package:flutter/services.dart';
|
||
|
||
import 'video_decode_plugin_platform_interface.dart';
|
||
import 'nalu_utils.dart';
|
||
import 'frame_dependency_manager.dart';
|
||
|
||
/// 视频解码器配置
|
||
class VideoDecoderConfig {
|
||
/// 视频宽度
|
||
final int width;
|
||
|
||
/// 视频高度
|
||
final int height;
|
||
|
||
/// 编码类型,默认h264
|
||
final String codecType;
|
||
|
||
/// 构造函数
|
||
VideoDecoderConfig({
|
||
required this.width,
|
||
required this.height,
|
||
this.codecType = 'h264',
|
||
});
|
||
|
||
/// 转换为Map
|
||
Map<String, dynamic> toMap() {
|
||
return {
|
||
'width': width,
|
||
'height': height,
|
||
'codecType': codecType,
|
||
};
|
||
}
|
||
}
|
||
|
||
/// 视频解码插件主类
|
||
class VideoDecodePlugin {
|
||
static const MethodChannel _channel = MethodChannel('video_decode_plugin');
|
||
|
||
static int? _textureId;
|
||
|
||
/// 获取默认纹理ID
|
||
static int? get textureId => _textureId;
|
||
|
||
static final _depManager = FrameDependencyManager();
|
||
|
||
/// onFrameRendered回调类型(解码并已开始渲染)
|
||
static void Function(int textureId)? _onFrameRendered;
|
||
|
||
/// 设置onFrameRendered监听
|
||
static void setOnFrameRenderedListener(
|
||
void Function(int textureId) callback) {
|
||
_onFrameRendered = callback;
|
||
_channel.setMethodCallHandler(_handleMethodCall);
|
||
}
|
||
|
||
static Future<void> _handleMethodCall(MethodCall call) async {
|
||
if (call.method == 'onFrameRendered') {
|
||
final int? textureId = call.arguments['textureId'];
|
||
if (_onFrameRendered != null && textureId != null) {
|
||
_onFrameRendered!(textureId);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 初始化解码器
|
||
static Future<int?> initDecoder(VideoDecoderConfig config) async {
|
||
final textureId =
|
||
await _channel.invokeMethod<int>('initDecoder', config.toMap());
|
||
_textureId = textureId;
|
||
return textureId;
|
||
}
|
||
|
||
/// 解码视频帧(参数扩展,仅供内部调用)
|
||
static Future<bool> _decodeFrame({
|
||
required Uint8List frameData,
|
||
required int frameType, // 0=I帧, 1=P帧
|
||
required int timestamp, // 毫秒或微秒
|
||
required int frameSeq, // 帧序号
|
||
int? refIFrameSeq, // P帧时可选
|
||
}) async {
|
||
if (_textureId == null) return false;
|
||
final params = {
|
||
'textureId': _textureId,
|
||
'frameData': frameData,
|
||
'frameType': frameType,
|
||
'timestamp': timestamp,
|
||
'frameSeq': frameSeq,
|
||
if (refIFrameSeq != null) 'refIFrameSeq': refIFrameSeq,
|
||
};
|
||
final result = await _channel.invokeMethod<bool>('decodeFrame', params);
|
||
return result ?? false;
|
||
}
|
||
|
||
/// 释放解码器资源
|
||
static Future<bool> releaseDecoder() async {
|
||
if (_textureId == null) return true;
|
||
final result = await _channel.invokeMethod<bool>('releaseDecoder', {
|
||
'textureId': _textureId,
|
||
});
|
||
_textureId = null;
|
||
_depManager.updateSps(null);
|
||
_depManager.updatePps(null);
|
||
return result ?? false;
|
||
}
|
||
|
||
/// 获取平台版本
|
||
static Future<String?> getPlatformVersion() {
|
||
return VideoDecodePluginPlatform.instance.getPlatformVersion();
|
||
}
|
||
|
||
/// 检查当前平台是否支持
|
||
static bool get isPlatformSupported {
|
||
return Platform.isAndroid || Platform.isIOS;
|
||
}
|
||
|
||
///
|
||
/// [frameData]:帧数据
|
||
/// [frameType]:帧类型 0=I帧, 1=P帧
|
||
/// [timestamp]:帧时间戳(绝对时间戳)
|
||
/// [frameSeq]:帧序号
|
||
/// [splitNalFromIFrame]:true时遇到I帧自动从I帧分割NALU并依赖管理,false时直接发送原始数据(适配SPS/PPS/I帧独立推送场景)。
|
||
///
|
||
static Future<void> sendFrame({
|
||
required List<int> frameData,
|
||
required int frameType,
|
||
required int timestamp,
|
||
required int frameSeq,
|
||
bool splitNalFromIFrame = false,
|
||
int? refIFrameSeq, // P帧时可选
|
||
}) async {
|
||
if (Platform.isAndroid) {
|
||
await _sendFrameAndroid(
|
||
frameData: frameData,
|
||
frameType: frameType,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
splitNalFromIFrame: splitNalFromIFrame,
|
||
refIFrameSeq: refIFrameSeq,
|
||
);
|
||
} else if (Platform.isIOS) {
|
||
await _sendFrameIOS(
|
||
frameData: frameData,
|
||
frameType: frameType,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
refIFrameSeq: refIFrameSeq,
|
||
);
|
||
}
|
||
// 其他平台暂不支持
|
||
}
|
||
|
||
static Future<void> _sendFrameAndroid({
|
||
required List<int> frameData,
|
||
required int frameType,
|
||
required int timestamp,
|
||
required int frameSeq,
|
||
bool splitNalFromIFrame = false,
|
||
int? refIFrameSeq,
|
||
}) async {
|
||
if (splitNalFromIFrame && frameType == 0) {
|
||
// 优先使用缓存的SPS/PPS
|
||
if (_depManager.sps != null && _depManager.pps != null) {
|
||
await _decodeFrame(
|
||
frameData: _depManager.sps!,
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq - 2,
|
||
refIFrameSeq: frameSeq - 2,
|
||
);
|
||
await _decodeFrame(
|
||
frameData: _depManager.pps!,
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq - 1,
|
||
refIFrameSeq: frameSeq - 1,
|
||
);
|
||
await _decodeFrame(
|
||
frameData: Uint8List.fromList(frameData),
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
refIFrameSeq: frameSeq,
|
||
);
|
||
_depManager.updateIFrameSeq(frameSeq);
|
||
return;
|
||
}
|
||
// 首次无缓存时分割并缓存SPS/PPS
|
||
final nalus = NaluUtils.splitNalus(frameData);
|
||
|
||
List<int>? sps, pps;
|
||
for (final nalu in nalus) {
|
||
if (nalu.type == 7)
|
||
sps = nalu.data;
|
||
else if (nalu.type == 8) pps = nalu.data;
|
||
}
|
||
if (sps != null) {
|
||
_depManager.updateSps(Uint8List.fromList(sps));
|
||
}
|
||
if (pps != null) {
|
||
_depManager.updatePps(Uint8List.fromList(pps));
|
||
}
|
||
if (_depManager.sps == null || _depManager.pps == null) {
|
||
print('[VideoDecodePlugin] 丢弃I帧: 未缓存SPS/PPS');
|
||
return;
|
||
}
|
||
await _decodeFrame(
|
||
frameData: _depManager.sps!,
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq - 2,
|
||
refIFrameSeq: frameSeq - 2,
|
||
);
|
||
await _decodeFrame(
|
||
frameData: _depManager.pps!,
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq - 1,
|
||
refIFrameSeq: frameSeq - 1,
|
||
);
|
||
await _decodeFrame(
|
||
frameData: Uint8List.fromList(frameData),
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
refIFrameSeq: frameSeq,
|
||
);
|
||
_depManager.updateIFrameSeq(frameSeq);
|
||
return;
|
||
}
|
||
// 兼容直接推送SPS/PPS/I帧/P帧等场景,直接发送
|
||
// P帧依赖链完整性校验(提前)
|
||
if (frameType == 1) {
|
||
if (!_depManager.isIFrameDecoded(refIFrameSeq)) {
|
||
print('[丢帧] P帧依赖的I帧未解码,丢弃 frameSeq=$frameSeq, refIFrameSeq=$refIFrameSeq, dropCount=${_depManager.dropCount}, threshold=${_depManager.dropThresholdValue}');
|
||
return;
|
||
}
|
||
// 仅推送type=1的NALU,提升平台一致性
|
||
frameData = NaluUtils.filterNalusByType(frameData, 1);
|
||
}
|
||
await _decodeFrame(
|
||
frameData: Uint8List.fromList(frameData),
|
||
frameType: frameType,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
refIFrameSeq: frameType == 0 ? frameSeq : _depManager.lastIFrameSeq,
|
||
);
|
||
// 若为I帧,更新依赖管理
|
||
if (frameType == 0) _depManager.updateIFrameSeq(frameSeq);
|
||
}
|
||
|
||
static Future<void> _sendFrameIOS({
|
||
required List<int> frameData,
|
||
required int frameType,
|
||
required int timestamp,
|
||
required int frameSeq,
|
||
int? refIFrameSeq,
|
||
}) async {
|
||
// 仅P帧做AnnexB起始码检测和修正
|
||
if (frameType == 1) {
|
||
// 仅推送type=1的NALU,提升平台一致性
|
||
frameData = NaluUtils.filterNalusByType(frameData, 1);
|
||
if (frameData.isEmpty) {
|
||
print('[VideoDecodePlugin][警告] iOS端P帧未找到type=1的NALU,已丢弃');
|
||
return;
|
||
}
|
||
int startIndex = -1;
|
||
for (int i = 0; i < frameData.length - 3; i++) {
|
||
if (frameData[i] == 0x00 &&
|
||
frameData[i + 1] == 0x00 &&
|
||
frameData[i + 2] == 0x00 &&
|
||
frameData[i + 3] == 0x01) {
|
||
startIndex = i;
|
||
break;
|
||
}
|
||
}
|
||
if (startIndex == -1) {
|
||
print('[VideoDecodePlugin][警告] iOS端P帧仅type=1 NALU后无AnnexB起始码,已丢弃');
|
||
return;
|
||
}
|
||
if (startIndex > 0) {
|
||
frameData = frameData.sublist(startIndex);
|
||
}
|
||
}
|
||
if (frameType == 0) {
|
||
// I帧需分割NALU,严格按SPS->PPS->I帧顺序推送
|
||
final nalus = NaluUtils.splitNalus(frameData);
|
||
List<int>? sps, pps, idr;
|
||
for (final nalu in nalus) {
|
||
if (nalu.type == 7) sps = nalu.data;
|
||
if (nalu.type == 8) pps = nalu.data;
|
||
if (nalu.type == 5) idr = nalu.data;
|
||
}
|
||
if (sps != null) {
|
||
await _decodeFrame(
|
||
frameData: Uint8List.fromList(sps),
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
refIFrameSeq: frameSeq,
|
||
);
|
||
}
|
||
if (pps != null) {
|
||
await _decodeFrame(
|
||
frameData: Uint8List.fromList(pps),
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
refIFrameSeq: frameSeq,
|
||
);
|
||
}
|
||
if (idr != null) {
|
||
await _decodeFrame(
|
||
frameData: Uint8List.fromList(idr),
|
||
frameType: 0,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
refIFrameSeq: frameSeq,
|
||
);
|
||
}
|
||
_depManager.updateIFrameSeq(frameSeq);
|
||
return;
|
||
}
|
||
// 其他类型帧直接推送
|
||
await _decodeFrame(
|
||
frameData: Uint8List.fromList(frameData),
|
||
frameType: frameType,
|
||
timestamp: timestamp,
|
||
frameSeq: frameSeq,
|
||
refIFrameSeq: refIFrameSeq,
|
||
);
|
||
if (frameType == 0) _depManager.updateIFrameSeq(frameSeq);
|
||
}
|
||
}
|