// Copyright (c) 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.graphics.Color; import android.os.Handler; import android.os.Message; import android.os.RemoteException; import android.util.Log; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; 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.MediaController; import android.widget.MediaController.MediaPlayerControl; import android.widget.ProgressBar; import android.widget.TextView; import java.lang.ref.WeakReference; import org.chromium.base.CalledByNative; import org.chromium.base.JNINamespace; import org.chromium.content.common.IChildProcessService; // import org.chromium.content.R; //import org.chromium.content_shell_apk.R; import com.borqs.browser.R; @JNINamespace("content") public class ContentVideoView extends FrameLayout implements MediaPlayerControl, SurfaceHolder.Callback, View.OnTouchListener, View.OnKeyListener { 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; /** The video is streamed and its container is not valid for progressive * playback i.e the video's index (e.g moov atom) is not at the start of the * file. */ public static final int MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK = 2; // 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 = null; private int mVideoWidth = 0; private int mVideoHeight = 0; private int mCurrentBufferPercentage; private int mDuration; private MediaController mMediaController = null; private boolean mCanPause; private boolean mCanSeekBack; private boolean mCanSeekForward; // Native pointer to C++ ContentVideoView object. private int mNativeContentVideoView = 0; // webkit should have prepared the media private int mCurrentState = STATE_IDLE; // Strings for displaying media player errors static String mPlaybackErrorText; static String mUnknownErrorText; static String mErrorButton; static String mErrorTitle; static String mVideoLoadingText; // This view will contain the video. private VideoSurfaceView mVideoSurfaceView; // Progress view when the video is loading. private View mProgressView; private Surface mSurface = null; // There are can be at most 1 fullscreen video // TODO(qinmin): will change this once we move the creation of this class // to the host application private static ContentVideoView sContentVideoView = null; // The delegate will follow sContentVideoView. We would need to // move this to an instance variable if we allow multiple ContentVideoViews. private static ContentVideoViewContextDelegate sDelegate = null; private class VideoSurfaceView extends SurfaceView { public VideoSurfaceView(Context context) { super(context); } @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { if (mVideoWidth == 0 && mVideoHeight == 0) { setMeasuredDimension(1, 1); return; } 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 ProgressBar mProgressBar; private TextView mTextView; public ProgressView(Context context) { 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(mVideoLoadingText); addView(mProgressBar); addView(mTextView); } } private static class FullScreenMediaController extends MediaController { View mVideoView; public FullScreenMediaController(Context context, View video) { super(context); mVideoView = video; } @Override public void show() { super.show(); if (mVideoView != null) { mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE); } } @Override public void hide() { if (mVideoView != null) { mVideoView.setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE); } super.hide(); } } private Runnable mExitFullscreenRunnable = new Runnable() { @Override public void run() { destroyContentVideoView(); } }; public ContentVideoView(Context context) { this(context, 0); } private ContentVideoView(Context context, int nativeContentVideoView) { super(context); initResources(context); if (nativeContentVideoView == 0) return; mNativeContentVideoView = nativeContentVideoView; mCurrentBufferPercentage = 0; mVideoSurfaceView = new VideoSurfaceView(context); } private static void initResources(Context context) { if (mPlaybackErrorText != null) return; mPlaybackErrorText = context.getString( R.string.media_player_error_text_invalid_progressive_playback); mUnknownErrorText = context.getString( R.string.media_player_error_text_unknown); mErrorButton = context.getString( R.string.media_player_error_button); mErrorTitle = context.getString( R.string.media_player_error_title); mVideoLoadingText = context.getString( R.string.media_player_loading_video); } void showContentVideoView() { FrameLayout.LayoutParams layoutParams = new FrameLayout.LayoutParams( ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, Gravity.CENTER); this.addView(mVideoSurfaceView, layoutParams); View progressView = sDelegate.getVideoLoadingProgressView(); if (progressView != null) { mProgressView = progressView; } else { mProgressView = new ProgressView(getContext()); } this.addView(mProgressView, layoutParams); mVideoSurfaceView.setZOrderOnTop(true); mVideoSurfaceView.setOnKeyListener(this); mVideoSurfaceView.setOnTouchListener(this); mVideoSurfaceView.getHolder().addCallback(this); mVideoSurfaceView.getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS); mVideoSurfaceView.setFocusable(true); mVideoSurfaceView.setFocusableInTouchMode(true); mVideoSurfaceView.requestFocus(); } @CalledByNative public void onMediaPlayerError(int errorType) { Log.d(TAG, "OnMediaPlayerError: " + errorType); if (mCurrentState == STATE_ERROR || mCurrentState == STATE_PLAYBACK_COMPLETED) { return; } mCurrentState = STATE_ERROR; if (mMediaController != null) { mMediaController.hide(); } /* 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; } new AlertDialog.Builder(getContext()) .setTitle(mErrorTitle) .setMessage(message) .setPositiveButton(mErrorButton, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { /* Inform that the video is over. */ onCompletion(); } }) .setCancelable(false) .show(); } } @CalledByNative public void onVideoSizeChanged(int width, int height) { mVideoWidth = width; mVideoHeight = height; if (mVideoWidth != 0 && mVideoHeight != 0) { mVideoSurfaceView.getHolder().setFixedSize(mVideoWidth, mVideoHeight); } } @CalledByNative public void onBufferingUpdate(int percent) { mCurrentBufferPercentage = percent; } @CalledByNative public void onPlaybackComplete() { onCompletion(); } @CalledByNative public void updateMediaMetadata( int videoWidth, int videoHeight, int duration, boolean canPause, boolean canSeekBack, boolean canSeekForward) { mProgressView.setVisibility(View.GONE); mDuration = duration; mCanPause = canPause; mCanSeekBack = canSeekBack; mCanSeekForward = canSeekForward; mCurrentState = isPlaying() ? STATE_PLAYING : STATE_PAUSED; if (mMediaController != null) { mMediaController.setEnabled(true); // If paused , should show the controller for ever. if (isPlaying()) mMediaController.show(); else mMediaController.show(0); } onVideoSizeChanged(videoWidth, videoHeight); } public void destroyNativeView() { if (mNativeContentVideoView != 0) { mNativeContentVideoView = 0; destroyContentVideoView(); } } @Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { mVideoSurfaceView.setFocusable(true); mVideoSurfaceView.setFocusableInTouchMode(true); if (isInPlaybackState() && mMediaController != null) { mMediaController.show(); } } @Override public void surfaceCreated(SurfaceHolder holder) { mSurfaceHolder = holder; openVideo(); } @Override public void surfaceDestroyed(SurfaceHolder holder) { mSurfaceHolder = null; if (mNativeContentVideoView != 0) { nativeExitFullscreen(mNativeContentVideoView, true); mNativeContentVideoView = 0; post(mExitFullscreenRunnable); } removeMediaController(); } public void setMediaController(MediaController controller) { if (mMediaController != null) { mMediaController.hide(); } mMediaController = controller; attachMediaController(); } private void attachMediaController() { if (mMediaController != null) { mMediaController.setMediaPlayer(this); mMediaController.setAnchorView(mVideoSurfaceView); mMediaController.setEnabled(false); } } @CalledByNative public void openVideo() { if (mSurfaceHolder != null) { mCurrentState = STATE_IDLE; setMediaController(new FullScreenMediaController(sDelegate.getContext(), this)); if (mNativeContentVideoView != 0) { nativeUpdateMediaMetadata(mNativeContentVideoView); } mCurrentBufferPercentage = 0; if (mNativeContentVideoView != 0) { nativeSetSurface(mNativeContentVideoView, mSurfaceHolder.getSurface()); } } } private void onCompletion() { mCurrentState = STATE_PLAYBACK_COMPLETED; if (mMediaController != null) { mMediaController.hide(); } } @Override public boolean onTouch(View v, MotionEvent event) { if (isInPlaybackState() && mMediaController != null && event.getAction() == MotionEvent.ACTION_DOWN) { toggleMediaControlsVisiblity(); } return true; } @Override public boolean onTrackballEvent(MotionEvent ev) { if (isInPlaybackState() && mMediaController != null) { toggleMediaControlsVisiblity(); } return false; } @Override public boolean onKey(View v, int keyCode, KeyEvent event) { boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK && keyCode != KeyEvent.KEYCODE_VOLUME_UP && keyCode != KeyEvent.KEYCODE_VOLUME_DOWN && keyCode != KeyEvent.KEYCODE_VOLUME_MUTE && keyCode != KeyEvent.KEYCODE_CALL && keyCode != KeyEvent.KEYCODE_MENU && keyCode != KeyEvent.KEYCODE_SEARCH && keyCode != KeyEvent.KEYCODE_ENDCALL; if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) { if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) { if (isPlaying()) { pause(); mMediaController.show(); } else { start(); mMediaController.hide(); } return true; } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) { if (!isPlaying()) { start(); mMediaController.hide(); } return true; } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) { if (isPlaying()) { pause(); mMediaController.show(); } return true; } else { toggleMediaControlsVisiblity(); } } else if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { if (mNativeContentVideoView != 0) { nativeExitFullscreen(mNativeContentVideoView, false); destroyNativeView(); } return true; } else if (keyCode == KeyEvent.KEYCODE_MENU || keyCode == KeyEvent.KEYCODE_SEARCH) { return true; } return super.onKeyDown(keyCode, event); } private void toggleMediaControlsVisiblity() { if (mMediaController.isShowing()) { mMediaController.hide(); } else { mMediaController.show(); } } private boolean isInPlaybackState() { return (mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE); } public void start() { if (isInPlaybackState()) { if (mNativeContentVideoView != 0) { nativePlay(mNativeContentVideoView); } mCurrentState = STATE_PLAYING; } } public void pause() { if (isInPlaybackState()) { if (isPlaying()) { if (mNativeContentVideoView != 0) { nativePause(mNativeContentVideoView); } mCurrentState = STATE_PAUSED; } } } // cache duration as mDuration for faster access public int getDuration() { if (isInPlaybackState()) { if (mDuration > 0) { return mDuration; } if (mNativeContentVideoView != 0) { mDuration = nativeGetDurationInMilliSeconds(mNativeContentVideoView); } else { mDuration = 0; } return mDuration; } mDuration = -1; return mDuration; } public int getCurrentPosition() { if (isInPlaybackState() && mNativeContentVideoView != 0) { return nativeGetCurrentPosition(mNativeContentVideoView); } return 0; } public void seekTo(int msec) { if (mNativeContentVideoView != 0) { nativeSeekTo(mNativeContentVideoView, msec); } } public boolean isPlaying() { return mNativeContentVideoView != 0 && nativeIsPlaying(mNativeContentVideoView); } public int getBufferPercentage() { return mCurrentBufferPercentage; } public boolean canPause() { return mCanPause; } public boolean canSeekBackward() { return mCanSeekBack; } public boolean canSeekForward() { return mCanSeekForward; } @CalledByNative public static ContentVideoView createContentVideoView(int nativeContentVideoView) { if (sContentVideoView != null) return sContentVideoView; if (sDelegate != null && sDelegate.getContext() != null) { sContentVideoView = new ContentVideoView(sDelegate.getContext(), nativeContentVideoView); sDelegate.onShowCustomView(sContentVideoView); sContentVideoView.setBackgroundColor(Color.BLACK); sContentVideoView.showContentVideoView(); sContentVideoView.setVisibility(View.VISIBLE); return sContentVideoView; } return null; } public void removeMediaController() { if (mMediaController != null) { mMediaController.setEnabled(false); mMediaController.hide(); mMediaController = null; } } public void removeSurfaceView() { removeView(mVideoSurfaceView); removeView(mProgressView); mVideoSurfaceView = null; mProgressView = null; } @CalledByNative public static void destroyContentVideoView() { sDelegate.onDestroyContentVideoView(); if (sContentVideoView != null) { sContentVideoView.removeMediaController(); sContentVideoView.removeSurfaceView(); sContentVideoView.setVisibility(View.GONE); } sContentVideoView = null; } public static ContentVideoView getContentVideoView() { return sContentVideoView; } public static void registerContentVideoViewContextDelegate( ContentVideoViewContextDelegate delegate) { sDelegate = delegate; } @Override public boolean onTouchEvent(MotionEvent ev) { return true; } @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (keyCode == KeyEvent.KEYCODE_BACK && event.getAction() == KeyEvent.ACTION_UP) { destroyContentVideoView(); return true; } return super.onKeyDown(keyCode, event); } private native void nativeExitFullscreen(int nativeContentVideoView, boolean relaseMediaPlayer); private native int nativeGetCurrentPosition(int nativeContentVideoView); private native int nativeGetDurationInMilliSeconds(int nativeContentVideoView); private native void nativeUpdateMediaMetadata(int nativeContentVideoView); private native int nativeGetVideoWidth(int nativeContentVideoView); private native int nativeGetVideoHeight(int nativeContentVideoView); private native boolean nativeIsPlaying(int nativeContentVideoView); private native void nativePause(int nativeContentVideoView); private native void nativePlay(int nativeContentVideoView); private native void nativeSeekTo(int nativeContentVideoView, int msec); private native void nativeSetSurface(int nativeContentVideoView, Surface surface); @Override public int getAudioSessionId() { // TODO Auto-generated method stub return 0; } }