From f1576457c366197094478fe7557237c5067c3445 Mon Sep 17 00:00:00 2001 From: Flegma Date: Sat, 20 Jun 2026 21:46:47 +0200 Subject: [PATCH 1/7] feature: pure outro-branding helpers (version, cache key, env) + tests --- .../game-streamer/outro-branding.spec.ts | 48 +++++++++++++++++++ src/matches/game-streamer/outro-branding.ts | 45 +++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 src/matches/game-streamer/outro-branding.spec.ts create mode 100644 src/matches/game-streamer/outro-branding.ts diff --git a/src/matches/game-streamer/outro-branding.spec.ts b/src/matches/game-streamer/outro-branding.spec.ts new file mode 100644 index 00000000..16fadd5a --- /dev/null +++ b/src/matches/game-streamer/outro-branding.spec.ts @@ -0,0 +1,48 @@ +import { + DEFAULT_OUTRO_ACCENT, + computeOutroVersion, + outroCacheKey, + buildOutroEnv, +} from "./outro-branding"; + +describe("outro-branding", () => { + it("default accent is the stock amber triple", () => { + expect(DEFAULT_OUTRO_ACCENT).toBe("33 94% 58%"); + }); + + it("version is deterministic and 12 chars", () => { + const a = computeOutroVersion({ brandName: "ACME", accent: "1 2% 3%", etag: "e1" }); + const b = computeOutroVersion({ brandName: "ACME", accent: "1 2% 3%", etag: "e1" }); + expect(a).toBe(b); + expect(a).toHaveLength(12); + }); + + it("version changes when the logo etag changes", () => { + const a = computeOutroVersion({ brandName: "ACME", accent: "1 2% 3%", etag: "e1" }); + const b = computeOutroVersion({ brandName: "ACME", accent: "1 2% 3%", etag: "e2" }); + expect(a).not.toBe(b); + }); + + it("cache key embeds version + dims + fps", () => { + expect(outroCacheKey({ version: "abc123abc123", dims: "1920x1080", fps: 60 })) + .toBe("branding/outro_abc123abc123_1920x1080_60.mp4"); + }); + + it("hit env is just the cache URL", () => { + expect(buildOutroEnv({ hit: true, cacheUrl: "http://s3/x.mp4" })) + .toEqual({ CLIP_OUTRO_URL: "http://s3/x.mp4" }); + }); + + it("miss env carries render instruction + branding props", () => { + expect(buildOutroEnv({ + hit: false, putUrl: "http://put", logoUrl: "http://logo", + brandName: "ACME", accent: "33 94% 58%", + })).toEqual({ + CLIP_OUTRO_RENDER: "1", + CLIP_OUTRO_PUT_URL: "http://put", + CLIP_BRAND_LOGO_URL: "http://logo", + CLIP_BRAND_NAME: "ACME", + CLIP_BRAND_ACCENT: "33 94% 58%", + }); + }); +}); diff --git a/src/matches/game-streamer/outro-branding.ts b/src/matches/game-streamer/outro-branding.ts new file mode 100644 index 00000000..abc2d4dd --- /dev/null +++ b/src/matches/game-streamer/outro-branding.ts @@ -0,0 +1,45 @@ +import { createHash } from "node:crypto"; + +export const DEFAULT_OUTRO_ACCENT = "33 94% 58%"; + +export function computeOutroVersion(parts: { + brandName: string; + accent: string; + etag: string; +}): string { + return createHash("sha1") + .update(`${parts.brandName}|${parts.accent}|${parts.etag}`) + .digest("hex") + .slice(0, 12); +} + +export function outroCacheKey(args: { + version: string; + dims: string; + fps: number; +}): string { + return `branding/outro_${args.version}_${args.dims}_${args.fps}.mp4`; +} + +export type OutroEnvState = + | { hit: true; cacheUrl: string } + | { + hit: false; + putUrl: string; + logoUrl: string; + brandName: string; + accent: string; + }; + +export function buildOutroEnv(state: OutroEnvState): Record { + if (state.hit) { + return { CLIP_OUTRO_URL: state.cacheUrl }; + } + return { + CLIP_OUTRO_RENDER: "1", + CLIP_OUTRO_PUT_URL: state.putUrl, + CLIP_BRAND_LOGO_URL: state.logoUrl, + CLIP_BRAND_NAME: state.brandName, + CLIP_BRAND_ACCENT: state.accent, + }; +} From 692328617d3158dedf3374dff403a446a16e23b3 Mon Sep 17 00:00:00 2001 From: Flegma Date: Sat, 20 Jun 2026 21:51:45 +0200 Subject: [PATCH 2/7] fix: cast outro env state variant (api tsconfig strict off doesn't narrow the union) --- src/matches/game-streamer/outro-branding.ts | 34 ++++++++++++--------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/matches/game-streamer/outro-branding.ts b/src/matches/game-streamer/outro-branding.ts index abc2d4dd..9919fae5 100644 --- a/src/matches/game-streamer/outro-branding.ts +++ b/src/matches/game-streamer/outro-branding.ts @@ -21,25 +21,31 @@ export function outroCacheKey(args: { return `branding/outro_${args.version}_${args.dims}_${args.fps}.mp4`; } -export type OutroEnvState = - | { hit: true; cacheUrl: string } - | { - hit: false; - putUrl: string; - logoUrl: string; - brandName: string; - accent: string; - }; +export interface OutroEnvHit { + hit: true; + cacheUrl: string; +} +export interface OutroEnvMiss { + hit: false; + putUrl: string; + logoUrl: string; + brandName: string; + accent: string; +} +export type OutroEnvState = OutroEnvHit | OutroEnvMiss; +// The api's tsconfig has strict mode off, so a boolean discriminant does not +// narrow the union in the else branch — cast to the concrete variant instead. export function buildOutroEnv(state: OutroEnvState): Record { if (state.hit) { - return { CLIP_OUTRO_URL: state.cacheUrl }; + return { CLIP_OUTRO_URL: (state as OutroEnvHit).cacheUrl }; } + const miss = state as OutroEnvMiss; return { CLIP_OUTRO_RENDER: "1", - CLIP_OUTRO_PUT_URL: state.putUrl, - CLIP_BRAND_LOGO_URL: state.logoUrl, - CLIP_BRAND_NAME: state.brandName, - CLIP_BRAND_ACCENT: state.accent, + CLIP_OUTRO_PUT_URL: miss.putUrl, + CLIP_BRAND_LOGO_URL: miss.logoUrl, + CLIP_BRAND_NAME: miss.brandName, + CLIP_BRAND_ACCENT: miss.accent, }; } From 5fec05ad6ac9feb9f19ecbae4d31a543d48b2575 Mon Sep 17 00:00:00 2001 From: Flegma Date: Sat, 20 Jun 2026 21:51:46 +0200 Subject: [PATCH 3/7] =?UTF-8?q?feature:=20resolveOutroBranding=20=E2=80=94?= =?UTF-8?q?=20presigned=20cache/render=20env=20from=20panel=20branding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../game-streamer/game-streamer.service.ts | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/src/matches/game-streamer/game-streamer.service.ts b/src/matches/game-streamer/game-streamer.service.ts index 9fd634a1..4bfa96cd 100644 --- a/src/matches/game-streamer/game-streamer.service.ts +++ b/src/matches/game-streamer/game-streamer.service.ts @@ -19,6 +19,13 @@ import { GameStreamerStatusDto } from "./types/GameStreamerStatusDto"; import { AppConfig } from "../../configs/types/AppConfig"; import { SteamConfig } from "../../configs/types/SteamConfig"; import { resolveInClusterApiBase } from "../clips/clips.constants"; +import { S3Service } from "../../s3/s3.service"; +import { + DEFAULT_OUTRO_ACCENT, + computeOutroVersion, + outroCacheKey, + buildOutroEnv, +} from "./outro-branding"; import { LoggingService } from "../../k8s/logging/logging.service"; import { SteamAccountService, @@ -140,6 +147,7 @@ export class GameStreamerService { private readonly demoMetadata: DemoMetadataService, private readonly loggingService: LoggingService, private readonly steamAccounts: SteamAccountService, + private readonly s3: S3Service, ) { this.gameServerConfig = this.config.get("gameServers"); this.appConfig = this.config.get("app"); @@ -323,6 +331,61 @@ export class GameStreamerService { return value === "false" || value === "0" ? "0" : "1"; } + // Branded outro env for the render pod. Active only when a custom logo is + // set. Returns {} (stock outro), a presigned cache URL (hit), or a render + // instruction + presigned PUT + branding props (miss). Best-effort: any + // failure returns {} so the pod falls back to the baked stock outro. + public async resolveOutroBranding( + dims: string, + fps: number, + ): Promise> { + try { + const logoPath = await this.readSetting("public.logo_url"); + if (!logoPath) { + return {}; + } + const brandName = (await this.readSetting("public.brand_name")) ?? ""; + const accent = + (await this.readSetting("public.color_dark_tactical_amber")) ?? + (await this.readSetting("public.color_tactical_amber")) ?? + DEFAULT_OUTRO_ACCENT; + + let etag = logoPath; + try { + etag = (await this.s3.stat(logoPath))?.etag ?? logoPath; + } catch { + /* keep logoPath as the version seed */ + } + + const version = computeOutroVersion({ brandName, accent, etag }); + const key = outroCacheKey({ version, dims, fps }); + + if (await this.s3.has(key)) { + return buildOutroEnv({ + hit: true, + cacheUrl: await this.s3.getPresignedUrl(key, undefined, 3600, "get"), + }); + } + return buildOutroEnv({ + hit: false, + putUrl: await this.s3.getPresignedUrl(key, undefined, 3600, "put"), + logoUrl: await this.s3.getPresignedUrl( + logoPath, + undefined, + 3600, + "get", + ), + brandName, + accent, + }); + } catch (error) { + this.logger.warn( + `resolveOutroBranding failed: ${(error as Error)?.message ?? error}`, + ); + return {}; + } + } + public async resolveClipFps(): Promise<30 | 60> { const value = (await this.readSetting("clip_fps")) ?? process.env.CLIP_FPS ?? "60"; From 5c25d762ce74f116aa2d91c0ab9815d81ce718b8 Mon Sep 17 00:00:00 2001 From: Flegma Date: Sat, 20 Jun 2026 21:52:52 +0200 Subject: [PATCH 4/7] feature: pass branded outro env into batch + on-demand clip dispatch --- src/matches/clips/clips.service.ts | 6 ++++++ .../game-streamer/game-streamer.service.ts | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/matches/clips/clips.service.ts b/src/matches/clips/clips.service.ts index ecfda47e..1a4dc09c 100644 --- a/src/matches/clips/clips.service.ts +++ b/src/matches/clips/clips.service.ts @@ -188,6 +188,11 @@ export class ClipsService { `dest=${spec.destination}`, ); + const outroEnv = await this.gameStreamer.resolveOutroBranding( + dims, + spec.output.fps, + ); + try { await this.gameStreamer.dispatchClipRenderToPod(session.id, { job_id: jobId, @@ -201,6 +206,7 @@ export class ClipsService { })), output_dims: dims, output_fps: spec.output.fps, + outro_env: outroEnv, }); } catch (error) { this.logger.error( diff --git a/src/matches/game-streamer/game-streamer.service.ts b/src/matches/game-streamer/game-streamer.service.ts index 4bfa96cd..afcc9c79 100644 --- a/src/matches/game-streamer/game-streamer.service.ts +++ b/src/matches/game-streamer/game-streamer.service.ts @@ -1091,6 +1091,7 @@ export class GameStreamerService { }>; output_dims: string; output_fps: number; + outro_env?: Record; }, ) { const url = this.getDemoSpecUrl(sessionId, "render-clip", "demo"); @@ -2202,6 +2203,20 @@ export class GameStreamerService { name: "CLIP_BAKE_BRANDING", value: await this.resolveClipBakeBranding(), }); + { + // Batch jobs share the global clip resolution/fps, so the outro env is + // computed once at pod level (resolve_outro_file re-validates dims and + // falls back, so a mismatch is non-fatal). + const dims = + (await this.resolveClipResolution()) === "720p" + ? "1280x720" + : "1920x1080"; + const fps = await this.resolveClipFps(); + const outroEnv = await this.resolveOutroBranding(dims, fps); + for (const [name, value] of Object.entries(outroEnv)) { + env.push({ name, value }); + } + } env.push(...(await this.buildNodeCs2OptionsEnv(nodeId))); this.logger.log( From d84c4ba74bd5f37aeb27335a9387f868ae6019cd Mon Sep 17 00:00:00 2001 From: Flegma Date: Sat, 20 Jun 2026 21:57:01 +0200 Subject: [PATCH 5/7] docs: note empty CLIP_BRAND_NAME is intentional (logo-only outro) --- src/matches/game-streamer/game-streamer.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/matches/game-streamer/game-streamer.service.ts b/src/matches/game-streamer/game-streamer.service.ts index afcc9c79..6c085b7d 100644 --- a/src/matches/game-streamer/game-streamer.service.ts +++ b/src/matches/game-streamer/game-streamer.service.ts @@ -344,6 +344,9 @@ export class GameStreamerService { if (!logoPath) { return {}; } + // Empty brandName (no public.brand_name) is intentional: the pod renders + // a logo-only outro (the Outro composition hides the wordmark). Keep the + // CLIP_BRAND_NAME key with an empty value — do not drop it. const brandName = (await this.readSetting("public.brand_name")) ?? ""; const accent = (await this.readSetting("public.color_dark_tactical_amber")) ?? From ac7e2a0d7af3d9897bcd3a56a0dd9c732eac9912 Mon Sep 17 00:00:00 2001 From: Flegma Date: Sat, 20 Jun 2026 22:19:22 +0200 Subject: [PATCH 6/7] fix: compute resolveOutroBranding inside the dispatch try (code review) --- src/matches/clips/clips.service.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/matches/clips/clips.service.ts b/src/matches/clips/clips.service.ts index 1a4dc09c..bac7b887 100644 --- a/src/matches/clips/clips.service.ts +++ b/src/matches/clips/clips.service.ts @@ -188,12 +188,11 @@ export class ClipsService { `dest=${spec.destination}`, ); - const outroEnv = await this.gameStreamer.resolveOutroBranding( - dims, - spec.output.fps, - ); - try { + const outroEnv = await this.gameStreamer.resolveOutroBranding( + dims, + spec.output.fps, + ); await this.gameStreamer.dispatchClipRenderToPod(session.id, { job_id: jobId, token: sessionToken, From e3c8251ce426460c77c6b73d14c89c443daf072a Mon Sep 17 00:00:00 2001 From: Flegma Date: Sat, 20 Jun 2026 22:30:15 +0200 Subject: [PATCH 7/7] fix: pass S3_PUBLIC_ORIGIN to the demo-session pod for the outro URL allowlist --- src/matches/game-streamer/game-streamer.service.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/matches/game-streamer/game-streamer.service.ts b/src/matches/game-streamer/game-streamer.service.ts index 6c085b7d..67d02fd4 100644 --- a/src/matches/game-streamer/game-streamer.service.ts +++ b/src/matches/game-streamer/game-streamer.service.ts @@ -816,6 +816,9 @@ export class GameStreamerService { name: "CLIP_BAKE_BRANDING", value: await this.resolveClipBakeBranding(), }, + // Trusted S3 presign origin for render-clip.mjs's outro-env URL allowlist + // (independent of the demo source, so faceit/external demos still brand). + { name: "S3_PUBLIC_ORIGIN", value: this.appConfig.demosDomain }, ]; if (options.roundTicks != null) { env.push({