Serve live Viaplay from DRM vpxtra MPD, never play-live TVE.
This commit is contained in:
parent
a01bb70a0a
commit
721a2d1f2a
1 changed files with 167 additions and 4 deletions
|
|
@ -125,13 +125,13 @@ async function storedDrmPlay(
|
|||
const rows = await prisma.drmKey.findMany({
|
||||
where: { mediaGuid, source: "viaplay" },
|
||||
orderBy: { lastSeenAt: "desc" },
|
||||
take: 8,
|
||||
take: 24,
|
||||
});
|
||||
if (!rows.length) {
|
||||
const alt = await prisma.drmKey.findMany({
|
||||
where: { mediaGuid },
|
||||
orderBy: { lastSeenAt: "desc" },
|
||||
take: 8,
|
||||
take: 24,
|
||||
});
|
||||
rows.push(...alt);
|
||||
}
|
||||
|
|
@ -228,11 +228,15 @@ function isReusableLiveMpd(url: string | null | undefined): url is string {
|
|||
return !!u && isLiveDashMpd(u) && !isDontGoPlaceholder(u);
|
||||
}
|
||||
|
||||
/** TVE/KAP-zender is catch-up; echte live zit op vpxtra (cdn.viaplay.tv/vp/wv/…). */
|
||||
/** TVE is de play-live-placeholder (Don't go). Echte live zit op vpxtra. */
|
||||
function isTveSportMpd(url: string | null | undefined): boolean {
|
||||
return !!url && /\/tve\/|tve_vsport/i.test(url);
|
||||
}
|
||||
|
||||
function isXtraMpd(url: string | null | undefined): boolean {
|
||||
return !!url && /\/vp\/wv\/vpxtra|vpxtra\d+|vp-xtra/i.test(url);
|
||||
}
|
||||
|
||||
function preferLiveChannelUrl(urls: Array<string | null | undefined>): string | null {
|
||||
const cleaned = [
|
||||
...new Set(
|
||||
|
|
@ -298,6 +302,123 @@ async function probeChannelMpd(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Live: zelfde bron als de losse DRM-speler (vpxtra + keys), niet play-live/TVE.
|
||||
*/
|
||||
async function storedXtraChannelPlay(
|
||||
sessionSecret: string,
|
||||
mediaGuid: string,
|
||||
eventTitle?: string | null,
|
||||
extraMpdUrls?: Array<string | null | undefined>
|
||||
): Promise<StoredDrmPlay> {
|
||||
const { decryptSecret } = await import("../security/crypto");
|
||||
const drmRows = await prisma.drmKey.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ mediaGuid },
|
||||
{ mpdUrl: { contains: "vpxtra", mode: "insensitive" } },
|
||||
{ mpdUrl: { contains: "vp-xtra", mode: "insensitive" } },
|
||||
{ mpdUrl: { contains: "/vp/wv/", mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
orderBy: { lastSeenAt: "desc" },
|
||||
take: 80,
|
||||
});
|
||||
const channels = await prisma.liveChannel.findMany({
|
||||
where: {
|
||||
enabled: true,
|
||||
OR: [
|
||||
{ mpdUrl: { contains: "vpxtra", mode: "insensitive" } },
|
||||
{ streamUrlCache: { contains: "vpxtra", mode: "insensitive" } },
|
||||
],
|
||||
},
|
||||
take: 40,
|
||||
});
|
||||
|
||||
type Group = {
|
||||
keys: Array<{ kid: string; key: string }>;
|
||||
score: number;
|
||||
lastSeen: number;
|
||||
};
|
||||
const groups = new Map<string, Group>();
|
||||
const guidKeys: Array<{ kid: string; key: string }> = [];
|
||||
const seenGuid = new Set<string>();
|
||||
const xtraUrls: string[] = [];
|
||||
const titleHint = (eventTitle || "").trim().toLowerCase().slice(0, 18);
|
||||
|
||||
const addXtra = (
|
||||
urlRaw: string | null | undefined,
|
||||
kid: string,
|
||||
key: string,
|
||||
score: number,
|
||||
lastSeen: number
|
||||
) => {
|
||||
const url = urlRaw ? stripKapWindow(urlRaw.trim()) : "";
|
||||
if (!isXtraMpd(url) || !isReusableLiveMpd(url)) return;
|
||||
xtraUrls.push(url);
|
||||
const g = groups.get(url) || { keys: [], score: 0, lastSeen: 0 };
|
||||
if (!g.keys.some((k) => k.kid === kid)) g.keys.push({ kid, key });
|
||||
g.score += score;
|
||||
g.lastSeen = Math.max(g.lastSeen, lastSeen);
|
||||
groups.set(url, g);
|
||||
};
|
||||
|
||||
for (const row of drmRows) {
|
||||
let key: string;
|
||||
try {
|
||||
key = decryptSecret(row.keyEnc, sessionSecret);
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
if (row.mediaGuid === mediaGuid && !seenGuid.has(row.kid)) {
|
||||
seenGuid.add(row.kid);
|
||||
guidKeys.push({ kid: row.kid, key });
|
||||
}
|
||||
let score = 1;
|
||||
if (row.mediaGuid === mediaGuid) score += 1000;
|
||||
const name = (row.eventName || "").toLowerCase();
|
||||
if (titleHint && name.includes(titleHint)) score += 200;
|
||||
addXtra(row.mpdUrl, row.kid, key, score, row.lastSeenAt.getTime());
|
||||
}
|
||||
|
||||
for (const ch of channels) {
|
||||
const url = ch.streamUrlCache || ch.mpdUrl;
|
||||
if (!ch.kidEnc || !ch.keyEnc) {
|
||||
const dummy = preferLiveChannelUrl([url]);
|
||||
if (dummy) xtraUrls.push(dummy);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const kid = decryptSecret(ch.kidEnc, sessionSecret)
|
||||
.replace(/-/g, "")
|
||||
.toLowerCase();
|
||||
const key = decryptSecret(ch.keyEnc, sessionSecret)
|
||||
.replace(/-/g, "")
|
||||
.toLowerCase();
|
||||
if (!/^[0-9a-f]{32}$/.test(kid) || !/^[0-9a-f]{32}$/.test(key)) continue;
|
||||
addXtra(url, kid, key, 10, 0);
|
||||
} catch {
|
||||
const dummy = preferLiveChannelUrl([url]);
|
||||
if (dummy) xtraUrls.push(dummy);
|
||||
}
|
||||
}
|
||||
|
||||
const ranked = [...groups.entries()].sort(
|
||||
(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 };
|
||||
}
|
||||
const salvage = preferLiveChannelUrl([
|
||||
...xtraUrls,
|
||||
...(extraMpdUrls || []),
|
||||
]);
|
||||
if (guidKeys.length && salvage && isXtraMpd(salvage)) {
|
||||
return { keys: guidKeys, mpdUrl: salvage };
|
||||
}
|
||||
return { keys: [], mpdUrl: null };
|
||||
}
|
||||
|
||||
async function dropEventMpdCache(mediaGuid: string, reason: string) {
|
||||
await patchViaplayEventCache(mediaGuid, {
|
||||
mpdUrl: null,
|
||||
|
|
@ -332,6 +453,43 @@ export async function ensureViaplayEventReady(
|
|||
);
|
||||
}
|
||||
|
||||
// Live: DRM-tabel / live-lijst (vpxtra), nooit play-live TVE (Don't go).
|
||||
if (!ended) {
|
||||
const xtra = await storedXtraChannelPlay(
|
||||
sessionSecret,
|
||||
mediaGuid,
|
||||
event.title,
|
||||
settings.config.events.map((e) => e.mpdUrl)
|
||||
);
|
||||
if (xtra.keys.length && xtra.mpdUrl) {
|
||||
await patchViaplayEventCache(mediaGuid, {
|
||||
mpdUrl: xtra.mpdUrl,
|
||||
mpdCachedAt: new Date().toISOString(),
|
||||
hasKeys: true,
|
||||
lastPlayError: null,
|
||||
lastPlayAt: new Date().toISOString(),
|
||||
});
|
||||
recordLivePlay({
|
||||
guid: mediaGuid,
|
||||
ok: true,
|
||||
title: event.title,
|
||||
mediaPath: mediaPathOf(xtra.mpdUrl),
|
||||
hasLicense: true,
|
||||
fromCache: true,
|
||||
cookies: emptyCookies(),
|
||||
error: null,
|
||||
});
|
||||
return {
|
||||
streamUrl: xtra.mpdUrl,
|
||||
format: "dash",
|
||||
keys: xtra.keys,
|
||||
fromCache: true,
|
||||
mediaGuid,
|
||||
live: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
|
@ -561,6 +719,11 @@ export async function ensureViaplayEventReady(
|
|||
if (isDontGoPlaceholder(play.mpdUrl) || isDontGoPlaceholder(kap.mpdUrl)) {
|
||||
throw new Error("Viaplay gaf Don't go-placeholder i.p.v. de wedstrijdstream");
|
||||
}
|
||||
if (!ended && (isTveSportMpd(play.mpdUrl) || isTveSportMpd(kap.mpdUrl))) {
|
||||
throw new Error(
|
||||
"Viaplay play-live gaf TVE-placeholder (tve_vsport) i.p.v. de Xtra-livestream"
|
||||
);
|
||||
}
|
||||
if (!ended && !isLiveDashMpd(kap.mpdUrl)) {
|
||||
throw new Error("Viaplay gaf geen live-dash stream (Don't go/VOD-fallback)");
|
||||
}
|
||||
|
|
@ -596,7 +759,7 @@ export async function ensureViaplayEventReady(
|
|||
await upsertDrmKey(prisma, sessionSecret, {
|
||||
kid: k.kid,
|
||||
key: k.key,
|
||||
mpdUrl: kap.mpdUrl,
|
||||
mpdUrl: isTveSportMpd(kap.mpdUrl) ? null : kap.mpdUrl,
|
||||
eventName: event.title,
|
||||
channelName: play.channelName,
|
||||
source: "viaplay",
|
||||
|
|
|
|||
Loading…
Reference in a new issue