WebView 腾讯视频全屏播放

概述

本文讲述的是,通过 WebView加载带有视频的网页,实现点击网页上的全屏按钮,实现视频横屏、全屏播放的功能。具体代码可见Demo ,展现的有腾讯视频,Bilibili视频,土豆视频。

需求

最近有个需求,加载一个腾讯视频的网页,用户点击网页上的全屏按钮,就能实现设备横屏,全屏播放。

前提

设置属性

  1. AndroidManifest 声明网络权限

    <uses-permission android:name="android.permission.INTERNET"/>

  2. 屏幕旋转的时候,默认会重新走 Activity 的生命周期,所以要在所在的 Activity,声明当下面情况发生的时候,不重新走生命周期。

    android:configChanges="orientation|keyboard|keyboardHidden|screenSize"

  3. 声明 WebView settings 属性

    BaseWebView.java (继承 WebView 的父类)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    mSettings = this.getSettings();	//这里的 this 是指代 WebView
    if (null != mSettings) {
    // 网页内容的宽度是否可大于WebView控件的宽度
    mSettings.setLoadWithOverviewMode(false);
    // 保存表单数据
    mSettings.setSaveFormData(true);
    // 是否应该支持使用其屏幕缩放控件和手势缩放
    mSettings.setSupportZoom(true);
    mSettings.setBuiltInZoomControls(true);
    mSettings.setDisplayZoomControls(false);
    // 启动应用缓存
    mSettings.setAppCacheEnabled(true);
    // 设置缓存模式
    mSettings.setCacheMode(WebSettings.LOAD_DEFAULT);
    // setDefaultZoom api19被弃用
    // 设置此属性,可任意比例缩放。
    mSettings.setUseWideViewPort(true);
    // 缩放比例 1
    this.setInitialScale(1);
    // 告诉WebView启用JavaScript执行。默认的是false。
    mSettings.setJavaScriptEnabled(true);
    // 页面加载好以后,再放开图片
    //mSettings.setBlockNetworkImage(false);
    // 使用localStorage则必须打开
    mSettings.setDomStorageEnabled(true);
    // 排版适应屏幕
    mSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NARROW_COLUMNS);
    // WebView是否支持多个窗口。
    mSettings.setSupportMultipleWindows(true);

    // webview从5.0开始默认不允许混合模式,https中不能加载http资源,需要设置开启。
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
    mSettings.setMixedContentMode(WebSettings.MIXED_CONTENT_ALWAYS_ALLOW);
    }
    // 设置字体默认缩放大小(改变网页字体大小,setTextSize api14被弃用)
    //mSettings.setTextZoom(100);
    }

特殊情况处理

在尝试优酷视频加载的时候,

开始返回 url 是正常类型的

1
http://m.youku.com/video/id_XMjY2OTk2MDMwOA==.html?from=s1.8-1-1.2&spm=a2h0k.8191407.0.0

但是有时候返回的url是这样的,包含对应的 APP packegeName 的action。这时候如果 WebView 没做处理,就会导致页面加载不出来的情况。

1
intent://play?vid=XMjY2OTk2MDMwOA==&refer=&tuid=&ua=Mozilla%2F5.0%20(Linux%3B%20Android%204.4.2%3B%20LG-D802%20Build%2FKOT49I.D80220c)%20AppleWebKit%2F537.36%20(KHTML%2C%20like%20Gecko)%20Version%2F4.0%20Chrome%2F30.0.0.0%20Mobile%20Safari%2F537.36&source=exclusive-pageload&cookieid=1493731828915oxvHJU|seP6GQ#Intent;scheme=youku;package=com.youku.phone;end;

解决办法:

在 shouldOverrideUrlLoading 的时候,找出这种类型,判断是非当前有安装 action对应 APP,有的话用对应的 APP 打开,没有的话提取 url 加载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private class CustomWebClient extends WebViewClient {

/**
* (此接口 Android N 以后 deprecation)
* true: 不处理这个url,我自己来; false:webView加载这个url,我什么都不做
*/
@SuppressWarnings("deprecation")
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
final Uri uri = Uri.parse(url);
return handleUri(view, uri);
}

@TargetApi(Build.VERSION_CODES.N)
@Override
public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {

final Uri uri = request.getUrl();
return handleUri(view, uri);
}

private boolean handleUri(WebView view, final Uri uri) {
final String url = uri.toString();
//final String host = uri.getHost(); //m.youku.com
//final String scheme = uri.getScheme(); // http
/**
* 此处用来处理,部分机型加载网页,有时除了返回url。
* 还有返回带有 intent:// 的格式,该格式带有启动app的action,可以启动对应的app
*/
if (url.startsWith("intent://")) {
try {
Context context = view.getContext();
Intent intent = new Intent().parseUri(url, Intent.URI_INTENT_SCHEME);

if (intent != null) {
view.stopLoading();

PackageManager packageManager = context.getPackageManager();
ResolveInfo info = packageManager.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY);
if (info != null) {
context.startActivity(intent);
} else {
String fallbackUrl = intent.getStringExtra("browser_fallback_url");
view.loadUrl(fallbackUrl);

// or call external broswer
//Intent browserIntent = new Intent(Intent.ACTION_VIEW, Uri.parse(fallbackUrl));
//context.startActivity(browserIntent);
}

return true;
}
} catch (URISyntaxException e) {
}
}

return false;
}
}

销毁须知

WebView 很容易引发内存泄漏,解决方法有:

  1. 简单的方式:WebView 所在的 Activity 声明在另外一个进程里面,

    android:process=":VideWebView"

    并在 Activity onDestory的时候,杀死这个进程

    android.os.Process.killProcess(android.os.Process.myPid());

  2. 常规的方式:不新开进程,在 Activity onDestroy 的时候,手动调 webView.onDestory() 销毁方法。不然,除了会发生内存泄漏外,播放视频的时候,会因为 WebView 没有销毁,即使 back 键返回 Activity,还会出现音频持续播放的尴尬情况哈。

    VideoWebView.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * 自定义 WebView 手动销毁方法
    */
    public void onDestory() {
    super.onDestory();
    mOnVideoWebViewListener = null;
    mCallback = null;
    stopLoading();
    clearCache(true);
    clearFormData();
    clearMatches();
    clearHistory();
    clearDisappearingChildren();
    clearAnimation();
    removeAllViews();
    destroy();
    }

    好了,接下来看看我们应该如何解决全屏的问题,先从官方Webview文档找找答案吧,

官方解决方法

实现方法

打开硬件加速

此处为 WebView 所在的 Activity 开启硬件加速。在 Mainifest 声明即可。

android:hardwareAccelerated="true"

setWebChromeClient

并且重写 onShowCustomView(点击全屏按钮时候回调) 和 onHideCustomView(再次点击退出全屏的时候回调)

VideoWebView.java

this.setWebChromeClient(new CustomWebChromClient());

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
/**
* 响应改变浏览器中装饰元素的事件(JavaScript 警告,网页图标,状态条加载,网页标题的刷新,进入/退出全屏)
*/
private class CustomWebChromClient extends WebChromeClient {

/**
* 常规方式下,点击全屏按钮的时候调
* @param view
* @param callback
*/
@Override
public void onShowCustomView(View view, CustomViewCallback callback) {
super.onShowCustomView(view, callback);
mCallback=callback;
//回调给外层,由外层处理全屏触发后的逻辑
if (null != mOnVideoWebViewListener) {
mOnVideoWebViewListener.onShowCustomView(view, callback);
}
mFullScreenMode = true; //标识全屏
}

/**
* 常规方式下,点击退出全屏的时候调
*/
@Override
public void onHideCustomView() {
super.onHideCustomView();
//回调给外层,由外层处理退出全屏的逻辑
if (null != mOnVideoWebViewListener) {
mOnVideoWebViewListener.onHideCustomView(mCallback);
}
mFullScreenMode = false;//标识退出全屏
}
}

VideoWebViewActivity.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 设置进入/退出全屏的监听
* @param onVideoWebViewListener
*/
public void setOnVideoWebViewListener(OnVideoWebViewListener onVideoWebViewListener) {
mOnVideoWebViewListener = onVideoWebViewListener;
}

public interface OnVideoWebViewListener {

/**
* 常规方式:点击网页上的全屏按钮时,执行此方法
* @param view
* @param callback
*/
void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback);

/**
* 常规方式:再次点击网页上的全屏按钮,执行此方法
* @param callback
*/
void onHideCustomView(WebChromeClient.CustomViewCallback callback);
}

布局

webview 撑满界面,同时还有个 FrameLayout撑满。

(因为最外层为 LinearLayout,第一个 Child(WebView) 已经撑满了,所以 FrameLayout在默认加载的时候并不会出现在界面上)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">

<com.lkl.ansuote.demo.webviewdemo.video.VideoWebView
android:id="@+id/webview_video"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

<FrameLayout
android:id="@+id/framelayout_container"
android:layout_width="match_parent"
android:layout_height="match_parent"/>

</LinearLayout>

在 Activity 处理全屏逻辑

  1. 进入横屏:设置屏幕为横屏,隐藏 WebView,显示 mContainerLayout(就是刚刚布局里面撑满的 FrameLayout ),并把 WebView 内部全屏触发时候回调的 view add 当 FrameLayout上。

  2. 退出全屏:设置屏幕为竖屏,显示 WebView,隐藏 mContainerLayout。

VideoWebViewActivity.java

1
mVideoWebView.setOnVideoWebViewListener(new VideoWebViewListenerImp());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* 自定义全屏接口实现类
*/
class VideoWebViewListenerImp implements VideoWebView.OnVideoWebViewListener {

//....

@Override
public void onShowCustomView(View view, WebChromeClient.CustomViewCallback callback) {
setLandscape();
setFullScreen(view);
}

@Override
public void onHideCustomView(WebChromeClient.CustomViewCallback callback) {
setPortrait();
if (null != callback) {
callback.onCustomViewHidden();
}
setNormalScreen();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
/**
* 设置横屏
*/
private void setLandscape() {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_PORTRAIT) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
}
}

/**
* 设置竖屏
*/
private void setPortrait() {
if (getResources().getConfiguration().orientation == Configuration.ORIENTATION_LANDSCAPE) {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
}
}

/**
* 常规方式:设置全屏
* @param view
*/
private void setFullScreen(View view) {
if (null == mVideoWebView || null == mContainerLayout || null == view) {
return;
}
mVideoWebView.setVisibility(View.GONE);
mContainerLayout.addView(view, FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT);
mContainerLayout.setVisibility(View.VISIBLE);
}

/**
* 常规方式:设置正常模式(非全屏)
*/
private void setNormalScreen() {
if (null == mVideoWebView || null == mContainerLayout) {
return;
}
mContainerLayout.removeAllViews();
mContainerLayout.setVisibility(View.GONE);
mVideoWebView.setVisibility(View.VISIBLE);
}

结论

实际使用中,这个官方方法对于腾讯视频是没有效果的,因为点击全屏按钮的时候,根本不会回调 onShowCustomView 和 onHideCustomView。

对于土豆、优酷的视频,我用多部 Android 5.0,6.0的测试机,验证是有效的。但是我用的 4.4.2 的 LG G2 测试的时候,就是没效果,不会回调。

这种方式不是太稳定,同个网站,不同设备,因为 Android 版本差异,WebView 内核版本差异,有些有效,有些无效..

JS 注入

实现方法

在页面加载完成的时候,注入 JS。这样在点击网页全屏按钮的时候,就能回调我们本地的方法了。

onPageFinished 注入 JS

VideoWebView.java

1
2
3
4
5
6
7
8
9
10
11
12
13
this.setWebViewClient(new CustomWebClient());

private class CustomWebClient extends WebViewClient {

@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
//页面加载完成的时候,注入js
String js= TagUtils.getJs(url);
view.loadUrl(js);
}
//.....
}

Tag 的获取方式

TagUtils.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public class TagUtils {

private static String getTagByUrl(String url) {
if (url.contains("qq")) {
if (url.contains("iframe")) {
//(全屏视频。通过网页【分享】- 【通用代码】)
// https://v.qq.com/iframe/player.html?vid=m0394wjagsq&tiny=0&auto=0
return "tvp_fullscreen_button";
} else {
// 普通网页界面
// https://v.qq.com/x/page/m0394wjagsq.html
return "txp_btn_fullscreen";
}
} else if (url.contains("bilibili")) {
return "icon-widescreen"; //http://www.bilibili.com/mobile/index.html
}
return "";
}

public static String getJs(String url) {
String tag = getTagByUrl(url);
if (TextUtils.isEmpty(tag)) {
return "javascript:";
} else {
return "javascript:document.getElementsByClassName('" + tag + "')[0].addEventListener('click',function(){onClickFullScreenBtn.fullscreen();return false;});";
}
}
}

这里全屏网页指的是,整个页面都是全屏的网页:

普通网页指的是:

以普通的网页为例,说下tag应该这样获取,

用 chrome 打开网页,右键打开 inspect ,选中全屏按钮,就可以定位到对于的 tag。

本地方法定义

  1. 打开 JS 调用许可

    VideoWebView.java

    1
    this.addJavascriptInterface(new VideoJsObject(), "onClickFullScreenBtn");
  2. 接口定义

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    private class VideoJsObject {

    /**
    * 从线程回调,要更新 UI 操作,要 post 到主线程
    */
    @JavascriptInterface
    public void fullscreen() {
    if (null != mOnVideoWebViewListener) {
    if (mFullScreenMode) {
    mOnVideoWebViewListener.onJsExitFullScreenMode();
    } else {
    mOnVideoWebViewListener.onJsEnterFullSceenMode();
    }
    mFullScreenMode = !mFullScreenMode; //重置全屏状态
    }
    }
    }
  3. 在 Activity 处设置监听,处理横竖屏逻辑。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class VideoWebViewListenerImp implements VideoWebView.OnVideoWebViewListener {

    @Override
    public void onJsEnterFullSceenMode() {
    setLandscape();
    }

    @Override
    public void onJsExitFullScreenMode() {
    setPortrait();
    }

    //.....
    }

结论

在使用中,该方法对于不同机型,不同Android版本是适用的,如果出现偶尔回调实效的情况的同学,可以尝试在加载网页的不同进度阶段 onProgressChanged 尝试注入。终于解决腾讯视频加载问题,休息一下,就到这里哈。

后续问题

白屏

原本还是可以的,突然间用 WebView 打开网页就白屏了,难道是 URL 对应的页面改了?
我用浏览器,打开发现是这样的显示:NET::ERR_CERT_COMMON_NAME_INVALID

细想了下,所在环境的网络是发生了变化,现在变成了能科学上网了。所以 SSL 认证是时候发生错误,认定为不安全,WebView 处理的是终止继续访问,所以出现了白屏。

这样的体验肯定是不好的,遇到这种情况我们可不可以监听得到呢?答案是可以的。

private boolean mIgnoreSslError; //是否忽略ssl证书错误

1
2
3
4
5
6
7
8
9
10
11
12
13
14
private class CustomWebClient extends WebViewClient {

@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (mIgnoreSslError) {
// let's ignore ssl error
handler.proceed();
} else {
super.onReceivedSslError(view, handler, error);
//不忽略,显示自定义错误界面
}
}
//....
}

可以使用 handler.proceed() 忽略 SSL 错误,让 WebView 继续加载这个网页,但是强制加载不一定成功,而且这种加载方式是不安全的,GooglePlay 上架很可能是不能通过安全验证。另外一种处理方法是,让 WebView 执行原本逻辑(停止加载),此时显示自定义的提示界面。

相关参考
[1]WebView实现全屏播放的一种方法
[2]WebView中的视频全屏4种方法,特别是腾讯视频,真正解决全屏问题
[3]android内存优化之webview
[4]Webview avoid security alert from google play upon implementation of onReceivedSslError

坚持原创技术分享,您的支持将鼓励我继续创作!