Picture in picture
After this recipe you can show one clip as an inset over another: scaled down, placed in a corner, with rounded corners and a drop shadow. This is FFmpeg’s overlay and scale in one document.
Scale, place and round the inset
Section titled “Scale, place and round the inset”Every visual clip has a visual object: position, anchor, scale, rotation_degrees and opacity. A clip’s masks limit where it is visible. Put the inset on a track above the main picture, since later tracks draw on top.
{ "schema_version": 1, "id": "pip", "name": "Picture in picture", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "coast", "locator": "asset://coast", "kind": "image" }, { "id": "forest", "locator": "asset://forest", "kind": "image" } ], "timeline": { "tracks": [ { "id": "main", "name": "Main", "kind": "video", "clips": [ { "id": "main-clip", "source_id": "coast", "timeline_range": { "start": 0, "end": 480000 }, "source_range": { "start": 0, "end": 480000 } } ], "transitions": [] }, { "id": "inset", "name": "Inset", "kind": "video", "clips": [ { "id": "inset-clip", "source_id": "forest", "timeline_range": { "start": 60000, "end": 480000 }, "source_range": { "start": 0, "end": 420000 }, "visual": { "anchor": { "x": 1, "y": 1 }, "position": { "mode": "constant", "value": { "x": 0.96, "y": 0.93 } }, "scale": { "mode": "constant", "value": { "x": 0.32, "y": 0.32 } }, "opacity": { "mode": "keyframes", "keyframes": [ { "time": 60000, "value": 0, "interpolation": { "type": "cubic_bezier", "x1": 0.2, "y1": 0, "x2": 0, "y2": 1 } }, { "time": 108000, "value": 1, "interpolation": { "type": "hold" } } ] } }, "masks": [ { "shape": { "type": "rectangle", "center": { "mode": "constant", "value": { "x": 0.5, "y": 0.5 } }, "size": { "mode": "constant", "value": { "x": 1, "y": 1 } }, "corner_radius": { "mode": "constant", "value": 0.1 } } } ] } ], "transitions": [] } ] }}- Size. A source is first fitted inside the canvas, keeping its aspect ratio.
scalemultiplies that, so0.32draws the inset at 32% of the canvas width for a 16:9 photo on a 16:9 canvas. - Place.
anchoris the point of the source thatpositionputs on the canvas, both normalized (0 to 1). Withanchorat the bottom-right corner (1, 1) andpositionat (0.96, 0.93), the inset’s corner sits 4% from the right edge and 7% from the bottom, whatever its scale. The default anchor is the centre. - Round. The mask is a
rectanglethe size of the whole source (center0.5, 0.5 andsize1, 1) with acorner_radius. Mask geometry is in the clip’s source space, before its transform, so the mask scales and moves with the inset.corner_radiusis a fraction of the source’s shorter side. - Fade in.
opacityis keyed from 0 at 0.5 s to 1 at 0.9 s. A clip’svisualis keyed in timeline time, so these keys sit inside the clip’stimeline_range, which starts at 0.5 s.
Add a drop shadow
Section titled “Add a drop shadow”A shadow has to fall outside the inset, so it has to be drawn after the inset is placed. A clip’s visual and masks are applied last, so do the work in a composition instead: a graph that builds the clip’s picture. This one masks the clip, places it with a transform node, and runs alpha2.effect.drop-shadow on the result.
{ "schema_version": 1, "id": "pip-shadow", "name": "Picture in picture with a shadow", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "coast", "locator": "asset://coast", "kind": "image" }, { "id": "forest", "locator": "asset://forest", "kind": "image" } ], "compositions": [ { "id": "inset", "output": "shadow", "nodes": [ { "id": "clip", "op": { "type": "clip" } }, { "id": "rounded", "op": { "type": "shape_mask", "input": "clip", "masks": [ { "shape": { "type": "rectangle", "center": { "mode": "constant", "value": { "x": 0.5, "y": 0.5 } }, "size": { "mode": "constant", "value": { "x": 1, "y": 1 } }, "corner_radius": { "mode": "constant", "value": 0.1 } } } ] } }, { "id": "placed", "op": { "type": "transform", "input": "rounded", "transform": { "anchor": { "x": 1, "y": 1 }, "position": { "mode": "constant", "value": { "x": 0.96, "y": 0.93 } }, "scale": { "mode": "constant", "value": { "x": 0.32, "y": 0.32 } } } } }, { "id": "shadow", "op": { "type": "effect", "input": "placed", "effect": { "id": "alpha2.effect.drop-shadow", "version": 1, "params": { "opacity": { "mode": "constant", "value": { "type": "float", "value": 0.8 } }, "angle": { "mode": "constant", "value": { "type": "float", "value": -90 } }, "distance": { "mode": "constant", "value": { "type": "float", "value": 0.02 } }, "softness": { "mode": "constant", "value": { "type": "float", "value": 0.04 } } } } } } ] } ], "timeline": { "tracks": [ { "id": "main", "name": "Main", "kind": "video", "clips": [ { "id": "main-clip", "source_id": "coast", "timeline_range": { "start": 0, "end": 480000 }, "source_range": { "start": 0, "end": 480000 } } ], "transitions": [] }, { "id": "inset", "name": "Inset", "kind": "video", "clips": [ { "id": "inset-clip", "source_id": "forest", "timeline_range": { "start": 0, "end": 480000 }, "source_range": { "start": 0, "end": 480000 }, "composition": { "id": "inset" } } ], "transitions": [] } ] }}The graph, from the clip to the output:
clipis the inset’s own picture.roundedis ashape_masknode with the same rounded rectangle. Its geometry is in its input’s normalized space.placedis atransformnode. It takes the same fields as a clip’svisual, so the scale and position are the ones above.shadowruns the drop shadow over the placed picture, so the shadow follows the rounded outline.angleis in degrees, anddistanceandsoftnessare normalized lengths (the catalog defaults are 0.01).
The inset clip then uses the composition with "composition": { "id": "inset" } and keeps its default visual. Effect parameters are typed values ({ "type": "float", "value": 0.8 }). Parameters you leave out keep their defaults, here the black color. Composition keyframes are in composition time, which starts at the clip’s start.
Export it
Section titled “Export it”The sources are coast and forest, so the export needs a file for each.
alpha2-host export --project pip.json \ --asset coast=coast.jpg --asset forest=forest.jpg \ --out pip.mp4import { readFile } from "node:fs/promises";import { exportProject } from "@alpha2-sh/node";
const project = JSON.parse(await readFile("pip.json", "utf8"));await exportProject({ project, assets: { coast: "coast.jpg", forest: "forest.jpg" }, startTicks: 0, endTicks: 480_000, // 4 s frameRate: { num: 30, den: 1 }, output: "pip.mp4",});With video files instead of photos, change the sources’ kind to video and point --asset at the video files. The clips’ source_range then chooses which part of each video plays.
See also
Section titled “See also”- Effects and compositions: node types and composition time.
- Timeline schema:
visualandmaskson a clip. - Effects reference:
drop-shadow,outlineand the rest. - Vertical video:
scaleto fill a 9:16 frame.
