在全屏Android中的WebView播放HTML5视频
嗯,我一直在寻找的几天已经,如何在机器人的WebView显示HTML5视频的全屏模式。
Well, I've been searching few days already, how to display HTML5 video in full-screen mode on android WebView.
我设法播放HTML5视频在我的WebView。在全屏模式下显示视频时,问题丛生。
I managed to play HTML5 videos on my webview. Problems are arising when displaying video in fullscreen mode.
当我想通了,Android已经处理两种方式的< video>标签:
As I figured out, android has two ways of handling the < video > tag:
-
在Android版本&LT; = 2.3.3 ,该onShowCustomView方法被激发了,我可以有VideoView实例,并设置视频完成时设定控制器监听器等,所以到目前为止一切顺利。
On android versions <= 2.3.3, the onShowCustomView method is fired, and I can have the VideoView instance, and set listeners when the video completes, set controllers etc. So far so good.
在ICS(也可能是3.0及以上),它看起来像在&lt;视频>在不同的方式处理。当HTML5视频播放的onShowCustomView不会被调用的正常模式 - 它看起来像有就是播放视频web视图内的内部业务,和所有在定义的控件的&lt; video>标签显示 - 我不能访问它以任何方式。其实,如果视频播放在正常模式下,这是可以的,因为控制在那里,都在工作。
On ICS (and probably 3.0 and above), it looks like the < video > is handled in different way. When the HTML5 video is played the onShowCustomView isn't being called on normal mode - it looks like there is an internal business inside the WebView that plays the video, and all the controls that are defined in the < video > tag are shown - I can't access it in any way. Actually, if the video is played in normal mode this is OK because the controls are there and are working.
这导致我的大问题:显示在全屏模式下的视频的时候onShowCustomView被称为 - 但在ICS查看参数不是VideoView的一个实例
That led me to the big problem: when displaying the video in full screen mode the onShowCustomView is being called - but on ICS the "view" parameter isn't an instance of VideoView.
我设法找到了该实例是VideoSurfaceView,私有内部类HTML5VideoFullScreen类的。我们可以访问这个内部类的唯一方法是通过反射。
I managed to find out that the instance is of VideoSurfaceView, a private inner class of HTML5VideoFullScreen class. The only way we can access this inner-class is via reflection.
在看着grep的code这个类,我才知道,不像VideoView的HTML5VideoFullScreen $ VideoSurfaceView不持有MediaPlayer的实例,我可以听的事件或访问其控制。我唯一能做的就是采取这种VideoSurfaceView因为它并把它全屏幕布局内没有控制它。
After looking at GrepCode for this class, I learnt that unlike VideoView, the HTML5VideoFullScreen$VideoSurfaceView doesn't hold a MediaPlayer instance that I can listen to its events or access its controls. The only thing I can do is take this VideoSurfaceView as it is and put it inside a full-screen-layout without controlling it.
底线 - 在全屏幕显示视频,我不知道视频结束时,它的控制不显示 - 这是pretty的悲哀。我不能得到触发关闭全屏。
Bottom line - When displaying video in full-screen, I don't know when the video ends, its controls aren't shown - this is pretty sad. I can't get the trigger for closing the full-screen.
我试过几个不成功的解决方法:
I tried few unsuccessful workarounds:
-
反思:我试图达到HTML5VideoFullScreen实例,它持有的MediaPlayer成员,从内部类VideoSurfaceView。我没能得到它,我不知道这是可能的(ViewSurfaceView不持有其拥有者的实例)。
Reflection: I tried to reach the HTML5VideoFullScreen instance, which holds a MediaPlayer member, from the inner-class VideoSurfaceView. I didn't manage to get it, I'm not sure this is possible (ViewSurfaceView doesn't hold its owner's instance).
通过Javascript(onended,例如)注册为视频事件,以及处理我需要什么回来JAVA通过JavascriptInterface:我发现这个解决方案是不可靠的,因为,在做这个,我遇到了另一个问题:在&lt ; video>标签可以嵌套在。 iframe的来源是不是我的,我不能得到它的内容(或的getElementById的getElementsByTagName [I]是空值) - 这意味着,我不能达到的&lt;视频>的iframe内的元素。
Register for the video events via Javascript (onended, for example), and handle what I need back in JAVA via JavascriptInterface: I found this solution isn't reliable because while doing this I encountered another problem: the < video > tag can be nested in an . The iframe source isn't mine and I can't get its content (getElementById or getElementsByTagName[i] are nulls) - which means, I can't reach the < video > element inside the iframe.
我还在寻找一个解决办法,很少写关于这个问题。没有人设法解决这个问题?帮助将非常AP preciated!
I'm still searching for a solution, very little is written about this issue. Did anyone manage to solve it? Help would be much appreciated!
VideoView 类:Here (具有的MediaPlayer)
VideoView class: Here (has MediaPlayer)
HTML5VideoFullScreen $ VideoSurfaceView 类:Here (没有的MediaPlayer)
HTML5VideoFullScreen$VideoSurfaceView class: Here (doesn't have MediaPlayer)
编辑2014/01:改进的例子用法包括nonVideoLayout,videoLayout和videoLoading的意见,对于那些要求更多的例子code更好understading用户的
编辑一十二分之二千〇十三:与索尼Xperia设备的兼容性一些bug修复,但实际上影响到所有的设备的
编辑一十一分之二千零一十三:安卓4.4奇巧的版本(API等级19),其新的铬的WebView后,我不得不再次努力。有几个方面进行了改进。你应该升级到新版本。我松开下 WTFPL 此源。 接受捐赠(找到捐赠链接底部)。的
Edit 2013/11: after the release of Android 4.4 KitKat (API level 19) with its new Chromium webview, I had to work hard again. Several improvements were made. You should update to this new version. I release this source under WTFPL. Donations are accepted (find Donate link at bottom).
编辑2013/04:1个月的艰苦工作后,我终于取得了我需要的一切。我认为,我已经创建了这两个通用的类都可以解决你的问题。的
VideoEnabledWebChromeClient
可以单独使用,如果你不要求VideoEnabledWebView VideoEnabledWebView
必须始终依靠 VideoEnabledWebChromeClient
。 请阅读的所有的两个班认真的评论。
VideoEnabledWebChromeClient
can be used alone if you do not require the functionality that VideoEnabledWebView
adds. But VideoEnabledWebView
must always rely on a VideoEnabledWebChromeClient
. Please read all the comments of the both classes carefully.
import android.media.MediaPlayer;
import android.media.MediaPlayer.OnCompletionListener;
import android.media.MediaPlayer.OnErrorListener;
import android.media.MediaPlayer.OnPreparedListener;
import android.view.SurfaceView;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewGroup.LayoutParams;
import android.webkit.WebChromeClient;
import android.widget.FrameLayout;
/**
* This class serves as a WebChromeClient to be set to a WebView, allowing it to play video.
* Video will play differently depending on target API level (in-line, fullscreen, or both).
*
* It has been tested with the following video classes:
* - android.widget.VideoView (typically API level <11)
* - android.webkit.HTML5VideoFullScreen$VideoSurfaceView/VideoTextureView (typically API level 11-18)
* - com.android.org.chromium.content.browser.ContentVideoView$VideoSurfaceView (typically API level 19+)
*
* Important notes:
* - For API level 11+, android:hardwareAccelerated="true" must be set in the application manifest.
* - The invoking activity must call VideoEnabledWebChromeClient's onBackPressed() inside of its own onBackPressed().
* - Tested in Android API levels 8-19. Only tested on http://m.youtube.com.
*
* @author Cristian Perez (http://cpr.name)
*
*/
public class VideoEnabledWebChromeClient extends WebChromeClient implements OnPreparedListener, OnCompletionListener, OnErrorListener
{
public interface ToggledFullscreenCallback
{
public void toggledFullscreen(boolean fullscreen);
}
private View activityNonVideoView;
private ViewGroup activityVideoView;
private View loadingView;
private VideoEnabledWebView webView;
private boolean isVideoFullscreen; // Indicates if the video is being displayed using a custom view (typically full-screen)
private FrameLayout videoViewContainer;
private CustomViewCallback videoViewCallback;
private ToggledFullscreenCallback toggledFullscreenCallback;
/**
* Never use this constructor alone.
* This constructor allows this class to be defined as an inline inner class in which the user can override methods
*/
@SuppressWarnings("unused")
public VideoEnabledWebChromeClient()
{
}
/**
* Builds a video enabled WebChromeClient.
* @param activityNonVideoView A View in the activity's layout that contains every other view that should be hidden when the video goes full-screen.
* @param activityVideoView A ViewGroup in the activity's layout that will display the video. Typically you would like this to fill the whole layout.
*/
@SuppressWarnings("unused")
public VideoEnabledWebChromeClient(View activityNonVideoView, ViewGroup activityVideoView)
{
this.activityNonVideoView = activityNonVideoView;
this.activityVideoView = activityVideoView;
this.loadingView = null;
this.webView = null;
this.isVideoFullscreen = false;
}
/**
* Builds a video enabled WebChromeClient.
* @param activityNonVideoView A View in the activity's layout that contains every other view that should be hidden when the video goes full-screen.
* @param activityVideoView A ViewGroup in the activity's layout that will display the video. Typically you would like this to fill the whole layout.
* @param loadingView A View to be shown while the video is loading (typically only used in API level <11). Must be already inflated and without a parent view.
*/
@SuppressWarnings("unused")
public VideoEnabledWebChromeClient(View activityNonVideoView, ViewGroup activityVideoView, View loadingView)
{
this.activityNonVideoView = activityNonVideoView;
this.activityVideoView = activityVideoView;
this.loadingView = loadingView;
this.webView = null;
this.isVideoFullscreen = false;
}
/**
* Builds a video enabled WebChromeClient.
* @param activityNonVideoView A View in the activity's layout that contains every other view that should be hidden when the video goes full-screen.
* @param activityVideoView A ViewGroup in the activity's layout that will display the video. Typically you would like this to fill the whole layout.
* @param loadingView A View to be shown while the video is loading (typically only used in API level <11). Must be already inflated and without a parent view.
* @param webView The owner VideoEnabledWebView. Passing it will enable the VideoEnabledWebChromeClient to detect the HTML5 video ended event and exit full-screen.
* Note: The web page must only contain one video tag in order for the HTML5 video ended event to work. This could be improved if needed (see Javascript code).
*/
public VideoEnabledWebChromeClient(View activityNonVideoView, ViewGroup activityVideoView, View loadingView, VideoEnabledWebView webView)
{
this.activityNonVideoView = activityNonVideoView;
this.activityVideoView = activityVideoView;
this.loadingView = loadingView;
this.webView = webView;
this.isVideoFullscreen = false;
}
/**
* Indicates if the video is being displayed using a custom view (typically full-screen)
* @return true it the video is being displayed using a custom view (typically full-screen)
*/
public boolean isVideoFullscreen()
{
return isVideoFullscreen;
}
/**
* Set a callback that will be fired when the video starts or finishes displaying using a custom view (typically full-screen)
* @param callback A VideoEnabledWebChromeClient.ToggledFullscreenCallback callback
*/
public void setOnToggledFullscreen(ToggledFullscreenCallback callback)
{
this.toggledFullscreenCallback = callback;
}
@Override
public void onShowCustomView(View view, CustomViewCallback callback)
{
if (view instanceof FrameLayout)
{
// A video wants to be shown
FrameLayout frameLayout = (FrameLayout) view;
View focusedChild = frameLayout.getFocusedChild();
// Save video related variables
this.isVideoFullscreen = true;
this.videoViewContainer = frameLayout;
this.videoViewCallback = callback;
// Hide the non-video view, add the video view, and show it
activityNonVideoView.setVisibility(View.INVISIBLE);
activityVideoView.addView(videoViewContainer, new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT));
activityVideoView.setVisibility(View.VISIBLE);
if (focusedChild instanceof android.widget.VideoView)
{
// android.widget.VideoView (typically API level <11)
android.widget.VideoView videoView = (android.widget.VideoView) focusedChild;
// Handle all the required events
videoView.setOnPreparedListener(this);
videoView.setOnCompletionListener(this);
videoView.setOnErrorListener(this);
}
else
{
// Other classes, including:
// - android.webkit.HTML5VideoFullScreen$VideoSurfaceView, which inherits from android.view.SurfaceView (typically API level 11-18)
// - android.webkit.HTML5VideoFullScreen$VideoTextureView, which inherits from android.view.TextureView (typically API level 11-18)
// - com.android.org.chromium.content.browser.ContentVideoView$VideoSurfaceView, which inherits from android.view.SurfaceView (typically API level 19+)
// Handle HTML5 video ended event only if the class is a SurfaceView
// Test case: TextureView of Sony Xperia T API level 16 doesn't work fullscreen when loading the javascript below
if (webView != null && webView.getSettings().getJavaScriptEnabled() && focusedChild instanceof SurfaceView)
{
// Run javascript code that detects the video end and notifies the Javascript interface
String js = "javascript:";
js += "var _ytrp_html5_video_last;";
js += "var _ytrp_html5_video = document.getElementsByTagName('video')[0];";
js += "if (_ytrp_html5_video != undefined && _ytrp_html5_video != _ytrp_html5_video_last) {";
{
js += "_ytrp_html5_video_last = _ytrp_html5_video;";
js += "function _ytrp_html5_video_ended() {";
{
js += "_VideoEnabledWebView.notifyVideoEnd();"; // Must match Javascript interface name and method of VideoEnableWebView
}
js += "}";
js += "_ytrp_html5_video.addEventListener('ended', _ytrp_html5_video_ended);";
}
js += "}";
webView.loadUrl(js);
}
}
// Notify full-screen change
if (toggledFullscreenCallback != null)
{
toggledFullscreenCallback.toggledFullscreen(true);
}
}
}
@Override @SuppressWarnings("deprecation")
public void onShowCustomView(View view, int requestedOrientation, CustomViewCallback callback) // Available in API level 14+, deprecated in API level 18+
{
onShowCustomView(view, callback);
}
@Override
public void onHideCustomView()
{
// This method should be manually called on video end in all cases because it's not always called automatically.
// This method must be manually called on back key press (from this class' onBackPressed() method).
if (isVideoFullscreen)
{
// Hide the video view, remove it, and show the non-video view
activityVideoView.setVisibility(View.INVISIBLE);
activityVideoView.removeView(videoViewContainer);
activityNonVideoView.setVisibility(View.VISIBLE);
// Call back (only in API level <19, because in API level 19+ with chromium webview it crashes)
if (videoViewCallback != null && !videoViewCallback.getClass().getName().contains(".chromium."))
{
videoViewCallback.onCustomViewHidden();
}
// Reset video related variables
isVideoFullscreen = false;
videoViewContainer = null;
videoViewCallback = null;
// Notify full-screen change
if (toggledFullscreenCallback != null)
{
toggledFullscreenCallback.toggledFullscreen(false);
}
}
}
@Override
public View getVideoLoadingProgressView() // Video will start loading
{
if (loadingView != null)
{
loadingView.setVisibility(View.VISIBLE);
return loadingView;
}
else
{
return super.getVideoLoadingProgressView();
}
}
@Override
public void onPrepared(MediaPlayer mp) // Video will start playing, only called in the case of android.widget.VideoView (typically API level <11)
{
if (loadingView != null)
{
loadingView.setVisibility(View.GONE);
}
}
@Override
public void onCompletion(MediaPlayer mp) // Video finished playing, only called in the case of android.widget.VideoView (typically API level <11)
{
onHideCustomView();
}
@Override
public boolean onError(MediaPlayer mp, int what, int extra) // Error while playing video, only called in the case of android.widget.VideoView (typically API level <11)
{
return false; // By returning false, onCompletion() will be called
}
/**
* Notifies the class that the back key has been pressed by the user.
* This must be called from the Activity's onBackPressed(), and if it returns false, the activity itself should handle it. Otherwise don't do anything.
* @return Returns true if the event was handled, and false if was not (video view is not visible)
*/
public boolean onBackPressed()
{
if (isVideoFullscreen)
{
onHideCustomView();
return true;
}
else
{
return false;
}
}
}
VideoEnabledWebView类
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.util.AttributeSet;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import java.util.Map;
/**
* This class serves as a WebView to be used in conjunction with a VideoEnabledWebChromeClient.
* It makes possible:
* - To detect the HTML5 video ended event so that the VideoEnabledWebChromeClient can exit full-screen.
*
* Important notes:
* - Javascript is enabled by default and must not be disabled with getSettings().setJavaScriptEnabled(false).
* - setWebChromeClient() must be called before any loadData(), loadDataWithBaseURL() or loadUrl() method.
*
* @author Cristian Perez (http://cpr.name)
*
*/
public class VideoEnabledWebView extends WebView
{
public class JavascriptInterface
{
@android.webkit.JavascriptInterface
public void notifyVideoEnd() // Must match Javascript interface method of VideoEnabledWebChromeClient
{
// This code is not executed in the UI thread, so we must force that to happen
new Handler(Looper.getMainLooper()).post(new Runnable()
{
@Override
public void run()
{
if (videoEnabledWebChromeClient != null)
{
videoEnabledWebChromeClient.onHideCustomView();
}
}
});
}
}
private VideoEnabledWebChromeClient videoEnabledWebChromeClient;
private boolean addedJavascriptInterface;
public VideoEnabledWebView(Context context)
{
super(context);
addedJavascriptInterface = false;
}
@SuppressWarnings("unused")
public VideoEnabledWebView(Context context, AttributeSet attrs)
{
super(context, attrs);
addedJavascriptInterface = false;
}
@SuppressWarnings("unused")
public VideoEnabledWebView(Context context, AttributeSet attrs, int defStyle)
{
super(context, attrs, defStyle);
addedJavascriptInterface = false;
}
/**
* Indicates if the video is being displayed using a custom view (typically full-screen)
* @return true it the video is being displayed using a custom view (typically full-screen)
*/
public boolean isVideoFullscreen()
{
return videoEnabledWebChromeClient != null && videoEnabledWebChromeClient.isVideoFullscreen();
}
/**
* Pass only a VideoEnabledWebChromeClient instance.
*/
@Override @SuppressLint("SetJavaScriptEnabled")
public void setWebChromeClient(WebChromeClient client)
{
getSettings().setJavaScriptEnabled(true);
if (client instanceof VideoEnabledWebChromeClient)
{
this.videoEnabledWebChromeClient = (VideoEnabledWebChromeClient) client;
}
super.setWebChromeClient(client);
}
@Override
public void loadData(String data, String mimeType, String encoding)
{
addJavascriptInterface();
super.loadData(data, mimeType, encoding);
}
@Override
public void loadDataWithBaseURL(String baseUrl, String data, String mimeType, String encoding, String historyUrl)
{
addJavascriptInterface();
super.loadDataWithBaseURL(baseUrl, data, mimeType, encoding, historyUrl);
}
@Override
public void loadUrl(String url)
{
addJavascriptInterface();
super.loadUrl(url);
}
@Override
public void loadUrl(String url, Map<String, String> additionalHttpHeaders)
{
addJavascriptInterface();
super.loadUrl(url, additionalHttpHeaders);
}
private void addJavascriptInterface()
{
if (!addedJavascriptInterface)
{
// Add javascript interface to be called when the video ends (must be done before page load)
addJavascriptInterface(new JavascriptInterface(), "_VideoEnabledWebView"); // Must match Javascript interface name of VideoEnabledWebChromeClient
addedJavascriptInterface = true;
}
}
}
实例:
主要布局的 activity_main.xml 的中,我们把一个VideoEnabledWebView等二手观点:
Example usage:
Main layout activity_main.xml in which we put a VideoEnabledWebView and other used views:
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity" >
<!-- View that will be hidden when video goes fullscreen -->
<RelativeLayout
android:id="@+id/nonVideoLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<your.package.VideoEnabledWebView
android:id="@+id/webView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>
<!-- View where the video will be shown when video goes fullscreen -->
<RelativeLayout
android:id="@+id/videoLayout"
android:layout_width="match_parent"
android:layout_height="match_parent" >
<!-- View that will be shown while the fullscreen video loads (maybe include a spinner and a "Loading..." message) -->
<View
android:id="@+id/videoLoading"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:visibility="invisible" />
</RelativeLayout>
</RelativeLayout>
活动的的onCreate()的,在此我们将其初始化:
Activity's onCreate(), in which we initialize it:
private VideoEnabledWebView webView;
private VideoEnabledWebChromeClient webChromeClient;
@Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
// Set layout
setContentView(R.layout.activity_main);
// Save the web view
webView = (VideoEnabledWebView) findViewById(R.id.webView);
// Initialize the VideoEnabledWebChromeClient and set event handlers
View nonVideoLayout = findViewById(R.id.nonVideoLayout); // Your own view, read class comments
ViewGroup videoLayout = (ViewGroup) findViewById(R.id.videoLayout); // Your own view, read class comments
View loadingView = getLayoutInflater().inflate(R.layout.view_loading_video, null); // Your own view, read class comments
webChromeClient = new VideoEnabledWebChromeClient(nonVideoLayout, videoLayout, loadingView, webView) // See all available constructors...
{
// Subscribe to standard events, such as onProgressChanged()...
@Override
public void onProgressChanged(WebView view, int progress)
{
// Your code...
}
};
webChromeClient.setOnToggledFullscreen(new VideoEnabledWebChromeClient.ToggledFullscreenCallback()
{
@Override
public void toggledFullscreen(boolean fullscreen)
{
// Your code to handle the full-screen change, for example showing and hiding the title bar. Example:
if (fullscreen)
{
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags |= WindowManager.LayoutParams.FLAG_FULLSCREEN;
attrs.flags |= WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
getWindow().setAttributes(attrs);
if (android.os.Build.VERSION.SDK_INT >= 14)
{
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LOW_PROFILE);
}
}
else
{
WindowManager.LayoutParams attrs = getWindow().getAttributes();
attrs.flags &= ~WindowManager.LayoutParams.FLAG_FULLSCREEN;
attrs.flags &= ~WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON;
getWindow().setAttributes(attrs);
if (android.os.Build.VERSION.SDK_INT >= 14)
{
getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_VISIBLE);
}
}
}
});
webView.setWebChromeClient(webChromeClient);
// Navigate everywhere you want, this classes have only been tested on YouTube's mobile site
webView.loadUrl("http://m.youtube.com");
}
不要忘记调用的 onBack pressed()的:
@Override
public void onBackPressed()
{
// Notify the VideoEnabledWebChromeClient, and handle it ourselves if it doesn't handle it
if (!webChromeClient.onBackPressed())
{
if (webView.canGoBack())
{
webView.goBack();
}
else
{
// Close app (presumably)
super.onBackPressed();
}
}
}