화면 속 화면(PiP)
이 레시피를 마치면 한 클립을 다른 클립 위에 작은 화면으로 띄울 수 있습니다. 크기를 줄여 구석에 놓고, 모서리를 둥글게 하고, 그림자를 드리웁니다. FFmpeg의 overlay와 scale을 문서 하나로 표현하는 셈입니다.
삽입 화면의 크기, 위치, 모서리
섹션 제목: “삽입 화면의 크기, 위치, 모서리”시각 클립에는 모두 visual 객체가 있습니다. position, anchor, scale, rotation_degrees, opacity입니다. 클립의 masks는 클립이 보이는 영역을 제한합니다. 뒤에 오는 트랙이 위에 그려지므로 삽입 화면은 주 화면보다 위 트랙에 둡니다.
{ "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": [] } ] }}- 크기. 소스는 먼저 화면비를 유지한 채 캔버스 안에 맞춰집니다.
scale은 거기에 곱해지므로, 16:9 캔버스에 16:9 사진을0.32로 그리면 캔버스 너비의 32%가 됩니다. - 위치.
anchor는 소스의 어느 점을position이 가리키는 캔버스 위치에 놓을지 정하며, 둘 다 0~1로 정규화된 값입니다.anchor를 오른쪽 아래 모서리(1, 1)로,position을 (0.96, 0.93)으로 두면, 크기와 관계없이 삽입 화면의 모서리가 오른쪽에서 4%, 아래에서 7% 떨어진 곳에 놓입니다. 기본 앵커는 가운데입니다. - 둥근 모서리. 마스크는 소스 전체 크기(
center0.5, 0.5,size1, 1)의rectangle에corner_radius를 준 것입니다. 마스크 도형은 변환 전 클립의 소스 공간 기준이므로 삽입 화면과 함께 크기가 바뀌고 움직입니다.corner_radius는 소스 짧은 변에 대한 비율입니다. - 페이드 인.
opacity는 0.5초에 0, 0.9초에 1로 키를 잡았습니다. 클립의visual은 타임라인 시간 기준이므로 이 키들은 0.5초에 시작하는 클립의timeline_range안에 있습니다.
그림자 넣기
섹션 제목: “그림자 넣기”그림자는 삽입 화면 바깥에 드리워야 하므로, 삽입 화면을 배치한 다음에 그려야 합니다. 클립의 visual과 masks는 맨 마지막에 적용되므로, 이 작업은 클립의 그림을 만드는 그래프인 컴포지션에서 합니다. 아래 컴포지션은 클립에 마스크를 씌우고, transform 노드로 배치한 뒤, 결과에 alpha2.effect.drop-shadow를 적용합니다.
{ "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": [] } ] }}클립에서 출력까지의 그래프는 다음과 같습니다.
clip은 삽입 클립 자체의 그림입니다.rounded는 같은 둥근 사각형을 쓰는shape_mask노드입니다. 도형은 입력의 정규화 공간 기준입니다.placed는transform노드입니다. 클립의visual과 같은 필드를 받으므로 크기와 위치는 앞의 예와 같습니다.shadow는 배치된 그림에 그림자를 적용하므로, 그림자가 둥근 윤곽을 따라갑니다.angle은 도 단위이고,distance와softness는 정규화된 길이입니다(카탈로그 기본값은 0.01).
삽입 클립은 "composition": { "id": "inset" }으로 이 컴포지션을 쓰고, visual은 기본값으로 둡니다. 이펙트 파라미터는 타입이 붙은 값({ "type": "float", "value": 0.8 })입니다. 생략한 파라미터는 기본값을 따르며, 여기서는 검은색 color가 그렇습니다. 컴포지션의 키프레임은 클립 시작에서 0이 되는 컴포지션 시간 기준입니다.
export하기
섹션 제목: “export하기”소스가 coast와 forest이므로 export에는 각각의 파일이 필요합니다.
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초 frameRate: { num: 30, den: 1 }, output: "pip.mp4",});사진 대신 영상 파일을 쓰려면 소스의 kind를 video로 바꾸고 --asset에 영상 파일을 지정하십시오. 그러면 클립의 source_range가 각 영상의 어느 부분을 재생할지 정합니다.
