Coverage Report

Created: 2026-05-16 07:24

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/mpv/stream/stream_lavf.c
Line
Count
Source
1
/*
2
 * This file is part of mpv.
3
 *
4
 * mpv is free software; you can redistribute it and/or
5
 * modify it under the terms of the GNU Lesser General Public
6
 * License as published by the Free Software Foundation; either
7
 * version 2.1 of the License, or (at your option) any later version.
8
 *
9
 * mpv is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
 * GNU Lesser General Public License for more details.
13
 *
14
 * You should have received a copy of the GNU Lesser General Public
15
 * License along with mpv.  If not, see <http://www.gnu.org/licenses/>.
16
 */
17
18
#include <libavformat/avformat.h>
19
#include <libavformat/avio.h>
20
#include <libavutil/opt.h>
21
22
#include "options/path.h"
23
#include "common/common.h"
24
#include "common/msg.h"
25
#include "common/tags.h"
26
#include "common/av_common.h"
27
#include "demux/demux.h"
28
#include "misc/charset_conv.h"
29
#include "misc/thread_tools.h"
30
#include "stream.h"
31
#include "options/m_config.h"
32
#include "options/m_option.h"
33
34
#include "cookies.h"
35
36
#include "misc/bstr.h"
37
#include "mpv_talloc.h"
38
39
#define OPT_BASE_STRUCT struct stream_lavf_params
40
struct stream_lavf_params {
41
    char **avopts;
42
    bool cookies_enabled;
43
    char *cookies_file;
44
    char *useragent;
45
    char *referrer;
46
    char **http_header_fields;
47
    bool tls_verify;
48
    char *tls_ca_file;
49
    char *tls_cert_file;
50
    char *tls_key_file;
51
    double timeout;
52
    char *http_proxy;
53
};
54
55
const struct m_sub_options stream_lavf_conf = {
56
    .opts = (const m_option_t[]) {
57
        {"stream-lavf-o", OPT_KEYVALUELIST(avopts)},
58
        {"http-header-fields", OPT_STRINGLIST(http_header_fields)},
59
        {"user-agent", OPT_STRING(useragent)},
60
        {"referrer", OPT_STRING(referrer)},
61
        {"cookies", OPT_BOOL(cookies_enabled)},
62
        {"cookies-file", OPT_STRING(cookies_file), .flags = M_OPT_FILE},
63
        {"tls-verify", OPT_BOOL(tls_verify)},
64
        {"tls-ca-file", OPT_STRING(tls_ca_file), .flags = M_OPT_FILE},
65
        {"tls-cert-file", OPT_STRING(tls_cert_file), .flags = M_OPT_FILE},
66
        {"tls-key-file", OPT_STRING(tls_key_file), .flags = M_OPT_FILE},
67
        {"network-timeout", OPT_DOUBLE(timeout), M_RANGE(0, DBL_MAX)},
68
        {"http-proxy", OPT_STRING(http_proxy)},
69
        {0}
70
    },
71
    .size = sizeof(struct stream_lavf_params),
72
    .defaults = &(const struct stream_lavf_params){
73
        .useragent = "libmpv",
74
        .timeout = 60,
75
    },
76
};
77
78
static const char *const http_like[] =
79
    {"http", "https", "mmsh", "mmshttp", "httproxy", NULL};
80
81
static int open_f(stream_t *stream);
82
static struct mp_tags *read_icy(stream_t *stream);
83
84
static int fill_buffer(stream_t *s, void *buffer, int max_len)
85
453k
{
86
453k
    AVIOContext *avio = s->priv;
87
453k
    int r = avio_read_partial(avio, buffer, max_len);
88
453k
    return (r <= 0) ? -1 : r;
89
453k
}
90
91
static int write_buffer(stream_t *s, void *buffer, int len)
92
0
{
93
0
    AVIOContext *avio = s->priv;
94
0
    avio_write(avio, buffer, len);
95
0
    avio_flush(avio);
96
0
    if (avio->error)
97
0
        return -1;
98
0
    return len;
99
0
}
100
101
static int seek(stream_t *s, int64_t newpos)
102
816k
{
103
816k
    AVIOContext *avio = s->priv;
104
816k
    if (avio_seek(avio, newpos, SEEK_SET) < 0) {
105
816k
        return 0;
106
816k
    }
107
25
    return 1;
108
816k
}
109
110
static int64_t get_size(stream_t *s)
111
865k
{
112
865k
    AVIOContext *avio = s->priv;
113
865k
    return avio_size(avio);
114
865k
}
115
116
static void close_f(stream_t *stream)
117
19.9k
{
118
19.9k
    AVIOContext *avio = stream->priv;
119
    /* NOTE: As of 2011 write streams must be manually flushed before close.
120
     * Currently write_buffer() always flushes them after writing.
121
     * avio_close() could return an error, but we have no way to return that
122
     * with the current stream API.
123
     */
124
19.9k
    if (avio)
125
19.9k
        avio_close(avio);
126
19.9k
}
127
128
static int control(stream_t *s, int cmd, void *arg)
129
1.83M
{
130
1.83M
    AVIOContext *avio = s->priv;
131
1.83M
    switch(cmd) {
132
0
    case STREAM_CTRL_AVSEEK: {
133
0
        struct stream_avseek *c = arg;
134
0
        int64_t r = avio_seek_time(avio, c->stream_index, c->timestamp, c->flags);
135
0
        if (r >= 0) {
136
0
            stream_drop_buffers(s);
137
0
            return 1;
138
0
        }
139
0
        break;
140
0
    }
141
7.42k
    case STREAM_CTRL_HAS_AVSEEK: {
142
        // Starting at some point, read_seek is always available, and runtime
143
        // behavior decides whether it exists or not. FFmpeg's API doesn't
144
        // return anything helpful to determine seekability upfront, so here's
145
        // a hardcoded whitelist. Not our fault.
146
        // In addition we also have to jump through ridiculous hoops just to
147
        // get the fucking protocol name.
148
7.42k
        const char *proto = NULL;
149
7.42k
        if (avio->av_class && avio->av_class->child_next) {
150
            // This usually yields the URLContext (why does it even exist?),
151
            // which holds the name of the actual protocol implementation.
152
7.42k
            void *child = avio->av_class->child_next(avio, NULL);
153
7.42k
            AVClass *cl = *(AVClass **)child;
154
7.42k
            if (cl && cl->item_name)
155
7.42k
                proto = cl->item_name(child);
156
7.42k
        }
157
7.42k
        static const char *const has_read_seek[] = {
158
7.42k
            "rtmp", "rtmpt", "rtmpe", "rtmpte", "rtmps", "rtmpts", "mmsh", 0};
159
59.3k
        for (int n = 0; has_read_seek[n]; n++) {
160
51.9k
            if (avio->read_seek && proto && strcmp(proto, has_read_seek[n]) == 0)
161
0
                return 1;
162
51.9k
        }
163
7.42k
        break;
164
7.42k
    }
165
1.82M
    case STREAM_CTRL_GET_METADATA: {
166
1.82M
        *(struct mp_tags **)arg = read_icy(s);
167
1.82M
        if (!*(struct mp_tags **)arg)
168
1.82M
            break;
169
0
        return 1;
170
1.82M
    }
171
1.83M
    }
172
1.83M
    return STREAM_UNSUPPORTED;
173
1.83M
}
174
175
static int interrupt_cb(void *ctx)
176
2.43M
{
177
2.43M
    struct stream *stream = ctx;
178
2.43M
    return mp_cancel_test(stream->cancel);
179
2.43M
}
180
181
static const char * const prefix[] = { "lavf://", "ffmpeg://" };
182
183
void mp_setup_av_network_options(AVDictionary **dict, const char *target_fmt,
184
                                 struct mpv_global *global, struct mp_log *log)
185
43.2k
{
186
43.2k
    void *temp = talloc_new(NULL);
187
43.2k
    struct stream_lavf_params *opts =
188
43.2k
        mp_get_config_group(temp, global, &stream_lavf_conf);
189
190
    // HTTP specific options (other protocols ignore them)
191
43.2k
    if (opts->useragent)
192
43.2k
        av_dict_set(dict, "user_agent", opts->useragent, 0);
193
43.2k
    if (opts->cookies_enabled) {
194
3
        char *file = opts->cookies_file;
195
3
        if (file && file[0])
196
0
            file = mp_get_user_path(temp, global, file);
197
3
        char *cookies = cookies_lavf(temp, global, log, file);
198
3
        if (cookies && cookies[0])
199
0
            av_dict_set(dict, "cookies", cookies, 0);
200
3
    }
201
43.2k
    av_dict_set(dict, "tls_verify", opts->tls_verify ? "1" : "0", 0);
202
43.2k
    if (opts->tls_ca_file) {
203
0
        char *file = mp_get_user_path(temp, global, opts->tls_ca_file);
204
0
        av_dict_set(dict, "ca_file", file, 0);
205
0
    }
206
43.2k
    if (opts->tls_cert_file) {
207
0
        char *file = mp_get_user_path(temp, global, opts->tls_cert_file);
208
0
        av_dict_set(dict, "cert_file", file, 0);
209
0
    }
210
43.2k
    if (opts->tls_key_file) {
211
0
        char *file = mp_get_user_path(temp, global, opts->tls_key_file);
212
0
        av_dict_set(dict, "key_file", file, 0);
213
0
    }
214
43.2k
    char *cust_headers = talloc_strdup(temp, "");
215
43.2k
    if (opts->referrer) {
216
2
        cust_headers = talloc_asprintf_append(cust_headers, "Referer: %s\r\n",
217
2
                                              opts->referrer);
218
2
    }
219
43.2k
    if (opts->http_header_fields) {
220
0
        for (int n = 0; opts->http_header_fields[n]; n++) {
221
0
            cust_headers = talloc_asprintf_append(cust_headers, "%s\r\n",
222
0
                                                  opts->http_header_fields[n]);
223
0
        }
224
0
    }
225
43.2k
    if (strlen(cust_headers))
226
2
        av_dict_set(dict, "headers", cust_headers, 0);
227
43.2k
    av_dict_set(dict, "icy", "1", 0);
228
    // So far, every known protocol uses microseconds for this
229
    // Except rtsp.
230
43.2k
    if (opts->timeout > 0) {
231
43.2k
        if (target_fmt && strcmp(target_fmt, "rtsp") == 0) {
232
0
            mp_verbose(log, "Broken FFmpeg RTSP API => not setting timeout.\n");
233
43.2k
        } else {
234
43.2k
            char buf[80];
235
43.2k
            snprintf(buf, sizeof(buf), "%lld", (long long)(opts->timeout * 1e6));
236
43.2k
            av_dict_set(dict, "timeout", buf, 0);
237
43.2k
        }
238
43.2k
    }
239
43.2k
    if (opts->http_proxy && opts->http_proxy[0])
240
0
        av_dict_set(dict, "http_proxy", opts->http_proxy, 0);
241
242
43.2k
    mp_set_avdict(dict, opts->avopts);
243
244
43.2k
    talloc_free(temp);
245
43.2k
}
246
247
#define PROTO(...) (const char *[]){__VA_ARGS__, NULL}
248
249
// List of safe protocols and their aliases
250
static const char **safe_protos[] = {
251
    PROTO("data"),
252
    PROTO("gopher"),
253
    PROTO("gophers"),
254
    PROTO("http", "dav", "webdav"),
255
    PROTO("httpproxy"),
256
    PROTO("https", "davs", "webdavs"),
257
    PROTO("ipfs"),
258
    PROTO("ipns"),
259
    PROTO("mmsh", "mms", "mmshttp"),
260
    PROTO("mmst"),
261
    PROTO("rist"),
262
    PROTO("rtmp"),
263
    PROTO("rtmpe"),
264
    PROTO("rtmps"),
265
    PROTO("rtmpt"),
266
    PROTO("rtmpte"),
267
    PROTO("rtmpts"),
268
    PROTO("rtp"),
269
    PROTO("srt"),
270
    PROTO("srtp"),
271
    NULL,
272
};
273
274
static char **get_safe_protocols(void)
275
60.5k
{
276
60.5k
    int num = 0;
277
60.5k
    char **protocols = NULL;
278
60.5k
    char **ffmpeg_demuxers = mp_get_lavf_demuxers();
279
60.5k
    char **ffmpeg_protos = mp_get_lavf_protocols();
280
281
666k
    for (int i = 0; ffmpeg_protos[i]; i++) {
282
11.5M
        for (int j = 0; safe_protos[j]; j++) {
283
10.9M
            if (strcmp(ffmpeg_protos[i], safe_protos[j][0]) != 0)
284
10.9M
                continue;
285
121k
            for (int k = 0; safe_protos[j][k]; k++)
286
60.5k
                MP_TARRAY_APPEND(NULL, protocols, num, talloc_strdup(protocols, safe_protos[j][k]));
287
60.5k
            break;
288
10.9M
        }
289
605k
    }
290
291
    // rtsp is a demuxer not protocol in ffmpeg so it is handled separately
292
21.7M
    for (int i = 0; ffmpeg_demuxers[i]; i++) {
293
21.6M
        if (strcmp("rtsp", ffmpeg_demuxers[i]) == 0) {
294
0
            MP_TARRAY_APPEND(NULL, protocols, num, talloc_strdup(protocols, "rtsp"));
295
0
            MP_TARRAY_APPEND(NULL, protocols, num, talloc_strdup(protocols, "rtsps"));
296
0
            break;
297
0
        }
298
21.6M
    }
299
300
60.5k
    MP_TARRAY_APPEND(NULL, protocols, num, NULL);
301
302
60.5k
    talloc_free(ffmpeg_demuxers);
303
60.5k
    talloc_free(ffmpeg_protos);
304
305
60.5k
    return protocols;
306
60.5k
}
307
308
static char **get_unsafe_protocols(void)
309
23.4k
{
310
23.4k
    int num = 0;
311
23.4k
    char **protocols = NULL;
312
23.4k
    char **safe_protocols = get_safe_protocols();
313
23.4k
    char **ffmpeg_protos = mp_get_lavf_protocols();
314
315
258k
    for (int i = 0; ffmpeg_protos[i]; i++) {
316
234k
        bool safe_protocol = false;
317
445k
        for (int j = 0; safe_protocols[j]; j++) {
318
234k
            if (strcmp(ffmpeg_protos[i], safe_protocols[j]) == 0) {
319
23.4k
                safe_protocol = true;
320
23.4k
                break;
321
23.4k
            }
322
234k
        }
323
        // Skip to avoid name conflict with builtin mpv protocol.
324
234k
        if (strcmp(ffmpeg_protos[i], "bluray") == 0 || strcmp(ffmpeg_protos[i], "dvd") == 0)
325
0
            continue;
326
327
234k
        if (!safe_protocol)
328
211k
            MP_TARRAY_APPEND(NULL, protocols, num, talloc_strdup(protocols, ffmpeg_protos[i]));
329
234k
    }
330
331
23.4k
    MP_TARRAY_APPEND(NULL, protocols, num, talloc_strdup(protocols, "ffmpeg"));
332
23.4k
    MP_TARRAY_APPEND(NULL, protocols, num, talloc_strdup(protocols, "lavf"));
333
334
23.4k
    MP_TARRAY_APPEND(NULL, protocols, num, NULL);
335
336
23.4k
    talloc_free(ffmpeg_protos);
337
23.4k
    talloc_free(safe_protocols);
338
23.4k
    return protocols;
339
23.4k
}
340
341
// Escape http URLs with unescaped, invalid characters in them.
342
// libavformat's http protocol does not do this, and a patch to add this
343
// in a 100% safe case (spaces only) was rejected.
344
static char *normalize_url(void *ta_parent, const char *filename)
345
21.4k
{
346
21.4k
    bstr proto = mp_split_proto(bstr0(filename), NULL);
347
128k
    for (int n = 0; http_like[n]; n++) {
348
106k
        if (bstr_equals0(proto, http_like[n]))
349
            // Escape everything but reserved characters.
350
            // Also don't double-scape, so include '%'.
351
108
            return mp_url_escape(ta_parent, filename, ":/?#[]@!$&'()*+,;=%");
352
106k
    }
353
21.3k
    return (char *)filename;
354
21.4k
}
355
356
static int open_f(stream_t *stream)
357
21.4k
{
358
21.4k
    AVIOContext *avio = NULL;
359
21.4k
    int res = STREAM_ERROR;
360
21.4k
    AVDictionary *dict = NULL;
361
21.4k
    void *temp = talloc_new(NULL);
362
363
21.4k
    stream->seek = NULL;
364
21.4k
    stream->seekable = false;
365
366
21.4k
    int flags = stream->mode == STREAM_WRITE ? AVIO_FLAG_WRITE : AVIO_FLAG_READ;
367
368
21.4k
    const char *filename = stream->url;
369
21.4k
    if (!filename) {
370
0
        MP_ERR(stream, "No URL\n");
371
0
        goto out;
372
0
    }
373
64.4k
    for (int i = 0; i < MP_ARRAY_SIZE(prefix); i++)
374
42.9k
        if (!strncmp(filename, prefix[i], strlen(prefix[i])))
375
6.41k
            filename += strlen(prefix[i]);
376
21.4k
    if (!strncmp(filename, "rtsp:", 5) || !strncmp(filename, "rtsps:", 6)) {
377
        /* This is handled as a special demuxer, without a separate
378
         * stream layer. demux_lavf will do all the real work. Note
379
         * that libavformat doesn't even provide a protocol entry for
380
         * this (the rtsp demuxer's probe function checks for a "rtsp:"
381
         * filename prefix), so it has to be handled specially here.
382
         */
383
7
        stream->demuxer = "lavf";
384
7
        stream->lavf_type = "rtsp";
385
7
        talloc_free(temp);
386
7
        return STREAM_OK;
387
7
    }
388
389
    // Replace "mms://" with "mmsh://", so that most mms:// URLs just work.
390
    // Replace "dav://" or "webdav://" with "http://" and "davs://" or "webdavs://" with "https://"
391
21.4k
    bstr b_filename = bstr0(filename);
392
21.4k
    if (bstr_eatstart0(&b_filename, "mms://") ||
393
21.4k
        bstr_eatstart0(&b_filename, "mmshttp://"))
394
38
    {
395
38
        filename = talloc_asprintf(temp, "mmsh://%.*s", BSTR_P(b_filename));
396
21.4k
    } else if (bstr_eatstart0(&b_filename, "dav://") || bstr_eatstart0(&b_filename, "webdav://"))
397
64
    {
398
64
        filename = talloc_asprintf(temp, "http://%.*s", BSTR_P(b_filename));
399
21.3k
    } else if (bstr_eatstart0(&b_filename, "davs://") || bstr_eatstart0(&b_filename, "webdavs://"))
400
6
    {
401
6
        filename = talloc_asprintf(temp, "https://%.*s", BSTR_P(b_filename));
402
6
    }
403
404
21.4k
    av_dict_set(&dict, "reconnect", "1", 0);
405
21.4k
    av_dict_set(&dict, "reconnect_delay_max", "7", 0);
406
407
21.4k
    mp_setup_av_network_options(&dict, NULL, stream->global, stream->log);
408
409
21.4k
    AVIOInterruptCB cb = {
410
21.4k
        .callback = interrupt_cb,
411
21.4k
        .opaque = stream,
412
21.4k
    };
413
414
21.4k
    filename = normalize_url(stream, filename);
415
416
21.4k
    if (strncmp(filename, "rtmp", 4) == 0) {
417
2
        stream->demuxer = "lavf";
418
2
        stream->lavf_type = "flv";
419
        // Setting timeout enables listen mode - force it to disabled.
420
2
        av_dict_set(&dict, "timeout", "0", 0);
421
2
    }
422
423
21.4k
    int err = avio_open2(&avio, filename, flags, &cb, &dict);
424
21.4k
    if (err < 0) {
425
1.48k
        if (err == AVERROR_PROTOCOL_NOT_FOUND)
426
1.48k
            MP_ERR(stream, "Protocol not found. Make sure"
427
1.48k
                   " FFmpeg is compiled with networking support.\n");
428
1.48k
        goto out;
429
1.48k
    }
430
431
19.9k
    mp_avdict_print_unset(stream->log, MSGL_V, dict);
432
433
19.9k
    if (avio->av_class) {
434
19.9k
        uint8_t *mt = NULL;
435
19.9k
        if (av_opt_get(avio, "mime_type", AV_OPT_SEARCH_CHILDREN, &mt) >= 0) {
436
0
            stream->mime_type = talloc_strdup(stream, mt);
437
0
            av_free(mt);
438
0
        }
439
19.9k
    }
440
441
19.9k
    stream->priv = avio;
442
19.9k
    stream->seekable = avio->seekable & AVIO_SEEKABLE_NORMAL;
443
19.9k
    stream->seek = stream->seekable ? seek : NULL;
444
19.9k
    stream->fill_buffer = fill_buffer;
445
19.9k
    stream->write_buffer = write_buffer;
446
19.9k
    stream->get_size = get_size;
447
19.9k
    stream->control = control;
448
19.9k
    stream->close = close_f;
449
    // enable cache (should be avoided for files, but no way to detect this)
450
19.9k
    stream->streaming = true;
451
19.9k
    if (stream->info->stream_origin == STREAM_ORIGIN_NET)
452
13.6k
        stream->is_network = true;
453
19.9k
    res = STREAM_OK;
454
455
21.4k
out:
456
21.4k
    av_dict_free(&dict);
457
21.4k
    talloc_free(temp);
458
21.4k
    return res;
459
19.9k
}
460
461
static struct mp_tags *read_icy(stream_t *s)
462
1.82M
{
463
1.82M
    AVIOContext *avio = s->priv;
464
465
1.82M
    if (!avio->av_class)
466
0
        return NULL;
467
468
1.82M
    uint8_t *icy_header = NULL;
469
1.82M
    if (av_opt_get(avio, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN,
470
1.82M
                   &icy_header) < 0)
471
1.82M
        icy_header = NULL;
472
473
1.82M
    uint8_t *icy_packet;
474
1.82M
    if (av_opt_get(avio, "icy_metadata_packet", AV_OPT_SEARCH_CHILDREN,
475
1.82M
                   &icy_packet) < 0)
476
1.82M
        icy_packet = NULL;
477
478
    // Send a metadata update only 1. on start, and 2. on a new metadata packet.
479
    // To detect new packages, set the icy_metadata_packet to "-" once we've
480
    // read it (a bit hacky, but works).
481
482
1.82M
    struct mp_tags *res = NULL;
483
1.82M
    if ((!icy_header || !icy_header[0]) && (!icy_packet || !icy_packet[0]))
484
1.82M
        goto done;
485
486
0
    bstr packet = bstr0(icy_packet);
487
0
    if (bstr_equals0(packet, "-"))
488
0
        goto done;
489
490
0
    res = talloc_zero(NULL, struct mp_tags);
491
492
0
    bstr header = bstr0(icy_header);
493
0
    while (header.len) {
494
0
        bstr line = bstr_strip_linebreaks(bstr_getline(header, &header));
495
0
        bstr name, val;
496
0
        if (bstr_split_tok(line, ": ", &name, &val))
497
0
            mp_tags_set_bstr(res, name, val);
498
0
    }
499
500
0
    bstr head = bstr0("StreamTitle='");
501
0
    int i = bstr_find(packet, head);
502
0
    if (i >= 0) {
503
0
        packet = bstr_cut(packet, i + head.len);
504
0
        int end = bstr_find(packet, bstr0("\';"));
505
0
        packet = bstr_splice(packet, 0, end);
506
507
0
        bool allocated = false;
508
0
        struct demux_opts *opts = mp_get_config_group(NULL, s->global, &demux_conf);
509
0
        const char *charset = mp_charset_guess(s, s->log, packet, opts->meta_cp, 0);
510
0
        if (charset && !mp_charset_is_utf8(charset)) {
511
0
            bstr conv = mp_iconv_to_utf8(s->log, packet, charset, 0);
512
0
            if (conv.start && conv.start != packet.start) {
513
0
                allocated = true;
514
0
                packet = conv;
515
0
            }
516
0
        }
517
0
        mp_tags_set_bstr(res, bstr0("icy-title"), packet);
518
0
        talloc_free(opts);
519
0
        if (allocated)
520
0
            talloc_free(packet.start);
521
0
    }
522
523
0
    av_opt_set(avio, "icy_metadata_packet", "-", AV_OPT_SEARCH_CHILDREN);
524
525
1.82M
done:
526
1.82M
    av_free(icy_header);
527
1.82M
    av_free(icy_packet);
528
1.82M
    return res;
529
0
}
530
531
const stream_info_t stream_info_ffmpeg = {
532
    .name = "ffmpeg",
533
    .open = open_f,
534
    .get_protocols = get_safe_protocols,
535
    .can_write = true,
536
    .stream_origin = STREAM_ORIGIN_NET,
537
};
538
539
// Unlike above, this is not marked as safe, and can contain protocols which
540
// may do insecure things. (Such as "ffmpeg", which can access the "lavfi"
541
// pseudo-demuxer, which in turn gives access to filters that can access the
542
// local filesystem.)
543
const stream_info_t stream_info_ffmpeg_unsafe = {
544
    .name = "ffmpeg",
545
    .open = open_f,
546
    .get_protocols = get_unsafe_protocols,
547
    .stream_origin = STREAM_ORIGIN_UNSAFE,
548
    .can_write = true,
549
};