Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Streaming video list using Exoplayer

I'd like to implement streaming video list app. I used a RecyclerView to display my list item. Item type includes 4 type: Article, Status, Photo and Video. We should only focus on Video type. Here is my code for RecyclerView's adapter:

public class FollowedPostAdapter extends RecyclerView.Adapter implements OnFollowTagCallback, OnLikeCallback {
    private Context context;
    private List<PostItem> newsFeedList;
    public RecyclerView recyclerView;
    public LinearLayoutManager linearLayoutManager;
    private HashMap<Integer, DemoPlayer> playerList;

    private int visibleThreshold = 5;
    // private int previousTotal = 0;
    private int visibleItemCount, firstVisibleItem, totalItemCount;
    private boolean loading;
    private OnRecyclerViewLoadMoreListener loadMoreListener;


    private final String text_comment;
    private final String mReadMoreHtml;
    private long userId;


    public FollowedPostAdapter(Context context, RecyclerView recyclerView, List<PostItem> newsFeedList) {
        this.context = context;
        playerList = new HashMap<>();
        this.newsFeedList = newsFeedList;
        this.recyclerView = recyclerView;
        this.linearLayoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();

    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        RecyclerView.ViewHolder viewHolder;
        if (viewType == Constants.VIEWTYPE_ARTICLE) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.post_followed_article_item, parent, false);
            viewHolder = new ArticleViewHolder(view);
        } else if (viewType == Constants.VIEWTYPE_STATUS) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.post_followed_status_item, parent, false);
            viewHolder = new StatusViewHolder(view);
        } else if (viewType == Constants.VIEWTYPE_PHOTO) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.post_followed_photo_item, parent, false);
            viewHolder = new PhotoViewHolder(view);
        } else if (viewType == Constants.VIEWTYPE_VIDEO) {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.post_followed_video_item, parent, false);
            viewHolder = new VideoViewHolder(view);
        } else {
            View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.progressbar_item, parent, false);
            viewHolder = new ProgressBarViewHolder(view);
        }
        return viewHolder;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
        if (holder instanceof ArticleViewHolder) {
           // code

        } else if (holder instanceof StatusViewHolder) {
        // code

        } else if (holder instanceof PhotoViewHolder) {
         // code
        } else if (holder instanceof VideoViewHolder) {
            PostItem item = newsFeedList.get(position);
            VideoViewHolder mHolder = (VideoViewHolder) holder;
            LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) mHolder.videoView.getLayoutParams();
            lp.height = (int) (ScreenHelper.getScreenWidth((Activity) context) * Constants.POST_IMAGE_RATIO);
            mHolder.videoView.setLayoutParams(lp);
            mHolder.setup(item);
            Picasso.with(context).load(item.imageCover).error(R.drawable.ic_user_avatar).placeholder(R.drawable.ic_user_avatar).into(mHolder.iv_avatar);
            if (item.tags != null && item.tags.size() > 0) {
                // get first tag as main tag
                generateTagViews(mHolder.tag_flow_layout, item.tags.subList(0, 1), position);
                mHolder.tag_flow_layout.setVisibility(View.VISIBLE);
                mHolder.tag_flow_layout.setVisibility(View.VISIBLE);
                //   mHolder.indicator.setVisibility(View.VISIBLE);
            } else {
                mHolder.tag_flow_layout.setVisibility(View.GONE);
                // mHolder.indicator.setVisibility(View.GONE);
            }
            if (item.time_created != null) {
                mHolder.tv_time.setText(item.time_created);
                //mHolder.indicator.setVisibility(View.VISIBLE);
                mHolder.tv_time.setVisibility(View.VISIBLE);
            } else {
                //mHolder.indicator.setVisibility(View.GONE);
                mHolder.tv_time.setVisibility(View.GONE);
            }
            if (item.description_short.isEmpty())
                mHolder.tv_description.setVisibility(View.GONE);
            else {
                mHolder.tv_description.setText(item.description_short);
                mHolder.tv_description.setVisibility(View.VISIBLE);
            }

            mHolder.btn_comment.setText(String.valueOf(item.count_comment));
            mHolder.btn_like.setText(String.valueOf(item.count_like));
            mHolder.btn_unlike.setText(String.valueOf(item.count_unlike));
            mHolder.btn_share.setText(String.valueOf(item.count_share));
            if (item.tags.size() != 0) {
                int tagId = item.tags.get(0).tag_id;
                setFollowButtonActive(mHolder.btn_follow, TagHelper.isTagFollowed(tagId));
            } else
                setFollowButtonActive(mHolder.btn_follow, false);

        }

    }

    @Override
    public void onViewRecycled(RecyclerView.ViewHolder holder) {
        super.onViewRecycled(holder);
        if (holder instanceof VideoViewHolder) {
            DemoPlayer player = playerList.get(holder.getAdapterPosition());
            if (player != null) {
                player.release();
                playerList.remove(holder.getAdapterPosition());
            }
        }
    }


    public void pauseAllPlayers() {
        for (int i = 0; i <= newsFeedList.size(); i++) {
            DemoPlayer player = playerList.get(i);
            if (player != null) {
                if (player.getPlayerControl().isPlaying())
                    player.getPlayerControl().pause();
                RecyclerView.ViewHolder holder = recyclerView.findViewHolderForLayoutPosition(i);
                if (holder != null && holder instanceof VideoViewHolder) {
                    ((VideoViewHolder) holder).btn_play.setVisibility(View.VISIBLE);
                }

            }
        }
    }





    public void refreshData() {
        notifyDataSetChanged();
    }

    public class ArticleViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
        //
    }

    public class StatusViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {

        // code
    }

    public class PhotoViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
      // code
    }

    public class VideoViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener, SurfaceHolder.Callback, AudioCapabilitiesReceiver.Listener, DemoPlayer.Listener, DemoPlayer.CaptionListener, DemoPlayer.Id3MetadataListener {
        @Bind(R.id.iv_avatar)
        ImageView iv_avatar;
        @Bind(R.id.tag_flow_layout)
        FlowLayout tag_flow_layout;
        @Bind(R.id.tv_time)
        TextView tv_time;
        @Bind(R.id.btn_follow)
        FancyButton btn_follow;
        @Bind(R.id.btn_comment)
        FancyButton btn_comment;
        @Bind(R.id.btn_like)
        FancyButton btn_like;
        @Bind(R.id.btn_unlike)
        FancyButton btn_unlike;
        @Bind(R.id.btn_share)
        FancyButton btn_share;
        @Bind(R.id.root)
        FrameLayout videoView;
        @Bind(R.id.btn_play)
        ImageView btn_play;
        @Bind(R.id.tv_description)
        TextView tv_description;

        // player's variable
        private EventLogger eventLogger;

        //  private VideoControllerView mediaController;
        private View shutterView;
        private AspectRatioFrameLayout videoFrame;
        private SurfaceView surfaceView;
        private SubtitleLayout subtitleLayout;

        private DemoPlayer player;
        private boolean playerNeedsPrepare;

        private long playerPosition = 0;

        private Uri contentUri;
        private int contentType;
        private String contentId;

        public static final int TYPE_DASH = 0;
        public static final int TYPE_SS = 1;
        public static final int TYPE_HLS = 2;
        public static final int TYPE_OTHER = 3;

        private static final String EXT_DASH = ".mpd";
        private static final String EXT_SS = ".ism";
        private static final String EXT_HLS = ".m3u8";

        private AudioCapabilitiesReceiver audioCapabilitiesReceiver;

        public VideoViewHolder(View view) {
            super(view);
            ButterKnife.bind(this, view);
            iv_avatar.setOnClickListener(this);
            btn_follow.setOnClickListener(this);
            btn_comment.setOnClickListener(this);
            btn_like.setOnClickListener(this);
            btn_unlike.setOnClickListener(this);
            btn_share.setOnClickListener(this);

            // player's setup
            View root = view.findViewById(R.id.root);
            root.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    doPlayResume();
                }
            });
//            root.setOnTouchListener(new View.OnTouchListener() {
//                @Override
//                public boolean onTouch(View view, MotionEvent motionEvent) {
//                    if (motionEvent.getAction() == MotionEvent.ACTION_DOWN) {
//                        toggleControlsVisibility();
//                    } else if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
//                        view.performClick();
//                    }
//                    return true;
//                }
//            });
//            root.setOnKeyListener(new View.OnKeyListener() {
//                @Override
//                public boolean onKey(View v, int keyCode, KeyEvent event) {
//                    if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE
//                            || keyCode == KeyEvent.KEYCODE_MENU) {
//                        return false;
//                    }
//                    return mediaController.dispatchKeyEvent(event);
//                }
//            });
            shutterView = view.findViewById(R.id.shutter);
            videoFrame = (AspectRatioFrameLayout) view.findViewById(R.id.video_frame);
            surfaceView = (SurfaceView) view.findViewById(R.id.surface_view);
            surfaceView.getHolder().addCallback(this);
            subtitleLayout = (SubtitleLayout) view.findViewById(R.id.subtitles);
//            mediaController = new VideoControllerView(context);
//            mediaController.setAnchorView((ViewGroup) root);
            audioCapabilitiesReceiver = new AudioCapabilitiesReceiver(context, this);
            audioCapabilitiesReceiver.register();
        }

        @Override
        public void onClick(View v) {
            switch (v.getId()) {
                case R.id.iv_avatar:
                    viewTagDetail(getAdapterPosition());
                    break;
                case R.id.btn_follow:
                    followTag((FancyButton) v, getAdapterPosition());
                    break;
                case R.id.btn_comment:
                    commentPost(getAdapterPosition());
                    break;
                case R.id.btn_like:
                    likePost(getAdapterPosition(), btn_like, btn_unlike);
                    break;
                case R.id.btn_unlike:
                    unlikePost(getAdapterPosition(), btn_like, btn_unlike);
                    break;
                case R.id.btn_share:
                    sharePost(getAdapterPosition());
                    break;
                case R.id.btn_play:
                    doPlayResume();
                    break;
            }
        }

        public void setup(PostItem item) {
            releasePlayer();
            player = playerList.get(getAdapterPosition());
            contentUri = Uri.parse(item.link);
            contentType = TYPE_OTHER;
            contentId = String.valueOf(item.post_id);
            configureSubtitleView();
            if (player == null) {
                preparePlayer(false);
            } else {
                player.setBackgrounded(false);
            }
        }

        //        public void saveCurrentPosition() {
//            if (player != null)
//                videoItemList.get(getAdapterPosition()).position = player.getCurrentPosition();
//        }
        public void doPlayResume() {
            if (player == null) {
                return;
            }

            if (player.getPlayerControl().isPlaying()) {
                player.getPlayerControl().pause();
            } else {
                player.getPlayerControl().start();
            }
            showControls();
        }

        @Override
        public void onAudioCapabilitiesChanged(AudioCapabilities audioCapabilities) {
            if (player == null) {
                return;
            }
            boolean backgrounded = player.getBackgrounded();
            boolean playWhenReady = player.getPlayWhenReady();
            releasePlayer();
            preparePlayer(playWhenReady);
            player.setBackgrounded(backgrounded);
        }

        private DemoPlayer.RendererBuilder getRendererBuilder() {
            String userAgent = Util.getUserAgent(context, "ExoPlayerDemo");
            switch (contentType) {
                case TYPE_SS:
                    return new SmoothStreamingRendererBuilder(context, userAgent, contentUri.toString(),
                            new SmoothStreamingTestMediaDrmCallback());
                case TYPE_DASH:
                    return new DashRendererBuilder(context, userAgent, contentUri.toString(),
                            new WidevineTestMediaDrmCallback(contentId));
                case TYPE_HLS:
                    return new HlsRendererBuilder(context, userAgent, contentUri.toString());
                case TYPE_OTHER:
                    return new ExtractorRendererBuilder(context, userAgent, contentUri);
                default:
                    throw new IllegalStateException("Unsupported type: " + contentType);
            }
        }

        private void preparePlayer(boolean playWhenReady) {
            if (player == null) {
                player = new DemoPlayer(getRendererBuilder());
                playerList.put(getAdapterPosition(), player);
                player.addListener(this);
                player.setCaptionListener(this);
                player.setMetadataListener(this);
                player.seekTo(playerPosition);
                playerNeedsPrepare = true;
//                mediaController.setMediaPlayer(player.getPlayerControl());
//                mediaController.setEnabled(true);
                eventLogger = new EventLogger();
                eventLogger.startSession();
                player.addListener(eventLogger);
                player.setInfoListener(eventLogger);
                player.setInternalErrorListener(eventLogger);
            }
            if (playerNeedsPrepare) {
                player.prepare();
                playerNeedsPrepare = false;
            }
            player.setSurface(surfaceView.getHolder().getSurface());
            player.setPlayWhenReady(playWhenReady);
        }

        private void releasePlayer() {
            if (player != null) {
                player.release();
                player = null;
                eventLogger.endSession();
                eventLogger = null;
                btn_play.setVisibility(View.VISIBLE);
            }
        }

        @Override
        public void onStateChanged(boolean playWhenReady, int playbackState) {
            if (playbackState == ExoPlayer.STATE_ENDED) {
                showControls();
            }

        }

        @Override
        public void onError(Exception e) {
            if (e instanceof UnsupportedDrmException) {
                // Special case DRM failures.
                UnsupportedDrmException unsupportedDrmException = (UnsupportedDrmException) e;
                int stringId = Util.SDK_INT < 18 ? R.string.drm_error_not_supported
                        : unsupportedDrmException.reason == UnsupportedDrmException.REASON_UNSUPPORTED_SCHEME
                        ? R.string.drm_error_unsupported_scheme : R.string.drm_error_unknown;
                Toast.makeText(context, stringId, Toast.LENGTH_LONG).show();
            }
            playerNeedsPrepare = true;
            showControls();
        }

        @Override
        public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
            shutterView.setVisibility(View.GONE);
            videoFrame.setAspectRatio(
                    height == 0 ? 1 : (width * pixelWidthHeightRatio) / height);
        }

        private boolean haveTracks(int type) {
            return player != null && player.getTrackCount(type) > 0;
        }

//        private void toggleControlsVisibility() {
//            if (mediaController.isShowing()) {
//                mediaController.hide();
//            } else {
//                showControls();
//            }
//        }

        private void showControls() {
//            mediaController.show(5000);
            if (player.getPlayerControl().isPlaying())
                btn_play.setVisibility(View.GONE);
            else
                btn_play.setVisibility(View.VISIBLE);

        }

        @Override
        public void onCues(List<Cue> cues) {
            subtitleLayout.setCues(cues);
        }

        @Override
        public void onId3Metadata(Map<String, Object> metadata) {

        }

        @Override
        public void surfaceCreated(SurfaceHolder holder) {
            if (player != null) {
                player.setSurface(holder.getSurface());
            }
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder holder) {
            if (player != null) {
                holder.lockCanvas();
                player.blockingClearSurface();
            }
        }

        private void configureSubtitleView() {
            CaptionStyleCompat style;
            float fontScale;
            if (Util.SDK_INT >= 19) {
                style = getUserCaptionStyleV19();
                fontScale = getUserCaptionFontScaleV19();
            } else {
                style = CaptionStyleCompat.DEFAULT;
                fontScale = 1.0f;
            }
            subtitleLayout.setStyle(style);
            subtitleLayout.setFractionalTextSize(SubtitleLayout.DEFAULT_TEXT_SIZE_FRACTION * fontScale);
        }

        @TargetApi(19)
        private float getUserCaptionFontScaleV19() {
            CaptioningManager captioningManager =
                    (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
            return captioningManager.getFontScale();
        }

        @TargetApi(19)
        private CaptionStyleCompat getUserCaptionStyleV19() {
            CaptioningManager captioningManager =
                    (CaptioningManager) context.getSystemService(Context.CAPTIONING_SERVICE);
            return CaptionStyleCompat.createFromCaptionStyle(captioningManager.getUserStyle());
        }

        private int inferContentType(Uri uri, String fileExtension) {
            String lastPathSegment = !TextUtils.isEmpty(fileExtension) ? "." + fileExtension
                    : uri.getLastPathSegment();
            if (lastPathSegment == null) {
                return TYPE_OTHER;
            } else if (lastPathSegment.endsWith(EXT_DASH)) {
                return TYPE_DASH;
            } else if (lastPathSegment.endsWith(EXT_SS)) {
                return TYPE_SS;
            } else if (lastPathSegment.endsWith(EXT_HLS)) {
                return TYPE_HLS;
            } else {
                return TYPE_OTHER;
            }
        }
    }

    public class ProgressBarViewHolder extends RecyclerView.ViewHolder {
        public ProgressBarViewHolder(View view) {
            super(view);
        }
    }

    public void unregisterEventBus() {
        EventBus.getDefault().unregister(this);
    }
}
  • When item type is video, I create an ExoPlayer instance to play this video and add this instance to a HashMap for later use (with key value is the item's position). If a video item is being recycled, I release the player and remove it from HashMap.

  • Everything looks good but a problem. I suppose the video item is at 0 position (now we can see the video preview in this item). I scroll down the RecyclerView just enough to hide the item 0. At this moment, the VideoViewHolder of item 0 is not recycled yet. Then, I scroll up to reveal item 0. The view is now blank, not show the video preview. I click item 0 to play the video, the player only plays audio (sound), not show video. After playing a few seconds, the video is now visible.

  • I debugged and found that after I scrolled down to hide video item, the SurfaceView is destroyed. When scroll up to reveal video item, the SurfaceView is created. I think this is the reason why VideoViewHolder shows blank view instead of the video preview.

  • Here is my question: How to display video preview after scroll back to video item?

like image 382
Hoang Lam Avatar asked Jan 11 '16 08:01

Hoang Lam


People also ask

What apps use ExoPlayer?

ExoPlayer is an app-level media player built on top of low-level media APIs in Android. It is an open source project used by Google apps, including YouTube and Google TV.

How do I Auto Start video in ExoPlayer?

This is how I create the SimpleExoPlayer: /** Create a default TrackSelector **/ TrackSelector trackSelector = new DefaultTrackSelector(new Handler()); /** Create a default LoadControl **/ LoadControl loadControl = new DefaultLoadControl(); /** Create the player **/ mPlayer = ExoPlayerFactory.

What's the difference between exo player and VLC player?

VLC Media Player for Android is one free and open source cross-platform multimedia player. It can play almost any media files, discs, and streaming media. So it is a good choice. ExoPlayer is one open source project that plays media with minimal code.


1 Answers

Here is note from ExoPlayer documentation

SurfaceView rendering wasn’t properly synchronized with view animations until Android N. On earlier releases this could result in unwanted effects when a SurfaceView was placed into scrolling container, or when it was subjected to animation. Such effects included the SurfaceView’s contents appearing to lag slightly behind where it should be displayed, and the view turning black when subjected to animation.

To achieve smooth animation or scrolling of video prior to Android N, it’s therefore necessary to use TextureView rather than SurfaceView. If smooth animation or scrolling is not required then SurfaceView should be preferred

To enable TextureView surface_type you have to set it in your xml file like shown below

 <com.google.android.exoplayer2.ui.SimpleExoPlayerView
    android:id="@+id/playerView"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    app:surface_type="texture_view"/>
like image 104
Volodymyr Avatar answered Sep 19 '22 14:09

Volodymyr