Blog Infos
Author
Published
Topics
, ,
Author
Published

Posted by: Shashank Mishra

I have been playing around with Compose and recently implemented video playback in a list. While there are tons of great examples on
how to do it with a RecyclerView, I couldn’t find any for Compose.

So in this article, I will describe how I went about doing the same in Compose. This article assumes you have some familiarity with Compose and ExoPlayer.

Let’s get started then — this is what we will be building.

I have been playing around with Compose and recently implemented video playback in a list. While there are tons of great examples on how to do it with a RecyclerView, I couldn’t find any for Compose. So in this article, I will describe how I went about doing the same in Compose. This article assumes you have some familiarity with Compose and ExoPlayer.
Thinking it through ?

Before jumping into the code, let’s think about the scenarios we’ll be dealing with:
* A single video item is visible
* Multiple video items are visible

In a case of a single item, it’s pretty straightforward — we start the video playback as soon as the item is visible on the screen. In the case of multiple videos, however, we want to play the video which is closest to the center of the screen.

We also want to use a single ExoPlayer instance instead of creating a new one for each video.

With that in mind, let’s dive into the code ?

Detecting the currently playing item ?
val listState = rememberLazyListState()
val currentlyPlayingItem = determineCurrentlyPlayingItem(listState, state.items)
LazyColumn(state = listState) {
...
}
...
private fun determineCurrentlyPlayingItem(listState: LazyListState, items: TweetItems): TweetItem? {
val layoutInfo = listState.layoutInfo
val visibleTweets = layoutInfo.visibleItemsInfo.map { items[it.index] }
val tweetsWithVideo = visibleTweets.filter { it.hasAnimatedMedia }
return if (tweetsWithVideo.size == 1) {
tweetsWithVideo.first()
} else {
val midPoint = (layoutInfo.viewportStartOffset + layoutInfo.viewportEndOffset) / 2
val itemsFromCenter =
layoutInfo.visibleItemsInfo.sortedBy { abs((it.offset + it.size / 2) - midPoint) }
itemsFromCenter.map { items[it.index] }.firstOrNull { it.hasAnimatedMedia }
}
}
view raw LazyColumn.kt hosted with ❤ by GitHub

There’s a lot going on in the code so let’s go through it bit by bit. Firstly, we will be providing a listState to the LazyColumn as we will be using it to access the currently visible tweets. Moving onto the method determineCurrentlyPlayingItem, we first check if any of the visible tweets have a video or gif in them. If we only find one such tweet, then we simply return that to be played. Else, we sort the column items by their distance from the center of the column and return the first item from that list.

Instantiating ExoPlayer ?

Next up, we need to maintain an instance of ExoPlayer and update it to play the currentlyPlayingItem determined in the previous step. We will use remember to maintain an instance of SimpleExoPlayer for the lifetime of the screen.

val context = LocalContext.current
val exoPlayer = remember {
SimpleExoPlayer.Builder(context).build().apply {
repeatMode = Player.REPEAT_MODE_ALL
}
}
view raw LazyColumn.kt hosted with ❤ by GitHub

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Putting a Jetpack on your legacy codebase

At Pinterest, we are always working on using the latest technologies when possible in our 10+ year old codebase.
Watch Video

Putting a Jetpack on your legacy codebase

Kurt Nelson
Senior Software Engineer

Putting a Jetpack on your legacy codebase

Kurt Nelson
Senior Software Engi ...

Putting a Jetpack on your legacy codebase

Kurt Nelson
Senior Software Engineer

Jobs

For video playback, we will pass the currentlyPlayingItem to the following method which will update the media source of the player.

@Composable
private fun updateCurrentlyPlayingItem(exoPlayer: SimpleExoPlayer, tweet: TweetItem?) {
val context = LocalContext.current
LaunchedEffect(tweet) {
exoPlayer.apply {
if (tweet != null)) {
val dataSourceFactory = DefaultDataSourceFactory(
context,
Util.getUserAgent(context, context.packageName)
)
val source = ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(Uri.parse(tweet.media.url)))
setMediaSource(source)
prepare()
playWhenReady = true
} else {
stop()
}
}
}
}
view raw LazyColumn.kt hosted with ❤ by GitHub

However, if the tweet is null, we stop the playback so that any off-screen video isn’t playing anymore.

While it might seem like we are done, there is little thing we have forgotten about.

Just because we are in Compose-land doesn’t mean we get to ignore the Android lifecycle. We will need to pause/resume the playback based on the lifecycle state as well as dispose of the ExoPlayer instance when the lifecycle is destroyed.

Thankfully, Compose provides a handy DisposableEffect which allows you to perform cleanup whenever the key changes. We will use a lifecycleOwner as the key.

val lifecycleOwner by rememberUpdatedState(LocalLifecycleOwner.current)
DisposableEffect(lifecycleOwner) {
val lifecycle = lifecycleOwner.lifecycle
val observer = LifecycleEventObserver { _, event ->
when (event) {
Lifecycle.Event.ON_PAUSE -> {
exoPlayer.playWhenReady = false
}
Lifecycle.Event.ON_RESUME -> {
exoPlayer.playWhenReady = true
}
Lifecycle.Event.ON_DESTROY -> {
exoPlayer.run {
stop()
release()
}
}
}
}
lifecycle.addObserver(observer)
onDispose {
lifecycle.removeObserver(observer)
}
}
view raw LazyColumn.kt hosted with ❤ by GitHub

This code is pretty self-explanatory — we just update the player for various lifecycle events.

Playing the video ▶️

Last but not the least, we need to use the ExoPlayer instance we created to actually play the video. Since there is no PlayerView Composable yet, we will leverage Compose interoperability and use AndroidView instead.

Box {
AndroidView(
factory = { context ->
context.inflate<PlayerView>(R.layout.player_view).apply {
layoutParams = ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)
player = exoPlayer
}
}
)
}
view raw LazyColumn.kt hosted with ❤ by GitHub
Fin ??

Well, that’s about it. I was pleasantly surprised by how simple the Composeimplementation is as compared to its View based counterpart as we don’t have to deal with the recycling of views.
Feel free to hit me up at @SHKM9 if you have any further questions or thoughts!

View original article at:


Originally published: July 20, 2021

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
Compose is a relatively young technology for writing declarative UI. Many developers don’t even…
READ MORE
blog
When it comes to the contentDescription-attribute, I’ve noticed a couple of things Android devs…
READ MORE
blog
In this article we’ll go through how to own a legacy code that is…
READ MORE
blog
Compose is part of the Jetpack Library released by Android last spring. Create Android…
READ MORE

Leave a Reply

Your email address will not be published. Required fields are marked *

Fill out this field
Fill out this field
Please enter a valid email address.

Menu