package com.kaltura.playersdk.players; import android.content.Context; import android.drm.DrmErrorEvent; import android.drm.DrmEvent; import android.media.MediaPlayer; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.view.Gravity; import android.view.SurfaceHolder; import android.widget.FrameLayout; import android.widget.VideoView; import com.kaltura.playersdk.tracks.TrackFormat; import com.kaltura.playersdk.tracks.TrackType; import com.kaltura.playersdk.PlayerViewController; import com.kaltura.playersdk.drm.WidevineDrmClient; import java.util.Collections; import java.util.Set; import static com.kaltura.playersdk.utils.LogUtils.LOGD; import static com.kaltura.playersdk.utils.LogUtils.LOGE; import static com.kaltura.playersdk.utils.LogUtils.LOGI; import static com.kaltura.playersdk.utils.LogUtils.LOGW; /** * Created by noamt on 10/27/15. */ public class KWVCPlayer extends FrameLayout implements KPlayer { private static final String TAG = "KWVCPlayer"; private static final long PLAYHEAD_UPDATE_INTERVAL = 200; @Nullable private VideoView mPlayer; private String mAssetUri; private String mLicenseUri; private WidevineDrmClient mDrmClient; @NonNull private KPlayerListener mListener; @NonNull private KPlayerCallback mCallback; private boolean mShouldCancelPlay; private boolean mShouldPlayWhenReady; @Nullable private PlayheadTracker mPlayheadTracker; private PrepareState mPrepareState; @NonNull private PlayerState mSavedState; private boolean isFirstPreparation = true; private int mCurrentPosition; private boolean mWasDestroyed; private String mLastSentEvent = ""; public static Set<KMediaFormat> supportedFormats(Context context) { if (WidevineDrmClient.isSupported(context)) { return Collections.singleton(KMediaFormat.wvm_widevine); } return Collections.emptySet(); } /** * Construct a new Widevine Classic player. * @param context * @throws UnsupportedOperationException if Widevine Classic is not supported by platform. */ public KWVCPlayer(Context context) { super(context); mDrmClient = new WidevineDrmClient(context); mDrmClient.setEventListener(new WidevineDrmClient.EventListener() { @Override public void onError(final DrmErrorEvent event) { mShouldCancelPlay = true; KWVCPlayer.this.post(new Runnable() { @Override public void run() { mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.ErrorKey, "DRM error"); } }); } @Override public void onEvent(DrmEvent event) { } }); mSavedState = new PlayerState(); // Set no-op listeners so we don't have to check for null on use setPlayerListener(null); setPlayerCallback(null); } // Convert file:///local/path/a.wvm to /local/path/a.wvm // Convert http://example.com/path/a.wvm to widevine://example.com/path/a.wvm // Every else remains the same. public static String getWidevineAssetPlaybackUri(String assetUri) { if (assetUri.startsWith("file:")) { assetUri = Uri.parse(assetUri).getPath(); } else if (assetUri.startsWith("http:")) { assetUri = assetUri.replaceFirst("^http:", "widevine:"); } return assetUri; } // Convert file:///local/path/a.wvm to /local/path/a.wvm // Convert widevine://example.com/path/a.wvm to http://example.com/path/a.wvm // Everything else remains the same. public static String getWidevineAssetAcquireUri(String assetUri) { if (assetUri.startsWith("file:")) { assetUri = Uri.parse(assetUri).getPath(); } else if (assetUri.startsWith("widevine:")) { assetUri = assetUri.replaceFirst("widevine", "http"); } return assetUri; } @Override public void setPlayerListener(KPlayerListener listener) { if (listener == null) { // Create a no-op listener listener = new KPlayerListener() { public void eventWithValue(KPlayer player, String eventName, String eventValue) {} public void eventWithJSON(KPlayer player, String eventName, String jsonValue) {} public void asyncEvaluate(String expression, String expressionID, PlayerViewController.EvaluateListener evaluateListener) {} public void contentCompleted(KPlayer currentPlayer) {} }; } mListener = listener; } @Override public void setPlayerCallback(KPlayerCallback callback) { if (callback == null) { // Create a no-op callback callback = new KPlayerCallback() { public void playerStateChanged(int state) {} }; } mCallback = callback; } @Override public void setPlayerSource(String source) { mAssetUri = source; if (mLicenseUri != null) { isFirstPreparation = true; preparePlayer(); } else { LOGD(TAG, "setPlayerSource: waiting for licenseUri."); } } @Override public void setLicenseUri(String licenseUri) { mLicenseUri = licenseUri; if (mAssetUri != null) { preparePlayer(); } else { LOGD(TAG, "setLicenseUri: Waiting for assetUri."); } } @Override public long getCurrentPlaybackTime() { int currentPos = (mPlayer != null) ? mPlayer.getCurrentPosition() : 0; LOGD(TAG, "getCurrentPlaybackTime: current position = " + currentPos); return currentPos; } @Override public void setCurrentPlaybackTime(long currentPlaybackTime) { if (mPlayer != null) { LOGD(TAG, "setCurrentPlaybackTime: seekTo currentPlaybackTime " + currentPlaybackTime); mPlayer.seekTo((int) (currentPlaybackTime)); } } @Override public long getDuration() { return mPlayer != null ? mPlayer.getDuration() : 0; } @Override public void play() { // If already playing, don't do anything. if (mWasDestroyed || mPlayer == null || mPlayer.isPlaying()) { return; } // If play should be canceled, don't even start. if (mShouldCancelPlay) { mShouldCancelPlay = false; return; } // If not prepared, ask player to start when prepared. if (mPrepareState != PrepareState.Prepared) { mShouldPlayWhenReady = true; preparePlayer(); return; } if (mSavedState.position != 0) { mShouldPlayWhenReady = true; setCurrentPlaybackTime(mSavedState.position); // will start playing after seek complete return; //mSavedState.position = 0; } mPlayer.requestFocus(); mPlayer.start(); if (mPlayheadTracker == null) { mPlayheadTracker = new PlayheadTracker(); } mPlayheadTracker.start(); changePlayPauseState("play"); } @Override public void pause() { if (mWasDestroyed) { return; } if (mPrepareState == PrepareState.Ended) { return; } if (mPlayer != null) { if (mPlayer.isPlaying()) { mPlayer.pause(); changePlayPauseState("pause"); } } stopPlayheadTracker(); } private void changePlayPauseState(final String state) { if (mPlayer == null || state == null) { return; } mPlayer.postDelayed(new Runnable() { @Override public void run() { if (state == KPlayerListener.PauseKey) { if (mLastSentEvent == KPlayerListener.PauseKey){ return; } if (mPlayer != null && (!mPlayer.isPlaying()) || mLastSentEvent == KPlayerListener.PlayKey) { mLastSentEvent = KPlayerListener.PauseKey; mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.PauseKey, null); return; } } else if (state == KPlayerListener.PlayKey) { if (mLastSentEvent == KPlayerListener.PlayKey){ return; } if (mPlayer != null && (mPlayer.isPlaying() || mLastSentEvent == KPlayerListener.PauseKey)) { LOGD(TAG, "changePlayPauseState SEND PLAYKEY"); mLastSentEvent = KPlayerListener.PlayKey; mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.PlayKey, null); return; } } else { LOGE(TAG, "Unsupported state " + state + " was used in changePlayPauseState"); return; } changePlayPauseState(state); } }, 100); } @Override public boolean isPlaying() { if (mPlayer != null) { return mPlayer.isPlaying(); } return false; } @Override public void switchToLive() { LOGW(TAG, "switchToLive is not implemented for Widevine Classic player"); } @Override public TrackFormat getTrackFormat(TrackType trackType, int index) { return null; } @Override public int getTrackCount(TrackType trackType) { return 0; } @Override public int getCurrentTrackIndex(TrackType trackType) { return -1; } @Override public void switchTrack(TrackType trackType, int newIndex) { } @Override public void attachSurfaceViewToPlayer() { // not required in case of multiplayer and WV classic } @Override public void detachSurfaceViewFromPlayer() { // not required in case of multiplayer and WV classic } @Override public void setPrepareWithConfigurationMode() { } @Override public void setPrepareWithConfigurationModeOff() { } public void savePosition() { if(mPlayer != null) { LOGD(TAG, "savePosition mPlayer.getCurrentPosition() = " + mPlayer.getCurrentPosition() + " but mCurrentPosition = " + mCurrentPosition); mSavedState.position = mCurrentPosition;//mPlayer.getCurrentPosition(); } } private void savePlayerState() { savePosition(); saveState(); pause(); } private void recoverPlayerState() { LOGD(TAG, "recoverPlayer mSavedState.position = " + mSavedState.position + " mCurrentPosition: " + mCurrentPosition + " getCurrentPlaybackTime() = " + getCurrentPlaybackTime()); if(getCurrentPlaybackTime() != mCurrentPosition) { LOGD(TAG, "recoverPlayer seekTo mCurrentPosition = " + mCurrentPosition); mPlayer.seekTo(mCurrentPosition); mShouldPlayWhenReady = mSavedState.playing; } else if (mSavedState.playing){ play(); } } @Override public void freezePlayer() { // if (mPlayer != null) { // savePlayerState(); // mPlayer.suspend(); // } } private void saveState() { if (mPlayer != null) { if (!mPlayer.isPlaying()) { mShouldPlayWhenReady = false; } LOGD(TAG, "saveState mPlayer.getCurrentPosition() = " + mPlayer.getCurrentPosition() + " mCurrentPosition = " + mCurrentPosition); mSavedState.set(mPlayer.isPlaying(), mCurrentPosition); if (mPlayer.getDuration() == mPlayer.getCurrentPosition()) { mSavedState.set(false, mPlayer.getCurrentPosition()); } } else { LOGD(TAG, "saveState mPlayer == null, position = 0"); mSavedState.set(false, 0); } } @Override public void removePlayer() { LOGD(TAG, "removePlayer"); savePosition(); saveState(); pause(); if (mPlayer != null) { mPlayer.stopPlayback(); removeView(mPlayer); mPlayer = null; } stopPlayheadTracker(); mPrepareState = PrepareState.NotPrepared; } private void stopPlayheadTracker() { if (mPlayheadTracker != null) { mCurrentPosition = (int) mPlayheadTracker.getPlaybackTime() * 1000; mPlayheadTracker.stop(); mPlayheadTracker = null; } } @Override public void recoverPlayer(boolean isPlaying) { LOGD(TAG, "recoverPlayer mSavedState.position = " + mSavedState.position + " mSavedState.playing: " + mSavedState.playing + " mCurrentPosition: " + mCurrentPosition); if (mPlayer != null) { LOGD(TAG, "inside recoverPlayer mCurrentPosition = " + mCurrentPosition + " isPlaying = " + isPlaying); mSavedState.set(mSavedState.playing, mCurrentPosition); mPlayer.resume(); mWasDestroyed = false; LOGD(TAG, "recover PLAY"); play(); LOGD(TAG, "recover SEEK"); setCurrentPlaybackTime(mCurrentPosition); if (!isPlaying) { LOGD(TAG, "recover PAUSE"); pause(); } } } @Override public void setShouldCancelPlay(boolean shouldCancelPlay) { mShouldCancelPlay = shouldCancelPlay; } private void preparePlayer() { if (mAssetUri==null || mLicenseUri==null) { String errMsg = "Prepare error: both assetUri and licenseUri must be set"; LOGE(TAG, errMsg); mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.ErrorKey, errMsg); return; } if (mPrepareState == PrepareState.Preparing) { LOGD(TAG, "Already preparing"); return; } String widevineUri = getWidevineAssetPlaybackUri(mAssetUri); // Start preparing. mPrepareState = PrepareState.Preparing; mPlayer = new VideoView(getContext()); LayoutParams lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.CENTER); this.addView(mPlayer, lp); mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() { @Override public void onCompletion(MediaPlayer mp) { mCallback.playerStateChanged(KPlayerCallback.ENDED); } }); mPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() { @Override public boolean onError(MediaPlayer mp, int what, int extra) { String errMsg = "VideoView:onError what = " + what; LOGE(TAG, errMsg); if (what == -38) { return true; } if(what == MediaPlayer.MEDIA_ERROR_SERVER_DIED || what == MediaPlayer.MEDIA_ERROR_UNKNOWN || what == MediaPlayer.MEDIA_ERROR_IO) { //mp.reset(); stopPlayheadTracker(); mWasDestroyed = true; mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.PauseKey, null); return true; } mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.ErrorKey, TAG + "-" + errMsg + "(" + what + "," + extra + ")"); return true; // prevents the VideoView error popups } }); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { mPlayer.setOnInfoListener(new MediaPlayer.OnInfoListener() { @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { LOGI(TAG, "onInfo(" + what + "," + extra + ")"); return false; } }); } mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { mPrepareState = PrepareState.Prepared; final KWVCPlayer kplayer = KWVCPlayer.this; mp.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() { @Override public void onSeekComplete(MediaPlayer mp) { LOGD(TAG, "XXX state = onSeekComplete " + mShouldPlayWhenReady + " mPrepareState = " + mPrepareState); if (mCurrentPosition == -1) { mCurrentPosition = mPlayer.getCurrentPosition(); return; } if (mPrepareState == PrepareState.Ended) { return; } if(mShouldPlayWhenReady){ mShouldPlayWhenReady = false; mSavedState.set(true, 0); play(); } else { saveState(); } mListener.eventWithValue(kplayer, KPlayerListener.SeekedKey, null); mCallback.playerStateChanged(KPlayerCallback.SEEKED); } }); // mp.setOnBufferingUpdateListener(new MediaPlayer.OnBufferingUpdateListener() { // @Override // public void onBufferingUpdate(MediaPlayer mp, int percent) { // if (!mWasDestroyed) { // int currPos = mp.getCurrentPosition(); // LOGD(TAG, "percent = " + percent + " " + currPos + "/" + mp.getDuration()); // //NO NEED TO SEND THIS EXTRA DATA ---- mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.BufferingChangeKey, (percent < 99 && currPos < mp.getDuration()) ? "true" : "false"); // } // } // }); if (mSavedState.playing) { LOGD(TAG, "mSavedState.playing = TRUE"); // we were already playing, so just resume playback from the saved position mShouldPlayWhenReady = true; long currentPBTime = getCurrentPlaybackTime(); if(currentPBTime != mSavedState.position) { //if we need seek first - play will be activate on seek complete LOGD(TAG, "inside setOnPreparedListener seekTo " + mSavedState.position + " but getCurrentPlaybackTime() = " + currentPBTime); mCurrentPosition = mSavedState.position; mPlayer.seekTo(mSavedState.position); } //mCallback.playerStateChanged(KPlayerCallback.SEEKED); if (mSavedState.playing) { LOGD(TAG, "SENDING PLAY KEY"); mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.PlayKey, null); } } else { if(!mShouldCancelPlay) { if (isFirstPreparation) { LOGD(TAG, "STARTING FIRST PLAY"); isFirstPreparation = false; mListener.eventWithValue(kplayer, KPlayerListener.DurationChangedKey, Float.toString(kplayer.getDuration() / 1000f)); mListener.eventWithValue(kplayer, KPlayerListener.LoadedMetaDataKey, ""); mListener.eventWithValue(kplayer, KPlayerListener.CanPlayKey, null); mCallback.playerStateChanged(KPlayerCallback.CAN_PLAY); } if (mShouldPlayWhenReady) { mShouldPlayWhenReady = false; play(); } } else { pause(); } } } }); mPlayer.getHolder().addCallback(new SurfaceHolder.Callback2() { @Override public void surfaceRedrawNeeded(SurfaceHolder holder) { LOGD(TAG, "surfaceRedrawNeeded"); } @Override public void surfaceCreated(SurfaceHolder holder) { LOGD(TAG, "surfaceCreated"); } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { LOGD(TAG, "surfaceChanged"); } @Override public void surfaceDestroyed(SurfaceHolder holder) { LOGD(TAG, "surfaceDestroyed"); mWasDestroyed = true; savePlayerState(); } }); mPlayer.setVideoURI(Uri.parse(widevineUri)); String assetAcquireUri = getWidevineAssetAcquireUri(mAssetUri); if(mDrmClient.needToAcquireRights(assetAcquireUri)) { mDrmClient.acquireRights(assetAcquireUri, mLicenseUri); } } private enum PrepareState { NotPrepared, Preparing, Prepared, Ended } private class PlayerState { boolean playing; int position; void set(boolean playing, int position) { this.playing = playing; this.position = position; } } class PlayheadTracker { Handler mHandler; float playbackTime; Runnable mRunnable = new Runnable() { @Override public void run() { try { if (mPlayer.getCurrentPosition() == mPlayer.getDuration()){ LOGD(TAG, "--- Video onCompletion ---"); mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.TimeUpdateKey, Float.toString(mPlayer.getDuration() / 1000f)); mSavedState.playing = false; mSavedState.position = mPlayer.getDuration(); mPlayer.pause(); mShouldPlayWhenReady = false; mLastSentEvent = KPlayerListener.EndedKey; mPlayer.seekTo(mPlayer.getDuration()); mPlayer.resume(); mCallback.playerStateChanged(KPlayerCallback.ENDED); mPrepareState = PrepareState.Ended; stopPlayheadTracker(); mPlayer.seekTo(mPlayer.getDuration()); savePlayerState(); mCurrentPosition = mPlayer.getDuration(); mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.TimeUpdateKey, Float.toString(mCurrentPosition/ 1000f)); return; } if (mPlayer != null && mPlayer.isPlaying()) { int currPos = mPlayer.getCurrentPosition(); LOGD(TAG, "progress status = " + currPos + "/" + mPlayer.getDuration()); if (currPos > mPlayer.getDuration()) { playbackTime = mPlayer.getDuration() / 1000f; } else { playbackTime = currPos / 1000f; mCurrentPosition = currPos; } mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.TimeUpdateKey, Float.toString(playbackTime)); } } catch (IllegalStateException e) { String errMsg = "Player Error "; LOGE(TAG, errMsg + e.getMessage()); mListener.eventWithValue(KWVCPlayer.this, KPlayerListener.ErrorKey, errMsg + e.getMessage()); } if (mHandler != null) { mHandler.postDelayed(this, PLAYHEAD_UPDATE_INTERVAL); } } }; float getPlaybackTime() { return playbackTime; } void start() { if (mHandler == null) { mHandler = new Handler(Looper.getMainLooper()); mHandler.postDelayed(mRunnable, PLAYHEAD_UPDATE_INTERVAL); } else { LOGD(TAG, "Tracker is already started"); } } void stop() { if (mHandler != null) { mHandler.removeCallbacks(mRunnable); mHandler = null; } else { LOGD(TAG, "Tracker is not started, nothing to stop"); } } } }