feat(review): High quality playback switch and per-version ProRes download
Deploy / deploy (push) Successful in 3m23s

Worker 1.7.0 (preview.highQuality, default on) now, after the AE render:
- encodes a High quality MP4 from the delivery MOV (full-res H.264 High,
  CRF 14 capped at 100 Mbps) -> Version.hqFileUrl
- uploads the MOV itself -> Version.highResKey/Filename/Size
Both best-effort (the preview never fails over them), with a lease
keep-alive during the long encode/uploads; the export timeline records
what was attached.

Player: HQ switch when a High quality source exists; keeps frame, play
state, speed and zoom, remembered per browser, falls back to Standard if
the file won't load. ProRes itself stays download-only (browser support).

Downloads: the client portal's High Res button serves the version's
ProRes (client-code filename), falling back to the shot's manual file;
staff get a ProRes button on the review page.

Housekeeping (lib/render-pipeline/high-res.ts): HQ MP4 + ProRes are kept
while a version is latest, client-shared, approved or younger than
preview.highResRetentionDays (14); otherwise deleted from storage. Runs
per task after finalize and hourly from the reaper; deleting a version or
a reslate removes its files. The Standard MP4 is never touched.

Also: ProRes/Handles top-bar buttons go icon-only below lg so they don't
overlap the version selector.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
twotalesanimation
2026-09-22 16:32:18 +02:00
parent bae46c2968
commit 9660cdc21c
27 changed files with 793 additions and 18 deletions
+19 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from "vitest";
import { clientShotCode, clientTaskTitle } from "./shot-code";
import { clientHighResFilename, clientShotCode, clientTaskTitle } from "./shot-code";
describe("clientShotCode", () => {
it("prefers the client's alias", () => {
@@ -43,3 +43,21 @@ describe("clientTaskTitle", () => {
expect(clientTaskTitle("UNG_106_010_020_cmp", same)).toBe("UNG_106_010_020_cmp");
});
});
describe("clientHighResFilename", () => {
const shot = { shotCode: "UNG_104_051_040", clientCode: "EP4_0510" };
it("uses the slate's client naming when the shot has an alias", () => {
expect(clientHighResFilename("UNG_104_051_040_cmp_TT_v006.mov", shot)).toBe("EP4_0510_v006.mov");
});
it("keeps our own name when there is no alias", () => {
expect(clientHighResFilename("UNG_104_051_040_cmp_TT_v006.mov", { shotCode: "UNG_104_051_040" })).toBe(
"UNG_104_051_040_cmp_TT_v006.mov"
);
});
it("swaps the code in a name that doesn't follow the convention", () => {
expect(clientHighResFilename("final_UNG_104_051_040.mov", shot)).toBe("final_EP4_0510.mov");
});
});