호스트 프로토콜
alpha2-host serve는 표준 입력으로 요청을 받고 표준 출력으로 응답하며, 한 줄에 JSON 객체 하나를 주고받습니다. 이 페이지를 읽고 나면 프로세스를 띄우고 줄 단위로 읽고 쓸 수 있는 어떤 언어로든 alpha2 클라이언트를 작성할 수 있습니다. Go, Python, Ruby, C#, 셸 스크립트 모두 가능합니다.
@alpha2-sh/node와 Python 패키지도 이 프로토콜의 클라이언트입니다. Node.js나 Python에서는 이 패키지들을 쓰세요. 이 페이지는 프로토콜 버전 1을 설명합니다. alpha2-host를 구하는 방법은 CLI에 있습니다.
세션의 흐름
섹션 제목: “세션의 흐름”- 표준 입력과 출력을 파이프로 연결해
alpha2-host serve를 실행합니다. - 첫 줄을 읽습니다. 호스트는 무언가를 읽기 전에
{"protocolVersion":1,"type":"ready"}를 씁니다. - 요청을 한 줄에 하나씩 씁니다. 각 요청에는
"protocolVersion": 1,requestId(비어 있지 않은 문자열),operation(capabilities,conform,export,still,cancel중 하나)이 있습니다. - 같은
requestId를 가진 응답이 올 때까지 줄을 읽습니다. export가 실행되는 동안에는progress줄이 먼저 옵니다. - 더 보낼 요청이 없으면 표준 입력을 닫습니다. 호스트는 읽은 요청에 모두 응답한 뒤 종료합니다.
{"protocolVersion":1,"requestId":"1","operation":"capabilities"}호스트는 각 요청에 해당 requestId를 담은 응답 하나로 답합니다.
{"protocolVersion":1,"requestId":"1","ok":true,"result":{...}}{"protocolVersion":1,"requestId":"1","ok":false,"error":{"code":"INVALID_INPUT","message":"..."}}메시지
섹션 제목: “메시지”모든 메시지는 방향과 관계없이 UTF-8 텍스트 한 줄에 담긴 JSON 객체 하나이며, "protocolVersion": 1을 포함합니다. 호스트가 쓰는 내용은 ASCII입니다. 문자열 안의 ASCII 밖 문자는 모두 \uXXXX 이스케이프로 쓰므로, 레거시 코드 페이지로 디코딩하는 쪽에서도 같은 텍스트를 읽습니다.
- 앞선 요청의 응답을 받기 전에 다음 요청을 써도 됩니다. 응답과 요청은
requestId로 짝지으세요. - 호스트가 읽을 수 없는 요청에 쓸 만한
requestId도 없으면"requestId": "unknown"으로 응답합니다. - 요청 한 줄은 최대 128 MiB, 그 안의
project는 최대 64 MiB입니다. - 표준 오류에는 사람이 읽는 진단 메시지만 나오며, 프로토콜 메시지는 나오지 않습니다. 로그로 흘려보내거나 별도 스레드에서 읽으세요. 아무도 읽지 않는 파이프는 가득 차서 호스트를 멈추게 할 수 있습니다.
진행 상황
섹션 제목: “진행 상황”export가 실행되는 동안 호스트는 진행 상황 줄도 씁니다.
{"protocolVersion":1,"type":"progress","requestId":"1","phase":"frames","completed":12,"total":30}phase는 프레임을 인코딩하는 동안 frames, 모든 프레임을 인코딩하고 파일을 마무리하는 동안 finalizing입니다. "restarted": true이고 completed가 0인 줄은 GPU 상주 인코더가 프레임을 거부해 export가 readback 경로로 처음부터 다시 시작했다는 뜻입니다. export는 응답이 도착해야 끝난 것입니다.
호스트가 멈출 때
섹션 제목: “호스트가 멈출 때”- GPU가 필요한 첫 요청에서 호스트가 GPU 디바이스를 시작하지 못하면
{"protocolVersion":1,"type":"fatal","error":{...}}를 쓰고 종료합니다. 그 요청과 이후 요청은 응답을 받지 못합니다.conform에는 GPU가 필요 없습니다. - 디바이스를 잃었거나 디코더 또는 인코더 스레드가 멈추면 호스트는 해당 응답을 쓴 뒤 종료합니다. 이후 요청은 새 프로세스를 띄워 보내세요.
- 오랫동안 아무것도 쓰지 않는 호스트는 실행되기 전에 붙잡혀 있는 것입니다(macOS의 Gatekeeper 검사, Windows의 백신 검사나 App Control 정책). 공식 클라이언트는 30초 뒤
HOST_START_TIMEOUT으로 포기하니, 직접 만든 클라이언트도 그렇게 하세요.
Python으로 만든 완전한 클라이언트
섹션 제목: “Python으로 만든 완전한 클라이언트”이 클라이언트는 Python 표준 라이브러리만 씁니다. 미디어가 없는(단색) 2초짜리 프로젝트를 export하고, 진행 상황을 출력하며, 호스트가 쓰는 모든 종류의 줄을 처리합니다.
"""표준 라이브러리만 사용해 `alpha2-host serve`로 프로젝트를 export합니다."""import jsonimport subprocessimport sys
HOST = sys.argv[1] if len(sys.argv) > 1 else "alpha2-host"
def send(host, message): # 한 줄에 JSON 객체 하나, 프로토콜 버전 1. host.stdin.write(json.dumps({"protocolVersion": 1, **message}) + "\n") host.stdin.flush()
def read(host): line = host.stdout.readline() if not line: raise RuntimeError("alpha2-host exited without answering") return json.loads(line)
with open("project.json", encoding="utf-8") as file: project = json.load(file)
host = subprocess.Popen( [HOST, "serve"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, encoding="utf-8",)
# 호스트는 무언가를 읽기 전에 {"type": "ready"}를 씁니다.ready = read(host)assert ready.get("type") == "ready", ready
send(host, { "requestId": "export-1", "operation": "export", "project": project, "assets": {}, "startTicks": 0, "endTicks": 240_000, # 2초: 1초에 120,000틱 "frameRate": {"num": 30, "den": 1}, "output": "solid.webm", "overwrite": True,})
while True: message = read(host) if message.get("type") == "progress": print(f"{message['phase']}: {message['completed']}/{message['total']}", file=sys.stderr) elif message.get("type") == "fatal": raise RuntimeError(message["error"]["message"]) elif message.get("requestId") == "export-1": break
host.stdin.close() # 더 보낼 요청 없음: 호스트는 응답을 마치면 종료합니다host.wait()
if not message["ok"]: error = message["error"] raise SystemExit(f"{error['code']}: {error['message']}")print("wrote", message["result"]["output"])클라이언트가 읽는 프로젝트는 다음과 같습니다.
{ "schema_version": 1, "id": "solid", "name": "Solid", "settings": { "width": 1280, "height": 720, "frame_rate": { "num": 30, "den": 1 }, "sample_rate": 48000 }, "sources": [ { "id": "backdrop", "kind": "generated", "generator": { "type": "fill", "paint": { "type": "solid", "color": { "red": 0.17, "green": 0.36, "blue": 0.99, "alpha": 1 } } } } ], "timeline": { "tracks": [ { "id": "video-1", "name": "Main video", "kind": "video", "clips": [ { "id": "clip-1", "source_id": "backdrop", "timeline_range": { "start": 0, "end": 240000 }, "source_range": { "start": 0, "end": 240000 } } ] } ] }}호스트 경로를 인수로 주어 실행합니다.
python3 export_client.py ./alpha2-0.5.0-linux-x64-bundled/bin/alpha2-host모든 릴리스 아카이브에서 VP9는 소프트웨어로 인코딩되므로, 출력을 WebM으로 해서 GPU나 H.264 인코더가 없는 머신에서도 예제가 실행되게 했습니다. CPU 렌더러를 강제하려면 호스트 환경에 ALPHA2_RENDERER=cpu를 설정하세요.
capabilities
섹션 제목: “capabilities”다른 필드는 받지 않습니다. 결과는 이 호스트가 할 수 있는 일을 보고합니다.
capabilities.gpu: 어댑터, 등급(tier),memory.budgetBytes.capabilities.decode: 디코딩 모드와 하드웨어 디코더.capabilities.encode: 컨테이너별로 제공하는 코덱(encode.containers), 비디오와 오디오 인코더, GPU 상주 인코딩 경로,encode.h264.status(available,gpu-only,unavailable중 하나이며reason,remedy가 함께 옴).capabilities.features와capabilities.adapters(호스트가 렌더링에 쓰는 GPU 어댑터와 그 이유. CPU 렌더러에서는null).effectCatalog: 내장 이펙트와 트랜지션.- 어댑터, FFmpeg, 운영체제 버전.
conform
섹션 제목: “conform”렌더링하지 않고 GPU도 쓰지 않은 채, 각 소스가 어떤 미디어를 읽게 되는지와 무엇이 이를 막는지 보고합니다.
| 필드 | 타입 | 기본값 | |
|---|---|---|---|
project |
프로젝트 문서 | 필수 | |
assets |
{ "<source-id>": "<path>" } |
{} |
소스별 원본 파일. |
proxies |
{ "<source-id>": [{ "path", "madeFrom"?, "colorPipeline"? }] } |
{} |
원본별 프록시, 선호 순서대로. |
frames |
{ "<source-id>": ["<path>" or null, ...] } |
{} |
이미지 시퀀스별 프레임 파일. 빈 자리는 null. |
purpose |
preview, preview-proxy, thumbnail, remote-edit, export-original, export-proxy |
export-original |
미디어를 읽는 용도. |
결과는 { "catalog": ..., "report": ... }입니다. export를 막는 문제가 있으면 report.ready가 false입니다. 타입은 alpha2/assets의 AssetCatalog와 ConformReport입니다.
export
섹션 제목: “export”프로젝트의 한 구간을 렌더링해 MP4, MOV, Matroska, WebM 파일로 인코딩합니다.
| 필드 | 기본값 | |
|---|---|---|
project |
필수 | 프로젝트 문서. |
assets |
필수 | { "<source-id>": "<path>" }. export가 읽는 소스별 파일. |
startTicks, endTicks |
필수 | 틱 단위 타임라인 범위. 프레임 그리드에 맞게 넓혀집니다. |
frameRate |
필수 | { "num", "den" }. |
output |
필수 | 쓸 파일. container가 없으면 확장자(.mp4, .m4v, .mov, .mkv, .webm)가 컨테이너를 정하며, 확장자는 컨테이너와 일치해야 합니다. |
overwrite |
false |
export가 모두 끝나면 output 위치의 일반 파일을 교체합니다. |
container |
출력 확장자에 따름 | mp4, mov, mkv, webm. |
videoCodec |
컨테이너 기본값 | h264, hevc, vp9, av1, mpeg4-part2, prores. |
audioCodec |
컨테이너 기본값 | aac, opus. |
videoQuality |
high |
very-low, low, medium, high, very-high, 또는 { "bitrate", "mode"?, "quantizer"? }. |
audioQuality |
high |
레벨, 또는 { "bitrate" }. |
includeAudio |
true |
|
alpha |
false |
투명도를 유지합니다. webm이나 mkv의 vp9(8비트), 또는 prores 4444. |
bitDepth |
prores는 10, 그 외는 8 |
10비트 그림. hevc, av1, vp9, prores용. |
loudness |
없음 | { "targetLufs", "truePeakCeilingDbtp" }. includeAudio가 필요합니다. |
fonts |
[] |
폴백 순서대로 나열한 폰트 파일. 텍스트가 나오는 프로젝트에는 필요합니다. |
effects |
[] |
내장 이펙트 외의 런타임 이펙트 정의. |
effectAssets |
{} |
{ "<resource-id>": "<path>" }. .cube LUT 등. |
proxies, frames |
{} |
conform과 같습니다. |
media |
original |
proxies는 최신 프록시가 있는 곳에서 프록시를 읽습니다. |
resources |
{} |
{ "<source-id>": { "<locator>": "<path>" } }. SVG나 Lottie 소스가 참조하는 파일. |
videoEncoder |
호스트가 선택 | capabilities가 보고하는 인코더. |
inputPolicy |
auto |
auto, gpu-required, cpu-readback. |
strategy |
render |
render, prefer-copy, require-copy. |
renderCache |
없음 | { "directory", "quotaBytes"?, "backlog"? }. |
memory |
호스트 기본값 | 바이트 단위 예산: inFlightBytes, decodeQueueBytes, reverseCacheBytes, audioWindowBytes, providerSourceBytes, decoderSessionBytes, scale. |
컨테이너마다 지원하는 코덱이 정해져 있으며, 각 목록의 첫 번째가 기본값입니다.
| 컨테이너 | 비디오 | 오디오 |
|---|---|---|
mp4 |
h264, hevc, vp9, av1, mpeg4-part2 |
aac, opus |
mov |
h264, hevc, mpeg4-part2, prores |
aac |
mkv |
h264, hevc, vp9, av1, mpeg4-part2 |
aac, opus |
webm |
vp9, av1 |
opus |
결과에는 프레임, 사용한 인코더와 레이트 컨트롤, 프레임이 인코더에 전달된 방식(videoPipeline), 기록한 파일의 절대 경로(output), 각 소스가 읽은 미디어(media), 소요 시간(renderMs, fps) 등이 보고됩니다. @alpha2-sh/node는 요청과 결과의 타입을 ExportProjectRequest와 NativeExportResult로 정의합니다.
호스트는 output 옆에 <stem>.a2partial.<ext>를 쓰고, 완성되면 제자리로 옮깁니다. 완성된 렌더링 결과를 옮길 수 없으면 <stem>.a2kept.<ext>로 남겨 두고 요청은 OUTPUT_PUBLISH_FAILED로 실패하며, 그 details.keptPath가 남겨 둔 파일을 가리킵니다.
still
섹션 제목: “still”프레임을 PNG 파일로 렌더링합니다. 각 프레임은 export가 그 시점에 그리는 프레임과 같습니다. 요청 하나는 모든 프레임에 대해 디코딩을 한 번만 하며, 진행 상황 줄은 쓰지 않습니다.
| 필드 | 기본값 | |
|---|---|---|
project, assets |
필수 | export와 같습니다. |
stills |
필수 | [{ "time": <ticks>, "output": "<path>" }, ...], 1~256프레임. |
size |
캔버스 크기 | { "width", "height" }, 각각 1~16384. |
overwrite |
false |
export와 같으며, 모든 출력에 적용됩니다. |
alpha |
false |
프로젝트 배경 없이 그립니다. |
proxies, frames, media, resources, effectAssets, fonts, effects |
export와 같습니다. |
결과는 { "stills": [{ "output", "frameTime", "width", "height", "publish" }], "renderer", "renderMs", "timings", "media", "decodeDiagnostics" }이며, 요청한 프레임 순서를 따릅니다. 각 PNG는 8비트 RGBA이고, 디스플레이 인코딩된 sRGB에 straight 알파입니다.
요청을 취소하려면 target에 해당 요청을 지정한 cancel 줄을 씁니다.
{"protocolVersion":1,"requestId":"1:cancel","operation":"cancel","target":"1"}이 요청에는 즉시 {"protocolVersion":1,"requestId":"1:cancel","ok":true,"result":{"target":"1","state":"running"}}처럼 응답합니다.
running: 대상이 지금 처리 중인 요청입니다. export나 still은 멈추고 작성 중이던 파일을 지운 뒤CANCELLED로 응답합니다. 호스트는 계속 요청을 처리합니다.pending: 그 id로 처리 중인 요청이 없습니다. 나중에 그 id의 요청을 읽거나 시작하면 실행하지 않고CANCELLED로 응답합니다.
취소한 요청의 응답을 받을 때까지 표준 입력을 열어 두세요. 표준 입력을 닫는 것은 “멈춰라”가 아니라 “더 보낼 요청이 없다”는 뜻입니다. SIGTERM, SIGINT, SIGHUP(Windows에서는 콘솔 제어 이벤트)도 실행 중인 export를 같은 방식으로 취소한 뒤 프로세스를 끝냅니다. serve를 띄운 프로세스가 종료되면 호스트도 실행 중인 작업을 취소하고 종료하므로, 클라이언트가 강제 종료되어도 export가 계속 실행된 채 남지 않습니다.
| 코드 | 의미 |
|---|---|
UNSUPPORTED_PROTOCOL |
protocolVersion이 없거나 1이 아닙니다. |
INVALID_REQUEST |
줄이 요청이 아닙니다. UTF-8이 아님, JSON 객체가 아님, 너무 김, operation이 알 수 없거나 없음, 필드가 없거나 타입이 틀림, requestId가 비어 있음, cancel에 target이 없음 등입니다. 또는 출력이 요청의 입력 중 하나입니다. |
INVALID_INPUT |
받아들일 수 없는 값입니다. 유효하지 않은 프로젝트, 범위, 레이트, 설정이거나 함께 쓸 수 없는 설정 조합입니다. |
CONFORM_FAILED |
export의 미디어가 준비되지 않았습니다. details가 conform 보고서입니다. |
MISSING_INPUT |
렌더링에 필요한 파일(에셋, 이펙트 리소스, 폰트, 프레임)이 주어지지 않았습니다. |
INCOMPLETE_COMPOSITION |
컴포지션 그래프에 연결되지 않은 입력이나 출력이 있습니다. |
EFFECT_ERROR |
런타임 이펙트의 정의, 파라미터, 셰이더가 실패했습니다. |
GPU_INIT_FAILED |
호스트가 GPU 디바이스를 만들지 못했습니다. |
HARDWARE_DECODE_UNAVAILABLE |
디코딩 모드가 hardware-required인데 호스트가 하드웨어 디코딩을 할 수 없습니다. |
GPU_DEVICE_LOST |
GPU 디바이스를 잃었거나, 디바이스가 주어진 작업을 더 이상 실행하지 않습니다. |
GPU_DEVICE_HUNG |
export 자체의 작업 때문에 GPU 디바이스를 잃었습니다. GPU에서 다시 실행하면 GPU가 또 멈춥니다. |
GPU_TIMEOUT |
GPU 작업이 제시간에 끝나지 않았습니다. |
DECODE_STALLED |
디코더가 더 이상 진행하지 않습니다. |
ENCODE_STALLED |
인코더나 먹서가 더 이상 진행하지 않습니다. |
COPY_INELIGIBLE |
strategy: "require-copy"인데 범위를 복사할 수 없습니다. |
GPU_OUTPUT_UNAVAILABLE |
inputPolicy: "gpu-required"인데 GPU 상주 인코딩 경로가 이 export를 처리할 수 없습니다. |
ENCODE_ERROR |
인코딩이나 먹싱이 실패했습니다. |
ENCODER_BUSY |
비디오 인코더를 하나도 열지 못했고, 하드웨어 인코더는 다른 export가 세션을 점유하고 있어 거부했습니다. |
IO_ERROR |
파일을 읽거나 쓸 수 없거나, output이 이미 있는데 요청이 덮어쓰지 않습니다. |
OUTPUT_IN_USE |
실행 중인 다른 export가 같은 output에 쓰고 있습니다. |
OUTPUT_PUBLISH_FAILED |
출력은 완전히 렌더링되었지만 제자리로 옮기지 못했습니다. details.keptPath가 남겨 둔 위치를 가리킵니다. |
CANCELLED |
요청이 취소되었습니다. |
RESOURCE_LIMIT |
예산이나 한도 때문에 작업이 거부되었습니다. 예를 들어 메모리 예산이 export에 비해 너무 작은 경우입니다. |
INTERNAL |
호스트의 결함으로 요청이 실패했습니다. 호스트는 이후 요청에 계속 응답합니다. |
ALPHA2_ERROR |
다른 코드로 설명되지 않는 실패입니다. |
실패한 export에는 error.failure도 있습니다. 실패 분류(code, 예: OUT_OF_MEMORY, DEVICE_LOST, INVALID_INPUT, CANCELLED), 실패한 단계와 프레임, 프로토콜 코드, 진단 정보가 들어 있습니다. ENCODER_BUSY 실패에는 retryAfterMs도 있습니다. 타입은 alpha2/export-failure의 ExportFailure이며, alpha2/export-retry가 이를 바탕으로 재시도 여부를 결정합니다. 검증과 오류를 참고하세요.
