Jason Pan

iPhone 上加载 .mp4 异常排查

潘忠显 / 2021-09-13


本文记录了我排查 iPhone XR 播放 .mp4 视频异常的过程,包括使用 Safari 查看 iPhone 和模拟器的 HTTP 相关细节、视频网络包的抓取、自行搭建视频 HTTP 服务、视频格式查看与转换

我之前对前端技术了解较少,排查过程走了不少弯路,不免贻笑大方,文中谬误也敬请各位高手指出。但通过这一排查过程,我也对 MPEG-4 的编码标准和 HTML5的部分技术都有了初步认识。

一、背景

有同学反馈游戏录屏的视频在 iPhone XR (IOS 14.5) 上无法播放,一直显示加载失败,并提供了页面链接

image-loading-error

为排除干扰,直接使用 Safari 访问的 .mp4 的视频链接:在 Mac OS 上自带的Simulator IOS 14.4 上加载失败;而实体机 iPhone 11 (IOS 13.6)可以正常播放。

初步定位在 IOS 14 上的默认浏览器播放该视频过程中有问题,可能是网络问题、视频播放兼容性问题。接下来进行详细的错误原因分析。

中间排查过程过于详细,直接点击看-解决方案

二、iOS Safari 网页检查器

上边提到了两个设备,一个是实体机,一个是模拟器,两者地位是平等的。为了看看safari在播放视频的过程中究竟发生了什么,我这里需要使用 safari 的网页检查器。

iOS 上 Safari 的网页检查器是需要在 Mac 上打开观察的,主要的步骤是:

safari-develop-tool

三、网络包比较

弯路:问题跟网络包没关系,可以跳过,也可以参考排查方法

打开视频的过程中,两个 iOS 系统上 Safari 请求的内容都是相同的(左侧为实体机,右侧为模拟器):

network-package-comparison

中间的 data: 的内容应该是请求的本地资源,是播放器相关的一些图标,不涉及网络。第一个请求和最后一组请求是外部视频资源相关的请求。

经仔细比较,发现请求 .mp4 资源的前几次请求和返回都相同,只有最后一次请求和返回不同(红框中标注的):在请求视频的 Range: bytes=0-8635313 内容时,低版本 iOS 可以正常的访问并返回,而高版本 iOS 则出现了网络错误。

3.1 请求比较

直接从 safari 的网页检查器中拷贝出 HTTP Headers,比较最后一次网络请求的 HTTP 请求内容:

http-header-comparison

可以看到,14.4 上的请求比 13.6 上的请求“少了三部分”内容:

写一个脚本(内容详见附录)进行测试,通过结果可以看出,这两个 HTTP/1.1Host: 头都不能缺失:

different-http-header-response

但实际上 safari 网页检查工具中会显示有状态码以及有响应头:

safari-detail-info

上述现象,可能是因为复制出的 HTTP Header 跟实际发送的不同,因此需要自己搭建一个视频服务来确认下这部分。

3.2 部署视频服务

简单的通过 Nginx 在 dev-cloud 上提供 HTTP 视频服务:

http {
    ...
    include /etc/nginx/conf.d/*.conf;

    server {
        listen       8001;
        listen       [::]:8001;
        server_name  _;
        root         /usr/share/nginx/html;
    ...

3.3 确认实际请求头

模拟器中直接填写我们自己搭建服务的URL http://{ip}/a.mp4,抓包可以看到,最后一次 HTTP 请求(Range: bytes=0-8635313)的 HTTP Headers 是正常的,并能收到了真正的 HTTP 返回:

wireshark-real-http-detail

但是,在接收 HTTP 返回的过程中,作为客户端的浏览器,只收了 830kB(Ack=838407) 之后,主动发出了FIN,中断了后续数据包的接收:

wireshark-safari-close-connection


是不是视频小了就可以加载成功了?答案是否定的,通过 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 级以下的版本:

会不会是 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 的视频,实际效果也是能够正确播放:

h5-video-action

这种方式能够节省带宽成本,但不足之处是:需要后台进行额外计算,同时需要每个视频会存储多个格式而造成存储空间增加。

h5-video-choice-succ

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)