iOS 0.5.47: seek bar on F1 VOD, hide controls after tracks sheet, prefer NL then EN audio.
This commit is contained in:
parent
42c856eb47
commit
ef566e2756
5 changed files with 142 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 = 59;
|
||||
CURRENT_PROJECT_VERSION = 60;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -360,7 +360,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.46;
|
||||
MARKETING_VERSION = 0.5.47;
|
||||
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 = 59;
|
||||
CURRENT_PROJECT_VERSION = 60;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -393,7 +393,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.46;
|
||||
MARKETING_VERSION = 0.5.47;
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -792,6 +792,7 @@ final class ClearKeyDashProxy {
|
|||
/// Master: strip DRM keys, proxy alle variant/audio URI's — audio→ck=a, video→ck=v.
|
||||
private func rewriteHlsMaster(_ lines: [String], playlistURL: URL) -> String {
|
||||
var out: [String] = []
|
||||
var audioLines: [String] = []
|
||||
var i = 0
|
||||
while i < lines.count {
|
||||
let line = lines[i]
|
||||
|
|
@ -818,7 +819,12 @@ final class ClearKeyDashProxy {
|
|||
}
|
||||
if trimmed.hasPrefix("#EXT-X-MEDIA:") {
|
||||
let lane: CryptoLane = trimmed.contains("TYPE=AUDIO") ? .audio : .shared
|
||||
out.append(rewriteHlsTagURIs(line, base: playlistURL, lane: lane))
|
||||
let rewritten = rewriteHlsTagURIs(line, base: playlistURL, lane: lane)
|
||||
if lane == .audio {
|
||||
audioLines.append(rewritten)
|
||||
} else {
|
||||
out.append(rewritten)
|
||||
}
|
||||
i += 1
|
||||
continue
|
||||
}
|
||||
|
|
@ -832,9 +838,78 @@ final class ClearKeyDashProxy {
|
|||
}
|
||||
i += 1
|
||||
}
|
||||
if !audioLines.isEmpty {
|
||||
let ranked = Self.rankHlsAudioLines(audioLines)
|
||||
if let idx = out.firstIndex(where: { $0.hasPrefix("#EXT-X-STREAM-INF") }) {
|
||||
out.insert(contentsOf: ranked, at: idx)
|
||||
} else {
|
||||
out.append(contentsOf: ranked)
|
||||
}
|
||||
}
|
||||
return out.joined(separator: "\n")
|
||||
}
|
||||
|
||||
/// NL → Engels → rest. Winnaar krijgt DEFAULT=YES zodat VLC niet op Spaans start.
|
||||
private static func rankHlsAudioLines(_ lines: [String]) -> [String] {
|
||||
let scored = lines.enumerated().map { i, line in (line, hlsAudioScore(line) * 100 - i) }
|
||||
let sorted = scored.sorted { $0.1 > $1.1 }.map(\.0)
|
||||
guard let first = sorted.first else { return lines }
|
||||
var out: [String] = []
|
||||
for (i, line) in sorted.enumerated() {
|
||||
out.append(hlsSetAttr(hlsSetAttr(line, key: "DEFAULT", value: i == 0 ? "YES" : "NO"), key: "AUTOSELECT", value: "YES"))
|
||||
}
|
||||
let lang = hlsMediaAttr(first, "LANGUAGE") ?? "-"
|
||||
let name = hlsMediaAttr(first, "NAME") ?? "-"
|
||||
proxyLog.info("hls audio default lang=\(lang, privacy: .public) name=\(name, privacy: .public) count=\(sorted.count, privacy: .public)")
|
||||
return out
|
||||
}
|
||||
|
||||
private static func hlsAudioScore(_ line: String) -> Int {
|
||||
let lang = (hlsMediaAttr(line, "LANGUAGE") ?? "").lowercased()
|
||||
let name = (hlsMediaAttr(line, "NAME") ?? "").lowercased()
|
||||
if ["nl", "dut", "nld"].contains(lang) || name.contains("nederlands") || name.contains("dutch") {
|
||||
return 100
|
||||
}
|
||||
if ["en", "eng"].contains(lang) || name.contains("english") || name.contains("engels") {
|
||||
return 50
|
||||
}
|
||||
if name.contains("international") { return 35 }
|
||||
return 0
|
||||
}
|
||||
|
||||
private static func hlsMediaAttr(_ line: String, _ key: String) -> String? {
|
||||
let pattern = #"\#(key)=(?:"([^"]*)"|([^,]*))"#
|
||||
guard let re = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { return nil }
|
||||
let ns = line as NSString
|
||||
guard let m = re.firstMatch(in: line, options: [], range: NSRange(location: 0, length: ns.length)) else { return nil }
|
||||
if m.numberOfRanges > 1, m.range(at: 1).location != NSNotFound {
|
||||
let v = ns.substring(with: m.range(at: 1)).trimmingCharacters(in: .whitespaces)
|
||||
return v.isEmpty ? nil : v
|
||||
}
|
||||
if m.numberOfRanges > 2, m.range(at: 2).location != NSNotFound {
|
||||
let v = ns.substring(with: m.range(at: 2)).trimmingCharacters(in: .whitespaces)
|
||||
return v.isEmpty ? nil : v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
private static func hlsSetAttr(_ line: String, key: String, value: String) -> String {
|
||||
let pattern = #"\#(key)=(?:"[^"]*"|[^,]*)"#
|
||||
let replacement = "\(key)=\"\(value)\""
|
||||
guard let re = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else {
|
||||
return line + ",\(replacement)"
|
||||
}
|
||||
let ns = line as NSString
|
||||
let range = NSRange(location: 0, length: ns.length)
|
||||
if re.firstMatch(in: line, options: [], range: range) != nil {
|
||||
return re.stringByReplacingMatches(in: line, options: [], range: range, withTemplate: replacement)
|
||||
}
|
||||
if line.hasPrefix("#EXT-X-MEDIA:") {
|
||||
return "#EXT-X-MEDIA:\(replacement)," + String(line.dropFirst("#EXT-X-MEDIA:".count))
|
||||
}
|
||||
return line + ",\(replacement)"
|
||||
}
|
||||
|
||||
private func rewriteHlsTagURIs(_ line: String, base: URL, lane: CryptoLane = .shared) -> String {
|
||||
guard let regex = try? NSRegularExpression(pattern: #"URI="([^"]+)""#, options: []) else {
|
||||
return line
|
||||
|
|
@ -1093,7 +1168,7 @@ final class ClearKeyDashProxy {
|
|||
private static func preferredAudioOrder(_ audios: [DashHlsRep]) -> [DashHlsRep] {
|
||||
let aac = audios.filter(\.isAac)
|
||||
let pool = aac.isEmpty ? audios : aac
|
||||
let preferred = Locale.current.language.languageCode?.identifier.lowercased() ?? "nl"
|
||||
let preferred = "nl"
|
||||
return pool.sorted { a, b in
|
||||
let aScore = Self.audioLocaleScore(a, preferred: preferred)
|
||||
let bScore = Self.audioLocaleScore(b, preferred: preferred)
|
||||
|
|
|
|||
|
|
@ -95,6 +95,8 @@ struct PlayerScreen: View {
|
|||
|
||||
private var isLive: Bool { request.isLive }
|
||||
private var useClearKeyAV: Bool { request.isLive && request.needsClearKey && clearKeyPreferAV }
|
||||
/// F1-replay e.d. komen via live-request binnen, maar hebben een echte duur.
|
||||
private var showSeekChrome: Bool { !isLive || (!useClearKeyAV && vlc.canSeek) }
|
||||
|
||||
private var stateText: String { useClearKeyAV ? av.stateText : vlc.stateText }
|
||||
private var isPlaying: Bool { useClearKeyAV ? av.isPlaying : vlc.isPlaying }
|
||||
|
|
@ -186,7 +188,7 @@ struct PlayerScreen: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showTracks) {
|
||||
.sheet(isPresented: $showTracks, onDismiss: { scheduleHide() }) {
|
||||
tracksSheet
|
||||
.presentationDetents([.medium, .large])
|
||||
.presentationDragIndicator(.visible)
|
||||
|
|
@ -269,7 +271,7 @@ struct PlayerScreen: View {
|
|||
.onTapGesture { hideControls() }
|
||||
|
||||
VStack(spacing: 14) {
|
||||
if !isLive {
|
||||
if showSeekChrome {
|
||||
Slider(
|
||||
value: Binding(
|
||||
get: { Double(vlc.position) },
|
||||
|
|
@ -290,7 +292,7 @@ struct PlayerScreen: View {
|
|||
}
|
||||
|
||||
HStack(spacing: 36) {
|
||||
if !isLive {
|
||||
if showSeekChrome {
|
||||
Button {
|
||||
vlc.seek(toMs: max(vlc.currentPositionMs() - 10_000, 0))
|
||||
scheduleHide()
|
||||
|
|
@ -307,7 +309,7 @@ struct PlayerScreen: View {
|
|||
.foregroundStyle(.white)
|
||||
.shadow(radius: 8)
|
||||
}
|
||||
if !isLive {
|
||||
if showSeekChrome {
|
||||
Button {
|
||||
vlc.seek(toMs: vlc.currentPositionMs() + 10_000)
|
||||
scheduleHide()
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
@Published var currentAudioId: String = ""
|
||||
@Published var pipPossible = false
|
||||
@Published var pipActive = false
|
||||
/// VOD / F1-replay via playLive: finite duration + VLC seekable.
|
||||
@Published var canSeek = false
|
||||
|
||||
static let offSubtitleId = PlayerTrack.offId
|
||||
|
||||
|
|
@ -52,6 +54,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
private var usePipDrawable = false
|
||||
private var stopping = false
|
||||
private var stopGeneration = 0
|
||||
private var didApplyPreferredAudio = false
|
||||
|
||||
override init() {
|
||||
// Shared library = alle demux/access plugins (DASH adaptive in VLC 4).
|
||||
|
|
@ -191,6 +194,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
stateText = "Verbinden…"
|
||||
subtitleTracks = []
|
||||
audioTracks = []
|
||||
canSeek = false
|
||||
didApplyPreferredAudio = false
|
||||
liveMode = false
|
||||
clearKeyProxyActive = false
|
||||
plainVideoView?.layoutIfNeeded()
|
||||
|
|
@ -255,6 +260,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
liveRetryCount = 0
|
||||
everPlayed = false
|
||||
liveStartedAt = Date()
|
||||
canSeek = false
|
||||
didApplyPreferredAudio = false
|
||||
liveMode = true
|
||||
usePipDrawable = false
|
||||
pipPossible = false
|
||||
|
|
@ -345,6 +352,15 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
}
|
||||
mediaPlayer.media = media
|
||||
mediaPlayer.play()
|
||||
let gen = stopGeneration
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.6) { [weak self] in
|
||||
guard let self, self.stopGeneration == gen, !self.stopping else { return }
|
||||
self.refreshTracks()
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 4.0) { [weak self] in
|
||||
guard let self, self.stopGeneration == gen, !self.stopping else { return }
|
||||
self.refreshTracks()
|
||||
}
|
||||
}
|
||||
|
||||
private func dutchClearKeyError(_ error: Error) -> String {
|
||||
|
|
@ -523,6 +539,40 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
}
|
||||
audioTracks = audios
|
||||
currentAudioId = selectedAudio
|
||||
applyPreferredAudioIfNeeded()
|
||||
}
|
||||
|
||||
private func applyPreferredAudioIfNeeded() {
|
||||
guard !didApplyPreferredAudio else { return }
|
||||
let list = mediaPlayer.audioTracks
|
||||
guard !list.isEmpty else { return }
|
||||
didApplyPreferredAudio = true
|
||||
let ranked = list.enumerated().map { i, t in
|
||||
(track: t, score: Self.audioPreferenceScore(language: t.language, name: t.trackName, description: t.trackDescription, index: i))
|
||||
}
|
||||
guard let best = ranked.max(by: { $0.score < $1.score }), best.score > 0 else { return }
|
||||
if !best.track.isSelected {
|
||||
best.track.isSelectedExclusively = true
|
||||
}
|
||||
currentAudioId = best.track.trackId
|
||||
}
|
||||
|
||||
/// NL eerst, anders Engels. Geen match → 0 (niet forceren).
|
||||
static func audioPreferenceScore(language: String?, name: String?, description: String?, index: Int) -> Int {
|
||||
let blob = [language, name, description]
|
||||
.compactMap { $0?.lowercased() }
|
||||
.joined(separator: " ")
|
||||
if blob.isEmpty { return 0 }
|
||||
let nl = ["nederlands", "dutch", " holland", "nl ", "dut", "nld"]
|
||||
if nl.contains(where: { blob.contains($0) }) || blob == "nl" || blob.hasPrefix("nl-") || blob.hasPrefix("nl_") {
|
||||
return 100 - index
|
||||
}
|
||||
let en = ["engels", "english", "international", "eng", "en "]
|
||||
if en.contains(where: { blob.contains($0) }) || blob == "en" || blob.hasPrefix("en-") || blob.hasPrefix("en_")
|
||||
|| blob == "eng" {
|
||||
return 50 - index
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private enum TrackKind { case subtitle, audio }
|
||||
|
|
@ -688,6 +738,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
self.liveRetryCount = 0
|
||||
self.everPlayed = false
|
||||
self.liveMode = false
|
||||
self.canSeek = false
|
||||
self.didApplyPreferredAudio = false
|
||||
self.isPlaying = false
|
||||
self.hasError = false
|
||||
self.errorMessage = nil
|
||||
|
|
@ -804,6 +856,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
timeText = Self.format(ms: t)
|
||||
lengthText = Self.format(ms: lengthMs)
|
||||
isPlaying = mediaPlayer.isPlaying
|
||||
let seekable = mediaPlayer.isSeekable && lengthMs > 3000
|
||||
if seekable != canSeek { canSeek = seekable }
|
||||
}
|
||||
|
||||
private static func format(ms: Int32) -> String {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ struct SettingsScreen: View {
|
|||
|
||||
Section("Info") {
|
||||
LabeledContent("App", value: "VMC iOS")
|
||||
LabeledContent("Versie", value: "0.5.46")
|
||||
LabeledContent("Versie", value: "0.5.47")
|
||||
LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit")
|
||||
LabeledContent("Events", value: app.eventsEnabled ? "Aan" : "Uit")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue