진행 구독 (SSE)
개요
oe.broadcast.subscribe()로 TTS·MIC 방송 세션의 진행 상태를 Server-Sent Events(SSE)로 실시간 구독합니다. 연결 직후 현재 스냅샷을 1회 받고, 이후 상태가 바뀔 때마다 새 스냅샷을 받습니다. terminal: true인 스냅샷을 받으면 더 이상 이벤트가 오지 않으므로 구독을 종료하세요.
시작하기 전에
- 파트너 앱에
HISTORYcapability grant 가 있어야 합니다. - 구독할
sessionId가 호출 앱 소유의 방송 세션이어야 합니다. TTS 방송의send()/fromContent()가 반환하는sessionId, 또는 MIC 방송의start()가 반환하는MicSession.sessionId를 사용하세요.
방송 진행 구독 — oe.broadcast.subscribe()
방송 세션의 진행 상태를 Server-Sent Events(SSE)로 실시간 구독합니다.
기본 정보
| 메서드 | 앱 capability | 참조 handle 조건 | 반환 타입 |
|---|---|---|---|
oe.broadcast.subscribe(sessionId, options) | HISTORY | sessionId 가 호출 앱 소유의 방송 세션이어야 함 | { close(): void } |
옵션
| 이름 | 타입 | 설명 | 기본값 | 필수 |
|---|---|---|---|---|
onPhase | (snapshot: PhaseSnapshot) => void | 진행 스냅샷 수신 콜백 | - | O |
onError | (err: OpenEchoError) => void | 에러 수신 콜백 | - | X |
onClose | () => void | 구독 종료 통지 콜백. 원인과 무관하게 정확히 1회 호출됨 | - | X |
세션 객체
subscribe() 는 호출 즉시 세션 객체를 반환합니다.
| 이름 | 타입 | 설명 |
|---|---|---|
close() | () => void | 구독을 수동으로 종료합니다. 멱등 — 여러 번 호출해도 안전합니다 |
이벤트
| 이벤트 | 호출 시점 | 페이로드 |
|---|---|---|
onPhase | 스냅샷 수신마다(연결 직후 현재 스냅샷 1회 포함) | PhaseSnapshot |
onError | 복구 불가능한 에러 발생 시(throw 되지 않음) | OpenEchoError |
onClose | 구독 종료 시(terminal 수신·에러·close() 호출 — 원인 무관 1회) | 없음 |
참고:
terminal: true인PhaseSnapshot을 수신하면 구독을 종료해야 합니다.
예제
const sub = oe.broadcast.subscribe(sessionId, {
onPhase: (snapshot) => {
console.log(snapshot.phase, snapshot.terminal);
if (snapshot.terminal) {
sub.close();
}
},
onError: (err) => {
console.error(err.code, err.httpStatus, err.message);
},
});
// 구독 수동 종료
sub.close();
대표 에러
| 에러 | HTTP 상태 | 원인 | 해결 방법 |
|---|---|---|---|
TicketUnauthorized | 401 | SSE 스트림 티켓 인증 실패 | 새로 구독을 시작하세요 |
SessionExpired | 410 | 세션의 이벤트 보존 기간이 지남 | 재연결로 복구할 수 없습니다. 새 방송을 시작하세요 |
PhaseSnapshot 필드
| 필드 | 타입 | 설명 |
|---|---|---|
sessionId | string | 방송 세션 ID |
phase | string | 현재 방송 단계 |
terminal | boolean | true이면 더 이상 이벤트가 발생하지 않는 최종 상태 |
reason | string | null | 실패 시 사유 |
lastEventId | number | SSE 재연결 마커. SDK가 내부적으로 Last-Event-ID 헤더에 사용합니다 |
receipt | BroadcastReceipt | null | terminal 시점의 방송 결과(resolvedDeviceCount·skipped 등) |
devices | DeviceProgress[] | 디바이스별 진행 상태입니다. 제공되지 않을 수 있으므로 항상 snapshot.devices ?? []처럼 처리하세요 |
skippedAtDispatch | number | 송출 시점에 skip 된 대상 수입니다. 제공되지 않을 수 있습니다 |
phase 값 의미
| phase | terminal | 설명 |
|---|---|---|
SYNTHESIZING | false | TTS 음성 합성 진행 중(MIC 세션에는 나타나지 않음) |
PLAYING | false | 디바이스에서 재생 중 |
DONE | true | 모든 대상 방송 완료 |
PARTIAL | true | 부분 완료. 일부 디바이스가 제외되었거나 실패했습니다. 제외 디바이스는 receipt.skipped로 확인하세요 |
FAILED | true | 방송 실패 |
STOPPED | true | stop()으로 중단됨 |
terminal: true가 수신되면 구독을 종료해야 합니다.
디바이스별 진행 — devices
snapshot.devices는 세션 내 각 디바이스의 진행 상태입니다. 같은 phase 안에서도 디바이스 상태가 바뀌면 새 스냅샷이 전달됩니다.
TTS 방송에서 status 는 PENDING·PLAYING·DONE·FAILED 중 하나이며 실시간 재생 상태를 반영합니다.
const sub = oe.broadcast.subscribe(sessionId, {
onPhase: (snapshot) => {
for (const d of snapshot.devices ?? []) {
console.log(d.deviceName, d.status);
}
},
});
MIC 방송의 devices 어휘와 해석은 MIC 방송을 참조하세요. TTS와 다른 값 집합을 쓰며, 재생 상태가 아니라 조인 결과를 나타냅니다.
종결 판정 — terminal 스냅샷 처리
구독을 시작하면 현재 스냅샷을 1회 즉시 받고, 이후 상태가 바뀔 때마다 새 스냅샷을 받습니다. terminal: true인 스냅샷을 받으면 구독을 종료하세요.
receipt.skipped와 receipt.resolvedDeviceCount를 함께 보면 부분 완료 여부를 판정할 수 있습니다.
let sub: { close(): void };
sub = oe.broadcast.subscribe(sessionId, {
onPhase: (snapshot) => {
const partial =
snapshot.terminal &&
snapshot.receipt != null &&
snapshot.receipt.resolvedDeviceCount > 0 &&
(snapshot.receipt.skipped?.length ?? 0) > 0;
console.log(snapshot.phase, snapshot.terminal, partial);
if (snapshot.terminal) {
sub.close();
}
},
});
에러 처리 — onError 콜백
SSE 에러는 throw되지 않고 onError 콜백으로 전달됩니다. 에러 객체는 OpenEchoError를 상속하며 code와 httpStatus를 보유합니다.
oe.broadcast.subscribe(sessionId, {
onPhase: (snap) => {},
onError: (err) => {
console.error(err.code, err.httpStatus);
},
});
종료 통지 — onClose 콜백
onClose는 구독이 끝날 때, 즉 terminal phase 수신·복구 불가 에러·close() 호출 중 무엇이 원인이든 정확히 1회 호출됩니다. 원인별 분기(onPhase의 terminal / onError)와 별개로 종료 시 정리 로직을 한곳에 두고 싶을 때 사용하세요.
const sub = oe.broadcast.subscribe(sessionId, {
onPhase: (snap) => {},
onError: (err) => {},
onClose: () => {
console.log("구독 종료");
},
});
terminal phase 수신 시 SDK는 내부적으로 스트림을 정리하고 onClose를 호출합니다. 이후 close()를 추가로 호출해도 안전합니다(멱등).
세션 만료 — SessionExpired
방송 세션의 이벤트 보존 기간이 지난 후 구독하면 SessionExpired 에러가 onError로 전달됩니다. 이 경우 재연결해도 동일한 세션을 복구할 수 없습니다.
자동 재연결
일시적인 네트워크 단절이나 SSE 연결 중단 시 SDK가 lastEventId를 Last-Event-ID 헤더에 실어 자동으로 재연결합니다. 애플리케이션 코드에서 별도 처리가 필요하지 않습니다.
단, SessionExpired가 onError로 전달된 경우는 서버가 이벤트를 더 이상 보존하지 않는 것이므로 자동 재연결 대상이 아닙니다.
TTS·MIC 방송과의 연계
TTS 방송 또는 MIC 방송을 시작한 직후 반환되는 sessionId로 바로 구독하는 것이 일반적인 패턴입니다.
const receipt = await oe.broadcast.tts.send({
targetHandle,
text: "출입을 통제합니다",
language: "ko",
idempotencyKey: crypto.randomUUID(),
});
if (!receipt.sessionId) {
console.warn("방송 거부:", receipt.denyReason);
} else {
oe.broadcast.subscribe(receipt.sessionId, {
onPhase: (snap) => {
console.log(snap.phase, snap.terminal, snap.receipt);
},
onError: (err) => {
console.error(err.code, err.httpStatus);
},
});
}
기존 세션 ID만 있으면 방송을 시작한 화면이 아니어도 oe.broadcast.subscribe(sessionId, ...)로 사후 조회·디버깅 구독이 가능합니다.
참고 코드
레퍼런스 앱의 구독 통합 패턴은 partner-app/src/features/tts/TtsBroadcast.tsx에서 확인할 수 있습니다.

