package com.brentvatne.react; import android.annotation.SuppressLint; import android.content.res.AssetFileDescriptor; import android.graphics.Matrix; import android.media.MediaPlayer; import android.media.TimedMetaData; import android.net.Uri; import android.os.Handler; import android.util.Log; import android.view.MotionEvent; import android.webkit.CookieManager; import android.widget.MediaController; import com.android.vending.expansion.zipfile.APKExpansionSupport; import com.android.vending.expansion.zipfile.ZipResourceFile; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.LifecycleEventListener; import com.facebook.react.bridge.WritableMap; import com.facebook.react.bridge.WritableArray; import com.facebook.react.bridge.WritableNativeArray; import com.facebook.react.uimanager.ThemedReactContext; import com.facebook.react.uimanager.events.RCTEventEmitter; import com.yqritc.scalablevideoview.ScalableType; import com.yqritc.scalablevideoview.ScalableVideoView; import com.yqritc.scalablevideoview.ScaleManager; import com.yqritc.scalablevideoview.Size; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.HashMap; import java.util.Map; import java.lang.Math; @SuppressLint("ViewConstructor") public class ReactVideoView extends ScalableVideoView implements MediaPlayer.OnPreparedListener, MediaPlayer.OnErrorListener, MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnCompletionListener, MediaPlayer.OnInfoListener, LifecycleEventListener, MediaController.MediaPlayerControl { public enum Events { EVENT_LOAD_START("onVideoLoadStart"), EVENT_LOAD("onVideoLoad"), EVENT_ERROR("onVideoError"), EVENT_PROGRESS("onVideoProgress"), EVENT_TIMED_METADATA("onTimedMetadata"), EVENT_SEEK("onVideoSeek"), EVENT_END("onVideoEnd"), EVENT_STALLED("onPlaybackStalled"), EVENT_RESUME("onPlaybackResume"), EVENT_READY_FOR_DISPLAY("onReadyForDisplay"); private final String mName; Events(final String name) { mName = name; } @Override public String toString() { return mName; } } public static final String EVENT_PROP_FAST_FORWARD = "canPlayFastForward"; public static final String EVENT_PROP_SLOW_FORWARD = "canPlaySlowForward"; public static final String EVENT_PROP_SLOW_REVERSE = "canPlaySlowReverse"; public static final String EVENT_PROP_REVERSE = "canPlayReverse"; public static final String EVENT_PROP_STEP_FORWARD = "canStepForward"; public static final String EVENT_PROP_STEP_BACKWARD = "canStepBackward"; public static final String EVENT_PROP_DURATION = "duration"; public static final String EVENT_PROP_PLAYABLE_DURATION = "playableDuration"; public static final String EVENT_PROP_CURRENT_TIME = "currentTime"; public static final String EVENT_PROP_SEEK_TIME = "seekTime"; public static final String EVENT_PROP_NATURALSIZE = "naturalSize"; public static final String EVENT_PROP_WIDTH = "width"; public static final String EVENT_PROP_HEIGHT = "height"; public static final String EVENT_PROP_ORIENTATION = "orientation"; public static final String EVENT_PROP_METADATA = "metadata"; public static final String EVENT_PROP_TARGET = "target"; public static final String EVENT_PROP_METADATA_IDENTIFIER = "identifier"; public static final String EVENT_PROP_METADATA_VALUE = "value"; public static final String EVENT_PROP_ERROR = "error"; public static final String EVENT_PROP_WHAT = "what"; public static final String EVENT_PROP_EXTRA = "extra"; private ThemedReactContext mThemedReactContext; private RCTEventEmitter mEventEmitter; private Handler mProgressUpdateHandler = new Handler(); private Runnable mProgressUpdateRunnable = null; private Handler videoControlHandler = new Handler(); private MediaController mediaController; private String mSrcUriString = null; private String mSrcType = "mp4"; private boolean mSrcIsNetwork = false; private boolean mSrcIsAsset = false; private ScalableType mResizeMode = ScalableType.LEFT_TOP; private boolean mRepeat = false; private boolean mPaused = false; private boolean mMuted = false; private float mVolume = 1.0f; private float mProgressUpdateInterval = 250.0f; private float mRate = 1.0f; private boolean mPlayInBackground = false; private boolean mActiveStatePauseStatus = false; private boolean mActiveStatePauseStatusInitialized = false; private int mMainVer = 0; private int mPatchVer = 0; private boolean mMediaPlayerValid = false; // True if mMediaPlayer is in prepared, started, paused or completed state. private int mVideoDuration = 0; private int mVideoBufferedDuration = 0; private boolean isCompleted = false; private boolean mUseNativeControls = false; public ReactVideoView(ThemedReactContext themedReactContext) { super(themedReactContext); mThemedReactContext = themedReactContext; mEventEmitter = themedReactContext.getJSModule(RCTEventEmitter.class); themedReactContext.addLifecycleEventListener(this); initializeMediaPlayerIfNeeded(); setSurfaceTextureListener(this); mProgressUpdateRunnable = new Runnable() { @Override public void run() { if (mMediaPlayerValid && !isCompleted &&!mPaused) { WritableMap event = Arguments.createMap(); event.putDouble(EVENT_PROP_CURRENT_TIME, mMediaPlayer.getCurrentPosition() / 1000.0); event.putDouble(EVENT_PROP_PLAYABLE_DURATION, mVideoBufferedDuration / 1000.0); //TODO:mBufferUpdateRunnable mEventEmitter.receiveEvent(getId(), Events.EVENT_PROGRESS.toString(), event); // Check for update after an interval mProgressUpdateHandler.postDelayed(mProgressUpdateRunnable, Math.round(mProgressUpdateInterval)); } } }; } @Override public boolean onTouchEvent(MotionEvent event) { if (mUseNativeControls) { initializeMediaControllerIfNeeded(); mediaController.show(); } return super.onTouchEvent(event); } @Override @SuppressLint("DrawAllocation") protected void onLayout(boolean changed, int left, int top, int right, int bottom) { super.onLayout(changed, left, top, right, bottom); if (!changed || !mMediaPlayerValid) { return; } int videoWidth = getVideoWidth(); int videoHeight = getVideoHeight(); if (videoWidth == 0 || videoHeight == 0) { return; } Size viewSize = new Size(getWidth(), getHeight()); Size videoSize = new Size(videoWidth, videoHeight); ScaleManager scaleManager = new ScaleManager(viewSize, videoSize); Matrix matrix = scaleManager.getScaleMatrix(mScalableType); if (matrix != null) { setTransform(matrix); } } private void initializeMediaPlayerIfNeeded() { if (mMediaPlayer == null) { mMediaPlayerValid = false; mMediaPlayer = new MediaPlayer(); mMediaPlayer.setScreenOnWhilePlaying(true); mMediaPlayer.setOnVideoSizeChangedListener(this); mMediaPlayer.setOnErrorListener(this); mMediaPlayer.setOnPreparedListener(this); mMediaPlayer.setOnBufferingUpdateListener(this); mMediaPlayer.setOnCompletionListener(this); mMediaPlayer.setOnInfoListener(this); if (Build.VERSION.SDK_INT >= 23) { mMediaPlayer.setOnTimedMetaDataAvailableListener(new TimedMetaDataAvailableListener()); } } } private void initializeMediaControllerIfNeeded() { if (mediaController == null) { mediaController = new MediaController(this.getContext()); } } public void cleanupMediaPlayerResources() { if ( mediaController != null ) { mediaController.hide(); } if ( mMediaPlayer != null ) { mMediaPlayerValid = false; release(); } } public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset) { setSrc(uriString,type,isNetwork,isAsset,0,0); } public void setSrc(final String uriString, final String type, final boolean isNetwork, final boolean isAsset, final int expansionMainVersion, final int expansionPatchVersion) { mSrcUriString = uriString; mSrcType = type; mSrcIsNetwork = isNetwork; mSrcIsAsset = isAsset; mMainVer = expansionMainVersion; mPatchVer = expansionPatchVersion; mMediaPlayerValid = false; mVideoDuration = 0; mVideoBufferedDuration = 0; initializeMediaPlayerIfNeeded(); mMediaPlayer.reset(); try { if (isNetwork) { // Use the shared CookieManager to access the cookies // set by WebViews inside the same app CookieManager cookieManager = CookieManager.getInstance(); Uri parsedUrl = Uri.parse(uriString); Uri.Builder builtUrl = parsedUrl.buildUpon(); String cookie = cookieManager.getCookie(builtUrl.build().toString()); Map headers = new HashMap(); if (cookie != null) { headers.put("Cookie", cookie); } setDataSource(uriString); } else if (isAsset) { if (uriString.startsWith("content://")) { Uri parsedUrl = Uri.parse(uriString); setDataSource(mThemedReactContext, parsedUrl); } else { setDataSource(uriString); } } else { ZipResourceFile expansionFile= null; AssetFileDescriptor fd= null; if(mMainVer>0) { try { expansionFile = APKExpansionSupport.getAPKExpansionZipFile(mThemedReactContext, mMainVer, mPatchVer); fd = expansionFile.getAssetFileDescriptor(uriString.replace(".mp4","") + ".mp4"); } catch (IOException e) { e.printStackTrace(); } catch (NullPointerException e) { e.printStackTrace(); } } if(fd==null) { int identifier = mThemedReactContext.getResources().getIdentifier( uriString, "drawable", mThemedReactContext.getPackageName() ); if (identifier == 0) { identifier = mThemedReactContext.getResources().getIdentifier( uriString, "raw", mThemedReactContext.getPackageName() ); } setRawData(identifier); } else { setDataSource(fd.getFileDescriptor(), fd.getStartOffset(),fd.getLength()); } } } catch (Exception e) { e.printStackTrace(); return; } WritableMap src = Arguments.createMap(); src.putString(ReactVideoViewManager.PROP_SRC_URI, uriString); src.putString(ReactVideoViewManager.PROP_SRC_TYPE, type); src.putBoolean(ReactVideoViewManager.PROP_SRC_IS_NETWORK, isNetwork); if(mMainVer>0) { src.putInt(ReactVideoViewManager.PROP_SRC_MAINVER, mMainVer); if(mPatchVer>0) { src.putInt(ReactVideoViewManager.PROP_SRC_PATCHVER, mPatchVer); } } WritableMap event = Arguments.createMap(); event.putMap(ReactVideoViewManager.PROP_SRC, src); mEventEmitter.receiveEvent(getId(), Events.EVENT_LOAD_START.toString(), event); try { prepareAsync(this); } catch (Exception e) { e.printStackTrace(); } } public void setResizeModeModifier(final ScalableType resizeMode) { mResizeMode = resizeMode; if (mMediaPlayerValid) { setScalableType(resizeMode); invalidate(); } } public void setRepeatModifier(final boolean repeat) { mRepeat = repeat; if (mMediaPlayerValid) { setLooping(repeat); } } public void setPausedModifier(final boolean paused) { mPaused = paused; if ( !mActiveStatePauseStatusInitialized ) { mActiveStatePauseStatus = mPaused; mActiveStatePauseStatusInitialized = true; } if (!mMediaPlayerValid) { return; } if (mPaused) { if (mMediaPlayer.isPlaying()) { pause(); } } else { if (!mMediaPlayer.isPlaying()) { start(); // Also Start the Progress Update Handler mProgressUpdateHandler.post(mProgressUpdateRunnable); } } } public void setMutedModifier(final boolean muted) { mMuted = muted; if (!mMediaPlayerValid) { return; } if (mMuted) { setVolume(0, 0); } else { setVolume(mVolume, mVolume); } } public void setVolumeModifier(final float volume) { mVolume = volume; setMutedModifier(mMuted); } public void setProgressUpdateInterval(final float progressUpdateInterval) { mProgressUpdateInterval = progressUpdateInterval; } public void setRateModifier(final float rate) { mRate = rate; if (mMediaPlayerValid) { // TODO: Implement this. Log.e(ReactVideoViewManager.REACT_CLASS, "Setting playback rate is not yet supported on Android"); } } public void applyModifiers() { setResizeModeModifier(mResizeMode); setRepeatModifier(mRepeat); setPausedModifier(mPaused); setMutedModifier(mMuted); setProgressUpdateInterval(mProgressUpdateInterval); // setRateModifier(mRate); } public void setPlayInBackground(final boolean playInBackground) { mPlayInBackground = playInBackground; } public void setControls(boolean controls) { this.mUseNativeControls = controls; } @Override public void onPrepared(MediaPlayer mp) { mMediaPlayerValid = true; mVideoDuration = mp.getDuration(); WritableMap naturalSize = Arguments.createMap(); naturalSize.putInt(EVENT_PROP_WIDTH, mp.getVideoWidth()); naturalSize.putInt(EVENT_PROP_HEIGHT, mp.getVideoHeight()); if (mp.getVideoWidth() > mp.getVideoHeight()) naturalSize.putString(EVENT_PROP_ORIENTATION, "landscape"); else naturalSize.putString(EVENT_PROP_ORIENTATION, "portrait"); WritableMap event = Arguments.createMap(); event.putDouble(EVENT_PROP_DURATION, mVideoDuration / 1000.0); event.putDouble(EVENT_PROP_CURRENT_TIME, mp.getCurrentPosition() / 1000.0); event.putMap(EVENT_PROP_NATURALSIZE, naturalSize); // TODO: Actually check if you can. event.putBoolean(EVENT_PROP_FAST_FORWARD, true); event.putBoolean(EVENT_PROP_SLOW_FORWARD, true); event.putBoolean(EVENT_PROP_SLOW_REVERSE, true); event.putBoolean(EVENT_PROP_REVERSE, true); event.putBoolean(EVENT_PROP_FAST_FORWARD, true); event.putBoolean(EVENT_PROP_STEP_BACKWARD, true); event.putBoolean(EVENT_PROP_STEP_FORWARD, true); mEventEmitter.receiveEvent(getId(), Events.EVENT_LOAD.toString(), event); applyModifiers(); if (mUseNativeControls) { initializeMediaControllerIfNeeded(); mediaController.setMediaPlayer(this); mediaController.setAnchorView(this); videoControlHandler.post(new Runnable() { @Override public void run() { mediaController.setEnabled(true); mediaController.show(); } }); } // Select track (so we can use it to listen to timed meta data updates) mp.selectTrack(0); } @Override public boolean onError(MediaPlayer mp, int what, int extra) { WritableMap error = Arguments.createMap(); error.putInt(EVENT_PROP_WHAT, what); error.putInt(EVENT_PROP_EXTRA, extra); WritableMap event = Arguments.createMap(); event.putMap(EVENT_PROP_ERROR, error); mEventEmitter.receiveEvent(getId(), Events.EVENT_ERROR.toString(), event); return true; } @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { switch (what) { case MediaPlayer.MEDIA_INFO_BUFFERING_START: mEventEmitter.receiveEvent(getId(), Events.EVENT_STALLED.toString(), Arguments.createMap()); break; case MediaPlayer.MEDIA_INFO_BUFFERING_END: mEventEmitter.receiveEvent(getId(), Events.EVENT_RESUME.toString(), Arguments.createMap()); break; case MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START: mEventEmitter.receiveEvent(getId(), Events.EVENT_READY_FOR_DISPLAY.toString(), Arguments.createMap()); break; default: } return false; } @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { // Select track (so we can use it to listen to timed meta data updates) mp.selectTrack(0); mVideoBufferedDuration = (int) Math.round((double) (mVideoDuration * percent) / 100.0); } @Override public void seekTo(int msec) { if (mMediaPlayerValid) { WritableMap event = Arguments.createMap(); event.putDouble(EVENT_PROP_CURRENT_TIME, getCurrentPosition() / 1000.0); event.putDouble(EVENT_PROP_SEEK_TIME, msec / 1000.0); mEventEmitter.receiveEvent(getId(), Events.EVENT_SEEK.toString(), event); super.seekTo(msec); if (isCompleted && mVideoDuration != 0 && msec < mVideoDuration) { isCompleted = false; } } } @Override public int getBufferPercentage() { return 0; } @Override public boolean canPause() { return true; } @Override public boolean canSeekBackward() { return true; } @Override public boolean canSeekForward() { return true; } @Override public int getAudioSessionId() { return 0; } @Override public void onCompletion(MediaPlayer mp) { isCompleted = true; mEventEmitter.receiveEvent(getId(), Events.EVENT_END.toString(), null); } // This is not fully tested and does not work for all forms of timed metadata @TargetApi(23) // 6.0 public class TimedMetaDataAvailableListener implements MediaPlayer.OnTimedMetaDataAvailableListener { public void onTimedMetaDataAvailable(MediaPlayer mp, TimedMetaData data) { WritableMap event = Arguments.createMap(); try { String rawMeta = new String(data.getMetaData(), "UTF-8"); WritableMap id3 = Arguments.createMap(); id3.putString(EVENT_PROP_METADATA_VALUE, rawMeta.substring(rawMeta.lastIndexOf("\u0003") + 1)); id3.putString(EVENT_PROP_METADATA_IDENTIFIER, "id3/TDEN"); WritableArray metadata = new WritableNativeArray(); metadata.pushMap(id3); event.putArray(EVENT_PROP_METADATA, metadata); event.putDouble(EVENT_PROP_TARGET, getId()); } catch (UnsupportedEncodingException e) { e.printStackTrace(); } mEventEmitter.receiveEvent(getId(), Events.EVENT_TIMED_METADATA.toString(), event); } } @Override protected void onDetachedFromWindow() { mMediaPlayerValid = false; super.onDetachedFromWindow(); } @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); if(mMainVer>0) { setSrc(mSrcUriString, mSrcType, mSrcIsNetwork,mSrcIsAsset,mMainVer,mPatchVer); } else { setSrc(mSrcUriString, mSrcType, mSrcIsNetwork,mSrcIsAsset); } } @Override public void onHostPause() { if (mMediaPlayer != null && !mPlayInBackground) { mActiveStatePauseStatus = mPaused; // Pause the video in background setPausedModifier(true); } } @Override public void onHostResume() { if (mMediaPlayer != null && !mPlayInBackground) { new Handler().post(new Runnable() { @Override public void run() { // Restore original state setPausedModifier(mActiveStatePauseStatus); } }); } } @Override public void onHostDestroy() { } }