Skip to content

Instantly share code, notes, and snippets.

@HugoGresse
Last active September 5, 2018 03:42
Show Gist options
  • Save HugoGresse/669284a16ea25124228b to your computer and use it in GitHub Desktop.
Save HugoGresse/669284a16ea25124228b to your computer and use it in GitHub Desktop.
ExoPlayer implementation with a persisten TextureView & SurfaceTexture among context
import android.annotation.TargetApi;
import android.content.Context;
import android.graphics.Color;
import android.graphics.SurfaceTexture;
import android.os.Build;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Surface;
import android.view.TextureView;
import android.view.ViewGroup;
/**
* Extends ExoPlayer using a TextureView. The playing keep the same surface to draw onto during all
* player lifetime.
*
* Created by Hugo Gresse on 02/03/15.
*/
@TargetApi(Build.VERSION_CODES.JELLY_BEAN)
public class DynamicExoPlayer extends BasePlayer implements TextureView.SurfaceTextureListener {
protected TextureView mTextureView;
protected SurfaceTexture mSavedSurfaceTexture;
protected boolean mRequestNewAttach;
protected boolean mLastTextureDestroyed;
public DynamicExoPlayer(Context context, MediaFile mediaFile, VideoPlayerListener nativeVideoPlayerListener) {
super(context, mediaFile, nativeVideoPlayerListener);
}
public static final String LOG_TAG = "DynamicExoPlayer";
@Override
public void attach(Context context, ViewGroup viewGroup) {
if(mTextureView != null){
Log.d(LOG_TAG, "removeTextureView");
ViewGroup parent = (ViewGroup)mTextureView.getParent();
parent.getLayoutParams().width = parent.getWidth();
parent.removeView(mTextureView);
}
mContext = context;
mRootViewGroup = viewGroup;
mMuteButton = (MuteButton) mRootViewGroup.findViewById(R.id.buttonMute);
super.setButtonListener();
mRootViewGroup.setOnTouchListener(this);
// SurfaceView
TextureView textureView =
(TextureView) mRootViewGroup.findViewById(R.id.videoSurfaceLayout);
if(textureView == null){
ViewGroup videoContainer =
(ViewGroup) mRootViewGroup.findViewById(R.id.videoContainerFrameLayout);
if(mTextureView != null){
videoContainer.addView(mTextureView);
videoContainer.getLayoutParams().width = ViewGroup.LayoutParams.WRAP_CONTENT;
} else {
// When release after fullscreen finished/skip, the view is not added in normal
// layout (eg).
LayoutInflater layoutInflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
mTextureView = (TextureView) layoutInflater.inflate(
R.layout.nativevideolayout_textureview, mRootViewGroup, false);
videoContainer.addView(mTextureView);
}
} else {
mTextureView = textureView;
}
((ViewGroup)mTextureView.getParent()).setBackgroundColor(Color.BLACK);
mTextureView.setSurfaceTextureListener(this);
if(mTextureView.isSurfaceAvailable()){
mSavedSurfaceTexture = mTextureView.getSurfaceTexture();
attachSurfaceAndInit(mSavedSurfaceTexture);
} else if (mSavedSurfaceTexture != null && mLastTextureDestroyed) {
mTextureView.setSurfaceTexture(mSavedSurfaceTexture);
} else {
mRequestNewAttach = true;
}
mVideoWidthHeightRatio = (float) NumberUtils.round((float) 16 / 9, 3);
updateVideoRatio();
}
/**
* Update video width/height ratio
*/
@Override
public void updateVideoRatio(){
mTextureView.setVideoWidthHeightRatio(mVideoWidthHeightRatio);
}
/**
* Release the player by clearing all collection, release player. The player should not be used
* after this. To check if the player has been released, call {@link #isReleased()}
*/
@Override
public void release(){
super.release();
mRequestNewAttach = mLastTextureDestroyed = false;
}
/*----------------------------------------
* Protected Methods
*/
protected FullPlayer.RendererBuilder getRendererBuilder() {
return new DefaultRendererBuilder(mContext, mMediaFile.getMediaFileURI(), null);
}
protected void maybeStartPlayback() {
if(mTextureView.getSurfaceTexture() == null && mSavedSurfaceTexture != null){
Log.d(LOG_TAG, "mRequestNewAttach true : is attaching surface");
mAutoPlay = true;
return;
}
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerDidStartPlaying();
}
mPlayer.setPlayWhenReady(true);
mAutoPlay = false;
}
/**
* Attach a valid SurfaceTexture to the player
* @param surfaceTexture the surface to attach to the player
*/
protected void attachSurfaceAndInit(SurfaceTexture surfaceTexture){
// recheck mute button state
((MuteButton) mRootViewGroup.findViewById(R.id.buttonMute)).setMuted(mIsMute);
if (mPlayer != null) {
if(mSeekHandler == null){
startPlayerTimeListener();
}
mRequestNewAttach = false;
mPlayer.setSurface(new Surface(surfaceTexture));
if(mAutoPlay){
start();
}
}
}
/*----------------------------------------
* TextureView.SurfaceTextureListener
*/
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
Log.d(LOG_TAG, "onSurfaceTextureAvailable size=" + width + "x" + height + ", st=" + surfaceTexture);
// If this is our first time though, we're going to use the SurfaceTexture that
// the TextureView provided. If not, we're going to replace the current one with
// the original.
if (mSavedSurfaceTexture == null) {
mSavedSurfaceTexture = surfaceTexture;
} else {
// Can't do it here in Android <= 4.4. The TextureView doesn't add a
// listener on the new SurfaceTexture, so it never sees any updates.
// Needs to happen from activity onCreate() -- see recreateView().
//Log.d(LTAG, "using saved st=" + mSavedSurfaceTexture);
//mTextureView.setSurfaceTexture(mSavedSurfaceTexture);
}
if(mNativeVideoPlayerListenerList != null){
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerViewAttached();
}
}
attachSurfaceAndInit(surfaceTexture);
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
Log.d(LOG_TAG, "onSurfaceTextureDestroyed");
mLastTextureDestroyed = true;
if(mSavedSurfaceTexture != null && mRequestNewAttach){
mTextureView.setSurfaceTexture(mSavedSurfaceTexture);
mRequestNewAttach = false;
if(mAutoPlay){
start();
}
}
return (mSavedSurfaceTexture == null);
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
}
import android.content.Context;
import android.os.Handler;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.Toast;
import com.google.android.exoplayer.ExoPlayer;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* Based on Exoplayer FullPlayerActivity
*
* When the surface is destroyed, for example when the user switch app, the video is paused by
* blocking surface.
*
*
* Created by Hugo Gresse on 17/11/2014.
*/
public abstract class BasePlayer implements VideoPlayer, FullPlayer.Listener, View.OnTouchListener{
private static final String LOG_TAG = "BasePlayer";
public static final int RENDERER_COUNT = 2;
public static final int TYPE_VIDEO = 0;
public static final int TYPE_AUDIO = 1;
protected Context mContext;
/**
* Note that only the first listener will received publishProgress event
*/
protected CopyOnWriteArrayList<VideoPlayerListener> mNativeVideoPlayerListenerList;
protected EventLogger mEventLogger;
protected MediaFile mMediaFile;
protected FullPlayer mPlayer;
protected float mVideoWidthHeightRatio;
protected Handler mSeekHandler;
protected long mLastPosition;
protected long mPlayerPosition;
protected ViewGroup mRootViewGroup;
protected MuteButton mMuteButton;
protected boolean mAutoPlay = false;
protected boolean mIsReady = false;
protected boolean mIsMute = false;
protected boolean mRatioAlreadyCalculated = false;
public BasePlayer(Context context,
MediaFile mediaFile,
VideoPlayerListener nativeVideoPlayerListener) {
mContext = context;
mMediaFile = mediaFile;
mNativeVideoPlayerListenerList = new CopyOnWriteArrayList<>();
mNativeVideoPlayerListenerList.add(nativeVideoPlayerListener);
}
/**
* Init player
*/
@Override
public void init(){
if (mPlayer == null) {
mPlayer = new FullPlayer(getRendererBuilder());
mPlayer.addListener(this);
mPlayer.seekTo(mPlayerPosition);
try {
mEventLogger = new EventLogger();
mEventLogger.startSession();
} catch (Exception e){
e.printStackTrace();
}
mPlayer.addListener(mEventLogger);
mPlayer.setInfoListener(mEventLogger);
mPlayer.setInternalErrorListener(mEventLogger);
}
}
/**
* Called when surface has changed (entering a new activity with a new layout eg). This will
* attach the surface contained inside the viewGroup to the player. Also setting additional
* listener on the given view.
* @param context current app context
* @param viewGroup viewGroup containing the surface
*/
@Override
public abstract void attach(Context context, ViewGroup viewGroup);
/**
* Update video width/height ratio
*/
@Override
public abstract void updateVideoRatio();
/**
* Internal called when video size changed
* @param width video width
* @param height video height
* @param pixelRatio (optional) pixel ratio
*/
@Override
public void onVideoSizeChanged(int width, int height, float pixelRatio){
// originaly, ratio was calculated from video. Not anymore : one received and if ratio is
// diferent, update it
// mVideoWidthHeightRatio = height == 0 ? 1 : (float) width / height;
if(!mRatioAlreadyCalculated && mVideoWidthHeightRatio != (float) width/height){
mVideoWidthHeightRatio = (float) width/height;
mRatioAlreadyCalculated = true;
}
// setVideoWidthHeightRatio();
}
/**
* Pre load the video to begin buffering while video is not started
*/
@Override
public void preLoad(){
mPlayer.prepare();
}
/**
* Start or resume.
*/
@Override
public void start(){
mAutoPlay = true;
maybeStartPlayback();
}
/**
* Pause the player
*/
@Override
public void pause(){
mAutoPlay = false;
if(!isReleased()){
mPlayer.setPlayWhenReady(false);
}
}
/**
* Mute current video
*/
@Override
public void mute(){
mMuteButton.setMuted(mIsMute = true);
mPlayer.setMute(mIsMute);
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerDidMute();
}
}
/**
* Unmute ucrrent video
*/
@Override
public void unMute(){
mMuteButton.setMuted(mIsMute = false);
mPlayer.setMute(mIsMute);
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerDidUnmute();
}
}
/**
* Release the player by clearing all collection, release player. The player should not be used
* after this. To check if the player has been released, call {@link #isReleased()}
*/
@Override
public void release(){
if (mPlayer != null) {
Log.d(LOG_TAG, "release");
mPlayerPosition = mPlayer.getCurrentPosition();
mPlayer.removeListener(this);
mPlayer.release();
mPlayer = null;
mEventLogger.endSession();
mEventLogger = null;
}
}
/**
* Check if player has been released
* @return true if released, false either
*/
@Override
public boolean isReleased(){
if(mPlayer == null){
return true;
}
return false;
}
/**
* Check if player is playing a video
* @return true if is playing, false otherweise
*/
@Override
public boolean isPlaying(){
if(null != mPlayer){
return mPlayer.getPlayWhenReady();
} else {
return false;
}
}
/**
* Check if the video is in fullscreen.
* @return true if in fullscreen, false otherweise
*/
@Override
public boolean isFullscreen(){
return false;
}
/**
* Get current video duration
* @return video duration
*/
@Override
public long getDuration(){
return mPlayer.getDuration();
}
/**
* Register a new player listener to be notified by player event.
* See {@link VideoPlayerListener}
*/
@Override
public void addPlayerListener(VideoPlayerListener listener){
if(mNativeVideoPlayerListenerList != null){
mNativeVideoPlayerListenerList.add(listener);
}
}
/**
* Unregister athe given listener.
* See {@link #addPlayerListener(VideoPlayerListener)}
* See {@link VideoPlayerListener}
*/
@Override
public void removePlayerListener(VideoPlayerListener listener){
if(mNativeVideoPlayerListenerList != null){
mNativeVideoPlayerListenerList.remove(listener);
}
}
/*----------------------------------------
* Protected Methods
*/
protected abstract FullPlayer.RendererBuilder getRendererBuilder();
protected abstract void maybeStartPlayback();
/**
* Check every second the current time of the player
*/
protected void startPlayerTimeListener(){
mSeekHandler = new Handler();
final int delay = 1000; //milliseconds
mLastPosition = 0;
mSeekHandler.postDelayed(new Runnable() {
public void run() {
if(null == mPlayer){
return;
}
if(mLastPosition == mPlayer.getCurrentPosition()){
mSeekHandler.postDelayed(this, delay);
return;
}
mLastPosition = mPlayer.getCurrentPosition();
if(mNativeVideoPlayerListenerList != null
&& mNativeVideoPlayerListenerList.get(0) != null){
mNativeVideoPlayerListenerList.get(0).nativeVideoPlayerPublishProgress(mPlayer.getCurrentPosition());
}
// rerun handler if next time to sent event is not the end
mSeekHandler.postDelayed(this, delay);
}
}, delay);
}
protected void setButtonListener(){
// Update button visibility when a new view is attached
mMuteButton.setMuted(mIsMute);
ImageButton skipButton = (ImageButton)mRootViewGroup.findViewById(R.id.buttonSkip);
mMuteButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if (mIsMute) {
unMute();
} else {
mute();
}
}
});
skipButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(mNativeVideoPlayerListenerList != null){
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerDidSkip();
}
}
}
});
}
/*----------------------------------------
* FullPlayer.Listener
*/
@Override
public void onStateChanged(boolean playWhenReady, int playbackState) {
switch (playbackState){
case ExoPlayer.STATE_READY:
// prevent multiple isReady event sending by sending only the first one
if(!mIsReady){
mIsReady = true;
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerIsLoaded();
}
}
break;
case ExoPlayer.STATE_ENDED:
if(mNativeVideoPlayerListenerList != null){
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerFinishPlaying();
}
mNativeVideoPlayerListenerList = null;
}
// prevent Player for sending more than one finish playing event
break;
}
}
@Override
public void onError(Exception e) {
Log.e(LOG_TAG, "Playback failed", e);
Toast.makeText(mContext, "Playback failed", Toast.LENGTH_SHORT).show();
if(mNativeVideoPlayerListenerList != null){
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerOnError(e);
}
}
}
/*----------------------------------------
* Touch event
*/
private boolean mIsNativeClick = false;
private float mStartNativeX;
private float mStartNativeY;
private final float SCROLL_THRESHOLD = 10;
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction() & MotionEvent.ACTION_MASK) {
case MotionEvent.ACTION_DOWN:
mStartNativeX = event.getX();
mStartNativeY = event.getY();
mIsNativeClick = true;
return true;
case MotionEvent.ACTION_MOVE:
if (mIsNativeClick && (Math.abs(mStartNativeX - event.getX()) > SCROLL_THRESHOLD
|| Math.abs(mStartNativeY - event.getY()) > SCROLL_THRESHOLD)) {
mIsNativeClick = false;
}
break;
case MotionEvent.ACTION_UP:
if (mIsNativeClick && !isReleased() && mNativeVideoPlayerListenerList != null) {
for(VideoPlayerListener listener : mNativeVideoPlayerListenerList){
listener.nativeVideoPlayerDidTouch();
}
return true;
}
}
return false;
}
}
@HugoGresse
Copy link
Author

If you are using canvas I guess I can't help you @qqli007

@lipangit
Copy link

lipangit commented Nov 1, 2016

@HugoGresse Can you help me, In this project there are two TextureView, I want to switch TextureView when click the TextureView without delay, I make many try these days but faild, Can you help me ?

If succeed the project will use exoplayer instead of ijkplayer, It will much better.

@lipangit
Copy link

lipangit commented Nov 2, 2016

@HugoGresse hello

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment