实现连麦
本文档介绍如何在使用实时音视频产品(ZEGO Express SDK)实现基本直播功能的基础上,结合即时通讯产品(ZIM SDK)实现连麦功能。
本文是基于 示例代码 来介绍的如何实现连麦功能,因此文档中的部分方法为示例代码封装方法。
前提条件
在开始之前,请确保您已完成以下步骤:
- 已集成 SDK,详情请参考 CDN 直播快速开始 或 超低延迟直播快速开始 文档。
- 下载与本文档配套的 示例代码。
- 在 管理控制台 中注册并创建项目,同时开通 “即时通讯” 服务。
预览效果
您可以通过本文档提供的 示例代码 实现以下效果:
主页 | 直播页面 | 接收连麦请求 | 开始连麦 |
---|---|---|---|
![]() | ![]() | ![]() | ![]() |
技术原理
什么是信令
基于信令实现的连麦流程,信令是一种协议或消息,用于管理网络中的通信和连接。ZEGO 将所有信令功能打包到一个 SDK 中,为您提供了一个现成的实时信令 API。
如何通过 ZIM SDK 接口发送和接收信令消息
ZIM SDK 提供了丰富的发送和接收消息的功能,详情请参考 收发消息。此处需要您使用可自定义的信令消息:ZIMCommandMessage
,完整示例代码在 ZIMService.swift 中找到。
-
在房间中通过调用 sendMessage 发送信令消息 (
ZIMCommandMessage
):NSData *bytes = [protocolStr dataUsingEncoding:NSUTF8StringEncoding]; if (!bytes) { // 处理转换失败的情况 return; } ZIMCommandMessage *commandMessage = [[ZIMCommandMessage alloc] initWithMessage:bytes]; [self.zim sendMessage:commandMessage toConversationID:self.roomID conversationType:ZIMConversationTypeRoom // 假设有ZIMConversationType枚举定义 configuration:[ZIMMessageSendConfig new] // 假设ZIMMessageSendConfig有一个无参构造方法 notification:nil completion:^(id msg, NSError *error) { // ... if (self.callback) { // 假设callback是一个接受两个参数的Block self.callback((long long)error.code, error.localizedDescription); } }];
-
发送消息后,房间中的其他用户将通过 onReceiveRoomMessage 回调接收到信令消息。您可以通过以下方式监听此回调:
@interface ZIMService () <ZIMEventHandler> @end @implementation ZIMService // 实现协议方法,接收房间消息 - (void)zim:(ZIM *)zim receiveRoomMessage:(NSArray<ZIMMessage *> *)messageList fromRoomID:(NSString *)roomID { for (ZIMMessage *message in messageList) { if (message.type == ZIMMessageTypeCommand) { ZIMCommandMessage *commandMessage = (ZIMCommandMessage *)message; NSData *messageData = commandMessage.message; NSString *customSignalingJson = [NSString stringWithUTF8String:[messageData bytes]]; // ... } else { // 处理非命令类型的消息 } } } @end
如何自定义业务信令
此处完整示例代码,可以在 ZIMService+RoomRequest.swift 中查看。
JSON 信令编码
由于简单的 String
本身难以表达复杂信息,信令可以封装在 JSON
格式中,使您更方便地编写信令的协议内容。
以最简单的信令的 JSON 格式为例:{"room_request_type": 10000}
,在这样一份 JSON 信令中,您可以使用 room_request_type
字段表达不同的信令类型,例如:
- 发送连麦请求:
{"room_request_type": 10000}
- 取消连麦请求:
{"room_request_type": 10001}
- 接受连麦请求:
{"room_request_type": 10002}
- 拒绝连麦请求:
{"room_request_type": 10003}
此外,您还可以为信令扩展其他常见字段,例如 senderID
、receiverID
:
@interface ZIMService (RoomRequest)
- (void)sendRoomRequestWithReceiverID:(NSString *)receiverID extendedData:(NSString *)extendedData callback:(RoomRequestCallback)callback;
@end
@interface RoomRequest : NSObject <NSCoding>
@property (nonatomic, copy) NSString *requestID;
@property (nonatomic, assign) RoomRequestAction actionType;
@property (nonatomic, copy) NSString *senderID;
@property (nonatomic, copy) NSString *receiverID;
@property (nonatomic, copy) NSString *extendedData;
- (instancetype)initWithRequestID:(NSString *)requestID actionType:(RoomRequestAction)actionType senderID:(NSString *)senderID receiverID:(NSString *)receiverID extendedData:(NSString *)extendedData;
- (NSString *)jsonString;
@end
JSON 信令解码
接收信令的用户,可以解码 JSON 信令,并根据其中的字段了解和处理具体的业务逻辑,例如:
@interface LiveStreamingViewController () <ZIMServiceDelegate>
@end
@implementation LiveStreamingViewController
- (void)onInComingRoomRequestReceived:(RoomRequest *)request {
[self showReaDot];
}
- (void)onInComingRoomRequestCancelled:(RoomRequest *)request {
[self showReaDot];
}
- (void)onOutgoingRoomRequestAccepted:(RoomRequest *)request {
[self onReceiveAcceptCoHostApply];
}
- (void)onOutgoingRoomRequestRejected:(RoomRequest *)request {
[self onReceiveRefuseCoHostApply];
}
@end
扩展信令
基于这种模式,当您需要进行任何业务协议扩展时,只需扩展信令的 type
字段,就可以轻松实现新的业务逻辑,例如:
- 禁言观众:接收到相应的信令后,可阻止用户发送直播间弹幕。
- 发送虚拟礼物:接收到信令后,展示礼物特效。
- 移除观众:接收到信令后,提示观众已被移除并退出房间。
其他扩展信息:
通过以下内容,可进一步了解连麦信令的实现,您将能够轻松扩展您的直播业务信令。
本文档中的示例代码是纯客户端 API + ZEGO 解决方案。如果您有自己的业务服务端,并希望进行更多的逻辑扩展,您可以使用我们的 服务端 API 来传递信令,并结合您服务端的房间业务逻辑,以提高您的应用的可靠性。
实现流程
集成并使用 ZIM SDK
如果您之前没有使用过 ZIM SDK,您可以阅读以下部分:
使用 Swift Package Manager 自动集成 SDK
-
打开 Xcode 并点击菜单栏 “File > Add Packages...”,在 “Apple Swift Packages” 弹窗的 “Search or Enter Package URL” 输入框中填写如下 URL 并敲击回车键确认:
https://github.com/zegolibrary/zim-ios
-
在 “Dependency Rule” 中指定你想要集成的 SDK 版本(建议使用默认的 “Up to Next Major Version”),然后点击 “Add Package“ 导入 SDK。你也可以参考 Apple 官方文档 进行设置。
-
导入 ZIM SDK。
#import <ZIM/ZIM.h>
-
在 SDK 中创建 ZIM 实例,一个实例对应的是一个用户,表示一个用户以客户端的身份登录系统。
- (void)initWithAppID:(UInt32)appID appSign:(NSString *)appSign { ZIMAppConfig *zimConfig = [[ZIMAppConfig alloc] init]; zimConfig.appID = appID; zimConfig.appSign = appSign; self.zim = [ZIM shared]; if (self.zim == nil) { self.zim = [ZIM createWithConfig:zimConfig]; } [self.zim setEventHandler:self]; }
稍后,我们将为您提供如何使用 ZIM SDK 实现连麦功能的详细指南。
(必选)管理多个 SDK
在本文档描述的直播场景中,您需要使用 ZIM SDK
来实现连麦功能,然后使用 Express SDK
实现直播功能。
为了使您的应用代码更加有条理,我们建议您通过使用以下方式来管理这些 SDK:
我们将在后续文档中使用 ZIMService 和 ExpressService 进行示例说明。
为 ZIM SDK
创建一个 ZIMService
类,它管理与 SDK 的交互并存储所需数据,详情请参考 ZIMService.swift 中的完整代码。
@interface ZIMService : NSObject
// ...
- (void)initWithAppID:(UInt32)appID appSign:(NSString *)appSign {
ZIMAppConfig *zimConfig = [[ZIMAppConfig alloc] init];
zimConfig.appID = appID;
zimConfig.appSign = appSign ?: @"";
self.zim = [ZIM shared];
if (self.zim == nil) {
self.zim = [ZIM createWithConfig:zimConfig];
}
[self.zim setEventHandler:self];
}
// ...
@end
同样,为 Express SDK
创建一个 ExpressService
类,它管理与 SDK 的交互并存储所需数据,详情请参考 ExpressService.swift 中的完整代码。
@interface ExpressService : NSObject
// ...
- (void)initWithAppID:(UInt32)appID appSign:(NSString *)appSign {
ZegoEngineProfile *profile = [[ZegoEngineProfile alloc] init];
profile.appID = appID;
profile.appSign = appSign;
profile.scenario = ZegoScenarioBroadcast;
[ZegoExpressEngine createEngineWith:profile eventHandler:self];
}
// ...
@end
您可以根据需求,向服务中添加相关 SDK 接口的方法。
例如,当您需要登录时,可以向 ZIMService
添加 connectUser
方法。
@interface ZIMService : NSObject
// ...
- (void)connectUserWithUserID:(NSString *)userID
userName:(NSString *)userName
token:(NSString *)token
callback:(CommonCallback)callback {
ZIMUserInfo *user = [[ZIMUserInfo alloc] init];
user.userID = userID;
user.userName = userName;
self.userInfo = user;
[self.zim loginWithUser:user token:token ?: @"" completion:^(NSError *error) {
if (callback) {
callback((int64_t)error.code, error.localizedDescription);
}
}];
}
@end
完整代码,请参考 ZegoSDKManager.swift。
@interface ZegoSDKManager : NSObject
@property (class, nonatomic, readonly) ZegoSDKManager *shared;
- (void)initWithAppID:(UInt32)appID appSign:(NSString *)appSign;
@end
@implementation ZegoSDKManager
+ (instancetype)shared {
static ZegoSDKManager *sharedInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
sharedInstance = [[ZegoSDKManager alloc] init];
});
return sharedInstance;
}
- (void)initWithAppID:(UInt32)appID appSign:(NSString *)appSign {
[self.expressService initWithAppID:appID appSign:appSign];
[self.zimService initWithAppID:appID appSign:appSign];
}
@end
通过此方式,您已实现了一个单例类,来管理您需要的 SDK 服务。因此,您可以在项目的任何地方获取此类的实例,并使用它来执行与 SDK 相关的逻辑,例如:
- 当应用启动时:调用
ZegoSDKManager.shared.initWithAppID(appID,appSign);
。 - 当需要登录时:调用
ZegoSDKManager.shared.loginRoom(roomID,scenario,callback);
。 - 当结束直播时:调用
ZegoSDKManager.shared.logoutRoom();。
稍后,我们将介绍如何基于此添加连麦功能。
发送或取消连麦请求
发送和取消连麦请求的实现相似,只是信令的类型不同。此处以发送为例,来说明示例代码的实现。
在示例代码中,直播页面(观众视角)的右下角放置了一个申请连麦按钮。当按钮被点击时,将执行以下操作。
- 信令的 JSON 编码,其中
room_request_type
在示例代码中定义为applyCoHost
。 - 调用示例代码中封装的 sendRoomRequest 发送信令。(
sendRoomRequest
简化了ZIM SDK
的 sendMessage 接口。)- 如果方法调用成功:本地端(即观众端)的申请状态将切换为申请连麦,请求连麦按钮将切换为取消连麦。
- 如果方法调用失败:将提示错误信息。在实际应用开发中,建议您使用更用户友好的 UI 来提示连麦申请失败。
- (IBAction)coHostAction:(UIButton *)sender {
//...
RoomRequestType requestType = sender.isSelected ? RoomRequestTypeApplyCoHost : RoomRequestTypeCancelCoHostApply;
NSDictionary *commandDict = @{@"room_request_type": @(requestType)};
if (requestType == RoomRequestTypeApplyCoHost) {
// sendRoomRequest 为示例代码封装方法:https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/master/best_practice/ZegoCloudSDKDemo/Internal/SDK/ZIM/ZIMService%2BRoomRequest.swift#L9
[[ZegoSDKManager sharedManager].zimService sendRoomRequest:receiverID extendedData:[commandDict jsonString] completion:^(int code, NSString *message, NSString *messageID) {
//...
}];
} else {
RoomRequest *roomRequest = [ZegoSDKManager sharedManager].zimService.roomRequestDict[mRoomRequest.requestID ?: @""];
if (!roomRequest) { return; }
//cancelRoomRequest 为示例代码封装方法:https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/master/best_practice/ZegoCloudSDKDemo/Internal/SDK/ZIM/ZIMService%2BRoomRequest.swift#L67
[[ZegoSDKManager sharedManager].zimService cancelRoomRequest:roomRequest completion:^(int code, NSString *message, NSString *messageID) {
//...
}];
}
}
- 本地端(观众端)将等待主播的响应。
- 如果主播拒绝了连麦请求:本地端的申请状态将切换为不申请。
- 如果主播接受了连麦请求:将开始连麦(有关开始和结束连麦的详细信息,请参考 开始或结束连麦部分)。
接受或拒绝连麦请求
- 在示例代码中,当主播收到连麦请求的信令时,将弹出确认对话框,等待主播选择接受或拒绝。
- 主播响应后,会发送接受或拒绝的信令。此处不再进一步描述发送信令的相关逻辑。完整代码可以在 ApplyCoHostListViewController.swift 中查看。
相关代码片段如下,完整代码可在 ApplyCoHostListViewController.swift 中找到。
- (void)onInComingRoomRequestReceived:(RoomRequest *)request {
[self.tableView reloadData];
}
- (void)onInComingRoomRequestCancelled:(RoomRequest *)request {
[self.tableView reloadData];
}
- (void)agreeCoHostApply:(RoomRequest *)request {
RoomRequest *roomRequest = ZegoSDKManager.shared.zimService.roomRequestDict[request.requestID];
if (!roomRequest) {
return;
}
//acceptRoomRequest 为示例代码封装方法 https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/master/best_practice/ZegoCloudSDKDemo/Internal/SDK/ZIM/ZIMService%2BRoomRequest.swift#L27
[ZegoSDKManager.shared.zimService acceptRoomRequest:roomRequest completion:^(NSInteger code, NSString *message, NSString *messageID) {
//...
[self.tableView reloadData];
}];
}
- (void)disAgreeCoHostApply:(RoomRequest *)request {
RoomRequest *roomRequest = ZegoSDKManager.shared.zimService.roomRequestDict[request.requestID];
if (!roomRequest) {
return;
}
//rejectRoomRequest 为示例代码封装方法:https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/master/best_practice/ZegoCloudSDKDemo/Internal/SDK/ZIM/ZIMService%2BRoomRequest.swift#L47
[ZegoSDKManager.shared.zimService rejectRoomRequest:roomRequest completion:^(NSInteger code, NSString *message, NSString *messageID) {
//...
[self.tableView reloadData];
}];
}
开始连麦
当观众接收到主播同意连麦的信令时,他们可以通过调用 Express SDK
的相关方法来预览和推流,从而成为连麦主播。完整代码可以在 LiveStreamingViewController.swift and ExpressService+Stream.swift 中查看。
- (void)addCoHost:(NSString *)streamID userID:(NSString *)userID userName:(NSString *)userName isMySelf:(BOOL)isMySelf {
BOOL isHost = [streamID hasSuffix:@"_host"];
if (isHost) {
//startPublishingStream 为示例代码封装方法:https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/master/best_practice/ZegoCloudSDKDemo/Internal/SDK/Express/ExpressService%2BStream.swift#L24
[[ZegoSDKManager shared].expressService startPlayingStream:mainVideoView.renderView streamID:streamID];
[self updateUserNameLabel:userName];
[mainVideoView updateWithUserID:userID userName:userName];
} else {
if (isMySelf) {
[[ZegoSDKManager shared].expressService startPublishingStream:streamID];
}
CoHostViewModel *coHostViewModel = [[CoHostViewModel alloc] init];
coHostViewModel.user = [[ZegoSDKUser alloc] initWithId:userID name:userName];
coHostViewModel.streamID = streamID;
[coHostVideoViews insertObject:coHostViewModel atIndex:0];
coHostVideoContainerView.coHostModels = coHostVideoViews;
[self updateCoHostContainerFrame];
}
}
结束连麦
当观众结束连麦后,他们需要调用 Express SDK
的相关方法停止预览和推流,完整的代码可以在 LiveStreamingViewController.swift 中查看。以下是关键代码:
- (IBAction)endCoHostAction:(UIButton *)sender {
NSString *localUserID = ZegoSDKManager.shared.expressService.currentUser.id;
//stopPublishingStream 为示例代码封装方法:https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/master/best_practice/ZegoCloudSDKDemo/Internal/SDK/Express/ExpressService%2BStream.swift#L35
[ZegoSDKManager.shared.expressService stopPublishingStream];
//stopPreview 为示例代码封装方法:https://github.com/ZEGOCLOUD/zegocloud_sdk_demo_ios/blob/44169b0962cf1a08fbb6e798eb452de6c66554f6/best_practice/ZegoCloudSDKDemo/Internal/SDK/Express/ExpressService%2BStream.swift#L20
[ZegoSDKManager.shared.expressService stopPreview];
self.coHostVideoViews = [self.coHostVideoViews filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(ZegoUserView * _Nullable userView, NSDictionary<NSString *,id> * _Nullable bindings) {
return ![userView.user.id isEqualToString:localUserID];
}]];
self.coHostVideoContainerView.coHostModels = self.coHostVideoViews;
[self updateCoHostContainerFrame];
self.coHostButton.hidden = liveManager.isPKStarted;
self.endCoHostButton.hidden = YES;
self.flipButton.hidden = YES;
self.micButton.hidden = YES;
self.cameraButton.hidden = YES;
self.flipButtonConstraint.constant = 16;
}
在实现视频通话、直播等视频场景时,请注意视频分辨率与价格之间的关系。
在同一房间内播放多个视频流时,将基于分辨率总和进行计费,并且不同分辨率将对应不同的计费档次。
计算最终分辨率时,包括以下视频流:
- 直播视频画面(例如主播画面、连麦画面、PK 对战画面等)
- 视频通话中,每个人的视频画面
- 屏幕共享画面
- 云录制服务的分辨率
- 直播创作分辨率
在您的应用上线前,请确保您已检查所有配置并确认适用于您业务场景的计费档次,以避免不必要的损失。更多详情请参见 定价。
完成连麦功能
恭喜您,完成上述步骤后,您已经实现了连麦功能。