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

7.4 KiB

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".

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:

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:

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:

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:

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:

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:

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
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:

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.