Audio and video calls on Mac

更新时间:
复制 MD 格式

Learn how to integrate the ARTC SDK into your Mac project to build a simple real-time communication 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

Alibaba Cloud ARTC SDK provides an open source sample project for real-time audio and video interaction. Download or view the sample code.

Prerequisites

  • Development tool: Xcode 14.0 or later. We recommend the latest stable release.

  • Test device: A Mac running macOS 10.13 or later.

  • Network environment: A stable network connection.

  • Application preparation: Obtain the AppID and AppKey for your Real-Time Communication application. For details, see Create an application.

Create a project (optional)

This section explains how to create a project and add the required permissions for real-time audio and video interaction. If you already have a project, skip this section.

  1. Open Xcode and choose File → New → Project. Select an app template and click Next. Set Interface to Storyboard and Language to Objective-C or Swift.

  1. As needed, update the project configuration, including the bundle identifier, code signing, and deployment target.

Configure the project

Step 1: Configure permissions

In the Project Navigator, click the project file and switch to the Info tab. Add permissions for the camera and microphone.

Key

Type

Value

Privacy - Microphone Usage Description

String

Describe the purpose for using the microphone, for example: "Required for voice chat."

Privacy - Camera Usage Description

String

Describe the purpose for using the camera, for example: "Required for video chat."

To publish your app to the App Store, you must enable sandbox mode. Some sandbox settings are required, while others are optional and depend on your app's features.

Navigate to the Signing & Capabilities tab. In the App Sandbox section, the required options are Incoming Connections (Server) and Outgoing Connections (Client) under Network, and Camera and Audio Input under Hardware.

For optional settings, if your app does not access local files, you can set the User Selected File permission to None. If your app includes features like playing background music or pushing local files, set the User Selected File permission to Read/Write.

Step 2: Import the ARTC SDK

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

  2. Copy the files from the SDK package to your project directory. If you do not use AAC for audio encoding, you can skip adding PluginAAC.framework.

  3. Open Xcode and add the dynamic libraries from the SDK. Set the Embed property for these libraries to Embed & Sign.

Step 3: Create the user interface

Create a user interface based on your real-time audio and video interaction scenario. For example, in a multi-user video call scenario, you can create a Window Controller view. Add a Custom View control to the Video Chat Controller's view. This control serves as a container. When a user joins the call, add their call view to this container. When a user leaves, remove their view and refresh the layout.

The Video Chat Controller's view contains two video rendering areas, Local View and Remote View, a Scroll View (with an embedded Text View), and a Leave Channel Button. The Window Controller is linked to the Video Chat Controller's content through a segue.

Procedure

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 shows a basic implementation of an audio and video call:

Basic procedure sample code

@interface VideoChatController ()<AliRtcEngineDelegate> {
    NSString * userId;
    NSString * channelId ;
    NSString * userName ;
}
@property (nonatomic, strong)IBOutlet NSButton *    leaveChannelButton ;
@property (nonatomic, strong)IBOutlet NSView *      localView ;
@property (nonatomic, strong)IBOutlet NSView *      remoteView ;
@property (nonatomic, strong)IBOutlet NSTextView *  statusListBox ;
@end
@implementation VideoChatController
/*  appid and key */
#define ARTC_APP_ID  ""
#define ARTC_APP_KEY ""
- (void)viewDidLoad {
    [super viewDidLoad];
    _engine = [AliRtcEngine sharedInstance:self extras:nil] ;
    // Do view setup here.
    [_statusListBox setEditable:FALSE];
}
- (void)dealloc {
    [self leaveChannelButton:nil];
}
-(IBAction)leaveChannelButton:(id)sender {
    if ( _engine == nil ) {
        return ;
    }
    [_engine stopPreview];
    [_engine leaveChannel] ;
    [AliRtcEngine destroy] ;
    _engine = nil ;
    [[[self view] window]close];
}
- (void)JoinChannel:(NSString *_Nonnull)channelId
              userid:(NSString *_Nonnull)userId
            userName:(NSString * _Nullable)userName {
    self->userId = userId ;
    self->channelId = channelId;
    uint64_t timestamp = time(NULL) + 24 * 60 * 60;
    AliRtcAuthInfo *info = [[AliRtcAuthInfo alloc]init];
    info.channelId = self->channelId;
    info.userId    = self->userId;
    info.appId     = @ARTC_APP_ID;
    info.nonce     = @"";
    info.timestamp = timestamp;
    NSString *token_str = [NSString stringWithFormat:@"%@%@%@%@%@%lld",
                           info.appId,
                           @ARTC_APP_KEY,
                           info.channelId,
                           info.userId,
                           @"",
                           info.timestamp];
    info.token = [AppDefine generateJoinToken:token_str];
    AliVideoCanvas * canvas = [[AliVideoCanvas alloc] init];
    canvas.view = _localView ;
    canvas.renderMode = AliRtcRenderModeAuto;
    canvas.rotation = AliRtcRotationMode_0 ;
    [_engine setLocalViewConfig:canvas forTrack:AliRtcVideoTrackCamera];
    int ret = [_engine startPreview];
    if ( ret != 0 ) {
    }
    [_engine setDefaultSubscribeAllRemoteAudioStreams:TRUE] ;
    [_engine setDefaultSubscribeAllRemoteVideoStreams:TRUE] ;
    /*
     Configure the audio profile and scene
     */
    [_engine setAudioProfile:AliRtcEngineHighQualityMode audio_scene:AliRtcSceneMusicMode];
    /*
     Configure the channel profile and user role
     */
    [_engine setChannelProfile:AliRtcInteractivelive];
    [_engine setClientRole:AliRtcClientRoleInteractive];
    [_engine publishLocalAudioStream:TRUE] ;
    [_engine publishLocalVideoStream:TRUE];
    /*
     Configure the video encoder
     */
    AliRtcVideoEncoderConfiguration * videoConfig = [[AliRtcVideoEncoderConfiguration alloc]init];
    videoConfig.dimensions = CGSizeMake(1280, 720);
    videoConfig.bitrate = 1200;         // kbps
    videoConfig.frameRate = 15;
    videoConfig.keyFrameInterval = 2000;
    [_engine setVideoEncoderConfiguration:videoConfig];
    [_engine joinChannel:info name:userName onResult:^(NSInteger errCode, NSString * _Nonnull channel, NSInteger elapsed) {
        if( errCode != 0 && ![self->_engine isInCall] ){
        } else {
        }
    }];
}
- (void)statusListBoxAddString:(NSString *)lineString withColor:(NSColor *)color {
    NSDictionary *attributes = @{
        NSFontAttributeName: [NSFont systemFontOfSize:11],
        NSForegroundColorAttributeName: color //[NSColor redColor]
    };
    NSAttributedString * NSAttriString = [[NSAttributedString alloc] initWithString:lineString attributes:attributes];
    [[_statusListBox textStorage] appendAttributedString:NSAttriString];
    NSAttributedString * nextLine = [[NSAttributedString alloc] initWithString:@"\n"];
    [[_statusListBox textStorage] appendAttributedString:nextLine];
}
#pragma mark - Engine Delegates
- (void)onJoinChannelResult:(int)result channel:(NSString *_Nonnull)channel elapsed:(int) elapsed {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"join channel ret=%d cid:%@ elapsed:%d",
                                      result, channel, elapsed ]
                           withColor:[NSColor redColor]];
    });
}
- (void)onLeaveChannelResult:(int)result stats:(AliRtcStats)stats {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"leave channel ret=%d call duration:%lld", result, stats.call_duration ]
                           withColor:[NSColor redColor]];
    });
}
- (void)onAudioPublishStateChanged:(AliRtcPublishState)oldState
                          newState:(AliRtcPublishState)newState
              elapseSinceLastState:(NSInteger)elapseSinceLastState
                           channel:(NSString *_Nonnull)channel{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"audio stream state change:%ld->%ld",
                                        oldState, newState ]
                           withColor:[NSColor blueColor]];
    });
}
- (void)onVideoPublishStateChanged:(AliRtcPublishState)oldState
                          newState:(AliRtcPublishState)newState
              elapseSinceLastState:(NSInteger)elapseSinceLastState
                           channel:(NSString *_Nonnull)channel{
    dispatch_async(dispatch_get_main_queue(), ^{
        [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"video stream state change:%ld->%ld",
                                      oldState, newState ]
                           withColor:[NSColor blueColor]];
    });
}
- (void)onDualStreamPublishStateChanged:(AliRtcPublishState)oldState
                               newState:(AliRtcPublishState)newState
                   elapseSinceLastState:(NSInteger)elapseSinceLastState
                                channel:(NSString *_Nonnull)channel{
}
- (void)onScreenSharePublishStateChanged:(AliRtcPublishState)oldState
                                newState:(AliRtcPublishState)newState
                    elapseSinceLastState:(NSInteger)elapseSinceLastState
                                 channel:(NSString *_Nonnull)channel {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self statusListBoxAddString:[[NSString alloc]initWithFormat:
                                      @"screen share stream state change:%ld->%ld",
                                      oldState, newState ]
                           withColor:[NSColor blueColor]];
    });
}
- (void)onRemoteUserOnLineNotify:(NSString *)uid elapsed:(int)elapsed {
    dispatch_async(dispatch_get_main_queue(), ^{
        [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"uid:%@ online reason:%d",
                                      uid, elapsed]
                           withColor:[NSColor blueColor]];
    });
}
- (void)onRemoteUserOffLineNotify:(NSString *)userID
                    offlineReason:(AliRtcUserOfflineReason)reason{
    // Remove the corresponding canvas
    dispatch_async(dispatch_get_main_queue(), ^{
        /*
         Call this function on the main thread
         */
        [self->_engine setRemoteViewConfig:nil uid:userID forTrack:AliRtcVideoTrackScreen];
        [self->_engine setRemoteViewConfig:nil uid:userID forTrack:AliRtcVideoTrackCamera];
        [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"uid:%@ offline reason:%ld",
                                      userID, reason]
                           withColor:[NSColor blueColor]];
    });
}
- (void)onBye:(int)code {
}
- (void)onLocalDeviceException:(AliRtcLocalDeviceType)deviceType
                 exceptionType:(AliRtcLocalDeviceExceptionType)exceptionType
                       message:(NSString *_Nullable)msg {
    dispatch_async(dispatch_get_main_queue(), ^{
        // Process on the main thread
    });
}
- (void)onRemoteTrackAvailableNotify:(NSString *)uid
                          audioTrack:(AliRtcAudioTrack)audioTrack
                          videoTrack:(AliRtcVideoTrack)videoTrack {
    dispatch_async(dispatch_get_main_queue(), ^{
        if ( videoTrack == AliRtcVideoTrackCamera ) {
            [self->_engine setRemoteViewConfig:nil uid:uid forTrack:AliRtcVideoTrackScreen];
            AliVideoCanvas * remoteCanvas = [[AliVideoCanvas alloc] init];
            remoteCanvas.view = self->_remoteView ;
            remoteCanvas.rotation = AliRtcRotationMode_0 ;
            remoteCanvas.renderMode = AliRtcRenderModeAuto;
            [self->_engine setRemoteViewConfig:remoteCanvas uid:uid forTrack:AliRtcVideoTrackCamera];
        }
        if ( videoTrack == AliRtcVideoTrackScreen ) {
            [self->_engine setRemoteViewConfig:nil uid:uid forTrack:AliRtcVideoTrackCamera];
            AliVideoCanvas * remoteCanvas = [[AliVideoCanvas alloc] init];
            remoteCanvas.view = self->_remoteView ;
            remoteCanvas.rotation = AliRtcRotationMode_0 ;
            // Letterbox mode is recommended for screen sharing
            remoteCanvas.renderMode = AliRtcRenderModeFill ;
            [self->_engine setRemoteViewConfig:remoteCanvas uid:uid forTrack:AliRtcVideoTrackScreen];
        }
        if ( videoTrack == AliRtcVideoTrackNo ) {
            [self->_engine setRemoteViewConfig:nil uid:uid forTrack:AliRtcVideoTrackCamera];
            [self->_engine setRemoteViewConfig:nil uid:uid forTrack:AliRtcVideoTrackScreen];
        }
    });
}

1. Request permissions

The SDK checks for necessary permissions when a user joins a call. However, to ensure a smooth user experience, we recommend that you request camera and microphone permissions before you initiate the call.

#import <AVFoundation/AVFoundation.h>
@implementation PrivacyAuthorizer
+ (void)authorCamera:(void (^ __nullable)(BOOL granted))completion{
    dispatch_block_t workBlock;
    if (@available(macOS 10.14, *)) {
        AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
        if(authStatus == AVAuthorizationStatusAuthorized) {
            workBlock = ^{
                if (completion) completion(YES);
            };
            // do your logic
        } else if(authStatus == AVAuthorizationStatusDenied || authStatus == AVAuthorizationStatusRestricted){
            workBlock = ^{
                if (completion) completion(NO);
            };
            // denied
        } else if(authStatus == AVAuthorizationStatusNotDetermined){
            // not determined?!
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
                [PrivacyAuthorizer authorCamera:completion];
            }];
            return;
        } else {
            // impossible, unknown authorization status
        }
    }else {
        workBlock = ^{
            if (completion) completion(YES);
        };
    }
    dispatch_async(dispatch_get_main_queue(), workBlock);
}
+ (void)authorMicphone:(void (^ __nullable)(BOOL granted))completion{
    dispatch_block_t workBlock;
    if (@available(macOS 10.14, *)) {
        AVAuthorizationStatus authStatus = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio];
        if(authStatus == AVAuthorizationStatusAuthorized) {
            workBlock = ^{
                if (completion) completion(YES);
            };
            // do your logic
        } else if(authStatus == AVAuthorizationStatusDenied || authStatus == AVAuthorizationStatusRestricted){
            workBlock = ^{
                if (completion) completion(NO);
            };
            // denied
        } else if(authStatus == AVAuthorizationStatusNotDetermined){
            // not determined?!
            [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) {
                [PrivacyAuthorizer authorMicphone:completion];
            }];
            return;
        } else {
            // impossible, unknown authorization status
        }
    }else {
        workBlock = ^{
            if (completion) completion(YES);
        };
    }
    dispatch_async(dispatch_get_main_queue(), workBlock);
}
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
    // Insert code here to initialize your application
    // Check permissions
    [PrivacyAuthorizer authorCamera:^(BOOL granted) {
        if (!granted) {
            NSLog(@"Camera_granted == %hhd",granted);
            dispatch_async(dispatch_get_main_queue(), ^{
                NSAlert *alert = [[NSAlert alloc] init];
                [alert setMessageText:@"Camera permission is not enabled. Please enable it in <Security & Privacy>."];
                [alert setAlertStyle:NSAlertStyleInformational];
                [alert beginSheetModalForWindow:[self->loginViewController.view window] completionHandler:^(NSModalResponse returnCode) {
                }];
            });
        }
    }];
    [PrivacyAuthorizer authorMicphone:^(BOOL granted) {
        if (!granted) {
            NSLog(@"Camera_granted == %hhd",granted);
            dispatch_async(dispatch_get_main_queue(), ^{
                NSAlert *alert = [[NSAlert alloc] init];
                [alert setMessageText:@"Microphone permission is not enabled. Please enable it in <Security & Privacy>."];
                [alert setAlertStyle:NSAlertStyleInformational];
                [alert beginSheetModalForWindow:[self->loginViewController.view window] completionHandler:^(NSModalResponse returnCode) {
                }];
            });
        }
    }];
}

2. Get an authentication token

Joining an ARTC channel requires an authentication token to authenticate the user's identity. For the token generation rules, see Token Authentication. You can generate a token by using a single-parameter method or a multi-parameter method, and each method requires you to call a different joinChannel API in the SDK.

For production environments:

Because generating an authentication token requires your AppKey, hardcoding it in the client application poses a security risk. We strongly recommend that you generate tokens on your app server and pass them to the client.

For development and debugging:

During development and debugging, if your app server does not have token generation logic, you can temporarily use the logic from the API example to generate a temporary token. The following code shows an example:

#import <CommonCrypto/CommonDigest.h>
#import <CommonCrypto/CommonHMAC.h>
@implementation AppDefine
+ (NSString *)stringFromBytes:(uint8_t *)bytes length:(int)length {
    NSMutableString *strArray = [NSMutableString string];
    for (int i = 0; i < length; i++) {
        [strArray appendFormat:@"%02x", bytes[i]];
    }
    return [strArray copy];
}
+(NSString*)generateJoinToken:(NSString *)oc_str {
    const char * cstring = [oc_str UTF8String];
    size_t length = [oc_str length] ;
    uint8_t sha256_buffer[CC_SHA256_DIGEST_LENGTH];
    CC_SHA256(cstring, (CC_LONG)length, sha256_buffer);
    return [AppDefine stringFromBytes:sha256_buffer length:CC_SHA256_DIGEST_LENGTH];
}

3. Import the ARTC SDK

// Import the ARTC module
#import <AliRTCSdk/AliRTCSdk.h>

4. Create and initialize the engine

  • Create the RTC engine

Call the getInstance[1/2] method to create an AliRtcEngine instance.

@property (nonatomic, strong, nullable) AliRtcEngine *engine;
// Create the engine and set the delegate
_engine = [AliRtcEngine sharedInstance:self extras:nil] ;
...
  • Initialize the engine

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

      Depending on your business needs, you can choose the interactive live mode for interactive entertainment scenarios, or the communication mode for one-to-one or one-to-many calls. Choosing the right mode ensures a smooth user experience and efficient use of network resources. You must call setChannelProfile before calling setClientRole.

      Mode

      Publish

      Subscribe

      Description

      Interactive live mode

      1. Role-based restrictions apply. Only users with the broadcaster 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 live mode, events such as a broadcaster joining or leaving the session, or starting to publish a stream, are sent to the audience in real time. This ensures the audience is always aware of the broadcaster's status. Conversely, audience activities are not sent to broadcasters, which prevents disruption to the live stream.

      2. If your application's requirements may change or you are unsure whether the audience will need to interact, we recommend using interactive live mode. This mode provides the flexibility to meet different interaction needs by changing user roles.

      Communication mode

      All participants can publish streams.

      All participants can subscribe to streams.

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

      2. This mode is functionally equivalent to assigning the broadcaster role to all users in interactive live mode. It simplifies operations by requiring fewer API calls to achieve the desired functionality.

    • Call setClientRole to set the user role to AliRTCSdkInteractive (broadcaster) or AliRTCSdkLive (audience). Note: The broadcaster role publishes and subscribes to streams by default. The audience role subscribes to streams by default, with local preview and publishing disabled.

      Note

      When a user stops publishing by switching from the broadcaster role to the audience role, the SDK stops publishing local audio and video streams. When a user starts publishing by switching from the audience role to the broadcaster role, the SDK resumes publishing local streams. In either case, subscribed streams are not affected.

      // Set the channel profile to interactive live mode. AliRtcInteractivelive is used for RTC scenarios.
      [_engine setChannelProfile:AliRtcInteractivelive];
      // Set the user role. Use AliRtcClientRoleInteractive to both publish and subscribe. Use AliRtcClientRolelive to only subscribe.
      [_engine setClientRole:AliRtcClientRoleInteractive];
  • Implement common callbacks

    When you create the engine, you set a Delegate object to implement the necessary callbacks. The SDK first attempts to recover from exceptions internally. For errors it cannot resolve, it notifies your application through these callbacks.

    Cause

    Callback and parameters

    Solution

    Description

    Authentication failed

    The result parameter in the onJoinChannelResult callback returns AliRtcErrJoinBadToken.

    Your app must verify the authentication token.

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

    Authentication token is about to expire

    onAuthInfoWillExpire

    Your app must retrieve a new authentication token and call refreshAuthInfo.

    An authentication expiration error can occur when a user calls an API or during program execution. The error is reported through an API callback or a separate error callback.

    Authentication token has expired

    onAuthInfoExpired

    Your app must have the user rejoin the channel.

    An authentication expiration error can occur when a user calls an API or during program execution. The error is reported through an API callback or a separate error callback.

    Network connection exception

    The onConnectionStatusChange callback returns AliRtcConnectionStatusFailed.

    Your app must have the user rejoin the channel.

    The SDK can automatically recover from network interruptions for a certain period. However, if the disconnection time exceeds the preset threshold, a timeout occurs. In this case, your app should check the network status and guide the user to rejoin the channel.

    Kicked offline

    onBye

    • AliRtcOnByeUserReplaced: Check if multiple users are using the same UserID.

    • AliRtcOnByeBeKickedOut: The user was kicked by the app server and must rejoin the channel.

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

    The RTC service allows an administrator to remove participants.

    Local device exception

    onLocalDeviceException

    Your app needs to check permissions and whether the hardware is functioning correctly.

    The RTC service supports device detection and diagnostics. When a local device exception occurs, the service notifies the client through a callback. If the SDK cannot resolve the issue, your app needs to intervene and check the device status.

    - (void)onJoinChannelResult:(int)result channel:(NSString *_Nonnull)channel elapsed:(int) elapsed {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"join channel ret=%d cid:%@ elapsed:%d",
                                          result, channel, elapsed ]
                               withColor:[NSColor redColor]];
        });
    }
    - (void)onLeaveChannelResult:(int)result stats:(AliRtcStats)stats {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"leave channel ret=%d call duration:%lld", result, stats.call_duration ]
                               withColor:[NSColor redColor]];
        });
    }
    - (void)onAuthInfoWillExpire {
        dispatch_async(dispatch_get_main_queue(), ^{
        });
    }
    - (void)onAuthInfoExpired {
        /* token expired! */
        dispatch_async(dispatch_get_main_queue(), ^{
        });
    }
    - (void)onConnectionStatusChange:(AliRtcConnectionStatus)status
                              reason:(AliRtcConnectionStatusChangeReason)reason {
        dispatch_async(dispatch_get_main_queue(), ^{
        });
    }
    - (void)onRemoteUserOnLineNotify:(NSString *)uid elapsed:(int)elapsed {
        dispatch_async(dispatch_get_main_queue(), ^{
            [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"uid:%@ online reason:%d",
                                          uid, elapsed]
                               withColor:[NSColor blueColor]];
        });
    }
    - (void)onRemoteUserOffLineNotify:(NSString *)userID
                        offlineReason:(AliRtcUserOfflineReason)reason{
        // Remove the corresponding canvas
        dispatch_async(dispatch_get_main_queue(), ^{
            /*
             This function is recommended to be called on the main thread
             */
            [self->_engine setRemoteViewConfig:nil uid:userID forTrack:AliRtcVideoTrackScreen];
            [self->_engine setRemoteViewConfig:nil uid:userID forTrack:AliRtcVideoTrackCamera];
            [self statusListBoxAddString:[[NSString alloc]initWithFormat:@"uid:%@ offline reason:%ld",
                                          userID, reason]
                               withColor:[NSColor blueColor]];
        });
    }
    - (void)onLocalDeviceException:(AliRtcLocalDeviceType)deviceType
                     exceptionType:(AliRtcLocalDeviceExceptionType)exceptionType
                           message:(NSString *_Nullable)msg {
        dispatch_async(dispatch_get_main_queue(), ^{
            // Process on the main thread
        });
    }
    - (void)onBye:(int)code {
        dispatch_async(dispatch_get_main_queue(), ^{
            // Process on the main thread
        });
    }
    

5. Configure audio and video properties

  • Configure audio properties

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

    /*
      Configure the audio profile and scene
    */
    [_engine setAudioProfile:AliRtcEngineHighQualityMode audio_scene:AliRtcSceneMusicMode];
    
  • Configure video properties

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

    // Set video encoder configuration
    AliRtcVideoEncoderConfiguration * videoConfig = [[AliRtcVideoEncoderConfiguration alloc]init];
    videoConfig.dimensions = CGSizeMake(1280, 720);
    videoConfig.bitrate = 1200;         	// kbps
    videoConfig.frameRate = 15;				
    videoConfig.keyFrameInterval = 2000;	// ms
    [_engine setVideoEncoderConfiguration:videoConfig];
    

6. Configure publishing and subscribing

Configure the publishing of audio and video streams and set the default behavior to subscribe to all user streams:

  • Call publishLocalAudioStream to publish the audio stream.

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

// By default, the SDK publishes the audio stream, so this call is optional.
[_engine publishLocalAudioStream:true];
// By default, the SDK publishes the video stream for video calls, so this call is optional.
// For an audio-only call, you must call publishLocalVideoStream(false) to disable video publishing.
[_engine publishLocalVideoStream:true];
// Set the default behavior to subscribe to 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 audio and video streams and subscribes to all remote streams in the channel. You can call the methods described above to change this behavior.

7. Start local preview

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

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

AliVideoCanvas * canvas = [[AliVideoCanvas alloc] init];
canvas.view = _localView ;
canvas.renderMode = AliRtcRenderModeAuto;
canvas.rotation = AliRtcRotationMode_0 ;
[_engine setLocalViewConfig:canvas forTrack:AliRtcVideoTrackCamera];
int ret = [_engine startPreview];

8. Join a channel

Call joinChannel to join a channel. We recommend that you use the single-parameter method by calling the joinChannel[3/4] API. After the call, check both its return value and the result from the onJoinChannelResult callback. If the function returns 0 and the result is 0, you have successfully joined the channel. Otherwise, check whether the provided token is invalid.

uint64_t timestamp = time(NULL) + 24 * 60 * 60;
AliRtcAuthInfo *info = [[AliRtcAuthInfo alloc]init];
info.channelId = self->channelId;
info.userId    = self->userId;
info.appId     = @ARTC_APP_ID;
info.nonce     = @"";
info.timestamp = timestamp;
NSString *token_str = [NSString stringWithFormat:@"%@%@%@%@%@%lld",
                           info.appId,
                           @ARTC_APP_KEY,
                           info.channelId,
                           info.userId,
                           @"",
                           info.timestamp];
info.token = [AppDefine generateJoinToken:token_str];
int ret = [_engine joinChannel:info name:userName onResult:^(NSInteger errCode, NSString * _Nonnull channel, NSInteger elapsed) {
    if( errCode != 0 && ![self->_engine isInCall] ){
        // Handle errors
    } else {
    }
}];
Note
  • After joining the channel, the SDK automatically publishes and subscribes to streams based on the pre-join configurations.

  • The SDK enables automatic publishing and subscribing by default to reduce the number of API calls your client needs to make.

9. Set up the remote view

When a remote user starts or stops publishing a stream, the onRemoteTrackAvailableNotify callback is triggered. You can set up or remove the remote view within this callback. The following code shows an example:

- (void)onRemoteTrackAvailableNotify:(NSString *)uid
                          audioTrack:(AliRtcAudioTrack)audioTrack
                          videoTrack:(AliRtcVideoTrack)videoTrack {
    dispatch_async(dispatch_get_main_queue(), ^{
        if ( videoTrack == AliRtcVideoTrackCamera ) {
            [self->_engine setRemoteViewConfig:nil uid:uid forTrack:AliRtcVideoTrackScreen];
            AliVideoCanvas * remoteCanvas = [[AliVideoCanvas alloc] init];
            remoteCanvas.view = self->_remoteView ;
            remoteCanvas.rotation = AliRtcRotationMode_0 ;
            remoteCanvas.renderMode = AliRtcRenderModeAuto;
            [self->_engine setRemoteViewConfig:remoteCanvas uid:uid forTrack:AliRtcVideoTrackCamera];
        }
        if ( videoTrack == AliRtcVideoTrackScreen ) {
            [self->_engine setRemoteViewConfig:nil uid:uid forTrack:AliRtcVideoTrackCamera];
            AliVideoCanvas * remoteCanvas = [[AliVideoCanvas alloc] init];
            remoteCanvas.view = self->_remoteView ;
            remoteCanvas.rotation = AliRtcRotationMode_0 ;
            // Letterbox mode is recommended for screen sharing
            remoteCanvas.renderMode = AliRtcRenderModeFill ;
            [self->_engine setRemoteViewConfig:remoteCanvas uid:uid forTrack:AliRtcVideoTrackScreen];
        }
        if ( videoTrack == AliRtcVideoTrackNo ) {
            [self->_engine setRemoteViewConfig:nil uid:uid forTrack:AliRtcVideoTrackCamera];
            [self->_engine setRemoteViewConfig:nil uid:uid forTrack:AliRtcVideoTrackScreen];
        }
    });
}

10. Leave channel and destroy engine

When the real-time audio and video interaction ends, leave the channel and destroy the engine. Follow these steps to end the session:

  1. Call stopPreview to stop the video preview.

  2. Call leaveChannel to leave the channel.

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

[_engine stopPreview];
[_engine leaveChannel] ;
[AliRtcEngine destroy] ;
_engine = nil ;

11. See it in action

In the ARTC Example v1.0 configuration form, set Channel to 12356, UserID to Test01, UserName to Test01, and select VideoChat for ExampleType. Then, click Join to join the channel.

After joining the channel, the local and remote video views are displayed side-by-side in the upper part of the window, and the video renders correctly. The log output shows join channel ret=0 cid:12356 elapsed:56, which indicates a successful join. The audio stream state changes from 0 to 2, then to 3, and the video stream state changes from 0 to 2, indicating that the audio and video streams are ready. You can click LeaveChannel in the bottom-right corner to leave the channel.