import Flutter import UIKit import AVFoundation 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 { result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil)) return } // 释放旧解码器和纹理 decoder?.release() decoder = nil if let tid = textureId { textureRegistry?.unregisterTexture(tid) textureId = nil } // 注册Flutter纹理 guard let registry = textureRegistry else { result(FlutterError(code: "NO_TEXTURE_REGISTRY", message: "无法获取纹理注册表", details: nil)) return } let textureId = registry.register(self) self.textureId = textureId // 创建解码器 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) } /// 新增:去除NALU起始码的工具方法(增强日志与健壮性) private func stripStartCode(_ data: Data) -> Data { let originalLen = data.count 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 }() 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.. 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 { stripped = data.subdata(in: 3.. 0 ? (stripped[0] & 0x1F) : 0 if strippedLen < 3 || (strippedType != 7 && strippedType != 8) { print("[VideoDecodePlugin][警告] strip后NALU长度或类型异常,type=", strippedType, "len=", strippedLen) } // 只在异常时输出警告,不再输出详细内容 return stripped } /// 解码视频帧 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 }() print("[VideoDecodePlugin][调试] handleDecodeFrame: frameType=\(frameType), naluType=\(naluType), cachedSpsLen=\(cachedSps?.count ?? 0), cachedPpsLen=\(cachedPps?.count ?? 0)") // 缓存SPS/PPS(去除起始码)并立即尝试初始化解码器 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帧 // 先提取第一个合法NALU,直接推送 let firstNalu = extractFirstValidNalu(data) if firstNalu.isEmpty { return } print("[VideoDecodePlugin] 发送I帧, 长度: \(firstNalu.count), 头部: \(firstNalu.prefix(8).map { String(format: "%02X", $0) }.joined(separator: " ")), cachedSps长度: \(cachedSps?.count ?? 0), cachedPps长度: \(cachedPps?.count ?? 0)") decoder?.decodeFrame(frameData: firstNalu, frameType: frameType, timestamp: Int64(timestamp), frameSeq: frameSeq, refIFrameSeq: refIFrameSeq, sps: cachedSps, pps: cachedPps) } else { // 先提取第一个合法NALU,直接推送 let firstNalu = extractFirstValidNalu(data) if firstNalu.isEmpty { return } print("[VideoDecodePlugin] 发送P/B帧, 长度: \(firstNalu.count), 头部: \(firstNalu.prefix(8).map { String(format: "%02X", $0) }.joined(separator: " "))") 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? { var pixelBuffer: CVPixelBuffer? textureQueue.sync { pixelBuffer = self.latestPixelBuffer } if let pb = pixelBuffer { return Unmanaged.passRetained(pb) } return nil } // 新增side_data检测工具 private func checkNaluForSideData(_ nalu: Data, naluType: UInt8) -> Bool { if (naluType == 5 && nalu.count > 10000) || (naluType != 7 && naluType != 8 && nalu.count > 10000) { print("[VideoDecodePlugin][警告] NALU长度异常,可能包含side_data,type=\(naluType),len=\(nalu.count)") return true } return false } // 新增:提取第一个合法NALU工具 private func extractFirstValidNalu(_ nalu: Data) -> Data { guard let start = nalu.range(of: Data([0x00, 0x00, 0x00, 0x01]))?.lowerBound else { print("[VideoDecodePlugin][警告] NALU无AnnexB起始码,丢弃该帧") return Data() } let searchRange = (start+4)..