Docs

Integrate a video room SDK

Add a video room or video call to your app: install the SDK, issue a token from your server and join. Samples for Flutter, React and Kotlin.

What you build

A video room is the one building block behind video calls, group calls and meetings. Everyone who joins the same room name sees and hears each other. Two people in a room is a video call; more people is a group call or a meeting.

Integrating it takes three steps, and the same flow works on every platform:

  1. Install the SDK

    Flutter for Android and iOS apps, Web for React and browsers, or the native Android SDK for Kotlin and Java.

  2. Issue a token from your server

    Your backend signs a short-lived token for one user and one room. The App Certificate never ships in an app.

  3. Join and publish

    The app asks your endpoint for a token, joins the room with it, and turns on the camera and microphone.

1. Install the SDK

terminal
flutter pub add gravix_rtc
terminal
npm install gravix-rtc
app/build.gradle.kts
implementation("com.gravixcloud:gravix-rtc-android:0.2.0")

Full platform requirements and permissions are on the SDKs page.

2. Get a token from your server

Sign up to get your App ID, App Certificate and region URL. Your backend uses them to sign a token for each signed-in user. The App Certificate stays on your server. Read more in access tokens.

server.ts
// server.ts (your backend, Node 18+). This file never ships inside an app.
import express from 'express';
import { createToken } from 'gravix-rtc-token';

const APP_ID = process.env.GRAVIX_APP_ID!;                   // from your dashboard
const APP_CERTIFICATE = process.env.GRAVIX_APP_CERTIFICATE!; // server only, never in an app
const REGION_URL = process.env.GRAVIX_URL!;                   // wss://YOUR-REGION-URL

const app = express();
app.use(express.json());

// requireUser is your own login check: the identity comes from YOUR session,
// never from the request body.
app.post('/rtc/token', requireUser, async (req, res) => {
  const room = req.body?.room;
  if (typeof room !== 'string' || !/^[\w.-]{1,64}$/.test(room)) {
    return res.status(400).json({ error: 'invalid room' });
  }
  const token = await createToken(APP_ID, APP_CERTIFICATE, {
    room,                       // the library adds your App ID prefix
    identity: req.user.id,
    name: req.user.name,
    canPublish: true,           // false = listen only; decide it here, not in the app
    ttlSeconds: 60 * 60,        // short-lived: 1 hour
  });
  res.json({ token, url: REGION_URL });
});

app.listen(3000);

3. Join the room

Join with the token and turn on the camera and microphone. Remote video arrives as tracks you attach to a view. Use the same room name for everyone who should see each other.

lib/room.dart
import 'package:gravix_rtc/gravix_rtc.dart';

// One provider per signed-in user. It caches each token until it is about to expire.
final tokens = GravixTokenProvider.endpoint(
  Uri.parse('https://YOUR-BACKEND/rtc/token'),
  headers: {'Authorization': 'Bearer $sessionJwt'}, // your app's own login session
);

final room = GravixRoomService();

Future<void> join(String roomId, String userId, String name) async {
  room.onRemoteVideoTrack = (uid, track) {
    // show it with VideoTrackRenderer(track)
  };

  final ok = await room.connectWithTokenProvider(
    tokenProvider: tokens,
    request: GravixTokenRequest(
      room: roomId,
      identity: userId,
      name: name,
      canPublish: true,
    ),
    publishMic: true,
    enableVideo: true,
  );
  if (!ok) {
    // room.lastTokenError is set when your token endpoint failed
  }
}

Future<void> leave() => room.disconnect();
room.ts
import { Room, RoomEvent, GravixTokenProvider, connectWithTokenProvider } from 'gravix-rtc';

// Calls YOUR backend with the user's session cookie. Tokens are cached until they expire.
const tokens = GravixTokenProvider.endpoint('https://YOUR-BACKEND/rtc/token', {
  credentials: 'include',
});

const room = new Room();
const stage = document.getElementById('stage')!;

room
  .on(RoomEvent.TrackSubscribed, (track) => {
    stage.appendChild(track.attach()); // a <video> or <audio> element
  })
  .on(RoomEvent.TrackUnsubscribed, (track) => {
    track.detach().forEach((el) => el.remove());
  });

export async function join(roomId: string, userId: string, name: string) {
  await connectWithTokenProvider(room, {
    tokenProvider: tokens,
    request: { room: roomId, identity: userId, name },
  });
  await room.localParticipant.setMicrophoneEnabled(true);
  await room.localParticipant.setCameraEnabled(true);
}

export const leave = () => room.disconnect();
CallActivity.kt
import com.gravixcloud.rtc.GravixRtc
import com.gravixcloud.rtc.events.RoomEvent
import com.gravixcloud.rtc.events.collect
import com.gravixcloud.rtc.regions.GravixRegionMeasurements
import com.gravixcloud.rtc.renderer.SurfaceViewRenderer
import com.gravixcloud.rtc.room.track.VideoTrack

class CallActivity : AppCompatActivity() {
    private val room by lazy { GravixRtc.create(applicationContext) }

    // url and token come from YOUR backend: POST https://YOUR-BACKEND/rtc/token -> { token, url }.
    // Call after RECORD_AUDIO and CAMERA are granted.
    private fun join(url: String, token: String, renderer: SurfaceViewRenderer) {
        room.initVideoRenderer(renderer)
        lifecycleScope.launch {
            launch {
                room.events.collect { event ->
                    when (event) {
                        is RoomEvent.TrackSubscribed -> (event.track as? VideoTrack)?.addRenderer(renderer)
                        is RoomEvent.Resuming, is RoomEvent.Reconnecting -> showReconnecting(true)
                        is RoomEvent.Resumed, is RoomEvent.Reconnected -> showReconnecting(false)
                        else -> Unit
                    }
                }
            }
            // the measured nearest region when one was measured, else url unchanged
            room.connect(GravixRegionMeasurements.resolveConnectUrl(url), token)
            room.localParticipant.setMicrophoneEnabled(true)
            room.localParticipant.setCameraEnabled(true)
        }
    }

    private fun showReconnecting(on: Boolean) {
        title = if (on) "Reconnecting..." else "In call"
    }

    override fun onDestroy() {
        room.disconnect()
        super.onDestroy()
    }
}

Next steps

Stuck? Talk to a person.

Start your first video room

Sign up free for your App ID, App Certificate and 10,000 free minutes, or log in to your dashboard.