Skip to content

WIP: Fixes for crypto, byte ranges, and wrapping jsfetch urls #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions bindings.c
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,9 @@ int libavjs_with_cli() {
}

#ifndef LIBAVJS_WITH_CLI
static int ffmpeg_get_total_size_bytes() { return 0; }
static int ffmpeg_get_out_time_ms() { return 0; }
static void ffmpeg_interrupt() { }
int ffmpeg_main() { return 0; }
int ffprobe_main() { return 0; }
#endif
Expand Down
5 changes: 4 additions & 1 deletion configs/configs/h264-aac-mp3/ffmpeg-config.txt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
--enable-protocol=data --enable-protocol=file
--enable-protocol=data --enable-protocol=file --enable-protocol=jsfetch --enable-protocol=crypto
--enable-filter=aresample
--enable-filter=asetnsamples
--enable-muxer=mp4
--enable-muxer=matroska
--enable-demuxer=matroska
--enable-demuxer=aac
--enable-demuxer=hls
--enable-muxer=hls
--enable-parser=aac
--enable-decoder=aac
--enable-decoder=h264
--enable-parser=h264
--enable-bsf=h264_metadata
--enable-bsf=extract_extradata
Expand Down
2 changes: 1 addition & 1 deletion dist/libav-6.5.7.1-h264-aac-mp3.mjs

Large diffs are not rendered by default.

7 changes: 4 additions & 3 deletions dist/libav-6.5.7.1-h264-aac-mp3.wasm.mjs

Large diffs are not rendered by default.

Binary file modified dist/libav-6.5.7.1-h264-aac-mp3.wasm.wasm
Binary file not shown.
6 changes: 6 additions & 0 deletions dist/libav.types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1820,6 +1820,9 @@ libavjs_with_swscale(): Promise<number>;
libavjs_create_main_thread(): Promise<number>;
ffmpeg_main(a0: number,a1: number): Promise<number>;
ffprobe_main(a0: number,a1: number): Promise<number>;
ffmpeg_interrupt(): Promise<void>;
ffmpeg_get_out_time_ms(): Promise<number>;
ffmpeg_get_total_size_bytes(): Promise<number>;
AVFrame_channel_layout(ptr: number): Promise<number>;
AVFrame_channel_layout_s(ptr: number, val: number): Promise<void>;
AVFrame_channel_layouthi(ptr: number): Promise<number>;
Expand Down Expand Up @@ -3988,6 +3991,9 @@ libavjs_with_swscale_sync(): number;
libavjs_create_main_thread_sync(): number;
ffmpeg_main_sync(a0: number,a1: number): number | Promise<number>;
ffprobe_main_sync(a0: number,a1: number): number | Promise<number>;
ffmpeg_interrupt_sync(): void | Promise<void>;
ffmpeg_get_out_time_ms_sync(): number | Promise<number>;
ffmpeg_get_total_size_bytes_sync(): number | Promise<number>;
AVFrame_channel_layout_sync(ptr: number): number;
AVFrame_channel_layout_s_sync(ptr: number, val: number): void;
AVFrame_channel_layouthi_sync(ptr: number): number;
Expand Down
5 changes: 4 additions & 1 deletion funcs.json
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,10 @@
["libavjs_create_main_thread", "number", []],

["ffmpeg_main", "number", ["number", "number"], {"async": true}],
["ffprobe_main", "number", ["number", "number"], {"async": true}]
["ffprobe_main", "number", ["number", "number"], {"async": true}],
["ffmpeg_interrupt", null, [], {"async": true}],
["ffmpeg_get_out_time_ms", "number", [], {"async": true}],
["ffmpeg_get_total_size_bytes", "number", [], {"async": true}]
],

"fs": [
Expand Down
211 changes: 200 additions & 11 deletions patches/ffmpeg/07-jsfetch-protocol.diff
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,62 @@ Index: ffmpeg-6.0.1/libavformat/hls.c
===================================================================
--- ffmpeg-6.0.1.orig/libavformat/hls.c
+++ ffmpeg-6.0.1/libavformat/hls.c
@@ -673,6 +673,8 @@ static int open_url(AVFormatContext *s,
--- a/libavformat/hls.c
+++ b/libavformat/hls.c
@@ -311,6 +311,36 @@ static void free_rendition_list(HLSContext *c)
c->n_renditions = 0;
}

+// When the M3U8 contains any absolute URIs - i.e init segment, AES key, or just normal segments,
+// the jsfetch: protocol needs to be prepended.
+static int jsfetch_wrap_url(const char *url, char **out_url)
+{
+ char *url_fixed = NULL;
+
+ if (av_strstart(url, "http://", NULL) || av_strstart(url, "https://", NULL)) {
+ url_fixed = av_malloc(strlen("jsfetch:") + strlen(url) + 1);
+ if (!url_fixed)
+ return AVERROR(ENOMEM);
+ strcpy(url_fixed, "jsfetch:");
+ strcat(url_fixed, url);
+ } else if (av_strstart(url, "crypto+http://", NULL) || av_strstart(url, "crypto+https://", NULL)) {
+ const char *url_tail = url + strlen("crypto+");
+ url_fixed = av_malloc(strlen("crypto+jsfetch:") + strlen(url_tail) + 1);
+ if (!url_fixed)
+ return AVERROR(ENOMEM);
+ strcpy(url_fixed, "crypto+jsfetch:");
+ strcat(url_fixed, url_tail);
+ } else {
+ // No transformation needed; just duplicate original URL
+ url_fixed = av_strdup(url);
+ if (!url_fixed)
+ return AVERROR(ENOMEM);
+ }
+
+ *out_url = url_fixed;
+ return 0;
+}
+
static struct playlist *new_playlist(HLSContext *c, const char *url,
const char *base)
{
@@ -438,7 +468,14 @@ static struct segment *new_init_section(struct playlist *pls,
return NULL;
}
}
- sec->url = av_strdup(ptr);
+ char *wrapped_url = NULL;
+ int ret = jsfetch_wrap_url(ptr, &wrapped_url);
+ if (ret < 0) {
+ sec->url = av_strdup(ptr);
+ fprintf(stderr, "Failed to wrap URL: %s\n", av_err2str(ret));
+ } else {
+ sec->url = av_strdup(wrapped_url);
+ }
if (!sec->url) {
av_free(sec);
return NULL;
@@ -680,6 +717,8 @@ static int open_url(AVFormatContext *s, AVIOContext **pb, const char *url,
is_http = 1;
} else if (av_strstart(proto_name, "data", NULL)) {
;
Expand All @@ -26,11 +81,68 @@ Index: ffmpeg-6.0.1/libavformat/hls.c
} else
return AVERROR_INVALIDDATA;

Index: ffmpeg-6.0.1/libavformat/jsfetch.c
@@ -981,7 +1020,14 @@ static int parse_playlist(HLSContext *c, const char *url,
av_free(seg);
goto fail;
}
- seg->url = av_strdup(tmp_str);
+ char *wrapped_url = NULL;
+ int ret = jsfetch_wrap_url(tmp_str, &wrapped_url);
+ if (ret < 0) {
+ seg->url = av_strdup(tmp_str);
+ fprintf(stderr, "Failed to wrap URL: %s\n", av_err2str(ret));
+ } else {
+ seg->url = av_strdup(wrapped_url);
+ }
if (!seg->url) {
av_free(seg->key);
av_free(seg);
@@ -1294,6 +1340,16 @@ static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg,
* (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);
+
+ // Pre-populate the byte range header if needed. Passed to jsfetch in AVDictionary.
+ char range_header[128] = {0};
+ if (seg->url_offset >= 0 && seg->size > 0) {
+ snprintf(range_header, sizeof(range_header), "bytes=%" PRId64 "-%" PRId64, seg->url_offset, seg->url_offset + seg->size - 1);
+ av_dict_set(&opts, "range_header", range_header, 0);
+ } else if (seg->url_offset >= 0) {
+ snprintf(range_header, sizeof(range_header), "bytes=%" PRId64 "-", seg->url_offset);
+ av_dict_set(&opts, "range_header", range_header, 0);
+ }
}

av_log(pls->parent, AV_LOG_VERBOSE, "HLS request for url '%s', offset %"PRId64", playlist %d\n",
@@ -1348,14 +1404,15 @@ static int open_input(HLSContext *c, struct playlist *pls, struct segment *seg,
* 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);
- }
- }
+ // Breaks seeking on M3U8s containing byte ranges. Not needed for "http" anyways.
+ // 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);
Index: ffmpeg-6.0.1/libavformat/jsfetch.c
===================================================================
--- /dev/null
+++ ffmpeg-6.0.1/libavformat/jsfetch.c
@@ -0,0 +1,188 @@
@@ -0,0 +1,194 @@
+/*
+ * JavaScript fetch metaprotocol for ffmpeg client
+ * Copyright (c) 2023 Yahweasel and contributors
Expand Down Expand Up @@ -70,7 +182,6 @@ Index: ffmpeg-6.0.1/libavformat/jsfetch.c
+static const AVOption options[] = {
+ { NULL }
+};
+
+#if CONFIG_JSFETCH_PROTOCOL
+static const AVClass jsfetch_context_class = {
+ .class_name = "jsfetch",
Expand All @@ -82,14 +193,17 @@ Index: ffmpeg-6.0.1/libavformat/jsfetch.c
+/**
+ * Open a fetch connection (JavaScript side).
+ */
+EM_JS(int, jsfetch_open_js, (const char *url), {
+EM_JS(int, jsfetch_open_js, (const char *url, char* range_header, bool has_range), {
+ return Asyncify.handleAsync(function() {
+ return Promise.all([]).then(function() {
+ url = UTF8ToString(url);
+ if (url.slice(0, 8) === "jsfetch:")
+ return fetch(url.slice(8));
+ else
+ return fetch(url);
+ var headers = {};
+ if (has_range) {
+ var range = range_header ? UTF8ToString(range_header) : undefined;
+ headers.Range = range;
+ }
+ var fetchUrl = url.startsWith("jsfetch:") ? url.slice(8) : url;
+ return fetch(fetchUrl, { headers });
+ }).then(function(response) {
+ if (!Module.libavjsJSFetch)
+ Module.libavjsJSFetch = {ctr: 1, fetches: {}};
Expand Down Expand Up @@ -124,7 +238,11 @@ Index: ffmpeg-6.0.1/libavformat/jsfetch.c
+{
+ JSFetchContext *ctx = h->priv_data;
+ h->is_streamed = 1;
+ ctx->idx = jsfetch_open_js(url);
+
+ AVDictionaryEntry *entry = av_dict_get(*options, "range_header", NULL, 0);
+ const char *range_ptr = entry ? entry->value : NULL;
+ bool has_range = range_ptr != NULL;
+ ctx->idx = jsfetch_open_js(url, range_ptr, has_range);
+ return (ctx->idx > 0) ? 0 : ctx->idx;
+}
+
Expand Down Expand Up @@ -216,7 +334,7 @@ Index: ffmpeg-6.0.1/libavformat/jsfetch.c
+ .priv_data_size = sizeof(JSFetchContext),
+ .priv_data_class = &jsfetch_context_class,
+ .flags = URL_PROTOCOL_FLAG_NETWORK,
+ .default_whitelist = "jsfetch,http,https"
+ .default_whitelist = "jsfetch,http,https,crypto"
+};
+#endif
Index: ffmpeg-6.0.1/libavformat/protocols.c
Expand All @@ -234,3 +352,74 @@ Index: ffmpeg-6.0.1/libavformat/protocols.c
#include "libavformat/protocol_list.c"

const AVClass *ff_urlcontext_child_class_iterate(void **iter)

Index: ffmpeg-6.0.1/libavformat/mov.c
===================================================================
From https://trac.ffmpeg.org/ticket/7359
https://github.com/FFmpeg/FFmpeg/commit/380a518c439d4e5e3cf17b97e4a06259e8048f99
Note: this patch isn't in 7.1.1.
--- ffmpeg-6.0.1.orig/libavformat/mov.c
+++ ffmpeg-6.0.1/libavformat/mov.c
@@ -10416,15 +10416,15 @@ static int mov_switch_root(AVFormatContext *s, int64_t target, int index)

if (index >= 0 && index < mov->frag_index.nb_items)
target = mov->frag_index.item[index].moof_offset;
- if (avio_seek(s->pb, target, SEEK_SET) != target) {
+ if (target >= 0 && avio_seek(s->pb, target, SEEK_SET) != target) {
av_log(mov->fc, AV_LOG_ERROR, "root atom offset 0x%"PRIx64": partial file\n", target);
return AVERROR_INVALIDDATA;
}

mov->next_root_atom = 0;
- if (index < 0 || index >= mov->frag_index.nb_items)
+ if ((index < 0 && target >= 0) || index >= mov->frag_index.nb_items)
index = search_frag_moof_offset(&mov->frag_index, target);
- if (index < mov->frag_index.nb_items &&
+ if (index >= 0 && index < mov->frag_index.nb_items &&
mov->frag_index.item[index].moof_offset == target) {
if (index + 1 < mov->frag_index.nb_items)
mov->next_root_atom = mov->frag_index.item[index + 1].moof_offset;
@@ -10554,10 +10554,43 @@ static int mov_read_packet(AVFormatContext *s, AVPacket *pkt)
MOVStreamContext *sc;
AVIndexEntry *sample;
AVStream *st = NULL;
+ FFStream *avsti = NULL;
int64_t current_index;
int ret;
+ int i;
mov->fc = s;
retry:
+ if (s->pb->pos == 0) {
+
+ // Discard current fragment index
+ if (mov->frag_index.allocated_size > 0) {
+ av_freep(&mov->frag_index.item);
+ mov->frag_index.nb_items = 0;
+ mov->frag_index.allocated_size = 0;
+ mov->frag_index.current = -1;
+ mov->frag_index.complete = 0;
+ }
+
+ for (i = 0; i < s->nb_streams; i++) {
+ AVStream *avst = s->streams[i];
+ MOVStreamContext *msc = avst->priv_data;
+
+ // Clear current sample
+ mov_current_sample_set(msc, 0);
+ msc->ctts_index = 0;
+
+ // Discard current index entries
+ avsti = ffstream(avst);
+ if (avsti->index_entries_allocated_size > 0) {
+ av_freep(&avsti->index_entries);
+ avsti->index_entries_allocated_size = 0;
+ avsti->nb_index_entries = 0;
+ }
+ }
+
+ if ((ret = mov_switch_root(s, -1, -1)) < 0)
+ return ret;
+ }
sample = mov_find_next_sample(s, &st);
if (!sample || (mov->next_root_atom && sample->pos > mov->next_root_atom)) {
if (!mov->next_root_atom)
47 changes: 45 additions & 2 deletions patches/ffmpeg/08-fftools.diff
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,54 @@ Index: ffmpeg-7.1/fftools/ffmpeg.c
}
av_freep(&vstats_filename);
of_enc_stats_close();
@@ -943,7 +950,12 @@ static int64_t getmaxrss(void)
@@ -544,6 +551,19 @@ void update_benchmark(const char *fmt, ...)
current_time = t;
}
}
+static volatile int curr_transcode_pts_ms = 0;
+int ffmpeg_get_out_time_ms(void);
+int ffmpeg_get_out_time_ms()
+{
+ return curr_transcode_pts_ms;
+}
+
+static volatile int curr_total_size_bytes = 0;
+int ffmpeg_get_total_size_bytes(void);
+int ffmpeg_get_total_size_bytes()
+{
+ return curr_total_size_bytes;
+}

static void print_report(int is_last_report, int64_t timer_start, int64_t cur_time, int64_t pts)
{
@@ -645,6 +665,9 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti

if (total_size < 0) av_bprintf(&buf_script, "total_size=N/A\n");
else av_bprintf(&buf_script, "total_size=%"PRId64"\n", total_size);
+ if (total_size > 0) {
+ curr_total_size_bytes = total_size;
+ }
if (pts == AV_NOPTS_VALUE) {
av_bprintf(&buf_script, "out_time_us=N/A\n");
av_bprintf(&buf_script, "out_time_ms=N/A\n");
@@ -654,6 +677,7 @@ static void print_report(int is_last_report, int64_t timer_start, int64_t cur_ti
av_bprintf(&buf_script, "out_time_ms=%"PRId64"\n", pts);
av_bprintf(&buf_script, "out_time=%s%02"PRId64":%02d:%02d.%06d\n",
hours_sign, hours, mins, secs, us);
+ curr_transcode_pts_ms = (pts / 1000);
}

if (nb_frames_dup || nb_frames_drop)
@@ -943,7 +967,17 @@ static int64_t getmaxrss(void)
#endif
}

+#ifdef __EMSCRIPTEN__
+void ffmpeg_interrupt(void);
+void ffmpeg_interrupt() {
+ received_nb_signals++;
+}
+
+int ffmpeg_main(int argc, char **argv);
+int ffmpeg_main(int argc, char **argv)
+#else
Expand All @@ -45,7 +88,7 @@ Index: ffmpeg-7.1/fftools/ffmpeg.c
{
Scheduler *sch = NULL;

@@ -1009,8 +1021,12 @@ finish:
@@ -1009,8 +1043,12 @@ finish:
ret = 0;

ffmpeg_cleanup(ret);
Expand Down
Loading