210 lines
9.2 KiB
Swift
210 lines
9.2 KiB
Swift
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..<data.count)
|
||
} else if data.count > 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01 {
|
||
stripped = data.subdata(in: 3..<data.count)
|
||
}
|
||
let strippedLen = stripped.count
|
||
let strippedType: UInt8 = stripped.count > 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<CVPixelBuffer>? {
|
||
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)..<nalu.count
|
||
if let next = nalu[searchRange].range(of: Data([0x00, 0x00, 0x00, 0x01]))?.lowerBound {
|
||
let end = searchRange.lowerBound + next
|
||
return nalu[start..<end]
|
||
} else {
|
||
return nalu[start..<nalu.count]
|
||
}
|
||
}
|
||
}
|