首页 > 代码库 > Android HTML5 audio autoplay无效问题

Android HTML5 audio autoplay无效问题

转载请标明出处:
http://blog.csdn.net/xuehuayous/article/details/52474888
本文出自:【Kevin.zhou的博客】

前言:在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);

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

<script type="text/javascript"> $(function () { $(‘pre.prettyprint code‘).each(function () { var lines = $(this).text().split(‘\n‘).length; var $numbering = $(‘
    ‘).addClass(‘pre-numbering‘).hide(); $(this).addClass(‘has-numbering‘).parent().append($numbering); for (i = 1; i <= lines; i++) { $numbering.append($(‘
  • ‘).text(i)); }; $numbering.fadeIn(1700); }); }); </script>

    Android HTML5 audio autoplay无效问题