package org.edx.mobile.player; import android.graphics.Point; import android.graphics.SurfaceTexture; import android.media.AudioManager; import android.media.MediaPlayer; import android.media.MediaPlayer.OnBufferingUpdateListener; import android.media.MediaPlayer.OnCompletionListener; import android.media.MediaPlayer.OnErrorListener; import android.media.MediaPlayer.OnInfoListener; import android.media.MediaPlayer.OnPreparedListener; import android.view.Surface; import android.view.TextureView; import android.view.View.OnClickListener; import org.edx.mobile.logger.Logger; import org.edx.mobile.view.OnSwipeListener; import java.io.File; import java.io.FileInputStream; import java.util.Locale; @SuppressWarnings("serial") public class Player extends MediaPlayer implements OnErrorListener, OnPreparedListener, OnBufferingUpdateListener, OnCompletionListener, OnInfoListener, IPlayer { // Player states public enum PlayerState { RESET, URI_SET, PREPARED, PLAYING, PAUSED, ERROR, LAGGING, PLAYBACK_COMPLETE, STOPPED } private PlayerState state; private int bufferPercent; private boolean isSeekable; private boolean isFullScreen; private boolean isPlayingLocally; private boolean playWhenPrepared; private boolean autoHideControls; private transient IPlayerListener callback; private transient PlayerController controller; private int lastCurrentPosition; private int lastFreezePosition; private int lastDuration; private boolean isFrozen; private PlayerState freezeState; private int seekToWhenPrepared; private String videoTitle; private String lmsURL; private String videoUri; private static final Logger logger = new Logger(Player.class.getName()); public Player() { init(); setOnErrorListener(this); setOnPreparedListener(this); setOnBufferingUpdateListener(this); setOnCompletionListener(this); setLooping(true); setOnInfoListener(this); } /** * Resets all the fields of this player. */ private void init() { state = PlayerState.RESET; bufferPercent = 0; isSeekable = true; isFullScreen = false; isPlayingLocally = true; playWhenPrepared = false; lastCurrentPosition = 0; lastDuration = 0; isFrozen = false; autoHideControls = true; } @Override public void onCompletion(MediaPlayer mp) { // sometimes, error also causes onCompletion() call // avoid on completion if player got an error if (state != PlayerState.ERROR) { state = PlayerState.PLAYBACK_COMPLETE; if (callback != null) { callback.onPlaybackComplete(); } seekTo(0); logger.debug("Playback complete"); } } @Override public void onBufferingUpdate(MediaPlayer mp, int percent) { bufferPercent = percent; } @Override public int getBufferPercentage() { return bufferPercent; } @Override public boolean canPause() { if (state == PlayerState.PLAYING || state == PlayerState.PREPARED || state == PlayerState.PAUSED || state == PlayerState.PLAYBACK_COMPLETE || state == PlayerState.LAGGING) { logger.debug("Can pause = TRUE"); return true; } logger.debug("Can pause = FALSE"); return false; } @Override public boolean canSeekBackward() { if ((state == PlayerState.PLAYING || state == PlayerState.PREPARED || state == PlayerState.STOPPED || state == PlayerState.PAUSED || state == PlayerState.LAGGING) && isSeekable) { logger.debug("Can seek back = TRUE"); return true; } logger.debug("Can seek back = FALSE"); return false; } @Override public boolean canSeekForward() { if ((state == PlayerState.PLAYING || state == PlayerState.PREPARED || state == PlayerState.STOPPED || state == PlayerState.PAUSED || state == PlayerState.LAGGING) && isSeekable) { logger.debug("Can seek forward = TRUE"); return true; } logger.debug("Can seek forward = FALSE"); return false; } @Override public boolean isFullScreen() { return isFullScreen; } @Override public void toggleFullScreen() { setFullScreen(!isFullScreen); if (callback != null) { callback.onFullScreen(isFullScreen); } } @Override public void callLMSServer(String lmsUrl) { if (callback != null) { callback.callLMSServer(lmsUrl); } } @Override public void callSettings(Point p) { if (callback != null) { callback.callSettings(p); } } @Override public void onPrepared(MediaPlayer mp) { state = PlayerState.PREPARED; if (callback != null) { callback.onPrepared(); } if (seekToWhenPrepared >= 0) { seekTo(seekToWhenPrepared); } if (playWhenPrepared) { start(); state = PlayerState.PLAYING; } } @Override public boolean onInfo(MediaPlayer mp, int what, int extra) { if (what == MediaPlayer.MEDIA_INFO_NOT_SEEKABLE) { isSeekable = false; if (callback != null) { callback.onVideoNotSeekable(); } logger.debug("Track not seekable"); } else if (what == MediaPlayer.MEDIA_INFO_METADATA_UPDATE) { logger.debug("Metadata update received"); } else if (what == MediaPlayer.MEDIA_INFO_UNKNOWN) { logger.debug("Unknown info"); } else if (what == MediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING) { state = PlayerState.LAGGING; if (callback != null) { callback.onVideoLagging(); } logger.debug("Video track lagging"); } logger.debug("INFO: what=" + what + ";extra=" + extra); return true; } @Override public boolean onError(MediaPlayer player, int what, int extra) { if (lastCurrentPosition != 0) { seekToWhenPrepared = lastCurrentPosition; } state = PlayerState.ERROR; if (callback != null) { callback.onError(); } if (what == MediaPlayer.MEDIA_ERROR_UNKNOWN) { logger.warn("ERROR: unknown"); } else if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) { logger.warn("ERROR: server died"); } else if (what == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) { logger.warn("ERROR: video container invalid for progressive playback"); } logger.warn("ERROR: what=" + what + ";extra=" + extra); // return TRUE here, so that onCompletionListener will NOT be called return true; } @Override public void setUri(String uri, int seekTo) throws Exception { load(uri, seekTo, false); } @Override public void setUriAndPlay(String uri, int seekTo) throws Exception { load(uri, seekTo, true); } @Override public void restart(int seekTo) throws Exception { logger.debug("RestartFreezePosition=" + seekTo); lastCurrentPosition = 0; // if seekTo=lastCurrentPosition then seekTo() method will not work load(videoUri, seekTo, playWhenPrepared); } @Override public void restart() throws Exception { restart(seekToWhenPrepared); } private void load(String videoUri, int seekTo, boolean playWhenPrepared) throws Exception { this.videoUri = videoUri; this.seekToWhenPrepared = seekTo; this.playWhenPrepared = playWhenPrepared; this.isFrozen = false; this.lastCurrentPosition = 0; // reset last seek position // re-display controller, so that it shows latest data if (this.controller != null) { this.controller.hide(); } reset(); state = PlayerState.RESET; bufferPercent = 0; setAudioStreamType(AudioManager.STREAM_MUSIC); if (videoUri != null) { if (videoUri.startsWith("http")) { // this is web URL setDataSource(videoUri); state = PlayerState.URI_SET; isPlayingLocally = false; } else { // this is file path FileInputStream fs = new FileInputStream(new File(videoUri)); setDataSource(fs.getFD()); fs.close(); state = PlayerState.URI_SET; isPlayingLocally = true; } prepareAsync(); // notify that the player is now preparing if (callback != null) { callback.onPreparing(); } } } @Override public boolean isPlayingLocally() { return isPlayingLocally; } @Override public boolean isInError() { return state == PlayerState.ERROR; } @Override public boolean isReset() { return state == PlayerState.RESET; } @Override public void setFullScreen(boolean isFullScreen) { this.isFullScreen = isFullScreen; if (controller != null) { controller.setTopBarVisibility(isFullScreen); } } @Override public void setPreview(final Preview preview) { if (preview == null) { return; } preview.setSurfaceTextureListener(new TextureView.SurfaceTextureListener() { @Override public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) { try { logger.debug("Player state=" + state); Surface surface = new Surface(surfaceTexture); setSurface(surface); // Keep screen ON while playing // if using SurfaceHolder, just call setScreenOnWhilePlaying(true); // When not using SurfaceHolder, need to use // WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON logger.debug("Surface created and set to the player"); // preview last shown frame if not playing if (!isPlaying()) { seekTo(lastCurrentPosition); } } catch (Exception ex) { logger.error(ex); } } @Override public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) { } @Override public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) { return true; } @Override public void onSurfaceTextureUpdated(SurfaceTexture surface) { } }); preview.setOnTouchListener(new OnSwipeListener(preview.getContext()) { @Override public void onSwipeLeft() { super.onSwipeLeft(); if (controller != null) { controller.playNext(); } } @Override public void onSwipeRight() { super.onSwipeRight(); if (controller != null) { controller.playPrevious(); } } @Override public void onClick() { super.onClick(); if (controller != null && state != PlayerState.RESET && state != PlayerState.URI_SET) { logger.debug("Player touched"); if (controller.isShowing() && autoHideControls) { controller.hide(); } else { controller.setLmsUrl(lmsURL); controller.setTitle(videoTitle); controller.show(); } } } }); } @Override public void setPlayerListener(IPlayerListener listener) { this.callback = listener; } @Override public void setController(PlayerController cont) { // handle old controller object first if (controller == null && this.controller != null) { this.controller.setMediaPlayer(null); } if (this.controller != null) { // hide old controller while setting new this.controller.hide(); this.controller = null; } // now handle new controller object this.controller = cont; if (this.controller != null) { this.controller.setMediaPlayer(this); logger.debug("Controller set"); } } @Override /** * Controls will be hidden immediately if changed to false */ public void setAutoHideControls(boolean autoHide) { autoHideControls = autoHide; if (!autoHide && autoHideControls) { hideController(); } } @Override public boolean getAutoHideControls() { return autoHideControls; } @Override public void showController() { if (controller != null) { controller.hide(); controller.setTitle(videoTitle); controller.setLmsUrl(lmsURL); if (autoHideControls) { controller.resetShowTimeoutMS(); } else { controller.setShowTimeoutMS(0); } controller.show(); logger.debug("Player controller shown"); } } @Override public void hideController() { if (controller != null) { controller.hide(); } } @Override public void requestAccessibilityFocusPausePlay() { if (controller != null && controller.isShowing()) { controller.requestAccessibilityFocusPausePlay(); } } @Override public void freeze() { if (isFrozen) { // keep this method one-shot return; } isFrozen = true; freezeState = state; setPreview(null); setController(null); if (isPlaying()) { pause(); } if (state == PlayerState.URI_SET && playWhenPrepared) { playWhenPrepared = false; } lastCurrentPosition = getCurrentPosition(); lastFreezePosition = lastCurrentPosition; if (lastCurrentPosition > 0) { seekToWhenPrepared = lastCurrentPosition; } logger.debug("FreezePosition=" + lastFreezePosition); } @Override public void unfreeze() { if (isFrozen) { logger.debug("unFreezePosition=" + lastFreezePosition); lastCurrentPosition = getCurrentPosition(); seekTo(lastFreezePosition); if (freezeState == PlayerState.PLAYING || freezeState == PlayerState.LAGGING) { start(); } else if (freezeState == PlayerState.URI_SET) { if (state == PlayerState.PREPARED) { // start playing as player is already prepared start(); } else { playWhenPrepared = true; if (callback != null) { callback.onPreparing(); } } } else if (freezeState == PlayerState.PAUSED) { pause(); } } isFrozen = false; } @Override public void setVideoTitle(String title) { this.videoTitle = title; } public int getLastFreezePosition() { return lastFreezePosition; } // ------------------------------------------------------------------- /* * Player Methods below. */ @Override public void reset() { super.reset(); state = PlayerState.RESET; } @Override public synchronized void start() throws IllegalStateException { if (state == PlayerState.PREPARED || state == PlayerState.PAUSED || state == PlayerState.STOPPED || state == PlayerState.LAGGING || state == PlayerState.PLAYBACK_COMPLETE) { super.start(); if (callback != null) { // mark playing callback.onPlaybackStarted(); } state = PlayerState.PLAYING; logger.debug("Playback started"); // reload controller showController(); requestAccessibilityFocusPausePlay(); } else { logger.warn("Cannot start"); } } @Override public void stop() throws IllegalStateException { if (state == PlayerState.PREPARED || state == PlayerState.PLAYING || state == PlayerState.PAUSED || state == PlayerState.LAGGING || state == PlayerState.PLAYBACK_COMPLETE) { super.stop(); state = PlayerState.STOPPED; logger.debug("Playback stopped"); } else { logger.warn("Playback cannot stop"); } } @Override public void pause() throws IllegalStateException { if (isPlaying()) { super.pause(); state = PlayerState.PAUSED; if (callback != null) { callback.onPlaybackPaused(); } logger.debug("Playback paused"); } else { logger.warn("Playback scannot pause"); } } @Override public synchronized void seekTo(int msec) throws IllegalStateException { int delta = lastCurrentPosition - msec; if (delta < 0) { delta = delta * (-1); } if (msec > 0 && lastCurrentPosition > 0 && (delta <= 1000)) { // no need to perform seek if current position is almost same as seekTo // Delta of 1000 is used to skip seek of 1 sec difference from current position logger.debug(String.format(Locale.US, "Skipping seek to %d from %d ; state=%s", msec, lastCurrentPosition, state.toString())); return; } if (msec < 0) { // cannot seek to invalid position msec = 0; } if (state == PlayerState.PREPARED || state == PlayerState.PLAYING || state == PlayerState.PAUSED || state == PlayerState.STOPPED || state == PlayerState.PLAYBACK_COMPLETE || state == PlayerState.LAGGING) { logger.debug(String.format(Locale.US, "seeking to %d from %d ; state=%s", msec, lastCurrentPosition, state.toString())); super.seekTo(msec); lastCurrentPosition = msec; logger.debug("playback seeked"); try { // wait for a while, so that Player gets into a stable state after seek Thread.sleep(10); } catch (InterruptedException e) { logger.error(e); } } else { logger.warn("Cannot seek"); } } @Override public synchronized int getCurrentPosition() { try { if (state == PlayerState.PREPARED || state == PlayerState.PLAYING || state == PlayerState.PAUSED || state == PlayerState.STOPPED || state == PlayerState.PLAYBACK_COMPLETE || state == PlayerState.LAGGING) { lastCurrentPosition = super.getCurrentPosition(); } } catch (Exception ex) { logger.error(ex); } return lastCurrentPosition; } @Override public int getDuration() { try { if (state == PlayerState.PREPARED || state == PlayerState.PLAYING || state == PlayerState.PAUSED || state == PlayerState.STOPPED || state == PlayerState.PLAYBACK_COMPLETE || state == PlayerState.LAGGING) { lastDuration = super.getDuration(); } } catch (Exception ex) { logger.error(ex); } return lastDuration; } @Override public boolean isPlaying() { if (state == PlayerState.PLAYING || state == PlayerState.LAGGING) { logger.debug("isPlaying = TRUE"); return super.isPlaying(); } logger.debug("isPlaying = FALSE; state=" + state); return false; } @Override public boolean isPaused() { if (state == PlayerState.PAUSED) { logger.debug("isPaused = TRUE"); return true; } logger.debug("isPaused = FALSE; state=" + state); return false; } @Override public boolean isFrozen() { return isFrozen; } @Override public void setLMSUrl(String url) { this.lmsURL = url; } @Override public void setNextPreviousListeners(OnClickListener next, OnClickListener prev) { if (controller != null) { controller.setNextPreviousListeners(next, prev); } } @Override public PlayerController getController() { return controller; } @Override public void callPlayerSeeked(long previousPos, long nextPos, boolean isRewindClicked) { if (callback != null) { callback.callPlayerSeeked(previousPos, nextPos, isRewindClicked); } } }