19、从网络流提取:MediaExtractor支持HTTP/HTTPS流吗?代理设置

说实话,这个问题我当年刚接触MediaExtractor时也纠结过。那时候项目急着上线,产品经理说“视频要从服务器直接拉”,我心想:MediaExtractor不是只能读本地文件吗?后来踩了一圈坑才发现——嗯,它其实能读网络流,但方式跟你想象的可能不太一样。

MediaExtractor到底能不能直接读HTTP/HTTPS?

直接回答:不能。MediaExtractor的setDataSource方法,如果你传一个“https://...”的字符串进去,在Android 4.4以下会直接抛异常。从Android 4.4(API 19)开始,Google才加入了网络URL的支持。

但别高兴太早。我测试过,即使API级别够了,很多机型仍然会失败。为什么?因为MediaExtractor底层依赖的是系统自带的媒体解析器,不同厂商的ROM实现差异很大。我曾经在小米6上跑得好好的,换到华为P30就报“无法打开文件”。

核心结论:MediaExtractor的setDataSource(String path)虽然支持HTTP/HTTPS,但稳定性堪忧。生产环境中,我强烈建议你走“下载到本地再解析”的路线。

那网络流提取的正确姿势是什么?

我个人习惯的做法是:先用HttpURLConnection或OkHttp把视频文件下载到应用的缓存目录,然后再用MediaExtractor去读本地文件。你可能会问:“这样不是多了一步吗?效率会不会低?”

其实不然。你想想看,MediaExtractor在解析视频时,需要频繁地seek到不同位置去读关键帧信息。如果直接读网络流,每次seek都要重新发起HTTP请求,那才叫慢。本地文件读取是毫秒级的,网络请求是百毫秒甚至秒级的。

不过,如果你非要实时流式处理,也不是没办法。Android提供了另一个API:setDataSource(MediaDataSource)。这个接口允许你自定义数据源,你可以把网络流包装成一个MediaDataSource实现类。

实战:用MediaDataSource包装网络流

下面是我项目中用过的一个简化版实现。注意,这里只演示核心逻辑,生产环境需要加上断点续传、重试机制等。

public class HttpMediaDataSource extends MediaDataSource {
    private InputStream inputStream;
    private long totalSize = -1;
    
    public HttpMediaDataSource(String url) throws IOException {
        HttpURLConnection conn = (HttpURLConnection) new URL(url).openConnection();
        conn.setRequestMethod("GET");
        conn.connect();
        
        // 获取文件总大小(服务器必须返回Content-Length)
        totalSize = conn.getContentLengthLong();
        inputStream = conn.getInputStream();
    }
    
    @Override
    public int readAt(long position, byte[] buffer, int offset, int size) throws IOException {
        // 这里需要实现随机访问,但网络流默认不支持seek
        // 实际项目中需要维护一个缓冲区,或者重新建立连接
        // 简化处理:只支持顺序读取
        return inputStream.read(buffer, offset, size);
    }
    
    @Override
    public long getSize() throws IOException {
        return totalSize;
    }
    
    @Override
    public void close() throws IOException {
        if (inputStream != null) {
            inputStream.close();
        }
    }
}

注意:上面的代码只是一个演示骨架。readAt方法要实现真正的随机访问,你需要自己管理一个缓存池,或者每次seek时重新建立HTTP连接并设置Range头。我曾经在这个坑里爬了三天——直接传InputStream给MediaExtractor,结果它一调用readAt(0, ...)读完开头,接着就跳到文件中间去读moov box了,而我的流还停在开头位置。

代理设置:企业内网必备技能

说到代理,我记得有一次在银行项目里,所有网络请求都必须经过公司代理服务器。那时候MediaExtractor直接读URL肯定不行,因为系统底层不会自动走代理。

解决方案其实很简单:自己管理代理连接。你可以在创建HttpURLConnection时手动设置代理:

Proxy proxy = new Proxy(Proxy.Type.HTTP, 
    new InetSocketAddress("proxy.company.com", 8080));
HttpURLConnection conn = (HttpURLConnection) 
    new URL(url).openConnection(proxy);
// 如果需要代理认证
String auth = "username:password";
String encoded = Base64.encodeToString(auth.getBytes(), Base64.NO_WRAP);
conn.setRequestProperty("Proxy-Authorization", "Basic " + encoded);

然后把包装好的InputStream传给MediaDataSource。这样代理问题就解决了。

流程图:网络流提取的决策路径

下面这张图总结了我平时做网络流提取时的决策逻辑。你可以根据实际场景选择路线。

网络流提取决策流程图 开始:网络视频URL API ≥ 19? 且厂商支持? 直接setDataSource (不推荐生产环境) 需要实时流? 实现MediaDataSource 手动管理代理+缓存 下载到本地缓存 支持断点续传 MediaExtractor开始工作

避坑指南:我踩过的三个大坑

  • 坑一:Content-Length缺失。有些视频服务器不返回文件总大小,MediaDataSource的getSize()返回-1,MediaExtractor直接罢工。我曾经对接一个CDN,对方说“我们流式传输不提供大小”,结果我不得不先发一个HEAD请求去获取大小。
  • 坑二:HTTPS证书问题。自签名证书的服务器,MediaExtractor直接读URL会报SSLException。解决方案还是走MediaDataSource,自己用HttpsURLConnection设置自定义TrustManager。
  • 坑三:重定向。很多视频链接会302跳转,MediaExtractor的setDataSource(String)默认不跟随重定向。我建议你在包装MediaDataSource之前,先用OkHttp或HttpURLConnection手动处理完重定向,拿到最终的InputStream。

性能对比:本地 vs 网络直读

方案 首次打开速度 seek响应 稳定性 推荐场景
直接setDataSource(URL) 快(无需下载) 极慢(每次seek发请求) 低(厂商差异大) 不推荐
MediaDataSource包装 中等(需建立连接) 依赖缓存策略 中(需自己处理异常) 实时直播流
下载到本地 慢(需等待下载) 快(本地随机读取) 高(系统原生支持) 点播、非实时

我的建议:除非你的场景是直播流且必须实时解析,否则一律走“下载到本地”路线。你想想看,用户看视频本来就要缓冲,你先把文件下载到缓存目录,MediaExtractor解析完第一帧后,播放器可以直接从本地读,体验反而更好。

最后说一句,代理设置这块,如果你用的是OkHttp,设置代理会更优雅。OkHttp的Builder可以直接设置proxy和proxyAuthenticator,然后你把OkHttp的ResponseBody转换成InputStream,再包装成MediaDataSource。这样代码更简洁,也更容易维护。

好了,关于网络流提取和代理设置,我就分享这么多。记住一句话:能用本地就别用网络,非用网络就自己管连接


公众号:蓝海资料掘金营,微信deep3321