iOS 0.5.49: F1 VOD seek by trimming HLS playlist; scrub slider only seeks on release.
This commit is contained in:
parent
011891c89c
commit
e6607654a2
5 changed files with 113 additions and 29 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 = 61;
|
CURRENT_PROJECT_VERSION = 62;
|
||||||
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.48;
|
MARKETING_VERSION = 0.5.49;
|
||||||
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 = 61;
|
CURRENT_PROJECT_VERSION = 62;
|
||||||
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.48;
|
MARKETING_VERSION = 0.5.49;
|
||||||
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)";
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,8 @@ final class ClearKeyDashProxy {
|
||||||
/// (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.
|
/// Som van EXTINF bij VOD/ENDLIST — VLC live-pad meldt anders duur 0 / niet-zoekbaar.
|
||||||
private var lastVodDurationMs: Int64 = 0
|
private var lastVodDurationMs: Int64 = 0
|
||||||
|
/// VLC kan HLS-cbcs via de proxy niet zoeken; we knippen de playlist vanaf dit punt.
|
||||||
|
private var playbackStartOffsetSec: Double = 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
|
||||||
|
|
@ -143,6 +145,13 @@ final class ClearKeyDashProxy {
|
||||||
withState { lastVodDurationMs }
|
withState { lastVodDurationMs }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func setPlaybackStartOffset(ms: Int64) {
|
||||||
|
withState {
|
||||||
|
playbackStartOffsetSec = Double(max(0, ms)) / 1000.0
|
||||||
|
lastMediaPlaylistByPath = [:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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.
|
||||||
/// - Parameter preferHlsForAVPlayer: forceer altijd een lokale `.m3u8` (DASH wordt omgezet).
|
/// - Parameter preferHlsForAVPlayer: forceer altijd een lokale `.m3u8` (DASH wordt omgezet).
|
||||||
func start(
|
func start(
|
||||||
|
|
@ -173,6 +182,7 @@ final class ClearKeyDashProxy {
|
||||||
self.nextStableSeq = 1
|
self.nextStableSeq = 1
|
||||||
self.lastMediaPlaylistByPath = [:]
|
self.lastMediaPlaylistByPath = [:]
|
||||||
self.lastVodDurationMs = 0
|
self.lastVodDurationMs = 0
|
||||||
|
self.playbackStartOffsetSec = 0
|
||||||
self.cachedMpdXML = nil
|
self.cachedMpdXML = nil
|
||||||
self.cachedMpdAt = nil
|
self.cachedMpdAt = nil
|
||||||
self.cryptoLock.lock()
|
self.cryptoLock.lock()
|
||||||
|
|
@ -392,6 +402,7 @@ final class ClearKeyDashProxy {
|
||||||
nextStableSeq = 1
|
nextStableSeq = 1
|
||||||
lastMediaPlaylistByPath = [:]
|
lastMediaPlaylistByPath = [:]
|
||||||
lastVodDurationMs = 0
|
lastVodDurationMs = 0
|
||||||
|
playbackStartOffsetSec = 0
|
||||||
cachedMpdXML = nil
|
cachedMpdXML = nil
|
||||||
cachedMpdAt = nil
|
cachedMpdAt = nil
|
||||||
cryptoLock.lock()
|
cryptoLock.lock()
|
||||||
|
|
@ -788,14 +799,20 @@ final class ClearKeyDashProxy {
|
||||||
startIdx = 0
|
startIdx = 0
|
||||||
}
|
}
|
||||||
let window = Array(segments.suffix(from: startIdx))
|
let window = Array(segments.suffix(from: startIdx))
|
||||||
let seq = seqBase + startIdx
|
|
||||||
if isComplete || (isF1 && startIdx == 0 && window.count > 40) {
|
if isComplete || (isF1 && startIdx == 0 && window.count > 40) {
|
||||||
let ms = Self.playlistDurationMs(window.map(\.inf), fallbackSeconds: targetDuration)
|
let ms = Self.playlistDurationMs(window.map(\.inf), fallbackSeconds: targetDuration)
|
||||||
withState {
|
withState {
|
||||||
if ms > self.lastVodDurationMs { self.lastVodDurationMs = ms }
|
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)")
|
let seekSec = withState { self.playbackStartOffsetSec }
|
||||||
|
var playFrom = 0
|
||||||
|
if seekSec > 0.4, isComplete || isF1 {
|
||||||
|
playFrom = Self.segmentIndex(at: seekSec, infLines: window.map(\.inf), fallbackSeconds: targetDuration)
|
||||||
|
}
|
||||||
|
let playWindow = Array(window.suffix(from: playFrom))
|
||||||
|
let seq = seqBase + startIdx + playFrom
|
||||||
|
proxyLog.info("hls rewrite segs=\(segments.count, privacy: .public) keep=\(playWindow.count, privacy: .public) endlist=\(hasEndList, privacy: .public) type=\(playlistType ?? "-", privacy: .public) f1=\(isF1, privacy: .public) startIdx=\(startIdx, privacy: .public) seek=\(Int(seekSec), privacy: .public)s vodMs=\(self.lastVodDurationMs, privacy: .public)")
|
||||||
|
|
||||||
out.append("#EXTM3U")
|
out.append("#EXTM3U")
|
||||||
out.append("#EXT-X-VERSION:7")
|
out.append("#EXT-X-VERSION:7")
|
||||||
|
|
@ -816,7 +833,7 @@ final class ClearKeyDashProxy {
|
||||||
out.append(h)
|
out.append(h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for seg in window {
|
for seg in playWindow {
|
||||||
out.append(seg.inf)
|
out.append(seg.inf)
|
||||||
let resolved = join(base: asDirectory(playlistURL.deletingLastPathComponent()), relative: seg.uri)
|
let resolved = join(base: asDirectory(playlistURL.deletingLastPathComponent()), relative: seg.uri)
|
||||||
out.append(proxyAbsoluteURL(resolved, lane: lane))
|
out.append(proxyAbsoluteURL(resolved, lane: lane))
|
||||||
|
|
@ -830,18 +847,31 @@ final class ClearKeyDashProxy {
|
||||||
private static func playlistDurationMs(_ infLines: [String], fallbackSeconds: Int) -> Int64 {
|
private static func playlistDurationMs(_ infLines: [String], fallbackSeconds: Int) -> Int64 {
|
||||||
var total: Double = 0
|
var total: Double = 0
|
||||||
for inf in infLines {
|
for inf in infLines {
|
||||||
let raw = inf.trimmingCharacters(in: .whitespaces)
|
total += extinfSeconds(inf, fallback: fallbackSeconds)
|
||||||
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)
|
return Int64(total * 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func segmentIndex(at seconds: Double, infLines: [String], fallbackSeconds: Int) -> Int {
|
||||||
|
guard !infLines.isEmpty else { return 0 }
|
||||||
|
var acc = 0.0
|
||||||
|
for (i, inf) in infLines.enumerated() {
|
||||||
|
acc += extinfSeconds(inf, fallback: fallbackSeconds)
|
||||||
|
if acc >= seconds { return i }
|
||||||
|
}
|
||||||
|
return max(0, infLines.count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func extinfSeconds(_ inf: String, fallback: Int) -> Double {
|
||||||
|
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)) {
|
||||||
|
return seconds
|
||||||
|
}
|
||||||
|
return Double(max(fallback, 1))
|
||||||
|
}
|
||||||
|
|
||||||
/// 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()
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,8 @@ struct PlayerScreen: View {
|
||||||
@State private var hideTask: Task<Void, Never>?
|
@State private var hideTask: Task<Void, Never>?
|
||||||
@State private var heartbeatTask: Task<Void, Never>?
|
@State private var heartbeatTask: Task<Void, Never>?
|
||||||
@State private var showTracks = false
|
@State private var showTracks = false
|
||||||
|
@State private var sliderDraft: Double = 0
|
||||||
|
@State private var isScrubbing = false
|
||||||
@State private var trackTab = 0
|
@State private var trackTab = 0
|
||||||
@State private var subtitleHits: [SubtitleHit] = []
|
@State private var subtitleHits: [SubtitleHit] = []
|
||||||
@State private var subsLoading = false
|
@State private var subsLoading = false
|
||||||
|
|
@ -195,6 +197,15 @@ struct PlayerScreen: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func formatClock(ratio: Double, durationMs: Int64) -> String {
|
||||||
|
let ms = Int(Double(max(durationMs, 0)) * min(max(ratio, 0), 1) / 1000)
|
||||||
|
let h = ms / 3600
|
||||||
|
let m = (ms % 3600) / 60
|
||||||
|
let s = ms % 60
|
||||||
|
if h > 0 { return String(format: "%d:%02d:%02d", h, m, s) }
|
||||||
|
return String(format: "%d:%02d", m, s)
|
||||||
|
}
|
||||||
|
|
||||||
/// Letterbox/pillarbox buiten de VLC-surface zodat FreeType onderaan het beeld zit.
|
/// Letterbox/pillarbox buiten de VLC-surface zodat FreeType onderaan het beeld zit.
|
||||||
private static func fittedVideoSize(in container: CGSize, aspect: CGFloat) -> CGSize {
|
private static func fittedVideoSize(in container: CGSize, aspect: CGFloat) -> CGSize {
|
||||||
guard container.width > 1, container.height > 1, aspect > 0 else { return container }
|
guard container.width > 1, container.height > 1, aspect > 0 else { return container }
|
||||||
|
|
@ -274,18 +285,28 @@ struct PlayerScreen: View {
|
||||||
if showSeekChrome {
|
if showSeekChrome {
|
||||||
Slider(
|
Slider(
|
||||||
value: Binding(
|
value: Binding(
|
||||||
get: { Double(vlc.position) },
|
get: { isScrubbing ? sliderDraft : Double(vlc.position) },
|
||||||
set: {
|
set: { sliderDraft = $0 }
|
||||||
vlc.seek(ratio: Float($0))
|
),
|
||||||
|
in: 0...1,
|
||||||
|
onEditingChanged: { editing in
|
||||||
|
if editing {
|
||||||
|
isScrubbing = true
|
||||||
|
sliderDraft = Double(vlc.position)
|
||||||
|
scheduleHide(cancelOnly: true)
|
||||||
|
} else {
|
||||||
|
isScrubbing = false
|
||||||
|
vlc.seek(ratio: Float(sliderDraft))
|
||||||
scheduleHide()
|
scheduleHide()
|
||||||
}
|
}
|
||||||
),
|
}
|
||||||
in: 0...1
|
|
||||||
)
|
)
|
||||||
.tint(McColors.accent)
|
.tint(McColors.accent)
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Text(vlc.timeText).font(McFonts.caption).foregroundStyle(.white.opacity(0.85))
|
Text(isScrubbing ? Self.formatClock(ratio: sliderDraft, durationMs: vlc.durationMs()) : vlc.timeText)
|
||||||
|
.font(McFonts.caption)
|
||||||
|
.foregroundStyle(.white.opacity(0.85))
|
||||||
Spacer()
|
Spacer()
|
||||||
Text(vlc.lengthText).font(McFonts.caption).foregroundStyle(.white.opacity(0.85))
|
Text(vlc.lengthText).font(McFonts.caption).foregroundStyle(.white.opacity(0.85))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,6 +56,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
private var stopGeneration = 0
|
private var stopGeneration = 0
|
||||||
private var didApplyPreferredAudio = false
|
private var didApplyPreferredAudio = false
|
||||||
private var knownVodDurationMs: Int64 = 0
|
private var knownVodDurationMs: Int64 = 0
|
||||||
|
/// Playlist is geknipt vanaf dit punt (VLC-tijd 0 = deze offset).
|
||||||
|
private var seekOriginMs: 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).
|
||||||
|
|
@ -198,6 +200,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
canSeek = false
|
canSeek = false
|
||||||
didApplyPreferredAudio = false
|
didApplyPreferredAudio = false
|
||||||
knownVodDurationMs = 0
|
knownVodDurationMs = 0
|
||||||
|
seekOriginMs = 0
|
||||||
liveMode = false
|
liveMode = false
|
||||||
clearKeyProxyActive = false
|
clearKeyProxyActive = false
|
||||||
plainVideoView?.layoutIfNeeded()
|
plainVideoView?.layoutIfNeeded()
|
||||||
|
|
@ -265,6 +268,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
canSeek = false
|
canSeek = false
|
||||||
didApplyPreferredAudio = false
|
didApplyPreferredAudio = false
|
||||||
knownVodDurationMs = 0
|
knownVodDurationMs = 0
|
||||||
|
seekOriginMs = 0
|
||||||
liveMode = true
|
liveMode = true
|
||||||
usePipDrawable = false
|
usePipDrawable = false
|
||||||
pipPossible = false
|
pipPossible = false
|
||||||
|
|
@ -455,18 +459,44 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(toMs: Int64) {
|
func seek(toMs: Int64) {
|
||||||
mediaPlayer.time = VLCTime(number: NSNumber(value: toMs))
|
let target = max(0, toMs)
|
||||||
|
if knownVodDurationMs > 3000, clearKeyProxyActive, let base = livePlayURL {
|
||||||
|
let endPad = max(knownVodDurationMs - 1500, 0)
|
||||||
|
let clamped = min(target, endPad)
|
||||||
|
seekOriginMs = clamped
|
||||||
|
ClearKeyDashProxy.shared.setPlaybackStartOffset(ms: clamped)
|
||||||
|
var comps = URLComponents(url: base, resolvingAgainstBaseURL: false)
|
||||||
|
var items = (comps?.queryItems ?? []).filter { $0.name != "vmcSeek" }
|
||||||
|
items.append(URLQueryItem(name: "vmcSeek", value: String(clamped)))
|
||||||
|
comps?.queryItems = items
|
||||||
|
if let url = comps?.url {
|
||||||
|
position = knownVodDurationMs > 0 ? Float(Double(clamped) / Double(knownVodDurationMs)) : 0
|
||||||
|
timeText = Self.format(ms: Int32(min(clamped, Int64(Int32.max))))
|
||||||
|
startLiveMedia(url: url, format: livePlayFormat)
|
||||||
|
invalidatePipPlaybackState()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mediaPlayer.time = VLCTime(number: NSNumber(value: target))
|
||||||
invalidatePipPlaybackState()
|
invalidatePipPlaybackState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func seek(ratio: Float) {
|
func seek(ratio: Float) {
|
||||||
|
let dur = durationMs()
|
||||||
|
if dur > 3000 {
|
||||||
|
seek(toMs: Int64(Double(dur) * Double(min(max(ratio, 0), 1))))
|
||||||
|
return
|
||||||
|
}
|
||||||
mediaPlayer.position = Double(ratio)
|
mediaPlayer.position = Double(ratio)
|
||||||
invalidatePipPlaybackState()
|
invalidatePipPlaybackState()
|
||||||
}
|
}
|
||||||
|
|
||||||
func currentPositionMs() -> Int64 { Int64(mediaPlayer.time.intValue) }
|
func currentPositionMs() -> Int64 {
|
||||||
|
Int64(mediaPlayer.time.intValue) + seekOriginMs
|
||||||
|
}
|
||||||
|
|
||||||
func durationMs() -> Int64 {
|
func durationMs() -> Int64 {
|
||||||
|
if knownVodDurationMs > 3000 { return knownVodDurationMs }
|
||||||
let v = mediaPlayer.media?.length.intValue ?? lengthMs
|
let v = mediaPlayer.media?.length.intValue ?? lengthMs
|
||||||
return Int64(max(v, 0))
|
return Int64(max(v, 0))
|
||||||
}
|
}
|
||||||
|
|
@ -761,6 +791,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
self.canSeek = false
|
self.canSeek = false
|
||||||
self.didApplyPreferredAudio = false
|
self.didApplyPreferredAudio = false
|
||||||
self.knownVodDurationMs = 0
|
self.knownVodDurationMs = 0
|
||||||
|
self.seekOriginMs = 0
|
||||||
self.isPlaying = false
|
self.isPlaying = false
|
||||||
self.hasError = false
|
self.hasError = false
|
||||||
self.errorMessage = nil
|
self.errorMessage = nil
|
||||||
|
|
@ -870,14 +901,16 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
|
|
||||||
private func refreshTime() {
|
private func refreshTime() {
|
||||||
guard !stopping else { return }
|
guard !stopping else { return }
|
||||||
let t = mediaPlayer.time.intValue
|
let t = Int64(mediaPlayer.time.intValue) + seekOriginMs
|
||||||
let l = mediaPlayer.media?.length.intValue ?? 0
|
let l = mediaPlayer.media?.length.intValue ?? 0
|
||||||
if l > 0 { lengthMs = l }
|
if knownVodDurationMs > 3000 {
|
||||||
position = Float(mediaPlayer.position)
|
|
||||||
timeText = Self.format(ms: t)
|
|
||||||
if l <= 0, knownVodDurationMs > 3000 {
|
|
||||||
lengthMs = Int32(min(knownVodDurationMs, Int64(Int32.max)))
|
lengthMs = Int32(min(knownVodDurationMs, Int64(Int32.max)))
|
||||||
|
position = Float(Double(t) / Double(knownVodDurationMs))
|
||||||
|
} else {
|
||||||
|
if l > 0 { lengthMs = l }
|
||||||
|
position = Float(mediaPlayer.position)
|
||||||
}
|
}
|
||||||
|
timeText = Self.format(ms: Int32(min(max(t, 0), Int64(Int32.max))))
|
||||||
lengthText = Self.format(ms: lengthMs)
|
lengthText = Self.format(ms: lengthMs)
|
||||||
isPlaying = mediaPlayer.isPlaying
|
isPlaying = mediaPlayer.isPlaying
|
||||||
let seekable = (knownVodDurationMs > 3000 || mediaPlayer.isSeekable) && lengthMs > 3000
|
let seekable = (knownVodDurationMs > 3000 || mediaPlayer.isSeekable) && lengthMs > 3000
|
||||||
|
|
|
||||||
|
|
@ -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.48")
|
LabeledContent("Versie", value: "0.5.49")
|
||||||
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