// 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);
}