Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions apps/playground/src/m5-worker-proof.ts
Original file line number Diff line number Diff line change
Expand Up @@ -868,10 +868,7 @@ function assertNativeOutput(
);
const color = frame.colorSpace;
requireFixture(
color.fullRange !== true &&
(color.matrix === null || color.matrix === "bt709") &&
(color.primaries === null || color.primaries === "bt709") &&
(color.transfer === null || color.transfer === "bt709"),
matchesDecodedBt709ColorSpace(color),
`decoded color metadata contradicts BT.709 limited range at ordinal ${String(
expected.ordinal
)}`
Expand All @@ -882,6 +879,18 @@ function assertNativeOutput(
);
}

function matchesDecodedBt709ColorSpace(color: VideoColorSpace): boolean {
const limitedBt709 = color.fullRange !== true &&
(color.matrix === null || color.matrix === "bt709") &&
(color.primaries === null || color.primaries === "bt709") &&
(color.transfer === null || color.transfer === "bt709");
const webKitNormalizedBt709 = color.fullRange === true &&
color.matrix === "bt709" &&
color.primaries === "bt709" &&
color.transfer === "iec61966-2-1";
return limitedBt709 || webKitNormalizedBt709;
}

function updateCreditEvidence(
credit: {
maxOutstandingFrames: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# Grass Rabbit Vercel Git Deployment Implementation Plan

> **For agentic workers:** Execute each task in order and verify the Git-trigger and proxy contract before handoff.

**Goal:** Deploy the grass-rabbit demo from Git pushes while rebuilding it only when files under `examples/grass-rabbit` change, and keep it reachable at `pixelpoint.io/aval/`.

**Architecture:** Keep `aval-demo` as its own Vercel project rooted at `examples/grass-rabbit`. Its Vercel build first compiles the local workspace packages needed by the demo, then runs the existing Vite `/aval/` build. A repository-scoped ignored-build command skips commits with no changes inside the demo directory. The PixelPoint website continues to reverse-proxy `/aval` to the demo project's stable production alias.

**Tech Stack:** Vercel Git integration, npm workspaces, Vite, Vercel rewrites

---

### Task 1: Make the demo build reproducible on Vercel

**Files:**
- Modify: `examples/grass-rabbit/package.json`
- Modify: `examples/grass-rabbit/vercel.json`

- [x] Add a Vercel build script that builds required local workspace packages before Vite.
- [x] Declare the build command and output directory in version-controlled Vercel configuration.
- [x] Add an ignored-build command that proceeds only when `examples/grass-rabbit` changed.
- [x] Validate the JSON and run the exact production build locally.

### Task 2: Persist the public `/aval` proxy

**Files:**
- Preserve/verify: `/Users/alex/Projects/pixelpoint-website/vercel.json`
- Preserve/verify: `/Users/alex/Projects/pixelpoint-website/.gitignore`

- [x] Verify `/aval`, `/aval/`, and nested assets proxy to the `aval-demo` production alias.
- [x] Ensure the proxy and `.vercel` ignore rule remain as intentional website-repository changes.

### Task 3: Connect the existing Vercel project to GitHub

**Files:**
- Local-only: `examples/grass-rabbit/.vercel/project.json`

- [x] Link the local demo directory to the existing `pixelpoint/aval-demo` project.
- [x] Connect that Vercel project to `pixel-point/aval` on GitHub.
- [x] Confirm the project root is `examples/grass-rabbit` and the production branch is `main`.

### Task 4: Verify the complete deployment contract

- [x] Confirm the built HTML and assets use the `/aval/` base path.
- [x] Verify the production build includes the current Safari runtime fix.
- [x] Test the built demo in WebKit through the `/aval/` route.
- [x] Confirm unrelated commits are skipped by the ignored-build command and demo-directory commits proceed.
- [x] Report the exact files that need to be committed and pushed in each repository.
18 changes: 18 additions & 0 deletions etc/api/player-web.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,8 @@ export class BrowserFrameBackend implements FrameRendererBackend_2 {
setPresentationGeometry(geometry: Readonly<PresentationGeometry>): boolean;
// (undocumented)
upload(kind: FrameTextureKind, index: number, pixels: Uint8Array): void;
// (undocumented)
uploadFrame(kind: FrameTextureKind, index: number, frame: CopyableVideoFrame, layout: Readonly<FrameSourceLayout>): void;
}

// @public (undocumented)
Expand Down Expand Up @@ -632,6 +634,8 @@ export class BrowserOpaqueFrameBackend implements OpaqueFrameRendererBackend {
readPixels(): Uint8Array;
// (undocumented)
upload(kind: FrameTextureKind, index: number, pixels: Uint8Array): void;
// (undocumented)
uploadFrame(kind: FrameTextureKind, index: number, frame: CopyableVideoFrame, layout: Readonly<FrameSourceLayout>): void;
}

// @public @deprecated (undocumented)
Expand Down Expand Up @@ -2220,6 +2224,18 @@ interface FrameRendererTimerHost {
export { FrameRendererTimerHost }
export { FrameRendererTimerHost as OpaqueFrameRendererTimerHost }

// @public (undocumented)
export interface FrameSourceLayout {
// (undocumented)
readonly height: number;
// (undocumented)
readonly width: number;
// (undocumented)
readonly x: number;
// (undocumented)
readonly y: number;
}

// @public (undocumented)
type FrameTextureKind = "resident" | "stream";
export { FrameTextureKind }
Expand Down Expand Up @@ -3210,6 +3226,8 @@ export interface OpaqueFrameRendererBackend {
readPixels?(): Uint8Array;
// (undocumented)
upload(kind: FrameTextureKind, index: number, pixels: Uint8Array): void;
// (undocumented)
uploadFrame?(kind: FrameTextureKind, index: number, frame: CopyableVideoFrame, layout: Readonly<FrameSourceLayout>): void;
}

// @public (undocumented)
Expand Down
2 changes: 2 additions & 0 deletions examples/grass-rabbit/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.vercel
.env*
1 change: 1 addition & 0 deletions examples/grass-rabbit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"type": "module",
"scripts": {
"build": "vite build --base=/aval/",
"build:vercel": "npm --prefix ../.. run build -w @pixel-point/aval-graph && npm --prefix ../.. run build -w @pixel-point/aval-format && npm --prefix ../.. run build -w @pixel-point/aval-player-web && npm --prefix ../.. run build -w @pixel-point/aval-element && npm --prefix ../.. run build -w @pixel-point/aval-grass-rabbit-example",
"compile": "avl compile motion.json --out public/grass-rabbit.avl --force",
"dev": "vite --host 127.0.0.1"
},
Expand Down
6 changes: 6 additions & 0 deletions examples/grass-rabbit/vercel.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"framework": "vite",
"installCommand": "npm --prefix ../.. ci",
"buildCommand": "npm run build:vercel",
"outputDirectory": "dist",
"ignoreCommand": "git diff --quiet HEAD^ HEAD ./",
"rewrites": [
{
"source": "/aval/grass-rabbit.avl",
Expand Down
34 changes: 29 additions & 5 deletions packages/player-web/src/decoder-worker/core-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -262,11 +262,7 @@ export function validateDecodedFrame(
true
);
}
if (
!isNonContradictoryBt709Limited(frame.colorSpace) ||
expected.colorSpace !== null &&
!matchesColorSpace(frame.colorSpace, expected.colorSpace)
) {
if (!matchesDecodedBt709ColorSpace(frame.colorSpace, expected.colorSpace)) {
throw new DecoderWorkerCoreError(
"DECODER_OUTPUT_INVALID",
"decoder output color space did not match the configured rendition",
Expand All @@ -291,6 +287,34 @@ function isNonContradictoryBt709Limited(actual: VideoColorSpace): boolean {
);
}

function matchesDecodedBt709ColorSpace(
actual: VideoColorSpace,
expected: DecoderWorkerColorSpaceExpectation | null
): boolean {
if (isNonContradictoryBt709Limited(actual)) {
return expected === null || matchesColorSpace(actual, expected);
}
return isWebKitNormalizedBt709(actual) &&
(expected === null || isExactBt709Limited(expected));
}

/** WebKit exposes decoded BT.709 video through this complete normalized tuple. */
function isWebKitNormalizedBt709(actual: VideoColorSpace): boolean {
return actual.fullRange === true &&
actual.matrix === "bt709" &&
actual.primaries === "bt709" &&
actual.transfer === "iec61966-2-1";
}

function isExactBt709Limited(
expected: DecoderWorkerColorSpaceExpectation
): boolean {
return expected.fullRange === false &&
expected.matrix === "bt709" &&
expected.primaries === "bt709" &&
expected.transfer === "bt709";
}

export function normalizeCoreError(
error: unknown,
code: DecoderWorkerErrorCode,
Expand Down
106 changes: 99 additions & 7 deletions packages/player-web/src/decoder-worker/decoder-worker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
DECODER_WORKER_PROTOCOL_VERSION,
type DecoderWorkerClientPort,
type DecoderWorkerAvcConfig,
type DecoderWorkerColorSpaceExpectation,
type DecoderWorkerLimits,
type DecoderWorkerMessagePort,
type DecoderWorkerSample
Expand Down Expand Up @@ -794,6 +795,90 @@ describe("dedicated decoder worker boundary", () => {
await fixture.dispose();
});

it.each([
["unspecified", null],
["exact BT.709 limited", {
fullRange: false,
matrix: "bt709",
primaries: "bt709",
transfer: "bt709"
}]
] as const)(
"accepts WebKit-normalized BT.709 output with %s expectations",
async (_label, expectedColorSpace) => {
const fixture = createFixture(
{ maxOutstandingFrames: 1 },
{ expectedColorSpace }
);
await fixture.configure();
await fixture.client.activateGeneration(1);
await fixture.client.submit(1, createUnitSamples(0, 0, 1));

const frame = fixture.decoder.emitNext({
codedWidth: 2,
displayWidth: 2,
fullRange: true,
matrix: "bt709",
primaries: "bt709",
transfer: "iec61966-2-1"
});
await fixture.client.waitForFrames(1, { timeoutMs: 100 });
fixture.client.takeFrame()?.close();
await drainMessages();

expect(frame.closeCalls).toBe(1);
expect(fixture.decoder.closeCalls).toBe(0);
await fixture.dispose();
}
);

it.each([
{
label: "a near-match output tuple",
expectedColorSpace: null,
outputTransfer: "bt709"
},
{
label: "a noncanonical configured expectation",
expectedColorSpace: {
fullRange: false,
matrix: "bt709",
primaries: "bt709",
transfer: "iec61966-2-1"
},
outputTransfer: "iec61966-2-1"
}
] as const)(
"rejects WebKit color-space compatibility for $label",
async ({ expectedColorSpace, outputTransfer }) => {
const fixture = createFixture(
{ maxOutstandingFrames: 1 },
{ expectedColorSpace }
);
await fixture.configure();
await fixture.client.activateGeneration(1);
await fixture.client.submit(1, createUnitSamples(0, 0, 1));
const waiting = fixture.client.waitForFrames(1, { timeoutMs: 100 });

const frame = fixture.decoder.emitNext({
codedWidth: 2,
displayWidth: 2,
fullRange: true,
matrix: "bt709",
primaries: "bt709",
transfer: outputTransfer
});
await expect(waiting).rejects.toMatchObject({
code: "DECODER_OUTPUT_INVALID",
fatal: true
});
expect(frame.closeCalls).toBe(1);
expect(fixture.decoder.closeCalls).toBe(1);

await fixture.dispose();
}
);

it("closes the decoder when decode() throws and settles the client failure", async () => {
const fixture = createFixture({}, { decoderRejectTag: 0 });
await fixture.configure();
Expand Down Expand Up @@ -1004,6 +1089,7 @@ function createFixture(
config: VideoDecoderConfig
) => VideoDecoderConfig;
readonly codec?: DecoderWorkerAvcConfig["codec"];
readonly expectedColorSpace?: DecoderWorkerColorSpaceExpectation | null;
} = {}
): Fixture {
const { clientPort, workerPort } = createPortPair();
Expand Down Expand Up @@ -1073,7 +1159,7 @@ function createFixture(
displayWidth: 2,
displayHeight: 2,
visibleRect: { x: 0, y: 0, width: 2, height: 2 },
colorSpace: null
colorSpace: options.expectedColorSpace ?? null
},
limits
}),
Expand Down Expand Up @@ -1151,9 +1237,9 @@ class FakeVideoFrame {
public readonly visibleRect = { x: 0, y: 0, width: 2, height: 2 };
public readonly colorSpace: {
readonly fullRange: boolean | null;
readonly matrix: null;
readonly primaries: null;
readonly transfer: null;
readonly matrix: VideoMatrixCoefficients | null;
readonly primaries: VideoColorPrimaries | null;
readonly transfer: VideoTransferCharacteristics | null;
};
public closeCalls = 0;

Expand All @@ -1163,6 +1249,9 @@ class FakeVideoFrame {
readonly codedWidth: number;
readonly displayWidth: number;
readonly fullRange?: boolean;
readonly matrix?: VideoMatrixCoefficients;
readonly primaries?: VideoColorPrimaries;
readonly transfer?: VideoTransferCharacteristics;
}
) {
this.timestamp = chunk.timestamp;
Expand All @@ -1173,9 +1262,9 @@ class FakeVideoFrame {
this.displayHeight = 2;
this.colorSpace = {
fullRange: geometry.fullRange ?? null,
matrix: null,
primaries: null,
transfer: null
matrix: geometry.matrix ?? null,
primaries: geometry.primaries ?? null,
transfer: geometry.transfer ?? null
};
}

Expand Down Expand Up @@ -1251,6 +1340,9 @@ class FakeVideoDecoder implements WorkerVideoDecoderAdapter {
readonly codedWidth: number;
readonly displayWidth: number;
readonly fullRange?: boolean;
readonly matrix?: VideoMatrixCoefficients;
readonly primaries?: VideoColorPrimaries;
readonly transfer?: VideoTransferCharacteristics;
} = {
codedWidth: 2,
displayWidth: 2
Expand Down
1 change: 1 addition & 0 deletions packages/player-web/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ export {
type FrameRendererOptions,
type FrameRendererSnapshot,
type FrameRendererTimerHost,
type FrameSourceLayout,
type FrameTextureKind
} from "./runtime/frame-renderer.js";
export {
Expand Down
Loading
Loading