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_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 60;
|
||||
CURRENT_PROJECT_VERSION = 61;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -360,7 +360,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.47;
|
||||
MARKETING_VERSION = 0.5.48;
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
@ -378,7 +378,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 60;
|
||||
CURRENT_PROJECT_VERSION = 61;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -393,7 +393,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.47;
|
||||
MARKETING_VERSION = 0.5.48;
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -61,6 +61,8 @@ final class ClearKeyDashProxy {
|
|||
private var lastMediaPlaylistByPath: [String: String] = [:]
|
||||
/// 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).
|
||||
/// 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 cachedMpdAt: Date?
|
||||
private let mpdCacheTTL: TimeInterval = 0.8
|
||||
|
|
@ -134,6 +136,11 @@ final class ClearKeyDashProxy {
|
|||
struct StartResult {
|
||||
let url: URL
|
||||
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.
|
||||
|
|
@ -165,6 +172,7 @@ final class ClearKeyDashProxy {
|
|||
self.segmentSeqByKey = [:]
|
||||
self.nextStableSeq = 1
|
||||
self.lastMediaPlaylistByPath = [:]
|
||||
self.lastVodDurationMs = 0
|
||||
self.cachedMpdXML = nil
|
||||
self.cachedMpdAt = nil
|
||||
self.cryptoLock.lock()
|
||||
|
|
@ -239,6 +247,10 @@ final class ClearKeyDashProxy {
|
|||
"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.
|
||||
if preferHlsForAVPlayer {
|
||||
await prefetchInitsForAVPlayer()
|
||||
|
|
@ -247,7 +259,37 @@ final class ClearKeyDashProxy {
|
|||
guard let local = URL(string: "http://127.0.0.1:\(bound.port)\(entryPath)") else {
|
||||
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 {
|
||||
|
|
@ -349,6 +391,7 @@ final class ClearKeyDashProxy {
|
|||
segmentSeqByKey = [:]
|
||||
nextStableSeq = 1
|
||||
lastMediaPlaylistByPath = [:]
|
||||
lastVodDurationMs = 0
|
||||
cachedMpdXML = nil
|
||||
cachedMpdAt = nil
|
||||
cryptoLock.lock()
|
||||
|
|
@ -746,7 +789,13 @@ final class ClearKeyDashProxy {
|
|||
}
|
||||
let window = Array(segments.suffix(from: 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("#EXT-X-VERSION:7")
|
||||
|
|
@ -778,6 +827,21 @@ final class ClearKeyDashProxy {
|
|||
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-…`).
|
||||
private static func hlsMediaLane(for playlistURL: URL) -> CryptoLane {
|
||||
let s = playlistURL.path.lowercased()
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
private var stopping = false
|
||||
private var stopGeneration = 0
|
||||
private var didApplyPreferredAudio = false
|
||||
private var knownVodDurationMs: Int64 = 0
|
||||
|
||||
override init() {
|
||||
// Shared library = alle demux/access plugins (DASH adaptive in VLC 4).
|
||||
|
|
@ -196,6 +197,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
audioTracks = []
|
||||
canSeek = false
|
||||
didApplyPreferredAudio = false
|
||||
knownVodDurationMs = 0
|
||||
liveMode = false
|
||||
clearKeyProxyActive = false
|
||||
plainVideoView?.layoutIfNeeded()
|
||||
|
|
@ -262,6 +264,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
liveStartedAt = Date()
|
||||
canSeek = false
|
||||
didApplyPreferredAudio = false
|
||||
knownVodDurationMs = 0
|
||||
liveMode = true
|
||||
usePipDrawable = false
|
||||
pipPossible = false
|
||||
|
|
@ -281,6 +284,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
)
|
||||
playURL = started.url
|
||||
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.
|
||||
if preferHls || ClearKeyDashProxy.isHlsURL(started.url) || ClearKeyDashProxy.isHlsURL(url) {
|
||||
playFormat = "hls"
|
||||
|
|
@ -345,7 +355,13 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
return
|
||||
}
|
||||
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).
|
||||
if mediaPlayer.drawable == nil {
|
||||
applyDrawable(force: true)
|
||||
|
|
@ -399,10 +415,14 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
to media: VLCMedia,
|
||||
url: URL,
|
||||
format: String?,
|
||||
viaLocalProxy: Bool
|
||||
viaLocalProxy: Bool,
|
||||
asVod: Bool = false
|
||||
) {
|
||||
// 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(":live-caching=1000")
|
||||
} else {
|
||||
|
|
@ -740,6 +760,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
self.liveMode = false
|
||||
self.canSeek = false
|
||||
self.didApplyPreferredAudio = false
|
||||
self.knownVodDurationMs = 0
|
||||
self.isPlaying = false
|
||||
self.hasError = false
|
||||
self.errorMessage = nil
|
||||
|
|
@ -854,9 +875,12 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
if l > 0 { lengthMs = l }
|
||||
position = Float(mediaPlayer.position)
|
||||
timeText = Self.format(ms: t)
|
||||
if l <= 0, knownVodDurationMs > 3000 {
|
||||
lengthMs = Int32(min(knownVodDurationMs, Int64(Int32.max)))
|
||||
}
|
||||
lengthText = Self.format(ms: lengthMs)
|
||||
isPlaying = mediaPlayer.isPlaying
|
||||
let seekable = mediaPlayer.isSeekable && lengthMs > 3000
|
||||
let seekable = (knownVodDurationMs > 3000 || mediaPlayer.isSeekable) && lengthMs > 3000
|
||||
if seekable != canSeek { canSeek = seekable }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ struct SettingsScreen: View {
|
|||
|
||||
Section("Info") {
|
||||
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("Events", value: app.eventsEnabled ? "Aan" : "Uit")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue