Skip to content

Commit f0bfe1a

Browse files
committed
* Enable the crypto protocol to support AES encrypted M3U8s
* Properly prepend http/https URLs with `jsfetch:`. This is to address situations where the m3u8 was pre-pended with `jsfetch:` but the M3U8 contains absolute URLs. Since those start with i.e `http://`, ffmpeg can't fetch them. * Add support for byte ranges in jsfetch * Add `ffmpeg_get_out_time_ms` and `ffmpeg_get_out_time_ms` for progress tracking * Add `ffmpeg_interrupt` to abort a running download * Add the patch from [https://trac.ffmpeg.org/ticket/7359](https://trac.ffmpeg.org/ticket/7359) which address issues with seeking HLS streams. This patch is not in ffmpeg 7.1.1 release
1 parent d07b9f8 commit f0bfe1a

9 files changed

+267
-19
lines changed

bindings.c

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,9 @@ int libavjs_with_cli() {
492492
}
493493

494494
#ifndef LIBAVJS_WITH_CLI
495+
static int ffmpeg_get_total_size_bytes() { return 0; }
496+
static int ffmpeg_get_out_time_ms() { return 0; }
497+
static void ffmpeg_interrupt() { }
495498
int ffmpeg_main() { return 0; }
496499
int ffprobe_main() { return 0; }
497500
#endif

configs/configs/h264-aac-mp3/ffmpeg-config.txt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
1-
--enable-protocol=data --enable-protocol=file
1+
--enable-protocol=data --enable-protocol=file --enable-protocol=jsfetch --enable-protocol=crypto
22
--enable-filter=aresample
33
--enable-filter=asetnsamples
44
--enable-muxer=mp4
55
--enable-muxer=matroska
66
--enable-demuxer=matroska
77
--enable-demuxer=aac
8+
--enable-demuxer=hls
9+
--enable-muxer=hls
810
--enable-parser=aac
911
--enable-decoder=aac
12+
--enable-decoder=h264
1013
--enable-parser=h264
1114
--enable-bsf=h264_metadata
1215
--enable-bsf=extract_extradata

dist/libav-6.5.7.1-h264-aac-mp3.mjs

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

dist/libav-6.5.7.1-h264-aac-mp3.wasm.mjs

Lines changed: 4 additions & 3 deletions
Large diffs are not rendered by default.
654 KB
Binary file not shown.

dist/libav.types.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1820,6 +1820,9 @@ libavjs_with_swscale(): Promise<number>;
18201820
libavjs_create_main_thread(): Promise<number>;
18211821
ffmpeg_main(a0: number,a1: number): Promise<number>;
18221822
ffprobe_main(a0: number,a1: number): Promise<number>;
1823+
ffmpeg_interrupt(): Promise<void>;
1824+
ffmpeg_get_out_time_ms(): Promise<number>;
1825+
ffmpeg_get_total_size_bytes(): Promise<number>;
18231826
AVFrame_channel_layout(ptr: number): Promise<number>;
18241827
AVFrame_channel_layout_s(ptr: number, val: number): Promise<void>;
18251828
AVFrame_channel_layouthi(ptr: number): Promise<number>;
@@ -3988,6 +3991,9 @@ libavjs_with_swscale_sync(): number;
39883991
libavjs_create_main_thread_sync(): number;
39893992
ffmpeg_main_sync(a0: number,a1: number): number | Promise<number>;
39903993
ffprobe_main_sync(a0: number,a1: number): number | Promise<number>;
3994+
ffmpeg_interrupt_sync(): void | Promise<void>;
3995+
ffmpeg_get_out_time_ms_sync(): number | Promise<number>;
3996+
ffmpeg_get_total_size_bytes_sync(): number | Promise<number>;
39913997
AVFrame_channel_layout_sync(ptr: number): number;
39923998
AVFrame_channel_layout_s_sync(ptr: number, val: number): void;
39933999
AVFrame_channel_layouthi_sync(ptr: number): number;

funcs.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,10 @@
126126
["libavjs_create_main_thread", "number", []],
127127

128128
["ffmpeg_main", "number", ["number", "number"], {"async": true}],
129-
["ffprobe_main", "number", ["number", "number"], {"async": true}]
129+
["ffprobe_main", "number", ["number", "number"], {"async": true}],
130+
["ffmpeg_interrupt", null, [], {"async": true}],
131+
["ffmpeg_get_out_time_ms", "number", [], {"async": true}],
132+
["ffmpeg_get_total_size_bytes", "number", [], {"async": true}]
130133
],
131134

132135
"fs": [

patches/ffmpeg/07-jsfetch-protocol.diff

Lines changed: 200 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,62 @@ Index: ffmpeg-6.0.1/libavformat/hls.c
1717
===================================================================
1818
--- ffmpeg-6.0.1.orig/libavformat/hls.c
1919
+++ ffmpeg-6.0.1/libavformat/hls.c
20-
@@ -673,6 +673,8 @@ static int open_url(AVFormatContext *s,
20+
--- a/libavformat/hls.c
21+
+++ b/libavformat/hls.c
22+
@@ -311,6 +311,36 @@ static void free_rendition_list(HLSContext *c)
23+
c->n_renditions = 0;
24+
}
25+
26+
+// When the M3U8 contains any absolute URIs - i.e init segment, AES key, or just normal segments,
27+
+// the jsfetch: protocol needs to be prepended.
28+
+static int jsfetch_wrap_url(const char *url, char **out_url)
29+
+{
30+
+ char *url_fixed = NULL;
31+
+
32+
+ if (av_strstart(url, "http://", NULL) || av_strstart(url, "https://", NULL)) {
33+
+ url_fixed = av_malloc(strlen("jsfetch:") + strlen(url) + 1);
34+
+ if (!url_fixed)
35+
+ return AVERROR(ENOMEM);
36+
+ strcpy(url_fixed, "jsfetch:");
37+
+ strcat(url_fixed, url);
38+
+ } else if (av_strstart(url, "crypto+http://", NULL) || av_strstart(url, "crypto+https://", NULL)) {
39+
+ const char *url_tail = url + strlen("crypto+");
40+
+ url_fixed = av_malloc(strlen("crypto+jsfetch:") + strlen(url_tail) + 1);
41+
+ if (!url_fixed)
42+
+ return AVERROR(ENOMEM);
43+
+ strcpy(url_fixed, "crypto+jsfetch:");
44+
+ strcat(url_fixed, url_tail);
45+
+ } else {
46+
+ // No transformation needed; just duplicate original URL
47+
+ url_fixed = av_strdup(url);
48+
+ if (!url_fixed)
49+
+ return AVERROR(ENOMEM);
50+
+ }
51+
+
52+
+ *out_url = url_fixed;
53+
+ return 0;
54+
+}
55+
+
56+
static struct playlist *new_playlist(HLSContext *c, const char *url,
57+
const char *base)
58+
{
59+
@@ -438,7 +468,14 @@ static struct segment *new_init_section(struct playlist *pls,
60+
return NULL;
61+
}
62+
}
63+
- sec->url = av_strdup(ptr);
64+
+ char *wrapped_url = NULL;
65+
+ int ret = jsfetch_wrap_url(ptr, &wrapped_url);
66+
+ if (ret < 0) {
67+
+ sec->url = av_strdup(ptr);
68+
+ fprintf(stderr, "Failed to wrap URL: %s\n", av_err2str(ret));
69+
+ } else {
70+
+ sec->url = av_strdup(wrapped_url);
71+
+ }
72+
if (!sec->url) {
73+
av_free(sec);
74+
return NULL;
75+
@@ -680,6 +717,8 @@ static int open_url(AVFormatContext *s, AVIOContext **pb, const char *url,
2176
is_http = 1;
2277
} else if (av_strstart(proto_name, "data", NULL)) {
2378
;
@@ -26,11 +81,68 @@ Index: ffmpeg-6.0.1/libavformat/hls.c
2681
} else
2782
return AVERROR_INVALIDDATA;
2883

29-
Index: ffmpeg-6.0.1/libavformat/jsfetch.c
84+
@@ -981,7 +1020,14 @@ static int parse_playlist(HLSContext *c, const char *url,
85+
av_free(seg);
86+
goto fail;
87+
}
88+
- seg->url = av_strdup(tmp_str);
89+
+ char *wrapped_url = NULL;
90+
+ int ret = jsfetch_wrap_url(tmp_str, &wrapped_url);
91+
+ if (ret < 0) {
92+
+ seg->url = av_strdup(tmp_str);
93+
+ fprintf(stderr, "Failed to wrap URL: %s\n", av_err2str(ret));
94+
+ } else {
95+
+ seg->url = av_strdup(wrapped_url);
96+
+ }
97+
if (!seg->url) {
98+
av_free(seg->key);
99+
av_free(seg);
100+
@@ -1294,6 +1340,16 @@ static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg,
101+
* (if this is in fact a HTTP request) */
102+
av_dict_set_int(&opts, "offset", seg->url_offset, 0);
103+
av_dict_set_int(&opts, "end_offset", seg->url_offset + seg->size, 0);
104+
+
105+
+ // Pre-populate the byte range header if needed. Passed to jsfetch in AVDictionary.
106+
+ char range_header[128] = {0};
107+
+ if (seg->url_offset >= 0 && seg->size > 0) {
108+
+ snprintf(range_header, sizeof(range_header), "bytes=%" PRId64 "-%" PRId64, seg->url_offset, seg->url_offset + seg->size - 1);
109+
+ av_dict_set(&opts, "range_header", range_header, 0);
110+
+ } else if (seg->url_offset >= 0) {
111+
+ snprintf(range_header, sizeof(range_header), "bytes=%" PRId64 "-", seg->url_offset);
112+
+ av_dict_set(&opts, "range_header", range_header, 0);
113+
+ }
114+
}
115+
116+
av_log(pls->parent, AV_LOG_VERBOSE, "HLS request for url '%s', offset %"PRId64", playlist %d\n",
117+
@@ -1348,14 +1404,15 @@ static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg,
118+
* as would be expected. Wrong offset received from the server will not be
119+
* noticed without the call, though.
120+
*/
121+
- if (ret == 0 && !is_http && seg->url_offset) {
122+
- int64_t seekret = avio_seek(*in, seg->url_offset, SEEK_SET);
123+
- if (seekret < 0) {
124+
- av_log(pls->parent, AV_LOG_ERROR, "Unable to seek to offset %"PRId64" of HLS segment '%s'\n", seg->url_offset, seg->url);
125+
- ret = seekret;
126+
- ff_format_io_close(pls->parent, in);
127+
- }
128+
- }
129+
+ // Breaks seeking on M3U8s containing byte ranges. Not needed for "http" anyways.
130+
+ // if (ret == 0 && !is_http && seg->url_offset) {
131+
+ // int64_t seekret = avio_seek(*in, seg->url_offset, SEEK_SET);
132+
+ // if (seekret < 0) {
133+
+ // av_log(pls->parent, AV_LOG_ERROR, "Unable to seek to offset %"PRId64" of HLS segment '%s'\n", seg->url_offset, seg->url);
134+
+ // ret = seekret;
135+
+ // ff_format_io_close(pls->parent, in);
136+
+ // }
137+
+ // }
138+
139+
cleanup:
140+
av_dict_free(&opts);
141+
Index: ffmpeg-6.0.1/libavformat/jsfetch.c
30142
===================================================================
31143
--- /dev/null
32144
+++ ffmpeg-6.0.1/libavformat/jsfetch.c
33-
@@ -0,0 +1,188 @@
145+
@@ -0,0 +1,194 @@
34146
+/*
35147
+ * JavaScript fetch metaprotocol for ffmpeg client
36148
+ * Copyright (c) 2023 Yahweasel and contributors
@@ -70,7 +182,6 @@ Index: ffmpeg-6.0.1/libavformat/jsfetch.c
70182
+static const AVOption options[] = {
71183
+ { NULL }
72184
+};
73-
+
74185
+#if CONFIG_JSFETCH_PROTOCOL
75186
+static const AVClass jsfetch_context_class = {
76187
+ .class_name = "jsfetch",
@@ -82,14 +193,17 @@ Index: ffmpeg-6.0.1/libavformat/jsfetch.c
82193
+/**
83194
+ * Open a fetch connection (JavaScript side).
84195
+ */
85-
+EM_JS(int, jsfetch_open_js, (const char *url), {
196+
+EM_JS(int, jsfetch_open_js, (const char *url, char* range_header, bool has_range), {
86197
+ return Asyncify.handleAsync(function() {
87198
+ return Promise.all([]).then(function() {
88199
+ url = UTF8ToString(url);
89-
+ if (url.slice(0, 8) === "jsfetch:")
90-
+ return fetch(url.slice(8));
91-
+ else
92-
+ return fetch(url);
200+
+ var headers = {};
201+
+ if (has_range) {
202+
+ var range = range_header ? UTF8ToString(range_header) : undefined;
203+
+ headers.Range = range;
204+
+ }
205+
+ var fetchUrl = url.startsWith("jsfetch:") ? url.slice(8) : url;
206+
+ return fetch(fetchUrl, { headers });
93207
+ }).then(function(response) {
94208
+ if (!Module.libavjsJSFetch)
95209
+ Module.libavjsJSFetch = {ctr: 1, fetches: {}};
@@ -124,7 +238,11 @@ Index: ffmpeg-6.0.1/libavformat/jsfetch.c
124238
+{
125239
+ JSFetchContext *ctx = h->priv_data;
126240
+ h->is_streamed = 1;
127-
+ ctx->idx = jsfetch_open_js(url);
241+
+
242+
+ AVDictionaryEntry *entry = av_dict_get(*options, "range_header", NULL, 0);
243+
+ const char *range_ptr = entry ? entry->value : NULL;
244+
+ bool has_range = range_ptr != NULL;
245+
+ ctx->idx = jsfetch_open_js(url, range_ptr, has_range);
128246
+ return (ctx->idx > 0) ? 0 : ctx->idx;
129247
+}
130248
+
@@ -216,7 +334,7 @@ Index: ffmpeg-6.0.1/libavformat/jsfetch.c
216334
+ .priv_data_size = sizeof(JSFetchContext),
217335
+ .priv_data_class = &jsfetch_context_class,
218336
+ .flags = URL_PROTOCOL_FLAG_NETWORK,
219-
+ .default_whitelist = "jsfetch,http,https"
337+
+ .default_whitelist = "jsfetch,http,https,crypto"
220338
+};
221339
+#endif
222340
Index: ffmpeg-6.0.1/libavformat/protocols.c
@@ -234,3 +352,74 @@ Index: ffmpeg-6.0.1/libavformat/protocols.c
234352
#include "libavformat/protocol_list.c"
235353

236354
const AVClass *ff_urlcontext_child_class_iterate(void **iter)
355+
356+
Index: ffmpeg-6.0.1/libavformat/mov.c
357+
===================================================================
358+
From https://trac.ffmpeg.org/ticket/7359
359+
https://github.com/FFmpeg/FFmpeg/commit/380a518c439d4e5e3cf17b97e4a06259e8048f99
360+
Note: this patch isn't in 7.1.1.
361+
--- ffmpeg-6.0.1.orig/libavformat/mov.c
362+
+++ ffmpeg-6.0.1/libavformat/mov.c
363+
@@ -10416,15 +10416,15 @@ static int mov_switch_root(AVFormatContext *s, int64_t target, int index)
364+
365+
if (index >= 0 && index < mov->frag_index.nb_items)
366+
target = mov->frag_index.item[index].moof_offset;
367+
- if (avio_seek(s->pb, target, SEEK_SET) != target) {
368+
+ if (target >= 0 && avio_seek(s->pb, target, SEEK_SET) != target) {
369+
av_log(mov->fc, AV_LOG_ERROR, "root atom offset 0x%"PRIx64": partial file\n", target);
370+
return AVERROR_INVALIDDATA;
371+
}
372+
373+
mov->next_root_atom = 0;
374+
- if (index < 0 || index >= mov->frag_index.nb_items)
375+
+ if ((index < 0 && target >= 0) || index >= mov->frag_index.nb_items)
376+
index = search_frag_moof_offset(&mov->frag_index, target);
377+
- if (index < mov->frag_index.nb_items &&
378+
+ if (index >= 0 && index < mov->frag_index.nb_items &&
379+
mov->frag_index.item[index].moof_offset == target) {
380+
if (index + 1 < mov->frag_index.nb_items)
381+
mov->next_root_atom = mov->frag_index.item[index + 1].moof_offset;
382+
@@ -10554,10 +10554,43 @@ static int mov_read_packet(AVFormatContext *s, AVPacket *pkt)
383+
MOVStreamContext *sc;
384+
AVIndexEntry *sample;
385+
AVStream *st = NULL;
386+
+ FFStream *avsti = NULL;
387+
int64_t current_index;
388+
int ret;
389+
+ int i;
390+
mov->fc = s;
391+
retry:
392+
+ if (s->pb->pos == 0) {
393+
+
394+
+ // Discard current fragment index
395+
+ if (mov->frag_index.allocated_size > 0) {
396+
+ av_freep(&mov->frag_index.item);
397+
+ mov->frag_index.nb_items = 0;
398+
+ mov->frag_index.allocated_size = 0;
399+
+ mov->frag_index.current = -1;
400+
+ mov->frag_index.complete = 0;
401+
+ }
402+
+
403+
+ for (i = 0; i < s->nb_streams; i++) {
404+
+ AVStream *avst = s->streams[i];
405+
+ MOVStreamContext *msc = avst->priv_data;
406+
+
407+
+ // Clear current sample
408+
+ mov_current_sample_set(msc, 0);
409+
+ msc->ctts_index = 0;
410+
+
411+
+ // Discard current index entries
412+
+ avsti = ffstream(avst);
413+
+ if (avsti->index_entries_allocated_size > 0) {
414+
+ av_freep(&avsti->index_entries);
415+
+ avsti->index_entries_allocated_size = 0;
416+
+ avsti->nb_index_entries = 0;
417+
+ }
418+
+ }
419+
+
420+
+ if ((ret = mov_switch_root(s, -1, -1)) < 0)
421+
+ return ret;
422+
+ }
423+
sample = mov_find_next_sample(s, &st);
424+
if (!sample || (mov->next_root_atom && sample->pos > mov->next_root_atom)) {
425+
if (!mov->next_root_atom)

patches/ffmpeg/08-fftools.diff

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,54 @@ Index: ffmpeg-7.1/fftools/ffmpeg.c
3232
}
3333
av_freep(&vstats_filename);
3434
of_enc_stats_close();
35-
@@ -943,7 +950,12 @@ static int64_t getmaxrss(void)
35+
@@ -544,6 +551,19 @@ void update_benchmark(const char *fmt, ...)
36+
current_time = t;
37+
}
38+
}
39+
+static volatile int curr_transcode_pts_ms = 0;
40+
+int ffmpeg_get_out_time_ms(void);
41+
+int ffmpeg_get_out_time_ms()
42+
+{
43+
+ return curr_transcode_pts_ms;
44+
+}
45+
+
46+
+static volatile int curr_total_size_bytes = 0;
47+
+int ffmpeg_get_total_size_bytes(void);
48+
+int ffmpeg_get_total_size_bytes()
49+
+{
50+
+ return curr_total_size_bytes;
51+
+}
52+
53+
static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time, int64_t pts)
54+
{
55+
@@ -645,6 +665,9 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
56+
57+
if (total_size < 0) av_bprintf(&buf_script, "total_size=N/A\n");
58+
else av_bprintf(&buf_script, "total_size=%"PRId64"\n", total_size);
59+
+ if (total_size > 0) {
60+
+ curr_total_size_bytes = total_size;
61+
+ }
62+
if (pts == AV_NOPTS_VALUE) {
63+
av_bprintf(&buf_script, "out_time_us=N/A\n");
64+
av_bprintf(&buf_script, "out_time_ms=N/A\n");
65+
@@ -654,6 +677,7 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
66+
av_bprintf(&buf_script, "out_time_ms=%"PRId64"\n", pts);
67+
av_bprintf(&buf_script, "out_time=%s%02"PRId64":%02d:%02d.%06d\n",
68+
hours_sign, hours, mins, secs, us);
69+
+ curr_transcode_pts_ms = (pts / 1000);
70+
}
71+
72+
if (nb_frames_dup || nb_frames_drop)
73+
@@ -943,7 +967,17 @@ static int64_t getmaxrss(void)
3674
#endif
3775
}
3876

3977
+#ifdef __EMSCRIPTEN__
78+
+void ffmpeg_interrupt(void);
79+
+void ffmpeg_interrupt() {
80+
+ received_nb_signals++;
81+
+}
82+
+
4083
+int ffmpeg_main(int argc, char **argv);
4184
+int ffmpeg_main(int argc, char **argv)
4285
+#else
@@ -45,7 +88,7 @@ Index: ffmpeg-7.1/fftools/ffmpeg.c
4588
{
4689
Scheduler *sch = NULL;
4790

48-
@@ -1009,8 +1021,12 @@ finish:
91+
@@ -1009,8 +1043,12 @@ finish:
4992
ret = 0;
5093

5194
ffmpeg_cleanup(ret);

0 commit comments

Comments
 (0)