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 {
|
|
|
|
|
|
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]
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-21 10:56:28 +08:00
|
|
|
|
}
|