关于Android HTML5 audio autoplay无效问题的解决方案
前言:在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无效问题的解决方案,希望对大家有所帮助