/src/mpv/demux/demux_mf.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 <math.h> |
19 | | #include <stdbool.h> |
20 | | #include <stdio.h> |
21 | | #include <stdlib.h> |
22 | | #include <sys/types.h> |
23 | | #include <sys/stat.h> |
24 | | |
25 | | #include "osdep/io.h" |
26 | | |
27 | | #include "mpv_talloc.h" |
28 | | #include "common/msg.h" |
29 | | #include "options/options.h" |
30 | | #include "options/m_config.h" |
31 | | #include "options/path.h" |
32 | | #include "misc/ctype.h" |
33 | | |
34 | | #include "stream/stream.h" |
35 | | #include "demux.h" |
36 | | #include "stheader.h" |
37 | | #include "codec_tags.h" |
38 | | |
39 | 5.77k | #define MF_MAX_FILE_SIZE (1024 * 1024 * 256) |
40 | | |
41 | | typedef struct mf { |
42 | | struct mp_log *log; |
43 | | struct sh_stream *sh; |
44 | | int curr_frame; |
45 | | int nr_of_files; |
46 | | char **names; |
47 | | // optional |
48 | | struct stream **streams; |
49 | | } mf_t; |
50 | | |
51 | | |
52 | | static void mf_add(mf_t *mf, const char *fname) |
53 | 69.0k | { |
54 | 69.0k | char *entry = talloc_strdup(mf, fname); |
55 | 69.0k | MP_TARRAY_APPEND(mf, mf->names, mf->nr_of_files, entry); |
56 | 69.0k | } |
57 | | |
58 | | static mf_t *open_mf_pattern(void *talloc_ctx, struct demuxer *d, char *filename) |
59 | 468 | { |
60 | 468 | struct mp_log *log = d->log; |
61 | 468 | int error_count = 0; |
62 | 468 | int count = 0; |
63 | | |
64 | 468 | mf_t *mf = talloc_zero(talloc_ctx, mf_t); |
65 | 468 | mf->log = log; |
66 | | |
67 | 468 | if (filename[0] == '@') { |
68 | 300 | struct stream *s = stream_create(filename + 1, |
69 | 300 | d->stream_origin | STREAM_READ, d->cancel, d->global); |
70 | 300 | if (s && !s->is_directory) { |
71 | 27.1k | while (1) { |
72 | 27.1k | char buf[512]; |
73 | 27.1k | int len = stream_read_peek(s, buf, sizeof(buf)); |
74 | 27.1k | if (!len) |
75 | 176 | break; |
76 | 26.9k | bstr data = (bstr){buf, len}; |
77 | 26.9k | int pos = bstrchr(data, '\n'); |
78 | 26.9k | data = bstr_splice(data, 0, pos < 0 ? data.len : pos + 1); |
79 | 26.9k | bstr fname = bstr_strip(data); |
80 | 26.9k | if (fname.len) { |
81 | 18.7k | if (bstrchr(fname, '\0') >= 0) { |
82 | 0 | mp_err(log, "invalid filename\n"); |
83 | 0 | break; |
84 | 0 | } |
85 | 18.7k | char *entry = bstrto0(mf, fname); |
86 | 18.7k | if (!mp_path_exists(entry) && !mp_is_url(fname)) { |
87 | 10.5k | mp_verbose(log, "file not found: '%s'\n", entry); |
88 | 10.5k | } else { |
89 | 8.22k | MP_TARRAY_APPEND(mf, mf->names, mf->nr_of_files, entry); |
90 | 8.22k | } |
91 | 18.7k | } |
92 | 26.9k | stream_seek_skip(s, stream_tell(s) + data.len); |
93 | 26.9k | } |
94 | 176 | free_stream(s); |
95 | | |
96 | 176 | goto exit_mf; |
97 | 176 | } |
98 | 124 | free_stream(s); |
99 | 124 | mp_info(log, "%s is not indirect filelist\n", filename + 1); |
100 | 124 | } |
101 | | |
102 | 292 | if (strchr(filename, ',')) { |
103 | 132 | bstr bfilename = bstr0(filename); |
104 | 132 | mp_info(log, "filelist: %.*s\n", BSTR_P(bfilename)); |
105 | | |
106 | 184k | while (bfilename.len) { |
107 | 183k | bstr bfname; |
108 | 183k | bstr_split_tok(bfilename, ",", &bfname, &bfilename); |
109 | 183k | char *fname2 = bstrdup0(mf, bfname); |
110 | | |
111 | 183k | if (!mp_path_exists(fname2) && !mp_is_url(bfname)) |
112 | 174k | mp_verbose(log, "file not found: '%s'\n", fname2); |
113 | 9.70k | else { |
114 | 9.70k | mf_add(mf, fname2); |
115 | 9.70k | } |
116 | 183k | talloc_free(fname2); |
117 | 183k | } |
118 | | |
119 | 132 | goto exit_mf; |
120 | 132 | } |
121 | | |
122 | 160 | bstr bfilename = bstr0(filename); |
123 | 160 | if (mp_is_url(bfilename)) |
124 | 4 | goto exit_mf; |
125 | | |
126 | 156 | size_t fname_avail = bfilename.len + 32; |
127 | 156 | char *fname = talloc_size(mf, fname_avail); |
128 | | |
129 | | #if HAVE_GLOB && !defined(FUZZING_BUILD_MODE_UNSAFE_FOR_PRODUCTION) |
130 | | if (!strchr(filename, '%')) { |
131 | | // append * if none present |
132 | | snprintf(fname, fname_avail, "%s%c", filename, |
133 | | strchr(filename, '*') ? 0 : '*'); |
134 | | mp_info(log, "search expr: %s\n", fname); |
135 | | |
136 | | glob_t gg; |
137 | | if (glob(fname, 0, NULL, &gg)) { |
138 | | talloc_free(mf); |
139 | | return NULL; |
140 | | } |
141 | | |
142 | | for (int i = 0; i < gg.gl_pathc; i++) { |
143 | | if (mp_path_isdir(gg.gl_pathv[i])) |
144 | | continue; |
145 | | mf_add(mf, gg.gl_pathv[i]); |
146 | | } |
147 | | globfree(&gg); |
148 | | goto exit_mf; |
149 | | } |
150 | | #endif |
151 | | |
152 | | // We're using arbitrary user input as printf format with 1 int argument. |
153 | | // Any format which uses exactly 1 int argument would be valid, but for |
154 | | // simplicity we reject all conversion specifiers except %% and simple |
155 | | // integer specifier: %[.][NUM]d where NUM is 1-3 digits (%.d is valid) |
156 | 156 | const char *f = filename; |
157 | 156 | int MAXDIGS = 3, nspec = 0, c; |
158 | 156 | bool bad_spec = false; |
159 | | |
160 | 20.9k | while (nspec < 2 && (c = *f++)) { |
161 | 20.8k | if (c != '%') |
162 | 20.4k | continue; |
163 | | |
164 | 386 | if (*f == '%') { |
165 | | // '%%', which ends up as an explicit % in the output. |
166 | | // Skipping forwards as it doesn't require further attention. |
167 | 351 | f++; |
168 | 351 | continue; |
169 | 351 | } |
170 | | |
171 | | // Now c == '%' and *f != '%', thus we have entered territory of format |
172 | | // specifiers which we are interested in. |
173 | 35 | nspec++; |
174 | | |
175 | 35 | if (*f == '.') |
176 | 0 | f++; |
177 | | |
178 | 71 | for (int ndig = 0; mp_isdigit(*f) && ndig < MAXDIGS; ndig++, f++) |
179 | 36 | /* no-op */; |
180 | | |
181 | 35 | if (*f != 'd') { |
182 | 21 | bad_spec = true; // not int, or beyond our validation capacity |
183 | 21 | break; |
184 | 21 | } |
185 | | |
186 | | // *f is 'd' |
187 | 14 | f++; |
188 | 14 | } |
189 | | |
190 | | // nspec==0 (zero specifiers) is rejected because fname wouldn't advance. |
191 | 156 | if (bad_spec || nspec != 1) { |
192 | 147 | mp_err(log, |
193 | 147 | "unsupported expr format: '%s' - exactly one format specifier of the form %%[.][NUM]d is expected\n", |
194 | 147 | filename); |
195 | 147 | goto exit_mf; |
196 | 147 | } |
197 | | |
198 | 9 | mp_info(log, "search expr: %s\n", filename); |
199 | | |
200 | 34 | while (error_count < 5) { |
201 | 29 | if (snprintf(fname, fname_avail, filename, count++) >= fname_avail) { |
202 | 4 | mp_err(log, "format result too long: '%s'\n", filename); |
203 | 4 | goto exit_mf; |
204 | 4 | } |
205 | 25 | if (!mp_path_exists(fname)) { |
206 | 25 | error_count++; |
207 | 25 | mp_verbose(log, "file not found: '%s'\n", fname); |
208 | 25 | } else { |
209 | 0 | mf_add(mf, fname); |
210 | 0 | } |
211 | 25 | } |
212 | | |
213 | 468 | exit_mf: |
214 | 468 | mp_info(log, "number of files: %d\n", mf->nr_of_files); |
215 | 468 | return mf; |
216 | 9 | } |
217 | | |
218 | | static mf_t *open_mf_single(void *talloc_ctx, struct mp_log *log, char *filename) |
219 | 59.3k | { |
220 | 59.3k | mf_t *mf = talloc_zero(talloc_ctx, mf_t); |
221 | 59.3k | mf->log = log; |
222 | 59.3k | mf_add(mf, filename); |
223 | 59.3k | return mf; |
224 | 59.3k | } |
225 | | |
226 | | static void demux_seek_mf(demuxer_t *demuxer, double seek_pts, int flags) |
227 | 303 | { |
228 | 303 | mf_t *mf = demuxer->priv; |
229 | 303 | double newpos = seek_pts * mf->sh->codec->fps; |
230 | 303 | if (flags & SEEK_FACTOR) |
231 | 0 | newpos = seek_pts * (mf->nr_of_files - 1); |
232 | 303 | if (flags & SEEK_FORWARD) { |
233 | 0 | newpos = ceil(newpos); |
234 | 303 | } else { |
235 | 303 | newpos = MPMIN(floor(newpos), mf->nr_of_files - 1); |
236 | 303 | } |
237 | 303 | mf->curr_frame = MPCLAMP((int)newpos, 0, mf->nr_of_files); |
238 | 303 | } |
239 | | |
240 | | static bool demux_mf_read_packet(struct demuxer *demuxer, |
241 | | struct demux_packet **pkt) |
242 | 20.3k | { |
243 | 20.3k | mf_t *mf = demuxer->priv; |
244 | 20.3k | if (mf->curr_frame >= mf->nr_of_files) |
245 | 2.14k | return false; |
246 | 20.3k | bool ok = false; |
247 | | |
248 | 18.1k | struct stream *entry_stream = NULL; |
249 | 18.1k | if (mf->streams) |
250 | 1.97k | entry_stream = mf->streams[mf->curr_frame]; |
251 | 18.1k | struct stream *stream = entry_stream; |
252 | 18.1k | if (!stream) { |
253 | 16.2k | char *filename = mf->names[mf->curr_frame]; |
254 | 16.2k | if (filename) { |
255 | 16.2k | stream = stream_create(filename, demuxer->stream_origin | STREAM_READ, |
256 | 16.2k | demuxer->cancel, demuxer->global); |
257 | 16.2k | } |
258 | 16.2k | } |
259 | | |
260 | 18.1k | if (stream) { |
261 | 5.77k | stream_seek(stream, 0); |
262 | 5.77k | bstr data = stream_read_complete(stream, NULL, MF_MAX_FILE_SIZE); |
263 | 5.77k | if (data.len) { |
264 | 3.54k | demux_packet_t *dp = new_demux_packet(demuxer->packet_pool, data.len); |
265 | 3.54k | if (dp) { |
266 | 3.54k | memcpy(dp->buffer, data.start, data.len); |
267 | 3.54k | dp->pts = mf->curr_frame / mf->sh->codec->fps; |
268 | 3.54k | dp->keyframe = true; |
269 | 3.54k | dp->stream = mf->sh->index; |
270 | 3.54k | *pkt = dp; |
271 | 3.54k | ok = true; |
272 | 3.54k | } |
273 | 3.54k | } |
274 | 5.77k | talloc_free(data.start); |
275 | 5.77k | } |
276 | | |
277 | 18.1k | if (stream && stream != entry_stream) |
278 | 3.80k | free_stream(stream); |
279 | | |
280 | 18.1k | mf->curr_frame++; |
281 | | |
282 | 18.1k | if (!ok) |
283 | 18.1k | MP_ERR(demuxer, "error reading image file\n"); |
284 | | |
285 | 18.1k | return true; |
286 | 20.3k | } |
287 | | |
288 | | // map file extension/type to a codec name |
289 | | |
290 | | static const struct { |
291 | | const char *type; |
292 | | const char *codec; |
293 | | } type2format[] = { |
294 | | { "bmp", "bmp" }, |
295 | | { "dpx", "dpx" }, |
296 | | { "j2c", "jpeg2000" }, |
297 | | { "j2k", "jpeg2000" }, |
298 | | { "jp2", "jpeg2000" }, |
299 | | { "jpc", "jpeg2000" }, |
300 | | { "jpeg", "mjpeg" }, |
301 | | { "jpg", "mjpeg" }, |
302 | | { "jps", "mjpeg" }, |
303 | | { "jls", "ljpeg" }, |
304 | | { "thm", "mjpeg" }, |
305 | | { "db", "mjpeg" }, |
306 | | { "pcd", "photocd" }, |
307 | | { "pfm", "pfm" }, |
308 | | { "phm", "phm" }, |
309 | | { "hdr", "hdr" }, |
310 | | { "pcx", "pcx" }, |
311 | | { "png", "png" }, |
312 | | { "pns", "png" }, |
313 | | { "ptx", "ptx" }, |
314 | | { "tga", "targa" }, |
315 | | { "tif", "tiff" }, |
316 | | { "tiff", "tiff" }, |
317 | | { "sgi", "sgi" }, |
318 | | { "sun", "sunrast" }, |
319 | | { "ras", "sunrast" }, |
320 | | { "rs", "sunrast" }, |
321 | | { "ra", "sunrast" }, |
322 | | { "im1", "sunrast" }, |
323 | | { "im8", "sunrast" }, |
324 | | { "im24", "sunrast" }, |
325 | | { "im32", "sunrast" }, |
326 | | { "sunras", "sunrast" }, |
327 | | { "xbm", "xbm" }, |
328 | | { "pam", "pam" }, |
329 | | { "pbm", "pbm" }, |
330 | | { "pgm", "pgm" }, |
331 | | { "pgmyuv", "pgmyuv" }, |
332 | | { "ppm", "ppm" }, |
333 | | { "pnm", "ppm" }, |
334 | | { "gif", "gif" }, // usually handled by demux_lavf |
335 | | { "pix", "brender_pix" }, |
336 | | { "exr", "exr" }, |
337 | | { "pic", "pictor" }, |
338 | | { "qoi", "qoi" }, |
339 | | { "xface", "xface" }, |
340 | | { "xwd", "xwd" }, |
341 | | { "svg", "svg" }, |
342 | | { "webp", "webp" }, |
343 | | { "jxl", "jpegxl" }, |
344 | | {0} |
345 | | }; |
346 | | |
347 | | static const char *probe_format(mf_t *mf, char *type, enum demux_check check) |
348 | 59.5k | { |
349 | 59.5k | if (check > DEMUX_CHECK_REQUEST) |
350 | 31.2k | return NULL; |
351 | 28.3k | char *org_type = type; |
352 | 28.3k | if (!type || !type[0]) { |
353 | 28.3k | char *p = strrchr(mf->names[0], '.'); |
354 | 28.3k | if (p) |
355 | 16.6k | type = p + 1; |
356 | 28.3k | } |
357 | 1.38M | for (int i = 0; type2format[i].type; i++) { |
358 | 1.35M | if (type && strcasecmp(type, type2format[i].type) == 0) |
359 | 2.19k | return type2format[i].codec; |
360 | 1.35M | } |
361 | 26.1k | if (check == DEMUX_CHECK_REQUEST) { |
362 | 93 | if (!org_type) { |
363 | 93 | MP_ERR(mf, "file type was not set! (try --mf-type=ext)\n"); |
364 | 93 | } else { |
365 | 0 | MP_ERR(mf, "--mf-type set to an unknown codec!\n"); |
366 | 0 | } |
367 | 93 | } |
368 | 26.1k | return NULL; |
369 | 28.3k | } |
370 | | |
371 | | static int demux_open_mf(demuxer_t *demuxer, enum demux_check check) |
372 | 59.7k | { |
373 | 59.7k | mf_t *mf; |
374 | | |
375 | 59.7k | if (strncmp(demuxer->stream->url, "mf://", 5) == 0 && |
376 | 468 | demuxer->stream->info && strcmp(demuxer->stream->info->name, "mf") == 0) |
377 | 468 | { |
378 | 468 | mf = open_mf_pattern(demuxer, demuxer, demuxer->stream->url + 5); |
379 | 59.3k | } else { |
380 | 59.3k | mf = open_mf_single(demuxer, demuxer->log, demuxer->stream->url); |
381 | 59.3k | int bog = 0; |
382 | 59.3k | MP_TARRAY_APPEND(mf, mf->streams, bog, demuxer->stream); |
383 | 59.3k | } |
384 | | |
385 | 59.7k | if (!mf || mf->nr_of_files < 1) |
386 | 199 | goto error; |
387 | | |
388 | 59.5k | const char *codec = mp_map_mimetype_to_video_codec(demuxer->stream->mime_type); |
389 | 59.5k | if (!codec || (demuxer->opts->mf_type && demuxer->opts->mf_type[0])) |
390 | 59.5k | codec = probe_format(mf, demuxer->opts->mf_type, check); |
391 | 59.5k | if (!codec) |
392 | 57.3k | goto error; |
393 | | |
394 | 2.19k | mf->curr_frame = 0; |
395 | | |
396 | | // create a new video stream header |
397 | 2.19k | struct sh_stream *sh = demux_alloc_sh_stream(STREAM_VIDEO); |
398 | 2.19k | if (mf->nr_of_files == 1) { |
399 | 2.02k | MP_VERBOSE(demuxer, "Assuming this is an image format.\n"); |
400 | 2.02k | sh->image = true; |
401 | 2.02k | } |
402 | | |
403 | 2.19k | struct mp_codec_params *c = sh->codec; |
404 | 2.19k | c->codec = codec; |
405 | 2.19k | c->disp_w = 0; |
406 | 2.19k | c->disp_h = 0; |
407 | 2.19k | c->fps = demuxer->opts->mf_fps; |
408 | 2.19k | c->reliable_fps = true; |
409 | | |
410 | 2.19k | demux_add_sh_stream(demuxer, sh); |
411 | | |
412 | 2.19k | mf->sh = sh; |
413 | 2.19k | demuxer->priv = (void *)mf; |
414 | 2.19k | demuxer->seekable = true; |
415 | 2.19k | demuxer->duration = mf->nr_of_files / mf->sh->codec->fps; |
416 | | |
417 | 2.19k | return 0; |
418 | | |
419 | 57.5k | error: |
420 | 57.5k | return -1; |
421 | 59.5k | } |
422 | | |
423 | | static void demux_close_mf(demuxer_t *demuxer) |
424 | 59.7k | { |
425 | 59.7k | } |
426 | | |
427 | | const demuxer_desc_t demuxer_desc_mf = { |
428 | | .name = "mf", |
429 | | .desc = "image files (mf)", |
430 | | .read_packet = demux_mf_read_packet, |
431 | | .open = demux_open_mf, |
432 | | .close = demux_close_mf, |
433 | | .seek = demux_seek_mf, |
434 | | }; |