Broadcast Viewer
Connection Information
| Item | Value |
|---|---|
| Base path | https://vas-poc.vurbo.ai/broadcast |
| Protocol | HTTP + Server-Sent Events (SSE) |
| Data format | text/event-stream |
| Authentication | Token authentication (no API Key required) |
Note: The base path for Broadcast SSE differs from the other SSE APIs.
Endpoint Overview
| Method | Endpoint | Description |
|---|---|---|
| GET | /broadcast/{token}/text | Viewer real-time subtitle stream |
GET /broadcast/{token}/text
Description
Viewers connect through a share Token to receive an SSE stream of real-time transcription and translation.
Use Cases
- Viewers watch real-time subtitles
- Multi-language translation subtitle display
- TTS audio playback
Authentication
Token authentication (no API Key required): verification is performed via the {token} in the URL path.
Password protection note: When a broadcast is configured with password protection, viewers must first obtain a
viewer_access_tokenthrough the password verification API, then include this Token in the SSE connection's query parameter. See Authentication for details.
Request Parameters
| Parameter | Location | Type | Required | Description |
|---|---|---|---|---|
token | path | string | Yes | Broadcast share Token (4-character short code, character set a-z0-9) |
lang | query | string | No | Filter to a specific translation language (e.g., en-US) |
tts | query | boolean | No | Whether to enable TTS (true / false, defaults to false) |
viewer_access_token | query | string | Conditional | Viewer access Token (required for password-protected broadcasts) |
Request Examples
// Receive all languages
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text'
);
// Receive only English translations
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text?lang=en-US'
);
// Receive English translations and enable TTS
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text?lang=en-US&tts=true'
);
// Password-protected broadcast
const eventSource = new EventSource(
'https://vas-poc.vurbo.ai/broadcast/a3f9/text?viewer_access_token=xxx'
);
Event Types
| Event | Description | Notes |
|---|---|---|
connected | Connection confirmation | - |
queued | Added to the waiting queue | Queueing mechanism |
admitted | Entered the live stream from the queue | Queueing mechanism |
origin | Original text (STT) | - |
translation | Translation result | - |
tts_ready | TTS audio ready | - |
paused | Broadcast paused | Host paused or disconnected |
resumed | Broadcast resumed | Host resumed |
ended | Broadcast ended | - |
kicked | Kicked out | Viewer management |
speaker_renamed | Speaker renamed | - |
speaker_reassigned | Single-sentence speaker reassigned | - |
speakers_merged | Speakers merged | - |
recording_started | New recording started | - |
max_viewers_changed | Viewer limit changed | - |
access_type_changed | Access type changed | - |
pass_code_changed | Password changed | - |
standby | Standby phase notification | - |
phase_changed | Phase change notification | - |
announcement | Host announcement | - |
error | Error | - |
Event Formats
1. connected - Connection Confirmation
{
"available_langs": ["en-US", "ja-JP"],
"tts_languages": ["en-US"],
"phase": "standby",
"recognition_mode": "single"
}
| Field | Type | Description |
|---|---|---|
available_langs | array | List of available translation languages |
tts_languages | array | List of languages with TTS enabled (an empty array means no TTS) |
phase | string | Broadcast phase: standby or live |
recognition_mode | string | Recognition mode: single (single speaker) or multi_speaker (multi-speaker diarization) |
2. queued - In Queue
{
"position": 3,
"estimated_wait": "About 2 minutes"
}
| Field | Type | Description |
|---|---|---|
position | number | Position in the queue (1 = next) |
estimated_wait | string | Estimated wait time |
3. admitted - Entered the Live Stream
{
"message": "Entered the live stream"
}
4. origin - Original Text (STT)
{
"sid": 1,
"text": "Hello everyone",
"is_final": true,
"language": "zh-TW",
"speaker_id": "Guest-1",
"speaker_label": "Royx",
"start_time": "00:05"
}
| Field | Type | Description |
|---|---|---|
sid | number | Sentence ID |
text | string | Original text content |
is_final | boolean | Whether this is the final result |
language | string | Original text language |
speaker_id | string | Original speaker ID (conversation mode, optional; immutable) |
speaker_label | string | Display label (conversation mode, optional; after applying speaker_aliases, equals speaker_id when no alias exists) |
start_time | string | Sentence start time, format mm:ss (aligned with the host WS origin and History init_sentence); not sent during the standby phase, counted from 00:00 in the live phase |
5. translation - Translation Result
{
"sid": 1,
"language": "en-US",
"text": "Hello everyone",
"speaker_id": "Guest-1",
"speaker_label": "Royx",
"is_final": true
}
| Field | Type | Description |
|---|---|---|
sid | number | Corresponding sentence ID |
language | string | Translation language |
text | string | Translated content |
speaker_id | string | Original speaker ID (conversation mode, optional; immutable) |
speaker_label | string | Display label (conversation mode, optional; after applying speaker_aliases, equals speaker_id when no alias exists) |
is_final | boolean | Whether this is the final result |
6. tts_ready - TTS Audio
{
"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
}
]
}
| Field | Type | Description |
|---|---|---|
sid | number | Corresponding sentence ID |
language | string | TTS language |
transcript | string | Original transcript (original text) |
text | string | Translated text |
audio | string | Base64-encoded MP3 audio |
format | string | Audio format, always "mp3" |
duration_ms | number | Audio duration (milliseconds) |
boundaries | array | Word boundaries (optional, see the table below) |
Word Boundaries fields (each object in the boundaries array):
| Field | Type | Description |
|---|---|---|
offset_ms | number | Start time of the word in the audio (ms) |
duration_ms | number | Pronunciation duration of the word (ms) |
text | string | Word text |
text_offset | number | Starting position of the word in the text |
word_length | number | Character length of the word |
Note:
- The host must specify which languages have TTS enabled via the
tts_configparameter in thestartcommand- Only viewers who subscribe to that language and have TTS enabled will receive this event
- It is sent only during the
livephase; no TTS is sent during thestandbyphase
7. paused - Paused
{
"message": "Live stream is paused"
}
| Field | Type | Description |
|---|---|---|
message | string | Notification message |
8. resumed - Resumed
{
"message": "Live stream has resumed"
}
| Field | Type | Description |
|---|---|---|
message | string | Notification message |
9. ended - Ended
{
"message": "Broadcast has ended"
}
| Field | Type | Description |
|---|---|---|
message | string | End message |
10. kicked - Kicked Out
{
"message": "Removed by the host"
}
11. speaker_renamed - Speaker Renamed
For conversation mode only. Sent when the host performs a global rename of a speaker.
{
"speaker_id": "Guest-1",
"new_label": "Royx",
"affected_sids": [1, 3, 5]
}
| Field | Type | Description |
|---|---|---|
speaker_id | string | Resolved original speaker ID (even if the input is a display label, the event returns the original ID) |
new_label | string | New display label (e.g., Royx) |
affected_sids | array | List of affected sentence IDs |
12. speaker_reassigned - Speaker Identity Reassigned
For conversation mode only. Sent when the host reassigns the speaker of a single sentence.
{
"sid": 3,
"old_speaker_id": "Guest-1",
"new_speaker_id": "Guest-2",
"new_speaker_label": "Amy"
}
| Field | Type | Description |
|---|---|---|
sid | number | The reassigned sentence ID |
old_speaker_id | string | Original speaker ID (e.g., Guest-1) |
new_speaker_id | string | New original speaker ID (e.g., Guest-2) |
new_speaker_label | string | New speaker display label (after applying speaker_aliases; equals new_speaker_id when no alias exists) |
13. speakers_merged - Speakers Merged
For conversation mode only. Sent when the host merges speakers. After the merge, all sentences of that speaker are reassigned to the target speaker.
{
"source_speaker_id": "Guest-2",
"target_speaker_id": "Guest-1",
"target_speaker_label": "Manager Wang",
"affected_sids": [3, 5, 7]
}
| Field | Type | Description |
|---|---|---|
source_speaker_id | string | Original ID of the merged speaker (e.g., Guest-2) |
target_speaker_id | string | Original ID of the merge target speaker (e.g., Guest-1) |
target_speaker_label | string | Target speaker display label (after applying speaker_aliases; equals the original ID when no alias exists) |
affected_sids | array | List of affected sentence IDs |
14. recording_started - New Recording Started
Sent when the host starts a new recording segment. After receiving this event, viewers should clear the old subtitles on screen.
This event carries no data (data is empty).
15. max_viewers_changed - Viewer Limit Changed
Sent when the host changes the viewer limit.
{
"max_viewers": 200
}
| Field | Type | Description |
|---|---|---|
max_viewers | number | New viewer limit |
16. access_type_changed - Access Type Changed
Sent when the host changes the broadcast's access type (public ↔ password). After receiving this event, the viewer's SSE connection is disconnected and must be reestablished.
This event carries no data (data is empty).
17. pass_code_changed - Password Changed
Sent when the host changes the broadcast password. After receiving this event, the viewer's SSE connection is disconnected and the password must be re-verified before reconnecting.
This event carries no data (data is empty).
18. standby - Standby Phase
When a viewer connects during the standby phase, they receive this event immediately after the
connectedevent, indicating that the broadcast has not officially started yet. The host can dynamically update the standby message via the WebSocketset_standby_messageaction; after the update, all viewers receive a newstandbyevent.
{
"message": "The presentation is about to begin...",
"translations": {
"en-US": "The presentation is about to begin...",
"ja-JP": "プレゼンテーションがまもなく始まります..."
}
}
| Field | Type | Description |
|---|---|---|
message | string | Standby phase display message (original text) |
translations | object | Translation results (optional); the key is the language code and the value is the translated text |
19. phase_changed - Phase Change
Sent when the broadcast switches from the standby phase to the live phase.
{
"phase": "live",
"message": "Broadcast has started"
}
| Field | Type | Description |
|---|---|---|
phase | string | New phase: live (live phase) |
message | string | Phase change message |
20. announcement - Announcement
An announcement message sent by the host; all viewers receive it.
{
"message": "The meeting will end in 5 minutes",
"translations": {
"en-US": "The meeting will end in 5 minutes",
"ja-JP": "会議は5分後に終了します"
}
}
| Field | Type | Description |
|---|---|---|
message | string | Announcement message content (original text) |
translations | object | Translation results (optional); the key is the language code and the value is the translated text |
21. error - Error
{
"error_code": "broadcast_session_ended",
"message": "Broadcast session ended",
"severity": "error"
}
Sentence-level errors (such as a translation failure for a specific language) additionally carry sid and translation_language, allowing the viewer to mark a specific language failure for a specific sentence:
{
"error_code": "llm_content_filtered",
"severity": "warning",
"message": "Content filtered",
"context": "translation",
"sid": 5,
"translation_language": "ja",
"timestamp": "2026-04-26T10:30:45.123Z"
}
Session-level translation service errors (escalated when consecutive failures reach a threshold, added in v1.3.8) do not carry sid or translation_language, are broadcast to all viewers, and the frontend should display a global notice while keeping the connection alive:
{
"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
}
}
| Field | Type | Description |
|---|---|---|
error_code | string | Error code |
message | string | Error message |
severity | string | Severity: warning / error / fatal |
context | string | Optional. Error context (such as broadcast, translation) |
sid | int | Optional. Sentence number for a sentence-level error (e.g., a translation failure for that sentence); not carried for session-level errors |
translation_language | string | Optional. Target language of the translation failure (viewers use this to determine whether it is related to that language) |
details | object | Optional. Debug information (e.g., translation_service_unavailable carries provider, last_error_code, fail_count) |
timestamp | string | Optional. Time the error occurred (ISO 8601) |
Heartbeat Mechanism
The SSE connection uses a heartbeat to keep the connection alive:
- Interval: 15 seconds
- Format: SSE comment (starting with
:) - No frontend handling is needed; the browser ignores it automatically
: heartbeat
Specific Error Codes
| Error Code | HTTP Status | Description | Recommended Handling |
|---|---|---|---|
broadcast_session_not_found | 404 | Broadcast not found | Verify that the Token is correct |
broadcast_session_not_started | 404 | Broadcast has not started yet | Retry later |
broadcast_session_ended | 410 | Broadcast ended | Notify the user that the broadcast has ended |
broadcast_token_revoked | 410 | Broadcast revoked | Notify the user that the broadcast is unavailable |
broadcast_capacity_exceeded | 503 | Viewer count has reached the limit | Join the waiting queue |
broadcast_password_required | 401 | Password verification required | Guide the viewer to enter the password and obtain a viewer_access_token |
translation_service_unavailable | - | Translation service consecutive failures reached the threshold (added in v1.3.8, no sid) | Display a global notice "Translation temporarily unavailable", no disconnection needed; the original text (STT) continues to stream normally |
Frontend Example
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(`Phase: ${data.phase}, Mode: ${data.recognition_mode}`);
console.log(`Available translations: ${data.available_langs.join(', ')}`);
});
eventSource.addEventListener('queued', (e) => {
const data = JSON.parse(e.data);
console.log(`In queue, position: ${data.position}, estimated wait: ${data.estimated_wait}`);
});
eventSource.addEventListener('admitted', (e) => {
console.log('Entered the live stream');
});
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(`Translation (${data.language}): ${data.text}`);
});
eventSource.addEventListener('tts_ready', (e) => {
const data = JSON.parse(e.data);
// Decode the Base64 MP3 and play it
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(`Live stream paused: ${data.message}`);
});
eventSource.addEventListener('resumed', (e) => {
console.log('Live stream has resumed');
});
eventSource.addEventListener('ended', (e) => {
const data = JSON.parse(e.data);
console.log(`Live stream ended: ${data.message}`);
eventSource.close();
});
eventSource.addEventListener('kicked', (e) => {
console.log('You have been removed');
eventSource.close();
});
eventSource.addEventListener('speaker_renamed', (e) => {
const data = JSON.parse(e.data);
console.log(`Speaker renamed: ${data.speaker_id} → ${data.new_label}`);
// Update the display labels of all affected sentences
});
eventSource.addEventListener('speaker_reassigned', (e) => {
const data = JSON.parse(e.data);
console.log(`Speaker of sentence ${data.sid} changed to: ${data.new_speaker_label}`);
// Update the display label of that sentence
});
eventSource.addEventListener('speakers_merged', (e) => {
const data = JSON.parse(e.data);
console.log(`Speakers merged: ${data.source_speaker_id} → ${data.target_speaker_label}`);
// Update the display labels of all affected sentences
});
eventSource.addEventListener('recording_started', () => {
console.log('New recording started, clearing old subtitles');
// Clear the old subtitles on screen
});
eventSource.addEventListener('max_viewers_changed', (e) => {
const data = JSON.parse(e.data);
console.log(`Viewer limit changed to: ${data.max_viewers}`);
});
eventSource.addEventListener('access_type_changed', () => {
console.log('Access type changed, reconnection required');
eventSource.close();
// Reconnect or guide the user to re-verify
});
eventSource.addEventListener('pass_code_changed', () => {
console.log('Password changed, re-verification required');
eventSource.close();
// Guide the user to re-enter the password
});
eventSource.addEventListener('standby', (e) => {
const data = JSON.parse(e.data);
const displayLang = 'en-US'; // The language selected by the viewer
const displayMessage = data.translations?.[displayLang] || data.message;
console.log(`Standby phase: ${displayMessage}`);
});
eventSource.addEventListener('phase_changed', (e) => {
const data = JSON.parse(e.data);
console.log(`Phase change: ${data.phase} - ${data.message}`);
// Remove the waiting screen and start displaying subtitles
});
eventSource.addEventListener('announcement', (e) => {
const data = JSON.parse(e.data);
const displayLang = 'en-US';
const displayMessage = data.translations?.[displayLang] || data.message;
console.log(`Announcement: ${displayMessage}`);
});
eventSource.addEventListener('error', (e) => {
if (e.data) {
const error = JSON.parse(e.data);
console.error(`Error [${error.error_code}]: ${error.message}`);
}
eventSource.close();
});
return eventSource;
}
// Base64-to-Blob utility function
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 });
}
Version: V1.5.7 Last Updated: 2026-05-20