SSE API

廣播觀眾 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 中。詳見 認證機制。

請求參數

參數位置類型必填說明
tokenpathstring是廣播分享 Token(4 字元短碼,字符集 a-z0-9)
langquerystring否篩選特定翻譯語言(如 en-US)
ttsqueryboolean否是否啟用 TTS(true / false,預設 false)
viewer_access_tokenquerystring條件觀眾存取 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_readyTTS 音訊就緒-
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_langsarray可用的翻譯語言列表
tts_languagesarray有啟用 TTS 的語言列表(空陣列表示無 TTS)
phasestring廣播階段:standby(預備)或 live(正式)
recognition_modestring辨識模式:single(單人)或 multi_speaker(多人語者分離)

2. queued - 排隊中

注意:以下多個事件的 message / estimated_wait 是伺服器固定回傳的顯示字串。請直接呈現或改用自家文案,不要解析、也不要拿來做條件判斷。

{
  "position": 3,
  "estimated_wait": "約 2 分鐘"
}
欄位類型說明
positionnumber佇列中的位置(1 = 下一個)
estimated_waitstring預估等待時間。伺服器固定值:少於 1 分鐘 或 約 N 分鐘

3. admitted - 進入直播

{
  "message": "已進入直播"
}
欄位類型說明
messagestring提示訊息(伺服器回傳的固定字串,僅供顯示)

4. origin - 原文(STT)

{
  "sid": 1,
  "text": "大家好",
  "is_final": true,
  "language": "zh-TW",
  "speaker_id": "Guest-1",
  "speaker_label": "Royx",
  "start_time": "00:05"
}
欄位類型說明
sidnumber句子 ID
textstring原文內容
is_finalboolean是否為最終結果
languagestring原文語言
speaker_idstring原始說話者 ID(多人對話模式,可選;不可變)
speaker_labelstring顯示標籤(多人對話模式,可選;套用 speaker_aliases 後,無 alias 時等於 speaker_id)
start_timestring句子開始時間,格式 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
}
欄位類型說明
sidnumber對應的句子 ID
languagestring翻譯語言
textstring翻譯內容
speaker_idstring原始說話者 ID(多人對話模式,可選;不可變)
speaker_labelstring顯示標籤(多人對話模式,可選;套用 speaker_aliases 後,無 alias 時等於 speaker_id)
is_finalboolean是否為最終結果

同一句的翻譯可能先送出數次 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
    }
  ]
}
欄位類型說明
sidnumber對應的句子 ID
languagestringTTS 語言
transcriptstring原始逐字稿(原文)
textstring翻譯後的文字
audiostringBase64 編碼的 MP3 音訊
formatstring音訊格式,固定為 "mp3"
duration_msnumber音訊時長(毫秒)
boundariesarrayWord Boundaries(可選,見下表)

Word Boundaries 欄位(boundaries 陣列中的每個物件):

欄位類型說明
offset_msnumber該單字在音訊中的開始時間(ms)
duration_msnumber該單字的發音時長(ms)
textstring單字文字
text_offsetnumber該單字在文字中的起始位置
word_lengthnumber該單字的字元長度

注意:

  • 主講者需在 start 指令中透過 tts_config 參數指定哪些語言啟用 TTS
  • 只有訂閱該語言且啟用 TTS 的觀眾會收到此事件
  • 只在 live 階段發送,standby 階段不會發送 TTS

7. paused - 暫停

{
  "message": "廣播已暫停"
}
欄位類型說明
messagestring提示訊息

8. resumed - 恢復

{
  "message": "廣播已恢復"
}
欄位類型說明
messagestring提示訊息

9. ended - 結束

{
  "message": "廣播已結束"
}
欄位類型說明
messagestring結束訊息

10. kicked - 被踢除

{
  "message": "Kicked by host"
}
欄位類型說明
messagestring被踢除原因,僅供顯示、請勿解析。主講者手動踢除時為固定值 Kicked by host;因存取類型或密碼變更而被踢除時,為說明變更原因的中文提示

11. speaker_renamed - 說話者重命名

多人對話模式專用。當主播執行全域重命名說話者時發送。

{
  "speaker_id": "Guest-1",
  "new_label": "Royx",
  "affected_sids": [1, 3, 5]
}
欄位類型說明
speaker_idstring解析後的原始語者 ID(即使輸入是顯示標籤,事件回傳仍是原始 ID)
new_labelstring新顯示標籤(如 Royx)
affected_sidsarray受影響的句子 ID 列表

12. speaker_reassigned - 語者身份修改

多人對話模式專用。當主播修改單句的說話者時發送。

{
  "sid": 3,
  "old_speaker_id": "Guest-1",
  "new_speaker_id": "Guest-2",
  "new_speaker_label": "Amy"
}
欄位類型說明
sidnumber被修改的句子 ID
old_speaker_idstring原始語者 ID(如 Guest-1)
new_speaker_idstring新的原始語者 ID(如 Guest-2)
new_speaker_labelstring新語者顯示標籤(套用 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_idstring被合併的原始語者 ID(如 Guest-2)
target_speaker_idstring合併目標的原始語者 ID(如 Guest-1)
target_speaker_labelstring目標語者顯示標籤(套用 speaker_aliases 後;無 alias 時等於原始 ID)
affected_sidsarray受影響的句子 ID 列表:原屬來源語者的句子,加上顯示名稱因合併而改變的目標語者原有句子(例如來源語者的自訂名稱轉給目標語者時)

14. recording_started - 新錄音開始

當主講者開始新的錄音段時發送。觀眾收到此事件後應清除畫面上的舊字幕。

此事件不帶任何資料(data 為空)。


15. max_viewers_changed - 觀眾上限變更

當主講者變更觀眾上限時發送。

{
  "max_viewers": 200
}
欄位類型說明
max_viewersnumber新的觀眾上限

16. access_type_changed - 存取類型變更

注意:此事件已停用(名稱保留供相容),伺服器不會送出。存取類型改為需要密碼時,觀眾收到的是 kicked(message 為變更說明),須重新驗證密碼後重連。


17. pass_code_changed - 密碼變更

注意:此事件已停用(名稱保留供相容),伺服器不會送出。密碼變更時,觀眾收到的是 kicked(message 為變更說明),須重新驗證密碼後重連。


18. standby - 預備階段

當觀眾在預備階段連線時,會在 connected 事件後立即收到此事件,表示廣播尚未正式開始。 主講者可透過 WebSocket set_standby_message action 動態更新預備訊息,更新後所有觀眾會收到新的 standby 事件。

{
  "message": "演講即將開始...",
  "translations": {
    "en-US": "The presentation is about to begin...",
    "ja-JP": "プレゼンテーションがまもなく始まります..."
  }
}
欄位類型說明
messagestring預備階段顯示訊息(原文)
translationsobject翻譯結果(可選),key 為語言代碼,value 為翻譯文字

19. phase_changed - 階段變更

當廣播從預備階段切換到正式階段時發送。

{
  "phase": "live",
  "message": "廣播已開始"
}
欄位類型說明
phasestring新階段:live(正式階段)
messagestring階段變更訊息

20. announcement - 公告

主講者發送的公告訊息,所有觀眾都會收到。

{
  "message": "會議5分鐘後結束",
  "translations": {
    "en-US": "The meeting will end in 5 minutes",
    "ja-JP": "会議は5分後に終了します"
  }
}
欄位類型說明
messagestring公告訊息內容(原文)
translationsobject翻譯結果(可選),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_codestring錯誤碼
messagestring錯誤訊息(伺服器回傳的固定英文顯示文字,僅供顯示、請勿解析)
severitystring嚴重度:warning / error / fatal
contextstring可選。錯誤上下文(如 broadcast、translation)
sidint可選。句子級錯誤的句子編號(如該句翻譯失敗);session-level 錯誤不帶
translation_languagestring可選。翻譯失敗的目標語言(觀眾依此判斷是否該語言相關)
detailsobject可選。除錯資訊(如 translation_service_unavailable 帶 provider、last_error_code、fail_count)
timestampstring可選。錯誤發生時間(ISO 8601)
request_idstring可選。請求追蹤 ID;僅連線階段錯誤與排隊逾時錯誤帶出,句子級/會話級錯誤不帶

22. host_disconnected - 主辦斷線

主辦端連線中斷、進入重連寬限期時發送。廣播被凍結但尚未結束,觀眾不應關閉連線,只需顯示提示等待 host_reconnected 或 ended。

{
  "message": "主辦暫時離線,重連中"
}
欄位類型說明
messagestring提示訊息(伺服器回傳的固定字串,僅供顯示)

23. host_reconnected - 主辦重新連線

主辦端續接成功時發送。

{
  "message": "主辦已重新連線",
  "resumed": true
}
欄位類型說明
messagestring提示訊息(伺服器回傳的固定字串,僅供顯示)
resumedboolean是否同時恢復播放。false 代表主辦在斷線前已手動暫停,續接後維持暫停狀態

心跳機制

SSE 連線使用心跳保持連線活躍:

  • 間隔:15 秒
  • 格式:SSE 註解(以 : 開頭)
  • 前端無需處理,瀏覽器會自動忽略
: heartbeat

特有錯誤碼

錯誤碼HTTP 狀態碼說明處理建議
broadcast_session_not_found404找不到廣播(Token 不存在或已撤銷皆回此碼)確認 Token 正確
broadcast_not_ready503Token 有效,但廣播尚未啟動或已結束稍後重試
broadcast_session_not_started503廣播尚未開始稍後重試
broadcast_session_ended410廣播已結束提示使用者廣播結束
broadcast_capacity_exceeded503觀眾人數與等待佇列皆已滿稍後重試
broadcast_queue_timeout-(連線中的 error 事件)排隊等待超過 5 分鐘重新連線
broadcast_password_required401需要密碼驗證引導觀眾輸入密碼並取得 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

Copyright © 2026