콘텐츠로 이동

호스트 프로토콜

alpha2-host serve는 표준 입력으로 요청을 받고 표준 출력으로 응답하며, 한 줄에 JSON 객체 하나를 주고받습니다. 이 페이지를 읽고 나면 프로세스를 띄우고 줄 단위로 읽고 쓸 수 있는 어떤 언어로든 alpha2 클라이언트를 작성할 수 있습니다. Go, Python, Ruby, C#, 셸 스크립트 모두 가능합니다.

@alpha2-sh/node와 Python 패키지도 이 프로토콜의 클라이언트입니다. Node.js나 Python에서는 이 패키지들을 쓰세요. 이 페이지는 프로토콜 버전 1을 설명합니다. alpha2-host를 구하는 방법은 CLI에 있습니다.

  1. 표준 입력과 출력을 파이프로 연결해 alpha2-host serve를 실행합니다.
  2. 첫 줄을 읽습니다. 호스트는 무언가를 읽기 전에 {"protocolVersion":1,"type":"ready"}를 씁니다.
  3. 요청을 한 줄에 하나씩 씁니다. 각 요청에는 "protocolVersion": 1, requestId(비어 있지 않은 문자열), operation(capabilities, conform, export, still, cancel 중 하나)이 있습니다.
  4. 같은 requestId를 가진 응답이 올 때까지 줄을 읽습니다. export가 실행되는 동안에는 progress 줄이 먼저 옵니다.
  5. 더 보낼 요청이 없으면 표준 입력을 닫습니다. 호스트는 읽은 요청에 모두 응답한 뒤 종료합니다.
{"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하고, 진행 상황을 출력하며, 호스트가 쓰는 모든 종류의 줄을 처리합니다.

export_client.py
"""표준 라이브러리만 사용해 `alpha2-host serve`로 프로젝트를 export합니다."""
import json
import subprocess
import 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"])

클라이언트가 읽는 프로젝트는 다음과 같습니다.

project.json
{
"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.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, 운영체제 버전.

렌더링하지 않고 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입니다.

프로젝트의 한 구간을 렌더링해 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가 남겨 둔 파일을 가리킵니다.

프레임을 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가 이를 바탕으로 재시도 여부를 결정합니다. 검증과 오류를 참고하세요.

  • CLI: 같은 호스트로 export를 한 번에 하나씩 실행합니다.
  • Node.js와 Python: 이 프로토콜의 클라이언트.
  • 프로젝트 모델: project에 들어가는 내용.