# Preload channels (/en/realtime-media/broadcast-streaming/build/optimize-quality-and-connection/preload-channels)

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

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

    This page explains how to implement channel preloading to achieve near-instant channel entry. By preloading channel information and media streams before users join, you can significantly reduce the time required to render the first video frame and create a smoother viewing experience.

    The following video demonstrates the channel preload experience:

    <video src="https://assets-docs.agora.io/images/video-sdk/preload.mp4" style="{ width: '100%', height: 'auto' }">
      Your browser does not support the <code>video</code> element.
    </video>

    The following figure illustrates the best practice when preloading channels:

    ![preload channels best practice](https://assets-docs.agora.io/images/video-sdk/preload-channels-best-practice.png)

    Following these guidelines, you can reduce the first frame loading time for Chrome browsers on Windows and macOS to as low as 300 milliseconds.

    ## Implementation [#implementation]

    Follow these steps to implement channel preloading:

    ### Preload channel list [#preload-channel-list]

    On the channel list page, preload the channels within the user's view area with the `preload` method.

    * If the total number of channels does not exceed 10, preload all channels.
    * If the total number of channels exceeds 10, preload only the first 10 channels. If the user scrolls the channel list, wait for the scrolling to stop, then preload the new channels in the user's view area.

    To speed up the subsequent joining of the channel, use a [wildcard token](/en/realtime-media/broadcast-streaming/build/authenticate-users/deploy-token-server#generate-wildcard-tokens) in the `token` parameter of the `preload` method.

    ### Pre-join individual channels [#pre-join-individual-channels]

    Implement pre-joining for different mouse interactions:

    * Before entering the channel

      When a user hovers over a channel, best practice is to join the channel and subscribe to audio and video before entering. The specific operations include the following:

      * Call `join` to join the channel and subscribe to the host's audio and video streams.
      * Render the video in advance in a small preview window.

      To speed up joining the channel, use a [wildcard token](/en/realtime-media/broadcast-streaming/build/authenticate-users/deploy-token-server#generate-wildcard-tokens) in the `token` parameter of the `join` method.

    * Entering the channel

      To reduce the time it takes to render the first frame of the host's video, enable subscribing to the host's media stream as soon as the user enters the channel. When calling `join`, set `autoSubscribe` to `true` in `options`:

      ```js
      // Create client
      const client = AgoraRTC.createClient({
          mode: "live",
          codec: "vp8",
          role: "host",
      });

      // Enables auto subscription when the client joins a channel
      await client.join(APP_ID, CNAME, TOKEN, UID / null, {
          autoSubscribe: true,
      });
      ```

      The following table compares the steps from when a user enters the channel to when the host's media stream is rendered, before and after enabling this function:

      | Before                                                                                                                                                                                                                                                                                                                 | After                                                                                                                                                                                                                                                                       |
      | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
      | 1. The user joins the channel.
      2. The channel sends the host/media information.
      3. The user subscribes to the media stream in the channel.
      4. The Agora backend starts sending the media stream in the channel.
      5. The user waits for the Agora backend to send the media stream, receives it, and renders it locally. | 1) The user joins the channel.
      2) The Agora backend immediately sends the media stream in the channel.
      3) The channel sends the host/media information.
      4) After the user subscribes to the media stream, they can immediately receive and render the media stream locally. |

      ![first-frame-rendring](https://assets-docs.agora.io/images/video-sdk/first-frame-rendering.png)

      <CalloutContainer type="info">
        <CalloutTitle>
          Note
        </CalloutTitle>

        <CalloutDescription>
          Joining a channel incurs additional fees due to the transmission of media streams. See [Pricing](/en/realtime-media/broadcast-streaming/reference/pricing#cost-calculation) for details.
        </CalloutDescription>
      </CalloutContainer>

      When a user clicks on the channel in the list, take the following actions:

      * Switch the app interface from the channel list page to the single channel page. Best practice is to add a page switching animation.
      * Call `IRemoteAudioTrack.play` and `IRemoteVideoTrack.play`. The video needs to be passed in the specified DOM element.

    * Leaving without entering

      When a user hovers over a channel but does not click to enter, best practice is to call `leave`.

    ### Precautions [#precautions]

    * Wildcard tokens can speed up the process of joining a channel but may cause room overflow. Decide whether to use wildcard tokens based on your specific needs.
    * Pre-joining channels incurs additional charges.

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

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

    In live streaming applications, a key user experience performance metric is how quickly a viewer can join or switch channels and how fast the remote video stream is rendered on their device. This document explores methods and best practices for reducing first-frame rendering time in apps using Video SDK and provides sample code for desktop and mobile devices.

    Agora provides an open source `VideoLoaderAPI` sample project on GitHub that includes APIs for fast channel loading and switching. To integrate the APIs into your project, copy the following files from the [`videoloaderapi`](https://github.com/AgoraIO-Community/VideoLoaderAPI/tree/main/Android/lib_videoloaderapi/src/main/java/io/agora/videoloaderapi) directory to your project:

    ```bash
    videoloaderapi/
    ├── OnLiveRoomItemTouchEventHandler.kt
    ├── OnPageScrollEventHandler.kt
    ├── OnPageScrollEventHandler2.kt
    ├── OnRoomListScrollEventHandler.kt
    ├── VideoLoader.kt
    └── VideoLoaderImpl.kt
    ```

    <CalloutContainer type="info">
      <CalloutTitle>
        Note
      </CalloutTitle>

      <CalloutDescription>
        To facilitate future code upgrades, do not modify the names and paths of these files.
      </CalloutDescription>
    </CalloutContainer>

    Follow these steps to implement fast channel joining and switching:

    ### Prepare to use the VideoLoader API [#prepare-to-use-the-videoloader-api]

    Take the following steps before using the fast joining and fast switching features:

    1. Initialize an `RtcEngine` instance.

       Call the `create` method to create and initialize an `RtcEngine` instance:

       ```kotlin
       // Initialize RtcEngine
       private val mRtcEngine by lazy {
           RtcEngine.create(RtcEngineConfig().apply {
               mContext = applicationContext
               // Pass in the App Id you obtained from Agora console
               mAppId = BuildConfig.AGORA_APP_ID
               mEventHandler = object : IRtcEngineEventHandler() {}
           })
       }
       ```

    2. Use a wildcard token.

       To speed up the process of users joining a channel, use a [wildcard token](/en/realtime-media/broadcast-streaming/build/authenticate-users/deploy-token-server#generate-wildcard-tokens). Generate the token on your server and pass it to the client for authentication.

       <CalloutContainer type="info">
         <CalloutTitle>
           Note
         </CalloutTitle>

         <CalloutDescription>
           Using wildcard tokens carries security risks, such as room bombing. Evaluate whether wildcard tokens are appropriate for your use case before implementation.
         </CalloutDescription>
       </CalloutContainer>

    \:::

    ### Fast channel joining [#fast-channel-joining]

    This section explains how to create a seamless, instant-opening experience in live broadcast scenarios.

    1. Add a UI element for the live broadcast room list.

       Implement a UI element to display a list of live broadcast rooms. The following example uses a `RecyclerView`:

       ```xml
       <androidx.recyclerview.widget.RecyclerView
           android:id="@+id/rvRooms"
           android:layout_width="match_parent"
           android:layout_height="match_parent"
           android:paddingHorizontal="7.5dp"
           android:overScrollMode="never"
           app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
           app:spanCount="2"
           android:clipToPadding="false"
           android:clipChildren="false"
           android:paddingBottom="50dp"
           android:visibility="visible" />
       ```

    2. Listen for scrolling events.

       Create an `OnRoomListScrollEventHandler` instance and register it as a proxy for live room list scroll events in the UI. To create the `OnRoomListScrollEventHandler` instance, pass the following parameters to its constructor:

       * `mRtcEngine`: A previously initialized `RtcEngine` instance.
       * `localUid`: The uid of the local user.

       The `OnRoomListScrollEventHandler` listens for scroll events in the live broadcast room list and applies best practices encapsulated within the class. It preloads channels for live broadcast rooms that appear on the screen (`preloadChannel`).

       ```kotlin
       // Code snippet from RoomListActivity
       class RoomListActivity : AppCompatActivity() {
           private val mBinding by lazy { ShowRoomListActivityBinding.inflate(LayoutInflater.from(this)) }
           // Create an OnRoomListScrollEventHandler instance
           private val onRoomListScrollEventHandler: OnRoomListScrollEventHandler = object : OnRoomListScrollEventHandler(mRtcEngine, RtcEngineInstance.localUid()) {}
           // Business service module
           private val mService by lazy { ShowServiceProtocol.getImplInstance() }

           override fun onCreate(savedInstanceState: Bundle?) {
               // Set the OnRoomListScrollEventHandler instance to the live room list to listen for scroll events
               mBinding.rvRooms.addOnScrollListener(onRoomListScrollEventHandler as OnRoomListScrollEventHandler)

               // Get room List
               mService.getRoomList { roomList ->
                   // After getting the room list, pass the result to the onRoomListScrollEventHandler instance
                   onRoomListScrollEventHandler?.updateRoomList(roomList)
               }
           }
       }
       ```

    3. Listen for touch events.

       Create an `OnLiveRoomItemTouchEventHandler` instance and register it as a proxy for touch events in a single live room UI. To create the `OnLiveRoomItemTouchEventHandler` instance, pass the following parameters to its constructor:

       * `mRtcEngine`: A previously initialized `RtcEngine` instance.
       * `roomInfo`: A `VideoLoader.RoomInfo` instance.

       The `OnLiveRoomItemTouchEventHandler` instance listens for touch events in a live broadcast room and implements the best practices encapsulated within the class. When a user taps a live room, they enter it automatically. You do not need to explicitly call `joinChannel` or similar methods in the business layer.

       ```kotlin
       // Code snippet from RoomListActivity
       class RoomListActivity : AppCompatActivity() {
           private val mBinding by lazy { ShowRoomListActivityBinding.inflate(LayoutInflater.from(this)) }
           private lateinit var mRoomAdapter: BindingSingleAdapter<ShowRoomDetailModel, ShowRoomItemBinding>

           override fun onCreate(savedInstanceState: Bundle?) {
               mRoomAdapter = object : BindingSingleAdapter<ShowRoomDetailModel, ShowRoomItemBinding>() {
                   override fun onBindViewHolder(
                       holder: BindingViewHolder<ShowRoomItemBinding>,
                       position: Int
                   ) {
                       // UI for individual live streams
                       val roomInfo = getItem(position) ?: return
                       // Create an OnLiveRoomItemTouchEventHandler instance
                       val onTouchEventHandler = object : OnLiveRoomItemTouchEventHandler(
                           // Previously initialized RtcEngine instance
                           mRtcEngine,
                           // Room Information
                           VideoLoader.RoomInfo(
                               roomInfo.roomId,
                               arrayListOf(
                                   VideoLoader.AnchorInfo(
                                       roomInfo.roomId,
                                       roomInfo.ownerId.toInt(),
                                       // Token mentioned above
                                       // Sample code for wildcard token
                                       RtcEngineInstance.generalToken()
                                   )
                               )
                           ),
                           RtcEngineInstance.localUid()
                       ) {
                           override fun onTouch(v: View?, event: MotionEvent?): Boolean {
                               when (event!!.action) {
                                   MotionEvent.ACTION_UP -> {
                                       if (RtcEngineInstance.generalToken() != "") {
                                           super.onTouch(v, event)
                                           // Listen to the ACTION_UP event and go to the in-stream page
                                           goLiveDetailActivity(list, position, roomInfo)
                                       }
                                   }
                               }
                               return true
                           }

                           // Informs you to render the host screen
                           override fun onRequireRenderVideo(info: VideoLoader.AnchorInfo): VideoLoader.VideoCanvasContainer? {
                               // The best time to render the host screen
                               return ...
                           }
                       }

                       // Set the OnLiveRoomItemTouchEventHandler instance to a single live room to listen for touch events
                       binding.root.setOnTouchListener(onTouchEventHandler)
                   }
               }
               mBinding.rvRooms.adapter = mRoomAdapter
           }
       }
       ```

       The `OnLiveRoomItemTouchEventHandler` instance automatically adds the host screen to the container and renders it when it receives the `onRequireRenderVideo` event. To ensure smooth rendering, create a container for the host screen in advance and return it to the handler when the event occurs.

    ### Fast channel switching [#fast-channel-switching]

    This section explains how to enable viewers to switch live broadcast channels quickly.

    1. Implement a UI component for sliding between live broadcast rooms.

       The following example uses a `ViewPager2` element:

       ```xml
       <androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"
           android:id="@+id/viewPager2"
           android:orientation="vertical"
           android:layout_width="match_parent"
           android:overScrollMode="never"
           xmlns:app="http://schemas.android.com/apk/res-auto"
           app:layout_scrollEffect="none"
           android:layout_height="match_parent" />
       ```

    2. Listen for live room switching events.

       Create an `OnPageScrollEventHandler` instance and register it as the slide event proxy for the `ViewPager2` live broadcast room. To create the `OnPageScrollEventHandler` instance, pass the following parameters to the constructor:

       * `mRtcEngine`: A previously initialized `RtcEngine` instance.
       * `localUid`: The `uid` of the local user.
       * `needPreJoin`: Determines whether to prejoin the adjacent live broadcast rooms (above and below the current one). Setting this to `true` improves instant switching but increases resource consumption.
       * `sliceMode`: Toggles the timing of outputting pictures in the live broadcast room.

       The `OnPageScrollEventHandler` instance implements best practices for managing live broadcast rooms. It listens for the `ViewPager2` slide event of the live broadcast room and optimally switches audio/video subscription behavior between rooms. It triggers the following events at the corresponding `position`:

       * `onPageStartLoading`: Just started loading/displaying the live broadcast room.
       * `onPageLoaded`: Live room loading/display completed
       * `onPageLeft`: Left the live broadcast room
       * `onRequireRenderVideo`: The best time to render the host view of the corresponding live broadcast room. At this time, pass in the container for the host screen of the corresponding live broadcast room. The `OnPageScrollEventHandler` instance adds and renders the host screen on the container.

       ```kotlin
       // Code snippet from LiveViewPagerActivity
       class LiveViewPagerActivity : AppCompatActivity() {
           private val mBinding by lazy { LiveViewPagerActivityBinding.inflate(LayoutInflater.from(this)) }
           // Use a circular array to store a list of live rooms that a user can switch between
           private val vpFragments = SparseArray<LiveViewPagerFragment>()
           // Create OnPageScrollEventHandler instance
           private var onPageScrollEventHandler: OnPageScrollEventHandler? = object : OnPageScrollEventHandler(
               // Previously initialized RtcEngine instance
               RtcEngineInstance.rtcEngine,
               // Local user's uid
               RtcEngineInstance.localUid(),
               // Set whether or not to join the two live rooms adjacent to the current live room ahead of time
               // If set to true, this results in faster switching, but at an increased cost
               needPreJoin,
               // Toggles the timing of the live feed out via the onPageScrollStateChanged event.
               sliceMode
           ) {
               override fun onPageScrollStateChanged(state: Int) {
                   when (state) {
                       ViewPager2.SCROLL_STATE_SETTLING -> binding.viewPager2.isUserInputEnabled = false
                       ViewPager2.SCROLL_STATE_IDLE -> binding.viewPager2.isUserInputEnabled = true
                   }
                   super.onPageScrollStateChanged(state)
               }

               override fun onPageStartLoading(position: Int) {
                   // Notify the corresponding position of the live room to start displaying
                   vpFragments[position]?.startLoadPageSafely()
               }

               override fun onPageLoaded(position: Int) {
                   // Notify the corresponding position that the live room has been displayed
                   vpFragments[position]?.onPageLoaded()
               }

               override fun onPageLeft(position: Int) {
                   // Notify the corresponding position that the live room has left
                   vpFragments[position]?.stopLoadPage(true)
               }

               override fun onRequireRenderVideo(position: Int, info: VideoLoader.AnchorInfo): VideoLoader.VideoCanvasContainer? {
                   // The best time to render the host screen of the corresponding live room, notify the live room of the corresponding position and return the container of the corresponding host screen.
                   return vpFragments[position]?.initAnchorVideoView(info)
               }
           }

           // The actions that are performed when an Activity is created are described below.
           override fun onCreate(savedInstanceState: Bundle?) {
               // To be added
               ...
           }
       }
       ```

       Call `updateRoomList` when the Activity is created to pass the initial live broadcast room list to the `onPageScrollEventHandler` instance. Additionally, call `onRoomCreated` within the `createFragment` event of `FragmentStateAdapter` to notify `onPageScrollEventHandler` that the corresponding live room has been created. Add the following code inside `override fun onCreate(savedInstanceState: Bundle?) {}`:

       ```kotlin
       override fun onCreate(savedInstanceState: Bundle?) {
           // When initializing the Activity, pass the initial live room list to the onPageScrollEventHandler instance
           onPageScrollEventHandler?.updateRoomList(list)

           // Set ViewPager2 to keep at least one page (Fragment) on each side of the current page in memory
           binding.viewPager2.offscreenPageLimit = 1

           // Create a FragmentStateAdapter to manage pages representing live rooms
           val fragmentAdapter = object : FragmentStateAdapter(this) {
               // If ViewPager2 allows sliding, it holds an infinite number of pages
               // Otherwise, it returns 1, holding only a single page.
               override fun getItemCount() = if (mScrollable) Int.MAX_VALUE else 1

               // Create a LiveViewPagerFragment for each live feed
               override fun createFragment(position: Int): Fragment {
                   val roomInfo = if (mScrollable) {
                       mRoomInfoList[position % mRoomInfoList.size]
                   } else {
                       mRoomInfoList[selectedRoomIndex]
                   }
                   return LiveViewPagerFragment.newInstance(
                       roomInfo,
                       onPageScrollEventHandler as OnPageScrollEventHandler, position
                   ).apply {
                       // Store the created LiveViewPagerFragment in vpFragments
                       vpFragments.put(position, this)

                       // Create a list of hosts for the live room
                       val anchorList = arrayListOf(
                           VideoLoader.AnchorInfo(
                               roomInfo.roomId,
                               roomInfo.ownerId.toInt(),
                               RtcEngineInstance.generalToken()
                           )
                       )

                       // Notify onPageScrollEventHandler that the corresponding room has been created
                       onPageScrollEventHandler?.onRoomCreated(
                           position,
                           VideoLoader.RoomInfo(roomInfo.roomId, hostList),
                           position == binding.viewPager2.currentItem
                       )
                   }
               }
           }
           binding.viewPager2.adapter = fragmentAdapter

           // Set whether the user can manually swipe pages
           // In a typical live room scenario, viewers can swipe manually, but hosts cannot.
           binding.viewPager2.isUserInputEnabled = mScrollable

           if (mScrollable) {
               // If swiping is enabled:
               // - Register the OnPageScrollEventHandler instance to listen for page change events
               // - Calculate the latest position
               binding.viewPager2.registerOnPageChangeCallback(
                   onPageScrollEventHandler as OnPageChangeCallback
               )
               binding.viewPager2.setCurrentItem(
                   Int.MAX_VALUE / 2 - Int.MAX_VALUE / 2 % mRoomInfoList.size + selectedRoomIndex,
                   false
               )
           } else {
               // If swiping is disabled, set the current position to 0
               currLoadPosition = 0
           }
       }
       ```

    ### Release resources [#release-resources]

    After using the `VideoLoaderAPI`, you do not need to call `leaveChannel` in the business layer to leave the live broadcast. To release resources, refer to the following sample code:

    ```kotlin
    VideoLoader.getImplInstance(mRtcEngine).cleanCache()
    VideoLoader.release()
    RtcEngineEx.destroy()
    ```

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

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

    In live streaming applications, a key user experience performance metric is how quickly a viewer can join or switch channels and how fast the remote video stream is rendered on their device. This document explores methods and best practices for reducing first-frame rendering time in apps using Video SDK and provides sample code for desktop and mobile devices.

    Agora provides an open source [`VideoLoaderAPI`](https://github.com/AgoraIO-Community/VideoLoaderAPI) sample project on GitHub that includes APIs for fast channel loading and switching. To integrate the APIs into your project, take the following steps:

    1. Copy the following folders and files from the repository to the same folder as your `*.xcodeproj` file:

       * [`VideoLoaderAPI`](https://github.com/AgoraIO-Community/VideoLoaderAPI/tree/main/iOS/VideoLoaderAPI) folder

       * [`VideoLoaderAPI.podspec`](https://github.com/AgoraIO-Community/VideoLoaderAPI/blob/main/iOS/VideoLoaderAPI.podspec) file

       ![preload channels best practice](https://assets-docs.agora.io/images/video-sdk/preload-channels-ios-videoloader.png)

       <CalloutContainer type="info">
         <CalloutTitle>
           Note
         </CalloutTitle>

         <CalloutDescription>
           To facilitate subsequent code upgrades, do not modify the names and paths of the files you add.
         </CalloutDescription>
       </CalloutContainer>

    \:::

    1. In the terminal, go to the directory where the `*.xcodeproj` file is located and run the `pod init` command. A **Podfile** text file is generated in the project folder.

    2. Open the **Podfile** file and modify it as follows:

       ```ruby
       source 'https://github.com/CocoaPods/Specs.git'
       use_frameworks!

       platform :ios, '13.0'

       # Change the target to the actual Target name in your project.
       target 'VideoLoaderAPI_Example' do
       pod 'VideoLoaderAPI', :path => 'VideoLoaderAPI.podspec'

           target 'VideoLoaderAPI_Tests' do
               inherit! :search_paths
           end
       end
       ```

    3. In the terminal, go to the directory where the `*.xcodeproj` file is located and execute `pod install` to install the dependencies.

       ![preload channels pod install](https://assets-docs.agora.io/images/video-sdk/preload-channels-ios-pod-install.png)

       After successful installation of the dependency library, you see a `*.xcworkspace` file.

    4. (Optional) If you have previously integrated a Video SDK dependency in your project, open the `VideoLoaderAPI.podspec` file and modify the dependency version as needed.

       ```bash
       # 4.1.1.19 is the default version installed in step 4, if you are using a different version, change it to the current one.
       s.dependency 'AgoraRtcEngine_Special_iOS', '4.1.1.19'
       ```

    Follow these steps to implement fast channel joining and switching:

    ### Prepare to use the VideoLoader API [#prepare-to-use-the-videoloader-api-1]

    Take the following steps before using the fast joining and fast switching features:

    1. Initialize `AgoraRtcEngineKit` and `VideoLoader` instances.

       In the default `ViewController` of the project file, call the `sharedEngine(with:)` method of Video SDK to create and initialize an `AgoraRtcEngineKit` instance. Then, initialize the `VideoLoader`.

       ```swift
       func prepareEngine() {
           // Create an Instance of AgoraRtcEngineKit
           let engine = _createRtcEngine()
           // Obtain a shared instance of VideoLoaderApiImpl
           let loader = VideoLoaderApiImpl.shared
           loader.addListener(listener: self)
           // Creating a VideoLoaderConfig instance
           let config = VideoLoaderConfig()
           // Setting up an AgoraRtcEngineKit instance
           config.rtcEngine = engine
           // Initialize VideoLoader
           loader.setup(config: config)
       }

       // Initializing AgoraRtcEngineKit
       private func _createRtcEngine() -> AgoraRtcEngineKit {
           let config = AgoraRtcEngineConfig()
           // Use the App Id of your project from the console
           config.appId = KeyCenter.AppId
           config.channelProfile = .liveBroadcasting
           config.audioScenario = .gameStreaming
           config.areaCode = .global
           let engine = AgoraRtcEngineKit.sharedEngine(with: config,
                                                       delegate: nil)
           return engine
       }
       ```

    2. Use a wildcard token.

       To speed up the process of users joining a channel, use a [wildcard token](/en/realtime-media/broadcast-streaming/build/authenticate-users/deploy-token-server#generate-wildcard-tokens). Generate the token on your server and pass it to the client for authentication.

       <CalloutContainer type="info">
         <CalloutTitle>
           Note
         </CalloutTitle>

         <CalloutDescription>
           Using wildcard tokens carries security risks, such as room bombing. Evaluate whether wildcard tokens are appropriate for your use case before implementation.
         </CalloutDescription>
       </CalloutContainer>

    \:::

    ### Fast channel joining [#fast-channel-joining-1]

    This section explains how to create a seamless, instant-opening experience in live broadcast scenarios.

    1. Add a UI component for the live broadcast room list.

       Implement a UI component to display a list of live broadcast rooms. The following example uses a `UICollectionView`:

       ```swift
       private lazy var listView: UICollectionView = {
           let layout = UICollectionViewFlowLayout()
           layout.minimumLineSpacing = 5
           layout.minimumInteritemSpacing = 5
           layout.sectionInset = .zero

           let w = view.bounds.width / 2 - 5
           layout.itemSize = CGSize(width: w, height: w * 1.5)

           let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
           collectionView.backgroundColor = .white
           collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "videoloader_listcell")
           collectionView.scrollsToTop = false
           collectionView.delegate = self.delegateHandler
           collectionView.dataSource = self
           collectionView.contentInsetAdjustmentBehavior = .never
           collectionView.showsVerticalScrollIndicator = false

           return collectionView
       }()
       ```

    2. Listen for scrolling events.

       Create an instance of `AGCollectionLoadingDelegateHandler` and assign it as the delegate for the live room list UI to handle scrolling events. When initializing the `AGCollectionLoadingDelegateHandler` instance, pass the local user's `uid` in the `localUid` parameter of the constructor.

       The `AGCollectionLoadingDelegateHandler` listens for scrolling events in the live broadcast room list and automatically preloads the live room channels that come into view (`preloadChannel`). This improves loading speed and enhances the user experience.

       ```swift
       // The following code is from the ViewController for the live room list.
       class ShowRoomListVC: UIViewController {
           // Create an instance of AGCollectionLoadingDelegateHandler.
           private lazy var delegateHandler: AGCollectionLoadingDelegateHandler = {
               let handler = AGCollectionLoadingDelegateHandler(localUid: localUid)
               return handler
           }()
           // Business service module for managing room data.
           private lazy var service: ShowServiceProtocol = AppContext.showServiceImp("")
           // List of live rooms
           private var roomList = [ShowRoomListModel]() {
               didSet {
                   // Update delegateHandler with the latest room list
                   delegateHandler.roomList = AGRoomArray(roomList: roomList)
                   collectionView.reloadData()
               }
           }

           override func viewDidLoad() {
               super.viewDidLoad()
               // Perform any additional setup after loading the view.
               // ...

               prepareEngine()

               // Set delegate and data source for the room list UI.
               listView.delegate = self.delegateHandler
               listView.dataSource = self

               // Fetch the live room list from the service.
               service.getRoomList(page: 1) { [weak self] error, roomList in
                   self?.roomList = roomList ?? []
               }
           }
       }
       ```

    3. Listen for touch events.

       In the `DataSource` protocol of the native `listView`, create a Cell inside the `cellForItemAt` method. When initializing the Cell, use the `ag_addPreloadTap` method to listen for touch events on a specific live broadcast room.

       When calling `ag_addPreloadTap`, provide the following parameters:

       * `roomInfo`: A `VideoLoader.RoomInfo` instance containing details of the live room.
       * `localUid`: The uid of the local user.
       * `enableProcess`: A callback function triggered when the user taps the Cell. Use this function to implement business logic, such as verifying whether the token has been successfully obtained.
       * `onRequireRenderVideo`: A callback function that handles rendering the host's video feed. It is recommended to create the video container in advance and assign the returned object to it when the `onRequireRenderVideo` event is triggered. This ensures that `ag_addPreloadTap` automatically attaches and renders the host’s video on the designated container.
       * `completion`: A callback function executed when the user enters the live broadcast page.

       When a user taps a live broadcast room, `ag_addPreloadTap` automatically follows best practices, including joining the selected live broadcast channel. Consequently, you do not need to manually call `joinChannelByToken` or similar methods to handle channel joining within your business logic.

       ```swift
       // The code snippet comes from the ViewController for the list of live streams.
       extension ShowRoomListVC: UICollectionViewDataSource {
           func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
               // Return the total number of rooms in the room list
               return roomList.count
           }

           func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
               let cell: ShowRoomListCell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(ShowRoomListCell.self), for: indexPath) as! ShowRoomListCell
               // Get the room data corresponding to the current index
               let room = roomList[indexPath.item]

               // Configure the Cell with room details
               // ...

               // Add a touch event to handle user interaction
               cell.ag_addPreloadTap(roomInfo: room, localUid: delegateHandler.localUid) { [weak self] state in
                   if AppContext.shared.rtcToken?.count ?? 0 == 0 {
                       // If no token has been obtained
                       if state == .began {
                           // If the touch event starts (pressing down), retrieve a new token
                           self?.preGenerateToken()
                       } else if state == .ended {
                           // If the touch event completes but the token is still unavailable, prevent the user from entering the live room
                           ToastView.show(text: "Token does not exist, try again!")
                       }
                       return false
                   }
                   return true
               } onRequireRenderVideo: { _ in
                   // Provide a container for rendering the host's video feed (if available)
                   return nil
               } completion: { [weak self] in
                   // Enter the live stream
                   self?.joinRoom(room)
               }

               return cell
           }
       }
       ```

    ### Fast channel switching [#fast-channel-switching-1]

    This section explains how to enable viewers to switch live broadcast channels quickly.

    1. Implement a UI component for sliding between live broadcast rooms.

       The following example uses a `UICollectionView`:

       ```swift
       private lazy var collectionView: UICollectionView = {
           let layout = UICollectionViewFlowLayout()
           layout.minimumLineSpacing = 0
           layout.minimumInteritemSpacing = 0
           layout.sectionInset = .zero
           layout.itemSize = self.view.bounds.size
           let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
           collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(UICollectionViewCell.self))
           collectionView.scrollsToTop = false
           collectionView.isPagingEnabled = true
           collectionView.contentInsetAdjustmentBehavior = .never
           collectionView.bounces = false
           collectionView.showsVerticalScrollIndicator = false
           return collectionView
       }()
       ```

    2. Listen for live room switching events.

       Create an `AGCollectionSlicingDelegateHandler` instance and register it as the slide event delegate for the `CollectionView` live broadcast room. To create the `AGCollectionSlicingDelegateHandler` instance, pass the following parameters to the constructor:

       * `localUid`: `uid` of the local user.
       * `needPreJoin`: Determines whether to prejoin the adjacent live broadcast rooms (above and below the current one). Setting this to `true` improves instant switching but increases resource consumption.
       * `videoType`: Switch the timing of live broadcast room video display.
       * `audioType`: Switch the timing of the live broadcast room audio display.
       * `onRequireRenderVideo`: The callback function to render the host view of the corresponding live broadcast room. At this time, pass in the container for the host screen of the corresponding live broadcast room. The `AGCollectionSlicingDelegateHandler` instance adds and renders the host screen on that container.

       The `AGCollectionSlicingDelegateHandler` instance listens to the `CollectionView` slide event of the live broadcast room. It implements the best practices encapsulated in the `AGCollectionSlicingDelegateHandler` class and switches the audio/video subscription behavior of different live broadcast rooms at the best time.

       When creating the `ViewController`, call the `roomList` method to pass the initial live broadcast room list information to the `AGCollectionSlicingDelegateHandler` instance.

       ```swift
       // The code snippet comes from the ViewController for the room list
       class ShowLivePagesViewController: ViewController {
           // Creating an AGCollectionSlicingDelegateHandler instance
           private lazy var delegateHandler = {
               let handler = AGCollectionSlicingDelegateHandler(localUid: localUid, needPrejoin: needPrejoin)
               handler.videoSlicingType = videoType
               handler.audioSlicingType = audioType
               handler.onRequireRenderVideo = { [weak self] info, cell, indexPath in
                   // Perform the appropriate action at the optimal time to render the host screen
                   return ...
               }
               return handler
           }()

           // Pass the initial live room list information to the AGCollectionSlicingDelegateHandler instance
           var roomList: [ShowRoomListModel]? {
               didSet {
                   delegateHandler.roomList = AGRoomArray(roomList: roomList)
               }
           }

           // Register the delegateHandler instance as the slide event proxy for the CollectionView in the live streaming room
           override func viewDidLoad() {
               super.viewDidLoad()

               // Setting up a proxy
               collectionView.delegate = delegateHandler
               collectionView.dataSource = self
               ...
           }
       }
       ```

    ### Release resources [#release-resources-1]

    After using the `VideoLoaderAPI`, you do not need to call `leaveChannel` in the business layer to leave the live broadcast. To release resources, refer to the following sample code:

    ```swift
    VideoLoaderApiImpl.shared.cleanCache()
    AgoraRtcEngineKit.destroy()
    ```

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

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

    In live streaming applications, a key user experience performance metric is how quickly a viewer can join or switch channels and how fast the remote video stream is rendered on their device. This document explores methods and best practices for reducing first-frame rendering time in apps using Video SDK and provides sample code for desktop and mobile devices.

    Agora provides an open source [`VideoLoaderAPI`](https://github.com/AgoraIO-Community/VideoLoaderAPI) sample project on GitHub that includes APIs for fast channel loading and switching. To integrate the APIs into your project, take the following steps:

    1. Copy the following folders and files from the repository to the same folder as your `*.xcodeproj` file:

       * [`VideoLoaderAPI`](https://github.com/AgoraIO-Community/VideoLoaderAPI/tree/main/iOS/VideoLoaderAPI) folder

       * [`VideoLoaderAPI.podspec`](https://github.com/AgoraIO-Community/VideoLoaderAPI/blob/main/iOS/VideoLoaderAPI.podspec) file

       ![preload channels best practice](https://assets-docs.agora.io/images/video-sdk/preload-channels-ios-videoloader.png)

       <CalloutContainer type="info">
         <CalloutTitle>
           Note
         </CalloutTitle>

         <CalloutDescription>
           To facilitate subsequent code upgrades, do not modify the names and paths of the files you add.
         </CalloutDescription>
       </CalloutContainer>

    \:::

    1. In the terminal, go to the directory where the `*.xcodeproj` file is located and run the `pod init` command. A **Podfile** text file is generated in the project folder.

    2. Open the **Podfile** file and modify it as follows:

       ```ruby
       source 'https://github.com/CocoaPods/Specs.git'
       use_frameworks!

       platform :ios, '13.0'

       # Change the target to the actual Target name in your project.
       target 'VideoLoaderAPI_Example' do
       pod 'VideoLoaderAPI', :path => 'VideoLoaderAPI.podspec'

           target 'VideoLoaderAPI_Tests' do
               inherit! :search_paths
           end
       end
       ```

    3. In the terminal, go to the directory where the `*.xcodeproj` file is located and execute `pod install` to install the dependencies.

       ![preload channels pod install](https://assets-docs.agora.io/images/video-sdk/preload-channels-ios-pod-install.png)

       After successful installation of the dependency library, you see a `*.xcworkspace` file.

    4. (Optional) If you have previously integrated a Video SDK dependency in your project, open the `VideoLoaderAPI.podspec` file and modify the dependency version as needed.

       ```bash
       # 4.1.1.19 is the default version installed in step 4, if you are using a different version, change it to the current one.
       s.dependency 'AgoraRtcEngine_Special_iOS', '4.1.1.19'
       ```

    Follow these steps to implement fast channel joining and switching:

    ### Prepare to use the VideoLoader API [#prepare-to-use-the-videoloader-api-2]

    Take the following steps before using the fast joining and fast switching features:

    1. Initialize `AgoraRtcEngineKit` and `VideoLoader` instances.

       In the default `ViewController` of the project file, call the `sharedEngine(with:)` method of Video SDK to create and initialize an `AgoraRtcEngineKit` instance. Then, initialize the `VideoLoader`.

       ```swift
       func prepareEngine() {
           // Create an Instance of AgoraRtcEngineKit
           let engine = _createRtcEngine()
           // Obtain a shared instance of VideoLoaderApiImpl
           let loader = VideoLoaderApiImpl.shared
           loader.addListener(listener: self)
           // Creating a VideoLoaderConfig instance
           let config = VideoLoaderConfig()
           // Setting up an AgoraRtcEngineKit instance
           config.rtcEngine = engine
           // Initialize VideoLoader
           loader.setup(config: config)
       }

       // Initializing AgoraRtcEngineKit
       private func _createRtcEngine() -> AgoraRtcEngineKit {
           let config = AgoraRtcEngineConfig()
           // Use the App Id of your project from the console
           config.appId = KeyCenter.AppId
           config.channelProfile = .liveBroadcasting
           config.audioScenario = .gameStreaming
           config.areaCode = .global
           let engine = AgoraRtcEngineKit.sharedEngine(with: config,
                                                       delegate: nil)
           return engine
       }
       ```

    2. Use a wildcard token.

       To speed up the process of users joining a channel, use a [wildcard token](/en/realtime-media/broadcast-streaming/build/authenticate-users/deploy-token-server#generate-wildcard-tokens). Generate the token on your server and pass it to the client for authentication.

       <CalloutContainer type="info">
         <CalloutTitle>
           Note
         </CalloutTitle>

         <CalloutDescription>
           Using wildcard tokens carries security risks, such as room bombing. Evaluate whether wildcard tokens are appropriate for your use case before implementation.
         </CalloutDescription>
       </CalloutContainer>

    \:::

    ### Fast channel joining [#fast-channel-joining-2]

    This section explains how to create a seamless, instant-opening experience in live broadcast scenarios.

    1. Add a UI component for the live broadcast room list.

       Implement a UI component to display a list of live broadcast rooms. The following example uses a `UICollectionView`:

       ```swift
       private lazy var listView: UICollectionView = {
           let layout = UICollectionViewFlowLayout()
           layout.minimumLineSpacing = 5
           layout.minimumInteritemSpacing = 5
           layout.sectionInset = .zero

           let w = view.bounds.width / 2 - 5
           layout.itemSize = CGSize(width: w, height: w * 1.5)

           let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
           collectionView.backgroundColor = .white
           collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: "videoloader_listcell")
           collectionView.scrollsToTop = false
           collectionView.delegate = self.delegateHandler
           collectionView.dataSource = self
           collectionView.contentInsetAdjustmentBehavior = .never
           collectionView.showsVerticalScrollIndicator = false

           return collectionView
       }()
       ```

    2. Listen for scrolling events.

       Create an instance of `AGCollectionLoadingDelegateHandler` and assign it as the delegate for the live room list UI to handle scrolling events. When initializing the `AGCollectionLoadingDelegateHandler` instance, pass the local user's `uid` in the `localUid` parameter of the constructor.

       The `AGCollectionLoadingDelegateHandler` listens for scrolling events in the live broadcast room list and automatically preloads the live room channels that come into view (`preloadChannel`). This improves loading speed and enhances the user experience.

       ```swift
       // The following code is from the ViewController for the live room list.
       class ShowRoomListVC: UIViewController {
           // Create an instance of AGCollectionLoadingDelegateHandler.
           private lazy var delegateHandler: AGCollectionLoadingDelegateHandler = {
               let handler = AGCollectionLoadingDelegateHandler(localUid: localUid)
               return handler
           }()
           // Business service module for managing room data.
           private lazy var service: ShowServiceProtocol = AppContext.showServiceImp("")
           // List of live rooms
           private var roomList = [ShowRoomListModel]() {
               didSet {
                   // Update delegateHandler with the latest room list
                   delegateHandler.roomList = AGRoomArray(roomList: roomList)
                   collectionView.reloadData()
               }
           }

           override func viewDidLoad() {
               super.viewDidLoad()
               // Perform any additional setup after loading the view.
               // ...

               prepareEngine()

               // Set delegate and data source for the room list UI.
               listView.delegate = self.delegateHandler
               listView.dataSource = self

               // Fetch the live room list from the service.
               service.getRoomList(page: 1) { [weak self] error, roomList in
                   self?.roomList = roomList ?? []
               }
           }
       }
       ```

    3. Listen for touch events.

       In the `DataSource` protocol of the native `listView`, create a Cell inside the `cellForItemAt` method. When initializing the Cell, use the `ag_addPreloadTap` method to listen for touch events on a specific live broadcast room.

       When calling `ag_addPreloadTap`, provide the following parameters:

       * `roomInfo`: A `VideoLoader.RoomInfo` instance containing details of the live room.
       * `localUid`: The uid of the local user.
       * `enableProcess`: A callback function triggered when the user taps the Cell. Use this function to implement business logic, such as verifying whether the token has been successfully obtained.
       * `onRequireRenderVideo`: A callback function that handles rendering the host's video feed. It is recommended to create the video container in advance and assign the returned object to it when the `onRequireRenderVideo` event is triggered. This ensures that `ag_addPreloadTap` automatically attaches and renders the host’s video on the designated container.
       * `completion`: A callback function executed when the user enters the live broadcast page.

       When a user taps a live broadcast room, `ag_addPreloadTap` automatically follows best practices, including joining the selected live broadcast channel. Consequently, you do not need to manually call `joinChannelByToken` or similar methods to handle channel joining within your business logic.

       ```swift
       // The code snippet comes from the ViewController for the list of live streams.
       extension ShowRoomListVC: UICollectionViewDataSource {
           func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
               // Return the total number of rooms in the room list
               return roomList.count
           }

           func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
               let cell: ShowRoomListCell = collectionView.dequeueReusableCell(withReuseIdentifier: NSStringFromClass(ShowRoomListCell.self), for: indexPath) as! ShowRoomListCell
               // Get the room data corresponding to the current index
               let room = roomList[indexPath.item]

               // Configure the Cell with room details
               // ...

               // Add a touch event to handle user interaction
               cell.ag_addPreloadTap(roomInfo: room, localUid: delegateHandler.localUid) { [weak self] state in
                   if AppContext.shared.rtcToken?.count ?? 0 == 0 {
                       // If no token has been obtained
                       if state == .began {
                           // If the touch event starts (pressing down), retrieve a new token
                           self?.preGenerateToken()
                       } else if state == .ended {
                           // If the touch event completes but the token is still unavailable, prevent the user from entering the live room
                           ToastView.show(text: "Token does not exist, try again!")
                       }
                       return false
                   }
                   return true
               } onRequireRenderVideo: { _ in
                   // Provide a container for rendering the host's video feed (if available)
                   return nil
               } completion: { [weak self] in
                   // Enter the live stream
                   self?.joinRoom(room)
               }

               return cell
           }
       }
       ```

    ### Fast channel switching [#fast-channel-switching-2]

    This section explains how to enable viewers to switch live broadcast channels quickly.

    1. Implement a UI component for sliding between live broadcast rooms.

       The following example uses a `UICollectionView`:

       ```swift
       private lazy var collectionView: UICollectionView = {
           let layout = UICollectionViewFlowLayout()
           layout.minimumLineSpacing = 0
           layout.minimumInteritemSpacing = 0
           layout.sectionInset = .zero
           layout.itemSize = self.view.bounds.size
           let collectionView = UICollectionView(frame: self.view.bounds, collectionViewLayout: layout)
           collectionView.register(UICollectionViewCell.self, forCellWithReuseIdentifier: NSStringFromClass(UICollectionViewCell.self))
           collectionView.scrollsToTop = false
           collectionView.isPagingEnabled = true
           collectionView.contentInsetAdjustmentBehavior = .never
           collectionView.bounces = false
           collectionView.showsVerticalScrollIndicator = false
           return collectionView
       }()
       ```

    2. Listen for live room switching events.

       Create an `AGCollectionSlicingDelegateHandler` instance and register it as the slide event delegate for the `CollectionView` live broadcast room. To create the `AGCollectionSlicingDelegateHandler` instance, pass the following parameters to the constructor:

       * `localUid`: `uid` of the local user.
       * `needPreJoin`: Determines whether to prejoin the adjacent live broadcast rooms (above and below the current one). Setting this to `true` improves instant switching but increases resource consumption.
       * `videoType`: Switch the timing of live broadcast room video display.
       * `audioType`: Switch the timing of the live broadcast room audio display.
       * `onRequireRenderVideo`: The callback function to render the host view of the corresponding live broadcast room. At this time, pass in the container for the host screen of the corresponding live broadcast room. The `AGCollectionSlicingDelegateHandler` instance adds and renders the host screen on that container.

       The `AGCollectionSlicingDelegateHandler` instance listens to the `CollectionView` slide event of the live broadcast room. It implements the best practices encapsulated in the `AGCollectionSlicingDelegateHandler` class and switches the audio/video subscription behavior of different live broadcast rooms at the best time.

       When creating the `ViewController`, call the `roomList` method to pass the initial live broadcast room list information to the `AGCollectionSlicingDelegateHandler` instance.

       ```swift
       // The code snippet comes from the ViewController for the room list
       class ShowLivePagesViewController: ViewController {
           // Creating an AGCollectionSlicingDelegateHandler instance
           private lazy var delegateHandler = {
               let handler = AGCollectionSlicingDelegateHandler(localUid: localUid, needPrejoin: needPrejoin)
               handler.videoSlicingType = videoType
               handler.audioSlicingType = audioType
               handler.onRequireRenderVideo = { [weak self] info, cell, indexPath in
                   // Perform the appropriate action at the optimal time to render the host screen
                   return ...
               }
               return handler
           }()

           // Pass the initial live room list information to the AGCollectionSlicingDelegateHandler instance
           var roomList: [ShowRoomListModel]? {
               didSet {
                   delegateHandler.roomList = AGRoomArray(roomList: roomList)
               }
           }

           // Register the delegateHandler instance as the slide event proxy for the CollectionView in the live streaming room
           override func viewDidLoad() {
               super.viewDidLoad()

               // Setting up a proxy
               collectionView.delegate = delegateHandler
               collectionView.dataSource = self
               ...
           }
       }
       ```

    ### Release resources [#release-resources-2]

    After using the `VideoLoaderAPI`, you do not need to call `leaveChannel` in the business layer to leave the live broadcast. To release resources, refer to the following sample code:

    ```swift
    VideoLoaderApiImpl.shared.cleanCache()
    AgoraRtcEngineKit.destroy()
    ```

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