video_decode_plugin/ios/Classes/VideoDecodePlugin.swift
2025-05-07 15:07:36 +08:00

210 lines
9.2 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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_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]
}
}
}