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.
- 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 runpod installbefore 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 Branchmainrule 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:
MediaStreamis nowRTCStream.track.kindis now an enum (.audioor.video), not aString.- Only the SDK can create tracks and frames. To use a custom track, create it with
try await VideoSDK.createCameraVideoTrack(...)ortry await VideoSDK.createMicrophoneAudioTrack(...), which return aCustomStreamTrack(CustomRTCMediaStreamin 3.2.1). RTCMTLVideoViewcan no longer be subclassed. If a storyboard or XIB uses it, set Module toVideoSDKRTCin the Identity inspector.RTCMTLVideoViewno longer hasdelegate(RTCVideoViewDelegate),rotationOverrideorisMetalAvailable(). To learn the video size, add your ownRTCVideoRendererto the track and readsetSize(_:). A custom renderer receives the SDK'sRTCVideoFrame, so switch onframe.buffer(case .cvPixelBuffer(let pixelBuffer),case .i420(let buffer)) instead of casting it. Add@unknown default:to that switch, and to anyswitchontrack.kind. Without it, Swift 5 warns and Swift 6 fails to compile.- Setting
isEnabledtofalseon a remote participant'sRTCAudioTracknow asks the server to stop sending you that participant's audio, andtrueresumes it. Other participants still hear them. To mute your own microphone, callmuteMic(). - If you read
MediaStream.producerorMediaStream.consumer(RTCStream.producerandconsumerin 4.0.0), they now expose onlyid,kind,track,pausedandclosed, pluspause()andresume()onConsumer, which are nowasync throwslikeMediaStream.pause()andresume(). Members such asappData,rtpParameters,stats,close()andProducer'spause()/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.1 | createRoom 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,metaDataandmaxResolutionare optional andnilby default. Leave one out and the SDK picks the value: a random participant ID, a random name,multiStreamon,.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.idanddisplayNameare empty untilonRoomJoined(). Read them there. defaultCamera:now takes aFacingMode(.frontor.back).- The type of
meetingin your code becomesRoom?. The rest of this guide calls itroom.
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.1 | Methods | Add |
|---|---|---|
| Plain call | Room: 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 |
throws | Participant: enableMic(), disableMic(), enableCam(), disableCam(), setQuality(_:) | await |
async | Room: changeMode(_:), enableScreenShare(), disableScreenShare() | try |
Key points:
VideoSDKErrornow conforms toError. Catch it withcatch let error as VideoSDKErrorand readcode,nameandmessage.muteMic(),unmuteMic(),enableCam()anddisableCam()now throwERROR_ALREADY_IN_REQUESTED_STATEwhen the mic or camera is already in that state, andERROR_OPERATION_IN_PROGRESSwhile 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()andresume()now throwERROR_ACTION_NOT_SUPPORTEDfor 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 onERROR_ALREADY_IN_REQUESTED_STATEtoo, as above, or decide fromroom.isMicEnabled/room.isCamEnabled, which follow the SDK's own state. - If
enableCam(customCameraTrack:)orunmuteMic(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 toonErrorasERROR_CUSTOM_VIDEO_TRACK_ENDEDorERROR_CUSTOM_AUDIO_TRACK_ENDED, and the device turns on without it. AgentParticipantfollows the same changes asParticipant.- PubSub and RealtimeStore methods were already
async throws, but every failure is now aVideoSDKError.PubSubError,RealtimeStoreErrorandAgentParticipantErrorare removed, so acatch let error as PubSubErrorno longer compiles. CatchVideoSDKErrorand readcode,nameandmessage. Subscribing the same listener to a topic twice now throwsERROR_INVALID_PARAMETER, and unsubscribing a listener that is not subscribed does nothing.VideoSDKErroris not aLocalizedError, so useerror.messageinstead oferror.localizedDescription. - Avoid
try?on calls whose failures the user should see. Most of these errors no longer reachonError(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:
captureImagethrowsERROR_WEBCAM_NOT_ENABLEDwhen the participant's camera is off andERROR_CAPTURE_IMAGE_FAILEDwhen the capture fails.captureImagenow takeswidth:beforeheight:, the reverse of 3.2.1.captureImagenow also works for remote participants whose camera is on, and takes an optionalflash: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
nildrops the frame. 3.2.1 sent the original frame instead, so return the input buffer, notnil, when your processor has nothing to change. VideoSDKBackgroundProcessorreturnsniluntil 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.bufferis now an enum (.cvPixelBufferor.i420), and the SDK always passes.cvPixelBufferholding a BGRA buffer.rotationis always._0, andtimestampNs(spelledtimeStampNsin 3.2.1) is the time of the call, not the capture time. Aswitchonframe.bufferneeds@unknown default:(see section 1).- You still attach the processor with
room?.setVideoProcessor(processor:). To remove it, callroom?.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
onErroras well as being thrown, so make sure you don't show them twice:ParticipantandRTCStreammethods, such asenableMic(),pin(_:),remove(),setQuality(_:),pause()andresume(), called before joining, while reconnecting or after leaving.changeMode(_:),uploadBase64FileandfetchBase64Filecalled before joining, while reconnecting or after leaving.- An unknown device ID in
changeCam(deviceId:)orchangeMic(deviceId:). ERROR_CAPTURE_IMAGE_FAILEDfromcaptureImage, and mostsend(_:reliable:)failures.
- An action called after you have left the room now throws
ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED. 3.2.1 reportedERROR_ALREADY_IN_REQUESTED_STATEfor that case.leave()itself still usesERROR_ALREADY_IN_REQUESTED_STATE(see section 7). muteMic()anddisableCam()called while the room is reconnecting now throwERROR_ROOM_RECONNECTINGand 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 onceonRoomStateChanged(roomState:)reportsCONNECTED.- Failures to open the camera or microphone still arrive only in
onError(error:), even when your call started the device.enableCam()andunmuteMic()return before the device opens, andjoin()returns before themicEnabled/camEnableddevices open, so a denied permission or a device in use is never thrown. Errors the SDK raises on its own also still arrive inonError(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()throwsERROR_OPERATION_IN_PROGRESSwhile a join is running, orERROR_ALREADY_IN_REQUESTED_STATEonce you are in the room. - A failed join sets
roomStatetoFAILED(you getonRoomStateChanged(roomState:)) and throws. leave()throwsERROR_ALREADY_IN_REQUESTED_STATEafter the room has ended, andERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINEDbefore you have joined.- If
leave()cancels a join, the pendingjoin()returns without throwing. TreatonRoomJoined(), not the return ofjoin(), as the sign that you are in the room. join()returns before the mic and camera frommicEnabled/camEnabledstart. Wait foronStreamEnabledbefore 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.
isScreenSharingistruefrom the moment the picker appears until the share ends. disableScreenShare()throwsERROR_ALREADY_IN_REQUESTED_STATEwhen sharing is already off.enableScreenShare()throwsERROR_ALREADY_IN_REQUESTED_STATEwhile your broadcast is running, andERROR_OPERATION_IN_PROGRESSwhile 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
| Constant | 3.2.1 | 4.0.0 |
|---|---|---|
ERROR_VIDEO_PRODUCE_CODEC_NOT_SUPPORTED | 3008 | 3045 |
ERROR_MULTISTREAM_NOT_SUPPORTED | 3009 | 3046 |
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:
| Failure | 3.2.1 | 4.0.0 |
|---|---|---|
| Camera access refused at the prompt | ERROR_GET_VIDEO_MEDIA_PERMISSION_DENIED (3014) | ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED (3017) |
| Camera access denied in Settings | ERROR_GET_VIDEO_MEDIA_PERMISSION_DENIED (3014) | ERROR_CAMERA_PERMISSION_DENIED_BY_OS (3019) |
| Microphone access refused at the prompt | ERROR_GET_AUDIO_MEDIA_PERMISSION_DENIED (3015) | ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED (3018) |
| Microphone access denied in Settings | ERROR_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:
| Code | 3.2.1 | 4.0.0 |
|---|---|---|
| 3035 | ERROR_ACTION_PERFORMED_BEFORE_MEETING_JOINED | ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED |
| 3065 | ERROR_MEETING_RECONNECTING | ERROR_ROOM_RECONNECTING |
Same number, new meaning: none. 3008 and 3009 are no longer used.
Removed codes: none.
Key points:
- The default
messagechanged for 3011 to 3016, 3035 andERROR_MULTISTREAM_NOT_SUPPORTED, and many errors now carry a message written for the call that failed. Compare codes or constants, nevermessagetext. - A thrown
VideoSDKErrorcan 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
| Code | Constant |
|---|---|
| 3017 | ERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED |
| 3018 | ERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED |
| 3019 | ERROR_CAMERA_PERMISSION_DENIED_BY_OS |
| 3020 | ERROR_MICROPHONE_PERMISSION_DENIED_BY_OS |
| 3021 | ERROR_CAMERA_NOT_FOUND |
| 3022 | ERROR_MICROPHONE_NOT_FOUND |
| 3023 | ERROR_CAMERA_IN_USE |
| 3024 | ERROR_MICROPHONE_IN_USE |
| 3025 | ERROR_CAMERA_PERMISSION_OR_AUTOPLAY_ISSUE |
| 3026 | ERROR_VIDEO_SOURCE_INITIATION_FAILED |
| 3027 | ERROR_WEBCAM_TRACK_ENDED |
| 3028 | ERROR_MICROPHONE_TRACK_ENDED |
| 3029 | ERROR_INVALID_CUSTOM_VIDEO_TRACK |
| 3030 | ERROR_INVALID_CUSTOM_AUDIO_TRACK |
| 3031 | ERROR_CUSTOM_VIDEO_TRACK_ENDED |
| 3032 | ERROR_CUSTOM_AUDIO_TRACK_ENDED |
| 3033 | ERROR_CAMERA_ACCESS_UNAVAILABLE |
| 3034 | ERROR_MICROPHONE_ACCESS_UNAVAILABLE |
| 3036 | ERROR_RN_CAMERA_ACCESS_DENIED_OR_DISMISSED |
| 3037 | ERROR_RN_CAMERA_NOT_FOUND |
| 3038 | ERROR_RN_MIC_ACCESS_DENIED_OR_DISMISSED |
| 3039 | ERROR_RN_MIC_NOT_FOUND |
| 3040 | ERROR_RN_CAMERA_ACCESS_UNAVAILABLE |
| 3041 | ERROR_RN_MIC_ACCESS_UNAVAILABLE |
| 3042 | ERROR_RN_CAMERA_TRACK_ENDED |
| 3043 | ERROR_RN_MIC_TRACK_ENDED |
| 3044 | ERROR_ROOM_MEDIA_CONNECTION_FAILED |
| 3048 | ERROR_MEDIA_NOT_READY |
| 3049 | ERROR_WEBCAM_PRODUCE_FAILED |
| 3050 | ERROR_MEDIA_DEVICE_NOT_INITIALIZED |
| 3051 | ERROR_NO_WEBCAM_DEVICE_FOUND |
| 3052 | ERROR_NO_SUPPORTED_VIDEO_CODEC |
| 3053 | ERROR_VIDEO_TRACK_CREATION_FAILED |
| 3055 | ERROR_OPERATION_IN_PROGRESS |
| 3056 | ERROR_OPERATION_ABORTED |
| 3057 | ERROR_END_ROOM_FAILED |
| 3058 | ERROR_MICROPHONE_PRODUCE_FAILED |
| 3059 | ERROR_NO_MICROPHONE_DEVICE_FOUND |
| 3060 | ERROR_AUDIO_TRACK_CREATION_FAILED |
| 3061 | ERROR_SCREENSHARE_PRODUCE_FAILED |
| 3062 | ERROR_WEBCAM_NOT_ENABLED |
| 3063 | ERROR_SET_WEBCAM_QUALITY_FAILED |
| 3064 | ERROR_MEDIA_DEVICE_NOT_LOADED |
| 3066 | ERROR_ACTION_NOT_SUPPORTED_IN_MODE |
| 3067 | ERROR_INVALID_MODE |
| 3068 | ERROR_ACTION_NOT_SUPPORTED |
| 3069 | ERROR_CAPTURE_IMAGE_FAILED |
| 3070 | ERROR_SEND_FAILED |
| 3071 | ERROR_INVALID_CUSTOM_TRACK_CONFIG |
| 3072 | ERROR_PAYLOAD_TOO_LARGE |
| 3073 | ERROR_INVALID_PARAMETER |
| 3074 | ERROR_FILE_UPLOAD_FAILED |
| 3075 | ERROR_FILE_FETCH_FAILED |
| 3076 | ERROR_WEBCAM_CLOSE_PRODUCER_FAILED |
| 3077 | ERROR_MICROPHONE_CLOSE_PRODUCER_FAILED |
| 3078 | ERROR_SCREENSHARE_CLOSE_PRODUCER_FAILED |
| 3079 | ERROR_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
deviceIdof a camera fromVideoSDK.getCameras(), as inchangeCam(deviceId: camera.deviceId).getCameras()now returnsCameraDeviceInfovalues, not names, anddeviceIdis the camera'sAVCaptureDevice.uniqueID. A camera name you stored from 3.2.1 no longer matches. An unknown ID throwsERROR_NO_WEBCAM_DEVICE_FOUND. - If the camera is off,
changeCamonly selects the camera. The nextenableCam()uses it. - With the camera on,
changeCamturns it off and on again with the new camera, so you getonStreamDisabledand thenonStreamEnabledfor your webcam stream, and other participants see your video stop and start.changeMicdoes the same while the mic is on. 3.2.1'sswitchWebcam()andchangeMic(selectedDevice:)switched without these events. changeMic(selectedDevice:)is nowchangeMic(deviceId:)and works the same way: pass thedeviceIdof a device fromVideoSDK.getMics().nilpicks the next microphone, an unknown ID throwsERROR_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:)orchangeMic(customMicrophoneTrack:). Both take aCustomStreamTrack.
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
)
| Removed | Use instead |
|---|---|
Mode.CONFERENCE | Mode.SEND_AND_RECV |
Mode.VIEWER | Mode.SIGNALLING_ONLY. RECV_ONLY is a different mode: it receives media, and VIEWER received none. |
PreferredCodec | VideoCodec, with the same cases and raw values |
AuthorisationDelegate | Nothing. Remove the conformance and keep handlePermission() as your own method. |
RTCCVPixelBuffer and the rest of the WebRTC framework | The RTC* types in VideoSDKRTC (section 1) and onFrameReceived(pixelBuffer:) (section 5) |
Key points:
Mode(rawValue: "CONFERENCE")andMode(rawValue: "VIEWER")now returnnil.
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.1 | 4.0.0 |
|---|---|
Meeting | Room |
meetingState, MeetingState | roomState, RoomState |
meetingMode | roomMode |
pubsub | pubSub |
activePresenterId | getActivePresenterId() |
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, mode | Read-only. The SDK updates them. |
Events
| 3.2.1 | 4.0.0 |
|---|---|
MeetingEventListener | RoomEventListener |
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: String | TranscriptionType?: .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.1 | 4.0.0 |
|---|---|
MediaStream | RTCStream |
Participant.enableWebcam(), disableWebcam() | enableCam(), disableCam() |
Participant.isAgent, AgentParticipant.agentId, agentState | isAgent(), getAgentId(), getAgentState() |
captureImage(height:width:) | captureImage(width:height:flash:) (section 4) |
CustomRTCMediaStream | CustomStreamTrack. Its videoCodec is now codec, and facingMode is a FacingMode?. |
CustomVideoTrackConfig, CustomAudioTrackConfig | CustomCameraTrackConfig, 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(_:) throws | VideoSDK.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.downstreamUrl | Removed. Use playbackHlsUrl or livestreamUrl, which are now String?. |
Enums and config types
| 3.2.1 | 4.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 required | Every 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 |
CameraPosition | Removed. Use FacingMode. |
setSharedKey(_:) with an empty key | Throws ERROR_INVALID_PARAMETER |
Also new in 4.0.0
| API | What it does |
|---|---|
Room.getActiveSpeakerId() | The ID of the participant speaking now, or nil |
Room.isMicEnabled, Room.isCamEnabled | Whether 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:
createCameraVideoTrackandcreateMicrophoneAudioTracknow throwVideoSDKErrorvalues, such asERROR_CAMERA_ACCESS_DENIED_OR_DISMISSED,ERROR_CAMERA_NOT_FOUND,ERROR_CAMERA_IN_USE,ERROR_INVALID_CUSTOM_TRACK_CONFIGand the microphone equivalents. They never ask for permission: until access is granted they throwERROR_CAMERA_ACCESS_DENIED_OR_DISMISSEDorERROR_MICROPHONE_ACCESS_DENIED_OR_DISMISSED.VideoSDK.getVideoPermission()andVideoSDK.getAudioPermission()return before the user answers, so create the track only after access is granted, for example onceawait AVCaptureDevice.requestAccess(for: .video)returnstrue.- Both are
async throws, so call them withtry 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 itsonEndedhandler receives the error.
15. Other behaviour changes
| What | 3.2.1 | 4.0.0, and what to do |
|---|---|---|
initMeeting(secureKey:) | Accepted, but not used for encryption | Removed. To turn on end-to-end encryption, set a key with BaseKeyProvider.setSharedKey(_:) and pass the provider to VideoSDK.setKeyProvider(_:) |
HLSUrl.downstreamUrl | Held the stream's downstream URL | Removed. Use playbackHlsUrl or livestreamUrl |
VideoSDK.setLogLevel(level:) | No SDK logs until you called it | The level is .INFO from the start |
New in 4.0.0
| Feature | API |
|---|---|
| Pre-call device and network test | VideoSDK.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 translation | startTranslation(config:), stopTranslation(), changeTranslationLanguage(_:), translationState, onTranslationStateChanged(state:), onTranslationText(_:), onTranslationLanguageChanged(participantId:language:), createRoom(translationLanguage:speakingLanguage:) |
| Screen-share quality and control | enableScreenShare(_:) 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 receive | createRoom(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 controls | createRoom(maxResolution:) with MaxResolution, createRoom(dynaCast:), Room.setWebcamQuality(_:), Participant.setScreenShareQuality(_:), Participant.setViewPort(width:height:) |
| Default camera | createRoom(defaultCamera:) with a FacingMode |
| Custom tracks | cameraId and microphoneId parameters, CustomStreamTrack.onEnded, isEnded, stop(), Room.changeCam(customCameraTrack:), Room.changeMic(customMicrophoneTrack:) |
| Codec information | Room.getAppliedVideoConfig() |
| More stats | Participant.getTransportStats(), Participant.getShareAudioStats() |
| Photo flash | captureImage(width:height:flash:) with CaptureFlash (.OFF, .ON), VideoSDK.setScreenFlash(_:), WhiteOverlayScreenFlash |
| Per-participant E2EE keys | VideoSDK.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 helpers | RTCStream.createRenderView(), videoTracks, audioTracks, participantId, RTCVideoTrack.isShare |
| Error code property | VideoSDKError.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

