video_decode_plugin/ios/Classes/VideoDecodePlugin.swift

210 lines
9.2 KiB
Swift
Raw Normal View History

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_datatype=\(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
}