本文来源于一次老旧 Android 大屏的视频播放改造。需求看起来很普通:网页打开后自动播放宣传视频,播完一条自动播放下一条,并且保留声音。
真正实施以后,问题却横跨了视频编码、MP4 容器、浏览器缓冲、HTTP Range、IIS 反向代理、HLS 分段和终端硬件解码能力。
最终的稳定方案不是继续降低 MP4 清晰度,而是:
统一转码为保守的 H.264 Baseline
+ AAC-LC
+ 4 秒 HLS v3 / MPEG-TS 分段
+ IIS 静态文件直出
+ 横竖屏提前合成为单路 1920×1080 视频这篇文章只记录 Android 4.4 网页视频播放本身,不涉及具体业务系统。
一、现场设备
终端是一台 65 英寸、16:9 的安卓大屏,硬件大致为:
Android 4.4.4;
1 GB 运行内存;
8 GB 内部存储;
2018 年前后的八核 ARM 平台;
1920×1080 屏幕;
使用自研 Android 应用内置的 WebView 加载网页。
这种设备的问题不只是“系统版本旧”。真正薄弱的是整条媒体链路:旧 WebView、旧版 MediaPlayer、有限的硬件解码器、很小的内存,以及 Android 4.4 媒体栈对网络流的兼容差异。
本地 Windows 浏览器播放正常,并不能证明大屏能够播放。
二、最初的故障现象
最开始直接给 <video> 设置 MP4 地址:
<video autoplay preload="auto" playsinline webkit-playsinline></video>实际表现是:
页面能够正常打开;
视频可以自动播放并且有声音;
播放大约 2~5 秒后,画面停止;
页面上的时钟仍然每秒更新;
视频进入
waiting或stalled状态;有时黑屏,只能听到前两秒声音;
同一个文件在 Windows 电脑上完全正常。
这个现象首先排除了“整个网页卡死”。JavaScript 定时器仍在运行,出问题的是媒体加载或解码链路。
三、第一轮尝试:降低 MP4 分辨率和码率
一开始怀疑 1080P 对旧设备压力太大,于是依次尝试:
1080P 降码率;
720P;
480P;
H.264 Baseline;
关闭 B 帧;
降低参考帧数量;
固定 25 fps;
限制峰值码率;
将 MP4 的
moov信息移动到文件头部。
用于验证兼容性的 480P 命令如下:
ffmpeg -i input.mp4 -vf "scale=854:480:flags=lanczos,fps=25" -c:v libx264 -preset medium -profile:v baseline -level:v 3.0 -pix_fmt yuv420p -b:v 500k -maxrate 600k -bufsize 300k -refs 1 -bf 0 -g 50 -keyint_min 50 -sc_threshold 0 -c:a aac -b:a 96k -ar 44100 -ac 2 -movflags +faststart -brand isom -map_metadata -1 output_android44.mp4这份 480P 文件终于能够在真机上连续播放,说明问题确实与媒体规格有关。
但它只能算诊断结果,不能算最终方案。65 英寸屏幕播放 854×480,近距离观看明显发糊。继续把分辨率升回 720P 或 1080P 后,哪怕严格限制码率,旧 WebView 仍可能在几秒后停滞。
四、不要只看“分辨率”和“平均码率”
两个都标为“720P、H.264、500 Kbps”的文件,兼容性可能完全不同。至少还要检查:
H.264 Profile 和 Level;
像素格式是否为
yuv420p;是否包含 B 帧;
参考帧数量;
GOP 长度和关键帧间隔;
是恒定帧率还是可变帧率;
瞬时峰值码率;
音频是否为 AAC-LC;
MP4 major brand;
moov是否位于文件前部。
可以先用 ffprobe 检查素材:
ffprobe -v error -show_entries stream=index,codec_name,profile,pix_fmt,width,height,r_frame_rate,avg_frame_rate,level -show_entries format=format_name,duration,bit_rate -of json input.mov现场曾出现过这样的情况:某些 HandBrake 生成的 mp42 文件在电脑上正常,在旧 WebView 中持续停滞;换成 FFmpeg/Lavf 输出的 isom 容器后,低规格文件可以播放。
这不代表 mp42 天生有问题,而是提醒我们:旧设备的兼容性不能只靠文件扩展名判断。
五、第二轮尝试:检查 HTTP Range 和 IIS 代理
MP4 渐进播放依赖字节范围请求。可以使用 curl 检查服务器是否正确响应:
curl.exe -r 0-1023 -D - -o NUL "https://example.com/videos/test.mp4"理想响应至少应包含:
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 1024
Content-Range: bytes 0-1023/文件总大小
Content-Type: video/mp4最初的视频经过 IIS ARR 转发时,虽然返回了 206 Partial Content 和正确的 Content-Range,但响应使用了:
Transfer-Encoding: chunked这对现代浏览器通常不是问题,但旧版 Android 媒体栈对“Range 响应再叠加分块传输”的容错能力很差。
后来把视频目录改成 IIS 静态文件直出,响应变成明确的 Content-Length。这是必要优化,但真机依然会在约两秒后卡住。
这个结果非常关键:
Range 响应正确,是 MP4 稳定播放的必要条件,但不是充分条件。服务器传输方式有风险,终端对长 MP4 的缓冲、跳转或解码也同样有问题。
六、第三轮尝试:整文件下载到内存后播放
为了绕开 Range 和代理问题,还尝试过先用 XHR 下载整个 MP4,转成 Blob URL,再交给 <video> 播放。
结果是:
页面可以显示缓存进度;
文件能够缓存到 100%;
真正播放时仍然黑屏;
一秒左右后再次停滞。
而且这条路对 1 GB 内存的终端非常危险。一个几十 MB 的视频可能同时存在:
网络响应缓冲;
JavaScript ArrayBuffer;
Blob;
媒体解码缓冲;
视频帧缓存。
即使没有立刻崩溃,也可能触发频繁 GC、WebView 被系统回收或后续视频无法加载。
因此最终明确放弃整文件 Blob 缓存。
七、真正稳定的方向:HLS 短分段
反复降低 MP4 参数仍不稳定后,最终改成:
HLS v3
+ MPEG-TS
+ 4 秒一个分段
+ VOD 完整播放列表与一个几十 MB 的 MP4 相比,HLS 把视频拆成许多独立小文件:
index.m3u8
segment_0000.ts
segment_0001.ts
segment_0002.ts
...它的优势是:
每次只请求一个很小的 TS 分段;
不依赖长文件中的频繁 Range 跳转;
终端缓冲量更可控;
单个请求失败时恢复成本更低;
不需要把完整视频放进 JavaScript 内存;
很适合顺序播放的宣传视频。
在这台 Android 4.4 真机上,HLS 分段比同规格的完整 MP4 明显稳定。
这不是说所有 Android 4.4 都一定支持 HLS。不同厂商的 WebView 和媒体组件差异很大,最终仍然必须真机验证。
八、最终采用的编码参数
最终输出统一为:
画布: 1920×1080
帧率: 25 fps,恒定帧率
视频编码: H.264 Baseline
Level: 4.0
像素格式: yuv420p
平均视频码率: 3500 Kbps
峰值码率: 4200 Kbps
VBV 缓冲: 2100 Kbps
参考帧: 1
B 帧: 0
GOP: 100 帧,即 4 秒
音频: AAC-LC、96 Kbps、44.1 kHz、双声道
HLS 分段: 4 秒、MPEG-TS、VOD
基础转码命令如下:
ffmpeg -i input.mp4 -vf "scale=1920:1080:flags=lanczos,fps=25" -c:v libx264 -preset medium -profile:v baseline -level:v 4.0 -pix_fmt yuv420p -b:v 3500k -maxrate 4200k -bufsize 2100k -refs 1 -bf 0 -flags +cgop -g 100 -keyint_min 100 -sc_threshold 0 -c:a aac -b:a 96k -ar 44100 -ac 2 -map_metadata -1 -f hls -hls_time 4 -hls_list_size 0 -hls_playlist_type vod -hls_segment_filename "output/segment_%04d.ts" "output/index.m3u8"-g 100 与 25 fps × 4 秒 对齐,使 HLS 尽量在关键帧处分段。关闭 B 帧、只保留一个参考帧,是为了降低旧硬件解码器的压力。
如果源文件可能是 MOV,不需要先手工改成 MP4。FFmpeg 可以直接读取 MOV,再输出相同的 HLS 目录。
九、竖屏视频不能直接拉伸
宣传素材里经常同时存在 16:9 横屏和 9:16 竖屏视频。
把竖屏视频强行拉伸到 1920×1080 会严重变形;直接使用 object-fit: cover 又会裁掉人物头部、字幕和二维码。
最终采用的方式是,在转码阶段把竖屏视频提前合成为一条 1920×1080 视频:
将原视频复制为前景和背景两路;
背景放大铺满 1920×1080;
背景裁切、模糊并适当降低亮度和饱和度;
清晰前景按原比例缩放到 920 像素高;
前景居中叠加,顶部保留 80 像素安全距离;
最后只输出一路已经合成的视频。
竖屏滤镜如下:
[0:v]split=2[bg0][fg0];
[bg0]scale=1920:1080:force_original_aspect_ratio=increase:flags=lanczos,
crop=1920:1080,boxblur=20:2,eq=brightness=-0.18:saturation=0.75[bg];
[fg0]scale=-2:920:flags=lanczos[fg];
[bg][fg]overlay=(W-w)/2:80,format=yuv420p,fps=25[v]完整竖屏命令:
ffmpeg -i input.mov -filter_complex "[0:v]split=2[bg0][fg0];[bg0]scale=1920:1080:force_original_aspect_ratio=increase:flags=lanczos,crop=1920:1080,boxblur=20:2,eq=brightness=-0.18:saturation=0.75[bg];[fg0]scale=-2:920:flags=lanczos[fg];[bg][fg]overlay=(W-w)/2:80,format=yuv420p,fps=25[v]" -map "[v]" -map "0:a:0?" -c:v libx264 -preset medium -profile:v baseline -level:v 4.0 -pix_fmt yuv420p -b:v 3500k -maxrate 4200k -bufsize 2100k -refs 1 -bf 0 -flags +cgop -g 100 -keyint_min 100 -sc_threshold 0 -c:a aac -b:a 96k -ar 44100 -ac 2 -map_metadata -1 -f hls -hls_time 4 -hls_list_size 0 -hls_playlist_type vod -hls_segment_filename "output/segment_%04d.ts" "output/index.m3u8"这样处理还有一个很重要的好处:终端只需要解码一路视频。不要在 1 GB 内存的旧设备上,同时播放一层清晰视频和一层动态模糊背景视频。
十、横屏视频也要预留安全内容区
页面通常还有顶栏和底部信息栏。如果直接把标准 16:9 视频铺满整个屏幕,再让 UI 覆盖在上面,视频顶部和底部的字幕就可能被挡住。
最终使用 1920×920 作为清晰内容安全区:
1920×1080 输出画布
├── 顶部背景区:80 px
├── 清晰内容区:1920×920
└── 底部背景区:80 px横屏前景缩放使用:
scale=1920:920:force_original_aspect_ratio=decrease:flags=lanczos外围仍然使用同源模糊背景补满 1920×1080。网页发生少量上下裁切时,只会裁掉背景,不会遮住清晰原画中的字幕和主体。
十一、IIS 必须让 HLS 静态直出
IIS 至少要配置两个 MIME 类型:
.m3u8 application/vnd.apple.mpegurl
.ts video/mp2t如果站点还使用 ARR 代理其他请求,视频目录最好映射成独立静态虚拟目录。静态视频改写规则必须放在宽泛代理规则之前,并设置 stopProcessing="true":
<rule name="ScreenVideosStatic" stopProcessing="true">
<match url="^media/videos/(.*)$" />
<action type="Rewrite" url="screen-videos-static/{R:1}" />
</rule>错误的顺序是:先用通配规则把整个媒体目录代理到后端,再在后面添加静态规则。前面的规则已经命中时,后面的静态规则根本没有机会执行。
还要确认:
IIS 应用池账号对目录具有读取权限;
.mp4MIME 为video/mp4;.m3u8和.ts请求返回 200;m3u8 中的相对分段路径能够正确访问;
不要给 TS 分段套上会改变响应体的压缩或转码模块;
部署后从终端所在网络直接访问,而不是只在服务器本机测试。
十二、FFmpeg 命令无法识别
Windows PowerShell 中如果出现:
ffmpeg:无法将“ffmpeg”项识别为 cmdlet、函数、脚本文件或可运行程序说明 FFmpeg 不在 PATH 中。
如果 ffmpeg.exe 就在当前目录,可以使用:
.\ffmpeg.exe -version
.\ffprobe.exe -version如果安装在其他目录,直接使用完整路径:
& "D:\tools\ffmpeg\bin\ffmpeg.exe" -version
& "D:\tools\ffmpeg\bin\ffprobe.exe" -version自动转码程序也建议配置绝对路径。需要特别检查 bin\ffprobe.exe 中间的反斜杠,路径手误不会启动任何转码进程。
十三、网页自动播放与声音
这台终端运行的是自研 Android WebView 应用。MainActivity 中已经调用 settings.setMediaPlaybackRequiresUserGesture(false),允许媒体在没有用户手势时启动;应用清单同时开启了硬件加速。因此网页可以在打开后直接播放视频和声音,不需要为了本次视频方案修改或重新打包 APK。
网页端的基本写法是:
video.autoplay = true;
video.muted = false;
video.volume = 1;
video.src = 'https://example.com/videos/demo/index.m3u8';
video.load();
var result = video.play();
if (result && typeof result.then === 'function') {
result.catch(function (error) {
console.log(error.name, error.message);
});
}但是现代桌面 Chrome 和 Edge 通常会阻止“没有用户操作的有声自动播放”,并返回 NotAllowedError。这是浏览器策略,不代表 Android 套壳播放失败,也不能靠延迟 500 毫秒或先静音再取消静音稳定绕过。
如果套壳本身要求用户手势,网页代码也无法强行突破。应先用一个低规格短视频真机验证“能否自动播放并出声”,确认壳的能力后再排查编码和网络。
十四、如何确认 HLS 是否正确生成
首先检查输出目录:
output/
├── index.m3u8
├── segment_0000.ts
├── segment_0001.ts
└── ...然后打开 index.m3u8,至少应能看到:
#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:4
#EXT-X-PLAYLIST-TYPE:VOD
...
#EXT-X-ENDLIST再依次检查:
m3u8 能否直接下载;
第一段 TS 能否直接下载;
分段地址是否返回正确的
Content-Type;使用 ffprobe 能否读取第一段 TS;
Windows 浏览器能否完整播放;
Android 真机能否连续跨越多个分段;
播放十分钟后内存是否仍然稳定;
播放结束后能否正常切换下一条视频。
不要只验证开头两秒。旧设备最容易在第一次跨分段、第一次切视频或长时间运行后暴露问题。
十五、最容易发生的误判
误判一:电脑能播放,所以视频没问题
现代电脑浏览器通常使用更完整的软件解码和硬件加速链路。它能播放,只能证明文件没有完全损坏,不能证明旧 Android 的硬件解码器兼容。
误判二:页面时钟还在动,所以视频也应该正常
HTML 页面、JavaScript 定时器和底层媒体播放器是不同链路。页面正常只能说明 WebView 主线程没有完全卡死。
误判三:返回 206 就说明服务器没有问题
还要继续检查 Content-Range、Content-Length、是否经过 chunked 传输,以及真实终端是否能稳定发起后续请求。
误判四:降到 720P 就一定兼容
分辨率只是一个变量。Profile、Level、B 帧、参考帧、峰值码率、GOP、帧率、音频和容器都可能影响旧设备。
误判五:把整个文件缓存下来最稳
对于 1 GB 内存的 WebView,这通常是用内存风险换网络风险,而且不能解决底层解码器不兼容。
误判六:继续降低清晰度总能解决
480P 能播放而 720P、1080P 不稳定时,不应该无止境降低画质。大屏对清晰度要求很高,此时应考虑改变传输形态,例如 HLS 短分段。
十六、最终经验
这次排障最重要的经验不是某一条 FFmpeg 参数,而是必须分层验证:
1. 页面是否仍在运行
2. video 元素进入了什么状态
3. 有声自动播放是否被 WebView 允许
4. 原视频的编码、音频和容器是什么
5. 低规格基准视频能否播放
6. HTTP Range 响应是否规范
7. 请求是否经过反向代理和 chunked 传输
8. 完整 MP4 是否在旧媒体栈中停滞
9. HLS 是否能够连续跨越多个分段
10. 长时间轮播时内存是否稳定最终在这台设备上稳定工作的组合是:
保守的 H.264 Baseline 编码
+ 25 fps 恒定帧率
+ 关闭 B 帧、单参考帧
+ 4 秒关键帧与 HLS 分段对齐
+ AAC-LC 音频
+ IIS 静态文件直出
+ 不使用整文件 Blob 缓存
+ 横竖视频在服务端提前合成为单路画面老旧 Android 大屏不是完全不能播放高清视频,但不能再用现代浏览器“给一个 MP4 地址就结束”的思路处理。把编码复杂度、网络请求长度、内存占用和画面合成工作提前控制好,旧设备依然可以稳定承担长时间的视频轮播。
本文为个人实践复盘。设备型号、地址和业务信息均已省略或泛化;不同 Android 4.4 设备的系统 WebView、媒体组件和硬件解码器差异较大,参数应以真机连续播放结果为准。