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 中。詳見 認證機制。
請求參數
| 參數 | 位置 | 類型 | 必填 | 說明 |
|---|---|---|---|---|
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 | 廣播暫停 | 主講者暫停或斷線 |
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_langs | array | 可用的翻譯語言列表 |
tts_languages | array | 有啟用 TTS 的語言列表(空陣列表示無 TTS) |
phase | string | 廣播階段:standby(預備)或 live(正式) |
recognition_mode | string | 辨識模式:single(單人)或 multi_speaker(多人語者分離) |
2. queued - 排隊中
{
"position": 3,
"estimated_wait": "約 2 分鐘"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
position | number | 佇列中的位置(1 = 下一個) |
estimated_wait | string | 預估等待時間 |
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"
}
| 欄位 | 類型 | 說明 |
|---|---|---|
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 對齊);standby 階段不送此欄位,live 階段從 00:00 起算 |
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 | 是否為最終結果 |
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": "已被主講者移除"
}
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 - 存取類型變更
當主講者變更廣播的存取類型(public ↔ password)時發送。收到此事件後,觀眾 SSE 連線會被斷開,需重新連線。
此事件不帶任何資料(data 為空)。
17. pass_code_changed - 密碼變更
當主講者變更廣播密碼時發送。收到此事件後,觀眾 SSE 連線會被斷開,需重新驗證密碼並連線。
此事件不帶任何資料(data 為空)。
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": "廣播已結束",
"severity": "error"
}
句子級錯誤(如某語言的翻譯失敗)會額外帶 sid 與 translation_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 新增)不帶 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": "azure_openai",
"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) |
心跳機制
SSE 連線使用心跳保持連線活躍:
- 間隔:15 秒
- 格式:SSE 註解(以
:開頭) - 前端無需處理,瀏覽器會自動忽略
: heartbeat
特有錯誤碼
| 錯誤碼 | HTTP 狀態碼 | 說明 | 處理建議 |
|---|---|---|---|
broadcast_session_not_found | 404 | 找不到廣播 | 確認 Token 正確 |
broadcast_session_not_started | 404 | 廣播尚未開始 | 稍後重試 |
broadcast_session_ended | 410 | 廣播已結束 | 提示使用者廣播結束 |
broadcast_token_revoked | 410 | 廣播已撤銷 | 提示使用者廣播不可用 |
broadcast_capacity_exceeded | 503 | 觀眾人數已達上限 | 加入等待佇列 |
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) => {
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