Use Chrome/122 on every Viaplay CDN hop, like the standalone MPD player.

iOS only sent that UA when the hostname looked like viaplay; stored MPDs often do not. Event plays now pin the browser session for the whole fetch chain, still direct to the CDN.
This commit is contained in:
Jos Vooges | STH 2026-09-28 21:19:22 +02:00
parent 6b2eaf89c6
commit 084bcdaeff
7 changed files with 41 additions and 18 deletions

View file

@ -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 = 69; CURRENT_PROJECT_VERSION = 70;
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.56; MARKETING_VERSION = 0.5.57;
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 = 69; CURRENT_PROJECT_VERSION = 70;
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.56; MARKETING_VERSION = 0.5.57;
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)";

View file

@ -47,7 +47,8 @@ final class AVLivePlayerController: NSObject, ObservableObject {
url: URL, url: URL,
fallback: URL? = nil, fallback: URL? = nil,
format: String? = nil, format: String? = nil,
clearKeys: [ClearKeyEntry] = [] clearKeys: [ClearKeyEntry] = [],
browserCdnHeaders: Bool = false
) async { ) async {
generation &+= 1 generation &+= 1
let gen = generation let gen = generation
@ -78,7 +79,8 @@ final class AVLivePlayerController: NSObject, ObservableObject {
let started = try await ClearKeyDashProxy.shared.start( let started = try await ClearKeyDashProxy.shared.start(
manifestURL: url, manifestURL: url,
keys: clearKeys, keys: clearKeys,
preferHlsForAVPlayer: true preferHlsForAVPlayer: true,
browserCdnHeaders: browserCdnHeaders
) )
guard gen == generation, !stopping else { return } guard gen == generation, !stopping else { return }
playURL = started.url playURL = started.url

View file

@ -217,7 +217,8 @@ final class ClearKeyDashProxy {
func start( func start(
manifestURL: URL, manifestURL: URL,
keys: [ClearKeyEntry], keys: [ClearKeyEntry],
preferHlsForAVPlayer: Bool = false preferHlsForAVPlayer: Bool = false,
browserCdnHeaders: Bool = false
) async throws -> StartResult { ) async throws -> StartResult {
let map = ClearKeyKeyMap(entries: keys) let map = ClearKeyKeyMap(entries: keys)
guard !map.isEmpty else { guard !map.isEmpty else {
@ -235,7 +236,11 @@ final class ClearKeyDashProxy {
self.keyMap = map self.keyMap = map
self.isHlsMode = hls self.isHlsMode = hls
self.preferHlsOutput = preferHlsForAVPlayer self.preferHlsOutput = preferHlsForAVPlayer
self.useViaplayBrowserHeaders = ViaplayPlaybackHeaders.matches(manifestURL) self.useViaplayBrowserHeaders =
browserCdnHeaders || ViaplayPlaybackHeaders.matches(manifestURL)
if self.useViaplayBrowserHeaders {
proxyLog.info("CDN headers=Chrome/122 for whole session")
}
self.dashVideoRep = nil self.dashVideoRep = nil
self.dashAudioRep = nil self.dashAudioRep = nil
self.dashAudioReps = [] self.dashAudioReps = []
@ -1112,7 +1117,7 @@ final class ClearKeyDashProxy {
req.timeoutInterval = 15 req.timeoutInterval = 15
req.cachePolicy = .reloadIgnoringLocalCacheData req.cachePolicy = .reloadIgnoringLocalCacheData
req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges") req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges")
let browser = withState { useViaplayBrowserHeaders } || ViaplayPlaybackHeaders.matches(current) let browser = withState { useViaplayBrowserHeaders }
let session: URLSession let session: URLSession
if browser { if browser {
ViaplayPlaybackHeaders.apply(to: &req) ViaplayPlaybackHeaders.apply(to: &req)

View file

@ -21,6 +21,12 @@ struct PlayRequest: Identifiable {
/// Provider-/titel-logo voor laad-animatie (events, films, series). /// Provider-/titel-logo voor laad-animatie (events, films, series).
let logoUrl: String? let logoUrl: String?
/// Zelfde Chrome/122-UA als de werkende losse MPD-speler, voor de hele CDN-sessie.
var usesViaplayCdn: Bool {
ViaplayPlaybackHeaders.matches(url)
|| (watchId ?? "").localizedCaseInsensitiveContains("viaplay")
}
static func vod( static func vod(
url: URL, url: URL,
title: String, title: String,
@ -579,7 +585,8 @@ struct PlayerScreen: View {
fallback: request.fallbackUrl, fallback: request.fallbackUrl,
format: request.format, format: request.format,
clearKeys: keys, clearKeys: keys,
preferHls: true preferHls: true,
browserCdnHeaders: request.usesViaplayCdn
) )
let maxWaitNs: UInt64 = 18_000_000_000 let maxWaitNs: UInt64 = 18_000_000_000
let stepNs: UInt64 = 200_000_000 let stepNs: UInt64 = 200_000_000
@ -597,7 +604,8 @@ struct PlayerScreen: View {
url: request.url, url: request.url,
fallback: request.fallbackUrl, fallback: request.fallbackUrl,
format: request.format, format: request.format,
clearKeys: keys clearKeys: keys,
browserCdnHeaders: request.usesViaplayCdn
) )
let maxWaitNs: UInt64 = 18_000_000_000 let maxWaitNs: UInt64 = 18_000_000_000
let stepNs: UInt64 = 200_000_000 let stepNs: UInt64 = 200_000_000
@ -620,7 +628,8 @@ struct PlayerScreen: View {
fallback: request.fallbackUrl, fallback: request.fallbackUrl,
format: request.format, format: request.format,
clearKeys: keys, clearKeys: keys,
preferHls: true preferHls: true,
browserCdnHeaders: request.usesViaplayCdn
) )
waited = 0 waited = 0
while waited < 8_000_000_000 && !vlc.isPlaying && !vlc.hasError { while waited < 8_000_000_000 && !vlc.isPlaying && !vlc.hasError {
@ -638,7 +647,8 @@ struct PlayerScreen: View {
url: request.url, url: request.url,
fallback: request.fallbackUrl, fallback: request.fallbackUrl,
format: request.format, format: request.format,
clearKeys: [] clearKeys: [],
browserCdnHeaders: request.usesViaplayCdn
) )
let maxWaitNs: UInt64 = 4_000_000_000 let maxWaitNs: UInt64 = 4_000_000_000
let stepNs: UInt64 = 200_000_000 let stepNs: UInt64 = 200_000_000

View file

@ -278,7 +278,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
fallback: URL? = nil, fallback: URL? = nil,
format: String? = nil, format: String? = nil,
clearKeys: [ClearKeyEntry] = [], clearKeys: [ClearKeyEntry] = [],
preferHls: Bool = false preferHls: Bool = false,
browserCdnHeaders: Bool = false
) async { ) async {
// Annuleer pending finishStop / delayed proxy-stop van vorige sessie. // Annuleer pending finishStop / delayed proxy-stop van vorige sessie.
stopGeneration &+= 1 stopGeneration &+= 1
@ -310,7 +311,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
let started = try await ClearKeyDashProxy.shared.start( let started = try await ClearKeyDashProxy.shared.start(
manifestURL: url, manifestURL: url,
keys: clearKeys, keys: clearKeys,
preferHlsForAVPlayer: preferHls preferHlsForAVPlayer: preferHls,
browserCdnHeaders: browserCdnHeaders
) )
playURL = started.url playURL = started.url
proxySession = started.session proxySession = started.session

View file

@ -768,6 +768,9 @@ private struct HubPlaybackModifier: ViewModifier {
set: { if !$0 { playback.info = nil } } set: { if !$0 { playback.info = nil } }
)) { )) {
if let info = playback.info, let url = URL(string: info.streamUrl) { if let info = playback.info, let url = URL(string: info.streamUrl) {
let sid = info.streamId
let viaplay = ViaplayPlaybackHeaders.matches(url)
|| sid.localizedCaseInsensitiveContains("viaplay")
PlayerScreen(request: .live( PlayerScreen(request: .live(
url: url, url: url,
title: info.name, title: info.name,
@ -776,8 +779,9 @@ private struct HubPlaybackModifier: ViewModifier {
drm: info.drm, drm: info.drm,
watchSessionId: info.watchSessionId, watchSessionId: info.watchSessionId,
watchKind: "event", watchKind: "event",
watchId: info.streamId.isEmpty ? nil : info.streamId, watchId: viaplay ? (sid.isEmpty ? "viaplay" : sid) : (sid.isEmpty ? nil : sid),
logoUrl: info.logoUrl logoUrl: info.logoUrl,
isLive: info.live ?? true
)) ))
.environmentObject(app) .environmentObject(app)
} }

View file

@ -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.56") LabeledContent("Versie", value: "0.5.57")
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")
} }