root/content/public/android/java/src/org/chromium/content/browser/ContentVideoView.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. JNINamespace
  2. onMeasure
  3. initResources
  4. showContentVideoView
  5. getSurfaceView
  6. onMediaPlayerError
  7. onVideoSizeChanged
  8. onBufferingUpdate
  9. onPlaybackComplete
  10. onUpdateMediaMetadata
  11. surfaceChanged
  12. surfaceCreated
  13. surfaceDestroyed
  14. openVideo
  15. onCompletion
  16. isInPlaybackState
  17. start
  18. pause
  19. getDuration
  20. getCurrentPosition
  21. seekTo
  22. isPlaying
  23. createContentVideoView
  24. removeSurfaceView
  25. exitFullscreen
  26. onExitFullscreen
  27. destroyContentVideoView
  28. getContentVideoView
  29. onKeyUp
  30. acquireAnchorView
  31. setAnchorViewPosition
  32. releaseAnchorView
  33. getNativeViewAndroid
  34. nativeGetSingletonJavaContentVideoView
  35. nativeExitFullscreen
  36. nativeGetCurrentPosition
  37. nativeGetDurationInMilliSeconds
  38. nativeRequestMediaMetadata
  39. nativeGetVideoWidth
  40. nativeGetVideoHeight
  41. nativeIsPlaying
  42. nativePause
  43. nativePlay
  44. nativeSeekTo
  45. nativeSetSurface

// Copyright 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

package org.chromium.content.browser;

import android.app.Activity;
import android.app.AlertDialog;
import android.content.Context;
import android.content.DialogInterface;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.Surface;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;

import org.chromium.base.CalledByNative;
import org.chromium.base.JNINamespace;
import org.chromium.base.ThreadUtils;
import org.chromium.ui.base.ViewAndroid;
import org.chromium.ui.base.ViewAndroidDelegate;
import org.chromium.ui.base.WindowAndroid;

/**
 * This class implements accelerated fullscreen video playback using surface view.
 */
@JNINamespace("content")
public class ContentVideoView extends FrameLayout
        implements SurfaceHolder.Callback, ViewAndroidDelegate {

    private static final String TAG = "ContentVideoView";

    /* Do not change these values without updating their counterparts
     * in include/media/mediaplayer.h!
     */
    private static final int MEDIA_NOP = 0; // interface test message
    private static final int MEDIA_PREPARED = 1;
    private static final int MEDIA_PLAYBACK_COMPLETE = 2;
    private static final int MEDIA_BUFFERING_UPDATE = 3;
    private static final int MEDIA_SEEK_COMPLETE = 4;
    private static final int MEDIA_SET_VIDEO_SIZE = 5;
    private static final int MEDIA_ERROR = 100;
    private static final int MEDIA_INFO = 200;

    /**
     * Keep these error codes in sync with the code we defined in
     * MediaPlayerListener.java.
     */
    public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2;
    public static final int MEDIA_ERROR_INVALID_CODE = 3;

    // all possible internal states
    private static final int STATE_ERROR              = -1;
    private static final int STATE_IDLE               = 0;
    private static final int STATE_PLAYING            = 1;
    private static final int STATE_PAUSED             = 2;
    private static final int STATE_PLAYBACK_COMPLETED = 3;

    private SurfaceHolder mSurfaceHolder;
    private int mVideoWidth;
    private int mVideoHeight;
    private int mDuration;

    // Native pointer to C++ ContentVideoView object.
    private long mNativeContentVideoView;

    // webkit should have prepared the media
    private int mCurrentState = STATE_IDLE;

    // Strings for displaying media player errors
    private String mPlaybackErrorText;
    private String mUnknownErrorText;
    private String mErrorButton;
    private String mErrorTitle;
    private String mVideoLoadingText;

    // This view will contain the video.
    private VideoSurfaceView mVideoSurfaceView;

    // Progress view when the video is loading.
    private View mProgressView;

    // The ViewAndroid is used to keep screen on during video playback.
    private ViewAndroid mViewAndroid;

    private final ContentVideoViewClient mClient;

    private class VideoSurfaceView extends SurfaceView {

        public VideoSurfaceView(Context context) {
            super(context);
        }

        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
            int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
            if (mVideoWidth > 0 && mVideoHeight > 0) {
                if (mVideoWidth * height  > width * mVideoHeight) {
                    height = width * mVideoHeight / mVideoWidth;
                } else if (mVideoWidth * height  < width * mVideoHeight) {
                    width = height * mVideoWidth / mVideoHeight;
                }
            }
            setMeasuredDimension(width, height);
        }
    }

    private static class ProgressView extends LinearLayout {

        private final ProgressBar mProgressBar;
        private final TextView mTextView;

        public ProgressView(Context context, String videoLoadingText) {
            super(context);
            setOrientation(LinearLayout.VERTICAL);
            setLayoutParams(new LinearLayout.LayoutParams(
                    LinearLayout.LayoutParams.WRAP_CONTENT,
                    LinearLayout.LayoutParams.WRAP_CONTENT));
            mProgressBar = new ProgressBar(context, null, android.R.attr.progressBarStyleLarge);
            mTextView = new TextView(context);
            mTextView.setText(videoLoadingText);
            addView(mProgressBar);
            addView(mTextView);
        }
    }

    private final Runnable mExitFullscreenRunnable = new Runnable() {
        @Override
        public void run() {
            exitFullscreen(true);
        }
    };

    protected ContentVideoView(Context context, long nativeContentVideoView,
            ContentVideoViewClient client) {
        super(context);
        mNativeContentVideoView = nativeContentVideoView;
        mViewAndroid = new ViewAndroid(new WindowAndroid(context.getApplicationContext()), this);
        mClient = client;
        initResources(context);
        mVideoSurfaceView = new VideoSurfaceView(context);
        showContentVideoView();
        setVisibility(View.VISIBLE);
        mClient.onShowCustomView(this);
    }

    private void initResources(Context context) {
        if (mPlaybackErrorText != null) return;
        mPlaybackErrorText = context.getString(
                org.chromium.content.R.string.media_player_error_text_invalid_progressive_playback);
        mUnknownErrorText = context.getString(
                org.chromium.content.R.string.media_player_error_text_unknown);
        mErrorButton = context.getString(
                org.chromium.content.R.string.media_player_error_button);
        mErrorTitle = context.getString(
                org.chromium.content.R.string.media_player_error_title);
        mVideoLoadingText = context.getString(
                org.chromium.content.R.string.media_player_loading_video);
    }

    protected void showContentVideoView() {
        mVideoSurfaceView.getHolder().addCallback(this);
        this.addView(mVideoSurfaceView, new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.MATCH_PARENT,
                Gravity.CENTER));

        mProgressView = mClient.getVideoLoadingProgressView();
        if (mProgressView == null) {
            mProgressView = new ProgressView(getContext(), mVideoLoadingText);
        }
        this.addView(mProgressView, new FrameLayout.LayoutParams(
                ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT,
                Gravity.CENTER));
    }

    protected SurfaceView getSurfaceView() {
        return mVideoSurfaceView;
    }

    @CalledByNative
    public void onMediaPlayerError(int errorType) {
        Log.d(TAG, "OnMediaPlayerError: " + errorType);
        if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) {
            return;
        }

        // Ignore some invalid error codes.
        if (errorType == MEDIA_ERROR_INVALID_CODE) {
            return;
        }

        mCurrentState = STATE_ERROR;

        /* Pop up an error dialog so the user knows that
         * something bad has happened. Only try and pop up the dialog
         * if we're attached to a window. When we're going away and no
         * longer have a window, don't bother showing the user an error.
         *
         * TODO(qinmin): We need to review whether this Dialog is OK with
         * the rest of the browser UI elements.
         */
        if (getWindowToken() != null) {
            String message;

            if (errorType == MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
                message = mPlaybackErrorText;
            } else {
                message = mUnknownErrorText;
            }

            try {
                new AlertDialog.Builder(getContext())
                    .setTitle(mErrorTitle)
                    .setMessage(message)
                    .setPositiveButton(mErrorButton,
                            new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int whichButton) {
                            /* Inform that the video is over.
                             */
                            onCompletion();
                        }
                    })
                    .setCancelable(false)
                    .show();
            } catch (RuntimeException e) {
                Log.e(TAG, "Cannot show the alert dialog, error message: " + message, e);
            }
        }
    }

    @CalledByNative
    private void onVideoSizeChanged(int width, int height) {
        mVideoWidth = width;
        mVideoHeight = height;
        // This will trigger the SurfaceView.onMeasure() call.
        mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight);
    }

    @CalledByNative
    protected void onBufferingUpdate(int percent) {
    }

    @CalledByNative
    private void onPlaybackComplete() {
        onCompletion();
    }

    @CalledByNative
    protected void onUpdateMediaMetadata(
            int videoWidth,
            int videoHeight,
            int duration,
            boolean canPause,
            boolean canSeekBack,
            boolean canSeekForward) {
        mDuration = duration;
        mProgressView.setVisibility(View.GONE);
        mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED;
        onVideoSizeChanged(videoWidth, videoHeight);
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
    }

    @Override
    public void surfaceCreated(SurfaceHolder holder) {
        mSurfaceHolder = holder;
        openVideo();
    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        if (mNativeContentVideoView != 0) {
            nativeSetSurface(mNativeContentVideoView, null);
        }
        mSurfaceHolder = null;
        post(mExitFullscreenRunnable);
    }

    @CalledByNative
    protected void openVideo() {
        if (mSurfaceHolder != null) {
            mCurrentState = STATE_IDLE;
            if (mNativeContentVideoView != 0) {
                nativeRequestMediaMetadata(mNativeContentVideoView);
                nativeSetSurface(mNativeContentVideoView,
                        mSurfaceHolder.getSurface());
            }
        }
    }

    protected void onCompletion() {
        mCurrentState = STATE_PLAYBACK_COMPLETED;
    }


    protected boolean isInPlaybackState() {
        return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE);
    }

    protected void start() {
        if (isInPlaybackState()) {
            if (mNativeContentVideoView != 0) {
                nativePlay(mNativeContentVideoView);
            }
            mCurrentState = STATE_PLAYING;
        }
    }

    protected void pause() {
        if (isInPlaybackState()) {
            if (isPlaying()) {
                if (mNativeContentVideoView != 0) {
                    nativePause(mNativeContentVideoView);
                }
                mCurrentState = STATE_PAUSED;
            }
        }
    }

    // cache duration as mDuration for faster access
    protected int getDuration() {
        if (isInPlaybackState()) {
            if (mDuration > 0) {
                return mDuration;
            }
            if (mNativeContentVideoView != 0) {
                mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView);
            } else {
                mDuration = 0;
            }
            return mDuration;
        }
        mDuration = -1;
        return mDuration;
    }

    protected int getCurrentPosition() {
        if (isInPlaybackState() && mNativeContentVideoView != 0) {
            return nativeGetCurrentPosition(mNativeContentVideoView);
        }
        return 0;
    }

    protected void seekTo(int msec) {
        if (mNativeContentVideoView != 0) {
            nativeSeekTo(mNativeContentVideoView, msec);
        }
    }

    protected boolean isPlaying() {
        return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView);
    }

    @CalledByNative
    private static ContentVideoView createContentVideoView(
            Context context, long nativeContentVideoView, ContentVideoViewClient client,
            boolean legacy) {
        ThreadUtils.assertOnUiThread();
        // The context needs be Activity to create the ContentVideoView correctly.
        if (!(context instanceof Activity)) {
            Log.w(TAG, "Wrong type of context, can't create fullscreen video");
            return null;
        }
        if (legacy) {
            return new ContentVideoViewLegacy(context, nativeContentVideoView, client);
        } else {
            return new ContentVideoView(context, nativeContentVideoView, client);
        }
    }

    public void removeSurfaceView() {
        removeView(mVideoSurfaceView);
        removeView(mProgressView);
        mVideoSurfaceView = null;
        mProgressView = null;
    }

    public void exitFullscreen(boolean relaseMediaPlayer) {
        destroyContentVideoView(false);
        if (mNativeContentVideoView != 0) {
            nativeExitFullscreen(mNativeContentVideoView, relaseMediaPlayer);
            mNativeContentVideoView = 0;
        }
    }

    @CalledByNative
    private void onExitFullscreen() {
        exitFullscreen(false);
    }

    /**
     * This method shall only be called by native and exitFullscreen,
     * To exit fullscreen, use exitFullscreen in Java.
     */
    @CalledByNative
    protected void destroyContentVideoView(boolean nativeViewDestroyed) {
        if (mVideoSurfaceView != null) {
            removeSurfaceView();
            setVisibility(View.GONE);

            // To prevent re-entrance, call this after removeSurfaceView.
            mClient.onDestroyContentVideoView();
        }
        if (nativeViewDestroyed) {
            mNativeContentVideoView = 0;
        }
    }

    public static ContentVideoView getContentVideoView() {
        return nativeGetSingletonJavaContentVideoView();
    }

    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        if (keyCode == KeyEvent.KEYCODE_BACK) {
            exitFullscreen(false);
            return true;
        }
        return super.onKeyUp(keyCode, event);
    }

    @Override
    public View acquireAnchorView() {
        View anchorView = new View(getContext());
        addView(anchorView);
        return anchorView;
    }

    @Override
    public void setAnchorViewPosition(View view, float x, float y, float width, float height) {
        Log.e(TAG, "setAnchorViewPosition isn't implemented");
    }

    @Override
    public void releaseAnchorView(View anchorView) {
        removeView(anchorView);
    }

    @CalledByNative
    private long getNativeViewAndroid() {
        return mViewAndroid.getNativePointer();
    }

    private static native ContentVideoView nativeGetSingletonJavaContentVideoView();
    private native void nativeExitFullscreen(long nativeContentVideoView,
            boolean relaseMediaPlayer);
    private native int nativeGetCurrentPosition(long nativeContentVideoView);
    private native int nativeGetDurationInMilliSeconds(long nativeContentVideoView);
    private native void nativeRequestMediaMetadata(long nativeContentVideoView);
    private native int nativeGetVideoWidth(long nativeContentVideoView);
    private native int nativeGetVideoHeight(long nativeContentVideoView);
    private native boolean nativeIsPlaying(long nativeContentVideoView);
    private native void nativePause(long nativeContentVideoView);
    private native void nativePlay(long nativeContentVideoView);
    private native void nativeSeekTo(long nativeContentVideoView, int msec);
    private native void nativeSetSurface(long nativeContentVideoView, Surface surface);
}

/* [<][>][^][v][top][bottom][index][help] */