Explorar el Código

[*] Swift重写, 进度50%

yanxuyao hace 1 mes
padre
commit
3dbd1cd7f8

+ 148 - 48
QGVAPlayer/QGVAPlayer/LNSwift/Core/LNControllers.swift

@@ -122,11 +122,14 @@ public final class LNAnimatedImageBufferManager: NSObject {
 
 public let kLNVAPDecoderSeekStart = "kLNVAPDecoderSeekStart"
 public let kLNVAPDecoderSeekFinish = "kLNVAPDecoderSeekFinish"
+public let kQGVAPDecoderSeekStart = kLNVAPDecoderSeekStart
+public let kQGVAPDecoderSeekFinish = kLNVAPDecoderSeekFinish
 
 @objcMembers
 open class LNBaseDecoder: NSObject {
     public dynamic var currentDecodeFrame: Int = -1
     public private(set) var fileInfo: LNBaseDFileInfo
+    public var initializationError: NSError?
 
     public required init(fileInfo: LNBaseDFileInfo) {
         self.fileInfo = fileInfo
@@ -134,6 +137,14 @@ open class LNBaseDecoder: NSObject {
         self.fileInfo.occupiedCount += 1
     }
 
+    @objc(initWith:error:)
+    public convenience init(fileInfo: LNBaseDFileInfo, error: NSErrorPointer) {
+        self.init(fileInfo: fileInfo)
+        if let err = initializationError {
+            error?.pointee = err
+        }
+    }
+
     open func decodeFrame(_ frameIndex: Int, buffers: NSMutableArray) {}
     open func shouldStopDecode(_ nextFrameIndex: Int) -> Bool { false }
     open func isFrameIndexBeyondEnd(_ frameIndex: Int) -> Bool { false }
@@ -169,8 +180,6 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
 
     private var lastDecodeFrame: Int = -1
 
-    private var constructError: NSError?
-
     @objc(errorDescriptionForCode:)
     public static func errorDescription(for code: LNMP4HWDErrorCode) -> String {
         switch code {
@@ -186,7 +195,9 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
     public required init(fileInfo: LNBaseDFileInfo) {
         super.init(fileInfo: fileInfo)
         parser = (fileInfo as? LNMP4HWDFileInfo)?.mp4Parser
-        _ = onInputStart()
+        if !onInputStart() {
+            isFinish = true
+        }
     }
 
     public override func decodeFrame(_ frameIndex: Int, buffers: NSMutableArray) {
@@ -219,11 +230,11 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
 
     private func onInputStart() -> Bool {
         guard !fileInfo.filePath.isEmpty else {
-            constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.fileNotExist.rawValue, userInfo: ["location": fileInfo.filePath])
+            initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.fileNotExist.rawValue, userInfo: ["location": fileInfo.filePath])
             return false
         }
         guard FileManager.default.fileExists(atPath: fileInfo.filePath) else {
-            constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.fileNotExist.rawValue, userInfo: ["location": fileInfo.filePath])
+            initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.fileNotExist.rawValue, userInfo: ["location": fileInfo.filePath])
             return false
         }
 
@@ -243,7 +254,7 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
         vpsData = parser.vpsData
 
         guard let spsData, let ppsData else {
-            constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
+            initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
             return false
         }
 
@@ -263,12 +274,12 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
                 formatDescriptionOut: &formatDescription
             )
             if status != noErr {
-                constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBDesc.rawValue, userInfo: ["location": fileInfo.filePath])
+                initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBDesc.rawValue, userInfo: ["location": fileInfo.filePath])
                 return false
             }
         } else if parser.videoCodecID == .h265 {
             guard #available(iOS 11.0, *), let vpsData else {
-                constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
+                initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
                 return false
             }
             guard vpsData.count > 0, spsData.count > 0, ppsData.count > 0 else { return false }
@@ -288,7 +299,7 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
                 formatDescriptionOut: &formatDescription
             )
             if status != noErr {
-                constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBDesc.rawValue, userInfo: ["location": fileInfo.filePath])
+                initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBDesc.rawValue, userInfo: ["location": fileInfo.filePath])
                 return false
             }
         }
@@ -299,7 +310,7 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
     private func createDecompressionSession() -> Bool {
         guard let formatDescription else { return false }
 
-        var pixelFormat: UInt32 = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
+        let pixelFormat: UInt32 = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
         let attrs = [
             kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: pixelFormat)
         ] as NSDictionary
@@ -314,7 +325,7 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
         )
 
         if status != noErr {
-            constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBSession.rawValue, userInfo: ["location": fileInfo.filePath])
+            initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBSession.rawValue, userInfo: ["location": fileInfo.filePath])
             return false
         }
         return true
@@ -420,7 +431,7 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
         guard let pixelBuffer = pixelBuffer else { return }
 
         let newFrame = LNMP4AnimatedImageFrame()
-        newFrame.pixelBuffer = pixelBuffer as? CVPixelBuffer
+        newFrame.pixelBuffer = pixelBuffer
         newFrame.frameIndex = frameIndex
         newFrame.decodeTime = Date().timeIntervalSince(startDate) * 1000.0
         newFrame.defaultFps = Int32(fps)
@@ -502,6 +513,11 @@ public final class LNAnimatedImageDecodeManager: NSObject {
         setupAudioPlayerIfNeed()
     }
 
+    @objc(initWith:config:delegate:)
+    public convenience init(_ fileInfo: LNBaseDFileInfo, config: LNAnimatedImageDecodeConfig, delegate: LNAnimatedImageDecoderDelegate?) {
+        self.init(fileInfo: fileInfo, config: config, delegate: delegate)
+    }
+
     @objc(consumeDecodedFrame:)
     public func consumeDecodedFrame(_ frameIndex: Int) -> LNBaseAnimatedImageFrame? {
         objc_sync_enter(self)
@@ -579,9 +595,12 @@ public final class LNAnimatedImageDecodeManager: NSObject {
     private func createDecoders(by config: LNAnimatedImageDecodeConfig) {
         guard let decoderDelegate else { return }
         for _ in 0..<max(config.threadCount, 1) {
-            let decoderClass = decoderDelegate.decoderClass(for: self)
-            guard let cls = decoderClass as? LNBaseDecoder.Type else { continue }
+            guard let cls = decoderDelegate.decoderClass(for: self) as? LNBaseDecoder.Type else { continue }
             let decoder = cls.init(fileInfo: fileInfo)
+            if let error = decoder.initializationError {
+                decoderDelegate.decoderDidFailDecode?(nil, error: error)
+                break
+            }
             decoders.append(decoder)
         }
     }
@@ -631,6 +650,11 @@ public final class LNVAPConfigManager: NSObject {
         setupConfig()
     }
 
+    @objc(initWith:)
+    public convenience init(_ fileInfo: LNMP4HWDFileInfo) {
+        self.init(fileInfo: fileInfo)
+    }
+
     @objc(loadConfigResources)
     public func loadConfigResources() {
         if model.resources.isEmpty {
@@ -645,7 +669,7 @@ public final class LNVAPConfigManager: NSObject {
         }
 
         guard let loadImage = delegate?.vap_loadImageWithURL else {
-            delegate?.onVAPConfigResourcesLoaded(model, error: nil)
+            // Keep parity with OC: if no image loader delegate, return directly.
             return
         }
 
@@ -701,48 +725,64 @@ public final class LNVAPConfigManager: NSObject {
     private func setupConfig() {
         guard let vapc = fileInfo.mp4Parser?.rootBox?.subBox(ofType: .vapc) else {
             hasValidConfig = false
+            LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "config can not find vapc box")
             return
         }
 
         hasValidConfig = true
         guard let vapcData = fileInfo.mp4Parser?.readData(of: vapc, length: Int(vapc.length - 8), offset: 8),
               let dict = (try? JSONSerialization.jsonObject(with: vapcData)) as? [String: Any] else {
+            LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "fail to parse config as dictionary")
             return
         }
         parseConfigDictionary(dict)
     }
 
     private func parseConfigDictionary(_ configDic: [String: Any]) {
-        guard let commonInfoDic = configDic["info"] as? [String: Any] else { return }
-        let sourcesArr = configDic["src"] as? [[String: Any]] ?? []
-        let framesArr = configDic["frame"] as? [[String: Any]] ?? []
+        guard let commonInfoDic = lnDictionary(configDic["info"]) else {
+            LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "has no commonInfoDic")
+            return
+        }
+        let sourcesArr = lnArray(configDic["src"])
+        let framesArr = lnArray(configDic["frame"])
 
         let configModel = LNVAPConfigModel()
         self.model = configModel
 
         let commonInfo = LNVAPCommonInfo()
-        commonInfo.version = commonInfoDic["v"] as? Int ?? 0
-        commonInfo.framesCount = commonInfoDic["f"] as? Int ?? 0
-        commonInfo.size = CGSize(width: commonInfoDic["w"] as? CGFloat ?? 0, height: commonInfoDic["h"] as? CGFloat ?? 0)
-        commonInfo.videoSize = CGSize(width: commonInfoDic["videoW"] as? CGFloat ?? 0, height: commonInfoDic["videoH"] as? CGFloat ?? 0)
-        commonInfo.targetOrientaion = LNVAPOrientation(rawValue: commonInfoDic["orien"] as? Int ?? 0) ?? .none
-        commonInfo.fps = commonInfoDic["fps"] as? Int ?? 0
-        commonInfo.isMerged = (commonInfoDic["isVapx"] as? Int ?? 0) == 1
+        commonInfo.version = lnInt(commonInfoDic["v"])
+        commonInfo.framesCount = lnInt(commonInfoDic["f"])
+        commonInfo.size = CGSize(width: lnFloat(commonInfoDic["w"]), height: lnFloat(commonInfoDic["h"]))
+        commonInfo.videoSize = CGSize(width: lnFloat(commonInfoDic["videoW"]), height: lnFloat(commonInfoDic["videoH"]))
+        commonInfo.targetOrientaion = LNVAPOrientation(rawValue: lnInt(commonInfoDic["orien"])) ?? .none
+        commonInfo.fps = lnInt(commonInfoDic["fps"])
+        commonInfo.isMerged = lnInt(commonInfoDic["isVapx"]) == 1
+        commonInfo.alphaAreaRect = lnRect(commonInfoDic["aFrame"])
+        commonInfo.rgbAreaRect = lnRect(commonInfoDic["rgbFrame"])
         configModel.info = commonInfo
 
         fileInfo.mp4Parser?.fps = commonInfo.fps
 
         var sources: [String: LNVAPSourceInfo] = [:]
-        for sourceDic in sourcesArr {
-            guard let sourceID = sourceDic["srcId"] as? String else { continue }
+        for sourceObj in sourcesArr {
+            guard let sourceDic = lnDictionary(sourceObj) else {
+                LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "sourceDic is not dic")
+                continue
+            }
+            let sourceID = lnString(sourceDic["srcId"])
+            guard !sourceID.isEmpty else {
+                LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "has no sourceID")
+                continue
+            }
             let sourceInfo = LNVAPSourceInfo()
-            sourceInfo.type = sourceDic["srcType"] as? String
-            sourceInfo.loadType = sourceDic["loadType"] as? String
-            sourceInfo.contentTag = sourceDic["srcTag"] as? String
-            sourceInfo.style = sourceDic["style"] as? String
-            sourceInfo.fitType = sourceDic["fitType"] as? String
-            sourceInfo.size = CGSize(width: sourceDic["w"] as? CGFloat ?? 0, height: sourceDic["h"] as? CGFloat ?? 0)
-            if let colorHex = sourceDic["color"] as? String {
+            sourceInfo.type = lnString(sourceDic["srcType"])
+            sourceInfo.loadType = lnString(sourceDic["loadType"])
+            sourceInfo.contentTag = lnString(sourceDic["srcTag"])
+            sourceInfo.style = lnString(sourceDic["style"])
+            sourceInfo.fitType = lnString(sourceDic["fitType"])
+            sourceInfo.size = CGSize(width: lnFloat(sourceDic["w"]), height: lnFloat(sourceDic["h"]))
+            let colorHex = lnString(sourceDic["color"])
+            if !colorHex.isEmpty {
                 sourceInfo.color = UIColor.lnColor(hex: colorHex)
             }
             sources[sourceID] = sourceInfo
@@ -750,26 +790,33 @@ public final class LNVAPConfigManager: NSObject {
         configModel.resources = Array(sources.values)
 
         var mergedConfig: [NSNumber: [LNVAPMergedInfo]] = [:]
-        for frameMergedDic in framesArr {
-            let frameIndex = frameMergedDic["i"] as? Int ?? 0
-            let mergedObjs = frameMergedDic["obj"] as? [[String: Any]] ?? []
+        for frameObj in framesArr {
+            guard let frameMergedDic = lnDictionary(frameObj) else {
+                LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "frameMergedDic is not dic")
+                continue
+            }
+            let frameIndex = lnInt(frameMergedDic["i"])
+            let mergedObjs = lnArray(frameMergedDic["obj"])
             var mergedInfos: [LNVAPMergedInfo] = []
 
-            for mergeInfoDic in mergedObjs {
-                guard let sourceID = mergeInfoDic["srcId"] as? String,
-                      let sourceInfo = sources[sourceID] else { continue }
+            for mergeObj in mergedObjs {
+                guard let mergeInfoDic = lnDictionary(mergeObj) else {
+                    LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "mergeInfoDic is not dic")
+                    continue
+                }
+                let sourceID = lnString(mergeInfoDic["srcId"])
+                guard let sourceInfo = sources[sourceID] else {
+                    LNVAPLogger.log(level: 3, file: #file, line: #line, func: #function, module: "LNVAPConfigManager", message: "sourceInfo is nil")
+                    continue
+                }
 
                 let mergeInfo = LNVAPMergedInfo()
                 mergeInfo.source = sourceInfo
-                mergeInfo.renderIndex = mergeInfoDic["z"] as? Int ?? 0
+                mergeInfo.renderIndex = lnInt(mergeInfoDic["z"])
                 mergeInfo.needMask = mergeInfoDic["mFrame"] != nil
-                if let frame = mergeInfoDic["frame"] as? [CGFloat], frame.count == 4 {
-                    mergeInfo.renderRect = CGRect(x: frame[0], y: frame[1], width: frame[2], height: frame[3])
-                }
-                if let mFrame = mergeInfoDic["mFrame"] as? [CGFloat], mFrame.count == 4 {
-                    mergeInfo.maskRect = CGRect(x: mFrame[0], y: mFrame[1], width: mFrame[2], height: mFrame[3])
-                }
-                mergeInfo.maskRotation = mergeInfoDic["mt"] as? Int ?? 0
+                mergeInfo.renderRect = lnRect(mergeInfoDic["frame"])
+                mergeInfo.maskRect = lnRect(mergeInfoDic["mFrame"])
+                mergeInfo.maskRotation = lnInt(mergeInfoDic["mt"])
                 mergedInfos.append(mergeInfo)
             }
             mergedConfig[NSNumber(value: frameIndex)] = mergedInfos.sorted(by: { $0.renderIndex < $1.renderIndex })
@@ -779,6 +826,59 @@ public final class LNVAPConfigManager: NSObject {
     }
 }
 
+private extension LNVAPConfigManager {
+    func lnInt(_ value: Any?) -> Int {
+        if let n = value as? NSNumber { return n.intValue }
+        if let s = value as? NSString { return s.integerValue }
+        if let s = value as? String { return Int(s) ?? 0 }
+        return 0
+    }
+
+    func lnFloat(_ value: Any?) -> CGFloat {
+        if let n = value as? NSNumber { return CGFloat(n.doubleValue) }
+        if let s = value as? NSString { return CGFloat(s.doubleValue) }
+        if let s = value as? String { return CGFloat(Double(s) ?? 0) }
+        return 0
+    }
+
+    func lnString(_ value: Any?) -> String {
+        if let s = value as? String { return s }
+        if let s = value as? NSString { return s as String }
+        if let n = value as? NSNumber { return n.stringValue }
+        return ""
+    }
+
+    func lnArray(_ value: Any?) -> [Any] {
+        if let arr = value as? [Any] { return arr }
+        if let arr = value as? NSArray { return arr.compactMap { $0 } }
+        return []
+    }
+
+    func lnDictionary(_ value: Any?) -> [String: Any]? {
+        if let dict = value as? [String: Any] { return dict }
+        if let dict = value as? NSDictionary {
+            var out: [String: Any] = [:]
+            for (k, v) in dict {
+                if let key = k as? String {
+                    out[key] = v
+                }
+            }
+            return out
+        }
+        return nil
+    }
+
+    func lnRect(_ value: Any?) -> CGRect {
+        if let arr = value as? NSArray {
+            return arr.ln_rectValue()
+        }
+        if let arr = value as? [Any] {
+            return (arr as NSArray).ln_rectValue()
+        }
+        return .zero
+    }
+}
+
 private extension UIColor {
     static func lnColor(hex: String) -> UIColor? {
         let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)

+ 22 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Core/LNPlaybackTypes.swift

@@ -43,6 +43,17 @@ public typealias LNVAPGestureEventBlock = (_ gestureRecognizer: UIGestureRecogni
     @objc optional func lnPlayerLoadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion)
 }
 
+@objc public protocol LNVAPLegacyPlaybackDelegate: AnyObject {
+    @objc optional func shouldStartPlayMP4(_ container: UIView, config: LNVAPConfigModel) -> Bool
+    @objc optional func viewDidStartPlayMP4(_ container: UIView)
+    @objc optional func viewDidPlayMP4AtFrame(_ frame: LNMP4AnimatedImageFrame, view container: UIView)
+    @objc optional func viewDidStopPlayMP4(_ lastFrameIndex: Int, view container: UIView)
+    @objc optional func viewDidFinishPlayMP4(_ totalFrameCount: Int, view container: UIView)
+    @objc optional func viewDidFailPlayMP4(_ error: NSError)
+    @objc optional func contentForVapTag(_ tag: String, resource: LNVAPSourceInfo) -> String?
+    @objc optional func loadVapImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion)
+}
+
 @objc public protocol LNVAPWrapPlaybackDelegate: AnyObject {
     @objc optional func lnWrapViewShouldStart(_ wrapView: LNVAPWrapView, config: LNVAPConfigModel) -> Bool
     @objc optional func lnWrapViewDidStart(_ wrapView: LNVAPWrapView)
@@ -53,3 +64,14 @@ public typealias LNVAPGestureEventBlock = (_ gestureRecognizer: UIGestureRecogni
     @objc optional func lnWrapViewContent(forTag tag: String, resource: LNVAPSourceInfo) -> String?
     @objc optional func lnWrapViewLoadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion)
 }
+
+@objc public protocol LNVAPWrapLegacyPlaybackDelegate: AnyObject {
+    @objc optional func vapWrap_viewshouldStartPlayMP4(_ container: UIView, config: LNVAPConfigModel) -> Bool
+    @objc optional func vapWrap_viewDidStartPlayMP4(_ container: UIView)
+    @objc optional func vapWrap_viewDidPlayMP4AtFrame(_ frame: LNMP4AnimatedImageFrame, view container: UIView)
+    @objc optional func vapWrap_viewDidStopPlayMP4(_ lastFrameIndex: Int, view container: UIView)
+    @objc optional func vapWrap_viewDidFinishPlayMP4(_ totalFrameCount: Int, view container: UIView)
+    @objc optional func vapWrap_viewDidFailPlayMP4(_ error: NSError)
+    @objc optional func vapWrapview_contentForVapTag(_ tag: String, resource: LNVAPSourceInfo) -> String?
+    @objc optional func vapWrapView_loadVapImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion)
+}

+ 71 - 20
QGVAPlayer/QGVAPlayer/LNSwift/Model/LNModels.swift

@@ -197,19 +197,33 @@ public final class LNVAPMergedInfo: NSObject {
 
 @objcMembers
 public final class LNVAPMaskInfo: NSObject {
-    public var data: Data = Data()
+    public var data: Data = Data() {
+        didSet { cachedTexture = nil }
+    }
     public var sampleRect: CGRect = .zero
-    public var dataSize: CGSize = .zero
+    public var dataSize: CGSize = .zero {
+        didSet { cachedTexture = nil }
+    }
     public var blurLength: Int = 0
 
     private var cachedTexture: MTLTexture?
+    private var cachedDeviceID: ObjectIdentifier?
 
-    public var texture: MTLTexture? {
-        if cachedTexture == nil {
-            cachedTexture = LNVAPTextureLoader.loadTexture(withData: data, device: MTLCreateSystemDefaultDevice(), width: dataSize.width, height: dataSize.height)
+    @objc(textureWithDevice:)
+    public func texture(with device: MTLDevice?) -> MTLTexture? {
+        guard let device else { return nil }
+        let deviceID = ObjectIdentifier(device)
+        if let cachedTexture, cachedDeviceID == deviceID {
+            return cachedTexture
         }
+        cachedTexture = LNVAPTextureLoader.loadTexture(withData: data, device: device, width: dataSize.width, height: dataSize.height)
+        cachedDeviceID = cachedTexture == nil ? nil : deviceID
         return cachedTexture
     }
+
+    public var texture: MTLTexture? {
+        texture(with: MTLCreateSystemDefaultDevice())
+    }
 }
 
 @objcMembers
@@ -241,6 +255,11 @@ public final class LNVAPTextureLoader: NSObject {
     @objc(getAppropriateFontWith:rect:designedSize:bold:textSize:)
     public static func getAppropriateFont(with text: String?, rect fitFrame: CGRect, designedSize: CGFloat, bold isBold: Bool, textSize: UnsafeMutablePointer<CGSize>?) -> UIFont? { nil }
 #else
+    private struct LNVAPAttachmentFragmentParameter {
+        var needOriginRGB: Int32
+        var fillColor: SIMD4<Float>
+    }
+
     @objc(loadVapColorFillBufferWith:device:)
     public static func loadVapColorFillBuffer(with color: UIColor?, device: MTLDevice?) -> MTLBuffer? {
         guard let device else { return nil }
@@ -249,33 +268,55 @@ public final class LNVAPTextureLoader: NSObject {
         var blue: CGFloat = 0
         var alpha: CGFloat = 0
         if let color { color.getRed(&red, green: &green, blue: &blue, alpha: &alpha) }
-
-        var params = [Float(color == nil ? 1 : 0), Float(red), Float(green), Float(blue), Float(alpha)]
-        return device.makeBuffer(bytes: &params, length: params.count * MemoryLayout<Float>.size, options: .storageModeShared)
+        var params = LNVAPAttachmentFragmentParameter(
+            needOriginRGB: color == nil ? 1 : 0,
+            fillColor: SIMD4<Float>(Float(red), Float(green), Float(blue), Float(alpha))
+        )
+        return device.makeBuffer(bytes: &params, length: MemoryLayout<LNVAPAttachmentFragmentParameter>.stride, options: .storageModeShared)
     }
 
     @objc(loadTextureWithImage:device:)
     public static func loadTexture(with image: UIImage?, device: MTLDevice?) -> MTLTexture? {
-        guard let image, let device else { return nil }
+        guard let image else {
+            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "attempt to loadTexture with nil image")
+            return nil
+        }
+        guard let device else { return nil }
+        guard let cgImage = image.cgImage else {
+            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "attempt to loadTexture with invalid CGImage")
+            return nil
+        }
         if #available(iOS 10.0, *) {
             let loader = MTKTextureLoader(device: device)
-            return try? loader.newTexture(cgImage: image.cgImage!, options: [
-                .origin: MTKTextureLoader.Origin.flippedVertically,
-                .SRGB: NSNumber(value: false)
-            ])
+            do {
+                return try loader.newTexture(cgImage: cgImage, options: [
+                    .origin: MTKTextureLoader.Origin.flippedVertically,
+                    .SRGB: NSNumber(value: false)
+                ])
+            } catch {
+                LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "loadTexture error: \(error.localizedDescription)")
+                return nil
+            }
         }
         return cgLoadTexture(with: image, device: device)
     }
 
     @objc(loadTextureWithData:device:width:height:)
     public static func loadTexture(withData data: Data?, device: MTLDevice?, width: CGFloat, height: CGFloat) -> MTLTexture? {
-        guard let data, let device, !data.isEmpty else { return nil }
-        let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Unorm, width: Int(width), height: Int(height), mipmapped: false)
+        guard let data, !data.isEmpty else {
+            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "attempt to loadTexture with nil data")
+            return nil
+        }
+        guard let device else { return nil }
+        let textureWidth = Int(width)
+        let textureHeight = Int(height)
+        guard textureWidth > 0, textureHeight > 0 else { return nil }
+        let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Unorm, width: textureWidth, height: textureHeight, mipmapped: false)
         guard let texture = device.makeTexture(descriptor: descriptor) else { return nil }
-        let region = MTLRegionMake3D(0, 0, 0, Int(width), Int(height), 1)
+        let region = MTLRegionMake3D(0, 0, 0, textureWidth, textureHeight, 1)
         data.withUnsafeBytes { ptr in
             if let base = ptr.baseAddress {
-                texture.replace(region: region, mipmapLevel: 0, withBytes: base, bytesPerRow: Int(width))
+                texture.replace(region: region, mipmapLevel: 0, withBytes: base, bytesPerRow: textureWidth)
             }
         }
         return texture
@@ -283,12 +324,18 @@ public final class LNVAPTextureLoader: NSObject {
 
     @objc(drawingImageForText:color:size:bold:)
     public static func drawingImage(forText textStr: String?, color: UIColor?, size: CGSize, bold: Bool) -> UIImage? {
-        guard let textStr, !textStr.isEmpty else { return nil }
+        guard let textStr, !textStr.isEmpty else {
+            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "draw text resource fail because text is nil")
+            return nil
+        }
         let textColor = color ?? .black
         let rect = CGRect(x: 0, y: 0, width: size.width / 2.0, height: size.height / 2.0)
 
         var textSize = CGSize.zero
-        guard let font = getAppropriateFont(with: textStr, rect: rect, designedSize: rect.height * 0.8, bold: bold, textSize: &textSize) else { return nil }
+        guard let font = getAppropriateFont(with: textStr, rect: rect, designedSize: rect.height * 0.8, bold: bold, textSize: &textSize) else {
+            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "draw text resource \(textStr) failed because font is nil")
+            return nil
+        }
 
         let paragraphStyle = NSMutableParagraphStyle()
         paragraphStyle.alignment = .center
@@ -332,7 +379,10 @@ public final class LNVAPTextureLoader: NSObject {
 
         guard let colorSpace = CGColorSpace(name: CGColorSpace.sRGB),
               let rawData = calloc(height * width * bytesPerPixel, MemoryLayout<UInt8>.size),
-              let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) else { return nil }
+              let context = CGContext(data: rawData, width: width, height: height, bitsPerComponent: 8, bytesPerRow: bytesPerRow, space: colorSpace, bitmapInfo: CGImageAlphaInfo.premultipliedLast.rawValue | CGBitmapInfo.byteOrder32Big.rawValue) else {
+            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "cg_loadTexture alloc context failed")
+            return nil
+        }
 
         context.translateBy(x: 0, y: CGFloat(height))
         context.scaleBy(x: 1, y: -1)
@@ -341,6 +391,7 @@ public final class LNVAPTextureLoader: NSObject {
         let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: width, height: height, mipmapped: false)
         guard let texture = device.makeTexture(descriptor: descriptor) else {
             free(rawData)
+            LNVAPLogger.log(level: 2, file: #file, line: #line, func: #function, module: "common", message: "cg_loadTexture failed getting texture")
             return nil
         }
 

+ 149 - 26
QGVAPlayer/QGVAPlayer/LNSwift/Parser/LNMP4Parser.swift

@@ -5,6 +5,39 @@ public let kLNBoxTypeLengthInBytes = 4
 public let kLNBoxLargeSizeLengthInBytes = 8
 public let kLNBoxLargeSizeFlagLengthInBytes = 1
 
+// Legacy aliases for QG-prefixed MP4 parser constants.
+public let kQGBoxSizeLengthInBytes = kLNBoxSizeLengthInBytes
+public let kQGBoxTypeLengthInBytes = kLNBoxTypeLengthInBytes
+public let kQGBoxLargeSizeLengthInBytes = kLNBoxLargeSizeLengthInBytes
+public let kQGBoxLargeSizeFlagLengthInBytes = kLNBoxLargeSizeFlagLengthInBytes
+
+// Legacy aliases for QG-prefixed Swift type references.
+public typealias QGMP4CodecType = LNMP4CodecType
+public typealias QGMP4TrackType = LNMP4TrackType
+public typealias QGMP4VideoStreamCodecID = LNMP4VideoStreamCodecID
+public typealias QGMP4BoxType = LNMP4BoxType
+public typealias QGMP4Box = LNMP4Box
+public typealias QGMP4MdatBox = LNMP4MdatBox
+public typealias QGMP4AvccBox = LNMP4AvccBox
+public typealias QGMP4HvccBox = LNMP4HvccBox
+public typealias QGMP4MvhdBox = LNMP4MvhdBox
+public typealias QGMP4StsdBox = LNMP4StsdBox
+public typealias QGMP4TrackBox = LNMP4TrackBox
+public typealias QGStscEntry = LNStscEntry
+public typealias QGMP4StscBox = LNMP4StscBox
+public typealias QGMP4StcoBox = LNMP4StcoBox
+public typealias QGMP4StssBox = LNMP4StssBox
+public typealias QGMP4CttsBox = LNMP4CttsBox
+public typealias QGSttsEntry = LNSttsEntry
+public typealias QGMP4SttsBox = LNMP4SttsBox
+public typealias QGMP4StszBox = LNMP4StszBox
+public typealias QGMP4HdlrBox = LNMP4HdlrBox
+public typealias QGMP4Sample = LNMP4Sample
+public typealias QGChunkOffsetEntry = LNChunkOffsetEntry
+public typealias QGMP4BoxFactory = LNMP4BoxFactory
+public typealias QGMP4Parser = LNMP4Parser
+public typealias QGMP4ParserProxy = LNMP4ParserProxy
+
 private func lnReadU32(_ bytes: UnsafePointer<UInt8>, _ offset: Int) -> UInt32 {
     (UInt32(bytes[offset]) << 24)
     | (UInt32(bytes[offset + 1]) << 16)
@@ -403,7 +436,7 @@ public final class LNMP4BoxFactory: NSObject {
 @objc
 public protocol LNMP4ParserDelegate: AnyObject {
     @objc optional func didParseMP4Box(_ box: LNMP4Box, parser: LNMP4Parser)
-    @objc optional func mp4FileDidFinishParse(_ parser: LNMP4Parser)
+    @objc(MP4FileDidFinishParse:) optional func mp4FileDidFinishParse(_ parser: LNMP4Parser)
 }
 
 @objcMembers
@@ -420,6 +453,7 @@ public final class LNMP4Parser: NSObject {
         super.init()
     }
 
+
     deinit {
         fileHandle?.closeFile()
     }
@@ -454,6 +488,10 @@ public final class LNMP4Parser: NSObject {
                 guard let parsed = readBoxTypeAndLength(offset: offset) else { break }
                 let type = parsed.type
                 let length = parsed.length
+                if length == 0 {
+                    // Invalid box length; avoid infinite loop on malformed data.
+                    break
+                }
 
                 if offset + length > calBox.startIndexInBytes + calBox.length {
                     break
@@ -532,6 +570,9 @@ public final class LNMP4Parser: NSObject {
             }
             if finalLength == 0 { return nil }
         }
+        if finalLength == 0 {
+            return nil
+        }
 
         return (type, finalLength)
     }
@@ -567,27 +608,119 @@ public final class LNMP4Parser: NSObject {
 public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
     private var parser: LNMP4Parser
 
-    public var picWidth: Int = 0
-    public var picHeight: Int = 0
-    public var fps: Int = 0
-    public var duration: Double = 0
+    private var _picWidth: Int = 0
+    private var _picHeight: Int = 0
+    private var _fps: Int = 0
+    private var _duration: Double = 0
     public var spsData: Data?
     public var ppsData: Data?
-    public var videoSamples: [LNMP4Sample] = []
-    public var videoSyncSampleIndexes: [NSNumber] = []
+    private var _videoSamples: [LNMP4Sample]?
+    private var _videoSyncSampleIndexes: [NSNumber]?
     public var rootBox: LNMP4Box?
     public var videoTrackBox: LNMP4TrackBox?
     public var audioTrackBox: LNMP4TrackBox?
     public var vpsData: Data?
     public var videoCodecID: LNMP4VideoStreamCodecID = .unknown
 
+    public var picWidth: Int {
+        get {
+            if _picWidth == 0 {
+                _picWidth = readPicWidth()
+            }
+            return _picWidth
+        }
+        set {
+            _picWidth = newValue
+        }
+    }
+
+    public var picHeight: Int {
+        get {
+            if _picHeight == 0 {
+                _picHeight = readPicHeight()
+            }
+            return _picHeight
+        }
+        set {
+            _picHeight = newValue
+        }
+    }
+
+    public var duration: Double {
+        get {
+            if _duration == 0 {
+                _duration = readDuration()
+            }
+            return _duration
+        }
+        set {
+            _duration = newValue
+        }
+    }
+
+    public var videoSamples: [LNMP4Sample] {
+        get {
+            if _videoSamples == nil {
+                _videoSamples = buildVideoSamples()
+            }
+            return _videoSamples ?? []
+        }
+        set {
+            _videoSamples = newValue
+        }
+    }
+
+    public var videoSyncSampleIndexes: [NSNumber] {
+        get {
+            if _videoSyncSampleIndexes == nil {
+                if let stss = videoTrackBox?.subBox(ofType: .stss) as? LNMP4StssBox {
+                    _videoSyncSampleIndexes = stss.syncSamples
+                } else {
+                    _videoSyncSampleIndexes = []
+                }
+            }
+            return _videoSyncSampleIndexes ?? []
+        }
+        set {
+            _videoSyncSampleIndexes = newValue
+        }
+    }
+
+    public var fps: Int {
+        get {
+            if _fps == 0 {
+                let samples = videoSamples
+                let totalDuration = duration
+                if samples.isEmpty || totalDuration <= 0 {
+                    return 0
+                }
+                _fps = Int(lround(Double(samples.count) / totalDuration))
+            }
+            return _fps
+        }
+        set {
+            _fps = newValue
+        }
+    }
+
     public init(filePath: String) {
         parser = LNMP4Parser(filePath: filePath)
         super.init()
         parser.delegate = self
     }
 
+
     public func parse() {
+        _picWidth = 0
+        _picHeight = 0
+        _fps = 0
+        _duration = 0
+        _videoSamples = nil
+        _videoSyncSampleIndexes = nil
+        spsData = nil
+        ppsData = nil
+        vpsData = nil
+
         parser.parse()
         rootBox = parser.rootBox
         parseVideoDecoderConfigRecord()
@@ -595,9 +728,6 @@ public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
 
     @objc(readPacketOfSample:)
     public func readPacket(ofSample sampleIndex: Int) -> Data? {
-        if videoSamples.isEmpty {
-            videoSamples = buildVideoSamples()
-        }
         guard sampleIndex >= 0, sampleIndex < videoSamples.count else { return nil }
         guard let fileHandle = parser.fileHandle else { return nil }
         let sample = videoSamples[sampleIndex]
@@ -607,7 +737,9 @@ public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
 
     @objc(readDataOfBox:length:offset:)
     public func readData(of box: LNMP4Box, length: Int, offset: Int) -> Data? {
-        guard length > 0, offset + length <= box.length else { return nil }
+        guard length > 0, offset >= 0 else { return nil }
+        let end = UInt64(offset) + UInt64(length)
+        guard end <= box.length else { return nil }
         guard let fileHandle = parser.fileHandle else { return nil }
         fileHandle.seek(toFileOffset: box.startIndexInBytes + UInt64(offset))
         return fileHandle.readData(ofLength: length)
@@ -620,21 +752,6 @@ public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
         } else if videoCodecID == .h265 {
             parseHvccDecoderConfigRecord()
         }
-
-        if picWidth == 0 { picWidth = readPicWidth() }
-        if picHeight == 0 { picHeight = readPicHeight() }
-        if duration == 0 { duration = readDuration() }
-
-        if videoSamples.isEmpty {
-            videoSamples = buildVideoSamples()
-        }
-        if fps == 0, duration > 0 {
-            fps = Int(lround(Double(videoSamples.count) / duration))
-        }
-
-        if let stss = videoTrackBox?.subBox(ofType: .stss) as? LNMP4StssBox {
-            videoSyncSampleIndexes = stss.syncSamples
-        }
     }
 
     private func buildVideoSamples() -> [LNMP4Sample] {
@@ -647,6 +764,7 @@ public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
         }
 
         let ctts = videoTrackBox.subBox(ofType: .ctts) as? LNMP4CttsBox
+        let keySampleIndexSet = Set(videoSyncSampleIndexes.map { $0.intValue })
 
         var samples: [LNMP4Sample] = []
         var ptsAccumulator: UInt64 = 0
@@ -683,7 +801,9 @@ public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
             sample.sampleDelta = sttsEntry.sampleDelta
             sample.sampleSize = stsz.sampleSizes[i].uint32Value
             sample.pts = ptsAccumulator + UInt64(cttsValue)
+            sample.dts = ptsAccumulator
             sample.streamOffset = sampleOffset
+            sample.isKeySample = keySampleIndexSet.contains(i)
             samples.append(sample)
 
             stscEntrySampleOffset += sample.sampleSize
@@ -724,10 +844,12 @@ public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
 
         let bytes = [UInt8](extraData)
         var index = 30
+        guard index < bytes.count else { return }
         let arrayNum = Int(bytes[index])
         index += 1
 
         for _ in 0..<arrayNum {
+            guard index + 2 <= bytes.count else { return }
             let value = Int(bytes[index])
             index += 1
             let naluType = value & 0x3F
@@ -735,6 +857,7 @@ public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
             index += 2
 
             for _ in 0..<naluNum {
+                guard index + 2 <= bytes.count else { return }
                 let naluLength = (Int(bytes[index]) << 8) + Int(bytes[index + 1])
                 index += 2
                 guard index + naluLength <= bytes.count else { return }

+ 33 - 198
QGVAPlayer/QGVAPlayer/LNSwift/Render/LNRenderers.swift

@@ -22,94 +22,6 @@ import simd
     func onViewUnavailableStatus()
 }
 
-private enum LNLegacyRuntime {
-    static func cls(_ name: String) -> AnyClass? {
-        NSClassFromString(name) ?? NSClassFromString("QGVAPlayer.\(name)")
-    }
-
-    static func instantiate(_ className: String) -> NSObject? {
-        guard let t = cls(className) as? NSObject.Type else { return nil }
-        return t.init()
-    }
-
-    static func set(_ obj: NSObject, _ key: String, _ value: Any?) {
-        let setter = "set\(key.prefix(1).uppercased())\(key.dropFirst()):"
-        let sel = NSSelectorFromString(setter)
-        guard obj.responds(to: sel) else { return }
-        obj.setValue(value, forKey: key)
-    }
-
-    static func callInitWithFrame(_ obj: NSObject, frame: CGRect) -> AnyObject? {
-        let sel = NSSelectorFromString("initWithFrame:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return nil }
-        typealias Fn = @convention(c) (AnyObject, Selector, CGRect) -> AnyObject
-        return unsafeBitCast(imp, to: Fn.self)(obj, sel, frame)
-    }
-
-    static func callInitWithFrameBlend(_ obj: NSObject, frame: CGRect, blend: Int) -> AnyObject? {
-        let sel = NSSelectorFromString("initWithFrame:blendMode:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return nil }
-        typealias Fn = @convention(c) (AnyObject, Selector, CGRect, Int) -> AnyObject
-        return unsafeBitCast(imp, to: Fn.self)(obj, sel, frame, blend)
-    }
-
-    static func callInitWithMetalLayer(_ obj: NSObject, layer: AnyObject) -> AnyObject? {
-        let sel = NSSelectorFromString("initWithMetalLayer:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return nil }
-        typealias Fn = @convention(c) (AnyObject, Selector, AnyObject) -> AnyObject
-        return unsafeBitCast(imp, to: Fn.self)(obj, sel, layer)
-    }
-
-    static func callInitWithMetalLayerBlend(_ obj: NSObject, layer: AnyObject, blend: Int) -> AnyObject? {
-        let sel = NSSelectorFromString("initWithMetalLayer:blendMode:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return nil }
-        typealias Fn = @convention(c) (AnyObject, Selector, AnyObject, Int) -> AnyObject
-        return unsafeBitCast(imp, to: Fn.self)(obj, sel, layer, blend)
-    }
-
-    static func callRenderPixelBuffer(_ obj: NSObject, pixelBuffer: CVPixelBuffer?, layer: AnyObject?) {
-        let sel = NSSelectorFromString("renderPixelBuffer:metalLayer:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return }
-        typealias Fn = @convention(c) (AnyObject, Selector, CVPixelBuffer?, AnyObject?) -> Void
-        unsafeBitCast(imp, to: Fn.self)(obj, sel, pixelBuffer, layer)
-    }
-
-    static func callRenderPixelBufferMerge(_ obj: NSObject, pixelBuffer: CVPixelBuffer?, layer: AnyObject?, infos: NSArray?) {
-        let sel = NSSelectorFromString("renderPixelBuffer:metalLayer:mergeInfos:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return }
-        typealias Fn = @convention(c) (AnyObject, Selector, CVPixelBuffer?, AnyObject?, NSArray?) -> Void
-        unsafeBitCast(imp, to: Fn.self)(obj, sel, pixelBuffer, layer, infos)
-    }
-
-    static func callDisplay(_ obj: NSObject, pixelBuffer: CVPixelBuffer?) {
-        let sel = NSSelectorFromString("display:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return }
-        typealias Fn = @convention(c) (AnyObject, Selector, CVPixelBuffer?) -> Void
-        unsafeBitCast(imp, to: Fn.self)(obj, sel, pixelBuffer)
-    }
-
-    static func callDisplayMerge(_ obj: NSObject, pixelBuffer: CVPixelBuffer?, infos: NSArray?) {
-        let sel = NSSelectorFromString("display:mergeInfos:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return }
-        typealias Fn = @convention(c) (AnyObject, Selector, CVPixelBuffer?, NSArray?) -> Void
-        unsafeBitCast(imp, to: Fn.self)(obj, sel, pixelBuffer, infos)
-    }
-
-    static func callDisplayPixelBuffer(_ obj: NSObject, pixelBuffer: CVPixelBuffer?) {
-        let sel = NSSelectorFromString("displayPixelBuffer:")
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return }
-        typealias Fn = @convention(c) (AnyObject, Selector, CVPixelBuffer?) -> Void
-        unsafeBitCast(imp, to: Fn.self)(obj, sel, pixelBuffer)
-    }
-
-    static func callNoArgs(_ obj: NSObject, _ selector: String) {
-        let sel = NSSelectorFromString(selector)
-        guard obj.responds(to: sel), let imp = obj.method(for: sel) else { return }
-        typealias Fn = @convention(c) (AnyObject, Selector) -> Void
-        unsafeBitCast(imp, to: Fn.self)(obj, sel)
-    }
-}
-
 private final class LNPixelBufferLayerRenderer {
     private let ciContext = CIContext(options: nil)
 
@@ -589,26 +501,13 @@ private final class LNVAPMetalCoreRenderer {
             maskTexture = nil
             return
         }
-        let width = Int(maskInfo.dataSize.width)
-        let height = Int(maskInfo.dataSize.height)
-        guard width > 0,
-              height > 0,
+        guard maskInfo.dataSize.width > 0,
+              maskInfo.dataSize.height > 0,
               !maskInfo.data.isEmpty else {
             maskTexture = nil
             return
         }
-        let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .r8Unorm, width: width, height: height, mipmapped: false)
-        descriptor.usage = [.shaderRead]
-        descriptor.storageMode = .shared
-        guard let texture = device.makeTexture(descriptor: descriptor) else {
-            maskTexture = nil
-            return
-        }
-        maskInfo.data.withUnsafeBytes { bytes in
-            guard let base = bytes.baseAddress else { return }
-            texture.replace(region: MTLRegionMake2D(0, 0, width, height), mipmapLevel: 0, withBytes: base, bytesPerRow: width)
-        }
-        maskTexture = texture
+        maskTexture = maskInfo.texture(with: device)
     }
 
     private func makeMaskBlurBuffer() -> MTLBuffer? {
@@ -676,57 +575,6 @@ private final class LNMetalUnavailableBridge: NSObject {
     }
 }
 
-private enum LNVAPLegacyMapper {
-    static func toLegacyCommonInfo(_ info: LNVAPCommonInfo?) -> NSObject? {
-        guard let info, let obj = LNLegacyRuntime.instantiate("QGVAPCommonInfo") else { return nil }
-        LNLegacyRuntime.set(obj, "version", info.version)
-        LNLegacyRuntime.set(obj, "framesCount", info.framesCount)
-        LNLegacyRuntime.set(obj, "size", NSValue(cgSize: info.size))
-        LNLegacyRuntime.set(obj, "videoSize", NSValue(cgSize: info.videoSize))
-        LNLegacyRuntime.set(obj, "targetOrientaion", info.targetOrientaion.rawValue)
-        LNLegacyRuntime.set(obj, "fps", info.fps)
-        LNLegacyRuntime.set(obj, "isMerged", info.isMerged)
-        LNLegacyRuntime.set(obj, "alphaAreaRect", NSValue(cgRect: info.alphaAreaRect))
-        LNLegacyRuntime.set(obj, "rgbAreaRect", NSValue(cgRect: info.rgbAreaRect))
-        return obj
-    }
-
-    static func toLegacyMaskInfo(_ info: LNVAPMaskInfo?) -> NSObject? {
-        guard let info, let obj = LNLegacyRuntime.instantiate("QGVAPMaskInfo") else { return nil }
-        LNLegacyRuntime.set(obj, "data", info.data)
-        LNLegacyRuntime.set(obj, "dataSize", NSValue(cgSize: info.dataSize))
-        LNLegacyRuntime.set(obj, "positionInVideoRect", NSValue(cgRect: info.sampleRect))
-        return obj
-    }
-
-    static func toLegacySourceInfo(_ info: LNVAPSourceInfo?) -> NSObject? {
-        guard let info, let obj = LNLegacyRuntime.instantiate("QGVAPSourceInfo") else { return nil }
-        LNLegacyRuntime.set(obj, "contentTag", info.contentTag)
-        LNLegacyRuntime.set(obj, "contentTagValue", info.contentTagValue)
-        LNLegacyRuntime.set(obj, "color", info.color)
-        LNLegacyRuntime.set(obj, "size", NSValue(cgSize: info.size))
-        LNLegacyRuntime.set(obj, "sourceImage", info.sourceImage)
-        LNLegacyRuntime.set(obj, "texture", info.texture)
-        LNLegacyRuntime.set(obj, "colorParamsBuffer", info.colorParamsBuffer)
-        return obj
-    }
-
-    static func toLegacyMergedInfos(_ infos: [LNVAPMergedInfo]) -> NSArray {
-        let out = NSMutableArray(capacity: infos.count)
-        for info in infos {
-            guard let obj = LNLegacyRuntime.instantiate("QGVAPMergedInfo") else { continue }
-            LNLegacyRuntime.set(obj, "source", toLegacySourceInfo(info.source))
-            LNLegacyRuntime.set(obj, "renderIndex", info.renderIndex)
-            LNLegacyRuntime.set(obj, "renderRect", NSValue(cgRect: info.renderRect))
-            LNLegacyRuntime.set(obj, "needMask", info.needMask)
-            LNLegacyRuntime.set(obj, "maskRect", NSValue(cgRect: info.maskRect))
-            LNLegacyRuntime.set(obj, "maskRotation", info.maskRotation)
-            out.add(obj)
-        }
-        return out
-    }
-}
-
 @objcMembers
 public final class LNHWDMetalRenderer: NSObject {
     public var blendMode: QGHWDTextureBlendMode {
@@ -734,15 +582,11 @@ public final class LNHWDMetalRenderer: NSObject {
             if #available(iOS 13.0, *), let swiftRenderer = swiftRenderer as? LNHWDMetalCoreRenderer {
                 swiftRenderer.updateBlendMode(blendMode)
             }
-            if let legacy = legacyRenderer {
-                LNLegacyRuntime.set(legacy, "blendMode", blendMode.rawValue)
-            }
         }
     }
 
     private let fallback = LNPixelBufferLayerRenderer()
     private let fallbackLayer = CALayer()
-    private let legacyRenderer: NSObject?
     private let swiftRenderer: AnyObject?
 
     @objc(initWithMetalLayer:blendMode:)
@@ -753,13 +597,6 @@ public final class LNHWDMetalRenderer: NSObject {
         } else {
             self.swiftRenderer = nil
         }
-
-        if self.swiftRenderer == nil, let rendererObj = LNLegacyRuntime.instantiate("QGHWDMetalRenderer") {
-            let created = LNLegacyRuntime.callInitWithMetalLayerBlend(rendererObj, layer: metalLayer, blend: Int(blendMode.rawValue))
-            self.legacyRenderer = (created as? NSObject) ?? rendererObj
-        } else {
-            self.legacyRenderer = nil
-        }
         super.init()
     }
 
@@ -772,10 +609,6 @@ public final class LNHWDMetalRenderer: NSObject {
             swiftRenderer.render(pixelBuffer: pixelBuffer, metalLayer: metalLayer)
             return
         }
-        if let legacy = legacyRenderer {
-            LNLegacyRuntime.callRenderPixelBuffer(legacy, pixelBuffer: pixelBuffer, layer: metalLayer)
-            return
-        }
         guard let pixelBuffer else { return }
         let target = (metalLayer as? CALayer) ?? fallbackLayer
         fallback.render(pixelBuffer: pixelBuffer, into: target)
@@ -786,9 +619,6 @@ public final class LNHWDMetalRenderer: NSObject {
         if #available(iOS 13.0, *), let swiftRenderer = swiftRenderer as? LNHWDMetalCoreRenderer {
             swiftRenderer.dispose()
         }
-        if let legacy = legacyRenderer {
-            LNLegacyRuntime.callNoArgs(legacy, "dispose")
-        }
     }
 }
 
@@ -853,6 +683,7 @@ public final class LNHWDMetalView: UIView {
 
     @objc(dispose)
     public func dispose() {
+        fallbackRenderer.dispose()
         renderLayer.contents = nil
     }
 }
@@ -864,24 +695,29 @@ public final class LNVAPMetalRenderer: NSObject {
             if #available(iOS 13.0, *), let swiftRenderer = swiftRenderer as? LNVAPMetalCoreRenderer {
                 swiftRenderer.commonInfo = commonInfo
             }
-            guard let legacy = legacyRenderer else { return }
-            LNLegacyRuntime.set(legacy, "commonInfo", LNVAPLegacyMapper.toLegacyCommonInfo(commonInfo))
         }
     }
 
     public var maskInfo: LNVAPMaskInfo? {
         didSet {
+            if let maskInfo,
+               (maskInfo.data.isEmpty || maskInfo.dataSize.width <= 0 || maskInfo.dataSize.height <= 0) {
+                LNVAPLogger.log(level: 2,
+                                file: #file,
+                                line: #line,
+                                func: #function,
+                                module: "Render",
+                                message: "setMaskInfo ignored: invalid mask data or size")
+                return
+            }
             if #available(iOS 13.0, *), let swiftRenderer = swiftRenderer as? LNVAPMetalCoreRenderer {
                 swiftRenderer.maskInfo = maskInfo
             }
-            guard let legacy = legacyRenderer else { return }
-            LNLegacyRuntime.set(legacy, "maskInfo", LNVAPLegacyMapper.toLegacyMaskInfo(maskInfo))
         }
     }
 
     private let fallback = LNPixelBufferLayerRenderer()
     private let fallbackLayer = CALayer()
-    private let legacyRenderer: NSObject?
     private let swiftRenderer: AnyObject?
 
     @objc(initWithMetalLayer:)
@@ -891,13 +727,6 @@ public final class LNVAPMetalRenderer: NSObject {
         } else {
             self.swiftRenderer = nil
         }
-
-        if let rendererObj = LNLegacyRuntime.instantiate("QGVAPMetalRenderer") {
-            let created = LNLegacyRuntime.callInitWithMetalLayer(rendererObj, layer: metalLayer)
-            self.legacyRenderer = (created as? NSObject) ?? rendererObj
-        } else {
-            self.legacyRenderer = nil
-        }
         super.init()
     }
 
@@ -911,16 +740,6 @@ public final class LNVAPMetalRenderer: NSObject {
             return
         }
 
-        if let legacy = legacyRenderer {
-            LNLegacyRuntime.set(legacy, "commonInfo", LNVAPLegacyMapper.toLegacyCommonInfo(commonInfo))
-            LNLegacyRuntime.set(legacy, "maskInfo", LNVAPLegacyMapper.toLegacyMaskInfo(maskInfo))
-            LNLegacyRuntime.callRenderPixelBufferMerge(legacy,
-                                                       pixelBuffer: pixelBuffer,
-                                                       layer: metalLayer,
-                                                       infos: LNVAPLegacyMapper.toLegacyMergedInfos(mergeInfos))
-            return
-        }
-
         guard let pixelBuffer else { return }
         let target = (metalLayer as? CALayer) ?? fallbackLayer
         fallback.render(pixelBuffer: pixelBuffer, into: target)
@@ -931,9 +750,6 @@ public final class LNVAPMetalRenderer: NSObject {
         if #available(iOS 13.0, *), let swiftRenderer = swiftRenderer as? LNVAPMetalCoreRenderer {
             swiftRenderer.dispose()
         }
-        if let legacy = legacyRenderer {
-            LNLegacyRuntime.callNoArgs(legacy, "dispose")
-        }
     }
 }
 
@@ -946,6 +762,7 @@ public final class LNVAPMetalView: UIView {
     }
     public var commonInfo: LNVAPCommonInfo?
     public var maskInfo: LNVAPMaskInfo?
+    private var drawableSizeShouldUpdate = true
 
     private let unavailableBridge = LNMetalUnavailableBridge()
     private var renderLayer: CALayer = CALayer()
@@ -983,10 +800,27 @@ public final class LNVAPMetalView: UIView {
     public override func layoutSubviews() {
         super.layoutSubviews()
         renderLayer.frame = bounds
+        drawableSizeShouldUpdate = true
+    }
+
+    public override func didMoveToWindow() {
+        super.didMoveToWindow()
+        drawableSizeShouldUpdate = true
     }
 
     @objc(display:mergeInfos:)
     public func display(_ pixelBuffer: CVPixelBuffer?, mergeInfos: [LNVAPMergedInfo]) {
+        if window == nil {
+            delegate?.onMetalViewUnavailable()
+            return
+        }
+        if drawableSizeShouldUpdate {
+            if #available(iOS 13.0, *), let metalLayer = renderLayer as? CAMetalLayer {
+                let nativeScale = UIScreen.main.nativeScale
+                metalLayer.drawableSize = CGSize(width: bounds.width * nativeScale, height: bounds.height * nativeScale)
+            }
+            drawableSizeShouldUpdate = false
+        }
         fallbackRenderer.commonInfo = commonInfo
         fallbackRenderer.maskInfo = maskInfo
         fallbackRenderer.renderPixelBuffer(pixelBuffer, metalLayer: renderLayer, mergeInfos: mergeInfos)
@@ -994,6 +828,7 @@ public final class LNVAPMetalView: UIView {
 
     @objc(dispose)
     public func dispose() {
+        fallbackRenderer.dispose()
         renderLayer.contents = nil
     }
 }

+ 80 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Utils/LNUtilities.swift

@@ -438,6 +438,17 @@ public final class LNVAPDeviceUtil: NSObject {
     }
 }
 
+public extension UIDevice {
+    @objc(hwd_isSimulator)
+    func hwd_isSimulator() -> Bool {
+        #if targetEnvironment(simulator)
+        return true
+        #else
+        return false
+        #endif
+    }
+}
+
 public extension NSArray {
     @objc(ln_rectValue)
     func ln_rectValue() -> CGRect {
@@ -453,6 +464,11 @@ public extension NSArray {
                       width: (self[2] as? NSNumber)?.doubleValue ?? ((self[2] as? NSString)?.doubleValue ?? 0),
                       height: (self[3] as? NSNumber)?.doubleValue ?? ((self[3] as? NSString)?.doubleValue ?? 0))
     }
+
+    @objc(hwd_rectValue)
+    func hwd_rectValue() -> CGRect {
+        ln_rectValue()
+    }
 }
 
 public extension NSDictionary {
@@ -494,6 +510,31 @@ public extension NSDictionary {
         guard let key else { return nil }
         return self[key] as? NSArray
     }
+
+    @objc(hwd_floatValue:)
+    func hwd_floatValue(_ key: String?) -> CGFloat {
+        ln_floatValue(forKey: key)
+    }
+
+    @objc(hwd_integerValue:)
+    func hwd_integerValue(_ key: String?) -> Int {
+        ln_integerValue(forKey: key)
+    }
+
+    @objc(hwd_stringValue:)
+    func hwd_stringValue(_ key: String?) -> String {
+        ln_stringValue(forKey: key)
+    }
+
+    @objc(hwd_dicValue:)
+    func hwd_dicValue(_ key: String?) -> NSDictionary? {
+        ln_dictionaryValue(forKey: key)
+    }
+
+    @objc(hwd_arrValue:)
+    func hwd_arrValue(_ key: String?) -> NSArray? {
+        ln_arrayValue(forKey: key)
+    }
 }
 
 public extension UIColor {
@@ -559,6 +600,11 @@ public extension UIColor {
 
         return UIColor(red: r, green: g, blue: b, alpha: a)
     }
+
+    @objc(hwd_colorWithHexString:)
+    static func hwd_color(withHexString hexString: String) -> UIColor? {
+        ln_color(hexString: hexString)
+    }
 }
 
 private final class LNGestureBlockTarget: NSObject {
@@ -599,6 +645,21 @@ public extension UIGestureRecognizer {
         }
         objc_setAssociatedObject(self, &lnGestureTargetKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
     }
+
+    @objc(initWithVapActionBlock:)
+    convenience init(vapActionBlock: @escaping (Any) -> Void) {
+        self.init(lnActionBlock: vapActionBlock)
+    }
+
+    @objc(addVapActionBlock:)
+    func addVapActionBlock(_ block: @escaping (Any) -> Void) {
+        ln_addActionBlock(block)
+    }
+
+    @objc(removeAllVapActionBlocks)
+    func removeAllVapActionBlocks() {
+        ln_removeAllActionBlocks()
+    }
 }
 
 private enum LNNotificationQueueStore {
@@ -678,4 +739,23 @@ public extension NotificationCenter {
             block(note, target)
         }
     }
+
+    @objc(hwd_addSafeObserver:selector:name:object:)
+    func hwd_addSafeObserver(_ observer: NSObject, selector: Selector, name: Notification.Name?, object: Any?) {
+        ln_addSafeObserver(observer, selector: selector, name: name, object: object)
+    }
+
+    @objc(hwd_addSafeObserver:selector:name:object:queue:)
+    func hwd_addSafeObserver(_ observer: NSObject, selector: Selector, name: Notification.Name?, object: Any?, queue: OperationQueue?) {
+        guard let queue else {
+            ln_addSafeObserver(observer, selector: selector, name: name, object: object)
+            return
+        }
+        ln_addSafeObserver(observer, selector: selector, name: name, object: object, queue: queue)
+    }
+
+    @objc(hwd_addWeakObserver:name:usingBlock:)
+    func hwd_addWeakObserver(_ weakObserver: NSObject, name: Notification.Name?, using block: @escaping (Notification, NSObject) -> Void) {
+        ln_addWeakObserver(weakObserver, name: name, using: block)
+    }
 }

+ 539 - 203
QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPPlayerView.swift

@@ -1,37 +1,49 @@
 import UIKit
 import CoreVideo
+import Metal
+
+private let lnDefaultFPS = 20
+private let lnMinFPS = 1
+private let lnMaxFPS = 60
+private let lnMaxCompatibleVAPVersion = 2
 
 @objcMembers
 public final class LNVAPPlayerView: UIView {
     private let playbackView = UIView()
-    private let delegateBridge = LNPlayerDelegateBridge()
-    private var didStart = false
+    private var core: LNPlayerCore!
+
+    fileprivate var didStart = false
 
     public weak var delegate: LNVAPPlaybackDelegate?
+    public weak var legacyDelegate: LNVAPLegacyPlaybackDelegate?
 
-    public var enterBackgroundOperation: LNEnterBackgroundOperation = .stop {
-        didSet { LNPlaybackRuntime.set(playbackView, key: "hwd_enterBackgroundOP", value: enterBackgroundOperation.legacyRawValue) }
+    public var enterBackgroundOperation: LNEnterBackgroundOperation {
+        get { core.enterBackgroundOperation }
+        set { core.enterBackgroundOperation = newValue }
     }
 
     public var renderByOpenGL: Bool {
-        get { LNPlaybackRuntime.boolValue(playbackView, key: "hwd_renderByOpenGL") }
-        set { LNPlaybackRuntime.set(playbackView, key: "hwd_renderByOpenGL", value: newValue) }
+        get { core.renderByOpenGL }
+        set { core.renderByOpenGL = newValue }
     }
 
     public var fps: Int {
-        get { LNPlaybackRuntime.intValue(playbackView, key: "hwd_fps") }
-        set { LNPlaybackRuntime.set(playbackView, key: "hwd_fps", value: newValue) }
+        get { core.fps }
+        set { core.fps = newValue }
+    }
+
+    public var maskInfo: LNVAPMaskInfo? {
+        get { core.maskInfo }
+        set { core.maskInfo = newValue }
     }
 
     public override init(frame: CGRect) {
         super.init(frame: frame)
-        delegateBridge.owner = self
         setupPlaybackView()
     }
 
     public required init?(coder: NSCoder) {
         super.init(coder: coder)
-        delegateBridge.owner = self
         setupPlaybackView()
     }
 
@@ -40,14 +52,14 @@ public final class LNVAPPlayerView: UIView {
         playbackView.frame = bounds
     }
 
+    deinit {
+        core.stop()
+    }
+
     @objc(lnPlayWithFilePath:repeatCount:)
     public func lnPlay(filePath: String, repeatCount: Int) {
         didStart = false
-        let played = LNPlaybackRuntime.playHWDMP4(playbackView, filePath: filePath, repeatCount: repeatCount, delegate: delegateBridge)
-        if !played {
-            let error = NSError(domain: "LNVAPPlayerView", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing runtime selector playHWDMP4:repeatCount:delegate:"])
-            delegate?.lnPlayerDidFail?(self, error: error)
-        }
+        core.play(filePath: filePath, repeatCount: repeatCount)
     }
 
     @objc(lnPlayWithFilePath:)
@@ -55,32 +67,57 @@ public final class LNVAPPlayerView: UIView {
         lnPlay(filePath: filePath, repeatCount: 0)
     }
 
+    // Legacy-compatible (LN-prefixed) entry that accepts deprecated parameters.
+    // `blendMode` is retained for call-site compatibility and intentionally ignored.
+    @objc(lnPlayDeprecatedWithFilePath:fps:blendMode:repeatCount:delegate:)
+    public func lnPlayDeprecated(filePath: String,
+                                 fps: Int,
+                                 blendMode: Int,
+                                 repeatCount: Int,
+                                 delegate: LNVAPPlaybackDelegate?) {
+        _ = blendMode
+        self.delegate = delegate
+        self.fps = fps
+        lnPlay(filePath: filePath, repeatCount: repeatCount)
+    }
+
+
+
     @objc(lnStop)
     public func lnStop() {
-        LNPlaybackRuntime.callNoArg(playbackView, selectorName: "stopHWDMP4")
-        notifyStopIfNeeded()
+        core.stop()
     }
 
+
+
     @objc(lnPause)
     public func lnPause() {
-        LNPlaybackRuntime.callNoArg(playbackView, selectorName: "pauseHWDMP4")
+        core.pause()
     }
 
+
+
     @objc(lnResume)
     public func lnResume() {
-        LNPlaybackRuntime.callNoArg(playbackView, selectorName: "resumeHWDMP4")
+        core.resume()
     }
 
+
+
     @objc(lnSetMute:)
     public func lnSetMute(_ mute: Bool) {
-        LNPlaybackRuntime.callBool(playbackView, selectorName: "setMute:", value: mute)
+        core.isMute = mute
     }
 
+
+
     @objc(lnEnableOldVersion:)
     public func lnEnableOldVersion(_ enable: Bool) {
-        LNPlaybackRuntime.callBool(playbackView, selectorName: "enableOldVersion:", value: enable)
+        core.enableOldVersion = enable
     }
 
+
+
     @objc(lnAddTapGestureWithTarget:action:)
     public func lnAddTapGesture(target: Any, action: Selector) {
         let tap = UITapGestureRecognizer(target: target, action: action)
@@ -90,58 +127,68 @@ public final class LNVAPPlayerView: UIView {
 
     @objc(lnAddVapTapGesture:)
     public func lnAddVapTapGesture(_ handler: @escaping LNVAPGestureEventBlock) {
-        let added = LNPlaybackRuntime.addVapTapGesture(playbackView) { gesture, insideSource, source in
-            handler(gesture, insideSource, source)
-        }
-        if !added {
-            let tap = UITapGestureRecognizer()
-            tap.ln_addActionBlock { sender in
-                guard let gesture = sender as? UIGestureRecognizer else { return }
-                handler(gesture, false, nil)
-            }
-            playbackView.addGestureRecognizer(tap)
-            playbackView.isUserInteractionEnabled = true
-        }
+        let tap = UITapGestureRecognizer()
+        lnAddVapGesture(tap, callback: handler)
     }
 
     @objc(lnAddVapGesture:callback:)
     public func lnAddVapGesture(_ gestureRecognizer: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) {
-        let added = LNPlaybackRuntime.addVapGesture(playbackView, gestureRecognizer: gestureRecognizer) { gesture, insideSource, source in
-            callback(gesture, insideSource, source)
-        }
-        if !added {
-            gestureRecognizer.ln_addActionBlock { sender in
-                guard let gesture = sender as? UIGestureRecognizer else { return }
+        gestureRecognizer.ln_addActionBlock { [weak self] sender in
+            guard let self, let gesture = sender as? UIGestureRecognizer else { return }
+            let point = gesture.location(in: self.playbackView)
+            if let source = self.core.displayingSource(at: point) {
+                callback(gesture, true, source)
+            } else {
                 callback(gesture, false, nil)
             }
-            playbackView.addGestureRecognizer(gestureRecognizer)
-            playbackView.isUserInteractionEnabled = true
         }
+        playbackView.addGestureRecognizer(gestureRecognizer)
+        playbackView.isUserInteractionEnabled = true
+    }
+
+    @objc(addVapTapGesture:)
+    public func addVapTapGesture(_ handler: @escaping LNVAPGestureEventBlock) {
+        lnAddVapTapGesture(handler)
+    }
+
+    @objc(addVapGesture:callback:)
+    public func addVapGesture(_ gestureRecognizer: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) {
+        lnAddVapGesture(gestureRecognizer, callback: callback)
     }
 
     fileprivate func notifyShouldStart(_ config: LNVAPConfigModel) -> Bool {
-        delegate?.lnPlayerShouldStart?(self, config: config) ?? true
+        if let allow = delegate?.lnPlayerShouldStart?(self, config: config) {
+            return allow
+        }
+        return legacyDelegate?.shouldStartPlayMP4?(self, config: config) ?? true
     }
 
     fileprivate func notifyStart() {
         didStart = true
         delegate?.lnPlayerDidStart?(self)
+        legacyDelegate?.viewDidStartPlayMP4?(self)
     }
 
     fileprivate func notifyPlay(_ frame: LNMP4AnimatedImageFrame) {
         delegate?.lnPlayerDidPlay?(self, frame: frame)
+        legacyDelegate?.viewDidPlayMP4AtFrame?(frame, view: self)
     }
 
     fileprivate func notifyFinish(_ totalFrameCount: Int) {
         delegate?.lnPlayerDidFinish?(self, totalFrameCount: totalFrameCount)
+        legacyDelegate?.viewDidFinishPlayMP4?(totalFrameCount, view: self)
     }
 
     fileprivate func notifyFail(_ error: NSError) {
         delegate?.lnPlayerDidFail?(self, error: error)
+        legacyDelegate?.viewDidFailPlayMP4?(error)
     }
 
     fileprivate func contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String? {
-        delegate?.lnPlayerContent?(forTag: tag, resource: resource)
+        if let text = delegate?.lnPlayerContent?(forTag: tag, resource: resource) {
+            return text
+        }
+        return legacyDelegate?.contentForVapTag?(tag, resource: resource)
     }
 
     fileprivate func loadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion) {
@@ -149,12 +196,22 @@ public final class LNVAPPlayerView: UIView {
             handler(url, context, completion)
             return
         }
-        completion(nil, nil, url)
+        if (legacyDelegate as AnyObject?)?.responds(to: #selector(LNVAPLegacyPlaybackDelegate.loadVapImage(withURL:context:completion:))) == true {
+            legacyDelegate?.loadVapImage?(withURL: url, context: context, completion: completion)
+            return
+        }
+        let error = NSError(
+            domain: NSURLErrorDomain,
+            code: -1,
+            userInfo: ["msg": "delegate does not implement lnPlayerLoadImage"]
+        )
+        completion(nil, error, url)
     }
 
-    fileprivate func notifyStopIfNeeded() {
+    fileprivate func notifyStopIfNeeded(lastFrameIndex: Int) {
         if didStart {
             delegate?.lnPlayerDidStop?(self)
+            legacyDelegate?.viewDidStopPlayMP4?(lastFrameIndex, view: self)
         }
         didStart = false
     }
@@ -163,232 +220,511 @@ public final class LNVAPPlayerView: UIView {
         playbackView.frame = bounds
         playbackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
         addSubview(playbackView)
-        LNPlaybackRuntime.set(playbackView, key: "hwd_enterBackgroundOP", value: enterBackgroundOperation.legacyRawValue)
+        core = LNPlayerCore(owner: self, hostView: playbackView)
     }
 }
 
-private final class LNPlayerDelegateBridge: NSObject {
+private final class LNPlayerCore: NSObject {
     weak var owner: LNVAPPlayerView?
+    private weak var hostView: UIView?
 
-    @objc(shouldStartPlayMP4:config:)
-    func shouldStartPlayMP4(_ container: UIView, config: NSObject) -> Bool {
-        guard let owner else { return true }
-        return owner.notifyShouldStart(LNPlaybackLegacyMapper.toConfig(config))
-    }
+    var enterBackgroundOperation: LNEnterBackgroundOperation = .stop
+    var renderByOpenGL: Bool = false
+    var fps: Int = 0
+    var enableOldVersion: Bool = false
+    var isMute: Bool = false
 
-    @objc(viewDidStartPlayMP4:)
-    func viewDidStartPlayMP4(_ container: UIView) {
-        owner?.notifyStart()
+    var maskInfo: LNVAPMaskInfo? {
+        didSet {
+            vapMetalView?.maskInfo = maskInfo
+        }
     }
 
-    @objc(viewDidPlayMP4AtFrame:view:)
-    func viewDidPlayMP4AtFrame(_ frame: NSObject, view container: UIView) {
-        owner?.notifyPlay(LNPlaybackLegacyMapper.toFrame(frame))
+    private var currentFrame: LNMP4AnimatedImageFrame?
+    private var fileInfo: LNMP4HWDFileInfo?
+    private var decodeManager: LNAnimatedImageDecodeManager?
+    private var decodeConfig: LNAnimatedImageDecodeConfig = .defaultConfig()
+    private var callbackQueue: OperationQueue?
+
+    private var openGLView: LNHWDMP4OpenGLView?
+    private var metalView: LNHWDMetalView?
+    private var vapMetalView: LNVAPMetalView?
+
+    private var isFinish = true
+    private var onPause = false
+    private var onSeek = false
+    private var repeatCount = 0
+
+    private var configManager: LNVAPConfigManager?
+    private let renderQueue = DispatchQueue(label: "com.ln.vap.render")
+    private var filePath: String = ""
+
+    init(owner: LNVAPPlayerView, hostView: UIView) {
+        self.owner = owner
+        self.hostView = hostView
+        super.init()
+        registerNotifications()
     }
 
-    @objc(viewDidStopPlayMP4:view:)
-    func viewDidStopPlayMP4(_ lastFrameIndex: Int, view container: UIView) {
-        owner?.notifyStopIfNeeded()
+    deinit {
+        NotificationCenter.default.removeObserver(self)
+        stopInternal(triggerDelegate: false)
     }
 
-    @objc(viewDidFinishPlayMP4:view:)
-    func viewDidFinishPlayMP4(_ totalFrameCount: Int, view container: UIView) {
-        owner?.notifyFinish(totalFrameCount)
+    func play(filePath: String, repeatCount: Int) {
+        if Thread.isMainThread {
+            playOnMain(filePath: filePath, repeatCount: repeatCount)
+        } else {
+            DispatchQueue.main.async { [weak self] in
+                self?.playOnMain(filePath: filePath, repeatCount: repeatCount)
+            }
+        }
     }
 
-    @objc(viewDidFailPlayMP4:)
-    func viewDidFailPlayMP4(_ error: NSError) {
-        owner?.notifyFail(error)
+    func stop() {
+        if Thread.isMainThread {
+            stopInternal(triggerDelegate: true)
+        } else {
+            DispatchQueue.main.async { [weak self] in
+                self?.stopInternal(triggerDelegate: true)
+            }
+        }
     }
 
-    @objc(contentForVapTag:resource:)
-    func contentForVapTag(_ tag: String, resource: NSObject) -> String? {
-        guard let owner else { return nil }
-        return owner.contentForTag(tag, resource: LNPlaybackLegacyMapper.toSource(resource))
+    func pause() {
+        onPause = true
+        decodeManager?.tryToPauseAudioPlay()
     }
 
-    @objc(loadVapImageWithURL:context:completion:)
-    func loadVapImageWithURL(_ url: String, context: NSDictionary, completion: @escaping (UIImage?, NSError?, String) -> Void) {
-        owner?.loadImage(withURL: url, context: context, completion: completion)
+    func resume() {
+        onPause = false
+        openGLView?.pause = false
+        decodeManager?.tryToResumeAudioPlay()
     }
-}
 
-private enum LNPlaybackLegacyMapper {
-    static func toConfig(_ object: NSObject) -> LNVAPConfigModel {
-        if let config = object as? LNVAPConfigModel {
-            return config
+    func displayingSource(at point: CGPoint) -> LNVAPSourceDisplayItem? {
+        guard let configManager,
+              let model = configManager.model.info,
+              let currentFrame else {
+            return nil
+        }
+
+        let mergeInfos = configManager.model.mergedConfig[NSNumber(value: currentFrame.frameIndex)] ?? []
+            .sorted { $0.renderIndex > $1.renderIndex }
+
+        guard model.size.width > 0, model.size.height > 0 else {
+            return nil
         }
-        let config = LNVAPConfigModel()
-        if let infoObj = object.value(forKey: "info") as? NSObject {
-            config.info = toCommonInfo(infoObj)
+        guard let hostView else {
+            return nil
         }
-        if let resources = object.value(forKey: "resources") as? [NSObject] {
-            config.resources = resources.map { toSource($0) }
+
+        let xRatio = hostView.bounds.width / model.size.width
+        let yRatio = hostView.bounds.height / model.size.height
+
+        for mergeInfo in mergeInfos {
+            let srcRect = mergeInfo.renderRect
+            let frame = CGRect(
+                x: srcRect.minX * xRatio,
+                y: srcRect.minY * yRatio,
+                width: srcRect.width * xRatio,
+                height: srcRect.height * yRatio
+            )
+            if frame.contains(point) {
+                let item = LNVAPSourceDisplayItem()
+                item.sourceInfo = mergeInfo.source
+                item.frame = frame
+                return item
+            }
         }
-        return config
+        return nil
     }
 
-    static func toCommonInfo(_ object: NSObject) -> LNVAPCommonInfo {
-        let info = LNVAPCommonInfo()
-        info.version = intValue(object, key: "version")
-        info.framesCount = intValue(object, key: "framesCount")
-        info.size = cgSizeValue(object, key: "size")
-        info.videoSize = cgSizeValue(object, key: "videoSize")
-        info.targetOrientaion = LNVAPOrientation(rawValue: intValue(object, key: "targetOrientaion")) ?? .none
-        info.fps = intValue(object, key: "fps")
-        info.isMerged = boolValue(object, key: "isMerged")
-        info.alphaAreaRect = cgRectValue(object, key: "alphaAreaRect")
-        info.rgbAreaRect = cgRectValue(object, key: "rgbAreaRect")
-        return info
+    private var useVapMetalView: Bool {
+        configManager?.hasValidConfig == true
     }
 
-    static func toSource(_ object: NSObject) -> LNVAPSourceInfo {
-        if let source = object as? LNVAPSourceInfo {
-            return source
+    private func playOnMain(filePath: String, repeatCount: Int) {
+        guard !filePath.isEmpty else {
+            notifyFail(message: "play failed: empty file path", code: LNMP4HWDErrorCode.fileNotExist.rawValue)
+            return
         }
-        let source = LNVAPSourceInfo()
-        source.type = stringValue(object, key: "type")
-        source.loadType = stringValue(object, key: "loadType")
-        source.contentTag = stringValue(object, key: "contentTag")
-        source.contentTagValue = stringValue(object, key: "contentTagValue")
-        source.color = object.value(forKey: "color") as? UIColor
-        source.style = stringValue(object, key: "style")
-        source.size = cgSizeValue(object, key: "size")
-        source.fitType = stringValue(object, key: "fitType")
-        source.sourceImage = object.value(forKey: "sourceImage") as? UIImage
-        return source
-    }
+        guard FileManager.default.fileExists(atPath: filePath) else {
+            notifyFail(message: "play failed: file does not exist", code: LNMP4HWDErrorCode.fileNotExist.rawValue)
+            return
+        }
+
+        stopInternal(triggerDelegate: false)
 
-    static func toFrame(_ object: NSObject) -> LNMP4AnimatedImageFrame {
-        if let frame = object as? LNMP4AnimatedImageFrame {
-            return frame
+        isFinish = false
+        onPause = false
+        onSeek = false
+        self.filePath = filePath
+        self.repeatCount = repeatCount
+
+        if callbackQueue == nil, owner?.delegate != nil {
+            let queue = OperationQueue()
+            queue.maxConcurrentOperationCount = 1
+            callbackQueue = queue
+        }
+
+        let info = LNMP4HWDFileInfo()
+        info.filePath = filePath
+        info.mp4Parser = LNMP4ParserProxy(filePath: filePath)
+        info.mp4Parser?.parse()
+        fileInfo = info
+
+        let configManager = LNVAPConfigManager(fileInfo: info)
+        configManager.delegate = self
+        self.configManager = configManager
+
+        if configManager.model.info?.version ?? 0 > lnMaxCompatibleVAPVersion {
+            notifyFail(message: "not compatible vap version", code: LNMP4HWDErrorCode.invalidMP4File.rawValue)
+            stopInternal(triggerDelegate: false)
+            return
+        }
+
+        if !configManager.hasValidConfig && !enableOldVersion {
+            notifyFail(message: "missing vapc box and oldVersion disabled", code: LNMP4HWDErrorCode.invalidMP4File.rawValue)
+            stopInternal(triggerDelegate: false)
+            return
+        }
+
+#if targetEnvironment(simulator)
+        notifyFail(message: "not allowed in simulator", code: LNMP4HWDErrorCode.invalidMP4File.rawValue)
+        stopInternal(triggerDelegate: false)
+        return
+#endif
+
+        setupRenderViewIfNeed()
+
+        decodeManager = LNAnimatedImageDecodeManager(fileInfo: info, config: decodeConfig, delegate: self)
+        configManager.loadConfigResources()
+    }
+
+    private func setupRenderViewIfNeed() {
+        guard let hostView else { return }
+
+        if renderByOpenGL {
+            if openGLView == nil {
+                let view = LNHWDMP4OpenGLView(frame: hostView.bounds)
+                view.displayDelegate = self
+                view.translatesAutoresizingMaskIntoConstraints = false
+                hostView.addSubview(view)
+                NSLayoutConstraint.activate([
+                    view.topAnchor.constraint(equalTo: hostView.topAnchor),
+                    view.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
+                    view.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
+                    view.trailingAnchor.constraint(equalTo: hostView.trailingAnchor)
+                ])
+                view.setupGL()
+                openGLView = view
+            }
+            openGLView?.pause = false
+            return
+        }
+
+        if useVapMetalView {
+            if vapMetalView == nil {
+                let view = LNVAPMetalView(frame: hostView.bounds)
+                view.delegate = self
+                view.translatesAutoresizingMaskIntoConstraints = false
+                hostView.addSubview(view)
+                NSLayoutConstraint.activate([
+                    view.topAnchor.constraint(equalTo: hostView.topAnchor),
+                    view.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
+                    view.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
+                    view.trailingAnchor.constraint(equalTo: hostView.trailingAnchor)
+                ])
+                vapMetalView = view
+            }
+            vapMetalView?.commonInfo = configManager?.model.info
+            vapMetalView?.maskInfo = maskInfo
+            return
+        }
+
+        if metalView == nil {
+            let view = LNHWDMetalView(frame: hostView.bounds, blendMode: .alphaLeft)
+            view.delegate = self
+            view.translatesAutoresizingMaskIntoConstraints = false
+            hostView.addSubview(view)
+            NSLayoutConstraint.activate([
+                view.topAnchor.constraint(equalTo: hostView.topAnchor),
+                view.bottomAnchor.constraint(equalTo: hostView.bottomAnchor),
+                view.leadingAnchor.constraint(equalTo: hostView.leadingAnchor),
+                view.trailingAnchor.constraint(equalTo: hostView.trailingAnchor)
+            ])
+            metalView = view
+        }
+        metalView?.blendMode = .alphaLeft
+    }
+
+    private func runRenderLoop() {
+        let waitFrame: TimeInterval = 16.0 / 1000.0
+        let minLoop: TimeInterval = 1.0 / 1000.0
+
+        renderQueue.async { [weak self] in
+            guard let self else { return }
+            var lastRenderingInterval: TimeInterval = 0
+            var lastRenderingDuration: TimeInterval = 0
+
+            while true {
+                var shouldBreak = false
+                autoreleasepool {
+                    if self.isFinish {
+                        shouldBreak = true
+                        return
+                    }
+                    if self.onPause || self.onSeek {
+                        lastRenderingInterval = Date.timeIntervalSinceReferenceDate
+                        Thread.sleep(forTimeInterval: waitFrame)
+                        return
+                    }
+
+                    var nextFrame: LNMP4AnimatedImageFrame?
+                    DispatchQueue.main.sync {
+                        nextFrame = self.displayNextFrame()
+                    }
+
+                    var duration = (nextFrame?.duration ?? 0) / 1000.0
+                    if duration <= 0 {
+                        duration = waitFrame
+                    }
+
+                    let currentInterval = Date.timeIntervalSinceReferenceDate
+                    if let frame = nextFrame, frame.frameIndex != 0 {
+                        duration -= ((currentInterval - lastRenderingInterval) - lastRenderingDuration)
+                    }
+                    duration = max(minLoop, duration)
+
+                    lastRenderingInterval = currentInterval
+                    lastRenderingDuration = duration
+                    Thread.sleep(forTimeInterval: duration)
+                }
+                if shouldBreak {
+                    break
+                }
+            }
         }
-        let frame = LNMP4AnimatedImageFrame()
-        frame.frameIndex = intValue(object, key: "frameIndex")
-        frame.duration = timeIntervalValue(object, key: "duration")
-        frame.pts = uint64Value(object, key: "pts")
-        frame.defaultFps = Int32(intValue(object, key: "defaultFps"))
-        return frame
     }
 
-    static func toSourceDisplayItem(_ object: AnyObject?) -> LNVAPSourceDisplayItem? {
-        guard let object else { return nil }
-        if let item = object as? LNVAPSourceDisplayItem {
-            return item
+    private func displayNextFrame() -> LNMP4AnimatedImageFrame? {
+        guard !onPause, !isFinish else { return nil }
+        guard let decodeManager else { return nil }
+
+        let nextIndex = (currentFrame?.frameIndex ?? -1) + 1
+        guard let nextFrame = decodeManager.consumeDecodedFrame(nextIndex) as? LNMP4AnimatedImageFrame,
+              nextFrame.frameIndex == nextIndex else {
+            return nil
         }
-        guard let sourceObj = object as? NSObject else { return nil }
-        let item = LNVAPSourceDisplayItem()
-        item.frame = cgRectValue(sourceObj, key: "frame")
-        if let sourceInfoObj = sourceObj.value(forKey: "sourceInfo") as? NSObject {
-            item.sourceInfo = toSource(sourceInfoObj)
+
+        if nextIndex == 0 {
+            decodeManager.tryToStartAudioPlay()
+        }
+
+        nextFrame.duration = appropriateDuration(for: nextFrame)
+
+        if renderByOpenGL {
+            openGLView?.displayPixelBuffer(nextFrame.pixelBuffer)
+        } else if useVapMetalView {
+            let mergeInfos = configManager?.model.mergedConfig[NSNumber(value: nextFrame.frameIndex)] ?? []
+            vapMetalView?.display(nextFrame.pixelBuffer, mergeInfos: mergeInfos)
+        } else {
+            metalView?.display(nextFrame.pixelBuffer)
+        }
+
+        currentFrame = nextFrame
+
+        callbackQueue?.addOperation { [weak self] in
+            guard let self, let owner = self.owner else { return }
+            if nextIndex == 0 {
+                owner.notifyStart()
+            }
+            owner.notifyPlay(nextFrame)
         }
-        return item
+
+        return nextFrame
     }
 
-    private static func intValue(_ object: NSObject, key: String) -> Int {
-        (object.value(forKey: key) as? NSNumber)?.intValue ?? 0
+    private func appropriateDuration(for frame: LNMP4AnimatedImageFrame) -> TimeInterval {
+        var preferredFPS = fps
+        if preferredFPS < lnMinFPS || preferredFPS > lnMaxFPS {
+            let defaultFPS = Int(frame.defaultFps)
+            if defaultFPS >= lnMinFPS && defaultFPS <= lnMaxFPS {
+                preferredFPS = defaultFPS
+            } else {
+                preferredFPS = lnDefaultFPS
+            }
+        }
+        return 1000.0 / Double(preferredFPS)
     }
 
-    private static func uint64Value(_ object: NSObject, key: String) -> UInt64 {
-        (object.value(forKey: key) as? NSNumber)?.uint64Value ?? 0
+    private func stopInternal(triggerDelegate: Bool) {
+        repeatCount = 0
+        if isFinish {
+            return
+        }
+
+        isFinish = true
+        onPause = true
+
+        openGLView?.pause = true
+        openGLView?.dispose()
+        openGLView?.removeFromSuperview()
+        openGLView = nil
+
+        metalView?.dispose()
+        metalView?.removeFromSuperview()
+        metalView = nil
+
+        vapMetalView?.dispose()
+        vapMetalView?.removeFromSuperview()
+        vapMetalView = nil
+
+        decodeManager?.tryToStopAudioPlay()
+
+        if triggerDelegate {
+            let lastFrameIndex = currentFrame?.frameIndex ?? -1
+            performCallback { [weak self] in
+                guard let self, let owner = self.owner else { return }
+                owner.notifyStopIfNeeded(lastFrameIndex: lastFrameIndex)
+            }
+        } else {
+            owner?.didStart = false
+        }
+
+        decodeManager = nil
+        currentFrame = nil
+        fileInfo = nil
+        configManager = nil
     }
 
-    private static func timeIntervalValue(_ object: NSObject, key: String) -> TimeInterval {
-        (object.value(forKey: key) as? NSNumber)?.doubleValue ?? 0
+    private func notifyFail(message: String, code: Int) {
+        let error = NSError(domain: LNMP4FrameHWDecoder.errorDomain, code: code, userInfo: [NSLocalizedDescriptionKey: message])
+        performCallback { [weak self] in
+            self?.owner?.notifyFail(error)
+        }
     }
 
-    private static func boolValue(_ object: NSObject, key: String) -> Bool {
-        (object.value(forKey: key) as? NSNumber)?.boolValue ?? false
+    private func performCallback(_ block: @escaping () -> Void) {
+        if let callbackQueue {
+            callbackQueue.addOperation(block)
+        } else {
+            block()
+        }
     }
 
-    private static func stringValue(_ object: NSObject, key: String) -> String? {
-        object.value(forKey: key) as? String
+    private func registerNotifications() {
+        NotificationCenter.default.addObserver(self, selector: #selector(onEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onSeekStart(_:)), name: NSNotification.Name(kLNVAPDecoderSeekStart), object: nil)
+        NotificationCenter.default.addObserver(self, selector: #selector(onSeekFinish(_:)), name: NSNotification.Name(kLNVAPDecoderSeekFinish), object: nil)
     }
 
-    private static func cgSizeValue(_ object: NSObject, key: String) -> CGSize {
-        (object.value(forKey: key) as? NSValue)?.cgSizeValue ?? .zero
+    @objc private func onEnterBackground() {
+        switch enterBackgroundOperation {
+        case .pauseAndResume:
+            pause()
+        default:
+            stop()
+        }
     }
 
-    private static func cgRectValue(_ object: NSObject, key: String) -> CGRect {
-        (object.value(forKey: key) as? NSValue)?.cgRectValue ?? .zero
+    @objc private func onEnterForeground() {
+        if enterBackgroundOperation == .pauseAndResume {
+            resume()
+        }
     }
-}
 
-private enum LNPlaybackRuntime {
-    private typealias LNObjCGestureBlock = @convention(block) (UIGestureRecognizer, Bool, AnyObject?) -> Void
+    @objc private func onSeekStart(_ notification: Notification) {
+        guard let object = notification.object, decodeManager?.containsThisDeocder(object) == true else { return }
+        onSeek = true
+    }
 
-    static func callNoArg(_ target: NSObject, selectorName: String) {
-        let selector = NSSelectorFromString(selectorName)
-        guard target.responds(to: selector) else { return }
-        typealias Function = @convention(c) (AnyObject, Selector) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector)
+    @objc private func onSeekFinish(_ notification: Notification) {
+        guard let object = notification.object, decodeManager?.containsThisDeocder(object) == true else { return }
+        onSeek = false
     }
+}
 
-    static func callBool(_ target: NSObject, selectorName: String, value: Bool) {
-        let selector = NSSelectorFromString(selectorName)
-        guard target.responds(to: selector) else { return }
-        typealias Function = @convention(c) (AnyObject, Selector, Bool) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector, value)
+extension LNPlayerCore: LNAnimatedImageDecoderDelegate {
+    @objc(decoderClassForManager:)
+    func decoderClass(for manager: LNAnimatedImageDecodeManager) -> AnyClass {
+        LNMP4FrameHWDecoder.self
     }
 
-    static func playHWDMP4(_ target: NSObject, filePath: String, repeatCount: Int, delegate: AnyObject?) -> Bool {
-        let selector = NSSelectorFromString("playHWDMP4:repeatCount:delegate:")
-        guard target.responds(to: selector) else { return false }
-        typealias Function = @convention(c) (AnyObject, Selector, NSString, Int, AnyObject?) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector, filePath as NSString, repeatCount, delegate)
-        return true
+    func shouldSetupAudioPlayer() -> Bool {
+        !isMute
     }
 
-    static func addVapTapGesture(_ target: NSObject, callback: @escaping LNVAPGestureEventBlock) -> Bool {
-        let selector = NSSelectorFromString("addVapTapGesture:")
-        guard target.responds(to: selector) else { return false }
-        let block: LNObjCGestureBlock = { gesture, insideSource, sourceObj in
-            callback(gesture, insideSource, LNPlaybackLegacyMapper.toSourceDisplayItem(sourceObj))
+    func decoderDidFinishDecode(_ decoder: LNBaseDecoder) {
+        performCallback { [weak self] in
+            guard let self, let owner = self.owner else { return }
+            let totalFrameCount = (self.currentFrame?.frameIndex ?? -1) + 1
+            owner.notifyFinish(totalFrameCount)
+        }
+
+        var nextRepeatCount = repeatCount
+        let shouldRepeat: Bool
+        if nextRepeatCount == -1 {
+            shouldRepeat = true
+        } else if nextRepeatCount > 0 {
+            nextRepeatCount -= 1
+            shouldRepeat = true
+        } else {
+            shouldRepeat = false
+        }
+
+        if shouldRepeat {
+            let path = filePath
+            DispatchQueue.main.async { [weak self] in
+                self?.playOnMain(filePath: path, repeatCount: nextRepeatCount)
+            }
+            return
         }
-        typealias Function = @convention(c) (AnyObject, Selector, AnyObject) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector, unsafeBitCast(block, to: AnyObject.self))
-        return true
+        stopInternal(triggerDelegate: true)
     }
 
-    static func addVapGesture(_ target: NSObject, gestureRecognizer: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) -> Bool {
-        let selector = NSSelectorFromString("addVapGesture:callback:")
-        guard target.responds(to: selector) else { return false }
-        let block: LNObjCGestureBlock = { gesture, insideSource, sourceObj in
-            callback(gesture, insideSource, LNPlaybackLegacyMapper.toSourceDisplayItem(sourceObj))
+    func decoderDidFailDecode(_ decoder: LNBaseDecoder?, error: NSError) {
+        stopInternal(triggerDelegate: false)
+        performCallback { [weak self] in
+            self?.owner?.notifyFail(error)
         }
-        typealias Function = @convention(c) (AnyObject, Selector, UIGestureRecognizer, AnyObject) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector, gestureRecognizer, unsafeBitCast(block, to: AnyObject.self))
-        return true
+    }
+}
+
+extension LNPlayerCore: LNHWDMP4OpenGLViewDelegate, LNHWDMetalViewDelegate, LNVAPMetalViewDelegate {
+    func onViewUnavailableStatus() {
+        stopInternal(triggerDelegate: true)
     }
 
-    static func set(_ target: NSObject, key: String, value: Any) {
-        let setter = setterSelectorName(for: key)
-        let setterSelector = NSSelectorFromString(setter)
-        guard target.responds(to: setterSelector) else { return }
-        target.setValue(value, forKey: key)
+    func onMetalViewUnavailable() {
+        stopInternal(triggerDelegate: true)
     }
+}
+
+extension LNPlayerCore: LNVAPConfigDelegate {
+    func onVAPConfigResourcesLoaded(_ config: LNVAPConfigModel, error: NSError?) {
+        guard let configManager else { return }
+
+        if let device = MTLCreateSystemDefaultDevice() {
+            configManager.loadMTLTextures(device)
+            configManager.loadMTLBuffers(device)
+        }
+
+        if let owner, !owner.notifyShouldStart(config) {
+            stopInternal(triggerDelegate: true)
+            return
+        }
 
-    static func intValue(_ target: NSObject, key: String) -> Int {
-        let selector = NSSelectorFromString(key)
-        guard target.responds(to: selector) else { return 0 }
-        if let value = target.value(forKey: key) as? NSNumber { return value.intValue }
-        return 0
+        runRenderLoop()
     }
 
-    static func boolValue(_ target: NSObject, key: String) -> Bool {
-        let selector = NSSelectorFromString(key)
-        guard target.responds(to: selector) else { return false }
-        if let value = target.value(forKey: key) as? NSNumber { return value.boolValue }
-        return false
+    func vap_contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String? {
+        owner?.contentForTag(tag, resource: resource)
     }
 
-    private static func setterSelectorName(for key: String) -> String {
-        guard let first = key.first else { return "" }
-        return "set\(String(first).uppercased())\(key.dropFirst()):"
+    func vap_loadImageWithURL(_ urlStr: String, context: [AnyHashable: Any], completion: @escaping (UIImage?, NSError?, String) -> Void) {
+        owner?.loadImage(withURL: urlStr, context: context as NSDictionary, completion: completion)
     }
 }

+ 176 - 266
QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPWrapView.swift

@@ -1,54 +1,54 @@
 import UIKit
-import CoreVideo
 
 @objcMembers
 public final class LNVAPWrapView: UIView {
-    private let playbackView: UIView
-    private let delegateBridge: LNWrapDelegateBridge
-    private var didStart = false
+    private var playerView: LNVAPPlayerView?
+    private var delegateBridge = LNWrapDelegateBridge()
 
     public weak var delegate: LNVAPWrapPlaybackDelegate?
+    public weak var legacyDelegate: LNVAPWrapLegacyPlaybackDelegate?
 
     public var contentModeOption: LNVAPWrapContentMode = .scaleToFill {
-        didSet { LNWrapRuntime.set(playbackView, key: "contentMode", value: contentModeOption.legacyRawValue) }
+        didSet {
+            applyContentModeIfPossible()
+        }
+    }
+
+    @objc(lnWrapContentMode)
+    public var lnWrapContentMode: Int {
+        get { contentModeOption.legacyRawValue }
+        set { contentModeOption = LNVAPWrapContentMode(rawValue: newValue) ?? .scaleToFill }
     }
 
-    public var autoDestroyAfterFinish: Bool {
-        get { LNWrapRuntime.boolValue(playbackView, key: "autoDestoryAfterFinish") }
-        set { LNWrapRuntime.set(playbackView, key: "autoDestoryAfterFinish", value: newValue) }
+    public var autoDestroyAfterFinish: Bool = true
+
+    @objc(autoDestoryAfterFinish)
+    public var autoDestoryAfterFinish: Bool {
+        get { autoDestroyAfterFinish }
+        set { autoDestroyAfterFinish = newValue }
     }
 
     public override init(frame: CGRect) {
-        let runtimeView = LNWrapRuntime.makeLegacyWrapView(frame: frame) ?? UIView(frame: frame)
-        self.playbackView = runtimeView
-        self.delegateBridge = LNWrapDelegateBridge()
         super.init(frame: frame)
         delegateBridge.owner = self
-        setupWrapView()
     }
 
     public required init?(coder: NSCoder) {
-        let runtimeView = LNWrapRuntime.makeLegacyWrapView(frame: .zero) ?? UIView(frame: .zero)
-        self.playbackView = runtimeView
-        self.delegateBridge = LNWrapDelegateBridge()
         super.init(coder: coder)
         delegateBridge.owner = self
-        setupWrapView()
     }
 
     public override func layoutSubviews() {
         super.layoutSubviews()
-        playbackView.frame = bounds
+        if contentModeOption == .scaleToFill {
+            playerView?.frame = bounds
+        }
     }
 
     @objc(lnPlayWithFilePath:repeatCount:)
     public func lnPlay(filePath: String, repeatCount: Int) {
-        didStart = false
-        let played = LNWrapRuntime.playHWDMP4(playbackView, filePath: filePath, repeatCount: repeatCount, delegate: delegateBridge)
-        if !played {
-            let error = NSError(domain: "LNVAPWrapView", code: -1, userInfo: [NSLocalizedDescriptionKey: "Missing runtime selector playHWDMP4:repeatCount:delegate:"])
-            notifyFail(error)
-        }
+        let player = initPlayerViewIfNeed()
+        player.lnPlay(filePath: filePath, repeatCount: repeatCount)
     }
 
     @objc(lnPlayWithFilePath:)
@@ -56,89 +56,129 @@ public final class LNVAPWrapView: UIView {
         lnPlay(filePath: filePath, repeatCount: 0)
     }
 
+    @objc(lnPlayDeprecatedWithFilePath:fps:blendMode:repeatCount:delegate:)
+    public func lnPlayDeprecated(filePath: String,
+                                 fps: Int,
+                                 blendMode: Int,
+                                 repeatCount: Int,
+                                 delegate: LNVAPWrapPlaybackDelegate?) {
+        _ = blendMode
+        self.delegate = delegate
+        let player = initPlayerViewIfNeed()
+        player.fps = fps
+        player.lnPlay(filePath: filePath, repeatCount: repeatCount)
+    }
+
+
     @objc(lnStop)
     public func lnStop() {
-        LNWrapRuntime.callNoArg(playbackView, selectorName: "stopHWDMP4")
-        notifyStopIfNeeded()
+        playerView?.lnStop()
     }
 
+
     @objc(lnPause)
     public func lnPause() {
-        LNWrapRuntime.callNoArg(playbackView, selectorName: "pauseHWDMP4")
+        playerView?.lnPause()
     }
 
+
     @objc(lnResume)
     public func lnResume() {
-        LNWrapRuntime.callNoArg(playbackView, selectorName: "resumeHWDMP4")
+        playerView?.lnResume()
     }
 
+
     @objc(lnSetMute:)
     public func lnSetMute(_ mute: Bool) {
-        LNWrapRuntime.callBool(playbackView, selectorName: "setMute:", value: mute)
+        let player = initPlayerViewIfNeed()
+        player.lnSetMute(mute)
     }
 
+
+
     @objc(lnAddVapTapGesture:)
     public func lnAddVapTapGesture(_ handler: @escaping LNVAPGestureEventBlock) {
-        let added = LNWrapRuntime.addVapTapGesture(playbackView) { gesture, insideSource, source in
-            handler(gesture, insideSource, source)
-        }
-        if !added {
-            let tap = UITapGestureRecognizer()
-            tap.ln_addActionBlock { sender in
-                guard let gesture = sender as? UIGestureRecognizer else { return }
-                handler(gesture, false, nil)
-            }
-            playbackView.addGestureRecognizer(tap)
-            playbackView.isUserInteractionEnabled = true
-        }
+        let player = initPlayerViewIfNeed()
+        player.lnAddVapTapGesture(handler)
     }
 
     @objc(lnAddVapGesture:callback:)
     public func lnAddVapGesture(_ gestureRecognizer: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) {
-        let added = LNWrapRuntime.addVapGesture(playbackView, gestureRecognizer: gestureRecognizer) { gesture, insideSource, source in
-            callback(gesture, insideSource, source)
+        let player = initPlayerViewIfNeed()
+        player.lnAddVapGesture(gestureRecognizer, callback: callback)
+    }
+
+    @objc(addVapTapGesture:)
+    public func addVapTapGesture(_ handler: @escaping LNVAPGestureEventBlock) {
+        lnAddVapTapGesture(handler)
+    }
+
+    @objc(addVapGesture:callback:)
+    public func addVapGesture(_ gestureRecognizer: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) {
+        lnAddVapGesture(gestureRecognizer, callback: callback)
+    }
+
+    public override func hitTest(_ hitPoint: CGPoint, with event: UIEvent?) -> UIView? {
+        if !isUserInteractionEnabled || isHidden || alpha < 0.01 {
+            return nil
         }
-        if !added {
-            gestureRecognizer.ln_addActionBlock { sender in
-                guard let gesture = sender as? UIGestureRecognizer else { return }
-                callback(gesture, false, nil)
+        if point(inside: hitPoint, with: event) {
+            for subview in subviews.reversed() {
+                let convertedPoint = convert(hitPoint, to: subview)
+                if let hitView = subview.hitTest(convertedPoint, with: event) {
+                    return hitView
+                }
             }
-            playbackView.addGestureRecognizer(gestureRecognizer)
-            playbackView.isUserInteractionEnabled = true
+            return nil
         }
+        return nil
     }
 
     fileprivate func shouldStart(with config: LNVAPConfigModel) -> Bool {
-        delegate?.lnWrapViewShouldStart?(self, config: config) ?? true
+        applyContentMode(with: config)
+        if let allow = delegate?.lnWrapViewShouldStart?(self, config: config) {
+            return allow
+        }
+        return legacyDelegate?.vapWrap_viewshouldStartPlayMP4?(self, config: config) ?? true
     }
 
     fileprivate func notifyStart() {
-        didStart = true
         delegate?.lnWrapViewDidStart?(self)
+        legacyDelegate?.vapWrap_viewDidStartPlayMP4?(self)
     }
 
     fileprivate func notifyPlay(_ frame: LNMP4AnimatedImageFrame) {
         delegate?.lnWrapViewDidPlay?(self, frame: frame)
+        legacyDelegate?.vapWrap_viewDidPlayMP4AtFrame?(frame, view: self)
     }
 
     fileprivate func notifyFinish(_ totalFrameCount: Int) {
         delegate?.lnWrapViewDidFinish?(self, totalFrameCount: totalFrameCount)
+        legacyDelegate?.vapWrap_viewDidFinishPlayMP4?(totalFrameCount, view: self)
     }
 
-    fileprivate func notifyStopIfNeeded() {
-        if didStart {
-            delegate?.lnWrapViewDidStop?(self)
+    fileprivate func notifyStop() {
+        delegate?.lnWrapViewDidStop?(self)
+        legacyDelegate?.vapWrap_viewDidStopPlayMP4?(0, view: self)
+        DispatchQueue.main.async { [weak self] in
+            guard let self else { return }
+            if self.autoDestroyAfterFinish {
+                self.playerView?.removeFromSuperview()
+                self.playerView = nil
+            }
         }
-        didStart = false
     }
 
     fileprivate func notifyFail(_ error: NSError) {
         delegate?.lnWrapViewDidFail?(self, error: error)
-        didStart = false
+        legacyDelegate?.vapWrap_viewDidFailPlayMP4?(error)
     }
 
     fileprivate func contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String? {
-        delegate?.lnWrapViewContent?(forTag: tag, resource: resource)
+        if let text = delegate?.lnWrapViewContent?(forTag: tag, resource: resource) {
+            return text
+        }
+        return legacyDelegate?.vapWrapview_contentForVapTag?(tag, resource: resource)
     }
 
     fileprivate func loadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion) {
@@ -146,241 +186,111 @@ public final class LNVAPWrapView: UIView {
             handler(url, context, completion)
             return
         }
+        if (legacyDelegate as AnyObject?)?.responds(to: #selector(LNVAPWrapLegacyPlaybackDelegate.vapWrapView_loadVapImage(withURL:context:completion:))) == true {
+            legacyDelegate?.vapWrapView_loadVapImage?(withURL: url, context: context, completion: completion)
+            return
+        }
         completion(nil, nil, url)
     }
 
-    private func setupWrapView() {
-        playbackView.frame = bounds
-        playbackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
-        addSubview(playbackView)
-        LNWrapRuntime.set(playbackView, key: "contentMode", value: contentModeOption.legacyRawValue)
-        LNWrapRuntime.set(playbackView, key: "delegate", value: delegateBridge)
-    }
-}
-
-private final class LNWrapDelegateBridge: NSObject {
-    weak var owner: LNVAPWrapView?
-
-    @objc(vapWrap_viewshouldStartPlayMP4:config:)
-    func vapWrap_viewshouldStartPlayMP4(_ container: UIView, config: NSObject) -> Bool {
-        guard let owner else { return true }
-        return owner.shouldStart(with: LNWrapLegacyMapper.toConfig(config))
-    }
-
-    @objc(vapWrap_viewDidStartPlayMP4:)
-    func vapWrap_viewDidStartPlayMP4(_ container: UIView) {
-        owner?.notifyStart()
-    }
-
-    @objc(vapWrap_viewDidPlayMP4AtFrame:view:)
-    func vapWrap_viewDidPlayMP4AtFrame(_ frame: NSObject, view container: UIView) {
-        owner?.notifyPlay(LNWrapLegacyMapper.toFrame(frame))
-    }
-
-    @objc(vapWrap_viewDidStopPlayMP4:view:)
-    func vapWrap_viewDidStopPlayMP4(_ lastFrameIndex: Int, view container: UIView) {
-        owner?.notifyStopIfNeeded()
-    }
-
-    @objc(vapWrap_viewDidFinishPlayMP4:view:)
-    func vapWrap_viewDidFinishPlayMP4(_ totalFrameCount: Int, view container: UIView) {
-        owner?.notifyFinish(totalFrameCount)
-    }
-
-    @objc(vapWrap_viewDidFailPlayMP4:)
-    func vapWrap_viewDidFailPlayMP4(_ error: NSError) {
-        owner?.notifyFail(error)
-    }
-
-    @objc(vapWrapview_contentForVapTag:resource:)
-    func vapWrapview_contentForVapTag(_ tag: String, resource: NSObject) -> String? {
-        guard let owner else { return nil }
-        return owner.contentForTag(tag, resource: LNWrapLegacyMapper.toSource(resource))
-    }
-
-    @objc(vapWrapView_loadVapImageWithURL:context:completion:)
-    func vapWrapView_loadVapImageWithURL(_ url: String, context: NSDictionary, completion: @escaping (UIImage?, NSError?, String) -> Void) {
-        owner?.loadImage(withURL: url, context: context, completion: completion)
-    }
-}
-
-private enum LNWrapLegacyMapper {
-    static func toConfig(_ object: NSObject) -> LNVAPConfigModel {
-        if let config = object as? LNVAPConfigModel {
-            return config
+    private func initPlayerViewIfNeed() -> LNVAPPlayerView {
+        if let playerView {
+            return playerView
         }
-        let config = LNVAPConfigModel()
-        if let infoObj = object.value(forKey: "info") as? NSObject {
-            config.info = toCommonInfo(infoObj)
-        }
-        if let resources = object.value(forKey: "resources") as? [NSObject] {
-            config.resources = resources.map { toSource($0) }
-        }
-        return config
-    }
-
-    static func toCommonInfo(_ object: NSObject) -> LNVAPCommonInfo {
-        let info = LNVAPCommonInfo()
-        info.version = intValue(object, key: "version")
-        info.framesCount = intValue(object, key: "framesCount")
-        info.size = cgSizeValue(object, key: "size")
-        info.videoSize = cgSizeValue(object, key: "videoSize")
-        info.targetOrientaion = LNVAPOrientation(rawValue: intValue(object, key: "targetOrientaion")) ?? .none
-        info.fps = intValue(object, key: "fps")
-        info.isMerged = boolValue(object, key: "isMerged")
-        info.alphaAreaRect = cgRectValue(object, key: "alphaAreaRect")
-        info.rgbAreaRect = cgRectValue(object, key: "rgbAreaRect")
-        return info
-    }
-
-    static func toSource(_ object: NSObject) -> LNVAPSourceInfo {
-        if let source = object as? LNVAPSourceInfo {
-            return source
-        }
-        let source = LNVAPSourceInfo()
-        source.type = stringValue(object, key: "type")
-        source.loadType = stringValue(object, key: "loadType")
-        source.contentTag = stringValue(object, key: "contentTag")
-        source.contentTagValue = stringValue(object, key: "contentTagValue")
-        source.color = object.value(forKey: "color") as? UIColor
-        source.style = stringValue(object, key: "style")
-        source.size = cgSizeValue(object, key: "size")
-        source.fitType = stringValue(object, key: "fitType")
-        source.sourceImage = object.value(forKey: "sourceImage") as? UIImage
-        return source
-    }
-
-    static func toFrame(_ object: NSObject) -> LNMP4AnimatedImageFrame {
-        if let frame = object as? LNMP4AnimatedImageFrame {
-            return frame
-        }
-        let frame = LNMP4AnimatedImageFrame()
-        frame.frameIndex = intValue(object, key: "frameIndex")
-        frame.duration = timeIntervalValue(object, key: "duration")
-        frame.pts = uint64Value(object, key: "pts")
-        frame.defaultFps = Int32(intValue(object, key: "defaultFps"))
-        return frame
-    }
-
-    static func toSourceDisplayItem(_ object: AnyObject?) -> LNVAPSourceDisplayItem? {
-        guard let object else { return nil }
-        if let item = object as? LNVAPSourceDisplayItem {
-            return item
-        }
-        guard let sourceObj = object as? NSObject else { return nil }
-        let item = LNVAPSourceDisplayItem()
-        item.frame = cgRectValue(sourceObj, key: "frame")
-        if let sourceInfoObj = sourceObj.value(forKey: "sourceInfo") as? NSObject {
-            item.sourceInfo = toSource(sourceInfoObj)
+        let view = LNVAPPlayerView(frame: bounds)
+        view.delegate = delegateBridge
+        addSubview(view)
+        playerView = view
+        applyContentModeIfPossible()
+        return view
+    }
+
+    private func applyContentModeIfPossible() {
+        guard let playerView else { return }
+        guard let config = delegateBridge.lastConfig else {
+            playerView.frame = bounds
+            return
         }
-        return item
-    }
-
-    private static func intValue(_ object: NSObject, key: String) -> Int {
-        (object.value(forKey: key) as? NSNumber)?.intValue ?? 0
-    }
-
-    private static func uint64Value(_ object: NSObject, key: String) -> UInt64 {
-        (object.value(forKey: key) as? NSNumber)?.uint64Value ?? 0
-    }
-
-    private static func timeIntervalValue(_ object: NSObject, key: String) -> TimeInterval {
-        (object.value(forKey: key) as? NSNumber)?.doubleValue ?? 0
-    }
-
-    private static func boolValue(_ object: NSObject, key: String) -> Bool {
-        (object.value(forKey: key) as? NSNumber)?.boolValue ?? false
+        applyContentMode(with: config)
     }
 
-    private static func stringValue(_ object: NSObject, key: String) -> String? {
-        object.value(forKey: key) as? String
-    }
-
-    private static func cgSizeValue(_ object: NSObject, key: String) -> CGSize {
-        (object.value(forKey: key) as? NSValue)?.cgSizeValue ?? .zero
-    }
+    private func applyContentMode(with config: LNVAPConfigModel) {
+        guard let playerView else { return }
+        guard let info = config.info, info.size.width > 0, info.size.height > 0 else {
+            playerView.frame = bounds
+            return
+        }
 
-    private static func cgRectValue(_ object: NSObject, key: String) -> CGRect {
-        (object.value(forKey: key) as? NSValue)?.cgRectValue ?? .zero
+        let layoutWidth = bounds.width
+        let layoutHeight = bounds.height
+        guard layoutWidth > 0, layoutHeight > 0 else { return }
+
+        let layoutRatio = layoutWidth / layoutHeight
+        let videoRatio = info.size.width / info.size.height
+
+        switch contentModeOption {
+        case .scaleToFill:
+            playerView.frame = bounds
+        case .aspectFit:
+            let realSize: CGSize
+            if layoutRatio < videoRatio {
+                let width = layoutWidth
+                realSize = CGSize(width: width, height: width / videoRatio)
+            } else {
+                let height = layoutHeight
+                realSize = CGSize(width: height * videoRatio, height: height)
+            }
+            playerView.frame = CGRect(origin: .zero, size: realSize)
+            playerView.center = CGPoint(x: bounds.midX, y: bounds.midY)
+        case .aspectFill:
+            let realSize: CGSize
+            if layoutRatio > videoRatio {
+                let width = layoutWidth
+                realSize = CGSize(width: width, height: width / videoRatio)
+            } else {
+                let height = layoutHeight
+                realSize = CGSize(width: height * videoRatio, height: height)
+            }
+            playerView.frame = CGRect(origin: .zero, size: realSize)
+            playerView.center = CGPoint(x: bounds.midX, y: bounds.midY)
+        }
     }
 }
 
-private enum LNWrapRuntime {
-    private typealias LNObjCGestureBlock = @convention(block) (UIGestureRecognizer, Bool, AnyObject?) -> Void
+private final class LNWrapDelegateBridge: NSObject, LNVAPPlaybackDelegate {
+    weak var owner: LNVAPWrapView?
+    var lastConfig: LNVAPConfigModel?
 
-    static func makeLegacyWrapView(frame: CGRect) -> UIView? {
-        guard let cls = NSClassFromString("QGVAPWrapView") as? UIView.Type ??
-                NSClassFromString("QGVAPlayer.QGVAPWrapView") as? UIView.Type else {
-            return nil
-        }
-        return cls.init(frame: frame)
+    func lnPlayerShouldStart(_ playerView: LNVAPPlayerView, config: LNVAPConfigModel) -> Bool {
+        lastConfig = config
+        return owner?.shouldStart(with: config) ?? true
     }
 
-    static func callNoArg(_ target: NSObject, selectorName: String) {
-        let selector = NSSelectorFromString(selectorName)
-        guard target.responds(to: selector) else { return }
-        typealias Function = @convention(c) (AnyObject, Selector) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector)
+    func lnPlayerDidStart(_ playerView: LNVAPPlayerView) {
+        owner?.notifyStart()
     }
 
-    static func callBool(_ target: NSObject, selectorName: String, value: Bool) {
-        let selector = NSSelectorFromString(selectorName)
-        guard target.responds(to: selector) else { return }
-        typealias Function = @convention(c) (AnyObject, Selector, Bool) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector, value)
+    func lnPlayerDidPlay(_ playerView: LNVAPPlayerView, frame: LNMP4AnimatedImageFrame) {
+        owner?.notifyPlay(frame)
     }
 
-    static func playHWDMP4(_ target: NSObject, filePath: String, repeatCount: Int, delegate: AnyObject?) -> Bool {
-        let selector = NSSelectorFromString("playHWDMP4:repeatCount:delegate:")
-        guard target.responds(to: selector) else { return false }
-        typealias Function = @convention(c) (AnyObject, Selector, NSString, Int, AnyObject?) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector, filePath as NSString, repeatCount, delegate)
-        return true
+    func lnPlayerDidStop(_ playerView: LNVAPPlayerView) {
+        owner?.notifyStop()
     }
 
-    static func addVapTapGesture(_ target: NSObject, callback: @escaping LNVAPGestureEventBlock) -> Bool {
-        let selector = NSSelectorFromString("addVapTapGesture:")
-        guard target.responds(to: selector) else { return false }
-        let block: LNObjCGestureBlock = { gesture, insideSource, sourceObj in
-            callback(gesture, insideSource, LNWrapLegacyMapper.toSourceDisplayItem(sourceObj))
-        }
-        typealias Function = @convention(c) (AnyObject, Selector, AnyObject) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector, unsafeBitCast(block, to: AnyObject.self))
-        return true
-    }
-
-    static func addVapGesture(_ target: NSObject, gestureRecognizer: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) -> Bool {
-        let selector = NSSelectorFromString("addVapGesture:callback:")
-        guard target.responds(to: selector) else { return false }
-        let block: LNObjCGestureBlock = { gesture, insideSource, sourceObj in
-            callback(gesture, insideSource, LNWrapLegacyMapper.toSourceDisplayItem(sourceObj))
-        }
-        typealias Function = @convention(c) (AnyObject, Selector, UIGestureRecognizer, AnyObject) -> Void
-        let imp = target.method(for: selector)
-        unsafeBitCast(imp, to: Function.self)(target, selector, gestureRecognizer, unsafeBitCast(block, to: AnyObject.self))
-        return true
+    func lnPlayerDidFinish(_ playerView: LNVAPPlayerView, totalFrameCount: Int) {
+        owner?.notifyFinish(totalFrameCount)
     }
 
-    static func set(_ target: NSObject, key: String, value: Any) {
-        let setter = setterSelectorName(for: key)
-        let setterSelector = NSSelectorFromString(setter)
-        guard target.responds(to: setterSelector) else { return }
-        target.setValue(value, forKey: key)
+    func lnPlayerDidFail(_ playerView: LNVAPPlayerView, error: NSError) {
+        owner?.notifyFail(error)
     }
 
-    static func boolValue(_ target: NSObject, key: String) -> Bool {
-        let selector = NSSelectorFromString(key)
-        guard target.responds(to: selector) else { return false }
-        if let value = target.value(forKey: key) as? NSNumber { return value.boolValue }
-        return false
+    func lnPlayerContent(forTag tag: String, resource: LNVAPSourceInfo) -> String? {
+        owner?.contentForTag(tag, resource: resource)
     }
 
-    private static func setterSelectorName(for key: String) -> String {
-        guard let first = key.first else { return "" }
-        return "set\(String(first).uppercased())\(key.dropFirst()):"
+    func lnPlayerLoadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion) {
+        owner?.loadImage(withURL: url, context: context, completion: completion)
     }
 }