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?
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.
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.
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.
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"/>
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With