فهرست منبع

[*] 暂时提交

yanxuyao 1 ماه پیش
والد
کامیت
836394169f

+ 39 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Bridges/LN_API_PARITY_TABLE.md

@@ -0,0 +1,39 @@
+# LN API Parity Table (OC -> Swift/LN)
+
+## UIView+VAP
+- `playHWDMp4:` -> `LNVAPFacade.lnPlayPlayer:filePath:` -> Done
+- `playHWDMP4:delegate:` -> `LNVAPFacade.lnPlayPlayer:filePath:delegate:` -> Done
+- `playHWDMP4:repeatCount:delegate:` -> `LNVAPFacade.lnPlayPlayer:filePath:repeatCount:delegate:` -> Done
+- `stopHWDMP4` -> `LNVAPFacade.lnStopPlayer:` -> Done
+- `pauseHWDMP4` -> `LNVAPFacade.lnPausePlayer:` -> Done
+- `resumeHWDMP4` -> `LNVAPFacade.lnResumePlayer:` -> Done
+- `registerHWDLog:` -> `LNVAPFacade.lnRegisterLogHandler:` -> Done
+- `enableOldVersion:` -> `LNVAPFacade.lnEnableOldVersionOnPlayer:enable:` -> Done
+- `setMute:` -> `LNVAPFacade.lnSetMutePlayer:mute:` -> Done
+- `addVapTapGesture:` -> `LNVAPFacade.lnAddPlayerVapTapGesture:handler:` -> Done
+- `addVapGesture:callback:` -> `LNVAPFacade.lnAddPlayerVapGesture:gesture:callback:` -> Done
+- `hwd_fps` -> `LNVAPFacade.lnSetPlayerFPS:fps:` / `lnGetPlayerFPS:` -> Done
+- `hwd_renderByOpenGL` -> `LNVAPFacade.lnSetPlayerRenderByOpenGL:enabled:` / `lnGetPlayerRenderByOpenGL:` -> Done
+- `hwd_enterBackgroundOP` -> `LNVAPFacade.lnSetPlayerEnterBackgroundOperation:operation:` / `lnGetPlayerEnterBackgroundOperation:` -> Done
+- `hwd_currentFrame` -> `LNVAPFacade.lnGetPlayerCurrentFrame:` -> Done
+- `hwd_MP4FilePath` -> `LNVAPFacade.lnGetPlayerMP4FilePath:` -> Done
+
+## UIView (MP4HWDDeprecated)
+- `playHWDMP4:fps:blendMode:repeatCount:delegate:` family -> `LNVAPFacade.lnPlayPlayerDeprecated:filePath:fps:blendMode:repeatCount:delegate:` -> Done
+- Note: `blendMode` kept only for compatibility; behavior follows current VAP path.
+
+## QGVAPWrapView
+- `playHWDMP4:repeatCount:delegate:` -> `LNVAPFacade.lnPlayWrap:filePath:repeatCount:delegate:` -> Done
+- `play(no repeat/delegate convenience)` -> `LNVAPFacade.lnPlayWrap:filePath:` / `lnPlayWrap:filePath:delegate:` -> Done
+- `stopHWDMP4` -> `LNVAPFacade.lnStopWrap:` -> Done
+- `pauseHWDMP4` -> `LNVAPFacade.lnPauseWrap:` -> Done
+- `resumeHWDMP4` -> `LNVAPFacade.lnResumeWrap:` -> Done
+- `setMute:` -> `LNVAPFacade.lnSetMuteWrap:mute:` -> Done
+- `addVapTapGesture:` -> `LNVAPFacade.lnAddWrapTapGesture:handler:` -> Done
+- `addVapGesture:callback:` -> `LNVAPFacade.lnAddWrapGesture:gesture:callback:` -> Done
+- `contentMode` -> `LNVAPFacade.lnSetWrapContentMode:mode:` / `lnGetWrapContentMode:` -> Done
+- `autoDestoryAfterFinish` -> `LNVAPFacade.lnSetWrapAutoDestroyAfterFinish:enabled:` / `lnGetWrapAutoDestroyAfterFinish:` -> Done
+
+## Behavioral Notes
+- Legacy selector names are not re-declared on `LNVAPPlayerView/LNVAPWrapView` when they conflict with `UIView` category selectors; the migration path is via `LNVAPFacade`.
+- Wrap legacy delegate callbacks now pass internal player container view (`playerView`) first, matching OC wrap behavior (`vapView`).

+ 44 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Bridges/LN_BEHAVIOR_CHECKLIST.md

@@ -0,0 +1,44 @@
+# LN Behavior Checklist (OC vs LN)
+
+## Playback Lifecycle
+1. Play success path
+- Trigger: play with valid mp4
+- Expected order: shouldStart -> didStart(frame0) -> didPlay(...) -> didFinish -> didStop
+- Note: repeatCount > 0/-1 should loop before final stop
+
+2. Pause / Resume
+- Trigger: pause during playback then resume
+- Expected: pause should not emit stop callback; resume continues frame progression
+
+3. Stop
+- Trigger: explicit stop
+- Expected: didStop called once; internal render/decode/audio released
+
+4. Failure path
+- Trigger: invalid path / decode failure
+- Expected order: stop (cleanup) then fail callback (matches OC fail path)
+
+5. Background behavior
+- Trigger: app enters background
+- Expected:
+  - stop: emits stop and releases
+  - pauseAndResume: pause in background, resume in foreground
+  - doNothing: no automatic control
+
+## Wrap Legacy Container Semantics
+- For legacy wrap delegate callbacks, container parameter should be internal player container view (`playerView`) when available.
+
+## Config Resource Loading
+- If no `vap_loadImageWithURL`, resource loading returns early (OC parity).
+- Text local resources should still render text image.
+
+## Manual Demo Verification Steps
+1. QGVAPlayerDemo
+- Play PlayerView (repeat -1), tap to stop
+- Play WrapView (repeat -1), verify callbacks and auto destroy
+
+2. QGVAPlayerDemoSwift
+- Play, tap to stop, verify no crash and callbacks received
+
+3. Negative test
+- Pass invalid file path; verify fail callback and resources cleaned

+ 37 - 0
QGVAPlayer/QGVAPlayer/LNSwift/Bridges/LN_DEMO_VERIFICATION.md

@@ -0,0 +1,37 @@
+# LN Demo Verification (Manual)
+
+## QGVAPlayerDemo (ObjC)
+1. Tap `LN PlayerView`
+- Expect logs in order:
+  - `[LN Demo OC] player did start`
+  - repeated `[LN Demo OC] player did play frame=...`
+- Tap on video area:
+  - `[LN Demo OC] player did stop`
+
+2. Tap `LN WrapView`
+- Expect logs:
+  - `[LN Demo OC] wrap did start`
+  - repeated `[LN Demo OC] wrap did play frame=...`
+- On stop/end:
+  - `[LN Demo OC] wrap did stop` or `wrap did finish total=...`
+
+3. Tap `LN Fail Path`
+- Expect fail log:
+  - `[LN Demo OC] player did fail error=...`
+
+## QGVAPlayerDemoSwift
+1. Tap `LN Swift API`
+- Expect logs:
+  - `[LN Demo Swift] player did start`
+  - repeated `player did play frame=...`
+- Tap on video area:
+  - `[LN Demo Swift] player did stop`
+
+2. Tap `LN Swift Fail Path`
+- Expect fail log:
+  - `[LN Demo Swift] player did fail error=...`
+
+## Expected lifecycle constraints
+- pause/resume should not emit stop immediately
+- explicit stop should emit stop once
+- invalid path should emit fail callback

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

@@ -75,6 +75,7 @@ public final class LNAnimatedImageBufferManager: NSObject {
     public var buffers: NSMutableArray
     private let config: LNAnimatedImageDecodeConfig
 
+    @objc(initWithConfig:)
     public init(config: LNAnimatedImageDecodeConfig) {
         self.config = config
         self.buffers = NSMutableArray(capacity: max(config.bufferCount, 0))

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

@@ -14,4 +14,210 @@ public final class LNVAPFacade: NSObject {
     @objc public static func lnUseDefaultLogBridge() {
         // Swift façade keeps API surface centralized; legacy logger registration remains available in OC layer.
     }
+
+    @objc(lnRegisterLogHandler:)
+    public static func lnRegisterLogHandler(_ handler: @escaping LNVAPLogger.LNLogHandler) {
+        LNVAPLogger.registerExternalLog(handler)
+    }
+
+    @objc(lnClearLogHandler)
+    public static func lnClearLogHandler() {
+        LNVAPLogger.clearLogHandler()
+    }
+
+    // MARK: - Wrap View Ops (ObjC-friendly façade)
+
+    @objc(lnPlayWrap:filePath:repeatCount:)
+    public static func lnPlayWrap(_ wrapView: LNVAPWrapView, filePath: String, repeatCount: Int) {
+        wrapView.lnPlay(filePath: filePath, repeatCount: repeatCount)
+    }
+
+    @objc(lnPlayWrap:filePath:)
+    public static func lnPlayWrap(_ wrapView: LNVAPWrapView, filePath: String) {
+        wrapView.lnPlay(filePath: filePath)
+    }
+
+    @objc(lnPlayWrap:filePath:repeatCount:delegate:)
+    public static func lnPlayWrap(_ wrapView: LNVAPWrapView, filePath: String, repeatCount: Int, delegate: LNVAPWrapPlaybackDelegate?) {
+        wrapView.delegate = delegate
+        wrapView.lnPlay(filePath: filePath, repeatCount: repeatCount)
+    }
+
+    @objc(lnPlayWrap:filePath:delegate:)
+    public static func lnPlayWrap(_ wrapView: LNVAPWrapView, filePath: String, delegate: LNVAPWrapPlaybackDelegate?) {
+        lnPlayWrap(wrapView, filePath: filePath, repeatCount: 0, delegate: delegate)
+    }
+
+    @objc(lnPlayWrapDeprecated:filePath:fps:blendMode:repeatCount:delegate:)
+    public static func lnPlayWrapDeprecated(_ wrapView: LNVAPWrapView,
+                                            filePath: String,
+                                            fps: Int,
+                                            blendMode: Int,
+                                            repeatCount: Int,
+                                            delegate: LNVAPWrapPlaybackDelegate?) {
+        wrapView.lnPlayDeprecated(filePath: filePath, fps: fps, blendMode: blendMode, repeatCount: repeatCount, delegate: delegate)
+    }
+
+    @objc(lnStopWrap:)
+    public static func lnStopWrap(_ wrapView: LNVAPWrapView) {
+        wrapView.lnStop()
+    }
+
+    @objc(lnPauseWrap:)
+    public static func lnPauseWrap(_ wrapView: LNVAPWrapView) {
+        wrapView.lnPause()
+    }
+
+    @objc(lnResumeWrap:)
+    public static func lnResumeWrap(_ wrapView: LNVAPWrapView) {
+        wrapView.lnResume()
+    }
+
+    @objc(lnSetMuteWrap:mute:)
+    public static func lnSetMuteWrap(_ wrapView: LNVAPWrapView, mute: Bool) {
+        wrapView.lnSetMute(mute)
+    }
+
+    @objc(lnSetWrapContentMode:mode:)
+    public static func lnSetWrapContentMode(_ wrapView: LNVAPWrapView, mode: LNVAPWrapContentMode) {
+        wrapView.contentModeOption = mode
+    }
+
+    @objc(lnGetWrapContentMode:)
+    public static func lnGetWrapContentMode(_ wrapView: LNVAPWrapView) -> LNVAPWrapContentMode {
+        wrapView.contentModeOption
+    }
+
+    @objc(lnSetWrapAutoDestroyAfterFinish:enabled:)
+    public static func lnSetWrapAutoDestroyAfterFinish(_ wrapView: LNVAPWrapView, enabled: Bool) {
+        wrapView.autoDestroyAfterFinish = enabled
+    }
+
+    @objc(lnGetWrapAutoDestroyAfterFinish:)
+    public static func lnGetWrapAutoDestroyAfterFinish(_ wrapView: LNVAPWrapView) -> Bool {
+        wrapView.autoDestroyAfterFinish
+    }
+
+    @objc(lnAddWrapTapGesture:handler:)
+    public static func lnAddWrapTapGesture(_ wrapView: LNVAPWrapView, handler: @escaping LNVAPGestureEventBlock) {
+        wrapView.lnAddVapTapGesture(handler)
+    }
+
+    @objc(lnAddWrapGesture:gesture:callback:)
+    public static func lnAddWrapGesture(_ wrapView: LNVAPWrapView, gesture: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) {
+        wrapView.lnAddVapGesture(gesture, callback: callback)
+    }
+
+    // MARK: - Player View Ops
+
+    @objc(lnPlayPlayer:filePath:repeatCount:)
+    public static func lnPlayPlayer(_ playerView: LNVAPPlayerView, filePath: String, repeatCount: Int) {
+        playerView.lnPlay(filePath: filePath, repeatCount: repeatCount)
+    }
+
+    @objc(lnPlayPlayer:filePath:)
+    public static func lnPlayPlayer(_ playerView: LNVAPPlayerView, filePath: String) {
+        playerView.lnPlay(filePath: filePath)
+    }
+
+    @objc(lnPlayPlayer:filePath:repeatCount:delegate:)
+    public static func lnPlayPlayer(_ playerView: LNVAPPlayerView, filePath: String, repeatCount: Int, delegate: LNVAPPlaybackDelegate?) {
+        playerView.delegate = delegate
+        playerView.lnPlay(filePath: filePath, repeatCount: repeatCount)
+    }
+
+    @objc(lnPlayPlayer:filePath:delegate:)
+    public static func lnPlayPlayer(_ playerView: LNVAPPlayerView, filePath: String, delegate: LNVAPPlaybackDelegate?) {
+        lnPlayPlayer(playerView, filePath: filePath, repeatCount: 0, delegate: delegate)
+    }
+
+    @objc(lnPlayPlayerDeprecated:filePath:fps:blendMode:repeatCount:delegate:)
+    public static func lnPlayPlayerDeprecated(_ playerView: LNVAPPlayerView,
+                                              filePath: String,
+                                              fps: Int,
+                                              blendMode: Int,
+                                              repeatCount: Int,
+                                              delegate: LNVAPPlaybackDelegate?) {
+        playerView.lnPlayDeprecated(filePath: filePath, fps: fps, blendMode: blendMode, repeatCount: repeatCount, delegate: delegate)
+    }
+
+    @objc(lnStopPlayer:)
+    public static func lnStopPlayer(_ playerView: LNVAPPlayerView) {
+        playerView.lnStop()
+    }
+
+    @objc(lnPausePlayer:)
+    public static func lnPausePlayer(_ playerView: LNVAPPlayerView) {
+        playerView.lnPause()
+    }
+
+    @objc(lnResumePlayer:)
+    public static func lnResumePlayer(_ playerView: LNVAPPlayerView) {
+        playerView.lnResume()
+    }
+
+    @objc(lnSetMutePlayer:mute:)
+    public static func lnSetMutePlayer(_ playerView: LNVAPPlayerView, mute: Bool) {
+        playerView.lnSetMute(mute)
+    }
+
+    @objc(lnEnableOldVersionOnPlayer:enable:)
+    public static func lnEnableOldVersionOnPlayer(_ playerView: LNVAPPlayerView, enable: Bool) {
+        playerView.lnEnableOldVersion(enable)
+    }
+
+    @objc(lnSetPlayerFPS:fps:)
+    public static func lnSetPlayerFPS(_ playerView: LNVAPPlayerView, fps: Int) {
+        playerView.fps = fps
+    }
+
+    @objc(lnGetPlayerFPS:)
+    public static func lnGetPlayerFPS(_ playerView: LNVAPPlayerView) -> Int {
+        playerView.fps
+    }
+
+    @objc(lnSetPlayerRenderByOpenGL:enabled:)
+    public static func lnSetPlayerRenderByOpenGL(_ playerView: LNVAPPlayerView, enabled: Bool) {
+        playerView.renderByOpenGL = enabled
+    }
+
+    @objc(lnGetPlayerRenderByOpenGL:)
+    public static func lnGetPlayerRenderByOpenGL(_ playerView: LNVAPPlayerView) -> Bool {
+        playerView.renderByOpenGL
+    }
+
+    @objc(lnSetPlayerEnterBackgroundOperation:operation:)
+    public static func lnSetPlayerEnterBackgroundOperation(_ playerView: LNVAPPlayerView, operation: LNEnterBackgroundOperation) {
+        playerView.enterBackgroundOperation = operation
+    }
+
+    @objc(lnGetPlayerEnterBackgroundOperation:)
+    public static func lnGetPlayerEnterBackgroundOperation(_ playerView: LNVAPPlayerView) -> LNEnterBackgroundOperation {
+        playerView.enterBackgroundOperation
+    }
+
+    @objc(lnGetPlayerCurrentFrame:)
+    public static func lnGetPlayerCurrentFrame(_ playerView: LNVAPPlayerView) -> LNMP4AnimatedImageFrame? {
+        playerView.lnCurrentFrame
+    }
+
+    @objc(lnGetPlayerMP4FilePath:)
+    public static func lnGetPlayerMP4FilePath(_ playerView: LNVAPPlayerView) -> String {
+        playerView.lnMP4FilePath
+    }
+
+    @objc(lnAddPlayerTapGesture:target:action:)
+    public static func lnAddPlayerTapGesture(_ playerView: LNVAPPlayerView, target: Any, action: Selector) {
+        playerView.lnAddTapGesture(target: target, action: action)
+    }
+
+    @objc(lnAddPlayerVapTapGesture:handler:)
+    public static func lnAddPlayerVapTapGesture(_ playerView: LNVAPPlayerView, handler: @escaping LNVAPGestureEventBlock) {
+        playerView.lnAddVapTapGesture(handler)
+    }
+
+    @objc(lnAddPlayerVapGesture:gesture:callback:)
+    public static func lnAddPlayerVapGesture(_ playerView: LNVAPPlayerView, gesture: UIGestureRecognizer, callback: @escaping LNVAPGestureEventBlock) {
+        playerView.lnAddVapGesture(gesture, callback: callback)
+    }
 }

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

@@ -145,6 +145,7 @@ open class LNMP4Box: NSObject {
     public weak var superBox: LNMP4Box?
     public var subBoxes: [LNMP4Box] = []
 
+    @objc(initWithType:startIndex:length:)
     public init(type: LNMP4BoxType, startIndex: UInt64, length: UInt64) {
         self.type = type
         self.startIndexInBytes = startIndex
@@ -447,6 +448,7 @@ public final class LNMP4Parser: NSObject {
 
     private let filePath: String
 
+    @objc(initWithFilePath:)
     public init(filePath: String) {
         self.filePath = filePath
         self.fileHandle = FileHandle(forReadingAtPath: filePath)
@@ -703,6 +705,7 @@ public final class LNMP4ParserProxy: NSObject, LNMP4ParserDelegate {
         }
     }
 
+    @objc(initWithFilePath:)
     public init(filePath: String) {
         parser = LNMP4Parser(filePath: filePath)
         super.init()

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

@@ -116,6 +116,16 @@ public final class LNVAPPlayerView: UIView {
         core.enableOldVersion = enable
     }
 
+    @objc(lnCurrentFrame)
+    public var lnCurrentFrame: LNMP4AnimatedImageFrame? {
+        core.currentFrameSnapshot
+    }
+
+    @objc(lnMP4FilePath)
+    public var lnMP4FilePath: String {
+        core.currentFilePath
+    }
+
 
 
     @objc(lnAddTapGestureWithTarget:action:)
@@ -257,6 +267,9 @@ private final class LNPlayerCore: NSObject {
     private let renderQueue = DispatchQueue(label: "com.ln.vap.render")
     private var filePath: String = ""
 
+    var currentFrameSnapshot: LNMP4AnimatedImageFrame? { currentFrame }
+    var currentFilePath: String { filePath }
+
     init(owner: LNVAPPlayerView, hostView: UIView) {
         self.owner = owner
         self.hostView = hostView

+ 14 - 5
QGVAPlayer/QGVAPlayer/LNSwift/View/LNVAPWrapView.swift

@@ -52,6 +52,7 @@ public final class LNVAPWrapView: UIView {
         player.lnPlay(filePath: filePath, repeatCount: repeatCount)
     }
 
+
     @objc(lnPlayWithFilePath:)
     public func lnPlay(filePath: String) {
         lnPlay(filePath: filePath, repeatCount: 0)
@@ -78,18 +79,21 @@ public final class LNVAPWrapView: UIView {
     }
 
 
+
     @objc(lnPause)
     public func lnPause() {
         playerView?.lnPause()
     }
 
 
+
     @objc(lnResume)
     public func lnResume() {
         playerView?.lnResume()
     }
 
 
+
     @objc(lnSetMute:)
     public func lnSetMute(_ mute: Bool) {
         let player = initPlayerViewIfNeed()
@@ -98,6 +102,7 @@ public final class LNVAPWrapView: UIView {
 
 
 
+
     @objc(lnAddVapTapGesture:)
     public func lnAddVapTapGesture(_ handler: @escaping LNVAPGestureEventBlock) {
         let player = initPlayerViewIfNeed()
@@ -141,29 +146,29 @@ public final class LNVAPWrapView: UIView {
         if let allow = delegate?.lnWrapViewShouldStart?(self, config: config) {
             return allow
         }
-        return legacyDelegate?.vapWrap_viewshouldStartPlayMP4?(self, config: config) ?? true
+        return legacyDelegate?.vapWrap_viewshouldStartPlayMP4?(legacyContainerView(), config: config) ?? true
     }
 
     fileprivate func notifyStart() {
         delegate?.lnWrapViewDidStart?(self)
-        legacyDelegate?.vapWrap_viewDidStartPlayMP4?(self)
+        legacyDelegate?.vapWrap_viewDidStartPlayMP4?(legacyContainerView())
     }
 
     fileprivate func notifyPlay(_ frame: LNMP4AnimatedImageFrame) {
         delegate?.lnWrapViewDidPlay?(self, frame: frame)
-        legacyDelegate?.vapWrap_viewDidPlayMP4AtFrame?(frame, view: self)
+        legacyDelegate?.vapWrap_viewDidPlayMP4AtFrame?(frame, view: legacyContainerView())
     }
 
     fileprivate func notifyFinish(_ totalFrameCount: Int) {
         let computedCount = max(totalFrameCount, delegateBridge.lastPlayedFrameIndex + 1)
         delegate?.lnWrapViewDidFinish?(self, totalFrameCount: computedCount)
-        legacyDelegate?.vapWrap_viewDidFinishPlayMP4?(computedCount, view: self)
+        legacyDelegate?.vapWrap_viewDidFinishPlayMP4?(computedCount, view: legacyContainerView())
     }
 
     fileprivate func notifyStop() {
         delegate?.lnWrapViewDidStop?(self)
         let lastFrameIndex = delegateBridge.lastPlayedFrameIndex
-        legacyDelegate?.vapWrap_viewDidStopPlayMP4?(lastFrameIndex, view: self)
+        legacyDelegate?.vapWrap_viewDidStopPlayMP4?(lastFrameIndex, view: legacyContainerView())
         delegateBridge.lastPlayedFrameIndex = 0
         DispatchQueue.main.async { [weak self] in
             guard let self else { return }
@@ -210,6 +215,10 @@ public final class LNVAPWrapView: UIView {
         return view
     }
 
+    private func legacyContainerView() -> UIView {
+        playerView ?? self
+    }
+
     private func applyContentModeIfPossible() {
         guard let playerView else { return }
         guard let config = delegateBridge.lastConfig else {

+ 33 - 0
QGVAPlayer/scripts/check_ln_api_parity.py

@@ -0,0 +1,33 @@
+#!/usr/bin/env python3
+import re
+from pathlib import Path
+
+root = Path('/Users/yanxuyao/Vap/QGVAPlayer/QGVAPlayer')
+parity = root / 'LNSwift/Bridges/LN_API_PARITY_TABLE.md'
+facade = root / 'LNSwift/Core/LNVAPFacade.swift'
+
+parity_text = parity.read_text(encoding='utf-8')
+facade_text = facade.read_text(encoding='utf-8')
+
+expected = set()
+for token in re.findall(r'`LNVAPFacade\.([^`]+)`', parity_text):
+    # token may include " / `lnXxx:`" in same line; split conservatively.
+    for part in token.split(' / '):
+        part = part.strip()
+        if not part:
+            continue
+        # keep selector-like text only
+        if re.match(r'^[A-Za-z_][A-Za-z0-9_:]*$', part):
+            expected.add(part)
+
+exposed = set(re.findall(r'@objc\(([^\)]+)\)', facade_text))
+missing = sorted(s for s in expected if s not in exposed)
+
+print(f'expected_from_table={len(expected)}')
+print(f'exposed_in_facade={len(exposed)}')
+if missing:
+    print('MISSING_SELECTORS:')
+    for s in missing:
+        print(s)
+    raise SystemExit(1)
+print('OK: parity table selectors all exposed in LNVAPFacade')

+ 55 - 5
QGVAPlayerDemo/QGVAPlayerDemo/ViewController.m

@@ -10,6 +10,7 @@
 
 @property (nonatomic, strong) UIButton *vapButton;
 @property (nonatomic, strong) UIButton *vapWrapButton;
+@property (nonatomic, strong) UIButton *vapFailButton;
 @property (nonatomic, strong) LNVAPPlayerView *playerView;
 @property (nonatomic, strong) LNVAPWrapView *wrapView;
 
@@ -32,6 +33,12 @@
     [_vapWrapButton setTitle:@"LN WrapView" forState:UIControlStateNormal];
     [_vapWrapButton addTarget:self action:@selector(playWithWrapView) forControlEvents:UIControlEventTouchUpInside];
     [self.view addSubview:_vapWrapButton];
+
+    _vapFailButton = [[UIButton alloc] initWithFrame:CGRectMake(0, CGRectGetMaxY(_vapWrapButton.frame) + 60, CGRectGetWidth(self.view.frame), 90)];
+    _vapFailButton.backgroundColor = [UIColor lightGrayColor];
+    [_vapFailButton setTitle:@"LN Fail Path" forState:UIControlStateNormal];
+    [_vapFailButton addTarget:self action:@selector(playInvalidPath) forControlEvents:UIControlEventTouchUpInside];
+    [self.view addSubview:_vapFailButton];
 }
 
 - (void)setupAudioSession {
@@ -48,12 +55,12 @@
     self.playerView.delegate = self;
     self.playerView.enterBackgroundOperation = LNEnterBackgroundOperationStop;
     [self.playerView lnEnableOldVersion:YES];
-    [self.playerView lnSetMute:YES];
+    [LNVAPFacade lnSetMutePlayer:self.playerView mute:YES];
     [self.playerView lnAddTapGestureWithTarget:self action:@selector(onPlayerTap:)];
     [self.view addSubview:self.playerView];
 
     NSString *resPath = [NSString stringWithFormat:@"%@/Resource/demo.mp4", NSBundle.mainBundle.resourcePath];
-    [self.playerView lnPlayWithFilePath:resPath repeatCount:-1];
+    [LNVAPFacade lnPlayPlayer:self.playerView filePath:resPath repeatCount:-1 delegate:self];
 }
 
 - (void)playWithWrapView {
@@ -62,28 +69,71 @@
     self.wrapView.contentModeOption = LNVAPWrapContentModeAspectFit;
     self.wrapView.autoDestroyAfterFinish = YES;
     self.wrapView.delegate = self;
-    [self.wrapView lnSetMute:YES];
+    [LNVAPFacade lnSetMuteWrap:self.wrapView mute:YES];
     [self.view addSubview:self.wrapView];
 
     NSString *resPath = [NSString stringWithFormat:@"%@/Resource/vap.mp4", NSBundle.mainBundle.resourcePath];
-    [self.wrapView lnPlayWithFilePath:resPath repeatCount:-1];
+    [LNVAPFacade lnPlayWrap:self.wrapView filePath:resPath repeatCount:-1 delegate:self];
 }
 
 - (void)onPlayerTap:(UIGestureRecognizer *)gesture {
-    [self.playerView lnStop];
+    [LNVAPFacade lnStopPlayer:self.playerView];
     [gesture.view removeFromSuperview];
 }
 
+- (void)playInvalidPath {
+    [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.view addSubview:self.playerView];
+    [LNVAPFacade lnPlayPlayer:self.playerView filePath:@"/tmp/not_exist_vap.mp4" repeatCount:0 delegate:self];
+}
+
+- (void)lnPlayerDidStart:(LNVAPPlayerView *)playerView {
+    NSLog(@"[LN Demo OC] player did start");
+}
+
+- (void)lnPlayerDidPlay:(LNVAPPlayerView *)playerView frame:(LNMP4AnimatedImageFrame *)frame {
+    NSLog(@"[LN Demo OC] player did play frame=%ld", (long)frame.frameIndex);
+}
+
+- (void)lnPlayerDidFinish:(LNVAPPlayerView *)playerView totalFrameCount:(NSInteger)totalFrameCount {
+    NSLog(@"[LN Demo OC] player did finish total=%ld", (long)totalFrameCount);
+}
+
 - (void)lnPlayerDidStop:(LNVAPPlayerView *)playerView {
+    NSLog(@"[LN Demo OC] player did stop");
     dispatch_async(dispatch_get_main_queue(), ^{
         [playerView removeFromSuperview];
     });
 }
 
+- (void)lnPlayerDidFail:(LNVAPPlayerView *)playerView error:(NSError *)error {
+    NSLog(@"[LN Demo OC] player did fail error=%@", error);
+}
+
+- (void)lnWrapViewDidStart:(LNVAPWrapView *)wrapView {
+    NSLog(@"[LN Demo OC] wrap did start");
+}
+
+- (void)lnWrapViewDidPlay:(LNVAPWrapView *)wrapView frame:(LNMP4AnimatedImageFrame *)frame {
+    NSLog(@"[LN Demo OC] wrap did play frame=%ld", (long)frame.frameIndex);
+}
+
+- (void)lnWrapViewDidFinish:(LNVAPWrapView *)wrapView totalFrameCount:(NSInteger)totalFrameCount {
+    NSLog(@"[LN Demo OC] wrap did finish total=%ld", (long)totalFrameCount);
+}
+
 - (void)lnWrapViewDidStop:(LNVAPWrapView *)wrapView {
+    NSLog(@"[LN Demo OC] wrap did stop");
     dispatch_async(dispatch_get_main_queue(), ^{
         [wrapView removeFromSuperview];
     });
 }
 
+- (void)lnWrapViewDidFail:(LNVAPWrapView *)wrapView error:(NSError *)error {
+    NSLog(@"[LN Demo OC] wrap did fail error=%@", error);
+}
+
 @end

+ 39 - 3
QGVAPlayerDemoSwift/QGVAPlayerDemoSwift/ViewController.swift

@@ -6,6 +6,7 @@ import QGVAPlayer
 
 final class ViewController: UIViewController, LNVAPPlaybackDelegate {
     private let vapButton = UIButton()
+    private let failButton = UIButton()
     private var playerView: LNVAPPlayerView?
 
     override func viewDidLoad() {
@@ -16,6 +17,12 @@ final class ViewController: UIViewController, LNVAPPlaybackDelegate {
         vapButton.setTitle("LN Swift API", for: .normal)
         vapButton.addTarget(self, action: #selector(playVap), for: .touchUpInside)
         view.addSubview(vapButton)
+
+        failButton.frame = CGRect(x: 0, y: 220, width: view.frame.width, height: 90)
+        failButton.backgroundColor = .lightGray
+        failButton.setTitle("LN Swift Fail Path", for: .normal)
+        failButton.addTarget(self, action: #selector(playInvalidPath), for: .touchUpInside)
+        view.addSubview(failButton)
     }
 
     @objc private func playVap() {
@@ -24,24 +31,53 @@ final class ViewController: UIViewController, LNVAPPlaybackDelegate {
         player.center = view.center
         player.delegate = self
         player.enterBackgroundOperation = .stop
-        player.lnSetMute(true)
+        LNVAPFacade.lnSetMutePlayer(player, mute: 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)
+        LNVAPFacade.lnPlayPlayer(player, filePath: mp4Path, repeatCount: -1, delegate: self)
     }
 
     @objc private func onTap(_ gesture: UIGestureRecognizer) {
-        playerView?.lnStop()
+        if let playerView {
+            LNVAPFacade.lnStopPlayer(playerView)
+        }
         gesture.view?.removeFromSuperview()
     }
 
+    @objc private func playInvalidPath() {
+        playerView?.removeFromSuperview()
+        let player = LNVAPFacade.lnMakePlayerView(frame: view.bounds)
+        player.center = view.center
+        player.delegate = self
+        view.addSubview(player)
+        playerView = player
+        LNVAPFacade.lnPlayPlayer(player, filePath: "/tmp/not_exist_vap.mp4", repeatCount: 0, delegate: self)
+    }
+
+    func lnPlayerDidStart(_ playerView: LNVAPPlayerView) {
+        print("[LN Demo Swift] player did start")
+    }
+
+    func lnPlayerDidPlay(_ playerView: LNVAPPlayerView, frame: LNMP4AnimatedImageFrame) {
+        print("[LN Demo Swift] player did play frame=\\(frame.frameIndex)")
+    }
+
+    func lnPlayerDidFinish(_ playerView: LNVAPPlayerView, totalFrameCount: Int) {
+        print("[LN Demo Swift] player did finish total=\\(totalFrameCount)")
+    }
+
     func lnPlayerDidStop(_ playerView: LNVAPPlayerView) {
+        print("[LN Demo Swift] player did stop")
         DispatchQueue.main.async {
             playerView.removeFromSuperview()
         }
     }
+
+    func lnPlayerDidFail(_ playerView: LNVAPPlayerView, error: NSError) {
+        print("[LN Demo Swift] player did fail error=\\(error)")
+    }
 }