Skip to main content
Android
iOS
macOS
Web
Windows
Electron
Flutter
React Native
React JS
Unity
Unreal Engine
Unreal (Blueprint)

Preload channels

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.

Prerequisites

To test the code used in this page you need to have:

  • An Agora account and project.
  • A computer with Internet access. Ensure that no firewall is blocking your network communication.

Project setup

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 directory to your project:


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

Note

To facilitate future code upgrades, do not modify the names and paths of these files.

Implementation

Follow these steps to implement fast channel joining and switching:

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:


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

  2. Use a wildcard token.

    To speed up the process of users joining a channel, use a wildcard token. Generate the token on your server and pass it to the client for authentication.

    Note

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

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:


    _12
    <androidx.recyclerview.widget.RecyclerView
    _12
    android:id="@+id/rvRooms"
    _12
    android:layout_width="match_parent"
    _12
    android:layout_height="match_parent"
    _12
    android:paddingHorizontal="7.5dp"
    _12
    android:overScrollMode="never"
    _12
    app:layoutManager="androidx.recyclerview.widget.GridLayoutManager"
    _12
    app:spanCount="2"
    _12
    android:clipToPadding="false"
    _12
    android:clipChildren="false"
    _12
    android:paddingBottom="50dp"
    _12
    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).


    _19
    // Code snippet from RoomListActivity
    _19
    class RoomListActivity : AppCompatActivity() {
    _19
    private val mBinding by lazy { ShowRoomListActivityBinding.inflate(LayoutInflater.from(this)) }
    _19
    // Create an OnRoomListScrollEventHandler instance
    _19
    private val onRoomListScrollEventHandler: OnRoomListScrollEventHandler = object : OnRoomListScrollEventHandler(mRtcEngine, RtcEngineInstance.localUid()) {}
    _19
    // Business service module
    _19
    private val mService by lazy { ShowServiceProtocol.getImplInstance() }
    _19
    _19
    override fun onCreate(savedInstanceState: Bundle?) {
    _19
    // Set the OnRoomListScrollEventHandler instance to the live room list to listen for scroll events
    _19
    mBinding.rvRooms.addOnScrollListener(onRoomListScrollEventHandler as OnRoomListScrollEventHandler)
    _19
    _19
    // Get room List
    _19
    mService.getRoomList { roomList ->
    _19
    // After getting the room list, pass the result to the onRoomListScrollEventHandler instance
    _19
    onRoomListScrollEventHandler?.updateRoomList(roomList)
    _19
    }
    _19
    }
    _19
    }

  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.


    _59
    // Code snippet from RoomListActivity
    _59
    class RoomListActivity : AppCompatActivity() {
    _59
    private val mBinding by lazy { ShowRoomListActivityBinding.inflate(LayoutInflater.from(this)) }
    _59
    private lateinit var mRoomAdapter: BindingSingleAdapter<ShowRoomDetailModel, ShowRoomItemBinding>
    _59
    _59
    override fun onCreate(savedInstanceState: Bundle?) {
    _59
    mRoomAdapter = object : BindingSingleAdapter<ShowRoomDetailModel, ShowRoomItemBinding>() {
    _59
    override fun onBindViewHolder(
    _59
    holder: BindingViewHolder<ShowRoomItemBinding>,
    _59
    position: Int
    _59
    ) {
    _59
    // UI for individual live streams
    _59
    val roomInfo = getItem(position) ?: return
    _59
    // Create an OnLiveRoomItemTouchEventHandler instance
    _59
    val onTouchEventHandler = object : OnLiveRoomItemTouchEventHandler(
    _59
    // Previously initialized RtcEngine instance
    _59
    mRtcEngine,
    _59
    // Room Information
    _59
    VideoLoader.RoomInfo(
    _59
    roomInfo.roomId,
    _59
    arrayListOf(
    _59
    VideoLoader.AnchorInfo(
    _59
    roomInfo.roomId,
    _59
    roomInfo.ownerId.toInt(),
    _59
    // Token mentioned above
    _59
    // Sample code for wildcard token
    _59
    RtcEngineInstance.generalToken()
    _59
    )
    _59
    )
    _59
    ),
    _59
    RtcEngineInstance.localUid()
    _59
    ) {
    _59
    override fun onTouch(v: View?, event: MotionEvent?): Boolean {
    _59
    when (event!!.action) {
    _59
    MotionEvent.ACTION_UP -> {
    _59
    if (RtcEngineInstance.generalToken() != "") {
    _59
    super.onTouch(v, event)
    _59
    // Listen to the ACTION_UP event and go to the in-stream page
    _59
    goLiveDetailActivity(list, position, roomInfo)
    _59
    }
    _59
    }
    _59
    }
    _59
    return true
    _59
    }
    _59
    _59
    // Informs you to render the host screen
    _59
    override fun onRequireRenderVideo(info: VideoLoader.AnchorInfo): VideoLoader.VideoCanvasContainer? {
    _59
    // The best time to render the host screen
    _59
    return ...
    _59
    }
    _59
    }
    _59
    _59
    // Set the OnLiveRoomItemTouchEventHandler instance to a single live room to listen for touch events
    _59
    binding.root.setOnTouchListener(onTouchEventHandler)
    _59
    }
    _59
    }
    _59
    mBinding.rvRooms.adapter = mRoomAdapter
    _59
    }
    _59
    }

    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

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:


    _8
    <androidx.viewpager2.widget.ViewPager2 xmlns:android="http://schemas.android.com/apk/res/android"
    _8
    android:id="@+id/viewPager2"
    _8
    android:orientation="vertical"
    _8
    android:layout_width="match_parent"
    _8
    android:overScrollMode="never"
    _8
    xmlns:app="http://schemas.android.com/apk/res-auto"
    _8
    app:layout_scrollEffect="none"
    _8
    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.

    _52
    // Code snippet from LiveViewPagerActivity
    _52
    class LiveViewPagerActivity : AppCompatActivity() {
    _52
    private val mBinding by lazy { LiveViewPagerActivityBinding.inflate(LayoutInflater.from(this)) }
    _52
    // Use a circular array to store a list of live rooms that a user can switch between
    _52
    private val vpFragments = SparseArray<LiveViewPagerFragment>()
    _52
    // Create OnPageScrollEventHandler instance
    _52
    private var onPageScrollEventHandler: OnPageScrollEventHandler? = object : OnPageScrollEventHandler(
    _52
    // Previously initialized RtcEngine instance
    _52
    RtcEngineInstance.rtcEngine,
    _52
    // Local user's uid
    _52
    RtcEngineInstance.localUid(),
    _52
    // Set whether or not to join the two live rooms adjacent to the current live room ahead of time
    _52
    // If set to true, this results in faster switching, but at an increased cost
    _52
    needPreJoin,
    _52
    // Toggles the timing of the live feed out via the onPageScrollStateChanged event.
    _52
    sliceMode
    _52
    ) {
    _52
    override fun onPageScrollStateChanged(state: Int) {
    _52
    when (state) {
    _52
    ViewPager2.SCROLL_STATE_SETTLING -> binding.viewPager2.isUserInputEnabled = false
    _52
    ViewPager2.SCROLL_STATE_IDLE -> binding.viewPager2.isUserInputEnabled = true
    _52
    }
    _52
    super.onPageScrollStateChanged(state)
    _52
    }
    _52
    _52
    override fun onPageStartLoading(position: Int) {
    _52
    // Notify the corresponding position of the live room to start displaying
    _52
    vpFragments[position]?.startLoadPageSafely()
    _52
    }
    _52
    _52
    override fun onPageLoaded(position: Int) {
    _52
    // Notify the corresponding position that the live room has been displayed
    _52
    vpFragments[position]?.onPageLoaded()
    _52
    }
    _52
    _52
    override fun onPageLeft(position: Int) {
    _52
    // Notify the corresponding position that the live room has left
    _52
    vpFragments[position]?.stopLoadPage(true)
    _52
    }
    _52
    _52
    override fun onRequireRenderVideo(position: Int, info: VideoLoader.AnchorInfo): VideoLoader.VideoCanvasContainer? {
    _52
    // 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.
    _52
    return vpFragments[position]?.initAnchorVideoView(info)
    _52
    }
    _52
    }
    _52
    _52
    // The actions that are performed when an Activity is created are described below.
    _52
    override fun onCreate(savedInstanceState: Bundle?) {
    _52
    // To be added
    _52
    ...
    _52
    }
    _52
    }

    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?) {}:


    _67
    override fun onCreate(savedInstanceState: Bundle?) {
    _67
    // When initializing the Activity, pass the initial live room list to the onPageScrollEventHandler instance
    _67
    onPageScrollEventHandler?.updateRoomList(list)
    _67
    _67
    // Set ViewPager2 to keep at least one page (Fragment) on each side of the current page in memory
    _67
    binding.viewPager2.offscreenPageLimit = 1
    _67
    _67
    // Create a FragmentStateAdapter to manage pages representing live rooms
    _67
    val fragmentAdapter = object : FragmentStateAdapter(this) {
    _67
    // If ViewPager2 allows sliding, it holds an infinite number of pages
    _67
    // Otherwise, it returns 1, holding only a single page.
    _67
    override fun getItemCount() = if (mScrollable) Int.MAX_VALUE else 1
    _67
    _67
    // Create a LiveViewPagerFragment for each live feed
    _67
    override fun createFragment(position: Int): Fragment {
    _67
    val roomInfo = if (mScrollable) {
    _67
    mRoomInfoList[position % mRoomInfoList.size]
    _67
    } else {
    _67
    mRoomInfoList[selectedRoomIndex]
    _67
    }
    _67
    return LiveViewPagerFragment.newInstance(
    _67
    roomInfo,
    _67
    onPageScrollEventHandler as OnPageScrollEventHandler, position
    _67
    ).apply {
    _67
    // Store the created LiveViewPagerFragment in vpFragments
    _67
    vpFragments.put(position, this)
    _67
    _67
    // Create a list of hosts for the live room
    _67
    val anchorList = arrayListOf(
    _67
    VideoLoader.AnchorInfo(
    _67
    roomInfo.roomId,
    _67
    roomInfo.ownerId.toInt(),
    _67
    RtcEngineInstance.generalToken()
    _67
    )
    _67
    )
    _67
    _67
    // Notify onPageScrollEventHandler that the corresponding room has been created
    _67
    onPageScrollEventHandler?.onRoomCreated(
    _67
    position,
    _67
    VideoLoader.RoomInfo(roomInfo.roomId, hostList),
    _67
    position == binding.viewPager2.currentItem
    _67
    )
    _67
    }
    _67
    }
    _67
    }
    _67
    binding.viewPager2.adapter = fragmentAdapter
    _67
    _67
    // Set whether the user can manually swipe pages
    _67
    // In a typical live room scenario, viewers can swipe manually, but hosts cannot.
    _67
    binding.viewPager2.isUserInputEnabled = mScrollable
    _67
    _67
    if (mScrollable) {
    _67
    // If swiping is enabled:
    _67
    // - Register the OnPageScrollEventHandler instance to listen for page change events
    _67
    // - Calculate the latest position
    _67
    binding.viewPager2.registerOnPageChangeCallback(
    _67
    onPageScrollEventHandler as OnPageChangeCallback
    _67
    )
    _67
    binding.viewPager2.setCurrentItem(
    _67
    Int.MAX_VALUE / 2 - Int.MAX_VALUE / 2 % mRoomInfoList.size + selectedRoomIndex,
    _67
    false
    _67
    )
    _67
    } else {
    _67
    // If swiping is disabled, set the current position to 0
    _67
    currLoadPosition = 0
    _67
    }
    _67
    }

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:


_3
VideoLoader.getImplInstance(mRtcEngine).cleanCache()
_3
VideoLoader.release()
_3
RtcEngineEx.destroy()

Interactive Live Streaming