项目踩坑指南

Android 4.4 老旧大屏播放视频:从 MP4 两秒卡死到 HLS 分段的完整踩坑记录

记录一台 Android 4.4 老旧大屏从 MP4 播放两秒卡死,到采用 H.264 Baseline、4 秒 HLS 分段和 IIS 静态直出后稳定播放 1080P 视频的完整排障过程。

目录

本文来源于一次老旧 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 秒后,画面停止;

  • 页面上的时钟仍然每秒更新;

  • 视频进入 waitingstalled 状态;

  • 有时黑屏,只能听到前两秒声音;

  • 同一个文件在 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 10025 fps × 4 秒 对齐,使 HLS 尽量在关键帧处分段。关闭 B 帧、只保留一个参考帧,是为了降低旧硬件解码器的压力。

如果源文件可能是 MOV,不需要先手工改成 MP4。FFmpeg 可以直接读取 MOV,再输出相同的 HLS 目录。

九、竖屏视频不能直接拉伸

宣传素材里经常同时存在 16:9 横屏和 9:16 竖屏视频。

把竖屏视频强行拉伸到 1920×1080 会严重变形;直接使用 object-fit: cover 又会裁掉人物头部、字幕和二维码。

最终采用的方式是,在转码阶段把竖屏视频提前合成为一条 1920×1080 视频:

  1. 将原视频复制为前景和背景两路;

  2. 背景放大铺满 1920×1080;

  3. 背景裁切、模糊并适当降低亮度和饱和度;

  4. 清晰前景按原比例缩放到 920 像素高;

  5. 前景居中叠加,顶部保留 80 像素安全距离;

  6. 最后只输出一路已经合成的视频。

竖屏滤镜如下:

[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 应用池账号对目录具有读取权限;

  • .mp4 MIME 为 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

再依次检查:

  1. m3u8 能否直接下载;

  2. 第一段 TS 能否直接下载;

  3. 分段地址是否返回正确的 Content-Type

  4. 使用 ffprobe 能否读取第一段 TS;

  5. Windows 浏览器能否完整播放;

  6. Android 真机能否连续跨越多个分段;

  7. 播放十分钟后内存是否仍然稳定;

  8. 播放结束后能否正常切换下一条视频。

不要只验证开头两秒。旧设备最容易在第一次跨分段、第一次切视频或长时间运行后暴露问题。

十五、最容易发生的误判

误判一:电脑能播放,所以视频没问题

现代电脑浏览器通常使用更完整的软件解码和硬件加速链路。它能播放,只能证明文件没有完全损坏,不能证明旧 Android 的硬件解码器兼容。

误判二:页面时钟还在动,所以视频也应该正常

HTML 页面、JavaScript 定时器和底层媒体播放器是不同链路。页面正常只能说明 WebView 主线程没有完全卡死。

误判三:返回 206 就说明服务器没有问题

还要继续检查 Content-RangeContent-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、媒体组件和硬件解码器差异较大,参数应以真机连续播放结果为准。

本文为个人实践复盘,不构成医疗建议。涉及的患者、医院、厂商、系统名称、数据与时间均已脱敏或调整。