video_decode_plugin/ios/Classes/VideoDecodePlugin.swift

256 lines
9.6 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 {
2025-06-25 18:47:40 +08:00
print("[VideoDecodePlugin][错误] 参数解析失败:\(String(describing: call.arguments))")
2025-05-07 15:07:36 +08:00
result(FlutterError(code: "INVALID_ARGS", message: "参数错误", details: nil))
return
}
2025-06-25 18:47:40 +08:00
2025-05-07 15:07:36 +08:00
//
decoder?.release()
decoder = nil
if let tid = textureId {
textureRegistry?.unregisterTexture(tid)
textureId = nil
}
2025-06-25 18:47:40 +08:00
2025-05-07 15:07:36 +08:00
// Flutter
guard let registry = textureRegistry else {
2025-06-25 18:47:40 +08:00
print("[VideoDecodePlugin][错误] 无法获取纹理注册表")
2025-05-07 15:07:36 +08:00
result(FlutterError(code: "NO_TEXTURE_REGISTRY", message: "无法获取纹理注册表", details: nil))
return
}
2025-06-25 18:47:40 +08:00
2025-05-07 15:07:36 +08:00
let textureId = registry.register(self)
self.textureId = textureId
2025-06-25 18:47:40 +08:00
2025-05-07 15:07:36 +08:00
//
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)
}
2025-06-25 18:47:40 +08:00
/// NALU
2025-05-07 15:07:36 +08:00
private func stripStartCode(_ data: Data) -> Data {
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)
}
return stripped
}
2025-06-25 18:47:40 +08:00
// side_data
private func checkNaluForSideData(_ nalu: Data, naluType: UInt8) -> Bool {
let maxSize = naluType == 5 ? 150_000 : 30_000 // I150KBP30KB
if nalu.count > maxSize {
print("[VideoDecodePlugin][警告] NALU长度异常可能包含side_datatype=\(naluType)len=\(nalu.count)")
return true
}
return false
}
// NALU
private func extractFirstValidNalu(_ nalu: Data) -> Data {
// 34
var start = -1
var startCodeLength = 0
// 4
if nalu.count >= 4 && nalu[0] == 0x00 && nalu[1] == 0x00 && nalu[2] == 0x00 && nalu[3] == 0x01 {
start = 0
startCodeLength = 4
}
// 3
else if nalu.count >= 3 && nalu[0] == 0x00 && nalu[1] == 0x00 && nalu[2] == 0x01 {
start = 0
startCodeLength = 3
}
//
else {
for i in 0..<(nalu.count - 4) {
if nalu[i] == 0x00 && nalu[i + 1] == 0x00 {
if nalu[i + 2] == 0x00 && nalu[i + 3] == 0x01 {
start = i
startCodeLength = 4
break
} else if nalu[i + 2] == 0x01 {
start = i
startCodeLength = 3
break
}
}
}
}
if start == -1 {
print("[VideoDecodePlugin][警告] NALU无AnnexB起始码丢弃该帧")
return Data()
}
let searchRange = (start + startCodeLength)..<nalu.count
var end = nalu.count
// 34
for i in searchRange.lowerBound..<(nalu.count - 3) {
if nalu[i] == 0x00 && nalu[i + 1] == 0x00 {
if i + 3 < nalu.count && nalu[i + 2] == 0x00 && nalu[i + 3] == 0x01 {
end = i
break
} else if nalu[i + 2] == 0x01 {
end = i
break
}
}
}
let extractedNalu = nalu[start..<end]
// NALU
if extractedNalu.count >= startCodeLength + 1 {
let naluType = extractedNalu[startCodeLength] & 0x1F
if checkNaluForSideData(extractedNalu, naluType: naluType) {
return Data()
}
}
return extractedNalu
}
2025-05-07 15:07:36 +08:00
///
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
}()
2025-06-25 18:47:40 +08:00
// SPS/PPS
2025-05-07 15:07:36 +08:00
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
2025-06-25 18:47:40 +08:00
// NALU
2025-05-07 15:07:36 +08:00
let firstNalu = extractFirstValidNalu(data)
2025-06-25 18:47:40 +08:00
if firstNalu.isEmpty {
result(false)
return
}
2025-05-07 15:07:36 +08:00
decoder?.decodeFrame(frameData: firstNalu, frameType: frameType, timestamp: Int64(timestamp), frameSeq: frameSeq, refIFrameSeq: refIFrameSeq, sps: cachedSps, pps: cachedPps)
} else {
2025-06-25 18:47:40 +08:00
// NALU
2025-05-07 15:07:36 +08:00
let firstNalu = extractFirstValidNalu(data)
2025-06-25 18:47:40 +08:00
if firstNalu.isEmpty {
result(false)
return
}
2025-05-07 15:07:36 +08:00
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
}
2025-04-21 10:56:28 +08:00
}