Migration Guide
This guide covers what you need to change when upgrading the Flutter SDK from v4.0.1 to v5.0.0. Many names changed, several calls changed shape, actions now throw errors you must catch, and Web, macOS and Windows are no longer supported.
Update the dependency first:
dependencies:
videosdk: ^5.0.0
Then run flutter clean and flutter pub get.
v5.0.0 changes where your app can run and how it builds:
- Android and iOS only. Web, macOS and Windows are not supported. If your app ships on any of them, stay on v4.0.1.
- Android: set
minSdkVersionto 24 or higher (v4.0.1 needed 23). The SDK runs on arm64-v8a and armeabi-v7a only, not on x86 and x86_64 emulators. Test on an ARM device or an arm64 emulator image. To keep Google Play from offering your app to those devices, addndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") }todefaultConfiginandroid/app/build.gradle.kts(ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' }in a Groovybuild.gradle), and adddisable-abi-filtering=truetoandroid/gradle.properties(Flutter 3.41 or later). Without that line, Flutter adds its own ABIs to the filter and yours has no effect. - iOS: the plugin now ships as a Swift Package and needs Flutter 3.41 or later. Turn on Flutter's Swift Package Manager support for the project by adding
config:withenable-swift-package-manager: trueunderflutter:in your app'spubspec.yaml, or runflutter config --enable-swift-package-manageron every machine that builds your app, including CI.
Sections 1 to 7 cover the changes that stop your app compiling, plus a few in the same areas that still compile but behave differently, such as setting track.enabled or reading RTCVideoRenderer.textureId (section 1) or checking a track factory's result for null (section 3). Section 6 lists every renamed API. Read section 3 even if your app compiles: on Android the SDK no longer asks for camera or microphone permission, so your app must ask before it joins. Sections 8 to 12 compile without errors but behave differently at runtime, so the analyzer will not lead you to them. Section 13 lists what was removed.
1. Remove videosdk_webrtc
v5.0.0 no longer uses videosdk_webrtc, and your app must not depend on it either: on iOS it clashes with the SDK. RTCVideoView, RTCVideoRenderer, RTCVideoViewObjectFit and MediaDeviceInfo keep their names and now come from package:videosdk/videosdk.dart, so delete the dependency and the import.
The media objects changed with it. A participant's stream is now RTCStream (it was Stream), and CustomTrack is now CustomStreamTrack. The members you reached through videosdk_webrtc are gone.
Before
import 'package:videosdk/videosdk.dart';
import 'package:videosdk_webrtc/flutter_webrtc.dart';
Future<void> switchCamera(CustomTrack cameraTrack) async {
final MediaStreamTrack videoTrack =
cameraTrack.mediaStream.getVideoTracks().first;
await Helper.switchCamera(videoTrack);
}
After
import 'package:videosdk/videosdk.dart';
Future<void> switchCamera(Room room) async {
await room.changeCam(); // no arguments: the next camera
}
| v4.0.1 | v5.0.0 |
|---|---|
Helper.switchCamera(track) | room.changeCam() in a room. Before joining, stop the track and create a new one with the other facingMode. |
track.enabled = false to mute | room.muteMic() or room.disableCam(). Setting enabled no longer mutes anything. |
mediaStream.dispose(), track.stop() | customTrack.stop() |
stream.track.pause(), stream.track.resume() | stream.pause(), stream.resume() |
stream.track.getStats() | participant.getVideoStats(), getAudioStats() or getShareStats() |
stream.track.peerId | stream.participantId |
stream.track and its other members, such as track, stream, rtpReceiver, rtpParameters, spatialLayer and close() | Removed. Use stream.id, stream.kind and stream.participantId. |
RTCVideoRenderer.textureId in a Texture widget | Always null. Render with RTCVideoView. |
Key points:
- Remove
videosdk_media_effectstoo, because it depends onvideosdk_webrtc. For noise suppression, passnoiseSuppressionincreateMicrophoneAudioTrack(noiseConfig:)(section 3). A background image has no replacement on Android. On iOS,room.setVideoProcessor()takes the image's file path or URL (section 7). Background blur is new inVideoSDK.setVideoEffects(). MediaStreamandMediaStreamTrackcan no longer be imported. Where you annotatedcameraTrack.mediaStreamwith one of them, let Dart infer it:final media = cameraTrack.mediaStream;.- v5.0.0 no longer pulls in
random_string,sdp_transform,h264_profile_level_id,flutter_foreground_task,http,encrypt,videosdk_room_stats,videosdk_otel,dart_webrtc,device_info_plus,platform_detect,socket_io_client,protobuforfixnum, nor the packages they brought with them, such asshared_preferences,path_provider,cryptoandlogging. If your code imports a package that is not in your ownpubspec.yaml, add it there: afterflutter pub get,dart analyzereports each one asTarget of URI doesn't exist. flutter_foreground_taskalso addedWAKE_LOCK,RECEIVE_BOOT_COMPLETEDandPOST_NOTIFICATIONSto your app's merged Android manifest. v5.0.0 does not, so declare any of them that your own code uses inandroid/app/src/main/AndroidManifest.xml.RTCVideoRenderer.initialize()now completes at once. Keeping the call is harmless.
2. VideoSDK.createRoom() parameters changed
Three parameters were removed, three were renamed, and maxResolution changed type and default.
Before
final room = VideoSDK.createRoom(
roomId: roomId,
token: token,
displayName: "Alex",
customCameraVideoTrack: cameraTrack,
customMicrophoneAudioTrack: micTrack,
maxResolution: 'hd',
defaultCameraIndex: 1, // the front camera in the 4.x examples
debugMode: true,
);
After
final room = VideoSDK.createRoom(
roomId: roomId,
token: token,
participantName: "Alex",
customCameraTrack: cameraTrack,
customMicrophoneTrack: micTrack,
maxResolution: MaxResolution.HD,
defaultCamera: FacingMode.FRONT,
);
| Parameter | v4.0.1 | v5.0.0 |
|---|---|---|
displayName | String, required | Renamed participantName, and optional. Leave it out and the SDK picks a random name. |
customCameraVideoTrack, customMicrophoneAudioTrack | CustomTrack? | Renamed customCameraTrack and customMicrophoneTrack, of type CustomStreamTrack? |
maxResolution | String, default 'sd' | MaxResolution enum. Left out, the SDK uses MaxResolution.HD. |
mode | Mode, including Mode.CONFERENCE and Mode.VIEWER | Mode.SEND_AND_RECV, Mode.RECV_ONLY or Mode.SIGNALLING_ONLY. CONFERENCE and VIEWER are removed. |
participantId, multiStream, mode, metaData, signalingBaseUrl, preferredProtocol | Dart default values | Optional, with no Dart default. Left out, the SDK applies its own default, the same as before. |
defaultCameraIndex | int, default 0 | Removed. Use defaultCamera. |
defaultCamera | — | FacingMode, default FacingMode.FRONT |
debugMode | bool, default true | Removed, with no replacement. VideoSDK.setLogLevel() sets how much the SDK logs. |
notification, NotificationInfo | Text of the Android screen-share notification | Removed. The SDK shows its own notification. |
Key points:
setQuality()now gets a higher layer by default. In v4.0.1 the camera always opened at 1280x720, andmaxResolution(default'sd') only chose the layersetQuality()received:"high"got the middle one. The v5.0.0 default,MaxResolution.HD, keeps the camera at 1280x720 and gives"high"the top layer, so remote video uses more bandwidth.MaxResolution.SDkeeps the v4.0.1 layers, but also opens the camera at 960x540.defaultCamerapicks the camera by direction, not by its position in the device list. In v4.0.1 the list fromVideoSDK.getVideoDevices()starts with a back camera on Android and iOS phones, so the default index0usually opened a back camera. v5.0.0 opens the front camera unless you passdefaultCamera: FacingMode.BACK.- To see the SDK's own logs in
flutter run, callawait VideoSDK.enableLogForwarding((level, message) => debugPrint('[$level] $message'));.
3. Track factories are typed and throw
createCameraVideoTrack() and createMicrophoneAudioTrack() now return Future<CustomStreamTrack> instead of dynamic. CustomTrack is renamed CustomStreamTrack, and its configs are renamed too: CustomVideoTrackConfig is CustomCameraTrackConfig, and CustomAudioTrackConfig is CustomMicrophoneTrackConfig. Two behaviour changes come with that: a failure throws VideoSDKError instead of returning null and reporting on VideoSDK.on(Events.error), and on Android the SDK no longer asks the user for permission.
Before
VideoSDK.on(Events.error, (error) {
log("Could not create the track: ${error['message']}");
});
// Asked for camera permission itself, and returned null on failure.
CustomTrack cameraTrack = await VideoSDK.createCameraVideoTrack(
encoderConfig: CustomVideoTrackConfig.h720p_w1280p,
facingMode: FacingMode.user,
);
After
final permissions = await VideoSDK.requestPermissions(Permissions.VIDEO);
if (permissions['video'] != true) return;
final CustomStreamTrack cameraTrack;
try {
cameraTrack = await VideoSDK.createCameraVideoTrack(
encoderConfig: CustomCameraTrackConfig.h720p_w1280p,
facingMode: FacingMode.FRONT,
);
} on VideoSDKError catch (error) {
// For example 3017 (permission denied), 3021 (no camera), 3023 (camera in use).
log("Could not create the track: ${error.code} ${error.name}");
return;
}
Key points:
- Ask for permission before you join. On Android the SDK never asks for camera or microphone permission: not in the track factories, and not when
join(),unmuteMic(),enableCam()or achangeMode()toMode.SEND_AND_RECVturns a device on. Without permission that device stays off and the SDK reports3017(camera) or3018(microphone). v4.0.1 asked in all of these places. CallVideoSDK.requestPermissions()before you create tracks, calljoin(), or callchangeMode(Mode.SEND_AND_RECV). - Without permission the factories throw
3017(camera) or3018(microphone). Other codes:3021and3022for no camera or microphone,3023and3024for a device in use,3026when the camera fails to open, and3071for an unknowncameraIdormicrophoneId. noiseConfigis aNoiseConfigand decides every flag. PassNoiseConfig(noiseSuppression: true, echoCancellation: true, autoGainControl: true)instead of a map. In v4.0.1, a flag you left out ofcreateMicrophoneAudioTrack(noiseConfig:)took its value from theencoderConfigprofile. In v5.0.0, once you pass aNoiseConfig, every flag you leave out is turned on, except the newhighPassFilter, which stays off. Pass all ofnoiseSuppression,echoCancellationandautoGainControl, or pass nonoiseConfigto keep the profile's values.maxLayertakesEncodingLayer.MAX_LAYER_2orEncodingLayer.MAX_LAYER_3instead of anint.CustomStreamTrack.stop()replacesdispose(). The newonEndedcallback tells you when the track ends, with aVideoSDKErrorwhen the device was lost or access was revoked.
4. Screen share takes a track
enableScreenShare() no longer takes enableAudio or source. Create a screen-share track with VideoSDK.createScreenShareVideoTrack() and pass it as customTrack.
Before
await room.enableScreenShare(enableAudio: true);
After
final CustomStreamTrack? shareTrack = await VideoSDK.createScreenShareVideoTrack(
encoderConfig: CustomScreenShareTrackConfig.h720p_15fps,
withAudio: true, // Android only
);
// On Android, null means the user declined the capture prompt.
if (shareTrack != null) {
await room.enableScreenShare(customTrack: shareTrack);
}
Key points:
- On Android,
createScreenShareVideoTrack()asks for capture consent and returnsnullif the user declines. On iOS, oneenableScreenShare()call opens the system broadcast picker. withAudioworks on Android only. iOS screen share sends no audio.enableScreenShare()without a track still works. It shares atCustomScreenShareTrackConfig.h720p_15fpsand asks for consent itself. To use another profile, create a track.sourceandRoom.getScreenShareSources()chose a desktop screen or window. Desktop is not supported, andgetScreenShareSources()is removed.- iOS screen share still needs the Broadcast Upload Extension described in Flutter iOS Screen Share. Use the extension files that page lists: an older extension built on
ScreenShareRing.swiftsends no video with v5.0.0.
5. PubSub subscribe() takes a listener object
subscribe() now takes the topic, one PubSubMessageListener that carries all four callbacks, and an optional PubSubSubscribeOptions. It returns nothing: stored messages arrive on onOldMessagesReceived. Failures throw VideoSDKError instead of the PubSubException types.
Before
Future<void> openChat() async {
try {
await room.pubSub.subscribe(
"CHAT",
onMessage,
options: const PubSubSubscribeOptions(oldMessageLimit: 50),
onOldMessagesReceived: (batch, isLast) {
setState(() => messages.insertAll(0, batch));
},
);
} on PubSubException catch (e) {
log("Subscribe failed: ${e.name}");
}
}
Future<void> closeChat() => room.pubSub.unsubscribe("CHAT", onMessage);
After
late final chatListener = PubSubMessageListener(
onMessageReceived: (PubSubMessage message) {
setState(() => messages.add(message));
},
onOldMessagesReceived: (List<PubSubMessage> batch, PubSubHistoryInfo info) {
setState(() => messages.insertAll(0, batch));
},
);
Future<void> openChat() async {
try {
await room.pubSub.subscribe(
"CHAT",
chatListener,
const PubSubSubscribeOptions(oldMessageLimit: 50),
);
} on VideoSDKError catch (e) {
log("Subscribe failed: ${e.code} ${e.name}");
}
}
// Pass the same listener object to unsubscribe.
Future<void> closeChat() => room.pubSub.unsubscribe("CHAT", chatListener);
| v4.0.1 | v5.0.0 |
|---|---|
subscribe(topic, onMessage, {options, onOldMessagesReceived, onBatchReceived, onMessageDrop}) returning PubSubMessages | subscribe(topic, PubSubMessageListener(...), [PubSubSubscribeOptions]) returning Future<void> |
onOldMessagesReceived: (messages, bool isLast) | onOldMessagesReceived: (messages, PubSubHistoryInfo info), with info.isLast |
onMessageDrop: (int droppedCount) | onMessageDrop: (PubSubMessageDropInfo info), with info.droppedCount |
PubSubRealtimeOverflow | RealtimeOverflow.QUEUE or RealtimeOverflow.DROP |
unsubscribe(topic, onMessage) | unsubscribe(topic, listener), with the same listener object |
PubSubMessages | Removed |
PubSubMessage.timestamp (DateTime) | A String, as the server sends it |
PubSubException and its five subtypes | VideoSDKError |
PubSubMessage.sendOnly on received messages | Removed. PubSubPublishOptions(sendOnly: ...) for sending is unchanged. |
Key points:
- Each listener is its own subscription, with its own options and its own history. Subscribing the same listener to a topic twice throws
VideoSDKError3073. - A PubSub call made before the room is joined throws a
VideoSDKErrorwith code3035, so subscribe from yourEvents.roomJoinedhandler. While the room reconnects, a call can throwVideoSDKError3065. RealtimeStore.observe(key, listener)no longer returns an observer id. Stop withstopObserving(key, listener), passing the same function.RealtimeStorecalls made before join also throw aVideoSDKErrorwith code3035. In v4.0.1 they reported3022onEvents.errorand returnednullor an empty string.
6. Renamed APIs
Many APIs have new names. The old names are removed, not deprecated, so the analyzer points you to each one.
Before
room.on(Events.participantLeft, (String participantId, LeaveReason reason) {
if (reason == LeaveReason.removePeer) showRemovedBanner(participantId);
});
await room.changeCam(cameras.first);
await participant.setQuality("high");
After
room.on(Events.participantLeft, (Participant participant, LeaveReason reason) {
if (reason == LeaveReason.REMOVE_PEER) showRemovedBanner(participant.id);
});
await room.changeCam(deviceId: cameras.first.deviceId);
await participant.setQuality(VideoQuality.HIGH);
VideoSDK
| v4.0.1 | v5.0.0 |
|---|---|
VideoSDK.getVideoDevices(), getAudioDevices() | VideoSDK.getCameras(), getMics(). getDevices() returns both. |
VideoDeviceInfo, AudioDeviceInfo | CameraDeviceInfo, MicrophoneDeviceInfo |
room.selectedCam, selectedMic, selectedSpeaker (and the *Id getters) | VideoSDK.getSelectedVideoDevice(), VideoSDK.getSelectedAudioDevice(). They report the devices in use from join on, and are null before join. |
VideoSDK.logLevel | VideoSDK.getLogLevel() |
VideoSDK.keyProvider | VideoSDK.getKeyProvider() |
BaseKeyProvider.setKey(participantId, key) | VideoSDK.setParticipantKey(participantId, key). A null key removes it, and so does the new BaseKeyProvider.removeKey(participantId) on the installed provider. |
BaseKeyProvider(discardFrameWhenCryptorNotReady: ...) | BaseKeyProvider(). Remove the argument; nothing replaces it. |
VideoSDK.applyVideoProcessor(videoProcessorName:), VideoSDK.removeVideoProcessor() | room.setVideoProcessor(name), room.removeVideoProcessor(). Both work before join(). |
Room
| v4.0.1 | v5.0.0 |
|---|---|
micEnabled, camEnabled | isMicEnabled, isCamEnabled |
activeSpeakerId, activePresenterId | getActiveSpeakerId(), getActivePresenterId() |
livestreamState | liveStreamState |
hlsDownstreamUrl, hlsUrls | hlsUrl, an HLSUrl with playbackHlsUrl and livestreamUrl |
changeCam(device) | changeCam(deviceId: device.deviceId), or changeCam(customCameraTrack: track) |
changeMic(device) | changeMic(deviceId: device.deviceId), or changeMic(customMicrophoneTrack: track) |
enableCam(customVideoTrack), unmuteMic(customAudioTrack) | The optional track is a CustomStreamTrack. |
switchAudioDevice(device) | setAudioDevice(device) |
startHls(), stopHls() | startHLS(), stopHLS() |
config maps on startRecording(), startHls(), startLivestream() | RecordingConfig, HLSConfig, LivestreamConfig, with ConfigLayout |
startLivestream(outputs) with {'url', 'streamKey'} maps | startLivestream([LivestreamOutput(url: ..., streamKey: ...)]) |
postTranscriptionConfig: on startRecording(), startHls() | transcription: |
startLivestream(outputs, postTranscriptionConfig:) | startLivestream(outputs, config:). The transcription argument is removed. |
startTranscription(transcriptionConfig:) | startTranscription(config:) |
requestMediaRelay(meetingId), stopMediaRelay(meetingId) | The parameter is destinationRoomId. |
requestMediaRelay(..., kinds: ['audio', 'video']) | kinds: [StreamKind.AUDIO, StreamKind.VIDEO]. The values are AUDIO, VIDEO and SHARE; share_audio has no replacement. |
pauseAllStreams(kind: 'video'), resumeAllStreams(kind: ...) | kind: StreamKind.VIDEO. The values are AUDIO, VIDEO and SHARE. |
Participant
| v4.0.1 | v5.0.0 |
|---|---|
unmuteMic(), muteMic() on a remote participant | enableMic(), disableMic() |
quality (String?) | videoQuality (VideoQuality?) |
setQuality("high") | setQuality(VideoQuality.HIGH). The values are HIGH, MEDIUM and LOW. |
isAgent | isAgent() |
AgentParticipant.agentId, agentState | getAgentId(), getAgentState() |
Types and enums
| v4.0.1 | v5.0.0 |
|---|---|
Stream | RTCStream. kind is a MediaType (VIDEO, AUDIO, SHARE or SHARE_AUDIO), and participantId says whose stream it is. |
CustomTrack, CustomVideoTrackConfig, CustomAudioTrackConfig | CustomStreamTrack, CustomCameraTrackConfig, CustomMicrophoneTrackConfig (section 3) |
TranscriptionConfig(webhookUrl:, summaryConfig:) | TranscriptionConfig(webhookURL:, summary:), plus the new modelConfig |
PostTranscriptionConfig(summaryConfig:) | PostTranscriptionConfig(summary:). SummaryConfig.prompt is optional. |
E2EEState.EncryptionSucessed, DecryptionSucessed | E2EEState.EncryptionSuccess, DecryptionSuccess |
TranslationText.type (String?) | TranslationType?: PARTIAL, FULL or null |
'recording': {'enabled': true} in a livestream config map | LivestreamConfig(recording: LivestreamRecordingConfig(enabled: true)) |
FacingMode.user, FacingMode.environment | FacingMode.FRONT, FacingMode.BACK |
Lower-case values of RoomState, LogLevel, LeaveReason, AgentState and CaptureFlash, such as RoomState.connected | Upper case, such as RoomState.CONNECTED, LogLevel.INFO, LeaveReason.MANUAL_LEAVE_CALLED, AgentState.SPEAKING and CaptureFlash.ON. LeaveReason.DUPLICATE_PARTICIPANT is new. |
Events
| v4.0.1 | v5.0.0 |
|---|---|
liveStreamStarted, liveStreamStopped, liveStreamStateChanged | livestreamStarted, livestreamStopped, onLivestreamStateChanged |
e2eeStateChanged | onE2EEStateChanged, on the room and on the participant (section 10) |
hlsStarted, hlsStopped | Removed. Use hlsStateChanged. |
hlsStateChanged, (Map data) | (String state, HLSUrl? hlsUrl) |
participantLeft, (String participantId, LeaveReason reason) | (Participant participant, LeaveReason reason), delivered once per departure |
participantModeChanged map key mode (String) | Mode |
pinStateChanged map key state (a {cam, share} map) | ParticipantPinState, with cam and share |
micRequested, cameraRequested map | Adds participantId, who sent the request |
streamStateChanged on a stream, (String state, int timestamp) | (StreamState state, int timestamp): StreamState.ACTIVE, ENDED, STUCK, FREEZE_DETECTED or FREEZE_RESOLVED |
transcriptionStateChanged, (Map data) | (TranscriptionState state) |
translationLanguageChanged, (String language) | (String? participantId, String language) |
error, (Map error) | (VideoSDKError error) (section 11) |
entryResponded map key id | participantId |
roomJoined after switchTo(), map key switchingRoomId | switchRoomId |
mediaRelayRequestReceived, (meetingId, peerId, displayName, accept, reject) | (participantId, roomId, displayName, accept, reject). The first two arguments swapped places. |
mediaRelayStarted, mediaRelayStopped, mediaRelayError with a meetingId | The same arguments. The first one is now named roomId. |
mediaRelayRequestResponse, decision (String) | RelayDecision?: ACCEPTED or REJECTED |
pausedAllStreams, resumedAllStreams, (String kind) | (StreamKind? kind), null when every kind was paused or resumed |
VideoSDK.on(Events.deviceChanged, ...) | room.setAudioDeviceChangeListener((selectedDevice, devices) {...}) |
Other signature changes
| v4.0.1 | v5.0.0 |
|---|---|
A switch over Events or CustomScreenShareTrackConfig with no default case | Add cases for the new events (dataReceived, videoEffectsStats, externalCallRinging, externalCallAnswered, externalCallRejected and externalCallHangup) and for CustomScreenShareTrackConfig.h480p_15fps, h480p_30fps and h720p_30fps, or add a default. |
A class that extends or implements Participant, such as a test mock | Update the overrides to the new names above. captureImage() gained CaptureFlash flash = CaptureFlash.OFF, and the actions listed in section 8 return Future<void>. |
A class that implements Room, such as a test fake | Update the overrides to the new names above. end(), stopTranslation() and the undocumented setViewPort() return Future<void>, and enableScreenShare() takes an optional customTrack. |
Events.userMessage, UserMessage and CustomTrack.toMap() (undocumented) | Removed, with no replacement. |
7. Native video processors
Android: a custom processor now implements the SDK's VideoFrameProcessor instead of VideoProcessor from videosdk_webrtc. The method is onFrameCaptured(), and VideoFrame comes from the SDK.
Before
import live.videosdk.webrtc.VideoProcessor
import org.webrtc.VideoFrame
class YourOwnBackgroundProcessor : VideoProcessor {
override fun onFrameReceived(videoFrame: VideoFrame): VideoFrame {
return applyEffect(videoFrame)
}
}
After
import live.videosdk.rtc.android.listeners.VideoFrameProcessor
import videosdk.org.webrtc.VideoFrame
class YourOwnBackgroundProcessor : VideoFrameProcessor {
override fun onFrameCaptured(videoFrame: VideoFrame): VideoFrame {
return applyEffect(videoFrame)
}
}
Key points:
- Register the processor in
MainActivitywithVideoSDK.getInstance().registerVideoProcessor()as before. Apply it from Dart withroom.setVideoProcessor(name)instead ofVideoSDK.applyVideoProcessor(videoProcessorName:), and remove it withroom.removeVideoProcessor(). Both work beforejoin(). A name that was never registered makes the call throw. VideoSDK.getInstance().setVideoProcessor(processor)in Kotlin applies a processor to the current room directly. It throwsIllegalStateExceptionuntil Dart has calledcreateRoom().VideoFramenow ships with the SDK asvideosdk.org.webrtc.VideoFrame, so the processor no longer needs theio.github.webrtc-sdk:androiddependency. Remove it from your app'sbuild.gradle.- The SDK passes I420 frames and releases both the frame it passes in and the frame you return. Do not call
release()on either. - iOS: custom processors are not supported in v5.0.0. The
VideoSDK.getInstance.registerVideoProcessor()registry is gone, so remove that code from yourAppDelegate, together with the processor class built onvideosdk_webrtc. - iOS:
room.setVideoProcessor(name)reads the name as the file path or URL of a background image, and replaces the camera background with that image. - For background blur on both platforms, use
VideoSDK.setVideoEffects().
8. Actions return a Future that throws VideoSDKError
In v4.0.1, an action that could not run, for example before join or while the room reconnected, reported the failure on Events.error and returned normally. In v5.0.0, Room, Participant and RTCStream actions return a Future that throws VideoSDKError when the SDK refuses them. On Android, most actions called while the room reconnects still report 3065 on Events.error and return without throwing, so keep your Events.error handler.
This compiles without a warning. A call you do not await turns a refusal into an unhandled async error, which crash reporters record as a crash.
Before
void onEndCallPressed(Room room) {
room.end();
}
void onPinPressed(Participant participant) {
participant.pin();
}
After
Future<void> onEndCallPressed(Room room) async {
try {
await room.end();
} on VideoSDKError catch (error) {
log("Could not end the meeting: ${error.code} ${error.name}");
}
}
Future<void> onPinPressed(Participant participant) async {
try {
await participant.pin();
} on VideoSDKError catch (error) {
log("Could not pin: ${error.code} ${error.name}");
}
}
These members returned void or dynamic in v4.0.1 and now return Future<void>:
| Class | Members |
|---|---|
Room | end(), stopTranslation() |
Participant | disableMic() and enableMic() (were muteMic() and unmuteMic()), enableCam(), disableCam(), pin(), unpin(), remove(), setQuality(), setViewPort(), consumeMicStreams(), stopConsumingMicStreams(), consumeWebcamStreams(), stopConsumingWebcamStreams() |
RTCStream (was Stream) | pause() |
Key points:
- The other
Roomactions, such asmuteMic(),enableCam()andstartRecording(), already returned aFuturein v4.0.1. They can now throw too. - Common codes:
3054when there is nothing to do, for examplemuteMic()on a muted microphone,3055when the same action is already running,3065while the room reconnects, and3035for an action after the room has left. - Turn on the
unawaited_futuresanddiscarded_futureslints inanalysis_options.yamlto find calls you do not await.unawaited_futureschecksasyncfunctions, anddiscarded_futureschecks other functions, such as the Before handlers above. Neither flags an arrow callback such asonPressed: () => room.end(), so check those by hand.discarded_futuresalso flags otherFuturecalls, so you may want it on only while you migrate. Events.erroralso reports some failures that a call of yours caused, such as a failedroom.send()(3070), so the same failure can reach both yourcatchand yourEvents.errorhandler. Handle each one in one place.AgentParticipantchanged the same way:pin(),unpin()andremove()returnFuture<void>.
9. Joining and leaving a room
join() now completes once the room is joined and throws when it is not. A room is single-use: after Events.roomLeft, or after a failed join(), it removes your handlers and releases itself.
Before
Future<void> startMeeting(Room room) async {
room.on(Events.error, (error) {
log("Meeting error: ${error['code']} ${error['message']}");
});
await room.join();
}
void endCall(Room room) {
room.leave();
}
After
Future<void> startMeeting(Room room) async {
room.on(Events.error, (VideoSDKError error) {
log("Meeting error: ${error.code} ${error.message}");
});
// On Android, join() no longer asks for camera or microphone permission.
await VideoSDK.requestPermissions();
try {
await room.join();
} on VideoSDKError catch (error) {
// For example 4002 (invalid token) or 3073 (empty room id).
log("Join failed: ${error.code} ${error.name}");
}
}
Future<void> endCall(Room room) async {
try {
await room.leave();
} on VideoSDKError catch (error) {
log("Leave failed: ${error.code} ${error.name}");
}
}
Key points:
- In v4.0.1,
join()reported failures onEvents.error. In v5.0.0 it throws them. - On Android,
join()andchangeMode(Mode.SEND_AND_RECV)no longer ask for camera or microphone permission, and without it the microphone and camera stay off. CallVideoSDK.requestPermissions()first (section 3). - To join again, create a new
RoomwithVideoSDK.createRoom(). Callingjoin()on a room that has left throws aStateError. - Calling
join()again on the same room throwsVideoSDKError3055while the first join is still running, and3054once the room is joined. - Only one
Roomexists at a time. A newcreateRoom()releases the previous room, and leaves its room if it had joined. - If your screen goes away while
join()is still running, wait for it and then leave:room.join().then((_) => room.leave()).ignore();. - There is no
dispose()to call. Remove any cleanup that disposed the room. room.localParticipantexists fromcreateRoom(), as in v4.0.1. Before join it carries the id and name you passed, empty when you passed none; join fills in the ones the room assigned.
10. Stream events also arrive on the Room
Handlers added with participant.on() for streamEnabled, streamDisabled, streamPaused and streamResumed still work, and so do e2eeStateChanged handlers once you rename the event to onE2EEStateChanged. The stream type is now RTCStream. The Room now also delivers each of these events, for every participant.
Before
@override
void initState() {
super.initState();
widget.participant.on(Events.streamEnabled, (Stream stream) {
if (stream.kind == 'video') {
setState(() => videoStream = stream);
}
});
}
After
@override
void initState() {
super.initState();
widget.participant.on(Events.streamEnabled, (RTCStream stream) {
if (stream.kind == MediaType.VIDEO) {
setState(() => videoStream = stream);
}
});
}
| Event | On the participant | On the room (new) |
|---|---|---|
streamEnabled, streamDisabled | (RTCStream stream) | (RTCStream stream). stream.participantId says whose stream it is. |
streamPaused, streamResumed | (String? kind, String? reason) | (String? kind, String? reason). It does not say whose stream it is. |
onE2EEStateChanged | (E2EEState state, RTCStream? stream) | (Map<String, dynamic> data), with participantId, streamId and state, an E2EEState |
Key points:
- Take room handlers off with
room.off()when the widget that added them goes away. The room outlives a participant tile. participant.streamsis still kept up to date, so a widget can pick up the streams that already exist when it is built.
11. Error codes: code is an int, and 3xxx codes were renumbered
Events.error now delivers a VideoSDKError instead of a Map. Its code is an int, where v4.0.1 sent a String. Errors thrown by actions are the same VideoSDKError, with code, name and message. A handler declared as (error) with no type still compiles, and error['code'] then fails at runtime, so give the parameter its type. Most 3xxx numbers now mean a different error.
Before
room.on(Events.error, (error) {
if (error['code'] == "4009") {
showRoomFullDialog();
}
});
room.join();
After
room.on(Events.error, (VideoSDKError error) {
final code = error.code; // an int, for example 4009
log("Meeting error: $code ${error.name}");
});
try {
await room.join();
} on VideoSDKError catch (error) {
// A full room fails join() itself. Show the dialog only here, because the
// same failure can also reach Events.error.
if (error.code == 4009) showRoomFullDialog();
}
Same error, new code. Update any check on these numbers:
| Error | v4.0.1 | v5.0.0 |
|---|---|---|
ERROR_CAMERA_IN_USE | 3003 | 3023 |
ERROR_MICROPHONE_IN_USE | 3004 | 3024 |
ERROR_CAMERA_ACCESS_UNAVAILABLE | 3005 | 3033 |
ERROR_MICROPHONE_ACCESS_UNAVAILABLE | 3006 | 3034 |
ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED | 3007 | 3017 |
ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED | 3008 | 3018 |
ERROR_CAMERA_PERMISSION_DENIED_BY_OS | 3009 | 3019 |
ERROR_MICROPHONE_PERMISSION_DENIED_BY_OS | 3010 | 3020 |
ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED, now ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED | 3022 | 3035 |
ERROR_CHANGE_WEBCAM | 3024 | Not reported. A failed changeCam() throws another code, for example 3051 for an unknown camera. |
ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED | 3025 | 3045 |
ERROR_MULTISTREAM_NOT_SUPPORTED | 3026 | 3046 |
ERROR_MEETING_RECONNECTING, now ERROR_ROOM_RECONNECTING | 3027 | 3065 |
Same code, new meaning. A v4.0.1 check on these numbers now matches a different error:
| Code | v4.0.1 | v5.0.0 |
|---|---|---|
3010 | ERROR_MICROPHONE_PERMISSION_DENIED_BY_OS | ERROR_BLUETOOTH_AUDIO_ROUTE_FAILED (Android only) |
3011 | ERROR_STARTING_VIDEO | ERROR_GET_VIDEO_MEDIA |
3012 | ERROR_STARTING_AUDIO | ERROR_GET_AUDIO_MEDIA |
3013 | ERROR_STARTING_SCREENSHARE | ERROR_GET_DISPLAY_MEDIA |
3014 | ERROR_SCREENSHARE_PERMISSION_DENIED | ERROR_GET_VIDEO_MEDIA_PERMISSION_DENIED |
3015 | ERROR_SCREENSHARE_NOT_SUPPORTED | ERROR_GET_AUDIO_MEDIA_PERMISSION_DENIED |
3016 | ERROR_STARTING_FOREGROUND_SERVICE | ERROR_GET_DISPLAY_MEDIA_PERMISSION_DENIED |
3017 | ERROR_CREATING_VIDEO_TRACK | ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED |
3018 | ERROR_CREATING_AUDIO_TRACK | ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED |
3019 | ERROR_CAMERA_DEVICE_INCOMPATIBLE | ERROR_CAMERA_PERMISSION_DENIED_BY_OS |
3020 | ERROR_MICROPHONE_DEVICE_INCOMPATIBLE | ERROR_MICROPHONE_PERMISSION_DENIED_BY_OS |
3021 | ERROR_CHANGING_MODE | ERROR_CAMERA_NOT_FOUND |
3022 | ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED | ERROR_MICROPHONE_NOT_FOUND |
3023 | ERROR_JOINING_MEETING | ERROR_CAMERA_IN_USE |
3024 | ERROR_CHANGE_WEBCAM | ERROR_MICROPHONE_IN_USE |
3025 | ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED | ERROR_CAMERA_PERMISSION_OR_AUTOPLAY_ISSUE |
3026 | ERROR_MULTISTREAM_NOT_SUPPORTED | ERROR_VIDEO_SOURCE_INITIATION_FAILED |
3027 | ERROR_MEETING_RECONNECTING | ERROR_WEBCAM_TRACK_ENDED |
3001 to 3009 are not reported in v5.0.0.
No longer reported. These v4.0.1 errors have no v5.0.0 code:
| v4.0.1 code | Error |
|---|---|
3001 | ERROR_CUSTOM_VIDEO_TRACK_DISPOSED |
3002 | ERROR_CUSTOM_AUDIO_TRACK_DISPOSED |
3011 | ERROR_STARTING_VIDEO |
3012 | ERROR_STARTING_AUDIO |
3013 | ERROR_STARTING_SCREENSHARE |
3014 | ERROR_SCREENSHARE_PERMISSION_DENIED |
3015 | ERROR_SCREENSHARE_NOT_SUPPORTED |
3016 | ERROR_STARTING_FOREGROUND_SERVICE |
3017 | ERROR_CREATING_VIDEO_TRACK |
3018 | ERROR_CREATING_AUDIO_TRACK |
3019 | ERROR_CAMERA_DEVICE_INCOMPATIBLE |
3020 | ERROR_MICROPHONE_DEVICE_INCOMPATIBLE |
3021 | ERROR_CHANGING_MODE |
3023 | ERROR_JOINING_MEETING |
Codes you will see most often in v5.0.0:
| Code | Name | Meaning |
|---|---|---|
3035 | ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED | The call needs a joined room. |
3051 | ERROR_NO_WEBCAM_DEVICE_FOUND | No camera was found. |
3054 | ERROR_ALREADY_IN_REQUESTED_STATE | Nothing to do: it is already in that state. |
3055 | ERROR_OPERATION_IN_PROGRESS | The same operation is already running. |
3056 | ERROR_OPERATION_ABORTED | The operation was aborted. |
3059 | ERROR_NO_MICROPHONE_DEVICE_FOUND | No microphone was found. |
3065 | ERROR_ROOM_RECONNECTING | The room is reconnecting. Try again once it is back. |
3071 | ERROR_INVALID_CUSTOM_TRACK_CONFIG | The track configuration is not one the SDK knows. |
3073 | ERROR_INVALID_PARAMETER | An argument is invalid, for example an empty room id. |
4000 | UNKNOWN_ERROR | The error could not be classified. The original message is kept. |
4002 | INVALID_TOKEN | The token was rejected. |
Key points:
- Other 4xxx and 5xxx numbers are unchanged. Two names now use the catalogue's spelling:
4009isMAX_PARTCIPANT_REACHED, where v4.0.1 sentMAX_PARTICIPANT_REACHED, and4023isUNAUTHORIZED_PARTCIPANT_ID, where v4.0.1 sentUNAUTHORIZED_PARTICIPANT_ID. Compare these two by code, not by name. - The
VideoSDKErrorsmap is removed. Compare with theVideoSDKError.*constants instead, such asVideoSDKError.INVALID_TOKEN: errors are equal when theircodeis equal. - Error names say
ROOMwhere v4.0.1 saidMEETING:ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED,ERROR_ROOM_RECONNECTING,ERROR_ROOM_MEDIA_CONNECTION_FAILED,ERROR_UNABLE_TO_JOIN_ROOM,ERROR_END_ROOM_FAILED,INVALID_ROOM_ID,UNAUTHORIZED_ROOM_ID,ROOM_ID_DISABLEDandSWITCH_ROOM_FAILED. The codes are unchanged, so a check onerror.namemust change and a check oncodedoes not.
12. Other behaviour changes
| What | v4.0.1 | v5.0.0 |
|---|---|---|
participant.mode | Updated when the participant changed mode | Updated just before Events.participantModeChanged fires, for the local participant too. changeMode() does not change it when it returns: wait for the event. |
Events.qualityLimitation values | Lower case, such as cpu and detected | Upper case: the type is CONGESTION, BANDWIDTH or CPU, and the state is DETECTED or RESOLVED. The handler is (String type, String state, int? timestamp). Update any string comparison. |
An error thrown in a room.on() handler | For some events, such as Events.participantLeft, the SDK caught it and only logged it | Reaches your app as an uncaught error, which crash reporters record. Check that each handler's parameter types match the event: Events.participantLeft passes (Participant participant, LeaveReason reason), so a handler that declares a String first parameter throws. The other handlers for the event still run. |
getVideoStats(), getAudioStats(), getShareStats() | Returned the latest sample from stats the SDK collected in the background, or an empty list before the first one | Return the sample fetched by your previous call and start fetching the next one. The first call after a participant joins returns null, so call them from a timer. |
room.changeMic() on Android and iOS | Threw UnsupportedError once joined | Works. A device picked before join is used when you join. It also moves the speaker route, because the phone has a single audio route, and VideoSDK.getSelectedAudioDevice() returns it. setAudioDevice() changes the route without touching the microphone. |
Ids and labels from VideoSDK.getAudioDevices(), now getMics() | Android ids speaker, earpiece, headset and bluetooth, with labels such as Speakerphone. iOS ids were the audio port's UID | Android ids SPEAKER_PHONE, EARPIECE, WIRED_HEADSET and BLUETOOTH, and the label is the same string. On iOS the id is the same string as the label. Update code that picks a device by id or label, and show your own names in device pickers. |
Ids and labels from VideoSDK.getVideoDevices(), now getCameras() | Android labels such as Camera 1, Facing front, Orientation 270. iOS ids were the camera's unique id | Android labels such as Camera 1 (Front Camera). On iOS the id is the camera's name. |
An unknown cameraId or microphoneId in createCameraVideoTrack() or createMicrophoneAudioTrack() | Used the default device | Throws 3071. A device id saved by a v4.0.1 build may not match, so pick from a fresh getCameras() or getMics() list. |
13. Removed or no longer delivered
| v4.0.1 | v5.0.0 |
|---|---|
| Web, macOS and Windows support | Not supported. Stay on v4.0.1 for those platforms. |
Events.videoQualityChanged | Not delivered. No replacement. |
VideoSDK.on(Events.deviceChanged) | Removed. Use room.setAudioDeviceChangeListener((selectedDevice, devices) {...}), or call VideoSDK.getDevices() when you need the current list. |
VideoSDK.on(Events.error) for track errors | Not delivered. Catch VideoSDKError from the track factories (section 3), and listen with room.on(Events.error) for room errors. |
createRoom(debugMode:), createRoom(defaultCameraIndex:) | Removed: debugMode has no replacement (VideoSDK.setLogLevel() sets how much the SDK logs); use createRoom(defaultCamera:) for defaultCameraIndex (section 2) |
enableScreenShare(enableAudio:, source:), Room.getScreenShareSources() | createScreenShareVideoTrack(withAudio:). Desktop source picking has no replacement (section 4). |
PubSubException, PubSubMeetingNotJoined, PubSubMeetingReconnecting, PubSubPublishFailed, PubSubSubscribeFailed, PubSubUnsubscribeFailed | VideoSDKError |
PubSubMessage.sendOnly on received messages | No replacement |
Mode.CONFERENCE, Mode.VIEWER | Removed. Use Mode.SEND_AND_RECV or Mode.RECV_ONLY. |
createRoom(notification:), NotificationInfo | Removed. On Android, the SDK shows its own screen-share notification. |
startLivestream(postTranscriptionConfig:) | Removed. Transcribe a recording or an HLS stream with transcription: instead. |
Events.hlsStarted, Events.hlsStopped | Removed. Use Events.hlsStateChanged. |
room.enableRelayMedia(), room.disableRelayMedia() | Removed, with no replacement. |
Character, room.createCharacter() and the character* events (undocumented) | Removed, with no replacement. |
| Custom video processors on iOS | No replacement (section 7) |
videosdk_media_effects plugin | Noise suppression: createMicrophoneAudioTrack(noiseConfig:). Background image: no replacement on Android; on iOS, room.setVideoProcessor() with the image path or URL. |
MediaDeviceType, VideoSDK.mediaDevices, loadMediaDevices(), isFirefox(), setAppleAudioConfiguration(), getDeviceInfo(), requestIOSScreenSharePermission(), e2eeType (undocumented) | Removed. Use VideoSDK.getCameras(), getMics() and getDevices() for devices. |
Room.localParticipantId, getParticipentId(), switchingRoom(), isSendAndReceiveMode(), isViewerMode(), deviceInfo, baseUrl, videoSDKTelemetery (undocumented) | Removed. Use room.localParticipant.id and participant.mode. |
Room.enableShare(), disableShare(), pause(consumer), resume(consumer), setConsumerQuality(), setScreenShareQuality() and setViewPort() on the Room (undocumented) | Removed. Use enableScreenShare() and disableScreenShare(), stream.pause() and stream.resume(), and the Participant methods setQuality(), setScreenShareQuality() and setViewPort(). |
RTCStream.track, copyWith(), RealtimeStore.dispose(), RTCVideoRenderer.audioOutput() (undocumented) | Removed, with no replacement. |
BaseKeyProvider.key, sharedKey, keyMap, keyProvider | Hidden from the API reference. Use setSharedKey() on BaseKeyProvider and VideoSDK.setParticipantKey() for keys. |
14. New in v5.0.0
| Feature | API |
|---|---|
| Pre-call test | VideoSDK.runPreCallTest(PreCallTestOptions(token: token)) returns a Future<PreCallTestResult>. Stop it early with VideoSDK.stopPreCallTest(). Run it, and stop it, before createRoom(): creating a room does not stop a running test. Failures throw PreCallTestException. See Precall. |
| Video effects | VideoSDK.setVideoEffects(VideoEffectsConfig(...)) or room.setVideoEffects() for framing, low light, style, color filter and background blur, plus getAppliedVideoEffectsConfig(), publishFramingSubject() and Events.videoEffectsStats. On Android, framing on the default VideoEffectsPipeline.LEGACY needs com.google.mlkit:face-detection:16.1.7 and blur needs com.google.mlkit:segmentation-selfie:16.0.0-beta6 in your app's build.gradle. On iOS, background blur needs iOS 15 or later. |
| Data channel | room.send() sends a String or a List<int> of up to 15 KiB. Receive it with Events.dataReceived, which delivers a DataMessage with senderId, text, data, isBinary, reliable and timestamp. |
| Typed errors | VideoSDKError with code, name and message |
| Log forwarding | VideoSDK.enableLogForwarding((level, message) {...}) and disableLogForwarding() |
| Version info | VideoSDK.sdkVersion and VideoSDK.getBuildId() for bug reports |
| Capture flash | captureImage(flash: CaptureFlash.ON) and VideoSDK.setScreenFlash(true). Local participant only. |
| Outgoing video quality | createRoom(dynaCast: true) lowers your webcam quality while nobody watches its larger layers, and has no effect with multiStream: false. room.setWebcamQuality(VideoQuality.MEDIUM) caps it. |
| Received screen-share quality | participant.setScreenShareQuality(VideoQuality) |
| More statistics | participant.getTransportStats() and participant.getShareAudioStats() |
| Applied settings | room.getAppliedVideoConfig() |
| Room attributes | room.getAttributes() returns the room's server-side attributes. |
| Room state | room.roomState, room.isScreenSharing, room.transcriptionState, room.translationState |
| Stream details | StreamState for streamStateChanged, MediaType.SHARE_AUDIO for screen-share audio, CameraDeviceInfo.facingMode |
| Error constants | VideoSDKError.*, such as VideoSDKError.INVALID_TOKEN; errors are equal by code |
| Device switching | room.changeCam() with no arguments moves to the next camera, and room.changeMic() now works on Android and iOS. room.setAudioDeviceChangeListener() reports route changes. |
| Entry requests | room.respondEntry(participantId, 'allowed') answers a join request from anywhere, not only from the Events.entryRequested handler. |
| Listener cleanup | room.removeAllEventListeners() and participant.removeAllEventListeners() |
| Phone calls | Events.externalCallRinging, externalCallAnswered, externalCallRejected and externalCallHangup, each with a CallType: INCOMING or OUTGOING |
| Track end | CustomStreamTrack.onEnded is called once when a track ends, with a VideoSDKError when the device was lost or access was revoked. |
| E2EE off | VideoSDK.removeKeyProvider() turns end-to-end encryption off. BaseKeyProvider.removeKey(participantId) removes one participant's key. |
| Mirroring | RTCVideoRenderer.setMirror(bool) flips every view of a renderer, overriding RTCVideoView(mirror:). |
| Screen-share profiles | CustomScreenShareTrackConfig.h480p_15fps, h480p_30fps and h720p_30fps |
| High-pass filter | highPassFilter in NoiseConfig |
| Realtime translation | startTranslation(), stopTranslation() and changeTranslationLanguage() are now documented. See Realtime Translation. |
API Reference
Got a Question? Ask us on discord

