Docs

Online classrooms

A teacher on camera with slides on a shared screen, and a class that watches, listens and speaks when called on. Samples for Flutter and React.

What you build

A class is one room with one name per lesson, for example the lesson's ID in your app.

  • The teacher publishes camera and microphone, and shares their screen for slides.
  • Students join to watch and listen. A student the teacher calls on gets a new token and joins again with the microphone on.

Install the SDK and add the microphone and camera permissions as in the call guide. Students who may be called on need the microphone permission too.

1. Roles on your server

Roles live in the token. Your token endpoint decides who teaches and who may talk right now; the app only asks. The teacher gets every source. A student gets a listen-only token, or a microphone-only token while called on.

server.ts
// server.ts (your backend). The token endpoint from the docs, with classroom roles.
app.post('/rtc/token', requireUser, async (req, res) => {
  const room = req.body?.room;
  if (typeof room !== 'string' || !(await isEnrolled(room, req.user.id))) {
    return res.status(403).json({ error: 'not in this class' });
  }
  // Your own data: who teaches this class, and which students
  // the teacher has called on to speak right now.
  const isTeacher = await teachesClass(room, req.user.id);
  const mayTalk = isTeacher || (await isCalledOn(room, req.user.id));

  const token = await createToken(APP_ID, APP_CERTIFICATE, {
    room,
    identity: req.user.id,
    name: req.user.name,
    canPublish: mayTalk,  // other students listen and watch
    canPublishSources: isTeacher
      ? ['camera', 'microphone', 'screen_share', 'screen_share_audio']
      : ['microphone'],   // a student who is called on talks, nothing else
    ttlSeconds: 2 * 60 * 60, // one lesson
  });
  res.json({ token, url: REGION_URL });
});

isEnrolled, teachesClass and isCalledOn are your own code. How a student raises a hand, and how the teacher calls on them, is your app's own feature too, for example a button that calls your backend.

2. Create the room

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

// One token provider per signed-in user. It posts { room, identity, name, can_publish }
// to YOUR endpoint and caches each token until shortly before it expires.
final tokens = GravixTokenProvider.endpoint(
  Uri.parse('https://YOUR-BACKEND/rtc/token'),
  headers: {'Authorization': 'Bearer $sessionJwt'}, // your app's own login session
);

// One room service per call, meeting or class screen.
final room = GravixRoomService();
classroom.ts
import { Room, GravixTokenProvider } from 'gravix-rtc';

// One token provider for the app. It posts { room, identity, name, can_publish }
// to YOUR endpoint with the user's session cookie, and caches each token.
const tokens = GravixTokenProvider.endpoint('https://YOUR-BACKEND/rtc/token', {
  credentials: 'include',
});

// One Room per call, meeting or class screen.
const room = new Room();

3. Teacher: teach and share slides

The teacher joins with camera and microphone, then shares the screen with the slides on it.

lib/classroom.dart
// Teacher: camera and mic on as part of the join.
final ok = await room.connectWithTokenProvider(
  tokenProvider: tokens,
  request: GravixTokenRequest(room: classId, identity: userId, name: userName, canPublish: true),
  publishMic: true,
  enableVideo: true,
);

// Share the slides: the whole screen, or another app on it.
await room.localParticipant?.setScreenShareEnabled(true);
classroom.ts
import { connectWithTokenProvider } from 'gravix-rtc';

// Teacher: join, then camera and mic.
await connectWithTokenProvider(room, {
  tokenProvider: tokens,
  request: { room: classId, identity: userId, name: userName, canPublish: true },
});
await room.localParticipant.setMicrophoneEnabled(true);
await room.localParticipant.setCameraEnabled(true);

// Share the slides: a screen, a window or a browser tab.
await room.localParticipant.setScreenShareEnabled(true);

Stop sharing with setScreenShareEnabled(false). Screen sharing on iOS needs a Broadcast Upload Extension, and on the web it works in desktop browsers. Details are in the meetings guide.

4. Students: watch and listen

Students join without publishing. The teacher's camera and the shared screen arrive as two video tracks; tell them apart by their source. The teacher's audio plays on its own.

lib/classroom.dart
// Student: join without publishing. The teacher's camera and slides arrive as video tracks.
VideoTrack? teacherCamera;
VideoTrack? slides;

room.onRemoteVideoTrack = (uid, track) => setState(() {
  if (track.source == TrackSource.screenShareVideo) {
    slides = track;
  } else {
    teacherCamera = track;
  }
});
room.onRemoteVideoTrackRemoved = (uid) => setState(() {
  final teacher = room.room?.remoteParticipants[uid];
  if (teacher?.getTrackPublicationBySource(TrackSource.screenShareVideo) == null) slides = null;
  if (teacher?.getTrackPublicationBySource(TrackSource.camera) == null) teacherCamera = null;
});

await room.connectWithTokenProvider(
  tokenProvider: tokens,
  request: GravixTokenRequest(room: classId, identity: userId, name: userName),
  publishMic: false,
  enableVideo: false,
);
// Show slides large and teacherCamera small, each with VideoTrackRenderer.
classroom.ts
import { RoomEvent, Track, connectWithTokenProvider } from 'gravix-rtc';

const teacherBox = document.getElementById('teacher')!;
const slidesBox = document.getElementById('slides')!;

// Student: the teacher's camera, mic and slides arrive as subscribed tracks.
room
  .on(RoomEvent.TrackSubscribed, (track, publication) => {
    const box = publication.source === Track.Source.ScreenShare ? slidesBox : teacherBox;
    box.appendChild(track.attach());
  })
  .on(RoomEvent.TrackUnsubscribed, (track) => {
    track.detach().forEach((el) => el.remove());
  });

await connectWithTokenProvider(room, {
  tokenProvider: tokens,
  request: { room: classId, identity: userId, name: userName }, // canPublish defaults to false
});

In the browser, show a button that calls room.startAudio() when room.canPlaybackAudio is false.

5. Let a student speak

Publish rights are fixed in the token a person joined with. To let a student talk, your server marks them as called on, and the student's app asks for a new token and joins again with the microphone on. To send them back to listening, do the same with a listen-only token.

lib/classroom.dart
// The teacher called on this student, and your server now allows it.
// Ask for a new token (not from the cache) and rejoin with the mic on.
try {
  final credentials = await tokens.getCredentials(
    GravixTokenRequest(room: classId, identity: userId, name: userName, canPublish: true),
    forceRefresh: true,
  );
  await room.reconnectWithToken(credentials.token, publishMic: true);
} on GravixTokenException catch (e) {
  debugPrint(e.message); // your endpoint said no, or did not answer
}

// Back to listening: a new listen-only token, without the mic.
final credentials = await tokens.getCredentials(
  GravixTokenRequest(room: classId, identity: userId, name: userName),
  forceRefresh: true,
);
await room.reconnectWithToken(credentials.token);
classroom.ts
import { connectWithTokenProvider } from 'gravix-rtc';

// The teacher called on this student, and your server now allows it.
// Drop any cached token for this request, rejoin, then turn the mic on.
const request = { room: classId, identity: userId, name: userName, canPublish: true };
tokens.invalidate(request);
await room.disconnect();
await connectWithTokenProvider(room, { tokenProvider: tokens, request });
await room.localParticipant.setMicrophoneEnabled(true);

The student hears a short gap while rejoining, usually under a second. Others in the class see the student leave and come back. If you need students to start talking without rejoining, contact us.

6. Leave the class

lib/classroom.dart
// Leave the class. This also stops your camera, mic and screen share.
await room.disconnect();

// When the screen goes away for good.
await room.dispose();
classroom.ts
// Leave the class. This also stops your camera, mic and screen share.
await room.disconnect();

Teacher controls

From your own code, the teacher decides who is in the class (your token endpoint signs tokens only for enrolled students) and who may talk (step 5). Muting a student for the whole class, or removing a student from a class that is already running, is set up with our team. Contact us to enable it.

Recording a class

Classes can be recorded in the cloud, so students who missed a lesson can catch up. Recording is set up with our team, including where the files go and how your app shares them. Contact us to set it up. While a class is being recorded, the SDKs tell every participant (see recording in the meetings guide).

AI tutors

An AI tutor can join a class as a video participant, to answer questions or practise with a learner. AI agents are set up with our team. See AI video agents for what they do.

Stuck? Talk to a person, or let your AI coding assistant do the wiring with the AI assistant guide.

Open your online classroom

Sign up free for your App ID, App Certificate and 10,000 free minutes. For recording, teacher controls and AI tutors, talk to our team.