本文档将介绍如何在您的Mac项目中集成 ARTC SDK, 快速实现一个简单的实时音视频互动App,适用于互动直播和视频通话等场景。
功能简介
在开始之前,了解以下几个关键概念会很有帮助:
ARTC SDK:这是阿里云的实时音视频产品,帮助开发者快速实现实时音视频互动的SDK。
GRTN:阿里云全球实时传输网络,提供超低延时、高音质、安全可靠的音视频通讯服务。
频道:相当于一个虚拟的房间,所有加入同一频道的用户都可以进行实时音视频互动。
主播:可在频道内发布音视频流,并可订阅其他主播发布的音视频流。
观众:可在频道内订阅音视频流,不能发布音视频流。
实现实时音视频互动的基本流程如下:
用户需要调用
setChannelProfile(设置频道场景),后调用joinChannel加入频道:视频通话场景:所有用户都是主播角色,可以进行推流和拉流。
互动直播场景:需要调用
setClientRole(设置角色),在频道内推流的用户设置主播角色;如果用户只需要拉流,不需要推流,则设置观众角色。
加入频道后,不同角色的用户有不同的推拉流行为:
所有加入频道内的用户都可以接收频道内的音视频流。
主播角色可以在频道内推音视频流。
观众如果需要推流,需要调用
setClientRole方法,将用户角色切换成主播,便可以推流。
示例项目
阿里云ARTC SDK提供了开源的实时音视频互动示例项目供客户参考,您可以前往下载或查看示例源码。
前提条件
开发工具:Xcode 14.0 及以上版本,推荐使用最新正式版本。
测试设备:Mac 10.13 及以上版本的Mac设备。
网络环境:需要稳定的网络连接。
应用准备:获取实时音视频应用的AppID和AppKey,详情请参见创建应用。
创建项目(可选)
本节将介绍如何创建项目并为项目添加体验音视频互动必需的权限。如果已有项目可跳过。
打开 Xcode,选择 File → New → Project,选择App的模板,下一步后Interface 选择 Storyboard,Language 选择 Object C或Swift。

根据需要,修改工程配置,包括Bundle Identifier、Signing、Minimum Deployments等。
配置项目
步骤一:权限配置
点击项目导航栏的工程工程文件,切换到Info标签栏,添加摄像头和麦克风权限

Key | Type | Value |
Privacy - Microphone Usage Description | String | 使用麦克风的目的,例如:需要使用麦克风进行语音聊天。 |
Privacy - Camera Usage Description | String | 使用摄像头的目的,例如:需要使用摄像头进行视频聊天。 |
发布到APP Store需要开启沙盒模式,这里是沙盒的一般性设置,红色框内为必选,蓝色框是根据产品需要决定;

可选项目,例如如果不会访问本地文件,可以设置User Selected File权限为None,如果有伴奏、推送本地文件功能,就需要开启蓝色框内的选项;
步骤二:导入 ARTC SDK
在SDK下载中,获取最新的 ARTC SDK 文件并解压。
将 SDK 包内的文件,拷贝到你的项目路径下,如果不使用aac做音频编码,可以不添加PluginAAC.framework。
打开 Xcode,添加对应动态库,确保添加的动态库 Embed 属性设置为 Embed & Sign。
步骤三:创建用户界面
根据实时音视频互动场景需要,创建相应的用户界面。阿里云提供了一个以视频多人通话场景为例,创建一个Window Controller 视图,在Video Chat Controller的View上增加Custom View控件,后续在有人加入通话时,在该容器上添加通话视图;有人离开通话时,从该容器上移除通话视图,同时刷新布局。

实现步骤
本节介绍如何使用阿里云 ARTC SDK 快速实现一个基础的实时音视频互动应用。你可以先将完整示例代码复制到项目,快速体验功能,再通过以下步骤了解核心 API 的调用。
下图展示了实现音视频互动的基本流程:
下面列出一段实现音视频通话基本流程的完整代码以供参考:
1、申请权限请求
进入音视频通话时,虽然SDK会检查是否已在App中授予了所需要的权限,为保障体验,建议在发起通话前检查视频拍摄及麦克风采集的权限。
#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
//检测权限
[PrivacyAuthorizer authorCamera:^(BOOL granted) {
if (!granted) {
NSLog(@"Camera_granted == %hhd",granted);
dispatch_async(dispatch_get_main_queue(), ^{
NSAlert *alert = [[NSAlert alloc] init];
[alert setMessageText:@"未开启摄像头权限,请在<安全与隐私>里开启"];
[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:@"未开启麦克风权限,请在<安全与隐私>里开启"];
[alert setAlertStyle:NSAlertStyleInformational];
[alert beginSheetModalForWindow:[self->loginViewController.view window] completionHandler:^(NSModalResponse returnCode) {
}];
});
}
}];
}
2、鉴权Token
加入ARTC频道需要一个鉴权Token,用于鉴权用户的合法身份,其鉴权Token生成规则参见:Token鉴权。Token 生成有两种方式:单参数方式和多参数方式,不同的Token生成方式需要调用SDK不同的加入频道(joinChannel)的接口。
上线发布阶段:
由于Token的生成需要使用AppKey,写死在客户端存在泄露的风险,因此强烈建议线上业务通过业务Server生成下发给客户端。
开发调试阶段:
开发调试阶段,如果业务Server还没有生成Token的逻辑,可以暂时参考APIExample上的Token生成逻辑,生成临时Token,其参考代码如下:
#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、导入ARTC SDK 组件
// 导入ARTC模块
#import <AliRTCSdk/AliRTCSdk.h>4、创建并初始化引擎
创建RTC引擎
调用getInstance[1/2]接口创建引擎AliRTCEngine
@property (nonatomic, strong, nullable) AliRtcEngine *engine;
// 创建引擎并设置回调
_engine = [AliRtcEngine sharedInstance:self extras:nil] ;
...
初始化引擎
调用
setChannelProfile设置频道为AliRTCInteractiveLive(互动模式)。根据具体的业务需求,可以选择适用于互动娱乐场景的互动模式,或者适合一对一或一对多广播的通信模式。正确的模式选择能够确保用户体验的流畅性并有效利用网络资源。您可以根据业务场景选择合适的模式,在设置setChannelProfile之后才能调用setClientRole;
模式
推流
拉流
模式介绍
互动模式
有角色限制,只有被赋予主播身份的用户可以进行推流操作。
在整个过程中,参与者可以灵活地切换角色。
无角色限制,所有参与者都拥有拉流的权限。
在互动模式中,主播加入或退出会议、以及开始推送直播流的事件都会实时通知给观众端,确保观众能够及时了解主播的动态。反之,观众的任何活动不会通告给主播,保持了主播的直播流程不受干扰。
在互动模式下,主播角色负责进行直播互动,而观众角色则主要接收内容,通常不参与直播的互动过程。若业务需求未来可能发生变化,导致不确定是否需要支持观众的互动参与,建议默认采用互动模式。这种模式具有较高的灵活性,可通过调整用户角色权限来适应不同的互动需求。
通信模式
无角色限制,所有参与者都拥有推流权限。
无角色限制,所有参与者都拥有拉流的权限。
在通信模式下,会议参与者能够相互察觉到彼此的存在。
该模式虽然没有区分用户角色,但实际上与互动模式中的主播角色相对应;目的是为了简化操作,让用户能够通过调用更少的API来实现所需的功能。
调用
setClientRole设置用户角色为AliRTCSdkInteractive(主播)或者AliRTCSdkLive(观众)。注意:主播角色默认推拉流,观众角色默认关闭预览和推流,只拉流。说明当用户从主播角色转换到观众角色时(通常被称作“下麦”),系统将停止推送本地的音视频流,已经订阅的音视频流不受影响;当用户从观众播角色转换到主播角色时(通常被称作“上麦”),系统将会推送本地的音视频流,已经订阅的音视频流不受影响。
// 设置频道模式为互动模式,RTC下都使用AliRtcInteractivelive [_engine setChannelProfile:AliRtcInteractivelive]; // 设置用户角色,既需要推流也需要拉流使用AliRtcClientRoleInteractive, 只拉流不推流使用AliRtcClientRolelive [_engine setClientRole:AliRtcClientRoleInteractive];
实现常用的回调
创建引擎时,已经设置了Delegate对象,接下来需要实现这些回调接口。SDK设计为在遇到异常情况时,首先尝试内部重试机制以恢复正常状态。对于那些SDK无法自行解决的错误,会通过明确定义的回调接口(API)通知到您的应用程序,以下回调是 SDK 无法进行处理的:
异常发生原因
回调及参数
解决方案
说明
鉴权失败
onJoinChannelResult回调中的result返回AliRtcErrJoinBadToken
发生错误时App需要检查Token是否正确。
在用户主动调用API时,若鉴权失败,系统将在调用API的回调中返回鉴权失败的错误信息。
鉴权将要过期
onAuthInfoWillExpire
发生该异常时App需要重新获取最新的鉴权信息后,再调用refreshAuthInfo刷新鉴权信息。
鉴权过期错误在两种情况下出现:用户调用API或程序执行期间。因此,错误反馈将通过API回调或通过独立的错误回调通知。
鉴权过期
onAuthInfoExpired
发生该异常时App需要重新入会。
鉴权过期错误在两种情况下出现:用户调用API或程序执行期间。因此,错误反馈将通过API回调或通过独立的错误回调通知。
网络连接异常
onConnectionStatusChange回调返回AliRtcConnectionStatusFailed。
发生该异常时APP需要重新入会。
SDK具备一定时间断网自动恢复能力,但若断线时间超出预设阈值,会触发超时并断开连接。此时,App应检查网络状态并指导用户重新加入会议。
被踢下线
onBye
AliRtcOnByeUserReplaced:当发生该异常时排查用户userid是否相同。
AliRtcOnByeBeKickedOut:当发生该异常时,表示被业务踢下线,需要重新入会。
AliRtcOnByeChannelTerminated:当发生该异常时,表示房间被销毁,需要重新入会。
RTC服务提供了管理员可以主动移除参与者的功能。
本地设备异常
onLocalDeviceException
发生该异常时App需要检测权限、设备硬件是否正常。
RTC服务支持设备检测和异常诊断的能力;当本地设备发生异常时,RTC服务会通过回调的方式通知客户本地设备异常,此时,若SDK无法自行解决问题,则App需要介入以查看设备是否正常。
- (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{ /* 删除对应的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(), ^{ /* to main thread process */ }); } - (void)onBye:(int)code { dispatch_async(dispatch_get_main_queue(), ^{ /* to main thread process */ }); }
5、设置音视频属性
设置音频相关属性
调用
setAudioProfile设置音频的编码模式和音频场景。/* config audio profile and Scene */ [_engine setAudioProfile:AliRtcEngineHighQualityMode audio_scene:AliRtcSceneMusicMode];设置视频相关属性
可以设置推出的视频流的分辨率、码率、帧率等信息。
// set video encoder config 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、设置推拉流属性
设置推送音视频流及默认拉所有用户的流:
调用
publishLocalAudioStream推送音频流。调用
publishLocalVideoStream推送视频流,如果是语音通话,可以设置成false。
// SDK默认会publish音频,publishLocalVideoStream(true)可以不调用
[_engine publishLocalVideoStream:true];
// SDK默认会publish视频,如果是视频通话,publishLocalAudioStream(true)可以不调用
// 如果是纯语音通话 则需要设置publishLocalVideoStream(false)设置不publish视频
[_engine publishLocalAudioStream:true];
// 设置默认订阅远端的音频和视频流
[_engine setDefaultSubscribeAllRemoteAudioStreams:true];
[_engine subscribeAllRemoteAudioStreams:true];
[_engine setDefaultSubscribeAllRemoteVideoStreams:true];
[_engine subscribeAllRemoteVideoStreams:true];
注意:SDK默认是自动推拉流模式,默认会推送音视频流及订阅频道内所有用户的音视频流,可以通过调用上面的接口关闭自动推拉流模式。
7、开启本地预览
调用
setLocalViewConfig设置本地渲染视图,同时设置本地的视频显示属性。调用
startPreview方法,开启本地视频预览。
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、加入频道
调用joinChannel加入频道,这里推荐使用单参数方式,需要调用joinChannel[3/4]接口。调用完加入频道后,需要同时判断返回值,及在onJoinChannelResult回调中拿到加入频道结果,如果返回0并且result为0,则表示加入频道成功,否则需要检查传进来的Token是否非法。
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] ){
// 出错处理
} else {
}
}];
入会后会按照入会前设定的参数执行相应的推流和拉流。
SDK默认会自动推拉流,以减少客户端需要调用的API数量。
9、设置远端视图
远端用户并进行推流或停止推流时,会触发onRemoteTrackAvailableNotify回调,在回调会设置或移除远端视图,示例代码如下:
- (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 ;
/* 屏幕共享建议加黑边模式 */
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、离开房间并销毁引擎
音视频互动结束,需要离开房间并销毁引擎,按照下列步骤结束音视频互动
调用
stopPreview停止视频预览。调用
leaveChannel离会。调用
destroy销毁引擎,并释放相关资源。
[_engine stopPreview];
[_engine leaveChannel] ;
[AliRtcEngine destroy] ;
_engine = nil ;11、效果演示

