SSE API

Broadcast Viewer

連線資訊

項目
基礎路徑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廣播暫停主講者暫停或斷線
resumed廣播恢復主講者恢復
ended廣播結束-
kicked被踢除觀眾管理
speaker_renamed說話者重命名-
speaker_reassigned單句說話者修改-
speakers_merged語者合併-
recording_started新錄音開始-
max_viewers_changed觀眾上限變更-
access_type_changed存取類型變更-
pass_code_changed密碼變更-
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 - 排隊中

{
  "position": 3,
  "estimated_wait": "約 2 分鐘"
}
欄位類型說明
positionnumber佇列中的位置(1 = 下一個)
estimated_waitstring預估等待時間

3. admitted - 進入直播

{
  "message": "已進入直播"
}

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 對齊);standby 階段不送此欄位live 階段從 00:00 起算

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是否為最終結果

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": "已被主講者移除"
}

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 - 存取類型變更

當主講者變更廣播的存取類型(public ↔ password)時發送。收到此事件後,觀眾 SSE 連線會被斷開,需重新連線。

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


17. pass_code_changed - 密碼變更

當主講者變更廣播密碼時發送。收到此事件後,觀眾 SSE 連線會被斷開,需重新驗證密碼並連線。

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


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": "廣播已結束",
  "severity": "error"
}

句子級錯誤(如某語言的翻譯失敗)會額外帶 sidtranslation_language,供觀眾端標示某句的某個語言失敗:

{
  "error_code": "llm_content_filtered",
  "severity": "warning",
  "message": "LLM 內容被過濾",
  "context": "translation",
  "sid": 5,
  "translation_language": "ja",
  "timestamp": "2026-04-26T10:30:45.123Z"
}

Session-level 翻譯服務錯誤(連續失敗達閾值升級,v1.3.8 新增)不帶 sidtranslation_language,廣播給所有觀眾,前端應顯示全域提示但連線仍維持:

{
  "error_code": "translation_service_unavailable",
  "severity": "error",
  "message": "Translation service unavailable",
  "context": "translation",
  "timestamp": "2026-04-26T10:30:45.123Z",
  "details": {
    "provider": "azure_openai",
    "last_error_code": "llm_provider_error",
    "fail_count": 5
  }
}
欄位類型說明
error_codestring錯誤碼
messagestring錯誤訊息
severitystring嚴重度:warning / error / fatal
contextstring可選。錯誤上下文(如 broadcasttranslation
sidint可選。句子級錯誤的句子編號(如該句翻譯失敗);session-level 錯誤不帶
translation_languagestring可選。翻譯失敗的目標語言(觀眾依此判斷是否該語言相關)
detailsobject可選。除錯資訊(如 translation_service_unavailableproviderlast_error_codefail_count
timestampstring可選。錯誤發生時間(ISO 8601)

心跳機制

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

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

特有錯誤碼

錯誤碼HTTP 狀態碼說明處理建議
broadcast_session_not_found404找不到廣播確認 Token 正確
broadcast_session_not_started404廣播尚未開始稍後重試
broadcast_session_ended410廣播已結束提示使用者廣播結束
broadcast_token_revoked410廣播已撤銷提示使用者廣播不可用
broadcast_capacity_exceeded503觀眾人數已達上限加入等待佇列
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) => {
    console.log('您已被移除');
    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('access_type_changed', () => {
    console.log('存取類型已變更,需重新連線');
    eventSource.close();
    // 重新連線或引導使用者重新驗證
  });

  eventSource.addEventListener('pass_code_changed', () => {
    console.log('密碼已變更,需重新驗證');
    eventSource.close();
    // 引導使用者重新輸入密碼
  });

  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.5.7 最後更新:2026-05-20

Copyright © 2026