PK battle is a friendly competition between two hosts, showcasing engaging interactions between the hosts for the audience to enjoy.
There are usually two ways to play:
This doc will introduce how to implement the PK battles 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 | Livestream page | Receive PK battle request | PK battle |
---|---|---|---|
Below is the structure and outline of the content in this doc:
If you are planning to implement the "PK battles coordinated by the business server", ignore this section.
Additionally, if your server does not have a signaling channel to send notifications to the client, we recommend using the Command message (signaling message)
of the ZIM server API Send in-room messages to achieve this.
A similar approach to call invitation is used here to implement PK battle invitations:
Based on the call invitation (signaling) feature provided by the In-app Chat (referred to as ZIM SDK), which provides the capability of call invitation, allowing you to send, cancel, accept, and reject an invitation, you can achieve PK battle invitations, room invitations, and similar functions - you can use the extendedData
field provided by ZIMCallInviteConfig, which allows you to customize the type of this invitation, thus achieving different functions.
For example, you can encode the business agreement into JSON and attach it to the extendedData
:
{
"room_id": "Room10001",
"user_name": "Alice",
"type": "start_pkbattles" // or "video_call" , "voice_call"
}
In this way, the receiving user can judge and execute different business logic based on the type
field after receiving the invitation.
The process of implementing call invitation based on this is as follows: (taking "Alice invites Bob to a PK battle, Bob accepts and connects the PK battle" as an example)
sequenceDiagram participant Alice participant sdk1 as SDK participant server as ZEGOCLOUD Server participant sdk2 as SDK participant Bob Alice ->> sdk1 : Send PK battles request sdk1 ->> server: Alice's PK battle request server ->> sdk2 : Alice's PK battle request sdk2 ->> Bob : Alice's PK battle request Bob ->> Bob : Bob agrees to PK battle with Alice Bob ->> sdk2 : Send signal agreeing to PK battle sdk2 ->> server: Bob's agreement signal to PK battle server ->> sdk1 : Bob's agreement signal to PK battle sdk1 ->> Alice : Bob's agreement signal to PK battle Note over Alice, Bob: start PK battle
For the specific usage of these interfaces, please refer to Call invitation (signaling).
When using this interface to implement PK battle invitations, it is important to note that in the extendedData
field of the invitation interface, both parties' information needs to be passed:
A When initiating a PK battle invitation, in addition to the mentioned type
, it is also necessary to pass one's own roomID
and userName
to the other party - so that the other party knows the relevant information to start the PK battle logic.
B When accepting a PK battle invitation, similarly, in addition to the type
, one's own roomID
and userName
need to be passed.
When inviting for the first time, please call callInvite
and set it to advanced mode. In this mode, you need to use callEnd
or callQuit
to end or quit the PK battle. And you can continue to invite others to join the PK by using callingInvite
.
var invitees:[String] = [] // List of invitees
invitees.append("421234") // ID of the invitee
let config = ZIMCallInviteConfig()
config.timeout = 200 // Timeout for invitation in seconds, range 1-600
// mode represents the call invitation mode, ADVANCED represents setting to advanced mode.
config.mode = .advanced
zim?.callInvite(with: invitees, config: config, callback: { requestID, sentInfo, errorInfo in
// The callID here is generated by the SDK internally to uniquely identify
a call invitation after the user initiates a call;
// later, when the initiator cancels the call, or the invitee accepts/rejects the call, this callID will be used.
})
For more details on these two aspects, refer to the following sections.
Before starting, please make sure you are familiar with the following concepts:
Through the stream-mixing service, multiple published media streams can compose a single stream , which allows audiences to play just one stream to improve quality and reduce performance cost.
For more details, refer to stream mixing
The PK Battles solution requires the use of stream mixing: stream mixing refers to combining multiple streams into one, so that audiences only need to play this stream to watch the footage of multiple hosts. The necessity and significant advantages of using stream mixing are as follows:
Before the PK battle starts, each host will publish a stream, and the audience can directly play the stream of the host. However, after the PK battle starts, the method of playing the stream will change:
The above is the basic framework of stream publishing & playing in the PK scenario. In the following sections, we will provide a detailed explanation of this solution based on the logic of the hosts and the audience.
When the host is ready to start the PK battle, the following operations need to be performed:
Generally, the stream ID is related to the room ID and user ID. For example, in the accompanying demo of this doc, the stream ID rule is "${roomID}_${userID}_main_host"
. Therefore, you can concatenate each host's stream ID using this rule and then call startPlayingStream
to play the stream of the opponent. The information of both sides can be obtained from the callback of the invitation interface and the extendedData
passed between both sides.
If your PK battle is scheduled and matched by the server and the start PK signal is sent down by the server, you need to include the
userID
,roomID
,userName
, and other information of the opponent host when sending the PK start notification to the host on the server side.
Stream mixing can be initiated by the client or the server, and you can choose accordingly:
The accompanying demo of this doc uses the "Manual stream mixing initiated by the host client" approach.
There are some details to note about the stream mixing parameters:
Stream mixing layout: Usually, during a PK battle, each audience member sees the host of their own room on the left side. Therefore, after the PK starts, stream mixing tasks need to be initiated - that is, each host in each room needs to initiate a stream mixing task. When mixing the streams, each host needs to place their own video on the left side of the stream mixing layout. The layout
parameter can be referred to in the following code or you can refer to the Mix the live streams doc for more details on stream mixing layouts.
Stream mixing resolution: Taking the default 540p resolution of the host as an example, each single stream has a resolution of width=540, height=960
. After combining the two streams side by side, the resolution of the mixed stream should be width=540*2, height=960
. If you want to lower the resolution of the mixed stream, you can maintain this aspect ratio and reduce the stream mixing resolution, for example, using a 540*480
resolution with width=540*2/2, height=960/2
. Note that if you need to lower the stream mixing resolution, you also need to make corresponding adjustments to the layout
parameter. Our demo uses a mixed stream resolution of width=810 and height=720
.
Stream mixing task ID and stream ID: Usually, each stream mixing task has only one output stream, and the same applies to the PK battle scenario. Therefore, you can use the same ID for both the stream mixing task ID and the stream ID, such as '${roomID}__mix'
. This makes it easier to manage the stream mixing tasks in the future.
Here is an example code snippet with the complete stream mixing parameters:
func updatePKMixTask(callback: ZegoMixerStartCallback?) {
guard let pkInfo = pkInfo else { return }
var pkStreamList: [String] = []
for pkUser in pkInfo.pkUserList {
if pkUser.hasAccepted {
pkStreamList.append(pkUser.pkUserStream)
}
}
let videoConfig = ZegoMixerVideoConfig()
videoConfig.resolution = MixVideoSize
videoConfig.bitrate = 1500
videoConfig.fps = 15
var mixInputList: [ZegoMixerInput] = []
if let layOutConfig = liveManager.getMixLayoutConfig(streamList: pkStreamList, videoConfig: videoConfig) {
mixInputList = layOutConfig
} else {
mixInputList = getMixVideoInputs(streamList: pkStreamList, videoConfig: videoConfig)
}
currentInputList = mixInputList
if let currentMixerTask = currentMixerTask {
currentMixerTask.setInputList(mixInputList)
} else {
let mixStreamID = "\(ZegoSDKManager.shared.expressService.currentRoomID ?? "")_mix"
currentMixerTask = ZegoMixerTask(taskID: mixStreamID)
currentMixerTask!.setVideoConfig(videoConfig)
currentMixerTask!.setInputList(mixInputList)
let mixerOutput: ZegoMixerOutput = ZegoMixerOutput(target: mixStreamID)
var mixerOutputList: [ZegoMixerOutput] = []
mixerOutputList.append(mixerOutput)
currentMixerTask!.setOutputList(mixerOutputList)
currentMixerTask!.enableSoundLevel(true)
currentMixerTask!.setAudioConfig(ZegoMixerAudioConfig.default())
}
ZegoSDKManager.shared.expressService.startMixerTask(currentMixerTask!) { errorCode, info in
if errorCode == 0 {
self.updatePKRoomAttributes()
}
guard let callback = callback else { return }
callback(errorCode, info)
}
}
In the client-initiated stream mixing approach, it is important to check the error code returned when calling the stream mixing interface at this step. If the error code is not 0, it means that the stream mixing has failed. In this case, appropriate actions should be taken on the client-side, such as retrying the stream mixing task, to ensure the normal progress of the PK battle.
If you want to set the layout for mixing the streams, you can customize the layout by using the setInputList
method of ZegoMixerTask
. Here we show some simple setting rules.
For example, if you have two people, you can set the layout to have each person occupying half of the screen. You can set it like this:
private func getMixVideoInputs(streamList: [String], videoConfig: ZegoMixerVideoConfig) ->
[ZegoMixerInput] {
var inputList: [ZegoMixerInput] = []
if (streamList.count == 2) {
for i in 0...1 {
let left = (Int(videoConfig.resolution.width) / streamList.count) * i
let top = 0
let width: Int = Int(MixVideoSize.width / 2)
let height: Int = Int(MixVideoSize.height)
let rect = CGRect(x: left, y: top, width: width, height: height)
let input = ZegoMixerInput(streamID: streamList[i], contentType: .video, layout: rect)
input.renderMode = .fill
input.soundLevelID = 0
input.volume = 100
inputList.append(input)
}
} else {
//...
}
return inputList;
}
If you have more than two people, you can set up the layout as N rows with N columns. You can set it up like this:
private func getMixVideoInputs(streamList: [String], videoConfig: ZegoMixerVideoConfig) ->
[ZegoMixerInput] {
var inputList: [ZegoMixerInput] = []
//...
if (streamList.count == 3) {
for i in 0...(streamList.count - 1) {
let left = i == 0 ? 0 : Int(MixVideoSize.width / 2);
let top = i == 2 ? Int(MixVideoSize.height / 2) : 0;
let width: Int = Int(MixVideoSize.width / 2)
let height: Int = i == 0 ? Int(MixVideoSize.height) : Int(MixVideoSize.height / 2)
let rect = CGRect(x: left, y: top, width: width, height: height)
let input = ZegoMixerInput(streamID: streamList[i], contentType: .video, layout: rect)
input.renderMode = .fill
input.soundLevelID = 0
input.volume = 100
inputList.append(input)
}
} else if (streamList.count == 4) {
let row: Int = 2
let column: Int = 2
let cellWidth = Int(Int(MixVideoSize.width) / column)
let cellHeight = Int(Int(MixVideoSize.width) / row)
var left: Int
var top: Int
for i in 0...(streamList.count - 1) {
left = cellWidth * (i % column)
top = cellHeight * (i < column ? 0 : 1)
let rect = CGRect(x: left, y: top, width: cellWidth, height: cellHeight)
let input = ZegoMixerInput(streamID: streamList[i], contentType: .video, layout: rect)
input.renderMode = .fill
input.soundLevelID = 0
input.volume = 100
inputList.append(input)
}
} else if (streamList.count == 5) {
var lastLeft: Int = 0
var height: Int = 432
for i in 0...(streamList.count - 1) {
if (i == 2) {
lastLeft = 0
}
let width: Int = i < 2 ? Int(MixVideoSize.width / 2) : Int(MixVideoSize.width / 3)
let left = lastLeft + (width * (i < 2 ? i : (i - 2)))
let top: Int = i > 1 ? height : 0
let rect = CGRect(x: left, y: top, width: width, height: height)
let input = ZegoMixerInput(streamID: streamList[i], contentType: .video, layout: rect)
input.renderMode = .fill
input.soundLevelID = 0
input.volume = 100
inputList.append(input)
}
} else if (streamList.count > 5) {
let row: Int = streamList.count % 3 == 0 ? (streamList.count / 3) : (streamList.count / 3) + 1;
let column: Int = 3
let cellWidth: Int = Int(MixVideoSize.width) / column
let cellHeight: Int = Int(MixVideoSize.height) / row
var left: Int
var top: Int
for i in 0...(streamList.count - 1) {
left = cellWidth * (i % column)
top = cellHeight * (i < column ? 0 : 1)
let rect = CGRect(x: left, y: top, width: cellWidth, height: cellHeight)
let input = ZegoMixerInput(streamID: streamList[i], contentType: .video, layout: rect)
input.renderMode = .fill
input.soundLevelID = 0
input.volume = 100
inputList.append(input)
}
}
return inputList;
}
So the demo has implemented the default layout for mixing streams: supports PK layout for 2 to 9 players.
If you need a more complex custom layout, please refer to this complete document on mixing layouts to understand the way of mixing layouts and use the ZEGOLiveStreamingManager.shared.eventDelegates.add(self)
in the Demo to modify the layout:
func getMixLayoutConfig(streamList: [String], videoConfig: ZegoMixerVideoConfig) -> [ZegoMixerInput] {
var inputList: [ZegoMixerInput] = []
// ... your logic
return inputList
}
If you want to manage stream mixing from the server, you need to start these two stream mixing tasks on the server side when the PK battle begins. Refer to the above instructions for setting the stream mixing parameters when initiating from the client.
For details on how to manage stream mixing tasks from the server side, refer to the server-side API:
Start stream mixing: Start stream mixing, Callback on stream mixing started
Stop stream mixing: Stop stream mixing, Callback on stream mixing stopped
In the server-initiated stream mixing approach, the server needs to use Callback on logged out room to monitor the client status of the host. If the server detects that the host has exited abnormally, it needs to promptly stop the corresponding stream mixing task and send a notification to the host to end the PK battle.
If your server does not have a signaling channel to send notifications to the client, we recommend using the Command message (signaling message)
in the Send in-room messages server-side API of ZIM to achieve this.
When the PK battle starts, it is necessary to notify the audience that the PK battle has begun. After receiving the notification, the audience can handle the logic of watching the PK. How to notify the audience? We recommend using the room attribute feature of the ZIM SDK to achieve this.
This feature allows app clients to set and synchronize custom room attributes in the room. Room attributes are stored on the ZEGOCLOUD server in a Key-Value manner, and the ZEGOCLOUD server handles write conflict arbitration and other issues to ensure data consistency.
At the same time, modifications made by app clients to room attributes are synchronized to all other audiences in the room in real-time through the ZEGOCLOUD server.
Each room allows a maximum of 20 attributes to be set, with a
key
length limit of 16 bytes and avalue
length limit of 1024 bytes.
When the PK starts, the host needs to call setRoomAttributes to set the additional attributes of the room, indicating that the room has entered the PK state. It is recommended to include the following information in the room's additional attributes:
host_user_id
: The userID of the host of this room.request_id
: The requestID of this PK.pk_users
: Participating anchors in PK.When setting the room attributes, make sure to set the isDeleteAfterOwnerLeft
parameter to false
. This is to prevent the room's additional attributes from being deleted when the host exits the room abnormally, which would cause the PK battle to be unable to be resumed.
Example code snippet for generating room attributes:
func updatePKRoomAttributes() {
guard let pkInfo = pkInfo else { return }
var pkDict: [String: String] = [:]
if let hostUser = liveManager.hostUser {
pkDict["host_user_id"] = hostUser.id
}
pkDict["request_id"] = pkInfo.requestID
var pkAcceptedUserList: [PKUser] = []
for pkUser in pkInfo.pkUserList {
if pkUser.hasAccepted {
pkAcceptedUserList.append(pkUser)
}
}
for pkUser in pkAcceptedUserList {
for zegoMixerInput in currentInputList {
if pkUser.pkUserStream == zegoMixerInput.streamID {
pkUser.edgeInsets = rectToEdgeInset(rect: zegoMixerInput.layout)
}
}
}
let pkUsers = pkAcceptedUserList.compactMap({ user in
let userJson = user.toDict()
return userJson
})
pkDict["pk_users"] = pkUsers.toJsonString()
let config = ZIMRoomAttributesSetConfig()
config.isDeleteAfterOwnerLeft = false
ZegoSDKManager.shared.zimService.setRoomAttributes(pkDict, callback: nil)
}
After the settings are completed, the audience will receive the onRoomAttributesUpdated callback. Now let's explain the logic on the audience side.
After receiving the onRoomAttributesUpdated callback, if the audience finds that there are newly added fields related to PK battle, they can start processing the logic of watching PK.
In the accompanying demo of this doc, the stream ID rule for mixing streams is
${currentRoomID}__mix
. It is recommended that you also design and use this kind of rule related to the room ID.
The method for playing normal streams and mixed streams is the same. The audience can call startPlayingStream
to start playing the mixed stream.
There are two details that need to be handled:
Once the audience successfully plays the mixed stream, they can start watching the PK battle. Since the mixed stream already contains the audio and video of both hosts, the audience doesn't need to render the single stream of the host during the PK.
Therefore, the audience can use mutePlayStreamAudio and mutePlayStreamVideo to temporarily stop playing the audio and video of the host's single stream. This can further reduce costs and avoid unnecessary traffic consumption and performance loss on the audience's devices.
It is not recommended to use stopPlayingStream to stop playing the single stream of the host at this time. If this is done, the audience will need to re-play the stream after the PK ends, and the speed of stream switching will be much slower compared to using
mute
. This will result in a poor user experience.
In the demo, the host can manually click the end button to terminate the PK battle.
When the host clicks the end button, they also need to notify the other host that the PK has ended. This can be achieved by endPKBattle
method in ZEGOLivesSreamingManager
.When the other host receives this notification, they also need to handle the logic for ending the PK. The difference between quitPKBattle
and endPKBattle
is that the former only allows the player to quit the PK, while the latter will make everyone stop PK.
The following operations need to be performed to end the PK battle:
Host:
Audience:
When the audience receives the callback onRoomAttributesUpdated indicating that the PK-related attributes have been removed, they can start handling the following logic:
By leveraging the periodic sending of SEI messages, it can be treated as a "heartbeat" between clients, and the heartbeat can be used to detect abnormal situations in a PK. Specifically, when SEI messages from the host are not received for a certain period of time, it can be assumed that the host is experiencing an abnormal situation. The logic is as follows:
When an abnormality is detected in a host, the demo will render a "host reconnecting" prompt on the host's video screen.
func onPKUserConnecting(userID: String, duration: Int) {
if userID == pkUser?.userID {
//...
connectingTipView.isHidden = duration > 5000 ? false : true
}
}
In addition, you also need to define a maximum timeout period. For example, in the Demo, if a PK host has no SEI for more than 60 seconds, all users participating in the PK will remove that exceptional host from the PK.
See LiveStreamingViewController.swift
func onPKUserConnecting(userID: String, duration: Int) {
if duration > 60000 {
if userID != ZegoSDKManager.shared.currentUser?.id {
liveManager.removeUserFromPKBattle(userID: userID)
} else {
liveManager.quitPKBattle()
}
}
}
If you want to challenge a random host to PK, you may need to use the server for matchmaking. For example, the client can send a request to the server, and the server will respond with the user ID of the target host. After receiving this user ID, the client can call the startPKBattle
function to send an automatic PK request to the target host using this user ID. When using this interface, the host receiving the PK request will automatically agree to start the PK by default.
Depending on the specific streaming scenario, different methods are used to obtain the device status.
Scenario 1: During PK, how do hosts obtain the device status of each other?
In this scenario, where hosts are streaming and interacting with each other in real-time, you can use onRemoteCameraStateUpdate and onRemoteMicStateUpdate to obtain the camera and microphone status of the other host.
It is important to note that this feature needs to be enabled by calling setEngineConfig after calling createEngine. Here is an example of the code:
let config: ZegoEngineConfig = ZegoEngineConfig()
config.advancedConfig = ["notify_remote_device_unknown_status": "true", "notify_remote_device_init_status":"true"]
ZegoExpressEngine.setEngineConfig(config)
If your audience is using
Interactive Live Streaming
to play the stream, you can also use this method. You can further understand the concepts ofLive Streaming
andInteractive Live Streaming
here: Live Streaming vs. Interactive Live Streaming
Scenario 2: Audience obtaining the device status of the host
When playing the stream for Live Streaming
or Mixed Stream
, it is recommended to use the SEI (Supplemental Enhancement Information) solution to obtain the device status of the host. This includes the following two cases in the PK battle scenario:
In this case, the audience cannot receive the callbacks mentioned in "Scenario 1". Therefore, when the host is publishing the stream, they need to update their device status using sendSEI, and the playing side will receive the callback onPlayerRecvSEI.
For a detailed explanation of the SEI feature, refer to Convey extra information using SEI.
Additional information about SEI and stream mixing: The mixing server will re-encode the SEI of all input streams into the output mixed stream. Therefore, the SEI information sent by the host can be received by the audience in both rooms and the other host.
To send SEI, you need to create a timer to periodically send SEI messages. It is recommended to send them every 200ms. In the timer, regularly send the following information to synchronize device status:
{
'type': 0, // device_state
'senderID': myUserID, // Due to the mixing of SEI from multiple streams into the mixed stream, you need to add a senderID identifier in the SEI.
'cam': false, // true:on, false:off
'mic': true, // true:on, false:off
}
You can refer to the relevant code in the demo for the specific implementation of this part.
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.
In most scenarios, co-hosting is not supported during a PK battle. Therefore, if you have implemented the co-hosting feature, you need to consider this: Once the room enters the PK battle state, all client roles need to temporarily disable all co-hosting-related functionalities.
Here are some specific recommendations:
In general, the sound level can be rendered using Visualize the sound level.
However, when the audience plays the mixed stream, the approach is slightly different:
enableSoundLevel
to true
so that the mixed stream includes the sound level information of the input streams.soundLevelID
to each input stream to allow the playing side to determine whose sound level data it is.soundLevelID
can be used to determine whose sound level it belongs to.During a PK, there may be a need to temporarily mute the audio of the other host. After muting, both the host and the audience in the room will not be able to hear the audio of the other host.
contentType
of the other host's input stream to ZegoMixerInputContentTypeVideoOnly
(There is no need to call StopMixerTask
, just call startMixerTask
directly).After the client successfully initiates the mixing of streams, the client can report to the business server that the room has entered the PK state.
You can use git diff to view these changes and follow our documentation to check the changes item by item and add them to your project.
First, you need to upgrade the SDK version in Package Dependencies, please use the following or higher version of SDK.
ZIM 2.13.1
ZegoExpressEngine 3.12.4
The 'internal' of the demo has been fully upgraded to adapt to the multi-player PK function. This part has undergone significant changes. We recommend that you directly delete the old 'internal' folder and copy the new 'internal' to your project.
We do not recommend making any modifications to the internal
.
However, if you have to modify the code in internal
for some reason, after copying the new internal
to your project, you need to apply the changes you made to the old internal
to the new internal
as well. This is an inevitable step.
The 'components' and 'ViewControllers' of the demo, as well as the layout files in 'res', have also been upgraded to adapt to the multi-player PK. The changes in this part are relatively small:
New files can be directly copied to your project.
There are some changes in the old files, and you can refer to git diff to gradually upgrade.
After completing the upgrade, please do comprehensive testing on the new and old functions. If you encounter any suspected bugs during testing, please test our demo directly in the same way to identify whether it is a problem with the demo or an issue introduced by other reasons.