This doc will introduce how to implement the co-hosting feature in the live streaming scenario.
Before you begin, make sure you complete the following:
You can achieve the following effect with the demo provided in this doc:
Homepage | Live stream page | Receive co-hosting request | Start co-hosting |
---|---|---|---|
The process of co-hosting implemented based on signaling, signaling is a protocol or message to manage communication and connections in networks. ZEGOCLOUD packages all signaling capabilities into a SDK, providing you with a readily available real-time signaling API.
The ZIM SDK provides rich functionality for sending and receiving messages, see Send & Receive messages (signaling). And here, you will need to use the customizable signaling message: ZIMCommandMessage
Complete demo code for this section can be found at ZIMService.swift.
(1) Send signals (ZIMCommandMessage
) in the room by calling sendMessage
with the following:
let bytes = protocolStr.data(using: .utf8)!
let commandMessage = ZIMCommandMessage(message: bytes)
zim?.sendMessage(commandMessage, toConversationID: roomID, conversationType: .room, config: ZIMMessageSendConfig(), notification: nil, callback: { msg, error in
// ...
callback?(Int64(error.code.rawValue), error.message)
})
(2) After sending, other users in the room will receive the signal from the receiveRoomMessage callback. You can listen to this callback by following below:
extension ZIMService: ZIMEventHandler {
// ...
func zim(_ zim: ZIM, receiveRoomMessage messageList: [ZIMMessage], fromRoomID: String) {
for message in messageList {
if message.type == .command {
let commandMessage = message as! ZIMCommandMessage
let customSignalingJson = String(data: commandMessage.message, encoding: .utf8)!
//...
} else {
// ..
}
}
}
// ...
}
Complete demo code for this section can be found at ZIMService+RoomRequest.swift
JSON signal encoding
Since a simple String
itself is difficult to express complex information, signals can be encapsulated in JSON
format, making it more convenient for you to organize the protocol content of the signals.
Taking the simplest JSON signal as an example: {"room_request_type": 10000}
, in such a JSON signal, you can use the room_request_type
field to express different signal types, such as:
{"room_request_type": 10000}
{"room_request_type": 10001}
{"room_request_type": 10002}
{"room_request_type": 10003}
In addition, you can also extend other common fields for signals, such as senderID
and receiverID
, such as:
extension ZIMService {
func sendRoomRequest(_ receiverID: String, extendedData: String, callback: RoomRequestCallback?){
// ...
}
}
public class RoomRequest: NSObject, Codable {
public var requestID : String
public var actionType: RoomRequestAction
public var senderID: String
public var receiverID: String
public var extendedData: String
init(requestID: String = "", actionType: RoomRequestAction, senderID: String, receiverID: String, extendedData: String = "") {
self.requestID = requestID
self.actionType = actionType
self.senderID = senderID
self.receiverID = receiverID
self.extendedData = extendedData
}
public func jsonString() -> String? {
let jsonObject: [String: Any] = ["action_type" : actionType.rawValue, "sender_id": senderID, "receiver_id": receiverID, "request_id": requestID, "extended_data": extendedData]
return jsonObject.jsonString
}
}
JSON signal decoding
And users who receive signals can process specific business logic based on the fields in it, such as:
extension LiveStreamingViewController: ZIMServiceDelegate {
func onInComingRoomRequestReceived(request: RoomRequest) {
showReaDot()
}
func onInComingRoomRequestCancelled(request: RoomRequest) {
showReaDot()
}
func onOutgoingRoomRequestAccepted(request: RoomRequest) {
onReceiveAcceptCoHostApply()
}
func onOutgoingRoomRequestRejected(request: RoomRequest) {
onReceiveRefuseCoHostApply()
}
}
Further extending signals
Based on this pattern, when you need to do any protocol extensions in your business, you only need to extend the type
field of the signal to easily implement new business logic, such as:
Friendly reminder: After reading the following text and further understanding the implementation of co-hosting signals, you will be able to easily extend your live streaming business signals.
The demo in this document is a pure client API + ZEGOCLOUD solution. If you have your own business server and want to do more logical extensions, you can use our Server API to pass signals and combine your server's room business logic to increase the reliability of your app.
sequenceDiagram participant Alice participant appServer as App Server participant server as ZEGOCLOUD Server participant sdk as SDK participant Bob Alice ->> appServer : Send co-hosting request appServer ->> appServer : Process your own business logic appServer ->> server: Alice's co-hosting request server ->> sdk : Alice's co-hosting request sdk ->> Bob : Alice's co-hosting request Note over Alice, Bob: ...
If you have not used the ZIM SDK before, you can read the following section:
Integrate the SDK automatically with Swift Package Manager
Open Xcode and click "File > Add Packages..." in the menu bar, enter the following URL in the "Search or Enter Package URL" search box of the "Apple Swift Packages" pop-up window:
https://github.com/zegolibrary/zim-ios
Specify the SDK version you want to integrate in "Dependency Rule" (Recommended: use the default rule "Up to Next Major Version"), and then click "Add Package" to import the SDK. You can refer to Apple Documentation for more details.
After successful integration, you can use the Zim SDK like this:
import ZIM
Creating a ZIM instance is the very first step, an instance corresponds to a user logging in to the system as a client.
func initWithAppID(_ appID: UInt32, appSign: String?) {
let zimConfig: ZIMAppConfig = ZIMAppConfig()
zimConfig.appID = appID
zimConfig.appSign = appSign
self.zim = ZIM.shared()
if self.zim == nil {
self.zim = ZIM.create(with: zimConfig)
}
self.zim?.setEventHandler(self)
}
Later on, we will provide you with detailed instructions on how to use the ZIM SDK to develop the co-hosting feature.
In most cases, you need to use multiple SDKs together. For example, in the live streaming scenario described in this doc, you need to use the zim sdk
to implement the co-hosting feature, and then use the zego_express_engine sdk
to implement the live streaming feature.
If your app has direct calls to SDKs everywhere, it can make the code difficult to manage and troubleshoot. To make your app code more organized, we recommend the following way to manage these SDKs:
Create a ZIMService
class for the zim sdk
, which manages the interaction with the SDK and stores the necessary data. Please refer to the complete code in ZIMService.swift.
class ZIMService: NSObject {
// ...
func initWithAppID(_ appID: UInt32, appSign: String?) {
let zimConfig: ZIMAppConfig = ZIMAppConfig()
zimConfig.appID = appID
zimConfig.appSign = appSign ?? ""
self.zim = ZIM.shared()
if self.zim == nil {
self.zim = ZIM.create(with: zimConfig)
}
self.zim?.setEventHandler(self)
}
// ...
}
Similarly, create an ExpressService
class for the zego_express_engine sdk
, which manages the interaction with the SDK and stores the necessary data. Please refer to the complete code in ExpressService.swift.
class ExpressService: NSObject {
// ...
public func initWithAppID(appID: UInt32, appSign: String) {
let profile = ZegoEngineProfile()
profile.appID = appID
profile.appSign = appSign
profile.scenario = .broadcast
ZegoExpressEngine.createEngine(with: profile, eventHandler: self)
}
// ...
}
With the service, you can add methods to the service whenever you need to use any SDK interface.
E.g., easily add the connectUser method to the ZIMService when you need to implement login:
class ZIMService: NSObject {
// ...
func connectUser(userID: String, userName: String, token: String?, callback: CommonCallback?) {
let user = ZIMUserInfo()
user.userID = userID
user.userName = userName
userInfo = user
zim?.login(with: user, token: token ?? "") { error in
callback?(Int64(error.code.rawValue), error.message)
}
}
}
As shown below. Please refer to the complete code in ZegoSDKManager.swift.
class ZegoSDKManager: NSObject {
static let shared = ZegoSDKManager()
var expressService = ExpressService.shared
var zimService = ZIMService.shared
func initWithAppID(_ appID: UInt32, appSign: String) {
expressService.initWithAppID(appID, appSign: appSign)
zimService.initWithAppID(appID, appSign: appSign)
}
}
In this way, you have implemented a singleton class that manages the SDK services you need. From now on, you can get an instance of this class anywhere in your project and use it to execute SDK-related logic, such as:
ZegoSDKManager.shared.initWithAppID(appID,appSign);
ZegoSDKManager.shared.loginRoom(roomID,scenario,callback);
ZegoSDKManager.shared.logoutRoom();
Later, we will introduce how to add co-hosting feature based on this.
The implementation of sending and canceling co-hosting requests is similar, with only the type of signal being different. Here, sending will be used as an example to explain the implementation of the demo.
In the Demo, a request co-host button has been placed in the lower right corner of the LivePage
as seen from the audience perspective. When the button is clicked, the following actions will be executed.
room_request_type
is defined as applyCoHost
in the demo.sendRoomRequest
to send the signal. (sendRoomRequest
simplifies the sendMessage
interface of ZIM SDK
.)Request Co-host
button will switch to Cancel CoHost
.
@IBAction func coHostAction(_ sender: UIButton) {
//...
let requestType: RoomRequestType = sender.isSelected ? .applyCoHost : .cancelCoHostApply
let commandDict: [String: AnyObject] = ["room_request_type": requestType.rawValue as AnyObject]
if requestType == .applyCoHost {
ZegoSDKManager.shared.zimService.sendRoomRequest(receiverID, extendedData: commandDict.jsonString) { code, message, messageID in
//...
}
} else {
let roomRequest: RoomRequest? = ZegoSDKManager.shared.zimService.roomRequestDict[mRoomRequest?.requestID ?? ""]
guard let roomRequest = roomRequest else { return }
ZegoSDKManager.shared.zimService.cancelRoomRequest(roomRequest) { code, message, messageID in
//...
}
}
}
The relevant code snippet is as follows, and the complete code can be found in ApplyCoHostListViewController.swift
func onInComingRoomRequestReceived(request: RoomRequest) {
tableView.reloadData()
}
func onInComingRoomRequestCancelled(request: RoomRequest) {
tableView.reloadData()
}
func agreeCoHostApply(request: RoomRequest) {
let roomRequest: RoomRequest? = ZegoSDKManager.shared.zimService.roomRequestDict[request.requestID]
guard let roomRequest = roomRequest else { return }
ZegoSDKManager.shared.zimService.acceptRoomRequest(roomRequest) { code, message, messageID in
//...
self.tableView.reloadData()
}
}
func disAgreeCoHostApply(request: RoomRequest) {
let roomRequest: RoomRequest? = ZegoSDKManager.shared.zimService.roomRequestDict[request.requestID]
guard let roomRequest = roomRequest else { return }
ZegoSDKManager.shared.zimService.rejectRoomRequest(roomRequest) { code, message, messageID in
//...
self.tableView.reloadData()
}
}
The logic after starting co-hosting is the same as Implementation. If you are not familiar with how to publish and play streams and render them, refer to Implementation.
When the audience receives the signal that the host agrees to co-host, they can become a co-host and start co-host live streaming by calling related methods of zego_express_engine
for previewing and publishing streams.
Complete code can be found in LiveStreamingViewController.swift and ExpressService+Stream.swift.
func addCoHost(_ streamID: String, _ userID: String, _ userName: String, isMySelf: Bool = false) {
// ...
let videoView = VideoView()
videoView.update(userID, userName)
coHostVideoViews.append(videoView)
if isMySelf {
ZegoSDKManager.shared.expressService.startPublishingStream(liveManager.getCoHostMainStreamID())
ZegoSDKManager.shared.expressService.startPreview(videoView.renderView,viewMode: .aspectFill)
} else {
ZegoSDKManager.shared.expressService.startPlayingStream(videoView.renderView, streamID: streamID)
}
// ...
}
After the audience ends co-hosting, they need to call relevant methods of zego_express_engine
to stop previewing and publishing streams. The complete code can be found in the endCoHostAction. And the key code is as follows:
@IBAction func endCoHostAction(_ sender: UIButton) {
let localUserID = ZegoSDKManager.shared.expressService.currentUser!.id
ZegoSDKManager.shared.expressService.stopPublishLocalVideo()
ZegoSDKManager.shared.expressService.stopPreview()
coHostVideoViews.forEach( { $0.removeFromSuperview() } )
coHostVideoViews = coHostVideoViews.filter({ $0.userID != localUserID })
updateCoHostConstraints()
coHostButton.isHidden = false
endCoHostButton.isHidden = true
flipButton.isHidden = true
micButton.isHidden = true
cameraButton.isHidden = true
flipButtonConstraint.constant = 16;
}
Resolution And Pricing Attention!
Please pay close attention to the relationship between video resolution and price when implementing video call, live streaming, and other video scenarios.
When playing multiple video streams in the same room, the billing will be based on the sum of the resolutions, and different resolutions will correspond to different billing tiers.
The video streams that are included in the calculation of the final resolution are as follows:
Before your app goes live, please make sure you have reviewed all configurations and confirmed the billing tiers for your business scenario to avoid unnecessary losses. For more details, please refer to Pricing.
Congratulations! Hereby you have completed the development of the co-hosting feature.
If you have any suggestions or comments, feel free to share them with us via Discord. We value your feedback.