# Optimize first-frame rendering (/en/realtime-media/video/build/capture-and-render-video/optimize-frame-rendering)

> For AI agents: see the complete documentation index at [llms.txt](/llms.txt).

<_PlatformTabsGroup groupMode="structured" canonicalPlatform="android" platforms="[&#x22;android&#x22;,&#x22;ios&#x22;,&#x22;macos&#x22;,&#x22;windows&#x22;]" showTabs="true">
  <_PlatformPanel platform="android">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="android" />

    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 [#prerequisites]

    Complete the steps in the [SDK Quickstart](/en/realtime-media/video/get-started-sdk) to build a basic Video Calling app.

    ## Understand the tech [#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:

    ![Optimize video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-tech.jpg)

    ## Implement fast first-frame rendering [#implement-fast-first-frame-rendering]

    This section describes the implementation logic for both solutions.

    <Tabs defaultValue="preload">
      <TabsList>
        <TabsTrigger value="preload">
          Preload and initialize early
        </TabsTrigger>

        <TabsTrigger value="subscribe">
          Join early, subscribe on demand
        </TabsTrigger>
      </TabsList>

      <TabsContent value="preload">
        The following figure illustrates the essential steps:

        ### Sequence diagram for implementation [#sequence-diagram-for-implementation]

        ![Sequence diagram for optimized video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-solution-1.svg)

        ### Set up a Video SDK instance [#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.

        <CalloutContainer type="info">
          <CalloutDescription>
            Initialize the engine only once. Avoid creating and destroying it multiple times.
          </CalloutDescription>
        </CalloutContainer>

        ```kotlin
        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 [#enable-accelerated-rendering]

        Call [`enableInstantMediaRendering`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengine.html#api_irtcengine_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.

        ```kotlin
        // Enable accelerated rendering before joining the channel
        mRtcEngine?.enableInstantMediaRendering()
        ```

        ### Set a video scenario [#set-a-video-scenario]

        Use [`setVideoScenario`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengine.html#api_irtcengine_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`.

        ```kotlin
        // Set the video scenario
        mRtcEngine?.setVideoScenario(Constants.APPLICATION_SCENARIO_1V1)
        ```

        ### Preload a channel [#preload-a-channel]

        Joining a channel involves acquiring server resources and establishing a connection. Call [`preloadChannel`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengine.html#api_irtcengine_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`.

        ```kotlin
        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 [#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:**

        ```kotlin
        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:**

        ```kotlin
        // 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 [#monitor-rendering-performance]

        Use [`startMediaRenderingTracing`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengine.html#api_irtcengine_startmediarenderingtracing) to monitor first-frame rendering metrics. Results are reported via [`onVideoRenderingTracingResult`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onvideorenderingtracingresult).

        <CalloutContainer type="info">
          <CalloutDescription>
            Call this method when the user initiates joining. For example, on a **Join** button tap. This gives accurate first-frame timing.
          </CalloutDescription>
        </CalloutContainer>

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

        ### Join a channel [#join-a-channel]

        Call [`joinChannel`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengine.html#api_irtcengine_joinchannel2) 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](/en/realtime-media/video/build/authenticate-users/deploy-token-server#generate-wildcard-tokens).

        ```kotlin
        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 [#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.

        <CalloutContainer type="info">
          <CalloutDescription>
            Don’t block the callback thread with network calls, file I/O, or heavy processing.
          </CalloutDescription>
        </CalloutContainer>

        #### Best practices [#best-practices]

        * Avoid complex operations in `onJoinChannelSuccess`.
        * Don’t block `onUserJoined` or other rendering-related callbacks.
        * Use background threads for heavy logic.
      </TabsContent>

      <TabsContent value="subscribe">
        The following figure illustrates the essential steps:

        ### Sequence diagram for implementation [#sequence-diagram-for-implementation-1]

        ![Sequence diagram for optimized video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-solution-2.svg)

        ### Set up the rendering view [#set-up-the-rendering-view-1]

        If you know the host’s user ID before joining the channel, call [`setupRemoteVideoEx`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineex.html#api_irtcengineex_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`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineex.html#api_irtcengineex_setupremotevideoex).

        ```kotlin
        val canvas = VideoCanvas(getView(), VideoCanvas.RENDER_MODE_FIT, farNextChannel.remoteUid)
        mRtcEngine?.setupRemoteVideoEx(canvas, connection)
        ```

        ### Join a channel without automatically subscribing [#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:

        1. Call [`joinChannelEx`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineex.html#api_irtcengineex_joinchannelex) to join the channel.
        2. In [`ChannelMediaOptions`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_channelmediaoptions.html), set `autoSubscribeAudio` and `autoSubscribeVideo` to `false`.
        3. Subscribe manually when the user is ready to view content.

           ```kotlin
           // Define connection and event handler
           val connection = RtcConnection(channelId, uid)
           val eventHandler = mRtcEventHandler

           // Set channel media options
           val options = ChannelMediaOptions().apply {
               channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING
               clientRoleType = Constants.CLIENT_ROLE_AUDIENCE
               autoSubscribeAudio = false
               autoSubscribeVideo = false
           }

           // Join the channel without subscribing
           mRtcEngine?.joinChannelEx(token, connection, options, mRtcEventHandler)
           ```

        ### Subscribe to streams and start rendering [#subscribe-to-streams-and-start-rendering]

        When the user chooses to view content:

        1. Resume media subscription using [`muteRemoteVideoStreamEx`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineex.html#api_irtcengineex_muteremotevideostreamex) and [`muteRemoteAudioStreamEx`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineex.html#api_irtcengineex_muteremoteaudiostreamex).
        2. Call [`startMediaRenderingTracingEx`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineex.html#api_irtcengineex_startmediarenderingtracingex) to log rendering metrics.
        3. The SDK reports results in the [`onVideoRenderingTracingResult`](https://api-ref.agora.io/en/video-sdk/android/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onvideorenderingtracingresult) callback, which you can use for performance analysis.

           ```kotlin
           fun switchToChannel() {
               // Start video rendering tracing
               mRtcEngine?.startMediaRenderingTracingEx(connection)

               // Resume remote media subscriptions
               mRtcEngine?.muteRemoteVideoStreamEx(remoteUid, false, connection)
               mRtcEngine?.muteRemoteAudioStreamEx(remoteUid, false, connection)
           }
           ```
      </TabsContent>
    </Tabs>

    ## Troubleshooting [#troubleshooting]

    Refer to [Slow first-frame rendering of remote video when using the Agora Video SDK](/en/api-reference/faq/quality/optimize_video_rendering).

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="ios">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="ios" />

    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 [#prerequisites-1]

    Complete the steps in the [SDK Quickstart](/en/realtime-media/video/get-started-sdk) to build a basic Video Calling app.

    ## Understand the tech [#understand-the-tech-1]

    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:

    ![Optimize video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-tech.jpg)

    ## Implement fast first-frame rendering [#implement-fast-first-frame-rendering-1]

    This section describes the implementation logic for both solutions.

    <Tabs defaultValue="preload">
      <TabsList>
        <TabsTrigger value="preload">
          Preload and initialize early
        </TabsTrigger>

        <TabsTrigger value="subscribe">
          Join early, subscribe on demand
        </TabsTrigger>
      </TabsList>

      <TabsContent value="preload">
        The following figure illustrates the essential steps:

        ### Sequence diagram for implementation [#sequence-diagram-for-implementation-2]

        ![Sequence diagram for optimized video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-solution-1.svg)

        ### Set up a Video SDK instance [#set-up-a-video-sdk-instance-1]

        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.

        <CalloutContainer type="info">
          <CalloutDescription>
            Initialize the engine only once. Avoid creating and destroying it multiple times.
          </CalloutDescription>
        </CalloutContainer>

        ```swift
        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 [#enable-accelerated-rendering-1]

        Call [`enableInstantMediaRendering`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/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.

        ```swift
        // Enable accelerated rendering before joining the channel
        agoraKit?.enableInstantMediaRendering()
        ```

        ### Set a video scenario [#set-a-video-scenario-1]

        Use [`setVideoScenario`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/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`.

        ```swift
        // Set the video scenario
        agoraKit?.setVideoScenario(.application1V1Scenario)
        ```

        ### Preload a channel [#preload-a-channel-1]

        Joining a channel involves acquiring server resources and establishing a connection. Call [`preloadChannel`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/preloadchannel\(bytoken\:channelid\:uid:\)) 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`.

        ```swift
        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 [#set-up-the-rendering-view-2]

        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:**

        ```swift
        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:**

        ```swift
        // 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 [#monitor-rendering-performance-1]

        Use [`startMediaRenderingTracing`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/startmediarenderingtracing\(\)) to monitor first-frame rendering metrics. Results are reported via [`videoRenderingTracingResultOfUid`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginedelegate/rtcengine\(_\:videorenderingtracingresultofuid\:currentevent\:tracinginfo:\)).

        <CalloutContainer type="info">
          <CalloutDescription>
            Call this method when the user initiates joining. For example, on a **Join** button tap. This gives accurate first-frame timing.
          </CalloutDescription>
        </CalloutContainer>

        ```swift
        private func onJoinClicked() {
            agoraKit?.startMediaRenderingTracing()
            agoraKit?.joinChannel(byToken: token, channelId: channelId, info: nil, uid: uid, joinSuccess: nil)
        }
        ```

        ### Join a channel [#join-a-channel-1]

        Call [`joinChannel`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/joinchannel\(bytoken\:channelid\:uid\:mediaoptions\:joinsuccess:\)) 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](/en/realtime-media/video/build/authenticate-users/deploy-token-server#generate-wildcard-tokens).

        ```swift
        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 [#optimize-callback-performance-1]

        The SDK runs callbacks like `onJoinChannelSuccess` on the same thread. If one callback is slow, it can delay others—including rendering events.

        <CalloutContainer type="info">
          <CalloutDescription>
            Don’t block the callback thread with network calls, file I/O, or heavy processing.
          </CalloutDescription>
        </CalloutContainer>

        #### Best practices [#best-practices-1]

        * Avoid complex operations in `onJoinChannelSuccess`.
        * Don’t block `onUserJoined` or other rendering-related callbacks.
        * Use background threads for heavy logic.
      </TabsContent>

      <TabsContent value="subscribe">
        The following figure illustrates the essential steps:

        ### Sequence diagram for implementation [#sequence-diagram-for-implementation-3]

        ![Sequence diagram for optimized video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-solution-2.svg)

        ### Set up the rendering view [#set-up-the-rendering-view-3]

        If you know the host’s user ID before joining the channel, call [`setupRemoteVideoEx`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/setupremotevideoex\(_\:connection:\)) 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`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/setupremotevideoex\(_\:connection:\)).

        ```swift
        // 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 [#join-a-channel-without-automatically-subscribing-1]

        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:

        1. Call [`joinChannelEx`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/joinchannelex\(bytoken\:connection\:delegate\:mediaoptions\:joinsuccess:\)) to join the channel.
        2. In [`AgoraRtcChannelMediaOptions`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcchannelmediaoptions), set `autoSubscribeAudio` and `autoSubscribeVideo` to `false`.
        3. Subscribe manually when the user is ready to view content.

           ```swift
           // 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 [#subscribe-to-streams-and-start-rendering-1]

        When the user chooses to view content:

        1. Resume media subscription using [`muteRemoteVideoStreamEx`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/muteremotevideostreamex\(_\:mute\:connection:\)) and [`muteRemoteAudioStreamEx`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/muteremoteaudiostreamex\(_\:mute\:connection:\)).
        2. Call [`startMediaRenderingTracingEx`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginekit/startmediarenderingtracingex\(_:\)) to log rendering metrics.
        3. The SDK reports results in the [`videoRenderingTracingResultOfUid`](https://api-ref.agora.io/en/video-sdk/ios/4.x/documentation/agorartckit/agorartcenginedelegate/rtcengine\(_\:videorenderingtracingresultofuid\:currentevent\:tracinginfo:\)) callback, which you can use for performance analysis.

           ```swift
           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)
           }
           ```
      </TabsContent>
    </Tabs>

    ## Troubleshooting [#troubleshooting-1]

    Refer to [Slow first-frame rendering of remote video when using the Agora Video SDK](/en/api-reference/faq/quality/optimize_video_rendering).

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="macos">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="macos" />

    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 [#prerequisites-2]

    Complete the steps in the [SDK Quickstart](/en/realtime-media/video/get-started-sdk) to build a basic Video Calling app.

    ## Understand the tech [#understand-the-tech-2]

    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:

    ![Optimize video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-tech.jpg)

    ## Implement fast first-frame rendering [#implement-fast-first-frame-rendering-2]

    This section describes the implementation logic for both solutions.

    <Tabs defaultValue="preload">
      <TabsList>
        <TabsTrigger value="preload">
          Preload and initialize early
        </TabsTrigger>

        <TabsTrigger value="subscribe">
          Join early, subscribe on demand
        </TabsTrigger>
      </TabsList>

      <TabsContent value="preload">
        The following figure illustrates the essential steps:

        ### Sequence diagram for implementation [#sequence-diagram-for-implementation-4]

        ![Sequence diagram for optimized video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-solution-1.svg)

        ### Set up a Video SDK instance [#set-up-a-video-sdk-instance-2]

        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.

        <CalloutContainer type="info">
          <CalloutDescription>
            Initialize the engine only once. Avoid creating and destroying it multiple times.
          </CalloutDescription>
        </CalloutContainer>

        ```swift
        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 [#enable-accelerated-rendering-2]

        Call [`enableInstantMediaRendering`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/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.

        ```swift
        // Enable accelerated rendering before joining the channel
        agoraKit?.enableInstantMediaRendering()
        ```

        ### Set a video scenario [#set-a-video-scenario-2]

        Use [`setVideoScenario`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/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`.

        ```swift
        // Set the video scenario
        agoraKit?.setVideoScenario(.application1V1Scenario)
        ```

        ### Preload a channel [#preload-a-channel-2]

        Joining a channel involves acquiring server resources and establishing a connection. Call [`preloadChannel`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/preloadchannel\(bytoken\:channelid\:uid:\)) 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`.

        ```swift
        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 [#set-up-the-rendering-view-4]

        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:**

        ```swift
        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:**

        ```swift
        // 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 [#monitor-rendering-performance-2]

        Use [`startMediaRenderingTracing`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/startmediarenderingtracing\(\)) to monitor first-frame rendering metrics. Results are reported via [`videoRenderingTracingResultOfUid`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginedelegate/rtcengine\(_\:videorenderingtracingresultofuid\:currentevent\:tracinginfo:\)).

        <CalloutContainer type="info">
          <CalloutDescription>
            Call this method when the user initiates joining. For example, on a **Join** button tap. This gives accurate first-frame timing.
          </CalloutDescription>
        </CalloutContainer>

        ```swift
        private func onJoinClicked() {
            agoraKit?.startMediaRenderingTracing()
            agoraKit?.joinChannel(byToken: token, channelId: channelId, info: nil, uid: uid, joinSuccess: nil)
        }
        ```

        ### Join a channel [#join-a-channel-2]

        Call [`joinChannel`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/joinchannel\(bytoken\:channelid\:uid\:mediaoptions\:joinsuccess:\)) 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](/en/realtime-media/video/build/authenticate-users/deploy-token-server#generate-wildcard-tokens).

        ```swift
        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 [#optimize-callback-performance-2]

        The SDK runs callbacks like `onJoinChannelSuccess` on the same thread. If one callback is slow, it can delay others—including rendering events.

        <CalloutContainer type="info">
          <CalloutDescription>
            Don’t block the callback thread with network calls, file I/O, or heavy processing.
          </CalloutDescription>
        </CalloutContainer>

        #### Best practices [#best-practices-2]

        * Avoid complex operations in `onJoinChannelSuccess`.
        * Don’t block `onUserJoined` or other rendering-related callbacks.
        * Use background threads for heavy logic.
      </TabsContent>

      <TabsContent value="subscribe">
        The following figure illustrates the essential steps:

        ### Sequence diagram for implementation [#sequence-diagram-for-implementation-5]

        ![Sequence diagram for optimized video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-solution-2.svg)

        ### Set up the rendering view [#set-up-the-rendering-view-5]

        If you know the host’s user ID before joining the channel, call [`setupRemoteVideoEx`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/setupremotevideoex\(_\:connection:\)) 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`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/setupremotevideoex\(_\:connection:\)).

        ```swift
        // 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 [#join-a-channel-without-automatically-subscribing-2]

        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:

        1. Call [`joinChannelEx`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/joinchannelex\(bytoken\:connection\:delegate\:mediaoptions\:joinsuccess:\)) to join the channel.
        2. In [`AgoraRtcChannelMediaOptions`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcchannelmediaoptions), set `autoSubscribeAudio` and `autoSubscribeVideo` to `false`.
        3. Subscribe manually when the user is ready to view content.

           ```swift
           // 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 [#subscribe-to-streams-and-start-rendering-2]

        When the user chooses to view content:

        1. Resume media subscription using [`muteRemoteVideoStreamEx`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/muteremotevideostreamex\(_\:mute\:connection:\)) and [`muteRemoteAudioStreamEx`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/muteremoteaudiostreamex\(_\:mute\:connection:\)).
        2. Call [`startMediaRenderingTracingEx`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginekit/startmediarenderingtracingex\(_:\)) to log rendering metrics.
        3. The SDK reports results in the [`videoRenderingTracingResultOfUid`](https://api-ref.agora.io/en/video-sdk/macos/4.x/documentation/agorartckit/agorartcenginedelegate/rtcengine\(_\:videorenderingtracingresultofuid\:currentevent\:tracinginfo:\)) callback, which you can use for performance analysis.

           ```swift
           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)
           }
           ```
      </TabsContent>
    </Tabs>

    ## Troubleshooting [#troubleshooting-2]

    Refer to [Slow first-frame rendering of remote video when using the Agora Video SDK](/en/api-reference/faq/quality/optimize_video_rendering).

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>

  <_PlatformPanel platform="windows">
    <_PlatformProcessedMarker groupMode="structured" canonicalPlatform="android" platform="windows" />

    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 [#prerequisites-3]

    Complete the steps in the [SDK Quickstart](/en/realtime-media/video/get-started-sdk) to build a basic Video Calling app.

    ## Understand the tech [#understand-the-tech-3]

    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:

    ![Optimize video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-tech.jpg)

    ## Implement fast first-frame rendering [#implement-fast-first-frame-rendering-3]

    This section describes the implementation logic for both solutions.

    <Tabs defaultValue="preload">
      <TabsList>
        <TabsTrigger value="preload">
          Preload and initialize early
        </TabsTrigger>

        <TabsTrigger value="subscribe">
          Join early, subscribe on demand
        </TabsTrigger>
      </TabsList>

      <TabsContent value="preload">
        The following figure illustrates the essential steps:

        ### Sequence diagram for implementation [#sequence-diagram-for-implementation-6]

        ![Sequence diagram for optimized video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-solution-1.svg)

        ### Set up a Video SDK instance [#set-up-a-video-sdk-instance-3]

        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.

        <CalloutContainer type="info">
          <CalloutDescription>
            Initialize the engine only once. Avoid creating and destroying it multiple times.
          </CalloutDescription>
        </CalloutContainer>

        ```cpp
        class CAgoraQuickStartDlg {
        private:
            IRtcEngine* m_rtcEngine;

        public:
            // Create and initialize the engine during dialog initialization
            CAgoraQuickStartDlg() {
                m_rtcEngine = createAgoraRtcEngine();
                RtcEngineContext context;
                context.appId = "Your App ID";
                context.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING;
                // ... other necessary configurations

                m_rtcEngine->initialize(context);
            }
        };
        ```

        ### Enable accelerated rendering [#enable-accelerated-rendering-3]

        Call [`enableInstantMediaRendering`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengine.html#api_irtcengine_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.

        ```cpp
        // Enable accelerated rendering before joining the channel
        m_rtcEngine->enableInstantMediaRendering();
        ```

        ### Set a video scenario [#set-a-video-scenario-3]

        Use [`setVideoScenario`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengine.html#api_irtcengine_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`.

        ```cpp
        // Set the video scenario
        m_rtcEngine->setVideoScenarios(agora::rtc::APPLICATION_SCENARIO_1V1);
        ```

        ### Preload a channel [#preload-a-channel-3]

        Joining a channel involves acquiring server resources and establishing a connection. Call [`preloadChannel`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengine.html#api_irtcengine_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`.

        ```cpp
        int CAgoraQuickStartDlg::prepareChannelInfo() {
            m_uid = get_uid();
            m_channelId = get_channel_info();
            m_token = getTokenFromServer(m_channelId, m_uid);

            // Preload the channel
            m_rtcEngine->preloadChannel(m_token, m_channelId, m_uid);
        }
        ```

        ### Set up the rendering view [#set-up-the-rendering-view-6]

        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:**

        ```cpp
        void CAgoraQuickStartDlg::onShowChannels(const char* channelId, uid_t remoteUid) {
            VideoCanvas canvas;
            canvas.uid = remoteUid;
            m_rtcEngine->setupRemoteVideo(canvas);
        }

        void CAgoraQuickStartDlg::onEIDUserJoined(uid_t uid, int elapsed) {
            // Already set
        }
        ```

        * **Set the view when the user joins:**

        ```cpp
        void EventHandler::onUserJoined(uid_t uid, int elapsed) {
            // Forward to UI logic
        }

        void CAgoraQuickStartDlg::onEIDUserJoined(uid_t uid, int elapsed) {
            VideoCanvas canvas;
            canvas.uid = uid;
            m_rtcEngine->setupRemoteVideo(canvas);
        }
        ```

        ### Monitor rendering performance [#monitor-rendering-performance-3]

        Use [`startMediaRenderingTracing`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengine.html#api_irtcengine_startmediarenderingtracing) to monitor first-frame rendering metrics. Results are reported via [`onVideoRenderingTracingResult`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onvideorenderingtracingresult).

        <CalloutContainer type="info">
          <CalloutDescription>
            Call this method when the user initiates joining. For example, on a **Join** button tap. This gives accurate first-frame timing.
          </CalloutDescription>
        </CalloutContainer>

        ```cpp
        void on_join_clicked() {
            m_rtcEngine->startMediaRenderingTracing();
            m_rtcEngine->joinChannel(token, channelId, uid, options);
        }
        ```

        ### Join a channel [#join-a-channel-3]

        Call [`joinChannel`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengine.html#api_irtcengine_joinchannel2) 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](/en/realtime-media/video/build/authenticate-users/deploy-token-server#generate-wildcard-tokens).

        ```cpp
        int CAgoraQuickStartDlg::prepareChannelInfo() {
            m_uid = get_uid();
            m_channelId = get_channel_info();
            m_token = getTokenFromServer(m_channelId, m_uid);
        }

        int CAgoraQuickStartDlg::joinChannel() {
            ChannelMediaOptions options;
            return m_rtcEngine->joinChannel(m_token, m_channelId, m_uid, options);
        }
        ```

        ### Optimize callback performance [#optimize-callback-performance-3]

        The SDK runs callbacks like `onJoinChannelSuccess` on the same thread. If one callback is slow, it can delay others—including rendering events.

        <CalloutContainer type="info">
          <CalloutDescription>
            Don’t block the callback thread with network calls, file I/O, or heavy processing.
          </CalloutDescription>
        </CalloutContainer>

        #### Best practices [#best-practices-3]

        * Avoid complex operations in `onJoinChannelSuccess`.
        * Don’t block `onUserJoined` or other rendering-related callbacks.
        * Use background threads for heavy logic.
      </TabsContent>

      <TabsContent value="subscribe">
        The following figure illustrates the essential steps:

        ### Sequence diagram for implementation [#sequence-diagram-for-implementation-7]

        ![Sequence diagram for optimized video rendering](https://assets-docs.agora.io/images/video-sdk/optimize-video-rendering-solution-2.svg)

        ### Set up the rendering view [#set-up-the-rendering-view-7]

        If you know the host’s user ID before joining the channel, call [`setupRemoteVideoEx`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineex.html#api_irtcengineex_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`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineex.html#api_irtcengineex_setupremotevideoex).

        ```cpp
        // Set the host’s rendering view
        VideoCanvas canvas;
        canvas.uid = far_next_channel.remoteUid;
        canvas.view = getView();  // Replace with your rendering view
        m_rtcEngine->setupRemoteVideoEx(canvas, connection);
        ```

        ### Join a channel without automatically subscribing [#join-a-channel-without-automatically-subscribing-3]

        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:

        1. Call [`joinChannelEx`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineex.html#api_irtcengineex_joinchannelex) to join the channel.
        2. In [`ChannelMediaOptions`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_channelmediaoptions.html), set `autoSubscribeAudio` and `autoSubscribeVideo` to `false`.
        3. Subscribe manually when the user is ready to view content.

           ```cpp
           // Set channel media options
           ChannelMediaOptions options;
           options.channelProfile = CHANNEL_PROFILE_LIVE_BROADCASTING;
           options.clientRoleType = CHANNEL_ROLE_AUDIENCE;
           options.autoSubscribeAudio = false;
           options.autoSubscribeVideo = false;

           RtcConnection connection(far_next_channel.channel_id.c_str(), m_localUid);

           // Join the channel without subscribing
           m_rtcEngine->joinChannelEx(APP_TOKEN, connection, options, m_handler);
           ```

        ### Subscribe to streams and start rendering [#subscribe-to-streams-and-start-rendering-3]

        When the user chooses to view content:

        1. Resume media subscription using [`muteRemoteVideoStreamEx`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineex.html#api_irtcengineex_muteremotevideostreamex) and [`muteRemoteAudioStreamEx`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineex.html#api_irtcengineex_muteremoteaudiostreamex).
        2. Call [`startMediaRenderingTracingEx`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineex.html#api_irtcengineex_startmediarenderingtracingex) to log rendering metrics.
        3. The SDK reports results in the [`onVideoRenderingTracingResult`](https://api-ref.agora.io/en/video-sdk/cpp/4.x/API/class_irtcengineeventhandler.html#callback_irtcengineeventhandler_onvideorenderingtracingresult) callback, which you can use for performance analysis.

           ```cpp
           void CAgoraQuickStartDlg::switchNextChannel() {
               RtcConnection connection(next_channel.channel_id.c_str(), localUid);

               // Start video rendering tracing
               m_rtcEngine->startMediaRenderingTracingEx(connection);

               // Resume remote media subscriptions
               m_rtcEngine->muteRemoteVideoStreamEx(next_channel.remoteUid, false, connection);
               m_rtcEngine->muteRemoteAudioStreamEx(next_channel.remoteUid, false, connection);
           }
           ```
      </TabsContent>
    </Tabs>

    ## Troubleshooting [#troubleshooting-3]

    Refer to [Slow first-frame rendering of remote video when using the Agora Video SDK](/en/api-reference/faq/quality/optimize_video_rendering).

    <_PlatformProcessedMarker close="true" />
  </_PlatformPanel>
</_PlatformTabsGroup>
