에디터
This content is for v0.4. Switch to the latest version for up-to-date documentation.
@alpha2-sh/editor는 엔진의 프로젝트 문서 위에 만든 편집 SDK입니다. 이 페이지를 읽고 나면 편집 세션에 프로젝트를 담고, 검증과 실행 취소를 지원하는 타입이 지정된 커맨드로 프로젝트를 변경하고, 드래그 제스처를 처리하고, 세션을 브라우저 프리뷰, 오디오 재생, export에 연결할 수 있습니다.
패키지에는 네 가지 진입점이 있습니다.
| Import | 내용 | 실행 환경 |
|---|---|---|
@alpha2-sh/editor |
문서, 커맨드, 세션, 실행 취소 | 브라우저와 Node.js(DOM, React, WebAssembly, 오디오 없음) |
@alpha2-sh/editor/browser |
createBrowserBridge: 가져오기, 프리뷰, 재생, export |
브라우저 |
@alpha2-sh/editor/react |
EditorProvider, useEditor, useEditorSelector, EditorPreview |
React |
@alpha2-sh/editor/native-client |
createNativeClient: 직접 운영하는 네이티브 export 서비스에서 export |
브라우저 |
npm install @alpha2-sh/editor alpha2pnpm add @alpha2-sh/editor alpha2yarn add @alpha2-sh/editor alpha2에디터 문서는 엔진 프로젝트를 감쌉니다.
document.project는 엔진의 프로젝트입니다.document.assets는 빈(bin)에 있는 각 에셋을 설명합니다(id,kind,name,contentRevision, 그리고 선택적으로locator,fingerprint,missing,authoring).document.metadata는 에디터 전용 데이터를 담습니다: 마커, export 구간, 트랙과 클립 메타데이터, 그래프 레이아웃.
미디어 바이트는 문서에 저장되지 않습니다.
import { createEmptyDocument, serializeDocument, parseDocument } from "@alpha2-sh/editor";
const document = createEmptyDocument({ width: 1280, height: 720 }); // 지정하지 않으면 30 fps, 48 kHzconst json = serializeDocument(document); // 이식 가능한 JSONconst reopened = parseDocument(json); // 일반 엔진 프로젝트도 읽습니다createEmptyDocument(settings)는 미디어가 없는 문서를 만듭니다. settings로 따로 지정하지 않으면 1920x1080, 30 fps, 48 kHz입니다. serializeDocument는 검증을 통과하지 못한 문서에 대해 EditorError를 던집니다.
세션과 커맨드
섹션 제목: “세션과 커맨드”createEditor({ document, historyLimit, historyByteLimit })는 EditorSession을 만듭니다. 모든 변경은 dispatch(command)를 거칩니다. dispatch는 결과를 검증하고 { ok: true, value } 또는 { ok: false, error }를 반환합니다. 거부된 커맨드는 아무것도 바꾸지 않습니다.
import { createEditor, createEmptyDocument } from "@alpha2-sh/editor";
const editor = createEditor({ document: createEmptyDocument({ width: 1280, height: 720 }) });
const result = editor.dispatch({ type: "transaction", commands: [ { type: "asset.put", asset: { id: "backdrop", kind: "generated", name: "Backdrop", contentRevision: 0 }, source: { id: "backdrop", kind: "generated", generator: { type: "fill", paint: { type: "solid", color: { red: 0.17, green: 0.36, blue: 0.99, alpha: 1 } } }, }, }, { type: "track.add", track: { id: "video", name: "Video", kind: "video", clips: [] } }, { type: "clip.insert", trackId: "video", clip: { id: "clip-1", source_id: "backdrop", timeline_range: { start: 0, end: 240_000 }, source_range: { start: 0, end: 240_000 }, }, }, ],});if (!result.ok) throw result.error; // `code`가 있는 EditorError
editor.dispatch({ type: "clip.split", id: "clip-1", at: 120_000, newId: "clip-2" });editor.undo();editor.redo();await editor.dispose();transaction은 여러 커맨드를 적용하고, 최종 문서를 기준으로 한 번만 검사합니다. 실행 취소 단계도 하나입니다.canExecute(command)는 적용하지 않고dispatch와 같은 결과를 반환합니다.- 커맨드에 따로 명시되지 않으면 시간은 타임라인 틱(초당 120,000)입니다.
- 거부는
EditorError로 표현됩니다.code(invalid_command,locked등)로 분기하세요.operation과entityId는 어디서 발생했는지 알려 줍니다.
커맨드 목록
섹션 제목: “커맨드 목록”| 영역 | 커맨드 |
|---|---|
| 프로젝트 | project.update, metadata.update |
| 트랙 | track.add, track.remove, track.reorder, track.update, track.metadata |
| 클립 | clip.insert, clip.place(insert, overwrite, append), clip.delete, clip.update, clip.metadata, clip.move, clips.move, clip.trim, clip.split, clip.slip, clip.slide, clip.roll, clip.retime |
| 이펙트 | effect.add, effect.update, effect.remove, effect.reorder |
| 트랜지션 | transition.add, transition.resize, transition.remove |
| 자막 | cue.add, cue.update, cue.remove, cue.split, cue.trim |
| 컴포지션 그래프 | graph.create, graph.node.add, graph.node.update, graph.node.remove, graph.connect, graph.disconnect, graph.output, graph.group, graph.ungroup, graph.port.add, graph.control.add, graph.control.remove, graph.layout, composition.add, composition.create |
| 에셋과 마커 | asset.put, asset.remove, marker.add, marker.update, marker.remove |
| 일괄 처리 | transaction |
클립 커맨드는 alpha2/edit의 편집 연산 위에 만들어져 있습니다. 모든 필드는 패키지 선언의 Command 타입에 문서화되어 있습니다.
실행 취소
섹션 제목: “실행 취소”커밋된 변경은 각각 실행 취소 단계 하나가 됩니다. undo()와 redo()는 이동할 단계가 없으면 false를 반환합니다. 이력은 기본적으로 최대 120단계, 64 MiB까지 보관합니다. 바꾸려면 createEditor에 historyLimit과 historyByteLimit을 지정하세요. clearHistory()는 이력을 비웁니다.
드래그 제스처
섹션 제목: “드래그 제스처”beginGesture()는 문서와 이력을 건드리지 않고 드래그를 미리 보여 줍니다.
const gesture = editor.beginGesture();gesture.update({ type: "clip.move", id: "clip-2", at: 360_000 }); // editor.draft에 표시됩니다gesture.update({ type: "clip.move", id: "clip-2", at: 300_000 });gesture.commit(); // 실행 취소 단계 하나. gesture.cancel()은 단계를 추가하지 않습니다각 update는 제스처 시작 시점의 문서에 커맨드를 실행하고, 유효하면 그 결과를 editor.draft로 보여 줍니다. commit()은 마지막 후보를 완전히 검증한 뒤 커밋합니다. commit, cancel, dispatch, undo, redo, 또는 새 제스처가 시작되면 제스처가 끝나고, 그 뒤의 update는 stale_gesture를 반환합니다.
변경 감지하기
섹션 제목: “변경 감지하기”subscribe(listener)는 변경이 있을 때마다listener를 호출하고, 구독 해제 함수를 반환합니다.getSnapshot()은{ document, draft, selection, revision, generation, canUndo, canRedo }를 반환합니다. 다음 변경 전까지 같은 객체를 반환하므로useSyncExternalStore와 함께 쓸 수 있습니다.subscribeSelector(selector, listener, equal?)는 선택한 값이 바뀔 때만listener를 호출합니다.
const stop = editor.subscribeSelector( (snapshot) => snapshot.document.project.timeline.tracks.length, (count) => console.log("tracks:", count),);세션은 선택 상태(select(ids))와 클립보드(copy, paste)도 관리합니다.
브라우저에서 프리뷰, 재생, export
섹션 제목: “브라우저에서 프리뷰, 재생, export”@alpha2-sh/editor/browser는 세션을 alpha2 엔진에 연결합니다.
createBrowserBridge({ editor, runtimeAssets })로 브리지를 만듭니다.runtimeAssets에는 번들러가 제공하는 URL을 넘깁니다:alpha2/wasm/alpha2_wasm_bg.wasm(wasmUrl),alpha2/wasm/alpha2_audio_worklet.wasm(audioWasmUrl),alpha2/audio-worklet-processor모듈(workletUrl).bindPreview(canvas)로 캔버스를 바인딩합니다.- 파일을 가져오고 커맨드로 배치한 뒤
seek,play,pause를 호출합니다. startExport(options)로 export합니다.
import { createEditor, createEmptyDocument } from "@alpha2-sh/editor";import { createBrowserBridge } from "@alpha2-sh/editor/browser";
const editor = createEditor({ document: createEmptyDocument({ width: 1280, height: 720 }) });const bridge = createBrowserBridge({ editor, runtimeAssets: { wasmUrl, audioWasmUrl, workletUrl },});await bridge.bindPreview(canvas);
const { assetIds } = await bridge.importFiles(files); // <input type="file">에서 받은 파일const trackId = crypto.randomUUID();const placed = editor.dispatch({ type: "transaction", commands: [ { type: "track.add", track: { id: trackId, kind: "video", name: "Video", clips: [] } }, { type: "clip.insert", trackId, clip: { id: crypto.randomUUID(), source_id: assetIds[0], timeline_range: { start: 0, end: 120_000 }, source_range: { start: 0, end: 120_000 }, time_map: { type: "forward" }, }, }, ],});if (!placed.ok) throw placed.error;await bridge.seek(0);
const job = bridge.startExport({ container: "webm" });job.subscribe((progress) => console.log(progress.phase, progress.fraction));const artifact = await job.result;// artifact.kind "file": 오리진의 비공개 저장소에 있는 임시 파일.if (artifact.kind === "file") await artifact.release(); // 다운로드가 끝난 뒤
await bridge.dispose();await editor.dispose();가져오기
섹션 제목: “가져오기”importFiles(files)는 각 파일을 엔진으로 검사하고(비디오, 오디오, 이미지, 폰트,.cubeLUT), 실행 취소 가능한 한 단계로 문서에 추가합니다.importCaptions(file)은 SRT나 WebVTT 파일을 자막 에셋으로 읽어 들입니다.importSequence(files, { frameRate, missing })는 번호가 매겨진 PNG나 OpenEXR 파일들을 하나의 이미지 시퀀스로 가져옵니다.relink(id, file)은 에셋이 같은 미디어의 새 파일을 가리키도록 합니다.
프리뷰와 재생
섹션 제목: “프리뷰와 재생”bindPreview(canvas)는 캔버스에 그리며, 나중에dispose()할 바인딩으로 resolve됩니다.seek(tick)은 플레이헤드를 옮기고 그 프레임을 렌더링합니다.play(rate, { loop, policy })는 플레이헤드부터 재생하고,pause()는 멈춥니다. 오디오가 있으면 재생은 오디오 클럭을 따라갑니다.previewScale(브리지 옵션, 기본값 0.5)은 프리뷰 해상도를 정합니다. 1이면 텍스트와 이펙트가 export 결과와 픽셀 단위로 일치합니다. 나중에 바꾸려면setPreviewScale(scale)을 사용합니다.audio: false이면 화면만 프리뷰합니다.
워커는 페이지 스레드의 작업을 덜어 줍니다. 모두 선택 사항입니다.
runtimeAssets 필드 |
워커 진입점 | 페이지에서 덜어 내는 작업 |
|---|---|---|
workerFactory |
@alpha2-sh/editor/preview-worker |
프리뷰 렌더링. 지정하지 않거나 워커가 실패하면 메인 스레드가 렌더링합니다. |
hashWorkerFactory |
@alpha2-sh/editor/hash-worker |
가져온 파일의 해싱. |
extensionsWorkerFactory |
@alpha2-sh/editor/extensions-worker |
SVG, 애니메이션 이미지, Lottie, OpenEXR 읽기(extensionsWasmUrl과 함께). |
Export
섹션 제목: “Export”startExport(options)는 ExportJob을 반환합니다. result(파일의 promise), 진행 상황을 받는 subscribe(listener), cancel()이 있습니다.
| 옵션 | 기본값 | |
|---|---|---|
container |
mp4 |
mp4, mov, mkv, webm. 사용할 수 있는 코덱을 제한합니다. |
videoCodec |
컨테이너의 기본 코덱(avc, WebM은 vp9) |
avc, hevc, vp9, av1. |
audioCodec |
컨테이너의 코덱 중 이 브라우저가 인코딩할 수 있는 첫 번째 | aac 또는 opus. |
videoQuality, audioQuality |
high |
very-low, low, medium, high, very-high. |
includeAudio |
true |
|
frameRate |
프로젝트의 값 | 정확한 { num, den }. |
media |
original |
proxies는 최신 프록시가 있으면 그것을 읽습니다. |
output |
{ kind: "opfs" } |
오리진의 비공개 저장소에 있는 임시 파일. 사용자가 고른 파일은 { kind: "file", handle }, 메모리는 { kind: "memory", maxBytes }(기본값 512 MiB). |
videoDecode |
auto |
software는 CPU에서 디코딩합니다. |
컨테이너가 지원하지 않거나 이 브라우저가 인코딩할 수 없는 코덱은 export 렌더링 전에 거부됩니다. bridge.videoEncoders()로 미리 확인하세요. 작업 진행 상황의 phase는 preparing, rendering, finalizing, completed(또는 failed, cancelled, retrying) 순으로 진행되며, fraction은 0에서 1까지입니다.
React
섹션 제목: “React”@alpha2-sh/editor/react는 네 가지로 구성됩니다.
EditorProvider는 하위 트리에서 세션을 쓸 수 있게 합니다. 세션은 애플리케이션이 소유하므로, provider를 언마운트해도 세션은 dispose되지 않습니다.useEditor()는 그 세션을 반환합니다.useEditorSelector(selector, equal?)는 선택한 값이 바뀌면 다시 렌더링합니다. 인라인 selector를 써도 됩니다. 결과는 스냅숏마다 캐시됩니다.EditorPreview는 직접 만든 캔버스에 브리지의 프리뷰를 바인딩하고, 언마운트될 때 해제합니다.
import { EditorPreview, EditorProvider, useEditor, useEditorSelector } from "@alpha2-sh/editor/react";
function UndoButton() { const editor = useEditor(); const canUndo = useEditorSelector((snapshot) => snapshot.canUndo); return <button disabled={!canUndo} onClick={() => editor.undo()}>Undo</button>;}
function App({ editor, bridge }) { return ( <EditorProvider editor={editor}> <EditorPreview bridge={bridge} onError={reportError} /> <UndoButton /> </EditorProvider> );}서버에서 export하기
섹션 제목: “서버에서 export하기”@alpha2-sh/editor/native-client는 직접 운영하는 HTTP export 서비스(예: @alpha2-sh/node 기반)에서 문서를 export합니다. createNativeClient({ baseUrl, fetch? })는 클라이언트를 반환하며, 그 startExport(editor, assets, settings)는 문서의 미디어(bridge.nativeAssets에서 가져옴)를 업로드하고, 다운로드할 수 있는 파일이 나올 때까지 작업을 추적합니다. 설정은 브라우저 export와 같고, 네이티브 호스트가 인코딩할 수 있는 mpeg4-part2 비디오 코덱이 추가됩니다.
