Mac实现音视频通话

本文档将介绍如何在您的Mac项目中集成 ARTC SDK, 快速实现一个简单的实时音视频互动App,适用于互动直播和视频通话等场景。

功能简介

在开始之前,了解以下几个关键概念会很有帮助:

  • ARTC SDK:这是阿里云的实时音视频产品,帮助开发者快速实现实时音视频互动的SDK。

  • GRTN:阿里云全球实时传输网络,提供超低延时、高音质、安全可靠的音视频通讯服务。

  • 频道:相当于一个虚拟的房间,所有加入同一频道的用户都可以进行实时音视频互动。

  • 主播:可在频道内发布音视频流,并可订阅其他主播发布的音视频流。

  • 观众:可在频道内订阅音视频流,不能发布音视频流。

实现实时音视频互动的基本流程如下:

image
  1. 用户需要调用setChannelProfile(设置频道场景),后调用joinChannel加入频道:

    • 视频通话场景:所有用户都是主播角色,可以进行推流和拉流

    • 互动直播场景:需要调用setClientRole(设置角色),在频道内推流的用户设置主播角色;如果用户只需要拉流,不需要推流,则设置观众角色。

  2. 加入频道后,不同角色的用户有不同的推拉流行为:

    • 所有加入频道内的用户都可以接收频道内的音视频流。

    • 主播角色可以在频道内推音视频流

    • 观众如果需要推流,需要调用setClientRole方法,将用户角色切换成主播,便可以推流。

示例项目

阿里云ARTC SDK提供了开源的实时音视频互动示例项目供客户参考,您可以前往下载或查看示例源码

前提条件

  • 开发工具:Xcode 14.0 及以上版本,推荐使用最新正式版本。

  • 测试设备:Mac 10.13 及以上版本的Mac设备。

  • 网络环境:需要稳定的网络连接。

  • 应用准备:获取实时音视频应用的AppIDAppKey,详情请参见创建应用

创建项目(可选)

本节将介绍如何创建项目并为项目添加体验音视频互动必需的权限。如果已有项目可跳过。

  1. 打开 Xcode,选择 File → New → Project,选择App的模板,下一步后Interface 选择 Storyboard,Language 选择 Object CSwift。

image

  1. 根据需要,修改工程配置,包括Bundle Identifier、Signing、Minimum Deployments等。

配置项目

步骤一:权限配置

点击项目导航栏的工程工程文件,切换到Info标签栏,添加摄像头和麦克风权限

image.png

Key

Type

Value

Privacy - Microphone Usage Description

String

使用麦克风的目的,例如:需要使用麦克风进行语音聊天。

Privacy - Camera Usage Description

String

使用摄像头的目的,例如:需要使用摄像头进行视频聊天。

发布到APP Store需要开启沙盒模式,这里是沙盒的一般性设置,红色框内为必选,蓝色框是根据产品需要决定;

image.png

可选项目,例如如果不会访问本地文件,可以设置User Selected File权限为None,如果有伴奏、推送本地文件功能,就需要开启蓝色框内的选项;

步骤二:导入 ARTC SDK

  1. SDK下载中,获取最新的 ARTC SDK 文件并解压。

  2. 将 SDK 包内的文件,拷贝到你的项目路径下,如果不使用aac做音频编码,可以不添加PluginAAC.framework。

  3. 打开 Xcode,添加对应动态库,确保添加的动态库 Embed 属性设置为 Embed & Sign

步骤三:创建用户界面

根据实时音视频互动场景需要,创建相应的用户界面。阿里云提供了一个以视频多人通话场景为例,创建一个Window Controller 视图,在Video Chat ControllerView上增加Custom View控件,后续在有人加入通话时,在该容器上添加通话视图;有人离开通话时,从该容器上移除通话视图,同时刷新布局。

image.png

实现步骤

本节介绍如何使用阿里云 ARTC SDK 快速实现一个基础的实时音视频互动应用。你可以先将完整示例代码复制到项目,快速体验功能,再通过以下步骤了解核心 API 的调用。

下图展示了实现音视频互动的基本流程:

image

下面列出一段实现音视频通话基本流程的完整代码以供参考:

基本流程代码示例

@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] ;
  
    /*
     config audio profile and Scene
     */
    [_engine setAudioProfile:AliRtcEngineHighQualityMode audio_scene:AliRtcSceneMusicMode];


    /*
     config channel Profile and client Role
     */
    [_engine setChannelProfile:AliRtcInteractivelive];
    [_engine setClientRole:AliRtcClientRoleInteractive];
    
    [_engine publishLocalAudioStream:TRUE] ;
    [_engine publishLocalVideoStream:TRUE];
    /*
     config 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 - "Delegates of engine"

- (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{
    
    /* 删除对应的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)onBye:(int)code {
    
}

- (void)onLocalDeviceException:(AliRtcLocalDeviceType)deviceType
                 exceptionType:(AliRtcLocalDeviceExceptionType)exceptionType
                       message:(NSString *_Nullable)msg {
    
    dispatch_async(dispatch_get_main_queue(), ^{
        /* to main thread process */
        
    });
    
    
}


- (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];
        }
    });
    
}


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;

      模式

      推流

      拉流

      模式介绍

      互动模式

      1. 有角色限制,只有被赋予主播身份的用户可以进行推流操作。

      2. 在整个过程中,参与者可以灵活地切换角色。

      无角色限制,所有参与者都拥有拉流的权限。

      1. 在互动模式中,主播加入或退出会议、以及开始推送直播流的事件都会实时通知给观众端,确保观众能够及时了解主播的动态。反之,观众的任何活动不会通告给主播,保持了主播的直播流程不受干扰。

      2. 在互动模式下,主播角色负责进行直播互动,而观众角色则主要接收内容,通常不参与直播的互动过程。若业务需求未来可能发生变化,导致不确定是否需要支持观众的互动参与,建议默认采用互动模式。这种模式具有较高的灵活性,可通过调整用户角色权限来适应不同的互动需求。

      通信模式

      无角色限制,所有参与者都拥有推流权限。

      无角色限制,所有参与者都拥有拉流的权限。

      1. 在通信模式下,会议参与者能够相互察觉到彼此的存在。

      2. 该模式虽然没有区分用户角色,但实际上与互动模式中的主播角色相对应;目的是为了简化操作,让用户能够通过调用更少的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并且result0,则表示加入频道成功,否则需要检查传进来的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、离开房间并销毁引擎

音视频互动结束,需要离开房间并销毁引擎,按照下列步骤结束音视频互动

  1. 调用 stopPreview 停止视频预览。

  2. 调用leaveChannel离会。

  3. 调用destroy销毁引擎,并释放相关资源。

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

11、效果演示

image.png

image.png