Giter Site home page Giter Site logo

videopager's Introduction

demo

It's pretty straightforward to get started using ExoPlayer by following the library's Hello world! documentation. Once you throw Android's lifecycles and state management into the mix, however, things quickly get more complex. It's easy to accidentally leak memory and hard to coordinate things in a way that's efficient and looks good in the UI. And that's just for playback of videos on a simple screen! Combining ExoPlayer with a ViewPager or RecyclerView adds a whole 'nother layer of complexity.

With this repository I wanted to demonstrate a small app showing how to do Instagram/YouTube Shorts/TikTok style video paging in a way that reconciles Android lifecycles and state management.

The approach I took is to reuse the same ExoPlayer and PlayerView instance for all pages. The ExoPlayer instance lives in and is managed by MainViewModel, and a singular PlayerView gets attached to whichever ViewHolder is active on the current page. Having only one PlayerView makes for a lot less state management.

I've been a bit more verbose with comments than I typically would in this repository, for the purposes of clarity. For testability I've added abstractions that wrap a few ExoPlayer APIs. I've also used an MVI architecture; this increases boilerplate but greatly minimizes state management.

This repository includes examples of streaming videos over the network (from Reddit /r/tiktokcringe) and local assets. It can be modified to handle video data coming from any data source.

videopager's People

Contributors

aplourde avatar nickrose-ms avatar nihk avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

videopager's Issues

How to use with remote data

Hi, asking for help on how to use with remote data

I have a video app where a user posts a video to firebase realtime db and then i have to fetch and display the videos

My repository for fetching videos

override suspend fun getVidoRandVideos(): Flow<FlowDataState<List>> {
return realFire.getReference(firePath.getAllVideosPath()).also { it.keepSynced(true) }
.limitToFirst(12)
.awaitSingleValueEventList()

}

// getting the list of videos

override fun videoData(): Flow<List> {
return flowOf(videoData)
}
override suspend fun getData() {
getVidoRandVideos().collect {
when (it) {
is FlowDataState.Success -> {
videoData = it.data
}
is FlowDataState.Error -> {

            }
        }
    }
}

How can use the VideoDataRepositories to fetch this data instead of hard coding it ?

interface VideoDataRepository {
fun videoData(): Flow<List>
}

class AssetVideoDataRepository : VideoDataRepository {
override fun videoData(): Flow<List> {
return flowOf(videoData)
}
companion object {

// DONT WANT TO HARD CODE BUT SHOULD FETCH FROM REMOTE DATASOURCE AND ALSO BE ABLE TO OBSERVE IN THE VIEWMODEL
private val videoData = listOf(
RemoteVideo("https://firebasestorage.googleapis.com/v0/b96c-5868-4a4b-a48e-c8e9f3dd7b13?alt=media&token=14a443fd-75e9-42a8-880e-5b8a6df5e0a2", description="#nice the best" , tags= mapOf(), duration=831, videoId="2c7effa1-319e-4073-948f-20543c60503b", dateCreated=1628120270055, likes=0, views=0, authorUid="loUXJN7p4WbzYke94eiRq7QK64Y2", totalCommentsSize=0),
RemoteVideo("https://firebasestorage.googleapis.com/v0/boUXJN7p4WbzYke94eiRq7QK64Y2%2F699e8432-257a-4663-877e-9b29807d564f?alt=media&token=e5042726-2ec5-4d42-b8e4-23cf5169b780", description="#nice the best" , tags= mapOf(), duration=831, videoId="2c7effa1-319e-4073-948f-20543c60503b", dateCreated=1628120270055, likes=0, views=0, authorUid="loUXJN7p4WbzYke94eiRq7QK64Y2", totalCommentsSize=0),
RemoteVideo("https://-3af2-44ed-8d1a-8b09134aaf07", description="#nice the best" , tags= mapOf(), duration=831, videoId="2c7effa1-319e-4073-948f-20543c60503b", dateCreated=1628120270055, likes=0, views=0, authorUid="loUXJN7p4WbzYke94eiRq7QK64Y2", totalCommentsSize=0),
)
}
}

๐Ÿ’ญ How to support pagination with this?

๐Ÿค” How to support pagination with this?

I am trying to fetch the videos but with pagination. And I already setup required things, but not sure how to fit it in the end. As I find this code a bit complex architecture to understand. I don't have much experience with flows.

What I did for paging is:

  • VideoDataSource
  • PagerPagingAdapter

Now problem here is, in the code there are many things which are shared in a flow, which extends ViewResult, I am referring to the VideoPagerViewModel, according to the code I am able to use Flow<List> but according to my implementation I will be getting Flow<PagingData> this is where the issue is happening, I can't make the PagingData extend ViewResult directly also. So how can I achieve the pagination in this? Because in ShortsFragment I can see that code fetches all the videos at once.

ShortsFragment (๐Ÿ—๏ธ Already Present)

.
.
val states = viewModel.states
            .onEach { state ->
                // Await the list submission so that the adapter list is in sync with state.videoData
                adapter.awaitList(state.videoData)
                }
.
.

VideoPagerViewModel (๐Ÿ—๏ธ Already Present)

internal class VideoPagerViewModel(
    private val repository: VideoDataRepository,
    private val appPlayerFactory: AppPlayer.Factory,
    private val handle: PlayerSavedStateHandle,
    initialState: ViewState,
) : MviViewModel<ViewEvent, ViewResult, ViewState, ViewEffect>(initialState) {

    override fun onStart() {
        processEvent(LoadVideoDataEvent)
    }

    override fun Flow<ViewEvent>.toResults(): Flow<ViewResult> {
        // MVI boilerplate
        return merge(
            filterIsInstance<LoadVideoDataEvent>().toLoadVideoDataResults(),
            filterIsInstance<PlayerLifecycleEvent>().toPlayerLifecycleResults(),
            filterIsInstance<TappedPlayerEvent>().toTappedPlayerResults(),
            filterIsInstance<OnPageSettledEvent>().toPageSettledResults(),
            filterIsInstance<PauseVideoEvent>().toPauseVideoResults()
        )
    }

    private fun Flow<LoadVideoDataEvent>.toLoadVideoDataResults(): Flow<ViewResult> {
        return flatMapLatest { repository.videoData() }
            .map { videoData ->
                val appPlayer = states.value.appPlayer
                // If the player exists, it should be updated with the latest video data that came in
                appPlayer?.setUpWith(videoData, handle.get())
                // Capture any updated index so UI page state can stay in sync. For example, a video
                // may have been added to the page before the currently active one. That means the
                // the current video/page index will have changed
                val index = appPlayer?.currentPlayerState?.currentMediaItemIndex ?: 0
                LoadVideoDataResult(videoData, index)
            }
    }
.
.
.
}

VideoDataSource (๐Ÿ†• Added)

class VideoDataSource(private val videoDao: VideosDao?) : PagingSource<Int, VideoData>() {

    override fun getRefreshKey(state: PagingState<Int, VideoData>): Int? {
        return state.anchorPosition?.let { anchorPosition ->
            val anchorPage = state.closestPageToPosition(anchorPosition)
            anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
        }
    }

    override suspend fun load(params: LoadParams<Int>): LoadResult<Int, VideoData> {
        val page = params.key ?: 0

        return try {
            val videos = videoDao?.getPaginatedShorts(params.loadSize, page * params.loadSize)
                ?: emptyList()
            LoadResult.Page(
                data = videos,
                prevKey = if (page == 0) null else page - 1,
                nextKey = if (videos.isEmpty()) null else page + 1
            )
        } catch (e: Exception) {
            LoadResult.Error(e)
        }
    }
}

PagerPagingAdapter (๐Ÿ†• Added)

internal class PagerPagingAdapter(private val imageLoader: ImageLoader) :
    PagingDataAdapter<VideoData, PageViewHolder>(VideoDataDiffCallback) {

    private var recyclerView: RecyclerView? = null

    // Extra buffer capacity so that emissions can be sent outside a coroutine
    private val clicks = MutableSharedFlow<Unit>(extraBufferCapacity = 1)

    fun clicks() = clicks.asSharedFlow()

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): PageViewHolder {
        return LayoutInflater.from(parent.context)
            .let { inflater -> ShortsPageItemBinding.inflate(inflater, parent, false) }
            .let { binding ->
                PageViewHolder(binding, imageLoader) { clicks.tryEmit(Unit) }
            }
    }

    override fun onBindViewHolder(holder: PageViewHolder, position: Int) {
        getItem(position)?.let(holder::bind)
    }

    override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
        this.recyclerView = recyclerView
    }

    override fun onDetachedFromRecyclerView(recyclerView: RecyclerView) {
        this.recyclerView = null
    }

    /**
     * Attach [appPlayerView] to the ViewHolder at [position]. The player won't actually be visible in
     * the UI until [showPlayerFor] is also called.
     */
    suspend fun attachPlayerView(appPlayerView: AppPlayerView, position: Int) {
        awaitViewHolder(position).attach(appPlayerView)
    }

    // Hides the video preview image when the player is ready to be shown.
    suspend fun showPlayerFor(position: Int) {
        awaitViewHolder(position).hidePreviewImage()
    }

    suspend fun renderEffect(position: Int, effect: PageEffect) {
        awaitViewHolder(position).renderEffect(effect)
    }

    /**
     * The ViewHolder at [position] isn't always immediately available. In those cases, wait for
     * the RecyclerView to be laid out and re-query that ViewHolder.
     */
    private suspend fun awaitViewHolder(position: Int): PageViewHolder {
        if (itemCount == 0) error("Tried to get ViewHolder at position $position, but the list was empty")

        var viewHolder: PageViewHolder?

        do {
            viewHolder = recyclerView?.findViewHolderForAdapterPosition(position) as? PageViewHolder
        } while (currentCoroutineContext().isActive && viewHolder == null && recyclerView?.awaitNextLayout() == Unit)

        return requireNotNull(viewHolder)
    }

    private object VideoDataDiffCallback : DiffUtil.ItemCallback<VideoData>() {
        override fun areItemsTheSame(oldItem: VideoData, newItem: VideoData): Boolean {
            return oldItem.id == newItem.id
        }

        override fun areContentsTheSame(oldItem: VideoData, newItem: VideoData): Boolean {
            return oldItem == newItem
        }
    }
}

video with Images

first of all:Great code! I would like to display images and video in the same viewpager with a VideosViewHolder and an ImagesViewHolder. Please advise on how to go about this.

Open source license

Hello!

We would like to use your videopager library for our app in production. We did not find any license information anywhere. Would it be possible to use it and/or modify it in our codebase? Thanks!

create progress bar for every video

this is a perfect example, could you please give a hint or instruction on the best way if I want to add a linear progress bar within every screen, with the MVI?
thanks :)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.