# Deliver a landscape clip on a vertical letterbox canvas

Make a vertical review file while preserving the complete landscape composition. The fixture chooses a 160×90 content rectangle with 10-pixel side margins and 115-pixel top/bottom margins.

This complete kit uses Node standard-library modules only. Install/use your own FFmpeg; the binary is not redistributed. Set FFMPEG_BIN to its absolute path if it is not on PATH.

Run from this directory and select a NEW output directory. Existing output directories are refused. On a memory-managed workstation, put its local serialization wrapper before the Node command. No provider credentials or off-device fixture requests are used.

```sh
node run-recipe.mjs --out ./my-run
```

Recorded execution: 8 checks passed, including 1 expected rejections. Node v24.18.0. ffmpeg version 6.0 Copyright (c) 2000-2023 the FFmpeg developers

Preparation

- Prepare Node.js and FFmpeg with libx264, scale and pad available; set FFMPEG_BIN when necessary. The recorded environment was Node v24.18.0 and FFmpeg 6.0.
- Use the supplied original one-second landscape-source.mp4. It has ten frames, a red left edge and a yellow right edge, and intentionally contains no audio.
- Keep the chosen delivery geometry explicit: source 320×180, scaled content 160×90, output 180×320, placement x=10 and y=115. The example uses yuv444p for its small lossless comparison.

## 01. Establish composition markers

The script makes the landscape fixture with visible markers on both source edges. Those markers are acceptance targets: losing either edge during conversion would remove source composition, even if the result still has a valid portrait dimension.

## 02. Scale proportionally and add a canvas

Apply scale=160:90:flags=neighbor,pad=180:320:10:115:color=black,setsar=1. The 160×90 rectangle preserves 16:9 exactly; the outer canvas is 9:16. Its small side margins deliberately avoid rounding the landscape content to a distorted 180-pixel width.

## 03. Decode and compare the content rectangle

Fully decode the exported file into RGB. Compare its 160×90 interior against an independently scaled source frame, using a maximum channel difference tolerance of four. The recorded fixture measured zero. This tolerance and yuv444p fixture are not a general colour-management qualification.

## 04. Audit borders and frame count

Check ten output frames, black top and bottom padding, a red left marker and a yellow right marker. The negative case is a local arithmetic aspect-ratio guard: the deliberately unequal cross-products reject stretching. It does not encode a stretched bad file or exercise a general product validator. Keep the measured offsets and actual max RGB difference in the receipt.

Acceptance

- The actual output fully decodes to ten 180×320 RGB frames.
- The outer canvas is 9:16 and the inner 160×90 rectangle remains 16:9.
- The decoded interior differs by at most four channel values from the separately scaled source; the recorded run measured zero.
- Top and bottom sampled padding pixels are black within three channel values.
- The red left and yellow right composition markers remain inside the delivered picture.
- The local arithmetic guard rejects unequal aspect-ratio cross-products; it is not a test of a stretched FFmpeg output. All eight checks pass.

Troubleshooting

The subject fills the portrait frame and both edges disappear. Change the layout to the documented pad operation and rerun the two edge checks. Cropping is a different delivery decision.

The image looks slightly squeezed. Change only the content dimensions back to 160×90 before padding; verify the exact 16:9 ratio by cross-multiplication.

Pixel comparison differs after export. Compare the tool version and pixel format first. Rerun the supplied yuv444p fixture before changing the tolerance for a separate delivery codec.

Public receipt copies normalize only local checkout paths and include publicPathNormalization with the original receipt SHA-256; raw originals are retained outside public assets. The actual receipts are under executed/. failure-receipt.json lists expected negative cases, not failed-suite assertions. Original tiny fixtures are generated again into your new output directory; shipped executed inputs and outputs let you inspect the recorded run. Subprocesses have a 15-second timeout, an 8-MB output cap, argument arrays and no shell. Temporary raw buffers and local HTTP servers are cleaned in finally.

checksums.json records every distributed file except itself and the archive, avoiding self-reference. The ZIP includes that same checksum list. Recompute hashes after download; comparison requires trusting the expected list. execution-receipt.json records executed script hashes before and after the run. The complete kit archive is also bound by the publishing catalog asset inventory.

The measured file proves this layout conversion on the original tiny fixture. Confirm a distribution platform’s codec and resolution requirements separately before using the same geometry for a final upload.

Sources read on 2026-10-14

- FFmpeg filters: scale, pad, trim, setpts and showinfo: https://ffmpeg.org/ffmpeg-filters.html
- FFmpeg command-line documentation: maps, codecs and error exit: https://ffmpeg.org/ffmpeg.html
- Node.js child process API: bounded argument-array execution: https://nodejs.org/api/child_process.html
