iOS 0.5.48: show seek bar on F1 VOD (VLC live path reports no duration).
This commit is contained in:
parent
ef566e2756
commit
011891c89c
4 changed files with 99 additions and 11 deletions
|
|
@ -345,7 +345,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 60;
|
CURRENT_PROJECT_VERSION = 61;
|
||||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|
@ -360,7 +360,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks"
|
"@executable_path/Frameworks"
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.5.47;
|
MARKETING_VERSION = 0.5.48;
|
||||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|
@ -378,7 +378,7 @@
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 60;
|
CURRENT_PROJECT_VERSION = 61;
|
||||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
ENABLE_PREVIEWS = YES;
|
ENABLE_PREVIEWS = YES;
|
||||||
|
|
@ -393,7 +393,7 @@
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks"
|
"@executable_path/Frameworks"
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 0.5.47;
|
MARKETING_VERSION = 0.5.48;
|
||||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,8 @@ final class ClearKeyDashProxy {
|
||||||
private var lastMediaPlaylistByPath: [String: String] = [:]
|
private var lastMediaPlaylistByPath: [String: String] = [:]
|
||||||
/// Gedeelde MPD-snapshot zodat /hls/v.m3u8 en /hls/a.m3u8 dezelfde live-edge zien
|
/// Gedeelde MPD-snapshot zodat /hls/v.m3u8 en /hls/a.m3u8 dezelfde live-edge zien
|
||||||
/// (anders A/V MEDIA-SEQUENCE-desync → stall na ~8–10s).
|
/// (anders A/V MEDIA-SEQUENCE-desync → stall na ~8–10s).
|
||||||
|
/// Som van EXTINF bij VOD/ENDLIST — VLC live-pad meldt anders duur 0 / niet-zoekbaar.
|
||||||
|
private var lastVodDurationMs: Int64 = 0
|
||||||
private var cachedMpdXML: String?
|
private var cachedMpdXML: String?
|
||||||
private var cachedMpdAt: Date?
|
private var cachedMpdAt: Date?
|
||||||
private let mpdCacheTTL: TimeInterval = 0.8
|
private let mpdCacheTTL: TimeInterval = 0.8
|
||||||
|
|
@ -134,6 +136,11 @@ final class ClearKeyDashProxy {
|
||||||
struct StartResult {
|
struct StartResult {
|
||||||
let url: URL
|
let url: URL
|
||||||
let session: Int
|
let session: Int
|
||||||
|
let vodDurationMs: Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func vodDurationMs() -> Int64 {
|
||||||
|
withState { lastVodDurationMs }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start (of herstart) proxy voor dit manifest + keys. Geeft lokale MPD- of m3u8-URL terug.
|
/// Start (of herstart) proxy voor dit manifest + keys. Geeft lokale MPD- of m3u8-URL terug.
|
||||||
|
|
@ -165,6 +172,7 @@ final class ClearKeyDashProxy {
|
||||||
self.segmentSeqByKey = [:]
|
self.segmentSeqByKey = [:]
|
||||||
self.nextStableSeq = 1
|
self.nextStableSeq = 1
|
||||||
self.lastMediaPlaylistByPath = [:]
|
self.lastMediaPlaylistByPath = [:]
|
||||||
|
self.lastVodDurationMs = 0
|
||||||
self.cachedMpdXML = nil
|
self.cachedMpdXML = nil
|
||||||
self.cachedMpdAt = nil
|
self.cachedMpdAt = nil
|
||||||
self.cryptoLock.lock()
|
self.cryptoLock.lock()
|
||||||
|
|
@ -239,6 +247,10 @@ final class ClearKeyDashProxy {
|
||||||
"warmup \(kind, privacy: .public) ok bytes=\(body.count, privacy: .public) head=\(preview, privacy: .public)"
|
"warmup \(kind, privacy: .public) ok bytes=\(body.count, privacy: .public) head=\(preview, privacy: .public)"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if hls || preferHlsForAVPlayer {
|
||||||
|
await peekVodDuration(fromMaster: body)
|
||||||
|
}
|
||||||
|
|
||||||
// Prefetch init segments zodat CENC track-crypto klaar is vóór AVPlayer media vraagt.
|
// Prefetch init segments zodat CENC track-crypto klaar is vóór AVPlayer media vraagt.
|
||||||
if preferHlsForAVPlayer {
|
if preferHlsForAVPlayer {
|
||||||
await prefetchInitsForAVPlayer()
|
await prefetchInitsForAVPlayer()
|
||||||
|
|
@ -247,7 +259,37 @@ final class ClearKeyDashProxy {
|
||||||
guard let local = URL(string: "http://127.0.0.1:\(bound.port)\(entryPath)") else {
|
guard let local = URL(string: "http://127.0.0.1:\(bound.port)\(entryPath)") else {
|
||||||
throw ProxyError.bindFailed
|
throw ProxyError.bindFailed
|
||||||
}
|
}
|
||||||
return StartResult(url: local, session: gen)
|
return StartResult(url: local, session: gen, vodDurationMs: withState { lastVodDurationMs })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Haal één media-playlist op zodat we ENDLIST/duur kennen vóór VLC start.
|
||||||
|
private func peekVodDuration(fromMaster data: Data) async {
|
||||||
|
let text = String(data: data, encoding: .utf8) ?? String(decoding: data, as: UTF8.self)
|
||||||
|
var videoPath: String?
|
||||||
|
var audioPath: String?
|
||||||
|
for raw in text.components(separatedBy: .newlines) {
|
||||||
|
let line = raw.trimmingCharacters(in: .whitespaces)
|
||||||
|
let uri: String?
|
||||||
|
if line.hasPrefix("#"), let q = Self.hlsMediaAttr(line, "URI") {
|
||||||
|
uri = q
|
||||||
|
} else if line.hasPrefix("http://127.0.0.1:") {
|
||||||
|
uri = line
|
||||||
|
} else {
|
||||||
|
uri = nil
|
||||||
|
}
|
||||||
|
guard let uri, let url = URL(string: uri) else { continue }
|
||||||
|
let path = url.path + (url.query.map { "?\($0)" } ?? "")
|
||||||
|
guard path.hasPrefix("/a/") else { continue }
|
||||||
|
if path.contains("ck=v") { videoPath = path }
|
||||||
|
else if path.contains("ck=a") { audioPath = path }
|
||||||
|
else if videoPath == nil { videoPath = path }
|
||||||
|
}
|
||||||
|
guard let path = videoPath ?? audioPath else { return }
|
||||||
|
_ = try? await payload(for: path)
|
||||||
|
let ms = withState { lastVodDurationMs }
|
||||||
|
if ms > 0 {
|
||||||
|
proxyLog.info("vod duration \(ms, privacy: .public)ms from peek")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func prefetchInitsForAVPlayer() async {
|
private func prefetchInitsForAVPlayer() async {
|
||||||
|
|
@ -349,6 +391,7 @@ final class ClearKeyDashProxy {
|
||||||
segmentSeqByKey = [:]
|
segmentSeqByKey = [:]
|
||||||
nextStableSeq = 1
|
nextStableSeq = 1
|
||||||
lastMediaPlaylistByPath = [:]
|
lastMediaPlaylistByPath = [:]
|
||||||
|
lastVodDurationMs = 0
|
||||||
cachedMpdXML = nil
|
cachedMpdXML = nil
|
||||||
cachedMpdAt = nil
|
cachedMpdAt = nil
|
||||||
cryptoLock.lock()
|
cryptoLock.lock()
|
||||||
|
|
@ -746,7 +789,13 @@ final class ClearKeyDashProxy {
|
||||||
}
|
}
|
||||||
let window = Array(segments.suffix(from: startIdx))
|
let window = Array(segments.suffix(from: startIdx))
|
||||||
let seq = seqBase + startIdx
|
let seq = seqBase + startIdx
|
||||||
proxyLog.info("hls rewrite segs=\(segments.count, privacy: .public) keep=\(window.count, privacy: .public) endlist=\(hasEndList, privacy: .public) type=\(playlistType ?? "-", privacy: .public) f1=\(isF1, privacy: .public) startIdx=\(startIdx, privacy: .public)")
|
if isComplete || (isF1 && startIdx == 0 && window.count > 40) {
|
||||||
|
let ms = Self.playlistDurationMs(window.map(\.inf), fallbackSeconds: targetDuration)
|
||||||
|
withState {
|
||||||
|
if ms > self.lastVodDurationMs { self.lastVodDurationMs = ms }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxyLog.info("hls rewrite segs=\(segments.count, privacy: .public) keep=\(window.count, privacy: .public) endlist=\(hasEndList, privacy: .public) type=\(playlistType ?? "-", privacy: .public) f1=\(isF1, privacy: .public) startIdx=\(startIdx, privacy: .public) vodMs=\(self.lastVodDurationMs, privacy: .public)")
|
||||||
|
|
||||||
out.append("#EXTM3U")
|
out.append("#EXTM3U")
|
||||||
out.append("#EXT-X-VERSION:7")
|
out.append("#EXT-X-VERSION:7")
|
||||||
|
|
@ -778,6 +827,21 @@ final class ClearKeyDashProxy {
|
||||||
return out.joined(separator: "\n")
|
return out.joined(separator: "\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func playlistDurationMs(_ infLines: [String], fallbackSeconds: Int) -> Int64 {
|
||||||
|
var total: Double = 0
|
||||||
|
for inf in infLines {
|
||||||
|
let raw = inf.trimmingCharacters(in: .whitespaces)
|
||||||
|
let rest = raw.hasPrefix("#EXTINF:") ? String(raw.dropFirst(8)) : raw
|
||||||
|
let token = rest.split(separator: ",", maxSplits: 1).first.map(String.init) ?? ""
|
||||||
|
if let seconds = Double(token.trimmingCharacters(in: .whitespaces)) {
|
||||||
|
total += seconds
|
||||||
|
} else {
|
||||||
|
total += Double(max(fallbackSeconds, 1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Int64(total * 1000)
|
||||||
|
}
|
||||||
|
|
||||||
/// Audio- vs video-playlist herkennen (ESPN: `audio-aac-…` vs `cmaf-cenc-…`).
|
/// Audio- vs video-playlist herkennen (ESPN: `audio-aac-…` vs `cmaf-cenc-…`).
|
||||||
private static func hlsMediaLane(for playlistURL: URL) -> CryptoLane {
|
private static func hlsMediaLane(for playlistURL: URL) -> CryptoLane {
|
||||||
let s = playlistURL.path.lowercased()
|
let s = playlistURL.path.lowercased()
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
private var stopping = false
|
private var stopping = false
|
||||||
private var stopGeneration = 0
|
private var stopGeneration = 0
|
||||||
private var didApplyPreferredAudio = false
|
private var didApplyPreferredAudio = false
|
||||||
|
private var knownVodDurationMs: Int64 = 0
|
||||||
|
|
||||||
override init() {
|
override init() {
|
||||||
// Shared library = alle demux/access plugins (DASH adaptive in VLC 4).
|
// Shared library = alle demux/access plugins (DASH adaptive in VLC 4).
|
||||||
|
|
@ -196,6 +197,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
audioTracks = []
|
audioTracks = []
|
||||||
canSeek = false
|
canSeek = false
|
||||||
didApplyPreferredAudio = false
|
didApplyPreferredAudio = false
|
||||||
|
knownVodDurationMs = 0
|
||||||
liveMode = false
|
liveMode = false
|
||||||
clearKeyProxyActive = false
|
clearKeyProxyActive = false
|
||||||
plainVideoView?.layoutIfNeeded()
|
plainVideoView?.layoutIfNeeded()
|
||||||
|
|
@ -262,6 +264,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
liveStartedAt = Date()
|
liveStartedAt = Date()
|
||||||
canSeek = false
|
canSeek = false
|
||||||
didApplyPreferredAudio = false
|
didApplyPreferredAudio = false
|
||||||
|
knownVodDurationMs = 0
|
||||||
liveMode = true
|
liveMode = true
|
||||||
usePipDrawable = false
|
usePipDrawable = false
|
||||||
pipPossible = false
|
pipPossible = false
|
||||||
|
|
@ -281,6 +284,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
)
|
)
|
||||||
playURL = started.url
|
playURL = started.url
|
||||||
proxySession = started.session
|
proxySession = started.session
|
||||||
|
if started.vodDurationMs > 3000 {
|
||||||
|
knownVodDurationMs = started.vodDurationMs
|
||||||
|
let clamped = Int32(min(started.vodDurationMs, Int64(Int32.max)))
|
||||||
|
lengthMs = clamped
|
||||||
|
lengthText = Self.format(ms: clamped)
|
||||||
|
canSeek = true
|
||||||
|
}
|
||||||
// Lokale clear-HLS of remote HLS.
|
// Lokale clear-HLS of remote HLS.
|
||||||
if preferHls || ClearKeyDashProxy.isHlsURL(started.url) || ClearKeyDashProxy.isHlsURL(url) {
|
if preferHls || ClearKeyDashProxy.isHlsURL(started.url) || ClearKeyDashProxy.isHlsURL(url) {
|
||||||
playFormat = "hls"
|
playFormat = "hls"
|
||||||
|
|
@ -345,7 +355,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
media.delegate = self
|
media.delegate = self
|
||||||
Self.applyLiveOptions(to: media, url: url, format: format, viaLocalProxy: clearKeyProxyActive)
|
Self.applyLiveOptions(
|
||||||
|
to: media,
|
||||||
|
url: url,
|
||||||
|
format: format,
|
||||||
|
viaLocalProxy: clearKeyProxyActive,
|
||||||
|
asVod: knownVodDurationMs > 3000
|
||||||
|
)
|
||||||
// Drawable één keer vóór play — nooit opnieuw mid-stream (audio-only / crash).
|
// Drawable één keer vóór play — nooit opnieuw mid-stream (audio-only / crash).
|
||||||
if mediaPlayer.drawable == nil {
|
if mediaPlayer.drawable == nil {
|
||||||
applyDrawable(force: true)
|
applyDrawable(force: true)
|
||||||
|
|
@ -399,10 +415,14 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
to media: VLCMedia,
|
to media: VLCMedia,
|
||||||
url: URL,
|
url: URL,
|
||||||
format: String?,
|
format: String?,
|
||||||
viaLocalProxy: Bool
|
viaLocalProxy: Bool,
|
||||||
|
asVod: Bool = false
|
||||||
) {
|
) {
|
||||||
// Stabiele live-start: te lage caching + drop-late-frames liet streams meteen stoppen.
|
// Stabiele live-start: te lage caching + drop-late-frames liet streams meteen stoppen.
|
||||||
if viaLocalProxy {
|
// VOD (F1-replay): géén live-caching, anders blijft VLC isSeekable=false / duur=0.
|
||||||
|
if asVod {
|
||||||
|
media.addOption(":network-caching=1500")
|
||||||
|
} else if viaLocalProxy {
|
||||||
media.addOption(":network-caching=1200")
|
media.addOption(":network-caching=1200")
|
||||||
media.addOption(":live-caching=1000")
|
media.addOption(":live-caching=1000")
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -740,6 +760,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
self.liveMode = false
|
self.liveMode = false
|
||||||
self.canSeek = false
|
self.canSeek = false
|
||||||
self.didApplyPreferredAudio = false
|
self.didApplyPreferredAudio = false
|
||||||
|
self.knownVodDurationMs = 0
|
||||||
self.isPlaying = false
|
self.isPlaying = false
|
||||||
self.hasError = false
|
self.hasError = false
|
||||||
self.errorMessage = nil
|
self.errorMessage = nil
|
||||||
|
|
@ -854,9 +875,12 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
if l > 0 { lengthMs = l }
|
if l > 0 { lengthMs = l }
|
||||||
position = Float(mediaPlayer.position)
|
position = Float(mediaPlayer.position)
|
||||||
timeText = Self.format(ms: t)
|
timeText = Self.format(ms: t)
|
||||||
|
if l <= 0, knownVodDurationMs > 3000 {
|
||||||
|
lengthMs = Int32(min(knownVodDurationMs, Int64(Int32.max)))
|
||||||
|
}
|
||||||
lengthText = Self.format(ms: lengthMs)
|
lengthText = Self.format(ms: lengthMs)
|
||||||
isPlaying = mediaPlayer.isPlaying
|
isPlaying = mediaPlayer.isPlaying
|
||||||
let seekable = mediaPlayer.isSeekable && lengthMs > 3000
|
let seekable = (knownVodDurationMs > 3000 || mediaPlayer.isSeekable) && lengthMs > 3000
|
||||||
if seekable != canSeek { canSeek = seekable }
|
if seekable != canSeek { canSeek = seekable }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ struct SettingsScreen: View {
|
||||||
|
|
||||||
Section("Info") {
|
Section("Info") {
|
||||||
LabeledContent("App", value: "VMC iOS")
|
LabeledContent("App", value: "VMC iOS")
|
||||||
LabeledContent("Versie", value: "0.5.47")
|
LabeledContent("Versie", value: "0.5.48")
|
||||||
LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit")
|
LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit")
|
||||||
LabeledContent("Events", value: app.eventsEnabled ? "Aan" : "Uit")
|
LabeledContent("Events", value: app.eventsEnabled ? "Aan" : "Uit")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue