安卓下 webView 视频类代理的问题

需求是要通过 webView 实现代理,现在的问题是,在 Android5.0及以上的版本完全没有问题,但在5.0以下的版本中,除了视频类的(url 是 mp4),其他的比如音频,图片等都没问题可以走到代理服务器上,甚至mp4的有部分也能走到代理上去。
下面是访问视频类网站一个页面,在代理服务器上的日志:

安卓5.0的:

171.34.73.121 - - [26/Oct/2015:19:02:54 +0800] "GET http://tv.sohu.com/upload/touch/img/playWite.png HTTP/1.1" 200 3848 "http://tv.sohu.com/upload/touch/css/foxPlayer.min.20150915.css" "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/45.0.2454.95 Mobile Safari/537.36" TCP_CLIENT_REFRESH_MISS:HIER_DIRECT
171.34.73.121 - - [26/Oct/2015:19:02:55 +0800] "GET http://data.vod.itc.cn/? HTTP/1.1" 301 654 "http://m.tv.sohu.com/v2389588.shtml?aid=9008319&channeled=1210040001&columnid=58" "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/45.0.2454.95 Mobile Safari/537.36" TCP_MISS:HIER_DIRECT
171.34.73.121 - - [26/Oct/2015:19:02:55 +0800] "GET http://218.8.51.138/sohu/s26h23eab6/v1/TmPATmX3TCqYJZqiXlhgk3aZ9LhajkqAjHqCsCPytHrChWoIymcAr.mp4? HTTP/1.1" 206 409 "http://m.tv.sohu.com/v2389588.shtml?aid=9008319&channeled=1210040001&columnid=58" "Mozilla/5.0 (Linux; Android 5.1.1; Nexus 5 Build/LMY48M; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/45.0.2454.95 Mobile Safari/537.36" TCP_MISS:HIER_DIRECT
171.34.73.121 - - [26/Oct/2015:19:03:03 +0800] "GET http://218.8.51.138/sohu/s26h23eab6/v1/TmPATmX3TCqYJZqiXlhgk3aZ9LhajkqAjHqCsCPytHrChWoIymcAr.mp4? HTTP/1.1" [b]200 535861 "-" "stagefright/1.2 (Linux;Android 5.1.1)[/b]" TCP_MISS_ABORTED:HIER_DIRECT
171.34.73.121 - - [26/Oct/2015:19:03:33 +0800] "GET http://218.8.51.138/sohu/s26h23eab6/v1/TmPATmX3TCqYJZqiXlhgk3aZ9LhajkqAjHqCsCPytHrChWoIymcAr.mp4? HTTP/1.1" [b]206 4215269 "-" "stagefright/1.2 (Linux;Android 5.1.1)[/b]" TCP_MISS:HIER_DIRECT

安卓5.0以下的:

171.34.73.121 - - [26/Oct/2015:18:54:17 +0800] "GET http://data.vod.itc.cn/? HTTP/1.1" 301 656 "http://m.tv.sohu.com/v2389588.shtml?aid=9008319&channeled=1210040001&columnid=58" "Mozilla/5.0 (Linux; Android 4.4.4; N918St Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36" TCP_MISS:HIER_DIRECT
171.34.73.121 - - [26/Oct/2015:18:54:17 +0800] "GET http://218.8.51.129/sohu/s26h23eab6/v1/TmPCq6IOfOeHWOyGom4ChMbDWpEvN4KN8Fd4Dp83DYXUyYbSoO2VqVw.mp4? HTTP/1.1" 200 2997 "http://m.tv.sohu.com/v2389588.shtml?aid=9008319&channeled=1210040001&columnid=58" "Mozilla/5.0 (Linux; Android 4.4.4; N918St Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36" TCP_MISS_ABORTED:HIER_DIRECT
171.34.73.121 - - [26/Oct/2015:18:54:30 +0800] "GET http://z.m.tv.sohu.com/playtime.gif? HTTP/1.1" 204 267 "http://m.tv.sohu.com/v2389588.shtml?aid=9008319&channeled=1210040001&columnid=58" "Mozilla/5.0 (Linux; Android 4.4.4; N918St Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36" TCP_MISS:HIER_DIRECT
171.34.73.121 - - [26/Oct/2015:18:55:08 +0800] "GET http://data.vod.itc.cn/? HTTP/1.1" 301 654 "http://m.tv.sohu.com/v2389588.shtml?aid=9008319&channeled=1210040001&columnid=58" "Mozilla/5.0 (Linux; Android 4.4.4; N918St Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36" TCP_MISS:HIER_DIRECT
171.34.73.121 - - [26/Oct/2015:18:55:08 +0800] "GET http://60.18.224.46/sohu/s26h23eab6/v1/TmxUTmouq9Bmo3v7qlJdaaJ69aq3cZlR0pqmE6BgXpxWsm3UtUw.mp4? HTTP/1.1" 200 36301 "http://m.tv.sohu.com/v2389588.shtml?aid=9008319&channeled=1210040001&columnid=58" "Mozilla/5.0 (Linux; Android 4.4.4; N918St Build/KTU84P) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/33.0.0.0 Mobile Safari/537.36" TCP_MISS_ABORTED:HIER_DIRECT

可以看到没有走到代理的请求有两个特点:

1. ua 是stagefright/1.2

2. 大小比较大

代理的实现代码如下:

import android.annotation.SuppressLint;
import android.content.Context;
import android.content.Intent;
import android.net.Proxy;
import android.util.ArrayMap;
import android.util.Log;
import android.webkit.WebView;

import org.apache.http.HttpHost;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class ProxyUtils {

    private static final String LOG_TAG = "proxy";

    /**
     * Set Proxy for Android 3.2 and below.
     */
    @SuppressWarnings("all")
    public static boolean setProxyUpToHC(WebView webview, String host, int port) {
        Log.d(LOG_TAG, "Setting proxy with <= 3.2 API.");

        HttpHost proxyServer = new HttpHost(host, port);
        // Getting network
        Class networkClass = null;
        Object network = null;
        try {
            networkClass = Class.forName("android.webkit.Network");
            if (networkClass == null) {
                Log.e(LOG_TAG, "failed to get class for android.webkit.Network");
                return false;
            }
            Method getInstanceMethod = networkClass.getMethod("getInstance", Context.class);
            if (getInstanceMethod == null) {
                Log.e(LOG_TAG, "failed to get getInstance method");
            }
            network = getInstanceMethod.invoke(networkClass, new Object[]{webview.getContext()});
        } catch (Exception ex) {
            Log.e(LOG_TAG, "error getting network: " + ex);
            return false;
        }
        if (network == null) {
            Log.e(LOG_TAG, "error getting network: network is null");
            return false;
        }
        Object requestQueue = null;
        try {
            Field requestQueueField = networkClass
                    .getDeclaredField("mRequestQueue");
            requestQueue = getFieldValueSafely(requestQueueField, network);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "error getting field value");
            return false;
        }
        if (requestQueue == null) {
            Log.e(LOG_TAG, "Request queue is null");
            return false;
        }
        Field proxyHostField = null;
        try {
            Class requestQueueClass = Class.forName("android.net.http.RequestQueue");
            proxyHostField = requestQueueClass
                    .getDeclaredField("mProxyHost");
        } catch (Exception ex) {
            Log.e(LOG_TAG, "error getting proxy host field");
            return false;
        }

        boolean temp = proxyHostField.isAccessible();
        try {
            proxyHostField.setAccessible(true);
            proxyHostField.set(requestQueue, proxyServer);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "error setting proxy host");
        } finally {
            proxyHostField.setAccessible(temp);
        }

        Log.d(LOG_TAG, "Setting proxy with <= 3.2 API successful!");
        return true;
    }

    @SuppressWarnings("all")
    public static boolean setProxyICS(WebView webview, String host, int port) {
        try {
            Log.d(LOG_TAG, "Setting proxy with 4.0 API.");

            Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge");
            Class params[] = new Class[1];
            params[0] = Class.forName("android.net.ProxyProperties");
            Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params);

            Class wv = Class.forName("android.webkit.WebView");
            Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore");
            Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webview);

            Class wvc = Class.forName("android.webkit.WebViewCore");
            Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame");
            Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance);

            Class bf = Class.forName("android.webkit.BrowserFrame");
            Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge");
            Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame);

            Class ppclass = Class.forName("android.net.ProxyProperties");
            Class pparams[] = new Class[3];
            pparams[0] = String.class;
            pparams[1] = int.class;
            pparams[2] = String.class;
            Constructor ppcont = ppclass.getConstructor(pparams);

            updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null));

            Log.d(LOG_TAG, "Setting proxy with 4.0 API successful!");
            return true;
        } catch (Exception ex) {
            Log.e(LOG_TAG, "failed to set HTTP proxy: " + ex);
            return false;
        }
    }

    /**
     * Set Proxy for Android 4.1 - 4.3.
     */
    @SuppressWarnings("all")
    public static boolean setProxyJB(WebView webview, String host, int port) {
        Log.d(LOG_TAG, "Setting proxy with 4.1 - 4.3 API.");

        try {
            Class wvcClass = Class.forName("android.webkit.WebViewClassic");
            Class wvParams[] = new Class[1];
            wvParams[0] = Class.forName("android.webkit.WebView");
            Method fromWebView = wvcClass.getDeclaredMethod("fromWebView", wvParams);
            Object webViewClassic = fromWebView.invoke(null, webview);

            Class wv = Class.forName("android.webkit.WebViewClassic");
            Field mWebViewCoreField = wv.getDeclaredField("mWebViewCore");
            Object mWebViewCoreFieldInstance = getFieldValueSafely(mWebViewCoreField, webViewClassic);

            Class wvc = Class.forName("android.webkit.WebViewCore");
            Field mBrowserFrameField = wvc.getDeclaredField("mBrowserFrame");
            Object mBrowserFrame = getFieldValueSafely(mBrowserFrameField, mWebViewCoreFieldInstance);

            Class bf = Class.forName("android.webkit.BrowserFrame");
            Field sJavaBridgeField = bf.getDeclaredField("sJavaBridge");
            Object sJavaBridge = getFieldValueSafely(sJavaBridgeField, mBrowserFrame);

            Class ppclass = Class.forName("android.net.ProxyProperties");
            Class pparams[] = new Class[3];
            pparams[0] = String.class;
            pparams[1] = int.class;
            pparams[2] = String.class;
            Constructor ppcont = ppclass.getConstructor(pparams);

            Class jwcjb = Class.forName("android.webkit.JWebCoreJavaBridge");
            Class params[] = new Class[1];
            params[0] = Class.forName("android.net.ProxyProperties");
            Method updateProxyInstance = jwcjb.getDeclaredMethod("updateProxy", params);

            updateProxyInstance.invoke(sJavaBridge, ppcont.newInstance(host, port, null));
        } catch (Exception ex) {
            Log.e(LOG_TAG, "Setting proxy with >= 4.1 API failed with error: " + ex.getMessage());
            return false;
        }

        Log.d(LOG_TAG, "Setting proxy with 4.1 - 4.3 API successful!");
        return true;
    }

    @SuppressLint("NewApi")
    @SuppressWarnings("all")
    public static boolean setProxyKKPlus(WebView webView, String host, int port, String applicationClassName) {
        Log.d(LOG_TAG, "Setting proxy with >= 4.4 API.");

        Context appContext = webView.getContext().getApplicationContext();
        System.setProperty("http.proxyHost", host);
        System.setProperty("http.proxyPort", port + "");
        System.setProperty("https.proxyHost", host);
        System.setProperty("https.proxyPort", port + "");
        try {
            Class applictionCls = Class.forName(applicationClassName);
            Field loadedApkField = applictionCls.getField("mLoadedApk");
            loadedApkField.setAccessible(true);
            Object loadedApk = loadedApkField.get(appContext);
            Class loadedApkCls = Class.forName("android.app.LoadedApk");
            Field receiversField = loadedApkCls.getDeclaredField("mReceivers");
            receiversField.setAccessible(true);
            ArrayMap receivers = (ArrayMap) receiversField.get(loadedApk);
            for (Object receiverMap : receivers.values()) {
                for (Object rec : ((ArrayMap) receiverMap).keySet()) {
                    Class clazz = rec.getClass();
                    if (clazz.getName().contains("ProxyChangeListener")) {
                        Method onReceiveMethod = clazz.getDeclaredMethod("onReceive", Context.class, Intent.class);
                        Intent intent = new Intent(Proxy.PROXY_CHANGE_ACTION);

                            onReceiveMethod.invoke(rec, appContext, intent);
                    }
                }
            }
            Log.d(LOG_TAG, "Setting proxy with >= 4.4 API successful!");
            return true;
        } catch (ClassNotFoundException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String exceptionAsString = sw.toString();
            Log.v(LOG_TAG, e.getMessage());
            Log.v(LOG_TAG, exceptionAsString);
        } catch (NoSuchFieldException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String exceptionAsString = sw.toString();
            Log.v(LOG_TAG, e.getMessage());
            Log.v(LOG_TAG, exceptionAsString);
        } catch (IllegalAccessException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String exceptionAsString = sw.toString();
            Log.v(LOG_TAG, e.getMessage());
            Log.v(LOG_TAG, exceptionAsString);
        } catch (IllegalArgumentException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String exceptionAsString = sw.toString();
            Log.v(LOG_TAG, e.getMessage());
            Log.v(LOG_TAG, exceptionAsString);
        } catch (NoSuchMethodException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String exceptionAsString = sw.toString();
            Log.v(LOG_TAG, e.getMessage());
            Log.v(LOG_TAG, exceptionAsString);
        } catch (InvocationTargetException e) {
            StringWriter sw = new StringWriter();
            e.printStackTrace(new PrintWriter(sw));
            String exceptionAsString = sw.toString();
            Log.v(LOG_TAG, e.getMessage());
            Log.v(LOG_TAG, exceptionAsString);
        } /*catch (InstantiationException e) {
            e.printStackTrace();
            Log.v(LOG_TAG, e.getMessage());
        }*/
        return false;
    }

    private static Object getFieldValueSafely(Field field, Object classInstance) throws IllegalArgumentException, IllegalAccessException {
        boolean oldAccessibleValue = field.isAccessible();
        field.setAccessible(true);
        Object result = field.get(classInstance);
        field.setAccessible(oldAccessibleValue);
        return result;
    }

    @SuppressWarnings("all")
    public static boolean setProxyHostField(HttpHost proxyServer) {
        // Getting network
        Class networkClass = null;
        Object network = null;
        try {
            networkClass = Class.forName("android.webkit.Network");
            Field networkField = networkClass.getDeclaredField("sNetwork");
            network = getFieldValueSafely(networkField, null);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "error getting network");
            return false;
        }
        if (network == null) {
            Log.e(LOG_TAG, "error getting network : null");
            return false;
        }
        Object requestQueue = null;
        try {
            Field requestQueueField = networkClass
                    .getDeclaredField("mRequestQueue");
            requestQueue = getFieldValueSafely(requestQueueField, network);
        } catch (Exception ex) {
            Log.e(LOG_TAG, "error getting field value");
            return false;
        }
        if (requestQueue == null) {
            Log.e(LOG_TAG, "Request queue is null");
            return false;
        }
        Field proxyHostField = null;
        try {
            Class requestQueueClass = Class.forName("android.net.http.RequestQueue");
            proxyHostField = requestQueueClass
                    .getDeclaredField("mProxyHost");
        } catch (Exception ex) {
            Log.e(LOG_TAG, "error getting proxy host field");
            return false;
        }
        //synchronized (synchronizer) {
            boolean temp = proxyHostField.isAccessible();
            try {
                proxyHostField.setAccessible(true);
                proxyHostField.set(requestQueue, proxyServer);
            } catch (Exception ex) {
                Log.e(LOG_TAG, "error setting proxy host");
            } finally {
                proxyHostField.setAccessible(temp);
            }
        //}
        return true;
    }
}

google 了很久都说是安卓版本的问题,导致对html5的 video 标签不能正确解析导致的。

现在的问题是

1. 难道安卓5.0以下的视频代理真的没有办法了?

2. 如果代理的问题无法解决,怎样通过代码可以判断当前页面中的视频无法做到代理给用户提示?这其实又是两个问题:

2.1 怎么判断页面中有video标签?因为有的视频页面是通过 js 加载<video>的,所有解析 html 也没用
2.2 怎么判断代理失败了?测试的时候发现5.0和4.4 的走的是同样的代码,但结果不同。有的手机在后台会有异常:MediaPlay 的 setDatasource 方法会抛出 IOException,但有的手机没有异常。
2015-10-29 10:25300