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

ConceptSame in TS & Java
Event namesstream-published ↔ onStreamPublished() (kebab β†’ camelCase auto-mapping)
Payload typesLocalPublication / Subscription / PublishFailure / ConnectionEvent
Field keysvideo / audio / screen β€” same names, mirroring me.video / me.audio / me.screen
Pre-call β†’ join semanticsauto-promote, no flicker, same LocalXStream instance throughout
Enum valuesMediaKind.Video, ConnectionState.Reconnecting, ErrorKind.Auth, DisconnectReason.RoomClose
Null-check patternpub.video may be null; check before use
LifecyclePre-call β†’ join β†’ render β†’ controls β†’ leave

What differs per platform

ConceptTS / JSJava (Android)
Listener registrationp.on('stream-subscribed', cb)p.addListener(new RemoteParticipantListener() { @Override public void onStreamSubscribed(...) })
Async SDK callsawait VideoSDK.join(...) (Promise-based)bg.execute(...) (background thread; method throws SDKError)
UI thread marshalingnot needed (single-threaded)ui.post(new Runnable() { ... }) from background
Property accessme.videome.getVideo() (getter convention)
Render targetHTMLVideoElementVideoView (Android view)
Optional checkif (pub.video) ...if (pub.getVideo() != null) ...

See also: VideoSDK Room LocalParticipant RemoteParticipant Types