关于Android HTML5 audio autoplay无效问题的解决方案,html5 autoplay

3
回复
1046
查看
[复制链接]

451

主题

1164

帖子

1957

安币

手工艺人

发表于 2017-12-28 11:15:15 | 显示全部楼层 |阅读模式

        前言:在android html5 开发中有不少人遇到过 audio 标签 autoplay在某些设备上无效的问题,网上大多是讲怎么在js中操作,即在特定的时刻调用audio的play()方法,在android上还是无效。

        一、解决方案

        在android 4.2添加了允许用户手势触发音视频播放接口,该接口默认为 true ,即默认不允许自动播放音视频,只能是用户交互的方式由用户自己促发播放。

[Java] 查看源文件 复制代码
webview webview = this.finishactivity(r.id.main_act_webview);
// ... ...
// 其他配置
// ... ...
// 设置4.2以后版本支持autoplay,非用户手势促发
if (build.version.sdk_int >= build.version_codes.jelly_bean_mr1) {
webview.getsettings().setmediaplaybackrequiresusergesture(false);
}

        通过以上配置就可以加载带有自动播放的音视频啦!

        二、 源码分析

        下面我们沿着该问题来窥探下webview的系统源码:

        1、 通过getsettings()获取到的webview的配置

[Java] 查看源文件 复制代码
/**
* gets the websettings object used to control the settings for this
* webview.
*
* @return a websettings object that can be used to control this webview's
* settings
*/
public websettings getsettings() {
checkthread();
return mprovider.getsettings();
}

        这里通过一个 mprovider来获取的配置信息,通过看webview的源码,我们可以看到,webview的所有操作都是交给 mprovider来进行的。

        2、 mpeovider是在哪初始化的?

[Java] 查看源文件 复制代码
/**
* @hide
*/
@suppresswarnings("deprecation") // for super() call into deprecated base class constructor.
protected webview(context context, attributeset attrs, int defstyleattr, int defstyleres,
map<string, object> javascriptinterfaces, boolean privatebrowsing) {
super(context, attrs, defstyleattr, defstyleres);
if (context == null) {
throw new illegalargumentexception("invalid context argument");
}
senforcethreadchecking = context.getapplicationinfo().targetsdkversion >=
build.version_codes.jelly_bean_mr2;
checkthread();
ensureprovidercreated();
mprovider.init(javascriptinterfaces, privatebrowsing);
// post condition of creating a webview is the cookiesyncmanager.getinstance() is allowed.
cookiesyncmanager.setgetinstanceisallowed();
}

        可以看到有个ensureprovidercreated()方法,就是在这里创建的mprovider:

[Java] 查看源文件 复制代码
private void ensureprovidercreated() {
checkthread();
if (mprovider == null) {
// as this can get called during the base class constructor chain, pass the minimum
// number of dependencies here; the rest are deferred to init().
mprovider = getfactory().createwebview(this, new privateaccess());
}
}

        ok,到此知道了mprovider是在webview的构造函数中创建的,并且webview的所有操作都是交给mprovider进行的。

        3、 但是这个mpeovider到底是谁派来的呢?

        看下webviewfactory#getfactory()做了什么操作:

[Java] 查看源文件 复制代码
static webviewfactoryprovider getprovider() {
synchronized (sproviderlock) {
// for now the main purpose of this function (and the factory abstraction) is to keep
// us honest and minimize usage of webview internals when binding the proxy.
if (sproviderinstance != null) return sproviderinstance;
final int uid = android.os.process.myuid();
if (uid == android.os.process.root_uid || uid == android.os.process.system_uid) {
throw new unsupportedoperationexception(
"for security reasons, webview is not allowed in privileged processes");
}
trace.tracebegin(trace.trace_tag_webview, "webviewfactory.getprovider()");
try {
class<webviewfactoryprovider> providerclass = getproviderclass();
strictmode.threadpolicy oldpolicy = strictmode.allowthreaddiskreads();
trace.tracebegin(trace.trace_tag_webview, "providerclass.newinstance()");
try {
sproviderinstance = providerclass.getconstructor(webviewdelegate.class)
.newinstance(new webviewdelegate());
if (debug) log.v(logtag, "loaded provider: " + sproviderinstance);
return sproviderinstance;
} catch (exception e) {
log.e(logtag, "error instantiating provider", e);
throw new androidruntimeexception(e);
} finally {
trace.traceend(trace.trace_tag_webview);
strictmode.setthreadpolicy(oldpolicy);
}
} finally {
trace.traceend(trace.trace_tag_webview);
}
}
}

        可见在23行返回了sproviderinstance, 是由 providerclass 通过反射创建的,15行中通过getproviderclass() 得到了providerclass.

[Java] 查看源文件 复制代码
private static class<webviewfactoryprovider> getproviderclass() {
try {
// first fetch the package info so we can log the webview package version.
spackageinfo = fetchpackageinfo();
log.i(logtag, "loading " + spackageinfo.packagename + " version " +
spackageinfo.versionname + " (code " + spackageinfo.versioncode + ")");
trace.tracebegin(trace.trace_tag_webview, "webviewfactory.loadnativelibrary()");
loadnativelibrary();
trace.traceend(trace.trace_tag_webview);
trace.tracebegin(trace.trace_tag_webview, "webviewfactory.getchromiumproviderclass()");
try {
return getchromiumproviderclass();
} catch (classnotfoundexception e) {
log.e(logtag, "error loading provider", e);
throw new androidruntimeexception(e);
} finally {
trace.traceend(trace.trace_tag_webview);
}
} catch (missingwebviewpackageexception e) {
// if the package doesn't exist, then try loading the null webview instead.
// if that succeeds, then this is a device without webview support; if it fails then
// swallow the failure, complain that the real webview is missing and rethrow the
// original exception.
try {
return (class<webviewfactoryprovider>) class.forname(null_webview_factory);
} catch (classnotfoundexception e2) {
// ignore.
}
log.e(logtag, "chromium webview package does not exist", e);
throw new androidruntimeexception(e);
}
}

        主要的 14行 返回了一个 getchromiumproviderclass(); 是不是有点熟悉,没错android在4.4开始使用强大的chromium替换掉了原来的webkit。来看下这个getchromiumproviderclass()。

[Java] 查看源文件 复制代码
// throws missingwebviewpackageexception
private static class<webviewfactoryprovider> getchromiumproviderclass()
throws classnotfoundexception {
application initialapplication = appglobals.getinitialapplication();
try {
// construct a package context to load the java code into the current app.
context webviewcontext = initialapplication.createpackagecontext(
spackageinfo.packagename,
context.context_include_code | context.context_ignore_security);
initialapplication.getassets().addassetpath(
webviewcontext.getapplicationinfo().sourcedir);
classloader clazzloader = webviewcontext.getclassloader();
trace.tracebegin(trace.trace_tag_webview, "class.forname()");
try {
return (class<webviewfactoryprovider>) class.forname(chromium_webview_factory, true,
clazzloader);
} finally {
trace.traceend(trace.trace_tag_webview);
}
} catch (packagemanager.namenotfoundexception e) {
throw new missingwebviewpackageexception(e);
}
}

        最后找到了这个 chromium_webview_factory, 可以看到在 webviewfactory 中的定义:

[Java] 查看源文件 复制代码
private static final string chromium_webview_factory =
"com.android.webview.chromium.webviewchromiumfactoryprovider";

        回答2小节的mprovider的初始化,在webviewchromiumfactoryprovider 的 createwebview(…) 中进行了mprovider的初始化:

[Java] 查看源文件 复制代码
@override
public webviewprovider createwebview(webview webview, webview.privateaccess privateaccess) {
webviewchromium wvc = new webviewchromium(this, webview, privateaccess);
synchronized (mlock) {
if (mwebviewstostart != null) {
mwebviewstostart.add(new weakreference<webviewchromium>(wvc));
}
}
resourceprovider.registerresources(webview.getcontext());
return wvc;
}

        ok,到这里就真正找到了mprovider 的真正初始化位置,其实它就是一个webviewchromium,不要忘了我们为什么费这么大劲找mprovider,其实是为了分析 webview.getsettings(),这样就回到了第一小节,通过getsettings()获取到的webview的配置。

        4、 settings的初始化

        通过第一小节,我们知道settings是mprovider的一个变量,要想找到settings就要到 webviewchromium 来看下:

[Java] 查看源文件 复制代码
@override
public websettings getsettings() {
return mwebsettings;
}

        接下来就是settings初始化的地方啦

[Java] 查看源文件 复制代码
@override
// bug=6790250 |javascriptinterfaces| was only ever used by the obsolete dumprendertree
// so is ignored. todo: remove it from webviewprovider.
public void init(final map<string, object> javascriptinterfaces,
final boolean privatebrowsing) {
if (privatebrowsing) {
mfactory.startyourengines(true);
final string msg = "private browsing is not supported in webview.";
if (mapptargetsdkversion >= build.version_codes.kitkat) {
throw new illegalargumentexception(msg);
} else {
log.w(tag, msg);
textview warninglabel = new textview(mwebview.getcontext());
warninglabel.settext(mwebview.getcontext().getstring(
com.android.internal.r.string.webviewchromium_private_browsing_warning));
mwebview.addview(warninglabel);
}
}
// we will defer real initialization until we know which thread to do it on, unless:
// - we are on the main thread already (common case),
// - the app is targeting >= jb mr2, in which case checkthread enforces that all usage
// comes from a single thread. (note in jb mr2 this exception was in webview.java).
if (mapptargetsdkversion >= build.version_codes.jelly_bean_mr2) {
mfactory.startyourengines(false);
checkthread();
} else if (!mfactory.hasstarted()) {
if (looper.mylooper() == looper.getmainlooper()) {
mfactory.startyourengines(true);
}
}
final boolean isaccessfromfileurlsgrantedbydefault =
mapptargetsdkversion < build.version_codes.jelly_bean;
final boolean arelegacyquirksenabled =
mapptargetsdkversion < build.version_codes.kitkat;
mcontentsclientadapter = new webviewcontentsclientadapter(mwebview);
mwebsettings = new contentsettingsadapter(new awsettings(
mwebview.getcontext(), isaccessfromfileurlsgrantedbydefault,
arelegacyquirksenabled));
mrunqueue.addtask(new runnable() {
@override
public void run() {
initforreal();
if (privatebrowsing) {
// intentionally irreversibly disable the webview instance, so that private
// user data cannot leak through misuse of a non-privatebrowing webview
// instance. can't just null out mawcontents as we never null-check it
// before use.
destroy();
}
}
});
}

        在第39行进行了 mwebsettings 的初始化,原来是 contentsettingsadapter。

        5、 setmediaplaybackrequiresusergesture() 分析

        经过以上我们队google大神的膜拜,我们找到了mwebsettings,下面来看下 setmediaplaybackrequiresusergesture方法:

[Java] 查看源文件 复制代码
@override
public void setmediaplaybackrequiresusergesture(boolean require) {
mawsettings.setmediaplaybackrequiresusergesture(require);
}

        好吧,又是调用的 mawsettings 的 setmediaplaybackrequiresusergesture 方法,那 mawsettings 是什么呢?

[Java] 查看源文件 复制代码
public contentsettingsadapter(awsettings awsettings) {
mawsettings = awsettings;
}

        原来是在构造函数中注入的,回到第4小节的最后,这里 new 了一个awsettings。

[Java] 查看源文件 复制代码
mwebsettings = new contentsettingsadapter(new awsettings(
mwebview.getcontext(), isaccessfromfileurlsgrantedbydefault,
arelegacyquirksenabled));

        那么久来 awsettings 中看下 setmediaplaybackrequiresusergesture 吧:

        该类位于系统源码 external/

[Java] 查看源文件 复制代码
/**
* see {@link android.webkit.websettings#setmediaplaybackrequiresusergesture}.
*/
public void setmediaplaybackrequiresusergesture(boolean require) {
synchronized (mawsettingslock) {
if (mmediaplaybackrequiresusergesture != require) {
mmediaplaybackrequiresusergesture = require;
meventhandler.updatewebkitpreferenceslocked();
}
}
}

        可以看到这里只是给一个变量 mmediaplaybackrequiresusergesture 设置了值,然后看到下面一个方法,豁然开朗:

[Java] 查看源文件 复制代码
@calledbynative
private boolean getmediaplaybackrequiresusergesturelocked() {
return mmediaplaybackrequiresusergesture;
}

        该方法是由jni层调用的,external/

[Java] 查看源文件 复制代码
web_prefs->user_gesture_required_for_media_playback =
java_awsettings_getmediaplaybackrequiresusergesturelocked(env, obj);

        可见在内核中去调用该接口,判断是否允许音视频的自动播放。


4

主题

9693

帖子

795

安币

代码手工艺人

Rank: 4

发表于 2017-12-29 22:39:26 | 显示全部楼层
帮帮顶顶!!

0

主题

9243

帖子

2066

安币

Android大神

Rank: 6Rank: 6

发表于 2017-12-31 08:38:14 | 显示全部楼层
支持,感谢,祝巴士越来越好~

2

主题

9664

帖子

2092

安币

Android大神

Rank: 6Rank: 6

QQ达人

发表于 2018-1-1 12:53:34 | 显示全部楼层
支持楼主,支持安卓巴士!
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

领先的中文移动开发者社区
18620764416
7*24全天服务
意见反馈:1294855032@qq.com

扫一扫关注我们

Powered by Discuz! X3.2© 2001-2019 Comsenz Inc.( 粤ICP备15117877号 )