110 lines
3.7 KiB
Markdown
110 lines
3.7 KiB
Markdown
# Use Playback Selector Implementation Plan
|
|
|
|
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
|
|
|
**Goal:** Add an asset variants endpoint and wire MediaPanel to use `pickVideoPlaybackVariant` for derived MP4 selection with a safe fallback.
|
|
|
|
**Architecture:** Introduce a minimal `/api/assets/:id/variants` route that returns `{ kind, size, key }` from `asset_variants`. MediaPanel fetches variants on-demand for videos, uses `pickVideoPlaybackVariant` to decide whether to request `video_mp4` (size 720), and falls back to original if the derived URL fails.
|
|
|
|
**Tech Stack:** Next.js App Router API routes, Postgres via `@tline/db`, Bun test runner.
|
|
|
|
### Task 1: Add variants API route
|
|
|
|
**Files:**
|
|
- Create: `apps/web/app/api/assets/[id]/variants/route.ts`
|
|
- Test: `apps/web/src/__tests__/variants-route.test.ts`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
Create `apps/web/src/__tests__/variants-route.test.ts`:
|
|
|
|
```ts
|
|
import { test, expect } from "bun:test";
|
|
|
|
test("variants route returns only kind/size/key fields", async () => {
|
|
const { shapeVariants } = await import("../../app/api/assets/[id]/variants/shape");
|
|
const rows = [
|
|
{ kind: "video_mp4", size: 720, key: "derived/video/a.mp4", mime_type: "video/mp4" },
|
|
];
|
|
expect(shapeVariants(rows)).toEqual([{ kind: "video_mp4", size: 720, key: "derived/video/a.mp4" }]);
|
|
});
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `bun test apps/web/src/__tests__/variants-route.test.ts`
|
|
Expected: FAIL (missing module or function)
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
- Create `apps/web/app/api/assets/[id]/variants/shape.ts` with `shapeVariants(rows)` that returns `{ kind, size, key }` only.
|
|
- Create `apps/web/app/api/assets/[id]/variants/route.ts`:
|
|
- Validate `id` with `z.string().uuid()`
|
|
- Query `asset_variants` by `asset_id`
|
|
- Return JSON array of `shapeVariants(rows)`
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `bun test apps/web/src/__tests__/variants-route.test.ts`
|
|
Expected: PASS
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add apps/web/app/api/assets/[id]/variants/route.ts apps/web/app/api/assets/[id]/variants/shape.ts \
|
|
apps/web/src/__tests__/variants-route.test.ts
|
|
git commit -m "feat: add asset variants endpoint"
|
|
```
|
|
|
|
### Task 2: Use playback selector in MediaPanel
|
|
|
|
**Files:**
|
|
- Modify: `apps/web/app/components/MediaPanel.tsx`
|
|
- Modify: `apps/web/app/lib/playback.ts`
|
|
- Test: `apps/web/src/__tests__/prefer-derived.test.ts`
|
|
|
|
**Step 1: Write the failing test**
|
|
|
|
Add to `apps/web/src/__tests__/prefer-derived.test.ts`:
|
|
|
|
```ts
|
|
import { test, expect } from "bun:test";
|
|
import { pickVideoPlaybackVariant } from "../../app/lib/playback";
|
|
|
|
test("pickVideoPlaybackVariant returns null when no variants", () => {
|
|
expect(
|
|
pickVideoPlaybackVariant({
|
|
originalMimeType: "video/x-matroska",
|
|
variants: [],
|
|
}),
|
|
).toBeNull();
|
|
});
|
|
```
|
|
|
|
**Step 2: Run test to verify it fails**
|
|
|
|
Run: `bun test apps/web/src/__tests__/prefer-derived.test.ts`
|
|
Expected: FAIL (function does not handle empty variants)
|
|
|
|
**Step 3: Write minimal implementation**
|
|
|
|
- Update `pickVideoPlaybackVariant` to return `null` when no `video_mp4` variants exist.
|
|
- Update `MediaPanel` video URL loader to:
|
|
1) Fetch `/api/assets/:id/variants`
|
|
2) Call `pickVideoPlaybackVariant`
|
|
3) If variant found → request `kind=video_mp4&size=720`
|
|
4) If not found or fetch fails → request `variant=original`
|
|
|
|
**Step 4: Run test to verify it passes**
|
|
|
|
Run: `bun test apps/web/src/__tests__/prefer-derived.test.ts`
|
|
Expected: PASS
|
|
|
|
**Step 5: Commit**
|
|
|
|
```bash
|
|
git add apps/web/app/components/MediaPanel.tsx apps/web/app/lib/playback.ts \
|
|
apps/web/src/__tests__/prefer-derived.test.ts
|
|
git commit -m "fix: use playback selector in MediaPanel"
|
|
```
|