Optimize video rendering
Updated
Best practices for reducing first-frame video rendering time.
First frame output time is the duration between when a user joins a channel and when they first see the remote video. A shorter first frame output time reduces perceived wait time by rendering video more quickly.
This guide describes two best practices to reduce video rendering time in Interactive Live Streaming.
Prerequisites
Complete the steps in the SDK Quickstart to build a basic Interactive Live Streaming app.
Understand the tech
To reduce video rendering time, Agora provides the following solutions:
-
Preload and initialize before joining the channel Complete time-consuming operations ahead of time, such as preloading the channel, configuring the rendering view, and enabling accelerated rendering for audio and video frames.
-
Join early, subscribe later Join the channel in advance but delay subscribing to the audio and video stream. When the user triggers the join operation, subscribe to the host's stream and begin rendering immediately.
The following table compares both solutions:
| Characteristic | Preload and initialize early | Join early, subscribe on demand |
|---|---|---|
| Applicable scenarios | Most audio and video use cases | Scenarios with very high requirements for first frame rendering speed |
| Core implementation | Initialize and configure video settings before joining | Join the channel early without subscribing; subscribe only when needed |
| Cost | Normal billing | May incur additional channel usage fees |
The following figure shows the time to output the first frame before optimization and with each solution:
Implement fast first-frame rendering
This section describes the implementation logic for both solutions.
The following figure illustrates the essential steps:
Sequence diagram for implementation
Set up a Video SDK instance
Creating and initializing the Video SDK engine takes time. To reduce first-frame display time, Agora recommends initializing the engine when the module is loaded, not when SDK functions are first called.
Initialize the engine only once. Avoid creating and destroying it multiple times.
class AgoraQuickStartViewController: UIViewController {
var agoraKit: AgoraRtcEngineKit?
override func viewDidLoad() {
super.viewDidLoad()
// Create and initialize the engine during view controller creation
initializeAgoraEngine()
}
private func initializeAgoraEngine() {
let config = AgoraRtcEngineConfig()
config.appId = "Your App ID"
config.channelProfile = .liveBroadcasting
config.delegate = self // You'll need to implement AgoraRtcEngineDelegate
agoraKit = AgoraRtcEngineKit.sharedEngine(with: config, delegate: self)
}
}Enable accelerated rendering
Call enableInstantMediaRendering to reduce the time it takes to render the first video frame and play audio after joining a channel.
- Call this method before joining a channel. Ideally, call it right after engine initialization.
- Both host and audience must call this method to benefit from faster rendering.
- To disable this feature, destroy the engine with
release, then reinitialize it.
// Enable accelerated rendering before joining the channel
agoraKit?.enableInstantMediaRendering()Set a video scenario
Use setVideoScenario to optimize performance for your specific use case. The SDK applies strategies tailored to the selected scenario.
For example, for a one-on-one call, use APPLICATION_SCENARIO_1V1.
// Set the video scenario
agoraKit?.setVideoScenario(.application1V1Scenario)Preload a channel
Joining a channel involves acquiring server resources and establishing a connection. Call preloadChannel to handle resource acquisition early and reduce join time.
- The
token,channelId, anduidmust match the values used injoinChannel. - Call
preloadChannelas soon as you retrieve the required info. - Don’t call
joinChannelimmediately afterpreloadChannel.
private func prepareChannelInfo() -> Int {
uid = getUid()
channelId = getChannelInfo()
token = getTokenFromServer(channelId: channelId, uid: uid)
// Preload the channel
agoraKit?.preloadChannel(byToken: token, channelId: channelId, uid: uid)
return 0 // Return success code, adjust as needed
}Set up the rendering view
Setting the rendering view early ensures the first frame displays properly. If the view is not ready, the first frame might be skipped.
If your app knows the remote user ID (For example, from Signaling), set the view immediately. Otherwise, use the onUserJoined callback.
- Set the remote view early:
func onShowChannels(channelId: String, remoteUid: UInt) {
let canvas = AgoraRtcVideoCanvas()
canvas.uid = remoteUid
canvas.renderMode = .fit
canvas.view = nil
agoraKit?.setupRemoteVideo(canvas)
}
func onEIDUserJoined(uid: UInt, elapsed: Int) {
// Already set - no additional setup needed
}- Set the view when the user joins:
// Event handler class
extension AgoraQuickStartViewController: AgoraRtcEngineDelegate {
func rtcEngine(_ engine: AgoraRtcEngineKit, didJoinedOfUid uid: UInt, elapsed: Int) {
// Forward to UI logic
DispatchQueue.main.async {
self.onEIDUserJoined(uid: uid, elapsed: elapsed)
}
}
}
func onEIDUserJoined(uid: UInt, elapsed: Int) {
let canvas = AgoraRtcVideoCanvas()
canvas.uid = uid
canvas.renderMode = .fit
canvas.view = surfaceView
agoraKit?.setupRemoteVideo(canvas)
}Monitor rendering performance
Use startMediaRenderingTracing to monitor first-frame rendering metrics. Results are reported via videoRenderingTracingResultOfUid.
Call this method when the user initiates joining. For example, on a Join button tap. This gives accurate first-frame timing.
private func onJoinClicked() {
agoraKit?.startMediaRenderingTracing()
agoraKit?.joinChannel(byToken: token, channelId: channelId, info: nil, uid: uid, joinSuccess: nil)
}Join a channel
Call joinChannel to enter the channel. To speed up first-frame playback, avoid delays like fetching a token in this method.
If you can’t retrieve a token early, consider using a wildcard token.
private func prepareChannelInfo() -> Int {
uid = getUid()
channelId = getChannelInfo()
token = getTokenFromServer(channelId: channelId, uid: uid)
return 0 // Return success code
}
private func joinChannel() -> Int {
let options = AgoraRtcChannelMediaOptions()
let result = agoraKit?.joinChannel(byToken: token, channelId: channelId, uid: uid, mediaOptions: options)
return result == 0 ? 0 : -1
}Optimize callback performance
The SDK runs callbacks like onJoinChannelSuccess on the same thread. If one callback is slow, it can delay others—including rendering events.
Don’t block the callback thread with network calls, file I/O, or heavy processing.
Best practices
- Avoid complex operations in
onJoinChannelSuccess. - Don’t block
onUserJoinedor other rendering-related callbacks. - Use background threads for heavy logic.
The following figure illustrates the essential steps:
Sequence diagram for implementation
Set up the rendering view
If you know the host’s user ID before joining the channel, call setupRemoteVideoEx as early as possible to set up the rendering view. This ensures the rendering pipeline is initialized in advance, helping avoid delays in displaying the first decoded frame.
If the host’s user ID is not available beforehand, wait for the onUserJoined callback, then call setupRemoteVideoEx.
// Define necessary variables first
let connection = AgoraRtcConnection(channelId: channelId, localUid: uid)
let remoteUid: UInt = 12345 // Replace with actual remote user ID
// Set the host's rendering view
let canvas = AgoraRtcVideoCanvas()
canvas.uid = remoteUid
canvas.renderMode = .fit
canvas.view = getView()
agoraKit?.setupRemoteVideoEx(canvas, connection: connection)Join a channel without automatically subscribing
Joining a channel typically takes the most time before the first video frame appears. For use cases like fast channel switching, delay subscribing to media streams to speed up rendering:
-
Call
joinChannelExto join the channel. -
In
AgoraRtcChannelMediaOptions, setautoSubscribeAudioandautoSubscribeVideotofalse. -
Subscribe manually when the user is ready to view content.
// Define connection and event handler let connection = AgoraRtcConnection(channelId: channelId, localUid: uid) let eventHandler = self // Set channel media options let options = AgoraRtcChannelMediaOptions() options.channelProfile = .liveBroadcasting options.clientRoleType = .audience options.autoSubscribeAudio = false options.autoSubscribeVideo = false // Join the channel without subscribing agoraKit?.joinChannelEx(byToken: token, connection: connection, delegate: self, mediaOptions: options, joinSuccess: nil)
Subscribe to streams and start rendering
When the user chooses to view content:
-
Resume media subscription using
muteRemoteVideoStreamExandmuteRemoteAudioStreamEx. -
Call
startMediaRenderingTracingExto log rendering metrics. -
The SDK reports results in the
videoRenderingTracingResultOfUidcallback, which you can use for performance analysis.func switchToChannel() { // Start video rendering tracing agoraKit?.startMediaRenderingTracingEx(connection) // Resume remote media subscriptions agoraKit?.muteRemoteVideoStreamEx(remoteUid, mute: false, connection: connection) agoraKit?.muteRemoteAudioStreamEx(remoteUid, mute: false, connection: connection) }
Troubleshooting
Refer to Slow first-frame rendering of remote video when using the Agora Video SDK.
