package me.ccrama.redditslide.Views; import android.content.Context; import android.graphics.Matrix; import android.graphics.PointF; import android.graphics.SurfaceTexture; import android.media.AudioManager; import android.media.MediaPlayer; import android.net.Uri; import android.util.AttributeSet; import android.util.Log; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ScaleGestureDetector; import android.view.Surface; import android.view.TextureView; import android.view.View; import android.widget.MediaController; import java.io.IOException; import me.ccrama.redditslide.util.LogUtil; /** * Created by vishna on 22/07/15. */ // //VideoView // // //Created by Alex Ross on 1/29/13 //Modified to accept a Matrix by Wiseman Designs // public class MediaVideoView extends TextureView implements MediaController.MediaPlayerControl { static final int NONE = 0; static final int DRAG = 1; static final int ZOOM = 2; static final int CLICK = 3; private static final String LOG_TAG = "VideoView"; // all possible internal states private static final int STATE_ERROR = -1; private static final int STATE_IDLE = 0; private static final int STATE_PREPARING = 1; private static final int STATE_PREPARED = 2; private static final int STATE_PLAYING = 3; private static final int STATE_PAUSED = 4; private static final int STATE_PLAYBACK_COMPLETED = 5; public int number; int mode = NONE; Matrix matrix = new Matrix(); ScaleGestureDetector mScaleDetector; float minScale = 1f; float maxScale = 5f; float[] m; PointF last = new PointF(); PointF start = new PointF(); float redundantXSpace, redundantYSpace; float width, height; float saveScale = 1f; float right, bottom, origWidth, origHeight, bmWidth, bmHeight; MediaPlayer.OnPreparedListener mOnPreparedListener; float lastFocusX; float lastFocusY; SurfaceTextureListener surfaceTextureListener = new SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(final SurfaceTexture surface, final int width, final int height) { LogUtil.v("Surface texture now avaialble."); surfaceTexture = surface; openVideo(); } @Override public void onSurfaceTextureSizeChanged(final SurfaceTexture surface, final int width, final int height) { LogUtil.v("Resized surface texture: " + width + '/' + height); surfaceWidth = width; surfaceHeight = height; boolean isValidState = (targetState == STATE_PLAYING); boolean hasValidSize = (videoWidth == width && videoHeight == height); if (mediaPlayer != null && isValidState && hasValidSize) { start(); } } @Override public boolean onSurfaceTextureDestroyed(final SurfaceTexture surface) { Log.i(LOG_TAG, "Destroyed surface number " + number); if (mediaController != null) mediaController.hide(); if (mediaPlayer != null) { mediaPlayer.reset(); mediaPlayer.release(); mediaPlayer = null; } return false; } @Override public void onSurfaceTextureUpdated(final SurfaceTexture surface) { } }; // currentState is a VideoView object's current state. // targetState is the state that a method caller intends to reach. // For instance, regardless the VideoView object's current state, // calling pause() intends to bring the object to a target state // of STATE_PAUSED. private int currentState = STATE_IDLE; private int targetState = STATE_IDLE; // Stuff we need for playing and showing a video private MediaPlayer mediaPlayer; private int videoWidth; private int videoHeight; private int surfaceWidth; private int surfaceHeight; private SurfaceTexture surfaceTexture; private Surface surface; private MediaController mediaController; private MediaPlayer.OnCompletionListener onCompletionListener; private MediaPlayer.OnPreparedListener onPreparedListener; private int currentBufferPercentage; private MediaPlayer.OnErrorListener onErrorListener; private MediaPlayer.OnInfoListener onInfoListener; private int mSeekWhenPrepared; // recording the seek position while preparing private boolean mCanPause; private boolean mCanSeekBack; private boolean mCanSeekForward; private Uri uri; //scale stuff private float widthScale = 1.0f; private float heightScale = 1.0f; private Context mContext; private int mAudioSession; // Listeners private MediaPlayer.OnBufferingUpdateListener bufferingUpdateListener = new MediaPlayer.OnBufferingUpdateListener() { @Override public void onBufferingUpdate(final MediaPlayer mp, final int percent) { currentBufferPercentage = percent; } }; private MediaPlayer.OnPreparedListener preparedListener = new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(final MediaPlayer mp) { currentState = STATE_PREPARED; LogUtil.v("Video prepared for " + number); videoWidth = mp.getVideoWidth(); videoHeight = mp.getVideoHeight(); mCanPause = mCanSeekBack = mCanSeekForward = true; if (mediaController != null) { mediaController.setEnabled(true); } if (mOnPreparedListener != null) { mOnPreparedListener.onPrepared(mediaPlayer); } requestLayout(); invalidate(); if ((videoWidth != 0) && (videoHeight != 0)) { LogUtil.v( "Video size for number " + number + ": " + videoWidth + '/' + videoHeight); if (targetState == STATE_PLAYING) { mediaPlayer.start(); } } else { if (targetState == STATE_PLAYING) { mediaPlayer.start(); } } } }; private MediaPlayer.OnVideoSizeChangedListener videoSizeChangedListener = new MediaPlayer.OnVideoSizeChangedListener() { @Override public void onVideoSizeChanged(final MediaPlayer mp, final int width, final int height) { LogUtil.v("Video size changed " + width + '/' + height + " number " + number); } }; private MediaPlayer.OnErrorListener errorListener = new MediaPlayer.OnErrorListener() { @Override public boolean onError(final MediaPlayer mp, final int what, final int extra) { currentState = STATE_ERROR; targetState = STATE_ERROR; Log.e(LOG_TAG, "There was an error during video playback."); return true; } }; public MediaVideoView(final Context context) { super(context); mContext = context; initVideoView(); } public MediaVideoView(final Context context, final AttributeSet attrs) { super(context, attrs); mContext = context; initVideoView(); } public MediaVideoView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); mContext = context; initVideoView(); } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && keyCode != KeyEvent.KEYCODE_VOLUME_UP && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && keyCode != KeyEvent.KEYCODE_MENU && keyCode != KeyEvent.KEYCODE_CALL && keyCode != KeyEvent.KEYCODE_ENDCALL; if (isInPlaybackState() && isKeyCodeSupported && mediaController != null) { if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { if (mediaPlayer.isPlaying()) { pause(); mediaController.show(); } else { start(); mediaController.hide(); } return true; } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { if (!mediaPlayer.isPlaying()) { start(); mediaController.hide(); } return true; } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { if (mediaPlayer.isPlaying()) { pause(); mediaController.show(); } return true; } else { toggleMediaControlsVisiblity(); } } return super.onKeyDown(keyCode, event); } @Override public boolean onTrackballEvent(MotionEvent ev) { if (isInPlaybackState() && mediaController != null) { toggleMediaControlsVisiblity(); } return false; } @Override public boolean onTouchEvent(MotionEvent ev) { if (isInPlaybackState() && mediaController != null && ev.getAction() == MotionEvent.ACTION_UP) { toggleMediaControlsVisiblity(); } return true; } @Override protected void onMeasure(final int widthMeasureSpec, final int heightMeasureSpec) { // Will resize the view if the video dimensions have been found. // video dimensions are found after onPrepared has been called by MediaPlayer LogUtil.v("onMeasure number " + number); int width = getDefaultSize(videoWidth, widthMeasureSpec); int height = getDefaultSize(videoHeight, heightMeasureSpec); if ((videoWidth > 0) && (videoHeight > 0)) { if ((videoWidth * height) > (width * videoHeight)) { LogUtil.v("Image too tall, correcting."); height = (width * videoHeight) / videoWidth; } else if ((videoWidth * height) < (width * videoHeight)) { LogUtil.v("Image too wide, correcting."); width = (height * videoWidth) / videoHeight; } else { LogUtil.v("Aspect ratio is correct."); } } LogUtil.v("Setting size: " + width + '/' + height + " for number " + number); setMeasuredDimension((int) (width * widthScale), (int) (height * heightScale)); } public void setSurfaceTexture(SurfaceTexture _surfaceTexture) { surfaceTexture = _surfaceTexture; } @Override public void start() { if (isInPlaybackState()) { mediaPlayer.start(); currentState = STATE_PLAYING; } targetState = STATE_PLAYING; } @Override public void pause() { if (isInPlaybackState()) { if (mediaPlayer.isPlaying()) { mediaPlayer.pause(); currentState = STATE_PAUSED; } } targetState = STATE_PAUSED; } @Override public int getDuration() { if (isInPlaybackState()) { return mediaPlayer.getDuration(); } return -1; } @Override public int getCurrentPosition() { if (isInPlaybackState()) { return mediaPlayer.getCurrentPosition(); } return 0; } @Override public void seekTo(int msec) { if (isInPlaybackState()) { mediaPlayer.seekTo(msec); mSeekWhenPrepared = 0; } else { mSeekWhenPrepared = msec; } } @Override public boolean isPlaying() { return isInPlaybackState() && mediaPlayer.isPlaying(); } @Override public int getBufferPercentage() { if (mediaPlayer != null) { return currentBufferPercentage; } return 0; } @Override public boolean canPause() { return mCanPause; } @Override public boolean canSeekBackward() { return mCanSeekBack; } @Override public boolean canSeekForward() { return mCanSeekForward; } @Override public int getAudioSessionId() { if (mAudioSession == 0) { MediaPlayer foo = new MediaPlayer(); mAudioSession = foo.getAudioSessionId(); foo.release(); } return mAudioSession; } public void initVideoView() { LogUtil.v("Initializing video view."); setAlpha(0); videoHeight = 0; videoWidth = 0; setFocusable(false); setSurfaceTextureListener(surfaceTextureListener); //todo make this work better! setOnTouchListener(new ZoomOnTouchListeners()); } public void openVideo() { if ((uri == null) || (surfaceTexture == null)) { LogUtil.v("Cannot open video, uri or surface is null number " + number); return; } animate().alpha(1); // Tell the music playback service to pause LogUtil.v("Opening video."); release(false); try { surface = new Surface(surfaceTexture); LogUtil.v("Creating media player number " + number); mediaPlayer = new MediaPlayer(); if (mAudioSession != 0) { mediaPlayer.setAudioSessionId(mAudioSession); } else { mAudioSession = mediaPlayer.getAudioSessionId(); } LogUtil.v("Setting surface."); mediaPlayer.setSurface(surface); LogUtil.v("Setting data source."); mediaPlayer.setDataSource(mContext, uri); LogUtil.v("Setting media player listeners."); mediaPlayer.setOnBufferingUpdateListener(bufferingUpdateListener); mediaPlayer.setOnPreparedListener(preparedListener); mediaPlayer.setOnErrorListener(errorListener); mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); mediaPlayer.setScreenOnWhilePlaying(true); mediaPlayer.setOnVideoSizeChangedListener(videoSizeChangedListener); LogUtil.v("Preparing media player."); mediaPlayer.prepareAsync(); currentState = STATE_PREPARING; attachMediaController(); } catch (IllegalStateException e) { currentState = STATE_ERROR; targetState = STATE_ERROR; e.printStackTrace(); } catch (IOException e) { currentState = STATE_ERROR; targetState = STATE_ERROR; e.printStackTrace(); } } public int resolveAdjustedSize(int desiredSize, int measureSpec) { LogUtil.v("Resolve called."); int result = desiredSize; int specMode = MeasureSpec.getMode(measureSpec); int specSize = MeasureSpec.getSize(measureSpec); switch (specMode) { case MeasureSpec.UNSPECIFIED: /* Parent says we can be as big as we want. Just don't be larger * than max size imposed on ourselves. */ result = desiredSize; break; case MeasureSpec.AT_MOST: /* Parent says we can be as big as we want, up to specSize. * Don't be larger than specSize, and don't be larger than * the max size imposed on ourselves. */ result = Math.min(desiredSize, specSize); break; case MeasureSpec.EXACTLY: // No choice. Do what we are told. result = specSize; break; } return result; } public void resume() { openVideo(); } public void setMatrix(Matrix mMatrix) { this.setTransform(mMatrix); } public void setMediaController(MediaController controller) { if (mediaController != null) { mediaController.hide(); } mediaController = controller; attachMediaController(); } public void setOnPreparedListener(MediaPlayer.OnPreparedListener onPreparedListener) { this.mOnPreparedListener = onPreparedListener; } public void setVideoPath(String path) { LogUtil.v("Setting video path to: " + path); setVideoURI(Uri.parse(path)); } public void setVideoURI(Uri _videoURI) { uri = _videoURI; openVideo(); requestLayout(); invalidate(); } public void setViewScale(float width, float height) { this.widthScale = width; this.heightScale = height; this.invalidate(); } public void setZOrderOnTop(boolean b) { } public void stopPlayback() { if (mediaPlayer != null) { mediaPlayer.stop(); mediaPlayer.release(); mediaPlayer = null; } } public void suspend() { release(false); } private void attachMediaController() { if (mediaPlayer != null && mediaController != null) { mediaController.setMediaPlayer(this); View anchorView = this.getParent() instanceof View ? (View) this.getParent() : this; mediaController.setAnchorView(anchorView); mediaController.setEnabled(isInPlaybackState()); } } private boolean isInPlaybackState() { return (mediaPlayer != null && currentState != STATE_ERROR && currentState != STATE_IDLE && currentState != STATE_PREPARING); } /* * release the media player in any state */ private void release(boolean cleartargetstate) { LogUtil.v("Releasing media player."); if (mediaPlayer != null) { mediaPlayer.reset(); mediaPlayer.release(); mediaPlayer = null; currentState = STATE_IDLE; if (cleartargetstate) { targetState = STATE_IDLE; } LogUtil.v("Released media player."); } else { LogUtil.v("Media player was null, did not release."); } } private void toggleMediaControlsVisiblity() { if (mediaController.isShowing()) { mediaController.hide(); } else { mediaController.show(); } } private class ZoomOnTouchListeners implements View.OnTouchListener { public ZoomOnTouchListeners() { super(); m = new float[9]; mScaleDetector = new ScaleGestureDetector(getContext(), new ScaleListener()); } @Override public boolean onTouch(View view, MotionEvent motionEvent) { mScaleDetector.onTouchEvent(motionEvent); matrix.getValues(m); float x = m[Matrix.MTRANS_X]; float y = m[Matrix.MTRANS_Y]; PointF curr = new PointF(motionEvent.getX(), motionEvent.getY()); switch (motionEvent.getActionMasked()) { case MotionEvent.ACTION_DOWN: last.set(motionEvent.getX(), motionEvent.getY()); start.set(last); mode = DRAG; break; case MotionEvent.ACTION_UP: mode = NONE; int xDiff = (int) Math.abs(curr.x - start.x); int yDiff = (int) Math.abs(curr.y - start.y); //if (xDiff < CLICK && yDiff < CLICK)//TODO click event? // performClick(); break; case MotionEvent.ACTION_POINTER_DOWN: last.set(motionEvent.getX(), motionEvent.getY()); start.set(last); mode = ZOOM; break; case MotionEvent.ACTION_MOVE: //if the mode is ZOOM or //if the mode is DRAG and already zoomed if (mode == ZOOM || (mode == DRAG && saveScale > minScale)) { float deltaX = curr.x - last.x;// x difference float deltaY = curr.y - last.y;// y difference float scaleWidth = Math.round( origWidth * saveScale);// width after applying current scale float scaleHeight = Math.round( origHeight * saveScale);// height after applying current scale //if scaleWidth is smaller than the views width //in other words if the image width fits in the view //limit left and right movement if (scaleWidth < width) { deltaX = 0; if (y + deltaY > 0) { deltaY = -y; } else if (y + deltaY < -bottom) deltaY = -(y + bottom); } //if scaleHeight is smaller than the views height //in other words if the image height fits in the view //limit up and down movement else if (scaleHeight < height) { deltaY = 0; if (x + deltaX > 0) { deltaX = -x; } else if (x + deltaX < -right) deltaX = -(x + right); } //if the image doesnt fit in the width or height //limit both up and down and left and right else { if (x + deltaX > 0) { deltaX = -x; } else if (x + deltaX < -right) deltaX = -(x + right); if (y + deltaY > 0) { deltaY = -y; } else if (y + deltaY < -bottom) deltaY = -(y + bottom); } //move the image with the matrix matrix.postTranslate(deltaX, deltaY); //set the last touch location to the current last.set(curr.x, curr.y); } break; case MotionEvent.ACTION_POINTER_UP: mode = NONE; break; } MediaVideoView.this.setTransform(matrix); MediaVideoView.this.invalidate(); return true; } private class ScaleListener extends ScaleGestureDetector.SimpleOnScaleGestureListener { @Override public boolean onScale(ScaleGestureDetector detector) { float mScaleFactor = detector.getScaleFactor(); float origScale = saveScale; saveScale *= mScaleFactor; if (saveScale > maxScale) { saveScale = maxScale; mScaleFactor = maxScale / origScale; } else if (saveScale < minScale) { saveScale = minScale; mScaleFactor = minScale / origScale; } right = width * saveScale - width - (2 * redundantXSpace * saveScale); bottom = height * saveScale - height - (2 * redundantYSpace * saveScale); if (origWidth * saveScale <= width || origHeight * saveScale <= height) { matrix.postScale(mScaleFactor, mScaleFactor, width / 2, height / 2); if (mScaleFactor < 1) { matrix.getValues(m); float x = m[Matrix.MTRANS_X]; float y = m[Matrix.MTRANS_Y]; if (mScaleFactor < 1) { if (Math.round(origWidth * saveScale) < width) { if (y < -bottom) { matrix.postTranslate(0, -(y + bottom)); } else if (y > 0) matrix.postTranslate(0, -y); } else { if (x < -right) { matrix.postTranslate(-(x + right), 0); } else if (x > 0) matrix.postTranslate(-x, 0); } } } } else { matrix.postScale(mScaleFactor, mScaleFactor, detector.getFocusX(), detector.getFocusY()); matrix.getValues(m); float x = m[Matrix.MTRANS_X]; float y = m[Matrix.MTRANS_Y]; if (mScaleFactor < 1) { if (x < -right) { matrix.postTranslate(-(x + right), 0); } else if (x > 0) matrix.postTranslate(-x, 0); if (y < -bottom) { matrix.postTranslate(0, -(y + bottom)); } else if (y > 0) matrix.postTranslate(0, -y); } } return true; } @Override public boolean onScaleBegin(ScaleGestureDetector detector) { mode = ZOOM; return true; } } } }