Bladeren bron

[*] Swift重写, 进度40%

yanxuyao 1 maand geleden
bovenliggende
commit
5b243f2b07
28 gewijzigde bestanden met toevoegingen van 5124 en 270 verwijderingen
  1. 44 0
      CONTROLLERS_API_MIGRATION_TABLE.md
  2. 10 0
      LN_STATIC_CHECK.md
  3. 41 0
      MIGRATION_GAP_CHECKLIST.md
  4. 51 0
      MODEL_PARSER_API_MIGRATION_TABLE.md
  5. 114 0
      QGVAPlayer/QGVAPlayer.xcodeproj/project.pbxproj
  6. 6 0
      QGVAPlayer/QGVAPlayer/Classes/QGVAPWrapView.h
  7. 1 2
      QGVAPlayer/QGVAPlayer/Classes/QGVAPlayer.h
  8. 7 1
      QGVAPlayer/QGVAPlayer/Classes/UIView+VAP.h
  9. 4 0
      QGVAPlayer/QGVAPlayer/Classes/Utils/Logger/QGVAPLogger.h
  10. 0 1
      QGVAPlayer/QGVAPlayer/Classes/VAPMacros.h
  11. 1 0
      QGVAPlayer/QGVAPlayer/Classes/Views/Metal/Vapx/QGVAPMetalRenderer.h
  12. 25 0
      QGVAPlayer/QGVAPlayer/LNSwift/Bridges/LNLegacyMappings.swift
  13. 807 0
      QGVAPlayer/QGVAPlayer/LNSwift/Core/LNControllers.swift
  14. 55 0
      QGVAPlayer/QGVAPlayer/LNSwift/Core/LNPlaybackTypes.swift
  15. 17 0
      QGVAPlayer/QGVAPlayer/LNSwift/Core/LNVAPFacade.swift
  16. 353 0
      QGVAPlayer/QGVAPlayer/LNSwift/Model/LNModels.swift
  17. 870 0
      QGVAPlayer/QGVAPlayer/LNSwift/Parser/LNMP4Parser.swift
  18. 1072 0
      QGVAPlayer/QGVAPlayer/LNSwift/Render/LNRenderers.swift
  19. 681 0
      QGVAPlayer/QGVAPlayer/LNSwift/Utils/LNUtilities.swift
  20. 394 0
      QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPPlayerView.swift
  21. 386 0
      QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPWrapView.swift
  22. 45 196
      QGVAPlayerDemo/QGVAPlayerDemo/ViewController.m
  23. 2 19
      QGVAPlayerDemoSwift/QGVAPlayerDemoSwift/QGVAPlayer-Bridging-Header.h
  24. 31 51
      QGVAPlayerDemoSwift/QGVAPlayerDemoSwift/ViewController.swift
  25. 35 0
      RENDER_API_MIGRATION_TABLE.md
  26. 20 0
      SWIFT_MIGRATION_MAP.md
  27. 20 0
      UTILS_API_MIGRATION_TABLE.md
  28. 32 0
      VIEW_API_MIGRATION_TABLE.md

+ 44 - 0
CONTROLLERS_API_MIGRATION_TABLE.md

@@ -0,0 +1,44 @@
+# Controllers API Migration Table (OC -> LN Swift)
+
+## Decode Core
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGAnimatedImageDecodeConfig` | `+defaultConfig` | `LNAnimatedImageDecodeConfig` | `@objc(defaultConfig) defaultConfig()` | Done |
+| `QGAnimatedImageDecodeThread` | `occupied`, `sequenceDec` | `LNAnimatedImageDecodeThread` | same semantic properties | Done |
+| `QGAnimatedImageDecodeThreadPool` | `+sharedPool`, `-getDecodeThread` | `LNAnimatedImageDecodeThreadPool` | `sharedPool()`, `getDecodeThread()` | Done |
+| `QGAnimatedImageBufferManager` | `-getBufferedFrame:` | `LNAnimatedImageBufferManager` | `@objc(getBufferedFrame:) getBufferedFrame(_:)` | Done |
+| `QGAnimatedImageBufferManager` | `-isBufferFull` | `LNAnimatedImageBufferManager` | `@objc(isBufferFull) isBufferFull()` | Done |
+| `QGAnimatedImageBufferManager` | `-popVideoFrame` | `LNAnimatedImageBufferManager` | `@objc(popVideoFrame) popVideoFrame()` | Done |
+| `QGBaseDecoder` | `-initWith:error:` | `LNBaseDecoder` | `init(fileInfo:)` | Done |
+| `QGBaseDecoder` | `-decodeFrame:buffers:` | `LNBaseDecoder` | `decodeFrame(_:buffers:)` | Done |
+| `QGBaseDecoder` | `-shouldStopDecode:` | `LNBaseDecoder` | `shouldStopDecode(_:)` | Done |
+| `QGBaseDecoder` | `-isFrameIndexBeyondEnd:` | `LNBaseDecoder` | `isFrameIndexBeyondEnd(_:)` | Done |
+| `QGAnimatedImageDecodeManager` | `-initWith:config:delegate:` | `LNAnimatedImageDecodeManager` | `init(fileInfo:config:delegate:)` | Done |
+| `QGAnimatedImageDecodeManager` | `-consumeDecodedFrame:` | `LNAnimatedImageDecodeManager` | `@objc(consumeDecodedFrame:) consumeDecodedFrame(_:)` | Done |
+| `QGAnimatedImageDecodeManager` | `-tryToStart/Stop/Pause/ResumeAudioPlay` | `LNAnimatedImageDecodeManager` | same semantic APIs | Done |
+| `QGAnimatedImageDecodeManager` | `-containsThisDeocder:` | `LNAnimatedImageDecodeManager` | `@objc(containsThisDeocder:) containsThisDeocder(_:)` | Done |
+
+## HW Decoder
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGMP4FrameHWDecoder` | `+errorDescriptionForCode:` | `LNMP4FrameHWDecoder` | `@objc(errorDescriptionForCode:) errorDescription(for:)` | Done |
+| `QGMP4FrameHWDecoder` | `-decodeFrame:buffers:` | `LNMP4FrameHWDecoder` | `override decodeFrame(_:buffers:)` | Done |
+| `QGMP4FrameHWDecoder` | `-shouldStopDecode:` | `LNMP4FrameHWDecoder` | `override shouldStopDecode(_:)` | Done |
+| `QGMP4FrameHWDecoder` | `-isFrameIndexBeyondEnd:` | `LNMP4FrameHWDecoder` | `override isFrameIndexBeyondEnd(_:)` | Done |
+| `QGMP4FrameHWDecoder` | `VT session init/reset/seek keyframe` | `LNMP4FrameHWDecoder` | same semantic flow in Swift | Done |
+
+## Config Manager
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGVAPConfigManager` | `-initWith:` | `LNVAPConfigManager` | `init(fileInfo:)` | Done |
+| `QGVAPConfigManager` | `-loadConfigResources` | `LNVAPConfigManager` | `@objc(loadConfigResources) loadConfigResources()` | Done |
+| `QGVAPConfigManager` | `-loadMTLTextures:` | `LNVAPConfigManager` | `@objc(loadMTLTextures:) loadMTLTextures(_:)` | Done |
+| `QGVAPConfigManager` | `-loadMTLBuffers:` | `LNVAPConfigManager` | `@objc(loadMTLBuffers:) loadMTLBuffers(_:)` | Done |
+
+## Files
+
+- `/Users/yanxuyao/Vap/QGVAPlayer/QGVAPlayer/LNSwift/Core/LNControllers.swift`
+

+ 10 - 0
LN_STATIC_CHECK.md

@@ -0,0 +1,10 @@
+# LN Static Check
+
+- Legacy Objective-C interface count: 68
+- LN Swift type count: 80
+- Render migration status: baseline implementation completed, behavior parity pending (see `RENDER_API_MIGRATION_TABLE.md`)
+- Utils migration status: baseline implementation + category helpers completed (see `UTILS_API_MIGRATION_TABLE.md`)
+- Build status:
+  - QGVAPlayer: PASS (iPhone 16, iOS 18.5 simulator, destination id `041C8A06-8630-4BBC-BCEA-1B693F9B77AD`)
+  - QGVAPlayerDemo: PASS
+  - QGVAPlayerDemoSwift: PASS

+ 41 - 0
MIGRATION_GAP_CHECKLIST.md

@@ -0,0 +1,41 @@
+# Swift 迁移遗漏清单与顺序(风险/依赖排序)
+
+## P0 基础可用性(已完成)
+
+1. Render 基线实现补齐(`LNRenderers.swift`)
+- 现状:已完成,且已加入 runtime-bridge 到 OC render 内核,非 bridge 场景使用 Swift fallback。
+- 验收:SDK + 两个 Demo 编译通过;调用路径可走 `render/display/dispose`。
+
+2. Utils 基础与类别能力补齐(`LNUtilities.swift`)
+- 现状:已完成,包含 logger/线程安全容器/weak proxy/metal util + NSArray/NSDictionary/UIColor/UIDevice/UIGestureRecognizer/NotificationCenter 的 LN 版本能力。
+- 验收:静态检查与三 scheme 编译通过。
+
+## P1 行为一致性(当前阶段)
+
+3. Render 行为对齐 OC(blend/mask/merge)
+- 风险:高。关系到视觉正确性。
+- 依赖:P0 Render 完成。
+- 现状:HWD 与 VAP 渲染均已切入 Swift Metal 主路径(含 mask/blur);`LNHWDMP4OpenGLView` 已切到 Swift 主路径并保留旧 OC 对照实现。当前剩余为像素级行为对齐与性能调优。
+- 验收:同素材下新旧实现视觉对照一致(alpha、mask、合成区域)。
+
+4. 播放回调契约补齐(LN delegate 覆盖旧能力)
+- 风险:中高。当前 LN delegate 缺少 frame/finish/资源注入等能力。
+- 依赖:P0/P1 Render。
+- 现状:已完成,`LNVAPPlaybackDelegate` / `LNVAPWrapPlaybackDelegate` 已覆盖 shouldStart/start/play/stop/finish/fail/content/loadImage。
+- 验收:旧公开回调能力在 LN API 中有等价入口。
+
+5. View 层主路径去 legacy 编译期依赖(`LNVAPPlayerView` / `LNVAPWrapView`)
+- 现状:已完成。通过 runtime bridge 调用旧实现,不再直接依赖 `UIView+VAP.h` / `QGVAPWrapView.h`。
+- 补充:`addVapTapGesture` / `addVapGesture` 已补齐 LN API,对齐旧手势契约。
+
+## P2 收口与替换(下一阶段)
+
+5. Demo 逐步从 legacy 内核切到 LN 内核
+- 风险:中。需要先有稳定 LN Render。
+- 依赖:P1。
+- 验收:Demo 在不依赖旧 category 主路径下完成播放闭环。
+
+6. 迁移对照表收敛到“Done/Not Done”
+- 风险:低。
+- 依赖:前述功能完成。
+- 验收:逐类表中不再存在“API Done / Impl Pending”。

+ 51 - 0
MODEL_PARSER_API_MIGRATION_TABLE.md

@@ -0,0 +1,51 @@
+# Model + Parser API Migration Table (OC -> LN Swift)
+
+## Model
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGBaseDFileInfo` | properties: `filePath`, `occupiedCount` | `LNBaseDFileInfo` | same semantic properties | Done |
+| `QGBaseAnimatedImageFrame` | properties: `frameIndex`, `duration`, `pts` | `LNBaseAnimatedImageFrame` | same semantic properties | Done |
+| `QGBaseAnimatedImageFrame+Displaying` | `-shouldFinishDisplaying` + `startDate`/`decodeTime` | `LNBaseAnimatedImageFrame` | `@objc(shouldFinishDisplaying) shouldFinishDisplaying()` + same properties | Done |
+| `QGMP4AnimatedImageFrame` | properties: `pixelBuffer`, `defaultFps` | `LNMP4AnimatedImageFrame` | same semantic properties | Done |
+| `QGMP4HWDFileInfo` | property: `mp4Parser` | `LNMP4HWDFileInfo` | property: `mp4Parser: LNMP4ParserProxy?` | Done |
+| `QGVAPConfigModel` | properties: `info/resources/mergedConfig` | `LNVAPConfigModel` | same semantic properties | Done |
+| `QGVAPCommonInfo` | common info properties | `LNVAPCommonInfo` | same semantic properties | Done |
+| `QGVAPSourceInfo` | source properties | `LNVAPSourceInfo` | same semantic properties | Done |
+| `QGVAPSourceDisplayItem` | `frame/sourceInfo` | `LNVAPSourceDisplayItem` | same semantic properties | Done |
+| `QGVAPMergedInfo` | `-vertexBufferWithContainerSize:maskContianerSize:device:` | `LNVAPMergedInfo` | `@objc(vertexBufferWithContainerSize:maskContianerSize:device:) vertexBuffer(containerSize:maskContianerSize:device:)` | Done |
+| `QGVAPMaskInfo` | computed readonly `texture` + mask properties | `LNVAPMaskInfo` | computed readonly `texture` + same semantic properties | Done |
+| `QGVAPTextureLoader` | `+loadVapColorFillBufferWith:device:` | `LNVAPTextureLoader` | same ObjC selector exposed via `@objc` | Done |
+| `QGVAPTextureLoader` | `+loadTextureWithImage:device:` | `LNVAPTextureLoader` | same ObjC selector exposed via `@objc` | Done |
+| `QGVAPTextureLoader` | `+loadTextureWithData:device:width:height:` | `LNVAPTextureLoader` | same ObjC selector exposed via `@objc` | Done |
+| `QGVAPTextureLoader` | `+drawingImageForText:color:size:bold:` | `LNVAPTextureLoader` | same ObjC selector exposed via `@objc` | Done |
+| `QGVAPTextureLoader` | `+getAppropriateFontWith:rect:designedSize:bold:textSize:` | `LNVAPTextureLoader` | same ObjC selector exposed via `@objc` | Done |
+
+## Parser
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGMP4BoxFactory` | `+isTypeValueValid:` | `LNMP4BoxFactory` | `@objc(isTypeValueValid:) isTypeValueValid(_:)` | Done |
+| `QGMP4BoxFactory` | `+boxClassForType:` | `LNMP4BoxFactory` | `@objc(boxClassForType:) boxClass(for:)` | Done |
+| `QGMP4BoxFactory` | `+createBoxForType:startIndex:length:` | `LNMP4BoxFactory` | `@objc(createBoxForType:startIndex:length:) createBox(for:startIndex:length:)` | Done |
+| `QGMP4Box` | `-initWithType:startIndex:length:` | `LNMP4Box` | `init(type:startIndex:length:)` | Done |
+| `QGMP4Box` | `-subBoxOfType:` | `LNMP4Box` | `@objc(subBoxOfType:) subBox(ofType:)` | Done |
+| `QGMP4Box` | `-superBoxOfType:` | `LNMP4Box` | `@objc(superBoxOfType:) superBox(ofType:)` | Done |
+| `QGMP4Box` + subclasses | `-boxDidParsed:` | `LNMP4Box` + subclasses | `boxDidParsed(_:)` (override in parsed box subclasses) | Done |
+| `QGMP4Parser` | `-initWithFilePath:` | `LNMP4Parser` | `init(filePath:)` | Done |
+| `QGMP4Parser` | `-parse` | `LNMP4Parser` | `parse()` | Done |
+| `QGMP4Parser` | `-readDataForBox:` | `LNMP4Parser` | `@objc(readDataForBox:) readData(for:)` | Done |
+| `QGMP4Parser` | `-readValue:length:` | `LNMP4Parser` | `@objc(readValue:length:) readValue(_:length:)` | Done |
+| `QGMP4ParserDelegate` | `-didParseMP4Box:parser:` | `LNMP4ParserDelegate` | `didParseMP4Box(_:parser:)` | Done |
+| `QGMP4ParserDelegate` | `-MP4FileDidFinishParse:` | `LNMP4ParserDelegate` | `mp4FileDidFinishParse(_:)` | Done (Swift-style name) |
+| `QGMP4ParserProxy` | `-initWithFilePath:` | `LNMP4ParserProxy` | `init(filePath:)` | Done |
+| `QGMP4ParserProxy` | `-parse` | `LNMP4ParserProxy` | `parse()` | Done |
+| `QGMP4ParserProxy` | `-readPacketOfSample:` | `LNMP4ParserProxy` | `@objc(readPacketOfSample:) readPacket(ofSample:)` | Done |
+| `QGMP4ParserProxy` | `-readDataOfBox:length:offset:` | `LNMP4ParserProxy` | `@objc(readDataOfBox:length:offset:) readData(of:length:offset:)` | Done |
+
+## Scope note
+
+- 本表是 **Model + Parser API 层对照**。实现文件位于:
+  - `/Users/yanxuyao/Vap/QGVAPlayer/QGVAPlayer/LNSwift/Model/LNModels.swift`
+  - `/Users/yanxuyao/Vap/QGVAPlayer/QGVAPlayer/LNSwift/Parser/LNMP4Parser.swift`
+- 已验证构建:`QGVAPlayer` / `QGVAPlayerDemo` / `QGVAPlayerDemoSwift` 全部通过。

+ 114 - 0
QGVAPlayer/QGVAPlayer.xcodeproj/project.pbxproj

@@ -7,6 +7,8 @@
 	objects = {
 
 /* Begin PBXBuildFile section */
+		0DCDE3A29A303DDAC7B34A68 /* LNModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = 85972E2BAAEE4BC57BC70D7D /* LNModels.swift */; };
+		424CEEC3D60F73A1EE8EBE0A /* LNVAPPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = BCFCF18C219ED666D3D894D2 /* LNVAPPlayerView.swift */; };
 		43BAD32225F2153500D17934 /* QGVAPWrapView.h in Headers */ = {isa = PBXBuildFile; fileRef = 43BAD32025F2153500D17934 /* QGVAPWrapView.h */; settings = {ATTRIBUTES = (Public, ); }; };
 		43BAD32325F2153500D17934 /* QGVAPWrapView.m in Sources */ = {isa = PBXBuildFile; fileRef = 43BAD32125F2153500D17934 /* QGVAPWrapView.m */; };
 		630723B122F0409200B15629 /* QGVAPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 630723A722F0409200B15629 /* QGVAPlayer.framework */; };
@@ -89,6 +91,14 @@
 		63BAD37922F0966300EAD4C4 /* UIDevice+VAPUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = 63BAD37722F0966300EAD4C4 /* UIDevice+VAPUtil.m */; };
 		649EF40D24122B4D00164950 /* QGVAPMaskInfo.h in Headers */ = {isa = PBXBuildFile; fileRef = 649EF40B24122B4C00164950 /* QGVAPMaskInfo.h */; };
 		649EF40E24122B4D00164950 /* QGVAPMaskInfo.m in Sources */ = {isa = PBXBuildFile; fileRef = 649EF40C24122B4C00164950 /* QGVAPMaskInfo.m */; };
+		69DCB6EF3D76FAEFFD1929CC /* LNLegacyMappings.swift in Sources */ = {isa = PBXBuildFile; fileRef = ADF46D8C8C93AA38D15B3345 /* LNLegacyMappings.swift */; };
+		6A449A4C2102FA07B1E481A1 /* LNMP4Parser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DFC2669FA8BE22005CBC78B /* LNMP4Parser.swift */; };
+		7644770111F618C09AAFB77A /* LNControllers.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC2DF61A816CBDC209E81671 /* LNControllers.swift */; };
+		7769C2ADCDBE6BA6706FA10F /* LNVAPFacade.swift in Sources */ = {isa = PBXBuildFile; fileRef = E18C9AF6293B68DAD4F8640E /* LNVAPFacade.swift */; };
+		939368FB3CEDE4E9942BC232 /* LNRenderers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0423F5B5825E119BA494D53B /* LNRenderers.swift */; };
+		D0B10869F3FC013BB0FB0BE2 /* LNVAPWrapView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1826895ABCB13AB1313356B1 /* LNVAPWrapView.swift */; };
+		D572F414BCB8C54DE8E43E75 /* LNUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 290BBFEF3F6577C4848CB922 /* LNUtilities.swift */; };
+		E1C9B98D15BDFC35B8FED6E9 /* LNPlaybackTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 81AC387A5CA8EA729FD6999D /* LNPlaybackTypes.swift */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -102,6 +112,10 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
+		0423F5B5825E119BA494D53B /* LNRenderers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNRenderers.swift; sourceTree = "<group>"; };
+		0DFC2669FA8BE22005CBC78B /* LNMP4Parser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNMP4Parser.swift; sourceTree = "<group>"; };
+		1826895ABCB13AB1313356B1 /* LNVAPWrapView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNVAPWrapView.swift; sourceTree = "<group>"; };
+		290BBFEF3F6577C4848CB922 /* LNUtilities.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNUtilities.swift; sourceTree = "<group>"; };
 		43BAD32025F2153500D17934 /* QGVAPWrapView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = QGVAPWrapView.h; sourceTree = "<group>"; };
 		43BAD32125F2153500D17934 /* QGVAPWrapView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = QGVAPWrapView.m; sourceTree = "<group>"; };
 		630723A722F0409200B15629 /* QGVAPlayer.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = QGVAPlayer.framework; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -188,6 +202,12 @@
 		63BAD37722F0966300EAD4C4 /* UIDevice+VAPUtil.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "UIDevice+VAPUtil.m"; sourceTree = "<group>"; };
 		649EF40B24122B4C00164950 /* QGVAPMaskInfo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = QGVAPMaskInfo.h; sourceTree = "<group>"; };
 		649EF40C24122B4C00164950 /* QGVAPMaskInfo.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = QGVAPMaskInfo.m; sourceTree = "<group>"; };
+		81AC387A5CA8EA729FD6999D /* LNPlaybackTypes.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNPlaybackTypes.swift; sourceTree = "<group>"; };
+		85972E2BAAEE4BC57BC70D7D /* LNModels.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNModels.swift; sourceTree = "<group>"; };
+		ADF46D8C8C93AA38D15B3345 /* LNLegacyMappings.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNLegacyMappings.swift; sourceTree = "<group>"; };
+		BCFCF18C219ED666D3D894D2 /* LNVAPPlayerView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNVAPPlayerView.swift; sourceTree = "<group>"; };
+		CC2DF61A816CBDC209E81671 /* LNControllers.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNControllers.swift; sourceTree = "<group>"; };
+		E18C9AF6293B68DAD4F8640E /* LNVAPFacade.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = LNVAPFacade.swift; sourceTree = "<group>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
@@ -209,6 +229,25 @@
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		0A1D725D941D13055DAA77FB /* View */ = {
+			isa = PBXGroup;
+			children = (
+				BCFCF18C219ED666D3D894D2 /* LNVAPPlayerView.swift */,
+				1826895ABCB13AB1313356B1 /* LNVAPWrapView.swift */,
+			);
+			name = View;
+			path = View;
+			sourceTree = "<group>";
+		};
+		0B83F9D6B6620704B0F0623E /* Render */ = {
+			isa = PBXGroup;
+			children = (
+				0423F5B5825E119BA494D53B /* LNRenderers.swift */,
+			);
+			name = Render;
+			path = Render;
+			sourceTree = "<group>";
+		};
 		6307239D22F0409200B15629 = {
 			isa = PBXGroup;
 			children = (
@@ -233,6 +272,7 @@
 				6307240422F0410600B15629 /* Shaders */,
 				630723C122F0410600B15629 /* Classes */,
 				630723AB22F0409200B15629 /* Info.plist */,
+				D19AA999E4A643A3A4BD84C8 /* LNSwift */,
 			);
 			path = QGVAPlayer;
 			sourceTree = "<group>";
@@ -429,6 +469,68 @@
 			path = Vapx;
 			sourceTree = "<group>";
 		};
+		9584B67AA626C136945E2165 /* Utils */ = {
+			isa = PBXGroup;
+			children = (
+				290BBFEF3F6577C4848CB922 /* LNUtilities.swift */,
+			);
+			name = Utils;
+			path = Utils;
+			sourceTree = "<group>";
+		};
+		9B1DBC00E63CB9306A9152B1 /* Parser */ = {
+			isa = PBXGroup;
+			children = (
+				0DFC2669FA8BE22005CBC78B /* LNMP4Parser.swift */,
+			);
+			name = Parser;
+			path = Parser;
+			sourceTree = "<group>";
+		};
+		D19AA999E4A643A3A4BD84C8 /* LNSwift */ = {
+			isa = PBXGroup;
+			children = (
+				DB2708DBFFB7748A7FACBB35 /* Core */,
+				0B83F9D6B6620704B0F0623E /* Render */,
+				9B1DBC00E63CB9306A9152B1 /* Parser */,
+				E82B388FB5556671C823FE65 /* Model */,
+				0A1D725D941D13055DAA77FB /* View */,
+				9584B67AA626C136945E2165 /* Utils */,
+				D8332413532597C3E92EED74 /* Bridges */,
+			);
+			name = LNSwift;
+			path = LNSwift;
+			sourceTree = "<group>";
+		};
+		D8332413532597C3E92EED74 /* Bridges */ = {
+			isa = PBXGroup;
+			children = (
+				ADF46D8C8C93AA38D15B3345 /* LNLegacyMappings.swift */,
+			);
+			name = Bridges;
+			path = Bridges;
+			sourceTree = "<group>";
+		};
+		DB2708DBFFB7748A7FACBB35 /* Core */ = {
+			isa = PBXGroup;
+			children = (
+				81AC387A5CA8EA729FD6999D /* LNPlaybackTypes.swift */,
+				E18C9AF6293B68DAD4F8640E /* LNVAPFacade.swift */,
+				CC2DF61A816CBDC209E81671 /* LNControllers.swift */,
+			);
+			name = Core;
+			path = Core;
+			sourceTree = "<group>";
+		};
+		E82B388FB5556671C823FE65 /* Model */ = {
+			isa = PBXGroup;
+			children = (
+				85972E2BAAEE4BC57BC70D7D /* LNModels.swift */,
+			);
+			name = Model;
+			path = Model;
+			sourceTree = "<group>";
+		};
 /* End PBXGroup section */
 
 /* Begin PBXHeadersBuildPhase section */
@@ -615,6 +717,16 @@
 				6307242322F0410600B15629 /* QGBaseAnimatedImageFrame+Displaying.m in Sources */,
 				632942DA231BED7D00B511BB /* QGVAPMetalUtil.m in Sources */,
 				6307241122F0410600B15629 /* QGVAPSafeMutableArray.m in Sources */,
+				E1C9B98D15BDFC35B8FED6E9 /* LNPlaybackTypes.swift in Sources */,
+				7769C2ADCDBE6BA6706FA10F /* LNVAPFacade.swift in Sources */,
+				7644770111F618C09AAFB77A /* LNControllers.swift in Sources */,
+				939368FB3CEDE4E9942BC232 /* LNRenderers.swift in Sources */,
+				6A449A4C2102FA07B1E481A1 /* LNMP4Parser.swift in Sources */,
+				0DCDE3A29A303DDAC7B34A68 /* LNModels.swift in Sources */,
+				424CEEC3D60F73A1EE8EBE0A /* LNVAPPlayerView.swift in Sources */,
+				D0B10869F3FC013BB0FB0BE2 /* LNVAPWrapView.swift in Sources */,
+				D572F414BCB8C54DE8E43E75 /* LNUtilities.swift in Sources */,
+				69DCB6EF3D76FAEFFD1929CC /* LNLegacyMappings.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};
@@ -775,6 +887,7 @@
 				PRODUCT_BUNDLE_IDENTIFIER = com.gami.vap;
 				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
 				SKIP_INSTALL = YES;
+				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Debug;
@@ -799,6 +912,7 @@
 				PRODUCT_BUNDLE_IDENTIFIER = com.gami.vap;
 				PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
 				SKIP_INSTALL = YES;
+				SWIFT_VERSION = 5.0;
 				TARGETED_DEVICE_FAMILY = "1,2";
 			};
 			name = Release;

+ 6 - 0
QGVAPlayer/QGVAPlayer/Classes/QGVAPWrapView.h

@@ -13,6 +13,10 @@
 // either express or implied. See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Legacy Objective-C compatibility layer. New development should use LN Swift APIs under LNSwift/.
+#ifndef QG_VAP_WRAP_VIEW_H
+#define QG_VAP_WRAP_VIEW_H
+
 #import <UIKit/UIKit.h>
 #import "UIView+VAP.h"
 
@@ -80,3 +84,5 @@ typedef NS_ENUM(NSUInteger, QGVAPWrapViewContentMode) {
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif /* QG_VAP_WRAP_VIEW_H */

+ 1 - 2
QGVAPlayer/QGVAPlayer/Classes/QGVAPlayer.h

@@ -15,6 +15,7 @@
 
 #import <UIKit/UIKit.h>
 #import "UIView+VAP.h"
+#import "QGVAPWrapView.h"
 
 //! Project version number for QGVAPlayer.
 FOUNDATION_EXPORT double QGVAPlayerVersionNumber;
@@ -23,5 +24,3 @@ FOUNDATION_EXPORT double QGVAPlayerVersionNumber;
 FOUNDATION_EXPORT const unsigned char QGVAPlayerVersionString[];
 
 // In this header, you should import all the public headers of your framework using statements like #import <QGVAPlayer/PublicHeader.h>
-
-

+ 7 - 1
QGVAPlayer/QGVAPlayer/Classes/UIView+VAP.h

@@ -13,6 +13,10 @@
 // either express or implied. See the License for the specific language governing permissions and
 // limitations under the License.
 
+// Legacy Objective-C compatibility layer. New development should use LN Swift APIs under LNSwift/.
+#ifndef QG_VIEW_VAP_H
+#define QG_VIEW_VAP_H
+
 #import <UIKit/UIKit.h>
 #import "VAPMacros.h"
 #import "QGVAPLogger.h"
@@ -24,7 +28,7 @@ typedef NS_ENUM(NSUInteger, HWDMP4EBOperationType) {
     HWDMP4EBOperationTypeDoNothing,         // VAP自身不进行控制,当外部进行控制时可以使用这个,仅用于防止覆盖外界的pause调用的问题
 };
 
-@class QGMP4AnimatedImageFrame,QGVAPConfigModel, QGVAPSourceInfo;
+@class QGMP4AnimatedImageFrame,QGVAPConfigModel, QGVAPSourceInfo, QGVAPMaskInfo;
 /** 注意:回调方法会在子线程被执行。*/
 @protocol HWDMP4PlayDelegate <NSObject>
 
@@ -97,3 +101,5 @@ typedef NS_ENUM(NSUInteger, HWDMP4EBOperationType) {
 - (void)playHWDMP4:(NSString *)filePath fps:(NSInteger)fps blendMode:(QGHWDTextureBlendMode)mode repeatCount:(NSInteger)repeatCount delegate:(id<HWDMP4PlayDelegate>)delegate __attribute__((deprecated("customized fps is not recommended, use playHWDMP4:repeatCount:delegate: instead")));
 
 @end
+
+#endif /* QG_VIEW_VAP_H */

+ 4 - 0
QGVAPlayer/QGVAPlayer/Classes/Utils/Logger/QGVAPLogger.h

@@ -14,6 +14,8 @@
 // limitations under the License.
 
 #import <Foundation/Foundation.h>
+#ifndef QG_VAP_LOGGER_H
+#define QG_VAP_LOGGER_H
 
 #define kQGVAPModuleCommon @"kQGVAPModuleCommon"
 
@@ -63,3 +65,5 @@ extern "C" {
 @end
 
 NS_ASSUME_NONNULL_END
+
+#endif /* QG_VAP_LOGGER_H */

+ 0 - 1
QGVAPlayer/QGVAPlayer/Classes/VAPMacros.h

@@ -50,7 +50,6 @@ return cValue; \
 
 #import "QGHWDShaderTypes.h"
 #import <UIKit/UIKit.h>
-#import "QGVAPMaskInfo.h"
 
 extern NSInteger const kQGHWDMP4DefaultFPS;      //默认fps 25
 extern NSInteger const kQGHWDMP4MinFPS;          //最小fps 1

+ 1 - 0
QGVAPlayer/QGVAPlayer/Classes/Views/Metal/Vapx/QGVAPMetalRenderer.h

@@ -15,6 +15,7 @@
 
 #import <Foundation/Foundation.h>
 #import "QGVAPConfigModel.h"
+#import "QGVAPMaskInfo.h"
 #import <Metal/Metal.h>
 #import "VAPMacros.h"
 

+ 25 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Bridges/LNLegacyMappings.swift

@@ -0,0 +1,25 @@
+import Foundation
+
+public enum LNLegacyMappings {
+    public static let entries: [String: String] = [
+        "UIView+VAP": "LNVAPPlayerView",
+        "QGVAPWrapView": "LNVAPWrapView",
+        "QGMP4Parser": "LNMP4Parser",
+        "QGMP4Box": "LNMP4Box",
+        "QGAnimatedImageDecodeManager": "LNAnimatedImageDecodeManager",
+        "QGAnimatedImageBufferManager": "LNAnimatedImageBufferManager",
+        "QGAnimatedImageDecodeThread": "LNAnimatedImageDecodeThread",
+        "QGAnimatedImageDecodeThreadPool": "LNAnimatedImageDecodeThreadPool",
+        "QGBaseDecoder": "LNBaseDecoder",
+        "QGMP4FrameHWDecoder": "LNMP4FrameHWDecoder",
+        "QGVAPConfigModel": "LNVAPConfigModel",
+        "QGVAPSourceInfo": "LNVAPSourceInfo",
+        "QGVAPSourceDisplayItem": "LNVAPSourceDisplayItem",
+        "QGVAPMaskInfo": "LNVAPMaskInfo",
+        "QGHWDMetalRenderer": "LNHWDMetalRenderer",
+        "QGHWDMetalView": "LNHWDMetalView",
+        "QGVAPMetalRenderer": "LNVAPMetalRenderer",
+        "QGVAPMetalView": "LNVAPMetalView",
+        "QGHWDMP4OpenGLView": "LNHWDMP4OpenGLView"
+    ]
+}

+ 807 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Core/LNControllers.swift

@@ -0,0 +1,807 @@
+import Foundation
+import UIKit
+import AVFoundation
+import VideoToolbox
+import CoreMedia
+import CoreVideo
+
+@objcMembers
+public final class LNAnimatedImageDecodeConfig: NSObject {
+    public var threadCount: Int = 1
+    public var bufferCount: Int = 5
+
+    @objc(defaultConfig)
+    public static func defaultConfig() -> LNAnimatedImageDecodeConfig {
+        let config = LNAnimatedImageDecodeConfig()
+        config.threadCount = 1
+        config.bufferCount = 5
+        return config
+    }
+}
+
+@objcMembers
+public final class LNAnimatedImageDecodeThread: Thread {
+    public var occupied: Bool = false
+
+    public var sequenceDec: String {
+        #if DEBUG
+        return description
+        #else
+        return description
+        #endif
+    }
+}
+
+@objcMembers
+public final class LNAnimatedImageDecodeThreadPool: NSObject {
+    public static let shared = LNAnimatedImageDecodeThreadPool()
+
+    private let lock = NSLock()
+    private var threads: [LNAnimatedImageDecodeThread] = []
+
+    @objc(sharedPool)
+    public static func sharedPool() -> LNAnimatedImageDecodeThreadPool {
+        shared
+    }
+
+    @objc(getDecodeThread)
+    public func getDecodeThread() -> LNAnimatedImageDecodeThread {
+        lock.lock()
+        defer { lock.unlock() }
+
+        if let freeThread = threads.first(where: { !$0.occupied }) {
+            return freeThread
+        }
+
+        let thread = LNAnimatedImageDecodeThread(target: self, selector: #selector(run), object: nil)
+        thread.start()
+        threads.append(thread)
+        return thread
+    }
+
+    @objc private func run() {
+        autoreleasepool {
+            let runLoop = RunLoop.current
+            runLoop.add(Port(), forMode: .default)
+            while !Thread.current.isCancelled {
+                runLoop.run(mode: .default, before: .distantFuture)
+            }
+        }
+    }
+}
+
+@objcMembers
+public final class LNAnimatedImageBufferManager: NSObject {
+    public var buffers: NSMutableArray
+    private let config: LNAnimatedImageDecodeConfig
+
+    public init(config: LNAnimatedImageDecodeConfig) {
+        self.config = config
+        self.buffers = NSMutableArray(capacity: max(config.bufferCount, 0))
+        super.init()
+    }
+
+    @objc(getBufferedFrame:)
+    public func getBufferedFrame(_ frameIndex: Int) -> LNBaseAnimatedImageFrame? {
+        if buffers.count == 0 { return nil }
+        let bufferIndex = frameIndex % buffers.count
+        if bufferIndex > buffers.count - 1 { return nil }
+        guard let frame = buffers.object(at: bufferIndex) as? LNBaseAnimatedImageFrame else { return nil }
+        guard frame.frameIndex == frameIndex else { return nil }
+        return frame
+    }
+
+    @objc(popVideoFrame)
+    public func popVideoFrame() -> LNBaseAnimatedImageFrame? {
+        guard buffers.count > 0 else { return nil }
+        guard let frame = buffers.firstObject as? LNBaseAnimatedImageFrame else { return nil }
+        buffers.removeObject(at: 0)
+        return frame
+    }
+
+    @objc(isBufferFull)
+    public func isBufferFull() -> Bool {
+        if buffers.count < config.bufferCount { return false }
+        for case let obj in buffers {
+            if !(obj is LNBaseAnimatedImageFrame) {
+                return false
+            }
+        }
+        return true
+    }
+}
+
+@objc public protocol LNAnimatedImageDecoderDelegate: AnyObject {
+    @objc(decoderClassForManager:)
+    func decoderClass(for manager: LNAnimatedImageDecodeManager) -> AnyClass
+
+    @objc optional func shouldSetupAudioPlayer() -> Bool
+    @objc optional func decoderDidFinishDecode(_ decoder: LNBaseDecoder)
+    @objc optional func decoderDidFailDecode(_ decoder: LNBaseDecoder?, error: NSError)
+}
+
+public let kLNVAPDecoderSeekStart = "kLNVAPDecoderSeekStart"
+public let kLNVAPDecoderSeekFinish = "kLNVAPDecoderSeekFinish"
+
+@objcMembers
+open class LNBaseDecoder: NSObject {
+    public dynamic var currentDecodeFrame: Int = -1
+    public private(set) var fileInfo: LNBaseDFileInfo
+
+    public required init(fileInfo: LNBaseDFileInfo) {
+        self.fileInfo = fileInfo
+        super.init()
+        self.fileInfo.occupiedCount += 1
+    }
+
+    open func decodeFrame(_ frameIndex: Int, buffers: NSMutableArray) {}
+    open func shouldStopDecode(_ nextFrameIndex: Int) -> Bool { false }
+    open func isFrameIndexBeyondEnd(_ frameIndex: Int) -> Bool { false }
+}
+
+@objc public enum LNMP4HWDErrorCode: Int {
+    case fileNotExist = 10000
+    case invalidMP4File = 10001
+    case canNotGetStreamInfo = 10002
+    case canNotGetStream = 10003
+    case errorCreateVTBDesc = 10004
+    case errorCreateVTBSession = 10005
+}
+
+@objcMembers
+public final class LNMP4FrameHWDecoder: LNBaseDecoder {
+    public static let errorDomain = "LNMP4HWDErrorDomain"
+
+    private var buffers: NSMutableArray?
+    private var parser: LNMP4ParserProxy?
+    private var decodeQueue = DispatchQueue(label: "com.ln.vap.decode")
+
+    private var decodeSession: VTDecompressionSession?
+    private var formatDescription: CMFormatDescription?
+
+    private var isFinish = false
+    private var finishFrameIndex: Int = -1
+    private var invalidRetryCount = 0
+
+    private var spsData: Data?
+    private var ppsData: Data?
+    private var vpsData: Data?
+
+    private var lastDecodeFrame: Int = -1
+
+    private var constructError: NSError?
+
+    @objc(errorDescriptionForCode:)
+    public static func errorDescription(for code: LNMP4HWDErrorCode) -> String {
+        switch code {
+        case .fileNotExist: return "文件不存在"
+        case .invalidMP4File: return "非法文件格式"
+        case .canNotGetStreamInfo: return "无法获取视频流信息"
+        case .canNotGetStream: return "无法获取视频流"
+        case .errorCreateVTBDesc: return "VTB创建desc失败"
+        case .errorCreateVTBSession: return "VTB创建session失败"
+        }
+    }
+
+    public required init(fileInfo: LNBaseDFileInfo) {
+        super.init(fileInfo: fileInfo)
+        parser = (fileInfo as? LNMP4HWDFileInfo)?.mp4Parser
+        _ = onInputStart()
+    }
+
+    public override func decodeFrame(_ frameIndex: Int, buffers: NSMutableArray) {
+        if frameIndex == currentDecodeFrame { return }
+        currentDecodeFrame = frameIndex
+        self.buffers = buffers
+
+        decodeQueue.async { [weak self] in
+            guard let self else { return }
+            if frameIndex != self.lastDecodeFrame + 1 { return }
+            self.decodeFrameInner(frameIndex, drop: false)
+        }
+    }
+
+    public override func shouldStopDecode(_ nextFrameIndex: Int) -> Bool {
+        isFinish
+    }
+
+    public override func isFrameIndexBeyondEnd(_ frameIndex: Int) -> Bool {
+        if finishFrameIndex > 0 {
+            return frameIndex >= finishFrameIndex
+        }
+        return false
+    }
+
+    deinit {
+        onInputEnd()
+        fileInfo.occupiedCount -= 1
+    }
+
+    private func onInputStart() -> Bool {
+        guard !fileInfo.filePath.isEmpty else {
+            constructError = 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])
+            return false
+        }
+
+        isFinish = false
+        spsData = nil
+        ppsData = nil
+        vpsData = nil
+
+        return initPPSAndSPS()
+    }
+
+    private func initPPSAndSPS() -> Bool {
+        guard let parser else { return false }
+
+        spsData = parser.spsData
+        ppsData = parser.ppsData
+        vpsData = parser.vpsData
+
+        guard let spsData, let ppsData else {
+            constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.canNotGetStreamInfo.rawValue, userInfo: ["location": fileInfo.filePath])
+            return false
+        }
+
+        if parser.videoCodecID == .h264 {
+            guard spsData.count > 0, ppsData.count > 0 else { return false }
+            let spsPtr = spsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
+            let ppsPtr = ppsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
+            var parameterSetPointers: [UnsafePointer<UInt8>] = [spsPtr, ppsPtr]
+            var parameterSetSizes: [Int] = [spsData.count, ppsData.count]
+
+            let status = CMVideoFormatDescriptionCreateFromH264ParameterSets(
+                allocator: kCFAllocatorDefault,
+                parameterSetCount: 2,
+                parameterSetPointers: &parameterSetPointers,
+                parameterSetSizes: &parameterSetSizes,
+                nalUnitHeaderLength: 4,
+                formatDescriptionOut: &formatDescription
+            )
+            if status != noErr {
+                constructError = 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])
+                return false
+            }
+            guard vpsData.count > 0, spsData.count > 0, ppsData.count > 0 else { return false }
+            let vpsPtr = vpsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
+            let spsPtr = spsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
+            let ppsPtr = ppsData.withUnsafeBytes { $0.baseAddress!.assumingMemoryBound(to: UInt8.self) }
+            var parameterSetPointers: [UnsafePointer<UInt8>] = [vpsPtr, spsPtr, ppsPtr]
+            var parameterSetSizes: [Int] = [vpsData.count, spsData.count, ppsData.count]
+
+            let status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(
+                allocator: kCFAllocatorDefault,
+                parameterSetCount: 3,
+                parameterSetPointers: &parameterSetPointers,
+                parameterSetSizes: &parameterSetSizes,
+                nalUnitHeaderLength: 4,
+                extensions: nil,
+                formatDescriptionOut: &formatDescription
+            )
+            if status != noErr {
+                constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBDesc.rawValue, userInfo: ["location": fileInfo.filePath])
+                return false
+            }
+        }
+
+        return createDecompressionSession()
+    }
+
+    private func createDecompressionSession() -> Bool {
+        guard let formatDescription else { return false }
+
+        var pixelFormat: UInt32 = kCVPixelFormatType_420YpCbCr8BiPlanarFullRange
+        let attrs = [
+            kCVPixelBufferPixelFormatTypeKey as String: NSNumber(value: pixelFormat)
+        ] as NSDictionary
+
+        let status = VTDecompressionSessionCreate(
+            allocator: kCFAllocatorDefault,
+            formatDescription: formatDescription,
+            decoderSpecification: nil,
+            imageBufferAttributes: attrs,
+            outputCallback: nil,
+            decompressionSessionOut: &decodeSession
+        )
+
+        if status != noErr {
+            constructError = NSError(domain: Self.errorDomain, code: LNMP4HWDErrorCode.errorCreateVTBSession.rawValue, userInfo: ["location": fileInfo.filePath])
+            return false
+        }
+        return true
+    }
+
+    private func decodeFrameInner(_ frameIndex: Int, drop: Bool) {
+        guard !isFinish else { return }
+        guard let buffers else { return }
+        guard let parser else { return }
+        guard let _ = spsData, let _ = ppsData else { return }
+
+        guard let packetData = parser.readPacket(ofSample: frameIndex), !packetData.isEmpty else {
+            finishFrameIndex = frameIndex
+            onInputEnd()
+            return
+        }
+
+        let currentPts: UInt64 = {
+            guard frameIndex >= 0, frameIndex < parser.videoSamples.count else { return 0 }
+            return parser.videoSamples[frameIndex].pts
+        }()
+
+        let startDate = Date()
+
+        var blockBuffer: CMBlockBuffer?
+        var status = CMBlockBufferCreateWithMemoryBlock(
+            allocator: kCFAllocatorDefault,
+            memoryBlock: nil,
+            blockLength: packetData.count,
+            blockAllocator: kCFAllocatorDefault,
+            customBlockSource: nil,
+            offsetToData: 0,
+            dataLength: packetData.count,
+            flags: 0,
+            blockBufferOut: &blockBuffer
+        )
+        guard status == kCMBlockBufferNoErr, let blockBuffer else { return }
+
+        packetData.withUnsafeBytes { rawPtr in
+            if let base = rawPtr.baseAddress {
+                CMBlockBufferReplaceDataBytes(with: base, blockBuffer: blockBuffer, offsetIntoDestination: 0, dataLength: packetData.count)
+            }
+        }
+
+        var sampleBuffer: CMSampleBuffer?
+        var sampleSizeArray = [packetData.count]
+        status = CMSampleBufferCreateReady(
+            allocator: kCFAllocatorDefault,
+            dataBuffer: blockBuffer,
+            formatDescription: formatDescription,
+            sampleCount: 1,
+            sampleTimingEntryCount: 0,
+            sampleTimingArray: nil,
+            sampleSizeEntryCount: 1,
+            sampleSizeArray: &sampleSizeArray,
+            sampleBufferOut: &sampleBuffer
+        )
+        guard status == noErr, let sampleBuffer, let decodeSession else { return }
+
+        var flagOut: VTDecodeInfoFlags = []
+        let decodeStatus = VTDecompressionSessionDecodeFrame(
+            decodeSession,
+            sampleBuffer: sampleBuffer,
+            flags: [],
+            infoFlagsOut: &flagOut
+        ) { [weak self] status, _, imageBuffer, _, _ in
+            guard let self else { return }
+            self.handleDecodePixelBuffer(
+                imageBuffer,
+                frameIndex: frameIndex,
+                currentPts: currentPts,
+                startDate: startDate,
+                status: status,
+                needDrop: drop,
+                buffers: buffers,
+                fps: parser.fps
+            )
+        }
+
+        if decodeStatus == kVTInvalidSessionErr {
+            invalidRetryCount += 1
+            if invalidRetryCount >= 3 { return }
+            resetDecoder()
+            findKeyFrameAndDecodeToCurrent(frameIndex)
+        } else {
+            invalidRetryCount = 0
+        }
+    }
+
+    private func handleDecodePixelBuffer(
+        _ pixelBuffer: CVImageBuffer?,
+        frameIndex: Int,
+        currentPts: UInt64,
+        startDate: Date,
+        status: OSStatus,
+        needDrop: Bool,
+        buffers: NSMutableArray,
+        fps: Int
+    ) {
+        lastDecodeFrame = frameIndex
+        if status != noErr { return }
+        if needDrop { return }
+        guard let pixelBuffer = pixelBuffer else { return }
+
+        let newFrame = LNMP4AnimatedImageFrame()
+        newFrame.pixelBuffer = pixelBuffer as? CVPixelBuffer
+        newFrame.frameIndex = frameIndex
+        newFrame.decodeTime = Date().timeIntervalSince(startDate) * 1000.0
+        newFrame.defaultFps = Int32(fps)
+        newFrame.pts = currentPts
+
+        buffers.add(newFrame)
+        let sorted = (buffers as? [LNMP4AnimatedImageFrame] ?? []).sorted { $0.pts < $1.pts }
+        buffers.removeAllObjects()
+        sorted.forEach { buffers.add($0) }
+    }
+
+    private func resetDecoder() {
+        if let decodeSession {
+            VTDecompressionSessionWaitForAsynchronousFrames(decodeSession)
+            VTDecompressionSessionInvalidate(decodeSession)
+            self.decodeSession = nil
+        }
+        _ = createDecompressionSession()
+    }
+
+    private func findKeyFrameAndDecodeToCurrent(_ frameIndex: Int) {
+        NotificationCenter.default.post(name: NSNotification.Name(kLNVAPDecoderSeekStart), object: self)
+
+        guard let parser else { return }
+        let keyframeIndexes = parser.videoSyncSampleIndexes
+        var index = keyframeIndexes.first?.intValue ?? 0
+        for num in keyframeIndexes {
+            if num.intValue < frameIndex {
+                index = num.intValue
+                continue
+            }
+            break
+        }
+
+        while index < frameIndex {
+            decodeFrameInner(index, drop: true)
+            index += 1
+        }
+        decodeFrameInner(frameIndex, drop: false)
+
+        NotificationCenter.default.post(name: NSNotification.Name(kLNVAPDecoderSeekFinish), object: self)
+    }
+
+    private func onInputEnd() {
+        if isFinish { return }
+        isFinish = true
+
+        if let decodeSession {
+            VTDecompressionSessionWaitForAsynchronousFrames(decodeSession)
+            VTDecompressionSessionInvalidate(decodeSession)
+            self.decodeSession = nil
+        }
+        formatDescription = nil
+        spsData = nil
+        ppsData = nil
+        vpsData = nil
+    }
+}
+
+@objcMembers
+public final class LNAnimatedImageDecodeManager: NSObject {
+    public weak var decoderDelegate: LNAnimatedImageDecoderDelegate?
+
+    private let config: LNAnimatedImageDecodeConfig
+    private let fileInfo: LNBaseDFileInfo
+    private var decoders: [LNBaseDecoder] = []
+    private var bufferManager: LNAnimatedImageBufferManager
+    private var audioPlayer: AVAudioPlayer?
+
+    public init(fileInfo: LNBaseDFileInfo, config: LNAnimatedImageDecodeConfig, delegate: LNAnimatedImageDecoderDelegate?) {
+        self.fileInfo = fileInfo
+        self.config = config
+        self.decoderDelegate = delegate
+        self.bufferManager = LNAnimatedImageBufferManager(config: config)
+        super.init()
+
+        createDecoders(by: config)
+        initializeBuffers(fromIndex: 0)
+        setupAudioPlayerIfNeed()
+    }
+
+    @objc(consumeDecodedFrame:)
+    public func consumeDecodedFrame(_ frameIndex: Int) -> LNBaseAnimatedImageFrame? {
+        objc_sync_enter(self)
+        defer { objc_sync_exit(self) }
+
+        if frameIndex == 0 && bufferManager.buffers.count < config.bufferCount {
+            return nil
+        }
+
+        let decodeFinished = checkIfDecodeFinish(frameIndex)
+        let frame = bufferManager.popVideoFrame()
+        if let frame {
+            frame.frameIndex = frameIndex
+            decodeFrame(frameIndex + config.bufferCount)
+            return frame
+        }
+
+        if !decodeFinished {
+            guard !decoders.isEmpty else { return nil }
+            let decoderIndex = decoders.count == 1 ? 0 : frameIndex % decoders.count
+            let decoder = decoders[decoderIndex]
+            if decoder.shouldStopDecode(frameIndex) {
+                decoderDelegate?.decoderDidFinishDecode?(decoder)
+                return nil
+            }
+            initializeBuffers(fromIndex: frameIndex)
+        }
+        return nil
+    }
+
+    @objc(tryToStartAudioPlay)
+    public func tryToStartAudioPlay() {
+        audioPlayer?.play()
+    }
+
+    @objc(tryToStopAudioPlay)
+    public func tryToStopAudioPlay() {
+        audioPlayer?.stop()
+    }
+
+    @objc(tryToPauseAudioPlay)
+    public func tryToPauseAudioPlay() {
+        audioPlayer?.pause()
+    }
+
+    @objc(tryToResumeAudioPlay)
+    public func tryToResumeAudioPlay() {
+        audioPlayer?.play()
+    }
+
+    @objc(containsThisDeocder:)
+    public func containsThisDeocder(_ decoder: Any) -> Bool {
+        decoders.contains { $0 === (decoder as AnyObject) }
+    }
+
+    private func checkIfDecodeFinish(_ frameIndex: Int) -> Bool {
+        guard !decoders.isEmpty else { return true }
+        let decoderIndex = decoders.count == 1 ? 0 : frameIndex % decoders.count
+        let decoder = decoders[decoderIndex]
+        if decoder.isFrameIndexBeyondEnd(frameIndex) {
+            decoderDelegate?.decoderDidFinishDecode?(decoder)
+            return true
+        }
+        return false
+    }
+
+    private func decodeFrame(_ frameIndex: Int) {
+        guard !decoders.isEmpty else { return }
+        let decoderIndex = decoders.count == 1 ? 0 : frameIndex % decoders.count
+        let decoder = decoders[decoderIndex]
+        if decoder.shouldStopDecode(frameIndex) { return }
+        decoder.decodeFrame(frameIndex, buffers: bufferManager.buffers)
+    }
+
+    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 }
+            let decoder = cls.init(fileInfo: fileInfo)
+            decoders.append(decoder)
+        }
+    }
+
+    private func initializeBuffers(fromIndex start: Int) {
+        for i in 0..<max(config.bufferCount, 0) {
+            decodeFrame(start + i)
+        }
+    }
+
+    private func setupAudioPlayerIfNeed() {
+        if decoderDelegate?.shouldSetupAudioPlayer?() == false {
+            return
+        }
+
+        guard let fileInfo = fileInfo as? LNMP4HWDFileInfo,
+              let mp4Parser = fileInfo.mp4Parser,
+              mp4Parser.audioTrackBox != nil else {
+            audioPlayer = nil
+            return
+        }
+
+        let url = URL(fileURLWithPath: self.fileInfo.filePath)
+        audioPlayer = try? AVAudioPlayer(contentsOf: url)
+    }
+}
+
+@objc public protocol LNVAPConfigDelegate: AnyObject {
+    @objc(onVAPConfigResourcesLoaded:error:)
+    func onVAPConfigResourcesLoaded(_ config: LNVAPConfigModel, error: NSError?)
+
+    @objc optional func vap_contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String?
+    @objc optional func vap_loadImageWithURL(_ urlStr: String, context: [AnyHashable: Any], completion: @escaping (UIImage?, NSError?, String) -> Void)
+}
+
+@objcMembers
+public final class LNVAPConfigManager: NSObject {
+    public weak var delegate: LNVAPConfigDelegate?
+    public var hasValidConfig: Bool = false
+    public var model: LNVAPConfigModel = .init()
+
+    private let fileInfo: LNMP4HWDFileInfo
+
+    public init(fileInfo: LNMP4HWDFileInfo) {
+        self.fileInfo = fileInfo
+        super.init()
+        setupConfig()
+    }
+
+    @objc(loadConfigResources)
+    public func loadConfigResources() {
+        if model.resources.isEmpty {
+            delegate?.onVAPConfigResourcesLoaded(model, error: nil)
+            return
+        }
+
+        if let contentProvider = delegate?.vap_contentForTag {
+            model.resources.forEach { resource in
+                resource.contentTagValue = contentProvider(resource.contentTag ?? "", resource)
+            }
+        }
+
+        guard let loadImage = delegate?.vap_loadImageWithURL else {
+            delegate?.onVAPConfigResourcesLoaded(model, error: nil)
+            return
+        }
+
+        let group = DispatchGroup()
+        var loadError: NSError?
+        for resource in model.resources {
+            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 {
+                    loadError = loadError ?? error ?? NSError(domain: "loadImageError:\(imageURL)", code: -1)
+                }
+                resource.sourceImage = image
+                group.leave()
+            }
+        }
+
+        group.notify(queue: .main) { [weak self] in
+            guard let self else { return }
+            self.delegate?.onVAPConfigResourcesLoaded(self.model, error: loadError)
+        }
+    }
+
+    @objc(loadMTLTextures:)
+    public func loadMTLTextures(_ device: MTLDevice) {
+        model.resources.forEach { resource in
+            resource.texture = LNVAPTextureLoader.loadTexture(with: resource.sourceImage, device: device)
+            resource.sourceImage = nil
+        }
+    }
+
+    @objc(loadMTLBuffers:)
+    public func loadMTLBuffers(_ device: MTLDevice) {
+        model.resources.forEach { resource in
+            resource.colorParamsBuffer = LNVAPTextureLoader.loadVapColorFillBuffer(with: resource.color, device: device)
+        }
+    }
+
+    private func setupConfig() {
+        guard let vapc = fileInfo.mp4Parser?.rootBox?.subBox(ofType: .vapc) else {
+            hasValidConfig = false
+            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 {
+            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]] ?? []
+
+        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
+        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 }
+            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.color = UIColor.lnColor(hex: colorHex)
+            }
+            sources[sourceID] = sourceInfo
+        }
+        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]] ?? []
+            var mergedInfos: [LNVAPMergedInfo] = []
+
+            for mergeInfoDic in mergedObjs {
+                guard let sourceID = mergeInfoDic["srcId"] as? String,
+                      let sourceInfo = sources[sourceID] else { continue }
+
+                let mergeInfo = LNVAPMergedInfo()
+                mergeInfo.source = sourceInfo
+                mergeInfo.renderIndex = mergeInfoDic["z"] as? Int ?? 0
+                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
+                mergedInfos.append(mergeInfo)
+            }
+            mergedConfig[NSNumber(value: frameIndex)] = mergedInfos.sorted(by: { $0.renderIndex < $1.renderIndex })
+        }
+
+        configModel.mergedConfig = mergedConfig
+    }
+}
+
+private extension UIColor {
+    static func lnColor(hex: String) -> UIColor? {
+        let cleaned = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
+        var int: UInt64 = 0
+        guard Scanner(string: cleaned).scanHexInt64(&int) else { return nil }
+
+        let a, r, g, b: UInt64
+        switch cleaned.count {
+        case 3:
+            (a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
+        case 6:
+            (a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
+        case 8:
+            (a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
+        default:
+            return nil
+        }
+
+        return UIColor(
+            red: CGFloat(r) / 255,
+            green: CGFloat(g) / 255,
+            blue: CGFloat(b) / 255,
+            alpha: CGFloat(a) / 255
+        )
+    }
+}

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

@@ -0,0 +1,55 @@
+import Foundation
+import UIKit
+
+public typealias LNVAPImageCompletion = (_ image: UIImage?, _ error: NSError?, _ imageURL: String) -> Void
+public typealias LNVAPGestureEventBlock = (_ gestureRecognizer: UIGestureRecognizer, _ insideSource: Bool, _ source: LNVAPSourceDisplayItem?) -> Void
+
+@objc public enum LNEnterBackgroundOperation: Int {
+    case stop
+    case pauseAndResume
+    case doNothing
+
+    var legacyRawValue: Int {
+        switch self {
+        case .stop: return 0
+        case .pauseAndResume: return 1
+        case .doNothing: return 2
+        }
+    }
+}
+
+@objc public enum LNVAPWrapContentMode: Int {
+    case scaleToFill
+    case aspectFit
+    case aspectFill
+
+    var legacyRawValue: Int {
+        switch self {
+        case .scaleToFill: return 0
+        case .aspectFit: return 1
+        case .aspectFill: return 2
+        }
+    }
+}
+
+@objc public protocol LNVAPPlaybackDelegate: AnyObject {
+    @objc optional func lnPlayerShouldStart(_ playerView: LNVAPPlayerView, config: LNVAPConfigModel) -> Bool
+    @objc optional func lnPlayerDidStart(_ playerView: LNVAPPlayerView)
+    @objc optional func lnPlayerDidPlay(_ playerView: LNVAPPlayerView, frame: LNMP4AnimatedImageFrame)
+    @objc optional func lnPlayerDidStop(_ playerView: LNVAPPlayerView)
+    @objc optional func lnPlayerDidFinish(_ playerView: LNVAPPlayerView, totalFrameCount: Int)
+    @objc optional func lnPlayerDidFail(_ playerView: LNVAPPlayerView, error: NSError)
+    @objc optional func lnPlayerContent(forTag tag: String, resource: LNVAPSourceInfo) -> String?
+    @objc optional func lnPlayerLoadImage(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)
+    @objc optional func lnWrapViewDidPlay(_ wrapView: LNVAPWrapView, frame: LNMP4AnimatedImageFrame)
+    @objc optional func lnWrapViewDidStop(_ wrapView: LNVAPWrapView)
+    @objc optional func lnWrapViewDidFinish(_ wrapView: LNVAPWrapView, totalFrameCount: Int)
+    @objc optional func lnWrapViewDidFail(_ wrapView: LNVAPWrapView, error: NSError)
+    @objc optional func lnWrapViewContent(forTag tag: String, resource: LNVAPSourceInfo) -> String?
+    @objc optional func lnWrapViewLoadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion)
+}

+ 17 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Core/LNVAPFacade.swift

@@ -0,0 +1,17 @@
+import Foundation
+import UIKit
+
+@objcMembers
+public final class LNVAPFacade: NSObject {
+    @objc public static func lnMakePlayerView(frame: CGRect) -> LNVAPPlayerView {
+        LNVAPPlayerView(frame: frame)
+    }
+
+    @objc public static func lnMakeWrapView(frame: CGRect) -> LNVAPWrapView {
+        LNVAPWrapView(frame: frame)
+    }
+
+    @objc public static func lnUseDefaultLogBridge() {
+        // Swift façade keeps API surface centralized; legacy logger registration remains available in OC layer.
+    }
+}

+ 353 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Model/LNModels.swift

@@ -0,0 +1,353 @@
+import Foundation
+import UIKit
+import CoreVideo
+import Metal
+import MetalKit
+
+private func lnRectForCenterFull(sourceSize: CGSize, renderSize: CGSize) -> CGRect {
+    guard sourceSize.width > 0, sourceSize.height > 0, renderSize.width > 0, renderSize.height > 0 else { return .zero }
+    let sourceRatio = sourceSize.width / sourceSize.height
+    let renderRatio = renderSize.width / renderSize.height
+    if sourceRatio > renderRatio {
+        let height = sourceSize.height
+        let width = height * renderRatio
+        return CGRect(x: (sourceSize.width - width) / 2.0, y: 0, width: width, height: height)
+    }
+    let width = sourceSize.width
+    let height = width / renderRatio
+    return CGRect(x: 0, y: (sourceSize.height - height) / 2.0, width: width, height: height)
+}
+
+private func lnSourceSizeForCenterFull(sourceSize: CGSize, renderSize: CGSize) -> CGSize {
+    guard sourceSize.width > 0, sourceSize.height > 0, renderSize.width > 0, renderSize.height > 0 else { return .zero }
+    let sourceRatio = sourceSize.width / sourceSize.height
+    let renderRatio = renderSize.width / renderSize.height
+    if sourceRatio > renderRatio {
+        let h = sourceSize.height
+        return CGSize(width: h * renderRatio, height: h)
+    }
+    let w = sourceSize.width
+    return CGSize(width: w, height: w / renderRatio)
+}
+
+private func lnGenVertices(rect: CGRect, containerSize: CGSize) -> [Float] {
+    guard containerSize.width > 0, containerSize.height > 0 else { return Array(repeating: 0, count: 16) }
+    let left = Float((rect.minX / containerSize.width) * 2.0 - 1.0)
+    let right = Float((rect.maxX / containerSize.width) * 2.0 - 1.0)
+    let top = Float(1.0 - (rect.minY / containerSize.height) * 2.0)
+    let bottom = Float(1.0 - (rect.maxY / containerSize.height) * 2.0)
+    return [left, bottom, 0, 1, right, bottom, 0, 1, left, top, 0, 1, right, top, 0, 1]
+}
+
+private func lnGenTextureCoordinates(rect: CGRect, containerSize: CGSize, reverse: Bool) -> [Float] {
+    guard containerSize.width > 0, containerSize.height > 0 else { return Array(repeating: 0, count: 8) }
+    let u0 = Float(rect.minX / containerSize.width)
+    let u1 = Float(rect.maxX / containerSize.width)
+    let v0 = Float(rect.minY / containerSize.height)
+    let v1 = Float(rect.maxY / containerSize.height)
+    return reverse ? [u0, v1, u1, v1, u0, v0, u1, v0] : [u0, v0, u1, v0, u0, v1, u1, v1]
+}
+
+@objc public enum LNVAPOrientation: Int {
+    case none = 0
+    case portrait = 1
+    case landscape = 2
+}
+
+@objcMembers
+public final class LNVAPAttachmentConstants: NSObject {
+    public static let fitTypeFitXY = "fitXY"
+    public static let fitTypeCenterFull = "centerFull"
+    public static let sourceTypeTextStr = "textStr"
+    public static let sourceTypeImgUrl = "imgUrl"
+    public static let sourceTypeText = "txt"
+    public static let sourceTypeImg = "img"
+    public static let sourceLoadTypeLocal = "local"
+    public static let sourceLoadTypeNet = "net"
+    public static let sourceStyleBoldText = "b"
+}
+
+@objcMembers
+public class LNBaseDFileInfo: NSObject {
+    public var filePath: String = ""
+    public var occupiedCount: Int = 0
+}
+
+@objcMembers
+public class LNBaseAnimatedImageFrame: NSObject {
+    public var frameIndex: Int = 0
+    public var duration: TimeInterval = 0
+    public var pts: UInt64 = 0
+    public var startDate: Date?
+    public var decodeTime: TimeInterval = 0
+
+    @objc(shouldFinishDisplaying)
+    public func shouldFinishDisplaying() -> Bool {
+        guard let startDate else { return true }
+        return Date().timeIntervalSince(startDate) * 1000.0 + 10.0 >= duration
+    }
+}
+
+@objcMembers
+public final class LNMP4AnimatedImageFrame: LNBaseAnimatedImageFrame {
+    public var pixelBuffer: CVPixelBuffer?
+    public var defaultFps: Int32 = 0
+}
+
+@objcMembers
+public final class LNMP4HWDFileInfo: LNBaseDFileInfo {
+    public var mp4Parser: LNMP4ParserProxy?
+}
+
+@objcMembers
+public final class LNVAPConfigModel: NSObject {
+    public var info: LNVAPCommonInfo?
+    public var resources: [LNVAPSourceInfo] = []
+    public var mergedConfig: [NSNumber: [LNVAPMergedInfo]] = [:]
+
+    public override var description: String {
+        "<\(Swift.type(of: self)): \(Unmanaged.passUnretained(self).toOpaque())> {info:\(String(describing: info)), configs:\(mergedConfig)}"
+    }
+}
+
+@objcMembers
+public final class LNVAPCommonInfo: NSObject {
+    public var version: Int = 0
+    public var framesCount: Int = 0
+    public var size: CGSize = .zero
+    public var videoSize: CGSize = .zero
+    public var targetOrientaion: LNVAPOrientation = .none
+    public var fps: Int = 0
+    public var isMerged: Bool = false
+    public var alphaAreaRect: CGRect = .zero
+    public var rgbAreaRect: CGRect = .zero
+
+    public override var description: String {
+        "<\(Swift.type(of: self)): \(Unmanaged.passUnretained(self).toOpaque())> {version:\(version), frames:\(framesCount), size:(\(size.width),\(size.height)), videoSize:(\(videoSize.width),\(videoSize.height)) orien:\(targetOrientaion.rawValue), fps:\(fps), merged:\(isMerged)}"
+    }
+}
+
+@objcMembers
+public final class LNVAPSourceInfo: NSObject {
+    public var type: String?
+    public var loadType: String?
+    public var contentTag: String?
+    public var contentTagValue: String?
+    public var color: UIColor?
+    public var style: String?
+    public var size: CGSize = .zero
+    public var fitType: String?
+
+    public var sourceImage: UIImage?
+    public var texture: MTLTexture?
+    public var colorParamsBuffer: MTLBuffer?
+
+    public override var description: String {
+        "<\(Swift.type(of: self)): \(Unmanaged.passUnretained(self).toOpaque())> {type:\(type ?? ""), tag:\(contentTag ?? "")-\(contentTagValue ?? "") fitType:\(fitType ?? "") }"
+    }
+}
+
+@objcMembers
+public final class LNVAPSourceDisplayItem: NSObject {
+    public var frame: CGRect = .zero
+    public var sourceInfo: LNVAPSourceInfo?
+}
+
+@objcMembers
+public final class LNVAPMergedInfo: NSObject {
+    public var source: LNVAPSourceInfo?
+    public var renderIndex: Int = 0
+    public var renderRect: CGRect = .zero
+    public var needMask: Bool = false
+    public var maskRect: CGRect = .zero
+    public var maskRotation: Int = 0
+
+    @objc(vertexBufferWithContainerSize:maskContianerSize:device:)
+    public func vertexBuffer(containerSize size: CGSize, maskContianerSize mSize: CGSize, device: MTLDevice) -> MTLBuffer? {
+        guard size.width > 0, size.height > 0, mSize.width > 0, mSize.height > 0 else { return nil }
+
+        let vertices = lnGenVertices(rect: renderRect, containerSize: size)
+        let maskCoordinates = lnGenTextureCoordinates(rect: maskRect, containerSize: mSize, reverse: true)
+        let sourceCoordinates: [Float]
+        if source?.fitType == LNVAPAttachmentConstants.fitTypeCenterFull, let source {
+            let sourceRect = lnRectForCenterFull(sourceSize: source.size, renderSize: renderRect.size)
+            let sourceSize = lnSourceSizeForCenterFull(sourceSize: source.size, renderSize: renderRect.size)
+            sourceCoordinates = lnGenTextureCoordinates(rect: sourceRect, containerSize: sourceSize, reverse: false)
+        } else {
+            sourceCoordinates = [0, 1, 1, 1, 0, 0, 1, 0]
+        }
+
+        var vertexData = [Float](repeating: 0, count: 32)
+        var idx = 0
+        for i in 0..<16 {
+            vertexData[idx] = vertices[i]
+            idx += 1
+            if i % 4 == 3 {
+                let row = i / 4
+                vertexData[idx] = sourceCoordinates[row * 2]; idx += 1
+                vertexData[idx] = sourceCoordinates[row * 2 + 1]; idx += 1
+                vertexData[idx] = maskCoordinates[row * 2]; idx += 1
+                vertexData[idx] = maskCoordinates[row * 2 + 1]; idx += 1
+            }
+        }
+
+        return device.makeBuffer(bytes: vertexData, length: vertexData.count * MemoryLayout<Float>.size, options: .storageModeShared)
+    }
+}
+
+@objcMembers
+public final class LNVAPMaskInfo: NSObject {
+    public var data: Data = Data()
+    public var sampleRect: CGRect = .zero
+    public var dataSize: CGSize = .zero
+    public var blurLength: Int = 0
+
+    private var cachedTexture: MTLTexture?
+
+    public var texture: MTLTexture? {
+        if cachedTexture == nil {
+            cachedTexture = LNVAPTextureLoader.loadTexture(withData: data, device: MTLCreateSystemDefaultDevice(), width: dataSize.width, height: dataSize.height)
+        }
+        return cachedTexture
+    }
+}
+
+@objcMembers
+public final class LNVAPTextureLoader: NSObject {
+#if targetEnvironment(simulator)
+    @objc(loadVapColorFillBufferWith:device:)
+    public static func loadVapColorFillBuffer(with color: UIColor?, device: MTLDevice?) -> MTLBuffer? { nil }
+
+    @objc(loadTextureWithImage:device:)
+    public static func loadTexture(with image: UIImage?, device: MTLDevice?) -> MTLTexture? { nil }
+
+    @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 texture = device.makeTexture(descriptor: descriptor) else { return nil }
+        let region = MTLRegionMake3D(0, 0, 0, Int(width), Int(height), 1)
+        data.withUnsafeBytes { ptr in
+            if let base = ptr.baseAddress {
+                texture.replace(region: region, mipmapLevel: 0, withBytes: base, bytesPerRow: Int(width))
+            }
+        }
+        return texture
+    }
+
+    @objc(drawingImageForText:color:size:bold:)
+    public static func drawingImage(forText textStr: String?, color: UIColor?, size: CGSize, bold: Bool) -> UIImage? { nil }
+
+    @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
+    @objc(loadVapColorFillBufferWith:device:)
+    public static func loadVapColorFillBuffer(with color: UIColor?, device: MTLDevice?) -> MTLBuffer? {
+        guard let device else { return nil }
+        var red: CGFloat = 0
+        var green: CGFloat = 0
+        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)
+    }
+
+    @objc(loadTextureWithImage:device:)
+    public static func loadTexture(with image: UIImage?, device: MTLDevice?) -> MTLTexture? {
+        guard let image, let device else { 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)
+            ])
+        }
+        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 texture = device.makeTexture(descriptor: descriptor) else { return nil }
+        let region = MTLRegionMake3D(0, 0, 0, Int(width), Int(height), 1)
+        data.withUnsafeBytes { ptr in
+            if let base = ptr.baseAddress {
+                texture.replace(region: region, mipmapLevel: 0, withBytes: base, bytesPerRow: Int(width))
+            }
+        }
+        return texture
+    }
+
+    @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 }
+        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 }
+
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.alignment = .center
+        paragraphStyle.lineBreakMode = .byTruncatingTail
+        let attributes: [NSAttributedString.Key: Any] = [.font: font, .paragraphStyle: paragraphStyle, .foregroundColor: textColor]
+
+        let renderer = UIGraphicsImageRenderer(size: rect.size)
+        return renderer.image { _ in
+            var drawRect = rect
+            drawRect.origin.y = (rect.height - font.lineHeight) / 2.0
+            textStr.draw(with: drawRect, options: .usesLineFragmentOrigin, attributes: attributes, context: nil)
+        }
+    }
+
+    @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? {
+        var font = isBold ? UIFont.boldSystemFont(ofSize: designedSize) : UIFont.systemFont(ofSize: designedSize)
+        guard let text, !text.isEmpty, fitFrame != .zero else {
+            textSize?.pointee = fitFrame.size
+            return font
+        }
+        var stringSize = (text as NSString).size(withAttributes: [.font: font])
+        var fontSize = designedSize
+        var remainCount = 100
+        while stringSize.width > fitFrame.width && fontSize > 2.0 && remainCount > 0 {
+            fontSize *= 0.9
+            remainCount -= 1
+            font = isBold ? UIFont.boldSystemFont(ofSize: fontSize) : UIFont.systemFont(ofSize: fontSize)
+            stringSize = (text as NSString).size(withAttributes: [.font: font])
+        }
+        textSize?.pointee = stringSize
+        return font
+    }
+
+    private static func cgLoadTexture(with image: UIImage, device: MTLDevice) -> MTLTexture? {
+        guard let imageRef = image.cgImage else { return nil }
+        let width = imageRef.width
+        let height = imageRef.height
+        let bytesPerPixel = 4
+        let bytesPerRow = bytesPerPixel * width
+
+        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 }
+
+        context.translateBy(x: 0, y: CGFloat(height))
+        context.scaleBy(x: 1, y: -1)
+        context.draw(imageRef, in: CGRect(x: 0, y: 0, width: width, height: height))
+
+        let descriptor = MTLTextureDescriptor.texture2DDescriptor(pixelFormat: .rgba8Unorm, width: width, height: height, mipmapped: false)
+        guard let texture = device.makeTexture(descriptor: descriptor) else {
+            free(rawData)
+            return nil
+        }
+
+        let region = MTLRegionMake3D(0, 0, 0, width, height, 1)
+        texture.replace(region: region, mipmapLevel: 0, withBytes: rawData, bytesPerRow: bytesPerRow)
+        free(rawData)
+        return texture
+    }
+#endif
+}

+ 870 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Parser/LNMP4Parser.swift

@@ -0,0 +1,870 @@
+import Foundation
+
+public let kLNBoxSizeLengthInBytes = 4
+public let kLNBoxTypeLengthInBytes = 4
+public let kLNBoxLargeSizeLengthInBytes = 8
+public let kLNBoxLargeSizeFlagLengthInBytes = 1
+
+private func lnReadU32(_ bytes: UnsafePointer<UInt8>, _ offset: Int) -> UInt32 {
+    (UInt32(bytes[offset]) << 24)
+    | (UInt32(bytes[offset + 1]) << 16)
+    | (UInt32(bytes[offset + 2]) << 8)
+    | UInt32(bytes[offset + 3])
+}
+
+private extension Data {
+    func lnU32(at offset: Int) -> UInt32 {
+        guard count >= offset + 4 else { return 0 }
+        return withUnsafeBytes { rawPtr in
+            guard let base = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
+            return lnReadU32(base, offset)
+        }
+    }
+
+    func lnU64(at offset: Int) -> UInt64 {
+        guard count >= offset + 8 else { return 0 }
+        return withUnsafeBytes { rawPtr in
+            guard let base = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
+            return lnReadU64(base.advanced(by: offset))
+        }
+    }
+}
+
+private func lnReadU64(_ bytes: UnsafePointer<UInt8>) -> UInt64 {
+    let b0 = UInt64(bytes[0]) << 56
+    let b1 = UInt64(bytes[1]) << 48
+    let b2 = UInt64(bytes[2]) << 40
+    let b3 = UInt64(bytes[3]) << 32
+    let b4 = UInt64(bytes[4]) << 24
+    let b5 = UInt64(bytes[5]) << 16
+    let b6 = UInt64(bytes[6]) << 8
+    let b7 = UInt64(bytes[7])
+    return b0 | b1 | b2 | b3 | b4 | b5 | b6 | b7
+}
+
+@objc public enum LNMP4CodecType: Int {
+    case unknown = 0
+    case video
+    case audio
+}
+
+@objc public enum LNMP4TrackType: UInt32 {
+    case video = 0x76696465
+    case audio = 0x736F756E
+    case hint = 0x68696E74
+}
+
+@objc public enum LNMP4VideoStreamCodecID: Int {
+    case unknown = 0
+    case h264
+    case h265
+}
+
+@objc public enum LNMP4BoxType: UInt32 {
+    case unknown = 0x0
+    case ftyp = 0x66747970
+    case free = 0x66726565
+    case mdat = 0x6D646174
+    case moov = 0x6D6F6F76
+    case mvhd = 0x6D766864
+    case iods = 0x696F6473
+    case trak = 0x7472616B
+    case tkhd = 0x746B6864
+    case edts = 0x65647473
+    case elst = 0x656C7374
+    case mdia = 0x6D646961
+    case mdhd = 0x6D646864
+    case hdlr = 0x68646C72
+    case minf = 0x6D696E66
+    case vmhd = 0x766D6864
+    case dinf = 0x64696E66
+    case dref = 0x64726566
+    case url = 0x0075726C
+    case stbl = 0x7374626C
+    case stsd = 0x73747364
+    case avc1 = 0x61766331
+    case avcC = 0x61766343
+    case stts = 0x73747473
+    case stss = 0x73747373
+    case stsc = 0x73747363
+    case stsz = 0x7374737A
+    case stco = 0x7374636F
+    case ctts = 0x63747473
+    case udta = 0x75647461
+    case meta = 0x6D657461
+    case ilst = 0x696C7374
+    case data = 0x64617461
+    case wide = 0x77696465
+    case loci = 0x6C6F6369
+    case smhd = 0x736D6864
+    case vapc = 0x76617063
+    case hvc1 = 0x68766331
+    case hvcC = 0x68766343
+}
+
+public typealias LNMP4BoxDataFetcher = (LNMP4Box) -> Data?
+
+@objcMembers
+open class LNMP4Box: NSObject {
+    public var type: LNMP4BoxType
+    public var length: UInt64
+    public var startIndexInBytes: UInt64
+    public weak var superBox: LNMP4Box?
+    public var subBoxes: [LNMP4Box] = []
+
+    public init(type: LNMP4BoxType, startIndex: UInt64, length: UInt64) {
+        self.type = type
+        self.startIndexInBytes = startIndex
+        self.length = length
+    }
+
+    @objc(subBoxOfType:)
+    public func subBox(ofType type: LNMP4BoxType) -> LNMP4Box? {
+        for subBox in subBoxes {
+            if subBox.type == type { return subBox }
+            if let box = subBox.subBox(ofType: type) { return box }
+        }
+        return nil
+    }
+
+    @objc(superBoxOfType:)
+    public func superBox(ofType type: LNMP4BoxType) -> LNMP4Box? {
+        guard let superBox else { return nil }
+        if superBox.type == type { return superBox }
+        return superBox.superBox(ofType: type)
+    }
+
+    open func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {}
+
+    private func description(level: Int) -> String {
+        var desc = "Box:\(typeString) offset:\(startIndexInBytes) size:\(length) "
+        for _ in 0..<level { desc = "|--\(desc)" }
+        desc = "\n\(desc)"
+        for sub in subBoxes {
+            desc += sub.description(level: level + 1)
+        }
+        return desc
+    }
+
+    public override var description: String {
+        description(level: 0)
+    }
+
+    private var typeString: String {
+        let value = type.rawValue
+        if value == 0 { return "unknown" }
+        let chars: [UInt8] = [
+            UInt8((value >> 24) & 0xFF),
+            UInt8((value >> 16) & 0xFF),
+            UInt8((value >> 8) & 0xFF),
+            UInt8(value & 0xFF)
+        ]
+        return String(bytes: chars.filter { $0 != 0 }, encoding: .ascii) ?? "unknown"
+    }
+}
+
+@objcMembers public final class LNMP4MdatBox: LNMP4Box {}
+@objcMembers public final class LNMP4AvccBox: LNMP4Box {}
+@objcMembers public final class LNMP4HvccBox: LNMP4Box {}
+@objcMembers public final class LNMP4MvhdBox: LNMP4Box {}
+@objcMembers public final class LNMP4StsdBox: LNMP4Box {}
+@objcMembers public final class LNMP4TrackBox: LNMP4Box {}
+
+@objcMembers
+public final class LNStscEntry: NSObject {
+    public var firstChunk: UInt32 = 0
+    public var samplesPerChunk: UInt32 = 0
+    public var sampleDescriptionIndex: UInt32 = 0
+}
+
+@objcMembers
+public final class LNMP4StscBox: LNMP4Box {
+    public var entries: [LNStscEntry] = []
+
+    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
+        guard let stscData = dataFetcher(self), stscData.count >= 16 else { return }
+        let entryCount = Int(stscData.lnU32(at: 12))
+        entries.removeAll(keepingCapacity: true)
+        for i in 0..<entryCount {
+            let base = 16 + i * 12
+            guard stscData.count >= base + 12 else { break }
+            let entry = LNStscEntry()
+            entry.firstChunk = stscData.lnU32(at: base)
+            entry.samplesPerChunk = stscData.lnU32(at: base + 4)
+            entry.sampleDescriptionIndex = stscData.lnU32(at: base + 8)
+            entries.append(entry)
+        }
+    }
+}
+
+@objcMembers
+public final class LNMP4StcoBox: LNMP4Box {
+    public var chunkCount: UInt32 = 0
+    public var chunkOffsets: [NSNumber] = []
+
+    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
+        guard let stcoData = dataFetcher(self), stcoData.count >= 16 else { return }
+        let entryCount = Int(stcoData.lnU32(at: 12))
+        chunkCount = UInt32(entryCount)
+        chunkOffsets.removeAll(keepingCapacity: true)
+        for i in 0..<entryCount {
+            let base = 16 + i * 4
+            guard stcoData.count >= base + 4 else { break }
+            chunkOffsets.append(NSNumber(value: stcoData.lnU32(at: base)))
+        }
+    }
+}
+
+@objcMembers
+public final class LNMP4StssBox: LNMP4Box {
+    public var syncSamples: [NSNumber] = []
+
+    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
+        guard let stssData = dataFetcher(self), stssData.count >= 16 else { return }
+        let sampleCount = Int(stssData.lnU32(at: 12))
+        syncSamples.removeAll(keepingCapacity: true)
+        for i in 0..<sampleCount {
+            let base = 16 + i * 4
+            guard stssData.count >= base + 4 else { break }
+            let index = Int(stssData.lnU32(at: base)) - 1
+            syncSamples.append(NSNumber(value: max(index, 0)))
+        }
+    }
+}
+
+@objcMembers
+public final class LNMP4CttsBox: LNMP4Box {
+    public var compositionOffsets: [NSNumber] = []
+
+    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
+        guard let cttsData = dataFetcher(self), cttsData.count >= 16 else { return }
+        let entryCount = Int(cttsData.lnU32(at: 12))
+        compositionOffsets.removeAll(keepingCapacity: true)
+        for i in 0..<entryCount {
+            let base = 16 + i * 8
+            guard cttsData.count >= base + 8 else { break }
+            let sampleCount = Int(cttsData.lnU32(at: base))
+            let compositionOffset = cttsData.lnU32(at: base + 4)
+            for _ in 0..<sampleCount {
+                compositionOffsets.append(NSNumber(value: compositionOffset))
+            }
+        }
+    }
+}
+
+@objcMembers
+public final class LNSttsEntry: NSObject {
+    public var sampleCount: UInt32 = 0
+    public var sampleDelta: UInt32 = 0
+}
+
+@objcMembers
+public final class LNMP4SttsBox: LNMP4Box {
+    public var entries: [LNSttsEntry] = []
+
+    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
+        guard let sttsData = dataFetcher(self), sttsData.count >= 16 else { return }
+        let entryCount = Int(sttsData.lnU32(at: 12))
+        entries.removeAll(keepingCapacity: true)
+        for i in 0..<entryCount {
+            let base = 16 + i * 8
+            guard sttsData.count >= base + 8 else { break }
+            let entry = LNSttsEntry()
+            entry.sampleCount = sttsData.lnU32(at: base)
+            entry.sampleDelta = sttsData.lnU32(at: base + 4)
+            entries.append(entry)
+        }
+    }
+}
+
+@objcMembers
+public final class LNMP4StszBox: LNMP4Box {
+    public var sampleCount: UInt32 = 0
+    public var sampleSizes: [NSNumber] = []
+
+    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
+        guard let stszData = dataFetcher(self), stszData.count >= 20 else { return }
+        let sampleSize = stszData.lnU32(at: 12)
+        let sampleCount = Int(stszData.lnU32(at: 16))
+        self.sampleCount = UInt32(sampleCount)
+        sampleSizes.removeAll(keepingCapacity: true)
+        for i in 0..<sampleCount {
+            if sampleSize > 0 {
+                sampleSizes.append(NSNumber(value: sampleSize))
+            } else {
+                let base = 20 + i * 4
+                guard stszData.count >= base + 4 else { break }
+                sampleSizes.append(NSNumber(value: stszData.lnU32(at: base)))
+            }
+        }
+    }
+}
+
+@objcMembers
+public final class LNMP4HdlrBox: LNMP4Box {
+    public var trackType: LNMP4TrackType = .video
+
+    public override func boxDidParsed(_ dataFetcher: LNMP4BoxDataFetcher) {
+        guard let hdlrData = dataFetcher(self), hdlrData.count >= 20 else { return }
+        let trackTypeValue = hdlrData.lnU32(at: 16)
+        trackType = LNMP4TrackType(rawValue: trackTypeValue) ?? .video
+    }
+}
+
+@objcMembers
+public final class LNMP4Sample: NSObject {
+    public var codecType: LNMP4CodecType = .unknown
+    public var sampleDelta: UInt32 = 0
+    public var sampleSize: UInt32 = 0
+    public var sampleIndex: UInt32 = 0
+    public var chunkIndex: UInt32 = 0
+    public var streamOffset: UInt32 = 0
+    public var pts: UInt64 = 0
+    public var dts: UInt64 = 0
+    public var isKeySample: Bool = false
+}
+
+@objcMembers
+public final class LNChunkOffsetEntry: NSObject {
+    public var samplesPerChunk: UInt32 = 0
+    public var offset: UInt32 = 0
+}
+
+@objcMembers
+public final class LNCttsEntry: NSObject {
+    public var sampleCount: UInt32 = 0
+    public var compositionOffset: UInt32 = 0
+}
+
+@objcMembers
+public final class LNMP4BoxFactory: NSObject {
+    @objc(isTypeValueValid:)
+    public static func isTypeValueValid(_ type: LNMP4BoxType) -> Bool {
+        boxClass(for: type) != nil
+    }
+
+    @objc(boxClassForType:)
+    public static func boxClass(for type: LNMP4BoxType) -> AnyClass? {
+        switch type {
+        case .stss:
+            return LNMP4StssBox.self
+        case .mdat:
+            return LNMP4MdatBox.self
+        case .avcC:
+            return LNMP4AvccBox.self
+        case .mdhd:
+            return LNMP4MvhdBox.self
+        case .stsd:
+            return LNMP4StsdBox.self
+        case .stsz:
+            return LNMP4StszBox.self
+        case .hdlr:
+            return LNMP4HdlrBox.self
+        case .stsc:
+            return LNMP4StscBox.self
+        case .stts:
+            return LNMP4SttsBox.self
+        case .stco:
+            return LNMP4StcoBox.self
+        case .hvcC:
+            return LNMP4HvccBox.self
+        case .ctts:
+            return LNMP4CttsBox.self
+        case .trak:
+            return LNMP4TrackBox.self
+        case .ftyp, .free, .moov, .mvhd, .tkhd, .edts, .elst, .mdia, .minf, .vmhd, .dinf, .dref, .url, .stbl, .avc1, .udta, .meta, .ilst, .data, .iods, .wide, .loci, .smhd, .vapc, .hvc1:
+            return LNMP4Box.self
+        default:
+            return nil
+        }
+    }
+
+    @objc(createBoxForType:startIndex:length:)
+    public static func createBox(for type: LNMP4BoxType, startIndex: UInt64, length: UInt64) -> LNMP4Box {
+        switch type {
+        case .stss: return LNMP4StssBox(type: type, startIndex: startIndex, length: length)
+        case .mdat: return LNMP4MdatBox(type: type, startIndex: startIndex, length: length)
+        case .avcC: return LNMP4AvccBox(type: type, startIndex: startIndex, length: length)
+        case .mdhd: return LNMP4MvhdBox(type: type, startIndex: startIndex, length: length)
+        case .stsd: return LNMP4StsdBox(type: type, startIndex: startIndex, length: length)
+        case .stsz: return LNMP4StszBox(type: type, startIndex: startIndex, length: length)
+        case .hdlr: return LNMP4HdlrBox(type: type, startIndex: startIndex, length: length)
+        case .stsc: return LNMP4StscBox(type: type, startIndex: startIndex, length: length)
+        case .stts: return LNMP4SttsBox(type: type, startIndex: startIndex, length: length)
+        case .stco: return LNMP4StcoBox(type: type, startIndex: startIndex, length: length)
+        case .hvcC: return LNMP4HvccBox(type: type, startIndex: startIndex, length: length)
+        case .ctts: return LNMP4CttsBox(type: type, startIndex: startIndex, length: length)
+        case .trak: return LNMP4TrackBox(type: type, startIndex: startIndex, length: length)
+        default: return LNMP4Box(type: type, startIndex: startIndex, length: length)
+        }
+    }
+}
+
+@objc
+public protocol LNMP4ParserDelegate: AnyObject {
+    @objc optional func didParseMP4Box(_ box: LNMP4Box, parser: LNMP4Parser)
+    @objc optional func mp4FileDidFinishParse(_ parser: LNMP4Parser)
+}
+
+@objcMembers
+public final class LNMP4Parser: NSObject {
+    public var rootBox: LNMP4Box?
+    public var fileHandle: FileHandle?
+    public weak var delegate: LNMP4ParserDelegate?
+
+    private let filePath: String
+
+    public init(filePath: String) {
+        self.filePath = filePath
+        self.fileHandle = FileHandle(forReadingAtPath: filePath)
+        super.init()
+    }
+
+    deinit {
+        fileHandle?.closeFile()
+    }
+
+    public func parse() {
+        guard !filePath.isEmpty, let fileHandle else { return }
+
+        let fileSize = fileHandle.seekToEndOfFile()
+        fileHandle.seek(toFileOffset: 0)
+
+        rootBox = LNMP4BoxFactory.createBox(for: .unknown, startIndex: 0, length: UInt64(fileSize))
+        guard let rootBox else { return }
+
+        var bfsQueue: [LNMP4Box] = [rootBox]
+
+        while !bfsQueue.isEmpty {
+            let calBox = bfsQueue.removeFirst()
+            if calBox.length <= UInt64(2 * (kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)) {
+                continue
+            }
+
+            var offset: UInt64 = calBox.superBox == nil ? 0 : (calBox.startIndexInBytes + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes))
+            if shouldResetOffset(calBox.type) {
+                calibrateOffset(&offset, boxType: calBox.type)
+            }
+
+            while true {
+                if offset + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes) > calBox.startIndexInBytes + calBox.length {
+                    break
+                }
+
+                guard let parsed = readBoxTypeAndLength(offset: offset) else { break }
+                let type = parsed.type
+                let length = parsed.length
+
+                if offset + length > calBox.startIndexInBytes + calBox.length {
+                    break
+                }
+
+                if !LNMP4BoxFactory.isTypeValueValid(type), offset == calBox.startIndexInBytes + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes) {
+                    break
+                }
+
+                let subBox = LNMP4BoxFactory.createBox(for: type, startIndex: offset, length: length)
+                subBox.superBox = calBox
+                calBox.subBoxes.append(subBox)
+                bfsQueue.append(subBox)
+                didParseBox(subBox)
+                offset += length
+            }
+        }
+
+        didFinishParseFile()
+    }
+
+    @objc(readDataForBox:)
+    public func readData(for box: LNMP4Box?) -> Data? {
+        guard let box, let fileHandle else { return nil }
+        fileHandle.seek(toFileOffset: box.startIndexInBytes)
+        return fileHandle.readData(ofLength: Int(box.length))
+    }
+
+    @objc(readValue:length:)
+    public func readValue(_ bytes: UnsafePointer<CChar>, length: Int) -> Int {
+        var value = 0
+        for i in 0..<length {
+            value += (Int(bytes[i]) & 0xff) << ((length - i - 1) * 8)
+        }
+        return value
+    }
+
+    private func readValue(_ bytes: UnsafePointer<UInt8>, length: Int) -> UInt64 {
+        var value: UInt64 = 0
+        for i in 0..<length {
+            value += UInt64(bytes[i] & 0xff) << UInt64((length - i - 1) * 8)
+        }
+        return value
+    }
+
+    private func readBoxTypeAndLength(offset: UInt64) -> (type: LNMP4BoxType, length: UInt64)? {
+        guard let fileHandle else { return nil }
+        fileHandle.seek(toFileOffset: offset)
+        let headerData = fileHandle.readData(ofLength: kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)
+        guard headerData.count >= kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes else {
+            return nil
+        }
+
+        let length: UInt64 = headerData.withUnsafeBytes { rawPtr in
+            guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
+            return readValue(bytes, length: kLNBoxSizeLengthInBytes)
+        }
+        let typeRaw: UInt32 = headerData.withUnsafeBytes { rawPtr in
+            guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
+            return UInt32(readValue(bytes.advanced(by: kLNBoxSizeLengthInBytes), length: kLNBoxTypeLengthInBytes))
+        }
+
+        var finalLength = length
+        let type = LNMP4BoxType(rawValue: typeRaw) ?? .unknown
+
+        if finalLength == UInt64(kLNBoxLargeSizeFlagLengthInBytes) {
+            let extendedOffset = offset + UInt64(kLNBoxSizeLengthInBytes + kLNBoxTypeLengthInBytes)
+            fileHandle.seek(toFileOffset: extendedOffset)
+            let largeSizeData = fileHandle.readData(ofLength: kLNBoxLargeSizeLengthInBytes)
+            guard largeSizeData.count >= kLNBoxLargeSizeLengthInBytes else {
+                return nil
+            }
+            finalLength = largeSizeData.withUnsafeBytes { rawPtr in
+                guard let bytes = rawPtr.baseAddress?.assumingMemoryBound(to: UInt8.self) else { return 0 }
+                return readValue(bytes, length: kLNBoxLargeSizeLengthInBytes)
+            }
+            if finalLength == 0 { return nil }
+        }
+
+        return (type, finalLength)
+    }
+
+    private func shouldResetOffset(_ type: LNMP4BoxType) -> Bool {
+        type == .stsd || type == .avc1 || type == .hvc1
+    }
+
+    private func calibrateOffset(_ offset: inout UInt64, boxType type: LNMP4BoxType) {
+        switch type {
+        case .stsd:
+            offset += 8
+        case .avc1, .hvc1:
+            offset += 78
+        default:
+            break
+        }
+    }
+
+    private func didParseBox(_ box: LNMP4Box) {
+        box.boxDidParsed { [weak self] in
+            self?.readData(for: $0)
+        }
+        delegate?.didParseMP4Box?(box, parser: self)
+    }
+
+    private func didFinishParseFile() {
+        delegate?.mp4FileDidFinishParse?(self)
+    }
+}
+
+@objcMembers
+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
+    public var spsData: Data?
+    public var ppsData: Data?
+    public var videoSamples: [LNMP4Sample] = []
+    public 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 init(filePath: String) {
+        parser = LNMP4Parser(filePath: filePath)
+        super.init()
+        parser.delegate = self
+    }
+
+    public func parse() {
+        parser.parse()
+        rootBox = parser.rootBox
+        parseVideoDecoderConfigRecord()
+    }
+
+    @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]
+        fileHandle.seek(toFileOffset: UInt64(sample.streamOffset))
+        return fileHandle.readData(ofLength: Int(sample.sampleSize))
+    }
+
+    @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 let fileHandle = parser.fileHandle else { return nil }
+        fileHandle.seek(toFileOffset: box.startIndexInBytes + UInt64(offset))
+        return fileHandle.readData(ofLength: length)
+    }
+
+    private func parseVideoDecoderConfigRecord() {
+        if videoCodecID == .h264 {
+            spsData = parseAvccSPSData()
+            ppsData = parseAvccPPSData()
+        } 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] {
+        guard let videoTrackBox,
+              let stts = videoTrackBox.subBox(ofType: .stts) as? LNMP4SttsBox,
+              let stsz = videoTrackBox.subBox(ofType: .stsz) as? LNMP4StszBox,
+              let stsc = videoTrackBox.subBox(ofType: .stsc) as? LNMP4StscBox,
+              let stco = videoTrackBox.subBox(ofType: .stco) as? LNMP4StcoBox else {
+            return []
+        }
+
+        let ctts = videoTrackBox.subBox(ofType: .ctts) as? LNMP4CttsBox
+
+        var samples: [LNMP4Sample] = []
+        var ptsAccumulator: UInt64 = 0
+
+        var stscEntryIndex: UInt32 = 0
+        var stscEntrySampleIndex: UInt32 = 0
+        var stscEntrySampleOffset: UInt32 = 0
+        var sttsEntryIndex: UInt32 = 0
+        var sttsEntrySampleIndex: UInt32 = 0
+        var stcoChunkLogicIndex: UInt32 = 0
+
+        for i in 0..<Int(stsz.sampleCount) {
+            if Int(stscEntryIndex) >= stsc.entries.count
+                || Int(sttsEntryIndex) >= stts.entries.count
+                || Int(stcoChunkLogicIndex) >= stco.chunkOffsets.count {
+                break
+            }
+
+            let stscEntry = stsc.entries[Int(stscEntryIndex)]
+            let sttsEntry = stts.entries[Int(sttsEntryIndex)]
+
+            let chunkOffset = stco.chunkOffsets[Int(stcoChunkLogicIndex)].uint32Value
+            let sampleOffset = chunkOffset + stscEntrySampleOffset
+
+            var cttsValue: UInt32 = 0
+            if let ctts, i < ctts.compositionOffsets.count {
+                cttsValue = ctts.compositionOffsets[i].uint32Value
+            }
+
+            let sample = LNMP4Sample()
+            sample.codecType = .video
+            sample.sampleIndex = UInt32(i)
+            sample.chunkIndex = stcoChunkLogicIndex
+            sample.sampleDelta = sttsEntry.sampleDelta
+            sample.sampleSize = stsz.sampleSizes[i].uint32Value
+            sample.pts = ptsAccumulator + UInt64(cttsValue)
+            sample.streamOffset = sampleOffset
+            samples.append(sample)
+
+            stscEntrySampleOffset += sample.sampleSize
+            ptsAccumulator += UInt64(sample.sampleDelta)
+
+            stscEntrySampleIndex += 1
+            if stscEntrySampleIndex >= stscEntry.samplesPerChunk {
+                if stcoChunkLogicIndex + 1 < UInt32(stco.chunkOffsets.count) {
+                    stcoChunkLogicIndex += 1
+                }
+                stscEntrySampleIndex = 0
+                stscEntrySampleOffset = 0
+            }
+
+            sttsEntrySampleIndex += 1
+            if sttsEntrySampleIndex >= sttsEntry.sampleCount {
+                sttsEntrySampleIndex = 0
+                if sttsEntryIndex + 1 < UInt32(stts.entries.count) {
+                    sttsEntryIndex += 1
+                }
+            }
+
+            if stscEntryIndex + 1 < UInt32(stsc.entries.count) {
+                let nextFirstChunk = stsc.entries[Int(stscEntryIndex + 1)].firstChunk
+                if stcoChunkLogicIndex >= nextFirstChunk - 1 {
+                    stscEntryIndex += 1
+                }
+            }
+        }
+
+        return samples
+    }
+
+    private func parseHvccDecoderConfigRecord() {
+        guard let hvcc = videoTrackBox?.subBox(ofType: .hvcC),
+              let extraData = parser.readData(for: hvcc),
+              extraData.count > 8 else { return }
+
+        let bytes = [UInt8](extraData)
+        var index = 30
+        let arrayNum = Int(bytes[index])
+        index += 1
+
+        for _ in 0..<arrayNum {
+            let value = Int(bytes[index])
+            index += 1
+            let naluType = value & 0x3F
+            let naluNum = (Int(bytes[index]) << 8) + Int(bytes[index + 1])
+            index += 2
+
+            for _ in 0..<naluNum {
+                let naluLength = (Int(bytes[index]) << 8) + Int(bytes[index + 1])
+                index += 2
+                guard index + naluLength <= bytes.count else { return }
+                let paramData = Data(bytes[index..<(index + naluLength)])
+
+                if naluType == 32 {
+                    vpsData = paramData
+                } else if naluType == 33 {
+                    spsData = paramData
+                } else if naluType == 34 {
+                    ppsData = paramData
+                }
+                index += naluLength
+            }
+        }
+    }
+
+    private func parseAvccSPSData() -> Data? {
+        guard let avcc = videoTrackBox?.subBox(ofType: .avcC),
+              let extraData = parser.readData(for: avcc),
+              extraData.count > 16 else { return nil }
+
+        let bytes = [UInt8](extraData)
+        let spsLength = (Int(bytes[14]) << 8) + Int(bytes[15])
+        let naluType = Int(bytes[16] & 0x1F)
+
+        guard spsLength + 16 <= bytes.count, naluType == 7 else { return nil }
+        return Data(bytes[16..<(16 + spsLength)])
+    }
+
+    private func parseAvccPPSData() -> Data? {
+        guard let avcc = videoTrackBox?.subBox(ofType: .avcC),
+              let extraData = parser.readData(for: avcc),
+              extraData.count > 16 else { return nil }
+
+        let bytes = [UInt8](extraData)
+        var spsCount = Int(bytes[13] & 0x1F)
+        let spsLength = (Int(bytes[14]) << 8) + Int(bytes[15])
+        var prefixLength = 16 + spsLength
+
+        while spsCount > 1 {
+            guard prefixLength + 2 < bytes.count else { return nil }
+            let nextSpsLength = (Int(bytes[prefixLength]) << 8) + Int(bytes[prefixLength + 1])
+            prefixLength += nextSpsLength
+            spsCount -= 1
+        }
+
+        guard prefixLength + 3 < bytes.count else { return nil }
+        let ppsLength = (Int(bytes[prefixLength + 1]) << 8) + Int(bytes[prefixLength + 2])
+        let naluType = Int(bytes[prefixLength + 3] & 0x1F)
+        guard naluType == 8, ppsLength + prefixLength + 3 <= bytes.count else { return nil }
+
+        return Data(bytes[(prefixLength + 3)..<(prefixLength + 3 + ppsLength)])
+    }
+
+    private func readPicWidth() -> Int {
+        guard videoCodecID != .unknown,
+              let box = videoTrackBox?.subBox(ofType: videoCodecID == .h264 ? .avc1 : .hvc1),
+              let fileHandle = parser.fileHandle else { return 0 }
+
+        fileHandle.seek(toFileOffset: box.startIndexInBytes + 32)
+        let widthData = fileHandle.readData(ofLength: 2)
+        guard widthData.count >= 2 else { return 0 }
+        let bytes = [UInt8](widthData)
+        return (Int(bytes[0]) << 8) + Int(bytes[1])
+    }
+
+    private func readPicHeight() -> Int {
+        guard videoCodecID != .unknown,
+              let box = videoTrackBox?.subBox(ofType: videoCodecID == .h264 ? .avc1 : .hvc1),
+              let fileHandle = parser.fileHandle else { return 0 }
+
+        fileHandle.seek(toFileOffset: box.startIndexInBytes + 34)
+        let heightData = fileHandle.readData(ofLength: 2)
+        guard heightData.count >= 2 else { return 0 }
+        let bytes = [UInt8](heightData)
+        return (Int(bytes[0]) << 8) + Int(bytes[1])
+    }
+
+    private func readDuration() -> Double {
+        guard let mvhd = rootBox?.subBox(ofType: .mvhd),
+              let mvhdData = parser.readData(for: mvhd),
+              mvhdData.count > 24 else { return 0 }
+
+        let version = Int(mvhdData.lnU32(at: 8))
+        var timeScaleIndex = 20
+        var durationIndex = 24
+        var durationLength = 4
+        if version == 1 {
+            timeScaleIndex = 28
+            durationIndex = 32
+            durationLength = 8
+        }
+
+        guard mvhdData.count >= durationIndex + durationLength else { return 0 }
+
+        let scale = Int(mvhdData.lnU32(at: timeScaleIndex))
+        let durationValue: Int
+        if durationLength == 4 {
+            durationValue = Int(mvhdData.lnU32(at: durationIndex))
+        } else {
+            durationValue = Int(mvhdData.lnU64(at: durationIndex))
+        }
+
+        guard scale != 0 else { return 0 }
+        return Double(durationValue) / Double(scale)
+    }
+
+    public func mp4FileDidFinishParse(_ parser: LNMP4Parser) {}
+
+    public func didParseMP4Box(_ box: LNMP4Box, parser: LNMP4Parser) {
+        switch box.type {
+        case .hdlr:
+            if let hdlr = box as? LNMP4HdlrBox,
+               let trackBox = box.superBox(ofType: .trak) as? LNMP4TrackBox {
+                switch hdlr.trackType {
+                case .video:
+                    videoTrackBox = trackBox
+                case .audio:
+                    audioTrackBox = trackBox
+                default:
+                    break
+                }
+            }
+        case .avc1:
+            videoCodecID = .h264
+        case .hvc1:
+            videoCodecID = .h265
+        default:
+            break
+        }
+    }
+}

+ 1072 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Render/LNRenderers.swift

@@ -0,0 +1,1072 @@
+import Foundation
+import UIKit
+import CoreVideo
+import CoreImage
+import QuartzCore
+import Metal
+import MetalKit
+import simd
+
+@objc public protocol LNHWDMetalViewDelegate: AnyObject {
+    @objc(onMetalViewUnavailable)
+    func onMetalViewUnavailable()
+}
+
+@objc public protocol LNVAPMetalViewDelegate: AnyObject {
+    @objc(onMetalViewUnavailable)
+    func onMetalViewUnavailable()
+}
+
+@objc public protocol LNHWDMP4OpenGLViewDelegate: AnyObject {
+    @objc(onViewUnavailableStatus)
+    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)
+
+    func render(pixelBuffer: CVPixelBuffer, into layer: CALayer) {
+        let image = CIImage(cvPixelBuffer: pixelBuffer)
+        guard let cgImage = ciContext.createCGImage(image, from: image.extent) else { return }
+        DispatchQueue.main.async {
+            layer.contents = cgImage
+        }
+    }
+}
+
+@available(iOS 13.0, *)
+private final class LNHWDMetalCoreRenderer {
+    private struct LNColorParameters {
+        var matrix: simd_float3x3
+        var offset: simd_float2
+    }
+
+    private static let kVertexFunctionName = "hwd_vertexShader"
+    private static let kFragmentFunctionName = "hwd_yuvFragmentShader"
+
+    private static let verticesByBlendMode: [[[Float]]] = [
+        // alpha left
+        [
+            [-1.0, -1.0, 0.0, 1.0, 0.5, 1.0, 0.0, 1.0],
+            [-1.0,  1.0, 0.0, 1.0, 0.5, 0.0, 0.0, 0.0],
+            [ 1.0, -1.0, 0.0, 1.0, 1.0, 1.0, 0.5, 1.0],
+            [ 1.0,  1.0, 0.0, 1.0, 1.0, 0.0, 0.5, 0.0]
+        ],
+        // alpha right
+        [
+            [-1.0, -1.0, 0.0, 1.0, 0.0, 1.0, 0.5, 1.0],
+            [-1.0,  1.0, 0.0, 1.0, 0.0, 0.0, 0.5, 0.0],
+            [ 1.0, -1.0, 0.0, 1.0, 0.5, 1.0, 1.0, 1.0],
+            [ 1.0,  1.0, 0.0, 1.0, 0.5, 0.0, 1.0, 0.0]
+        ],
+        // alpha top
+        [
+            [-1.0, -1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 0.5],
+            [-1.0,  1.0, 0.0, 1.0, 0.0, 0.5, 0.0, 0.0],
+            [ 1.0, -1.0, 0.0, 1.0, 1.0, 1.0, 1.0, 0.5],
+            [ 1.0,  1.0, 0.0, 1.0, 1.0, 0.5, 1.0, 0.0]
+        ],
+        // alpha bottom
+        [
+            [-1.0, -1.0, 0.0, 1.0, 0.0, 0.5, 0.0, 1.0],
+            [-1.0,  1.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.5],
+            [ 1.0, -1.0, 0.0, 1.0, 1.0, 0.5, 1.0, 1.0],
+            [ 1.0,  1.0, 0.0, 1.0, 1.0, 0.0, 1.0, 0.5]
+        ]
+    ]
+
+    private static let matrix601Full = simd_float3x3(
+        SIMD3<Float>(1.0, 1.0, 1.0),
+        SIMD3<Float>(0.0, -0.34413, 1.772),
+        SIMD3<Float>(1.402, -0.71414, 0.0)
+    )
+
+    private static let matrix709Full = simd_float3x3(
+        SIMD3<Float>(1.0, 1.0, 1.0),
+        SIMD3<Float>(0.0, -0.18732, 1.8556),
+        SIMD3<Float>(1.57481, -0.46813, 0.0)
+    )
+
+    private let device: MTLDevice
+    private let commandQueue: MTLCommandQueue
+    private let pipelineState: MTLRenderPipelineState
+    private var textureCache: CVMetalTextureCache?
+    private var vertexBuffer: MTLBuffer?
+    private var yuvMatrixBuffer: MTLBuffer?
+    private var currentColorMatrix = matrix601Full
+
+    init?(metalLayer: CAMetalLayer, blendMode: QGHWDTextureBlendMode) {
+        guard let device = MTLCreateSystemDefaultDevice(),
+              let queue = device.makeCommandQueue(),
+              let library = device.makeDefaultLibrary(),
+              let vertexFunc = library.makeFunction(name: Self.kVertexFunctionName),
+              let fragmentFunc = library.makeFunction(name: 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
+
+        metalLayer.device = device
+        metalLayer.framebufferOnly = false
+
+        let cacheStatus = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &textureCache)
+        guard cacheStatus == kCVReturnSuccess else { return nil }
+
+        updateBlendMode(blendMode)
+        updateYuvMatrixBuffer(matrix: Self.matrix601Full)
+    }
+
+    func updateBlendMode(_ blendMode: QGHWDTextureBlendMode) {
+        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) {
+        guard metalLayer.bounds.width > 0,
+              metalLayer.bounds.height > 0,
+              let drawable = metalLayer.nextDrawable(),
+              let commandBuffer = commandQueue.makeCommandBuffer(),
+              let descriptor = currentRenderPassDescriptor(texture: drawable.texture),
+              let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor),
+              let vertexBuffer,
+              let yuvMatrixBuffer else {
+            return
+        }
+
+        updateYuvMatrixIfNeeded(pixelBuffer: pixelBuffer)
+        guard let yTexture = makeTexture(pixelBuffer: pixelBuffer, plane: 0, format: .r8Unorm),
+              let uvTexture = makeTexture(pixelBuffer: pixelBuffer, plane: 1, format: .rg8Unorm) else {
+            return
+        }
+
+        encoder.setRenderPipelineState(pipelineState)
+        encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
+        encoder.setFragmentBuffer(yuvMatrixBuffer, offset: 0, index: 0)
+        encoder.setFragmentTexture(yTexture, index: 0)
+        encoder.setFragmentTexture(uvTexture, index: 1)
+        encoder.drawPrimitives(type: MTLPrimitiveType.triangleStrip, vertexStart: 0, vertexCount: 4)
+        encoder.endEncoding()
+
+        commandBuffer.present(drawable)
+        commandBuffer.commit()
+    }
+
+    func dispose() {
+        if let textureCache {
+            CVMetalTextureCacheFlush(textureCache, 0)
+        }
+        textureCache = nil
+        vertexBuffer = nil
+        yuvMatrixBuffer = nil
+    }
+
+    private func makeTexture(pixelBuffer: CVPixelBuffer, plane: Int, format: MTLPixelFormat) -> MTLTexture? {
+        guard let textureCache else { return nil }
+        let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, plane)
+        let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, plane)
+        var cvTexture: CVMetalTexture?
+        let status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
+                                                               textureCache,
+                                                               pixelBuffer,
+                                                               nil,
+                                                               format,
+                                                               width,
+                                                               height,
+                                                               plane,
+                                                               &cvTexture)
+        guard status == kCVReturnSuccess, let cvTexture else { return nil }
+        return CVMetalTextureGetTexture(cvTexture)
+    }
+
+    private func currentRenderPassDescriptor(texture: MTLTexture) -> MTLRenderPassDescriptor? {
+        let descriptor = MTLRenderPassDescriptor()
+        descriptor.colorAttachments[0].texture = texture
+        descriptor.colorAttachments[0].loadAction = .clear
+        descriptor.colorAttachments[0].storeAction = .store
+        descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
+        return descriptor
+    }
+
+    private func updateYuvMatrixIfNeeded(pixelBuffer: CVPixelBuffer) {
+        let matrixAttachment = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, nil)?.takeUnretainedValue()
+        let matrix = (matrixAttachment != nil && CFEqual(matrixAttachment, kCVImageBufferYCbCrMatrix_ITU_R_709_2)) ? Self.matrix709Full : Self.matrix601Full
+        guard matrix != currentColorMatrix else { return }
+        currentColorMatrix = matrix
+        updateYuvMatrixBuffer(matrix: matrix)
+    }
+
+    private func updateYuvMatrixBuffer(matrix: simd_float3x3) {
+        let params = LNColorParameters(matrix: matrix, offset: simd_float2(0.5, 0.5))
+        yuvMatrixBuffer = device.makeBuffer(bytes: [params], length: MemoryLayout<LNColorParameters>.stride, options: .storageModeShared)
+    }
+}
+
+@available(iOS 13.0, *)
+private final class LNVAPMetalCoreRenderer {
+    private struct LNColorParameters {
+        var matrix: simd_float3x3
+        var offset: simd_float2
+    }
+
+    private struct LNMaskParameters {
+        var weightMatrix: simd_float3x3
+        var coreSize: Int32
+        var texelOffset: Float
+    }
+
+    private static let matrix601Full = simd_float3x3(
+        SIMD3<Float>(1.0, 1.0, 1.0),
+        SIMD3<Float>(0.0, -0.34413, 1.772),
+        SIMD3<Float>(1.402, -0.71414, 0.0)
+    )
+
+    private static let matrix709Full = simd_float3x3(
+        SIMD3<Float>(1.0, 1.0, 1.0),
+        SIMD3<Float>(0.0, -0.18732, 1.8556),
+        SIMD3<Float>(1.57481, -0.46813, 0.0)
+    )
+
+    private let device: MTLDevice
+    private let pixelFormat: MTLPixelFormat
+    private let commandQueue: MTLCommandQueue
+    private let shaderLoader: LNVAPMetalShaderFunctionLoader
+    private var textureCache: CVMetalTextureCache?
+    private var defaultMainPipelineState: MTLRenderPipelineState?
+    private var mainPipelineStateForMask: MTLRenderPipelineState?
+    private var mainPipelineStateForMaskBlur: MTLRenderPipelineState?
+    private var attachmentPipelineState: MTLRenderPipelineState?
+
+    private var vertexBuffer: MTLBuffer?
+    private var yuvMatrixBuffer: MTLBuffer?
+    private var maskBlurBuffer: MTLBuffer?
+    private var maskTexture: MTLTexture?
+    private var currentColorMatrix = matrix601Full
+
+    var commonInfo: LNVAPCommonInfo? {
+        didSet {
+            updateMainVertexBuffer()
+        }
+    }
+
+    var maskInfo: LNVAPMaskInfo? {
+        didSet {
+            updateMainVertexBuffer()
+            updateMaskTexture()
+        }
+    }
+
+    init?(metalLayer: CAMetalLayer) {
+        guard let device = MTLCreateSystemDefaultDevice(),
+              let queue = device.makeCommandQueue() else {
+            return nil
+        }
+
+        self.device = device
+        self.pixelFormat = metalLayer.pixelFormat
+        self.commandQueue = queue
+        self.shaderLoader = LNVAPMetalShaderFunctionLoader(device: device)
+
+        metalLayer.device = device
+        metalLayer.framebufferOnly = false
+
+        let cacheStatus = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, device, nil, &textureCache)
+        guard cacheStatus == kCVReturnSuccess else { return nil }
+
+        updateYuvMatrixBuffer(matrix: Self.matrix601Full)
+    }
+
+    func render(pixelBuffer: CVPixelBuffer, metalLayer: CAMetalLayer, mergeInfos: [LNVAPMergedInfo]) {
+        guard metalLayer.bounds.width > 0,
+              metalLayer.bounds.height > 0,
+              let drawable = metalLayer.nextDrawable(),
+              let commandBuffer = commandQueue.makeCommandBuffer(),
+              let descriptor = currentRenderPassDescriptor(texture: drawable.texture),
+              let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: descriptor) else {
+            return
+        }
+
+        updateYuvMatrixIfNeeded(pixelBuffer: pixelBuffer)
+        guard let yTexture = makeTexture(pixelBuffer: pixelBuffer, plane: 0, format: .r8Unorm),
+              let uvTexture = makeTexture(pixelBuffer: pixelBuffer, plane: 1, format: .rg8Unorm) else {
+            return
+        }
+
+        drawBackground(encoder: encoder, yTexture: yTexture, uvTexture: uvTexture)
+        drawMergedAttachments(encoder: encoder, yTexture: yTexture, uvTexture: uvTexture, mergeInfos: mergeInfos)
+
+        encoder.endEncoding()
+        commandBuffer.present(drawable)
+        commandBuffer.commit()
+    }
+
+    func dispose() {
+        if let textureCache {
+            CVMetalTextureCacheFlush(textureCache, 0)
+        }
+        textureCache = nil
+        vertexBuffer = nil
+        yuvMatrixBuffer = nil
+        maskBlurBuffer = nil
+        maskTexture = nil
+        defaultMainPipelineState = nil
+        mainPipelineStateForMask = nil
+        mainPipelineStateForMaskBlur = nil
+        attachmentPipelineState = nil
+    }
+
+    private func drawBackground(encoder: MTLRenderCommandEncoder, yTexture: MTLTexture, uvTexture: MTLTexture) {
+        guard let vertexBuffer,
+              let yuvMatrixBuffer else {
+            return
+        }
+
+        if let maskInfo,
+           let maskTexture {
+            if maskInfo.blurLength > 0 {
+                guard let pipelineState = mainPipelineStateForMaskBlur ?? makeMaskBlurPipelineState(),
+                      let maskBlurBuffer = maskBlurBuffer ?? makeMaskBlurBuffer() else {
+                    return
+                }
+                encoder.setRenderPipelineState(pipelineState)
+                encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
+                encoder.setFragmentBuffer(yuvMatrixBuffer, offset: 0, index: 0)
+                encoder.setFragmentBuffer(maskBlurBuffer, offset: 0, index: 1)
+                encoder.setFragmentTexture(yTexture, index: 0)
+                encoder.setFragmentTexture(uvTexture, index: 1)
+                encoder.setFragmentTexture(maskTexture, index: 2)
+                encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
+                return
+            }
+
+            guard let pipelineState = mainPipelineStateForMask ?? makeMaskPipelineState() else {
+                return
+            }
+            encoder.setRenderPipelineState(pipelineState)
+            encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
+            encoder.setFragmentBuffer(yuvMatrixBuffer, offset: 0, index: 0)
+            encoder.setFragmentTexture(yTexture, index: 0)
+            encoder.setFragmentTexture(uvTexture, index: 1)
+            encoder.setFragmentTexture(maskTexture, index: 2)
+            encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
+            return
+        }
+
+        guard let pipelineState = defaultMainPipelineState ?? makeMainPipelineState() else {
+            return
+        }
+        encoder.setRenderPipelineState(pipelineState)
+        encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
+        encoder.setFragmentBuffer(yuvMatrixBuffer, offset: 0, index: 0)
+        encoder.setFragmentTexture(yTexture, index: 0)
+        encoder.setFragmentTexture(uvTexture, index: 1)
+        encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
+    }
+
+    private func drawMergedAttachments(encoder: MTLRenderCommandEncoder, yTexture: MTLTexture, uvTexture: MTLTexture, mergeInfos: [LNVAPMergedInfo]) {
+        guard !mergeInfos.isEmpty,
+              let commonInfo,
+              let pipelineState = attachmentPipelineState ?? makeAttachmentPipelineState(),
+              let yuvMatrixBuffer else {
+            return
+        }
+
+        for mergeInfo in mergeInfos {
+            guard let sourceTexture = mergeInfo.source?.texture,
+                  let colorParamsBuffer = mergeInfo.source?.colorParamsBuffer,
+                  let mergeVertexBuffer = mergeInfo.vertexBuffer(containerSize: commonInfo.size,
+                                                                 maskContianerSize: commonInfo.videoSize,
+                                                                 device: device) else {
+                continue
+            }
+            encoder.setRenderPipelineState(pipelineState)
+            encoder.setVertexBuffer(mergeVertexBuffer, offset: 0, index: 0)
+            encoder.setFragmentBuffer(yuvMatrixBuffer, offset: 0, index: 0)
+            encoder.setFragmentBuffer(colorParamsBuffer, offset: 0, index: 1)
+            encoder.setFragmentTexture(yTexture, index: 0)
+            encoder.setFragmentTexture(uvTexture, index: 1)
+            encoder.setFragmentTexture(sourceTexture, index: 2)
+            encoder.drawPrimitives(type: .triangleStrip, vertexStart: 0, vertexCount: 4)
+        }
+    }
+
+    private func makeMainPipelineState() -> MTLRenderPipelineState? {
+        guard let state = createPipelineState(vertexFunction: LNVAPMetalUtil.vapVertexFunctionName,
+                                              fragmentFunction: LNVAPMetalUtil.vapYUVFragmentFunctionName) else {
+            return nil
+        }
+        defaultMainPipelineState = state
+        return state
+    }
+
+    private func makeMaskPipelineState() -> MTLRenderPipelineState? {
+        guard let state = createPipelineState(vertexFunction: LNVAPMetalUtil.vapVertexFunctionName,
+                                              fragmentFunction: LNVAPMetalUtil.vapMaskFragmentFunctionName) else {
+            return nil
+        }
+        mainPipelineStateForMask = state
+        return state
+    }
+
+    private func makeMaskBlurPipelineState() -> MTLRenderPipelineState? {
+        guard let state = createPipelineState(vertexFunction: LNVAPMetalUtil.vapVertexFunctionName,
+                                              fragmentFunction: LNVAPMetalUtil.vapMaskBlurFragmentFunctionName) else {
+            return nil
+        }
+        mainPipelineStateForMaskBlur = state
+        return state
+    }
+
+    private func makeAttachmentPipelineState() -> MTLRenderPipelineState? {
+        guard let state = createPipelineState(vertexFunction: LNVAPMetalUtil.vapAttachmentVertexFunctionName,
+                                              fragmentFunction: LNVAPMetalUtil.vapAttachmentFragmentFunctionName) else {
+            return nil
+        }
+        attachmentPipelineState = state
+        return state
+    }
+
+    private func createPipelineState(vertexFunction: String, fragmentFunction: String) -> MTLRenderPipelineState? {
+        guard let vertexProgram = shaderLoader.loadFunction(withName: vertexFunction),
+              let fragmentProgram = shaderLoader.loadFunction(withName: fragmentFunction) else {
+            return nil
+        }
+
+        let descriptor = MTLRenderPipelineDescriptor()
+        descriptor.vertexFunction = vertexProgram
+        descriptor.fragmentFunction = fragmentProgram
+        descriptor.colorAttachments[0].pixelFormat = pixelFormat
+        descriptor.colorAttachments[0].isBlendingEnabled = true
+        descriptor.colorAttachments[0].rgbBlendOperation = .add
+        descriptor.colorAttachments[0].alphaBlendOperation = .add
+        descriptor.colorAttachments[0].sourceRGBBlendFactor = .sourceAlpha
+        descriptor.colorAttachments[0].sourceAlphaBlendFactor = .sourceAlpha
+        descriptor.colorAttachments[0].destinationRGBBlendFactor = .oneMinusSourceAlpha
+        descriptor.colorAttachments[0].destinationAlphaBlendFactor = .oneMinusSourceAlpha
+        return try? device.makeRenderPipelineState(descriptor: descriptor)
+    }
+
+    private func updateMainVertexBuffer() {
+        guard let commonInfo else {
+            vertexBuffer = nil
+            return
+        }
+
+        let vertices = LNVAPMetalUtil.vapMTLVerticesIdentity
+        let rgbCoordinates = LNVAPMetalUtil.genTextureCoordinates(rect: commonInfo.rgbAreaRect, containerSize: commonInfo.videoSize, reverse: false, degree: 0)
+        let alphaCoordinates = LNVAPMetalUtil.genTextureCoordinates(rect: commonInfo.alphaAreaRect, containerSize: commonInfo.videoSize, reverse: false, degree: 0)
+        let maskCoordinates: [Float]
+        if let maskInfo {
+            maskCoordinates = LNVAPMetalUtil.genTextureCoordinates(rect: maskInfo.sampleRect, containerSize: maskInfo.dataSize, reverse: false, degree: 0)
+        } else {
+            maskCoordinates = [Float](repeating: 0, count: 8)
+        }
+
+        var vertexData = [Float](repeating: 0, count: 40)
+        var index = 0
+        for i in 0..<16 {
+            vertexData[index] = vertices[i]
+            index += 1
+            if i % 4 == 3 {
+                let row = i / 4
+                vertexData[index] = rgbCoordinates[row * 2]
+                index += 1
+                vertexData[index] = rgbCoordinates[row * 2 + 1]
+                index += 1
+                vertexData[index] = alphaCoordinates[row * 2]
+                index += 1
+                vertexData[index] = alphaCoordinates[row * 2 + 1]
+                index += 1
+                vertexData[index] = maskCoordinates[row * 2]
+                index += 1
+                vertexData[index] = maskCoordinates[row * 2 + 1]
+                index += 1
+            }
+        }
+        vertexBuffer = device.makeBuffer(bytes: vertexData, length: vertexData.count * MemoryLayout<Float>.size, options: .storageModeShared)
+    }
+
+    private func updateMaskTexture() {
+        guard let maskInfo else {
+            maskTexture = nil
+            return
+        }
+        let width = Int(maskInfo.dataSize.width)
+        let height = Int(maskInfo.dataSize.height)
+        guard width > 0,
+              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
+    }
+
+    private func makeMaskBlurBuffer() -> MTLBuffer? {
+        let weight = simd_float3x3(
+            SIMD3<Float>(0.0625, 0.125, 0.0625),
+            SIMD3<Float>(0.125, 0.25, 0.125),
+            SIMD3<Float>(0.0625, 0.125, 0.0625)
+        )
+        let params = LNMaskParameters(weightMatrix: weight, coreSize: 3, texelOffset: 0.01)
+        let buffer = device.makeBuffer(bytes: [params], length: MemoryLayout<LNMaskParameters>.stride, options: .storageModeShared)
+        maskBlurBuffer = buffer
+        return buffer
+    }
+
+    private func makeTexture(pixelBuffer: CVPixelBuffer, plane: Int, format: MTLPixelFormat) -> MTLTexture? {
+        guard let textureCache else { return nil }
+        let width = CVPixelBufferGetWidthOfPlane(pixelBuffer, plane)
+        let height = CVPixelBufferGetHeightOfPlane(pixelBuffer, plane)
+        var cvTexture: CVMetalTexture?
+        let status = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
+                                                               textureCache,
+                                                               pixelBuffer,
+                                                               nil,
+                                                               format,
+                                                               width,
+                                                               height,
+                                                               plane,
+                                                               &cvTexture)
+        guard status == kCVReturnSuccess, let cvTexture else { return nil }
+        return CVMetalTextureGetTexture(cvTexture)
+    }
+
+    private func currentRenderPassDescriptor(texture: MTLTexture) -> MTLRenderPassDescriptor? {
+        let descriptor = MTLRenderPassDescriptor()
+        descriptor.colorAttachments[0].texture = texture
+        descriptor.colorAttachments[0].loadAction = .clear
+        descriptor.colorAttachments[0].storeAction = .store
+        descriptor.colorAttachments[0].clearColor = MTLClearColor(red: 0, green: 0, blue: 0, alpha: 0)
+        return descriptor
+    }
+
+    private func updateYuvMatrixIfNeeded(pixelBuffer: CVPixelBuffer) {
+        let matrixAttachment = CVBufferGetAttachment(pixelBuffer, kCVImageBufferYCbCrMatrixKey, nil)?.takeUnretainedValue()
+        let matrix = (matrixAttachment != nil && CFEqual(matrixAttachment, kCVImageBufferYCbCrMatrix_ITU_R_709_2)) ? Self.matrix709Full : Self.matrix601Full
+        guard matrix != currentColorMatrix else { return }
+        currentColorMatrix = matrix
+        updateYuvMatrixBuffer(matrix: matrix)
+    }
+
+    private func updateYuvMatrixBuffer(matrix: simd_float3x3) {
+        let params = LNColorParameters(matrix: matrix, offset: simd_float2(0.5, 0.5))
+        yuvMatrixBuffer = device.makeBuffer(bytes: [params], length: MemoryLayout<LNColorParameters>.stride, options: .storageModeShared)
+    }
+}
+
+private final class LNMetalUnavailableBridge: NSObject {
+    var callback: (() -> Void)?
+
+    @objc func onMetalViewUnavailable() {
+        callback?()
+    }
+
+    @objc func onViewUnavailableStatus() {
+        callback?()
+    }
+}
+
+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 {
+        didSet {
+            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:)
+    public init(metalLayer: AnyObject, blendMode: QGHWDTextureBlendMode) {
+        self.blendMode = blendMode
+        if #available(iOS 13.0, *), let metalLayer = metalLayer as? CAMetalLayer {
+            self.swiftRenderer = LNHWDMetalCoreRenderer(metalLayer: metalLayer, blendMode: blendMode)
+        } 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()
+    }
+
+    @objc(renderPixelBuffer:metalLayer:)
+    public func renderPixelBuffer(_ pixelBuffer: CVPixelBuffer?, metalLayer: AnyObject) {
+        if #available(iOS 13.0, *),
+           let pixelBuffer,
+           let metalLayer = metalLayer as? CAMetalLayer,
+           let swiftRenderer = swiftRenderer as? LNHWDMetalCoreRenderer {
+            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)
+    }
+
+    @objc(dispose)
+    public func dispose() {
+        if #available(iOS 13.0, *), let swiftRenderer = swiftRenderer as? LNHWDMetalCoreRenderer {
+            swiftRenderer.dispose()
+        }
+        if let legacy = legacyRenderer {
+            LNLegacyRuntime.callNoArgs(legacy, "dispose")
+        }
+    }
+}
+
+@objcMembers
+public final class LNHWDMetalView: UIView {
+    public weak var delegate: LNHWDMetalViewDelegate?
+    public weak var lnDelegate: LNHWDMetalViewDelegate? {
+        get { delegate }
+        set { delegate = newValue }
+    }
+    public var blendMode: QGHWDTextureBlendMode {
+        didSet {
+            fallbackRenderer.blendMode = blendMode
+        }
+    }
+
+    private let unavailableBridge = LNMetalUnavailableBridge()
+    private var renderLayer: CALayer = CALayer()
+    private lazy var fallbackRenderer = LNHWDMetalRenderer(metalLayer: renderLayer, blendMode: blendMode)
+
+    @objc(initWithFrame:blendMode:)
+    public init(frame: CGRect, blendMode: QGHWDTextureBlendMode) {
+        self.blendMode = blendMode
+        super.init(frame: frame)
+        setupRenderingLayer(frame: frame)
+    }
+
+    public required init?(coder: NSCoder) {
+        self.blendMode = .alphaLeft
+        super.init(coder: coder)
+        setupRenderingLayer(frame: bounds)
+    }
+
+    private func setupRenderingLayer(frame: CGRect) {
+        unavailableBridge.callback = { [weak self] in
+            self?.delegate?.onMetalViewUnavailable()
+        }
+
+        if #available(iOS 13.0, *) {
+            let metalLayer = CAMetalLayer()
+            metalLayer.pixelFormat = .bgra8Unorm
+            renderLayer = metalLayer
+        } else {
+            renderLayer = CALayer()
+        }
+
+        renderLayer.frame = frame
+        renderLayer.contentsScale = UIScreen.main.scale
+        renderLayer.contentsGravity = .resizeAspect
+        layer.addSublayer(renderLayer)
+    }
+
+    public override func layoutSubviews() {
+        super.layoutSubviews()
+        renderLayer.frame = bounds
+    }
+
+    @objc(display:)
+    public func display(_ pixelBuffer: CVPixelBuffer?) {
+        fallbackRenderer.renderPixelBuffer(pixelBuffer, metalLayer: renderLayer)
+    }
+
+    @objc(dispose)
+    public func dispose() {
+        renderLayer.contents = nil
+    }
+}
+
+@objcMembers
+public final class LNVAPMetalRenderer: NSObject {
+    public var commonInfo: LNVAPCommonInfo? {
+        didSet {
+            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 #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:)
+    public init(metalLayer: AnyObject) {
+        if #available(iOS 13.0, *), let metalLayer = metalLayer as? CAMetalLayer {
+            self.swiftRenderer = LNVAPMetalCoreRenderer(metalLayer: metalLayer)
+        } 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()
+    }
+
+    @objc(renderPixelBuffer:metalLayer:mergeInfos:)
+    public func renderPixelBuffer(_ pixelBuffer: CVPixelBuffer?, metalLayer: AnyObject, mergeInfos: [LNVAPMergedInfo]) {
+        if #available(iOS 13.0, *),
+           let pixelBuffer,
+           let metalLayer = metalLayer as? CAMetalLayer,
+           let swiftRenderer = swiftRenderer as? LNVAPMetalCoreRenderer {
+            swiftRenderer.render(pixelBuffer: pixelBuffer, metalLayer: metalLayer, mergeInfos: mergeInfos)
+            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)
+    }
+
+    @objc(dispose)
+    public func dispose() {
+        if #available(iOS 13.0, *), let swiftRenderer = swiftRenderer as? LNVAPMetalCoreRenderer {
+            swiftRenderer.dispose()
+        }
+        if let legacy = legacyRenderer {
+            LNLegacyRuntime.callNoArgs(legacy, "dispose")
+        }
+    }
+}
+
+@objcMembers
+public final class LNVAPMetalView: UIView {
+    public weak var delegate: LNVAPMetalViewDelegate?
+    public weak var lnDelegate: LNVAPMetalViewDelegate? {
+        get { delegate }
+        set { delegate = newValue }
+    }
+    public var commonInfo: LNVAPCommonInfo?
+    public var maskInfo: LNVAPMaskInfo?
+
+    private let unavailableBridge = LNMetalUnavailableBridge()
+    private var renderLayer: CALayer = CALayer()
+    private lazy var fallbackRenderer = LNVAPMetalRenderer(metalLayer: renderLayer)
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupRenderingLayer(frame: frame)
+    }
+
+    public required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupRenderingLayer(frame: bounds)
+    }
+
+    private func setupRenderingLayer(frame: CGRect) {
+        unavailableBridge.callback = { [weak self] in
+            self?.delegate?.onMetalViewUnavailable()
+        }
+
+        if #available(iOS 13.0, *) {
+            let metalLayer = CAMetalLayer()
+            metalLayer.pixelFormat = .bgra8Unorm
+            renderLayer = metalLayer
+        } else {
+            renderLayer = CALayer()
+        }
+
+        renderLayer.frame = frame
+        renderLayer.contentsScale = UIScreen.main.scale
+        renderLayer.contentsGravity = .resizeAspect
+        layer.addSublayer(renderLayer)
+    }
+
+    public override func layoutSubviews() {
+        super.layoutSubviews()
+        renderLayer.frame = bounds
+    }
+
+    @objc(display:mergeInfos:)
+    public func display(_ pixelBuffer: CVPixelBuffer?, mergeInfos: [LNVAPMergedInfo]) {
+        fallbackRenderer.commonInfo = commonInfo
+        fallbackRenderer.maskInfo = maskInfo
+        fallbackRenderer.renderPixelBuffer(pixelBuffer, metalLayer: renderLayer, mergeInfos: mergeInfos)
+    }
+
+    @objc(dispose)
+    public func dispose() {
+        renderLayer.contents = nil
+    }
+}
+
+@objcMembers
+public final class LNHWDMP4OpenGLView: UIView {
+    public weak var displayDelegate: LNHWDMP4OpenGLViewDelegate?
+    public var glContext: AnyObject?
+
+    public var blendMode: QGHWDTextureBlendMode = .alphaLeft {
+        didSet {
+            fallbackRenderer.blendMode = blendMode
+        }
+    }
+
+    public var pause: Bool = false
+    private var renderLayer: CALayer = CALayer()
+    private lazy var fallbackRenderer = LNHWDMetalRenderer(metalLayer: renderLayer, blendMode: blendMode)
+
+    public override init(frame: CGRect) {
+        super.init(frame: frame)
+        setupRenderingLayer(frame: frame)
+    }
+
+    public required init?(coder: NSCoder) {
+        super.init(coder: coder)
+        setupRenderingLayer(frame: bounds)
+    }
+
+    private func setupRenderingLayer(frame: CGRect) {
+        if #available(iOS 13.0, *) {
+            let metalLayer = CAMetalLayer()
+            metalLayer.pixelFormat = .bgra8Unorm
+            renderLayer = metalLayer
+        } else {
+            renderLayer = CALayer()
+        }
+
+        renderLayer.frame = frame
+        renderLayer.contentsScale = UIScreen.main.scale
+        renderLayer.contentsGravity = .resizeAspect
+        layer.addSublayer(renderLayer)
+    }
+
+    public override func layoutSubviews() {
+        super.layoutSubviews()
+        renderLayer.frame = bounds
+    }
+
+    @objc(setupGL)
+    public func setupGL() {
+        renderLayer.frame = bounds
+    }
+
+    @objc(displayPixelBuffer:)
+    public func displayPixelBuffer(_ pixelBuffer: CVPixelBuffer?) {
+        if window == nil {
+            displayDelegate?.onViewUnavailableStatus()
+            return
+        }
+        guard !pause, let pixelBuffer else { return }
+        fallbackRenderer.blendMode = blendMode
+        fallbackRenderer.renderPixelBuffer(pixelBuffer, metalLayer: renderLayer)
+    }
+
+    @objc(dispose)
+    public func dispose() {
+        fallbackRenderer.dispose()
+        renderLayer.contents = nil
+    }
+
+    @objc(updateBackingSize)
+    public func updateBackingSize() {
+        renderLayer.frame = bounds
+    }
+}

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

@@ -0,0 +1,681 @@
+import Foundation
+import UIKit
+import MetalKit
+import AVFoundation
+import ObjectiveC
+
+@objcMembers
+public final class LNVAPLogger: NSObject {
+    public typealias LNLogHandler = (_ level: Int, _ file: String, _ line: Int, _ function: String, _ module: String, _ message: String) -> Void
+
+    private static let lock = NSRecursiveLock()
+    private static var handler: LNLogHandler?
+
+    @objc(registerLogHandler:)
+    public static func registerLogHandler(_ handler: @escaping LNLogHandler) {
+        lock.lock()
+        self.handler = handler
+        lock.unlock()
+    }
+
+    @objc(clearLogHandler)
+    public static func clearLogHandler() {
+        lock.lock()
+        handler = nil
+        lock.unlock()
+    }
+
+    @objc(logLevel:file:line:func:module:message:)
+    public static func log(level: Int, file: String, line: Int, func function: String, module: String, message: String) {
+        let sanitized = message.replacingOccurrences(of: "%", with: "")
+
+        lock.lock()
+        let current = handler
+        lock.unlock()
+
+        if let current {
+            current(level, file, line, function, module, sanitized)
+            return
+        }
+
+#if DEBUG
+        let fileName = (file as NSString).lastPathComponent
+        NSLog("<\(level)> \(fileName)(\(line)):\(function) [\(module)] - \(sanitized)")
+#endif
+    }
+}
+
+@objcMembers
+public final class LNVAPSafeMutableArray: NSObject {
+    private let lock = NSRecursiveLock()
+    private var storage: NSMutableArray
+
+    public override init() {
+        self.storage = NSMutableArray()
+        super.init()
+    }
+
+    @objc(initWithCapacity:)
+    public init(capacity: Int) {
+        self.storage = NSMutableArray(capacity: capacity)
+        super.init()
+    }
+
+    @objc(initWithArray:)
+    public init(array: [Any]) {
+        self.storage = NSMutableArray(array: array)
+        super.init()
+    }
+
+    @objc(count)
+    public var count: Int {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.count
+    }
+
+    @objc(objectAtIndex:)
+    public func object(at index: Int) -> Any {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage[index]
+    }
+
+    @objc(firstObject)
+    public var firstObject: Any? {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.firstObject
+    }
+
+    @objc(lastObject)
+    public var lastObject: Any? {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.lastObject
+    }
+
+    @objc(addObject:)
+    public func add(_ object: Any) {
+        lock.lock()
+        storage.add(object)
+        lock.unlock()
+    }
+
+    @objc(insertObject:atIndex:)
+    public func insert(_ object: Any, at index: Int) {
+        lock.lock()
+        storage.insert(object, at: index)
+        lock.unlock()
+    }
+
+    @objc(removeObjectAtIndex:)
+    public func removeObject(at index: Int) {
+        lock.lock()
+        storage.removeObject(at: index)
+        lock.unlock()
+    }
+
+    @objc(removeLastObject)
+    public func removeLastObject() {
+        lock.lock()
+        storage.removeLastObject()
+        lock.unlock()
+    }
+
+    @objc(removeAllObjects)
+    public func removeAllObjects() {
+        lock.lock()
+        storage.removeAllObjects()
+        lock.unlock()
+    }
+
+    @objc(containsObject:)
+    public func contains(_ object: Any) -> Bool {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.contains(object)
+    }
+
+    @objc(allObjects)
+    public func allObjects() -> [Any] {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.copy() as? [Any] ?? []
+    }
+}
+
+@objcMembers
+public final class LNVAPSafeMutableDictionary: NSObject {
+    private let lock = NSRecursiveLock()
+    private var storage: NSMutableDictionary
+
+    public override init() {
+        self.storage = NSMutableDictionary()
+        super.init()
+    }
+
+    @objc(initWithCapacity:)
+    public init(capacity: Int) {
+        self.storage = NSMutableDictionary(capacity: capacity)
+        super.init()
+    }
+
+    @objc(initWithDictionary:)
+    public init(dictionary: [AnyHashable: Any]) {
+        self.storage = NSMutableDictionary(dictionary: dictionary)
+        super.init()
+    }
+
+    @objc(count)
+    public var count: Int {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.count
+    }
+
+    @objc(objectForKey:)
+    public func object(forKey key: NSCopying) -> Any? {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.object(forKey: key)
+    }
+
+    @objc(setObject:forKey:)
+    public func setObject(_ object: Any, forKey key: NSCopying) {
+        lock.lock()
+        storage.setObject(object, forKey: key)
+        lock.unlock()
+    }
+
+    @objc(removeObjectForKey:)
+    public func removeObject(forKey key: NSCopying) {
+        lock.lock()
+        storage.removeObject(forKey: key)
+        lock.unlock()
+    }
+
+    @objc(removeAllObjects)
+    public func removeAllObjects() {
+        lock.lock()
+        storage.removeAllObjects()
+        lock.unlock()
+    }
+
+    @objc(allKeys)
+    public var allKeys: [Any] {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.allKeys
+    }
+
+    @objc(allValues)
+    public var allValues: [Any] {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.allValues
+    }
+
+    @objc(dictionaryRepresentation)
+    public func dictionaryRepresentation() -> [AnyHashable: Any] {
+        lock.lock()
+        defer { lock.unlock() }
+        return storage.copy() as? [AnyHashable: Any] ?? [:]
+    }
+}
+
+@objcMembers
+public final class LNVAPWeakProxy: NSObject {
+    private weak var target: NSObjectProtocol?
+
+    @objc(initWithTarget:)
+    public init(target: NSObjectProtocol?) {
+        self.target = target
+        super.init()
+    }
+
+    @objc(proxyWithTarget:)
+    public static func proxy(with target: NSObjectProtocol?) -> LNVAPWeakProxy {
+        LNVAPWeakProxy(target: target)
+    }
+
+    public override func forwardingTarget(for aSelector: Selector!) -> Any? {
+        target
+    }
+
+    public override func responds(to aSelector: Selector!) -> Bool {
+        target?.responds(to: aSelector) ?? false
+    }
+}
+
+@objcMembers
+public final class LNVAPMetalShaderFunctionLoader: NSObject {
+    private let device: MTLDevice
+    private var defaultLibrary: MTLLibrary?
+
+    @objc(initWithDevice:)
+    public init(device: MTLDevice) {
+        self.device = device
+        super.init()
+    }
+
+    @objc(loadFunctionWithName:)
+    public func loadFunction(withName functionName: String) -> MTLFunction? {
+        if defaultLibrary == nil {
+            if let path = Bundle(for: Self.self).path(forResource: "default", ofType: "metallib") {
+                defaultLibrary = try? device.makeLibrary(filepath: path)
+            }
+            if defaultLibrary == nil {
+                defaultLibrary = device.makeDefaultLibrary()
+            }
+        }
+        return defaultLibrary?.makeFunction(name: functionName)
+    }
+}
+
+@objcMembers
+public final class LNVAPMetalUtil: NSObject {
+    public static let vapAttachmentVertexFunctionName = "vapAttachment_vertexShader"
+    public static let vapAttachmentFragmentFunctionName = "vapAttachment_FragmentShader"
+    public static let vapVertexFunctionName = "vap_vertexShader"
+    public static let vapYUVFragmentFunctionName = "vap_yuvFragmentShader"
+    public static let vapMaskFragmentFunctionName = "vap_maskFragmentShader"
+    public static let vapMaskBlurFragmentFunctionName = "vap_maskBlurFragmentShader"
+
+    public static let vapMTLVerticesIdentity: [Float] = [-1.0, -1.0, 0.0, 1.0, -1.0, 1.0, 0.0, 1.0, 1.0, -1.0, 0.0, 1.0, 1.0, 1.0, 0.0, 1.0]
+    public static let vapMTLTextureCoordinatesIdentity: [Float] = [0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0, 0.0]
+    public static let vapMTLTextureCoordinatesFor90: [Float] = [0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 1.0, 1.0]
+
+    @objc(genVerticesWithRect:containerSize:reverse:)
+    public static func genVertices(rect: CGRect, containerSize: CGSize, reverse: Bool) -> [Float] {
+        guard containerSize.width > 0, containerSize.height > 0 else { return vapMTLVerticesIdentity }
+
+        let originX = Float(-1 + 2 * rect.origin.x / containerSize.width)
+        let originY = Float(1 - 2 * rect.origin.y / containerSize.height)
+        let width = Float(2 * rect.size.width / containerSize.width)
+        let height = Float(2 * rect.size.height / containerSize.height)
+
+        if reverse {
+            return [originX, originY - height, 0.0, 1.0,
+                    originX, originY, 0.0, 1.0,
+                    originX + width, originY - height, 0.0, 1.0,
+                    originX + width, originY, 0.0, 1.0]
+        }
+
+        return [originX, originY, 0.0, 1.0,
+                originX, originY - height, 0.0, 1.0,
+                originX + width, originY, 0.0, 1.0,
+                originX + width, originY - height, 0.0, 1.0]
+    }
+
+    @objc(genTextureCoordinatesWithRect:containerSize:reverse:degree:)
+    public static func genTextureCoordinates(rect: CGRect, containerSize: CGSize, reverse: Bool, degree: Int) -> [Float] {
+        _ = degree
+        guard containerSize.width > 0, containerSize.height > 0 else { return vapMTLTextureCoordinatesIdentity }
+
+        let originX = Float(rect.origin.x / containerSize.width)
+        let originY = Float(rect.origin.y / containerSize.height)
+        let width = Float(rect.size.width / containerSize.width)
+        let height = Float(rect.size.height / containerSize.height)
+
+        if reverse {
+            return [originX, originY,
+                    originX, originY + height,
+                    originX + width, originY,
+                    originX + width, originY + height]
+        }
+
+        return [originX, originY + height,
+                originX, originY,
+                originX + width, originY + height,
+                originX + width, originY]
+    }
+
+    @objc(sourceSizeForCenterFullWithSourceSize:renderSize:)
+    public static func sourceSizeForCenterFull(sourceSize: CGSize, renderSize: CGSize) -> CGSize {
+        if sourceSize.width >= renderSize.width, sourceSize.height >= renderSize.height {
+            return sourceSize
+        }
+        return rect(inside: CGRect(origin: .zero, size: renderSize), aspectRatio: sourceSize, contentMode: .scaleAspectFill).size
+    }
+
+    @objc(rectForCenterFullWithSourceSize:renderSize:)
+    public static func rectForCenterFull(sourceSize: CGSize, renderSize: CGSize) -> CGRect {
+        if sourceSize.width >= renderSize.width, sourceSize.height >= renderSize.height {
+            return CGRect(x: (sourceSize.width - renderSize.width) / 2.0,
+                          y: (sourceSize.height - renderSize.height) / 2.0,
+                          width: renderSize.width,
+                          height: renderSize.height)
+        }
+
+        let fill = rect(inside: CGRect(origin: .zero, size: renderSize), aspectRatio: sourceSize, contentMode: .scaleAspectFill)
+        return CGRect(x: -fill.origin.x, y: -fill.origin.y, width: renderSize.width, height: renderSize.height)
+    }
+
+    @objc(rectInsideBoundingRect:aspectRatio:contentMode:)
+    public static func rect(inside boundingRect: CGRect, aspectRatio: CGSize, contentMode: UIView.ContentMode) -> CGRect {
+        guard aspectRatio.width > 0, aspectRatio.height > 0 else { return boundingRect }
+
+        switch contentMode {
+        case .scaleToFill:
+            return boundingRect
+        case .scaleAspectFit:
+            return AVMakeRect(aspectRatio: aspectRatio, insideRect: boundingRect)
+        case .scaleAspectFill:
+            let ratio = max(boundingRect.width / aspectRatio.width, boundingRect.height / aspectRatio.height)
+            let size = CGSize(width: aspectRatio.width * ratio, height: aspectRatio.height * ratio)
+            return CGRect(x: boundingRect.origin.x + (boundingRect.width - size.width) / 2.0,
+                          y: boundingRect.origin.y + (boundingRect.height - size.height) / 2.0,
+                          width: size.width,
+                          height: size.height)
+        case .center:
+            return CGRect(x: boundingRect.origin.x + (boundingRect.width - aspectRatio.width) / 2.0,
+                          y: boundingRect.origin.y + (boundingRect.height - aspectRatio.height) / 2.0,
+                          width: aspectRatio.width,
+                          height: aspectRatio.height)
+        case .top:
+            return CGRect(x: boundingRect.origin.x + (boundingRect.width - aspectRatio.width) / 2.0,
+                          y: boundingRect.origin.y,
+                          width: aspectRatio.width,
+                          height: aspectRatio.height)
+        case .bottom:
+            return CGRect(x: boundingRect.origin.x + (boundingRect.width - aspectRatio.width) / 2.0,
+                          y: boundingRect.maxY - aspectRatio.height,
+                          width: aspectRatio.width,
+                          height: aspectRatio.height)
+        case .left:
+            return CGRect(x: boundingRect.origin.x,
+                          y: boundingRect.origin.y + (boundingRect.height - aspectRatio.height) / 2.0,
+                          width: aspectRatio.width,
+                          height: aspectRatio.height)
+        case .right:
+            return CGRect(x: boundingRect.maxX - aspectRatio.width,
+                          y: boundingRect.origin.y + (boundingRect.height - aspectRatio.height) / 2.0,
+                          width: aspectRatio.width,
+                          height: aspectRatio.height)
+        case .topLeft:
+            return CGRect(origin: boundingRect.origin, size: aspectRatio)
+        case .topRight:
+            return CGRect(x: boundingRect.maxX - aspectRatio.width,
+                          y: boundingRect.origin.y,
+                          width: aspectRatio.width,
+                          height: aspectRatio.height)
+        case .bottomLeft:
+            return CGRect(x: boundingRect.origin.x,
+                          y: boundingRect.maxY - aspectRatio.height,
+                          width: aspectRatio.width,
+                          height: aspectRatio.height)
+        case .bottomRight:
+            return CGRect(x: boundingRect.maxX - aspectRatio.width,
+                          y: boundingRect.maxY - aspectRatio.height,
+                          width: aspectRatio.width,
+                          height: aspectRatio.height)
+        default:
+            return boundingRect
+        }
+    }
+}
+
+public extension UIView {
+    @objc func lnStopVAPIfNeeded() {
+        stopHWDMP4()
+    }
+}
+
+@objcMembers
+public final class LNVAPDeviceUtil: NSObject {
+    private static var cachedSystemVersion: Double = (UIDevice.current.systemVersion as NSString).doubleValue
+
+    public static var systemVersionNum: Double {
+        cachedSystemVersion
+    }
+
+    public static var defaultMTLResourceOption: MTLResourceOptions {
+        if #available(iOS 9.0, *) {
+            return .storageModeShared
+        }
+        return []
+    }
+}
+
+public extension NSArray {
+    @objc(ln_rectValue)
+    func ln_rectValue() -> CGRect {
+        guard count >= 4 else { return .zero }
+        for index in 0..<4 {
+            let value = self[index]
+            if !(value is NSNumber) && !(value is NSString) {
+                return .zero
+            }
+        }
+        return CGRect(x: (self[0] as? NSNumber)?.doubleValue ?? ((self[0] as? NSString)?.doubleValue ?? 0),
+                      y: (self[1] as? NSNumber)?.doubleValue ?? ((self[1] as? NSString)?.doubleValue ?? 0),
+                      width: (self[2] as? NSNumber)?.doubleValue ?? ((self[2] as? NSString)?.doubleValue ?? 0),
+                      height: (self[3] as? NSNumber)?.doubleValue ?? ((self[3] as? NSString)?.doubleValue ?? 0))
+    }
+}
+
+public extension NSDictionary {
+    @objc(ln_floatValueForKey:)
+    func ln_floatValue(forKey key: String?) -> CGFloat {
+        guard let key else { return 0 }
+        guard let value = self[key], !(value is NSNull) else { return 0 }
+        if let number = value as? NSNumber { return CGFloat(number.doubleValue) }
+        if let string = value as? NSString { return CGFloat(string.doubleValue) }
+        return 0
+    }
+
+    @objc(ln_integerValueForKey:)
+    func ln_integerValue(forKey key: String?) -> Int {
+        guard let key else { return 0 }
+        guard let value = self[key], !(value is NSNull) else { return 0 }
+        if let number = value as? NSNumber { return number.intValue }
+        if let string = value as? NSString { return string.integerValue }
+        return 0
+    }
+
+    @objc(ln_stringValueForKey:)
+    func ln_stringValue(forKey key: String?) -> String {
+        guard let key else { return "" }
+        guard let value = self[key], !(value is NSNull) else { return "" }
+        if let string = value as? String { return string }
+        if let number = value as? NSNumber { return number.description }
+        return ""
+    }
+
+    @objc(ln_dictionaryValueForKey:)
+    func ln_dictionaryValue(forKey key: String?) -> NSDictionary? {
+        guard let key else { return nil }
+        return self[key] as? NSDictionary
+    }
+
+    @objc(ln_arrayValueForKey:)
+    func ln_arrayValue(forKey key: String?) -> NSArray? {
+        guard let key else { return nil }
+        return self[key] as? NSArray
+    }
+}
+
+public extension UIColor {
+    @objc(ln_colorWithHexString:)
+    static func ln_color(hexString: String) -> UIColor? {
+        var normalized = hexString.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
+        if normalized.hasPrefix("#") {
+            normalized.removeFirst()
+        } else if normalized.hasPrefix("0X") {
+            normalized.removeFirst(2)
+        }
+
+        guard [3, 4, 6, 8].contains(normalized.count) else { return nil }
+
+        func hexValue(_ text: String) -> CGFloat? {
+            UInt64(text, radix: 16).map { CGFloat($0) / 255.0 }
+        }
+
+        let r: CGFloat
+        let g: CGFloat
+        let b: CGFloat
+        let a: CGFloat
+        switch normalized.count {
+        case 3, 4:
+            guard
+                let rr = hexValue(String(normalized[normalized.startIndex])),
+                let gg = hexValue(String(normalized[normalized.index(normalized.startIndex, offsetBy: 1)])),
+                let bb = hexValue(String(normalized[normalized.index(normalized.startIndex, offsetBy: 2)]))
+            else { return nil }
+            r = rr
+            g = gg
+            b = bb
+            if normalized.count == 4 {
+                let index = normalized.index(normalized.startIndex, offsetBy: 3)
+                guard let aa = hexValue(String(normalized[index])) else { return nil }
+                a = aa
+            } else {
+                a = 1
+            }
+        case 6, 8:
+            let start = normalized.startIndex
+            let r0 = normalized[start...normalized.index(start, offsetBy: 1)]
+            let g0 = normalized[normalized.index(start, offsetBy: 2)...normalized.index(start, offsetBy: 3)]
+            let b0 = normalized[normalized.index(start, offsetBy: 4)...normalized.index(start, offsetBy: 5)]
+            guard
+                let rr = hexValue(String(r0)),
+                let gg = hexValue(String(g0)),
+                let bb = hexValue(String(b0))
+            else { return nil }
+            r = rr
+            g = gg
+            b = bb
+            if normalized.count == 8 {
+                let a0 = normalized[normalized.index(start, offsetBy: 6)...normalized.index(start, offsetBy: 7)]
+                guard let aa = hexValue(String(a0)) else { return nil }
+                a = aa
+            } else {
+                a = 1
+            }
+        default:
+            return nil
+        }
+
+        return UIColor(red: r, green: g, blue: b, alpha: a)
+    }
+}
+
+private final class LNGestureBlockTarget: NSObject {
+    private let block: (Any) -> Void
+
+    init(block: @escaping (Any) -> Void) {
+        self.block = block
+    }
+
+    @objc func invoke(_ sender: Any) {
+        block(sender)
+    }
+}
+
+private var lnGestureTargetKey: UInt8 = 0
+
+public extension UIGestureRecognizer {
+    @objc(ln_initWithActionBlock:)
+    convenience init(lnActionBlock: @escaping (Any) -> Void) {
+        self.init()
+        ln_addActionBlock(lnActionBlock)
+    }
+
+    @objc(ln_addActionBlock:)
+    func ln_addActionBlock(_ block: @escaping (Any) -> Void) {
+        let target = LNGestureBlockTarget(block: block)
+        addTarget(target, action: #selector(LNGestureBlockTarget.invoke(_:)))
+        var targets = objc_getAssociatedObject(self, &lnGestureTargetKey) as? [LNGestureBlockTarget] ?? []
+        targets.append(target)
+        objc_setAssociatedObject(self, &lnGestureTargetKey, targets, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+    }
+
+    @objc(ln_removeAllActionBlocks)
+    func ln_removeAllActionBlocks() {
+        let targets = objc_getAssociatedObject(self, &lnGestureTargetKey) as? [LNGestureBlockTarget] ?? []
+        for target in targets {
+            removeTarget(target, action: #selector(LNGestureBlockTarget.invoke(_:)))
+        }
+        objc_setAssociatedObject(self, &lnGestureTargetKey, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+    }
+}
+
+private enum LNNotificationQueueStore {
+    static let lock = NSRecursiveLock()
+    static var store: [String: OperationQueue] = [:]
+}
+
+public extension Notification.Name {
+    var ln_notificationOperationQueue: OperationQueue {
+        get {
+            LNNotificationQueueStore.lock.lock()
+            defer { LNNotificationQueueStore.lock.unlock() }
+            if let queue = LNNotificationQueueStore.store[rawValue] {
+                return queue
+            }
+            let queue = OperationQueue()
+            queue.maxConcurrentOperationCount = 1
+            LNNotificationQueueStore.store[rawValue] = queue
+            return queue
+        }
+        nonmutating set {
+            LNNotificationQueueStore.lock.lock()
+            LNNotificationQueueStore.store[rawValue] = newValue
+            LNNotificationQueueStore.lock.unlock()
+        }
+    }
+}
+
+private final class LNWeakNotificationBox: NSObject {
+    weak var observer: NSObjectProtocol?
+    weak var center: NotificationCenter?
+    var token: NSObjectProtocol?
+
+    init(observer: NSObjectProtocol, center: NotificationCenter) {
+        self.observer = observer
+        self.center = center
+    }
+}
+
+public extension NotificationCenter {
+    @objc(ln_addSafeObserver:selector:name:object:)
+    func ln_addSafeObserver(_ observer: NSObject, selector: Selector, name: Notification.Name?, object: Any?) {
+        if #available(iOS 9.0, *) {
+            addObserver(observer, selector: selector, name: name, object: object)
+            return
+        }
+        let queue = name?.ln_notificationOperationQueue
+        let box = LNWeakNotificationBox(observer: observer, center: self)
+        box.token = addObserver(forName: name, object: object, queue: queue) { [weak box] note in
+            guard let box, let target = box.observer else {
+                if let token = box?.token {
+                    box?.center?.removeObserver(token)
+                }
+                return
+            }
+            _ = (target as AnyObject).perform(selector, with: note)
+        }
+    }
+
+    @objc(ln_addSafeObserver:selector:name:object:queue:)
+    func ln_addSafeObserver(_ observer: NSObject, selector: Selector, name: Notification.Name?, object: Any?, queue: OperationQueue) {
+        if let name {
+            name.ln_notificationOperationQueue = queue
+        }
+        ln_addSafeObserver(observer, selector: selector, name: name, object: object)
+    }
+
+    func ln_addWeakObserver(_ weakObserver: NSObject, name: Notification.Name?, using block: @escaping (Notification, NSObject) -> Void) {
+        let box = LNWeakNotificationBox(observer: weakObserver, center: self)
+        box.token = addObserver(forName: name, object: nil, queue: nil) { [weak box] note in
+            guard let box, let target = box.observer as? NSObject else {
+                if let token = box?.token {
+                    box?.center?.removeObserver(token)
+                }
+                return
+            }
+            block(note, target)
+        }
+    }
+}

+ 394 - 0
QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPPlayerView.swift

@@ -0,0 +1,394 @@
+import UIKit
+import CoreVideo
+
+@objcMembers
+public final class LNVAPPlayerView: UIView {
+    private let playbackView = UIView()
+    private let delegateBridge = LNPlayerDelegateBridge()
+    private var didStart = false
+
+    public weak var delegate: LNVAPPlaybackDelegate?
+
+    public var enterBackgroundOperation: LNEnterBackgroundOperation = .stop {
+        didSet { LNPlaybackRuntime.set(playbackView, key: "hwd_enterBackgroundOP", value: enterBackgroundOperation.legacyRawValue) }
+    }
+
+    public var renderByOpenGL: Bool {
+        get { LNPlaybackRuntime.boolValue(playbackView, key: "hwd_renderByOpenGL") }
+        set { LNPlaybackRuntime.set(playbackView, key: "hwd_renderByOpenGL", value: newValue) }
+    }
+
+    public var fps: Int {
+        get { LNPlaybackRuntime.intValue(playbackView, key: "hwd_fps") }
+        set { LNPlaybackRuntime.set(playbackView, key: "hwd_fps", value: 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()
+    }
+
+    public override func layoutSubviews() {
+        super.layoutSubviews()
+        playbackView.frame = bounds
+    }
+
+    @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)
+        }
+    }
+
+    @objc(lnPlayWithFilePath:)
+    public func lnPlay(filePath: String) {
+        lnPlay(filePath: filePath, repeatCount: 0)
+    }
+
+    @objc(lnStop)
+    public func lnStop() {
+        LNPlaybackRuntime.callNoArg(playbackView, selectorName: "stopHWDMP4")
+        notifyStopIfNeeded()
+    }
+
+    @objc(lnPause)
+    public func lnPause() {
+        LNPlaybackRuntime.callNoArg(playbackView, selectorName: "pauseHWDMP4")
+    }
+
+    @objc(lnResume)
+    public func lnResume() {
+        LNPlaybackRuntime.callNoArg(playbackView, selectorName: "resumeHWDMP4")
+    }
+
+    @objc(lnSetMute:)
+    public func lnSetMute(_ mute: Bool) {
+        LNPlaybackRuntime.callBool(playbackView, selectorName: "setMute:", value: mute)
+    }
+
+    @objc(lnEnableOldVersion:)
+    public func lnEnableOldVersion(_ enable: Bool) {
+        LNPlaybackRuntime.callBool(playbackView, selectorName: "enableOldVersion:", value: enable)
+    }
+
+    @objc(lnAddTapGestureWithTarget:action:)
+    public func lnAddTapGesture(target: Any, action: Selector) {
+        let tap = UITapGestureRecognizer(target: target, action: action)
+        playbackView.addGestureRecognizer(tap)
+        playbackView.isUserInteractionEnabled = true
+    }
+
+    @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
+        }
+    }
+
+    @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 }
+                callback(gesture, false, nil)
+            }
+            playbackView.addGestureRecognizer(gestureRecognizer)
+            playbackView.isUserInteractionEnabled = true
+        }
+    }
+
+    fileprivate func notifyShouldStart(_ config: LNVAPConfigModel) -> Bool {
+        delegate?.lnPlayerShouldStart?(self, config: config) ?? true
+    }
+
+    fileprivate func notifyStart() {
+        didStart = true
+        delegate?.lnPlayerDidStart?(self)
+    }
+
+    fileprivate func notifyPlay(_ frame: LNMP4AnimatedImageFrame) {
+        delegate?.lnPlayerDidPlay?(self, frame: frame)
+    }
+
+    fileprivate func notifyFinish(_ totalFrameCount: Int) {
+        delegate?.lnPlayerDidFinish?(self, totalFrameCount: totalFrameCount)
+    }
+
+    fileprivate func notifyFail(_ error: NSError) {
+        delegate?.lnPlayerDidFail?(self, error: error)
+    }
+
+    fileprivate func contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String? {
+        delegate?.lnPlayerContent?(forTag: tag, resource: resource)
+    }
+
+    fileprivate func loadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion) {
+        if let handler = delegate?.lnPlayerLoadImage {
+            handler(url, context, completion)
+            return
+        }
+        completion(nil, nil, url)
+    }
+
+    fileprivate func notifyStopIfNeeded() {
+        if didStart {
+            delegate?.lnPlayerDidStop?(self)
+        }
+        didStart = false
+    }
+
+    private func setupPlaybackView() {
+        playbackView.frame = bounds
+        playbackView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
+        addSubview(playbackView)
+        LNPlaybackRuntime.set(playbackView, key: "hwd_enterBackgroundOP", value: enterBackgroundOperation.legacyRawValue)
+    }
+}
+
+private final class LNPlayerDelegateBridge: NSObject {
+    weak var owner: LNVAPPlayerView?
+
+    @objc(shouldStartPlayMP4:config:)
+    func shouldStartPlayMP4(_ container: UIView, config: NSObject) -> Bool {
+        guard let owner else { return true }
+        return owner.notifyShouldStart(LNPlaybackLegacyMapper.toConfig(config))
+    }
+
+    @objc(viewDidStartPlayMP4:)
+    func viewDidStartPlayMP4(_ container: UIView) {
+        owner?.notifyStart()
+    }
+
+    @objc(viewDidPlayMP4AtFrame:view:)
+    func viewDidPlayMP4AtFrame(_ frame: NSObject, view container: UIView) {
+        owner?.notifyPlay(LNPlaybackLegacyMapper.toFrame(frame))
+    }
+
+    @objc(viewDidStopPlayMP4:view:)
+    func viewDidStopPlayMP4(_ lastFrameIndex: Int, view container: UIView) {
+        owner?.notifyStopIfNeeded()
+    }
+
+    @objc(viewDidFinishPlayMP4:view:)
+    func viewDidFinishPlayMP4(_ totalFrameCount: Int, view container: UIView) {
+        owner?.notifyFinish(totalFrameCount)
+    }
+
+    @objc(viewDidFailPlayMP4:)
+    func viewDidFailPlayMP4(_ error: NSError) {
+        owner?.notifyFail(error)
+    }
+
+    @objc(contentForVapTag:resource:)
+    func contentForVapTag(_ tag: String, resource: NSObject) -> String? {
+        guard let owner else { return nil }
+        return owner.contentForTag(tag, resource: LNPlaybackLegacyMapper.toSource(resource))
+    }
+
+    @objc(loadVapImageWithURL:context:completion:)
+    func loadVapImageWithURL(_ url: String, context: NSDictionary, completion: @escaping (UIImage?, NSError?, String) -> Void) {
+        owner?.loadImage(withURL: url, context: context, completion: completion)
+    }
+}
+
+private enum LNPlaybackLegacyMapper {
+    static func toConfig(_ object: NSObject) -> LNVAPConfigModel {
+        if let config = object as? LNVAPConfigModel {
+            return config
+        }
+        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)
+        }
+        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
+    }
+
+    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 static func cgRectValue(_ object: NSObject, key: String) -> CGRect {
+        (object.value(forKey: key) as? NSValue)?.cgRectValue ?? .zero
+    }
+}
+
+private enum LNPlaybackRuntime {
+    private typealias LNObjCGestureBlock = @convention(block) (UIGestureRecognizer, Bool, AnyObject?) -> Void
+
+    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)
+    }
+
+    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)
+    }
+
+    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
+    }
+
+    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))
+        }
+        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, LNPlaybackLegacyMapper.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
+    }
+
+    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)
+    }
+
+    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
+    }
+
+    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
+    }
+
+    private static func setterSelectorName(for key: String) -> String {
+        guard let first = key.first else { return "" }
+        return "set\(String(first).uppercased())\(key.dropFirst()):"
+    }
+}

+ 386 - 0
QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPWrapView.swift

@@ -0,0 +1,386 @@
+import UIKit
+import CoreVideo
+
+@objcMembers
+public final class LNVAPWrapView: UIView {
+    private let playbackView: UIView
+    private let delegateBridge: LNWrapDelegateBridge
+    private var didStart = false
+
+    public weak var delegate: LNVAPWrapPlaybackDelegate?
+
+    public var contentModeOption: LNVAPWrapContentMode = .scaleToFill {
+        didSet { LNWrapRuntime.set(playbackView, key: "contentMode", value: contentModeOption.legacyRawValue) }
+    }
+
+    public var autoDestroyAfterFinish: Bool {
+        get { LNWrapRuntime.boolValue(playbackView, key: "autoDestoryAfterFinish") }
+        set { LNWrapRuntime.set(playbackView, key: "autoDestoryAfterFinish", value: 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
+    }
+
+    @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)
+        }
+    }
+
+    @objc(lnPlayWithFilePath:)
+    public func lnPlay(filePath: String) {
+        lnPlay(filePath: filePath, repeatCount: 0)
+    }
+
+    @objc(lnStop)
+    public func lnStop() {
+        LNWrapRuntime.callNoArg(playbackView, selectorName: "stopHWDMP4")
+        notifyStopIfNeeded()
+    }
+
+    @objc(lnPause)
+    public func lnPause() {
+        LNWrapRuntime.callNoArg(playbackView, selectorName: "pauseHWDMP4")
+    }
+
+    @objc(lnResume)
+    public func lnResume() {
+        LNWrapRuntime.callNoArg(playbackView, selectorName: "resumeHWDMP4")
+    }
+
+    @objc(lnSetMute:)
+    public func lnSetMute(_ mute: Bool) {
+        LNWrapRuntime.callBool(playbackView, selectorName: "setMute:", value: 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
+        }
+    }
+
+    @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)
+        }
+        if !added {
+            gestureRecognizer.ln_addActionBlock { sender in
+                guard let gesture = sender as? UIGestureRecognizer else { return }
+                callback(gesture, false, nil)
+            }
+            playbackView.addGestureRecognizer(gestureRecognizer)
+            playbackView.isUserInteractionEnabled = true
+        }
+    }
+
+    fileprivate func shouldStart(with config: LNVAPConfigModel) -> Bool {
+        delegate?.lnWrapViewShouldStart?(self, config: config) ?? true
+    }
+
+    fileprivate func notifyStart() {
+        didStart = true
+        delegate?.lnWrapViewDidStart?(self)
+    }
+
+    fileprivate func notifyPlay(_ frame: LNMP4AnimatedImageFrame) {
+        delegate?.lnWrapViewDidPlay?(self, frame: frame)
+    }
+
+    fileprivate func notifyFinish(_ totalFrameCount: Int) {
+        delegate?.lnWrapViewDidFinish?(self, totalFrameCount: totalFrameCount)
+    }
+
+    fileprivate func notifyStopIfNeeded() {
+        if didStart {
+            delegate?.lnWrapViewDidStop?(self)
+        }
+        didStart = false
+    }
+
+    fileprivate func notifyFail(_ error: NSError) {
+        delegate?.lnWrapViewDidFail?(self, error: error)
+        didStart = false
+    }
+
+    fileprivate func contentForTag(_ tag: String, resource: LNVAPSourceInfo) -> String? {
+        delegate?.lnWrapViewContent?(forTag: tag, resource: resource)
+    }
+
+    fileprivate func loadImage(withURL url: String, context: NSDictionary, completion: @escaping LNVAPImageCompletion) {
+        if let handler = delegate?.lnWrapViewLoadImage {
+            handler(url, context, 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
+        }
+        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)
+        }
+        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
+    }
+
+    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 static func cgRectValue(_ object: NSObject, key: String) -> CGRect {
+        (object.value(forKey: key) as? NSValue)?.cgRectValue ?? .zero
+    }
+}
+
+private enum LNWrapRuntime {
+    private typealias LNObjCGestureBlock = @convention(block) (UIGestureRecognizer, Bool, AnyObject?) -> Void
+
+    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)
+    }
+
+    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)
+    }
+
+    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)
+    }
+
+    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
+    }
+
+    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
+    }
+
+    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)
+    }
+
+    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
+    }
+
+    private static func setterSelectorName(for key: String) -> String {
+        guard let first = key.first else { return "" }
+        return "set\(String(first).uppercased())\(key.dropFirst()):"
+    }
+}

+ 45 - 196
QGVAPlayerDemo/QGVAPlayerDemo/ViewController.m

@@ -1,239 +1,88 @@
 // ViewController.m
 // Tencent is pleased to support the open source community by making vap available.
-//
-// Copyright (C) 2020 Tencent.  All rights reserved.
-//
-// Licensed under the MIT License (the "License"); you may not use this file except in
-// compliance with the License. You may obtain a copy of the License at
-//
-// http://opensource.org/licenses/MIT
-//
-// Unless required by applicable law or agreed to in writing, software distributed under the License is
-// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
-// either express or implied. See the License for the specific language governing permissions and
-// limitations under the License.
 
 #import "ViewController.h"
-#import "UIView+VAP.h"
-#import "QGVAPWrapView.h"
-
+#import <QGVAPlayer/QGVAPlayer.h>
+#import <QGVAPlayer/QGVAPlayer-Swift.h>
 #import <AVFoundation/AVFoundation.h>
 
-@interface ViewController () <HWDMP4PlayDelegate, VAPWrapViewDelegate>
+@interface ViewController () <LNVAPPlaybackDelegate, LNVAPWrapPlaybackDelegate>
 
 @property (nonatomic, strong) UIButton *vapButton;
-@property (nonatomic, strong) UIButton *vapxButton;
-@property (nonatomic, strong) UIButton *vapWrapViewButton;
-
-@property (nonatomic, strong) VAPView *vapView;
+@property (nonatomic, strong) UIButton *vapWrapButton;
+@property (nonatomic, strong) LNVAPPlayerView *playerView;
+@property (nonatomic, strong) LNVAPWrapView *wrapView;
 
 @end
 
 @implementation ViewController
 
-//日志接口
-void qg_VAP_Logger_handler(VAPLogLevel level, const char* file, int line, const char* func, NSString *module, NSString *format, ...) {
-    
-    if (format.UTF8String == nil) {
-        NSLog(@"log包含非utf-8字符");
-        return;
-    }
-    if (level > VAPLogLevelDebug) {
-        va_list argList;
-        va_start(argList, format);
-        NSString* message = [[NSString alloc] initWithFormat:format arguments:argList];
-        file = [NSString stringWithUTF8String:file].lastPathComponent.UTF8String;
-        NSLog(@"<%@> %s(%@):%s [%@] - %@",@(level), file, @(line), func, module, message);
-        va_end(argList);
-    }
-}
-
 - (void)viewDidLoad {
     [super viewDidLoad];
     [self setupAudioSession];
-    
-    //日志
-    [UIView registerHWDLog:qg_VAP_Logger_handler];
-    
-    //vap-经典效果
+
     _vapButton = [[UIButton alloc] initWithFrame:CGRectMake(0, 100, CGRectGetWidth(self.view.frame), 90)];
     _vapButton.backgroundColor = [UIColor lightGrayColor];
-    [_vapButton setTitle:@"电竞方案(退后台结束)" forState:UIControlStateNormal];
+    [_vapButton setTitle:@"LN PlayerView" forState:UIControlStateNormal];
     [_vapButton addTarget:self action:@selector(playVap) forControlEvents:UIControlEventTouchUpInside];
     [self.view addSubview:_vapButton];
-    
-    //vapx-融合效果
-    _vapxButton = [[UIButton alloc] initWithFrame:CGRectMake(0, CGRectGetMaxY(_vapButton.frame)+60, CGRectGetWidth(self.view.frame), 90)];
-    _vapxButton.backgroundColor = [UIColor lightGrayColor];
-    [_vapxButton setTitle:@"融合特效(退后台暂停/恢复)" forState:UIControlStateNormal];
-    [_vapxButton addTarget:self action:@selector(playVapx) forControlEvents:UIControlEventTouchUpInside];
-    [self.view addSubview:_vapxButton];
-    
-    //使用WrapView,支持ContentMode
-    _vapWrapViewButton = [[UIButton alloc] initWithFrame:CGRectMake(0, CGRectGetMaxY(_vapxButton.frame)+60, CGRectGetWidth(self.view.frame), 90)];
-    _vapWrapViewButton.backgroundColor = [UIColor lightGrayColor];
-    [_vapWrapViewButton setTitle:@"WrapView-ContentMode" forState:UIControlStateNormal];
-    [_vapWrapViewButton addTarget:self action:@selector(playVapWithWrapView) forControlEvents:UIControlEventTouchUpInside];
-    [self.view addSubview:_vapWrapViewButton];
+
+    _vapWrapButton = [[UIButton alloc] initWithFrame:CGRectMake(0, CGRectGetMaxY(_vapButton.frame) + 60, CGRectGetWidth(self.view.frame), 90)];
+    _vapWrapButton.backgroundColor = [UIColor lightGrayColor];
+    [_vapWrapButton setTitle:@"LN WrapView" forState:UIControlStateNormal];
+    [_vapWrapButton addTarget:self action:@selector(playWithWrapView) forControlEvents:UIControlEventTouchUpInside];
+    [self.view addSubview:_vapWrapButton];
 }
 
 - (void)setupAudioSession {
-    AVAudioSession* avsession = [AVAudioSession sharedInstance];
+    AVAudioSession *session = [AVAudioSession sharedInstance];
     NSError *error = nil;
-    if (![avsession setCategory:AVAudioSessionCategoryPlayback withOptions:0 error:&error]) {
-        if (error) NSLog(@"AVAudioSession setCategory failed : %ld, %s", (long)error.code, [error.localizedDescription UTF8String]);
-        return;
-    }
-    if (![avsession setActive:YES error:&error]) {
-        if (error) NSLog(@"AVAudioSession setActive failed : %ld, %s", (long)error.code, [error.localizedDescription UTF8String]);
-    }
+    [session setCategory:AVAudioSessionCategoryPlayback withOptions:0 error:&error];
+    [session setActive:YES error:&error];
 }
 
-#pragma mark - 各种类型的播放
-
 - (void)playVap {
-    VAPView *mp4View = [[VAPView alloc] initWithFrame:CGRectMake(0, 0, 752/2, 752/2)];
-    //默认使用metal渲染,使用OpenGL请打开下面这个开关
-//    mp4View.hwd_renderByOpenGL = YES;
-    mp4View.center = self.view.center;
-    [self.view addSubview:mp4View];
-    mp4View.userInteractionEnabled = YES;
-    mp4View.hwd_enterBackgroundOP = HWDMP4EBOperationTypeStop;
-    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onImageviewTap:)];
-    [mp4View addGestureRecognizer:tap];
-    NSString *resPath = [NSString stringWithFormat:@"%@/Resource/demo.mp4", [[NSBundle mainBundle] resourcePath]];
-    //单纯播放的接口
-    //[mp4View playHWDMp4:resPath];
-    //指定素材混合模式,重复播放次数,delegate的接口
-    
-    //注意若素材不含vapc box,则必须用调用如下接口设置enable才可播放
-    [mp4View enableOldVersion:YES];
-    [mp4View playHWDMP4:resPath repeatCount:-1 delegate:self];
-}
-
-//vap动画
-- (void)playVapx {
-    NSString *mp4Path = [NSString stringWithFormat:@"%@/Resource/vap.mp4", [[NSBundle mainBundle] resourcePath]];
-    VAPView *mp4View = [[VAPView alloc] initWithFrame:self.view.bounds];
-    [self.view addSubview:mp4View];
-    mp4View.center = self.view.center;
-    mp4View.userInteractionEnabled = YES;
-    mp4View.hwd_enterBackgroundOP = HWDMP4EBOperationTypePauseAndResume; // ⚠️ 建议设置该选项时对机型进行判断,屏蔽低端机
-    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(onImageviewTap:)];
-    [mp4View addGestureRecognizer:tap];
-    [mp4View setMute:YES];
-    [mp4View playHWDMP4:mp4Path repeatCount:-1 delegate:self];
-    
-    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
-        [mp4View pauseHWDMP4];
-        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
-            [mp4View resumeHWDMP4];
-        });
-    });
-}
-
-/// 使用WrapView,支持ContentMode
-- (void)playVapWithWrapView {
-    static BOOL pause = NO;
-    QGVAPWrapView *wrapView = [[QGVAPWrapView alloc] initWithFrame:self.view.bounds];
-    wrapView.center = self.view.center;
-    wrapView.contentMode = QGVAPWrapViewContentModeAspectFit;
-    wrapView.autoDestoryAfterFinish = YES;
-    [self.view addSubview:wrapView];
-    NSString *resPath = [NSString stringWithFormat:@"%@/Resource/vap.mp4", [[NSBundle mainBundle] resourcePath]];
-    [wrapView setMute:YES];
-    [wrapView playHWDMP4:resPath repeatCount:-1 delegate:self];
-    UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(doNothingonImageviewTap:)];
-    
-    __weak __typeof(wrapView) weakWrapView = wrapView;
-    [wrapView addVapGesture:tap callback:^(UIGestureRecognizer *gestureRecognizer, BOOL insideSource, QGVAPSourceDisplayItem *source) {
-        if ((pause = !pause)) {
-            [weakWrapView pauseHWDMP4];
-        } else {
-            [weakWrapView resumeHWDMP4];
-        }
-    }];
-}
-
-#pragma mark -  mp4 hwd delegate
-
-#pragma mark -- 播放流程
-- (void)viewDidStartPlayMP4:(VAPView *)container {
-    
-}
+    [self.playerView removeFromSuperview];
+    self.playerView = [LNVAPFacade lnMakePlayerViewWithFrame:CGRectMake(0, 0, 752 / 2.0, 752 / 2.0)];
+    self.playerView.center = self.view.center;
+    self.playerView.delegate = self;
+    self.playerView.enterBackgroundOperation = LNEnterBackgroundOperationStop;
+    [self.playerView lnEnableOldVersion:YES];
+    [self.playerView lnSetMute:YES];
+    [self.playerView lnAddTapGestureWithTarget:self action:@selector(onPlayerTap:)];
+    [self.view addSubview:self.playerView];
 
-- (void)viewDidFinishPlayMP4:(NSInteger)totalFrameCount view:(UIView *)container {
-    //note:在子线程被调用
+    NSString *resPath = [NSString stringWithFormat:@"%@/Resource/demo.mp4", NSBundle.mainBundle.resourcePath];
+    [self.playerView lnPlayWithFilePath:resPath repeatCount:-1];
 }
 
-- (void)viewDidPlayMP4AtFrame:(QGMP4AnimatedImageFrame *)frame view:(UIView *)container {
-    //note:在子线程被调用
-}
-
-- (void)viewDidStopPlayMP4:(NSInteger)lastFrameIndex view:(UIView *)container {
-    //note:在子线程被调用
-    dispatch_async(dispatch_get_main_queue(), ^{
-        [container removeFromSuperview];
-    });
-}
-
-- (BOOL)shouldStartPlayMP4:(VAPView *)container config:(QGVAPConfigModel *)config {
-    return YES;
-}
+- (void)playWithWrapView {
+    [self.wrapView removeFromSuperview];
+    self.wrapView = [LNVAPFacade lnMakeWrapViewWithFrame:self.view.bounds];
+    self.wrapView.contentModeOption = LNVAPWrapContentModeAspectFit;
+    self.wrapView.autoDestroyAfterFinish = YES;
+    self.wrapView.delegate = self;
+    [self.wrapView lnSetMute:YES];
+    [self.view addSubview:self.wrapView];
 
-- (void)viewDidFailPlayMP4:(NSError *)error {
-    NSLog(@"%@", error.userInfo);
+    NSString *resPath = [NSString stringWithFormat:@"%@/Resource/vap.mp4", NSBundle.mainBundle.resourcePath];
+    [self.wrapView lnPlayWithFilePath:resPath repeatCount:-1];
 }
 
-#pragma mark -- 融合特效的接口 vapx
-
-//provide the content for tags, maybe text or url string ...
-- (NSString *)contentForVapTag:(NSString *)tag resource:(QGVAPSourceInfo *)info {
-    
-    NSDictionary *extraInfo = @{@"[sImg1]" : @"http://shp.qlogo.cn/pghead/Q3auHgzwzM6GuU0Y6q6sKHzq3MjY1aGibIzR4xrJc1VY/60",
-                                @"[textAnchor]" : @"我是主播名",
-                                @"[textUser]" : @"我是用户名😂😂",};
-    return extraInfo[tag];
+- (void)onPlayerTap:(UIGestureRecognizer *)gesture {
+    [self.playerView lnStop];
+    [gesture.view removeFromSuperview];
 }
 
-//provide image for url from tag content
-- (void)loadVapImageWithURL:(NSString *)urlStr context:(NSDictionary *)context completion:(VAPImageCompletionBlock)completionBlock {
-    
-    //call completionBlock as you get the image, both sync or asyn are ok.
-    //usually we'd like to make a net request
+- (void)lnPlayerDidStop:(LNVAPPlayerView *)playerView {
     dispatch_async(dispatch_get_main_queue(), ^{
-        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"%@/Resource/qq.png", [[NSBundle mainBundle] resourcePath]]];
-        //let's say we've got result here
-        completionBlock(image, nil, urlStr);
+        [playerView removeFromSuperview];
     });
 }
 
-#pragma mark - gesture
-
-- (void)onImageviewTap:(UIGestureRecognizer *)ges {
-    
-    [ges.view removeFromSuperview];
-}
-
-- (void)doNothingonImageviewTap:(UIGestureRecognizer *)ges {
-    
-}
-
-#pragma mark - WrapViewDelegate
-
-//provide the content for tags, maybe text or url string ...
-- (NSString *)vapWrapview_contentForVapTag:(NSString *)tag resource:(QGVAPSourceInfo *)info {
-    NSDictionary *extraInfo = @{@"[sImg1]" : @"http://shp.qlogo.cn/pghead/Q3auHgzwzM6GuU0Y6q6sKHzq3MjY1aGibIzR4xrJc1VY/60",
-                                @"[textAnchor]" : @"我是主播名",
-                                @"[textUser]" : @"我是用户名😂😂",};
-    return extraInfo[tag];
-}
-
-//provide image for url from tag content
-- (void)vapWrapView_loadVapImageWithURL:(NSString *)urlStr context:(NSDictionary *)context completion:(VAPImageCompletionBlock)completionBlock {
+- (void)lnWrapViewDidStop:(LNVAPWrapView *)wrapView {
     dispatch_async(dispatch_get_main_queue(), ^{
-        UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"%@/Resource/qq.png", [[NSBundle mainBundle] resourcePath]]];
-        completionBlock(image, nil, urlStr);
+        [wrapView removeFromSuperview];
     });
 }
 

+ 2 - 19
QGVAPlayerDemoSwift/QGVAPlayerDemoSwift/QGVAPlayer-Bridging-Header.h

@@ -1,23 +1,6 @@
-// QGVAPlayer-Bridging-Header.h
-// Tencent is pleased to support the open source community by making vap available.
-//
-// Copyright (C) 2020 Tencent.  All rights reserved.
-//
-// Licensed under the MIT License (the "License"); you may not use this file except in
-// compliance with the License. You may obtain a copy of the License at
-//
-// http://opensource.org/licenses/MIT
-//
-// Unless required by applicable law or agreed to in writing, software distributed under the License is
-// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
-// either express or implied. See the License for the specific language governing permissions and
-// limitations under the License.
-
 #ifndef QGVAPlayer_Bridging_Header_h
 #define QGVAPlayer_Bridging_Header_h
 
-#import "UIView+VAP.h"
-#import "QGVAPConfigModel.h"
-#import "QGVAPWrapView.h"
+// Intentionally left blank. Demo now uses Swift module import: `import QGVAPlayer`.
 
-#endif /* QGVAPlayer_Bridging_Header_h */
+#endif

+ 31 - 51
QGVAPlayerDemoSwift/QGVAPlayerDemoSwift/ViewController.swift

@@ -1,67 +1,47 @@
 // ViewController.swift
 // Tencent is pleased to support the open source community by making vap available.
-//
-// Copyright (C) 2020 Tencent.  All rights reserved.
-//
-// Licensed under the MIT License (the "License"); you may not use this file except in
-// compliance with the License. You may obtain a copy of the License at
-//
-// http://opensource.org/licenses/MIT
-//
-// Unless required by applicable law or agreed to in writing, software distributed under the License is
-// distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
-// either express or implied. See the License for the specific language governing permissions and
-// limitations under the License.
 
 import UIKit
+import QGVAPlayer
 
-class ViewController: UIViewController, HWDMP4PlayDelegate {
-    
-    let vapButton = UIButton()
+final class ViewController: UIViewController, LNVAPPlaybackDelegate {
+    private let vapButton = UIButton()
+    private var playerView: LNVAPPlayerView?
 
     override func viewDidLoad() {
         super.viewDidLoad()
-        // Do any additional setup after loading the view.
-        vapButton.frame = CGRect(x: 0, y: 100, width: self.view.frame.width, height: 90)
-        vapButton.backgroundColor = UIColor.lightGray
-        vapButton.setTitle("融合动效(更多示例请查看OC版本)", for: UIControl.State.normal)
-        vapButton.addTarget(self, action: #selector(playVapx), for: UIControl.Event.touchUpInside)
-        self.view.addSubview(vapButton)
+
+        vapButton.frame = CGRect(x: 0, y: 100, width: view.frame.width, height: 90)
+        vapButton.backgroundColor = .lightGray
+        vapButton.setTitle("LN Swift API", for: .normal)
+        vapButton.addTarget(self, action: #selector(playVap), for: .touchUpInside)
+        view.addSubview(vapButton)
     }
-    
-    @objc func playVapx() {
-        let vapView = UIView.init(frame: self.view.bounds)
-        let mp4Path = String.init(format: "%@/Resource/vap.mp4", Bundle.main.resourcePath!)
-        self.view.addSubview(vapView)
-        vapView.center = self.view.center
-        vapView.isUserInteractionEnabled = true
-        vapView.hwd_enterBackgroundOP = HWDMP4EBOperationType.stop
-        let gesture = UITapGestureRecognizer.init(target: self, action: #selector(onTap(gesture:)))
-        vapView.addGestureRecognizer(gesture)
-        vapView.playHWDMP4(mp4Path, repeatCount: -1, delegate: self)
-        
+
+    @objc private func playVap() {
+        playerView?.removeFromSuperview()
+        let player = LNVAPFacade.lnMakePlayerView(frame: view.bounds)
+        player.center = view.center
+        player.delegate = self
+        player.enterBackgroundOperation = .stop
+        player.lnSetMute(true)
+        player.lnEnableOldVersion(true)
+        player.lnAddTapGesture(target: self, action: #selector(onTap(_:)))
+        view.addSubview(player)
+        playerView = player
+
+        let mp4Path = "\(Bundle.main.resourcePath ?? "")/Resource/vap.mp4"
+        player.lnPlay(filePath: mp4Path, repeatCount: -1)
     }
-    
-    @objc func onTap(gesture: UIGestureRecognizer) {
-        gesture.view?.stopHWDMP4()
+
+    @objc private func onTap(_ gesture: UIGestureRecognizer) {
+        playerView?.lnStop()
         gesture.view?.removeFromSuperview()
     }
-    
-    func content(forVapTag tag: String!, resource info: QGVAPSourceInfo) -> String {
-        let extraInfo: [String:String] = ["[sImg1]" : "http://shp.qlogo.cn/pghead/Q3auHgzwzM6GuU0Y6q6sKHzq3MjY1aGibIzR4xrJc1VY/60",
-                                          "[textAnchor]" : "我是主播名",
-                                          "[textUser]" : "我是用户名😂😂",]
-        
-        return extraInfo[tag] ?? ""
-    }
-    
-    func loadVapImage(withURL urlStr: String!, context: [AnyHashable : Any]!, completion completionBlock: VAPImageCompletionBlock!) {
+
+    func lnPlayerDidStop(_ playerView: LNVAPPlayerView) {
         DispatchQueue.main.async {
-            let image = UIImage.init(named: String.init(format:"%@/Resource/qq.png", Bundle.main.resourcePath!))
-            completionBlock(image, nil, urlStr)
+            playerView.removeFromSuperview()
         }
     }
-
-
 }
-

+ 35 - 0
RENDER_API_MIGRATION_TABLE.md

@@ -0,0 +1,35 @@
+# Render API Migration Table (OC -> LN Swift)
+
+> 当前阶段结论:Render 已完成 **LN API + 可运行基线实现**,并新增 **Swift Metal 主路径**(HWD 全链路 + VAP 背景/merge/mask/blur),`LNHWDMP4OpenGLView` 也已改为 Swift 主路径(内部走 LN renderer);仍需做 **视觉行为对齐**。
+> 为保证工程可编译与双 Demo 可联调,仍保持不向 umbrella 暴露非模块化私有头。
+
+## Metal / OpenGL View & Renderer
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGHWDMetalRenderer` | `-initWithMetalLayer:blendMode:` | `LNHWDMetalRenderer` | `init(metalLayer:blendMode:)` | Baseline Done / Parity Pending |
+| `QGHWDMetalRenderer` | `-renderPixelBuffer:metalLayer:` | `LNHWDMetalRenderer` | `renderPixelBuffer(_:metalLayer:)` | Baseline Done / Parity Pending |
+| `QGHWDMetalRenderer` | `-dispose` | `LNHWDMetalRenderer` | `dispose()` | Baseline Done |
+| `QGHWDMetalView` | `-initWithFrame:blendMode:` | `LNHWDMetalView` | `init(frame:blendMode:)` | Baseline Done / API Alias Done |
+| `QGHWDMetalView` | `-display:` | `LNHWDMetalView` | `display(_:)` | Baseline Done / Parity Pending |
+| `QGHWDMetalView` | `-dispose` | `LNHWDMetalView` | `dispose()` | Baseline Done |
+| `QGVAPMetalRenderer` | `-initWithMetalLayer:` | `LNVAPMetalRenderer` | `init(metalLayer:)` | Baseline Done / Parity Pending |
+| `QGVAPMetalRenderer` | `-renderPixelBuffer:metalLayer:mergeInfos:` | `LNVAPMetalRenderer` | `renderPixelBuffer(_:metalLayer:mergeInfos:)` | Swift Core Done / Parity Pending |
+| `QGVAPMetalRenderer` | `-dispose` | `LNVAPMetalRenderer` | `dispose()` | Baseline Done |
+| `QGVAPMetalView` | `-display:mergeInfos:` | `LNVAPMetalView` | `display(_:mergeInfos:)` | Baseline Done / API Alias Done |
+| `QGVAPMetalView` | `-dispose` | `LNVAPMetalView` | `dispose()` | Baseline Done |
+| `QGHWDMP4OpenGLView` | `-setupGL` | `LNHWDMP4OpenGLView` | `setupGL()` | Swift 主路径 Done |
+| `QGHWDMP4OpenGLView` | `-displayPixelBuffer:` | `LNHWDMP4OpenGLView` | `displayPixelBuffer(_:)` | Swift 主路径 Done / Parity Pending |
+| `QGHWDMP4OpenGLView` | `-dispose` | `LNHWDMP4OpenGLView` | `dispose()` | Baseline Done |
+| `QGHWDMP4OpenGLView` | `-updateBackingSize` | `LNHWDMP4OpenGLView` | `updateBackingSize()` | Baseline Done |
+
+## Utility
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGVAPMetalShaderFunctionLoader` | `-initWithDevice:` | `LNVAPMetalShaderFunctionLoader` | `init(device:)` | Baseline Done |
+| `QGVAPMetalShaderFunctionLoader` | `-loadFunctionWithName:` | `LNVAPMetalShaderFunctionLoader` | `loadFunction(withName:)` | Baseline Done |
+
+## Files
+
+- `/Users/yanxuyao/Vap/QGVAPlayer/QGVAPlayer/LNSwift/Render/LNRenderers.swift`

+ 20 - 0
SWIFT_MIGRATION_MAP.md

@@ -0,0 +1,20 @@
+# Swift Migration Map (LN Prefix)
+
+旧 OC 对照实现保留在 `QGVAPlayer/QGVAPlayer/Classes`,新 Swift 实现在 `QGVAPlayer/QGVAPlayer/LNSwift`。
+
+| Legacy | Swift |
+|---|---|
+| UIView+VAP | LNVAPPlayerView |
+| QGVAPWrapView | LNVAPWrapView |
+| QGMP4Parser/QGMP4Box* | LNMP4Parser/LNMP4Box* |
+| QGAnimatedImageDecode* | LNAnimatedImageDecode* |
+| QGBaseDecoder/QGMP4FrameHWDecoder | LNBaseDecoder/LNMP4FrameHWDecoder |
+| QGVAPConfigModel/QGVAPSourceInfo | LNVAPConfigModel/LNVAPSourceInfo |
+| QGVAPMaskInfo/QGVAPTextureLoader | LNVAPMaskInfo/LNVAPTextureLoader |
+| QGHWDMetal*/QGVAPMetal*/QGHWDMP4OpenGLView | LNHWDMetal*/LNVAPMetal*/LNHWDMP4OpenGLView |
+| QGVAPLogger/QGVAPSafeMutable*/QGVAPWeakProxy | LNVAPLogger/LNVAPSafeMutable*/LNVAPWeakProxy |
+
+## Current Integration Notes
+
+- `LNVAPPlayerView` / `LNVAPWrapView` 已移除对 `UIView+VAP.h` / `QGVAPWrapView.h` 的编译期直接依赖,改为 runtime selector bridge。
+- legacy OC 实现仍保留用于行为对照;Swift 主路径优先通过 LN API 访问。

+ 20 - 0
UTILS_API_MIGRATION_TABLE.md

@@ -0,0 +1,20 @@
+# Utils API Migration Table (OC -> LN Swift)
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGVAPLogger` | `+log:file:line:func:module:message:` | `LNVAPLogger` | `@objc(logLevel:file:line:func:module:message:)` | Baseline Done |
+| `QGVAPLogger` | `+registerExternalLog:` | `LNVAPLogger` | `registerLogHandler(_:)` | Baseline Done |
+| `QGVAPSafeMutableArray` | 线程安全增删改查 | `LNVAPSafeMutableArray` | `count/object(at:)/add/insert/remove...` | Baseline Done |
+| `QGVAPSafeMutableDictionary` | 线程安全 key-value 操作 | `LNVAPSafeMutableDictionary` | `count/object(forKey:)/set/remove...` | Baseline Done |
+| `QGVAPWeakProxy` | `proxyWithTarget:` + 弱转发 | `LNVAPWeakProxy` | `proxy(with:)` + `forwardingTarget` | Baseline Done |
+| `QGVAPMetalShaderFunctionLoader` | `initWithDevice:` / `loadFunctionWithName:` | `LNVAPMetalShaderFunctionLoader` | 等价 API | Baseline Done |
+| `QGVAPMetalUtil` | 顶点/纹理坐标/布局计算 | `LNVAPMetalUtil` | `genVertices/genTextureCoordinates/...` | Baseline Done |
+| `UIDevice(VAPUtil)` | `+systemVersionNum` / `kDefaultMTLResourceOption` | `LNVAPDeviceUtil` | `systemVersionNum/defaultMTLResourceOption` | Baseline Done |
+| `NSArray(VAPUtil)` | `-hwd_rectValue` | `NSArray(LN)` | `ln_rectValue()` | Baseline Done |
+| `NSDictionary(VAPUtil)` | `hwd_float/integer/string/dic/arrValue` | `NSDictionary(LN)` | `ln_floatValue/ln_integerValue/ln_stringValue/...` | Baseline Done |
+| `UIColor(VAPUtil)` | `+hwd_colorWithHexString:` | `UIColor(LN)` | `ln_color(hexString:)` | Baseline Done |
+| `UIGestureRecognizer(VAPUtil)` | block 初始化/添加/移除 | `UIGestureRecognizer(LN)` | `ln_initWithActionBlock/ln_addActionBlock/ln_removeAllActionBlocks` | Baseline Done |
+| `NSNotificationCenter(VAPThreadSafe)` | 安全 observer/weak observer | `NotificationCenter(LN)` | `ln_addSafeObserver/ln_addWeakObserver` | Baseline Done |
+
+## Files
+- `/Users/yanxuyao/Vap/QGVAPlayer/QGVAPlayer/LNSwift/Utils/LNUtilities.swift`

+ 32 - 0
VIEW_API_MIGRATION_TABLE.md

@@ -0,0 +1,32 @@
+# View API Migration Table (OC -> LN Swift)
+
+## Player View
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `UIView(VAP)` | `playHWDMP4:repeatCount:delegate:` | `LNVAPPlayerView` | `lnPlay(filePath:repeatCount:)` | Done (runtime bridge) |
+| `UIView(VAP)` | `playHWDMp4:` / `playHWDMP4:delegate:` | `LNVAPPlayerView` | `lnPlay(filePath:)` + `delegate` property | Done (runtime bridge) |
+| `UIView(VAP)` | `stopHWDMP4` | `LNVAPPlayerView` | `lnStop()` | Done (runtime bridge) |
+| `UIView(VAP)` | `pauseHWDMP4` / `resumeHWDMP4` | `LNVAPPlayerView` | `lnPause()` / `lnResume()` | Done (runtime bridge) |
+| `UIView(VAP)` | `setMute:` | `LNVAPPlayerView` | `lnSetMute(_:)` | Done (runtime bridge) |
+| `UIView(VAP)` | `enableOldVersion:` | `LNVAPPlayerView` | `lnEnableOldVersion(_:)` | Done (runtime bridge) |
+| `UIView(VAP)` | `hwd_fps` / `hwd_renderByOpenGL` / `hwd_enterBackgroundOP` | `LNVAPPlayerView` | `fps` / `renderByOpenGL` / `enterBackgroundOperation` | Done (runtime bridge) |
+| `UIView(VAPGesture)` | `addVapTapGesture:` / `addVapGesture:callback:` | `LNVAPPlayerView` | `lnAddVapTapGesture(_:)` / `lnAddVapGesture(_:callback:)` | Done (runtime bridge + fallback) |
+
+## Wrap View
+
+| OC Class | OC API | Swift Class | Swift API | Status |
+|---|---|---|---|---|
+| `QGVAPWrapView` | `playHWDMP4:repeatCount:delegate:` | `LNVAPWrapView` | `lnPlay(filePath:repeatCount:)` | Done (runtime bridge) |
+| `QGVAPWrapView` | `playHWDMP4:...` 单次默认播放 | `LNVAPWrapView` | `lnPlay(filePath:)` | Done (runtime bridge) |
+| `QGVAPWrapView` | `stopHWDMP4` / `pauseHWDMP4` / `resumeHWDMP4` | `LNVAPWrapView` | `lnStop()` / `lnPause()` / `lnResume()` | Done (runtime bridge) |
+| `QGVAPWrapView` | `setMute:` | `LNVAPWrapView` | `lnSetMute(_:)` | Done (runtime bridge) |
+| `QGVAPWrapView` | `contentMode` / `autoDestoryAfterFinish` | `LNVAPWrapView` | `contentModeOption` / `autoDestroyAfterFinish` | Done (runtime bridge) |
+| `QGVAPWrapView` | `addVapTapGesture:` / `addVapGesture:callback:` | `LNVAPWrapView` | `lnAddVapTapGesture(_:)` / `lnAddVapGesture(_:callback:)` | Done (runtime bridge + fallback) |
+| `HWDMP4PlayDelegate` | `shouldStart/start/play/stop/finish/fail + content/loadImage` | `LNVAPPlaybackDelegate` | `lnPlayerShouldStart/.../lnPlayerLoadImage` | Done |
+| `VAPWrapViewDelegate` | `shouldStart/start/play/stop/finish/fail + content/loadImage` | `LNVAPWrapPlaybackDelegate` | `lnWrapViewShouldStart/.../lnWrapViewLoadImage` | Done |
+
+## Files
+
+- `/Users/yanxuyao/Vap/QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPPlayerView.swift`
+- `/Users/yanxuyao/Vap/QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPWrapView.swift`