콘텐츠로 이동

브라우저 (alpha2)

This content is for v0.4. Switch to the latest version for up-to-date documentation.

alpha2 패키지는 WebAssembly로 컴파일한 엔진과, 이를 구동하는 TypeScript 코드로 이루어져 있습니다. 이 페이지를 읽고 나면 페이지나 워커에서 엔진을 로드하고, 캔버스에 프로젝트를 렌더링하고, 애플리케이션이 디코딩한 프레임과 오디오를 엔진에 넘기고, 결과를 MP4, MOV, MKV, WebM 파일로 export할 수 있습니다.

엔진은 미디어 파일을 직접 열지 않습니다. 애플리케이션이 비디오, 이미지, 오디오를 디코딩해 업로드하면, 엔진은 프로젝트가 보여 주는 내용을 렌더링해 프레임과 믹싱된 오디오를 돌려줍니다. 가져오기, 실행 취소, export 브리지까지 갖춘 완성형 에디터가 필요하다면 이 패키지 위에 만들어진 @alpha2-sh/editor를 사용하세요.

터미널 창
npm install alpha2

mediabunny는 선택적 의존성입니다. alpha2/mediabunny의 디코더와 MP4, MOV, MKV, WebM 라이터를 제공합니다.

alpha2/wasm은 init(default export)과 엔진 객체인 WebEffectRegistry를 내보냅니다. init()은 모듈과 같은 위치에서 alpha2_wasm_bg.wasm을 로드합니다.

import init, { WebEffectRegistry } from "alpha2/wasm";
await init();

번들러가 .wasm 파일을 다른 곳으로 옮긴다면, 그 URL이나 Response, 바이트, 컴파일된 WebAssembly.Module을 module_or_path로 넘기세요. 패키지는 세 가지 바이너리를 내보냅니다.

파일 내용 로드 시점
alpha2/wasm/alpha2_wasm_bg.wasm 엔진 init()
alpha2/wasm/alpha2_audio_worklet.wasm 오디오 워크릿의 DSP 오디오 재생을 시작할 때
alpha2/wasm/alpha2_extensions_bg.wasm SVG, 애니메이션 이미지, Lottie, OpenEXR, 이미지 시퀀스 해당 소스가 있을 때만(확장)

예를 들어 Vite에서는 ?url로 URL을 import합니다.

import init from "alpha2/wasm";
import engineUrl from "alpha2/wasm/alpha2_wasm_bg.wasm?url";
await init({ module_or_path: engineUrl });

렌더러는 캔버스에 바인딩된 WebEffectRegistry입니다. 캔버스가 어디에 있는지에 따라 생성자를 고릅니다.

생성자 캔버스 그리는 방식
WebEffectRegistry.createForHtmlCanvas(canvas) 페이지의 HTMLCanvasElement WebGPU, 없으면 WebGL2
WebEffectRegistry.createForCanvas(canvas) OffscreenCanvas(보통 워커 안) WebGPU, 없으면 WebGL2
WebEffectRegistry.createCpu(canvas) OffscreenCanvas, 페이지 캔버스, 또는 undefined 엔진의 CPU 렌더러
const canvas = document.querySelector("canvas");
const engine = await WebEffectRegistry.createForHtmlCanvas(canvas);
console.log(engine.backend); // "webgpu" 또는 "webgl2"

브라우저에 WebGPU도 WebGL2도 없을 때(하드웨어 가속을 끈 Chrome, 차단 목록에 오른 GPU 등) createCpu는 엔진의 CPU 렌더러를 만듭니다. 같은 호출을 받으며, 단일 스레드의 WebAssembly에서 그린 뒤 캔버스의 2D 컨텍스트에 프리뷰를 표시합니다. 네이티브 CPU 렌더러와 정확히 같은 결과를 그리고, export에는 캔버스가 필요 없으며, 런타임 WGSL 이펙트는 EFFECT_ERROR로 거부합니다.

alpha2/robustness의 detectBrowserCapabilities()는 렌더러를 만들지 않고 브라우저를 조사합니다. renderer 필드는 렌더 워커의 renderer: "auto"가 사용할 렌더러로, 브라우저에 WebGPU나 WebGL2가 있으면 "gpu", 없으면 "cpu"입니다. wasm은 엔진을 로드할 수 있는지 여부를 알려 주므로, 엔진을 내려받기 전에 미리 확인할 수 있습니다.

import { detectBrowserCapabilities } from "alpha2/robustness";
const caps = await detectBrowserCapabilities();
// { webgpu, webgl2, renderer, webcodecs, offscreenCanvas, wasm,
// diagnostics: ["webgpu-unavailable", ...] }

프로젝트는 엔진의 JSON 문서입니다(프로젝트 모델 참고). 직접 작성해도 되고, alpha2/builder의 ProjectBuilder를 써도 됩니다. ProjectBuilder는 시간을 초 단위로 받고 id를 자동으로 생성합니다.

import { ProjectBuilder } from "alpha2/builder";
const builder = new ProjectBuilder({ name: "Tutorial project", width: 1280, height: 720, frameRate: 30 });
const image = builder.source("image", "asset://image-1", { id: "image-1" });
builder.videoTrack({ name: "Main video" }).clip({ source: image, at: 0, duration: 1 });
const project = builder.build(); // 모든 문제를 나열한 RangeError를 던집니다
  • new ProjectBuilder({ name, width, height, frameRate, id?, sampleRate?, background?, ids? }). frameRate는 숫자이거나 { num: 30000, den: 1001 } 같은 정확한 유리수입니다. sampleRate의 기본값은 48000, id의 기본값은 "project"입니다.
  • source(kind, locator, { id?, hasAudio? })는 video, audio, image 소스를 추가합니다. generated(generator)는 제너레이터를, composition(nodes, output)은 컴포지션 그래프를 추가합니다.
  • videoTrack(), audioTrack(), captionTrack()은 트랙 빌더를 반환합니다. clip({ source, at, duration, sourceIn?, sourceDuration?, effects?, with? })는 클립을 배치합니다. effects는 컴포지션을 통해 이펙트를 연결하고, with는 그 밖의 클립 필드를 설정합니다. transition(fromClipId, toClipId, effect, easing?)은 겹치는 두 클립을 트랜지션으로 잇습니다.
  • 빌더가 초를 받는 곳이면 어디서든 ticks(n)으로 틱(tick) 단위의 정확한 시간을 지정할 수 있습니다.

alpha2/project의 validateProject(project)는 아무것도 로드하지 않고, 엔진이 거부할 문제를 문자열 목록으로 반환합니다. validateProjectJson(text)는 JSON 텍스트에 대해 같은 일을 합니다. alpha2/project에는 문서의 TypeScript 타입, 한도, 틱 변환 함수(secondsToTicks, ticksToSeconds, samplesToTicks, ticksToSamples)도 들어 있습니다. 검증과 오류를 참고하세요.

import { validateProject } from "alpha2/project";
const problems = validateProject(project);
if (problems.length > 0) throw new Error(problems.join("\n"));
engine.loadProject(JSON.stringify(project)); // 다시 검사하고, 거부한 내용을 던집니다

loadProject는 id와 locator가 바뀌지 않은 소스에 대해 업로드된 프레임과 오디오를 유지하므로, 편집할 때 다시 디코딩할 필요가 없습니다. locator는 그대로인데 미디어가 바뀌었다면 새 소스 id가 필요합니다.

렌더러는 기본적으로 프로젝트 크기의 절반으로 프리뷰합니다. 로드한 뒤 캔버스를 previewSize에 맞추고 렌더러에 알려 주세요.

const [width, height] = engine.previewSize;
canvas.width = width;
canvas.height = height;
engine.resizeSurface(width, height);

engine.setPreviewScale(scale)로 배율을 바꿀 수 있습니다(0 초과, 최대 8). 1이면 프리뷰의 텍스트와 이펙트가 export 결과와 픽셀 단위로 일치합니다. 배율을 바꾼 뒤에는 서피스를 새 previewSize에 맞게 조정하세요.

  1. 엔진을 로드하고 렌더러를 만듭니다.
  2. 프로젝트를 로드합니다.
  3. 해당 시간에 필요한 소스마다 프레임을 업로드합니다. 정지 이미지는 한 번만 업로드하면 됩니다: premultiplied sRGB RGBA8 픽셀.
  4. 틱 단위 시간으로 renderAt을 호출합니다.
import init, { WebEffectRegistry } from "alpha2/wasm";
import { ProjectBuilder } from "alpha2/builder";
await init();
const canvas = document.querySelector("canvas");
const engine = await WebEffectRegistry.createForHtmlCanvas(canvas);
const builder = new ProjectBuilder({ name: "Tutorial project", width: 1280, height: 720, frameRate: 30 });
const image = builder.source("image", "asset://image-1", { id: "image-1" });
builder.videoTrack({ name: "Main video" }).clip({ source: image, at: 0, duration: 1 });
engine.loadProject(JSON.stringify(builder.build()));
const [width, height] = engine.previewSize;
canvas.width = width;
canvas.height = height;
engine.resizeSurface(width, height);
// 프로젝트 크기의 불투명한 흰색 이미지 하나(premultiplied sRGB RGBA8).
engine.uploadRgbaImage("image-1", 1280, 720, new Uint8Array(1280 * 720 * 4).fill(255));
const result = JSON.parse(engine.renderAt(0n)); // 시간 0의 프레임을 그립니다
console.log(result.missingSources); // []

자막, 텍스트 제너레이터, 레이아웃의 텍스트는 등록한 폰트로 그려집니다. 페이스 하나는 engine.addFont(family, bytes, faceIndex)로, 파일의 모든 페이스는 engine.addFontFile(family, bytes)로 등록합니다. 네이티브 export에도 같은 폰트 파일을 주면 프리뷰 텍스트와 export된 텍스트가 일치합니다.

엔진은 어떤 시간에 각 소스의 어느 프레임이 필요한지 결정하고, 여러분은 그 프레임을 디코딩해 업로드합니다. 이것이 프레임 공급(frame-supply) 모델입니다.

renderAt(ticks)는 JSON을 반환합니다.

{
"requestedTimeTicks": 0,
"presentedTimeTicks": null,
"missingSources": ["video-1"],
"missingFrames": [{ "clipId": "clip-1", "sourceId": "video-1", "mediaTimeTicks": "48000" }],
"repeated": false,
"unchanged": false
}
  • missingSources는 프레임이 없었던 소스의 이름입니다. missingFrames는 각 소스가 필요했던 정확한 소스 시간을 알려 줍니다. 트리밍하거나 속도를 바꾼 클립에서는 이 값이 타임라인 시간과 다릅니다.
  • presentedTimeTicks는 프레임이 한 번 표시되기 전까지 null입니다. repeated는 이전 화면을 다시 보여 주었다는 뜻입니다. unchanged는 프레임이 직전 프레임과 같아서 아무것도 렌더링하지 않았다는 뜻입니다.

frameNeeds(ticks)는 렌더링하기 전에 그 시간에 필요한 것을 나열합니다. renderAt과 같은 스냅숏에서 만든 JSON입니다.

const needs = JSON.parse(engine.frameNeeds(48_000n));
// [{ clipId: "clip-1", readerId: null, sourceId: "video-1", mediaTimeTicks: "48000" }]

틱 값은 64비트 범위를 모두 담을 수 있도록 10진수 문자열로 표현됩니다. 주어진 값을 그대로 사용하세요. 소스 시간을 직접 계산하면 안 됩니다(예: timelineSeconds - clip.start를 반올림하는 식). 디코더 커서는 클립마다 하나씩 유지합니다(readerId, 즉 소스를 직접 읽는 컴포지션 단계마다도 하나씩).

메서드 용도
uploadVideoFrame(sourceId, mediaTimeTicks, frame) 디코딩된 VideoFrame. 엔진이 소유권을 가져가 닫습니다. 프로젝트 크기에 맞춰 바로 세워 배치합니다. HDR 프레임은 거부합니다.
uploadRgbaFrame(sourceId, mediaTimeTicks, width, height, pixels) 특정 소스 시간의, 빈틈없이 채워진 premultiplied RGBA8.
uploadRgbaImage(sourceId, width, height, pixels) 모든 시간에 표시되는 정지 이미지.
uploadPresentedRgbaFrame / uploadPresentedRgbaImage 자체 크기의 RGBA8. 엔진이 모든 소스를 배치하는 방식대로 출력에 배치합니다. premultiplied 플래그와 출력 크기를 받습니다.
uploadYuvFrame(...) VideoFrame.copyTo가 쓰는 형식의 planar YCbCr 샘플. HDR(PQ 또는 HLG) 프레임은 이 방법으로 엔진에 전달합니다.

업로드한 다음 같은 시간을 다시 렌더링합니다. 메모리는 releaseFrame, 재생이 진행됨에 따라 trimFramesBefore(sourceId, ticks), 또는 clearFrames로 해제합니다. setUploadedFrameBudget(bytes)는 렌더러가 보관하는 양의 상한을 정합니다(기본값 1 GiB).

alpha2/frame-supply의 FrameSupply는 이 루프를 대신 실행합니다. frameNeeds를 읽고, 클립 커서마다 디코더를 한 번씩 호출하고, 결과를 업로드합니다. 같은 디코딩 프레임으로 귀결되는 요청들은 업로드 하나를 공유하며, 폐기된 업로드는 바이트 예산 안에서 보관합니다.

import { FrameSupply } from "alpha2/frame-supply";
const supply = new FrameSupply(engine); // { maxRetiredBytes, maxCachedImageBytes, maxDecodeConcurrency, ... }
const layers = new Map([["image-1", { id: "image-1", kind: "image" }]]);
const presented = await supply.render(0n, {
width: 1280, // 프로젝트 크기
height: 720,
liveSourceIds: new Set(layers.keys()),
getLayer: (id) => layers.get(id),
// 정지 이미지는 프로젝트 크기의 RGBA8로, 비디오는 { kind: "video-frame", frame }으로 resolve합니다.
pixelsFor: async (layer, seconds, options) => decode(layer, seconds, options),
isCurrent: () => true, // 탐색이나 편집으로 이 요청이 무효해지면 false를 반환합니다
});

render는 공급, 표시, 폐기를 한 번의 호출로 처리하며, 소스가 여전히 없으면 reject됩니다. supply는 업로드만 하므로 렌더링을 직접 하는 호스트에 적합합니다. 렌더링이 끝나면 uploaded 영수증을 retire에 넘기세요. 기본값은 폐기된 프레임 256 MiB, 캐시된 정지 이미지 256 MiB, 동시 디코딩 2개입니다.

alpha2/media는 디코더 계약(BrowserVideoDecoder: refill, flush, close)과 이를 구현한 두 가지를 정의합니다.

  • WebCodecsVideoDecoder(sourceId, source, options?)는 어떤 디먹서에서 나온 elementary 청크든 디코딩합니다. source는 VideoDecoderConfig와 chunksFrom(mediaTimeTicks, signal)을 제공하는데, 이는 지정한 시간 또는 그 이전의 키프레임부터 디코딩 순서대로 청크를 내놓습니다.
  • alpha2/mediabunny의 MediabunnyVideoDecoder.open(sourceId, blobOrBytes, options?)는 mediabunny로 파일의 디먹싱까지 처리합니다. 역재생 클립은 프레임 구간을 디코딩해 재생하고, 하드웨어 디코더가 실패하면 한 번에 한해 소프트웨어 디코더로 교체합니다.

리필 요청에는 소스, 정확한 소스 시간, 이유(prefetch, seek, evicted, device-recovery), 세대(generation), 커서를 소유한 소비자가 들어 있습니다.

import { decoderConsumerId } from "alpha2/media";
import { MediabunnyVideoDecoder } from "alpha2/mediabunny";
const decoder = await MediabunnyVideoDecoder.open("video-1", file); // File 또는 Blob
const [need] = JSON.parse(engine.frameNeeds(48_000n));
const decoded = await decoder.refill({
sourceId: need.sourceId,
mediaTimeTicks: BigInt(need.mediaTimeTicks),
reason: "seek",
generation: 0,
consumerId: decoderConsumerId(need.clipId, need.readerId),
});
engine.uploadVideoFrame(decoded.sourceId, decoded.requestedTimeTicks, decoded.frame);
engine.renderAt(48_000n);

디코더 옵션은 바이트와 프레임 한도입니다: maxCachedFrameBytes와 maxReadyFrameBytes(각 64 MiB), maxCachedFrames(2, 최솟값), stallTimeoutMs(5000).

DecoderRefillCoordinator(open, deliver, options?)는 여러 소스에 걸친 리필을 관리합니다. 소스와 소비자 조합마다 디코더를 하나씩 열고, 소스당 한 번에 하나, 전체로는 최대 maxConcurrency(2)개의 리필을 실행합니다. beginGeneration()은 탐색 이전의 요청을 중단합니다.

export는 프로젝트의 정확한 프레임 그리드에 맞춰 export 품질로 오프스크린 렌더링하며, 캔버스는 건드리지 않습니다. 각 프레임은 행 패딩이 있는 planar I420(BT.709, limited range)으로 돌아옵니다.

beginExport(startTicks, endTicks, rateNum, rateDen, codec)는 반열린 구간에 대해 export를 시작합니다. codec(h264, hevc, vp9, av1)은 크로마 샘플을 해당 코덱이 기대하는 위치에 배치하는 데만 쓰입니다. 그다음 beginExportFrame()으로 프레임을 요청하면 티켓이 반환됩니다.

import { videoFrameFromI420 } from "alpha2/video-frames";
engine.beginExport(0n, 120_000n, 30, 1, "h264"); // [0, 1초) 구간, 30/1 fps
for (;;) {
const ticket = engine.beginExportFrame();
if (ticket.kind === "complete") break;
if (ticket.kind === "waiting") {
// ticket.missingFrames: 이 프레임들을 디코딩해 업로드한 뒤 다시 요청합니다.
throw new Error(`upload ${ticket.missingSources} first`);
}
const frame = await ticket.readback;
const video = await videoFrameFromI420(frame.pixels, {
format: "I420",
codedWidth: frame.width,
codedHeight: frame.height,
layout: frame.layout,
colorSpace: frame.colorSpace,
timestamp: Math.round(Number(frame.timeTicks) / 0.12), // 틱을 마이크로초로 변환
});
video.close(); // 또는 VideoEncoder에 넘깁니다
}
engine.endExport();
  • waiting 티켓은 다음 export 프레임에 필요한 프레임을 알려 줍니다. 그 프레임들을 업로드하고 beginExportFrame을 다시 호출하면 같은 프레임부터 export가 이어집니다.
  • 리드백이 resolve되기 전에 beginExportFrame을 다시 호출할 수 있습니다. 그러면 직전 프레임을 읽어 오는 동안 GPU가 다음 프레임을 렌더링합니다.
  • exportProgress()는 완료된 프레임 수와 전체 프레임 수, 프레임 그리드에 맞춰 넓힌 구간(effectiveRange), export가 다루는 오디오 구간을 알려 줍니다.
  • videoFrameFromI420는 플레인 위에 VideoFrame을 만들며, 브라우저가 layout을 무시하는 경우에는 플레인을 다시 패킹합니다.

alpha2/exporter의 exportToSink(exporter, sink, refill, options?)는 이 루프를 인코더 싱크로 실행하며, 처리 중인 프레임을 최대 maxInFlightFrames(3)개, 리드백을 최대 maxInFlightBytes(128 MiB)까지 유지합니다. alpha2/mediabunny의 MediabunnySink는 MP4, MOV, MKV, WebM을 쓰는 WebCodecs 싱크입니다.

exporter는 렌더 백엔드와 통신합니다. 렌더 워커를 쓴다면 WorkerExportBackend가 그 백엔드입니다.

import { Alpha2Exporter, exportToSink } from "alpha2/exporter";
import { WorkerExportBackend } from "alpha2/worker";
import { decoderConsumerId } from "alpha2/media";
import { canEncodeVideoTrack, MediabunnySink, MediabunnyVideoDecoder } from "alpha2/mediabunny";
// `backend`는 초기화된 WorkerRenderBackend이고, `file`에는 소스 "video-1"이 들어 있습니다.
const decoder = await MediabunnyVideoDecoder.open("video-1", file);
const rateControl = { bitrate: 8_000_000, bitrateMode: "variable" };
const encoding = await canEncodeVideoTrack("avc", { width: 1280, height: 720, rate: rateControl });
if (encoding === null) throw new Error("this browser has no H.264 encoder");
const exporter = new Alpha2Exporter(
new WorkerExportBackend(backend, "h264"),
{ num: 30n, den: 1n },
0n,
240_000n,
);
const sink = new MediabunnySink({
container: "mp4",
videoCodec: "avc",
videoRateControl: rateControl,
...(encoding.codecString === undefined ? {} : { videoCodecString: encoding.codecString }),
frameRate: { num: 30, den: 1 },
destination: { kind: "memory", maxBytes: 256 * 1024 * 1024 },
});
const result = await exportToSink(exporter, sink, async (waiting) => {
for (const { clipId, readerId, sourceId, mediaTimeTicks } of waiting.missingFrames) {
const decoded = await decoder.refill({
sourceId, mediaTimeTicks, reason: "prefetch", generation: 0,
consumerId: decoderConsumerId(clipId, readerId),
});
await backend.uploadVideoFrame(decoded);
}
});
console.log(result.frames, result.fps);
const output = sink.output; // { kind: "memory", buffer: ArrayBuffer }
  • options.signal을 중단하면 코드가 cancelled인 ExportAbortedError로 reject되고, GPU를 잃으면 코드가 device-lost인 같은 오류로 reject됩니다. 어느 경우든 sink.abort는 한 번 실행됩니다.
  • 리필을 네 번 해도 여전히 대기 중인 프레임은 RESOURCE_LIMIT로 실패합니다.
  • destination: { kind: "stream", writable }은 먹싱하면서 탐색 가능한 스트림으로 기록하므로, 메모리에 담기에는 너무 큰 출력에 사용합니다.
  • alpha2/rate-control의 resolveVideoRateControl과 resolveAudioBitrate는 품질 수준(very-low부터 very-high까지)을 네이티브 export와 같은 수치로 변환합니다. alpha2/containers는 컨테이너별로 사용할 수 있는 코덱을 나열합니다.

엔진은 프로젝트의 오디오를 믹싱합니다: 클립 게인, 팬과 페이드, 이펙트, 트랙 뮤트와 솔로, 더킹, 마스터 버스(오디오 참고). 각 소스의 디코딩된 오디오를 프로젝트의 샘플레이트로 리샘플링해 업로드하고, 믹싱된 블록을 요청합니다.

// 프로젝트 레이트의 planar float 샘플. 플레인을 이어 붙입니다.
engine.uploadAudioBlock(
"music", // 소스 id
0n, // 소스에서 첫 샘플의 인덱스(프로젝트 레이트 기준)
48_000, // 샘플레이트
2, // 채널 수, 또는 ["FL", "FR"] 같은 FFmpeg 채널 이름
samples, // Float32Array
);
const block = engine.renderAudio(0n, 1024, true); // planar 출력, 채널을 이어 붙임
const missing = JSON.parse(engine.audioMissingSources()); // 블록에 빠진 소스
  • audioNeeds(startSample, frames)는 구간을 믹싱하기 전에 디코딩해야 할 소스 구간을 나열합니다. 미리 읽기 구간과, 탐색 후 이펙트가 다시 재생하는 이력 구간도 포함합니다.
  • 블록 하나는 채널당 최대 audioMaxBlock 샘플입니다. 엔진에 없는 오디오가 필요한 블록은 빈 구간을 섞어 믹싱하지 않고 audioMissingSources로 보고합니다.
  • alpha2/mediabunny의 MediabunnyAudioDecoder.open(sourceId, file, wasm)은 소스를 디코딩하고 엔진의 AudioResampler로 리샘플링합니다(wasm은 import * as wasm으로 가져온 alpha2/wasm 모듈). refill은 startSample, sampleRate, speakers, channels를 담은 블록을 반환하므로 바로 uploadAudioBlock에 넘길 수 있습니다.
  • trimAudioBefore(sourceId, ticks)는 재생이 지나간 오디오를 버립니다.

실시간 프리뷰에서는 믹싱된 블록이 공유 메모리의 링 버퍼를 통해 오디오 스레드로 전달됩니다.

  • alpha2/audio-timeline에는 AudioRingBuffer(링 버퍼), AudioPump(믹서로 링을 계속 채움), AudioPlaybackClock(오디오 스레드가 재생한 샘플 수를 셈)이 있습니다. 애니메이션 프레임마다 clock.timelineTicks를 표시하면 화면이 오디오 장치를 어긋남 없이 따라갑니다.
  • alpha2/audio-worklet에는 프로세서 모듈을 추가하고 DSP를 시작하는 Alpha2AudioWorkletHost.create(context, wasm, options)와, 노드가 링을 가리키게 하는 useAudioRing(node, ring.transfer)가 있습니다. loadAlpha2AudioWorkletWasm()은 패키지에 포함된 alpha2_audio_worklet.wasm을 가져옵니다.

alpha2/loudness는 라우드니스를 측정하고(ITU-R BS.1770-4, EBU R128) 노멀라이즈 계획을 세웁니다. alpha2/waveform은 파형 요약을 계산하고, alpha2/audio의 renderOfflineAudio는 export를 위해 구간을 싱크로 렌더링합니다.

alpha2/worker는 GPU 디바이스와 OffscreenCanvas를 소유한 전용 워커에서 엔진을 실행하므로, 렌더링이 페이지를 막지 않습니다. 워커는 시작할 때 프로젝트 하나를 로드합니다.

render-worker.js
import init, { WebEffectRegistry } from "alpha2/wasm";
import { createWasmRenderWorkerHost, installRenderWorker } from "alpha2/worker";
await init();
const project = await (await fetch("/project.json")).text();
installRenderWorker(
self,
createWasmRenderWorkerHost(WebEffectRegistry, project, {
renderer: "auto", // "auto"(기본값), "gpu", "cpu"
// fonts: [{ family: "Hanken Grotesk", bytes }],
}),
);

페이지에서는 WorkerRenderBackend가 워커를 구동하고, alpha2/player의 Alpha2Player가 이를 통해 시간을 표시합니다.

import { Alpha2Player } from "alpha2/player";
import { WorkerRenderBackend } from "alpha2/worker";
const worker = new Worker(new URL("./render-worker.js", import.meta.url), { type: "module" });
const player = new Alpha2Player(new WorkerRenderBackend(worker), async (requests) => {
// 각 DecoderRefillRequest를 디코딩해 player.uploadVideoFrame(decoded)로 업로드합니다.
});
const canvas = document.querySelector("canvas");
canvas.width = 1280;
canvas.height = 720;
await player.initialize(canvas.transferControlToOffscreen());
await player.seek(0n);
await player.present(4_000n);
  • new Alpha2Player(backend, onRefill?). 플레이어는 자체 클럭이 없습니다. present(ticks)는 주어진 시간을 그대로 표시합니다. 표시 요청을 순서대로 처리하고, 표시에 필요한 프레임이 없으면 onRefill에 요청한 뒤, 프레임이 도착하면 같은 시간을 다시 표시합니다. seek는 이전 요청을 위해 디코딩된 프레임을 버립니다.
  • 워커의 renderer 옵션은 "auto"(WebGPU, 없으면 WebGL2, 브라우저가 둘 다 제공하지 않으면 CPU 렌더러), "gpu"(전환하지 않음), "cpu" 중 하나입니다. WorkerRenderBackend.rendererChoice는 네이티브 호스트의 renderer, rendererReason처럼 워커가 어떤 렌더러를 만들었는지, CPU라면 그 이유가 무엇인지 알려 줍니다.
  • 디바이스를 잃으면 플레이어는 다음 표시 때 디바이스를 다시 만듭니다.

alpha2/edit에는 타임라인 편집 기능이, 유효한 프로젝트 하나를 다른 유효한 프로젝트로 바꾸는 순수 함수로 들어 있습니다. 각 함수는 { ok: true, project } 또는 { ok: false, error }를 반환하며 입력을 변경하지 않습니다. 시간은 정수 틱입니다.

import { split, rippleDelete, trim } from "alpha2/edit";
const result = split(project, "clip-1", 60_000, "clip-1b");
if (!result.ok) throw new Error(`${result.error.code}: ${result.error.message}`);
engine.loadProject(JSON.stringify(result.project));

제공하는 연산은 split, trim, rippleTrim, rippleDelete, rippleDeleteSpan, rollTrim, slip, slide, retime, insert, overwrite, append, move, moveToTrack, moveClips입니다. 리플 편집에 syncTracks를 넘기면 동기화 잠금된 트랙의 내용도 함께 이동합니다. deferValidation(run)은 여러 편집을 묶어 전체를 한 번에 검사합니다. 실행 취소, 선택, 스냅은 애플리케이션이나 에디터가 담당합니다.

Import 내용
alpha2/wasm 엔진: init, WebEffectRegistry(렌더링, export, 오디오 믹싱), 레이트 컨트롤·자막·라우드니스·파형 함수.
alpha2/project 프로젝트 문서의 타입과 한도, 틱 변환, validateProject.
alpha2/builder ProjectBuilder: 초 단위로 문서를 만들고 id를 자동 생성합니다.
alpha2/edit split, trim, ripple, roll, slip, slide, retime, insert, overwrite, append, move.
alpha2/captions SRT와 WebVTT를 자막 트랙으로 가져오기.
alpha2/geometry 에디터용 셰이프 지오메트리: 경계, 히트 테스트, 최근접점, 패스 길이, 트림, 리샘플링, 모션 패스.
alpha2/layout 레이아웃 제너레이터: measureLayout, CSS 형식의 속성으로 만드는 빌더, flexbox와 grid. 레이아웃 참고.
alpha2/effects 이펙트 정의, 내장 카탈로그의 타입, 런타임에 등록하는 WGSL 이펙트를 위한 RuntimeEffectRegistry.
alpha2/extensions, alpha2/extensions-wasm SVG, 애니메이션 이미지, Lottie, OpenEXR, 이미지 시퀀스 소스(loadExtensions).
alpha2/text WasmTextRasterizer(엔진의 것으로, 네이티브 export와 같은 픽셀)와 OffscreenTextRasterizer(브라우저의 것).
alpha2/assets 미디어 식별, 프록시, conform 보고서.
alpha2/frame-supply FrameSupply.
alpha2/media 디코더 계약, WebCodecsVideoDecoder, DecoderRefillCoordinator.
alpha2/mediabunny mediabunny 기반 디코더와 MP4, MOV, MKV, WebM 싱크.
alpha2/hdr-frames, alpha2/video-frames planar 및 HDR VideoFrame 업로드, export된 I420 플레인으로 VideoFrame 만들기.
alpha2/player, alpha2/worker Alpha2Player와 렌더 워커.
alpha2/exporter Alpha2Exporter와 exportToSink.
alpha2/export-destination export 바이트를 보낼 곳: 파일 또는 메모리.
alpha2/export-failure, alpha2/export-retry export 실패의 분류 방식과, 이를 바탕으로 한 재시도 정책.
alpha2/containers export 컨테이너와 각 컨테이너가 제공하는 코덱.
alpha2/rate-control 엔진 품질 수준별 출력 비트레이트.
alpha2/audio, alpha2/audio-worker 싱크로의 오프라인 오디오 렌더링(워커 안팎 모두).
alpha2/audio-timeline, alpha2/audio-worklet, alpha2/audio-worklet-processor 실시간 오디오: 공유 링 버퍼, 펌프, AudioWorklet과 그 프로세서 모듈.
alpha2/loudness, alpha2/waveform 라우드니스 측정과 노멀라이즈, 파형 요약.
alpha2/robustness detectBrowserCapabilities와 성능 저하 보고서.
alpha2 위 대부분을 한 번에 import: assets, audio, audio-timeline, audio-worker, audio-worklet, containers, effects, exporter, extensions, frame-supply, hdr-frames, loudness, media, player, project, rate-control, robustness, text, waveform, worker.

각 모듈의 TypeScript 선언에 전체 API가 문서화되어 있습니다.

  • React: Alpha2Player를 감싼 캔버스 컴포넌트.
  • 에디터: 문서, 커맨드, 실행 취소, 바로 쓸 수 있는 프리뷰·export 브리지.
  • 확장: 브라우저에서 SVG, Lottie, 애니메이션 이미지, OpenEXR 사용하기.
  • 시간과 틱, 결정론: 브라우저와 네이티브 export가 같은 프레임을 그리는 이유.