欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

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

程序员文章站 2024-03-05 20:20:19
前言:在android html5 开发中有不少人遇到过 audio 标签 autoplay在某些设备上无效的问题,网上大多是讲怎么在js中操作,即在特定的时刻调用audi...

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

一、解决方案

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

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的配置

/**
* 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是在哪初始化的?

/**
* @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:

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()做了什么操作:

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.

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()。

// 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 中的定义:

private static final string chromium_webview_factory =
"com.android.webview.chromium.webviewchromiumfactoryprovider";

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

@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 来看下:

@override
public websettings getsettings() {
return mwebsettings;
}

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

@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方法:

@override
public void setmediaplaybackrequiresusergesture(boolean require) {
mawsettings.setmediaplaybackrequiresusergesture(require);
}

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

public contentsettingsadapter(awsettings awsettings) {
mawsettings = awsettings;
}

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

mwebsettings = new contentsettingsadapter(new awsettings(
mwebview.getcontext(), isaccessfromfileurlsgrantedbydefault,
arelegacyquirksenabled));

那么久来 awsettings 中看下 setmediaplaybackrequiresusergesture 吧:

该类位于系统源码 external/​chromium_org/​android_webview/​java/​src/​org/​chromium/​android_webview/​awsettings.java

/**
* see {@link android.webkit.websettings#setmediaplaybackrequiresusergesture}.
*/
public void setmediaplaybackrequiresusergesture(boolean require) {
synchronized (mawsettingslock) {
if (mmediaplaybackrequiresusergesture != require) {
mmediaplaybackrequiresusergesture = require;
meventhandler.updatewebkitpreferenceslocked();
}
}
}

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

@calledbynative
private boolean getmediaplaybackrequiresusergesturelocked() {
return mmediaplaybackrequiresusergesture;
}

该方法是由jni层调用的,external/​chromium_org/​android_webview/native/aw_settings.cc 中我们看到了:

web_prefs->user_gesture_required_for_media_playback =
java_awsettings_getmediaplaybackrequiresusergesturelocked(env, obj);

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

以上所述是小编给大家介绍的关于android html5 audio autoplay无效问题的解决方案,希望对大家有所帮助