Implementing video playback in a scrolled list (ListView & RecyclerView)

In this article I will describe how to implement video playback in the list. The same that works in popular applications like Facebook, Instagram or Magisto:

Facebook:

Magisto:

Instagram:

This article is based on the opensource project: VideoPlayerManager.

All the code and a working sample is there. In this article a lot of things will be skipped, so if someone really needs to understand how it works it’s better to download the source code and read the article with the source code in your IDE. But even without the code this article will be good for understanding with what we are dealing here.

Two Problems

To implement what is needed we have to solve two problems:

  1. We have to manage video playback. In Android we have a class MediaPlayer.class that works together with SurfaceView and can playback a video. But it has a lot of drawbacks. We cannot use usual VideoView in the list. VideoView extends SurfaceView, and SurfaceView doesn’t have UI synchronization buffers. All this will lead us to the situation where video that is playing is trying to catch up the list when you scroll it. Synchronization buffers are present in TextureView but there is no VideoView that is based on TextureView in Android SDK version 15. So we need a view that extends TextureView and works with Android MediaPlayer. Also almost all methods (prepare, start, stop etc…) from MediaPlayer are basically calling native methods that work with hardware. Hardware can be tricky and if will do any work longer than 16ms (And it sure will) then we will see a lagging list. That’s why need to call them from background thread.
  2. We also need to know which view on the scrolled list is currently active to switch the playback when user scrolls. So basically we have to track the scroll and define the most visible view if it changes.

Managing Video Playback

Here we aim to provide following functionality:

Assume that movie playback is on. User scrolls the list and new item in the list becomes more visible than the one which video is playing. So now we have to stop the existing video playback and start the new one.

The main functionality is : stop previous playback, and start the new playback only after the old one stops.

Here is a video sample of how it works: When you press on the video thumbnail — current video playback stops and the other one starts.

VideoPlayerView

First thing that we need to implement is the VideoView based on the TextureView. We cannot use VideoView in scrolling list. Because video rendering will be messed up if user will scroll our list during playback.

I’ve divided this task into several parts:

  1. Created a ScalableTextureView. It’s a descendant of TextureView and it knows how to adjust the SurfaceTexture (on this surface texture the playback is running) and provides few options similar to ImageView scaleType.
public enum ScaleType {
CENTER_CROP, TOP, BOTTOM, FILL
}

2. Created VideoPlayerView. This is a descendant of ScalableTextureView and it contains all the functionality related to MediaPlayer.class. This custom view encapsulates MediaPlayer.class and provides an API very similar to VideoView. It has all the methods that are directly calling MediaPlayer : setDataSource, prepare, start, stop, pause, reset, release.

Video Player Manager and Messages Handler Thread

Video Playback Manager works together with the MessagesHandlerThread that is responsible for calling the methods of MediaPlayer . We need to call the methods like prepare(), start() etc. in a separate thread because they are directly connected to devices hardware. And there were cases when we called MediaPlayer.reset() in the UI thread but something went wrong with the player and this method was blocking the UI thread for almost 4 minutes! That’s why we don’t have to use asynchronous MediaPlayer.prepareAsync, we can use synchronous MediaPlayer.prepare . We are doing everything synchronously in a separate thread.

A flow related to starting new playback. Here is a few steps to do with MediaPlayer:

  1. Stop previous playback. It’s done by calling MediaPlayer.stop() method.
  2. Reset MediaPlayer by calling MediaPlayer.reset() method. We need to do it because in scrolling list your view might be reused and we want to have all resources released.
  3. Release MediaPlayer by calling MediaPlayer.release() method.
  4. Clear the instance of MediaPlayer. New MediaPlayer instance will be created when new playback on this view should be started.
  5. Create instance on MediaPlayer for new most visible view.
  6. Set the data source for new MediaPlayer by calling MediaPlayer.setDataSource(String url).
  7. Call MediaPlayer.prepare(). There is no need to use asynchronous MediaPlayer.prepareAsync().
  8. Call MediaPlayer.start()
  9. Wait for actual playback to start.

All these actions are wrapped into Messages that are processed in a separate thread, for example this is Stop message. It calls VideoPlayerView.stop(), which eventually calls the MediaPlayer.stop(). We need custom messages because we can set current state. We know is it Stopping or Stopped or else. It helps us to control which message is now in progress and what can we do about it if we need for example, start new playback.

/**
* This PlayerMessage calls {@link MediaPlayer#stop()} on the instance that is used inside {@link VideoPlayerView}
*/
public class Stop extends PlayerMessage {
public Stop(VideoPlayerView videoView, VideoPlayerManagerCallback callback) {
super(videoView, callback);
}

@Override
protected void performAction(VideoPlayerView currentPlayer) {
currentPlayer.stop();
}

@Override
protected PlayerMessageState stateBefore() {
return PlayerMessageState.STOPPING;
}

@Override
protected PlayerMessageState stateAfter() {
return PlayerMessageState.STOPPED;
}
}

If we need to start new playback we simply call a method on VideoPlayerManager. And it adds following set of messages to the MessagesHandlerThread:

// pause the queue processing and check current state
// if current state is "started" then stop old playback
mPlayerHandler.addMessage(new Stop(mCurrentPlayer, this));
mPlayerHandler.addMessage(new Reset(mCurrentPlayer, this));
mPlayerHandler.addMessage(new Release(mCurrentPlayer, this));
mPlayerHandler.addMessage(new ClearPlayerInstance(mCurrentPlayer, this));
// set new video player view
mPlayerHandler.addMessage(new SetNewViewForPlayback(newVideoPlayerView, this));
// start new playback
mPlayerHandler.addMessages(Arrays.asList(
new CreateNewPlayerInstance(videoPlayerView, this),
new SetAssetsDataSourceMessage(videoPlayerView, assetFileDescriptor, this), // I use local file for demo
new Prepare(videoPlayerView, this),
new Start(videoPlayerView, this)
));
// resume queue processing

The messages are run synchronously that’s why we can pause the queue processing in any time and post new messages, for example:

Current movie is in preparing state (MedaiPlayer.prepare() was called, and MediaPlayer.start() is waiting in the queue) and user scrolled the list so we need to start playback on a new view. In this case we :

  1. Pause queue processing
  2. Remove all pending messages
  3. Post “Stop”, “Reset”, “Release”, “Clear Player instance” to the queue. They will run right after we return from “Prepare”
  4. Post “Create new Media Player instance”, “Set Current Media Player”(this one changes the MediaPlayer object on which our messages are performed), “Set data source”, “Prepare”, “Start”. And this messages will start the playback on the new view.

OK, so we have the utilities to run the playback in the way we need: Stop previous playback and just then Start the next one.

Here is the gradle dependency for the library:

dependencies {
compile 'com.github.danylovolokh:video-player-manager:0.2.0'
}

Identifying the most visible view in the list. List Visibility Utils.

The first problem was to manage the Video Playback. The second problem is to track which view is the most visible and switch the playback into that view.

There is an entity called ListItemsVisibilityCalculator and its implementation SingleListViewItemActiveCalculator that does all the job.

Your model class that is used in adapter must implement ListItem interface in order to calculate visibility of the items in the list:

/**
* A general interface for list items.
* This interface is used by {@link ListItemsVisibilityCalculator}
*
* @author danylo.volokh
*/
public interface ListItem {
/**
* When this method is called, the implementation should provide a
* visibility percents in range 0 - 100 %
* @param view the view which visibility percent should be
* calculated.
* Note: visibility doesn't have to depend on the visibility of a
* full view.
* It might be calculated by calculating the visibility of any
* inner View
*
* @return percents of visibility
*/
int getVisibilityPercents(View view);

/**
* When view visibility become bigger than "current active" view
* visibility then the new view becomes active.
* This method is called
*/
void setActive(View newActiveView, int newActiveViewPosition);

/**
* There might be a case when not only new view becomes active,
* but also when no view is active.
* When view should stop being active this method is called
*/
void deactivate(View currentView, int position);
}

The ListItemsVisibilityCalculator tracks the direction of scroll and calculate visibility of items in runtime. The item visibility might depend on any view inside single item in the list. It’s up to you to implement getVisibilityPercents() method.

There is a default implementation of this method in the sample demo app:

/**
* This method calculates visibility percentage of currentView.
* This method works correctly when currentView is smaller then it's enclosure.
* @param currentView - view which visibility should be calculated
* @return currentView visibility percents
*/
@Override
public int getVisibilityPercents(View currentView) {

int percents = 100;

currentView.getLocalVisibleRect(mCurrentViewRect);

int height = currentView.getHeight();

if(viewIsPartiallyHiddenTop()){
// view is partially hidden behind the top edge
percents = (height - mCurrentViewRect.top) * 100 / height;
    } else if(viewIsPartiallyHiddenBottom(height)){
        percents = mCurrentViewRect.bottom * 100 / height;
}

return percents;
}

So, each view needs to know how to calculate its visibility percents. SingleListViewItemActiveCalculator will be polling this value from each view when scroll is happening so the implementation should not be very heavy.

When visibility of any neighbor item exceeds the visibility of current active item the setActive method will be called. And when it is we should switch the playback.

There is also an ItemsPositionGetter that works as an adapter between ListItemsVisibilityCalculator and ListView or RecyclerView. This way ListItemsVisibilityCalculator doesn’t know if it’s ListView or RecyclerView. It just does its job. But it needs some information that is provided via ItemsPositionGetter:

/**
* This class is an API for {@link ListItemsVisibilityCalculator}
* Using this class is can access all the data from RecyclerView /
* ListView
*
* There is two different implementations for ListView and for
* RecyclerView.
* RecyclerView introduced LayoutManager that's why some of data moved
* there
*
* Created by danylo.volokh on 9/20/2015.
*/
public interface ItemsPositionGetter {

View getChildAt(int position);

int indexOfChild(View view);

int getChildCount();

int getLastVisiblePosition();

int getFirstVisiblePosition();
}

Having that kind of logic in your model is messing up a bit with the idea of separating business logic from model. But with some modifications it might be isolated. And by the way it works fine even how it is now.

Here is the simple movie that shows how it works:

Here is a gradle dependency for the library:

dependencies {
compile 'com.github.danylovolokh:list-visibility-utils:0.2.0'
}

Combination of Video Player Manager and List Visibility Utils to implement video playback in the scrolling list.

Now we have two libraries that solves everything we need. Let’s combine them to get functionality we need.

Here the code from the fragment that uses RecyclerView:

  1. Initialize ListItemsVisibilityCalculator, and pass a reference to a list to it.
/**
* Only the one (most visible) view should be active (and playing).
* To calculate visibility of views we use {@link SingleListViewItemActiveCalculator}
*/
private final ListItemsVisibilityCalculator mVideoVisibilityCalculator = new SingleListViewItemActiveCalculator(
new DefaultSingleItemCalculatorCallback(), mList);

DefaultSingleItemCalculatorCallback just calls the ListItem.setActive method when active view changes, but you can override it by yourself and do whatever you need :

/**
* Methods of this callback will be called when new active item is found {@link Callback#activateNewCurrentItem(ListItem, View, int)}
* or when there is no active item {@link Callback#deactivateCurrentItem(ListItem, View, int)} - this might happen when user scrolls really fast
*/
public interface Callback<T extends ListItem>{
void activateNewCurrentItem(T item, View view, int position);
void deactivateCurrentItem(T item, View view, int position);
}

2. Initialize VideoPlayerManager.

/**
* Here we use {@link SingleVideoPlayerManager}, which means that only one video playback is possible.
*/
private final VideoPlayerManager<MetaData> mVideoPlayerManager = new SingleVideoPlayerManager(new PlayerItemChangeListener() {
@Override
public void onPlayerItemChanged(MetaData metaData) {

}
});

3. Set on scroll listener to the RecyclerView and pass the scroll events to the list visibility utils.

@Override
public void onScrollStateChanged(RecyclerView view, int scrollState) {
mScrollState = scrollState;
if(scrollState == RecyclerView.SCROLL_STATE_IDLE && mList.isEmpty()){

mVideoVisibilityCalculator.onScrollStateIdle(
mItemsPositionGetter,
mLayoutManager.findFirstVisibleItemPosition(),
mLayoutManager.findLastVisibleItemPosition());
}
}

@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
if(!mList.isEmpty()){
mVideoVisibilityCalculator.onScroll(
mItemsPositionGetter,
mLayoutManager.findFirstVisibleItemPosition(),
mLayoutManager.findLastVisibleItemPosition() -
         mLayoutManager.findFirstVisibleItemPosition() + 1,
mScrollState);
}
}
});

4. Create ItemsPositionGetter.

ItemsPositionGetter mItemsPositionGetter = 
new RecyclerViewItemPositionGetter(mLayoutManager, mRecyclerView);

5. And we call a method in onResume to start calculating the most visible item as soon as we open the screen.

@Override
public void onResume() {
super.onResume();
if(!mList.isEmpty()){
// need to call this method from list view handler in order to have filled list

mRecyclerView.post(new Runnable() {
@Override
public void run() {

mVideoVisibilityCalculator.onScrollStateIdle(
mItemsPositionGetter,
mLayoutManager.findFirstVisibleItemPosition(),
mLayoutManager.findLastVisibleItemPosition());

}
});
}
}

And that’s it. We have a set of videos that are playing in the scrolled list:

Basically this is just the explanation of most important parts. There is a lot more code in the sample app here:

https://github.com/danylovolokh/VideoPlayerManager

Please see the source code for more details.

Cheers ;)