From 80ab9cdb76a019d1aa7193359fe8eef0bbac57e5 Mon Sep 17 00:00:00 2001 From: Jos Vooges | STH Date: Mon, 28 Sep 2026 20:57:22 +0200 Subject: [PATCH] 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. --- apps/ios/VMC.xcodeproj/project.pbxproj | 8 +-- apps/ios/VMC/Playback/ClearKeyDashProxy.swift | 56 +++++++++++++++---- apps/ios/VMC/Playback/PlayerScreen.swift | 4 +- .../VMC/Playback/VLCPlayerController.swift | 14 +++-- apps/ios/VMC/UI/SettingsScreen.swift | 2 +- 5 files changed, 63 insertions(+), 21 deletions(-) diff --git a/apps/ios/VMC.xcodeproj/project.pbxproj b/apps/ios/VMC.xcodeproj/project.pbxproj index 28bc006..30029ed 100644 --- a/apps/ios/VMC.xcodeproj/project.pbxproj +++ b/apps/ios/VMC.xcodeproj/project.pbxproj @@ -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)"; diff --git a/apps/ios/VMC/Playback/ClearKeyDashProxy.swift b/apps/ios/VMC/Playback/ClearKeyDashProxy.swift index e10fc1a..ff9ed5f 100644 --- a/apps/ios/VMC/Playback/ClearKeyDashProxy.swift +++ b/apps/ios/VMC/Playback/ClearKeyDashProxy.swift @@ -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) } diff --git a/apps/ios/VMC/Playback/PlayerScreen.swift b/apps/ios/VMC/Playback/PlayerScreen.swift index 11f7512..ca8ef34 100644 --- a/apps/ios/VMC/Playback/PlayerScreen.swift +++ b/apps/ios/VMC/Playback/PlayerScreen.swift @@ -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. diff --git a/apps/ios/VMC/Playback/VLCPlayerController.swift b/apps/ios/VMC/Playback/VLCPlayerController.swift index 91eb89a..a7cc789 100644 --- a/apps/ios/VMC/Playback/VLCPlayerController.swift +++ b/apps/ios/VMC/Playback/VLCPlayerController.swift @@ -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") } diff --git a/apps/ios/VMC/UI/SettingsScreen.swift b/apps/ios/VMC/UI/SettingsScreen.swift index 15bb242..7fed467 100644 --- a/apps/ios/VMC/UI/SettingsScreen.swift +++ b/apps/ios/VMC/UI/SettingsScreen.swift @@ -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") }