Audio and video call on iOS

Updated at:

This guide shows how to integrate the ARTC SDK into your iOS project to build a simple real-time audio and video app for interactive live streaming and video calls.

Key concepts

Before you begin, it helps to understand the following key concepts:

  • ARTC SDK: Alibaba Cloud's SDK for quickly implementing real-time audio and video interactions.

  • GRTN: Alibaba Cloud's Global Realtime Transport Network, providing ultra-low latency, high-quality, secure, and reliable audio and video communication services.

  • channel: A virtual room for real-time audio and video interactions.

  • host: A role that allows a user to publish audio and video streams in a channel and subscribe to streams published by other hosts.

  • viewer: A role that enables a user to subscribe to audio and video streams in a channel but not publish streams.

image
  1. Call setChannelProfile to set the channel scenario, and then call joinChannel to join a channel:

    • In a video call scenario, all users have the host role and can publish and subscribe to streams.

    • In an interactive streaming scenario, you must call setClientRole to set the user role. Set the role to host for users who will publish a stream. If a user only needs to subscribe to a stream, set their role to viewer.

  2. After joining a channel, a user's role determines whether they can publish or subscribe to streams:

    • All users in a channel can subscribe to its audio and video streams.

    • A host can publish audio and video streams in the channel.

    • If a viewer needs to publish a stream, they must call the setClientRole method to switch their role to host.

Sample project

The Alibaba Cloud ARTC SDK includes an open-source sample project for real-time audio and video interaction. You can download it or view the sample code.

Prerequisites

Before you run the sample project, make sure that your development environment meets the following requirements:

  • Development tool: Xcode 14.0 or later. Use the latest official version.

  • Recommended configuration: CocoaPods 1.9.3 or later.

  • Test device: A device that runs iOS 9.0 or later.

Note

Use a physical device for testing. Some features may not be available on simulators.

  • Network environment: A stable network connection.

  • Application preparation: You will need the AppID and AppKey for your ApsaraVideo Real-time Communication application. For more information, see Create an application.

Create a project (optional)

This section guides you through creating a project and adding the required permissions for audio and video interaction. Skip this section if you already have a project.

  1. Open Xcode, go to File > New > Project, and select the App template. On the next screen, set Interface to Storyboard and Language to Swift.

  1. Modify the project settings as needed, including Bundle Identifier, Signing, and Minimum Deployments.

Configure your project

Step 1: Import the SDK

CocoaPods

  1. Open a terminal and install CocoaPods on your development machine. If CocoaPods is already installed, skip this step.

sudo gem install cocoapods
  1. Open a terminal, navigate to your project root directory, and run the following command to create a Podfile.

pod init
  1. Open the generated Podfile and add the ARTC SDK dependency. Replace ${latest version} in the code below with the specific version you want to install.

target 'MyApp' do
  use_frameworks!
  # Replace ${latest version} with a specific version number.
  pod 'AliVCSDK_ARTC', '~> ${latest version}'
end
  1. In the terminal, run the following command to install the CocoaPods dependencies for your project.

pod install
  1. When the command finishes, an .xcworkspace file is created in your project folder. From now on, always open your project using this file to ensure the dependencies are loaded.

    image

Manual

  1. Download the latest ARTC SDK package from SDK Download and unzip it.

  2. Copy the framework files from the unzipped SDK package to your project directory.

  3. Open your project in Xcode. Choose File -> Add Files to "xxx" to add the SDK library files to your project.

    image

  4. Select your target and set the embed option for the imported frameworks to "Embed & Sign".

image

Step 2: Set permissions

  • Add the required permissions for microphone and camera access.

In your Info.plist file, add the permission keys for camera and microphone access: Privacy - Camera Usage Description and Privacy - Microphone Usage Description.

image.png

  • Enable background audio mode (optional).

As shown, select Audio, AirPlay, and Picture in Picture.

image.png

Step 3: Create a user interface

Create a user interface for your real-time interaction scenario. For example, in a multi-person video call, create a ScrollView. When a user joins the call, add a video view to this container. When a user leaves, remove their video view and refresh the layout.

Code example

class VideoCallMainVC: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Do any additional setup after loading the view.
        self.title = self.channelId
        
        self.setup()
        self.startPreview()
        self.joinChannel()
    }
    
    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        
        self.leaveAnddestroyEngine()
    }
    
    @IBOutlet weak var contentScrollView: UIScrollView!
    var videoViewList: [VideoView] = []

    // Create a video call render view and add it to contentScrollView.
    func createVideoView(uid: String) -> VideoView {
        let view = VideoView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        view.uidLabel.text = uid
        
        self.contentScrollView.addSubview(view)
        self.videoViewList.append(view)
        self.updateVideoViewsLayout()
        return view
    }

    // Remove a video call render view from contentScrollView.
    func removeVideoView(uid: String) {
        let videoView = self.videoViewList.first { $0.uidLabel.text == uid }
        if let videoView = videoView {
            videoView.removeFromSuperview()
            self.videoViewList.removeAll(where: { $0 == videoView})
            self.updateVideoViewsLayout()
        }
    }
    // Refresh the layout of the subviews in contentScrollView.
    func updateVideoViewsLayout() {
        let margin = 24.0
        let width = (self.contentScrollView.bounds.width - margin * 3.0) / 2.0
        let height = width // width * 16.0 / 9.0
        let count = 2
        for i in 0..<self.videoViewList.count {
            let view = self.videoViewList[i]
            let x = Double(i % count) * (width + margin) + margin
            let y = Double(i / count) * (height + margin) + margin
            view.frame = CGRect(x: x, y: y, width: width, height: height)
        }
        self.contentScrollView.contentSize = CGSize(width: self.contentScrollView.bounds.width, height: margin + Double(self.videoViewList.count / count + 1) * height + margin)
    }
}

Implementation steps

This section explains how to use the ARTC SDK to build a basic real-time audio and video application. You can copy the code sample to test the features quickly, then follow the steps to understand the core API calls.

The following diagram shows the basic workflow for a real-time audio and video call:

image

The following code sample implements a basic video call:

Basic workflow code sample

class VideoCallMainVC: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        // Sets up the view, starts the local preview, and joins the channel.
        self.title = self.channelId

        self.setup()
        self.startPreview()
        self.joinChannel()
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)

        self.leaveAnddestroyEngine()
    }

    @IBOutlet weak var contentScrollView: UIScrollView!
    var videoViewList: [VideoView] = []

    var channelId: String = ""
    var userId: String = ""

    var rtcEngine: AliRtcEngine? = nil

    var joinToken: String? = nil

    func setup() {

        // Create and initialize the engine.
        let engine = AliRtcEngine.sharedInstance(self, extras:nil)

        // Set the log level.
        engine.setLogLevel(.info)

        // Set the channel profile to interactive mode. In RTC scenarios, always use AliRtcChannelProfile.interactivelive.
        engine.setChannelProfile(AliRtcChannelProfile.interactivelive)
        // Set the user role. The 'host' role (`roleInteractive`) can publish and subscribe. The 'viewer' role (`live`) can only subscribe.
        engine.setClientRole(AliRtcClientRole.roleInteractive)

        // Set the audio profile. The default is high-quality mode (AliRtcAudioProfile.engineHighQualityMode) and music scenario (AliRtcAudioScenario.sceneMusicMode).
        engine.setAudioProfile(AliRtcAudioProfile.engineHighQualityMode, audio_scene: AliRtcAudioScenario.sceneMusicMode)

        // Set the video encoder configuration.
        let config = AliRtcVideoEncoderConfiguration()
        config.dimensions = CGSize(width: 720, height: 1280)
        config.frameRate = 20
        config.bitrate = 1200
        config.keyFrameInterval = 2000
        config.orientationMode = AliRtcVideoEncoderOrientationMode.adaptive
        engine.setVideoEncoderConfiguration(config)
        engine.setCapturePipelineScaleMode(.post)

        // The SDK publishes the video stream by default, so calling publishLocalVideoStream(true) is optional.
        engine.publishLocalVideoStream(true)
        // The SDK publishes the audio stream by default. For a video call, calling publishLocalAudioStream(true) is optional.
        // For an audio-only call, you must call publishLocalVideoStream(false) to disable video publishing.
        engine.publishLocalAudioStream(true)

        // Set the default to subscribe to all remote audio and video streams.
        engine.setDefaultSubscribeAllRemoteAudioStreams(true)
        engine.subscribeAllRemoteAudioStreams(true)
        engine.setDefaultSubscribeAllRemoteVideoStreams(true)
        engine.subscribeAllRemoteVideoStreams(true)

        self.rtcEngine = engine
    }

    func joinChannel() {

        // Join the channel with a single-parameter token.
        if let joinToken = self.joinToken {
            let msg =  "JoinWithToken: \(joinToken)"

            let param = AliRtcChannelParam()
            let ret = self.rtcEngine?.joinChannel(joinToken, channelParam: param) { [weak self] errCode, channelId, userId, elapsed in
                                                                                   if errCode == 0 {
                                                                                       // success

                                                                                   }
                                                                                   else {
                                                                                       // failed
                                                                                   }

                                                                                   let resultMsg = "\(msg) \n CallbackErrorCode: \(errCode)"
                                                                                   resultMsg.printLog()
                                                                                   UIAlertController.showAlertWithMainThread(msg: resultMsg, vc: self!)
            }
            
            let resultMsg = "\(msg) \n ReturnErrorCode: \(ret ?? 0)"
            resultMsg.printLog()
            if ret != 0 {
                UIAlertController.showAlertWithMainThread(msg: resultMsg, vc: self)
            }
            return
        }
    }
    
    func startPreview() {
        let videoView = self.createVideoView(uid: self.userId)
        
        let canvas = AliVideoCanvas()
        canvas.view = videoView.canvasView
        canvas.renderMode = .auto
        canvas.mirrorMode = .onlyFrontCameraPreviewEnabled
        canvas.rotationMode = ._0
        
        self.rtcEngine?.setLocalViewConfig(canvas, for: AliRtcVideoTrack.camera)
        self.rtcEngine?.startPreview()
    }
    
    func leaveAnddestroyEngine() {
        self.rtcEngine?.stopPreview()
        self.rtcEngine?.leaveChannel()
        AliRtcEngine.destroy()
        self.rtcEngine = nil
    }
    
    // Creates a render view for a user and adds it to the scroll view.
    func createVideoView(uid: String) -> VideoView {
        let view = VideoView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
        view.uidLabel.text = uid
        
        self.contentScrollView.addSubview(view)
        self.videoViewList.append(view)
        self.updateVideoViewsLayout()
        return view
    }
    
    // Removes a video call render view from contentScrollView.
    func removeVideoView(uid: String) {
        let videoView = self.videoViewList.first { $0.uidLabel.text == uid }
        if let videoView = videoView {
            videoView.removeFromSuperview()
            self.videoViewList.removeAll(where: { $0 == videoView})
            self.updateVideoViewsLayout()
        }
    }
    
    // Refreshes the layout of subviews in contentScrollView.
    func updateVideoViewsLayout() {
        let margin = 24.0
        let width = (self.contentScrollView.bounds.width - margin * 3.0) / 2.0
        let height = width // width * 16.0 / 9.0
        let count = 2
        for i in 0..<self.videoViewList.count {
            let view = self.videoViewList[i]
            let x = Double(i % count) * (width + margin) + margin
            let y = Double(i / count) * (height + margin) + margin
            view.frame = CGRect(x: x, y: y, width: width, height: height)
        }
        self.contentScrollView.contentSize = CGSize(width: self.contentScrollView.bounds.width, height: margin + Double(self.videoViewList.count / count + 1) * height + margin)
    }
    
    /*
    // MARK: - Navigation

    // In a storyboard-based application, you will often want to do a little preparation before navigation
    override func prepare(for segue: UIStoryboardSegue, sender: Any?) {
        // Get the new view controller using segue.destination.
        // Pass the selected object to the new view controller.
    }
    */

}

extension VideoCallMainVC: AliRtcEngineDelegate {
    
    func onJoinChannelResult(_ result: Int32, channel: String, elapsed: Int32) {
        "onJoinChannelResult1 result: \(result)".printLog()
    }
    
    func onJoinChannelResult(_ result: Int32, channel: String, userId: String, elapsed: Int32) {
        "onJoinChannelResult2 result: \(result)".printLog()
    }
    
    func onRemoteUser(onLineNotify uid: String, elapsed: Int32) {
        // A remote user comes online.
        "onRemoteUserOlineNotify uid: \(uid)".printLog()
    }
    
    func onRemoteUserOffLineNotify(_ uid: String, offlineReason reason: AliRtcUserOfflineReason) {
        // A remote user goes offline.
        "onRemoteUserOffLineNotify uid: \(uid) reason: \(reason)".printLog()
    }
    
    
    func onRemoteTrackAvailableNotify(_ uid: String, audioTrack: AliRtcAudioTrack, videoTrack: AliRtcVideoTrack) {
        "onRemoteTrackAvailableNotify uid: \(uid) audioTrack: \(audioTrack)  videoTrack: \(videoTrack)".printLog()
        // The stream status of a remote user.
        if audioTrack != .no {
            let videoView = self.videoViewList.first { $0.uidLabel.text == uid }
            if videoView == nil {
                _ = self.createVideoView(uid: uid)
            }
        }
        if videoTrack != .no {
            var videoView = self.videoViewList.first { $0.uidLabel.text == uid }
            if videoView == nil {
                videoView = self.createVideoView(uid: uid)
            }
            
            let canvas = AliVideoCanvas()
            canvas.view = videoView!.canvasView
            canvas.renderMode = .auto
            canvas.mirrorMode = .onlyFrontCameraPreviewEnabled
            canvas.rotationMode = ._0
            self.rtcEngine?.setRemoteViewConfig(canvas, uid: uid, for: AliRtcVideoTrack.camera)
        }
        else {
            self.rtcEngine?.setRemoteViewConfig(nil, uid: uid, for: AliRtcVideoTrack.camera)
        }
        
        if audioTrack == .no && videoTrack == .no {
            self.removeVideoView(uid: uid)
            self.rtcEngine?.setRemoteViewConfig(nil, uid: uid, for: AliRtcVideoTrack.camera)
        }
    }
    
    func onAuthInfoWillExpire() {
        "onAuthInfoWillExpire".printLog()
        
        /* TODO: Handle this callback. The token is about to expire. Your app must fetch a new token for the current channel and user, then call refreshAuthInfo. */
    }
    
    func onBye(_ code: Int32) {
        "onBye code: \(code)".printLog()
        
        /* TODO: Handle this callback. It is triggered if another device logs in with the same UserID, which kicks the current device out of the channel. */
    }
    
    func onLocalDeviceException(_ deviceType: AliRtcLocalDeviceType, exceptionType: AliRtcLocalDeviceExceptionType, message msg: String?) {
        "onLocalDeviceException deviceType: \(deviceType)  exceptionType: \(exceptionType)".printLog()

        /* TODO: Handle this callback. Notify the user of a device error. This callback is triggered only after the SDK's internal recovery strategies fail. */
    }
    
    func onConnectionStatusChange(_ status: AliRtcConnectionStatus, reason: AliRtcConnectionStatusChangeReason) {
        "onConnectionStatusChange status: \(status)  reason: \(reason)".printLog()

        if status == .failed {
            /* TODO: Handle this callback. Notify the user of the connection failure. This callback is triggered only after the SDK's internal recovery strategies fail. */
        }
        else {
            /* TODO: Optional. Add business logic here, typically for data analytics or UI updates. */
        }
    }
}

For details on the complete sample code and how to run it, see Run the ARTC demo for iOS.

Step 1: Request permissions

The SDK checks for app permissions when a call starts, but for a smooth user experience, check for camera and microphone permissions before initiating the call.

func checkMicrophonePermission(completion: @escaping (Bool) -> Void) {
    let status = AVCaptureDevice.authorizationStatus(for: .audio)
    
    switch status {
    case .notDetermined:
        AVCaptureDevice.requestAccess(for: .audio) { granted in
            completion(granted)
        }
    case .authorized:
        completion(true)
    default:
        completion(false)
    }
}

func checkCameraPermission(completion: @escaping (Bool) -> Void) {
    let status = AVCaptureDevice.authorizationStatus(for: .video)
    
    switch status {
    case .notDetermined:
        AVCaptureDevice.requestAccess(for: .video) { granted in
            completion(granted)
        }
    case .authorized:
        completion(true)
    default:
        completion(false)
    }
}

// Example usage
checkMicrophonePermission { granted in
    if granted {
        print("Microphone access granted.")
    } else {
        print("Microphone access denied.")
    }
}

checkCameraPermission { granted in
    if granted {
        print("Camera access granted.")
    } else {
        print("Camera access denied.")
    }
}

Step 2: Get an authentication token

Joining an ARTC channel requires an authentication token to verify the user's identity. For the token generation rules, see Token Authentication. A token can be generated by using a single-parameter or multi-parameter method, and you must call a different joinChannel API method in the SDK based on the method you use.

In a production environment:

Generating a token requires your AppKey. Hardcoding the AppKey on the client is a security risk. We strongly recommend generating the token on your app server and then sending it to the client.

During development and debugging:

If your app server lacks token generation logic, you can temporarily generate a token on the client.

class ARTCTokenHelper: NSObject {

    /**
    * RTC AppId
    */
    public static let AppId = "<RTC AppId>"

    /**
    * RTC AppKey
    */
    public static let AppKey = "<RTC AppKey>"

    /**
    * Generate a multi-parameter token for joining a channel based on channelId, userId, and timestamp.
    */
    public func generateAuthInfoToken(appId: String = ARTCTokenHelper.AppId, appKey: String =  ARTCTokenHelper.AppKey, channelId: String, userId: String, timestamp: Int64) -> String {
        let stringBuilder = appId + appKey + channelId + userId + "\(timestamp)"
        let token = ARTCTokenHelper.GetSHA256(stringBuilder)
        return token
    }

    /**
    * Generate a single-parameter token for joining a channel based on channelId, userId, and nonce.
    */
    public func generateJoinToken(appId: String = ARTCTokenHelper.AppId, appKey: String =  ARTCTokenHelper.AppKey, channelId: String, userId: String, timestamp: Int64, nonce: String = "") -> String {
        let token = self.generateAuthInfoToken(appId: appId, appKey: appKey, channelId: channelId, userId: userId, timestamp: timestamp)

        let tokenJson: [String: Any] = [
            "appid": appId,
            "channelid": channelId,
            "userid": userId,
            "nonce": nonce,
            "timestamp": timestamp,
            "token": token
        ]

        if let jsonData = try? JSONSerialization.data(withJSONObject: tokenJson, options: []),
        let base64Token = jsonData.base64EncodedString() as String? {
            return base64Token
        }

        return ""
    }

    /**
    * Sign a string using SHA256.
    */
    private static func GetSHA256(_ input: String) -> String {
        // Convert the input string to data.
        let data = Data(input.utf8)

        // Create a buffer to store the hash result.
        var hash = [UInt8](repeating: 0, count: Int(CC_SHA256_DIGEST_LENGTH))

        // Calculate the SHA-256 hash.
        data.withUnsafeBytes {
            _ = CC_SHA256($0.baseAddress, CC_LONG(data.count), &hash)
        }

        // Convert the hash to a hexadecimal string.
        return hash.map { String(format: "%02hhx", $0) }.joined()
    }

}

Step 3: Import the ARTC SDK

// Import the ARTC module.
import AliVCSDK_ARTC

Step 4: Create and initialize the engine

  • Create the RTC engine

    Call the sharedInstance method to create an AliRtcEngine instance.

    private var rtcEngine: AliRtcEngine? = nil
    
    // Create the engine and set the delegate.
    let engine = AliRtcEngine.sharedInstance(self, extras:nil)
    ...
    self.rtcEngine = engine
  • Initialize the engine

    • Call setChannelProfile to set the channel profile to AliRTCInteractiveLive (interactive mode).

      Choose between interactive mode for entertainment scenarios and communication mode for one-to-one or group calls. Selecting the appropriate mode ensures optimal performance and network efficiency.

      Mode

      Publishing

      Subscribing

      Description

      interactive mode

      1. Limited by role. Only users with the host role can publish streams.

      2. Participants can switch roles at any time during the session.

      No role restrictions. All participants have permission to subscribe to streams.

      1. In interactive mode, when a host joins, leaves, or starts publishing a stream, viewers are notified in real time. However, viewer activities are not sent to the host, which prevents interruptions to the stream.

      2. The host drives the interaction, while the viewer consumes content. If viewers might need to interact, use this mode by default. Its flexibility allows you to change user roles as needed.

      communication mode

      No role restrictions. All participants have permission to publish streams.

      No role restrictions. All participants have permission to subscribe to streams.

      1. In communication mode, all participants are aware of each other's presence in the channel.

      2. This mode does not use roles and is functionally equivalent to all users being a host in interactive mode. This simplifies API calls.

    • Call setClientRole to set the user role to AliRTCSdkInteractive (host) or AliRTCSdkLive (viewer). By default, a host both publishes and subscribes to streams, while a viewer only subscribes. For viewers, local preview and publishing are disabled by default.

      Note: When a user becomes a viewer, the SDK stops local stream publishing but maintains subscriptions. When a user becomes a host, the SDK starts local stream publishing, and subscriptions are unaffected.

      // Set the channel profile to interactive mode. In RTC scenarios, always use AliRtcChannelProfile.interactivelive.
      engine.setChannelProfile(AliRtcChannelProfile.interactivelive)
      // Set the user role. Use AliRtcClientRole.roleInteractive for users who need to both publish and subscribe. Use AliRtcClientRole.live for users who only subscribe.
      engine.setClientRole(AliRtcClientRole.roleInteractive)
  • Implement common callbacks

    The SDK attempts to recover from issues automatically. For unrecoverable errors, it notifies your app via callbacks.

    Your application must handle the following key callbacks for unrecoverable SDK issues:

    Cause

    Callback and parameters

    Solution

    Description

    Authentication failed

    The result parameter in the onJoinChannelResult callback returns AliRtcErrJoinBadToken.

    Your app must verify the token.

    If authentication fails when a user calls an API, the API's callback returns an authentication failure error.

    Token about to expire

    onAuthInfoWillExpire

    When this callback is triggered, get a new token and call refreshAuthInfo to update the authentication information.

    This error can occur during an API call or at runtime and is reported through an API or error callback.

    Token expired

    onAuthInfoExpired

    When this callback is triggered, the user must rejoin the channel.

    This error can occur during an API call or at runtime and is reported through an API or error callback.

    Network connection issue

    The onConnectionStatusChange callback returns AliRtcConnectionStatusFailed.

    The user must rejoin the channel.

    The SDK automatically recovers from brief disconnections. If a disconnection exceeds the timeout threshold, your app should check the network and prompt the user to rejoin the channel.

    Kicked from channel

    onBye

    • AliRtcOnByeUserReplaced: Check if another device has joined with the same userId.

    • AliRtcOnByeBeKickedOut: The user was kicked by an administrator and must rejoin.

    • AliRtcOnByeChannelTerminated: The channel was terminated, and the user must rejoin.

    The RTC service allows an administrator to remove participants from a channel.

    Local device exception

    onLocalDeviceException

    Check app permissions and ensure the hardware is working correctly.

    When a local device exception occurs that the SDK cannot resolve, it notifies the app via a callback. The app should then intervene to check the device status.

    extension VideoCallMainVC: AliRtcEngineDelegate {
    
        func onJoinChannelResult(_ result: Int32, channel: String, elapsed: Int32) {
            "onJoinChannelResult1 result: \(result)".printLog()
        }
    
        func onJoinChannelResult(_ result: Int32, channel: String, userId: String, elapsed: Int32) {
            "onJoinChannelResult2 result: \(result)".printLog()
        }
    
        func onRemoteUser(onLineNotify uid: String, elapsed: Int32) {
            // A remote user comes online.
            "onRemoteUserOlineNotify uid: \(uid)".printLog()
        }
    
        func onRemoteUserOffLineNotify(_ uid: String, offlineReason reason: AliRtcUserOfflineReason) {
            // A remote user goes offline.
            "onRemoteUserOffLineNotify uid: \(uid) reason: \(reason)".printLog()
        }
    
        func onRemoteTrackAvailableNotify(_ uid: String, audioTrack: AliRtcAudioTrack, videoTrack: AliRtcVideoTrack) {
            "onRemoteTrackAvailableNotify uid: \(uid) audioTrack: \(audioTrack)  videoTrack: \(videoTrack)".printLog()
        }
    
        func onAuthInfoWillExpire() {
            "onAuthInfoWillExpire".printLog()
    
            /* TODO: Handle this callback. The token is about to expire. Your app must fetch a new token for the current channel and user, then call refreshAuthInfo. */
        }
    
        func onAuthInfoExpired() {
            "onAuthInfoExpired".printLog()
    
            /* TODO: This must be handled. Notify the user that the token has expired, then leave the channel and destroy the engine. */
        }
    
        func onBye(_ code: Int32) {
            "onBye code: \(code)".printLog()
    
            /* TODO: Handle this callback. It is triggered if another device logs in with the same UserID, which kicks the current device out of the channel. */
        }
    
        func onLocalDeviceException(_ deviceType: AliRtcLocalDeviceType, exceptionType: AliRtcLocalDeviceExceptionType, message msg: String?) {
            "onLocalDeviceException deviceType: \(deviceType)  exceptionType: \(exceptionType)".printLog()
    
            /* TODO: Handle this callback. Notify the user of a device error. This callback is triggered only after the SDK's internal recovery strategies fail. */
        }
    
        func onConnectionStatusChange(_ status: AliRtcConnectionStatus, reason: AliRtcConnectionStatusChangeReason) {
            "onConnectionStatusChange status: \(status)  reason: \(reason)".printLog()
    
            if status == .failed {
                /* TODO: Handle this callback. Notify the user of the connection failure. This callback is triggered only after the SDK's internal recovery strategies fail. */
            }
            else {
                /* TODO: Optional. Add business logic here, typically for data analytics or UI updates. */
            }
        }
    }

Step 5: Set audio and video properties

  • Set audio properties

    Call setAudioProfile to set the audio encoding mode and audio scenario.

    // Set the audio profile. The default is high-quality mode (AliRtcAudioProfile.engineHighQualityMode) and music scenario (AliRtcAudioScenario.sceneMusicMode).
    engine.setAudioProfile(AliRtcAudioProfile.engineHighQualityMode, audio_scene: AliRtcAudioScenario.sceneMusicMode)
  • Set video properties

    You can set properties for the published video stream, such as resolution, bitrate, and frame rate.

    // Set the video encoder configuration.
    let config = AliRtcVideoEncoderConfiguration()
    config.dimensions = CGSize(width: 720, height: 1280)
    config.frameRate = 20
    config.bitrate = 1200
    config.keyFrameInterval = 2000
    config.orientationMode = AliRtcVideoEncoderOrientationMode.adaptive
    engine.setVideoEncoderConfiguration(config)
    engine.setCapturePipelineScaleMode(.post)

Step 6: Set publishing and subscribing properties

Configure local stream publishing and default remote stream subscriptions:

  • Call publishLocalAudioStream to publish the local audio stream.

  • Call publishLocalVideoStream to publish the local video stream. For an audio-only call, set this to false.

// The SDK publishes the video stream by default, so calling publishLocalVideoStream(true) is optional.
engine.publishLocalVideoStream(true)
// The SDK publishes the audio stream by default. For a video call, calling publishLocalAudioStream(true) is optional.
// For an audio-only call, you must call publishLocalVideoStream(false) to disable video publishing.
engine.publishLocalAudioStream(true)

// Set the default to subscribe to all remote audio and video streams.
engine.setDefaultSubscribeAllRemoteAudioStreams(true)
engine.subscribeAllRemoteAudioStreams(true)
engine.setDefaultSubscribeAllRemoteVideoStreams(true)
engine.subscribeAllRemoteVideoStreams(true)
Note

By default, the SDK automatically publishes local streams and subscribes to remote streams. You can call the methods above to disable this automatic behavior.

Step 7: Start local preview

  • Call setLocalViewConfig to set up the local render view and configure local video display properties.

  • Call the startPreview method to start the local video preview.

let videoView = self.createVideoView(uid: self.userId)

let canvas = AliVideoCanvas()
canvas.view = videoView.canvasView
canvas.renderMode = .auto
canvas.mirrorMode = .onlyFrontCameraPreviewEnabled
canvas.rotationMode = ._0

self.rtcEngine?.setLocalViewConfig(canvas, for: AliRtcVideoTrack.camera)
self.rtcEngine?.startPreview()

Step 8: Join a channel

Call joinChannel to join a channel. We recommend that you use the single-parameter method by calling the joinChannel[3/3] interface. After the call, you must check the return value and also get the result from the onJoinChannelResult callback. If the return value is 0 and the result is 0, you have successfully joined the channel. Otherwise, check if the provided token is invalid.

let param = AliRtcChannelParam()
let ret = self.rtcEngine?.joinChannel(joinToken, channelParam: param) { [weak self] errCode, channelId, userId, elapsed in
    if errCode == 0 {
        // success
    }
    else {
        // failed
    }

    let resultMsg = "Join channel callback. ErrorCode: \(errCode)"
    resultMsg.printLog()
    UIAlertController.showAlertWithMainThread(msg: resultMsg, vc: self!)
}

let resultMsg = "Join channel API call. Return code: \(ret ?? 0)"
resultMsg.printLog()
if ret != 0 {
    UIAlertController.showAlertWithMainThread(msg: resultMsg, vc: self)
}
Note
  • After a user joins a channel, the SDK publishes and subscribes to streams according to the parameters set before joining.

  • The SDK automatically publishes and subscribes by default to reduce the number of API calls your client needs to make.

Step 9: Set the remote view

When a remote user starts or stops publishing a stream, the SDK triggers the onRemoteTrackAvailableNotify callback. In this callback, you can set up or remove the remote user's view. The following code shows an example:

func onRemoteTrackAvailableNotify(_ uid: String, audioTrack: AliRtcAudioTrack, videoTrack: AliRtcVideoTrack) {
    "onRemoteTrackAvailableNotify uid: \(uid) audioTrack: \(audioTrack)  videoTrack: \(videoTrack)".printLog()
    // The stream status of a remote user.
    if audioTrack != .no {
        let videoView = self.videoViewList.first { $0.uidLabel.text == uid }
        if videoView == nil {
            _ = self.createVideoView(uid: uid)
        }
    }
    if videoTrack != .no {
        var videoView = self.videoViewList.first { $0.uidLabel.text == uid }
        if videoView == nil {
            videoView = self.createVideoView(uid: uid)
        }
        
        let canvas = AliVideoCanvas()
        canvas.view = videoView!.canvasView
        canvas.renderMode = .auto
        canvas.mirrorMode = .onlyFrontCameraPreviewEnabled
        canvas.rotationMode = ._0
        self.rtcEngine?.setRemoteViewConfig(canvas, uid: uid, for: AliRtcVideoTrack.camera)
    }
    else {
        self.rtcEngine?.setRemoteViewConfig(nil, uid: uid, for: AliRtcVideoTrack.camera)
    }
    
    if audioTrack == .no && videoTrack == .no {
        self.removeVideoView(uid: uid)
        self.rtcEngine?.setRemoteViewConfig(nil, uid: uid, for: AliRtcVideoTrack.camera)
    }
}

Step 10: Leave channel and destroy engine

When the session ends, leave the channel and destroy the engine to release all resources. Follow these steps:

  1. Call stopPreview to stop the local video preview.

  2. Call leaveChannel to leave the channel.

  3. Call destroy to destroy the engine instance and release its resources.

self.rtcEngine?.stopPreview()
self.rtcEngine?.leaveChannel()
AliRtcEngine.destroy()
self.rtcEngine = nil

Step 11: Result

image.pngimage.png

Related documents

data structure

AliRtcEngine API