Daravelv1.13.0
ENID GitHub

LiveKit

LiveKit carries the video, audio and screen sharing of a virtual classroom or a meeting over WebRTC; the browser or the Flutter app connects to it with livekit_client. daravel_livekit is the server side: it signs the access tokens those clients connect with, manages rooms and participants through LiveKit's room service, and verifies LiveKit's webhooks. It speaks the same protocol as LiveKit's PHP SDK (agence104/livekit-server-sdk), so a Laravel and a Daravel application can share one LiveKit server.

dart pub add daravel_livekit
// bootstrap/app.dart
import 'package:daravel_livekit/daravel_livekit.dart';

Application createApp() => Application.configure(basePath: basePath)
    .withLivekit()
    // ...
    .create();
LIVEKIT_URL=https://livekit.example.com   # or LIVEKIT_HOST, as older apps name it
LIVEKIT_API_KEY=...
LIVEKIT_API_SECRET=...                    # or LIVEKIT_SECRET_KEY

Boot warns while the key or the secret is missing.

Tokens#

A client joins a room with a token the server signs:

router.post('/classes/{id}/join', (request) async {
  final user = (await Auth.user<User>())!;
  final token = await Livekit.token(
    identity: user.uuid,
    name: user.name,
    ttl: const Duration(hours: 2),
    video: VideoGrant(roomJoin: true, room: 'class-7a', roomAdmin: user.isTeacher),
  ).toJwt();
  return {'token': token, 'url': Livekit.config.url};
});

VideoGrant has LiveKit's fields (roomJoin, room, roomAdmin, canPublish, canSubscribe, canPublishData, canPublishSources, hidden, ...); a field left out is not sent, so LiveKit's default applies. Joining needs an identity, and a token lives 4 hours unless ttl says otherwise. LivekitToken.verify(jwt, apiKey: ..., apiSecret: ...) reads one back.

Rooms and participants#

final rooms = Livekit.rooms;

if ((await rooms.listRooms(['class-7a'])).isEmpty) {
  await rooms.createRoom('class-7a', maxParticipants: 40, metadata: '{"subject":"math"}');
}
final present = await rooms.listParticipants('class-7a');
await rooms.updateParticipant('class-7a', 'student-9', metadata: '{"hand":true}');
await rooms.removeParticipant('class-7a', 'student-9');
await rooms.sendData('class-7a', utf8.encode('{"type":"quiz"}'), topic: 'lk-chat');
await rooms.deleteRoom('class-7a');

Each call is signed with a 10-minute token carrying only the grant it needs (roomCreate, roomList, or roomAdmin on that room), as LiveKit's SDKs do. A refusal is a LivekitException with Twirp's code (not_found, permission_denied) and the HTTP status. The client sends through the application's HTTP client, so Http.fake() stubs it in tests.

Webhooks#

LiveKit posts events (room_started, room_finished, participant_joined, participant_left, track_published, egress_ended, ...) signed with the API secret:

router.post('/livekit/webhook', (request) async {
  final event = await Livekit.webhook(request);
  if (event.event == 'room_finished') {
    await Meeting.query().where('livekit_room_name', event.room!.name).update({'status': 'FINISHED'});
  }
  return Response.noContent();
});

Livekit.webhook checks the Authorization token (signature, expiry, issued under the API key) and that its sha256 claim is the base64 SHA-256 of the exact body, as LiveKit's SDKs do; anything else is a 401 LivekitWebhookException. There is no switch to skip that check: a webhook route that accepts unsigned events lets anyone mark a meeting as finished.

With Laravel#

bench/interop/livekit.sh runs LiveKit's PHP SDK and Daravel against each other: each verifies the other's tokens, and each accepts the other's signed webhooks and refuses a tampered body. The tests run against a LiveKit server in development mode, which accepts Daravel's join tokens. Egress and ingress, and SIP grants, are not covered yet.