Эх сурвалжийг харах

[*] 修复了部分可以引起问题的地方

yanxuyao 2 долоо хоног өмнө
parent
commit
2f48a08e39

+ 49 - 19
QGVAPlayer/QGVAPlayer/LNSwift/Core/LNControllers.swift

@@ -72,13 +72,15 @@ public final class LNAnimatedImageDecodeThreadPool: NSObject {
 
 @objcMembers
 public final class LNAnimatedImageBufferManager: NSObject {
-    public var buffers: NSMutableArray
+    // 使用线程安全数组,与 ObjC 版 QGVAPSafeMutableArray 保持一致,
+    // 避免解码线程写入与主线程读取之间的数据竞争。
+    public var buffers: LNVAPSafeMutableArray
     private let config: LNAnimatedImageDecodeConfig
 
     @objc(initWithConfig:)
     public init(config: LNAnimatedImageDecodeConfig) {
         self.config = config
-        self.buffers = NSMutableArray(capacity: max(config.bufferCount, 0))
+        self.buffers = LNVAPSafeMutableArray(capacity: max(config.bufferCount, 0))
         super.init()
     }
 
@@ -102,8 +104,9 @@ public final class LNAnimatedImageBufferManager: NSObject {
 
     @objc(isBufferFull)
     public func isBufferFull() -> Bool {
-        if buffers.count < config.bufferCount { return false }
-        for case let obj in buffers {
+        let all = buffers.allObjects()
+        if all.count < config.bufferCount { return false }
+        for obj in all {
             if !(obj is LNBaseAnimatedImageFrame) {
                 return false
             }
@@ -196,12 +199,23 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
         parser = (fileInfo as? LNMP4HWDFileInfo)?.mp4Parser
         if !onInputStart() {
             isFinish = true
+            // 确保 initializationError 不为 nil,使 createDecoders 能正确识别失败。
+            // onInputStart() 各失败路径已设置具体 error;此处仅作兜底保护。
+            if initializationError == nil {
+                initializationError = NSError(
+                    domain: Self.errorDomain,
+                    code: LNMP4HWDErrorCode.invalidMP4File.rawValue,
+                    userInfo: [NSLocalizedDescriptionKey: "decoder initialization failed", "location": fileInfo.filePath]
+                )
+            }
         }
     }
 
     public override func decodeFrame(_ frameIndex: Int, buffers: NSMutableArray) {
         if frameIndex == currentDecodeFrame { return }
         currentDecodeFrame = frameIndex
+        // 兼容外部传入 NSMutableArray 的调用(如旧桥接代码),
+        // 实际上 LNAnimatedImageBufferManager 传入的是 LNVAPSafeMutableArray(线程安全)。
         self.buffers = buffers
 
         decodeQueue.async { [weak self] in
@@ -258,7 +272,10 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
         }
 
         if parser.videoCodecID == .h264 {
-            guard spsData.count > 0, ppsData.count > 0 else { return false }
+            guard spsData.count > 0, ppsData.count > 0 else {
+                initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
+                return false
+            }
             let status = spsData.withUnsafeBytes { spsRawPtr in
                 ppsData.withUnsafeBytes { ppsRawPtr in
                     guard let spsBase = spsRawPtr.baseAddress,
@@ -290,7 +307,10 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
                 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 }
+            guard vpsData.count > 0, spsData.count > 0, ppsData.count > 0 else {
+                initializationError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
+                return false
+            }
             let status = vpsData.withUnsafeBytes { vpsRawPtr in
                 spsData.withUnsafeBytes { spsRawPtr in
                     ppsData.withUnsafeBytes { ppsRawPtr in
@@ -458,7 +478,8 @@ public final class LNMP4FrameHWDecoder: LNBaseDecoder {
         newFrame.pts = currentPts
 
         buffers.add(newFrame)
-        let sorted = (buffers as? [LNMP4AnimatedImageFrame] ?? []).sorted { $0.pts < $1.pts }
+        // 按 pts 排序:取出所有帧、排序后重新填入(LNVAPSafeMutableArray 内部加锁保证线程安全)
+        let sorted = buffers.compactMap { $0 as? LNMP4AnimatedImageFrame }.sorted { $0.pts < $1.pts }
         buffers.removeAllObjects()
         sorted.forEach { buffers.add($0) }
     }
@@ -609,7 +630,10 @@ public final class LNAnimatedImageDecodeManager: NSObject {
         let decoderIndex = decoders.count == 1 ? 0 : frameIndex % decoders.count
         let decoder = decoders[decoderIndex]
         if decoder.shouldStopDecode(frameIndex) { return }
-        decoder.decodeFrame(frameIndex, buffers: bufferManager.buffers)
+        // bufferManager.buffers 是 LNVAPSafeMutableArray(线程安全),
+        // 通过 backingArray 取得内部的 NSMutableArray 引用传给 decoder,
+        // 所有写操作仍经由 LNVAPSafeMutableArray 的加锁方法完成。
+        decoder.decodeFrame(frameIndex, buffers: bufferManager.buffers.backingMutableArray)
     }
 
     private func createDecoders(by config: LNAnimatedImageDecodeConfig) {
@@ -682,14 +706,30 @@ public final class LNVAPConfigManager: NSObject {
             return
         }
 
+        // Step 1: 填充 contentTagValue(与 ObjC 版一致,先于所有资源处理)
         if let contentProvider = delegate?.vap_contentForTag {
             model.resources.forEach { resource in
                 resource.contentTagValue = contentProvider(resource.contentTag ?? "", resource)
             }
         }
 
+        // Step 2: 本地文本资源独立处理(与 ObjC 版两个独立 if 结构对齐,
+        //         不依赖 loadImage delegate 是否存在)
+        for resource in model.resources {
+            if resource.type == LNVAPAttachmentConstants.sourceTypeText,
+               resource.loadType == LNVAPAttachmentConstants.sourceLoadTypeLocal {
+                resource.sourceImage = LNVAPTextureLoader.drawingImage(
+                    forText: resource.contentTagValue,
+                    color: resource.color,
+                    size: resource.size,
+                    bold: resource.style == LNVAPAttachmentConstants.sourceStyleBoldText
+                )
+            }
+        }
+
+        // Step 3: 网络图片资源异步加载
         guard let loadImage = delegate?.vap_loadImageWithURL else {
-            // Keep parity with OC: if no image loader delegate, return directly.
+            // 与 ObjC 版对齐:无 image loader delegate 时直接返回,不回调 onVAPConfigResourcesLoaded
             return
         }
 
@@ -699,18 +739,8 @@ public final class LNVAPConfigManager: NSObject {
             guard resource.type == LNVAPAttachmentConstants.sourceTypeImg,
                   resource.loadType == LNVAPAttachmentConstants.sourceLoadTypeNet,
                   let url = resource.contentTagValue else {
-                if resource.type == LNVAPAttachmentConstants.sourceTypeText,
-                   resource.loadType == LNVAPAttachmentConstants.sourceLoadTypeLocal {
-                    resource.sourceImage = LNVAPTextureLoader.drawingImage(
-                        forText: resource.contentTagValue,
-                        color: resource.color,
-                        size: resource.size,
-                        bold: resource.style == LNVAPAttachmentConstants.sourceStyleBoldText
-                    )
-                }
                 continue
             }
-
             group.enter()
             loadImage(url, ["resource": resource]) { image, error, imageURL in
                 if image == nil || error != nil {

+ 82 - 20
QGVAPlayer/QGVAPlayer/LNSwift/Render/LNRenderers.swift

@@ -96,35 +96,25 @@ private final class LNHWDMetalCoreRenderer {
 
     private let device: MTLDevice
     private let commandQueue: MTLCommandQueue
-    private let pipelineState: MTLRenderPipelineState
+    private var pipelineState: MTLRenderPipelineState?
     private var textureCache: CVMetalTextureCache?
     private var vertexBuffer: MTLBuffer?
     private var yuvMatrixBuffer: MTLBuffer?
     private var currentColorMatrix = matrix601Full
+    // 与 ObjC QGHWDMetalRenderer 对齐:标记渲染资源是否已被回收,用于 reconstruct
+    private var renderingResourcesDisposed = false
+    private var currentBlendMode: LNTextureBlendMode
+    private weak var metalLayerRef: CAMetalLayer?
 
     init?(metalLayer: CAMetalLayer, blendMode: LNTextureBlendMode) {
         guard let device = MTLCreateSystemDefaultDevice(),
               let queue = device.makeCommandQueue() else {
             return nil
         }
-
-        let shaderLoader = LNVAPMetalShaderFunctionLoader(device: device)
-        guard let vertexFunc = shaderLoader.loadFunction(withName: Self.kVertexFunctionName),
-              let fragmentFunc = shaderLoader.loadFunction(withName: Self.kFragmentFunctionName) else {
-            return nil
-        }
-
-        let desc = MTLRenderPipelineDescriptor()
-        desc.vertexFunction = vertexFunc
-        desc.fragmentFunction = fragmentFunc
-        desc.colorAttachments[0].pixelFormat = metalLayer.pixelFormat
-        guard let pipelineState = try? device.makeRenderPipelineState(descriptor: desc) else {
-            return nil
-        }
-
         self.device = device
         self.commandQueue = queue
-        self.pipelineState = pipelineState
+        self.currentBlendMode = blendMode
+        self.metalLayerRef = metalLayer
 
         metalLayer.device = device
         metalLayer.framebufferOnly = true
@@ -132,19 +122,25 @@ private final class LNHWDMetalCoreRenderer {
         let cacheStatus = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &textureCache)
         guard cacheStatus == kCVReturnSuccess else { return nil }
 
+        setupPipelineState(metalLayer: metalLayer)
         updateBlendMode(blendMode)
         updateYuvMatrixBuffer(matrix: Self.matrix601Full)
     }
 
     func updateBlendMode(_ blendMode: LNTextureBlendMode) {
+        currentBlendMode = blendMode
         let modeIndex = max(0, min(Int(blendMode.rawValue), Self.verticesByBlendMode.count - 1))
         let flat = Self.verticesByBlendMode[modeIndex].flatMap { $0 }
         vertexBuffer = device.makeBuffer(bytes: flat, length: flat.count * MemoryLayout<Float>.size, options: .storageModeShared)
     }
 
     func render(pixelBuffer: CVPixelBuffer, metalLayer: CAMetalLayer) {
+        // 与 ObjC QGHWDMetalRenderer.reconstructIfNeed 对齐:dispose 后自动重建渲染资源
+        reconstructIfNeeded(metalLayer: metalLayer)
+
         guard metalLayer.bounds.width > 0,
               metalLayer.bounds.height > 0,
+              let pipelineState,
               let drawable = metalLayer.nextDrawable(),
               let commandBuffer = commandQueue.makeCommandBuffer(),
               let descriptor = currentRenderPassDescriptor(texture: drawable.texture),
@@ -157,6 +153,7 @@ private final class LNHWDMetalCoreRenderer {
         updateYuvMatrixIfNeeded(pixelBuffer: pixelBuffer)
         guard let yTexture = makeTexture(pixelBuffer: pixelBuffer, plane: 0, format: .r8Unorm),
               let uvTexture = makeTexture(pixelBuffer: pixelBuffer, plane: 1, format: .rg8Unorm) else {
+            encoder.endEncoding()
             return
         }
 
@@ -179,6 +176,33 @@ private final class LNHWDMetalCoreRenderer {
         textureCache = nil
         vertexBuffer = nil
         yuvMatrixBuffer = nil
+        pipelineState = nil
+        renderingResourcesDisposed = true
+    }
+
+    // 与 ObjC reconstructIfNeed: 对齐:dispose 后下次渲染前自动重建资源
+    private func reconstructIfNeeded(metalLayer: CAMetalLayer) {
+        guard renderingResourcesDisposed else { return }
+        if textureCache == nil {
+            CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &textureCache)
+        }
+        setupPipelineState(metalLayer: metalLayer)
+        updateBlendMode(currentBlendMode)
+        updateYuvMatrixBuffer(matrix: currentColorMatrix)
+        renderingResourcesDisposed = false
+    }
+
+    private func setupPipelineState(metalLayer: CAMetalLayer) {
+        let shaderLoader = LNVAPMetalShaderFunctionLoader(device: device)
+        guard let vertexFunc = shaderLoader.loadFunction(withName: Self.kVertexFunctionName),
+              let fragmentFunc = shaderLoader.loadFunction(withName: Self.kFragmentFunctionName) else {
+            return
+        }
+        let desc = MTLRenderPipelineDescriptor()
+        desc.vertexFunction = vertexFunc
+        desc.fragmentFunction = fragmentFunc
+        desc.colorAttachments[0].pixelFormat = metalLayer.pixelFormat
+        pipelineState = try? device.makeRenderPipelineState(descriptor: desc)
     }
 
     private func makeTexture(pixelBuffer: CVPixelBuffer, plane: Int, format: MTLPixelFormat) -> MTLTexture? {
@@ -262,11 +286,11 @@ private final class LNVAPMetalCoreRenderer {
     private var maskBlurBuffer: MTLBuffer?
     private var maskTexture: MTLTexture?
     private var currentColorMatrix = matrix601Full
+    // 与 ObjC QGVAPMetalRenderer 对齐:标记渲染资源是否已被回收,用于 reconstruct
+    private var renderingResourcesDisposed = false
 
     var commonInfo: LNVAPCommonInfo? {
-        didSet {
-            updateMainVertexBuffer()
-        }
+        didSet { updateMainVertexBuffer() }
     }
 
     var maskInfo: LNVAPMaskInfo? {
@@ -297,6 +321,9 @@ private final class LNVAPMetalCoreRenderer {
     }
 
     func render(pixelBuffer: CVPixelBuffer, metalLayer: CAMetalLayer, mergeInfos: [LNVAPMergedInfo]) {
+        // 与 ObjC QGVAPMetalRenderer.reconstructIfNeed 对齐:dispose 后自动重建渲染资源
+        reconstructIfNeeded()
+
         guard metalLayer.bounds.width > 0,
               metalLayer.bounds.height > 0,
               let drawable = metalLayer.nextDrawable(),
@@ -309,6 +336,7 @@ private final class LNVAPMetalCoreRenderer {
         updateYuvMatrixIfNeeded(pixelBuffer: pixelBuffer)
         guard let yTexture = makeTexture(pixelBuffer: pixelBuffer, plane: 0, format: .r8Unorm),
               let uvTexture = makeTexture(pixelBuffer: pixelBuffer, plane: 1, format: .rg8Unorm) else {
+            encoder.endEncoding()
             return
         }
 
@@ -333,6 +361,19 @@ private final class LNVAPMetalCoreRenderer {
         mainPipelineStateForMask = nil
         mainPipelineStateForMaskBlur = nil
         attachmentPipelineState = nil
+        renderingResourcesDisposed = true
+    }
+
+    // 与 ObjC QGVAPMetalRenderer.reconstructIfNeed: 对齐:dispose 后下次渲染前自动重建资源
+    private func reconstructIfNeeded() {
+        guard renderingResourcesDisposed else { return }
+        if textureCache == nil {
+            CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &textureCache)
+        }
+        updateYuvMatrixBuffer(matrix: currentColorMatrix)
+        // 重建顶点缓冲(commonInfo/maskInfo 未变,直接重建)
+        updateMainVertexBuffer()
+        renderingResourcesDisposed = false
     }
 
     private func drawBackground(encoder: MTLRenderCommandEncoder, yTexture: MTLTexture, uvTexture: MTLTexture) {
@@ -648,6 +689,8 @@ public final class LNHWDMetalView: UIView {
     private let unavailableBridge = LNMetalUnavailableBridge()
     private var renderLayer: CALayer = CALayer()
     private lazy var fallbackRenderer = LNHWDMetalRenderer(metalLayer: renderLayer, blendMode: blendMode)
+    // 与 ObjC QGHWDMetalView 保持一致:视图尺寸或窗口变化后需要更新 drawableSize
+    private var drawableSizeShouldUpdate = true
 
     @objc(initWithFrame:blendMode:)
     public init(frame: CGRect, blendMode: LNTextureBlendMode) {
@@ -684,10 +727,29 @@ public final class LNHWDMetalView: UIView {
     public override func layoutSubviews() {
         super.layoutSubviews()
         renderLayer.frame = bounds
+        drawableSizeShouldUpdate = true
+    }
+
+    public override func didMoveToWindow() {
+        super.didMoveToWindow()
+        drawableSizeShouldUpdate = true
     }
 
     @objc(display:)
     public func display(_ pixelBuffer: CVPixelBuffer?) {
+        // 与 ObjC QGHWDMetalView 对齐:每次渲染前检查是否需要更新 drawableSize,
+        // 避免视图尺寸变化后画面出现模糊或错位。
+        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.blendMode = blendMode
         fallbackRenderer.renderPixelBuffer(pixelBuffer, metalLayer: renderLayer)
     }
 

+ 19 - 2
QGVAPlayer/QGVAPlayer/LNSwift/Utils/LNUtilities.swift

@@ -73,6 +73,15 @@ public final class LNVAPSafeMutableArray: NSObject {
         super.init()
     }
 
+    /// 返回内部的 NSMutableArray 引用,供需要 NSMutableArray 参数的接口使用。
+    /// 注意:调用方不应直接对返回值做并发写操作,所有写操作应通过
+    /// LNVAPSafeMutableArray 的加锁方法(add/removeObject 等)进行。
+    public var backingMutableArray: NSMutableArray {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage
+    }
+
     @objc(count)
     public var count: Int {
         lock.lock()
@@ -259,6 +268,10 @@ public final class LNVAPMetalShaderFunctionLoader: NSObject {
     private let device: MTLDevice
     private var defaultLibrary: MTLLibrary?
     private var fallbackLibrary: MTLLibrary?
+    // 与 ObjC QGVAPMetalShaderFunctionLoader 对齐:记录是否已尝试过加载,
+    // 避免每次 loadFunction 都触发重复的文件 IO 和编译操作。
+    private var alreadyLoadedDefaultLibrary = false
+    private var alreadyLoadedFallbackLibrary = false
 
     @objc(initWithDevice:)
     public init(device: MTLDevice) {
@@ -268,7 +281,9 @@ public final class LNVAPMetalShaderFunctionLoader: NSObject {
 
     @objc(loadFunctionWithName:)
     public func loadFunction(withName functionName: String) -> MTLFunction? {
-        if defaultLibrary == nil {
+        // 只在首次(或之前失败时)尝试加载 defaultLibrary
+        if defaultLibrary == nil && !alreadyLoadedDefaultLibrary {
+            alreadyLoadedDefaultLibrary = true
             if let path = Bundle(for: Self.self).path(forResource: "default", ofType: "metallib") {
                 defaultLibrary = try? device.makeLibrary(filepath: path)
             }
@@ -279,7 +294,9 @@ public final class LNVAPMetalShaderFunctionLoader: NSObject {
         if let function = defaultLibrary?.makeFunction(name: functionName) {
             return function
         }
-        if fallbackLibrary == nil {
+        // 只在首次(或之前失败时)尝试编译 fallbackLibrary
+        if fallbackLibrary == nil && !alreadyLoadedFallbackLibrary {
+            alreadyLoadedFallbackLibrary = true
             fallbackLibrary = try? device.makeLibrary(source: Self.fallbackShaderSource, options: nil)
         }
         return fallbackLibrary?.makeFunction(name: functionName)

+ 3 - 3
QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPWrapView.swift

@@ -39,9 +39,9 @@ public final class LNVAPWrapView: UIView {
 
     public override func layoutSubviews() {
         super.layoutSubviews()
-        if contentModeOption == .scaleToFill {
-            playerView?.frame = bounds
-        }
+        // 所有内容模式下都需要在 bounds 变化时(旋转、resize)重新应用布局。
+        // scaleToFill 直接填满;aspectFit/Fill 通过 applyContentModeIfPossible 重算。
+        applyContentModeIfPossible()
     }
 
     @objc(lnPlayWithFilePath:repeatCount:)