콘텐츠로 이동

Node.js와 Electron

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

@alpha2-sh/node는 네이티브 alpha2 렌더러인 alpha2-host의 Node.js 클라이언트입니다. 이 페이지를 읽고 나면 호스트를 설치하고, 진행 상황 표시와 취소를 지원하며 프로젝트를 동영상 파일로 export하고, 정지 프레임을 렌더링하고, 머신에서 어떤 형식으로 인코딩할 수 있는지 확인하고, Electron 앱에 호스트를 포함해 배포할 수 있습니다.

호스트는 GPU에서 렌더링하고(GPU가 없으면 CPU에서), 디코딩과 인코딩에는 FFmpeg 8을 씁니다. 클라이언트는 호스트를 자식 프로세스로 띄우고 호스트 프로토콜로 통신합니다.

  1. 패키지를 추가합니다.

    터미널 창
    npm install @alpha2-sh/node
  2. 이 패키지의 버전과 플랫폼에 맞는 alpha2-host를 내려받습니다.

    터미널 창
    npx @alpha2-sh/node install

패키지에는 alpha2-host가 들어 있지 않습니다. install 명령은 같은 버전의 GitHub 릴리스에서 해당 플랫폼의 bundled 아카이브를 가져와 릴리스의 checksums.sha256과 대조한 뒤, 모든 alpha2 클라이언트가 함께 쓰는 캐시에 풉니다(Python 패키지도 같은 캐시를 씁니다).

  • ALPHA2_HOST_CACHE_DIR가 설정되어 있으면 그 디렉터리를 씁니다.
  • 그렇지 않으면 Linux에서는 $XDG_CACHE_HOME 또는 ~/.cache, macOS에서는 ~/Library/Caches, Windows에서는 %LOCALAPPDATA% 아래의 alpha2 디렉터리를 씁니다.

--thin을 주면 대신 시스템의 FFmpeg 8을 불러오는 아카이브를 가져오고(Windows 제외), --force를 주면 다시 내려받습니다. 코드에서는 installHost(options)로 같은 작업을 할 수 있으며, cacheDirectory, kind(bundled 또는 thin), force, downloadUrl, onProgress, signal 옵션을 받고 { hostPath, directory, downloaded }로 resolve됩니다. ALPHA2_HOST_DOWNLOAD_URL(또는 downloadUrl)로 https://github.com/alpha2-sh/alpha2/releases/download와 같은 구조의 미러를 지정할 수 있습니다.

설치에 실패하면 Alpha2HostInstallError로 reject되며, 그 code는 UNSUPPORTED_PLATFORM, DOWNLOAD_FAILED, CHECKSUM_MISMATCH, EXTRACT_FAILED, ABORTED 중 하나입니다.

클라이언트가 호스트를 찾는 위치

섹션 제목: “클라이언트가 호스트를 찾는 위치”

클라이언트는 alpha2-host(Windows에서는 alpha2-host.exe)를 다음 순서로 찾습니다.

  1. hostPath 옵션
  2. ALPHA2_HOST_PATH 환경 변수(상대 경로는 cwd 옵션 기준, 없으면 작업 디렉터리 기준)
  3. Electron에서는 process.resourcesPath/bin/, 그다음 process.resourcesPath/
  4. 패키지 자체의 bin/ 디렉터리
  5. 캐시에 있는 패키지 버전의 아카이브(cacheDirectory 옵션, 없으면 위에서 설명한 디렉터리). bundled를 thin보다 먼저 찾습니다.
  6. PATH에 있는 디렉터리

어디에서도 찾지 못하면 클라이언트는 코드가 HOST_NOT_FOUND인 Alpha2HostError를 던집니다.

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>.alpha2-partial.<ext>를 쓰고, 완성되면 이름을 바꿔 제자리에 놓습니다.
  • 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.
audioCodec 컨테이너 기본값(aac, WebM은 opus) aac, opus.
videoQuality high very-low부터 very-high까지, 또는 { bitrate, mode?, quantizer? }.
audioQuality high 레벨, 또는 { bitrate }.
includeAudio true
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;
}

호스트는 export를 멈추고 작성 중이던 파일을 지운 뒤 종료하며, 프로세스가 사라지면 exportProject는 Alpha2AbortError로 reject됩니다. Windows에서는 클라이언트가 호스트 프로세스를 즉시 종료하므로 작성 중이던 파일이 남습니다. 같은 output으로 다음 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 }를 주면 다른 캔버스 크기로 렌더링합니다.

머신에서 가능한 작업 확인하기

섹션 제목: “머신에서 가능한 작업 확인하기”

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에서는 auto, macOS와 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_LOAD_FAILED, ABORTED 등)입니다. failure는 재시도 정책에 쓸 수 있도록 오류를 분류하며(alpha2/export-failure, alpha2/export-retry), stderr에는 호스트 표준 오류의 마지막 부분이 들어 있습니다.

하위 클래스 발생 시점
Alpha2ProjectValidationError 프로젝트가 유효하지 않습니다(problems).
Alpha2ConformError 미디어가 준비되지 않아 아무것도 렌더링하지 않았습니다(report에 없는 것, 오프라인인 것, 기록된 미디어와 다른 것이 나옵니다).
Alpha2GpuInitializationError 호스트가 GPU 디바이스를 만들지 못했습니다(renderer: "gpu"일 때만).
Alpha2AbortError 요청의 signal이 중단시켰습니다.

클라이언트는 메인 프로세스에서 실행합니다. 아카이브의 bin/과 lib/는 ASAR 아카이브 바깥에 두어 배포합니다. 예를 들어 애플리케이션 리소스 디렉터리(process.resourcesPath)에 bin/과 lib/로 넣습니다. Electron 메인 프로세스에서 클라이언트는 이 위치를 찾아봅니다. 호스트는 자신을 기준으로 FFmpeg 라이브러리를 찾으므로 bin/과 lib/는 나란히 두어야 합니다.

  • CLI: 같은 호스트를 셸에서 사용합니다.
  • 호스트 프로토콜: 모든 요청 필드와 오류 코드.
  • 에디터: 이 패키지로 만든 export 서비스에 에디터 문서를 보냅니다.