Skip to main content
Version: 5.x.x

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.

caution

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 minSdkVersion to 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, add ndk { abiFilters += listOf("arm64-v8a", "armeabi-v7a") } to defaultConfig in android/app/build.gradle.kts (ndk { abiFilters 'arm64-v8a', 'armeabi-v7a' } in a Groovy build.gradle), and add disable-abi-filtering=true to android/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: with enable-swift-package-manager: true under flutter: in your app's pubspec.yaml, or run flutter config --enable-swift-package-manager on 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.1v5.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 muteroom.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.peerIdstream.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 widgetAlways null. Render with RTCVideoView.

Key points:

  • Remove videosdk_media_effects too, because it depends on videosdk_webrtc. For noise suppression, pass noiseSuppression in createMicrophoneAudioTrack(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 in VideoSDK.setVideoEffects().
  • MediaStream and MediaStreamTrack can no longer be imported. Where you annotated cameraTrack.mediaStream with 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, protobuf or fixnum, nor the packages they brought with them, such as shared_preferences, path_provider, crypto and logging. If your code imports a package that is not in your own pubspec.yaml, add it there: after flutter pub get, dart analyze reports each one as Target of URI doesn't exist.
  • flutter_foreground_task also added WAKE_LOCK, RECEIVE_BOOT_COMPLETED and POST_NOTIFICATIONS to your app's merged Android manifest. v5.0.0 does not, so declare any of them that your own code uses in android/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,
);
Parameterv4.0.1v5.0.0
displayNameString, requiredRenamed participantName, and optional. Leave it out and the SDK picks a random name.
customCameraVideoTrack, customMicrophoneAudioTrackCustomTrack?Renamed customCameraTrack and customMicrophoneTrack, of type CustomStreamTrack?
maxResolutionString, default 'sd'MaxResolution enum. Left out, the SDK uses MaxResolution.HD.
modeMode, including Mode.CONFERENCE and Mode.VIEWERMode.SEND_AND_RECV, Mode.RECV_ONLY or Mode.SIGNALLING_ONLY. CONFERENCE and VIEWER are removed.
participantId, multiStream, mode, metaData, signalingBaseUrl, preferredProtocolDart default valuesOptional, with no Dart default. Left out, the SDK applies its own default, the same as before.
defaultCameraIndexint, default 0Removed. Use defaultCamera.
defaultCamera—FacingMode, default FacingMode.FRONT
debugModebool, default trueRemoved, with no replacement. VideoSDK.setLogLevel() sets how much the SDK logs.
notification, NotificationInfoText of the Android screen-share notificationRemoved. 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, and maxResolution (default 'sd') only chose the layer setQuality() 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.SD keeps the v4.0.1 layers, but also opens the camera at 960x540.
  • defaultCamera picks the camera by direction, not by its position in the device list. In v4.0.1 the list from VideoSDK.getVideoDevices() starts with a back camera on Android and iOS phones, so the default index 0 usually opened a back camera. v5.0.0 opens the front camera unless you pass defaultCamera: FacingMode.BACK.
  • To see the SDK's own logs in flutter run, call await 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 a changeMode() to Mode.SEND_AND_RECV turns a device on. Without permission that device stays off and the SDK reports 3017 (camera) or 3018 (microphone). v4.0.1 asked in all of these places. Call VideoSDK.requestPermissions() before you create tracks, call join(), or call changeMode(Mode.SEND_AND_RECV).
  • Without permission the factories throw 3017 (camera) or 3018 (microphone). Other codes: 3021 and 3022 for no camera or microphone, 3023 and 3024 for a device in use, 3026 when the camera fails to open, and 3071 for an unknown cameraId or microphoneId.
  • noiseConfig is a NoiseConfig and decides every flag. Pass NoiseConfig(noiseSuppression: true, echoCancellation: true, autoGainControl: true) instead of a map. In v4.0.1, a flag you left out of createMicrophoneAudioTrack(noiseConfig:) took its value from the encoderConfig profile. In v5.0.0, once you pass a NoiseConfig, every flag you leave out is turned on, except the new highPassFilter, which stays off. Pass all of noiseSuppression, echoCancellation and autoGainControl, or pass no noiseConfig to keep the profile's values.
  • maxLayer takes EncodingLayer.MAX_LAYER_2 or EncodingLayer.MAX_LAYER_3 instead of an int.
  • CustomStreamTrack.stop() replaces dispose(). The new onEnded callback tells you when the track ends, with a VideoSDKError when 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 returns null if the user declines. On iOS, one enableScreenShare() call opens the system broadcast picker.
  • withAudio works on Android only. iOS screen share sends no audio.
  • enableScreenShare() without a track still works. It shares at CustomScreenShareTrackConfig.h720p_15fps and asks for consent itself. To use another profile, create a track.
  • source and Room.getScreenShareSources() chose a desktop screen or window. Desktop is not supported, and getScreenShareSources() 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.swift sends 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.1v5.0.0
subscribe(topic, onMessage, {options, onOldMessagesReceived, onBatchReceived, onMessageDrop}) returning PubSubMessagessubscribe(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
PubSubRealtimeOverflowRealtimeOverflow.QUEUE or RealtimeOverflow.DROP
unsubscribe(topic, onMessage)unsubscribe(topic, listener), with the same listener object
PubSubMessagesRemoved
PubSubMessage.timestamp (DateTime)A String, as the server sends it
PubSubException and its five subtypesVideoSDKError
PubSubMessage.sendOnly on received messagesRemoved. 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 VideoSDKError 3073.
  • A PubSub call made before the room is joined throws a VideoSDKError with code 3035, so subscribe from your Events.roomJoined handler. While the room reconnects, a call can throw VideoSDKError 3065.
  • RealtimeStore.observe(key, listener) no longer returns an observer id. Stop with stopObserving(key, listener), passing the same function. RealtimeStore calls made before join also throw a VideoSDKError with code 3035. In v4.0.1 they reported 3022 on Events.error and returned null or 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.1v5.0.0
VideoSDK.getVideoDevices(), getAudioDevices()VideoSDK.getCameras(), getMics(). getDevices() returns both.
VideoDeviceInfo, AudioDeviceInfoCameraDeviceInfo, 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.logLevelVideoSDK.getLogLevel()
VideoSDK.keyProviderVideoSDK.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.1v5.0.0
micEnabled, camEnabledisMicEnabled, isCamEnabled
activeSpeakerId, activePresenterIdgetActiveSpeakerId(), getActivePresenterId()
livestreamStateliveStreamState
hlsDownstreamUrl, hlsUrlshlsUrl, 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'} mapsstartLivestream([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.1v5.0.0
unmuteMic(), muteMic() on a remote participantenableMic(), disableMic()
quality (String?)videoQuality (VideoQuality?)
setQuality("high")setQuality(VideoQuality.HIGH). The values are HIGH, MEDIUM and LOW.
isAgentisAgent()
AgentParticipant.agentId, agentStategetAgentId(), getAgentState()

Types and enums​

v4.0.1v5.0.0
StreamRTCStream. kind is a MediaType (VIDEO, AUDIO, SHARE or SHARE_AUDIO), and participantId says whose stream it is.
CustomTrack, CustomVideoTrackConfig, CustomAudioTrackConfigCustomStreamTrack, CustomCameraTrackConfig, CustomMicrophoneTrackConfig (section 3)
TranscriptionConfig(webhookUrl:, summaryConfig:)TranscriptionConfig(webhookURL:, summary:), plus the new modelConfig
PostTranscriptionConfig(summaryConfig:)PostTranscriptionConfig(summary:). SummaryConfig.prompt is optional.
E2EEState.EncryptionSucessed, DecryptionSucessedE2EEState.EncryptionSuccess, DecryptionSuccess
TranslationText.type (String?)TranslationType?: PARTIAL, FULL or null
'recording': {'enabled': true} in a livestream config mapLivestreamConfig(recording: LivestreamRecordingConfig(enabled: true))
FacingMode.user, FacingMode.environmentFacingMode.FRONT, FacingMode.BACK
Lower-case values of RoomState, LogLevel, LeaveReason, AgentState and CaptureFlash, such as RoomState.connectedUpper case, such as RoomState.CONNECTED, LogLevel.INFO, LeaveReason.MANUAL_LEAVE_CALLED, AgentState.SPEAKING and CaptureFlash.ON. LeaveReason.DUPLICATE_PARTICIPANT is new.

Events​

v4.0.1v5.0.0
liveStreamStarted, liveStreamStopped, liveStreamStateChangedlivestreamStarted, livestreamStopped, onLivestreamStateChanged
e2eeStateChangedonE2EEStateChanged, on the room and on the participant (section 10)
hlsStarted, hlsStoppedRemoved. 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 mapAdds 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 idparticipantId
roomJoined after switchTo(), map key switchingRoomIdswitchRoomId
mediaRelayRequestReceived, (meetingId, peerId, displayName, accept, reject)(participantId, roomId, displayName, accept, reject). The first two arguments swapped places.
mediaRelayStarted, mediaRelayStopped, mediaRelayError with a meetingIdThe 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.1v5.0.0
A switch over Events or CustomScreenShareTrackConfig with no default caseAdd 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 mockUpdate 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 fakeUpdate 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

YourOwnBackgroundProcessor.kt
import live.videosdk.webrtc.VideoProcessor
import org.webrtc.VideoFrame

class YourOwnBackgroundProcessor : VideoProcessor {
override fun onFrameReceived(videoFrame: VideoFrame): VideoFrame {
return applyEffect(videoFrame)
}
}

After

YourOwnBackgroundProcessor.kt
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 MainActivity with VideoSDK.getInstance().registerVideoProcessor() as before. Apply it from Dart with room.setVideoProcessor(name) instead of VideoSDK.applyVideoProcessor(videoProcessorName:), and remove it with room.removeVideoProcessor(). Both work before join(). 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 throws IllegalStateException until Dart has called createRoom().
  • VideoFrame now ships with the SDK as videosdk.org.webrtc.VideoFrame, so the processor no longer needs the io.github.webrtc-sdk:android dependency. Remove it from your app's build.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 your AppDelegate, together with the processor class built on videosdk_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>:

ClassMembers
Roomend(), stopTranslation()
ParticipantdisableMic() 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 Room actions, such as muteMic(), enableCam() and startRecording(), already returned a Future in v4.0.1. They can now throw too.
  • Common codes: 3054 when there is nothing to do, for example muteMic() on a muted microphone, 3055 when the same action is already running, 3065 while the room reconnects, and 3035 for an action after the room has left.
  • Turn on the unawaited_futures and discarded_futures lints in analysis_options.yaml to find calls you do not await. unawaited_futures checks async functions, and discarded_futures checks other functions, such as the Before handlers above. Neither flags an arrow callback such as onPressed: () => room.end(), so check those by hand. discarded_futures also flags other Future calls, so you may want it on only while you migrate.
  • Events.error also reports some failures that a call of yours caused, such as a failed room.send() (3070), so the same failure can reach both your catch and your Events.error handler. Handle each one in one place.
  • AgentParticipant changed the same way: pin(), unpin() and remove() return Future<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 on Events.error. In v5.0.0 it throws them.
  • On Android, join() and changeMode(Mode.SEND_AND_RECV) no longer ask for camera or microphone permission, and without it the microphone and camera stay off. Call VideoSDK.requestPermissions() first (section 3).
  • To join again, create a new Room with VideoSDK.createRoom(). Calling join() on a room that has left throws a StateError.
  • Calling join() again on the same room throws VideoSDKError 3055 while the first join is still running, and 3054 once the room is joined.
  • Only one Room exists at a time. A new createRoom() 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.localParticipant exists from createRoom(), 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);
}
});
}
EventOn the participantOn 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.streams is 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:

Errorv4.0.1v5.0.0
ERROR_CAMERA_IN_USE30033023
ERROR_MICROPHONE_IN_USE30043024
ERROR_CAMERA_ACCESS_UNAVAILABLE30053033
ERROR_MICROPHONE_ACCESS_UNAVAILABLE30063034
ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED30073017
ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED30083018
ERROR_CAMERA_PERMISSION_DENIED_BY_OS30093019
ERROR_MICROPHONE_PERMISSION_DENIED_BY_OS30103020
ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED, now ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED30223035
ERROR_CHANGE_WEBCAM3024Not reported. A failed changeCam() throws another code, for example 3051 for an unknown camera.
ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED30253045
ERROR_MULTISTREAM_NOT_SUPPORTED30263046
ERROR_MEETING_RECONNECTING, now ERROR_ROOM_RECONNECTING30273065

Same code, new meaning. A v4.0.1 check on these numbers now matches a different error:

Codev4.0.1v5.0.0
3010ERROR_MICROPHONE_PERMISSION_DENIED_BY_OSERROR_BLUETOOTH_AUDIO_ROUTE_FAILED (Android only)
3011ERROR_STARTING_VIDEOERROR_GET_VIDEO_MEDIA
3012ERROR_STARTING_AUDIOERROR_GET_AUDIO_MEDIA
3013ERROR_STARTING_SCREENSHAREERROR_GET_DISPLAY_MEDIA
3014ERROR_SCREENSHARE_PERMISSION_DENIEDERROR_GET_VIDEO_MEDIA_PERMISSION_DENIED
3015ERROR_SCREENSHARE_NOT_SUPPORTEDERROR_GET_AUDIO_MEDIA_PERMISSION_DENIED
3016ERROR_STARTING_FOREGROUND_SERVICEERROR_GET_DISPLAY_MEDIA_PERMISSION_DENIED
3017ERROR_CREATING_VIDEO_TRACKERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED
3018ERROR_CREATING_AUDIO_TRACKERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED
3019ERROR_CAMERA_DEVICE_INCOMPATIBLEERROR_CAMERA_PERMISSION_DENIED_BY_OS
3020ERROR_MICROPHONE_DEVICE_INCOMPATIBLEERROR_MICROPHONE_PERMISSION_DENIED_BY_OS
3021ERROR_CHANGING_MODEERROR_CAMERA_NOT_FOUND
3022ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINEDERROR_MICROPHONE_NOT_FOUND
3023ERROR_JOINING_MEETINGERROR_CAMERA_IN_USE
3024ERROR_CHANGE_WEBCAMERROR_MICROPHONE_IN_USE
3025ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTEDERROR_CAMERA_PERMISSION_OR_AUTOPLAY_ISSUE
3026ERROR_MULTISTREAM_NOT_SUPPORTEDERROR_VIDEO_SOURCE_INITIATION_FAILED
3027ERROR_MEETING_RECONNECTINGERROR_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 codeError
3001ERROR_CUSTOM_VIDEO_TRACK_DISPOSED
3002ERROR_CUSTOM_AUDIO_TRACK_DISPOSED
3011ERROR_STARTING_VIDEO
3012ERROR_STARTING_AUDIO
3013ERROR_STARTING_SCREENSHARE
3014ERROR_SCREENSHARE_PERMISSION_DENIED
3015ERROR_SCREENSHARE_NOT_SUPPORTED
3016ERROR_STARTING_FOREGROUND_SERVICE
3017ERROR_CREATING_VIDEO_TRACK
3018ERROR_CREATING_AUDIO_TRACK
3019ERROR_CAMERA_DEVICE_INCOMPATIBLE
3020ERROR_MICROPHONE_DEVICE_INCOMPATIBLE
3021ERROR_CHANGING_MODE
3023ERROR_JOINING_MEETING

Codes you will see most often in v5.0.0:

CodeNameMeaning
3035ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINEDThe call needs a joined room.
3051ERROR_NO_WEBCAM_DEVICE_FOUNDNo camera was found.
3054ERROR_ALREADY_IN_REQUESTED_STATENothing to do: it is already in that state.
3055ERROR_OPERATION_IN_PROGRESSThe same operation is already running.
3056ERROR_OPERATION_ABORTEDThe operation was aborted.
3059ERROR_NO_MICROPHONE_DEVICE_FOUNDNo microphone was found.
3065ERROR_ROOM_RECONNECTINGThe room is reconnecting. Try again once it is back.
3071ERROR_INVALID_CUSTOM_TRACK_CONFIGThe track configuration is not one the SDK knows.
3073ERROR_INVALID_PARAMETERAn argument is invalid, for example an empty room id.
4000UNKNOWN_ERRORThe error could not be classified. The original message is kept.
4002INVALID_TOKENThe token was rejected.

Key points:

  • Other 4xxx and 5xxx numbers are unchanged. Two names now use the catalogue's spelling: 4009 is MAX_PARTCIPANT_REACHED, where v4.0.1 sent MAX_PARTICIPANT_REACHED, and 4023 is UNAUTHORIZED_PARTCIPANT_ID, where v4.0.1 sent UNAUTHORIZED_PARTICIPANT_ID. Compare these two by code, not by name.
  • The VideoSDKErrors map is removed. Compare with the VideoSDKError.* constants instead, such as VideoSDKError.INVALID_TOKEN: errors are equal when their code is equal.
  • Error names say ROOM where v4.0.1 said MEETING: 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_DISABLED and SWITCH_ROOM_FAILED. The codes are unchanged, so a check on error.name must change and a check on code does not.

12. Other behaviour changes​

Whatv4.0.1v5.0.0
participant.modeUpdated when the participant changed modeUpdated just before Events.participantModeChanged fires, for the local participant too. changeMode() does not change it when it returns: wait for the event.
Events.qualityLimitation valuesLower case, such as cpu and detectedUpper 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() handlerFor some events, such as Events.participantLeft, the SDK caught it and only logged itReaches 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 oneReturn 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 iOSThrew UnsupportedError once joinedWorks. 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 UIDAndroid 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 idAndroid 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 deviceThrows 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.1v5.0.0
Web, macOS and Windows supportNot supported. Stay on v4.0.1 for those platforms.
Events.videoQualityChangedNot 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 errorsNot 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, PubSubUnsubscribeFailedVideoSDKError
PubSubMessage.sendOnly on received messagesNo replacement
Mode.CONFERENCE, Mode.VIEWERRemoved. Use Mode.SEND_AND_RECV or Mode.RECV_ONLY.
createRoom(notification:), NotificationInfoRemoved. 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.hlsStoppedRemoved. 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 iOSNo replacement (section 7)
videosdk_media_effects pluginNoise 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, keyProviderHidden from the API reference. Use setSharedKey() on BaseKeyProvider and VideoSDK.setParticipantKey() for keys.

14. New in v5.0.0​

FeatureAPI
Pre-call testVideoSDK.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 effectsVideoSDK.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 channelroom.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 errorsVideoSDKError with code, name and message
Log forwardingVideoSDK.enableLogForwarding((level, message) {...}) and disableLogForwarding()
Version infoVideoSDK.sdkVersion and VideoSDK.getBuildId() for bug reports
Capture flashcaptureImage(flash: CaptureFlash.ON) and VideoSDK.setScreenFlash(true). Local participant only.
Outgoing video qualitycreateRoom(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 qualityparticipant.setScreenShareQuality(VideoQuality)
More statisticsparticipant.getTransportStats() and participant.getShareAudioStats()
Applied settingsroom.getAppliedVideoConfig()
Room attributesroom.getAttributes() returns the room's server-side attributes.
Room stateroom.roomState, room.isScreenSharing, room.transcriptionState, room.translationState
Stream detailsStreamState for streamStateChanged, MediaType.SHARE_AUDIO for screen-share audio, CameraDeviceInfo.facingMode
Error constantsVideoSDKError.*, such as VideoSDKError.INVALID_TOKEN; errors are equal by code
Device switchingroom.changeCam() with no arguments moves to the next camera, and room.changeMic() now works on Android and iOS. room.setAudioDeviceChangeListener() reports route changes.
Entry requestsroom.respondEntry(participantId, 'allowed') answers a join request from anywhere, not only from the Events.entryRequested handler.
Listener cleanuproom.removeAllEventListeners() and participant.removeAllEventListeners()
Phone callsEvents.externalCallRinging, externalCallAnswered, externalCallRejected and externalCallHangup, each with a CallType: INCOMING or OUTGOING
Track endCustomStreamTrack.onEnded is called once when a track ends, with a VideoSDKError when the device was lost or access was revoked.
E2EE offVideoSDK.removeKeyProvider() turns end-to-end encryption off. BaseKeyProvider.removeKey(participantId) removes one participant's key.
MirroringRTCVideoRenderer.setMirror(bool) flips every view of a renderer, overriding RTCVideoView(mirror:).
Screen-share profilesCustomScreenShareTrackConfig.h480p_15fps, h480p_30fps and h720p_30fps
High-pass filterhighPassFilter in NoiseConfig
Realtime translationstartTranslation(), stopTranslation() and changeTranslationLanguage() are now documented. See Realtime Translation.

API Reference​

Got a Question? Ask us on discord