Home | History | Annotate | Download | only in browser
      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 
    108         public VideoSurfaceView(Context context) {
    109             super(context);
    110         }
    111 
    112         @Override
    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 
    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 
    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 
    188     protected ContentVideoViewClient getContentVideoViewClient() {
    189         return mClient;
    190     }
    191 
    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 
    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 
    223     protected SurfaceView getSurfaceView() {
    224         return mVideoSurfaceView;
    225     }
    226 
    227     @CalledByNative
    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
    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
    293     protected void onBufferingUpdate(int percent) {
    294     }
    295 
    296     @CalledByNative
    297     private void onPlaybackComplete() {
    298         onCompletion();
    299     }
    300 
    301     @CalledByNative
    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
    331     public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    332     }
    333 
    334     @Override
    335     public void surfaceCreated(SurfaceHolder holder) {
    336         mSurfaceHolder = holder;
    337         openVideo();
    338     }
    339 
    340     @Override
    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
    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 
    361     protected void onCompletion() {
    362         mCurrentState = STATE_PLAYBACK_COMPLETED;
    363     }
    364 
    365 
    366     protected boolean isInPlaybackState() {
    367         return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
    368     }
    369 
    370     protected void start() {
    371         if (isInPlaybackState()) {
    372             if (mNativeContentVideoView != 0) {
    373                 nativePlay(mNativeContentVideoView);
    374             }
    375             mCurrentState = STATE_PLAYING;
    376         }
    377     }
    378 
    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
    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 
    407     protected int getCurrentPosition() {
    408         if (isInPlaybackState() && mNativeContentVideoView != 0) {
    409             return nativeGetCurrentPosition(mNativeContentVideoView);
    410         }
    411         return 0;
    412     }
    413 
    414     protected void seekTo(int msec) {
    415         if (mNativeContentVideoView != 0) {
    416             nativeSeekTo(mNativeContentVideoView, msec);
    417         }
    418     }
    419 
    420     public boolean isPlaying() {
    421         return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
    422     }
    423 
    424     @CalledByNative
    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 
    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 
    445     public void removeSurfaceView() {
    446         removeView(mVideoSurfaceView);
    447         removeView(mProgressView);
    448         mVideoSurfaceView = null;
    449         mProgressView = null;
    450     }
    451 
    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
    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
    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 
    494     public static ContentVideoView getContentVideoView() {
    495         return nativeGetSingletonJavaContentVideoView();
    496     }
    497 
    498     @Override
    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
    508     public View acquireAnchorView() {
    509         View anchorView = new View(getContext());
    510         addView(anchorView);
    511         return anchorView;
    512     }
    513 
    514     @Override
    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
    520     public void releaseAnchorView(View anchorView) {
    521         removeView(anchorView);
    522     }
    523 
    524     @CalledByNative
    525     private long getNativeViewAndroid() {
    526         return mViewAndroid.getNativePointer();
    527     }
    528 
    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 
    538     private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
    539     private native void nativeExitFullscreen(long nativeContentVideoView,
    540             boolean relaseMediaPlayer);
    541     private native int nativeGetCurrentPosition(long nativeContentVideoView);
    542     private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView);
    543     private native void nativeRequestMediaMetadata(long nativeContentVideoView);
    544     private native int nativeGetVideoWidth(long nativeContentVideoView);
    545     private native int nativeGetVideoHeight(long nativeContentVideoView);
    546     private native boolean nativeIsPlaying(long nativeContentVideoView);
    547     private native void nativePause(long nativeContentVideoView);
    548     private native void nativePlay(long nativeContentVideoView);
    549     private native void nativeSeekTo(long nativeContentVideoView, int msec);
    550     private native void nativeSetSurface(long nativeContentVideoView, Surface surface);
    551     private native void nativeRecordFullscreenPlayback(
    552             long nativeContentVideoView, boolean isVideoPortrait, boolean isOrientationPortrait);
    553     private native void nativeRecordExitFullscreenPlayback(
    554             long nativeContentVideoView, boolean isOrientationPortrait,
    555             long playbackDurationBeforeOrientationChange,
    556             long playbackDurationAfterOrientationChange);
    557 }
    558