解决无法播放加密 M3U8 问题
记录解决 FSPlayer 无法播放加密 M3U8 的排查过程。
背景
有用户反馈有个 m3u8 在 FSPlayer 上无法播放,但是在 Safari 浏览器上可以正常播放,排查日志如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
[11:06:31 695] [FSPlayer] ijkmp_prepare_async()=0
[11:06:31 696] [FSPlayer] Opening 'https://bf.jisuziyuanbf.com/play/6dB0V8Xa/index.m3u8' for reading
[11:06:31 696] [FSPlayer] dns getaddrinfo uri = bf.jisuziyuanbf.com
[11:06:31 714] [FSPlayer] Original list of addresses:
[11:06:31 714] [FSPlayer] Address 185.34.146.254 port 443
[11:06:31 714] [FSPlayer] Interleaved list of addresses:
[11:06:31 714] [FSPlayer] Address 185.34.146.254 port 443
[11:06:31 714] [FSPlayer] Starting connection attempt to 185.34.146.254 port 443
cannot open file at line 51043 of [f0ca7bba1c]
os_unix.c:51043: (2) open(/private/var/db/DetachedSignatures) - No such file or directory
FSPlayerPlaybackSchedule:Initialized
FSPlayerPlaybackState:Paused
FSPlayerPlaybackSchedule:Preparing
FSPlayerPlaybackState:Paused
[11:06:31 930] [FSPlayer] Successfully connected to 185.34.146.254 port 443
[11:06:31 930] [FSPlayer] tcp did open uri = tcp://bf.jisuziyuanbf.com:443, ip = 185.34.146.254
[11:06:32 154] [FSPlayer] request: GET /play/6dB0V8Xa/index.m3u8 HTTP/1.1
User-Agent: fsplayer
Accept: */*
Range: bytes=0-
Connection: close
Host: bf.jisuziyuanbf.com
Icy-MetaData: 1
[11:06:32 658] [FSPlayer] header='HTTP/1.1 200 OK'
[11:06:32 658] [FSPlayer] http_code=200
[11:06:32 658] [FSPlayer] header='Server: openresty'
[11:06:32 658] [FSPlayer] header='Date: Wed, 11 Mar 2026 03:06:32 GMT'
[11:06:32 658] [FSPlayer] header='Content-Type: application/vnd.apple.mpegurl'
[11:06:32 658] [FSPlayer] header='Content-Length: 107'
[11:06:32 658] [FSPlayer] header='Connection: close'
[11:06:32 658] [FSPlayer] header='X-Response-Time: 0ms'
[11:06:32 658] [FSPlayer] header='Access-Control-Allow-Origin: *'
[11:06:32 659] [FSPlayer] header='Access-Control-Allow-Methods: *'
[11:06:32 659] [FSPlayer] header='Access-Control-Allow-Headers: *'
[11:06:32 659] [FSPlayer] header='Access-Control-Max-Age: 3600'
[11:06:32 659] [FSPlayer] header='Access-Control-Allow-Credentials: true'
[11:06:32 659] [FSPlayer] header='Expires: Wed, 11 Mar 2026 06:49:34 GMT'
[11:06:32 659] [FSPlayer] header='Cache-Control: max-age=14400'
[11:06:32 659] [FSPlayer] header='X-Cache: MISS'
[11:06:32 659] [FSPlayer] header='X-Cache: HIT'
[11:06:32 659] [FSPlayer] header=''
[11:06:32 660] [FSPlayer] Probing hls score:100 size:107
[11:06:32 662] [FSPlayer] you called a dummp demuxer
[11:06:32 662] [FSPlayer] Probing ijklas score:-1094995529 size:107
[11:06:32 662] [FSPlayer] you called a dummp demuxer
[11:06:32 662] [FSPlayer] Probing ijkplaceholder1 score:-1094995529 size:107
[11:06:32 662] [FSPlayer] you called a dummp demuxer
[11:06:32 662] [FSPlayer] Probing ijkplaceholder2 score:-1094995529 size:107
[11:06:32 662] [FSPlayer] you called a dummp demuxer
[11:06:32 662] [FSPlayer] Probing ijkplaceholder3 score:-1094995529 size:107
[11:06:32 662] [FSPlayer] you called a dummp demuxer
[11:06:32 662] [FSPlayer] Probing ijkplaceholder4 score:-1094995529 size:107
[11:06:32 663] [FSPlayer] Format hls probed with size=2048 and score=100
[11:06:32 663] [FSPlayer] Opening 'https://bf.jisuziyuanbf.com/play/hls/6dB0V8Xa/index.m3u8' for reading
[11:06:32 663] [FSPlayer] dns getaddrinfo uri = bf.jisuziyuanbf.com
[11:06:32 666] [FSPlayer] Original list of addresses:
[11:06:32 666] [FSPlayer] Address 185.34.146.254 port 443
[11:06:32 666] [FSPlayer] Interleaved list of addresses:
[11:06:32 666] [FSPlayer] Address 185.34.146.254 port 443
[11:06:32 666] [FSPlayer] Starting connection attempt to 185.34.146.254 port 443
[11:06:32 852] [FSPlayer] Successfully connected to 185.34.146.254 port 443
[11:06:32 852] [FSPlayer] tcp did open uri = tcp://bf.jisuziyuanbf.com:443, ip = 185.34.146.254
[11:06:33 041] [FSPlayer] request: GET /play/hls/6dB0V8Xa/index.m3u8 HTTP/1.1
User-Agent: fsplayer
Accept: */*
Range: bytes=0-
Connection: close
Host: bf.jisuziyuanbf.com
Icy-MetaData: 1
[11:06:33 492] [FSPlayer] header='HTTP/1.1 200 OK'
[11:06:33 492] [FSPlayer] http_code=200
[11:06:33 492] [FSPlayer] header='Server: openresty'
[11:06:33 493] [FSPlayer] header='Date: Wed, 11 Mar 2026 03:06:33 GMT'
[11:06:33 493] [FSPlayer] header='Content-Type: application/vnd.apple.mpegurl'
[11:06:33 493] [FSPlayer] header='Content-Length: 154392'
[11:06:33 493] [FSPlayer] header='Connection: close'
[11:06:33 493] [FSPlayer] header='X-Response-Time: 48ms'
[11:06:33 493] [FSPlayer] header='Access-Control-Allow-Origin: *'
[11:06:33 493] [FSPlayer] header='Access-Control-Allow-Methods: *'
[11:06:33 493] [FSPlayer] header='Access-Control-Allow-Headers: *'
[11:06:33 493] [FSPlayer] header='Access-Control-Max-Age: 3600'
[11:06:33 493] [FSPlayer] header='Access-Control-Allow-Credentials: true'
[11:06:33 493] [FSPlayer] header='Expires: Wed, 11 Mar 2026 06:49:35 GMT'
[11:06:33 493] [FSPlayer] header='Cache-Control: max-age=14400'
[11:06:33 494] [FSPlayer] header='X-Cache: MISS'
[11:06:33 494] [FSPlayer] header='X-Cache: HIT'
[11:06:33 494] [FSPlayer] header=''
[11:06:33 494] [FSPlayer] Skip ('#EXT-X-VERSION:3')
[11:06:34 125] [FSPlayer] Statistics: 154392 bytes read, 0 seeks
[11:06:34 126] [FSPlayer] new_program: id=0x0000
[11:06:34 126] [FSPlayer] HLS request for url 'https://p.jisuts.com:999/hls/835/20251203/2544316/plist0.ts', offset 0, playlist 0
[11:06:34 126] [FSPlayer] Opening 'https://bf.jisuziyuanbf.com/play/hls/6dB0V8Xa/enc.key' for reading
[11:06:34 126] [FSPlayer] dns getaddrinfo uri = bf.jisuziyuanbf.com
[11:06:34 128] [FSPlayer] Original list of addresses:
[11:06:34 128] [FSPlayer] Address 185.34.146.254 port 443
[11:06:34 128] [FSPlayer] Interleaved list of addresses:
[11:06:34 128] [FSPlayer] Address 185.34.146.254 port 443
[11:06:34 128] [FSPlayer] Starting connection attempt to 185.34.146.254 port 443
[11:06:34 322] [FSPlayer] Successfully connected to 185.34.146.254 port 443
[11:06:34 322] [FSPlayer] tcp did open uri = tcp://bf.jisuziyuanbf.com:443, ip = 185.34.146.254
[11:06:34 544] [FSPlayer] request: GET /play/hls/6dB0V8Xa/enc.key HTTP/1.1
User-Agent: fsplayer
Accept: */*
Range: bytes=0-
Connection: close
Host: bf.jisuziyuanbf.com
Icy-MetaData: 1
[11:06:34 931] [FSPlayer] header='HTTP/1.1 200 OK'
[11:06:34 931] [FSPlayer] http_code=200
[11:06:34 931] [FSPlayer] header='Server: openresty'
[11:06:34 931] [FSPlayer] header='Date: Wed, 11 Mar 2026 03:06:34 GMT'
[11:06:34 931] [FSPlayer] header='Content-Type: application/octet-stream'
[11:06:34 931] [FSPlayer] header='Content-Length: 16'
[11:06:34 931] [FSPlayer] header='Connection: close'
[11:06:34 931] [FSPlayer] header='X-Response-Time: 1ms'
[11:06:34 931] [FSPlayer] header='Access-Control-Allow-Origin: *'
[11:06:34 931] [FSPlayer] header='Access-Control-Allow-Methods: *'
[11:06:34 932] [FSPlayer] header='Access-Control-Allow-Headers: *'
[11:06:34 932] [FSPlayer] header='Access-Control-Max-Age: 3600'
[11:06:34 932] [FSPlayer] header='Access-Control-Allow-Credentials: true'
[11:06:34 932] [FSPlayer] header='Expires: Wed, 11 Mar 2026 06:49:38 GMT'
[11:06:34 932] [FSPlayer] header='Cache-Control: max-age=14400'
[11:06:34 932] [FSPlayer] header='X-Cache: MISS'
[11:06:34 932] [FSPlayer] header='X-Cache: HIT'
[11:06:34 932] [FSPlayer] header=''
[11:06:34 932] [FSPlayer] Statistics: 16 bytes read, 0 seeks
[11:06:34 933] [FSPlayer] Opening 'crypto+https://p.jisuts.com:999/hls/835/20251203/2544316/plist0.ts' for reading
[11:06:34 933] [FSPlayer] Failed to open segment 0 of playlist 0
[11:06:34 933] [FSPlayer] Segment 0 of playlist 0 failed too many times, skipping
[11:06:34 934] [FSPlayer] HLS request for url 'https://p.jisuts.com:999/hls/835/20251203/2544316/plist1.ts', offset 0, playlist 0
[11:06:34 934] [FSPlayer] Opening 'crypto+https://p.jisuts.com:999/hls/835/20251203/2544316/plist1.ts' for reading
[11:06:34 934] [FSPlayer] Failed to open segment 1 of playlist 0
[11:06:34 934] [FSPlayer] Segment 1 of playlist 0 failed too many times, skipping
[11:06:34 934] [FSPlayer] you called a dummp demuxer
[11:06:34 934] [FSPlayer] Probing ijklas score:-1094995529 size:0
[11:06:34 934] [FSPlayer] you called a dummp demuxer
[11:06:34 934] [FSPlayer] Probing ijkplaceholder1 score:-1094995529 size:0
[11:06:34 934] [FSPlayer] you called a dummp demuxer
[11:06:34 934] [FSPlayer] Probing ijkplaceholder2 score:-1094995529 size:0
[11:06:34 934] [FSPlayer] you called a dummp demuxer
[11:06:34 934] [FSPlayer] Probing ijkplaceholder3 score:-1094995529 size:0
[11:06:34 935] [FSPlayer] you called a dummp demuxer
[11:06:34 935] [FSPlayer] Probing ijkplaceholder4 score:-1094995529 size:0
[11:06:34 935] [FSPlayer] Error when loading first segment 'https://p.jisuts.com:999/hls/835/20251203/2544316/plist0.ts'
[11:06:34 936] [FSPlayer] Statistics: 107 bytes read, 0 seeks
[11:06:34 936] [FSPlayer] open failed:Invalid data found when processing input,err:-1094995529,[https://bf.jisuziyuanbf.com/play/6dB0V8Xa/index.m3u8]
这些是播放 m3u8 的标准流程,跟以往不同的是,在请求第一片 ts 之前先请求了 enc.key 这个文件,并且成功了,但接着请求这片 ts 时却失败了:Failed to open segment 0 of playlist 0,最终导致 hls demuxer 抛出了 -1094995529 错误。
经过验证 ts 地址没问题,可以正常下载,但是不能播放。注意到下载地址有些特殊,协议是 “crypto+https://” 加上先下载 key 这个举动,猜测大概率是加密的 m3u8。
编译 debug 版本的 FFmpeg 进行符号调试
命令如下,最重要的是加上 –debug 选项:./main.sh compile -rebuild -p macos -l ffmpeg7 -a arm64 –debug –skip-fmwk。
首先在 FFmpeg 的 hls.c 里找到 “failed too many times, skipping” 的日志出现在 read_data 函数里,在 Xcode 里下一个符号断点,通过单步确定内部调用了 ret = open_input(c, v, seg, &v->input); 来下载第一片 ts。函数定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg, AVIOContext **in)
{
AVDictionary *opts = NULL;
int ret;
int is_http = 0;
if (c->http_persistent)
av_dict_set(&opts, "multiple_requests", "1", 0);
if (seg->size >= 0) {
/* try to restrict the HTTP request to the part we want
* (if this is in fact a HTTP request) */
av_dict_set_int(&opts, "offset", seg->url_offset, 0);
av_dict_set_int(&opts, "end_offset", seg->url_offset + seg->size, 0);
}
av_log(pls->parent, AV_LOG_VERBOSE, "HLS request for url '%s', offset %"PRId64", playlist %d\n",
seg->url, seg->url_offset, pls->index);
if (seg->key_type == KEY_AES_128 || seg->key_type == KEY_SAMPLE_AES) {
if (strcmp(seg->key, pls->key_url)) {
AVIOContext *pb = NULL;
if (open_url(pls->parent, &pb, seg->key, &c->avio_opts, opts, NULL) == 0) {
ret = avio_read(pb, pls->key, sizeof(pls->key));
if (ret != sizeof(pls->key)) {
av_log(pls->parent, AV_LOG_ERROR, "Unable to read key file %s\n",
seg->key);
}
ff_format_io_close(pls->parent, &pb);
} else {
av_log(pls->parent, AV_LOG_ERROR, "Unable to open key file %s\n",
seg->key);
}
av_strlcpy(pls->key_url, seg->key, sizeof(pls->key_url));
}
}
if (seg->key_type == KEY_AES_128) {
char iv[33], key[33], url[MAX_URL_SIZE];
ff_data_to_hex(iv, seg->iv, sizeof(seg->iv), 0);
ff_data_to_hex(key, pls->key, sizeof(pls->key), 0);
if (strstr(seg->url, "://"))
snprintf(url, sizeof(url), "crypto+%s", seg->url);
else
snprintf(url, sizeof(url), "crypto:%s", seg->url);
av_dict_set(&opts, "key", key, 0);
av_dict_set(&opts, "iv", iv, 0);
ret = open_url(pls->parent, in, url, &c->avio_opts, opts, &is_http);
if (ret < 0) {
goto cleanup;
}
ret = 0;
} else {
ret = open_url(pls->parent, in, seg->url, &c->avio_opts, opts, &is_http);
}
/* Seek to the requested position. If this was a HTTP request, the offset
* should already be where want it to, but this allows e.g. local testing
* without a HTTP server.
*
* This is not done for HTTP at all as avio_seek() does internal bookkeeping
* of file offset which is out-of-sync with the actual offset when "offset"
* AVOption is used with http protocol, causing the seek to not be a no-op
* as would be expected. Wrong offset received from the server will not be
* noticed without the call, though.
*/
if (ret == 0 && !is_http && seg->url_offset) {
int64_t seekret = avio_seek(*in, seg->url_offset, SEEK_SET);
if (seekret < 0) {
av_log(pls->parent, AV_LOG_ERROR, "Unable to seek to offset %"PRId64" of HLS segment '%s'\n", seg->url_offset, seg->url);
ret = seekret;
ff_format_io_close(pls->parent, in);
}
}
cleanup:
av_dict_free(&opts);
pls->cur_seg_offset = 0;
return ret;
}
逻辑走到第 20 行,加密方式是 KEY_AES_128 里,并请求了 seg->key,然后构造 crypto+http 地址,不过也可能是 crypto: 取决于 url 是否包含 “://” 。
然后把 key 和 iv 放到 option 里,调用 open_url 开始发起新的请求,返回值 ret 是 -1330794744,需要进入函数继续分析,定义如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
static int open_url(AVFormatContext *s, AVIOContext **pb, const char *url,
AVDictionary **opts, AVDictionary *opts2, int *is_http_out)
{
HLSContext *c = s->priv_data;
AVDictionary *tmp = NULL;
const char *proto_name = NULL;
int ret;
int is_http = 0;
if (av_strstart(url, "crypto", NULL)) {
if (url[6] == '+' || url[6] == ':')
proto_name = avio_find_protocol_name(url + 7);
} else if (av_strstart(url, "data", NULL)) {
if (url[4] == '+' || url[4] == ':')
proto_name = avio_find_protocol_name(url + 5);
}
if (!proto_name)
proto_name = avio_find_protocol_name(url);
if (!proto_name)
return AVERROR_INVALIDDATA;
// only http(s) & file are allowed
if (av_strstart(proto_name, "file", NULL)) {
if (strcmp(c->allowed_extensions, "ALL") && !av_match_ext(url, c->allowed_extensions)) {
av_log(s, AV_LOG_ERROR,
"Filename extension of \'%s\' is not a common multimedia extension, blocked for security reasons.\n"
"If you wish to override this adjust allowed_extensions, you can set it to \'ALL\' to allow all\n",
url);
return AVERROR_INVALIDDATA;
}
} else if (av_strstart(proto_name, "http", NULL)) {
is_http = 1;
} else if (av_strstart(proto_name, "data", NULL)) {
;
} else
return AVERROR_INVALIDDATA;
if (!strncmp(proto_name, url, strlen(proto_name)) && url[strlen(proto_name)] == ':')
;
else if (av_strstart(url, "crypto", NULL) && !strncmp(proto_name, url + 7, strlen(proto_name)) && url[7 + strlen(proto_name)] == ':')
;
else if (av_strstart(url, "data", NULL) && !strncmp(proto_name, url + 5, strlen(proto_name)) && url[5 + strlen(proto_name)] == ':')
;
else if (strcmp(proto_name, "file") || !strncmp(url, "file,", 5))
return AVERROR_INVALIDDATA;
av_dict_copy(&tmp, *opts, 0);
av_dict_copy(&tmp, opts2, 0);
if (is_http && c->http_persistent && *pb) {
ret = open_url_keepalive(c->ctx, pb, url, &tmp);
if (ret == AVERROR_EXIT) {
av_dict_free(&tmp);
return ret;
} else if (ret < 0) {
if (ret != AVERROR_EOF)
av_log(s, AV_LOG_WARNING,
"keepalive request failed for '%s' with error: '%s' when opening url, retrying with new connection\n",
url, av_err2str(ret));
av_dict_copy(&tmp, *opts, 0);
av_dict_copy(&tmp, opts2, 0);
ret = s->io_open(s, pb, url, AVIO_FLAG_READ, &tmp);
}
} else {
ret = s->io_open(s, pb, url, AVIO_FLAG_READ, &tmp);
}
if (ret >= 0) {
// update cookies on http response with setcookies.
char *new_cookies = NULL;
if (!(s->flags & AVFMT_FLAG_CUSTOM_IO))
av_opt_get(*pb, "cookies", AV_OPT_SEARCH_CHILDREN, (uint8_t**)&new_cookies);
if (new_cookies)
av_dict_set(opts, "cookies", new_cookies, AV_DICT_DONT_STRDUP_VAL);
}
av_dict_free(&tmp);
if (is_http_out)
*is_http_out = is_http;
return ret;
}
逻辑走到了 67 行,调用 ret = s->io_open(s, pb, url, AVIO_FLAG_READ, &tmp); 发请求,此时的 url 是 “crypto+https://” 协议,前面的逻辑只是确定 crypto 后面跟的这个真正的协议存在。正是这里返回了 -1330794744 错误,这个错误码一直向上返,直到 read_data 函数里,c->seg_max_retry 没有指定,默认是 0 ,所以不会请求下一片,然后 goto reload,由于播放前指定了 format-opts : max_reload = 1,满足了 reload_count > c->max_reload 条件,于是返 AVERROR_EOF。
在 FFmpeg 里 -1330794744 对应的宏是 AVERROR_PROTOCOL_NOT_FOUND,所以问题已经能定位了是没有开启 crypto 协议导致的。