• Home
  • History
  • Annotate
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 package org.chromium.content.browser;
6 
7 import android.app.Activity;
8 import android.app.AlertDialog;
9 import android.content.Context;
10 import android.content.ContextWrapper;
11 import android.content.DialogInterface;
12 import android.graphics.Point;
13 import android.provider.Settings;
14 import android.util.Log;
15 import android.view.Display;
16 import android.view.Gravity;
17 import android.view.KeyEvent;
18 import android.view.Surface;
19 import android.view.SurfaceHolder;
20 import android.view.SurfaceView;
21 import android.view.View;
22 import android.view.ViewGroup;
23 import android.view.WindowManager;
24 import android.widget.FrameLayout;
25 import android.widget.LinearLayout;
26 import android.widget.ProgressBar;
27 import android.widget.TextView;
28 
29 import org.chromium.base.CalledByNative;
30 import org.chromium.base.JNINamespace;
31 import org.chromium.base.ThreadUtils;
32 import org.chromium.ui.base.ViewAndroid;
33 import org.chromium.ui.base.ViewAndroidDelegate;
34 import org.chromium.ui.base.WindowAndroid;
35 
36 /**
37  * This class implements accelerated fullscreen video playback using surface view.
38  */
39 @JNINamespace("content")
40 public class ContentVideoView extends FrameLayout
41         implements SurfaceHolder.Callback, ViewAndroidDelegate {
42 
43     private static final String TAG = "ContentVideoView";
44 
45     /* Do not change these values without updating their counterparts
46      * in include/media/mediaplayer.h!
47      */
48     private static final int MEDIA_NOP = 0; // interface test message
49     private static final int MEDIA_PREPARED = 1;
50     private static final int MEDIA_PLAYBACK_COMPLETE = 2;
51     private static final int MEDIA_BUFFERING_UPDATE = 3;
52     private static final int MEDIA_SEEK_COMPLETE = 4;
53     private static final int MEDIA_SET_VIDEO_SIZE = 5;
54     private static final int MEDIA_ERROR = 100;
55     private static final int MEDIA_INFO = 200;
56 
57     /**
58      * Keep these error codes in sync with the code we defined in
59      * MediaPlayerListener.java.
60      */
61     public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2;
62     public static final int MEDIA_ERROR_INVALID_CODE = 3;
63 
64     // all possible internal states
65     private static final int STATE_ERROR              = -1;
66     private static final int STATE_IDLE               = 0;
67     private static final int STATE_PLAYING            = 1;
68     private static final int STATE_PAUSED             = 2;
69     private static final int STATE_PLAYBACK_COMPLETED = 3;
70 
71     private SurfaceHolder mSurfaceHolder;
72     private int mVideoWidth;
73     private int mVideoHeight;
74     private int mDuration;
75 
76     // Native pointer to C++ ContentVideoView object.
77     private long mNativeContentVideoView;
78 
79     // webkit should have prepared the media
80     private int mCurrentState = STATE_IDLE;
81 
82     // Strings for displaying media player errors
83     private String mPlaybackErrorText;
84     private String mUnknownErrorText;
85     private String mErrorButton;
86     private String mErrorTitle;
87     private String mVideoLoadingText;
88 
89     // This view will contain the video.
90     private VideoSurfaceView mVideoSurfaceView;
91 
92     // Progress view when the video is loading.
93     private View mProgressView;
94 
95     // The ViewAndroid is used to keep screen on during video playback.
96     private ViewAndroid mViewAndroid;
97 
98     private final ContentVideoViewClient mClient;
99 
100     private boolean mInitialOrientation;
101     private boolean mPossibleAccidentalChange;
102     private boolean mUmaRecorded;
103     private long mOrientationChangedTime;
104     private long mPlaybackStartTime;
105 
106     private class VideoSurfaceView extends SurfaceView {
107 
VideoSurfaceView(Context context)108         public VideoSurfaceView(Context context) {
109             super(context);
110         }
111 
112         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)113         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
114             // set the default surface view size to (1, 1) so that it won't block
115             // the infobar. (0, 0) is not a valid size for surface view.
116             int width = 1;
117             int height = 1;
118             if (mVideoWidth > 0 && mVideoHeight > 0) {
119                 width = getDefaultSize(mVideoWidth, widthMeasureSpec);
120                 height = getDefaultSize(mVideoHeight, heightMeasureSpec);
121                 if (mVideoWidth * height  > width * mVideoHeight) {
122                     height = width * mVideoHeight / mVideoWidth;
123                 } else if (mVideoWidth * height  < width * mVideoHeight) {
124                     width = height * mVideoWidth / mVideoHeight;
125                 }
126             }
127             if (mUmaRecorded) {
128                 // If we have never switched orientation, record the orientation
129                 // time.
130                 if (mPlaybackStartTime == mOrientationChangedTime) {
131                    if (isOrientationPortrait() != mInitialOrientation) {
132                        mOrientationChangedTime = System.currentTimeMillis();
133                    }
134                 } else {
135                    // if user quickly switched the orientation back and force, don't
136                    // count it in UMA.
137                    if (!mPossibleAccidentalChange &&
138                            isOrientationPortrait() == mInitialOrientation &&
139                            System.currentTimeMillis() - mOrientationChangedTime < 5000) {
140                        mPossibleAccidentalChange = true;
141                    }
142                 }
143             }
144             setMeasuredDimension(width, height);
145         }
146     }
147 
148     private static class ProgressView extends LinearLayout {
149 
150         private final ProgressBar mProgressBar;
151         private final TextView mTextView;
152 
ProgressView(Context context, String videoLoadingText)153         public ProgressView(Context context, String videoLoadingText) {
154             super(context);
155             setOrientation(LinearLayout.VERTICAL);
156             setLayoutParams(new LinearLayout.LayoutParams(
157                     LinearLayout.LayoutParams.WRAP_CONTENT,
158                     LinearLayout.LayoutParams.WRAP_CONTENT));
159             mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
160             mTextView = new TextView(context);
161             mTextView.setText(videoLoadingText);
162             addView(mProgressBar);
163             addView(mTextView);
164         }
165     }
166 
167     private final Runnable mExitFullscreenRunnable = new Runnable() {
168         @Override
169         public void run() {
170             exitFullscreen(true);
171         }
172     };
173 
ContentVideoView(Context context, long nativeContentVideoView, ContentVideoViewClient client)174     protected ContentVideoView(Context context, long nativeContentVideoView,
175             ContentVideoViewClient client) {
176         super(context);
177         mNativeContentVideoView = nativeContentVideoView;
178         mViewAndroid = new ViewAndroid(new WindowAndroid(context.getApplicationContext()), this);
179         mClient = client;
180         mUmaRecorded = false;
181         mPossibleAccidentalChange = false;
182         initResources(context);
183         mVideoSurfaceView = new VideoSurfaceView(context);
184         showContentVideoView();
185         setVisibility(View.VISIBLE);
186     }
187 
getContentVideoViewClient()188     protected ContentVideoViewClient getContentVideoViewClient() {
189         return mClient;
190     }
191 
initResources(Context context)192     private void initResources(Context context) {
193         if (mPlaybackErrorText != null) return;
194         mPlaybackErrorText = context.getString(
195                 org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
196         mUnknownErrorText = context.getString(
197                 org.chromium.content.R.string.media_player_error_text_unknown);
198         mErrorButton = context.getString(
199                 org.chromium.content.R.string.media_player_error_button);
200         mErrorTitle = context.getString(
201                 org.chromium.content.R.string.media_player_error_title);
202         mVideoLoadingText = context.getString(
203                 org.chromium.content.R.string.media_player_loading_video);
204     }
205 
showContentVideoView()206     protected void showContentVideoView() {
207         mVideoSurfaceView.getHolder().addCallback(this);
208         this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
209                 ViewGroup.LayoutParams.WRAP_CONTENT,
210                 ViewGroup.LayoutParams.WRAP_CONTENT,
211                 Gravity.CENTER));
212 
213         mProgressView = mClient.getVideoLoadingProgressView();
214         if (mProgressView == null) {
215             mProgressView = new ProgressView(getContext(), mVideoLoadingText);
216         }
217         this.addView(mProgressView, new FrameLayout.LayoutParams(
218                 ViewGroup.LayoutParams.WRAP_CONTENT,
219                 ViewGroup.LayoutParams.WRAP_CONTENT,
220                 Gravity.CENTER));
221     }
222 
getSurfaceView()223     protected SurfaceView getSurfaceView() {
224         return mVideoSurfaceView;
225     }
226 
227     @CalledByNative
onMediaPlayerError(int errorType)228     public void onMediaPlayerError(int errorType) {
229         Log.d(TAG, "OnMediaPlayerError: " + errorType);
230         if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
231             return;
232         }
233 
234         // Ignore some invalid error codes.
235         if (errorType == MEDIA_ERROR_INVALID_CODE) {
236             return;
237         }
238 
239         mCurrentState = STATE_ERROR;
240 
241         if (!isActivityContext(getContext())) {
242             Log.w(TAG, "Unable to show alert dialog because it requires an activity context");
243             return;
244         }
245 
246         /* Pop up an error dialog so the user knows that
247          * something bad has happened. Only try and pop up the dialog
248          * if we're attached to a window. When we're going away and no
249          * longer have a window, don't bother showing the user an error.
250          *
251          * TODO(qinmin): We need to review whether this Dialog is OK with
252          * the rest of the browser UI elements.
253          */
254         if (getWindowToken() != null) {
255             String message;
256 
257             if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
258                 message = mPlaybackErrorText;
259             } else {
260                 message = mUnknownErrorText;
261             }
262 
263             try {
264                 new AlertDialog.Builder(getContext())
265                     .setTitle(mErrorTitle)
266                     .setMessage(message)
267                     .setPositiveButton(mErrorButton,
268                             new DialogInterface.OnClickListener() {
269                         @Override
270                         public void onClick(DialogInterface dialog, int whichButton) {
271                             /* Inform that the video is over.
272                              */
273                             onCompletion();
274                         }
275                     })
276                     .setCancelable(false)
277                     .show();
278             } catch (RuntimeException e) {
279                 Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e);
280             }
281         }
282     }
283 
284     @CalledByNative
onVideoSizeChanged(int width, int height)285     private void onVideoSizeChanged(int width, int height) {
286         mVideoWidth = width;
287         mVideoHeight = height;
288         // This will trigger the SurfaceView.onMeasure() call.
289         mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
290     }
291 
292     @CalledByNative
onBufferingUpdate(int percent)293     protected void onBufferingUpdate(int percent) {
294     }
295 
296     @CalledByNative
onPlaybackComplete()297     private void onPlaybackComplete() {
298         onCompletion();
299     }
300 
301     @CalledByNative
onUpdateMediaMetadata( int videoWidth, int videoHeight, int duration, boolean canPause, boolean canSeekBack, boolean canSeekForward)302     protected void onUpdateMediaMetadata(
303             int videoWidth,
304             int videoHeight,
305             int duration,
306             boolean canPause,
307             boolean canSeekBack,
308             boolean canSeekForward) {
309         mDuration = duration;
310         mProgressView.setVisibility(View.GONE);
311         mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
312         onVideoSizeChanged(videoWidth, videoHeight);
313         if (mUmaRecorded) return;
314         try {
315             if (Settings.System.getInt(getContext().getContentResolver(),
316                     Settings.System.ACCELEROMETER_ROTATION) == 0) {
317                 return;
318             }
319         } catch (Settings.SettingNotFoundException e) {
320             return;
321         }
322         mInitialOrientation = isOrientationPortrait();
323         mUmaRecorded = true;
324         mPlaybackStartTime = System.currentTimeMillis();
325         mOrientationChangedTime = mPlaybackStartTime;
326         nativeRecordFullscreenPlayback(
327                 mNativeContentVideoView, videoHeight > videoWidth, mInitialOrientation);
328     }
329 
330     @Override
surfaceChanged(SurfaceHolder holder, int format, int width, int height)331     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
332     }
333 
334     @Override
surfaceCreated(SurfaceHolder holder)335     public void surfaceCreated(SurfaceHolder holder) {
336         mSurfaceHolder = holder;
337         openVideo();
338     }
339 
340     @Override
surfaceDestroyed(SurfaceHolder holder)341     public void surfaceDestroyed(SurfaceHolder holder) {
342         if (mNativeContentVideoView != 0) {
343             nativeSetSurface(mNativeContentVideoView, null);
344         }
345         mSurfaceHolder = null;
346         post(mExitFullscreenRunnable);
347     }
348 
349     @CalledByNative
openVideo()350     protected void openVideo() {
351         if (mSurfaceHolder != null) {
352             mCurrentState = STATE_IDLE;
353             if (mNativeContentVideoView != 0) {
354                 nativeRequestMediaMetadata(mNativeContentVideoView);
355                 nativeSetSurface(mNativeContentVideoView,
356                         mSurfaceHolder.getSurface());
357             }
358         }
359     }
360 
onCompletion()361     protected void onCompletion() {
362         mCurrentState = STATE_PLAYBACK_COMPLETED;
363     }
364 
365 
isInPlaybackState()366     protected boolean isInPlaybackState() {
367         return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
368     }
369 
start()370     protected void start() {
371         if (isInPlaybackState()) {
372             if (mNativeContentVideoView != 0) {
373                 nativePlay(mNativeContentVideoView);
374             }
375             mCurrentState = STATE_PLAYING;
376         }
377     }
378 
pause()379     protected void pause() {
380         if (isInPlaybackState()) {
381             if (isPlaying()) {
382                 if (mNativeContentVideoView != 0) {
383                     nativePause(mNativeContentVideoView);
384                 }
385                 mCurrentState = STATE_PAUSED;
386             }
387         }
388     }
389 
390     // cache duration as mDuration for faster access
getDuration()391     protected int getDuration() {
392         if (isInPlaybackState()) {
393             if (mDuration > 0) {
394                 return mDuration;
395             }
396             if (mNativeContentVideoView != 0) {
397                 mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
398             } else {
399                 mDuration = 0;
400             }
401             return mDuration;
402         }
403         mDuration = -1;
404         return mDuration;
405     }
406 
getCurrentPosition()407     protected int getCurrentPosition() {
408         if (isInPlaybackState() && mNativeContentVideoView != 0) {
409             return nativeGetCurrentPosition(mNativeContentVideoView);
410         }
411         return 0;
412     }
413 
seekTo(int msec)414     protected void seekTo(int msec) {
415         if (mNativeContentVideoView != 0) {
416             nativeSeekTo(mNativeContentVideoView, msec);
417         }
418     }
419 
isPlaying()420     public boolean isPlaying() {
421         return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
422     }
423 
424     @CalledByNative
createContentVideoView( Context context, long nativeContentVideoView, ContentVideoViewClient client)425     private static ContentVideoView createContentVideoView(
426             Context context, long nativeContentVideoView, ContentVideoViewClient client) {
427         ThreadUtils.assertOnUiThread();
428         ContentVideoView videoView = new ContentVideoView(context, nativeContentVideoView, client);
429         if (videoView.getContentVideoViewClient().onShowCustomView(videoView)) {
430             return videoView;
431         }
432         return null;
433     }
434 
isActivityContext(Context context)435     private static boolean isActivityContext(Context context) {
436         // Only retrieve the base context if the supplied context is a ContextWrapper but not
437         // an Activity, given that Activity is already a subclass of ContextWrapper.
438         if (context instanceof ContextWrapper && !(context instanceof Activity)) {
439             context = ((ContextWrapper) context).getBaseContext();
440             return isActivityContext(context);
441         }
442         return context instanceof Activity;
443     }
444 
removeSurfaceView()445     public void removeSurfaceView() {
446         removeView(mVideoSurfaceView);
447         removeView(mProgressView);
448         mVideoSurfaceView = null;
449         mProgressView = null;
450     }
451 
exitFullscreen(boolean relaseMediaPlayer)452     public void exitFullscreen(boolean relaseMediaPlayer) {
453         destroyContentVideoView(false);
454         if (mNativeContentVideoView != 0) {
455             if (mUmaRecorded && !mPossibleAccidentalChange) {
456                 long currentTime = System.currentTimeMillis();
457                 long timeBeforeOrientationChange = mOrientationChangedTime - mPlaybackStartTime;
458                 long timeAfterOrientationChange = currentTime - mOrientationChangedTime;
459                 if (timeBeforeOrientationChange == 0) {
460                     timeBeforeOrientationChange = timeAfterOrientationChange;
461                     timeAfterOrientationChange = 0;
462                 }
463                 nativeRecordExitFullscreenPlayback(mNativeContentVideoView, mInitialOrientation,
464                         timeBeforeOrientationChange, timeAfterOrientationChange);
465             }
466             nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
467             mNativeContentVideoView = 0;
468         }
469     }
470 
471     @CalledByNative
onExitFullscreen()472     private void onExitFullscreen() {
473         exitFullscreen(false);
474     }
475 
476     /**
477      * This method shall only be called by native and exitFullscreen,
478      * To exit fullscreen, use exitFullscreen in Java.
479      */
480     @CalledByNative
destroyContentVideoView(boolean nativeViewDestroyed)481     protected void destroyContentVideoView(boolean nativeViewDestroyed) {
482         if (mVideoSurfaceView != null) {
483             removeSurfaceView();
484             setVisibility(View.GONE);
485 
486             // To prevent re-entrance, call this after removeSurfaceView.
487             mClient.onDestroyContentVideoView();
488         }
489         if (nativeViewDestroyed) {
490             mNativeContentVideoView = 0;
491         }
492     }
493 
getContentVideoView()494     public static ContentVideoView getContentVideoView() {
495         return nativeGetSingletonJavaContentVideoView();
496     }
497 
498     @Override
onKeyUp(int keyCode, KeyEvent event)499     public boolean onKeyUp(int keyCode, KeyEvent event) {
500         if (keyCode == KeyEvent.KEYCODE_BACK) {
501             exitFullscreen(false);
502             return true;
503         }
504         return super.onKeyUp(keyCode, event);
505     }
506 
507     @Override
acquireAnchorView()508     public View acquireAnchorView() {
509         View anchorView = new View(getContext());
510         addView(anchorView);
511         return anchorView;
512     }
513 
514     @Override
setAnchorViewPosition(View view, float x, float y, float width, float height)515     public void setAnchorViewPosition(View view, float x, float y, float width, float height) {
516         Log.e(TAG, "setAnchorViewPosition isn't implemented");
517     }
518 
519     @Override
releaseAnchorView(View anchorView)520     public void releaseAnchorView(View anchorView) {
521         removeView(anchorView);
522     }
523 
524     @CalledByNative
getNativeViewAndroid()525     private long getNativeViewAndroid() {
526         return mViewAndroid.getNativePointer();
527     }
528 
isOrientationPortrait()529     private boolean isOrientationPortrait() {
530         Context context = getContext();
531         WindowManager manager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
532         Display display = manager.getDefaultDisplay();
533         Point outputSize = new Point(0, 0);
534         display.getSize(outputSize);
535         return outputSize.x <= outputSize.y;
536     }
537 
nativeGetSingletonJavaContentVideoView()538     private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
nativeExitFullscreen(long nativeContentVideoView, boolean relaseMediaPlayer)539     private native void nativeExitFullscreen(long nativeContentVideoView,
540             boolean relaseMediaPlayer);
nativeGetCurrentPosition(long nativeContentVideoView)541     private native int nativeGetCurrentPosition(long nativeContentVideoView);
nativeGetDurationInMilliSeconds(long nativeContentVideoView)542     private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView);
nativeRequestMediaMetadata(long nativeContentVideoView)543     private native void nativeRequestMediaMetadata(long nativeContentVideoView);
nativeGetVideoWidth(long nativeContentVideoView)544     private native int nativeGetVideoWidth(long nativeContentVideoView);
nativeGetVideoHeight(long nativeContentVideoView)545     private native int nativeGetVideoHeight(long nativeContentVideoView);
nativeIsPlaying(long nativeContentVideoView)546     private native boolean nativeIsPlaying(long nativeContentVideoView);
nativePause(long nativeContentVideoView)547     private native void nativePause(long nativeContentVideoView);
nativePlay(long nativeContentVideoView)548     private native void nativePlay(long nativeContentVideoView);
nativeSeekTo(long nativeContentVideoView, int msec)549     private native void nativeSeekTo(long nativeContentVideoView, int msec);
nativeSetSurface(long nativeContentVideoView, Surface surface)550     private native void nativeSetSurface(long nativeContentVideoView, Surface surface);
nativeRecordFullscreenPlayback( long nativeContentVideoView, boolean isVideoPortrait, boolean isOrientationPortrait)551     private native void nativeRecordFullscreenPlayback(
552             long nativeContentVideoView, boolean isVideoPortrait, boolean isOrientationPortrait);
nativeRecordExitFullscreenPlayback( long nativeContentVideoView, boolean isOrientationPortrait, long playbackDurationBeforeOrientationChange, long playbackDurationAfterOrientationChange)553     private native void nativeRecordExitFullscreenPlayback(
554             long nativeContentVideoView, boolean isOrientationPortrait,
555             long playbackDurationBeforeOrientationChange,
556             long playbackDurationAfterOrientationChange);
557 }
558