Play Viaplay catch-up from the KAP window start, not the live edge.
This commit is contained in:
parent
721a2d1f2a
commit
f35200e0c3
7 changed files with 125 additions and 28 deletions
|
|
@ -345,7 +345,7 @@
|
|||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 71;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -360,7 +360,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.58;
|
||||
MARKETING_VERSION = 0.5.59;
|
||||
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 = 71;
|
||||
CURRENT_PROJECT_VERSION = 72;
|
||||
DEVELOPMENT_TEAM = X25NYX3K49;
|
||||
ENABLE_BITCODE = NO;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
|
|
@ -393,7 +393,7 @@
|
|||
"$(inherited)",
|
||||
"@executable_path/Frameworks"
|
||||
);
|
||||
MARKETING_VERSION = 0.5.58;
|
||||
MARKETING_VERSION = 0.5.59;
|
||||
OTHER_LDFLAGS = ("$(inherited)", "-ObjC");
|
||||
PRODUCT_BUNDLE_IDENTIFIER = nl.vonas.mediacluster.ios;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ final class AVLivePlayerController: NSObject, ObservableObject {
|
|||
fallback: URL? = nil,
|
||||
format: String? = nil,
|
||||
clearKeys: [ClearKeyEntry] = [],
|
||||
browserCdnHeaders: Bool = false
|
||||
browserCdnHeaders: Bool = false,
|
||||
forceVod: Bool = false
|
||||
) async {
|
||||
generation &+= 1
|
||||
let gen = generation
|
||||
|
|
@ -80,7 +81,8 @@ final class AVLivePlayerController: NSObject, ObservableObject {
|
|||
manifestURL: url,
|
||||
keys: clearKeys,
|
||||
preferHlsForAVPlayer: true,
|
||||
browserCdnHeaders: browserCdnHeaders
|
||||
browserCdnHeaders: browserCdnHeaders,
|
||||
forceVod: forceVod
|
||||
)
|
||||
guard gen == generation, !stopping else { return }
|
||||
playURL = started.url
|
||||
|
|
|
|||
|
|
@ -18,6 +18,10 @@ enum ViaplayPlaybackHeaders {
|
|||
return false
|
||||
}
|
||||
|
||||
static func isKapWindowURL(_ url: URL) -> Bool {
|
||||
url.path.range(of: #"/start/\d+/stop/\d+"#, options: .regularExpression) != nil
|
||||
}
|
||||
|
||||
static func apply(to req: inout URLRequest) {
|
||||
// User-Agent NIET op URLRequest zetten: CFNetwork vervangt die dan door VMC/… CFNetwork/…
|
||||
req.setValue(referer, forHTTPHeaderField: "Referer")
|
||||
|
|
@ -144,6 +148,8 @@ final class ClearKeyDashProxy {
|
|||
private var isHlsMode = false
|
||||
/// AVPlayer kan geen DASH: serve clear HLS (native of vanuit MPD gegenereerd).
|
||||
private var preferHlsOutput = false
|
||||
/// Catch-up/KAP: HLS vanaf het begin van het venster, niet live-edge.
|
||||
private var forceVodPlaylist = false
|
||||
/// Segment-CDN's zonder "viaplay" in de host krijgen dezelfde browser-UA als het manifest.
|
||||
private var useViaplayBrowserHeaders = false
|
||||
private var dashVideoRep: DashHlsRep?
|
||||
|
|
@ -274,7 +280,8 @@ final class ClearKeyDashProxy {
|
|||
manifestURL: URL,
|
||||
keys: [ClearKeyEntry],
|
||||
preferHlsForAVPlayer: Bool = false,
|
||||
browserCdnHeaders: Bool = false
|
||||
browserCdnHeaders: Bool = false,
|
||||
forceVod: Bool = false
|
||||
) async throws -> StartResult {
|
||||
let map = ClearKeyKeyMap(entries: keys)
|
||||
guard !map.isEmpty else {
|
||||
|
|
@ -293,11 +300,15 @@ final class ClearKeyDashProxy {
|
|||
self.keyMap = map
|
||||
self.isHlsMode = hls
|
||||
self.preferHlsOutput = preferHlsForAVPlayer
|
||||
self.forceVodPlaylist = forceVod
|
||||
self.useViaplayBrowserHeaders =
|
||||
browserCdnHeaders || ViaplayPlaybackHeaders.matches(manifestURL)
|
||||
if self.useViaplayBrowserHeaders {
|
||||
proxyLog.info("CDN headers=Chrome/122 for whole session")
|
||||
}
|
||||
if forceVod {
|
||||
proxyLog.info("catch-up/KAP: VOD HLS from window start (not live-edge)")
|
||||
}
|
||||
self.dashVideoRep = nil
|
||||
self.dashAudioRep = nil
|
||||
self.dashAudioReps = []
|
||||
|
|
@ -518,6 +529,7 @@ final class ClearKeyDashProxy {
|
|||
keyMap = nil
|
||||
isHlsMode = false
|
||||
preferHlsOutput = false
|
||||
forceVodPlaylist = false
|
||||
useViaplayBrowserHeaders = false
|
||||
dashVideoRep = nil
|
||||
dashAudioRep = nil
|
||||
|
|
@ -1564,8 +1576,11 @@ final class ClearKeyDashProxy {
|
|||
}
|
||||
|
||||
private func parseDashReps(_ xml: String, mpdURL: URL) -> [ParsedRep] {
|
||||
let isLive = xml.range(of: #"type\s*=\s*"dynamic""#, options: [.regularExpression, .caseInsensitive]) != nil
|
||||
let vod = withState { self.forceVodPlaylist } || ViaplayPlaybackHeaders.isKapWindowURL(mpdURL)
|
||||
let isLive = !vod && (
|
||||
xml.range(of: #"type\s*=\s*"dynamic""#, options: [.regularExpression, .caseInsensitive]) != nil
|
||||
|| xml.range(of: #"type\s*=\s*'dynamic'"#, options: [.regularExpression, .caseInsensitive]) != nil
|
||||
)
|
||||
var availabilityStart: Date?
|
||||
if let m = xml.range(of: #"availabilityStartTime\s*=\s*"([^"]+)""#, options: [.regularExpression, .caseInsensitive]) {
|
||||
let attr = String(xml[m])
|
||||
|
|
@ -1575,9 +1590,9 @@ final class ClearKeyDashProxy {
|
|||
}
|
||||
}
|
||||
|
||||
// Live multi-period: pak laatste Period (actuele edge).
|
||||
// Live multi-period: pak laatste Period (actuele edge). Catch-up/KAP: hele venster.
|
||||
var scopeXML = xml
|
||||
if let periodRegex = try? NSRegularExpression(
|
||||
if !vod, let periodRegex = try? NSRegularExpression(
|
||||
pattern: #"<Period\b([^>]*)>([\s\S]*?)</Period>"#,
|
||||
options: [.caseInsensitive]
|
||||
) {
|
||||
|
|
@ -1849,8 +1864,6 @@ final class ClearKeyDashProxy {
|
|||
var urls = rep.segmentURLs
|
||||
if rep.isLive, urls.count > 12 {
|
||||
urls = Array(urls.suffix(12))
|
||||
} else if urls.count > 20 {
|
||||
urls = Array(urls.suffix(20))
|
||||
}
|
||||
windowStartIndex = max(0, rep.segmentURLs.count - urls.count)
|
||||
for path in urls {
|
||||
|
|
@ -1894,16 +1907,17 @@ final class ClearKeyDashProxy {
|
|||
edgePad = max(0, total - 3)
|
||||
}
|
||||
let end = max(0, total - edgePad)
|
||||
// $Time$ (Viaplay/Ziggo): 10 segs. $Number$ (Prime/HBO): 5 segs ≈10s — sneller bufferen.
|
||||
// Catch-up/VOD: hele venster vanaf het begin. Live: kort stuk achter de edge.
|
||||
let windowLen: Int
|
||||
if !rep.isLive {
|
||||
windowLen = 12
|
||||
windowLen = min(end, 5000)
|
||||
} else if preferNumber {
|
||||
windowLen = 5
|
||||
} else {
|
||||
windowLen = 10
|
||||
}
|
||||
let start = max(0, end - windowLen)
|
||||
let start = rep.isLive ? max(0, end - windowLen) : 0
|
||||
let clipEnd = min(end, start + windowLen)
|
||||
windowStartIndex = start
|
||||
// Alleen het venster materialiseren (Prime/HBO: r≈1800).
|
||||
var absIndex = 0
|
||||
|
|
@ -1911,7 +1925,7 @@ final class ClearKeyDashProxy {
|
|||
let runCount = max(0, run.count)
|
||||
let runEnd = absIndex + runCount
|
||||
let overlapStart = max(start, absIndex)
|
||||
let overlapEnd = min(end, runEnd)
|
||||
let overlapEnd = min(clipEnd, runEnd)
|
||||
if overlapStart < overlapEnd {
|
||||
for i in overlapStart..<overlapEnd {
|
||||
let offsetInRun = i - absIndex
|
||||
|
|
@ -1934,7 +1948,7 @@ final class ClearKeyDashProxy {
|
|||
}
|
||||
}
|
||||
absIndex = runEnd
|
||||
if absIndex >= end { break }
|
||||
if absIndex >= clipEnd { break }
|
||||
}
|
||||
} else {
|
||||
// Alleen $Number$ zonder SegmentTimeline (Prime/HBO fallback).
|
||||
|
|
@ -1996,6 +2010,9 @@ final class ClearKeyDashProxy {
|
|||
"#EXT-X-MEDIA-SEQUENCE:\(mediaSequence)",
|
||||
"#EXT-X-INDEPENDENT-SEGMENTS",
|
||||
]
|
||||
if !rep.isLive {
|
||||
lines.append("#EXT-X-PLAYLIST-TYPE:VOD")
|
||||
}
|
||||
if !rep.initialization.isEmpty {
|
||||
let initURL = resolveTemplateURL(
|
||||
rep.initialization,
|
||||
|
|
|
|||
|
|
@ -27,6 +27,12 @@ struct PlayRequest: Identifiable {
|
|||
|| (watchId ?? "").localizedCaseInsensitiveContains("viaplay")
|
||||
}
|
||||
|
||||
/// KAP-terugkijken is een live-MPD met start/stop — niet naar live-edge (Don't go).
|
||||
var playAsVod: Bool {
|
||||
if !isLive { return true }
|
||||
return url.path.range(of: #"/start/\d+/stop/\d+"#, options: .regularExpression) != nil
|
||||
}
|
||||
|
||||
static func vod(
|
||||
url: URL,
|
||||
title: String,
|
||||
|
|
@ -586,7 +592,8 @@ struct PlayerScreen: View {
|
|||
format: request.format,
|
||||
clearKeys: keys,
|
||||
preferHls: true,
|
||||
browserCdnHeaders: request.usesViaplayCdn
|
||||
browserCdnHeaders: request.usesViaplayCdn,
|
||||
forceVod: request.playAsVod
|
||||
)
|
||||
let maxWaitNs: UInt64 = 18_000_000_000
|
||||
let stepNs: UInt64 = 200_000_000
|
||||
|
|
@ -605,7 +612,8 @@ struct PlayerScreen: View {
|
|||
fallback: request.fallbackUrl,
|
||||
format: request.format,
|
||||
clearKeys: keys,
|
||||
browserCdnHeaders: request.usesViaplayCdn
|
||||
browserCdnHeaders: request.usesViaplayCdn,
|
||||
forceVod: request.playAsVod
|
||||
)
|
||||
let maxWaitNs: UInt64 = 18_000_000_000
|
||||
let stepNs: UInt64 = 200_000_000
|
||||
|
|
@ -632,7 +640,8 @@ struct PlayerScreen: View {
|
|||
format: request.format,
|
||||
clearKeys: keys,
|
||||
preferHls: true,
|
||||
browserCdnHeaders: request.usesViaplayCdn
|
||||
browserCdnHeaders: request.usesViaplayCdn,
|
||||
forceVod: request.playAsVod
|
||||
)
|
||||
waited = 0
|
||||
while waited < 8_000_000_000 && !vlc.isPlaying && !vlc.hasError {
|
||||
|
|
|
|||
|
|
@ -279,7 +279,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
format: String? = nil,
|
||||
clearKeys: [ClearKeyEntry] = [],
|
||||
preferHls: Bool = false,
|
||||
browserCdnHeaders: Bool = false
|
||||
browserCdnHeaders: Bool = false,
|
||||
forceVod: Bool = false
|
||||
) async {
|
||||
// Annuleer pending finishStop / delayed proxy-stop van vorige sessie.
|
||||
stopGeneration &+= 1
|
||||
|
|
@ -296,7 +297,7 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
restoreAudioHint = ""
|
||||
knownVodDurationMs = 0
|
||||
seekOriginMs = 0
|
||||
liveMode = true
|
||||
liveMode = !forceVod
|
||||
usePipDrawable = false
|
||||
pipPossible = false
|
||||
|
||||
|
|
@ -312,7 +313,8 @@ final class VLCPlayerController: NSObject, ObservableObject, VLCMediaPlayerDeleg
|
|||
manifestURL: url,
|
||||
keys: clearKeys,
|
||||
preferHlsForAVPlayer: preferHls,
|
||||
browserCdnHeaders: browserCdnHeaders
|
||||
browserCdnHeaders: browserCdnHeaders,
|
||||
forceVod: forceVod
|
||||
)
|
||||
playURL = started.url
|
||||
proxySession = started.session
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ struct SettingsScreen: View {
|
|||
|
||||
Section("Info") {
|
||||
LabeledContent("App", value: "VMC iOS")
|
||||
LabeledContent("Versie", value: "0.5.58")
|
||||
LabeledContent("Versie", value: "0.5.59")
|
||||
LabeledContent("Live TV", value: app.iptvEnabled ? "Aan" : "Uit")
|
||||
LabeledContent("Events", value: app.eventsEnabled ? "Aan" : "Uit")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -116,6 +116,7 @@ function hasKapWindow(url: string | null | undefined): boolean {
|
|||
type StoredDrmPlay = {
|
||||
keys: Array<{ kid: string; key: string }>;
|
||||
mpdUrl: string | null;
|
||||
eventMatch?: boolean;
|
||||
};
|
||||
|
||||
async function storedDrmPlay(
|
||||
|
|
@ -407,16 +408,18 @@ async function storedXtraChannelPlay(
|
|||
(a, b) => b[1].score - a[1].score || b[1].lastSeen - a[1].lastSeen
|
||||
);
|
||||
for (const [url, g] of ranked) {
|
||||
if (g.keys.length) return { keys: g.keys, mpdUrl: url };
|
||||
if (g.keys.length) {
|
||||
return { keys: g.keys, mpdUrl: url, eventMatch: g.score >= 200 };
|
||||
}
|
||||
}
|
||||
const salvage = preferLiveChannelUrl([
|
||||
...xtraUrls,
|
||||
...(extraMpdUrls || []),
|
||||
]);
|
||||
if (guidKeys.length && salvage && isXtraMpd(salvage)) {
|
||||
return { keys: guidKeys, mpdUrl: salvage };
|
||||
return { keys: guidKeys, mpdUrl: salvage, eventMatch: true };
|
||||
}
|
||||
return { keys: [], mpdUrl: null };
|
||||
return { keys: [], mpdUrl: null, eventMatch: false };
|
||||
}
|
||||
|
||||
async function dropEventMpdCache(mediaGuid: string, reason: string) {
|
||||
|
|
@ -490,11 +493,75 @@ export async function ensureViaplayEventReady(
|
|||
}
|
||||
}
|
||||
|
||||
// Terugkijken: KAP op de zender-MPD van dit event (xtra als die bij dit guid hoort).
|
||||
if (ended) {
|
||||
const xtra = await storedXtraChannelPlay(
|
||||
sessionSecret,
|
||||
mediaGuid,
|
||||
event.title,
|
||||
settings.config.events.map((e) => e.mpdUrl)
|
||||
);
|
||||
const baseRaw =
|
||||
(xtra.eventMatch && xtra.mpdUrl) ||
|
||||
preferLiveChannelUrl([stored.mpdUrl, event.mpdUrl, cachedMpd]) ||
|
||||
stored.mpdUrl ||
|
||||
event.mpdUrl ||
|
||||
cachedMpd;
|
||||
const base = baseRaw ? stripKapWindow(baseRaw) : null;
|
||||
const keys = xtra.eventMatch && xtra.keys.length ? xtra.keys : cachedKeys;
|
||||
if (base && keys.length && isReusableLiveMpd(base)) {
|
||||
try {
|
||||
const kap = await resolveKapMpd(
|
||||
base,
|
||||
event.startMs,
|
||||
event.endMs,
|
||||
Math.min(tweaks.httpTimeoutMs, 12_000)
|
||||
);
|
||||
if (
|
||||
!isDontGoPlaceholder(kap.mpdUrl) &&
|
||||
!(kap.xml && mpdLooksLikeDontGo(kap.xml))
|
||||
) {
|
||||
await patchViaplayEventCache(mediaGuid, {
|
||||
mpdUrl: kap.mpdUrl,
|
||||
mpdCachedAt: new Date().toISOString(),
|
||||
hasPssh: kap.hasPssh,
|
||||
hasKeys: true,
|
||||
lastPlayError: null,
|
||||
lastPlayAt: new Date().toISOString(),
|
||||
});
|
||||
recordLivePlay({
|
||||
guid: mediaGuid,
|
||||
ok: true,
|
||||
title: event.title,
|
||||
mediaPath: mediaPathOf(kap.mpdUrl),
|
||||
hasLicense: true,
|
||||
fromCache: true,
|
||||
cookies: emptyCookies(),
|
||||
error: null,
|
||||
});
|
||||
return {
|
||||
streamUrl: kap.mpdUrl,
|
||||
format: "dash",
|
||||
keys,
|
||||
fromCache: true,
|
||||
mediaGuid,
|
||||
live: false,
|
||||
};
|
||||
}
|
||||
} catch {
|
||||
/* bestaande catch-up-paden hieronder */
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Zelfde bron als de DRM-tabel in admin: opgeslagen MPD + KID/KEY.
|
||||
// Alleen opnieuw play-live als die set ontbreekt of Don't go is.
|
||||
if (!opts?.forceRefresh && cachedKeys.length) {
|
||||
let url = ended
|
||||
? stored.mpdUrl || event.mpdUrl || cachedMpd
|
||||
? preferLiveChannelUrl([stored.mpdUrl, event.mpdUrl, cachedMpd]) ||
|
||||
stored.mpdUrl ||
|
||||
event.mpdUrl ||
|
||||
cachedMpd
|
||||
: preferLiveChannelUrl([event.mpdUrl, cachedMpd, stored.mpdUrl]);
|
||||
if (url && isReusableLiveMpd(url)) {
|
||||
if (!ended) url = stripKapWindow(url);
|
||||
|
|
|
|||
Loading…
Reference in a new issue