iPhone 上加载 .mp4 异常排查
潘忠显 / 2021-09-13
本文记录了我排查 iPhone XR 播放 .mp4 视频异常的过程,包括使用 Safari 查看 iPhone 和模拟器的 HTTP 相关细节、视频网络包的抓取、自行搭建视频 HTTP 服务、视频格式查看与转换。
我之前对前端技术了解较少,排查过程走了不少弯路,不免贻笑大方,文中谬误也敬请各位高手指出。但通过这一排查过程,我也对 MPEG-4 的编码标准和 HTML5的部分技术都有了初步认识。
一、背景
有同学反馈游戏录屏的视频在 iPhone XR (IOS 14.5) 上无法播放,一直显示加载失败,并提供了页面链接。
为排除干扰,直接使用 Safari 访问的 .mp4 的视频链接:在 Mac OS 上自带的Simulator IOS 14.4 上加载失败;而实体机 iPhone 11 (IOS 13.6)可以正常播放。
初步定位在 IOS 14 上的默认浏览器播放该视频过程中有问题,可能是网络问题、视频播放兼容性问题。接下来进行详细的错误原因分析。
中间排查过程过于详细,直接点击看-解决方案。
二、iOS Safari 网页检查器
上边提到了两个设备,一个是实体机,一个是模拟器,两者地位是平等的。为了看看safari在播放视频的过程中究竟发生了什么,我这里需要使用 safari 的网页检查器。
iOS 上 Safari 的网页检查器是需要在 Mac 上打开观察的,主要的步骤是:
- Mac上:Safari -> 偏好设置 -> 高级 -> 勾选 “菜单栏中显示 ‘开发’ 菜单”
- 实体机上:设置 -> Safari 浏览器 -> 高级 -> 勾选 “Web 检查器”
- 在实体机和模拟器中打开Safari,并进入要检查的页面
- Mac上:打开 Safari -> 开发 -> 选择对应的设备 -> 选择打开的页面
三、网络包比较
弯路:问题跟网络包没关系,可以跳过,也可以参考排查方法
打开视频的过程中,两个 iOS 系统上 Safari 请求的内容都是相同的(左侧为实体机,右侧为模拟器):
中间的 data:
的内容应该是请求的本地资源,是播放器相关的一些图标,不涉及网络。第一个请求和最后一组请求是外部视频资源相关的请求。
经仔细比较,发现请求 .mp4 资源的前几次请求和返回都相同,只有最后一次请求和返回不同(红框中标注的):在请求视频的 Range: bytes=0-8635313
内容时,低版本 iOS 可以正常的访问并返回,而高版本 iOS 则出现了网络错误。
3.1 请求比较
直接从 safari 的网页检查器中拷贝出 HTTP Headers,比较最后一次网络请求的 HTTP 请求内容:
可以看到,14.4 上的请求比 13.6 上的请求“少了三部分”内容:
- URI 之后的 HTTP版本,既
HTTP/1.1
- 没有
Host: xxx
的头 - 没有
Accept-Language
的头(这项不影响)
写一个脚本(内容详见附录)进行测试,通过结果可以看出,这两个 HTTP/1.1
和 Host:
头都不能缺失:
- 如果不带
HTTP/1.1
服务器可能路由,直接回一个文本内容的返回,而非 HTTP 协议 - 如果不带
Host
头,会报400
请求错误
但实际上 safari 网页检查工具中会显示有状态码以及有响应头:
上述现象,可能是因为复制出的 HTTP Header 跟实际发送的不同,因此需要自己搭建一个视频服务来确认下这部分。
3.2 部署视频服务
简单的通过 Nginx 在 dev-cloud 上提供 HTTP 视频服务:
- 安装 Nginx :
yum install nginx
- 查看配置文件位置:
nginx -t
- 查看配置文件中 HTTP 服务的目录
http {
...
include /etc/nginx/conf.d/*.conf;
server {
listen 8001;
listen [::]:8001;
server_name _;
root /usr/share/nginx/html;
...
- 将文件放到网站的根目录下:
wget -O /usr/share/nginx/html/a.mp4 ${mp4_url}
- 启动 Nginx 服务:
service nginx start
3.3 确认实际请求头
在模拟器中直接填写我们自己搭建服务的URL http://{ip}/a.mp4
,抓包可以看到,最后一次 HTTP 请求(Range: bytes=0-8635313)的 HTTP Headers 是正常的,并能收到了真正的 HTTP 返回:
但是,在接收 HTTP 返回的过程中,作为客户端的浏览器,只收了 830kB(Ack=838407) 之后,主动发出了FIN,中断了后续数据包的接收:
是不是视频小了就可以加载成功了?答案是否定的,通过 ffmpeg
指令将原视频的一部分保存成新文件:
ffmpeg -i a.mp4 -acodec copy -vcodec copy -ss 0 -t 00:00:00.5 b.mp4
b.mp4 只有 324KB,浏览器和服务器之间的通信可以正常完成,但是视频仍然无法播放。
四、视频格式
查询 Apple 官网的设备技术规格(链接),其中 iPhone XR 的指出,视频播放支持的格式包括:HEVC、H.264、MPEG-4 Part 2 与 Motion JPEG。
通过 mediainfo
指令来查看不能播放的文件格式内容,重要信息包括:
General
Complete name : a.mp4
Format : MPEG-4
Format profile : Base Media
Codec ID : isom (isom/iso2/avc1/mp41)
Video
ID : 2
Format : AVC
Format/Info : Advanced Video Codec
Format profile : High@L6
Format settings : CABAC / 1 Ref Frames
Format settings, CABAC : Yes
Format settings, Reference frames : 1 frame
Format settings, GOP : M=1, N=25
Codec ID : avc1
Codec ID/Info : Advanced Video Coding
Duration : 13 s 581 ms
Bit rate : 4 949 kb/s
Width : 1 600 pixels
Height : 720 pixels
Display aspect ratio : 2.222
Frame rate mode : Variable
Frame rate : 23.637 FPS
Minimum frame rate : 17.727 FPS
Maximum frame rate : 31.825 FPS
Standard : NTSC
Color space : YUV
Chroma subsampling : 4:2:0
Bit depth : 8 bits
Scan type : Progressive
Bits/(Pixel*Frame) : 0.182
Stream size : 8.01 MiB (97%)
Color range : Limited
Color primaries : BT.601 PAL
Transfer characteristics : BT.601
Matrix coefficients : BT.470 System B/G
Codec configuration box : avcC
该视频格式是 MPEG-4 AVC,这个就是 H.264 编码标准,详见 WiKi 介绍,其中最后一部分有介绍关于码流的 Levels。相同视频质量情况下,高 Level 能带来码流的减少,节省带宽,但同时提高了对硬件运算能力、缓存能力的要求。比如 PlayStation TV 也只支持"H.264/MPEG-4 AVC Baseline/Main/High Profile Level 4.0"。
根据这篇文章的介绍,老版本的 iPhone 仅支持部分 H.264 的编码标准,比如 iPhone 5S 的技术规格只支持 High Profile level 4.2 级以下的版本:
- Video formats supported: H.264 video up to 1080p, 60 frames per second, High Profile level 4.2 with AAC-LC audio up to 160 Kbps, 48kHz, stereo audio in .m4v, .mp4, and .mov file formats; …
会不会是 iPhone XR 也是只支持部分 Level,而不能播放的视频是 High@L6
。尝试将其转换为低 Level 的 .mp4 格式文件。
declare -a a=("3.2" "4.0" "4.1" "5.0" "5.1" "5.2")
for i in ${a[@]}; do
output_file_name=a.l`echo $i | sed "s/\.//g"`
ffmpeg -y -i a.mp4 -c:v libx264 -profile:v high -level:v $i -c:a copy $output_file_name.mp4
ffmpeg -i $output_file_name.mp4 -vsync vfr -c:a copy $output_file_name.vfr.mp4
done
以上基本会将原始视频转换成 CFR、VFR 的其他 Level 的 MPEG-4/AVC,通过浏览器均可打开观看。证明了是不能加载播放因为视频本身的 AVC Level 过高导致的。
五、解决方案
通过上述实验和分析,我们可以有以下两种方法来解决部分机型不能播放视频的问题。两种方式各有利弊,需要业务根据自身的需求进行选择。
5.1 后台存储多种 Level 的视频
[是否有相应的云服务可直接使用?]
用户上传录制的视频之后,后台将其转换成不同Level的视频。
H5 页面支持判断浏览器播放器的能力,选择拉取对应的视频:
<video poster="poster.jpg" controls>
<source src="a.mp4" type='video/mp4; codecs="avc1.64003C, mp4a.40.2"'>
<source src="a.l52.mp4" type='video/mp4; codecs="avc1.640034, mp4a.40.2"'>
<source src="a.l30.mp4" type='video/mp4; codecs="avc1.64001E, mp4a.40.2"'>
</video>
以下是修改为上述代码之后,模拟器的视频拉取情况。在第一个视频不能够正常加载之后,选择了再去拉取 Level 5.2 的视频,实际效果也是能够正确播放:
这种方式能够节省带宽成本,但不足之处是:需要后台进行额外计算,同时需要每个视频会存储多个格式而造成存储空间增加。
5.2 录制时降低 Level
在用户录制视频时,指定相对较低的 AVC Level 进行录制。
该方法的不足是:很难确定所有设备支持的最高 Level;在上传、下载都会带来带宽上的损失。
参考
附录
视频操作常用指令
mediainfo
查看视频文件信息
mediainfo input.mkv
mediainfo --fullscan input.mkv
ffmpeg
切分文件
# ffmpeg -i ORIGINALFILE.mp4 -acodec copy -vcodec copy -ss START -t LENGTH OUTFILE.mp4
ffmpeg -i ORIGINALFILE.mp4 -acodec copy -vcodec copy -ss 0 -t 00:15:00 OUTFILE-1.mp4
ffmpeg -i ORIGINALFILE.mp4 -acodec copy -vcodec copy -ss 00:15:00 -t 00:15:00 OUTFILE-2.mp4
ffmpeg
转换 profile 和 level
ffmpeg -i input.mp4 -c:v libx264 -profile:v high -level:v 4.0 -c:a copy output.mp4
HTTPS 发送原始数据
import socket
import ssl
def request_with_option(flag_with_host=False, flag_with_http_version=False):
print(
f'# flag_with_host={flag_with_host}, flag_with_http_version={flag_with_http_version}'
)
hostname = '1500004208.vod2.myqcloud.com'
path = '/6c99aad6vodcq1500004208/3abb64a13701925920121967660/ZbqL9E2vkH4A.mp4'
url = f'https://{hostname}{path}'
host_header_line = f"Host: {hostname}" if flag_with_host else ""
http_version = " HTTP/1.1" if flag_with_http_version else ""
context = ssl.create_default_context()
with socket.create_connection((hostname, 443)) as sock:
with context.wrap_socket(sock, server_hostname=hostname) as ssock:
if True:
request = f"""GET {path}{http_version}
Range: bytes=0-8635313
Accept: */*
{host_header_line}
Referer: {url}
Accept-Encoding: identity
Connection: Keep-Alive
User-Agent: Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/14.0.3 Mobile/15E148 Safari/604.1
X-Playback-Session-Id: 522EFF58-4BE4-4968-8E91-17B32A7E9140
""".replace('\n\n', '\n').replace('\n', '\r\n') + '\r\n'
ssock.sendall(str.encode(request))
print(ssock.recv(1024))
print("")
request_with_option()
request_with_option(flag_with_host=True)
request_with_option(flag_with_http_version=True)
request_with_option(True, True)