廣播觀眾 SSE
連線資訊
| 項目 | 值 |
|---|---|
| 基礎路徑 | https://vas-poc.vurbo.ai/broadcast |
| 協定 | HTTP + Server-Sent Events (SSE) |
| 資料格式 | text/event-stream |
| 認證方式 | Token 認證(無需 API Key) |
注意:Broadcast SSE 的基礎路徑與其他 SSE API 不同。
端點總覽
| 方法 | 端點 | 說明 |
|---|---|---|
| GET | /broadcast/{token}/text | 觀眾即時字幕串流 |
GET /broadcast/{token}/text
功能說明
觀眾透過分享 Token 連線,接收即時轉錄和翻譯的 SSE 串流。
使用場景
- 觀眾觀看即時字幕
- 多語言翻譯字幕顯示
- TTS 語音播放
認證方式
Token 認證(無需 API Key):透過 URL 路徑中的 {token} 進行驗證。
密碼保護說明:當廣播設定為密碼保護時,觀眾必須先透過密碼驗證 API 取得
viewer_access_token,再將此 Token 帶入 SSE 連線的 Query Parameter 中。詳見 認證機制。
請求參數
| 參數 | 位置 | 類型 | 必填 | 說明 |
|---|---|---|---|---|
token | path | string | 是 | 廣播分享 Token(4 字元短碼,字符集 a-z0-9) |
lang | query | string | 否 | 篩選特定翻譯語言(如 en-US) |
tts | query | boolean | 否 | 是否啟用 TTS(true / false,預設 false) |
viewer_access_token | query | string | 條件 | 觀眾存取 Token(密碼保護廣播時必填) |
請求範例
// 接收所有語言
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text'
);
// 只接收英文翻譯
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text?lang=en-US'
);
// 接收英文翻譯並啟用 TTS
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text?lang=en-US&tts=true'
);
// 密碼保護廣播
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text?viewer_access_token=xxx'
);
事件類型
| 事件 | 說明 | 備註 |
|---|---|---|
connected | 連線確認 | - |
queued | 已加入等待佇列 | 排隊機制 |
admitted | 從佇列進入直播 | 排隊機制 |
origin | 原文(STT) | - |
translation | 翻譯結果 | - |
tts_ready | TTS 音訊就緒 | - |
paused | 廣播暫停 | 主講者手動暫停(主辦斷線另送 host_disconnected) |
resumed | 廣播恢復 | 主講者恢復 |
ended | 廣播結束 | - |
kicked | 被踢除 | 觀眾管理 |
speaker_renamed | 說話者重命名 | - |
speaker_reassigned | 單句說話者修改 | - |
speakers_merged | 語者合併 | - |
recording_started | 新錄音開始 | - |
max_viewers_changed | 觀眾上限變更 | - |
access_type_changed | 注意:已不再送出 —— 存取類型改成需要密碼時,觀眾收到的是 kicked(需重新驗證) | - |
pass_code_changed | 注意:已不再送出 —— 密碼變更時,觀眾收到的是 kicked(需重新驗證) | - |
host_disconnected | 主辦暫時離線、重連中(廣播凍結但未結束) | - |
host_reconnected | 主辦已重新連線(data.resumed 指出是否同時恢復播放) | - |
standby | 預備階段通知 | - |
phase_changed | 階段變更通知 | - |
announcement | 主講者公告 | - |
error | 錯誤 | - |
事件格式
1. connected - 連線確認
{
"available_langs": ["en-US", "ja-JP"],
"tts_languages": ["en-US"],
"phase": "standby",
"recognition_mode": "single"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
available_langs | array | 可用的翻譯語言列表 |
tts_languages | array | 有啟用 TTS 的語言列表(空陣列表示無 TTS) |
phase | string | 廣播階段:standby(預備)或 live(正式) |
recognition_mode | string | 辨識模式:single(單人)或 multi_speaker(多人語者分離) |
2. queued - 排隊中
注意:以下多個事件的
message/estimated_wait是伺服器固定回傳的顯示字串。請直接呈現或改用自家文案,不要解析、也不要拿來做條件判斷。
{
"position": 3,
"estimated_wait": "約 2 分鐘"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
position | number | 佇列中的位置(1 = 下一個) |
estimated_wait | string | 預估等待時間。伺服器固定值:少於 1 分鐘 或 約 N 分鐘 |
3. admitted - 進入直播
{
"message": "已進入直播"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 提示訊息(伺服器回傳的固定字串,僅供顯示) |
4. origin - 原文(STT)
{
"sid": 1,
"text": "大家好",
"is_final": true,
"language": "zh-TW",
"speaker_id": "Guest-1",
"speaker_label": "Royx",
"start_time": "00:05"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 句子 ID |
text | string | 原文內容 |
is_final | boolean | 是否為最終結果 |
language | string | 原文語言 |
speaker_id | string | 原始說話者 ID(多人對話模式,可選;不可變) |
speaker_label | string | 顯示標籤(多人對話模式,可選;套用 speaker_aliases 後,無 alias 時等於 speaker_id) |
start_time | string | 句子開始時間,格式 mm:ss(與主辦方 WS origin 及 History init_sentence 對齊),從開播(live)起算,由 00:00 開始 |
觀眾只會收到開播之後的原文與翻譯。預備階段(
standby)的內容不會送給觀眾,開播後才連線的觀眾也不會在先前內容中看到。
5. translation - 翻譯結果
{
"sid": 1,
"language": "en-US",
"text": "Hello everyone",
"speaker_id": "Guest-1",
"speaker_label": "Royx",
"is_final": true
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 對應的句子 ID |
language | string | 翻譯語言 |
text | string | 翻譯內容 |
speaker_id | string | 原始說話者 ID(多人對話模式,可選;不可變) |
speaker_label | string | 顯示標籤(多人對話模式,可選;套用 speaker_aliases 後,無 alias 時等於 speaker_id) |
is_final | boolean | 是否為最終結果 |
同一句的翻譯可能先送出數次
is_final: false的中間結果,最後才送出is_final: true的定稿版本,請以sid加language對應並覆蓋顯示;只想顯示定稿結果時,略過is_final: false即可。中途進場的觀眾會補送最近幾句的原文與已定稿的翻譯,中間結果不會補送,也不保證涵蓋開播以來的全部內容。
6. tts_ready - TTS 音訊
{
"sid": 1,
"language": "en-US",
"transcript": "你好",
"text": "Hello",
"audio": "base64...",
"format": "mp3",
"duration_ms": 2340,
"boundaries": [
{
"offset_ms": 0,
"duration_ms": 320,
"text": "Hello",
"text_offset": 0,
"word_length": 5
}
]
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 對應的句子 ID |
language | string | TTS 語言 |
transcript | string | 原始逐字稿(原文) |
text | string | 翻譯後的文字 |
audio | string | Base64 編碼的 MP3 音訊 |
format | string | 音訊格式,固定為 "mp3" |
duration_ms | number | 音訊時長(毫秒) |
boundaries | array | Word Boundaries(可選,見下表) |
Word Boundaries 欄位(boundaries 陣列中的每個物件):
| 欄位 | 類型 | 說明 |
|---|---|---|
offset_ms | number | 該單字在音訊中的開始時間(ms) |
duration_ms | number | 該單字的發音時長(ms) |
text | string | 單字文字 |
text_offset | number | 該單字在文字中的起始位置 |
word_length | number | 該單字的字元長度 |
注意:
- 主講者需在
start指令中透過tts_config參數指定哪些語言啟用 TTS- 只有訂閱該語言且啟用 TTS 的觀眾會收到此事件
- 只在
live階段發送,standby階段不會發送 TTS
7. paused - 暫停
{
"message": "廣播已暫停"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 提示訊息 |
8. resumed - 恢復
{
"message": "廣播已恢復"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 提示訊息 |
9. ended - 結束
{
"message": "廣播已結束"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 結束訊息 |
10. kicked - 被踢除
{
"message": "Kicked by host"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 被踢除原因,僅供顯示、請勿解析。主講者手動踢除時為固定值 Kicked by host;因存取類型或密碼變更而被踢除時,為說明變更原因的中文提示 |
11. speaker_renamed - 說話者重命名
多人對話模式專用。當主播執行全域重命名說話者時發送。
{
"speaker_id": "Guest-1",
"new_label": "Royx",
"affected_sids": [1, 3, 5]
}
| 欄位 | 類型 | 說明 |
|---|---|---|
speaker_id | string | 解析後的原始語者 ID(即使輸入是顯示標籤,事件回傳仍是原始 ID) |
new_label | string | 新顯示標籤(如 Royx) |
affected_sids | array | 受影響的句子 ID 列表 |
12. speaker_reassigned - 語者身份修改
多人對話模式專用。當主播修改單句的說話者時發送。
{
"sid": 3,
"old_speaker_id": "Guest-1",
"new_speaker_id": "Guest-2",
"new_speaker_label": "Amy"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
sid | number | 被修改的句子 ID |
old_speaker_id | string | 原始語者 ID(如 Guest-1) |
new_speaker_id | string | 新的原始語者 ID(如 Guest-2) |
new_speaker_label | string | 新語者顯示標籤(套用 speaker_aliases 後;無 alias 時等於 new_speaker_id) |
13. speakers_merged - 語者合併
多人對話模式專用。當主播合併語者時發送。合併後,該語者的所有句子會歸屬到目標語者。
{
"source_speaker_id": "Guest-2",
"target_speaker_id": "Guest-1",
"target_speaker_label": "王經理",
"affected_sids": [3, 5, 7]
}
| 欄位 | 類型 | 說明 |
|---|---|---|
source_speaker_id | string | 被合併的原始語者 ID(如 Guest-2) |
target_speaker_id | string | 合併目標的原始語者 ID(如 Guest-1) |
target_speaker_label | string | 目標語者顯示標籤(套用 speaker_aliases 後;無 alias 時等於原始 ID) |
affected_sids | array | 受影響的句子 ID 列表:原屬來源語者的句子,加上顯示名稱因合併而改變的目標語者原有句子(例如來源語者的自訂名稱轉給目標語者時) |
14. recording_started - 新錄音開始
當主講者開始新的錄音段時發送。觀眾收到此事件後應清除畫面上的舊字幕。
此事件不帶任何資料(data 為空)。
15. max_viewers_changed - 觀眾上限變更
當主講者變更觀眾上限時發送。
{
"max_viewers": 200
}
| 欄位 | 類型 | 說明 |
|---|---|---|
max_viewers | number | 新的觀眾上限 |
16. access_type_changed - 存取類型變更
注意:此事件已停用(名稱保留供相容),伺服器不會送出。存取類型改為需要密碼時,觀眾收到的是
kicked(message為變更說明),須重新驗證密碼後重連。
17. pass_code_changed - 密碼變更
注意:此事件已停用(名稱保留供相容),伺服器不會送出。密碼變更時,觀眾收到的是
kicked(message為變更說明),須重新驗證密碼後重連。
18. standby - 預備階段
當觀眾在預備階段連線時,會在
connected事件後立即收到此事件,表示廣播尚未正式開始。 主講者可透過 WebSocketset_standby_messageaction 動態更新預備訊息,更新後所有觀眾會收到新的standby事件。
{
"message": "演講即將開始...",
"translations": {
"en-US": "The presentation is about to begin...",
"ja-JP": "プレゼンテーションがまもなく始まります..."
}
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 預備階段顯示訊息(原文) |
translations | object | 翻譯結果(可選),key 為語言代碼,value 為翻譯文字 |
19. phase_changed - 階段變更
當廣播從預備階段切換到正式階段時發送。
{
"phase": "live",
"message": "廣播已開始"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
phase | string | 新階段:live(正式階段) |
message | string | 階段變更訊息 |
20. announcement - 公告
主講者發送的公告訊息,所有觀眾都會收到。
{
"message": "會議5分鐘後結束",
"translations": {
"en-US": "The meeting will end in 5 minutes",
"ja-JP": "会議は5分後に終了します"
}
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 公告訊息內容(原文) |
translations | object | 翻譯結果(可選),key 為語言代碼,value 為翻譯文字 |
21. error - 錯誤
{
"error_code": "broadcast_session_ended",
"message": "Broadcast session ended",
"severity": "error"
}
句子級錯誤(如某語言的翻譯失敗)會額外帶 sid 與 translation_language,供觀眾端標示某句的某個語言失敗:
{
"error_code": "llm_content_filtered",
"severity": "warning",
"message": "Content filtered",
"context": "translation",
"sid": 5,
"translation_language": "ja-JP",
"timestamp": "2026-04-26T10:30:45.123Z"
}
Session-level 翻譯服務錯誤(連續失敗達閾值升級,v1.3.8 新增)不帶 sid 與 translation_language,廣播給所有觀眾,前端應顯示全域提示但連線仍維持:
{
"error_code": "translation_service_unavailable",
"severity": "error",
"message": "Translation service unavailable",
"context": "translation",
"timestamp": "2026-04-26T10:30:45.123Z",
"details": {
"provider": "llm_service",
"last_error_code": "llm_provider_error",
"fail_count": 5
}
}
| 欄位 | 類型 | 說明 |
|---|---|---|
error_code | string | 錯誤碼 |
message | string | 錯誤訊息(伺服器回傳的固定英文顯示文字,僅供顯示、請勿解析) |
severity | string | 嚴重度:warning / error / fatal |
context | string | 可選。錯誤上下文(如 broadcast、translation) |
sid | int | 可選。句子級錯誤的句子編號(如該句翻譯失敗);session-level 錯誤不帶 |
translation_language | string | 可選。翻譯失敗的目標語言(觀眾依此判斷是否該語言相關) |
details | object | 可選。除錯資訊(如 translation_service_unavailable 帶 provider、last_error_code、fail_count) |
timestamp | string | 可選。錯誤發生時間(ISO 8601) |
request_id | string | 可選。請求追蹤 ID;僅連線階段錯誤與排隊逾時錯誤帶出,句子級/會話級錯誤不帶 |
22. host_disconnected - 主辦斷線
主辦端連線中斷、進入重連寬限期時發送。廣播被凍結但尚未結束,觀眾不應關閉連線,只需顯示提示等待
host_reconnected或ended。
{
"message": "主辦暫時離線,重連中"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 提示訊息(伺服器回傳的固定字串,僅供顯示) |
23. host_reconnected - 主辦重新連線
主辦端續接成功時發送。
{
"message": "主辦已重新連線",
"resumed": true
}
| 欄位 | 類型 | 說明 |
|---|---|---|
message | string | 提示訊息(伺服器回傳的固定字串,僅供顯示) |
resumed | boolean | 是否同時恢復播放。false 代表主辦在斷線前已手動暫停,續接後維持暫停狀態 |
心跳機制
SSE 連線使用心跳保持連線活躍:
- 間隔:15 秒
- 格式:SSE 註解(以
:開頭) - 前端無需處理,瀏覽器會自動忽略
: heartbeat
特有錯誤碼
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
broadcast_session_not_found | 404 | 找不到廣播(Token 不存在或已撤銷皆回此碼) | 確認 Token 正確 |
broadcast_not_ready | 503 | Token 有效,但廣播尚未啟動或已結束 | 稍後重試 |
broadcast_session_not_started | 503 | 廣播尚未開始 | 稍後重試 |
broadcast_session_ended | 410 | 廣播已結束 | 提示使用者廣播結束 |
broadcast_capacity_exceeded | 503 | 觀眾人數與等待佇列皆已滿 | 稍後重試 |
broadcast_queue_timeout | -(連線中的 error 事件) | 排隊等待超過 5 分鐘 | 重新連線 |
broadcast_password_required | 401 | 需要密碼驗證 | 引導觀眾輸入密碼並取得 viewer_access_token |
translation_service_unavailable | - | 翻譯服務連續失敗達閾值(v1.3.8 新增,不帶 sid) | 顯示全域提示「翻譯暫不可用」,不需斷線,原文(STT)仍正常推送 |
前端範例
function connectBroadcast(token, lang = null) {
let url = `https://vas-poc.vurbo.ai/broadcast/${token}/text`;
if (lang) {
url += `?lang=${lang}`;
}
const eventSource = new EventSource(url);
eventSource.addEventListener('connected', (e) => {
const data = JSON.parse(e.data);
console.log(`階段:${data.phase},模式:${data.recognition_mode}`);
console.log(`可用翻譯:${data.available_langs.join(', ')}`);
});
eventSource.addEventListener('queued', (e) => {
const data = JSON.parse(e.data);
console.log(`排隊中,位置:${data.position},預估等待:${data.estimated_wait}`);
});
eventSource.addEventListener('admitted', (e) => {
console.log('已進入直播');
});
eventSource.addEventListener('origin', (e) => {
const data = JSON.parse(e.data);
console.log(`[SID ${data.sid}] ${data.text}`);
});
eventSource.addEventListener('translation', (e) => {
const data = JSON.parse(e.data);
console.log(`翻譯 (${data.language}): ${data.text}`);
});
eventSource.addEventListener('tts_ready', (e) => {
const data = JSON.parse(e.data);
// 解碼 Base64 MP3 並播放
const audioBlob = base64ToBlob(data.audio, 'audio/mp3');
const audioUrl = URL.createObjectURL(audioBlob);
const audio = new Audio(audioUrl);
audio.play();
});
eventSource.addEventListener('paused', (e) => {
const data = JSON.parse(e.data);
console.log(`直播暫停:${data.message}`);
});
eventSource.addEventListener('resumed', (e) => {
console.log('直播已恢復');
});
eventSource.addEventListener('ended', (e) => {
const data = JSON.parse(e.data);
console.log(`直播結束:${data.message}`);
eventSource.close();
});
eventSource.addEventListener('kicked', (e) => {
const data = JSON.parse(e.data);
console.log(`已離開直播:${data.message}`);
eventSource.close();
// 存取類型或密碼變更也會走這裡,需引導使用者重新驗證後再連線
});
eventSource.addEventListener('speaker_renamed', (e) => {
const data = JSON.parse(e.data);
console.log(`說話者重命名:${data.speaker_id} → ${data.new_label}`);
// 更新所有受影響句子的顯示標籤
});
eventSource.addEventListener('speaker_reassigned', (e) => {
const data = JSON.parse(e.data);
console.log(`句子 ${data.sid} 的說話者改為:${data.new_speaker_label}`);
// 更新該句子的顯示標籤
});
eventSource.addEventListener('speakers_merged', (e) => {
const data = JSON.parse(e.data);
console.log(`語者合併:${data.source_speaker_id} → ${data.target_speaker_label}`);
// 更新所有受影響句子的顯示標籤
});
eventSource.addEventListener('recording_started', () => {
console.log('新錄音開始,清除舊字幕');
// 清除畫面上的舊字幕
});
eventSource.addEventListener('max_viewers_changed', (e) => {
const data = JSON.parse(e.data);
console.log(`觀眾上限變更為:${data.max_viewers}`);
});
eventSource.addEventListener('host_disconnected', (e) => {
const data = JSON.parse(e.data);
console.log(`主辦離線:${data.message}`);
// 顯示「主辦重連中」,不要關閉連線
});
eventSource.addEventListener('host_reconnected', (e) => {
const data = JSON.parse(e.data);
console.log(`主辦已回來:${data.message}(已恢復播放:${data.resumed})`);
});
eventSource.addEventListener('standby', (e) => {
const data = JSON.parse(e.data);
const displayLang = 'en-US'; // 觀眾選擇的語言
const displayMessage = data.translations?.[displayLang] || data.message;
console.log(`預備階段:${displayMessage}`);
});
eventSource.addEventListener('phase_changed', (e) => {
const data = JSON.parse(e.data);
console.log(`階段變更:${data.phase} - ${data.message}`);
// 移除等待畫面,開始顯示字幕
});
eventSource.addEventListener('announcement', (e) => {
const data = JSON.parse(e.data);
const displayLang = 'en-US';
const displayMessage = data.translations?.[displayLang] || data.message;
console.log(`公告:${displayMessage}`);
});
eventSource.addEventListener('error', (e) => {
if (e.data) {
const error = JSON.parse(e.data);
console.error(`錯誤 [${error.error_code}]: ${error.message}`);
}
eventSource.close();
});
return eventSource;
}
// Base64 轉 Blob 工具函式
function base64ToBlob(base64, mimeType) {
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mimeType });
}
版本:V1.24.1 最後更新:2026-10-07