summaryrefslogtreecommitdiffstats
path: root/demux/demux_playlist.c
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
commit51de1d8436100f725f3576aefa24a2bd2057bc28 (patch)
treec6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /demux/demux_playlist.c
parentInitial commit. (diff)
downloadmpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz
mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'demux/demux_playlist.c')
-rw-r--r--demux/demux_playlist.c584
1 files changed, 584 insertions, 0 deletions
diff --git a/demux/demux_playlist.c b/demux/demux_playlist.c
new file mode 100644
index 0000000..63355be
--- /dev/null
+++ b/demux/demux_playlist.c
@@ -0,0 +1,584 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <dirent.h>
+
+#include <libavutil/common.h>
+
+#include "common/common.h"
+#include "options/options.h"
+#include "options/m_config.h"
+#include "common/msg.h"
+#include "common/playlist.h"
+#include "misc/charset_conv.h"
+#include "misc/thread_tools.h"
+#include "options/path.h"
+#include "stream/stream.h"
+#include "osdep/io.h"
+#include "misc/natural_sort.h"
+#include "demux.h"
+
+#define PROBE_SIZE (8 * 1024)
+
+enum dir_mode {
+ DIR_AUTO,
+ DIR_LAZY,
+ DIR_RECURSIVE,
+ DIR_IGNORE,
+};
+
+#define OPT_BASE_STRUCT struct demux_playlist_opts
+struct demux_playlist_opts {
+ int dir_mode;
+};
+
+struct m_sub_options demux_playlist_conf = {
+ .opts = (const struct m_option[]) {
+ {"directory-mode", OPT_CHOICE(dir_mode,
+ {"auto", DIR_AUTO},
+ {"lazy", DIR_LAZY},
+ {"recursive", DIR_RECURSIVE},
+ {"ignore", DIR_IGNORE})},
+ {0}
+ },
+ .size = sizeof(struct demux_playlist_opts),
+ .defaults = &(const struct demux_playlist_opts){
+ .dir_mode = DIR_AUTO,
+ },
+};
+
+static bool check_mimetype(struct stream *s, const char *const *list)
+{
+ if (s->mime_type) {
+ for (int n = 0; list && list[n]; n++) {
+ if (strcasecmp(s->mime_type, list[n]) == 0)
+ return true;
+ }
+ }
+ return false;
+}
+
+struct pl_parser {
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct stream *s;
+ char buffer[2 * 1024 * 1024];
+ int utf16;
+ struct playlist *pl;
+ bool error;
+ bool probing;
+ bool force;
+ bool add_base;
+ bool line_allocated;
+ enum demux_check check_level;
+ struct stream *real_stream;
+ char *format;
+ char *codepage;
+ struct demux_playlist_opts *opts;
+};
+
+
+static uint16_t stream_read_word_endian(stream_t *s, bool big_endian)
+{
+ unsigned int y = stream_read_char(s);
+ y = (y << 8) | stream_read_char(s);
+ if (!big_endian)
+ y = ((y >> 8) & 0xFF) | (y << 8);
+ return y;
+}
+
+// Read characters until the next '\n' (including), or until the buffer in s is
+// exhausted.
+static int read_characters(stream_t *s, uint8_t *dst, int dstsize, int utf16)
+{
+ if (utf16 == 1 || utf16 == 2) {
+ uint8_t *cur = dst;
+ while (1) {
+ if ((cur - dst) + 8 >= dstsize) // PUT_UTF8 writes max. 8 bytes
+ return -1; // line too long
+ uint32_t c;
+ uint8_t tmp;
+ GET_UTF16(c, stream_read_word_endian(s, utf16 == 2), return -1;)
+ if (s->eof)
+ break; // legitimate EOF; ignore the case of partial reads
+ PUT_UTF8(c, tmp, *cur++ = tmp;)
+ if (c == '\n')
+ break;
+ }
+ return cur - dst;
+ } else {
+ uint8_t buf[1024];
+ int buf_len = stream_read_peek(s, buf, sizeof(buf));
+ uint8_t *end = memchr(buf, '\n', buf_len);
+ int len = end ? end - buf + 1 : buf_len;
+ if (len > dstsize)
+ return -1; // line too long
+ memcpy(dst, buf, len);
+ stream_seek_skip(s, stream_tell(s) + len);
+ return len;
+ }
+}
+
+// On error, or if the line is larger than max-1, return NULL and unset s->eof.
+// On EOF, return NULL, and s->eof will be set.
+// Otherwise, return the line (including \n or \r\n at the end of the line).
+// If the return value is non-NULL, it's always the same as mem.
+// utf16: 0: UTF8 or 8 bit legacy, 1: UTF16-LE, 2: UTF16-BE
+static char *read_line(stream_t *s, char *mem, int max, int utf16)
+{
+ if (max < 1)
+ return NULL;
+ int read = 0;
+ while (1) {
+ // Reserve 1 byte of ptr for terminating \0.
+ int l = read_characters(s, &mem[read], max - read - 1, utf16);
+ if (l < 0 || memchr(&mem[read], '\0', l)) {
+ MP_WARN(s, "error reading line\n");
+ return NULL;
+ }
+ read += l;
+ if (l == 0 || (read > 0 && mem[read - 1] == '\n'))
+ break;
+ }
+ mem[read] = '\0';
+ if (!stream_read_peek(s, &(char){0}, 1) && read == 0) // legitimate EOF
+ return NULL;
+ return mem;
+}
+
+static char *pl_get_line0(struct pl_parser *p)
+{
+ char *res = read_line(p->s, p->buffer, sizeof(p->buffer), p->utf16);
+ if (res) {
+ int len = strlen(res);
+ if (len > 0 && res[len - 1] == '\n')
+ res[len - 1] = '\0';
+ } else {
+ p->error |= !p->s->eof;
+ }
+ return res;
+}
+
+static bstr pl_get_line(struct pl_parser *p)
+{
+ bstr line = bstr_strip(bstr0(pl_get_line0(p)));
+ const char *charset = mp_charset_guess(p, p->log, line, p->codepage, 0);
+ if (charset && !mp_charset_is_utf8(charset)) {
+ bstr utf8 = mp_iconv_to_utf8(p->log, line, charset, 0);
+ if (utf8.start && utf8.start != line.start) {
+ line = utf8;
+ p->line_allocated = true;
+ }
+ }
+ return line;
+}
+
+// Helper in case mp_iconv_to_utf8 allocates memory
+static void pl_free_line(struct pl_parser *p, bstr line)
+{
+ if (p->line_allocated) {
+ talloc_free(line.start);
+ p->line_allocated = false;
+ }
+}
+
+static void pl_add(struct pl_parser *p, bstr entry)
+{
+ char *s = bstrto0(NULL, entry);
+ playlist_add_file(p->pl, s);
+ talloc_free(s);
+}
+
+static bool pl_eof(struct pl_parser *p)
+{
+ return p->error || p->s->eof;
+}
+
+static bool maybe_text(bstr d)
+{
+ for (int n = 0; n < d.len; n++) {
+ unsigned char c = d.start[n];
+ if (c < 32 && c != '\n' && c != '\r' && c != '\t')
+ return false;
+ }
+ return true;
+}
+
+static int parse_m3u(struct pl_parser *p)
+{
+ bstr line = pl_get_line(p);
+ if (p->probing && !bstr_equals0(line, "#EXTM3U")) {
+ // Last resort: if the file extension is m3u, it might be headerless.
+ if (p->check_level == DEMUX_CHECK_UNSAFE) {
+ char *ext = mp_splitext(p->real_stream->url, NULL);
+ char probe[PROBE_SIZE];
+ int len = stream_read_peek(p->real_stream, probe, sizeof(probe));
+ bstr data = {probe, len};
+ if (ext && data.len >= 2 && maybe_text(data)) {
+ const char *exts[] = {"m3u", "m3u8", NULL};
+ for (int n = 0; exts[n]; n++) {
+ if (strcasecmp(ext, exts[n]) == 0)
+ goto ok;
+ }
+ }
+ }
+ pl_free_line(p, line);
+ return -1;
+ }
+
+ok:
+ if (p->probing) {
+ pl_free_line(p, line);
+ return 0;
+ }
+
+ char *title = NULL;
+ while (line.len || !pl_eof(p)) {
+ bstr line_dup = line;
+ if (bstr_eatstart0(&line_dup, "#EXTINF:")) {
+ bstr duration, btitle;
+ if (bstr_split_tok(line_dup, ",", &duration, &btitle) && btitle.len) {
+ talloc_free(title);
+ title = bstrto0(NULL, btitle);
+ }
+ } else if (bstr_startswith0(line_dup, "#EXT-X-")) {
+ p->format = "hls";
+ } else if (line_dup.len > 0 && !bstr_startswith0(line_dup, "#")) {
+ char *fn = bstrto0(NULL, line_dup);
+ struct playlist_entry *e = playlist_entry_new(fn);
+ talloc_free(fn);
+ e->title = talloc_steal(e, title);
+ title = NULL;
+ playlist_add(p->pl, e);
+ }
+ pl_free_line(p, line);
+ line = pl_get_line(p);
+ }
+ pl_free_line(p, line);
+ talloc_free(title);
+ return 0;
+}
+
+static int parse_ref_init(struct pl_parser *p)
+{
+ bstr line = pl_get_line(p);
+ if (!bstr_equals0(line, "[Reference]")) {
+ pl_free_line(p, line);
+ return -1;
+ }
+ pl_free_line(p, line);
+
+ // ASF http streaming redirection - this is needed because ffmpeg http://
+ // and mmsh:// can not automatically switch automatically between each
+ // others. Both protocols use http - MMSH requires special http headers
+ // to "activate" it, and will in other cases return this playlist.
+ static const char *const mmsh_types[] = {"audio/x-ms-wax",
+ "audio/x-ms-wma", "video/x-ms-asf", "video/x-ms-afs", "video/x-ms-wmv",
+ "video/x-ms-wma", "application/x-mms-framed",
+ "application/vnd.ms.wms-hdr.asfv1", NULL};
+ bstr burl = bstr0(p->s->url);
+ if (bstr_eatstart0(&burl, "http://") && check_mimetype(p->s, mmsh_types)) {
+ MP_INFO(p, "Redirecting to mmsh://\n");
+ playlist_add_file(p->pl, talloc_asprintf(p, "mmsh://%.*s", BSTR_P(burl)));
+ return 0;
+ }
+
+ while (!pl_eof(p)) {
+ line = pl_get_line(p);
+ bstr value;
+ if (bstr_case_startswith(line, bstr0("Ref"))) {
+ bstr_split_tok(line, "=", &(bstr){0}, &value);
+ if (value.len)
+ pl_add(p, value);
+ }
+ pl_free_line(p, line);
+ }
+ return 0;
+}
+
+static int parse_ini_thing(struct pl_parser *p, const char *header,
+ const char *entry)
+{
+ bstr line = {0};
+ while (!line.len && !pl_eof(p))
+ line = pl_get_line(p);
+ if (bstrcasecmp0(line, header) != 0) {
+ pl_free_line(p, line);
+ return -1;
+ }
+ if (p->probing) {
+ pl_free_line(p, line);
+ return 0;
+ }
+ pl_free_line(p, line);
+ while (!pl_eof(p)) {
+ line = pl_get_line(p);
+ bstr key, value;
+ if (bstr_split_tok(line, "=", &key, &value) &&
+ bstr_case_startswith(key, bstr0(entry)))
+ {
+ value = bstr_strip(value);
+ if (bstr_startswith0(value, "\"") && bstr_endswith0(value, "\""))
+ value = bstr_splice(value, 1, -1);
+ pl_add(p, value);
+ }
+ pl_free_line(p, line);
+ }
+ return 0;
+}
+
+static int parse_pls(struct pl_parser *p)
+{
+ return parse_ini_thing(p, "[playlist]", "File");
+}
+
+static int parse_url(struct pl_parser *p)
+{
+ return parse_ini_thing(p, "[InternetShortcut]", "URL");
+}
+
+static int parse_txt(struct pl_parser *p)
+{
+ if (!p->force)
+ return -1;
+ if (p->probing)
+ return 0;
+ MP_WARN(p, "Reading plaintext playlist.\n");
+ while (!pl_eof(p)) {
+ bstr line = pl_get_line(p);
+ if (line.len == 0)
+ continue;
+ pl_add(p, line);
+ pl_free_line(p, line);
+ }
+ return 0;
+}
+
+#define MAX_DIR_STACK 20
+
+static bool same_st(struct stat *st1, struct stat *st2)
+{
+ return st1->st_dev == st2->st_dev && st1->st_ino == st2->st_ino;
+}
+
+struct pl_dir_entry {
+ char *path;
+ char *name;
+ struct stat st;
+ bool is_dir;
+};
+
+static int cmp_dir_entry(const void *a, const void *b)
+{
+ struct pl_dir_entry *a_entry = (struct pl_dir_entry*) a;
+ struct pl_dir_entry *b_entry = (struct pl_dir_entry*) b;
+ if (a_entry->is_dir == b_entry->is_dir) {
+ return mp_natural_sort_cmp(a_entry->name, b_entry->name);
+ } else {
+ return a_entry->is_dir ? 1 : -1;
+ }
+}
+
+// Return true if this was a readable directory.
+static bool scan_dir(struct pl_parser *p, char *path,
+ struct stat *dir_stack, int num_dir_stack)
+{
+ if (strlen(path) >= 8192 || num_dir_stack == MAX_DIR_STACK)
+ return false; // things like mount bind loops
+
+ DIR *dp = opendir(path);
+ if (!dp) {
+ MP_ERR(p, "Could not read directory.\n");
+ return false;
+ }
+
+ struct pl_dir_entry *dir_entries = NULL;
+ int num_dir_entries = 0;
+ int path_len = strlen(path);
+ int dir_mode = p->opts->dir_mode;
+
+ struct dirent *ep;
+ while ((ep = readdir(dp))) {
+ if (ep->d_name[0] == '.')
+ continue;
+
+ if (mp_cancel_test(p->s->cancel))
+ break;
+
+ char *file = mp_path_join(p, path, ep->d_name);
+
+ struct stat st;
+ if (stat(file, &st) == 0 && S_ISDIR(st.st_mode)) {
+ if (dir_mode != DIR_IGNORE) {
+ for (int n = 0; n < num_dir_stack; n++) {
+ if (same_st(&dir_stack[n], &st)) {
+ MP_VERBOSE(p, "Skip recursive entry: %s\n", file);
+ goto skip;
+ }
+ }
+
+ struct pl_dir_entry d = {file, &file[path_len], st, true};
+ MP_TARRAY_APPEND(p, dir_entries, num_dir_entries, d);
+ }
+ } else {
+ struct pl_dir_entry f = {file, &file[path_len], .is_dir = false};
+ MP_TARRAY_APPEND(p, dir_entries, num_dir_entries, f);
+ }
+
+ skip: ;
+ }
+ closedir(dp);
+
+ if (dir_entries)
+ qsort(dir_entries, num_dir_entries, sizeof(dir_entries[0]), cmp_dir_entry);
+
+ for (int n = 0; n < num_dir_entries; n++) {
+ if (dir_mode == DIR_RECURSIVE && dir_entries[n].is_dir) {
+ dir_stack[num_dir_stack] = dir_entries[n].st;
+ char *file = dir_entries[n].path;
+ scan_dir(p, file, dir_stack, num_dir_stack + 1);
+ }
+ else {
+ playlist_add_file(p->pl, dir_entries[n].path);
+ }
+ }
+
+ return true;
+}
+
+static int parse_dir(struct pl_parser *p)
+{
+ if (!p->real_stream->is_directory)
+ return -1;
+ if (p->probing)
+ return 0;
+
+ char *path = mp_file_get_path(p, bstr0(p->real_stream->url));
+ if (!path)
+ return -1;
+
+ struct stat dir_stack[MAX_DIR_STACK];
+
+ if (p->opts->dir_mode == DIR_AUTO) {
+ struct MPOpts *opts = mp_get_config_group(NULL, p->global, &mp_opt_root);
+ p->opts->dir_mode = opts->shuffle ? DIR_RECURSIVE : DIR_LAZY;
+ talloc_free(opts);
+ }
+
+ scan_dir(p, path, dir_stack, 0);
+
+ p->add_base = false;
+
+ return p->pl->num_entries > 0 ? 0 : -1;
+}
+
+#define MIME_TYPES(...) \
+ .mime_types = (const char*const[]){__VA_ARGS__, NULL}
+
+struct pl_format {
+ const char *name;
+ int (*parse)(struct pl_parser *p);
+ const char *const *mime_types;
+};
+
+static const struct pl_format formats[] = {
+ {"directory", parse_dir},
+ {"m3u", parse_m3u,
+ MIME_TYPES("audio/mpegurl", "audio/x-mpegurl", "application/x-mpegurl")},
+ {"ini", parse_ref_init},
+ {"pls", parse_pls,
+ MIME_TYPES("audio/x-scpls")},
+ {"url", parse_url},
+ {"txt", parse_txt},
+};
+
+static const struct pl_format *probe_pl(struct pl_parser *p)
+{
+ int64_t start = stream_tell(p->s);
+ for (int n = 0; n < MP_ARRAY_SIZE(formats); n++) {
+ const struct pl_format *fmt = &formats[n];
+ stream_seek(p->s, start);
+ if (check_mimetype(p->s, fmt->mime_types)) {
+ MP_VERBOSE(p, "forcing format by mime-type.\n");
+ p->force = true;
+ return fmt;
+ }
+ if (fmt->parse(p) >= 0)
+ return fmt;
+ }
+ return NULL;
+}
+
+static int open_file(struct demuxer *demuxer, enum demux_check check)
+{
+ if (!demuxer->access_references)
+ return -1;
+
+ bool force = check < DEMUX_CHECK_UNSAFE || check == DEMUX_CHECK_REQUEST;
+
+ struct pl_parser *p = talloc_zero(NULL, struct pl_parser);
+ p->global = demuxer->global;
+ p->log = demuxer->log;
+ p->pl = talloc_zero(p, struct playlist);
+ p->real_stream = demuxer->stream;
+ p->add_base = true;
+
+ struct demux_opts *opts = mp_get_config_group(p, p->global, &demux_conf);
+ p->codepage = opts->meta_cp;
+
+ char probe[PROBE_SIZE];
+ int probe_len = stream_read_peek(p->real_stream, probe, sizeof(probe));
+ p->s = stream_memory_open(demuxer->global, probe, probe_len);
+ p->s->mime_type = demuxer->stream->mime_type;
+ p->utf16 = stream_skip_bom(p->s);
+ p->force = force;
+ p->check_level = check;
+ p->probing = true;
+ const struct pl_format *fmt = probe_pl(p);
+ free_stream(p->s);
+ playlist_clear(p->pl);
+ if (!fmt) {
+ talloc_free(p);
+ return -1;
+ }
+
+ p->probing = false;
+ p->error = false;
+ p->s = demuxer->stream;
+ p->utf16 = stream_skip_bom(p->s);
+ p->opts = mp_get_config_group(demuxer, demuxer->global, &demux_playlist_conf);
+ bool ok = fmt->parse(p) >= 0 && !p->error;
+ if (p->add_base)
+ playlist_add_base_path(p->pl, mp_dirname(demuxer->filename));
+ playlist_set_stream_flags(p->pl, demuxer->stream_origin);
+ demuxer->playlist = talloc_steal(demuxer, p->pl);
+ demuxer->filetype = p->format ? p->format : fmt->name;
+ demuxer->fully_read = true;
+ talloc_free(p);
+ if (ok)
+ demux_close_stream(demuxer);
+ return ok ? 0 : -1;
+}
+
+const demuxer_desc_t demuxer_desc_playlist = {
+ .name = "playlist",
+ .desc = "Playlist file",
+ .open = open_file,
+};