Search code examples
androidandroid-mediaplayerandroid-videoviewandroid-tv

Android VideoView buffering


I'm working on the Android TV app which uses VideoView to play the video from the url. Each time the video is paused and then resumed - it takes some time to pre-download it first (few seconds during which progress bar appears). The goal is to buffer the video during playback so that when I pause it and then resume - it resumes from pre-buffered state immediately without delay. I've also tried videoView.resume() method, but it doesn't help either. Is there a way to do that or am I doing something wrong?

Here is the code which plays/resumes the video:

public void onFragmentPlayPause(final Video video,
                                    final VideoQuality quality,
                                    final int position,
                                    final Boolean play) {
        stopSeeking();

        videoView.setVideoPath(video.videoUrl(quality));

        if (position == 0 || playbackState == LeanbackPlaybackState.IDLE) {
            setupCallbacks();
            playbackState = LeanbackPlaybackState.IDLE;
        }

        if (play && playbackState != LeanbackPlaybackState.PLAYING) {
            progressBar.setVisibility(View.VISIBLE);
            playbackState = LeanbackPlaybackState.PLAYING;
            if (position > 0) {
                videoView.seekTo(position);
                videoView.start();
            }
            playbackFragment.startProgressObservation(progressFlowable());
        } else {
            playbackState = LeanbackPlaybackState.PAUSED;
            videoView.pause();
            playbackFragment.stopProgressObservation();
        }

        updatePlaybackState(position);
        updateMetadata(video);
    }

Solution

  • Unfortunately, I've didn't find the way to achieve this with VideoView, only using ExoPlayer, thanks to @pskink for the right direction. The API was returning mp4 videos, so I've used ExtractorMediaSource. The complete demo example with support of multiple formats can be found on ExoPlayer GitHub page.

    Here is the final code I've ended up with using ExoPlayer:

    private void playPause(final Video video, final int position, final Boolean play) {
            if (position == 0 || playbackState == LeanbackPlaybackState.IDLE) {
                setupCallbacks();
                playbackState = LeanbackPlaybackState.IDLE;
            }
    
            if (play && playbackState != LeanbackPlaybackState.PLAYING) {
                progressBar.setVisibility(View.VISIBLE);
                playbackState = LeanbackPlaybackState.PLAYING;
                player.start();
                playbackFragment.startProgressObservation(progressFlowable());
            } else {
                playbackState = LeanbackPlaybackState.PAUSED;
                player.pause();
                playbackFragment.stopProgressObservation();
            }
    
            updatePlaybackState(position);
            updateMetadata(video);
        }
    

    And Player implementation:

    public class Player implements MediaController.MediaPlayerControl, ExoPlayer.EventListener {
    
        private static final DefaultBandwidthMeter BANDWIDTH_METER = new DefaultBandwidthMeter();
    
        private final SimpleExoPlayer exoPlayer;
        private final Context context;
    
        private OnErrorListener onErrorListener;
        private OnPreparedListener onPreparedListener;
        private OnCompletionListener onCompletionListener;
    
        private String url;
    
        public interface OnErrorListener {
            void onError(final Exception e);
        }
    
        public interface OnPreparedListener {
            void onPrepared();
        }
    
        public interface OnCompletionListener {
            void onComplete();
        }
    
        public Player(final Context context) {
            this.context = context;
    
            final @SimpleExoPlayer.ExtensionRendererMode int extensionRendererMode =
                    SimpleExoPlayer.EXTENSION_RENDERER_MODE_OFF;
            final TrackSelection.Factory videoTrackSelectionFactory =
                    new AdaptiveTrackSelection.Factory(BANDWIDTH_METER);
            this.exoPlayer = ExoPlayerFactory.newSimpleInstance(
                    context,
                    new DefaultTrackSelector(videoTrackSelectionFactory),
                    new DefaultLoadControl(),
                    null,
                    extensionRendererMode
            );
            this.exoPlayer.addListener(this);
        }
    
        @Override
        public boolean canPause() {
            return true;
        }
    
        @Override
        public boolean canSeekBackward() {
            return true;
        }
    
        @Override
        public boolean canSeekForward() {
            return true;
        }
    
        @Override
        public int getAudioSessionId() {
            throw new UnsupportedOperationException();
        }
    
        @Override
        public int getBufferPercentage() {
            return exoPlayer.getBufferedPercentage();
        }
    
        @Override
        public int getCurrentPosition() {
            return exoPlayer.getDuration() == com.google.android.exoplayer2.C.TIME_UNSET ? 0
                    : (int) exoPlayer.getCurrentPosition();
        }
    
        @Override
        public int getDuration() {
            return exoPlayer.getDuration() == com.google.android.exoplayer2.C.TIME_UNSET ? 0
                    : (int) exoPlayer.getDuration();
        }
    
        @Override
        public boolean isPlaying() {
            return exoPlayer.getPlayWhenReady();
        }
    
        @Override
        public void start() {
            exoPlayer.setPlayWhenReady(true);
        }
    
        @Override
        public void pause() {
            exoPlayer.setPlayWhenReady(false);
        }
    
        public void stop() {
            exoPlayer.seekTo(0);
            pause();
        }
    
        public void setOnErrorListener(final OnErrorListener onErrorListener) {
            this.onErrorListener = onErrorListener;
        }
    
        public void setOnPreparedListener(final OnPreparedListener onPreparedListener) {
            this.onPreparedListener = onPreparedListener;
        }
    
        public void setOnCompletionListener(final OnCompletionListener onCompletionListener) {
            this.onCompletionListener = onCompletionListener;
        }
    
        public void setVolume(final float volume) {
            exoPlayer.setVolume(volume);
        }
    
        public void release() {
            exoPlayer.release();
        }
    
        public void updateUrl(final String url) {
            this.url = url;
            exoPlayer.prepare(buildMediaSource(Uri.parse(url)));
        }
    
        public SimpleExoPlayer exoPlayer() {
            return exoPlayer;
        }
    
        @Override
        public void seekTo(final int timeMillis) {
            exoPlayer.seekTo(timeMillis);
        }
    
        @Override
        public void onTimelineChanged(final Timeline timeline, final Object manifest) {
        }
    
        @Override
        public void onTracksChanged(final TrackGroupArray trackGroups, final TrackSelectionArray trackSelections) {
        }
    
        @Override
        public void onLoadingChanged(final boolean isLoading) {
        }
    
        @Override
        public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
            if (playbackState == ExoPlayer.STATE_READY) {
                onPreparedListener.onPrepared();
            }
            if (playbackState == ExoPlayer.STATE_ENDED) {
                onCompletionListener.onComplete();
            }
        }
    
        @Override
        public void onPlayerError(final ExoPlaybackException error) {
            onErrorListener.onError(error);
        }
    
        @Override
        public void onPositionDiscontinuity() {
        }
    
        public String url() {
            return url;
        }
    
        private MediaSource buildMediaSource(final Uri uri) {
            return new ExtractorMediaSource(uri, buildDataSourceFactory(true), new DefaultExtractorsFactory(),
                    null, null);
        }
    
        private DataSource.Factory buildDataSourceFactory(final DefaultBandwidthMeter bandwidthMeter) {
            return new DefaultDataSourceFactory(context, bandwidthMeter,
                    buildHttpDataSourceFactory(bandwidthMeter));
        }
    
        private HttpDataSource.Factory buildHttpDataSourceFactory(final DefaultBandwidthMeter bandwidthMeter) {
            return new DefaultHttpDataSourceFactory(Util.getUserAgent(context, Application.TAG), bandwidthMeter);
        }
    
        private DataSource.Factory buildDataSourceFactory(final boolean useBandwidthMeter) {
            return buildDataSourceFactory(useBandwidthMeter ? BANDWIDTH_METER : null);
        }
    }