Skip to main content
Version: 3.x.x

Relay Media(PK Host) - Android

Overview​

The Relay Media feature enables hosts to relay their media (audio, video, screen share) to one or multiple other live streams. This creates cross-stream interactions similar to "PK battles" or collaborative broadcasts.

This feature allows audiences in one live stream to view and hear hosts from another live stream without changing rooms.

Key Concepts​

  • Media Relay: The process of transmitting a host's audio/video from one live stream to another
  • Source Room: The original live stream where the host is broadcasting
  • Destination Room: The target live stream(s) where the host's media will be relayed
  • Relay Kinds: The types of media that can be relayed: camera video, microphone audio and screen share (StreamKind.VIDEO, StreamKind.AUDIO, StreamKind.SHARE)

Sequence Diagram​

Implementation Guide​

note
  • requestMediaRelay() and stopMediaRelay() are suspend functions. Call them from a coroutine, such as lifecycleScope.launch (from androidx.lifecycle:lifecycle-runtime-ktx).
  • If the request fails, they throw VideoSDKException. requestMediaRelay() and stopMediaRelay() also throw it with 3073 (ERROR_INVALID_PARAMETER) when the destination room ID is empty.
  • Called before the live stream is joined, or while it is reconnecting, they do nothing and report 3035 (ERROR_ACTION_PERFORMED_BEFORE_ROOM_JOINED) or 3065 (ERROR_ROOM_RECONNECTING) on onError instead of throwing.

1. Requesting Media Relay​

requestMediaRelay()​

Parameters:

  • destinationRoomId (String, required) – ID of the target live stream
  • token (String?, default null) – Authentication token for the destination live stream.
    If you pass null, the SDK will use the existing authentication token.
  • kinds (List<StreamKind>?, default null) – Media types to relay. If null, audio, video and share are all relayed; an empty list relays none. Options:
    • StreamKind.VIDEO – Camera video
    • StreamKind.AUDIO – Microphone audio
    • StreamKind.SHARE – Screen share video
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import live.videosdk.rtc.android.Room
import live.videosdk.rtc.android.StreamKind
import live.videosdk.rtc.android.lib.VideoSDKException

class LiveStreamActivity : AppCompatActivity() {
// The Room returned by VideoSDK.createRoom()
private var liveStream: Room? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...
val switchButton: Button = findViewById(R.id.switchLiveStreamButton)
switchButton.setOnClickListener {
requestMediaRelayToAnotherRoom()
}
}

private fun requestMediaRelayToAnotherRoom() {
val liveStreamId = "liveStream-B"
// Secure token to authenticate in the destination live stream
val relayToken = "secureToken123"
// Specify the media types to relay
val mediaTypes = listOf(StreamKind.VIDEO, StreamKind.AUDIO)
lifecycleScope.launch {
try {
// Request media relay
liveStream!!.requestMediaRelay(
destinationRoomId = liveStreamId,
token = relayToken,
kinds = mediaTypes,
)
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Relay request failed: ${e.code} ${e.name}")
}
}
}
}

2. Handling Relay Requests (Destination Room)​

In the destination live stream, hosts (participants in SEND_AND_RECV mode) receive relay requests in onMediaRelayRequestReceived:

import android.content.Context
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import live.videosdk.rtc.android.Room
import live.videosdk.rtc.android.listeners.RoomEventListener

// liveStream is the Room returned by VideoSDK.createRoom()
class LiveStreamView(private val context: Context, private val liveStream: Room) {
init {
// Set up media relay request listener
liveStream.addEventListener(object : RoomEventListener() {
override fun onMediaRelayRequestReceived(
participantId: String,
roomId: String,
displayName: String,
accept: () -> Unit,
reject: () -> Unit,
) {
showRelayRequest(displayName, roomId, accept, reject)
}
})
}

private fun showRelayRequest(displayName: String, roomId: String, accept: () -> Unit, reject: () -> Unit) {
MaterialAlertDialogBuilder(context)
.setTitle("Relay Request : $displayName")
.setMessage("$displayName from $roomId wants to relay their media to this live stream")
.setPositiveButton("Approve") { _, _ -> accept() }
.setNegativeButton("Deny") { _, _ -> reject() }
.setCancelable(false)
.show()
}
}

roomId is the ID of the requesting live stream. Nothing is relayed until you call accept() or reject(). After accept(), the relayed host joins this live stream as a participant, so render them from onParticipantJoined as you would any host. This live stream gets no onMediaRelayStarted. Both need a joined live stream. If the live stream has ended or is reconnecting when the host answers, the answer is not sent and the reason arrives on onError.

3. Handling Request Responses (Source Room)​

In the source live stream, the answer to your request arrives in onMediaRelayRequestResponse:

import live.videosdk.rtc.android.Room
import live.videosdk.rtc.android.lib.RelayDecision
import live.videosdk.rtc.android.listeners.RoomEventListener

// liveStream is the Room returned by VideoSDK.createRoom()
class LiveStreamView(private val liveStream: Room) {
init {
// Set up media relay request response listener
liveStream.addEventListener(object : RoomEventListener() {
override fun onMediaRelayRequestResponse(decision: RelayDecision?, decidedBy: String, roomId: String) {
handleMediaRelayResponse(decision, decidedBy, roomId)
}
})
}

// Handling the media relay request response
private fun handleMediaRelayResponse(decision: RelayDecision?, decidedBy: String, roomId: String) {
if (decision == RelayDecision.ACCEPTED) {
// Relay request accepted by participant
println("Relay request to $roomId accepted by $decidedBy")
} else {
// Relay request rejected by participant
println("Relay request to $roomId rejected by $decidedBy")
}
}
}
  • decision: RelayDecision.ACCEPTED or RelayDecision.REJECTED. It is null if the server sent neither.
  • decidedBy: the participant who answered.
  • roomId: the room that answered. It is empty if the server did not say.

An accepted request is followed by onMediaRelayStarted in the source live stream. A rejected one is followed by nothing.

4. Tracking Active Relays​

In the source live stream, the requesting host gets onMediaRelayStarted with the destination live stream's ID once the relay is running:

import android.util.Log
import live.videosdk.rtc.android.Room
import live.videosdk.rtc.android.listeners.RoomEventListener

// liveStream is the Room returned by VideoSDK.createRoom()
class LiveStreamView(private val liveStream: Room) {
companion object {
private const val TAG = "LiveStreamView"
}
init {
// Set up media relay started listener
liveStream.addEventListener(object : RoomEventListener() {
override fun onMediaRelayStarted(roomId: String) {
handleMediaRelayStarted(roomId)
}
})
}
// Called when media relay starts
private fun handleMediaRelayStarted(liveStreamId: String) {
Log.d(TAG, "Media relay started to $liveStreamId")
// You can update UI or handle logic here
}
}

5. Stopping Media Relay​

To stop an ongoing relay:

stopMediaRelay()​

Parameters:

  • destinationRoomId (String, required): ID of the live stream where the relay should stop
import android.os.Bundle
import android.util.Log
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import live.videosdk.rtc.android.Room
import live.videosdk.rtc.android.lib.VideoSDKException

class LiveStreamActivity : AppCompatActivity() {
// The Room returned by VideoSDK.createRoom()
private var liveStream: Room? = null

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//..
val stopRelayButton = findViewById<Button>(R.id.btnStopRelay)
stopRelayButton.setOnClickListener {
handleStopMediaRelay()
}
}

// Called to stop media relay
private fun handleStopMediaRelay() {
val liveStreamId = "liveStream-B"
lifecycleScope.launch {
try {
liveStream!!.stopMediaRelay(liveStreamId)
} catch (e: VideoSDKException) {
Log.e("VideoSDK", "Stop relay failed: ${e.code} ${e.name}")
}
}
}
}

6. Handling Relay Stop Events​

When a relay stops for any reason, the requesting host gets onMediaRelayStopped with the destination live stream's ID. In the destination live stream, the relayed host leaves on onParticipantLeft with LeaveReason.REMOVE_PEER_MEDIA_RELAY_STOP.

import android.util.Log
import live.videosdk.rtc.android.Room
import live.videosdk.rtc.android.listeners.RoomEventListener

// liveStream is the Room returned by VideoSDK.createRoom()
class LiveStreamView(private val liveStream: Room) {
companion object {
private const val TAG = "LiveStreamView"
}

fun setupListeners() {
liveStream.addEventListener(object : RoomEventListener() {
override fun onMediaRelayStopped(roomId: String, reason: String?) {
handleMediaRelayStopped(roomId, reason)
}
})
}

private fun handleMediaRelayStopped(liveStreamId: String, reason: String?) {
Log.d(TAG, "Relay to $liveStreamId stopped. Reason: $reason")
// Update UI based on stop reason
showMessage(if (reason == null) "Relay to $liveStreamId ended" else "Relay to $liveStreamId ended: $reason")
}

private fun showMessage(message: String) {
Log.i(TAG, message)
}
}

reason is the server's wording, for example "Requested by sdk." after stopMediaRelay(), or null when the server sends none. Show it, but don't branch on it.

7. Handling Relay Errors​

To handle errors that may occur during relay:

import android.util.Log
import live.videosdk.rtc.android.Room
import live.videosdk.rtc.android.listeners.RoomEventListener

// liveStream is the Room returned by VideoSDK.createRoom()
class LiveStreamView(private val liveStream: Room) {
companion object {
private const val TAG = "LiveStreamView"
}

private fun setupListeners() {
liveStream.addEventListener(object : RoomEventListener() {
override fun onMediaRelayError(roomId: String, error: String) {
handleMediaRelayError(roomId, error)
}
})
}

private fun handleMediaRelayError(liveStreamId: String, error: String) {
Log.e(TAG, "Relay error to $liveStreamId: $error")
}
}

An error is not always followed by onMediaRelayStopped, so clear your relay UI here too.

Use Cases​

  1. Guest Appearances: Allow popular hosts to make guest appearances in other streams without leaving their audience
  2. Cross-Stream Competitions: Create "battles" or competitions between hosts in different streams
  3. Multi-Location Broadcasting: Connect hosts from different physical locations into a shared experience
  4. Expert Commentary: Bring in subject matter experts to comment on events in another stream

Troubleshooting​

Common Issues​

  1. Relay Request Not Received

    • Verify both live streams are active
    • Check that the destination live stream ID is correct
    • Ensure the token has proper permissions
  2. Media Not Visible After Acceptance

    • Verify network connectivity
    • Check that appropriate media kinds were specified
    • Ensure the host has enabled their camera/microphone
  3. Unexpected Relay Termination

    • Check for network connectivity issues
    • Verify that both live streams remain active
    • Look for error events with specific reasons

API Reference​

Got a Question? Ask us on discord