# Migrating to 5.0

5.0 runs the meeting on VideoSDK's native Android and iOS SDKs instead of the
Dart implementation that shipped through `3.12.x`. From `5.0.0-beta.8` the
Dart API also uses the same names as the Android, iOS and React Native SDKs;
every rename and removal is listed in
[API names aligned across SDKs](#api-names-aligned-across-sdks-500-beta8).

**Already on `4.0.1`?** The engine and all the project setup are the same —
skip to [From 4.0.1](#from-401) for the short list of what changed. The setup
sections below are for applications coming from `3.12.x`.

---

## 1. Platform support

**5.0 supports Android and iOS only.** Web, macOS and Windows are not in this
release — applications targeting them should stay on the latest `3.12.x`. The
Dart backend for those platforms slots back in behind the same API when they
return, so no application code changes when that happens.

## 2. Dart changes

The entry point is unchanged:

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

`VideoSDK`, `Room`, `Participant` and `Events` keep their names (`Stream` is
now `RTCStream`; see
[API names aligned across SDKs](#api-names-aligned-across-sdks-500-beta8)).
Five things to change:

**Remove any direct `videosdk_webrtc` dependency and its imports.** The video
view, the renderer and the media-device types now come from `videosdk` itself:

```dart
// before
import 'package:videosdk_webrtc/flutter_webrtc.dart';

// after — nothing extra; RTCVideoView, RTCVideoRenderer and
// MediaDeviceInfo are exported by package:videosdk/videosdk.dart
```

**`FacingMode` values are renamed** to match the Android and iOS SDKs:

| before | after |
|---|---|
| `FacingMode.user` | `FacingMode.FRONT` |
| `FacingMode.environment` | `FacingMode.BACK` |

Update anything passing `facingMode` to `createCameraVideoTrack()` or
`PreCallVideoConfig`.

**`enableScreenShare()` no longer takes `enableAudio` or `source`.** Set them on
the track instead, and `CustomTrack.toMap()` is removed:

```dart
// before
await room.enableScreenShare(enableAudio: true);

// after
final track = await VideoSDK.createScreenShareVideoTrack(
  encoderConfig: CustomScreenShareTrackConfig.h1080p_30fps,
  withAudio: true, // Android only
);
// Android returns null if the user declines the capture prompt.
if (track != null) await room.enableScreenShare(customTrack: track);
```

**The track factories are typed.** `createCameraVideoTrack()`,
`createMicrophoneAudioTrack()` and `createScreenShareVideoTrack()` return
`Future<CustomStreamTrack?>` instead of `dynamic`, so handle `null`:

```dart
final track = await VideoSDK.createCameraVideoTrack();
if (track != null) renderer.srcObject = track.mediaStream;
```

**Run `flutter pub get`, then do a clean rebuild** — `flutter clean` first. The
native SDKs are resolved at build time, so an incremental build over a 3.12.x
build tree will not pick them up.

## 3. Android setup

Three items in your app-level `android/app/build.gradle`:

```groovy
android {
    compileSdkVersion 36

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_17
        targetCompatibility JavaVersion.VERSION_17
    }

    defaultConfig {
        minSdkVersion 23      // the SDK's floor
        targetSdkVersion 36
    }
}
```

Java 17 is required — the WebRTC jar the native SDK depends on uses static
methods in the `EglBase` interface.

Permissions in `android/app/src/main/AndroidManifest.xml` are unchanged from
3.12.x; see the README for the full list.

## 4. iOS setup

**Deployment target 13.0 or higher** in `ios/Podfile`:

```ruby
platform :ios, '13.0'
```

**The iOS side ships as a Swift Package.** If your project is CocoaPods-only,
turn on Flutter's SPM support once:

```bash
flutter config --enable-swift-package-manager
```

**Usage descriptions** in `ios/Runner/Info.plist` are unchanged from 3.12.x
(`NSCameraUsageDescription`, `NSMicrophoneUsageDescription`).

## 5. iOS screen share: the Broadcast Upload Extension

This is the one genuinely new piece of setup. iOS screen share goes through
ReplayKit, which requires a Broadcast Upload Extension **inside your own app**.
The extension has to be signed with your team and your bundle id, so the SDK
cannot ship one for you.

A complete working target is in this repository at
[`example/ios/FlutterBroadcast`](example/ios/FlutterBroadcast) — copy it and
change the identifiers. The steps:

**1. Add the target.** In Xcode: *File → New → Target → Broadcast Upload
Extension*. Give it a name (the example uses `FlutterBroadcast`). Do **not**
include a UI extension.

**2. Add an App Group** shared by both targets. In *Signing & Capabilities*, add
*App Groups* to the Runner target **and** to the extension target, with the same
identifier — for an app with bundle id `com.example.app`, that would be
`group.com.example.app.ScreenBroadcast`. The app
and the extension talk over a socket in that group's shared container, so they
must match exactly.

**3. Copy the five Swift files** from `example/ios/FlutterBroadcast` into your
target:

| file | what it does |
|---|---|
| `SampleHandler.swift` | the `RPBroadcastSampleHandler` entry point |
| `SampleUploader.swift` | pushes sample buffers over the socket |
| `SocketConnection.swift` | the socket to the app |
| `DarwinNotification.swift`, `Atomic.swift` | start/stop signalling helpers |

In `SampleHandler.swift`, set your App Group id:

```swift
private enum Constants {
    static let appGroupIdentifier = "group.com.example.app.ScreenBroadcast"
}
```

**4. Point the app at the extension.** In `ios/Runner/Info.plist`:

```xml
<key>RTCAppGroupIdentifier</key>
<string>group.com.example.app.ScreenBroadcast</string>
<key>RTCScreenSharingExtension</key>
<string>com.example.app.FlutterBroadcast</string>
```

`RTCScreenSharingExtension` is the extension's **full bundle id**, which must be
prefixed with the app's own bundle id.

**5. Keep `CFBundleIdentifier` in the extension's `Info.plist`.** It must read:

```xml
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
```

Xcode validates the embedded binary's prefix from this key, not from the
`PRODUCT_BUNDLE_IDENTIFIER` build setting. Omitting it fails the build with
*"Embedded binary's bundle identifier is not prefixed with the parent app's"*.

## From 4.0.1

`4.0.1` already runs on the native Android and iOS SDKs, so none of the project
setup above changes. Five things to check in your Dart code.

**`FacingMode` is renamed.** 4.0.1 still used the browser names:

| 4.0.1 | 5.0 |
|---|---|
| `FacingMode.user` | `FacingMode.FRONT` |
| `FacingMode.environment` | `FacingMode.BACK` |

**`enableScreenShare()` takes a track.** See
[section 2](#2-dart-changes) above — `enableAudio` and `source` moved onto
`createScreenShareVideoTrack()`, and `CustomTrack.toMap()` is gone.

**PubSub's back-pressure API is back in `5.0.0-beta.8`** with a listener
object; see [API names aligned across SDKs](#api-names-aligned-across-sdks-500-beta8).
`5.0.0-beta.1`–`beta.7` did not export these:

| not in 5.0 | |
|---|---|
| `PubSubSubscribeOptions`, `PubSubRealtimeOverflow` | the `options:` argument to `subscribe()` |
| `onOldMessagesReceived`, `onBatchReceived`, `onMessageDrop` | the extra `subscribe()` callbacks |
| `PubSubException`, `PubSubPublishFailed`, `PubSubSubscribeFailed`, `PubSubUnsubscribeFailed`, `PubSubMeetingNotJoined`, `PubSubMeetingReconnecting` | the thrown exception types |
| `PubSubMessage.sendOnly` | the addressed-participant list |

`subscribe()` is back to its two-argument form:

```dart
// 4.0.1
await room.pubSub.subscribe("CHAT", onMessage,
    options: const PubSubSubscribeOptions(oldMessageLimit: 100),
    onBatchReceived: onBatch);

// 5.0
await room.pubSub.subscribe("CHAT", onMessage);
```

**Errors are one type now.** Where 4.0.1 threw the `PubSub*` exceptions above,
5.0 reports failures as `VideoSDKError`, which carries a numeric `code`, a
`message` and a `name`, and is used across the whole API rather than per
feature.

**The track factories are typed.** `createCameraVideoTrack()`,
`createMicrophoneAudioTrack()` and `createScreenShareVideoTrack()` return
`Future<CustomStreamTrack?>` instead of `dynamic`, so handle `null`:

```dart
final track = await VideoSDK.createCameraVideoTrack();
if (track != null) renderer.srcObject = track.mediaStream;
```

## API names aligned across SDKs (5.0.0-beta.8)

`5.0.0-beta.8` renames the Dart API to match the Android, iOS and React Native
SDKs. Nothing is kept under the old name.

### Creating a room

| before | after |
|---|---|
| `createRoom(displayName:)` (required) | `createRoom(participantName:)` — optional; the SDK picks a random name |
| `createRoom(customCameraVideoTrack:)` | `createRoom(customCameraTrack:)` |
| `createRoom(customMicrophoneAudioTrack:)` | `createRoom(customMicrophoneTrack:)` |
| `participantId = ''`, `maxResolution = hd`, `multiStream = true`, `mode = SEND_AND_RECV`, `metaData = {}`, `signalingBaseUrl = ''`, `preferredProtocol = UDP_OVER_TCP` | all nullable with no Dart default; left out, the native SDK applies the same defaults |
| `Mode.CONFERENCE`, `Mode.VIEWER` | removed — use `Mode.SEND_AND_RECV`, `Mode.RECV_ONLY` |
| `createRoom(notification:)`, `NotificationInfo` | removed — it never reached the screen-share notification |

### `VideoSDK`

| before | after |
|---|---|
| `VideoSDK.logLevel` | `VideoSDK.getLogLevel()` |
| `VideoSDK.keyProvider` | `VideoSDK.getKeyProvider()` |
| `VideoSDK.getVideoDevices()` / `getAudioDevices()` | `VideoSDK.getCameras()` / `getMics()` |
| `room.selectedCam`, `selectedMic`, `selectedSpeaker` (+ deprecated `*Id`) | `VideoSDK.getSelectedVideoDevice()`, `VideoSDK.getSelectedAudioDevice()` |
| `VideoSDK.on/off(Events.deviceChanged)`, `Events.deviceChanged` | `room.setAudioDeviceChangeListener(...)` |
| `VideoSDK.applyVideoProcessor(videoProcessorName:)` / `removeVideoProcessor()` | `room.setVideoProcessor(name)` / `room.removeVideoProcessor()` |
| `VideoSDK.runPreCallTest(...)` returning `PreCallTest` | returns `Future<PreCallTestResult>`; stop with `VideoSDK.stopPreCallTest()` |
| `BaseKeyProvider.setKey(id, key)` | `VideoSDK.setParticipantKey(id, key)`; a `null` key removes it |
| — | new `BaseKeyProvider.removeKey(id)`: on the installed provider, the same as `setParticipantKey(id, null)` |
| — | new `VideoSDK.removeKeyProvider()` turns E2EE off; `setKeyProvider` still takes a non-null provider |
| `BaseKeyProvider(discardFrameWhenCryptorNotReady: …)` | `BaseKeyProvider()` — the option had no effect |
| `setScreenFlash(ScreenFlash.whiteOverlay)` / `setScreenFlash(null)` | `setScreenFlash(true)` / `setScreenFlash(false)`; `ScreenFlash` is removed |
| `PreCallVideoConfig(optimizationMode:)`, `PreCallOptimizationMode` | removed — no SDK uses the content hint |

### Types

| before | after |
|---|---|
| `Stream` | `RTCStream`; `kind` is a `MediaType` (`VIDEO`, `AUDIO`, `SHARE`, `SHARE_AUDIO`), plus `participantId` and `off()` |
| `CustomTrack.ended` | `CustomStreamTrack.isEnded` (read-only) |
| `CustomTrack.videoEncoderConfig` | `CustomStreamTrack.encoderConfig` |
| `VideoSDKErrors` map | removed — compare with the `VideoSDKError.*` constants; errors are equal by `code` |
| `CustomTrack`, `dispose()` | `CustomStreamTrack`, `stop()`; new `onEnded` callback |
| `CustomVideoTrackConfig` / `CustomAudioTrackConfig` | `CustomCameraTrackConfig` / `CustomMicrophoneTrackConfig` |
| `VideoDeviceInfo` / `AudioDeviceInfo` | `CameraDeviceInfo` / `MicrophoneDeviceInfo` |
| `VideoSdkError` | `VideoSDKError` |
| `ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED`, `ERROR_MEETING_MEDIA_CONNECTION_FAILED`, `ERROR_UNABLE_TO_JOIN_MEETING`, `ERROR_END_MEETING_FAILED`, `ERROR_MEETING_RECONNECTING`, `INVALID_MEETING_ID`, `UNAUTHORIZED_MEETING_ID`, `MEETING_ID_DISABLED`, `SWITCH_MEETING_FAILED` | the same constants with `ROOM` in place of `MEETING` (`ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED`, `ERROR_ROOM_RECONNECTING`, `INVALID_ROOM_ID`, …); codes are unchanged |
| `maxLayer: 3` (`int`) | `maxLayer: EncodingLayer.MAX_LAYER_3` |
| `noiseConfig: {'noiseSuppression': false}` | `noiseConfig: NoiseConfig(noiseSuppression: false)` |
| quality strings `'low'`, `'med'`, `'high'` | `VideoQuality.LOW`, `MEDIUM`, `HIGH` |
| `TranscriptionConfig(webhookUrl:, summaryConfig:)` | `TranscriptionConfig(webhookURL:, summary:, modelConfig:)` |
| `PostTranscriptionConfig(summaryConfig:)` | `PostTranscriptionConfig(summary:)` |
| `SummaryConfig(prompt:)` required | optional |
| recording/HLS/livestream `config` maps | `RecordingConfig`, `HLSConfig`, `LivestreamConfig`, `ConfigLayout` |
| livestream outputs `{'url', 'streamKey'}` maps | `LivestreamOutput(url:, streamKey:)` |
| livestream config `'recording': {'enabled': true}` | `LivestreamConfig(recording: LivestreamRecordingConfig(enabled: true))` |
| `PubSubMessages`, `PubSubMessage.timestamp` (`DateTime`) | removed; `timestamp` is the server's `String` |
| `TranscriptionText.type` (`String?`) | `TranscriptionType?` — `REALTIME` (partial), `FULL_SENTENCE` (settled) or `null` |
| `TranslationText.type` (`String?`) | `TranslationType?` — `PARTIAL`, `FULL` or `null` |
| `realtimeStore.getValue(key)` returned `null` for a key that is not set | throws `VideoSDKError` `4046` (the server refuses it); `null` only when the server answers with no value |
| `RTCVideoView(mirror:)` only | also `RTCVideoRenderer.setMirror(bool)`, which overrides the view's `mirror:` |
| `E2EEState.EncryptionSucessed` / `DecryptionSucessed` | `EncryptionSuccess` / `DecryptionSuccess` |
| `FacingMode.front` / `back` | `FacingMode.FRONT` / `BACK` |
| `MaxResolution.hd` / `sd` | `MaxResolution.HD` / `SD` |
| `MediaType.video` / `audio` / `share` / `shareAudio` | `MediaType.VIDEO` / `AUDIO` / `SHARE` / `SHARE_AUDIO` |
| `Permissions.audio` / `video` / `audio_video` | `Permissions.AUDIO` / `VIDEO` / `AUDIO_VIDEO` |
| `ConfigMode.video_and_audio` / `audio` | `ConfigMode.VIDEO_AND_AUDIO` / `AUDIO` |
| `ConfigQuality.high` / `med` / `low` | `ConfigQuality.HIGH` / `MED` / `LOW` |
| `ConfigOrientation.landscape` / `portrait` | `ConfigOrientation.LANDSCAPE` / `PORTRAIT` |
| `CustomMicrophoneTrackConfig.speech_standard`, `high_quality`, … | `CustomMicrophoneTrackConfig.SPEECH_STANDARD`, `HIGH_QUALITY`, … (all six presets) |

Enum values are upper case now: `RoomState.CONNECTED`, `LogLevel.INFO`,
`LeaveReason.MANUAL_LEAVE_CALLED` (plus new `DUPLICATE_PARTICIPANT`),
`AgentState.SPEAKING`, `CaptureFlash.ON`.

### `Room`

| before | after |
|---|---|
| `Room.dispose()` | removed — the room releases itself after `Events.roomLeft` or a failed join, and when a later `createRoom` replaces it; create a new one to rejoin |
| `micEnabled` / `camEnabled` | `isMicEnabled` / `isCamEnabled`; new `isScreenSharing`, `roomState` |
| `activeSpeakerId` / `activePresenterId` | `getActiveSpeakerId()` / `getActivePresenterId()` |
| `livestreamState` | `liveStreamState` |
| `hlsDownstreamUrl`, `hlsUrls` | `hlsUrl` (`HLSUrl`) |
| `changeCam([device, track])` | `changeCam({deviceId, customCameraTrack})` |
| `changeMic([device, track])` | `changeMic({deviceId, customMicrophoneTrack})` |
| `enableCam([customVideoTrack])` / `unmuteMic([customAudioTrack])` | parameter renamed `customCameraTrack` / `customMicrophoneTrack` |
| `switchAudioDevice(device)` | `setAudioDevice(device)` |
| `startHls()` / `stopHls()` | `startHLS()` / `stopHLS()` |
| `postTranscriptionConfig:` on `startRecording`, `startHLS` | `transcription:` |
| `postTranscriptionConfig:` on `startLivestream` | removed — no native SDK took it |
| `startTranscription(transcriptionConfig:)` | `startTranscription(config:)` |
| `setWebcamQuality(String)` | `setWebcamQuality(VideoQuality)` |
| `requestMediaRelay(meetingId)` / `stopMediaRelay(meetingId)` | parameter is `destinationRoomId` |
| `pauseAllStreams(kind: 'video')` / `resumeAllStreams(kind:)` | `kind: StreamKind.VIDEO` (`AUDIO`, `VIDEO`, `SHARE`) |
| `requestMediaRelay(kinds: ['audio', …])` | `kinds: [StreamKind.AUDIO, …]`; `share_audio` is gone — neither native SDK relayed it |
| `enableRelayMedia`, `disableRelayMedia` | removed |
| `refreshConnection()`, `getAvailableVideoCodecs()`, `getActiveVideoCodec()`, `getAppliedAudioConfig()` | removed |
| — | new `respondEntry(participantId, decision)`, `removeAllEventListeners()`, `transcriptionState`, `translationState` |

`room.localParticipant` exists from `createRoom`, with the requested id and
name (empty when none was given); join fills in the ones the room assigned.

Removed with no replacement (undocumented leftovers): `Room.localParticipantId`,
`getParticipentId()`, `switchingRoom()`, `isSendAndReceiveMode()`, `isViewerMode()`,
`getScreenShareSources()`, `enableShare()` / `disableShare()` (use
`enableScreenShare()` / `disableScreenShare()`), `pause(Consumer)` / `resume(Consumer)`,
`setConsumerQuality()` / `setScreenShareQuality()` / `setViewPort()` keyed by
participant id (use the `Participant` methods), `deviceInfo`, `baseUrl`,
`videoSDKTelemetery`; `RTCStream.track` / `copyWith()`; `RealtimeStore.dispose()`;
`VideoSDK.isFirefox()`, `setAppleAudioConfiguration()`, `getDeviceInfo()`,
`mediaDevices`, `loadMediaDevices()`, `MediaDeviceType`, `requestIOSScreenSharePermission()`, `e2eeType`;
`RTCVideoRenderer.audioOutput()`.

`createRoom` now creates the native room, so pre-join calls such as
`setVideoProcessor` reach it directly. Only one `Room` exists at a time: a new
`createRoom` releases the previous one. If you drop a `Room` while its `join()`
is still running, await the join and then `leave()`:

```dart
room.join().then((_) => room.leave()).ignore();
```

On Android, the native `VideoSDK.getInstance().setVideoProcessor(processor)`
helper applies to the room Dart created and throws `IllegalStateException`
before `createRoom`.

### `Participant`

| before | after |
|---|---|
| `unmuteMic()` / `muteMic()` | `enableMic()` / `disableMic()` |
| `quality` (`String?`) | `videoQuality` (`VideoQuality?`) |
| `setQuality(String)` | `setQuality(VideoQuality)` |
| `isAgent` (getter) | `isAgent()` |
| `setScreenShareQuality(String)` | `setScreenShareQuality(VideoQuality)` |
| `AgentParticipant.agentId` / `agentState` | `getAgentId()` / `getAgentState()` |
| — | new `removeAllEventListeners()` |

### PubSub and realtime store

```dart
// before
final history = await room.pubSub.subscribe("CHAT", onMessage);
final id = await room.realtimeStore.observe("KEY", onValue);
await room.realtimeStore.stopObserving(id);

// after
final chat = PubSubMessageListener(
  onMessageReceived: onMessage,
  onOldMessagesReceived: (messages, info) {},
);
await room.pubSub.subscribe("CHAT", chat,
    const PubSubSubscribeOptions(oldMessageLimit: 50));
await room.pubSub.unsubscribe("CHAT", chat);

await room.realtimeStore.observe("KEY", onValue);
await room.realtimeStore.stopObserving("KEY", onValue);
```

Each listener is its own subscription, with its own options and its own
history. Subscribing the same listener to a topic twice throws `3073`.

### Events

| before | after |
|---|---|
| `liveStreamStarted`, `liveStreamStopped`, `liveStreamStateChanged` | `livestreamStarted`, `livestreamStopped`, `onLivestreamStateChanged` |
| `e2eeStateChanged` | `onE2EEStateChanged`, with the same payloads on the room and the participant |
| `hlsStarted`, `hlsStopped` | removed — use `hlsStateChanged` |
| `hlsStateChanged(Map)` | `hlsStateChanged(String state, HLSUrl? hlsUrl)` |
| `dataMessage` with `DataMessage.from/payload/bytes/reliability` | `dataReceived` with `senderId`, `text`, `data`, `reliable` |
| `participantLeft(String id, reason)` | `participantLeft(Participant participant, reason)`, delivered once (3.x delivered it twice) |
| `transcriptionStateChanged(Map)` | `transcriptionStateChanged(TranscriptionState)` |
| `translationLanguageChanged(language)` | `translationLanguageChanged(participantId, language)` |
| `error(Map)` | `error(VideoSDKError)` |
| `entryResponded` map key `id` | `participantId` |
| `roomJoined` map key `switchingRoomId` | `switchRoomId` |
| `mediaRelayRequestReceived(meetingId, peerId, …)` | `(participantId, roomId, displayName, accept, reject)` |
| `streamStateChanged(String state, …)` with `active`, `freeze-detected`, … | `(StreamState state, int timestamp)`: `StreamState.ACTIVE`, `FREEZE_DETECTED`, … |
| `participantModeChanged` map `mode` (`String`) | `Mode` |
| `pinStateChanged` map `state` (`{cam, share}` map) | `ParticipantPinState` |
| `e2eeStateChanged` (room) map `state` (`String`) | `E2EEState` |
| `micRequested` / `cameraRequested` map | adds `participantId` |
| `qualityLimitation(type, state)` | adds `timestamp` |
| `pausedAllStreams(String kind)` / `resumedAllStreams(String kind)` | `(StreamKind? kind)`; `null` when every kind was |
| `mediaRelayRequestResponse` `decision` (`String`) | `RelayDecision?` — `ACCEPTED`, `REJECTED` |
| — | new `externalCallRinging`, `externalCallAnswered`, `externalCallRejected`, `externalCallHangup` (each with a `CallType`: `INCOMING`, `OUTGOING`); `streamStateChanged` now delivered on the `RTCStream` |

---

Questions: [docs.videosdk.live](https://docs.videosdk.live) ·
[Discord](https://discord.gg/kgAvyxtTxv)
