Skip to main content

Optimize first-frame rendering

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 Video Calling.

Prerequisites

Complete the steps in the SDK Quickstart to build a basic Video Calling 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:

CharacteristicPreload and initialize earlyJoin early, subscribe on demand
Applicable scenariosMost audio and video use casesScenarios with very high requirements for first frame rendering speed
Core implementationInitialize and configure video settings before joiningJoin the channel early without subscribing; subscribe only when needed
CostNormal billingMay incur additional channel usage fees

The following figure shows the time to output the first frame before optimization and with each solution:

Optimize video rendering

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

Sequence diagram for optimized video rendering

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.

info

Initialize the engine only once. Avoid creating and destroying it multiple times.

class AgoraQuickStartActivity : AppCompatActivity() {    private var mRtcEngine: RtcEngine? = null        override fun onCreate(savedInstanceState: Bundle?) {        super.onCreate(savedInstanceState)                // Create and initialize the engine during activity creation        initializeAgoraEngine()    }        private fun initializeAgoraEngine() {        try {            val config = RtcEngineConfig().apply {                mContext = applicationContext                mAppId = "Your App ID"                mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING                mEventHandler = mRtcEventHandler // You'll need to define this            }                        mRtcEngine = RtcEngine.create(config)        } catch (e: Exception) {            throw RuntimeException("Error initializing RTC engine: ${e.message}")        }    }}

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 channelmRtcEngine?.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 scenariomRtcEngine?.setVideoScenario(Constants.APPLICATION_SCENARIO_1V1)

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, and uid must match the values used in joinChannel.
  • Call preloadChannel as soon as you retrieve the required info.
  • Don’t call joinChannel immediately after preloadChannel.
private fun prepareChannelInfo(): Int {    uid = getUid()    channelId = getChannelInfo()    token = getTokenFromServer(channelId, uid)        // Preload the channel    mRtcEngine?.preloadChannel(token, channelId, 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:

    fun onShowChannels(channelId: String, remoteUid: Int) {      val canvas = VideoCanvas(null, VideoCanvas.RENDER_MODE_FIT, remoteUid)      mRtcEngine?.setupRemoteVideo(canvas)  }  fun onEIDUserJoined(uid: Int, elapsed: Int) {      // Already set - no additional setup needed  }
  • Set the view when the user joins:

    // Event handler class  private val mRtcEventHandler = object : IRtcEngineEventHandler() {      override fun onUserJoined(uid: Int, elapsed: Int) {          // Forward to UI logic          runOnUiThread {              onEIDUserJoined(uid, elapsed)          }      }  }  fun onEIDUserJoined(uid: Int, elapsed: Int) {      val canvas = VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid)      mRtcEngine?.setupRemoteVideo(canvas)  }

Monitor rendering performance

Use startMediaRenderingTracing to monitor first-frame rendering metrics. Results are reported via onVideoRenderingTracingResult.

info

Call this method when the user initiates joining. For example, on a Join button tap. This gives accurate first-frame timing.

private fun onJoinClicked() {    mRtcEngine?.startMediaRenderingTracing()    mRtcEngine?.joinChannel(token, channelId, uid, options)}

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 fun prepareChannelInfo(): Int {    uid = getUid()    channelId = getChannelInfo()    token = getTokenFromServer(channelId, uid)    return 0 // Return success code}private fun joinChannel(): Int {    val options = ChannelMediaOptions()    return mRtcEngine?.joinChannel(token, channelId, uid, options) ?: -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.

info

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 onUserJoined or other rendering-related callbacks.
  • Use background threads for heavy logic.

Troubleshooting

Refer to Slow first-frame rendering of remote video when using the Agora Video SDK.