Fix ClearKey live/events on VLCKit 4 (adaptive demux, async proxy, no PiP vout).
This commit is contained in:
parent
878aa86121
commit
77ddc8b6a3
4 changed files with 179 additions and 107 deletions
|
|
@ -337,7 +337,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -352,7 +352,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.3;
|
||||
MARKETING_VERSION = 0.5.4;
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -370,7 +370,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 15;
|
||||
CURRENT_PROJECT_VERSION = 16;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -385,7 +385,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.3;
|
||||
MARKETING_VERSION = 0.5.4;
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -31,77 +31,96 @@ final class ClearKeyDashProxy {
|
|||
|
||||
/// Start (of herstart) proxy voor dit manifest + keys. Geeft lokale MPD-URL terug.
|
||||
/// Warmt de MPD-fetch op zodat VLC niet meteen een lege/fout-response krijgt.
|
||||
func start(manifestURL: URL, keys: [ClearKeyEntry]) throws -> URL {
|
||||
func start(manifestURL: URL, keys: [ClearKeyEntry]) async throws -> URL {
|
||||
let map = ClearKeyKeyMap(entries: keys)
|
||||
guard !map.isEmpty else {
|
||||
throw ProxyError.noKeys
|
||||
}
|
||||
stop()
|
||||
remoteManifest = manifestURL
|
||||
keyMap = map
|
||||
cryptoState = CencMp4Decryptor.SessionState()
|
||||
generation += 1
|
||||
|
||||
let boundPort: UInt16 = try await withCheckedThrowingContinuation { cont in
|
||||
queue.async {
|
||||
self.stopLocked()
|
||||
self.remoteManifest = manifestURL
|
||||
self.keyMap = map
|
||||
self.cryptoState = CencMp4Decryptor.SessionState()
|
||||
self.generation += 1
|
||||
|
||||
final class ResumeOnce: @unchecked Sendable {
|
||||
private var done = false
|
||||
private let lock = NSLock()
|
||||
func run(_ body: () -> Void) {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
guard !done else { return }
|
||||
done = true
|
||||
body()
|
||||
}
|
||||
}
|
||||
let once = ResumeOnce()
|
||||
|
||||
do {
|
||||
let params = NWParameters.tcp
|
||||
params.allowLocalEndpointReuse = true
|
||||
let listener = try NWListener(using: params, on: .any)
|
||||
self.listener = listener
|
||||
|
||||
listener.stateUpdateHandler = { [weak self] state in
|
||||
guard let self else { return }
|
||||
switch state {
|
||||
case .ready:
|
||||
let p = listener.port?.rawValue ?? 0
|
||||
self.port = p
|
||||
proxyLog.info("listener ready on port \(p, privacy: .public)")
|
||||
once.run {
|
||||
if p > 0 {
|
||||
cont.resume(returning: p)
|
||||
} else {
|
||||
cont.resume(throwing: ProxyError.bindFailed)
|
||||
}
|
||||
}
|
||||
case .failed(let err):
|
||||
proxyLog.error("listener failed: \(String(describing: err), privacy: .public)")
|
||||
once.run { cont.resume(throwing: err) }
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
listener.newConnectionHandler = { [weak self] conn in
|
||||
self?.handle(connection: conn)
|
||||
}
|
||||
listener.start(queue: self.queue)
|
||||
|
||||
self.queue.asyncAfter(deadline: .now() + 5) {
|
||||
once.run { cont.resume(throwing: ProxyError.bindFailed) }
|
||||
}
|
||||
} catch {
|
||||
once.run { cont.resume(throwing: error) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
guard boundPort > 0 else { throw ProxyError.bindFailed }
|
||||
let gen = generation
|
||||
|
||||
let params = NWParameters.tcp
|
||||
params.allowLocalEndpointReuse = true
|
||||
let listener = try NWListener(using: params, on: .any)
|
||||
self.listener = listener
|
||||
|
||||
let ready = DispatchSemaphore(value: 0)
|
||||
var readyError: Error?
|
||||
|
||||
listener.stateUpdateHandler = { [weak self] state in
|
||||
guard let self else { return }
|
||||
switch state {
|
||||
case .ready:
|
||||
if let p = listener.port?.rawValue {
|
||||
self.port = p
|
||||
}
|
||||
proxyLog.info("listener ready on port \(self.port, privacy: .public)")
|
||||
ready.signal()
|
||||
case .failed(let err):
|
||||
proxyLog.error("listener failed: \(String(describing: err), privacy: .public)")
|
||||
readyError = err
|
||||
ready.signal()
|
||||
default:
|
||||
break
|
||||
}
|
||||
let (body, _) = try await payload(for: "/manifest.mpd")
|
||||
guard !body.isEmpty, gen == generation else {
|
||||
throw ProxyError.emptyManifest
|
||||
}
|
||||
let preview = String(data: body.prefix(200), encoding: .utf8) ?? ""
|
||||
proxyLog.info("warmup MPD ok bytes=\(body.count, privacy: .public) head=\(preview, privacy: .public)")
|
||||
|
||||
listener.newConnectionHandler = { [weak self] conn in
|
||||
self?.handle(connection: conn)
|
||||
guard let local = URL(string: "http://127.0.0.1:\(boundPort)/manifest.mpd") else {
|
||||
throw ProxyError.bindFailed
|
||||
}
|
||||
|
||||
listener.start(queue: queue)
|
||||
_ = ready.wait(timeout: .now() + 5)
|
||||
if let readyError { throw readyError }
|
||||
guard port > 0 else { throw ProxyError.bindFailed }
|
||||
|
||||
// Warm-up: MPD ophalen + herschrijven vóór VLC start (live dynamic MPD's falen anders meteen).
|
||||
let warm = DispatchSemaphore(value: 0)
|
||||
var warmError: Error?
|
||||
Task {
|
||||
do {
|
||||
let (body, _) = try await self.payload(for: "/manifest.mpd")
|
||||
guard !body.isEmpty, gen == self.generation else {
|
||||
throw ProxyError.emptyManifest
|
||||
}
|
||||
let preview = String(data: body.prefix(200), encoding: .utf8) ?? ""
|
||||
proxyLog.info("warmup MPD ok bytes=\(body.count, privacy: .public) head=\(preview, privacy: .public)")
|
||||
} catch {
|
||||
warmError = error
|
||||
proxyLog.error("warmup failed: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
warm.signal()
|
||||
}
|
||||
_ = warm.wait(timeout: .now() + 20)
|
||||
if let warmError { throw warmError }
|
||||
|
||||
return URL(string: "http://127.0.0.1:\(port)/manifest.mpd")!
|
||||
return local
|
||||
}
|
||||
|
||||
func stop() {
|
||||
queue.sync { stopLocked() }
|
||||
}
|
||||
|
||||
private func stopLocked() {
|
||||
generation += 1
|
||||
listener?.cancel()
|
||||
listener = nil
|
||||
|
|
@ -303,7 +322,7 @@ final class ClearKeyDashProxy {
|
|||
var req = URLRequest(url: url)
|
||||
req.timeoutInterval = 25
|
||||
req.cachePolicy = .reloadIgnoringLocalCacheData
|
||||
req.setValue("VMC-iOS-ClearKey/0.5.2", forHTTPHeaderField: "User-Agent")
|
||||
req.setValue("VMC-iOS-ClearKey/0.5.4", forHTTPHeaderField: "User-Agent")
|
||||
req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges")
|
||||
let (data, resp) = try await urlSession.data(for: req)
|
||||
if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
|
||||
|
|
|
|||
|
|
@ -401,14 +401,14 @@ struct PlayerScreen: View {
|
|||
error = nil
|
||||
if request.isLive {
|
||||
let keys = request.needsClearKey ? (request.drm?.keys ?? []) : []
|
||||
player.playLive(
|
||||
await player.playLive(
|
||||
url: request.url,
|
||||
fallback: request.fallbackUrl,
|
||||
format: request.format,
|
||||
clearKeys: keys
|
||||
)
|
||||
// ClearKey/DASH: proxy-warmup + init/media + eventuele retry.
|
||||
let waitNs: UInt64 = request.needsClearKey ? 10_000_000_000 : 2_500_000_000
|
||||
let waitNs: UInt64 = request.needsClearKey ? 12_000_000_000 : 2_500_000_000
|
||||
try? await Task.sleep(nanoseconds: waitNs)
|
||||
if request.needsClearKey && (player.hasError || !player.isPlaying) {
|
||||
try? await Task.sleep(nanoseconds: 6_000_000_000)
|
||||
|
|
@ -423,7 +423,9 @@ struct PlayerScreen: View {
|
|||
}
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
loading = false
|
||||
player.refreshTracks()
|
||||
if !player.hasError {
|
||||
player.refreshTracks()
|
||||
}
|
||||
}
|
||||
|
||||
private func startHeartbeatIfNeeded() {
|
||||
|
|
|
|||
|
|
@ -30,9 +30,12 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
private let library: VLCLibrary
|
||||
let mediaPlayer: VLCMediaPlayer
|
||||
private let drawableHost = VLCPipDrawableHost()
|
||||
/// Plain UIView-target voor live/ClearKey (PiP sample-buffer-vout breekt DASH/proxy).
|
||||
private weak var plainVideoView: UIView?
|
||||
private var lengthMs: Int32 = 0
|
||||
private var tick: AnyCancellable?
|
||||
private var clearKeyProxyActive = false
|
||||
private var liveMode = false
|
||||
private var livePlayURL: URL?
|
||||
private var livePlayFormat: String?
|
||||
private var liveStartedAt: Date?
|
||||
|
|
@ -42,13 +45,12 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
private var sceneObserver: NSObjectProtocol?
|
||||
|
||||
override init() {
|
||||
// Eigen library-instance zodat opties/PiP-vout stabiel blijven.
|
||||
library = VLCLibrary(options: [])
|
||||
// Shared library = alle demux/access plugins (DASH adaptive in VLC 4).
|
||||
library = VLCLibrary.sharedLibrary()
|
||||
mediaPlayer = VLCMediaPlayer(library: library)
|
||||
super.init()
|
||||
mediaPlayer.delegate = self
|
||||
drawableHost.controller = self
|
||||
mediaPlayer.drawable = drawableHost
|
||||
configureAudioSession()
|
||||
observeSceneForAutoPip()
|
||||
}
|
||||
|
|
@ -60,8 +62,22 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
}
|
||||
|
||||
func attach(to view: UIView) {
|
||||
drawableHost.attach(to: view)
|
||||
mediaPlayer.drawable = drawableHost
|
||||
plainVideoView = view
|
||||
view.backgroundColor = .black
|
||||
applyDrawable()
|
||||
}
|
||||
|
||||
/// Live/ClearKey → plain UIView. VOD → PiP-drawable (AVSampleBuffer-pad).
|
||||
private func applyDrawable() {
|
||||
guard let view = plainVideoView else { return }
|
||||
if liveMode || clearKeyProxyActive {
|
||||
pipPossible = false
|
||||
drawableHost.detachVideo()
|
||||
mediaPlayer.drawable = view
|
||||
} else {
|
||||
drawableHost.attach(to: view)
|
||||
mediaPlayer.drawable = drawableHost
|
||||
}
|
||||
}
|
||||
|
||||
func startPictureInPicture() {
|
||||
|
|
@ -83,7 +99,15 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
stateText = "Verbinden…"
|
||||
subtitleTracks = []
|
||||
audioTracks = []
|
||||
let media = VLCMedia(url: url)
|
||||
liveMode = false
|
||||
clearKeyProxyActive = false
|
||||
applyDrawable()
|
||||
guard let media = VLCMedia(url: url) else {
|
||||
hasError = true
|
||||
errorMessage = "Kon media niet openen."
|
||||
stateText = "Fout"
|
||||
return
|
||||
}
|
||||
media.delegate = self
|
||||
media.addOption(":network-caching=2500")
|
||||
media.addOption(":http-reconnect")
|
||||
|
|
@ -118,13 +142,14 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
fallback: URL? = nil,
|
||||
format: String? = nil,
|
||||
clearKeys: [ClearKeyEntry] = []
|
||||
) {
|
||||
) async {
|
||||
hasError = false
|
||||
errorMessage = nil
|
||||
stateText = "Live laden…"
|
||||
liveRetryCount = 0
|
||||
everPlayed = false
|
||||
liveStartedAt = Date()
|
||||
liveMode = true
|
||||
|
||||
var playURL = url
|
||||
var playFormat = format
|
||||
|
|
@ -132,11 +157,11 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
|
||||
if !clearKeys.isEmpty {
|
||||
do {
|
||||
// VLCKit speelt geen CENC/ClearKey zelf; lokale proxy decrypt segmenten.
|
||||
playURL = try ClearKeyDashProxy.shared.start(manifestURL: url, keys: clearKeys)
|
||||
stateText = "ClearKey laden…"
|
||||
// Async: blokkeer de main thread niet (watchdog-crash).
|
||||
playURL = try await ClearKeyDashProxy.shared.start(manifestURL: url, keys: clearKeys)
|
||||
playFormat = "dash"
|
||||
clearKeyProxyActive = true
|
||||
stateText = "ClearKey laden…"
|
||||
} catch {
|
||||
hasError = true
|
||||
errorMessage = dutchClearKeyError(error)
|
||||
|
|
@ -145,6 +170,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
}
|
||||
}
|
||||
|
||||
applyDrawable()
|
||||
livePlayURL = playURL
|
||||
livePlayFormat = playFormat
|
||||
startLiveMedia(url: playURL, format: playFormat)
|
||||
|
|
@ -159,7 +185,12 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
}
|
||||
|
||||
private func startLiveMedia(url: URL, format: String?) {
|
||||
let media = VLCMedia(url: url)
|
||||
guard let media = VLCMedia(url: url) else {
|
||||
hasError = true
|
||||
errorMessage = "Kon live-stream niet openen."
|
||||
stateText = "Fout"
|
||||
return
|
||||
}
|
||||
media.delegate = self
|
||||
Self.applyLiveOptions(to: media, url: url, format: format)
|
||||
mediaPlayer.media = media
|
||||
|
|
@ -204,22 +235,19 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
url: URL,
|
||||
format: String?
|
||||
) {
|
||||
media.addOption(":network-caching=1200")
|
||||
media.addOption(":network-caching=1800")
|
||||
media.addOption(":http-reconnect")
|
||||
media.addOption(":live-caching=1000")
|
||||
media.addOption(":live-caching=1500")
|
||||
media.addOption(":freetype-rel-fontsize=\(subtitleRelFontSize)")
|
||||
// VLC 4: unified adaptive demux (DASH+HLS). Forceer GEEN :demux=dash (VLC3-module).
|
||||
media.addOption(":adaptive-logic=highest")
|
||||
media.addOption(":adaptive-use-access")
|
||||
|
||||
let fmt = (format ?? "").lowercased()
|
||||
let path = url.absoluteString.lowercased()
|
||||
let isDash = fmt == "dash" || path.contains(".mpd")
|
||||
let isHls = fmt == "hls" || path.contains(".m3u8") || path.contains("m3u8")
|
||||
|
||||
if isDash {
|
||||
media.addOption(":demux=dash")
|
||||
media.addOption(":adaptive-logic=highest")
|
||||
media.addOption(":adaptive-use-access")
|
||||
} else if isHls {
|
||||
media.addOption(":demux=hls")
|
||||
if isHls {
|
||||
// HLS mag nog steeds via adaptive; geen aparte demux forceren.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -284,8 +312,18 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
}
|
||||
|
||||
func refreshTracks() {
|
||||
// VLCKit 4 kan crashen op track-API tijdens error/opening — altijd defensief.
|
||||
guard mediaPlayer.media != nil else {
|
||||
subtitleTracks = [PlayerTrack(id: PlayerTrack.offId, name: "Uit")]
|
||||
audioTracks = []
|
||||
return
|
||||
}
|
||||
let textList = mediaPlayer.textTracks
|
||||
let audioList = mediaPlayer.audioTracks
|
||||
|
||||
var subs: [PlayerTrack] = [PlayerTrack(id: PlayerTrack.offId, name: "Uit")]
|
||||
for (index, track) in mediaPlayer.textTracks.enumerated() {
|
||||
var selectedSub = PlayerTrack.offId
|
||||
for (index, track) in textList.enumerated() {
|
||||
let label = Self.displayTrackLabel(
|
||||
rawName: track.trackName,
|
||||
fallbackIndex: index + 1,
|
||||
|
|
@ -294,15 +332,14 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
descriptionHint: track.trackDescription
|
||||
)
|
||||
subs.append(PlayerTrack(id: track.trackId, name: label))
|
||||
if track.isSelected { currentSubtitleId = track.trackId }
|
||||
}
|
||||
if !mediaPlayer.textTracks.contains(where: \.isSelected) {
|
||||
currentSubtitleId = PlayerTrack.offId
|
||||
if track.isSelected { selectedSub = track.trackId }
|
||||
}
|
||||
subtitleTracks = subs
|
||||
currentSubtitleId = selectedSub
|
||||
|
||||
var audios: [PlayerTrack] = []
|
||||
for (index, track) in mediaPlayer.audioTracks.enumerated() {
|
||||
var selectedAudio = currentAudioId
|
||||
for (index, track) in audioList.enumerated() {
|
||||
let label = Self.displayTrackLabel(
|
||||
rawName: track.trackName,
|
||||
fallbackIndex: index + 1,
|
||||
|
|
@ -311,12 +348,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
descriptionHint: track.trackDescription
|
||||
)
|
||||
audios.append(PlayerTrack(id: track.trackId, name: label))
|
||||
if track.isSelected { currentAudioId = track.trackId }
|
||||
if track.isSelected { selectedAudio = track.trackId }
|
||||
}
|
||||
if currentAudioId.isEmpty, let first = audios.first {
|
||||
currentAudioId = first.id
|
||||
if selectedAudio.isEmpty, let first = audios.first {
|
||||
selectedAudio = first.id
|
||||
}
|
||||
audioTracks = audios
|
||||
currentAudioId = selectedAudio
|
||||
}
|
||||
|
||||
private enum TrackKind { case subtitle, audio }
|
||||
|
|
@ -432,7 +470,9 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
func stop() {
|
||||
tick?.cancel()
|
||||
tick = nil
|
||||
stopPictureInPicture()
|
||||
if pipActive {
|
||||
stopPictureInPicture()
|
||||
}
|
||||
mediaPlayer.stop()
|
||||
mediaPlayer.media = nil
|
||||
livePlayURL = nil
|
||||
|
|
@ -440,6 +480,10 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
liveStartedAt = nil
|
||||
liveRetryCount = 0
|
||||
everPlayed = false
|
||||
liveMode = false
|
||||
pipWindow = nil
|
||||
pipPossible = false
|
||||
pipActive = false
|
||||
if clearKeyProxyActive {
|
||||
ClearKeyDashProxy.shared.stop()
|
||||
clearKeyProxyActive = false
|
||||
|
|
@ -452,7 +496,9 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
DispatchQueue.main.async { [weak self] in
|
||||
guard let self else { return }
|
||||
self.isPlaying = self.mediaPlayer.isPlaying
|
||||
self.invalidatePipPlaybackState()
|
||||
if self.pipPossible {
|
||||
self.invalidatePipPlaybackState()
|
||||
}
|
||||
switch newState {
|
||||
case .buffering: self.stateText = "Bufferen…"
|
||||
case .opening: self.stateText = "Openen…"
|
||||
|
|
@ -468,7 +514,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
if self.clearKeyProxyActive && !self.everPlayed {
|
||||
self.stateText = "Verbinden…"
|
||||
self.retryLiveIfNeeded()
|
||||
} else {
|
||||
} else if !self.hasError {
|
||||
self.stateText = "Gestopt"
|
||||
}
|
||||
case .error:
|
||||
|
|
@ -606,7 +652,12 @@ final class VLCPipDrawableHost: NSObject, VLCDrawable, VLCPictureInPictureDrawab
|
|||
}
|
||||
}
|
||||
|
||||
// VLCDrawable
|
||||
func detachVideo() {
|
||||
videoSubview?.removeFromSuperview()
|
||||
// Houd referentie; VLC kan hem opnieuw addSubview'en.
|
||||
}
|
||||
|
||||
// VLCDrawable (VLCKit 4: methods, not properties)
|
||||
func addSubview(_ view: UIView) {
|
||||
videoSubview = view
|
||||
guard let container else { return }
|
||||
|
|
@ -618,15 +669,16 @@ final class VLCPipDrawableHost: NSObject, VLCDrawable, VLCPictureInPictureDrawab
|
|||
view.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
}
|
||||
|
||||
var bounds: CGRect {
|
||||
func bounds() -> CGRect {
|
||||
container?.bounds ?? .zero
|
||||
}
|
||||
|
||||
// VLCPictureInPictureDrawable
|
||||
var mediaController: VLCPictureInPictureMediaControlling { self }
|
||||
func mediaController() -> (any VLCPictureInPictureMediaControlling)? { self }
|
||||
|
||||
func pictureInPictureReady() -> (VLCPictureInPictureWindowControlling) -> Void {
|
||||
func pictureInPictureReady() -> (((any VLCPictureInPictureWindowControlling)?) -> Void)? {
|
||||
{ [weak self] pip in
|
||||
guard let pip else { return }
|
||||
self?.controller?.onPictureInPictureReady(pip)
|
||||
}
|
||||
}
|
||||
|
|
@ -640,13 +692,12 @@ final class VLCPipDrawableHost: NSObject, VLCDrawable, VLCPictureInPictureDrawab
|
|||
controller?.mediaPlayer.pause()
|
||||
}
|
||||
|
||||
@objc(seekBy:completion:)
|
||||
func seekBy(_ offset: Int64, completion: @escaping () -> Void) {
|
||||
func seek(by offset: Int64, completion: @escaping () -> Void) {
|
||||
guard let player = controller?.mediaPlayer else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
let ok = player.jump(withOffset: Int(offset), completion: completion)
|
||||
let ok = player.jump(withOffset: Int32(clamping: offset), completion: completion)
|
||||
if !ok { completion() }
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue