Full example β publish, subscribe, render
End-to-end example showing pre-call β join β render local β render remote β controls β leave. Two parallel implementations: TypeScript (Web / JS) and Java (Android, anonymous-inner-class style β no lambdas).
Both examples use the same v1 event names, payload types, and lifecycle. The shapes (
LocalPublication, Subscription, PublishFailure, ConnectionEvent) are identical across languages β only the listener-registration syntax differs per platform.Jump to: TypeScript / JavaScript Β· Java (Android)
TypeScript / JavaScript
EventEmitter pattern (.on(eventName, callback)). The same code works in vanilla JS, React, Vue, and React Native (with appropriate render-target swaps).
import { VideoSDK, MediaKind, ConnectionState, ErrorKind } from '@videosdk/js';
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 1. PRE-CALL β acquire devices, build a preview
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const cameras = await VideoSDK.getCameras();
const mics = await VideoSDK.getMicrophones();
const camStream = await VideoSDK.createVideoStream({
device: cameras[0], // CameraDeviceInfo (typed)
resolution: 'h720',
});
const micStream = await VideoSDK.createAudioStream({
device: mics[0],
noiseSuppression: true,
});
camStream.attach(document.querySelector('#preview'));
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 2. JOIN β pre-call streams auto-promote, no flicker
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
let room;
try {
room = await VideoSDK.join({
token: getAuthToken(),
roomId: 'team-standup',
name: 'Alice',
metadata: { role: 'host' },
});
} catch (err) {
if (err.kind === ErrorKind.Auth) return showLoginUI();
if (err.code === 'ROOM_FULL') return showRoomFullToast();
if (err.code === 'TIMEOUT') return showRetryUI();
throw err;
}
const me = room.localParticipant;
// me.video === camStream (auto-promoted; same instance, no re-acquire)
// me.audio === micStream
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 3. LOCAL β react to your own publishes (any path: explicit, JoinOptions,
// or pre-call auto-promote)
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
me.on('stream-published', (pub) => {
// pub: LocalPublication β exactly one of pub.video / .audio / .screen is set
if (pub.video) pub.video.attach(document.querySelector('#local-cam'));
if (pub.screen) pub.screen.attach(document.querySelector('#local-screen'));
// pub.audio: usually no UI for local audio (no monitor playback by default)
});
me.on('stream-unpublished', ({ kind }) => {
if (kind === MediaKind.Video) document.querySelector('#local-cam').srcObject = null;
if (kind === MediaKind.Screen) document.querySelector('#local-screen').srcObject = null;
});
me.on('stream-publish-failed', (failure) => {
// failure: PublishFailure β initial failure OR mid-call runtime failure
if (failure.kind === MediaKind.Video) showToast(`Camera failed: ${failure.error.message}`);
if (failure.kind === MediaKind.Audio) showToast(`Mic failed: ${failure.error.message}`);
if (failure.error.retriable) {
offerRetry(() => me.publishVideo({ device: cameras[0] }));
}
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 4. REMOTE β one handler covers existing + future participants
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
const grid = document.querySelector('#grid');
room.on('participant-joined', (p) => { // fires retroactively for already-present
const tile = document.createElement('div');
tile.className = 'tile';
tile.dataset.participantId = p.id;
tile.innerHTML = `
<video class="cam"></video>
<div class="screen-overlay" hidden><video class="scr"></video></div>
<div class="name">${p.displayName}</div>
`;
grid.appendChild(tile);
const camEl = tile.querySelector('.cam');
const scrEl = tile.querySelector('.scr');
const scrOv = tile.querySelector('.screen-overlay');
// Notification: they published a kind. Trigger subscribe.
// (Fires retroactively for already-published kinds.)
p.on('stream-published', ({ kind }) => p.subscribe(kind));
p.on('stream-unpublished', ({ kind }) => {
if (kind === MediaKind.Screen) scrOv.hidden = true;
if (kind === MediaKind.Video) camEl.srcObject = null;
});
// Subscribe completion: attach the freshly-subscribed stream.
p.on('stream-subscribed', (sub) => {
// sub: Subscription β exactly one of sub.video / .audio / .screen is set
if (sub.video) sub.video.attach(camEl);
if (sub.screen) { scrOv.hidden = false; sub.screen.attach(scrEl); }
// sub.audio: no manual attach β remote audio auto-plays via SDK
});
p.on('stream-unsubscribed', ({ kind }) => {
// Typically no-op β unpublished + left handlers cover cleanup
});
});
room.on('participant-left', (p) => {
// SDK has already invalidated p's streams + auto-cleaned listeners on p
document.querySelector(`[data-participant-id="${p.id}"]`)?.remove();
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 5. CONNECTION STATE β banner UX + disconnect routing
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
room.on('connection-state-changed', (e) => {
if (e.state === ConnectionState.Reconnecting) {
showBanner(`Reconnecting⦠(attempt ${e.reconnecting.attempt}/${e.reconnecting.maxAttempts})`);
}
if (e.state === ConnectionState.Connected && e.previous === ConnectionState.Reconnecting) {
hideBanner();
}
if (e.state === ConnectionState.Disconnected) {
showToast(`Disconnected: ${e.disconnected.reason}`);
navigateToHome();
}
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 6. INTERACTIVE CONTROLS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
document.querySelector('#toggle-cam').addEventListener('click', async () => {
if (me.video) await me.unpublishVideo();
else await me.publishVideo({ device: cameras[0], resolution: 'h720' });
});
document.querySelector('#switch-cam').addEventListener('click', async () => {
if (me.video) await me.video.setInputDevice(cameras[1]); // hot-swap, no flicker
});
document.querySelector('#share-screen').addEventListener('click', async () => {
await me.publishScreen({ audio: true });
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 7. LEAVE
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
document.querySelector('#leave').addEventListener('click', async () => {
await room.leave();
// SDK stops all local streams, releases attached elements,
// removes all listeners, tears down PC + signaling.
});
Java (Android)
Listener-interface pattern (anonymous inner classes β no lambdas). Async SDK methods run on a background thread; UI updates posted back to the main thread.
SDK-defined listener interfaces (default empty methods so apps only override what they care about):
interface LocalParticipantListener {
default void onStreamPublished(LocalPublication pub) {}
default void onStreamUnpublished(MediaKind kind) {}
default void onStreamPublishFailed(PublishFailure failure) {}
}
interface RemoteParticipantListener {
default void onStreamPublished(MediaKind kind) {}
default void onStreamUnpublished(MediaKind kind) {}
default void onStreamSubscribed(Subscription sub) {}
default void onStreamUnsubscribed(MediaKind kind) {}
}
interface RoomListener {
default void onParticipantJoined(RemoteParticipant p) {}
default void onParticipantLeft(RemoteParticipant p) {}
default void onConnectionStateChanged(ConnectionEvent event) {}
}
package com.example.meeting;
import live.videosdk.client.VideoSDK;
import live.videosdk.client.Room;
import live.videosdk.client.LocalParticipant;
import live.videosdk.client.RemoteParticipant;
import live.videosdk.client.LocalParticipantListener;
import live.videosdk.client.RemoteParticipantListener;
import live.videosdk.client.RoomListener;
import live.videosdk.client.LocalPublication;
import live.videosdk.client.Subscription;
import live.videosdk.client.PublishFailure;
import live.videosdk.client.ConnectionEvent;
import live.videosdk.client.ConnectionState;
import live.videosdk.client.MediaKind;
import live.videosdk.client.ErrorKind;
import live.videosdk.client.SDKError;
import live.videosdk.client.JoinOptions;
import live.videosdk.client.PublishVideoOpts;
import live.videosdk.client.PublishAudioOpts;
import live.videosdk.client.PublishScreenOpts;
import live.videosdk.client.CameraDeviceInfo;
import live.videosdk.client.MicrophoneDeviceInfo;
import live.videosdk.client.LocalVideoStream;
import live.videosdk.client.LocalAudioStream;
import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.os.Handler;
import android.os.Looper;
import android.view.View;
import android.widget.Button;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MeetingActivity extends AppCompatActivity {
private final ExecutorService bg = Executors.newSingleThreadExecutor();
private final Handler ui = new Handler(Looper.getMainLooper());
private Room room;
private LocalParticipant me;
private List<CameraDeviceInfo> cameras;
private List<MicrophoneDeviceInfo> mics;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_meeting);
bg.execute(new Runnable() {
@Override public void run() {
try {
preCallAndJoin();
} catch (final SDKError err) {
ui.post(new Runnable() {
@Override public void run() { handleJoinError(err); }
});
}
}
});
wireControls();
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 1. PRE-CALL
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void preCallAndJoin() throws SDKError {
cameras = VideoSDK.getCameras();
mics = VideoSDK.getMicrophones();
PublishVideoOpts videoOpts = new PublishVideoOpts.Builder()
.device(cameras.get(0))
.resolution("h720")
.build();
final LocalVideoStream camStream = VideoSDK.createVideoStream(videoOpts);
PublishAudioOpts audioOpts = new PublishAudioOpts.Builder()
.device(mics.get(0))
.noiseSuppression(true)
.build();
final LocalAudioStream micStream = VideoSDK.createAudioStream(audioOpts);
ui.post(new Runnable() {
@Override public void run() {
camStream.attach((VideoView) findViewById(R.id.preview));
}
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 2. JOIN β pre-call streams auto-promote
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
JoinOptions joinOpts = new JoinOptions.Builder()
.token(getAuthToken())
.roomId("team-standup")
.name("Alice")
.metadata(Map.of("role", "host"))
.build();
room = VideoSDK.join(joinOpts);
me = room.getLocalParticipant();
// me.getVideo() === camStream (auto-promoted)
// me.getAudio() === micStream
ui.post(new Runnable() {
@Override public void run() {
setupLocalListener();
setupRoomListener();
}
});
}
private void handleJoinError(SDKError err) {
if (err.getKind() == ErrorKind.Auth) showLoginUI();
else if ("ROOM_FULL".equals(err.getCode())) showRoomFullToast();
else if ("TIMEOUT".equals(err.getCode())) showRetryUI();
else showGenericError(err);
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 3. LOCAL β react to your own publishes (any path)
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void setupLocalListener() {
me.addListener(new LocalParticipantListener() {
@Override
public void onStreamPublished(LocalPublication pub) {
// pub: LocalPublication β exactly one of pub.video / .audio / .screen is set
if (pub.getVideo() != null) {
pub.getVideo().attach((VideoView) findViewById(R.id.local_cam));
}
if (pub.getScreen() != null) {
pub.getScreen().attach((VideoView) findViewById(R.id.local_screen));
}
}
@Override
public void onStreamUnpublished(MediaKind kind) {
if (kind == MediaKind.Video) {
((VideoView) findViewById(R.id.local_cam)).clear();
} else if (kind == MediaKind.Screen) {
((VideoView) findViewById(R.id.local_screen)).clear();
}
}
@Override
public void onStreamPublishFailed(PublishFailure failure) {
// Initial failure (camera busy, permission denied)
// OR mid-call runtime failure (camera unplugged, encoder died)
String msg;
if (failure.getKind() == MediaKind.Video) {
msg = "Camera failed: " + failure.getError().getMessage();
} else if (failure.getKind() == MediaKind.Audio) {
msg = "Mic failed: " + failure.getError().getMessage();
} else {
msg = "Stream failed: " + failure.getError().getMessage();
}
showToast(msg);
if (Boolean.TRUE.equals(failure.getError().getRetriable())) {
offerRetry(new Runnable() {
@Override public void run() {
bg.execute(new Runnable() {
@Override public void run() {
try {
PublishVideoOpts opts = new PublishVideoOpts.Builder()
.device(cameras.get(0)).build();
me.publishVideo(opts);
} catch (SDKError e) { /* surfaces via this same listener */ }
}
});
}
});
}
}
});
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 4. REMOTE β one handler covers existing + future participants
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void setupRoomListener() {
room.addListener(new RoomListener() {
@Override
public void onParticipantJoined(final RemoteParticipant p) {
final Tile tile = createTile(p);
p.addListener(new RemoteParticipantListener() {
@Override
public void onStreamPublished(final MediaKind kind) {
// Notification: they published a kind. Trigger subscribe.
bg.execute(new Runnable() {
@Override public void run() {
try { p.subscribe(kind); } // single-kind overload; or p.subscribe(Arrays.asList(kind))
catch (SDKError e) { /* surfaces if subscribe rejected */ }
}
});
}
@Override
public void onStreamUnpublished(MediaKind kind) {
if (kind == MediaKind.Screen) {
tile.screenOverlay.setVisibility(View.GONE);
} else if (kind == MediaKind.Video) {
tile.cam.clear();
}
}
@Override
public void onStreamSubscribed(Subscription sub) {
// sub: Subscription β exactly one of sub.video / .audio / .screen is set
if (sub.getVideo() != null) {
sub.getVideo().attach(tile.cam);
}
if (sub.getScreen() != null) {
tile.screenOverlay.setVisibility(View.VISIBLE);
sub.getScreen().attach(tile.screen);
}
// sub.audio: auto-plays via SDK
}
@Override
public void onStreamUnsubscribed(MediaKind kind) {
// Typically no-op β unpublished + left handlers cover cleanup
}
});
}
@Override
public void onParticipantLeft(RemoteParticipant p) {
// SDK has already invalidated p's streams + auto-cleaned listeners on p
removeTile(p);
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 5. CONNECTION STATE
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
@Override
public void onConnectionStateChanged(ConnectionEvent event) {
if (event.getState() == ConnectionState.Reconnecting) {
ConnectionEvent.Reconnecting r = event.getReconnecting();
showBanner("Reconnecting⦠(attempt " + r.getAttempt()
+ "/" + r.getMaxAttempts() + ")");
} else if (event.getState() == ConnectionState.Connected) {
if (event.getPrevious() == ConnectionState.Reconnecting) hideBanner();
} else if (event.getState() == ConnectionState.Disconnected) {
showToast("Disconnected: " + event.getDisconnected().getReason());
navigateToHome();
}
}
});
}
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 6. INTERACTIVE CONTROLS
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
private void wireControls() {
// Toggle camera
findViewById(R.id.toggle_cam).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bg.execute(new Runnable() {
@Override public void run() {
try {
if (me.getVideo() != null) {
me.unpublishVideo();
} else {
PublishVideoOpts opts = new PublishVideoOpts.Builder()
.device(cameras.get(0))
.resolution("h720")
.build();
me.publishVideo(opts);
}
} catch (SDKError e) { /* surfaces via local listener */ }
}
});
}
});
// Switch camera mid-call β same stream instance, no flicker
findViewById(R.id.switch_cam).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bg.execute(new Runnable() {
@Override public void run() {
try {
if (me.getVideo() != null) me.getVideo().setInputDevice(cameras.get(1));
} catch (SDKError e) { /* */ }
}
});
}
});
// Start screen share
findViewById(R.id.share_screen).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bg.execute(new Runnable() {
@Override public void run() {
try {
PublishScreenOpts opts = new PublishScreenOpts.Builder()
.audio(true).build();
me.publishScreen(opts);
} catch (SDKError e) { /* */ }
}
});
}
});
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
// 7. LEAVE
// βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
findViewById(R.id.leave).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
bg.execute(new Runnable() {
@Override public void run() {
try {
room.leave();
// SDK stops all local streams, releases attached views,
// removes all listeners, tears down PC + signaling.
} catch (SDKError e) { /* */ }
}
});
}
});
}
@Override
protected void onDestroy() {
super.onDestroy();
bg.shutdown();
}
}
What's identical across both
| Concept | Same in TS & Java |
|---|---|
| Event names | stream-published β onStreamPublished() (kebab β camelCase auto-mapping) |
| Payload types | LocalPublication / Subscription / PublishFailure / ConnectionEvent |
| Field keys | video / audio / screen β same names, mirroring me.video / me.audio / me.screen |
| Pre-call β join semantics | auto-promote, no flicker, same LocalXStream instance throughout |
| Enum values | MediaKind.Video, ConnectionState.Reconnecting, ErrorKind.Auth, DisconnectReason.RoomClose |
| Null-check pattern | pub.video may be null; check before use |
| Lifecycle | Pre-call β join β render β controls β leave |
What differs per platform
| Concept | TS / JS | Java (Android) |
|---|---|---|
| Listener registration | p.on('stream-subscribed', cb) | p.addListener(new RemoteParticipantListener() { @Override public void onStreamSubscribed(...) }) |
| Async SDK calls | await VideoSDK.join(...) (Promise-based) | bg.execute(...) (background thread; method throws SDKError) |
| UI thread marshaling | not needed (single-threaded) | ui.post(new Runnable() { ... }) from background |
| Property access | me.video | me.getVideo() (getter convention) |
| Render target | HTMLVideoElement | VideoView (Android view) |
| Optional check | if (pub.video) ... | if (pub.getVideo() != null) ... |
See also: VideoSDK Room LocalParticipant RemoteParticipant Types