Web端视频播放失败的经验汇总

1,129 阅读9分钟

对音视频播放有些接触的应该知道,浏览器端对音视频的支持能力是有限的,比本地电脑播放要弱得多。

往往在本地电脑上能正常播放的视频,上传到服务器之后,用网页方便就会各种异常,比如黑屏,无声音,画面卡住等等。

在做 Web 端视频播放以来,团队遇到过不少播放失败的问题,总结了一些经验,这里做一次汇总。

PC Web端播放无画面,移动端播放正常

前方用户反馈,有个用户上传的视频在 PC 端播放无画面,移动端播放正常。将原视频下载下来,分析发现视频的 mime 是 audio/mp4; codecs="mp4v,mp4a.40.2"

这个 mime 明显不是一个正常的 MP4 视频,audio/mp4 表示这个是一个音频文件,而且视频轨的编码格式也不是 avc1,而是 mp4v,这个编码格式是不被浏览器支持的。

对比另一个正常的 MP4 视频,它的 mime 是 video/mp4; codecs="avc1.640028,mp4a.40.2"

mp4v 也是 MPEG-4 标准中的一种视频编码格式,但它属于 MPEG-4 的早期版本,现在并不常见,已经被 avc1 替代,所以浏览器不支持这种编码格式。

使用 ffmpeg,用 h264 将视频重新编码一下,就可以在 PC Web 端正常播放了。那移动端为什么能播放正常吗?大概是因为移动端的浏览器刚好支持 mp4v 编码格式。

ffmpeg -i pub.mp4 -c:v libx264 -c:a copy pub-new.mp4

iOS 设备播放无声音

用户反馈有个视频在电脑上播放正常,在移动端播放有画面无声音。开发将视频下载到本地分析发现,用户使用的是 Windows 电脑和 iOS 手机,实际视频只是在 iOS 的 Safari 浏览器上播放无声音,其他浏览器都正常。

使用 ffmpeg 转封装之后,发现视频在 Safari 上播放正常,所以大概不是视频编码的问题,而是视频封装数据的问题。对比前后两个文件,也没有发现足够有价值的信息,只发现转封装之后的视频的 moov box 在文件的开头,而原视频的 moov box 在文件的末尾。

受限于对 MP4 格式的深入理解,暂时只能认为是这个原因导致的。

ffmpeg -i input.mp4 -c copy output1.mp4

网页播放正常,Windows Media Player 播放失败

我们一般认为本地对音视频的支持能力强于浏览器,网页播放成功,本地播放应该也会成功。

但这个视频就是这么反常,用户反馈此视频在网页播放正常,却在本地播放失败。

开发跟进调查发现,用户本地播放使用的是 Window Media Player 播放,如果使用 VLC 播放其实是可以正常播放的。

使用 mp4box 查看,发现视频编码 codec 是 avc1.7a0028,在 cconcolato.github.io/media-mime-… 上可以查到这种格式。

其对应的编码格式是 AVC High 4:2:2 Level 4,也就是使用了 YUV422p 色彩取样格式,YUV422p 一般用于对色度要求高的领域,比如广播、电影制作,但目前网络上使用更广泛的编码格式是 YUV420p。

而 Windows Media Player 并不支持 YUV422p 格式,所以无法播放这个视频。

后面进一步发现,产生这个视频的原因,是 C++ 同事在视频编码部分的代码中,设置了 yuv422p 格式,将 yuv422p 改为 yuv420p 即可,后续生成的视频都可以用 Windows Media Player 正常播放了。

iOS 端无法播放

跟前面的 iOS 播放无声音的情况不同,这次发现的视频是在 iOS 端完全无法播放,其他平台都正常。

同样的,我们将视频下载到本地,使用 mp4box 查看视频编码,可以看到视频编码很正常,并没有使用不常用编码格式。视频下载 iOS 手机之后,也能正常播放。

所以,文件本身应该是没有问题的,那么问题就出在网络上了。

使用抓包工具可以发现,在浏览器端播放远程视频的时候,首先会发送一个请求获取远程视频的大小,服务端接收到之后,会返回一个 content-range 的请求头给客户端,告诉客户端你要的这个文件有多大。

在 iOS 端则是发送一个请求,在request header的"range"字段中: range: 'bytes=0-1'。我们发现这个无法播放的视频,服务器返回数据中没有 content-range 字段,而 content-type 则是返回 application/octet-stream,说明这是一个二进制文件,而能够正常播放的视频,返回的 content-type 是 video/mp4。

Firefox 中可以播放,Chrome 中无法播放

前方反馈某个视频在网页端无法播放,播放器中一直显示 loading 状态。但是用 VLC 可以正常播放,后来发现有 Firefox 浏览器也能正常播放,只是在 Chrome 浏览器中无法播放。

查看 Chrome 的 Media 看板(或者打开 chrome://media-internals/),会发现打印了很多 "ISO-BMFF container metadata for video frame indicates that the frame is a keyframe, but the video frame contents indicate the opposite." 信息:

这个提示说明视频异常的原因可能在关键帧上面。这个视频是 FLV 格式,一个 FLV Tag 中会存在 FrameType 和 nal_unit_type 用于表示当前帧类型。

调查发现,这个视频所有 FrameType = 1 (即关键帧)中的 nal_unit_type 都是 1(即都是非 IDR 帧);而能正常播放的 flv 流中,会存在关键帧的 nal_unit_type 是 5 的情况(即存在 IDR 帧,特别是流的第一个 video FLV tag 就是 IDR 帧);

简单来说就是,这个视频只有 I 帧而没有 IDR 帧,而不存在 IDR 帧的 FLV 流无法在 Chrome 播放

在 Chrome 的 bugs 列表可以查询到类似问题,bugs.chromium.org/p/chromium/…

关键回复如下:

To correctly buffer out-of-order GOPs into the timeline, MseBufferByPts now relies upon keyframeness of an AVC frame in MP4 being determined by checking if the AVC frame is IDR instead of trusting the frequently incorrect MP4 keyframeness metadata.

A stream that has no real IDR is not compatible with MSE IIUC. The fact that the stream may have worked previously in LegacyByDts implementation relied upon tolerances in the AVC decoder, not all of which might have tolerated such a non-compliant stream.

即,Chrome MSE 依赖 nal_unit_type 来判断当前是否为关键帧,而不是依赖 Frame Type。这就要求我们在做视频编码的时候,不能漏掉了 IDR 帧。

关于 I 帧 和 IDR 帧

我们都知道视频中有 IBP 帧的概念,但可能常常会忽略 IDR 帧的概念。

I 帧是一种关键帧,也称为自身独立帧。它是视频序列中的一个完整帧,不依赖于其他帧进行解码。

IDR 帧也是一种关键帧,它是一种特殊类型的 I 帧。IDR 帧在视频序列中起到刷新解码器的作用,它会清空解码器的参考图像缓存并重新开始解码。

PC 端播放正常,移动端播放卡住

前端反馈有个视频在PC Web 端播放正常,移动端播放到中间突然卡住。

仔细分析后发现,这个视频分为两段,移动端卡住的地方正好是两段视频切换的地方。而两段的差异是,前一段的视频分辨率是 720p,后一段的视频分辨率是 1080p。

使用 ffmpeg 刻意将两个分辨率不一致的视频拼接在一起,前面一段是 1080p,后一段是 720p,发现同样在移动端无法播放,在 PC 端可以正常播放。

所以,问题确认是分辨率不一致导致的,但为什么移动端无法播放,暂时还没有找到根本原因。

视频正常可播放,手动跳转进度会卡住

测试人员手上有个一小时以上的视频,如果从头开始播放,那么播放正常,但是一旦拖动进度条,视频画面就会卡住,只能听到声音。

因为这个视频有点大,分析起来有点麻烦,我尝试用 ffmpeg 只取开头一小段,发现下面这个命令执行之后,只有音频轨而没有视频轨,只有将 -ss 后面的 30 改为 0 才能正常取到视频轨。

ffmpeg -i error.mp4 -ss 30 -t 30 -vcodec copy -acodec copy -avoid_negative_ts make_zero 0684.mp4

那么差异应该还是在关键帧数据,使用 -ss 30 跳转之后,无法找到关键帧。使用 ffprobe 查看视频的关键帧信息,发现这个视频确实只有一个关键帧。

ffprobe -select_streams v:0 -skip_frame nokey -show_entries frame=pkt_pts_time error.mp4

使用下面的命令强制插入关键帧,每 3 秒插入一个关键帧,然后视频就可以正常拖动了。这里只取了前两分钟的数据,不影响判断。

ffmpeg -i error.mp4 -t 120 -force_key_frames "expr:gte(t,n_forced*3)" out3.mp4

后来进一步发现,这个视频只有一个关键帧的原因是视频编码的时候使用的 profile 是 baseline,改为 high 之后,视频中就会有多个关键帧了。而前面强制插入关键帧的过程,也会让视频编码的 profile 变为 high。

视频上传到云服务商后请求异常

用户反馈上传的视频无法播放,调查发现后台显示错误信息为:此视频不存在或为不支持的视频文件。这个文件肯定是存在的,可以直接从 CDN 服务端下载下来。

在本地使用 mp4explorer.zip 分析可以看到,有问题的视频有 500 多M,而且存在多个 mdat box,同时 moov box 放在了最后。

通过查看 MP4 的协议,可以看到在标准中并没有要求 mdat box 只能有一个,而且视频本身在 Chrome 和 vlc 中都是可以正常播放的(只是等待加载的时间要长)。

错误原因就是 CDN 厂商的此类视频格式支持不佳,后台接口要获取 moov,需要先把 mdat 缓存完,而这个视频的 mdat 太多,导致缓存时间过长,请求超时。