All Products
Search
Document Center

ApsaraVideo Live:Implement a voice chat room on iOS

Last Updated:Feb 28, 2026

Integrate the ApsaraVideo Real-time Communication (ARTC) SDK into your iOS project to build audio-only interactive applications such as voice calls and voice chat rooms.

Key concepts

TermDefinition
ARTC SDKThe SDK for ApsaraVideo Real-time Communication (ARTC). It adds real-time audio and video capabilities to your application.
ChannelA virtual space where users interact in real time.
StreamerA user who can publish and subscribe to audio and video streams in a channel. Maps to AliRtcClientRole.roleInteractive in the SDK.
ViewerA user who can subscribe to audio and video streams in a channel but cannot publish streams. Maps to AliRtcClientRole.rolelive in the SDK.

How channels, streamers, and viewers interact

All users call joinChannel to join a channel before publishing or subscribing to streams.

ScenarioRolesBehavior
Audio-only callAll users are streamersEach user can publish and subscribe to streams.
Voice chat roomStreamers publish streams; viewers only subscribeCall setClientRole to assign roles.

After joining a channel:

  • All users can receive audio and video streams from other users in the same channel.

  • Only streamers can publish audio and video streams into the channel.

  • To let a viewer publish a stream, call setClientRole to switch the user's role to streamer.

Sample project

Download or view the sample source code from the open-source ARTC SDK sample project.

Prerequisites

Before you begin, make sure you have:

  • Xcode 14.0 or later installed. Use the latest official version.

  • CocoaPods 1.9.3 or later installed.

  • A physical test device running iOS 9.0 or later.

  • A stable network connection.

  • An ARTC AppID and AppKey. For more information, see Create an application.

  • A project configured with the ARTC SDK, including audio and network permissions. For more information, see Implement an audio and video call.

Note

Use a physical device for testing. Simulators may lack certain features.

Implement audio-only interaction

1. Request permissions

Check camera and microphone permissions before starting a call. The SDK checks permissions automatically, but requesting them early ensures a smooth user experience.

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)
    }
}

// Usage example
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.")
    }
}

2. Get an authentication token

Joining an ARTC channel requires an authentication token that verifies the user's identity. A token can be generated using a single-parameter method or a multi-parameter method. The method determines which joinChannel API to call. For details on token generation, see Implement token-based authentication.

Production environments

Generate the token on your server and send it to the client. Hardcoding the AppKey on the client side poses a security risk.

Development and debugging

If your server does not yet generate tokens, temporarily use the token generation logic from the APIExample. The reference code follows:

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.
    * String signing (SHA256)
    */
    private static func GetSHA256(_ input: String) -> String {
        // Convert 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()
    }

}

3. Create and initialize the engine

Create the RTC engine

Call sharedInstance to create an RTC engine object.

private var rtcEngine: AliRtcEngine? = nil

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

Initialize the engine

Configure the channel profile, user role, and audio settings:

MethodPurposeValue
setChannelProfileSet channel profile to interactive live streamingAliRtcChannelProfile.interactivelive
setClientRoleAssign user roleStreamer: AliRtcClientRole.roleInteractive; Viewer: AliRtcClientRole.rolelive
setAudioProfileSet audio quality and scenario modeAliRtcAudioProfile.engineHighQualityMode, AliRtcAudioScenario.sceneMusicMode
// Set the channel profile to interactive live streaming. AliRtcInteractivelive is used for all RTC scenarios.
engine.setChannelProfile(AliRtcChannelProfile.interactivelive)
// Set the role.
if self.isAnchor {
    // For streamer mode, which requires publishing audio and video streams, set the role to AliRtcClientRoleInteractive.
    engine.setClientRole(AliRtcClientRole.roleInteractive)
}
else {
    // For viewer mode, which does not require publishing audio and video streams, set the role to AliRtcClientRolelive.
    engine.setClientRole(AliRtcClientRole.rolelive)
}

// Set the audio profile. By default, high-quality mode (AliRtcEngineHighQualityMode) and music mode (AliRtcSceneMusicMode) are used.
engine.setAudioProfile(AliRtcAudioProfile.engineHighQualityMode, audio_scene: AliRtcAudioScenario.sceneMusicMode)

4. Configure publishing and subscription

By default, the SDK automatically publishes and subscribes to audio and video streams in the channel.

  • After the role is set to viewer, the publishLocalAudioStream method becomes invalid.

  • The following configuration applies to both streamers and viewers.

// Set the audio profile. By default, high-quality mode (AliRtcEngineHighQualityMode) and music mode (AliRtcAudioScenario.sceneMusicMode) are used.
engine.setAudioProfile(AliRtcAudioProfile.engineHighQualityMode, audio_scene: AliRtcAudioScenario.sceneMusicMode)

// For a voice chat scenario, you do not need to publish a video stream.
engine.publishLocalVideoStream(false)

// Set the default subscription to remote audio streams.
engine.setDefaultSubscribeAllRemoteAudioStreams(true)
engine.subscribeAllRemoteAudioStreams(true)

5. Join a channel

Call joinChannel to join a channel and start the audio-only interaction.

Note

If the token was generated using the single-parameter method, call the single-parameter joinChannel[1/3] method. If the token was generated using the multi-parameter method, call the multi-parameter joinChannel[2/3] method. The onJoinChannelResult callback returns the result. A result of 0 indicates success. Any other value indicates a failure -- check whether the token is valid.

self.rtcEngine?.joinChannel(joinToken, channelId: nil, userId: nil, name: nil)

6. End the interaction

Leave the channel and destroy the engine to release resources:

  1. Call leaveChannel to leave the channel.

  2. Call destroy to destroy the engine and release resources.

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

7. (Optional) Switch between viewer and streamer roles

Call setClientRole to switch a viewer to streamer (to publish streams) or a streamer back to viewer (to stop publishing).

// Switch to the streamer role.
self.rtcEngine?.setClientRole(AliRtcClientRole.roleInteractive)

// Switch to the viewer role.
self.rtcEngine?.setClientRole(AliRtcClientRole.rolelive)

Handle engine callbacks

The SDK tries to recover from exceptions automatically. For errors that cannot be resolved internally, the SDK notifies your application through callbacks.

CauseCallback and parametersSolution
Authentication failedonJoinChannelResult returns AliRtcErrJoinBadTokenCheck whether the token is correct.
Token is about to expireonWillAuthInfoExpireGet the latest authentication information, then call refreshAuthInfo.
Token has expiredonAuthInfoExpiredHave the user rejoin the channel.
Network connectivity erroronConnectionStatusChange returns AliRtcConnectionStatusFailedThe SDK can recover from brief network disconnections automatically. If the disconnection exceeds the timeout threshold, check the network status and have the user rejoin the channel.
Kicked offlineonByeAliRtcOnByeUserReplaced: Check if the user ID is duplicated. AliRtcOnByeBeKickedOut: The user was kicked by the service. Rejoin the channel. AliRtcOnByeChannelTerminated: The channel was terminated. Rejoin the channel.
Local device exceptiononLocalDeviceExceptionCheck permissions and device hardware 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: You must handle this callback. The token is about to expire. Your application must get a new token for the current channel and user, and then call refreshAuthInfo. */
    }

    func onAuthInfoExpired() {
        "onAuthInfoExpired".printLog()

        /* TODO: You must handle this callback. Prompt the user that the token has expired, and then leave the channel and release the engine. */
    }

    func onBye(_ code: Int32) {
        "onBye code: \(code)".printLog()

        /* TODO: You must handle this callback. Your business may trigger a scenario where different devices with the same user ID compete for access. */
    }

    func onLocalDeviceException(_ deviceType: AliRtcLocalDeviceType, exceptionType: AliRtcLocalDeviceExceptionType, message msg: String?) {
        "onLocalDeviceException deviceType: \(deviceType)  exceptionType: \(exceptionType)".printLog()

        /* TODO: You must handle this callback. We recommend that your application prompts the user about the device error. This callback is triggered only after the SDK has tried all recovery policies and still cannot continue. */
    }

    func onConnectionStatusChange(_ status: AliRtcConnectionStatus, reason: AliRtcConnectionStatusChangeReason) {
        "onConnectionStatusChange status: \(status)  reason: \(reason)".printLog()

        if status == .failed {
            /* TODO: You must handle this callback. We recommend that your application prompts the user. This callback is triggered only after the SDK has tried all recovery policies and still cannot continue. */
        }
        else {
            /* TODO: Optional. Add business logic, usually for data statistics or UI changes. */
        }
    }
}

Related operations