Files
silo-server/docs/superpowers/plans/2026-07-28-transcode-resolution-clamp.md
63e18cf37f fix(playback): prevent transcode resolution upscaling (#503)
* docs(playback): design transcode resolution clamp

* docs(playback): plan transcode resolution clamp

* fix(playback): prevent transcode resolution upscaling

* refactor(playback): share transcode resolution tiers

---------

Co-authored-by: rxwatcher <rxwatcher@users.noreply.github.com>
Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
2026-07-28 13:07:05 -04:00

207 lines
7.4 KiB
Markdown

# Transcode Resolution Clamp Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Prevent encoded-video transports from targeting a resolution above the final effective source file.
**Architecture:** Add one pure resolution-normalization helper in the playback API handler and invoke it exactly once after alternate-file selection. Mutate the request's target resolution at that boundary so remote transport, local transport, persistence, activity reporting, and reconstruction consume the same normalized recipe.
**Tech Stack:** Go, `net/http` handler tests, Silo playback session and transcode-node test doubles.
## Global Constraints
- Do not apply the new clamp to video-copy requests; preserve their existing
downstream recipe normalization.
- Preserve empty or unrecognized requested/source resolutions unchanged.
- Clamp only recognized encoded-video targets above the recognized effective source.
- Do not alter bitrate, codecs, API shapes, status codes, settings, migrations, clients, or production configuration.
- Prove the regression RED before writing production code.
---
### Task 1: Normalize the Effective Transcode Recipe
**Files:**
- Modify: `internal/api/handlers/playback.go`
- Test: `internal/api/handlers/playback_test.go`
**Interfaces:**
- Consumes: `transcodeStartRequest.TargetResolution`, `transcodeStartRequest.TargetCodecVideo`, and `models.MediaFile.Resolution` after alternate-file selection.
- Produces: `clampEncodedTargetResolution(requestedResolution, sourceResolution string) string`.
- Produces: one normalized `req.TargetResolution` consumed unchanged by remote/local transport construction and `buildTranscodeSessionReplacement`.
- [ ] **Step 1: Strengthen the existing remote fallback test**
Change `TestHandleStartTranscode_PreservesRecomputedBaseMethodAfterFallback` to
request 2160p at 10000 kbps. Assert that the captured
`transcodenode.TranscodeStartRequest.TargetResolution` is `1080p`, while the
effective alternate remains file 99 and the resulting playback session stores
`TargetResolution == "1080p"`.
```go
strings.NewReader(`{"session_id":"` + startResp.SessionID +
`","seek_seconds":0,"target_resolution":"2160p",` +
`"target_codec_video":"h264","target_codec_audio":"aac",` +
`"target_bitrate_kbps":10000,"segment_duration":2,` +
`"subtitle_track_index":-1,"subtitle_burn_in":false}`)
if remoteStartReq.TargetResolution != "1080p" {
t.Fatalf("remote target resolution = %q, want 1080p", remoteStartReq.TargetResolution)
}
if session.TargetResolution != "1080p" {
t.Fatalf("session target resolution = %q, want 1080p", session.TargetResolution)
}
```
- [ ] **Step 2: Run the regression and verify RED**
Run:
```bash
go test ./internal/api/handlers -run TestHandleStartTranscode_PreservesRecomputedBaseMethodAfterFallback -count=1
```
Expected: FAIL because the captured remote request still contains `2160p`.
- [ ] **Step 3: Add pure normalization table tests**
Add `TestClampEncodedTargetResolution` beside the handler tests. Cover:
```go
tests := []struct {
name, requested, source, want string
}{
{"clamps 2160p to 1080p", "2160p", "1080p", "1080p"},
{"keeps lower target", "720p", "1080p", "720p"},
{"keeps equal target", "1080p", "1080p", "1080p"},
{"keeps empty target", "", "1080p", ""},
{"keeps unknown target", "source", "1080p", "source"},
{"keeps target for unknown source", "2160p", "native", "2160p"},
{"supports low tiers", "480p", "420p", "420p"},
}
```
- [ ] **Step 4: Add the minimal helper and post-selection call**
Add a package-level recognized-height map/helper near `resolutionRank`:
```go
func clampEncodedTargetResolution(requestedResolution, sourceResolution string) string {
requestedHeight, requestedKnown := transcodeResolutionHeight(requestedResolution)
sourceHeight, sourceKnown := transcodeResolutionHeight(sourceResolution)
if !requestedKnown || !sourceKnown || requestedHeight <= sourceHeight {
return requestedResolution
}
return sourceResolution
}
```
After the 4K alternate block and before transport planning:
```go
if !videoCopy {
req.TargetResolution = clampEncodedTargetResolution(
req.TargetResolution,
file.Resolution,
)
}
```
Use an exhaustive switch for the six supported transcode tiers. Do not modify
the FFmpeg filter builder or web client.
- [ ] **Step 5: Verify focused GREEN**
Run:
```bash
go test ./internal/api/handlers \
-run 'TestHandleStartTranscode_PreservesRecomputedBaseMethodAfterFallback|TestClampEncodedTargetResolution' \
-count=1
```
Expected: PASS.
- [ ] **Step 6: Add copy and local-boundary regression coverage**
Add focused tests proving:
- a copy-video request bypasses the new clamp and retains its existing
downstream empty-resolution recipe;
- a local encoded-video request receives the clamped target in the persisted
session and generated FFmpeg recipe boundary;
- a requested 720p encode for a 1080p effective file remains 720p.
Use the existing local fake-FFmpeg and session-manager patterns in
`playback_test.go`; do not introduce production test hooks.
- [ ] **Step 7: Run focused and package verification**
Run:
```bash
gofmt -w internal/api/handlers/playback.go internal/api/handlers/playback_test.go
go test ./internal/api/handlers -count=1
go test ./internal/playback -count=1
go test ./internal/api/handlers ./internal/playback -race -count=1
git diff --check
```
Expected: all commands exit 0.
- [ ] **Step 8: Commit implementation**
```bash
git add internal/api/handlers/playback.go internal/api/handlers/playback_test.go
git commit -m "fix(playback): prevent transcode resolution upscaling"
```
### Task 2: Independent Review and Final Verification
**Files:**
- Review: `docs/superpowers/specs/2026-07-28-transcode-resolution-clamp-design.md`
- Review: `internal/api/handlers/playback.go`
- Review: `internal/api/handlers/playback_test.go`
**Interfaces:**
- Consumes: committed spec and implementation diff from `upstream/main`.
- Produces: reviewer verdict and a clean, verified branch suitable for a separate pull request.
- [ ] **Step 1: Request focused correctness/security review**
Review the change against these invariants:
- no encoded output exceeds the effective source tier;
- copy and unknown-resolution compatibility is preserved;
- local, offloaded, persisted, and reconstruction paths share one value;
- no target-bitrate or API behavior was broadened;
- tests fail without the production correction.
- [ ] **Step 2: Address Critical or Important findings test-first**
For each accepted behavioral finding, add or strengthen a failing test, observe
RED, make the smallest production correction, and rerun focused GREEN. Do not
bundle unrelated cleanup.
- [ ] **Step 3: Run final verification**
Run:
```bash
go test ./internal/api/handlers ./internal/playback -count=1
go test ./internal/api/handlers ./internal/playback -race -count=1
go test ./... -count=1
make verify-local-paths
git diff --check
git status --short --branch
```
Expected: tests and repository policy checks exit 0; status contains no
uncommitted implementation changes.
- [ ] **Step 4: Prepare handoff**
Summarize the production evidence, exact normalization boundary, RED/GREEN
proof, focused/full verification, reviewer verdict, and deployment caveat.
Do not deploy or mutate production as part of this plan.