Serve stored Viaplay DRM MPD+keys to clients before play-live, matching the admin key table.

This commit is contained in:
Jos Vooges | STH 2026-09-28 20:27:08 +02:00
parent 133f51284d
commit 75969d87ec

View file

@ -108,10 +108,19 @@ function parseKey(value: string): { kid: string; key: string } | null {
return { kid: kid!, key: key! }; return { kid: kid!, key: key! };
} }
async function keysForMediaGuid( function hasKapWindow(url: string | null | undefined): boolean {
return !!url && /\/start\/\d+\/stop\/\d+/.test(url);
}
type StoredDrmPlay = {
keys: Array<{ kid: string; key: string }>;
mpdUrl: string | null;
};
async function storedDrmPlay(
sessionSecret: string, sessionSecret: string,
mediaGuid: string mediaGuid: string
): Promise<Array<{ kid: string; key: string }>> { ): Promise<StoredDrmPlay> {
const rows = await prisma.drmKey.findMany({ const rows = await prisma.drmKey.findMany({
where: { mediaGuid, source: "viaplay" }, where: { mediaGuid, source: "viaplay" },
orderBy: { lastSeenAt: "desc" }, orderBy: { lastSeenAt: "desc" },
@ -126,18 +135,30 @@ async function keysForMediaGuid(
rows.push(...alt); rows.push(...alt);
} }
const { decryptSecret } = await import("../security/crypto"); const { decryptSecret } = await import("../security/crypto");
const out: Array<{ kid: string; key: string }> = []; const keys: Array<{ kid: string; key: string }> = [];
const seen = new Set<string>(); const seen = new Set<string>();
let mpdUrl: string | null = null;
for (const row of rows) { for (const row of rows) {
if (seen.has(row.kid)) continue; if (seen.has(row.kid)) continue;
seen.add(row.kid); seen.add(row.kid);
try { try {
out.push({ kid: row.kid, key: decryptSecret(row.keyEnc, sessionSecret) }); keys.push({ kid: row.kid, key: decryptSecret(row.keyEnc, sessionSecret) });
} catch { } catch {
/* skip */ continue;
}
const url = row.mpdUrl?.trim() || null;
if (!mpdUrl && url && /^https?:\/\//i.test(url) && !isDontGoPlaceholder(url)) {
mpdUrl = url;
} }
} }
return out; return { keys, mpdUrl };
}
async function keysForMediaGuid(
sessionSecret: string,
mediaGuid: string
): Promise<Array<{ kid: string; key: string }>> {
return (await storedDrmPlay(sessionSecret, mediaGuid)).keys;
} }
async function keysForChannelPath( async function keysForChannelPath(
@ -262,9 +283,57 @@ export async function ensureViaplayEventReady(
const ended = Date.now() > event.endMs; const ended = Date.now() > event.endMs;
const cachedMpd = channelMpdFromEvents(settings.config.events, event); const cachedMpd = channelMpdFromEvents(settings.config.events, event);
let cachedKeys = await keysForMediaGuid(sessionSecret, mediaGuid); const stored = await storedDrmPlay(sessionSecret, mediaGuid);
let cachedKeys = stored.keys;
if (!cachedKeys.length) { if (!cachedKeys.length) {
cachedKeys = await keysForChannelPath(sessionSecret, event.mpdUrl || cachedMpd); cachedKeys = await keysForChannelPath(
sessionSecret,
stored.mpdUrl || event.mpdUrl || cachedMpd
);
}
// 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 = stored.mpdUrl || event.mpdUrl || cachedMpd;
if (url && isReusableLiveMpd(url)) {
if (ended && !hasKapWindow(url)) {
try {
const kap = await resolveKapMpd(
url,
event.startMs,
event.endMs,
Math.min(tweaks.httpTimeoutMs, 12_000)
);
if (isReusableLiveMpd(kap.mpdUrl)) url = kap.mpdUrl;
} catch {
/* gebruik de drm-MPD zoals de losse speler */
}
}
const trustStoredKap = ended && hasKapWindow(url);
const probed =
trustStoredKap || (await liveCachedMpdPlayable(url, tweaks.httpTimeoutMs));
if (probed && url) {
recordLivePlay({
guid: mediaGuid,
ok: true,
title: event.title,
mediaPath: mediaPathOf(url),
hasLicense: true,
fromCache: true,
cookies: emptyCookies(),
error: null,
});
return {
streamUrl: url,
format: "dash",
keys: cachedKeys,
fromCache: true,
mediaGuid,
live: !ended && !hasKapWindow(url),
};
}
}
} }
const cacheFresh = const cacheFresh =
@ -291,7 +360,7 @@ export async function ensureViaplayEventReady(
keys: cachedKeys, keys: cachedKeys,
fromCache: true, fromCache: true,
mediaGuid, mediaGuid,
live: !ended, live: !ended && !hasKapWindow(event.mpdUrl),
}; };
} }
await dropEventMpdCache( await dropEventMpdCache(
@ -382,7 +451,7 @@ export async function ensureViaplayEventReady(
keys: cachedKeys, keys: cachedKeys,
fromCache: true, fromCache: true,
mediaGuid, mediaGuid,
live: !ended, live: !ended && !hasKapWindow(event.mpdUrl),
}; };
} }
@ -522,7 +591,7 @@ export async function ensureViaplayEventReady(
fromCache: false, fromCache: false,
mediaGuid, mediaGuid,
accountId: account.id, accountId: account.id,
live: !ended, live: !ended && !hasKapWindow(kap.mpdUrl),
}; };
} catch (err) { } catch (err) {
lastError = err instanceof Error ? err : new Error(String(err)); lastError = err instanceof Error ? err : new Error(String(err));