# LiveKit Flutter

> Flutter plugin for real-time audio and video calls in Meeting Place SDK, using LiveKit for media transport, Matrix RTC for signalling, and per-participant end-to-end encryption.

The meeting_place_livekit_flutter plugin provides real-time audio and video call rendering for Flutter applications built on the Meeting Place SDK. It bridges the Matrix transport layer to LiveKit media transport with per-participant end-to-end encryption, and integrates with the meeting_place_matrix SDK to enable audio and video calls in Matrix-backed chat sessions.

## Core Concepts

- [LiveKit](https://livekit.io/): an open-source, scalable Selective Forwarding Unit (SFU) for real-time media transport of audio and video calls.

- [WebRTC](https://webrtc.org/): an open standard for peer-to-peer multimedia communication over IP networks, enabling audio and video streaming.

- Matrix RTC: a Matrix extension for managing call signalling and end-to-end encryption of real-time media via Matrix rooms.

- End-to-End Encryption (E2EE): per-participant encryption of call media streams using keys distributed by Matrix RTC, ensuring only authorised participants can decrypt media.

## Requirements

- Flutter >=3.24.0.

- Dart SDK >=3.8.0.

- [meeting_place_matrix](https://pub.dev/packages/meeting_place_matrix) SDK with a MatrixConfig that includes livekitServiceUrl and livekitSfuUrl.

- Vodozemac encryption runtime initialised once in main() before creating the SDK. See the [meeting_place_matrix README](https://pub.dev/packages/meeting_place_matrix#initializing-the-encryption-runtime) for details.

## Install Dependency

Package: meeting_place_livekit_flutter

```bash
flutter pub add meeting_place_livekit_flutter
```

Alternatively, add the package to your pubspec.yaml file manually:

```yaml
dependencies:
  meeting_place_livekit_flutter: ^
```

Then install it:

```bash
flutter pub get
```

Check the latest version on the [pub.dev registry](https://pub.dev/packages/meeting_place_livekit_flutter) or view the source on [GitHub](https://github.com/affinidi/affinidi-meetingplace-reference-app/tree/main/packages/meeting_place_livekit_flutter).

## Usage

Integrate the plugin into MeetingPlaceMatrixSDK.create() to handle audio and video calls. Set up in this order:

```dart
import 'package:flutter_vodozemac/flutter_vodozemac.dart' as fvod;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await fvod.init();
  // Continue with app startup
}
```

```dart
config: MatrixConfig(
  // ... other settings ...
  livekitServiceUrl: Uri.parse('https://your-livekit-jwt-service'),
  livekitSfuUrl: Uri.parse('wss://your-livekit-sfu'),
)
```

```dart
import 'package:flutter/widgets.dart';
import 'package:meeting_place_livekit_flutter/meeting_place_livekit_flutter.dart';
import 'package:meeting_place_matrix/meeting_place_matrix.dart';

final matrixSDK = await MeetingPlaceMatrixSDK.create(
  wallet: wallet,
  repositoryConfig: repositoryConfig,
  config: matrixConfig,  // With livekitServiceUrl and livekitSfuUrl
  rtcDelegate: FlutterMatrixRTCDelegate(),
  roomFactory: (_) => FlutterLiveKitRoom(),
  options: MeetingPlaceMatrixSdkOptions(
    // ... options
  ),
);
```

```dart
final chatSDK = await MeetingPlaceMatrixChatSDK.initialiseFromChannel(
  channel,
  coreSDK: matrixSDK,
  chatRepository: chatRepository,
);

final chat = await chatSDK.startChatSession();
```

Once set up, audio and video calls within Matrix rooms use Matrix RTC for signalling and LiveKit for media transport, with per-participant end-to-end encryption handled automatically.

For additional usage examples, see the [example folder](https://github.com/affinidi/affinidi-meetingplace-reference-app/tree/main/packages/meeting_place_livekit_flutter/example) on GitHub.

## Classes and Methods

### AudioVideoCallView

A StatelessWidget that renders the video track for a call participant. It returns SizedBox.shrink when hasVideo is false, the session is not LiveKit-backed, or the room has no renderable track for the given participantId.

hasVideo must reflect whether participantId currently has an active video track. Pass it from the controller state so the widget rebuilds only when the caller decides, not via an internal stream subscription.

#### AudioVideoCallView (constructor)

Creates an AudioVideoCallView widget.

Parameters

    session
    AudioVideoCallSession?
    Required
  The active call session. Pass null when there is no ongoing call.

    participantId
    String
    Required
  The unique identifier of the participant whose video track should be rendered.

    hasVideo
    bool
    Required
  Whether the participant currently has an active video track. Derive this from the controller state rather than an internal stream so that the widget rebuilds at the correct time.

    key
    Key?
    Optional
  An optional widget key.

    mirror
    bool
    Optional
  When true, mirrors the video view for the local camera preview. The view is mirrored only while the front camera is active. Peers are never mirrored. Defaults to false.

Example

```dart
import 'package:meeting_place_livekit_flutter/meeting_place_livekit_flutter.dart';

@override
Widget build(BuildContext context, WidgetRef ref) {
  final session = ref.watch(provider.select((s) => s.session));
  final hasVideo = ref.watch(
    provider.select(
      (s) =>
          s.participants
              .firstWhereOrNull((p) => p.participantId == participantId)
              ?.hasVideo ??
          false,
    ),
  );

  return AudioVideoCallView(
    session: session,
    participantId: participantId,
    hasVideo: hasVideo,
  );
}
```

### FlutterLiveKitRoom

Concrete LiveKit implementation of LiveKitRoom. Owns all livekit_client types so they do not leak into the SDK layer. Converts LiveKit events and participants into domain objects before publishing them through the interface.

#### FlutterLiveKitRoom (constructor)

Creates an instance of FlutterLiveKitRoom.

Parameters

    logger
    MeetingPlaceCoreSDKLogger?
    Optional
  Optional logger for diagnostic output.

    hardware
    Hardware?
    Optional
  Optional hardware configuration for audio and video devices.

Example

```dart
import 'package:meeting_place_livekit_flutter/meeting_place_livekit_flutter.dart';
import 'package:meeting_place_matrix/meeting_place_matrix.dart';

final matrixSDK = await MeetingPlaceMatrixSDK.create(
  wallet: wallet,
  repositoryConfig: repositoryConfig,
  config: matrixConfig,
  rtcDelegate: FlutterMatrixRTCDelegate(),
  roomFactory: (_) => FlutterLiveKitRoom(),
);
```

#### Properties

    ownParticipantId
    String?
    Optional
  The identity of the local participant in the room, or null when not connected.

    participants
    List<AudioVideoCallParticipant>
    Optional
  A snapshot of all current call participants mapped to domain objects.

#### connect

Connects to the LiveKit SFU at the given URL using the provided access token.

Parameters

    url
    String
    Required
  The WebSocket URL of the LiveKit SFU (for example, wss://your-livekit-sfu).

    token
    String
    Required
  The LiveKit access token for this participant.

    participantIdToDid
    Map<String, String>
    Optional
  A map of LiveKit participant identities to their corresponding DIDs. Defaults to an empty map.

    onE2EEStateChanged
    OnCallE2EEStateChanged?
    Optional
  Callback invoked when the E2EE state changes for a participant.

    onParticipantDisconnected
    OnParticipantDisconnected?
    Optional
  Callback invoked when a participant disconnects from the room.

    onParticipantsChanged
    void Function()?
    Optional
  Callback invoked whenever the participant list changes.

Example

```dart
import 'package:meeting_place_livekit_flutter/meeting_place_livekit_flutter.dart';

final room = FlutterLiveKitRoom();

await room.connect(
  url: 'wss://your-livekit-sfu',
  token: livekitAccessToken,
  participantIdToDid: {'alice-livekit-id': 'did:example:alice'},
  onParticipantsChanged: () => setState(() {}),
  onE2EEStateChanged: (participantId, state) {
    debugPrint('E2EE state for $participantId: $state');
  },
);
```

#### disconnect

Disconnects from the room and releases all resources.

Parameters

No parameters required.

Example

```dart
await room.disconnect();
```

#### setCameraEnabled

Enables or disables the local camera.

Parameters

    enabled
    bool
    Required
  Pass true to enable the camera or false to disable it.

Example

```dart
// Disable the camera
await room.setCameraEnabled(false);

// Enable the camera
await room.setCameraEnabled(true);
```

#### setMicrophoneEnabled

Enables or disables the local microphone.

Parameters

    enabled
    bool
    Required
  Pass true to enable the microphone or false to mute it.

Example

```dart
// Mute the microphone
await room.setMicrophoneEnabled(false);
```

#### setSpeakerphoneEnabled

Routes audio through the loudspeaker (true) or the earpiece (false).

Parameters

    enabled
    bool
    Required
  Pass true to route audio through the loudspeaker or false for the earpiece.

Example

```dart
await room.setSpeakerphoneEnabled(true);
```

#### switchCamera

Switches between the front and rear camera.

Parameters

No parameters required.

Example

```dart
await room.switchCamera();
```

#### setSharedKey

Sets the shared E2EE key for the call session.

Parameters

    key
    String
    Required
  The shared encryption key for this call session.

Example

```dart
await room.setSharedKey('');
```

#### ratchetKey

Ratchets the E2EE key for a specific participant, advancing the key material for forward secrecy.

Parameters

    participantId
    String
    Required
  The identifier of the participant whose key should be ratcheted.

    keyIndex
    int
    Required
  The index of the key to ratchet.

Example

```dart
await room.ratchetKey('alice-livekit-id', 0);
```

#### forceRemoteKeyframe

Forces the SFU to emit a fresh keyframe for a participant’s video stream. Useful when a video track becomes undecodable after an E2EE key rotation.

Parameters

    participantId
    String
    Required
  The identifier of the participant whose video keyframe should be forced.

Example

```dart
await room.forceRemoteKeyframe('alice-livekit-id');
```

#### renderableVideoTrackFor

Returns the renderable video track for a participant, or null when the room is not connected, the participant is not found, or they have no active video track.

Parameters

    participantId
    String
    Required
  The identifier of the participant whose video track should be retrieved.

Example

```dart
final track = room.renderableVideoTrackFor('alice-livekit-id');
if (track != null) {
  // Render the track
}
```

### FlutterMatrixRTCDelegate

Concrete matrix.WebRTCDelegate that bridges Matrix RTC to flutter_webrtc. Lives in the plugin so that the SDK stays pure Dart. Created once per app session and injected into matrix.VoIP at startup via MeetingPlaceMatrixSDK.create().

The keyProvider is set by the call service before a call starts, allowing per-participant E2EE keys distributed by Matrix RTC to flow into the LiveKit FrameCryptor layer.

#### FlutterMatrixRTCDelegate (constructor)

Creates an instance of FlutterMatrixRTCDelegate. Takes no arguments.

Parameters

No parameters required.

Example

```dart
import 'package:meeting_place_livekit_flutter/meeting_place_livekit_flutter.dart';
import 'package:meeting_place_matrix/meeting_place_matrix.dart';

final matrixSDK = await MeetingPlaceMatrixSDK.create(
  wallet: wallet,
  repositoryConfig: repositoryConfig,
  config: matrixConfig,
  rtcDelegate: FlutterMatrixRTCDelegate(),
  roomFactory: (_) => FlutterLiveKitRoom(),
);
```

#### updateKeyProvider

Sets the LiveKit KeyProvider that should receive per-participant E2EE keys distributed by Matrix RTC.

Parameters

    provider
    KeyProvider?
    Required
  The LiveKit key provider to receive E2EE keys. Pass null to clear the current provider.

Example

```dart
import 'package:meeting_place_livekit_flutter/meeting_place_livekit_flutter.dart';

final delegate = FlutterMatrixRTCDelegate();

// Set the key provider before starting a call
delegate.updateKeyProvider(myKeyProvider);

// Clear the key provider after the call ends
delegate.updateKeyProvider(null);
```
