MIC 방송
개요
oe.broadcast.mic.start()는 WebRTC 기반 실시간 마이크 방송 세션을 시작합니다. 세션은 stop()으로 종료하고, 진행 상태는 oe.broadcast.subscribe()로 구독해 확인합니다.
시작하기 전에
MIC 방송을 시작하기 전에 아래를 준비하세요.
- 파트너 앱에
MIC_EXECUTEcapability grant 가 있어야 합니다. 없으면CapabilityDenied에러가 발생합니다. - 브라우저가 WebRTC(
RTCPeerConnection)를 지원해야 합니다. - 사이트의 CSP(Content Security Policy)가 아래 요구사항을 충족해야 합니다.
- 방송 대상이 될 디바이스 또는 그룹의 handle 이 필요합니다.
대상 선택
방송 대상은 TTS 방송과 동일하게 targetHandle(디바이스 또는 그룹 handle) 또는 deviceHandles(디바이스 handle 다중)로 지정합니다.
const [devices, groups] = await Promise.all([
oe.targets.list(),
oe.groups.list(),
]);
각 항목의 handle 값을 대상 지정에 사용합니다. 그룹에 방송하면 그룹에 속한 ASSIGNED 상태 디바이스 전체가 대상이 됩니다.
CSP 요구사항
MIC를 사용하는 사이트의 Content-Security-Policy 헤더에 다음 지시문을 추가하세요.
script-src 'self';
connect-src 'self' https://openecho.example.com wss://*.livekit.cloud;
worker-src blob:;
| 지시문 | 이유 |
|---|---|
script-src | self-host UMD·MIC 코드가 자기 origin 에서 로드됩니다('self'). 번들러 경로에서 nonce 기반 CSP 를 쓰면 createClient({ scriptNonce })로 nonce 를 주입하세요 |
connect-src | Open Echo API 와 실시간 오디오 스트리밍 서버(WSS)에 연결합니다 |
worker-src blob: | 미디어 엔진 내부 Worker 를 사용합니다 |
CSP 가 요구사항을 충족하지 않으면 MIC 코드 로드가 차단되어 MicChunkLoadFailed 에러가 발생합니다(번들러 경로).
MIC 코드가 로드되는 방식
킷 동봉 self-host UMD(openecho-sdk.<version>.umd.js)는 MIC 를 인라인으로 포함합니다. 파일 하나만 로드하면 되고, micChunkUrl·scriptNonce 옵션도 필요 없습니다.
<script src="/assets/openecho-sdk.<version>.umd.js"></script>
번들러(npm tarball + Vite/webpack) 경로에서는 oe.broadcast.mic.start() 호출 시 SDK 가 MIC 모듈을 동적으로 로드합니다. 번들러가 이를 별도 청크로 코드 스플릿하므로, TTS 전용 앱은 core 번들에 실시간 미디어 엔진이 포함되지 않아 번들 크기 증가를 피할 수 있습니다. 추가 설정은 필요 없습니다.
createClient({ micChunkUrl })와 createClient({ scriptNonce })는 번들러 경로 전용 옵션입니다. MIC 코드의 URL을 직접 지정하거나 CSP nonce 를 주입할 때 사용하세요. self-host UMD 에서는 둘 다 무의미합니다.
WebRTC 지원 확인과 프리로드
지원 여부는 화면 진입 시 한 번 oe.broadcast.mic.isSupported()로 확인하세요. start() 직전 지연을 줄이려면 oe.broadcast.mic.preload()로 MIC 코드를 미리 로드할 수 있습니다. 중복 호출해도 안전합니다.
기본 정보
| 메서드 | 앱 capability | 참조 handle 조건 | 반환 타입 |
|---|---|---|---|
oe.broadcast.mic.isSupported() | 없음(클라이언트 전용 — 서버 호출 없음, 동기) | 없음 | boolean |
oe.broadcast.mic.preload() | 없음(클라이언트 전용 — MIC 코드 사전 로드, single-flight) | 없음 | Promise<void> |
요청
두 메서드 모두 입력 파라미터가 없습니다.
응답
isSupported()는 WebRTC(RTCPeerConnection) 지원 여부를 즉시 반환합니다(동기, Promise 아님). preload()는 로드가 끝나면 resolve 되는 Promise<void>이며 반환값은 없습니다.
예제
if (!oe.broadcast.mic.isSupported()) {
// "이 브라우저는 WebRTC 를 지원하지 않습니다" 안내를 표시하고 시작 UI 는 렌더하지 않습니다.
} else {
await oe.broadcast.mic.preload();
}
대표 에러
| 에러 | HTTP 상태 | 원인 | 해결 방법 |
|---|---|---|---|
MicChunkLoadFailed | - | preload() 중 MIC 코드 청크 다운로드 실패(번들러 경로) — 네트워크 또는 CSP 차단 | CSP script-src 설정을 먼저 확인한 뒤 재시도하세요(isSupported()는 동기 함수로 에러를 던지지 않음) |
MIC 시작 — oe.broadcast.mic.start()
마이크 권한을 요청하고 실시간 오디오 룸에 연결한 뒤 방송 세션을 시작합니다.
기본 정보
| 메서드 | 앱 capability | 참조 handle 조건 | 반환 타입 |
|---|---|---|---|
oe.broadcast.mic.start(input) | MIC_EXECUTE | targetHandle 또는 deviceHandles 가 유효한 노출 대상이어야 함(그룹은 ASSIGNED 디바이스 전체로 전개) | Promise<MicSession> |
옵션
| 이름 | 타입 | 설명 | 기본값 | 필수 |
|---|---|---|---|---|
targetHandle | string | 대상 디바이스 또는 그룹 handle | - | 조건부 |
deviceHandles | string[] | 대상 디바이스 handle 다중 지정 | - | 조건부 |
partial | boolean | 일부 대상이 사용 중이어도 나머지 대상으로 시작할지 여부 | - | X |
idempotencyKey | string | 중복 방송 방지 키 | - | O |
onSessionEnded | (reason: string) => void | 서버가 세션을 종료했거나 인증이 끊겼을 때 최대 1회 호출되는 콜백 | - | X |
주의:
targetHandle과deviceHandles는 동시에 지정할 수 없습니다. 둘 중 정확히 하나만 지정하세요.
세션 객체
start()는 마이크 확보와 실시간 오디오 룸 연결을 마친 뒤 세션 객체를 반환합니다.
| 이름 | 타입 | 설명 |
|---|---|---|
sessionId | string | 방송 세션 ID |
resolvedDeviceCount | number | 대상으로 확정된 디바이스 수 |
stop() | () => Promise<void> | 세션을 종료합니다. 멱등 — 여러 번 호출해도 안전합니다 |
이벤트
| 이벤트 | 호출 시점 | 페이로드 |
|---|---|---|
onSessionEnded | 서버가 세션을 종료했거나 인증이 연속 실패로 끊겼을 때. 파트너 코드가 직접 stop()을 호출한 경우에는 호출되지 않음 | reason: string(서버 종착 사유 코드 또는 "AUTH_FAILED") |
참고:
reason값 집합은 확장될 수 있으므로, 알려진 값만 분기하지 말고 모든 값을 종료로 처리하세요.
예제
const session = await oe.broadcast.mic.start({
targetHandle,
idempotencyKey: crypto.randomUUID(),
});
const session2 = await oe.broadcast.mic.start({
deviceHandles,
idempotencyKey: crypto.randomUUID(),
});
대표 에러
| 에러 | HTTP 상태 | 원인 | 해결 방법 |
|---|---|---|---|
CapabilityDenied | 403 | 앱에 MIC_EXECUTE capability grant 가 없음 | Open Echo 지원 채널로 grant 를 요청하세요 |
TargetUnresolved | 404 | targetHandle/deviceHandles 를 해석할 수 없음 | 대상 handle 이 유효한지 대상 목록을 다시 조회해 확인하세요 |
IdempotencyKeyReused | 409 | 동일한 idempotencyKey 로 다른 페이로드를 재전송함 | 새 방송마다 새 키를 생성하세요 |
MicUnavailable | - | 마이크 획득 실패(권한 거부·장치 없음 등) | 아래 주의 박스를 참고해 err.reason으로 안내하세요 |
MicChunkLoadFailed | - | MIC 코드 청크 다운로드 실패(번들러 경로) — 네트워크 또는 CSP 차단 | CSP script-src 설정을 먼저 확인하세요 |
주의:
MicUnavailable은 서버에 요청이 도달하기 전, 로컬 마이크 확보 단계에서 발생합니다 — 세션도 디바이스 점유도 생성되지 않습니다.err.reason으로 원인을 구분해 안내하세요:"permission-denied"(마이크 권한 거부) ·"not-found"(마이크 장치 없음) ·"other"(그 외). 동일한idempotencyKey로 재시도해도 마이크 실패 시에는 서버 결과가 재현되지 않고 다시MicUnavailable이 반환됩니다.
조인 상태 확인 — devices[]
start()가 반환하는 MicSession은 상태를 스스로 밀어주지 않습니다. 진행 상황은 oe.broadcast.subscribe()로 구독해서 확인하세요.
MIC 방송의 devices[]는 TTS 의 재생 상태와 달리 "조인 결과"입니다. 값은 PENDING·JOINED·FAILED·TIMEOUT 중 하나이며, 세션이 종결된 뒤에도 각 행은 마지막 값을 유지합니다.
const session = await oe.broadcast.mic.start({
targetHandle,
idempotencyKey: crypto.randomUUID(),
});
const sub = oe.broadcast.subscribe(session.sessionId, {
onPhase: (snapshot) => {
for (const d of snapshot.devices ?? []) {
console.log(d.deviceName, d.status);
}
const joined = (snapshot.devices ?? []).filter((d) => d.status === "JOINED").length;
console.log(`${joined} / ${session.resolvedDeviceCount} 대 조인`);
},
onError: (err) => {
console.error(err.code);
},
});
// 화면 이탈 시 반드시 정리
sub.close();
session.resolvedDeviceCount(대상으로 확정된 디바이스 수)와 snapshot.devices.length(현재까지 조인 시도가 관측된 수)를 비교하면 몇 대 중 몇 대가 응답했는지 진행 표시에 쓸 수 있습니다.
devices가 빈 배열로 도착하면 snapshot.skippedAtDispatch(정수)로 원인을 구분하세요. 0보다 크면 전 대상이 송출 시점에 skip 되어 정상적으로 빈 것이고, 0이거나 없으면 devices[] 투영을 지원하지 않는 이전 버전 서버의 응답일 가능성이 있습니다.
진행 스냅샷과 이벤트 구조는 진행 구독을 참조하세요.
서버측 종료 감지 — onSessionEnded 콜백
SDK는 세션이 살아있는 동안 내부적으로 heartbeat 를 보냅니다. 서버가 세션을 종료하거나 인증이 끊기면, SDK가 콜백 지정 여부와 무관하게 마이크와 실시간 오디오 룸 연결을 즉시 정리합니다. UI를 맞추려면 onSessionEnded로 이 시점을 감지하세요.
const session = await oe.broadcast.mic.start({
targetHandle,
idempotencyKey: crypto.randomUUID(),
onSessionEnded: (reason) => {
console.warn("MIC 방송이 서버 측 사유로 종료되었습니다:", reason);
},
});
로컬에서 session.stop()을 직접 호출한 경우에는 onSessionEnded가 발화하지 않습니다. 파트너 코드가 이미 종료를 알고 있기 때문입니다. 이 콜백은 파트너가 모르는 사이 세션이 끊겼을 때만 UI를 맞추기 위한 것입니다.
종착 사유 코드 전체 목록은 에러 처리를 참조하세요.
방송 중지
MicSession.stop()으로 진행 중인 세션을 종료합니다.
기본 정보
| 메서드 | 앱 capability | 참조 handle 조건 | 반환 타입 |
|---|---|---|---|
session.stop() | 없음(소유권만) | stop()을 호출한 MicSession이 담고 있는 sessionId가 호출 앱 소유의 세션이어야 함(발급받은 세션 객체에 이미 바인딩되어 있으므로 별도 인자로 넘기지 않음) | Promise<void> |
요청
입력 파라미터가 없습니다(session.stop() 자체가 이미 발급받은 세션에 바인딩됨).
응답
반환값이 없습니다(Promise<void>). 멱등 — 이미 종료된 세션에 다시 호출해도 안전합니다.
예제
const session = await oe.broadcast.mic.start({
targetHandle,
idempotencyKey: crypto.randomUUID(),
});
try {
await session.stop();
} catch {
// stop 실패해도 UI 는 종료 상태로 전환하는 것을 권장합니다.
}
주의: 페이지를 벗어나기 전에는 반드시
stop()을 호출해 마이크가 열린 채로 남지 않도록 하세요.
대표 에러
생략(사유: stop()은 멱등 설계 — 이미 종료된(TERMINAL/STOPPING) 세션에 재호출해도 에러를 던지지 않고 조용히 무시됩니다. 세션 소유권 불일치는 발급받은 세션 객체에 이미 바인딩되어 있어 실무상 발생하지 않는 경로입니다)
참고 코드
레퍼런스 앱의 MIC 방송 화면 구현은 partner-app/src/features/mic/MicBroadcast.tsx에서 확인할 수 있습니다.

