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:
parent
19aa0853a3
commit
80ab9cdb76
5 changed files with 63 additions and 21 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 = 67;
|
CURRENT_PROJECT_VERSION = 68;
|
||||||
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.54;
|
MARKETING_VERSION = 0.5.55;
|
||||||
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 = 67;
|
CURRENT_PROJECT_VERSION = 68;
|
||||||
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.54;
|
MARKETING_VERSION = 0.5.55;
|
||||||
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)";
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,24 @@ import Foundation
|
||||||
import Network
|
import Network
|
||||||
import os.log
|
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 let proxyLog = Logger(subsystem: "nl.vonas.mediacluster.ios", category: "ClearKeyProxy")
|
||||||
|
|
||||||
private final class ResumeOnce: @unchecked Sendable {
|
private final class ResumeOnce: @unchecked Sendable {
|
||||||
|
|
@ -50,6 +68,8 @@ final class ClearKeyDashProxy {
|
||||||
private var isHlsMode = false
|
private var isHlsMode = false
|
||||||
/// AVPlayer kan geen DASH: serve clear HLS (native of vanuit MPD gegenereerd).
|
/// AVPlayer kan geen DASH: serve clear HLS (native of vanuit MPD gegenereerd).
|
||||||
private var preferHlsOutput = false
|
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 dashVideoRep: DashHlsRep?
|
||||||
private var dashAudioRep: DashHlsRep?
|
private var dashAudioRep: DashHlsRep?
|
||||||
/// Beschikbare audio-reps (UI); master playlist heeft er maar één actief.
|
/// Beschikbare audio-reps (UI); master playlist heeft er maar één actief.
|
||||||
|
|
@ -77,6 +97,22 @@ final class ClearKeyDashProxy {
|
||||||
return URLSession(configuration: cfg)
|
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).
|
/// Serialiseert alleen manifest/playlist-requests (niet segment-decrypt).
|
||||||
/// Voorheen alles via één gate → segmenten blokkeerden live-refresh → stall na ~10s + zware buffer.
|
/// Voorheen alles via één gate → segmenten blokkeerden live-refresh → stall na ~10s + zware buffer.
|
||||||
private let playlistGate = PayloadGate()
|
private let playlistGate = PayloadGate()
|
||||||
|
|
@ -175,6 +211,7 @@ 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.dashVideoRep = nil
|
self.dashVideoRep = nil
|
||||||
self.dashAudioRep = nil
|
self.dashAudioRep = nil
|
||||||
self.dashAudioReps = []
|
self.dashAudioReps = []
|
||||||
|
|
@ -395,6 +432,7 @@ final class ClearKeyDashProxy {
|
||||||
keyMap = nil
|
keyMap = nil
|
||||||
isHlsMode = false
|
isHlsMode = false
|
||||||
preferHlsOutput = false
|
preferHlsOutput = false
|
||||||
|
useViaplayBrowserHeaders = false
|
||||||
dashVideoRep = nil
|
dashVideoRep = nil
|
||||||
dashAudioRep = nil
|
dashAudioRep = nil
|
||||||
dashAudioReps = []
|
dashAudioReps = []
|
||||||
|
|
@ -1047,19 +1085,17 @@ final class ClearKeyDashProxy {
|
||||||
var req = URLRequest(url: url)
|
var req = URLRequest(url: url)
|
||||||
req.timeoutInterval = 15
|
req.timeoutInterval = 15
|
||||||
req.cachePolicy = .reloadIgnoringLocalCacheData
|
req.cachePolicy = .reloadIgnoringLocalCacheData
|
||||||
let host = url.host?.lowercased() ?? ""
|
req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges")
|
||||||
if host.contains("viaplay") {
|
let browser = withState { useViaplayBrowserHeaders } || ViaplayPlaybackHeaders.matches(url)
|
||||||
req.setValue(
|
let session: URLSession
|
||||||
"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",
|
if browser {
|
||||||
forHTTPHeaderField: "User-Agent"
|
ViaplayPlaybackHeaders.apply(to: &req)
|
||||||
)
|
session = viaplayBrowserSession
|
||||||
req.setValue("https://viaplay.com/", forHTTPHeaderField: "Referer")
|
|
||||||
req.setValue("https://viaplay.com", forHTTPHeaderField: "Origin")
|
|
||||||
} else {
|
} else {
|
||||||
req.setValue("VMC-iOS-ClearKey/0.5.5", forHTTPHeaderField: "User-Agent")
|
req.setValue("VMC-iOS-ClearKey/0.5.5", forHTTPHeaderField: "User-Agent")
|
||||||
|
session = urlSession
|
||||||
}
|
}
|
||||||
req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges")
|
let (data, resp) = try await session.data(for: req)
|
||||||
let (data, resp) = try await urlSession.data(for: req)
|
|
||||||
if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
|
if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
|
||||||
throw ProxyError.upstream(http.statusCode)
|
throw ProxyError.upstream(http.statusCode)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -97,7 +97,7 @@ struct PlayerScreen: View {
|
||||||
@State private var clearKeyPreferAV = true
|
@State private var clearKeyPreferAV = true
|
||||||
|
|
||||||
private var isLive: Bool { request.isLive }
|
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.
|
/// F1-replay e.d. komen via live-request binnen, maar hebben een echte duur.
|
||||||
private var showSeekChrome: Bool { !isLive || (!useClearKeyAV && vlc.canSeek) }
|
private var showSeekChrome: Bool { !isLive || (!useClearKeyAV && vlc.canSeek) }
|
||||||
|
|
||||||
|
|
@ -566,7 +566,7 @@ struct PlayerScreen: View {
|
||||||
private func start() async {
|
private func start() async {
|
||||||
loading = true
|
loading = true
|
||||||
error = nil
|
error = nil
|
||||||
if request.isLive && request.needsClearKey {
|
if request.needsClearKey {
|
||||||
let keys = request.drm?.keys ?? []
|
let keys = request.drm?.keys ?? []
|
||||||
let fmt = (request.format ?? "").lowercased()
|
let fmt = (request.format ?? "").lowercased()
|
||||||
// ESPN e.d.: native HLS + aparte AUDIO-rendition → AVPlayer bevriest beeld bij rotatie.
|
// ESPN e.d.: native HLS + aparte AUDIO-rendition → AVPlayer bevriest beeld bij rotatie.
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,10 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
media.addOption(":freetype-rel-fontsize=\(Self.subtitleRelFontSize)")
|
media.addOption(":freetype-rel-fontsize=\(Self.subtitleRelFontSize)")
|
||||||
media.addOption(":freetype-bold=1")
|
media.addOption(":freetype-bold=1")
|
||||||
media.addOption(":sub-margin=0")
|
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 {
|
if mediaPlayer.drawable as AnyObject? !== drawableHost {
|
||||||
applyDrawable(force: true)
|
applyDrawable(force: true)
|
||||||
}
|
}
|
||||||
|
|
@ -316,6 +320,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
||||||
lengthMs = clamped
|
lengthMs = clamped
|
||||||
lengthText = Self.format(ms: clamped)
|
lengthText = Self.format(ms: clamped)
|
||||||
canSeek = true
|
canSeek = true
|
||||||
|
liveMode = false
|
||||||
}
|
}
|
||||||
// Lokale clear-HLS of remote HLS.
|
// Lokale clear-HLS of remote HLS.
|
||||||
if preferHls || ClearKeyDashProxy.isHlsURL(started.url) || ClearKeyDashProxy.isHlsURL(url) {
|
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(":http-reconnect")
|
||||||
media.addOption(":avcodec-hw=any")
|
media.addOption(":avcodec-hw=any")
|
||||||
media.addOption(":freetype-rel-fontsize=\(subtitleRelFontSize)")
|
media.addOption(":freetype-rel-fontsize=\(subtitleRelFontSize)")
|
||||||
let viaplay = url.host?.localizedCaseInsensitiveContains("viaplay") == true
|
if viaLocalProxy {
|
||||||
if viaplay && !viaLocalProxy {
|
media.addOption(":http-user-agent=VLC/3.0.21 LibVLC/3.0.21")
|
||||||
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")
|
} else if ViaplayPlaybackHeaders.matches(url) {
|
||||||
media.addOption(":http-referrer=https://viaplay.com/")
|
media.addOption(":http-user-agent=\(ViaplayPlaybackHeaders.userAgent)")
|
||||||
|
media.addOption(":http-referrer=\(ViaplayPlaybackHeaders.referer)")
|
||||||
} else {
|
} else {
|
||||||
media.addOption(":http-user-agent=VLC/3.0.21 LibVLC/3.0.21")
|
media.addOption(":http-user-agent=VLC/3.0.21 LibVLC/3.0.21")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.54")
|
LabeledContent("Versie", value: "0.5.55")
|
||||||
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