Proxy Viaplay DASH through the API so clients never fetch Don't go MPDs.

CDN returns the promo MPD when the app UA survives a redirect; the play API now serves a signed Chrome/122 proxy for MPD and segments. iOS pins that UA on every hop.
This commit is contained in:
Jos Vooges | STH 2026-09-28 21:13:32 +02:00
parent 80ab9cdb76
commit abdd829615
9 changed files with 292 additions and 34 deletions

View file

@ -39,6 +39,7 @@ import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
import nl.vonas.mediacluster.tv.data.ClearKeyEntry import nl.vonas.mediacluster.tv.data.ClearKeyEntry
import nl.vonas.mediacluster.tv.data.DrmInfo import nl.vonas.mediacluster.tv.data.DrmInfo
import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.nio.ByteBuffer import java.nio.ByteBuffer
@ -194,13 +195,22 @@ fun buildTvPlayer(context: Context): ExoPlayer {
private const val VIAPLAY_BROWSER_UA = private const val VIAPLAY_BROWSER_UA =
"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" "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"
private fun isViaplayCdnHost(host: String): Boolean = private fun isViaplayCdn(url: HttpUrl): Boolean {
host.contains("viaplay", ignoreCase = true) val host = url.host.lowercase()
val path = url.encodedPath.lowercase()
return host.contains("viaplay") ||
host.contains("viasat") ||
host.contains("live-dash") ||
host.contains("vod-dash") ||
path.contains("mediapkg") ||
path.contains("tve_vsport") ||
path.contains("vp-xtra")
}
/** Viaplay-CDN geeft Don't go terug op app-UA's; zelfde headers als de server-play. */ /** Viaplay-CDN geeft Don't go terug op app-UA's; zelfde headers als de server-play. */
private val viaplayBrowserHeaders = Interceptor { chain -> private val viaplayBrowserHeaders = Interceptor { chain ->
val req = chain.request() val req = chain.request()
if (!isViaplayCdnHost(req.url.host)) return@Interceptor chain.proceed(req) if (!isViaplayCdn(req.url)) return@Interceptor chain.proceed(req)
chain.proceed( chain.proceed(
req.newBuilder() req.newBuilder()
.header("User-Agent", VIAPLAY_BROWSER_UA) .header("User-Agent", VIAPLAY_BROWSER_UA)
@ -216,7 +226,7 @@ private val liveOkHttp: OkHttpClient by lazy {
.connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) .retryOnConnectionFailure(true)
.addInterceptor(viaplayBrowserHeaders) .addNetworkInterceptor(viaplayBrowserHeaders)
.build() .build()
} }

View file

@ -43,6 +43,7 @@ import io.github.anilbeesetti.nextlib.media3ext.ffdecoder.NextRenderersFactory
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_OFF
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_ON
import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER import androidx.media3.exoplayer.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER
import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import java.util.UUID import java.util.UUID
@ -203,13 +204,22 @@ fun buildTvPlayer(context: Context, audioPassthrough: Boolean = false): ExoPlaye
private const val VIAPLAY_BROWSER_UA = private const val VIAPLAY_BROWSER_UA =
"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" "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"
private fun isViaplayCdnHost(host: String): Boolean = private fun isViaplayCdn(url: HttpUrl): Boolean {
host.contains("viaplay", ignoreCase = true) val host = url.host.lowercase()
val path = url.encodedPath.lowercase()
return host.contains("viaplay") ||
host.contains("viasat") ||
host.contains("live-dash") ||
host.contains("vod-dash") ||
path.contains("mediapkg") ||
path.contains("tve_vsport") ||
path.contains("vp-xtra")
}
/** Viaplay-CDN geeft Don't go terug op app-UA's; zelfde headers als de server-play. */ /** Viaplay-CDN geeft Don't go terug op app-UA's; zelfde headers als de server-play. */
private val viaplayBrowserHeaders = Interceptor { chain -> private val viaplayBrowserHeaders = Interceptor { chain ->
val req = chain.request() val req = chain.request()
if (!isViaplayCdnHost(req.url.host)) return@Interceptor chain.proceed(req) if (!isViaplayCdn(req.url)) return@Interceptor chain.proceed(req)
chain.proceed( chain.proceed(
req.newBuilder() req.newBuilder()
.header("User-Agent", VIAPLAY_BROWSER_UA) .header("User-Agent", VIAPLAY_BROWSER_UA)
@ -225,7 +235,7 @@ private val liveOkHttp: OkHttpClient by lazy {
.connectTimeout(15, TimeUnit.SECONDS) .connectTimeout(15, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS)
.retryOnConnectionFailure(true) .retryOnConnectionFailure(true)
.addInterceptor(viaplayBrowserHeaders) .addNetworkInterceptor(viaplayBrowserHeaders)
.build() .build()
} }

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 = 68; CURRENT_PROJECT_VERSION = 69;
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.55; MARKETING_VERSION = 0.5.56;
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 = 68; CURRENT_PROJECT_VERSION = 69;
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.55; MARKETING_VERSION = 0.5.56;
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

@ -10,7 +10,12 @@ enum ViaplayPlaybackHeaders {
static func matches(_ url: URL) -> Bool { static func matches(_ url: URL) -> Bool {
let h = (url.host ?? "").lowercased() let h = (url.host ?? "").lowercased()
return h.contains("viaplay") || h.contains("viasat") let s = url.absoluteString.lowercased()
if h.contains("viaplay") || h.contains("viasat") { return true }
if s.contains("viaplay") || s.contains("viasat") { return true }
if h.contains("live-dash") || h.contains("vod-dash") { return true }
if s.contains("mediapkg") || s.contains("tve_vsport") || s.contains("vp-xtra") { return true }
return false
} }
static func apply(to req: inout URLRequest) { static func apply(to req: inout URLRequest) {
@ -22,6 +27,21 @@ enum ViaplayPlaybackHeaders {
private let proxyLog = Logger(subsystem: "nl.vonas.mediacluster.ios", category: "ClearKeyProxy") private let proxyLog = Logger(subsystem: "nl.vonas.mediacluster.ios", category: "ClearKeyProxy")
/// CFNetwork volgt redirects met de app-UA; wij volgen zelf en zetten Chrome/122 opnieuw.
private final class DenyRedirects: NSObject, URLSessionTaskDelegate {
func urlSession(
_ session: URLSession,
task: URLSessionTask,
willPerformHTTPRedirection response: HTTPURLResponse,
newRequest request: URLRequest,
completionHandler: @escaping (URLRequest?) -> Void
) {
completionHandler(nil)
}
}
private let viaplayDenyRedirects = DenyRedirects()
private final class ResumeOnce: @unchecked Sendable { private final class ResumeOnce: @unchecked Sendable {
private var done = false private var done = false
private let lock = NSLock() private let lock = NSLock()
@ -110,7 +130,11 @@ final class ClearKeyDashProxy {
"Origin": ViaplayPlaybackHeaders.origin, "Origin": ViaplayPlaybackHeaders.origin,
"Accept": "*/*", "Accept": "*/*",
] ]
return URLSession(configuration: cfg) return URLSession(
configuration: cfg,
delegate: viaplayDenyRedirects,
delegateQueue: nil
)
}() }()
/// Serialiseert alleen manifest/playlist-requests (niet segment-decrypt). /// Serialiseert alleen manifest/playlist-requests (niet segment-decrypt).
@ -1082,24 +1106,42 @@ final class ClearKeyDashProxy {
} }
private func fetchFollowingRedirects(_ url: URL) async throws -> (data: Data, finalURL: URL) { private func fetchFollowingRedirects(_ url: URL) async throws -> (data: Data, finalURL: URL) {
var req = URLRequest(url: url) var current = url
req.timeoutInterval = 15 for _ in 0..<8 {
req.cachePolicy = .reloadIgnoringLocalCacheData var req = URLRequest(url: current)
req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges") req.timeoutInterval = 15
let browser = withState { useViaplayBrowserHeaders } || ViaplayPlaybackHeaders.matches(url) req.cachePolicy = .reloadIgnoringLocalCacheData
let session: URLSession req.setValue("bytes", forHTTPHeaderField: "Accept-Ranges")
if browser { let browser = withState { useViaplayBrowserHeaders } || ViaplayPlaybackHeaders.matches(current)
ViaplayPlaybackHeaders.apply(to: &req) let session: URLSession
session = viaplayBrowserSession if browser {
} else { ViaplayPlaybackHeaders.apply(to: &req)
req.setValue("VMC-iOS-ClearKey/0.5.5", forHTTPHeaderField: "User-Agent") session = viaplayBrowserSession
session = urlSession } else {
req.setValue("VMC-iOS-ClearKey/0.5.5", forHTTPHeaderField: "User-Agent")
session = urlSession
}
let (data, resp) = try await session.data(for: req)
guard let http = resp as? HTTPURLResponse else {
return (data, current)
}
if (300...399).contains(http.statusCode) {
guard
let loc = http.value(forHTTPHeaderField: "Location"),
let next = URL(string: loc, relativeTo: current)?.absoluteURL
else {
throw ProxyError.upstream(http.statusCode)
}
proxyLog.info("cdn redirect \(http.statusCode, privacy: .public) -> \(next.host ?? "?", privacy: .public)")
current = next
continue
}
if !(200...299).contains(http.statusCode) {
throw ProxyError.upstream(http.statusCode)
}
return (data, http.url ?? current)
} }
let (data, resp) = try await session.data(for: req) throw ProxyError.upstream(310)
if let http = resp as? HTTPURLResponse, !(200...299).contains(http.statusCode) {
throw ProxyError.upstream(http.statusCode)
}
return (data, resp.url ?? url)
} }
/// Manifest ophalen via de sticky eind-URL (zoals ExoPlayer); faalt die, één keer opnieuw via het origineel. /// Manifest ophalen via de sticky eind-URL (zoals ExoPlayer); faalt die, één keer opnieuw via het origineel.

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.55") LabeledContent("Versie", value: "0.5.56")
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")
} }

View file

@ -0,0 +1,184 @@
import { createHmac, timingSafeEqual } from "node:crypto";
import { Readable } from "node:stream";
import type { FastifyInstance, FastifyReply, FastifyRequest } from "fastify";
import type { Config } from "../config";
import { VIAPLAY_USER_AGENT } from "./client";
const TTL_SEC = 8 * 3600;
export function isAllowedViaplayOrigin(url: string): boolean {
try {
const u = new URL(url);
if (u.protocol !== "https:") return false;
const h = u.hostname.toLowerCase();
return (
h.includes("viaplay") ||
h.includes("viasat") ||
/live-dash|vod-dash/.test(h)
);
} catch {
return false;
}
}
function sign(secret: string, exp: number, host: string): string {
return createHmac("sha256", secret)
.update(`viaplay-cdn:${exp}:${host.toLowerCase()}`)
.digest("base64url");
}
function safeEq(a: string, b: string): boolean {
const left = Buffer.from(a);
const right = Buffer.from(b);
if (left.length !== right.length) return false;
return timingSafeEqual(left, right);
}
/** Speel-URL voor apps: MPD + segmenten via onze API (Chrome/122), niet via de app-UA. */
export function wrapViaplayCdnUrl(opts: {
publicBase: string;
secret: string;
originUrl: string;
}): string {
const raw = String(opts.originUrl || "").trim();
if (!raw || /\/api\/v1\/client\/viaplay\/cdn\//i.test(raw)) return raw;
if (!isAllowedViaplayOrigin(raw)) return raw;
const origin = new URL(raw);
const exp = Math.floor(Date.now() / 1000) + TTL_SEC;
const sig = sign(opts.secret, exp, origin.hostname);
const base = opts.publicBase.replace(/\/+$/, "");
const rest = `${origin.hostname}${origin.pathname}${origin.search}`;
return `${base}/api/v1/client/viaplay/cdn/s/${sig}/${exp}/${rest}`;
}
function originFromSplat(
rest: string,
query: Record<string, unknown> | undefined
): string {
let origin = `https://${rest.replace(/^\/+/, "")}`;
const params = new URLSearchParams();
for (const [key, value] of Object.entries(query || {})) {
if (value == null) continue;
if (Array.isArray(value)) {
for (const item of value) params.append(key, String(item));
} else {
params.set(key, String(value));
}
}
const q = params.toString();
if (q) origin += (origin.includes("?") ? "&" : "?") + q;
return origin;
}
function proxyPrefix(publicBase: string, sig: string, exp: string): string {
return `${publicBase.replace(/\/+$/, "")}/api/v1/client/viaplay/cdn/s/${sig}/${exp}`;
}
function toProxyUrl(originUrl: string, prefix: string): string {
const u = new URL(originUrl);
return `${prefix}/${u.hostname}${u.pathname}${u.search}`;
}
function rewriteMpdThroughProxy(xml: string, prefix: string, originMpd: string): string {
let out = xml;
out = out.replace(/https:\/\/[^\s"'<>]+/gi, (url) => {
if (!isAllowedViaplayOrigin(url)) return url;
return toProxyUrl(url, prefix);
});
out = out.replace(/<BaseURL>([\s\S]*?)<\/BaseURL>/gi, (full, inner: string) => {
const raw = String(inner || "").trim();
if (!raw || raw.includes("/api/v1/client/viaplay/cdn/")) return full;
try {
const abs = new URL(raw, originMpd).toString();
if (isAllowedViaplayOrigin(abs)) {
return `<BaseURL>${toProxyUrl(abs, prefix)}</BaseURL>`;
}
} catch {
/* fallback hieronder */
}
const dir = originMpd.replace(/\/[^/]*$/, "/");
return `<BaseURL>${toProxyUrl(dir, prefix)}</BaseURL>`;
});
if (!/<BaseURL/i.test(out)) {
const dir = originMpd.replace(/\/[^/]*$/, "/");
out = out.replace(
/<MPD\b[^>]*>/i,
(open) => `${open}\n <BaseURL>${toProxyUrl(dir, prefix)}</BaseURL>`
);
}
return out;
}
export function registerViaplayCdnProxy(app: FastifyInstance, config: Config): void {
app.get(
"/api/v1/client/viaplay/cdn/s/:sig/:exp/*",
async (request: FastifyRequest, reply: FastifyReply) => {
const params = request.params as { sig: string; exp: string; "*": string };
const rest = String(params["*"] || "").trim();
const exp = Number(params.exp);
if (!rest || !Number.isFinite(exp) || exp < Date.now() / 1000 - 30) {
return reply.status(410).send("Viaplay-proxy verlopen");
}
const host = rest.split("/")[0]?.toLowerCase() || "";
const expected = sign(config.SESSION_SECRET, exp, host);
if (!safeEq(expected, params.sig)) {
return reply.status(403).send("Viaplay-proxy ongeldig");
}
const origin = originFromSplat(rest, request.query as Record<string, unknown>);
if (!isAllowedViaplayOrigin(origin)) {
return reply.status(400).send("Viaplay-proxy host geweigerd");
}
const range = request.headers.range;
const headers: Record<string, string> = {
Accept: "*/*",
"Accept-Language": "nl,en;q=0.9",
"User-Agent": VIAPLAY_USER_AGENT,
Referer: "https://viaplay.com/",
Origin: "https://viaplay.com",
};
if (typeof range === "string" && range) headers.Range = range;
const upstream = await fetch(origin, {
headers,
redirect: "follow",
signal: AbortSignal.timeout(30_000),
});
const ct = upstream.headers.get("content-type") || "";
const urlLooksMpd = /\.mpd$/i.test(new URL(origin).pathname);
const maybeMpd = urlLooksMpd || /xml|mpd|dash\+xml/i.test(ct);
if (!maybeMpd) {
reply.status(upstream.status);
if (ct) reply.header("content-type", ct);
const cr = upstream.headers.get("content-range");
if (cr) reply.header("content-range", cr);
const ar = upstream.headers.get("accept-ranges");
if (ar) reply.header("accept-ranges", ar);
reply.header("cache-control", "no-store");
if (!upstream.body) return reply.send();
return reply.send(Readable.fromWeb(upstream.body as never));
}
const buf = Buffer.from(await upstream.arrayBuffer());
const head = buf.subarray(0, Math.min(buf.length, 400)).toString("utf8");
const isMpd = /<MPD[\s>]/i.test(head);
reply.status(upstream.status);
if (isMpd && upstream.ok) {
const xml = rewriteMpdThroughProxy(
buf.toString("utf8"),
proxyPrefix(config.PUBLIC_URL, params.sig, params.exp),
origin
);
reply.header("content-type", "application/dash+xml; charset=utf-8");
reply.header("cache-control", "no-store");
return reply.send(xml);
}
if (ct) reply.header("content-type", ct);
reply.header("cache-control", "no-store");
return reply.send(buf);
}
);
}

View file

@ -1386,6 +1386,7 @@ export async function fetchMpdXml(
Accept: "*/*", Accept: "*/*",
"User-Agent": VIAPLAY_USER_AGENT, "User-Agent": VIAPLAY_USER_AGENT,
Referer: "https://viaplay.com/", Referer: "https://viaplay.com/",
Origin: "https://viaplay.com",
}, },
signal: AbortSignal.timeout(timeoutMs), signal: AbortSignal.timeout(timeoutMs),
}); });

View file

@ -10,6 +10,7 @@ import type { SubtitleSource } from "../opensubtitles/client";
import { GooglePlayAccessService } from "../google-play/service"; import { GooglePlayAccessService } from "../google-play/service";
import { subscribeViewersChanged } from "./admin-events"; import { subscribeViewersChanged } from "./admin-events";
import { registerAndroidTvClientUpdateRoutes } from "../install/routes"; import { registerAndroidTvClientUpdateRoutes } from "../install/routes";
import { registerViaplayCdnProxy, wrapViaplayCdnUrl } from "../viaplay/cdn-proxy";
export function registerViewerRoutes( export function registerViewerRoutes(
app: FastifyInstance, app: FastifyInstance,
@ -20,6 +21,7 @@ export function registerViewerRoutes(
const viewers = new ViewerService(config, playback, downloads); const viewers = new ViewerService(config, playback, downloads);
const profiles = new ViewerProfileService(); const profiles = new ViewerProfileService();
const googlePlay = new GooglePlayAccessService(config); const googlePlay = new GooglePlayAccessService(config);
registerViaplayCdnProxy(app, config);
registerAndroidTvClientUpdateRoutes(app, (authorization) => registerAndroidTvClientUpdateRoutes(app, (authorization) =>
viewers.authFromBearer(authorization) viewers.authFromBearer(authorization)
@ -1090,7 +1092,11 @@ export function registerViewerRoutes(
eventId: `viaplay-content:${guid}`, eventId: `viaplay-content:${guid}`,
name: displayTitle, name: displayTitle,
logoUrl: null, logoUrl: null,
streamUrl: played.streamUrl, streamUrl: wrapViaplayCdnUrl({
publicBase: config.PUBLIC_URL,
secret: config.SESSION_SECRET,
originUrl: played.streamUrl,
}),
format: played.format, format: played.format,
fallbackStreamUrl: null, fallbackStreamUrl: null,
fallbackFormat: null, fallbackFormat: null,

View file

@ -5,6 +5,7 @@ import { prisma } from "../database/client";
import { enrichZiggoGoArt, invalidateZiggoEpgArtCache } from "./ziggo-epg-art"; import { enrichZiggoGoArt, invalidateZiggoEpgArtCache } from "./ziggo-epg-art";
import { enrichEspnWatchArt, invalidateEspnWatchArtCache } from "./espn-watch-art"; import { enrichEspnWatchArt, invalidateEspnWatchArtCache } from "./espn-watch-art";
import { applyCategoryFallbacks } from "./event-fallbacks"; import { applyCategoryFallbacks } from "./event-fallbacks";
import { wrapViaplayCdnUrl } from "../viaplay/cdn-proxy";
const CACHE_TTL_MS = 60_000; const CACHE_TTL_MS = 60_000;
const FETCH_TIMEOUT_MS = 20_000; const FETCH_TIMEOUT_MS = 20_000;
@ -1130,7 +1131,11 @@ export async function getScheduleEventPlay(
eventId: pub.id, eventId: pub.id,
name: pub.name, name: pub.name,
logoUrl: pub.logoUrl ?? pub.imagePortrait, logoUrl: pub.logoUrl ?? pub.imagePortrait,
streamUrl: played.streamUrl, streamUrl: wrapViaplayCdnUrl({
publicBase: config.PUBLIC_URL,
secret: config.SESSION_SECRET,
originUrl: played.streamUrl,
}),
format: played.format, format: played.format,
fallbackStreamUrl: null, fallbackStreamUrl: null,
fallbackFormat: null, fallbackFormat: null,