2025-04-21 10:56:28 +08:00
|
|
|
|
import Flutter
|
|
|
|
|
|
import UIKit
|
2025-05-07 15:07:36 +08:00
|
|
|
|
import AVFoundation
|
2025-04-21 10:56:28 +08:00
|
|
|
|
|
2025-05-07 15:07:36 +08:00
|
|
|
|
public class VideoDecodePlugin: NSObject, FlutterPlugin, FlutterTexture {
|
|
|
|
|
|
private var channel: FlutterMethodChannel?
|
|
|
|
|
|
private var registrar: FlutterPluginRegistrar?
|
|
|
|
|
|
private var decoder: VideoDecoder?
|
|
|
|
|
|
private var textureId: Int64?
|
|
|
|
|
|
private var textureRegistry: FlutterTextureRegistry?
|
|
|
|
|
|
private var latestPixelBuffer: CVPixelBuffer?
|
|
|
|
|
|
private let textureQueue = DispatchQueue(label: "video_decode_plugin.texture.queue")
|
|
|
|
|
|
private var cachedSps: Data?
|
|
|
|
|
|
private var cachedPps: Data?
|
|
|
|
|
|
private var hasNotifiedFlutter = false
|
|
|
|
|
|
|
|
|
|
|
|
public static func register(with registrar: FlutterPluginRegistrar) {
|
|
|
|
|
|
let channel = FlutterMethodChannel(name: "video_decode_plugin", binaryMessenger: registrar.messenger())
|
|
|
|
|
|
let instance = VideoDecodePlugin()
|
|
|
|
|
|
instance.channel = channel
|
|
|
|
|
|
instance.registrar = registrar
|
|
|
|
|
|
instance.textureRegistry = registrar.textures()
|
|
|
|
|
|
registrar.addMethodCallDelegate(instance, channel: channel)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
|
|
|
|
switch call.method {
|
|
|
|
|
|
case "initDecoder":
|
|
|
|
|
|
handleInitDecoder(call: call, result: result)
|
|
|
|
|
|
case "decodeFrame":
|
|
|
|
|
|
handleDecodeFrame(call: call, result: result)
|
|
|
|
|
|
case "releaseDecoder":
|
|
|
|
|
|
handleReleaseDecoder(call: call, result: result)
|
|
|
|
|
|
default:
|
|
|
|
|
|
result(FlutterMethodNotImplemented)
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 初始化解码器
|
|
|
|
|
|
private func handleInitDecoder(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
|
|
|
|
guard let args = call.arguments as? [String: Any],
|
|
|
|
|
|
let width = args["width"] as? Int,
|
|
|
|
|
|
let height = args["height"] as? Int,
|
|
|
|
|
|
let codecType = args["codecType"] as? String else {
|
2025-06-25 18:47:40 +08:00
|
|
|
|
print("[VideoDecodePlugin][错误] 参数解析失败:\(String(describing: call.arguments))")
|
2025-05-07 15:07:36 +08:00
|
|
|
|
result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-06-25 18:47:40 +08:00
|
|
|
|
|
2025-05-07 15:07:36 +08:00
|
|
|
|
// 释放旧解码器和纹理
|
|
|
|
|
|
decoder?.release()
|
|
|
|
|
|
decoder = nil
|
|
|
|
|
|
if let tid = textureId {
|
|
|
|
|
|
textureRegistry?.unregisterTexture(tid)
|
|
|
|
|
|
textureId = nil
|
|
|
|
|
|
}
|
2025-06-25 18:47:40 +08:00
|
|
|
|
|
2025-05-07 15:07:36 +08:00
|
|
|
|
// 注册Flutter纹理
|
|
|
|
|
|
guard let registry = textureRegistry else {
|
2025-06-25 18:47:40 +08:00
|
|
|
|
print("[VideoDecodePlugin][错误] 无法获取纹理注册表")
|
2025-05-07 15:07:36 +08:00
|
|
|
|
result(FlutterError(code: "NO_TEXTURE_REGISTRY", message: "无法获取纹理注册表", details: nil))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-06-25 18:47:40 +08:00
|
|
|
|
|
2025-05-07 15:07:36 +08:00
|
|
|
|
let textureId = registry.register(self)
|
|
|
|
|
|
self.textureId = textureId
|
2025-06-25 18:47:40 +08:00
|
|
|
|
|
2025-05-07 15:07:36 +08:00
|
|
|
|
// 创建解码器
|
|
|
|
|
|
let decoder = VideoDecoder(width: width, height: height, codecType: codecType)
|
|
|
|
|
|
self.decoder = decoder
|
|
|
|
|
|
decoder.onFrameDecoded = { [weak self] pixelBuffer, _ in
|
|
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
|
self.textureQueue.async {
|
|
|
|
|
|
self.latestPixelBuffer = pixelBuffer
|
|
|
|
|
|
self.textureRegistry?.textureFrameAvailable(self.textureId ?? 0)
|
|
|
|
|
|
if !self.hasNotifiedFlutter {
|
|
|
|
|
|
self.hasNotifiedFlutter = true
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
|
self.channel?.invokeMethod("onFrameRendered", arguments: ["textureId": self.textureId ?? 0])
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
print("[VideoDecodePlugin] 解码器初始化成功,textureId=\(textureId)")
|
|
|
|
|
|
result(textureId)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 18:47:40 +08:00
|
|
|
|
/// 去除NALU起始码的工具方法
|
2025-05-07 15:07:36 +08:00
|
|
|
|
private func stripStartCode(_ data: Data) -> Data {
|
|
|
|
|
|
var stripped: Data = data
|
|
|
|
|
|
if data.count > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01 {
|
|
|
|
|
|
stripped = data.subdata(in: 4..<data.count)
|
|
|
|
|
|
} else if data.count > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 {
|
|
|
|
|
|
stripped = data.subdata(in: 3..<data.count)
|
|
|
|
|
|
}
|
|
|
|
|
|
return stripped
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-06-25 18:47:40 +08:00
|
|
|
|
// 修改:更合理的side_data检测工具
|
|
|
|
|
|
private func checkNaluForSideData(_ nalu: Data, naluType: UInt8) -> Bool {
|
|
|
|
|
|
let maxSize = naluType == 5 ? 150_000 : 30_000 // I帧最大150KB,P帧最大30KB
|
|
|
|
|
|
if nalu.count > maxSize {
|
|
|
|
|
|
print("[VideoDecodePlugin][警告] NALU长度异常,可能包含side_data,type=\(naluType),len=\(nalu.count)")
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 简化:提取第一个合法NALU工具
|
|
|
|
|
|
private func extractFirstValidNalu(_ nalu: Data) -> Data {
|
|
|
|
|
|
// 查找第一个起始码(支持3字节和4字节格式)
|
|
|
|
|
|
var start = -1
|
|
|
|
|
|
var startCodeLength = 0
|
|
|
|
|
|
|
|
|
|
|
|
// 检查4字节起始码
|
|
|
|
|
|
if nalu.count >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00 && nalu[2] == 0x00 && nalu[3] == 0x01 {
|
|
|
|
|
|
start = 0
|
|
|
|
|
|
startCodeLength = 4
|
|
|
|
|
|
}
|
|
|
|
|
|
// 检查3字节起始码
|
|
|
|
|
|
else if nalu.count >= 3 && nalu[0] == 0x00 && nalu[1] == 0x00 && nalu[2] == 0x01 {
|
|
|
|
|
|
start = 0
|
|
|
|
|
|
startCodeLength = 3
|
|
|
|
|
|
}
|
|
|
|
|
|
// 在数据中间查找起始码
|
|
|
|
|
|
else {
|
|
|
|
|
|
for i in 0..<(nalu.count - 4) {
|
|
|
|
|
|
if nalu[i] == 0x00 && nalu[i + 1] == 0x00 {
|
|
|
|
|
|
if nalu[i + 2] == 0x00 && nalu[i + 3] == 0x01 {
|
|
|
|
|
|
start = i
|
|
|
|
|
|
startCodeLength = 4
|
|
|
|
|
|
break
|
|
|
|
|
|
} else if nalu[i + 2] == 0x01 {
|
|
|
|
|
|
start = i
|
|
|
|
|
|
startCodeLength = 3
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if start == -1 {
|
|
|
|
|
|
print("[VideoDecodePlugin][警告] NALU无AnnexB起始码,丢弃该帧")
|
|
|
|
|
|
return Data()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let searchRange = (start + startCodeLength)..<nalu.count
|
|
|
|
|
|
var end = nalu.count
|
|
|
|
|
|
|
|
|
|
|
|
// 查找下一个起始码(同时支持3字节和4字节格式)
|
|
|
|
|
|
for i in searchRange.lowerBound..<(nalu.count - 3) {
|
|
|
|
|
|
if nalu[i] == 0x00 && nalu[i + 1] == 0x00 {
|
|
|
|
|
|
if i + 3 < nalu.count && nalu[i + 2] == 0x00 && nalu[i + 3] == 0x01 {
|
|
|
|
|
|
end = i
|
|
|
|
|
|
break
|
|
|
|
|
|
} else if nalu[i + 2] == 0x01 {
|
|
|
|
|
|
end = i
|
|
|
|
|
|
break
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let extractedNalu = nalu[start..<end]
|
|
|
|
|
|
|
|
|
|
|
|
// 验证提取的NALU
|
|
|
|
|
|
if extractedNalu.count >= startCodeLength + 1 {
|
|
|
|
|
|
let naluType = extractedNalu[startCodeLength] & 0x1F
|
|
|
|
|
|
if checkNaluForSideData(extractedNalu, naluType: naluType) {
|
|
|
|
|
|
return Data()
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return extractedNalu
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-07 15:07:36 +08:00
|
|
|
|
/// 解码视频帧
|
|
|
|
|
|
private func handleDecodeFrame(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
|
|
|
|
guard let args = call.arguments as? [String: Any],
|
|
|
|
|
|
let frameData = args["frameData"] as? FlutterStandardTypedData,
|
|
|
|
|
|
let frameType = args["frameType"] as? Int,
|
|
|
|
|
|
let timestamp = args["timestamp"] as? Int,
|
|
|
|
|
|
let frameSeq = args["frameSeq"] as? Int else {
|
|
|
|
|
|
result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
let refIFrameSeq = args["refIFrameSeq"] as? Int
|
|
|
|
|
|
let data = frameData.data
|
|
|
|
|
|
|
|
|
|
|
|
// 解析NALU类型
|
|
|
|
|
|
let naluType: UInt8 = {
|
|
|
|
|
|
if data.count > 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01 {
|
|
|
|
|
|
return data[4] & 0x1F
|
|
|
|
|
|
} else if data.count > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 {
|
|
|
|
|
|
return data[3] & 0x1F
|
|
|
|
|
|
}
|
|
|
|
|
|
return 0
|
|
|
|
|
|
}()
|
|
|
|
|
|
|
2025-06-25 18:47:40 +08:00
|
|
|
|
// 缓存SPS/PPS(去除起始码)
|
2025-05-07 15:07:36 +08:00
|
|
|
|
if naluType == 7 { // SPS
|
|
|
|
|
|
cachedSps = stripStartCode(data)
|
|
|
|
|
|
result(true)
|
|
|
|
|
|
return
|
|
|
|
|
|
} else if naluType == 8 { // PPS
|
|
|
|
|
|
cachedPps = stripStartCode(data)
|
|
|
|
|
|
result(true)
|
|
|
|
|
|
return
|
|
|
|
|
|
} else if naluType == 5 { // IDR/I帧
|
2025-06-25 18:47:40 +08:00
|
|
|
|
// 提取第一个合法NALU
|
2025-05-07 15:07:36 +08:00
|
|
|
|
let firstNalu = extractFirstValidNalu(data)
|
2025-06-25 18:47:40 +08:00
|
|
|
|
if firstNalu.isEmpty {
|
|
|
|
|
|
result(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-07 15:07:36 +08:00
|
|
|
|
decoder?.decodeFrame(frameData: firstNalu, frameType: frameType, timestamp: Int64(timestamp), frameSeq: frameSeq, refIFrameSeq: refIFrameSeq, sps: cachedSps, pps: cachedPps)
|
|
|
|
|
|
} else {
|
2025-06-25 18:47:40 +08:00
|
|
|
|
// 提取第一个合法NALU
|
2025-05-07 15:07:36 +08:00
|
|
|
|
let firstNalu = extractFirstValidNalu(data)
|
2025-06-25 18:47:40 +08:00
|
|
|
|
if firstNalu.isEmpty {
|
|
|
|
|
|
result(false)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-07 15:07:36 +08:00
|
|
|
|
decoder?.decodeFrame(frameData: firstNalu, frameType: frameType, timestamp: Int64(timestamp), frameSeq: frameSeq, refIFrameSeq: refIFrameSeq)
|
|
|
|
|
|
}
|
|
|
|
|
|
result(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// 释放解码器资源
|
|
|
|
|
|
private func handleReleaseDecoder(call: FlutterMethodCall, result: @escaping FlutterResult) {
|
|
|
|
|
|
decoder?.release()
|
|
|
|
|
|
decoder = nil
|
|
|
|
|
|
if let tid = textureId {
|
|
|
|
|
|
textureRegistry?.unregisterTexture(tid)
|
|
|
|
|
|
textureId = nil
|
|
|
|
|
|
}
|
|
|
|
|
|
latestPixelBuffer = nil
|
|
|
|
|
|
hasNotifiedFlutter = false
|
|
|
|
|
|
print("[VideoDecodePlugin] 解码器和纹理已释放")
|
|
|
|
|
|
result(true)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// MARK: - FlutterTexture协议实现
|
|
|
|
|
|
public func copyPixelBuffer() -> Unmanaged<CVPixelBuffer>? {
|
|
|
|
|
|
var pixelBuffer: CVPixelBuffer?
|
|
|
|
|
|
textureQueue.sync {
|
|
|
|
|
|
pixelBuffer = self.latestPixelBuffer
|
|
|
|
|
|
}
|
|
|
|
|
|
if let pb = pixelBuffer {
|
|
|
|
|
|
return Unmanaged.passRetained(pb)
|
|
|
|
|
|
}
|
|
|
|
|
|
return nil
|
|
|
|
|
|
}
|
2025-04-21 10:56:28 +08:00
|
|
|
|
}
|