Last active
September 5, 2018 03:42
-
-
Save HugoGresse/669284a16ea25124228b to your computer and use it in GitHub Desktop.
ExoPlayer implementation with a persisten TextureView & SurfaceTexture among context
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} |
If you are using canvas I guess I can't help you @qqli007
@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.
@HugoGresse hello
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
when I use the same SurfaceTexture when switch Activity,I have a error, lite this:
A/libc﹕ Fatal signal 11 (SIGSEGV) at 0x7dc35000 (code=1), thread 14119 (Thread-1108)
I use lockCanvas() and unlockCanvasAndPost() for rendering.
Do you have any idea about this error?