From d42e6dcbee24b917936c1d670ca659970c76d4d1 Mon Sep 17 00:00:00 2001 From: Likai Ding Date: Mon, 19 Aug 2013 16:33:24 +0800 Subject: Gallery2: support live streaming and bookmarks Users can input a URL for streaming display. URL bookmarking is supported. Change-Id: Ia69497cdcfee963ba2209119a5b9dc82b64497da --- .../android/gallery3d/app/ControllerOverlay.java | 2 + src/com/android/gallery3d/app/MovieActivity.java | 228 +++++- src/com/android/gallery3d/app/MoviePlayer.java | 782 ++++++++++++++++++++- src/com/android/gallery3d/app/TrimVideo.java | 6 + 4 files changed, 959 insertions(+), 59 deletions(-) mode change 100644 => 100755 src/com/android/gallery3d/app/MoviePlayer.java (limited to 'src/com/android') diff --git a/src/com/android/gallery3d/app/ControllerOverlay.java b/src/com/android/gallery3d/app/ControllerOverlay.java index 078f59e28..36eda6257 100644 --- a/src/com/android/gallery3d/app/ControllerOverlay.java +++ b/src/com/android/gallery3d/app/ControllerOverlay.java @@ -27,6 +27,8 @@ public interface ControllerOverlay { void onSeekEnd(int time, int trimStartTime, int trimEndTime); void onShown(); void onHidden(); + // get current video is from RTSP + boolean onIsRTSP(); void onReplay(); } diff --git a/src/com/android/gallery3d/app/MovieActivity.java b/src/com/android/gallery3d/app/MovieActivity.java index 937d309bc..6577fe271 100644 --- a/src/com/android/gallery3d/app/MovieActivity.java +++ b/src/com/android/gallery3d/app/MovieActivity.java @@ -20,6 +20,7 @@ import android.annotation.TargetApi; import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; +import android.app.KeyguardManager; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothDevice; import android.content.AsyncQueryHandler; @@ -41,6 +42,7 @@ import android.media.audiofx.BassBoost; import android.media.audiofx.Virtualizer; import android.media.MediaPlayer; import android.net.Uri; +import android.os.AsyncTask; import android.os.Build; import android.os.Bundle; import android.provider.MediaStore; @@ -66,6 +68,7 @@ import org.codeaurora.gallery3d.ext.IActivityHooker; import org.codeaurora.gallery3d.ext.MovieItem; import org.codeaurora.gallery3d.ext.IMovieItem; import org.codeaurora.gallery3d.video.ExtensionHelper; +import org.codeaurora.gallery3d.video.MovieTitleHelper; /** * This activity plays a video from a specified URI. @@ -77,8 +80,13 @@ import org.codeaurora.gallery3d.video.ExtensionHelper; public class MovieActivity extends Activity { @SuppressWarnings("unused") private static final String TAG = "MovieActivity"; + private static final boolean LOG = true; public static final String KEY_LOGO_BITMAP = "logo-bitmap"; public static final String KEY_TREAT_UP_AS_BACK = "treat-up-as-back"; + private static final String VIDEO_SDP_MIME_TYPE = "application/sdp"; + private static final String VIDEO_SDP_TITLE = "rtsp://"; + private static final String VIDEO_FILE_SCHEMA = "file"; + private static final String VIDEO_MIME_TYPE = "video/*"; private MoviePlayer mPlayer; private boolean mFinishOnCompletion; @@ -105,6 +113,9 @@ public class MovieActivity extends Activity { private IMovieItem mMovieItem; private IActivityHooker mMovieHooker; + private KeyguardManager mKeyguardManager; + private boolean mResumed = false; + private boolean mControlResumed = false; private final BroadcastReceiver mReceiver = new BroadcastReceiver() { @Override @@ -214,27 +225,8 @@ public class MovieActivity extends Activity { mPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() { @Override public void onPrepared(MediaPlayer mp) { - int sessionId = mp.getAudioSessionId(); - if (mBassBoostSupported) { - mBassBoostEffect = new BassBoost(0, sessionId); - } - if (mVirtualizerSupported) { - mVirtualizerEffect = new Virtualizer(0, sessionId); - } - if (mIsHeadsetOn) { - if (mPrefs.getBoolean(Key.global_enabled.toString(), false)) { - if (mBassBoostSupported) { - mBassBoostEffect.setStrength((short) - mPrefs.getInt(Key.bb_strength.toString(), 0)); - mBassBoostEffect.setEnabled(true); - } - if (mVirtualizerSupported) { - mVirtualizerEffect.setStrength((short) - mPrefs.getInt(Key.virt_strength.toString(), 0)); - mVirtualizerEffect.setEnabled(true); - } - } - } + mPlayer.onPrepared(mp); + initEffects(mp.getAudioSessionId()); } }); } @@ -413,10 +405,59 @@ public class MovieActivity extends Activity { } } + public void initEffects(int sessionId) { + // Singleton instance + if ((mBassBoostEffect == null) && mBassBoostSupported) { + mBassBoostEffect = new BassBoost(0, sessionId); + } + + if ((mVirtualizerEffect == null) && mVirtualizerSupported) { + mVirtualizerEffect = new Virtualizer(0, sessionId); + } + + if (mIsHeadsetOn) { + if (mPrefs.getBoolean(Key.global_enabled.toString(), false)) { + if (mBassBoostSupported) { + mBassBoostEffect.setStrength((short) + mPrefs.getInt(Key.bb_strength.toString(), 0)); + mBassBoostEffect.setEnabled(true); + } + if (mVirtualizerSupported) { + mVirtualizerEffect.setStrength((short) + mPrefs.getInt(Key.virt_strength.toString(), 0)); + mVirtualizerEffect.setEnabled(true); + } + } else { + if (mBassBoostSupported) { + mBassBoostEffect.setStrength((short) + mPrefs.getInt(Key.bb_strength.toString(), 0)); + } + if (mVirtualizerSupported) { + mVirtualizerEffect.setStrength((short) + mPrefs.getInt(Key.virt_strength.toString(), 0)); + } + } + } + + } + + public void releaseEffects() { + if (mBassBoostEffect != null) { + mBassBoostEffect.setEnabled(false); + mBassBoostEffect.release(); + mBassBoostEffect = null; + } + if (mVirtualizerEffect != null) { + mVirtualizerEffect.setEnabled(false); + mVirtualizerEffect.release(); + mVirtualizerEffect = null; + } + } + private Intent createShareIntent() { Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("video/*"); - intent.putExtra(Intent.EXTRA_STREAM, mUri); + intent.putExtra(Intent.EXTRA_STREAM, mMovieItem.getUri()); return intent; } @@ -450,6 +491,7 @@ public class MovieActivity extends Activity { AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); super.onStart(); mMovieHooker.onStart(); + registerScreenOff(); } @Override @@ -457,24 +499,36 @@ public class MovieActivity extends Activity { ((AudioManager) getSystemService(AUDIO_SERVICE)) .abandonAudioFocus(null); super.onStop(); + if (mControlResumed && mPlayer != null) { + mPlayer.onStop(); + mControlResumed = false; + } mMovieHooker.onStop(); + unregisterScreenOff(); } @Override public void onPause() { - mPlayer.onPause(); + // Audio track will be deallocated for local video playback, + // thus recycle effect here. + releaseEffects(); try { unregisterReceiver(mReceiver); } catch (IllegalArgumentException e) { // Do nothing } + mResumed = false; + if (mControlResumed && mPlayer != null) { + mControlResumed = !mPlayer.onPause(); + } super.onPause(); mMovieHooker.onPause(); } @Override public void onResume() { - mPlayer.onResume(); + invalidateOptionsMenu(); + if ((mVirtualizerSupported) || (mBassBoostSupported)) { final IntentFilter intentFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG); intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED); @@ -482,6 +536,14 @@ public class MovieActivity extends Activity { intentFilter.addAction(AudioManager.ACTION_AUDIO_BECOMING_NOISY); registerReceiver(mReceiver, intentFilter); } + + initEffects(mPlayer.getAudioSessionId()); + mResumed = true; + if (!isKeyguardLocked() && !mControlResumed && mPlayer != null) { + mPlayer.onResume(); + mControlResumed = true; + } + enhanceActionBar(); super.onResume(); mMovieHooker.onResume(); } @@ -507,6 +569,20 @@ public class MovieActivity extends Activity { mMovieHooker.onDestroy(); } + @Override + public void onWindowFocusChanged(boolean hasFocus) { + super.onWindowFocusChanged(hasFocus); + if (LOG) { + Log.v(TAG, "onWindowFocusChanged(" + hasFocus + ") isKeyguardLocked=" + + isKeyguardLocked() + + ", mResumed=" + mResumed + ", mControlResumed=" + mControlResumed); + } + if (hasFocus && !isKeyguardLocked() && mResumed && !mControlResumed && mPlayer != null) { + mPlayer.onResume(); + mControlResumed = true; + } + } + @Override public boolean onKeyDown(int keyCode, KeyEvent event) { return mPlayer.onKeyDown(keyCode, event) @@ -528,7 +604,109 @@ public class MovieActivity extends Activity { private void initMovieInfo(Intent intent) { Uri original = intent.getData(); String mimeType = intent.getType(); - mMovieItem = new MovieItem(original, mimeType, null); + if (VIDEO_SDP_MIME_TYPE.equalsIgnoreCase(mimeType) + && VIDEO_FILE_SCHEMA.equalsIgnoreCase(original.getScheme())) { + mMovieItem = new MovieItem(VIDEO_SDP_TITLE + original, mimeType, null); + } else { + mMovieItem = new MovieItem(original, mimeType, null); + } mMovieItem.setOriginalUri(original); } + + // we do not stop live streaming when other dialog overlays it. + private BroadcastReceiver mScreenOffReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (LOG) { + Log.v(TAG, "onReceive(" + intent.getAction() + ") mControlResumed=" + + mControlResumed); + } + if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { + // Only stop video. + if (mControlResumed) { + mPlayer.onStop(); + mControlResumed = false; + } + } + } + + }; + + private void registerScreenOff() { + IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + registerReceiver(mScreenOffReceiver, filter); + } + + private void unregisterScreenOff() { + unregisterReceiver(mScreenOffReceiver); + } + + private boolean isKeyguardLocked() { + if (mKeyguardManager == null) { + mKeyguardManager = (KeyguardManager) getSystemService(Context.KEYGUARD_SERVICE); + } + // isKeyguardSecure excludes the slide lock case. + boolean locked = (mKeyguardManager != null) + && mKeyguardManager.inKeyguardRestrictedInputMode(); + if (LOG) { + Log.v(TAG, "isKeyguardLocked() locked=" + locked + ", mKeyguardManager=" + + mKeyguardManager); + } + return locked; + } + + private void enhanceActionBar() { + final IMovieItem movieItem = mMovieItem;// remember original item + final Uri uri = mMovieItem.getUri(); + final String scheme = mMovieItem.getUri().getScheme(); + final String authority = mMovieItem.getUri().getAuthority(); + new AsyncTask() { + @Override + protected String doInBackground(Void... params) { + String title = null; + if (ContentResolver.SCHEME_FILE.equals(scheme)) { + title = MovieTitleHelper.getTitleFromMediaData(MovieActivity.this, uri); + } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)) { + title = MovieTitleHelper.getTitleFromDisplayName(MovieActivity.this, uri); + if (title == null) { + title = MovieTitleHelper.getTitleFromData(MovieActivity.this, uri); + } + } + if (title == null) { + title = MovieTitleHelper.getTitleFromUri(uri); + } + if (LOG) { + Log.v(TAG, "enhanceActionBar() task return " + title); + } + return title; + } + + @Override + protected void onPostExecute(String result) { + if (LOG) { + Log.v(TAG, "onPostExecute(" + result + ") movieItem=" + movieItem + + ", mMovieItem=" + mMovieItem); + } + movieItem.setTitle(result); + if (movieItem == mMovieItem) { + setActionBarTitle(result); + } + }; + }.execute(); + if (LOG) { + Log.v(TAG, "enhanceActionBar() " + mMovieItem); + } + } + + public void setActionBarTitle(String title) { + if (LOG) { + Log.v(TAG, "setActionBarTitle(" + title + ")"); + } + ActionBar actionBar = getActionBar(); + if (title != null) { + actionBar.setTitle(title); + } + } } diff --git a/src/com/android/gallery3d/app/MoviePlayer.java b/src/com/android/gallery3d/app/MoviePlayer.java old mode 100644 new mode 100755 index 09fd272ae..e632a826f --- a/src/com/android/gallery3d/app/MoviePlayer.java +++ b/src/com/android/gallery3d/app/MoviePlayer.java @@ -23,10 +23,14 @@ import android.content.Context; import android.content.DialogInterface; import android.content.DialogInterface.OnCancelListener; import android.content.DialogInterface.OnClickListener; +import android.content.DialogInterface.OnDismissListener; +import android.content.DialogInterface.OnShowListener; import android.content.Intent; import android.content.IntentFilter; +import android.graphics.Color; import android.media.AudioManager; import android.media.MediaPlayer; +import android.media.Metadata; import android.media.audiofx.AudioEffect; import android.media.audiofx.Virtualizer; import android.net.Uri; @@ -39,6 +43,7 @@ import android.view.SurfaceView; import android.view.View; import android.view.ViewGroup; import android.widget.VideoView; +import android.widget.Toast; import com.android.gallery3d.R; import com.android.gallery3d.common.ApiHelper; @@ -47,18 +52,27 @@ import com.android.gallery3d.util.CacheManager; import com.android.gallery3d.util.GalleryUtils; import org.codeaurora.gallery3d.ext.IMoviePlayer; import org.codeaurora.gallery3d.ext.IMovieItem; +import org.codeaurora.gallery3d.ext.MovieUtils; +import org.codeaurora.gallery3d.video.BookmarkEnhance; import org.codeaurora.gallery3d.video.ExtensionHelper; +import org.codeaurora.gallery3d.video.CodeauroraVideoView; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.DataInputStream; import java.io.DataOutputStream; +import java.util.HashMap; +import java.util.Map; public class MoviePlayer implements MediaPlayer.OnErrorListener, MediaPlayer.OnCompletionListener, - ControllerOverlay.Listener { + ControllerOverlay.Listener, + MediaPlayer.OnInfoListener, + MediaPlayer.OnPreparedListener, + MediaPlayer.OnSeekCompleteListener { @SuppressWarnings("unused") private static final String TAG = "MoviePlayer"; + private static final boolean LOG = false; private static final String KEY_VIDEO_POSITION = "video-position"; private static final String KEY_RESUMEABLE_TIME = "resumeable-timeout"; @@ -72,18 +86,30 @@ public class MoviePlayer implements private static final String CMDNAME = "command"; private static final String CMDPAUSE = "pause"; + private static final String KEY_VIDEO_CAN_SEEK = "video_can_seek"; + private static final String KEY_VIDEO_CAN_PAUSE = "video_can_pause"; + private static final String KEY_VIDEO_LAST_DURATION = "video_last_duration"; + private static final String KEY_VIDEO_LAST_DISCONNECT_TIME = "last_disconnect_time"; + private static final String KEY_VIDEO_STREAMING_TYPE = "video_streaming_type"; + private static final String KEY_VIDEO_STATE = "video_state"; + private static final String VIRTUALIZE_EXTRA = "virtualize"; private static final long BLACK_TIMEOUT = 500; + private static final int DELAY_REMOVE_MS = 10000; + public static final int SERVER_TIMEOUT = 8801; // If we resume the acitivty with in RESUMEABLE_TIMEOUT, we will keep playing. // Otherwise, we pause the player. private static final long RESUMEABLE_TIMEOUT = 3 * 60 * 1000; // 3 mins public static final int STREAMING_LOCAL = 0; + public static final int STREAMING_HTTP = 1; + public static final int STREAMING_RTSP = 2; + public static final int STREAMING_SDP = 3; private int mStreamingType = STREAMING_LOCAL; private Context mContext; - private final VideoView mVideoView; + private final CodeauroraVideoView mVideoView; private final View mRootView; private final Bookmarker mBookmarker; private final Uri mUri; @@ -94,6 +120,10 @@ public class MoviePlayer implements private long mResumeableTime = Long.MAX_VALUE; private int mVideoPosition = 0; private boolean mHasPaused = false; + private boolean mVideoHasPaused = false; + private boolean mCanResumed = false; + private boolean mFirstBePlayed = false; + private boolean mKeyguardLocked = false; private int mLastSystemUiVis = 0; // If the time bar is being dragged. @@ -104,10 +134,18 @@ public class MoviePlayer implements private Virtualizer mVirtualizer; + private MovieActivity mActivityContext;//for dialog and toast context private MoviePlayerExtension mPlayerExt = new MoviePlayerExtension(); + private RetryExtension mRetryExt = new RetryExtension(); + private ServerTimeoutExtension mServerTimeoutExt = new ServerTimeoutExtension(); private boolean mCanReplay; + private boolean mVideoCanSeek = false; + private boolean mVideoCanPause = false; + private boolean mWaitMetaData; + private boolean mIsShowResumingDialog; private TState mTState = TState.PLAYING; private IMovieItem mMovieItem; + private int mVideoLastDuration;//for duration displayed in init state private enum TState { PLAYING, @@ -141,11 +179,36 @@ public class MoviePlayer implements } }; + private Runnable mDelayVideoRunnable = new Runnable() { + @Override + public void run() { + if (LOG) { + Log.v(TAG, "mDelayVideoRunnable.run()"); + } + mVideoView.setVisibility(View.VISIBLE); + } + }; + + private final BroadcastReceiver mReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + if (Intent.ACTION_SCREEN_OFF.equals(intent.getAction())) { + mKeyguardLocked = true; + } else if (Intent.ACTION_USER_PRESENT.equals(intent.getAction())) { + if ((mCanResumed) && (!mVideoHasPaused)) { + playVideo(); + } + mKeyguardLocked = false; + mCanResumed = false; + } + } + }; + public MoviePlayer(View rootView, final MovieActivity movieActivity, IMovieItem info, Bundle savedInstance, boolean canReplay) { mContext = movieActivity.getApplicationContext(); mRootView = rootView; - mVideoView = (VideoView) rootView.findViewById(R.id.surface_view); + mVideoView = (CodeauroraVideoView) rootView.findViewById(R.id.surface_view); mBookmarker = new Bookmarker(movieActivity); mController = new MovieControllerOverlay(mContext); @@ -153,7 +216,7 @@ public class MoviePlayer implements mController.setListener(this); mController.setCanReplay(canReplay); - init(info, canReplay); + init(movieActivity, info, canReplay); mUri = mMovieItem.getUri(); mVideoView.setOnErrorListener(this); @@ -209,6 +272,12 @@ public class MoviePlayer implements mAudioBecomingNoisyReceiver = new AudioBecomingNoisyReceiver(); mAudioBecomingNoisyReceiver.register(); + // Listen for broadcasts related to user-presence + final IntentFilter filter = new IntentFilter(); + filter.addAction(Intent.ACTION_SCREEN_OFF); + filter.addAction(Intent.ACTION_USER_PRESENT); + mContext.registerReceiver(mReceiver, filter); + Intent i = new Intent(SERVICECMD); i.putExtra(CMDNAME, CMDPAUSE); movieActivity.sendBroadcast(i); @@ -221,11 +290,13 @@ public class MoviePlayer implements onRestoreInstanceState(savedInstance); mHasPaused = true; } else { - final Integer bookmark = mBookmarker.getBookmark(mUri); + mTState = TState.PLAYING; + mFirstBePlayed = true; + final BookmarkerInfo bookmark = mBookmarker.getBookmark(mMovieItem.getUri()); if (bookmark != null) { showResumeDialog(movieActivity, bookmark); } else { - startVideo(); + doStartVideo(false, 0, 0); } } } @@ -273,12 +344,12 @@ public class MoviePlayer implements onSaveInstanceStateMore(outState); } - private void showResumeDialog(Context context, final int bookmark) { + private void showResumeDialog(Context context, final BookmarkerInfo bookmark) { AlertDialog.Builder builder = new AlertDialog.Builder(context); builder.setTitle(R.string.resume_playing_title); builder.setMessage(String.format( context.getString(R.string.resume_playing_message), - GalleryUtils.formatDuration(context, bookmark / 1000))); + GalleryUtils.formatDuration(context, bookmark.mBookmark / 1000))); builder.setOnCancelListener(new OnCancelListener() { @Override public void onCancel(DialogInterface dialog) { @@ -289,42 +360,146 @@ public class MoviePlayer implements R.string.resume_playing_resume, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - mVideoView.seekTo(bookmark); - startVideo(); + // here try to seek for bookmark + mVideoCanSeek = true; + doStartVideo(true, bookmark.mBookmark, bookmark.mDuration); } }); builder.setNegativeButton( R.string.resume_playing_restart, new OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - startVideo(); + doStartVideo(true, 0, bookmark.mDuration); + } + }); + AlertDialog dialog = builder.create(); + dialog.setOnShowListener(new OnShowListener() { + @Override + public void onShow(DialogInterface arg0) { + mIsShowResumingDialog = true; } }); - builder.show(); + dialog.setOnDismissListener(new OnDismissListener() { + @Override + public void onDismiss(DialogInterface arg0) { + mIsShowResumingDialog = false; + } + }); + dialog.show(); } - public void onPause() { + public boolean onPause() { + if (LOG) { + Log.v(TAG, "onPause() isLiveStreaming()=" + isLiveStreaming()); + } + boolean pause = false; + if (isLiveStreaming()) { + pause = false; + } else { + doOnPause(); + pause = true; + } + if (LOG) { + Log.v(TAG, "onPause() , return " + pause); + } + return pause; + } + + // we should stop video anyway after this function called. + public void onStop() { + if (LOG) { + Log.v(TAG, "onStop() mHasPaused=" + mHasPaused); + } + if (!mHasPaused) { + doOnPause(); + } + } + + private void doOnPause() { + long start = System.currentTimeMillis(); + addBackground(); mHasPaused = true; mHandler.removeCallbacksAndMessages(null); - mVideoPosition = mVideoView.getCurrentPosition(); - mBookmarker.setBookmark(mUri, mVideoPosition, mVideoView.getDuration()); - mVideoView.suspend(); + int position = mVideoView.getCurrentPosition(); + mVideoPosition = position >= 0 ? position : mVideoPosition; + Log.v(TAG, "mVideoPosition is " + mVideoPosition); + int duration = mVideoView.getDuration(); + mVideoLastDuration = duration > 0 ? duration : mVideoLastDuration; + mBookmarker.setBookmark(mMovieItem.getUri(), mVideoPosition, mVideoLastDuration); + long end1 = System.currentTimeMillis(); + // change suspend to release for sync paused and killed case + mVideoView.stopPlayback(); mResumeableTime = System.currentTimeMillis() + RESUMEABLE_TIMEOUT; + mVideoView.setResumed(false);// avoid start after surface created + // Workaround for last-seek frame difference + mVideoView.setVisibility(View.INVISIBLE); + long end2 = System.currentTimeMillis(); + // TODO comments by sunlei + mServerTimeoutExt.recordDisconnectTime(); + if (LOG) { + Log.v(TAG, "doOnPause() save video info consume:" + (end1 - start)); + Log.v(TAG, "doOnPause() suspend video consume:" + (end2 - end1)); + Log.v(TAG, "doOnPause() mVideoPosition=" + mVideoPosition + ", mResumeableTime=" + + mResumeableTime + + ", mVideoLastDuration=" + mVideoLastDuration + ", mIsShowResumingDialog=" + + mIsShowResumingDialog); + } } public void onResume() { + mDragging = false;// clear drag info if (mHasPaused) { - mVideoView.seekTo(mVideoPosition); - mVideoView.resume(); + //M: same as launch case to delay transparent. + mVideoView.removeCallbacks(mDelayVideoRunnable); + mVideoView.postDelayed(mDelayVideoRunnable, BLACK_TIMEOUT); - // If we have slept for too long, pause the play - if (System.currentTimeMillis() > mResumeableTime) { - pauseVideo(); + if (mServerTimeoutExt.handleOnResume() || mIsShowResumingDialog) { + mHasPaused = false; + return; } + switch (mTState) { + case RETRY_ERROR: + mRetryExt.showRetry(); + break; + case STOPED: + mPlayerExt.stopVideo(); + break; + case COMPELTED: + mController.showEnded(); + if (mVideoCanSeek || mVideoView.canSeekForward()) { + mVideoView.seekTo(mVideoPosition); + } + mVideoView.setDuration(mVideoLastDuration); + break; + case PAUSED: + // if video was paused, so it should be started. + doStartVideo(true, mVideoPosition, mVideoLastDuration, false); + pauseVideo(); + break; + default: + doStartVideo(true, mVideoPosition, mVideoLastDuration); + pauseVideoMoreThanThreeMinutes(); + break; + } + mHasPaused = false; } mHandler.post(mProgressChecker); } + private void pauseVideoMoreThanThreeMinutes() { + // If we have slept for too long, pause the play + // If is live streaming, do not pause it too + long now = System.currentTimeMillis(); + if (now > mResumeableTime && !isLiveStreaming()) { + if (mVideoCanPause || mVideoView.canPause()) { + pauseVideo(); + } + } + if (LOG) { + Log.v(TAG, "pauseVideoMoreThanThreeMinutes() now=" + now); + } + } + public void onDestroy() { if (mVirtualizer != null) { mVirtualizer.release(); @@ -332,6 +507,8 @@ public class MoviePlayer implements } mVideoView.stopPlayback(); mAudioBecomingNoisyReceiver.unregister(); + mContext.unregisterReceiver(mReceiver); + mServerTimeoutExt.clearTimeoutDialog(); } // This updates the time bar display (if necessary). It is called every @@ -347,11 +524,13 @@ public class MoviePlayer implements return position; } - private void startVideo() { + private void doStartVideo(final boolean enableFasten, final int position, final int duration, + boolean start) { // For streams that we expect to be slow to start up, show a // progress spinner until playback starts. - String scheme = mUri.getScheme(); - if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme)) { + String scheme = mMovieItem.getUri().getScheme(); + if ("http".equalsIgnoreCase(scheme) || "rtsp".equalsIgnoreCase(scheme) + || "https".equalsIgnoreCase(scheme)) { mController.showLoading(); mHandler.removeCallbacks(mPlayingChecker); mHandler.postDelayed(mPlayingChecker, 250); @@ -360,22 +539,53 @@ public class MoviePlayer implements mController.hide(); } - mVideoView.start(); + if (onIsRTSP()) { + Map header = new HashMap(1); + header.put("CODEAURORA-ASYNC-RTSP-PAUSE-PLAY", "true"); + mVideoView.setVideoURI(mMovieItem.getUri(), header, !mWaitMetaData); + } else { + mVideoView.setVideoURI(mMovieItem.getUri(), null, !mWaitMetaData); + } + if (start) { + mVideoView.start(); + } //we may start video from stopVideo, //this case, we should reset canReplay flag according canReplay and loop boolean loop = mPlayerExt.getLoop(); boolean canReplay = loop ? loop : mCanReplay; mController.setCanReplay(canReplay); + if (position > 0 && (mVideoCanSeek || mVideoView.canSeekBackward() + || mVideoView.canSeekForward())) { + mVideoView.seekTo(position); + } + if (enableFasten) { + mVideoView.setDuration(duration); + } setProgress(); } + private void doStartVideo(boolean enableFasten, int position, int duration) { + ((AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE)).requestAudioFocus( + null, AudioManager.STREAM_MUSIC, + AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); + doStartVideo(enableFasten, position, duration, true); + } + private void playVideo() { + if (LOG) { + Log.v(TAG, "playVideo()"); + } + mTState = TState.PLAYING; mVideoView.start(); mController.showPlaying(); setProgress(); } private void pauseVideo() { + if (LOG) { + Log.v(TAG, "pauseVideo()"); + } + mTState = TState.PAUSED; mVideoView.pause(); mController.showPaused(); } @@ -383,6 +593,13 @@ public class MoviePlayer implements // Below are notifications from VideoView @Override public boolean onError(MediaPlayer player, int arg1, int arg2) { + mMovieItem.setError(); + if (mServerTimeoutExt.onError(player, arg1, arg2)) { + return true; + } + if (mRetryExt.onError(player, arg1, arg2)) { + return true; + } mHandler.removeCallbacksAndMessages(null); // VideoView will show an error dialog if we return false, so no need // to show more message. @@ -392,6 +609,14 @@ public class MoviePlayer implements @Override public void onCompletion(MediaPlayer mp) { + if (LOG) { + Log.v(TAG, "onCompletion() mCanReplay=" + mCanReplay); + } + if (mMovieItem.getError()) { + Log.w(TAG, "error occured, exit the video player!"); + mActivityContext.finish(); + return; + } if (mPlayerExt.getLoop()) { onReplay(); } else { //original logic @@ -433,6 +658,11 @@ public class MoviePlayer implements setProgress(); } + @Override + public void onSeekComplete(MediaPlayer mp) { + setProgress(); + } + @Override public void onShown() { mShowing = true; @@ -446,9 +676,58 @@ public class MoviePlayer implements showSystemUi(false); } + @Override + public boolean onInfo(MediaPlayer mp, int what, int extra) { + if (LOG) { + Log.v(TAG, "onInfo() what:" + what + " extra:" + extra); + } + if (what == MediaPlayer.MEDIA_INFO_METADATA_UPDATE && mServerTimeoutExt != null) { + Log.e(TAG, "setServerTimeout " + extra); + mServerTimeoutExt.setTimeout(extra * 1000); + } + if (mRetryExt.onInfo(mp, what, extra)) { + return true; + } + return false; + } + + @Override + public void onPrepared(MediaPlayer mp) { + if (LOG) { + Log.v(TAG, "onPrepared(" + mp + ")"); + } + getVideoInfo(mp); + boolean canPause = mVideoView.canPause(); + boolean canSeek = mVideoView.canSeekBackward() && mVideoView.canSeekForward(); + if (!canPause && !mVideoView.isTargetPlaying()) { + mVideoView.start(); + } + if (LOG) { + Log.v(TAG, "onPrepared() canPause=" + canPause + ", canSeek=" + canSeek); + } + } + + public boolean onIsRTSP() { + if (MovieUtils.isRtspStreaming(mMovieItem.getUri(), mMovieItem + .getMimeType())) { + Log.v(TAG, "onIsRTSP() is RTSP"); + return true; + } + Log.v(TAG, "onIsRTSP() is not RTSP"); + return false; + } + @Override public void onReplay() { - startVideo(); + if (LOG) { + Log.v(TAG, "onReplay()"); + } + mTState = TState.PLAYING; + mFirstBePlayed = true; + if (mRetryExt.handleOnReplay()) { + return; + } + doStartVideo(false, 0, 0); } // Below are key events passed from MovieActivity. @@ -462,14 +741,14 @@ public class MoviePlayer implements switch (keyCode) { case KeyEvent.KEYCODE_HEADSETHOOK: case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: - if (mVideoView.isPlaying()) { + if (mVideoView.isPlaying() && mVideoView.canPause()) { pauseVideo(); } else { playVideo(); } return true; case KEYCODE_MEDIA_PAUSE: - if (mVideoView.isPlaying()) { + if (mVideoView.isPlaying() && mVideoView.canPause()) { pauseVideo(); } return true; @@ -500,9 +779,14 @@ public class MoviePlayer implements || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE; } - private void init(IMovieItem info, boolean canReplay) { + private void init(MovieActivity movieActivity, IMovieItem info, boolean canReplay) { + mActivityContext = movieActivity; mCanReplay = canReplay; mMovieItem = info; + judgeStreamingType(info.getUri(), info.getMimeType()); + + mVideoView.setOnInfoListener(this); + mVideoView.setOnPreparedListener(this); } // We want to pause when the headset is unplugged. @@ -531,6 +815,13 @@ public class MoviePlayer implements mVideoView.setOnPreparedListener(listener); } + public boolean isFullBuffer() { + if (mStreamingType == STREAMING_RTSP || mStreamingType == STREAMING_SDP) { + return false; + } + return true; + } + public boolean isLocalFile() { if (mStreamingType == STREAMING_LOCAL) { return true; @@ -538,6 +829,58 @@ public class MoviePlayer implements return false; } + private void getVideoInfo(MediaPlayer mp) { + if (!MovieUtils.isLocalFile(mMovieItem.getUri(), mMovieItem.getMimeType())) { + Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL, + MediaPlayer.BYPASS_METADATA_FILTER); + if (data != null) { + // TODO comments by sunlei + mServerTimeoutExt.setVideoInfo(data); + } else { + Log.w(TAG, "Metadata is null!"); + } + } + } + + private void judgeStreamingType(Uri uri, String mimeType) { + if (LOG) { + Log.v(TAG, "judgeStreamingType(" + uri + ")"); + } + if (uri == null) { + return; + } + String scheme = uri.getScheme(); + mWaitMetaData = true; + if (MovieUtils.isSdpStreaming(uri, mimeType)) { + mStreamingType = STREAMING_SDP; + mWaitMetaData = false; + } else if (MovieUtils.isRtspStreaming(uri, mimeType)) { + mStreamingType = STREAMING_RTSP; + mWaitMetaData = false; + } else if (MovieUtils.isHttpStreaming(uri, mimeType)) { + mStreamingType = STREAMING_HTTP; + mWaitMetaData = false; + } else { + mStreamingType = STREAMING_LOCAL; + mWaitMetaData = false; + } + if (LOG) { + Log.v(TAG, "mStreamingType=" + mStreamingType + + " mCanGetMetaData=" + mWaitMetaData); + } + } + + public boolean isLiveStreaming() { + boolean isLive = false; + if (mStreamingType == STREAMING_SDP) { + isLive = true; + } + if (LOG) { + Log.v(TAG, "isLiveStreaming() return " + isLive); + } + return isLive; + } + public IMoviePlayer getMoviePlayerExt() { return mPlayerExt; } @@ -546,13 +889,62 @@ public class MoviePlayer implements return mVideoView; } - private void onSaveInstanceStateMore(Bundle outState) { + // Wait for any animation, ten seconds should be enough + private final Runnable mRemoveBackground = new Runnable() { + @Override + public void run() { + if (LOG) { + Log.v(TAG, "mRemoveBackground.run()"); + } + mRootView.setBackground(null); + } + }; + + private void removeBackground() { + if (LOG) { + Log.v(TAG, "removeBackground()"); + } + mHandler.removeCallbacks(mRemoveBackground); + mHandler.postDelayed(mRemoveBackground, DELAY_REMOVE_MS); + } + // add background for removing ghost image. + private void addBackground() { + if (LOG) { + Log.v(TAG, "addBackground()"); + } + mHandler.removeCallbacks(mRemoveBackground); + mRootView.setBackgroundColor(Color.BLACK); + } + + private void clearVideoInfo() { + mVideoPosition = 0; + mVideoLastDuration = 0; + + if (mServerTimeoutExt != null) { + mServerTimeoutExt.clearServerInfo(); + } + } + + private void onSaveInstanceStateMore(Bundle outState) { + outState.putInt(KEY_VIDEO_LAST_DURATION, mVideoLastDuration); + outState.putBoolean(KEY_VIDEO_CAN_SEEK, mVideoView.canSeekForward()); + outState.putBoolean(KEY_VIDEO_CAN_PAUSE, mVideoView.canPause()); + outState.putInt(KEY_VIDEO_STREAMING_TYPE, mStreamingType); + outState.putString(KEY_VIDEO_STATE, String.valueOf(mTState)); + mServerTimeoutExt.onSaveInstanceState(outState); + mRetryExt.onSaveInstanceState(outState); mPlayerExt.onSaveInstanceState(outState); } private void onRestoreInstanceState(Bundle icicle) { - + mVideoLastDuration = icicle.getInt(KEY_VIDEO_LAST_DURATION); + mVideoCanSeek = icicle.getBoolean(KEY_VIDEO_CAN_SEEK); + mVideoCanPause = icicle.getBoolean(KEY_VIDEO_CAN_PAUSE); + mStreamingType = icicle.getInt(KEY_VIDEO_STREAMING_TYPE); + mTState = TState.valueOf(icicle.getString(KEY_VIDEO_STATE)); + mServerTimeoutExt.onRestoreInstanceState(icicle); + mRetryExt.onRestoreInstanceState(icicle); mPlayerExt.onRestoreInstanceState(icicle); } @@ -560,7 +952,10 @@ public class MoviePlayer implements private static final String KEY_VIDEO_IS_LOOP = "video_is_loop"; + private BookmarkEnhance mBookmark;//for bookmark private boolean mIsLoop; + private boolean mLastPlaying; + private boolean mLastCanPaused; @Override public boolean getLoop() { @@ -587,7 +982,301 @@ public class MoviePlayer implements public void onSaveInstanceState(Bundle outState) { outState.putBoolean(KEY_VIDEO_IS_LOOP, mIsLoop); } + + @Override + public void stopVideo() { + if (LOG) { + Log.v(TAG, "stopVideo()"); + } + mTState = TState.STOPED; + mVideoView.clearSeek(); + mVideoView.clearDuration(); + mVideoView.stopPlayback(); + mVideoView.setResumed(false); + mVideoView.setVisibility(View.INVISIBLE); + mVideoView.setVisibility(View.VISIBLE); + clearVideoInfo(); + mFirstBePlayed = false; + mController.setCanReplay(true); + mController.showEnded(); + setProgress(); + } + + @Override + public boolean canStop() { + boolean stopped = false; + if (mController != null) { + //stopped = mOverlayExt.isPlayingEnd(); + } + if (LOG) { + Log.v(TAG, "canStop() stopped=" + stopped); + } + return !stopped; + } + + @Override + public void addBookmark() { + if (mBookmark == null) { + mBookmark = new BookmarkEnhance(mActivityContext); + } + String uri = String.valueOf(mMovieItem.getUri()); + if (mBookmark.exists(uri)) { + Toast.makeText(mActivityContext, R.string.bookmark_exist, Toast.LENGTH_SHORT) + .show(); + } else { + mBookmark.insert(mMovieItem.getTitle(), uri, + mMovieItem.getMimeType(), 0); + Toast.makeText(mActivityContext, R.string.bookmark_add_success, Toast.LENGTH_SHORT) + .show(); + } + } }; + + private class RetryExtension implements Restorable, MediaPlayer.OnErrorListener, + MediaPlayer.OnInfoListener { + private static final String KEY_VIDEO_RETRY_COUNT = "video_retry_count"; + private int mRetryDuration; + private int mRetryPosition; + private int mRetryCount; + + public void retry() { + doStartVideo(true, mRetryPosition, mRetryDuration); + if (LOG) { + Log.v(TAG, "retry() mRetryCount=" + mRetryCount + ", mRetryPosition=" + + mRetryPosition); + } + } + + public void clearRetry() { + if (LOG) { + Log.v(TAG, "clearRetry() mRetryCount=" + mRetryCount); + } + mRetryCount = 0; + } + + public boolean reachRetryCount() { + if (LOG) { + Log.v(TAG, "reachRetryCount() mRetryCount=" + mRetryCount); + } + if (mRetryCount > 3) { + return true; + } + return false; + } + + public int getRetryCount() { + if (LOG) { + Log.v(TAG, "getRetryCount() return " + mRetryCount); + } + return mRetryCount; + } + + public boolean isRetrying() { + boolean retry = false; + if (mRetryCount > 0) { + retry = true; + } + if (LOG) { + Log.v(TAG, "isRetrying() mRetryCount=" + mRetryCount); + } + return retry; + } + + @Override + public void onRestoreInstanceState(Bundle icicle) { + mRetryCount = icicle.getInt(KEY_VIDEO_RETRY_COUNT); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putInt(KEY_VIDEO_RETRY_COUNT, mRetryCount); + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + return false; + } + + @Override + public boolean onInfo(MediaPlayer mp, int what, int extra) { + return false; + } + + public boolean handleOnReplay() { + if (isRetrying()) { // from connecting error + clearRetry(); + int errorPosition = mVideoView.getCurrentPosition(); + int errorDuration = mVideoView.getDuration(); + doStartVideo(errorPosition > 0, errorPosition, errorDuration); + if (LOG) { + Log.v(TAG, "onReplay() errorPosition=" + errorPosition + ", errorDuration=" + + errorDuration); + } + return true; + } + return false; + } + + public void showRetry() { + if (mVideoCanSeek || mVideoView.canSeekForward()) { + mVideoView.seekTo(mVideoPosition); + } + mVideoView.setDuration(mVideoLastDuration); + mRetryPosition = mVideoPosition; + mRetryDuration = mVideoLastDuration; + } + } + private class ServerTimeoutExtension implements Restorable, MediaPlayer.OnErrorListener { + // for cmcc server timeout case + // please remember to clear this value when changed video. + private int mServerTimeout = -1; + private long mLastDisconnectTime; + private boolean mIsShowDialog = false; + private AlertDialog mServerTimeoutDialog; + + // check whether disconnect from server timeout or not. + // if timeout, return false. otherwise, return true. + private boolean passDisconnectCheck() { + if (!isFullBuffer()) { + // record the time disconnect from server + long now = System.currentTimeMillis(); + if (LOG) { + Log.v(TAG, "passDisconnectCheck() now=" + now + ", mLastDisconnectTime=" + + mLastDisconnectTime + + ", mServerTimeout=" + mServerTimeout); + } + if (mServerTimeout > 0 && (now - mLastDisconnectTime) > mServerTimeout) { + // disconnect time more than server timeout, notify user + notifyServerTimeout(); + return false; + } + } + return true; + } + + private void recordDisconnectTime() { + if (!isFullBuffer()) { + // record the time disconnect from server + mLastDisconnectTime = System.currentTimeMillis(); + } + if (LOG) { + Log.v(TAG, "recordDisconnectTime() mLastDisconnectTime=" + mLastDisconnectTime); + } + } + + private void clearServerInfo() { + mServerTimeout = -1; + } + + private void notifyServerTimeout() { + if (mServerTimeoutDialog == null) { + // for updating last position and duration. + if (mVideoCanSeek || mVideoView.canSeekForward()) { + mVideoView.seekTo(mVideoPosition); + } + mVideoView.setDuration(mVideoLastDuration); + AlertDialog.Builder builder = new AlertDialog.Builder(mActivityContext); + mServerTimeoutDialog = builder.setTitle(R.string.server_timeout_title) + .setMessage(R.string.server_timeout_message) + .setNegativeButton(android.R.string.cancel, new OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + if (LOG) { + Log.v(TAG, "NegativeButton.onClick() mIsShowDialog=" + + mIsShowDialog); + } + mController.showEnded(); + onCompletion(); + } + + }) + .setPositiveButton(R.string.resume_playing_resume, new OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + if (LOG) { + Log.v(TAG, "PositiveButton.onClick() mIsShowDialog=" + + mIsShowDialog); + } + doStartVideo(true, mVideoPosition, mVideoLastDuration); + } + + }) + .create(); + mServerTimeoutDialog.setOnDismissListener(new OnDismissListener() { + + public void onDismiss(DialogInterface dialog) { + if (LOG) { + Log.v(TAG, "mServerTimeoutDialog.onDismiss()"); + } + mIsShowDialog = false; + } + + }); + mServerTimeoutDialog.setOnShowListener(new OnShowListener() { + + public void onShow(DialogInterface dialog) { + if (LOG) { + Log.v(TAG, "mServerTimeoutDialog.onShow()"); + } + mIsShowDialog = true; + } + + }); + } + mServerTimeoutDialog.show(); + } + + private void clearTimeoutDialog() { + if (mServerTimeoutDialog != null && mServerTimeoutDialog.isShowing()) { + mServerTimeoutDialog.dismiss(); + } + mServerTimeoutDialog = null; + } + + @Override + public void onRestoreInstanceState(Bundle icicle) { + mLastDisconnectTime = icicle.getLong(KEY_VIDEO_LAST_DISCONNECT_TIME); + } + + @Override + public void onSaveInstanceState(Bundle outState) { + outState.putLong(KEY_VIDEO_LAST_DISCONNECT_TIME, mLastDisconnectTime); + } + + public boolean handleOnResume() { + if (mIsShowDialog && !isLiveStreaming()) { + // wait for user's operation + return true; + } + if (!passDisconnectCheck()) { + return true; + } + return false; + } + + public void setVideoInfo(Metadata data) { + if (data.has(SERVER_TIMEOUT)) { + mServerTimeout = data.getInt(SERVER_TIMEOUT); + if (LOG) { + Log.v(TAG, "get server timeout from metadata. mServerTimeout=" + + mServerTimeout); + } + } + } + + @Override + public boolean onError(MediaPlayer mp, int what, int extra) { + // if we are showing a dialog, cancel the error dialog + if (mIsShowDialog) { + return true; + } + return false; + } + + public void setTimeout(int timeout) { + mServerTimeout = timeout; + } + } } class Bookmarker { @@ -609,6 +1298,10 @@ class Bookmarker { public void setBookmark(Uri uri, int bookmark, int duration) { try { + // do not record or override bookmark if duration is not valid. + if (duration <= 0) { + return; + } BlobCache cache = CacheManager.getCache(mContext, BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, BOOKMARK_CACHE_MAX_BYTES, BOOKMARK_CACHE_VERSION); @@ -617,7 +1310,7 @@ class Bookmarker { DataOutputStream dos = new DataOutputStream(bos); dos.writeUTF(uri.toString()); dos.writeInt(bookmark); - dos.writeInt(duration); + dos.writeInt(Math.abs(duration)); dos.flush(); cache.insert(uri.hashCode(), bos.toByteArray()); } catch (Throwable t) { @@ -625,7 +1318,7 @@ class Bookmarker { } } - public Integer getBookmark(Uri uri) { + public BookmarkerInfo getBookmark(Uri uri) { try { BlobCache cache = CacheManager.getCache(mContext, BOOKMARK_CACHE_FILE, BOOKMARK_CACHE_MAX_ENTRIES, @@ -649,10 +1342,31 @@ class Bookmarker { || (bookmark > (duration - HALF_MINUTE))) { return null; } - return Integer.valueOf(bookmark); + return new BookmarkerInfo(bookmark, duration); } catch (Throwable t) { Log.w(TAG, "getBookmark failed", t); } return null; } } + +class BookmarkerInfo { + public final int mBookmark; + public final int mDuration; + + public BookmarkerInfo(int bookmark, int duration) { + this.mBookmark = bookmark; + this.mDuration = duration; + } + + @Override + public String toString() { + return new StringBuilder() + .append("BookmarkInfo(bookmark=") + .append(mBookmark) + .append(", duration=") + .append(mDuration) + .append(")") + .toString(); + } +} diff --git a/src/com/android/gallery3d/app/TrimVideo.java b/src/com/android/gallery3d/app/TrimVideo.java index a1fd26c4a..c5d721274 100644 --- a/src/com/android/gallery3d/app/TrimVideo.java +++ b/src/com/android/gallery3d/app/TrimVideo.java @@ -341,6 +341,12 @@ public class TrimVideo extends Activity implements public void onHidden() { } + @Override + public boolean onIsRTSP() { + // TODO Auto-generated method stub + return false; + } + @Override public void onReplay() { mVideoView.seekTo(mTrimStartTime); -- cgit v1.2.3