Node.js와 Electron
@alpha2-sh/node는 네이티브 alpha2 렌더러인 alpha2-host의 Node.js 클라이언트입니다. 이 페이지를 읽고 나면 호스트를 설치하고, 진행 상황 표시와 취소를 지원하며 프로젝트를 동영상 파일로 export하고, 정지 프레임을 렌더링하고, 머신에서 어떤 형식으로 인코딩할 수 있는지 확인하고, Electron 앱에 호스트를 포함해 배포할 수 있습니다.
호스트는 GPU에서 렌더링하고(GPU가 없으면 CPU에서), 디코딩과 인코딩에는 FFmpeg 8을 씁니다. 클라이언트는 호스트를 자식 프로세스로 띄우고 호스트 프로토콜로 통신합니다.
-
패키지를 추가합니다.
터미널 창 npm install @alpha2-sh/node터미널 창 pnpm add @alpha2-sh/node터미널 창 yarn add @alpha2-sh/node -
이 패키지의 버전과 플랫폼에 맞는
alpha2-host를 내려받습니다.터미널 창 npx @alpha2-sh/node install
install이 하는 일
섹션 제목: “install이 하는 일”패키지에는 alpha2-host가 들어 있지 않습니다. install 명령은 같은 버전의 GitHub 릴리스에서 해당 플랫폼의 bundled 아카이브를 가져와, 모든 alpha2 클라이언트가 함께 쓰는 캐시에 풉니다(Python 패키지도 같은 캐시를 씁니다).
ALPHA2_HOST_CACHE_DIR가 설정되어 있으면 그 디렉터리를 씁니다.- 그렇지 않으면 Linux에서는
$XDG_CACHE_HOME또는~/.cache, macOS에서는~/Library/Caches, Windows에서는%LOCALAPPDATA%아래의alpha2디렉터리를 씁니다.
--force를 주면 다시 내려받습니다. 코드에서는 installHost(options)로 같은 작업을 할 수 있으며, cacheDirectory, force, downloadUrl, onProgress, signal 옵션을 받고 { hostPath, directory, downloaded, verifiedBy }로 resolve됩니다.
SHA-256 고정
섹션 제목: “SHA-256 고정”배포된 패키지에는 해당 버전의 모든 호스트 아카이브의 SHA-256이 들어 있습니다(dist/host-archives.json). 설치 프로그램은 내려받은 파일을 이 고정값과 대조하므로 미러가 아카이브를 바꿔치기할 수 없으며, checksums.sha256은 아예 가져오지 않습니다. 소스 체크아웃에는 고정값이 없으므로 대신 릴리스의 checksums.sha256과 대조합니다. 결과의 verifiedBy가 어느 쪽인지(release-pin 또는 checksums) 알려 줍니다.
설치에 실패하면 Alpha2HostInstallError로 reject되며, 그 code는 UNSUPPORTED_PLATFORM, DOWNLOAD_FAILED, CHECKSUM_MISMATCH, EXTRACT_FAILED, ABORTED 중 하나입니다.
클라이언트가 호스트를 찾는 위치
섹션 제목: “클라이언트가 호스트를 찾는 위치”클라이언트는 alpha2-host(Windows에서는 alpha2-host.exe)를 다음 순서로 찾습니다.
hostPath옵션ALPHA2_HOST_PATH환경 변수(상대 경로는cwd옵션 기준, 없으면 작업 디렉터리 기준)- Electron에서는
process.resourcesPath/bin/, 그다음process.resourcesPath/ - 패키지 자체의
bin/디렉터리 - 캐시에 있는 패키지 버전의 아카이브(
cacheDirectory옵션, 없으면 위에서 설명한 디렉터리) PATH에 있는 디렉터리
어디에서도 찾지 못하면 클라이언트는 코드가 HOST_NOT_FOUND인 Alpha2HostError를 던집니다. 시스템의 FFmpeg 8을 쓰려면 소스에서 호스트를 빌드하고(cargo install alpha2-host) ALPHA2_HOST_PATH를 설정합니다.
프로젝트 export하기
섹션 제목: “프로젝트 export하기”import { Alpha2NodeClient } from "@alpha2-sh/node";
const client = new Alpha2NodeClient(); // `npx @alpha2-sh/node install`이 캐시에 넣은 호스트를 찾습니다const output = await client.exportProject({ project, // alpha2 프로젝트 문서 assets: { "image-1": "/media/image.png" }, // 소스별 파일 startTicks: 0, endTicks: 120_000, // 틱 단위: 1초에 120,000틱 frameRate: { num: 30, den: 1 }, videoQuality: "high", output: "/exports/tutorial.mp4", onProgress: ({ phase, completed, total }) => console.log(phase, completed, total),});console.log("wrote", output);exportProject는 파일이 완성되면 출력 경로로 resolve됩니다. export마다 별도의 alpha2-host 프로세스에서 실행됩니다.
- 클라이언트는 프로세스를 띄우기 전에
alpha2/project의validateProject로 프로젝트를 검사합니다. 유효하지 않은 프로젝트는problems목록을 담은Alpha2ProjectValidationError로 거부됩니다. - 이미 존재하는
output은 요청에overwrite: true가 없으면 거부됩니다.overwrite: true를 주면 export가 모두 끝난 뒤에야 기존 파일을 교체합니다. 요청의 입력 파일 중 하나와 같은 출력 경로는 항상 거부됩니다. - 호스트는 출력 옆에
<stem>.a2partial.<ext>를 쓰고, 완성되면 제자리로 옮깁니다. 완성된 파일을 옮길 수 없으면 그 옆에 남겨 두며, 오류(OUTPUT_PUBLISH_FAILED)의keptPath가 그 파일을 가리킵니다. onResult는 호스트가 보고하는 export 정보를 받습니다. 프레임, 인코더, 레이트 컨트롤, 소요 시간, 각 소스가 읽은 미디어 등이 들어 있습니다.
alpha2/builder의 ProjectBuilder는 로케이터가 파일 경로인 프로젝트를 만들며, 그 assets()가 해당 파일들의 assets 맵을 돌려줍니다. 브라우저 가이드를 참고하세요.
요청 필드
섹션 제목: “요청 필드”ExportProjectRequest에 모든 필드의 타입이 정의되어 있습니다. 호스트 프로토콜에도 같은 필드가 기본값과 함께 정리되어 있습니다.
| 필드 | 기본값 | |
|---|---|---|
project, assets |
필수 | 문서와, 소스 id별 파일 경로. |
startTicks, endTicks |
필수 | 타임라인 범위. 프레임 그리드에 맞게 넓혀집니다. |
frameRate |
필수 | 출력 프레임 레이트, { num, den }. |
output |
필수 | .mp4, .m4v, .mov, .mkv, .webm. |
overwrite |
false |
export가 모두 끝나면 기존 파일을 교체합니다. |
container |
출력 확장자에 따름 | mp4, mov, mkv, webm. |
videoCodec |
컨테이너 기본값(h264, WebM은 vp9) |
h264, hevc, vp9, av1, mpeg4-part2, prores(MOV 전용). |
audioCodec |
컨테이너 기본값(aac, WebM은 opus) |
aac, opus. |
videoQuality |
high |
very-low부터 very-high까지, 또는 { bitrate, mode?, quantizer? }. |
audioQuality |
high |
레벨, 또는 { bitrate }. |
includeAudio |
true |
|
alpha |
false |
투명도를 유지합니다. WebM이나 Matroska의 8비트 VP9, 또는 MOV의 ProRes 4444. |
bitDepth |
ProRes는 10, 그 외는 8 | 10비트에는 HEVC, AV1, VP9, ProRes가 필요합니다. |
loudness |
없음 | { targetLufs, truePeakCeilingDbtp }. 오디오를 정규화합니다. |
fonts |
[] |
모든 텍스트에 쓰는 폰트 파일, 폴백 순서대로. 텍스트가 나오는 프로젝트는 폰트가 없으면 실패합니다. |
effects, effectAssets |
없음 | 런타임 이펙트 정의와, .cube LUT 같은 이펙트 리소스 파일. |
proxies, media |
없음, original |
소스별 프록시 파일. media: "proxies"로 프록시를 읽습니다. |
frames, resources |
없음 | 이미지 시퀀스 프레임과, SVG나 Lottie 소스가 참조하는 파일. 확장을 참고하세요. |
renderCache |
없음 | { directory, quotaBytes?, backlog? }. 이전 export에서 렌더링한 프레임을 재사용합니다. |
memory |
호스트 기본값 | 바이트 단위 예산(inFlightBytes, decodeQueueBytes, decoderSessionBytes, …). |
strategy |
render |
prefer-copy나 require-copy는 렌더링하지 않고 손대지 않은 소스 패킷을 복사합니다. |
inputPolicy |
auto |
gpu-required 또는 cpu-readback. 프레임이 인코더에 전달되는 방식입니다. |
videoEncoder |
호스트가 선택 | capabilities에 나오는 FFmpeg 인코더 이름. |
videoDecode, renderer |
클라이언트 설정 | 이 export에 한해 클라이언트 설정을 덮어씁니다. |
signal, onProgress, onResult |
취소와 콜백. |
진행 상황과 취소
섹션 제목: “진행 상황과 취소”onProgress는 { phase, completed, total }을 받습니다. phase는 프레임을 인코딩하는 동안 frames, 파일을 플러시하고 닫는 동안 finalizing입니다. export는 exportProject가 resolve되어야 완료된 것입니다. 콜백이 프로미스를 반환하면 그 프로미스를 기다리며, 콜백이 예외를 던지거나 프로미스가 reject되면 export가 PROGRESS_CALLBACK_FAILED로 실패합니다.
signal(AbortSignal)로 export를 취소합니다.
const controller = new AbortController();process.once("SIGINT", () => controller.abort());
try { await client.exportProject({ ...request, signal: controller.signal });} catch (error) { if (error.code === "ABORTED") console.log("cancelled"); else throw error;}클라이언트가 호스트에 cancel 줄을 쓰면, 호스트는 export를 멈추고 작성 중이던 파일을 지운 뒤 응답합니다. 그다음 프로세스가 종료되고 exportProject는 Alpha2AbortError로 reject됩니다. 5초 안에 응답하지 않는 호스트는 강제로 종료됩니다. 호스트도 자신을 띄운 프로세스를 지켜보다가, 그 프로세스가 죽으면 export를 취소하고 종료합니다.
정지 프레임
섹션 제목: “정지 프레임”client.still({ project, assets, time, output })은 동영상을 인코딩하지 않고, export가 그 시점에 그리는 프레임 하나를 PNG로 렌더링합니다. client.stills({ project, assets, stills: [{ time, output }, ...] })는 호스트 하나로 최대 256프레임을 렌더링하며, 디코딩은 한 번만 합니다. time은 틱 단위입니다.
const result = await client.still({ project, assets, time: 60_000, output: "/tmp/check.png" });console.log(result.stills[0].frameTime); // 0.5초를 포함하는 그리드 프레임, 틱 단위PNG는 8비트 RGBA이며, 동영상 인코딩을 거치기 전의 그림입니다. size: { width, height }를 주면 다른 캔버스 크기로 렌더링하고, alpha: true를 주면 프로젝트 배경 없이 그립니다.
머신에서 가능한 작업 확인하기
섹션 제목: “머신에서 가능한 작업 확인하기”client.getCapabilities()는 호스트가 디코딩, 렌더링, 인코딩할 수 있는 것을 보고합니다.
const { capabilities } = await client.getCapabilities();const { encode } = capabilities;if (encode.h264.status !== "available") console.warn(encode.h264.reason, encode.h264.remedy);console.log(encode.containers); // 컨테이너별로 제공하는 코덱videoCodec 없이 MP4, MOV, Matroska로 export하면 H.264가 쓰이며, H.264를 인코딩할 수 없는 호스트는 다른 코덱으로 쓰는 대신 export를 거부합니다. encode.h264.status가 이를 요약합니다. available(모든 export 가능), gpu-only(GPU 상주 경로를 타는 export만 가능), unavailable 중 하나이며, reason과 remedy가 함께 옵니다. 하드웨어 H.264 인코더가 없는 환경에서는 npx @alpha2-sh/node install --openh264로 Cisco의 OpenH264를 설치합니다(CLI 참고).
디코딩 모드와 렌더러 조합별로 첫 getCapabilities 호출이 호스트를 띄웁니다. 그 호스트가 실행되는 동안 들어온 호출은 이를 함께 쓰고, 그 응답은 이후 호출에도 재사용됩니다.
client.conform({ project, assets })는 렌더링하지 않고 GPU도 쓰지 않은 채 미디어를 검사합니다. { catalog, report }로 resolve되며, export를 막는 문제가 있으면 report.ready가 false입니다.
렌더러와 하드웨어 디코딩
섹션 제목: “렌더러와 하드웨어 디코딩”둘 다 클라이언트 옵션이며, export별로 덮어쓸 수도 있습니다.
renderer:auto,gpu,cpu. 지정하지 않으면 호스트가ALPHA2_RENDERER를 읽고, 그것도 없으면auto를 씁니다.auto는 하드웨어 GPU가 있으면 GPU에서, 없으면 CPU에서 렌더링하며, 소프트웨어 GPU 어댑터(lavapipe, llvmpipe, WARP, SwiftShader)는 쓰지 않습니다.gpu는 찾은 어댑터를 무엇이든 쓰고, 어댑터가 없으면GPU_INIT_FAILED로 실패합니다.cpu는 그래픽 드라이버를 열지 않습니다. 런타임 WGSL 이펙트는 CPU 렌더러에서 실행할 수 없으므로, 그런 export는EFFECT_ERROR로 실패합니다.videoDecode:software,auto,hardware-required. 지정하지 않으면 호스트가 정합니다. Linux와 macOS에서는auto, Windows에서는software입니다.auto에서 하드웨어 디코더가 멈춘 export는 소프트웨어 디코딩으로 한 번 더 실행합니다.hardware-required는 CPU에서 프레임을 디코딩하는 대신 실패합니다.
const client = new Alpha2NodeClient({ renderer: "auto", videoDecode: "auto", concurrency: 2 });결과와 capabilities에는 renderer(gpu 또는 cpu), rendererReason(호스트가 CPU에서 렌더링하는 이유), 그리고 CPU에서는 rendererThreads가 보고됩니다.
동시 실행
섹션 제목: “동시 실행”concurrency(기본값 1)는 클라이언트 하나가 동시에 실행하는 호스트 프로세스 수, 즉 GPU를 함께 쓰는 export 수입니다. 그보다 많은 요청은 자리가 날 때까지 기다립니다. 모듈 수준의 exportProject(request, options), still, stills, getCapabilities(options)는 호스트 바이너리와 concurrency 조합마다 클라이언트 하나를 공유합니다.
import { exportProject } from "@alpha2-sh/node";
await exportProject(request, { concurrency: 2 });모든 reject는 code가 있는 Alpha2HostError입니다. 코드는 프로토콜의 오류 코드와 클라이언트 자체 코드(HOST_NOT_FOUND, HOST_EXITED, HOST_OOM_KILLED, HOST_LOAD_FAILED, HOST_START_TIMEOUT, HOST_QUARANTINED, HOST_BLOCKED, ABORTED 등)입니다. failure는 재시도 정책에 쓸 수 있도록 오류를 분류하며(alpha2/export-failure, alpha2/export-retry), stderr에는 호스트 표준 오류의 마지막 부분이 들어 있습니다.
| 하위 클래스 | 발생 시점 |
|---|---|
Alpha2ProjectValidationError |
프로젝트가 유효하지 않습니다(problems). |
Alpha2ConformError |
미디어가 준비되지 않아 아무것도 렌더링하지 않았습니다(report에 없는 것, 오프라인인 것, 기록된 미디어와 다른 것이 나옵니다). |
Alpha2GpuInitializationError |
호스트가 GPU 디바이스를 만들지 못했습니다(renderer: "gpu"일 때만). |
Alpha2AbortError |
요청의 signal이 중단시켰습니다. |
Electron
섹션 제목: “Electron”클라이언트는 메인 프로세스에서 실행합니다. 아카이브의 bin/과 lib/는 ASAR 아카이브 바깥에 두어 배포합니다. 예를 들어 애플리케이션 리소스 디렉터리(process.resourcesPath)에 bin/과 lib/로 넣습니다. Electron 메인 프로세스에서 클라이언트는 이 위치를 찾아봅니다. 호스트는 자신을 기준으로 FFmpeg 라이브러리를 찾으므로 bin/과 lib/는 나란히 두어야 합니다.
