Getting Started with Video Feeds on Android

The Bitmovin Android SDK provides an experimental Video Feed API for building short-form video feeds. The API is UI-framework independent: your app owns the list, pager, gestures, and overlay UI, while the SDK manages reusable Player instances, source assignment, preloading, lifecycle forwarding, and per-item status.

Prerequisites

  • Basic understanding of Android development with Kotlin
  • Bitmovin Player Android SDK added to your project (see Getting Started guide)
  • A set of playable SourceConfig objects
  • (optional) Jetpack Compose if you want to follow the UI example below

Overview

The Video Feed API is built around one controller for the whole feed:

  • VideoFeedPlayerController owns a small pool of Player instances and assigns them to feed items.
  • VideoFeedItem describes one item in the feed.
  • VideoFeedItemState exposes the current item, assigned player, status, and error state.
  • VideoFeedPlayerConfig configures the player pool, buffering policy, and fast-swipe behavior.

The controller is headless. It does not render UI or provide scrolling. Attach each non-null VideoFeedItemState.player to your own PlayerView, and call select(index) whenever the visible feed item changes.

📘

Experimental API

The Video Feed API is marked with @ExperimentalBitmovinApi. Opt in at the usage site or module level.

Add the dependency

Add the Video Feed module next to your existing Bitmovin Player dependency:

dependencies {
    implementation("com.bitmovin.player:player-video-feed:<bitmovin-player-version>")
}

If you use the Compose PlayerView wrapper, also add the Compose UI module:

dependencies {
    implementation("com.bitmovin.player:player-ui-web-compose:<bitmovin-player-version>")
}

Create feed items

Each feed item needs a stable, unique ID and a SourceConfig.

import com.bitmovin.player.api.source.SourceConfig
import com.bitmovin.player.api.source.SourceType
import com.bitmovin.player.api.videofeed.VideoFeedItem

private fun createFeedItems(): List<VideoFeedItem> = listOf(
    VideoFeedItem(
        id = "clip-1",
        sourceConfig = SourceConfig(
            url = "https://example.com/clip-1.m3u8",
            type = SourceType.Hls,
            title = "Clip 1",
        ),
    ),
    VideoFeedItem(
        id = "clip-2",
        sourceConfig = SourceConfig(
            url = "https://example.com/clip-2.m3u8",
            type = SourceType.Hls,
            title = "Clip 2",
        ),
    ),
)

Create the controller

Create one VideoFeedPlayerController for the whole feed, usually in a ViewModel. Keep the controller on the main thread and release it when the owner is cleared.

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import com.bitmovin.player.api.ExperimentalBitmovinApi
import com.bitmovin.player.api.videofeed.VideoFeedItem
import com.bitmovin.player.api.videofeed.VideoFeedItemState
import com.bitmovin.player.api.videofeed.VideoFeedPlayerConfig
import com.bitmovin.player.api.videofeed.VideoFeedPlayerController
import kotlinx.coroutines.flow.StateFlow

@OptIn(ExperimentalBitmovinApi::class)
class VideoFeedViewModel(application: Application) : AndroidViewModel(application) {
    private val controller = VideoFeedPlayerController(
        context = application.applicationContext,
        items = createFeedItems(),
        config = VideoFeedPlayerConfig(maxPlayers = 5),
    )

    val itemStates: StateFlow<List<VideoFeedItemState>> = controller.items

    init {
        // Optional: assign the first pooled player before the UI becomes visible.
        controller.preload(0)
    }

    fun select(index: Int) {
        controller.select(index)
    }

    fun appendItems(items: List<VideoFeedItem>) {
        controller.appendItems(items)
    }

    fun play() = controller.play()

    fun pause() = controller.pause()

    fun retry() = controller.retry()

    fun onStart() = controller.onStart()

    fun onResume() = controller.onResume()

    fun onPause() = controller.onPause()

    fun onStop() = controller.onStop()

    override fun onCleared() {
        controller.release()
    }
}

Render the current player

Observe controller.items and attach each assigned Player to your UI. The controller owns the players, so the UI must not destroy them.

The following Compose example uses a pager, but the same approach works with any list or custom UI.

import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.pager.VerticalPager
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import com.bitmovin.player.api.ExperimentalBitmovinApi
import com.bitmovin.player.api.ui.PlayerViewConfig
import com.bitmovin.player.api.ui.UiConfig
import com.bitmovin.player.api.ui.compose.wrapper.PlayerView
import com.bitmovin.player.api.videofeed.VideoFeedItemStatus
import kotlinx.coroutines.flow.distinctUntilChanged

@OptIn(ExperimentalFoundationApi::class, ExperimentalBitmovinApi::class)
@Composable
fun VideoFeedScreen(viewModel: VideoFeedViewModel) {
    val itemStates by viewModel.itemStates.collectAsState()
    val pagerState = rememberPagerState(pageCount = { itemStates.size })

    LaunchedEffect(pagerState) {
        snapshotFlow { pagerState.currentPage }
            .distinctUntilChanged()
            .collect(viewModel::select)
    }

    VerticalPager(
        state = pagerState,
        modifier = Modifier.fillMaxSize(),
    ) { page ->
        val itemState = itemStates[page]

        Box(Modifier.fillMaxSize()) {
            itemState.player?.let { player ->
                PlayerView(
                    player = player,
                    modifier = Modifier.fillMaxSize(),
                    playerViewConfig = PlayerViewConfig(uiConfig = UiConfig.Disabled),
                )
            }

            when (itemState.status) {
                VideoFeedItemStatus.Unassigned,
                VideoFeedItemStatus.Loading,
                -> Text("Loading…", modifier = Modifier.align(Alignment.Center))
                VideoFeedItemStatus.Failed -> Text("Playback failed", modifier = Modifier.align(Alignment.Center))
                VideoFeedItemStatus.Ready,
                VideoFeedItemStatus.Rendered,
                -> Unit
            }
        }
    }
}

Forward lifecycle events

Forward host lifecycle events to the controller so all pooled players stay in sync with the Activity or Fragment lifecycle.

import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner

class VideoFeedLifecycleObserver(
    private val viewModel: VideoFeedViewModel,
) : DefaultLifecycleObserver {
    override fun onStart(owner: LifecycleOwner) = viewModel.onStart()

    override fun onResume(owner: LifecycleOwner) = viewModel.onResume()

    override fun onPause(owner: LifecycleOwner) = viewModel.onPause()

    override fun onStop(owner: LifecycleOwner) = viewModel.onStop()
}

Append more items

For long or endless feeds, append more items before the user reaches the end. Appended items must use IDs that are unique across the whole controller feed.

fun onPageSelected(index: Int) {
    if (index >= itemStates.value.lastIndex - 1) {
        appendItems(createMoreFeedItems())
    }
    select(index)
}

Handle playback controls and errors

Use the controller for feed-level playback actions. Use VideoFeedItemState.status and VideoFeedItemState.error for loading, failed, and retry UI.

fun onPlayClicked() = viewModel.play()

fun onPauseClicked() = viewModel.pause()

fun onRetryClicked() = viewModel.retry()

Configure buffering and pool size

The default configuration works for most feeds. Tune maxPlayers if you need more or fewer pooled players.

import com.bitmovin.player.api.videofeed.VideoFeedBufferPolicy
import com.bitmovin.player.api.videofeed.VideoFeedPlayerConfig

val config = VideoFeedPlayerConfig(
    maxPlayers = 5,
    fastSwipeThresholdMs = 2_000L,
    bufferPolicy = VideoFeedBufferPolicy(
        currentForwardSeconds = 30.0,
        currentBackwardSeconds = 4.0,
        forwardSeconds = listOf(8.0, 5.0, 3.0),
        previousForwardSeconds = listOf(4.0, 2.0),
        fastSwipeFactor = 0.5,
    ),
)

Fast-swipe mode is entered when consecutive selections happen within fastSwipeThresholdMs. While active, configured buffer targets are multiplied by fastSwipeFactor to reduce buffering work for quickly skipped items.

Conclusion

🎉

That's it!

You have now set up a basic video feed using the Bitmovin Android SDK 🥳

These instructions cover the basic integration flow. The API reference provides more details about the available configuration and state APIs.

References


Did this page help you?