Skip to main content
Version: 4.x.x

Migration Guide

This guide covers upgrading the iOS SDK from 3.x.x to 4.0.0. Start by removing the old framework imports (section 1). Most actions are now async throws. Meeting is now Room and many methods and events have new names (section 2 and section 13).

Update the dependency first:

.package(url: "https://github.com/videosdk-live/videosdk-rtc-ios-spm.git", exact: "4.0.0")

// in your target's dependencies
.product(name: "VideoSDKRTCSwift", package: "videosdk-rtc-ios-spm")

In Xcode, choose File > Add Package Dependencies…, enter https://github.com/videosdk-live/videosdk-rtc-ios-spm, set Dependency Rule to Exact Version 4.0.0, and add the VideoSDKRTCSwift product to your app target. Your code still uses import VideoSDKRTC.

caution
  • 4.x is published only as a Swift package. There is no 4.x CocoaPods release. If you installed 3.x with CocoaPods, remove pod 'VideoSDKRTC' from your Podfile and run pod install before you add the package.
  • Change an existing package rule. If you already use the Swift package, set its rule to Exact Version 4.0.0. This includes a Branch main rule from the 3.x install steps. A rule of Up to Next Major from 3.x never selects 4.0.0.

1. Remove import WebRTC, import Mediasoup and import SocketIO​

These frameworks no longer ship with the SDK, so importing them fails with "No such module". import Starscream fails the same way. 3.2.1 brought Socket.IO and Starscream in with the SDK, so if your app uses either one directly, add it as its own package or pod. RTCMTLVideoView, RTCVideoTrack, RTCAudioTrack, RTCMediaStreamTrack, RTCVideoFrame and RTCVideoRenderer now come from VideoSDKRTC. They are smaller SDK types, not the WebRTC classes, so code that uses other WebRTC APIs must change.

Before

import VideoSDKRTC
import WebRTC

extension RoomViewController: ParticipantEventListener {
func onStreamEnabled(_ stream: MediaStream, forParticipant participant: Participant) {
if stream.track.kind == "video", let videoTrack = stream.track as? RTCVideoTrack {
participantVideoView.videoContentMode = .scaleAspectFill
videoTrack.add(participantVideoView)
}
}
}

After

import VideoSDKRTC

extension RoomViewController: ParticipantEventListener {
func onStreamEnabled(_ stream: RTCStream, forParticipant participant: Participant) {
if stream.track.kind == .video, let videoTrack = stream.track as? RTCVideoTrack {
participantVideoView.videoContentMode = .scaleAspectFill
videoTrack.add(participantVideoView)
}
}
}

Key points:

  • MediaStream is now RTCStream.
  • track.kind is now an enum (.audio or .video), not a String.
  • Only the SDK can create tracks and frames. To use a custom track, create it with try await VideoSDK.createCameraVideoTrack(...) or try await VideoSDK.createMicrophoneAudioTrack(...), which return a CustomStreamTrack (CustomRTCMediaStream in 3.2.1).
  • RTCMTLVideoView can no longer be subclassed. If a storyboard or XIB uses it, set Module to VideoSDKRTC in the Identity inspector.
  • RTCMTLVideoView no longer has delegate (RTCVideoViewDelegate), rotationOverride or isMetalAvailable(). To learn the video size, add your own RTCVideoRenderer to the track and read setSize(_:). A custom renderer receives the SDK's RTCVideoFrame, so switch on frame.buffer (case .cvPixelBuffer(let pixelBuffer), case .i420(let buffer)) instead of casting it. Add @unknown default: to that switch, and to any switch on track.kind. Without it, Swift 5 warns and Swift 6 fails to compile.
  • Setting isEnabled to false on a remote participant's RTCAudioTrack now asks the server to stop sending you that participant's audio, and true resumes it. Other participants still hear them. To mute your own microphone, call muteMic().
  • If you read MediaStream.producer or MediaStream.consumer (RTCStream.producer and consumer in 4.0.0), they now expose only id, kind, track, paused and closed, plus pause() and resume() on Consumer, which are now async throws like MediaStream.pause() and resume(). Members such as appData, rtpParameters, stats, close() and Producer's pause() / resume() are gone.

2. VideoSDK.createRoom(...) replaces config(token:) and initMeeting(...)​

VideoSDK.config(token:) and VideoSDK.initMeeting(...) are gone. Pass the token to VideoSDK.createRoom(roomId:token:...), which returns a Room. It throws ERROR_INVALID_PARAMETER for an empty room ID or token, so it needs try. Listen with RoomEventListener instead of MeetingEventListener. join() is now async throws too (see section 3).

Before

override func viewDidLoad() {
super.viewDidLoad()

VideoSDK.config(token: token)

meeting = VideoSDK.initMeeting(
meetingId: meetingId,
participantName: "John Doe",
micEnabled: true,
webcamEnabled: true
)
meeting?.addEventListener(self)
meeting?.join()
}

After

override func viewDidLoad() {
super.viewDidLoad()

do {
room = try VideoSDK.createRoom(
roomId: roomId,
token: token,
participantName: "John Doe",
micEnabled: true,
camEnabled: true
)
} catch {
print("Setup failed: \(error)") // ERROR_INVALID_PARAMETER for an empty token or room ID
return
}
room?.addEventListener(self)

Task {
do {
try await room?.join()
} catch {
print("Join failed: \(error)")
}
}
}
initMeeting in 3.2.1createRoom in 4.0.0
meetingId:roomId:
VideoSDK.config(token:)token:
webcamEnabled:camEnabled:
customCameraVideoStream:customCameraTrack:
customAudioTrack:customMicrophoneTrack:
secureKey:Removed. Use BaseKeyProvider.setSharedKey(_:) and VideoSDK.setKeyProvider(_:) for end-to-end encryption.

Key points:

  • participantId, participantName, multiStream, mode, preferredProtocol, metaData and maxResolution are optional and nil by default. Leave one out and the SDK picks the value: a random participant ID, a random name, multiStream on, .SEND_AND_RECV, .UDP_OVER_TCP, no metadata and .hd. 3.2.1 used an empty ID and the name "Guest".
  • If you pass no participant ID or name, room.localParticipant.id and displayName are empty until onRoomJoined(). Read them there.
  • defaultCamera: now takes a FacingMode (.front or .back).
  • The type of meeting in your code becomes Room?. The rest of this guide calls it room.

3. Actions are now async throws​

Almost every action must now be awaited and throws a VideoSDKError when it fails. Call it with try await. Button handlers and listener callbacks are not async, so wrap the call in a Task.

Before

@IBAction func toggleMicTapped(_ sender: Any) {
if micEnabled {
meeting?.muteMic()
} else {
meeting?.unmuteMic()
}
micEnabled.toggle()
}

After

@IBAction func toggleMicTapped(_ sender: Any) {
Task {
do {
if micEnabled {
try await room?.muteMic()
} else {
try await room?.unmuteMic()
}
micEnabled.toggle()
} catch let error as VideoSDKError where error == .ERROR_ALREADY_IN_REQUESTED_STATE {
micEnabled.toggle() // already in that state, for example after the mic failed to open
} catch let error as VideoSDKError where error == .ERROR_OPERATION_IN_PROGRESS {
// an earlier tap is still running
} catch let error as VideoSDKError {
print("Mic toggle failed: \(error.name) (\(error.code)): \(error.message)")
} catch {
print("Mic toggle failed: \(error)")
}
}
}

Every method below is async throws in 4.0.0:

In 3.2.1MethodsAdd
Plain callRoom: join(), leave(), end(), enableCam(customCameraTrack:), disableCam(), unmuteMic(customMicrophoneTrack:), muteMic(), changeMic(deviceId:), startRecording(...), stopRecording(), startLivestream(outputs:config:), stopLivestream(), startHLS(config:transcription:), stopHLS(), startTranscription(config:), stopTranscription(), startWhiteboard(), stopWhiteboard(), pauseAllStreams(_:), resumeAllStreams(_:), enableAdaptiveSubscription(), disableAdaptiveSubscription(), switchTo(roomId:token:), handleRelayRequestResponse(decision:peerId:roomId:), requestMediaRelay(destinationRoomId:token:kinds:), stopMediaRelay(destinationRoomId:)
Participant: remove(), pin(_:), unpin(_:)
RTCStream and Consumer: pause(), resume()
try await
throwsParticipant: enableMic(), disableMic(), enableCam(), disableCam(), setQuality(_:)await
asyncRoom: changeMode(_:), enableScreenShare(), disableScreenShare()try

Key points:

  • VideoSDKError now conforms to Error. Catch it with catch let error as VideoSDKError and read code, name and message.
  • muteMic(), unmuteMic(), enableCam() and disableCam() now throw ERROR_ALREADY_IN_REQUESTED_STATE when the mic or camera is already in that state, and ERROR_OPERATION_IN_PROGRESS while an earlier call for the same device is still running. 3.2.1 ignored both, so don't show these two to the user as failures. RTCStream.pause() and resume() now throw ERROR_ACTION_NOT_SUPPORTED for your own streams, which 3.2.1 ignored.
  • If the mic or camera fails to open, the SDK turns it off and reports only to onError, so a flag you flip after a successful call goes out of step. Flip it on ERROR_ALREADY_IN_REQUESTED_STATE too, as above, or decide from room.isMicEnabled / room.isCamEnabled, which follow the SDK's own state.
  • If enableCam(customCameraTrack:) or unmuteMic(customMicrophoneTrack:) is refused, for example before joining or while the device is already on, the SDK stops the custom track you passed, unless that track is already being sent. 3.2.1 left it running. Create a new track before you try again. A track that has ended is reported to onError as ERROR_CUSTOM_VIDEO_TRACK_ENDED or ERROR_CUSTOM_AUDIO_TRACK_ENDED, and the device turns on without it.
  • AgentParticipant follows the same changes as Participant.
  • PubSub and RealtimeStore methods were already async throws, but every failure is now a VideoSDKError. PubSubError, RealtimeStoreError and AgentParticipantError are removed, so a catch let error as PubSubError no longer compiles. Catch VideoSDKError and read code, name and message. Subscribing the same listener to a topic twice now throws ERROR_INVALID_PARAMETER, and unsubscribing a listener that is not subscribed does nothing. VideoSDKError is not a LocalizedError, so use error.message instead of error.localizedDescription.
  • Avoid try? on calls whose failures the user should see. Most of these errors no longer reach onError (see section 6).

4. captureImage, uploadBase64File and fetchBase64File return their result​

The completion-handler versions are gone. Each method now returns its value from try await and throws on failure instead of passing nil.

Before

func captureAndUpload(participant: Participant) {
do {
try participant.captureImage(height: 500, width: 500) { base64Image in
guard let base64Image else { return } // nil on any failure
self.meeting?.uploadBase64File(
base64Data: base64Image,
token: self.token,
fileName: "capture.jpeg"
) { url in
print("File URL: \(url ?? "upload failed")")
}
}
} catch {
print("Capture failed: \(error)")
}
}

After

func captureAndUpload(participant: Participant) {
Task {
do {
let base64Image = try await participant.captureImage(width: 500, height: 500)
let url = try await room?.uploadBase64File(
base64Data: base64Image,
token: token,
fileName: "capture.jpeg"
)
print("File URL: \(url ?? "")")
} catch {
print("Capture or upload failed: \(error)")
}
}
}

Key points:

  • captureImage throws ERROR_WEBCAM_NOT_ENABLED when the participant's camera is off and ERROR_CAPTURE_IMAGE_FAILED when the capture fails.
  • captureImage now takes width: before height:, the reverse of 3.2.1.
  • captureImage now also works for remote participants whose camera is on, and takes an optional flash: argument (default .OFF).
  • fetchBase64File(url:token:) changes the same way: let base64 = try await room?.fetchBase64File(url: url, token: token).

5. Custom video processors implement onFrameReceived(pixelBuffer:)​

RTCCVPixelBuffer no longer exists and apps can't create an RTCVideoFrame, so a processor that unwraps the frame's pixel buffer and returns a new frame no longer compiles. Implement the new onFrameReceived(pixelBuffer:) instead. It receives the camera frame as a BGRA CVPixelBuffer (kCVPixelFormatType_32BGRA) and returns the processed buffer.

Before

import VideoSDKRTC
import WebRTC

class BlurProcessor: VideoSDKVideoProcessor {
func onFrameReceived(frame: RTCVideoFrame) -> RTCVideoFrame? {
guard let buffer = frame.buffer as? RTCCVPixelBuffer,
let blurred = blurBackground(buffer.pixelBuffer) else { return frame }
return RTCVideoFrame(
buffer: RTCCVPixelBuffer(pixelBuffer: blurred),
rotation: frame.rotation,
timeStampNs: frame.timeStampNs
)
}
}

After

import VideoSDKRTC

class BlurProcessor: VideoSDKVideoProcessor {
func onFrameReceived(pixelBuffer: CVPixelBuffer) -> CVPixelBuffer? {
return blurBackground(pixelBuffer) ?? pixelBuffer // blurBackground(_:) is your own processing
}
}

Key points:

  • Implement one of the two hooks, not both. Returning nil drops the frame. 3.2.1 sent the original frame instead, so return the input buffer, not nil, when your processor has nothing to change.
  • VideoSDKBackgroundProcessor returns nil until its background image has downloaded, so no camera video is sent until then, or at all if the download fails.
  • onFrameReceived(frame:) still exists for code that only reads frames. frame.buffer is now an enum (.cvPixelBuffer or .i420), and the SDK always passes .cvPixelBuffer holding a BGRA buffer. rotation is always ._0, and timestampNs (spelled timeStampNs in 3.2.1) is the time of the call, not the capture time. A switch on frame.buffer needs @unknown default: (see section 1).
  • You still attach the processor with room?.setVideoProcessor(processor:). To remove it, call room?.removeVideoProcessor().

6. Room calls made at the wrong time throw instead of reaching onError​

This compiles once you add try await, but behaves differently. In 3.2.1, calling an action at the wrong time (before joining, while reconnecting or after leaving) sent the error to onError(error:) and the call returned quietly. In 4.0.0 a Room method throws the error and onError is not called. If your app shows errors only from onError, or calls with try?, these failures now go unnoticed.

Before

@IBAction func cameraTapped(_ sender: Any) {
// Called before joining: onError(error:) received ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED
meeting?.enableWebcam()
}

After

@IBAction func cameraTapped(_ sender: Any) {
Task {
do {
try await room?.enableCam()
} catch let error as VideoSDKError {
// Called before joining: ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED lands here, not in onError(error:)
showAlert(error.message)
} catch {
showAlert("\(error)")
}
}
}

Key points:

  • Some failures reach onError as well as being thrown, so make sure you don't show them twice:
    • Participant and RTCStream methods, such as enableMic(), pin(_:), remove(), setQuality(_:), pause() and resume(), called before joining, while reconnecting or after leaving.
    • changeMode(_:), uploadBase64File and fetchBase64File called before joining, while reconnecting or after leaving.
    • An unknown device ID in changeCam(deviceId:) or changeMic(deviceId:).
    • ERROR_CAPTURE_IMAGE_FAILED from captureImage, and most send(_:reliable:) failures.
  • An action called after you have left the room now throws ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED. 3.2.1 reported ERROR_ALREADY_IN_REQUESTED_STATE for that case. leave() itself still uses ERROR_ALREADY_IN_REQUESTED_STATE (see section 7).
  • muteMic() and disableCam() called while the room is reconnecting now throw ERROR_ROOM_RECONNECTING and leave the mic or camera on. 3.2.1 turned it off. If the user turns a device off during a reconnect, call the method again once onRoomStateChanged(roomState:) reports CONNECTED.
  • Failures to open the camera or microphone still arrive only in onError(error:), even when your call started the device. enableCam() and unmuteMic() return before the device opens, and join() returns before the micEnabled / camEnabled devices open, so a denied permission or a device in use is never thrown. Errors the SDK raises on its own also still arrive in onError(error:).
  • onSocketError(message:) is no longer called.

7. join() and leave() report state errors, and leave() can cancel a join​

This compiles once you add try await, but behaves differently. join() and leave() now check the room state and throw, where 3.2.1 ignored the call or reported to onError. A leave() that arrives while a join is still running now cancels the join.

Before

@IBAction func joinTapped(_ sender: Any) {
meeting?.join() // a second tap was ignored
}

@IBAction func leaveTapped(_ sender: Any) {
meeting?.leave() // errors went to onError(error:)
dismiss(animated: true)
}

After

@IBAction func joinTapped(_ sender: Any) {
Task {
do {
try await room?.join()
} catch let error as VideoSDKError
where error == .ERROR_OPERATION_IN_PROGRESS || error == .ERROR_ALREADY_IN_REQUESTED_STATE {
// A join is already running, or you are already in the room
} catch let error as VideoSDKError {
// The join failed and roomState is now FAILED
showAlert("Could not join: \(error.message)")
} catch {
showAlert("Could not join: \(error)")
}
}
}

@IBAction func leaveTapped(_ sender: Any) {
Task {
do {
try await room?.leave()
} catch {
// Already left (ERROR_ALREADY_IN_REQUESTED_STATE) or never joined
print("Leave: \(error)")
}
dismiss(animated: true)
}
}

Key points:

  • A second join() throws ERROR_OPERATION_IN_PROGRESS while a join is running, or ERROR_ALREADY_IN_REQUESTED_STATE once you are in the room.
  • A failed join sets roomState to FAILED (you get onRoomStateChanged(roomState:)) and throws.
  • leave() throws ERROR_ALREADY_IN_REQUESTED_STATE after the room has ended, and ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED before you have joined.
  • If leave() cancels a join, the pending join() returns without throwing. Treat onRoomJoined(), not the return of join(), as the sign that you are in the room.
  • join() returns before the mic and camera from micEnabled / camEnabled start. Wait for onStreamEnabled before you treat local media as on.

8. changeMode(_:) throws and no longer reapplies micEnabled / camEnabled​

This compiles once you add try, but behaves differently. In 3.2.1, changeMode(_:) did not report a failed switch, ignored a switch to the mode you were already in without telling you, and turned the mic and camera back on from initMeeting's micEnabled / webcamEnabled when you returned to SEND_AND_RECV. In 4.0.0 it throws when the switch fails, throws ERROR_ALREADY_IN_REQUESTED_STATE if you are already in that mode, and no longer reapplies createRoom's micEnabled / camEnabled.

Before

Task {
await meeting?.changeMode(.SEND_AND_RECV)
// The mic and camera came back on from micEnabled / webcamEnabled
}

After

Task {
do {
try await room?.changeMode(.SEND_AND_RECV)
// micEnabled / camEnabled are not applied again. Use onStreamEnabled to see
// what came back, and call unmuteMic() / enableCam() for anything you need.
} catch let error as VideoSDKError where error == .ERROR_ALREADY_IN_REQUESTED_STATE {
// Already in SEND_AND_RECV
} catch let error as VideoSDKError {
showAlert("Could not change mode: \(error.message)")
} catch {
showAlert("Could not change mode: \(error)")
}
}

9. Screen share throws when the app is not set up for it​

This compiles once you add try, but behaves differently. enableScreenShare() and disableScreenShare() now throw. enableScreenShare() throws ERROR_GET_DISPLAY_MEDIA when Info.plist has no RTCScreenSharingExtension or RTCAppGroupIdentifier key, or the app is not in that App Group. In 3.2.1 a missing extension key went unreported. disableScreenShare() throws when the share has already ended, so a share button that keeps its own flag can get stuck. Read isScreenSharing instead.

Before

@IBAction func shareTapped(_ sender: Any) {
Task {
if isSharing {
await meeting?.disableScreenShare()
} else {
await meeting?.enableScreenShare()
}
isSharing.toggle()
}
}

After

@IBAction func shareTapped(_ sender: Any) {
Task {
do {
if room?.isScreenSharing == true {
try await room?.disableScreenShare()
} else {
try await room?.enableScreenShare() // or enableScreenShare(.h1080p_15fps)
}
} catch let error as VideoSDKError where error == .ERROR_GET_DISPLAY_MEDIA {
// Check RTCScreenSharingExtension, RTCAppGroupIdentifier and the App Group entitlement
} catch let error as VideoSDKError
where error == .ERROR_ALREADY_IN_REQUESTED_STATE || error == .ERROR_OPERATION_IN_PROGRESS {
// Already in that state, or the previous broadcast is still stopping
} catch let error as VideoSDKError {
showAlert("Screen share failed: \(error.message)")
} catch {
showAlert("Screen share failed: \(error)")
}
}
}

Key points:

  • The new optional argument (CustomScreenShareTrackConfig, default .h720p_15fps) sets the screen-share quality separately from the camera.
  • The share can end without a call from you. The user can stop the broadcast from Control Center, and if no broadcast starts within about 30 seconds of the picker appearing, the SDK releases the share. isScreenSharing is true from the moment the picker appears until the share ends.
  • disableScreenShare() throws ERROR_ALREADY_IN_REQUESTED_STATE when sharing is already off. enableScreenShare() throws ERROR_ALREADY_IN_REQUESTED_STATE while your broadcast is running, and ERROR_OPERATION_IN_PROGRESS while the previous broadcast is still stopping. 3.2.1 ignored the first case and showed the picker again in the other two, so don't show these to the user as failures.
  • The Info.plist keys, the App Group and the Broadcast Upload Extension are unchanged. An extension built from the VideoSDK example keeps working.

10. Error codes that changed​

This compiles, but behaves differently. Two codec errors have new numbers, so code that compares the number with 3008 or 3009 stops matching. Compare with the constant instead. VideoSDKError equality compares only the code.

Before

func onError(error: VideoSDKError) {
if error.rawValue == 3008 {
// The device cannot send video with this codec
}
}

After

func onError(error: VideoSDKError) {
if error == .ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED { // 3045 in 4.0.0
// The device cannot send video with this codec
}
}

Same name, new number

Constant3.2.14.0.0
ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED30083045
ERROR_MULTISTREAM_NOT_SUPPORTED30093046

Same failure, new code. Camera and microphone permission failures now arrive in onError(error:) with new constants, so a check for 3014 or 3015 compiles but never matches:

Failure3.2.14.0.0
Camera access refused at the promptERROR_GET_VIDEO_MEDIA_PERMISSION_DENIED (3014)ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED (3017)
Camera access denied in SettingsERROR_GET_VIDEO_MEDIA_PERMISSION_DENIED (3014)ERROR_CAMERA_PERMISSION_DENIED_BY_OS (3019)
Microphone access refused at the promptERROR_GET_AUDIO_MEDIA_PERMISSION_DENIED (3015)ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED (3018)
Microphone access denied in SettingsERROR_GET_AUDIO_MEDIA_PERMISSION_DENIED (3015)ERROR_MICROPHONE_PERMISSION_DENIED_BY_OS (3020)

Same number, new name. The code is unchanged, so a check on the number still matches. A check on the constant or on error.name must use the new name:

Code3.2.14.0.0
3035ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINEDERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED
3065ERROR_MEETING_RECONNECTINGERROR_ROOM_RECONNECTING

Same number, new meaning: none. 3008 and 3009 are no longer used.

Removed codes: none.

Key points:

  • The default message changed for 3011 to 3016, 3035 and ERROR_MULTISTREAM_NOT_SUPPORTED, and many errors now carry a message written for the call that failed. Compare codes or constants, never message text.
  • A thrown VideoSDKError can carry a code that is not in the SDK's list of constants, so keep a default branch when you handle errors by code.
New error codes in 4.0.0
CodeConstant
3017ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED
3018ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED
3019ERROR_CAMERA_PERMISSION_DENIED_BY_OS
3020ERROR_MICROPHONE_PERMISSION_DENIED_BY_OS
3021ERROR_CAMERA_NOT_FOUND
3022ERROR_MICROPHONE_NOT_FOUND
3023ERROR_CAMERA_IN_USE
3024ERROR_MICROPHONE_IN_USE
3025ERROR_CAMERA_PERMISSION_OR_AUTOPLAY_ISSUE
3026ERROR_VIDEO_SOURCE_INITIATION_FAILED
3027ERROR_WEBCAM_TRACK_ENDED
3028ERROR_MICROPHONE_TRACK_ENDED
3029ERROR_INVALID_CUSTOM_VIDEO_TRACK
3030ERROR_INVALID_CUSTOM_AUDIO_TRACK
3031ERROR_CUSTOM_VIDEO_TRACK_ENDED
3032ERROR_CUSTOM_AUDIO_TRACK_ENDED
3033ERROR_CAMERA_ACCESS_UNAVAILABLE
3034ERROR_MICROPHONE_ACCESS_UNAVAILABLE
3036ERROR_RN_CAMERA_ACCESS_DENIED_OR_DISMISSED
3037ERROR_RN_CAMERA_NOT_FOUND
3038ERROR_RN_MIC_ACCESS_DENIED_OR_DISMISSED
3039ERROR_RN_MIC_NOT_FOUND
3040ERROR_RN_CAMERA_ACCESS_UNAVAILABLE
3041ERROR_RN_MIC_ACCESS_UNAVAILABLE
3042ERROR_RN_CAMERA_TRACK_ENDED
3043ERROR_RN_MIC_TRACK_ENDED
3044ERROR_ROOM_MEDIA_CONNECTION_FAILED
3048ERROR_MEDIA_NOT_READY
3049ERROR_WEBCAM_PRODUCE_FAILED
3050ERROR_MEDIA_DEVICE_NOT_INITIALIZED
3051ERROR_NO_WEBCAM_DEVICE_FOUND
3052ERROR_NO_SUPPORTED_VIDEO_CODEC
3053ERROR_VIDEO_TRACK_CREATION_FAILED
3055ERROR_OPERATION_IN_PROGRESS
3056ERROR_OPERATION_ABORTED
3057ERROR_END_ROOM_FAILED
3058ERROR_MICROPHONE_PRODUCE_FAILED
3059ERROR_NO_MICROPHONE_DEVICE_FOUND
3060ERROR_AUDIO_TRACK_CREATION_FAILED
3061ERROR_SCREENSHARE_PRODUCE_FAILED
3062ERROR_WEBCAM_NOT_ENABLED
3063ERROR_SET_WEBCAM_QUALITY_FAILED
3064ERROR_MEDIA_DEVICE_NOT_LOADED
3066ERROR_ACTION_NOT_SUPPORTED_IN_MODE
3067ERROR_INVALID_MODE
3068ERROR_ACTION_NOT_SUPPORTED
3069ERROR_CAPTURE_IMAGE_FAILED
3070ERROR_SEND_FAILED
3071ERROR_INVALID_CUSTOM_TRACK_CONFIG
3072ERROR_PAYLOAD_TOO_LARGE
3073ERROR_INVALID_PARAMETER
3074ERROR_FILE_UPLOAD_FAILED
3075ERROR_FILE_FETCH_FAILED
3076ERROR_WEBCAM_CLOSE_PRODUCER_FAILED
3077ERROR_MICROPHONE_CLOSE_PRODUCER_FAILED
3078ERROR_SCREENSHARE_CLOSE_PRODUCER_FAILED
3079ERROR_OPERATION_TIMED_OUT

11. switchWebcam() was removed​

Use changeCam() with no argument. It moves to the next camera, for example from the front camera to the back one, and like the other actions it is async throws.

Before

@IBAction func switchCameraTapped(_ sender: Any) {
meeting?.switchWebcam()
}

After

@IBAction func switchCameraTapped(_ sender: Any) {
Task {
do {
try await room?.changeCam() // no argument: the next camera, e.g. front to back
} catch let error as VideoSDKError {
showAlert("Could not switch camera: \(error.message)")
} catch {
showAlert("Could not switch camera: \(error)")
}
}
}

Key points:

  • To pick a camera, pass the deviceId of a camera from VideoSDK.getCameras(), as in changeCam(deviceId: camera.deviceId). getCameras() now returns CameraDeviceInfo values, not names, and deviceId is the camera's AVCaptureDevice.uniqueID. A camera name you stored from 3.2.1 no longer matches. An unknown ID throws ERROR_NO_WEBCAM_DEVICE_FOUND.
  • If the camera is off, changeCam only selects the camera. The next enableCam() uses it.
  • With the camera on, changeCam turns it off and on again with the new camera, so you get onStreamDisabled and then onStreamEnabled for your webcam stream, and other participants see your video stop and start. changeMic does the same while the mic is on. 3.2.1's switchWebcam() and changeMic(selectedDevice:) switched without these events.
  • changeMic(selectedDevice:) is now changeMic(deviceId:) and works the same way: pass the deviceId of a device from VideoSDK.getMics(). nil picks the next microphone, an unknown ID throws ERROR_NO_MICROPHONE_DEVICE_FOUND, and with the mic off it only selects the device.
  • To switch to a custom track during a call, use changeCam(customCameraTrack:) or changeMic(customMicrophoneTrack:). Both take a CustomStreamTrack.

12. Removed types and enum cases​

Mode.CONFERENCE, Mode.VIEWER, PreferredCodec and AuthorisationDelegate no longer exist, so code that uses them does not compile.

Before

meeting = VideoSDK.initMeeting(
meetingId: meetingId,
participantName: "Viewer",
mode: .VIEWER
)

After

room = try VideoSDK.createRoom(
roomId: roomId,
token: token,
participantName: "Viewer",
mode: .SIGNALLING_ONLY
)
RemovedUse instead
Mode.CONFERENCEMode.SEND_AND_RECV
Mode.VIEWERMode.SIGNALLING_ONLY. RECV_ONLY is a different mode: it receives media, and VIEWER received none.
PreferredCodecVideoCodec, with the same cases and raw values
AuthorisationDelegateNothing. Remove the conformance and keep handlePermission() as your own method.
RTCCVPixelBuffer and the rest of the WebRTC frameworkThe RTC* types in VideoSDKRTC (section 1) and onFrameReceived(pixelBuffer:) (section 5)

Key points:

  • Mode(rawValue: "CONFERENCE") and Mode(rawValue: "VIEWER") now return nil.

13. Renamed and removed APIs​

Many names changed. Code that uses an old name no longer compiles, except listener methods. RoomEventListener, ParticipantEventListener and RealtimeStoreEventListener have default implementations, so a listener method with an old name still compiles but is never called. Check every listener method against the tables below.

Before

extension RoomViewController: MeetingEventListener {
func onMeetingJoined() { }
func onMeetingLeft() { }
func onSpeakerChanged(participantId: String?) { }
}

meeting?.requestMediaRelay(meetingId: destinationMeetingId, token: token, kinds: ["video", "audio"])

After

extension RoomViewController: RoomEventListener {
func onRoomJoined() { }
func onRoomLeft(reason: LeaveReason) { }
func onSpeakerChanged(activeSpeakerId: String?) { }
}

Task {
do {
try await room?.requestMediaRelay(
destinationRoomId: destinationRoomId,
token: token, // optional: nil uses the room's token
kinds: [.VIDEO, .AUDIO]
)
} catch let error as VideoSDKError {
showAlert("Could not start the relay: \(error.message)")
} catch {
showAlert("Could not start the relay: \(error)")
}
}

Room

3.2.14.0.0
MeetingRoom
meetingState, MeetingStateroomState, RoomState
meetingModeroomMode
pubsubpubSub
activePresenterIdgetActivePresenterId()
enableWebcam(customVideoStream:), disableWebcam()enableCam(customCameraTrack:), disableCam()
changeWebcam(deviceId:)changeCam(deviceId:), with a CameraDeviceInfo.deviceId (section 11)
unmuteMic(customAudioStream:)unmuteMic(customMicrophoneTrack:)
changeMic(selectedDevice:)changeMic(deviceId:)
getMics()VideoSDK.getMics(), which returns MicrophoneDeviceInfo values. Switch with room.setAudioDevice(_:).
switchTo(meetingId:token:mode:)switchTo(roomId:token:). mode is removed: you keep your current mode, and can change it with changeMode(_:) after the switch. token is optional: nil uses the room's token.
requestMediaRelay(meetingId:token:kinds:)requestMediaRelay(destinationRoomId:token:kinds:). token is optional. kinds is now [StreamKind]? (.AUDIO, .VIDEO, .SHARE) instead of [String]?.
stopMediaRelay(meetingId:)stopMediaRelay(destinationRoomId:)
handleRelayRequestResponse(decision:peerId:meetingId:)handleRelayRequestResponse(decision:peerId:roomId:)
startRecording(webhookUrl:...)webhookUrl is optional
realtimeStore.set(key:value:), realtimeStore.get(key:)realtimeStore.setValue(key:value:), realtimeStore.getValue(key:)
realtimeStore.observe(key:forListener:), stopObserving(key:forListener:)observe(key:listener:), stopObserving(key:listener:)
pubSub.subscribe(topic:forListener:options:), unsubscribe(topic:forListener:)subscribe(topic:listener:options:), unsubscribe(topic:listener:)
Settable meetingState, hlsState, hlsUrls, liveStreamState, recordingState, transcriptionState, translationState, e2eeEnabled, and Participant.pinState, modeRead-only. The SDK updates them.

Events

3.2.14.0.0
MeetingEventListenerRoomEventListener
onMeetingJoined(), onMeetingJoined(switchMeetingId:)onRoomJoined(), onRoomJoined(switchRoomId:)
onMeetingLeft(), onMeetingLeft(reason:)onRoomLeft(reason:) only
onParticipantLeft(_:), onParticipantLeft(_:reason:)onParticipantLeft(_:reason:) only
onMeetingStateChanged(meetingState:)onRoomStateChanged(roomState:)
onSpeakerChanged(participantId:)onSpeakerChanged(activeSpeakerId:)
onPresenterChanged(activePresenterId:)onPresenterChanged(presenterId:)
onEntryRequested(id:name:allow:deny:)onEntryRequested(participantId:name:allow:deny:)
onEntryResponded(id:decision:)onEntryResponded(participantId:decision:)
onPinStateChanged(participantId:pinType:)onPinStateChanged(participantId:state:pinnedBy:). state is a ParticipantPinState, and pinnedBy is the ID of the participant who pinned, or nil.
onMicChanged(selectedDevice:)room.setAudioDeviceChangeListener(_:)
onMediaRelayRequestReceived(participantId:meetingId:displayName:accept:reject:)onMediaRelayRequestReceived(participantId:roomId:displayName:accept:reject:)
onMediaRelayRequestResponse(decision:decidedBy:meetingId:)onMediaRelayRequestResponse(decision:decidedBy:roomId:)
onMediaRelayStarted(meetingId:)onMediaRelayStarted(roomId:)
onMediaRelayStopped(meetingId:)onMediaRelayStopped(roomId:reason:)
onMediaRelayError(meetingId:error:)onMediaRelayError(roomId:error:)
ParticipantEventListener.e2eeStateChanged(forParticipant:state:_:)onE2EEStateChanged(forParticipant:state:_:)
RealtimeStoreEventListener.onValueChanged(key:value:updatedBy: String)updatedBy is a Participant?, nil when that participant has left
TranscriptionText.type: StringTranscriptionType?: .REALTIME, .FULL_SENTENCE, or nil when the type is missing or unknown. Append to a transcript only on .FULL_SENTENCE.
MediaStream.kind .state(value: .audio) / .state(value: .video)MediaType is flat: .audio, .video, .share, .shareAudio. Compare with stream.kind == .video.

Participants, tracks and devices

3.2.14.0.0
MediaStreamRTCStream
Participant.enableWebcam(), disableWebcam()enableCam(), disableCam()
Participant.isAgent, AgentParticipant.agentId, agentStateisAgent(), getAgentId(), getAgentState()
captureImage(height:width:)captureImage(width:height:flash:) (section 4)
CustomRTCMediaStreamCustomStreamTrack. Its videoCodec is now codec, and facingMode is a FacingMode?.
CustomVideoTrackConfig, CustomAudioTrackConfigCustomCameraTrackConfig, CustomMicrophoneTrackConfig
createCameraVideoTrack(encoderConfig:facingMode:multiStream:codec:bitrateMode:maxLayer:) throws -> CustomRTCMediaStream?createCameraVideoTrack(encoderConfig:facingMode:multiStream:cameraId:maxLayer:bitrateMode:codec:) async throws -> CustomStreamTrack. Pass the arguments in this order. facingMode is a FacingMode?, and the new cameraId takes a CameraDeviceInfo.deviceId. It throws instead of returning nil.
createAudioTrack(noiseConfig:encoderConfig:)createMicrophoneAudioTrack(encoderConfig:microphoneId:noiseConfig:) async throws -> CustomStreamTrack. Pass the arguments in this order.
NoiseConfig(noiseSupression:echoCancellation:autoGainControl:highPassFilter:)NoiseConfig(noiseSuppression:...). Every flag is optional, and the defaults are true, true, true, false.
VideoSDK.getCameras() -> [String]-> [CameraDeviceInfo], with label, deviceId and facingMode
VideoSDK.getAudioDevices() -> [String]VideoSDK.getMics() -> [MicrophoneDeviceInfo]
VideoSDK.setKeyProvider(_:) throwsVideoSDK.setKeyProvider(_:) does not throw. VideoSDK.removeKeyProvider() turns encryption off for the next join. Read the provider back with VideoSDK.getKeyProvider().
BaseKeyProvider(discardFrameWhenCryptorNotReady:)BaseKeyProvider(), with no parameters
PubSub.publish(topic:message:options: [String: Any])options: PubSubPublishOptions(persist:sendOnly:)
HLSUrl.downstreamUrlRemoved. Use playbackHlsUrl or livestreamUrl, which are now String?.

Enums and config types

3.2.14.0.0
LogLevel.none, .error, .warn, .info, .debug, .all.NONE, .ERROR, .WARN, .INFO, .DEBUG, .ALL
LeaveReason.websocketDisconnected, .manualLeaveCalled, ....WEBSOCKET_DISCONNECTED, .MANUAL_LEAVE_CALLED, ... The raw values are the same.
ConfigMode.video_and_audio, .audio.VIDEO_AND_AUDIO, .AUDIO. The raw values are the same.
ConfigQuality.high, .med, .low.HIGH, .MED, .LOW. The raw values are the same.
TranscriptionConfig(webHookUrl:summary:)TranscriptionConfig(webhookURL:summary:modelConfig:)
SummaryConfig(enabled:prompt:)prompt is optional
RecordingConfig, HLSConfig, ConfigLayout with every field requiredEvery field is optional. Leave one out and the server picks it. LivestreamConfig.layout and theme are optional too.
RealtimeOverflow.queue, .drop.QUEUE, .DROP. PubSubSubscribeOptions(oldMessageLimit:realtimeOverflow:maxQueue:newMessageLimit:) builds the options in one call.
ParticipantPinState.cam, .share: Bool?Bool
CameraPositionRemoved. Use FacingMode.
setSharedKey(_:) with an empty keyThrows ERROR_INVALID_PARAMETER

Also new in 4.0.0

APIWhat it does
Room.getActiveSpeakerId()The ID of the participant speaking now, or nil
Room.isMicEnabled, Room.isCamEnabledWhether your mic or camera is on. Before you join, whether it will turn on at join.
Room.setAudioDevice(_:)Switches audio to a MicrophoneDeviceInfo from VideoSDK.getMics(). Pass the Speaker entry for the loudspeaker. Throws for an unknown device.
Room.setAudioDeviceChangeListener(_:)Calls your closure with the selected device and the available ones, once right away and then on every audio route change
VideoSDK.getDevices(), getSelectedVideoDevice(), getSelectedAudioDevice()List all devices, and read the camera and audio device in use
VideoSDK.getLogLevel()The current log level. .INFO until you call setLogLevel(level:).
RTCMTLVideoView.setMirror(_:)Flips the rendered video horizontally

14. createCameraVideoTrack and createMicrophoneAudioTrack throw until permission is granted​

This compiles, but behaves differently. In 3.2.1, createCameraVideoTrack started the camera, so iOS asked for permission at that point. In 4.0.0 neither factory shows a prompt, and both throw until access is granted.

Key points:

  • createCameraVideoTrack and createMicrophoneAudioTrack now throw VideoSDKError values, such as ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED, ERROR_CAMERA_NOT_FOUND, ERROR_CAMERA_IN_USE, ERROR_INVALID_CUSTOM_TRACK_CONFIG and the microphone equivalents. They never ask for permission: until access is granted they throw ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED or ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED. VideoSDK.getVideoPermission() and VideoSDK.getAudioPermission() return before the user answers, so create the track only after access is granted, for example once await AVCaptureDevice.requestAccess(for: .video) returns true.
  • Both are async throws, so call them with try await. They wait up to 2 seconds for the device to open, without blocking the thread, and throw the errors that happen in that time. If the device then fails to open, the stream ends and its onEnded handler receives the error.

15. Other behaviour changes​

What3.2.14.0.0, and what to do
initMeeting(secureKey:)Accepted, but not used for encryptionRemoved. To turn on end-to-end encryption, set a key with BaseKeyProvider.setSharedKey(_:) and pass the provider to VideoSDK.setKeyProvider(_:)
HLSUrl.downstreamUrlHeld the stream's downstream URLRemoved. Use playbackHlsUrl or livestreamUrl
VideoSDK.setLogLevel(level:)No SDK logs until you called itThe level is .INFO from the start

New in 4.0.0​

FeatureAPI
Pre-call device and network testVideoSDK.runPreCallTest(_:onStatsChange:), VideoSDK.stopPreCallTest(). See Precall Setup.
Data messages (up to 15 KiB, not stored on the server)Room.send(_:reliable:) for a String or Data, RoomEventListener.onDataReceived(_:), DataMessage (reliable tells you how it was sent)
Real-time translationstartTranslation(config:), stopTranslation(), changeTranslationLanguage(_:), translationState, onTranslationStateChanged(state:), onTranslationText(_:), onTranslationLanguageChanged(participantId:language:), createRoom(translationLanguage:speakingLanguage:)
Screen-share quality and controlenableScreenShare(_:) with a CustomScreenShareTrackConfig, VideoSDK.createScreenShareVideoTrack(encoderConfig:multiStream:withAudio:), which returns a CustomStreamTrack to pass to enableScreenShare(_:) (iOS shares no audio, so withAudio has no effect), Room.isScreenSharing
Choose which media you receivecreateRoom(autoConsume:), Participant.consumeMicStreams(), consumeWebcamStreams(), stopConsumingMicStreams(), stopConsumingWebcamStreams(). The consume and stop-consuming methods take effect only when the room was created with autoConsume set to false.
Video quality controlscreateRoom(maxResolution:) with MaxResolution, createRoom(dynaCast:), Room.setWebcamQuality(_:), Participant.setScreenShareQuality(_:), Participant.setViewPort(width:height:)
Default cameracreateRoom(defaultCamera:) with a FacingMode
Custom trackscameraId and microphoneId parameters, CustomStreamTrack.onEnded, isEnded, stop(), Room.changeCam(customCameraTrack:), Room.changeMic(customMicrophoneTrack:)
Codec informationRoom.getAppliedVideoConfig()
More statsParticipant.getTransportStats(), Participant.getShareAudioStats()
Photo flashcaptureImage(width:height:flash:) with CaptureFlash (.OFF, .ON), VideoSDK.setScreenFlash(_:), WhiteOverlayScreenFlash
Per-participant E2EE keysVideoSDK.setParticipantKey(_:key:), BaseKeyProvider.removeKey(_:). On the installed provider, removeKey(_:) stops decrypting that participant on this device until a new key is set. onE2EEStateChanged reports a success state when encryption or decryption starts. Decryption failures are not reported, so a wrong or removed key raises no event. setKeyProvider(_:) now also accepts a provider that has only participant keys.
Rendering helpersRTCStream.createRenderView(), videoTracks, audioTracks, participantId, RTCVideoTrack.isShare
Error code propertyVideoSDKError.code

Room.enablePeerMic(_:), disablePeerMic(_:), enableAllParticipantsMic(), disableAllParticipantsMic(), pinParticipant(_:kind:) and unpinParticipant(_:kind:) are new but already deprecated. Use the Participant methods instead.


See also​

Got a Question? Ask us on discord