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。这样代理问题就解决了。
流程图:网络流提取的决策路径
下面这张图总结了我平时做网络流提取时的决策逻辑。你可以根据实际场景选择路线。
避坑指南:我踩过的三个大坑
- 坑一: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