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