콘텐츠로 이동

확장

확장 프로바이더는 일반 비디오·이미지 디코더가 읽지 못하는 포맷의 프레임을 공급합니다. 이 페이지를 읽고 나면 이런 소스를 프로젝트에 선언하고, 네이티브 호스트로 export하고, 브라우저에서 확장 모듈로 렌더링할 수 있습니다.

이 포맷들은 어디서나 같은 Rust 코드가 읽습니다. alpha2-host에는 이 코드가 내장되어 있고, 브라우저는 페이지가 이런 소스를 열 때만 별도의 WebAssembly 모듈(alpha2_extensions_bg.wasm)로 로드합니다.

소스는 provider에 프로바이더를 지정합니다. 프로바이더의 format에 따라 소스의 kind가 정해집니다.

provider.format 읽는 대상 소스 kind 설정
svg 정적 SVG 문서. 화면에 표시되는 크기로 래스터화합니다 image
animated_image GIF, 애니메이션 WebP 또는 APNG. 프레임마다 고유한 길이를 갖는 한 주기의 프레임입니다. 반복은 클립이 정합니다. video
lottie JSON 파일 또는 dotLottie 패키지 형태의 Lottie 애니메이션. 자체 프레임 단위로 평가합니다 video animation: 보여 줄 dotLottie 애니메이션의 id. 없으면 패키지의 첫 애니메이션입니다.
exr 정지 OpenEXR 이미지. 선형 광(linear-light) RGB(A)를 하프 정밀도로 읽습니다 image primaries: 파일의 색도 정보를 덮어씁니다
image_sequence 고정 속도로 차례대로 재생하는 PNG 또는 OpenEXR 정지 이미지 video frames, frame_rate, start_number, missing, primaries

primaries는 bt709, bt2020, p3_d65, aces_ap0, aces_ap1 중 하나입니다. 지정하지 않으면 OpenEXR 파일 자체의 색도 정보를 쓰고, 그것도 없으면 BT.709를 씁니다. 색상과 HDR을 참고하세요.

이미지 시퀀스는 각 프레임의 로케이터를 재생 순서대로 나열합니다.

  • frames: 로케이터 목록. null은 빠진 번호를 나타내며, missing이 그 표시 방법을 지정할 때만 쓸 수 있습니다.
  • frame_rate: 프레임을 재생하는 속도, { "num", "den" }.
  • start_number: 첫 프레임의 번호. 파일에 매겨진 번호를 그대로 씁니다(기본값 0).
  • missing: reject(기본값), hold(이전 프레임 표시), blank 중 하나.

모든 프로바이더를 쓰는 프로젝트

섹션 제목: “모든 프로바이더를 쓰는 프로젝트”
project.json
{
"schema_version": 1,
"id": "extensions",
"name": "Extensions",
"settings": {
"width": 1280,
"height": 720,
"frame_rate": { "num": 30, "den": 1 },
"sample_rate": 48000
},
"sources": [
{ "id": "logo", "kind": "image", "locator": "logo.svg", "provider": { "format": "svg" } },
{ "id": "sticker", "kind": "video", "locator": "sticker.gif", "provider": { "format": "animated_image" } },
{ "id": "intro", "kind": "video", "locator": "intro.lottie", "provider": { "format": "lottie", "animation": "main" } },
{ "id": "plate", "kind": "image", "locator": "plate.exr", "provider": { "format": "exr", "primaries": "p3_d65" } },
{
"id": "render",
"kind": "video",
"locator": "sequence://render",
"provider": {
"format": "image_sequence",
"frames": ["render_0001.png", "render_0002.png", null, "render_0004.png"],
"frame_rate": { "num": 24, "den": 1 },
"start_number": 1,
"missing": "hold"
}
}
],
"timeline": {
"tracks": [
{
"id": "back",
"name": "Background",
"kind": "video",
"clips": [
{ "id": "render-clip", "source_id": "render", "timeline_range": { "start": 0, "end": 20000 }, "source_range": { "start": 0, "end": 20000 } },
{ "id": "plate-clip", "source_id": "plate", "timeline_range": { "start": 20000, "end": 120000 }, "source_range": { "start": 0, "end": 100000 } }
]
},
{
"id": "front",
"name": "Graphics",
"kind": "video",
"clips": [
{ "id": "intro-clip", "source_id": "intro", "timeline_range": { "start": 0, "end": 60000 }, "source_range": { "start": 0, "end": 60000 } },
{ "id": "logo-clip", "source_id": "logo", "timeline_range": { "start": 60000, "end": 120000 }, "source_range": { "start": 0, "end": 60000 } }
]
},
{
"id": "top",
"name": "Sticker",
"kind": "video",
"clips": [
{ "id": "sticker-clip", "source_id": "sticker", "timeline_range": { "start": 0, "end": 120000 }, "source_range": { "start": 0, "end": 120000 } }
]
}
]
}
}

엔진은 소스의 locator를 해석하지 않으며, 직접 열지도 않습니다. 로케이터의 의미는 호스트가 정하므로, 같은 프로젝트가 네이티브에서는 디스크의 파일로, 브라우저에서는 페이지가 가져온 바이트로 동작합니다.

alpha2-host는 비디오와 이미지를 읽을 때와 같은 방식으로, 넘겨받은 파일에서 프로바이더 소스를 읽습니다.

대상 CLI 요청 필드(Node.js, Python, 프로토콜)
SVG, 애니메이션 이미지, Lottie, OpenEXR 소스의 파일 --asset <source-id>=<path> assets
이미지 시퀀스의 프레임 파일(순서대로, 빈 자리는 빈 경로) --frames <source-id>=<path>,<path>,... frames(빈 자리는 null)
SVG <image href>나 Lottie 이미지 에셋이 참조하는 이미지. 소스에 적힌 로케이터로 지정합니다 --resource <source-id> <locator> <path> resources
SVG 텍스트용 폰트 --font <path> fonts
터미널 창
alpha2-host export --project project.json \
--asset logo=art/logo.svg \
--asset sticker=art/sticker.gif \
--asset intro=art/intro.lottie \
--asset plate=plates/plate.exr \
--frames render=render/render_0001.png,render/render_0002.png,,render/render_0004.png \
--font fonts/HankenGrotesk.ttf \
--out extensions.mp4

같은 export를 Node.js에서 실행하면 다음과 같습니다.

await client.exportProject({
project,
assets: {
logo: "art/logo.svg",
sticker: "art/sticker.gif",
intro: "art/intro.lottie",
plate: "plates/plate.exr",
},
frames: {
render: ["render/render_0001.png", "render/render_0002.png", null, "render/render_0004.png"],
},
resources: { logo: { "art/badge.png": "/media/badge.png" } }, // logo.svg 안의 <image href>
fonts: ["fonts/HankenGrotesk.ttf"],
startTicks: 0,
endTicks: 120_000,
frameRate: { num: 30, den: 1 },
output: "extensions.mp4",
});

resources 항목에 없는 참조 이미지는 그려지지 않으며, export는 missing_image를 보고합니다. memory.providerSourceBytes(기본값 512 MiB)는 열려 있는 프로바이더 소스가 차지하는 메모리의 상한입니다. 이를 넘으면 직전 두 프레임에 보이지 않은 소스를 닫고, 필요할 때 다시 엽니다.

브라우저에서는 alpha2/extensions가 이 포맷들을 읽고, 여러분은 렌더링된 프레임을 다른 프레임과 똑같이 엔진에 업로드합니다(프레임 공급 참고).

  1. 페이지마다 한 번 loadExtensions()로 모듈을 로드합니다.
  2. SVG 텍스트를 그릴 폰트를 setFonts로 등록합니다.
  3. 각 프로바이더 소스를 파일 바이트로 엽니다: open(provider, bytes).
  4. frameNeeds가 보고하는 요구마다 해당 소스 시간으로 소스를 렌더링하고 결과를 업로드합니다.
import { loadExtensions } from "alpha2/extensions";
// 번들러가 alpha2/wasm/alpha2_extensions_bg.wasm을 옮긴다면 그 URL을 넘깁니다.
const extensions = await loadExtensions();
extensions.setFonts([fontBytes]); // 네이티브 export와 같은 폰트를 같은 순서로 넘깁니다
const bytesOf = async (url) => new Uint8Array(await (await fetch(url)).arrayBuffer());
const opened = new Map();
for (const source of project.sources) {
if (!source.provider || source.provider.format === "image_sequence") continue;
opened.set(source.id, {
kind: source.kind,
source: extensions.open(source.provider, await bytesOf(`/media/${source.locator}`)),
});
}
const { width, height } = project.settings;
const time = 60_000n;
for (const need of JSON.parse(engine.frameNeeds(time))) {
const entry = opened.get(need.sourceId);
if (!entry) continue; // 프로바이더 소스가 아닙니다
const ticks = BigInt(need.mediaTimeTicks);
const frame = entry.source.render(ticks, { width, height });
const still = entry.kind === "image";
if (frame.precision === "linear_f16") {
// OpenEXR: BT.709 원색 기준의 프리멀티플라이드 선형 광 하프 플로트.
if (still) engine.uploadPresentedLinearImage(need.sourceId, frame.width, frame.height, frame.pixels, width, height);
else engine.uploadPresentedLinearFrame(need.sourceId, ticks, frame.width, frame.height, frame.pixels, width, height);
} else if (still) {
engine.uploadPresentedRgbaImage(need.sourceId, frame.width, frame.height, frame.pixels, true, width, height);
} else {
engine.uploadPresentedRgbaFrame(need.sourceId, ticks, frame.width, frame.height, frame.pixels, true, width, height);
}
}
engine.renderAt(time);
  • render(ticks, fit)은 벡터 소스(SVG, Lottie)를 종횡비를 유지한 채 fit에 맞게 래스터화하고, 래스터 소스는 원래 크기로 렌더링합니다. uploadPresented* 메서드는 엔진이 모든 소스를 배치하는 방식과 똑같이 프레임을 출력에 배치합니다.
  • 8비트 프레임은 프리멀티플라이드 sRGB RGBA8(precision: "rgba8")입니다. OpenEXR 프레임은 precision: "linear_f16"이며, 0~1 범위 밖의 값도 유지됩니다.
  • frame.diagnostics는 missing_font, missing_image, unsupported_feature처럼 사용자에게 보여 줄 만한 알림을 나열합니다.
  • source.reader()는 같은 디코딩 데이터를 공유하면서 위치를 따로 갖는 리더를 만듭니다. 한 애니메이션을 서로 멀리 떨어진 시간에서 읽는 두 클립에 씁니다. source.release()로 해제합니다.

이미지 시퀀스는 자체 바이트를 받지 않습니다. 시퀀스의 프레임은 SVG가 참조하는 이미지와 마찬가지로 로케이터로 등록하는 리소스입니다. withResources는 호출 한 번에 필요한 것만 정확히 등록하고 끝나면 해제하므로, 시퀀스는 한 번에 한 프레임씩만 모듈에 들어갑니다.

const provider = project.sources.find((source) => source.id === "render").provider;
const read = (locator) => bytesOf(`/media/render/${locator}`);
const sequence = await extensions.withResources(
read,
() => extensions.openNeeds(provider),
() => extensions.open(provider),
);
const frame = await extensions.withResources(
read,
() => sequence.needs(ticks),
() => sequence.render(ticks, { width, height }),
);

리소스를 직접 관리하려면 registerResource(locator, bytes)와 releaseResource(locator)를 씁니다. 두 방식 중 하나만 쓰세요. withResources는 직접 등록한 로케이터도 해제합니다.

extensions.probe(bytes)는 프로바이더들에게 파일을 읽을 수 있는지 묻습니다. { provider, report }를 반환하고, 그 바이트를 맡는 프로바이더가 없으면 null을 반환하므로 그때는 여러분의 디코더로 읽으면 됩니다.

const found = extensions.probe(bytes);
if (found) {
const { format, width, height, timing, loop_count, items, color } = found.report;
// timing: { kind: "static" }, { kind: "frames", durations } 또는 { kind: "rate", rate, frames }
}

report.items는 dotLottie 패키지의 애니메이션 목록입니다. 그중 하나를 provider.animation으로 넘기세요.

모듈은 Rust 코드를 실행하므로, 손상된 파일을 만나면 중단될 수 있습니다. ExtensionModuleError는 호출 도중 모듈이 중단되었다는 뜻입니다. 모듈은 이미 재시작했으므로 이후 호출은 동작하지만, 관련된 소스는 실패 상태로 남습니다. StaleExtensionSourceError는 재시작 전에 열린 소스라는 뜻입니다. 리소스를 다시 등록하고 소스를 다시 여세요.

  • 브라우저: 프레임 공급과 업로드 메서드.
  • CLI: alpha2-host export와 플래그.
  • 색상과 HDR: OpenEXR의 선형 광이 작업 색 공간에 들어오는 방식.