Fix iOS Viaplay catch-up Don't go: send Chrome/122 on every CDN fetch.

Catch-up (live:false) skipped the ClearKey proxy so VLC hit the CDN without the working UA, and CFNetwork dropped URLRequest User-Agent unless it is set on the session.
This commit is contained in:
Jos Vooges | STH 2026-09-28 20:57:22 +02:00
parent 19aa0853a3
commit 80ab9cdb76
5 changed files with 63 additions and 21 deletions

View file

@ -345,7 +345,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 67;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = X25NYX3K49;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@ -360,7 +360,7 @@
"$(inherited)",
"@executable_path/Frameworks"
);
MARKETING_VERSION = 0.5.54;
MARKETING_VERSION = 0.5.55;
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 = 67;
CURRENT_PROJECT_VERSION = 68;
DEVELOPMENT_TEAM = X25NYX3K49;
ENABLE_BITCODE = NO;
ENABLE_PREVIEWS = YES;
@ -393,7 +393,7 @@
"$(inherited)",
"@executable_path/Frameworks"
);
MARKETING_VERSION = 0.5.54;
MARKETING_VERSION = 0.5.55;
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
PRODUCT_NAME = "$(TARGET_NAME)";

View file

@ -2,6 +2,24 @@ import Foundation
import Network
import os.log
enum ViaplayPlaybackHeaders {
static let userAgent =
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36"
static let referer = "https://viaplay.com/"
static let origin = "https://viaplay.com"
static func matches(_ url: URL) -> Bool {
let h = (url.host ?? "").lowercased()
return h.contains("viaplay") || h.contains("viasat")
}
static func apply(to req: inout URLRequest) {
req.setValue(userAgent, forHTTPHeaderField: "User-Agent")
req.setValue(referer, forHTTPHeaderField: "Referer")
req.setValue(origin, forHTTPHeaderField: "Origin")
}
}
private let proxyLog = Logger(subsystem: "nl.vonas.mediacluster.ios", category: "ClearKeyProxy")
private final class ResumeOnce: @unchecked Sendable {
@ -50,6 +68,8 @@ final class ClearKeyDashProxy {
private var isHlsMode = false
/// AVPlayer kan geen DASH: serve clear HLS (native of vanuit MPD gegenereerd).
private var preferHlsOutput = false
/// Segment-CDN's zonder "viaplay" in de host krijgen dezelfde browser-UA als het manifest.
private var useViaplayBrowserHeaders = false
private var dashVideoRep: DashHlsRep?
private var dashAudioRep: DashHlsRep?
/// Beschikbare audio-reps (UI); master playlist heeft er maar één actief.
@ -77,6 +97,22 @@ final class ClearKeyDashProxy {
return URLSession(configuration: cfg)
}()
/// CFNetwork overschrijft User-Agent op URLRequest met de app-UA; alleen session-headers blijven staan.
private let viaplayBrowserSession: URLSession = {
let cfg = URLSessionConfiguration.ephemeral
cfg.timeoutIntervalForRequest = 15
cfg.timeoutIntervalForResource = 45
cfg.httpMaximumConnectionsPerHost = 10
cfg.requestCachePolicy = .reloadIgnoringLocalCacheData
cfg.httpAdditionalHeaders = [
"User-Agent": ViaplayPlaybackHeaders.userAgent,
"Referer": ViaplayPlaybackHeaders.referer,
"Origin": ViaplayPlaybackHeaders.origin,
"Accept": "*/*",
]
return URLSession(configuration: cfg)
}()
/// Serialiseert alleen manifest/playlist-requests (niet segment-decrypt).
/// Voorheen alles via één gate → segmenten blokkeerden live-refresh → stall na ~10s + zware buffer.
private let playlistGate = PayloadGate()
@ -175,6 +211,7 @@ final class ClearKeyDashProxy {
self.keyMap = map
self.isHlsMode = hls
self.preferHlsOutput = preferHlsForAVPlayer
self.useViaplayBrowserHeaders = ViaplayPlaybackHeaders.matches(manifestURL)
self.dashVideoRep = nil
self.dashAudioRep = nil
self.dashAudioReps = []
@ -395,6 +432,7 @@ final class ClearKeyDashProxy {
keyMap = nil
isHlsMode = false
preferHlsOutput = false
useViaplayBrowserHeaders = false
dashVideoRep = nil
dashAudioRep = nil
dashAudioReps = []
@ -1047,19 +1085,17 @@ final class ClearKeyDashProxy {
var req = URLRequest(url: url)
req.timeoutInterval = 15
req.cachePolicy = .reloadIgnoringLocalCacheData
let host = url.host?.lowercased() ?? ""
if host.contains("viaplay") {
req.setValue(
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36",
forHTTPHeaderField: "User-Agent"
)
req.setValue("https://viaplay.com/", forHTTPHeaderField: "Referer")
req.setValue("https://viaplay.com", forHTTPHeaderField: "Origin")
req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges")
let browser = withState { useViaplayBrowserHeaders } || ViaplayPlaybackHeaders.matches(url)
let session: URLSession
if browser {
ViaplayPlaybackHeaders.apply(to: &req)
session = viaplayBrowserSession
} else {
req.setValue("VMC-iOS-ClearKey/0.5.5", forHTTPHeaderField: "User-Agent")
session = urlSession
}
req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges")
let (data, resp) = try await urlSession.data(for: req)
let (data, resp) = try await session.data(for: req)
if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
throw ProxyError.upstream(http.statusCode)
}

View file

@ -97,7 +97,7 @@ struct PlayerScreen: View {
@State private var clearKeyPreferAV = true
private var isLive: Bool { request.isLive }
private var useClearKeyAV: Bool { request.isLive && request.needsClearKey && clearKeyPreferAV }
private var useClearKeyAV: Bool { 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) }
@ -566,7 +566,7 @@ struct PlayerScreen: View {
private func start() async {
loading = true
error = nil
if request.isLive && request.needsClearKey {
if request.needsClearKey {
let keys = request.drm?.keys ?? []
let fmt = (request.format ?? "").lowercased()
// ESPN e.d.: native HLS + aparte AUDIO-rendition → AVPlayer bevriest beeld bij rotatie.

View file

@ -239,6 +239,10 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
media.addOption(":freetype-rel-fontsize=\(Self.subtitleRelFontSize)")
media.addOption(":freetype-bold=1")
media.addOption(":sub-margin=0")
if ViaplayPlaybackHeaders.matches(url) {
media.addOption(":http-user-agent=\(ViaplayPlaybackHeaders.userAgent)")
media.addOption(":http-referrer=\(ViaplayPlaybackHeaders.referer)")
}
if mediaPlayer.drawable as AnyObject? !== drawableHost {
applyDrawable(force: true)
}
@ -316,6 +320,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
lengthMs = clamped
lengthText = Self.format(ms: clamped)
canSeek = true
liveMode = false
}
// Lokale clear-HLS of remote HLS.
if preferHls || ClearKeyDashProxy.isHlsURL(started.url) || ClearKeyDashProxy.isHlsURL(url) {
@ -458,10 +463,11 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
media.addOption(":http-reconnect")
media.addOption(":avcodec-hw=any")
media.addOption(":freetype-rel-fontsize=\(subtitleRelFontSize)")
let viaplay = url.host?.localizedCaseInsensitiveContains("viaplay") == true
if viaplay && !viaLocalProxy {
media.addOption(":http-user-agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36")
media.addOption(":http-referrer=https://viaplay.com/")
if viaLocalProxy {
media.addOption(":http-user-agent=VLC/3.0.21 LibVLC/3.0.21")
} else if ViaplayPlaybackHeaders.matches(url) {
media.addOption(":http-user-agent=\(ViaplayPlaybackHeaders.userAgent)")
media.addOption(":http-referrer=\(ViaplayPlaybackHeaders.referer)")
} else {
media.addOption(":http-user-agent=VLC/3.0.21 LibVLC/3.0.21")
}

View file

@ -96,7 +96,7 @@ struct SettingsScreen: View {
Section("Info") {
LabeledContent("App", value: "VMC iOS")
LabeledContent("Versie", value: "0.5.54")
LabeledContent("Versie", value: "0.5.55")
LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit")
LabeledContent("Events", value: app.eventsEnabled ? "Aan" : "Uit")
}