Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to use ExoPlayer in a ListvVew or RecyclerView? #867

Open
NatsumeReiko opened this issue Oct 15, 2015 · 75 comments
Open

How to use ExoPlayer in a ListvVew or RecyclerView? #867

NatsumeReiko opened this issue Oct 15, 2015 · 75 comments
Assignees
Labels

Comments

@NatsumeReiko
Copy link

I want to use ExoPlayer in a RecyclerView as a part of row item.
I want to make a customer view and wrap the ExoPlayer in that view.

Do you have some advice?

Thank you!

@RikHeijdens
Copy link

You should keep track of a reference to the ExoPlayer and try to reuse the player for every item in the ListView by just loading new media on the player.

@NatsumeReiko
Copy link
Author

Thank you RikHeijdens.

Do you mean, I should make only one instance of ExoPlayer and switch the video link at properly timing?

What should I do, If I want to play two or three videos at the same time, because the height of the surfaceView is fixed and how many rows will be showed on the screen at the same depends on the height of the devices.

@RikHeijdens
Copy link

If you want to play multiple video's, for instance two at the same time you should instantiate 2 ExoPlayers, with 2 different SurfaceViews. And yes you switch the video as soon as the 'recycled' item moves out of the screen.

@chodison
Copy link

@RikHeijdens Supports two hard decoding at the same time for the device?

@RikHeijdens
Copy link

On my Nexus 5 I can play up to 6 video's at the same time, however you don't want to instantiate so many players because the performance will degrade pretty quickly after two instances.

@chodison
Copy link

BTW: In this case, not all devices are supported.

@NatsumeReiko
Copy link
Author

Thank you for your advice, I decided to try to use only one ExoPlayer. And I will upload some sample code, and hope get more advice.

@NatsumeReiko NatsumeReiko changed the title How to use ExoPlayer in a ListvVew or RecyclerView How to use ExoPlayer in a ListvVew or RecyclerView? Oct 16, 2015
@NatsumeReiko
Copy link
Author

Here is my source code, although it doesn't work very well, but I think it's the right way to do this.
And hoping and appreciating get more advice to complete this.

In this customized RecyclerView(ExoPlayerVideoRecyclerView), I make only one SurfaceView for video play, and add to the row root view and remove it every time I need.

At this time I got these 3 problems.

  1. The ratio of the video is not correct. The height of the video surface view is fiexed to 200dp, and the width is match the device.
  2. I want to show a progress bar before real play, and how can I set the listener to get the play start event.
  3. I keep getting this alarm: E/OMXMaster: A component of name 'OMX.qcom.audio.decoder.aac' already exists, ignoring this one.

About problem 1, I tried this code to reset the ration, but it doesn't seem work.

MediaCodecVideoTrackRenderer.EventListener

    @Override
    public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
        if (videoFrame != null) {
            videoFrame.setAspectRatio(
                    height == 0 ? 1 : (width * pixelWidthHeightRatio) / height);
        }
    }

From here is the code abstracted form next sample:
https://github.com/NatsumeReiko/ExoPlayerInRecyclerView

public class ExoPlayerVideoRecyclerView extends RecyclerView
        implements AudioCapabilitiesReceiver.Listener, MediaCodecVideoTrackRenderer.EventListener,
        SurfaceHolder.Callback {

    public ExoPlayerVideoRecyclerView(Context context) {
        super(context);
        initialize(context);
    }

    private void initialize(Context context) {
        mainHandler = new Handler();

        appContext = context.getApplicationContext();

        allocator = new DefaultAllocator(BUFFER_SEGMENT_SIZE);
        dataSource =
                new DefaultUriDataSource(appContext,
                        new DefaultBandwidthMeter(mainHandler, null),
                        Util.getUserAgent(appContext, "ExoPlayerDemo"));


        videoSurfaceView = new SurfaceView(appContext);

        videoSurfaceView.setLayoutParams(new ViewGroup.LayoutParams(LayoutParams.MATCH_PARENT,
                (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
                        getResources().getDimension(R.dimen.exoplayer_video_height)
                        , getResources().getDisplayMetrics())));

        videoSurfaceView.getHolder().addCallback(this);

        CookieHandler currentHandler = CookieHandler.getDefault();
        if (currentHandler != defaultCookieManager) {
            CookieHandler.setDefault(defaultCookieManager);
        }

        audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(appContext, this);
        audioCapabilitiesReceiver.register();

        player = ExoPlayer.Factory.newInstance(2);
        player.addListener(new ExoPlayer.Listener() {
            @Override
            public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
                switch (playbackState) {
                    case ExoPlayer.STATE_BUFFERING:
                        break;
                    case ExoPlayer.STATE_ENDED:
                        player.seekTo(0);
                        break;
                    case ExoPlayer.STATE_IDLE:
                        break;
                    case ExoPlayer.STATE_PREPARING:
                        break;
                    case ExoPlayer.STATE_READY:
                        break;
                    default:
                        break;
                }
            }

            @Override
            public void onPlayWhenReadyCommitted() {
            }

            @Override
            public void onPlayerError(ExoPlaybackException error) {
            }
        });


        addOnScrollListener(new RecyclerView.OnScrollListener() {
            @Override
            public void onScrollStateChanged(RecyclerView recyclerView, int newState) {
                super.onScrollStateChanged(recyclerView, newState);

                if (newState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {

                    play(getPlayTargetPosition());
                }
            }

            @Override
            public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
                super.onScrolled(recyclerView, dx, dy);
            }
        });
    }

    private void preparePlayer(int position) {

        Uri uri = Uri.parse(videoInfoList.get(position).videoUrl);

        // Build the sample source
        sampleSource =
                new ExtractorSampleSource(uri, dataSource, allocator, 10 * BUFFER_SEGMENT_SIZE);

        // Build the track renderers
        videoRenderer = new MediaCodecVideoTrackRenderer(sampleSource,
                MediaCodec.VIDEO_SCALING_MODE_SCALE_TO_FIT, -1, mainHandler, this, -1);
        audioRenderer = new MediaCodecAudioTrackRenderer(sampleSource);

        // Build the ExoPlayer and start playback
        player.prepare(videoRenderer, audioRenderer);

        playVideo();
    }

    //method to really do the play
    private void playVideo() {
        if (surfaceViewViable) {
            player.sendMessage(videoRenderer,
                    MediaCodecVideoTrackRenderer.MSG_SET_SURFACE,
                    videoSurfaceView.getHolder().getSurface());
            player.setPlayWhenReady(true);
        }
    }

        private void releasePlayer() {
        if (player != null) {
            player.release();
            player = null;
        }
    }

    private void removeVideoView(SurfaceView videoView) {

        ViewGroup parent = (ViewGroup) videoView.getParent();

        if (parent == null) {
            return;
        }

        int index = parent.indexOfChild(videoView);
        if (index >= 0) {
            parent.removeViewAt(index);
        }

    }

    private void play(int position) {
        if (position == playPosition) {
            return;
        }

        playPosition = position;
        removeVideoView(videoSurfaceView);

        // get target View position in RecyclerView
        int at = position - ((LinearLayoutManager) getLayoutManager()).findFirstVisibleItemPosition();

        View child = getChildAt(at);
        if (child == null) {
            return;
        }

        ExoPlayerVideoRecyclerViewAdapter.VideoViewHolder holder
                = (ExoPlayerVideoRecyclerViewAdapter.VideoViewHolder) child.getTag();
        if (holder == null) {
            playPosition = DEFAULT_PLAY_POSITION;
            return;
        }
        holder.videoContainer.addView(videoSurfaceView);
        videoFrame = holder.videoContainer;

        preparePlayer(playPosition);
    }
}


public class ExoPlayerVideoRecyclerViewAdapter
        extends RecyclerView.Adapter<RecyclerView.ViewHolder> {

        public ExoPlayerVideoRecyclerViewAdapter(Context appContext, List<VideoInfo> videoInfoList) {
        this.videoInfoList = videoInfoList;
        inflater = LayoutInflater.from(appContext.getApplicationContext());

    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        return new VideoViewHolder(inflater
                .inflate(R.layout.exoplayer_recycler_view_row, parent, false));

    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof VideoViewHolder) {
            setVideoViewHolder((VideoViewHolder) holder);
        }
    }

    private void setVideoViewHolder(VideoViewHolder holder) {
        holder.parent.setTag(holder);
    }

    @Override
    public int getItemCount() {
        return videoInfoList.size();
    }

    public static class VideoViewHolder extends RecyclerView.ViewHolder {

        AspectRatioFrameLayout videoContainer;
        View parent;

        public VideoViewHolder(View v) {
            super(v);
            parent = v;
            videoContainer = (AspectRatioFrameLayout) v.findViewById(R.id.video_frame);
        }
    }

    public void onRelease() {
        if (videoInfoList != null) {
            videoInfoList.clear();
            videoInfoList = null;
        }
    }

}

@qqli007
Copy link

qqli007 commented Nov 19, 2015

@NatsumeReiko ,how to goto fullscreen when click the item of the listview? Do you have any idea?

@jayshah123
Copy link

I have a use case (for multiple exoplayers) where there can be TextureViews in each page of viewpager, minimum 3 exoplayers would be allocated, one per textureview(in each page). Although I play only a single exoplayer at a time(the one belonging to textureview of focused page) and rest are in paused state,
how does bandwidth/performance etc. get affected ?
(Assume I am using simple MP4 over http - no adaptive streaming)
What would be best practice in such cases?

@xingstarx
Copy link

@jayshah123 I think you could see this repo https://github.com/xingstarx/InkeVerticalViewPagerLive

I hope that can help you

@mufumbo
Copy link

mufumbo commented Dec 16, 2016

@NatsumeReiko that's an interesting solution, but if you move the SurfaceView to another container, you would lose the paused state thumbnail if you need to resume the video while the user scroll. Do you think reusing surfaceView is much more performant than just reusing the player?

@escamoteur
Copy link

I'm facing almost the same challenge.

So I would like to know if it is feasable to use multiple SurfaceViews in a ListView and just switch the Player between them or is it better to reuse the same surface when I need it?

Thanks
Thomas

@artworkad
Copy link

Check out https://github.com/eneim/Toro

@Sandeeppal1083
Copy link

Sandeeppal1083 commented Jun 1, 2017

NatsumReiko or anyone , please help me in playing Hls Video inside recyclerview , i want to play Hls videos inside recyclerview, Please help me with code, i would be thankful for you sooo much.

@google google deleted a comment from michalliu Jun 19, 2017
@ojw28 ojw28 added the question label Jun 24, 2017
@gregkorossy
Copy link

I am facing the same problem with RecyclerView using ExoPlayer 2. I have multiple items representing either an audio or a video player and there might be an unknown number of items (probably more than 1) visible at the same time.
I implemented the player reuse by delegating it to a service so it can keep playing in the background too, which is good. My main problem is that the SimpleExoPlayerView has a lot of restrictions based on whether the player is set, such as it won't display controls nor album artwork if the player is null, and the class is final so I cannot just override what I want... I have the video thumbnails separately available, so it shouldn't be a problem, but now I have to use a separate image view item to show them on top of the player view. Also, I have to display a separate play button on top of these in order to set the player to the current player view and start playing the media.

Another problem I noticed with the player instance reuse is that if 2 items are visible at the same time, item A is playing, and I start playing item B, then item A will be "reset" to a default state with the total time set to 00:00 and the current "thumbnail" image (video) removed along with the controllers, which seems like a really bad UX.

My question is: what are the performance / networking / bandwidth effects of using more than 1 players, but playing only one at the same time (it would use some kind of an "obtain" mechanism by creating new player if there isn't an available one)? So if the user starts playing item B by pressing the play button, item A would pause. I'm afraid that pausing the players would not be enough because they would still keep buffering / holding data, but stopping them resets the current state, which means back to square one.

@eneim
Copy link
Contributor

eneim commented Aug 7, 2017

Thank @artworkad for mentioning my library :D. FYI @Gericop I have been struggling a long time with the same idea with you. I finally end up with the belief that ExoPlayer instance will not consume your CPU and much of your network as long as you don't ask it to (= calling player.setPlayWhenReady(true)). It may start fetching some meta data at preparing, but it should be fine with just that.

Having a Singleton Player will be scary (good for performance though). You can learn from how Youtube Player API doing so (closed source, yep, but enough study may turn to something I guess).

@mpainenz
Copy link

Your Toro library @eneim suffers from thread locking and slow performance when scrolling the recycler. Particularly when you fling the RecyclerView.

In my own testing, I have found that using a single player instance is much better. It removed all my issues with thread locking. I guess the players are contending for Codec access, or some other issue is occuring. This happens even if only one player is playing at a time.

The best approach I've found is to:

  1. Generate your own thumbnails. In my case, I am downloading the MP4's I want to play to disk, and using the ThumbnailUtils.createVideoThumbnail function to store the Thumbnail to disk, and display an ImageView in the RecyclerView.

  2. In each ViewHolder, store a separate MediaSource object, and create it during Bind, and release it during the Recycle event.

  3. In each ViewHolder layout, use a separate SimpleExoPlayerView object with no SimpleExoPlayer attached during Bind Time.

  4. Hold a single instance of a SimpleExoPlayer in the Adapter, or somewhere else in your project.

  5. Listen to the RecyclerView's LayoutManager OnScroll event, and in each scroll event, work out which item is in focus and needs to be in a play state. Prepare the player, attach the SimpleExoPlayer to the ViewHolders SimpleExoPlayerView and hide the Thumbnail overlay.

This method is working very well for me, I get a very fast experience.

@eneim
Copy link
Contributor

eneim commented Nov 13, 2017

You are right about the issue of having multi ExoPlayer instance in the library. Lately I also investigate in the case of using Single/Limited ExoPlayer instances. Your approach gonna be so much helpful. (It turns out that, to make it highly abstraction and easy to integrate, many works need to be done).

@mpainenz
Copy link

I have also found that you can further increase performance by using a TextureView instead of SimpleExoPlayerView, and only create one TextureView object instead of one per viewholder.

In your Adapter or Activity, store a single TextureView object, and pass it to the ViewHolder that is playing at runtime. Reuse the same TextureView item for each viewholder that is playing.

I cannot believe how much smoother my application runs this way.

@mpainenz
Copy link

Just an update to TextureView re-use in RecyclerView, it seems to be quite buggy when removing a TextureView from it's parent and moving it to a new View.

If you want to get that working, the better approach is to hold the TextureView inside the Activity view, and overlay the recyclerview on top. It's a difficult approach, but if you move the TextureView between parents, it seems to end up displaying a black screen on resize or program resume.

It's actually still quite fast to have a TextureView for each Viewholder, so that seems to be a simpler option.

@dishantkawatra
Copy link

Hi, when i switch the exoplayer from recycler view to a dialog with the help of getplayer() and setplayer() method then frames are hang for some sec and sometimes audio is audible but frames are not see please tell me how to resolve this issue with toro

eneim/toro#286

@sandeepyohans
Copy link

It's this very old issue which is still open. Is there a proper solution for using ExoPlayer is ListView or RecyclerView? A working sample would be great help for a beginner like me. @ojw28 @andrewlewis

@nirazv
Copy link

nirazv commented Sep 24, 2020

Can someone please help me to use ExoPlayer with ViewPager2 using RecyclerViewAdAPTER?

@paulocoutinhox
Copy link

Hi guys,

I really want only thing pls:

solve the problem that happen when i animated anything over the player view (surface view). It is flicking/lagging when i call any animation or change some object visibility. I don't know if layout things can be impacting it.

I solve all other problems, only left this one to be perfect.

And i understand that no one here have any obligation.

Project page:
https://github.com/paulo-coutinho/rvplayer

@nihk
Copy link

nihk commented Jun 21, 2021

Here's a simple app showing how I've been doing it with ViewPager2:
https://github.com/nihk/exo-viewpager-fun

demo

@Allan-Nava
Copy link

Here's a simple app showing how I've been doing it with ViewPager2:
https://github.com/nihk/exo-viewpager-fun

demo

The performance are good?

@nirazv
Copy link

nirazv commented Jul 4, 2021 via email

@user849651

This comment has been minimized.

@andrewlewis

This comment has been minimized.

@mecoFarid
Copy link

here is new concept to implement that so check this out Integrate RecyclerView with ExoPlayer — The clean way — and customization, article that demonstrates my solution is Meduim article My Youtube Demo and you can find demo repo here

Why do you even call this "clean". I mean you are creating ExoPlayer instance multiple times. And what does "clean" really mean here?

@mecoFarid
Copy link

Here's a simple app showing how I've been doing it with ViewPager2: https://github.com/nihk/exo-viewpager-fun

demo

Just to point out the pitfall here. https://github.com/nihk/videopager/blob/caf0f3c049d713cff8bba3e11798b3f99d62e606/exo/src/main/kotlin/com/exo/players/ExoAppPlayer.kt#L101. Why do you treat prepare() as a synchronous method. It is async method

@nihk
Copy link

nihk commented Feb 19, 2022

Here's a simple app showing how I've been doing it with ViewPager2: https://github.com/nihk/exo-viewpager-fun
demo

Just to point out the pitfall here. https://github.com/nihk/videopager/blob/caf0f3c049d713cff8bba3e11798b3f99d62e606/exo/src/main/kotlin/com/exo/players/ExoAppPlayer.kt#L101. Why do you treat prepare() as a synchronous method. It is async method

I'm not sure what you're asking. There's no treatment nor dependency on Player.prepare() being executed synchronously in that file.

@varun7952
Copy link

I have created a singleton exoplayer class where i am attaching and detaching playerview based on recyclerview positions. I will share my code soon.

@nirazv
Copy link

nirazv commented Aug 5, 2024

I have created a singleton exoplayer class where i am attaching and detaching playerview based on recyclerview positions. I will share my code soon.

now no one using recyclerview try jetpack compose.

@paulocoutinhox
Copy link

I have created a singleton exoplayer class where i am attaching and detaching playerview based on recyclerview positions. I will share my code soon.

Please, share.

@varun7952
Copy link

I have created a singleton exoplayer class where i am attaching and detaching playerview based on recyclerview positions. I will share my code soon.

now no one using recyclerview try jetpack compose.

99% of code of new exoplayer (media3) written in java even we are using Core java in our production app without using compose in most of the cases

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests