summaryrefslogtreecommitdiffstats
path: root/stream
diff options
context:
space:
mode:
Diffstat (limited to 'stream')
-rw-r--r--stream/cookies.c138
-rw-r--r--stream/cookies.h31
-rw-r--r--stream/dvb_tune.c652
-rw-r--r--stream/dvb_tune.h42
-rw-r--r--stream/dvbin.h143
-rw-r--r--stream/stream.c900
-rw-r--r--stream/stream.h269
-rw-r--r--stream/stream_avdevice.c32
-rw-r--r--stream/stream_bluray.c632
-rw-r--r--stream/stream_cb.c108
-rw-r--r--stream/stream_cdda.c369
-rw-r--r--stream/stream_concat.c179
-rw-r--r--stream/stream_dvb.c1161
-rw-r--r--stream/stream_dvdnav.c719
-rw-r--r--stream/stream_edl.c17
-rw-r--r--stream/stream_file.c377
-rw-r--r--stream/stream_lavf.c457
-rw-r--r--stream/stream_libarchive.c623
-rw-r--r--stream/stream_libarchive.h35
-rw-r--r--stream/stream_memory.c100
-rw-r--r--stream/stream_mf.c41
-rw-r--r--stream/stream_null.c35
-rw-r--r--stream/stream_slice.c181
23 files changed, 7241 insertions, 0 deletions
diff --git a/stream/cookies.c b/stream/cookies.c
new file mode 100644
index 0000000..fe61c0e
--- /dev/null
+++ b/stream/cookies.c
@@ -0,0 +1,138 @@
+/*
+ * HTTP Cookies
+ * Reads Netscape and Mozilla cookies.txt files
+ *
+ * Copyright (c) 2003 Dave Lambley <mplayer@davel.me.uk>
+ *
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include <string.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <dirent.h>
+#include <inttypes.h>
+
+#include "stream/stream.h"
+#include "options/options.h"
+#include "cookies.h"
+#include "common/msg.h"
+
+#define MAX_COOKIES 20
+
+typedef struct cookie_list_type {
+ char *name;
+ char *value;
+ char *domain;
+ char *path;
+
+ int secure;
+
+ struct cookie_list_type *next;
+} cookie_list_t;
+
+/* Like strdup, but stops at anything <31. */
+static char *col_dup(void *talloc_ctx, const char *src)
+{
+ int length = 0;
+ while (src[length] > 31)
+ length++;
+
+ return talloc_strndup(talloc_ctx, src, length);
+}
+
+/* Finds the start of all the columns */
+static int parse_line(char **ptr, char *cols[7])
+{
+ int col;
+ cols[0] = *ptr;
+
+ for (col = 1; col < 7; col++) {
+ for (; (**ptr) > 31; (*ptr)++);
+ if (**ptr == 0)
+ return 0;
+ (*ptr)++;
+ if ((*ptr)[-1] != 9)
+ return 0;
+ cols[col] = (*ptr);
+ }
+
+ return 1;
+}
+
+/* Loads a cookies.txt file into a linked list. */
+static struct cookie_list_type *load_cookies_from(void *ctx,
+ struct mpv_global *global,
+ struct mp_log *log,
+ const char *filename)
+{
+ mp_verbose(log, "Loading cookie file: %s\n", filename);
+ bstr data = stream_read_file(filename, ctx, global, 1000000);
+ if (!data.start) {
+ mp_verbose(log, "Error reading\n");
+ return NULL;
+ }
+
+ bstr_xappend(ctx, &data, (struct bstr){"", 1}); // null-terminate
+ char *ptr = data.start;
+
+ struct cookie_list_type *list = NULL;
+ while (*ptr) {
+ char *cols[7];
+ if (parse_line(&ptr, cols)) {
+ struct cookie_list_type *new;
+ new = talloc_zero(ctx, cookie_list_t);
+ new->name = col_dup(new, cols[5]);
+ new->value = col_dup(new, cols[6]);
+ new->path = col_dup(new, cols[2]);
+ new->domain = col_dup(new, cols[0]);
+ new->secure = (*(cols[3]) == 't') || (*(cols[3]) == 'T');
+ new->next = list;
+ list = new;
+ }
+ }
+
+ return list;
+}
+
+// Return a cookies string as expected by lavf (libavformat/http.c). The format
+// is like a Set-Cookie header (http://curl.haxx.se/rfc/cookie_spec.html),
+// separated by newlines.
+char *cookies_lavf(void *talloc_ctx,
+ struct mpv_global *global,
+ struct mp_log *log,
+ const char *file)
+{
+ void *tmp = talloc_new(NULL);
+ struct cookie_list_type *list = NULL;
+ if (file && file[0])
+ list = load_cookies_from(tmp, global, log, file);
+
+ char *res = talloc_strdup(talloc_ctx, "");
+
+ while (list) {
+ res = talloc_asprintf_append_buffer(res,
+ "%s=%s; path=%s; domain=%s; %s\n", list->name, list->value,
+ list->path, list->domain, list->secure ? "secure" : "");
+ list = list->next;
+ }
+
+ talloc_free(tmp);
+ return res;
+}
diff --git a/stream/cookies.h b/stream/cookies.h
new file mode 100644
index 0000000..491b87d
--- /dev/null
+++ b/stream/cookies.h
@@ -0,0 +1,31 @@
+/*
+ * HTTP Cookies
+ * Reads Netscape and Mozilla cookies.txt files
+ *
+ * Copyright (c) 2003 Dave Lambley <mplayer@davel.me.uk>
+ *
+ * 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/>.
+ */
+
+#ifndef MPLAYER_COOKIES_H
+#define MPLAYER_COOKIES_H
+
+char *cookies_lavf(void *talloc_ctx,
+ struct mpv_global *global,
+ struct mp_log *log,
+ const char *file);
+
+#endif /* MPLAYER_COOKIES_H */
diff --git a/stream/dvb_tune.c b/stream/dvb_tune.c
new file mode 100644
index 0000000..d18f70f
--- /dev/null
+++ b/stream/dvb_tune.c
@@ -0,0 +1,652 @@
+/* dvbtune - tune.c
+
+ Copyright (C) Dave Chapman 2001,2002
+ Copyright (C) Rozhuk Ivan <rozhuk.im@gmail.com> 2016 - 2017
+
+ Modified for use with MPlayer, for details see the changelog at
+ http://svn.mplayerhq.hu/mplayer/trunk/
+ $Id$
+
+ This program is free software; you can redistribute it and/or
+ modify it under the terms of the GNU General Public License
+ as published by the Free Software Foundation; either version 2
+ of the License, or (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ Or, point your browser to http://www.gnu.org/copyleft/gpl.html
+
+*/
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/ioctl.h>
+#include <poll.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+#include <linux/dvb/dmx.h>
+#include <linux/dvb/frontend.h>
+
+#include "osdep/io.h"
+#include "osdep/timer.h"
+#include "dvbin.h"
+#include "dvb_tune.h"
+#include "common/msg.h"
+
+/* Keep in sync with enum fe_delivery_system. */
+static const char *dvb_delsys_str[] = {
+ "UNDEFINED",
+ "DVB-C ANNEX A",
+ "DVB-C ANNEX B",
+ "DVB-T",
+ "DSS",
+ "DVB-S",
+ "DVB-S2",
+ "DVB-H",
+ "ISDBT",
+ "ISDBS",
+ "ISDBC",
+ "ATSC",
+ "ATSCMH",
+ "DTMB",
+ "CMMB",
+ "DAB",
+ "DVB-T2",
+ "TURBO",
+ "DVB-C ANNEX C",
+ NULL
+};
+
+const char *get_dvb_delsys(unsigned int delsys)
+{
+ if (SYS_DVB__COUNT__ <= delsys)
+ return dvb_delsys_str[0];
+ return dvb_delsys_str[delsys];
+}
+
+unsigned int dvb_get_tuner_delsys_mask(int fe_fd, struct mp_log *log)
+{
+ unsigned int ret_mask = 0, delsys;
+ struct dtv_property prop[1];
+ struct dtv_properties cmdseq = {.num = 1, .props = prop};
+
+ prop[0].cmd = DTV_ENUM_DELSYS;
+ if (ioctl(fe_fd, FE_GET_PROPERTY, &cmdseq) < 0) {
+ mp_err(log, "DVBv5: FE_GET_PROPERTY(DTV_ENUM_DELSYS) error: %d\n", errno);
+ return ret_mask;
+ }
+ unsigned int delsys_count = prop[0].u.buffer.len;
+ if (delsys_count == 0) {
+ mp_err(log, "DVBv5: Frontend returned no delivery systems!\n");
+ return ret_mask;
+ }
+ mp_verbose(log, "DVBv5: Number of supported delivery systems: %d\n", delsys_count);
+ for (unsigned int i = 0; i < delsys_count; i++) {
+ delsys = (unsigned int)prop[0].u.buffer.data[i];
+ DELSYS_SET(ret_mask, delsys);
+ mp_verbose(log, " %s\n", get_dvb_delsys(delsys));
+ }
+
+ return ret_mask;
+}
+
+int dvb_open_devices(dvb_priv_t *priv, unsigned int adapter,
+ unsigned int frontend, unsigned int demux_cnt)
+{
+ dvb_state_t *state = priv->state;
+
+ char frontend_dev[100], dvr_dev[100], demux_dev[100];
+ snprintf(frontend_dev, sizeof(frontend_dev), "/dev/dvb/adapter%u/frontend%u", adapter, frontend);
+ snprintf(dvr_dev, sizeof(dvr_dev), "/dev/dvb/adapter%u/dvr0", adapter);
+ snprintf(demux_dev, sizeof(demux_dev), "/dev/dvb/adapter%u/demux0", adapter);
+
+ MP_VERBOSE(priv, "Opening frontend device %s\n", frontend_dev);
+ state->fe_fd = open(frontend_dev, O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (state->fe_fd < 0) {
+ MP_ERR(priv, "Error opening frontend device: %d\n", errno);
+ return 0;
+ }
+
+ state->demux_fds_cnt = 0;
+ MP_VERBOSE(priv, "Opening %d demuxers\n", demux_cnt);
+ for (unsigned int i = 0; i < demux_cnt; i++) {
+ state->demux_fds[i] = open(demux_dev, O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (state->demux_fds[i] < 0) {
+ MP_ERR(priv, "Error opening demux0: %d\n", errno);
+ return 0;
+ }
+ state->demux_fds_cnt++;
+ }
+
+ state->dvr_fd = open(dvr_dev, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (state->dvr_fd < 0) {
+ MP_ERR(priv, "Error opening dvr device %s: %d\n", dvr_dev, errno);
+ return 0;
+ }
+
+ return 1;
+}
+
+int dvb_fix_demuxes(dvb_priv_t *priv, unsigned int cnt)
+{
+ dvb_state_t *state = priv->state;
+
+ char demux_dev[100];
+ snprintf(demux_dev, sizeof(demux_dev), "/dev/dvb/adapter%d/demux0",
+ state->adapters[state->cur_adapter].devno);
+
+ MP_VERBOSE(priv, "Changing demuxer count %d -> %d\n", state->demux_fds_cnt, cnt);
+ if (state->demux_fds_cnt >= cnt) {
+ for (int i = state->demux_fds_cnt - 1; i >= (int)cnt; i--) {
+ close(state->demux_fds[i]);
+ }
+ state->demux_fds_cnt = cnt;
+ } else {
+ for (int i = state->demux_fds_cnt; i < cnt; i++) {
+ state->demux_fds[i] = open(demux_dev,
+ O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (state->demux_fds[i] < 0) {
+ MP_ERR(priv, "Error opening demux0: %d\n", errno);
+ return 0;
+ }
+ state->demux_fds_cnt++;
+ }
+ }
+
+ return 1;
+}
+
+int dvb_set_ts_filt(dvb_priv_t *priv, int fd, uint16_t pid,
+ dmx_pes_type_t pestype)
+{
+ int i;
+ struct dmx_pes_filter_params pesFilterParams;
+
+ pesFilterParams.pid = pid;
+ pesFilterParams.input = DMX_IN_FRONTEND;
+ pesFilterParams.output = DMX_OUT_TS_TAP;
+ pesFilterParams.pes_type = pestype;
+ pesFilterParams.flags = DMX_IMMEDIATE_START;
+
+ {
+ int buffersize = 256 * 1024;
+ if (ioctl(fd, DMX_SET_BUFFER_SIZE, buffersize) < 0)
+ MP_ERR(priv, "Error in DMX_SET_BUFFER_SIZE %i: errno=%d\n",
+ pid, errno);
+ }
+
+ errno = 0;
+ if ((i = ioctl(fd, DMX_SET_PES_FILTER, &pesFilterParams)) < 0) {
+ MP_ERR(priv, "Error in DMX_SET_PES_FILTER %i: errno=%d\n",
+ pid, errno);
+ return 0;
+ }
+
+ return 1;
+}
+
+int dvb_get_pmt_pid(dvb_priv_t *priv, int devno, int service_id)
+{
+ /* We need special filters on the demux,
+ so open one locally, and close also here. */
+ char demux_dev[100];
+ snprintf(demux_dev, sizeof(demux_dev), "/dev/dvb/adapter%d/demux0", devno);
+
+ struct dmx_sct_filter_params fparams = {0};
+ fparams.pid = 0;
+ fparams.filter.filter[0] = 0x00;
+ fparams.filter.mask[0] = 0xff;
+ fparams.timeout = 0;
+ fparams.flags = DMX_IMMEDIATE_START | DMX_CHECK_CRC;
+
+ int pat_fd;
+ if ((pat_fd = open(demux_dev, O_RDWR)) < 0) {
+ MP_ERR(priv, "Opening PAT demux failed: %d", errno);
+ return -1;
+ }
+
+ if (ioctl(pat_fd, DMX_SET_FILTER, &fparams) < 0) {
+ MP_ERR(priv, "ioctl DMX_SET_FILTER failed: %d", errno);
+ close(pat_fd);
+ return -1;
+ }
+
+ int bytes_read;
+ unsigned char buft[4096];
+ unsigned char *bufptr = buft;
+
+ int pmt_pid = -1;
+
+ bool pat_read = false;
+ while (!pat_read) {
+ bytes_read = read(pat_fd, bufptr, sizeof(buft));
+ if (bytes_read < 0 && errno == EOVERFLOW)
+ bytes_read = read(pat_fd, bufptr, sizeof(buft));
+ if (bytes_read < 0) {
+ MP_ERR(priv, "PAT: read error: %d", errno);
+ close(pat_fd);
+ return -1;
+ }
+
+ int section_length = ((bufptr[1] & 0x0f) << 8) | bufptr[2];
+ if (bytes_read != section_length + 3)
+ continue;
+
+ bufptr += 8;
+ section_length -= 8;
+
+ /* assumes one section contains the whole pat */
+ pat_read = true;
+ while (section_length > 0) {
+ int this_service_id = (bufptr[0] << 8) | bufptr[1];
+ if (this_service_id == service_id) {
+ pmt_pid = ((bufptr[2] & 0x1f) << 8) | bufptr[3];
+ section_length = 0;
+ }
+ bufptr += 4;
+ section_length -= 4;
+ }
+ }
+ close(pat_fd);
+
+ return pmt_pid;
+}
+
+static void print_status(dvb_priv_t *priv, fe_status_t festatus)
+{
+ MP_VERBOSE(priv, "FE_STATUS:");
+ if (festatus & FE_HAS_SIGNAL)
+ MP_VERBOSE(priv, " FE_HAS_SIGNAL");
+ if (festatus & FE_TIMEDOUT)
+ MP_VERBOSE(priv, " FE_TIMEDOUT");
+ if (festatus & FE_HAS_LOCK)
+ MP_VERBOSE(priv, " FE_HAS_LOCK");
+ if (festatus & FE_HAS_CARRIER)
+ MP_VERBOSE(priv, " FE_HAS_CARRIER");
+ if (festatus & FE_HAS_VITERBI)
+ MP_VERBOSE(priv, " FE_HAS_VITERBI");
+ if (festatus & FE_HAS_SYNC)
+ MP_VERBOSE(priv, " FE_HAS_SYNC");
+ MP_VERBOSE(priv, "\n");
+}
+
+static int check_status(dvb_priv_t *priv, int fd_frontend, int tmout)
+{
+ fe_status_t festatus;
+ bool ok = false;
+ int locks = 0;
+
+ struct pollfd pfd[1];
+ pfd[0].fd = fd_frontend;
+ pfd[0].events = POLLPRI;
+
+ MP_VERBOSE(priv, "Getting frontend status\n");
+ int tm1 = (int)mp_time_sec();
+ while (!ok) {
+ festatus = 0;
+ if (poll(pfd, 1, tmout * 1000) > 0) {
+ if (pfd[0].revents & POLLPRI) {
+ if (ioctl(fd_frontend, FE_READ_STATUS, &festatus) >= 0) {
+ if (festatus & FE_HAS_LOCK)
+ locks++;
+ }
+ }
+ }
+ usleep(10000);
+ int tm2 = (int)mp_time_sec();
+ if ((festatus & FE_TIMEDOUT) || (locks >= 2) || (tm2 - tm1 >= tmout))
+ ok = true;
+ }
+
+ if (!(festatus & FE_HAS_LOCK)) {
+ MP_ERR(priv, "Not able to lock to the signal on the given frequency, "
+ "timeout: %d\n", tmout);
+ return -1;
+ }
+
+ int32_t strength = 0;
+ if (ioctl(fd_frontend, FE_READ_BER, &strength) >= 0)
+ MP_VERBOSE(priv, "Bit error rate: %d\n", strength);
+
+ strength = 0;
+ if (ioctl(fd_frontend, FE_READ_SIGNAL_STRENGTH, &strength) >= 0)
+ MP_VERBOSE(priv, "Signal strength: %d\n", strength);
+
+ strength = 0;
+ if (ioctl(fd_frontend, FE_READ_SNR, &strength) >= 0)
+ MP_VERBOSE(priv, "SNR: %d\n", strength);
+
+ strength = 0;
+ if (ioctl(fd_frontend, FE_READ_UNCORRECTED_BLOCKS, &strength) >= 0)
+ MP_VERBOSE(priv, "UNC: %d\n", strength);
+
+ print_status(priv, festatus);
+
+ return 0;
+}
+
+struct diseqc_cmd {
+ struct dvb_diseqc_master_cmd cmd;
+ uint32_t wait;
+};
+
+static int diseqc_send_msg(int fd, fe_sec_voltage_t v, struct diseqc_cmd *cmd,
+ fe_sec_tone_mode_t t, fe_sec_mini_cmd_t b)
+{
+ if (ioctl(fd, FE_SET_TONE, SEC_TONE_OFF) < 0)
+ return -1;
+ if (ioctl(fd, FE_SET_VOLTAGE, v) < 0)
+ return -1;
+ usleep(15 * 1000);
+ if (ioctl(fd, FE_DISEQC_SEND_MASTER_CMD, &cmd->cmd) < 0)
+ return -1;
+ usleep(cmd->wait * 1000);
+ usleep(15 * 1000);
+ if (ioctl(fd, FE_DISEQC_SEND_BURST, b) < 0)
+ return -1;
+ usleep(15 * 1000);
+ if (ioctl(fd, FE_SET_TONE, t) < 0)
+ return -1;
+ usleep(100000);
+
+ return 0;
+}
+
+/* digital satellite equipment control,
+ * specification is available from http://www.eutelsat.com/
+ */
+static int do_diseqc(int secfd, int sat_no, int polv, int hi_lo)
+{
+ struct diseqc_cmd cmd = { {{0xe0, 0x10, 0x38, 0xf0, 0x00, 0x00}, 4}, 0 };
+
+ /* param: high nibble: reset bits, low nibble set bits,
+ * bits are: option, position, polarizaion, band
+ */
+ cmd.cmd.msg[3] = 0xf0 | (((sat_no * 4) & 0x0f) | (hi_lo ? 1 : 0) | (polv ? 0 : 2));
+
+ return diseqc_send_msg(secfd, polv ? SEC_VOLTAGE_13 : SEC_VOLTAGE_18,
+ &cmd, hi_lo ? SEC_TONE_ON : SEC_TONE_OFF,
+ ((sat_no / 4) % 2) ? SEC_MINI_B : SEC_MINI_A);
+}
+
+static int dvbv5_tune(dvb_priv_t *priv, int fd_frontend,
+ unsigned int delsys, struct dtv_properties* cmdseq)
+{
+ MP_VERBOSE(priv, "Dumping raw tuning commands and values:\n");
+ for (int i = 0; i < cmdseq->num; ++i) {
+ MP_VERBOSE(priv, " %02d: 0x%x(%d) => 0x%x(%d)\n",
+ i, cmdseq->props[i].cmd, cmdseq->props[i].cmd,
+ cmdseq->props[i].u.data, cmdseq->props[i].u.data);
+ }
+ if (ioctl(fd_frontend, FE_SET_PROPERTY, cmdseq) < 0) {
+ MP_ERR(priv, "Error tuning channel\n");
+ return -1;
+ }
+ return 0;
+}
+
+static int tune_it(dvb_priv_t *priv, int fd_frontend, unsigned int delsys,
+ unsigned int freq, unsigned int srate, char pol,
+ int stream_id,
+ fe_spectral_inversion_t specInv, unsigned int diseqc,
+ fe_modulation_t modulation,
+ fe_code_rate_t HP_CodeRate,
+ fe_transmit_mode_t TransmissionMode,
+ fe_guard_interval_t guardInterval,
+ fe_bandwidth_t bandwidth,
+ fe_code_rate_t LP_CodeRate, fe_hierarchy_t hier,
+ int timeout)
+{
+ dvb_state_t *state = priv->state;
+
+ MP_VERBOSE(priv, "tune_it: fd_frontend %d, %s freq %lu, srate %lu, "
+ "pol %c, diseqc %u\n", fd_frontend,
+ get_dvb_delsys(delsys),
+ (long unsigned int)freq, (long unsigned int)srate,
+ (pol > ' ' ? pol : '-'), diseqc);
+
+ MP_VERBOSE(priv, "Using %s adapter %d\n",
+ get_dvb_delsys(delsys),
+ state->adapters[state->cur_adapter].devno);
+
+ {
+ /* discard stale QPSK events */
+ struct dvb_frontend_event ev;
+ while (true) {
+ if (ioctl(fd_frontend, FE_GET_EVENT, &ev) < 0)
+ break;
+ }
+ }
+
+ /* Prepare params, be verbose. */
+ int hi_lo = 0, bandwidth_hz = 0;
+ switch (delsys) {
+ case SYS_DVBT2:
+ case SYS_DVBT:
+ case SYS_ISDBT:
+ if (freq < 1000000)
+ freq *= 1000UL;
+ switch (bandwidth) {
+ case BANDWIDTH_5_MHZ:
+ bandwidth_hz = 5000000;
+ break;
+ case BANDWIDTH_6_MHZ:
+ bandwidth_hz = 6000000;
+ break;
+ case BANDWIDTH_7_MHZ:
+ bandwidth_hz = 7000000;
+ break;
+ case BANDWIDTH_8_MHZ:
+ bandwidth_hz = 8000000;
+ break;
+ case BANDWIDTH_10_MHZ:
+ bandwidth_hz = 10000000;
+ break;
+ case BANDWIDTH_AUTO:
+ if (freq < 474000000) {
+ bandwidth_hz = 7000000;
+ } else {
+ bandwidth_hz = 8000000;
+ }
+ break;
+ default:
+ bandwidth_hz = 0;
+ break;
+ }
+
+ MP_VERBOSE(priv, "tuning %s to %d Hz, bandwidth: %d\n",
+ get_dvb_delsys(delsys), freq, bandwidth_hz);
+ break;
+ case SYS_DVBS2:
+ case SYS_DVBS:
+ if (freq > 2200000) {
+ // this must be an absolute frequency
+ if (freq < SLOF) {
+ freq -= LOF1;
+ hi_lo = 0;
+ } else {
+ freq -= LOF2;
+ hi_lo = 1;
+ }
+ }
+ MP_VERBOSE(priv, "tuning %s to Freq: %u, Pol: %c Srate: %d, "
+ "22kHz: %s, LNB: %d\n", get_dvb_delsys(delsys), freq,
+ pol, srate, hi_lo ? "on" : "off", diseqc);
+
+ if (do_diseqc(fd_frontend, diseqc, (pol == 'V' ? 1 : 0), hi_lo) == 0) {
+ MP_VERBOSE(priv, "DISEQC setting succeeded\n");
+ } else {
+ MP_ERR(priv, "DISEQC setting failed\n");
+ return -1;
+ }
+
+ break;
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ MP_VERBOSE(priv, "tuning %s to %d, srate=%d\n",
+ get_dvb_delsys(delsys), freq, srate);
+ break;
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ MP_VERBOSE(priv, "tuning %s to %d, modulation=%d\n",
+ get_dvb_delsys(delsys), freq, modulation);
+ break;
+ default:
+ MP_VERBOSE(priv, "Unknown FE type, aborting.\n");
+ return 0;
+ }
+
+ /* S2API is the DVB API new since 2.6.28.
+ * It is needed to tune to new delivery systems, e.g. DVB-S2.
+ * It takes a struct with a list of pairs of command + parameter.
+ */
+
+ /* Reset before tune. */
+ struct dtv_property p_clear[] = {
+ { .cmd = DTV_CLEAR },
+ };
+ struct dtv_properties cmdseq_clear = {
+ .num = 1,
+ .props = p_clear
+ };
+ if (ioctl(fd_frontend, FE_SET_PROPERTY, &cmdseq_clear) < 0) {
+ MP_ERR(priv, "DTV_CLEAR failed\n");
+ }
+
+ /* Tune. */
+ switch (delsys) {
+ case SYS_DVBS:
+ case SYS_DVBS2:
+ {
+ struct dtv_property p[] = {
+ { .cmd = DTV_DELIVERY_SYSTEM, .u.data = delsys },
+ { .cmd = DTV_FREQUENCY, .u.data = freq },
+ { .cmd = DTV_MODULATION, .u.data = modulation },
+ { .cmd = DTV_SYMBOL_RATE, .u.data = srate },
+ { .cmd = DTV_INNER_FEC, .u.data = HP_CodeRate },
+ { .cmd = DTV_INVERSION, .u.data = specInv },
+ { .cmd = DTV_ROLLOFF, .u.data = ROLLOFF_AUTO },
+ { .cmd = DTV_PILOT, .u.data = PILOT_AUTO },
+ { .cmd = DTV_TUNE },
+ };
+ struct dtv_properties cmdseq = {
+ .num = sizeof(p) / sizeof(p[0]),
+ .props = p
+ };
+ if (dvbv5_tune(priv, fd_frontend, delsys, &cmdseq) != 0) {
+ goto error_tune;
+ }
+ }
+ break;
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ {
+ struct dtv_property p[] = {
+ { .cmd = DTV_DELIVERY_SYSTEM, .u.data = delsys },
+ { .cmd = DTV_FREQUENCY, .u.data = freq },
+ { .cmd = DTV_MODULATION, .u.data = modulation },
+ { .cmd = DTV_SYMBOL_RATE, .u.data = srate },
+ { .cmd = DTV_CODE_RATE_HP, .u.data = HP_CodeRate },
+ { .cmd = DTV_CODE_RATE_LP, .u.data = LP_CodeRate },
+ { .cmd = DTV_INVERSION, .u.data = specInv },
+ { .cmd = DTV_BANDWIDTH_HZ, .u.data = bandwidth_hz },
+ { .cmd = DTV_TRANSMISSION_MODE, .u.data = TransmissionMode },
+ { .cmd = DTV_GUARD_INTERVAL, .u.data = guardInterval },
+ { .cmd = DTV_HIERARCHY, .u.data = hier },
+ { .cmd = DTV_STREAM_ID, .u.data = stream_id },
+ { .cmd = DTV_TUNE },
+ };
+ struct dtv_properties cmdseq = {
+ .num = sizeof(p) / sizeof(p[0]),
+ .props = p
+ };
+ if (dvbv5_tune(priv, fd_frontend, delsys, &cmdseq) != 0) {
+ goto error_tune;
+ }
+ }
+ break;
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ {
+ struct dtv_property p[] = {
+ { .cmd = DTV_DELIVERY_SYSTEM, .u.data = delsys },
+ { .cmd = DTV_FREQUENCY, .u.data = freq },
+ { .cmd = DTV_MODULATION, .u.data = modulation },
+ { .cmd = DTV_SYMBOL_RATE, .u.data = srate },
+ { .cmd = DTV_INNER_FEC, .u.data = HP_CodeRate },
+ { .cmd = DTV_INVERSION, .u.data = specInv },
+ { .cmd = DTV_TUNE },
+ };
+ struct dtv_properties cmdseq = {
+ .num = sizeof(p) / sizeof(p[0]),
+ .props = p
+ };
+ if (dvbv5_tune(priv, fd_frontend, delsys, &cmdseq) != 0) {
+ goto error_tune;
+ }
+ }
+ break;
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ {
+ struct dtv_property p[] = {
+ { .cmd = DTV_DELIVERY_SYSTEM, .u.data = delsys },
+ { .cmd = DTV_FREQUENCY, .u.data = freq },
+ { .cmd = DTV_INVERSION, .u.data = specInv },
+ { .cmd = DTV_MODULATION, .u.data = modulation },
+ { .cmd = DTV_TUNE },
+ };
+ struct dtv_properties cmdseq = {
+ .num = sizeof(p) / sizeof(p[0]),
+ .props = p
+ };
+ if (dvbv5_tune(priv, fd_frontend, delsys, &cmdseq) != 0) {
+ goto error_tune;
+ }
+ }
+ break;
+ }
+
+ int tune_status = check_status(priv, fd_frontend, timeout);
+ if (tune_status != 0) {
+ MP_ERR(priv, "Error locking to channel\n");
+ }
+ return tune_status;
+
+error_tune:
+ MP_ERR(priv, "Error tuning channel\n");
+ return -1;
+}
+
+int dvb_tune(dvb_priv_t *priv, unsigned int delsys,
+ int freq, char pol, int srate, int diseqc,
+ int stream_id, fe_spectral_inversion_t specInv,
+ fe_modulation_t modulation, fe_guard_interval_t guardInterval,
+ fe_transmit_mode_t TransmissionMode, fe_bandwidth_t bandWidth,
+ fe_code_rate_t HP_CodeRate,
+ fe_code_rate_t LP_CodeRate, fe_hierarchy_t hier,
+ int timeout)
+{
+ MP_INFO(priv, "Tuning to %s frequency %lu Hz\n",
+ get_dvb_delsys(delsys), (long unsigned int) freq);
+
+ dvb_state_t *state = priv->state;
+
+ int ris = tune_it(priv, state->fe_fd, delsys, freq, srate, pol,
+ stream_id, specInv, diseqc, modulation,
+ HP_CodeRate, TransmissionMode, guardInterval,
+ bandWidth, LP_CodeRate, hier, timeout);
+
+ if (ris != 0)
+ MP_INFO(priv, "Tuning failed\n");
+
+ return ris == 0;
+}
diff --git a/stream/dvb_tune.h b/stream/dvb_tune.h
new file mode 100644
index 0000000..d7a7901
--- /dev/null
+++ b/stream/dvb_tune.h
@@ -0,0 +1,42 @@
+/*
+ * This file is part of MPlayer.
+ *
+ * MPlayer is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * MPlayer 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with MPlayer; if not, write to the Free Software Foundation, Inc.,
+ * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+ */
+
+#ifndef MPLAYER_DVB_TUNE_H
+#define MPLAYER_DVB_TUNE_H
+
+#include "dvbin.h"
+
+struct mp_log;
+
+
+const char *get_dvb_delsys(unsigned int delsys);
+unsigned int dvb_get_tuner_delsys_mask(int fe_fd, struct mp_log *log);
+int dvb_open_devices(dvb_priv_t *priv, unsigned int adapter,
+ unsigned int frontend, unsigned int demux_cnt);
+int dvb_fix_demuxes(dvb_priv_t *priv, unsigned int cnt);
+int dvb_set_ts_filt(dvb_priv_t *priv, int fd, uint16_t pid, dmx_pes_type_t pestype);
+int dvb_get_pmt_pid(dvb_priv_t *priv, int card, int service_id);
+int dvb_tune(dvb_priv_t *priv, unsigned int delsys,
+ int freq, char pol, int srate, int diseqc,
+ int stream_id, fe_spectral_inversion_t specInv,
+ fe_modulation_t modulation, fe_guard_interval_t guardInterval,
+ fe_transmit_mode_t TransmissionMode, fe_bandwidth_t bandWidth,
+ fe_code_rate_t HP_CodeRate, fe_code_rate_t LP_CodeRate,
+ fe_hierarchy_t hier, int timeout);
+
+#endif /* MPLAYER_DVB_TUNE_H */
diff --git a/stream/dvbin.h b/stream/dvbin.h
new file mode 100644
index 0000000..3daf747
--- /dev/null
+++ b/stream/dvbin.h
@@ -0,0 +1,143 @@
+/* Imported from the dvbstream project
+ *
+ * Modified for use with MPlayer, for details see the changelog at
+ * http://svn.mplayerhq.hu/mplayer/trunk/
+ * $Id$
+ */
+
+#ifndef MPLAYER_DVBIN_H
+#define MPLAYER_DVBIN_H
+
+#include "config.h"
+#include "stream.h"
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#define SLOF (11700 * 1000UL)
+#define LOF1 (9750 * 1000UL)
+#define LOF2 (10600 * 1000UL)
+
+#include <inttypes.h>
+#include <linux/dvb/dmx.h>
+#include <linux/dvb/frontend.h>
+#include <linux/dvb/version.h>
+
+#define MAX_ADAPTERS 16
+#define MAX_FRONTENDS 8
+
+#if DVB_API_VERSION < 5 || DVB_API_VERSION_MINOR < 8
+#error DVB support requires a non-ancient kernel
+#endif
+
+#define DVB_CHANNEL_LOWER -1
+#define DVB_CHANNEL_HIGHER 1
+
+#ifndef DMX_FILTER_SIZE
+#define DMX_FILTER_SIZE 32
+#endif
+
+typedef struct {
+ char *name;
+ unsigned int freq, srate, diseqc;
+ char pol;
+ unsigned int tpid, dpid1, dpid2, progid, ca, pids[DMX_FILTER_SIZE], pids_cnt;
+ bool is_dvb_x2; /* Used only in dvb_get_channels() and parse_vdr_par_string(), use delsys. */
+ unsigned int frontend;
+ unsigned int delsys;
+ unsigned int stream_id;
+ unsigned int service_id;
+ fe_spectral_inversion_t inv;
+ fe_modulation_t mod;
+ fe_transmit_mode_t trans;
+ fe_bandwidth_t bw;
+ fe_guard_interval_t gi;
+ fe_code_rate_t cr, cr_lp;
+ fe_hierarchy_t hier;
+} dvb_channel_t;
+
+typedef struct {
+ unsigned int NUM_CHANNELS;
+ unsigned int current;
+ dvb_channel_t *channels;
+} dvb_channels_list_t;
+
+typedef struct {
+ int devno;
+ unsigned int delsys_mask[MAX_FRONTENDS];
+ dvb_channels_list_t *list;
+} dvb_adapter_config_t;
+
+typedef struct {
+ unsigned int adapters_count;
+ dvb_adapter_config_t *adapters;
+ unsigned int cur_adapter;
+ unsigned int cur_frontend;
+
+ int fe_fd;
+ int dvr_fd;
+ int demux_fd[3], demux_fds[DMX_FILTER_SIZE], demux_fds_cnt;
+
+ bool is_on;
+ int retry;
+ unsigned int last_freq;
+ bool switching_channel;
+ bool stream_used;
+} dvb_state_t;
+
+typedef struct {
+ char *cfg_prog;
+ int cfg_devno;
+ int cfg_timeout;
+ char *cfg_file;
+ bool cfg_full_transponder;
+ int cfg_channel_switch_offset;
+} dvb_opts_t;
+
+typedef struct {
+ struct mp_log *log;
+
+ dvb_state_t *state;
+
+ char *prog;
+ int devno;
+
+ int opts_check_time;
+ dvb_opts_t *opts;
+ struct m_config_cache *opts_cache;
+} dvb_priv_t;
+
+
+/* Keep in sync with enum fe_delivery_system. */
+#define SYS_DVB__COUNT__ (SYS_DVBC_ANNEX_C + 1)
+
+
+#define DELSYS_BIT(__bit) (((unsigned int)1) << (__bit))
+
+#define DELSYS_SET(__mask, __bit) \
+ (__mask) |= DELSYS_BIT((__bit))
+
+#define DELSYS_IS_SET(__mask, __bit) \
+ (0 != ((__mask) & DELSYS_BIT((__bit))))
+
+
+#define DELSYS_SUPP_MASK \
+ ( \
+ DELSYS_BIT(SYS_DVBC_ANNEX_A) | \
+ DELSYS_BIT(SYS_DVBT) | \
+ DELSYS_BIT(SYS_DVBS) | \
+ DELSYS_BIT(SYS_DVBS2) | \
+ DELSYS_BIT(SYS_ATSC) | \
+ DELSYS_BIT(SYS_DVBC_ANNEX_B) | \
+ DELSYS_BIT(SYS_DVBT2) | \
+ DELSYS_BIT(SYS_ISDBT) | \
+ DELSYS_BIT(SYS_DVBC_ANNEX_C) \
+ )
+
+void dvb_update_config(stream_t *);
+int dvb_parse_path(stream_t *);
+int dvb_set_channel(stream_t *, unsigned int, unsigned int);
+dvb_state_t *dvb_get_state(stream_t *);
+
+#endif /* MPLAYER_DVBIN_H */
diff --git a/stream/stream.c b/stream/stream.c
new file mode 100644
index 0000000..dd67825
--- /dev/null
+++ b/stream/stream.c
@@ -0,0 +1,900 @@
+/*
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <limits.h>
+
+#include <strings.h>
+#include <assert.h>
+
+#include "osdep/io.h"
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+
+#include "common/common.h"
+#include "common/global.h"
+#include "demux/demux.h"
+#include "misc/bstr.h"
+#include "misc/thread_tools.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "osdep/timer.h"
+#include "stream.h"
+
+#include "options/m_option.h"
+#include "options/m_config.h"
+
+extern const stream_info_t stream_info_cdda;
+extern const stream_info_t stream_info_dvb;
+extern const stream_info_t stream_info_null;
+extern const stream_info_t stream_info_memory;
+extern const stream_info_t stream_info_mf;
+extern const stream_info_t stream_info_ffmpeg;
+extern const stream_info_t stream_info_ffmpeg_unsafe;
+extern const stream_info_t stream_info_avdevice;
+extern const stream_info_t stream_info_file;
+extern const stream_info_t stream_info_slice;
+extern const stream_info_t stream_info_fd;
+extern const stream_info_t stream_info_ifo_dvdnav;
+extern const stream_info_t stream_info_dvdnav;
+extern const stream_info_t stream_info_bdmv_dir;
+extern const stream_info_t stream_info_bluray;
+extern const stream_info_t stream_info_bdnav;
+extern const stream_info_t stream_info_edl;
+extern const stream_info_t stream_info_libarchive;
+extern const stream_info_t stream_info_cb;
+
+static const stream_info_t *const stream_list[] = {
+#if HAVE_CDDA
+ &stream_info_cdda,
+#endif
+ &stream_info_ffmpeg,
+ &stream_info_ffmpeg_unsafe,
+ &stream_info_avdevice,
+#if HAVE_DVBIN
+ &stream_info_dvb,
+#endif
+#if HAVE_DVDNAV
+ &stream_info_ifo_dvdnav,
+ &stream_info_dvdnav,
+#endif
+#if HAVE_LIBBLURAY
+ &stream_info_bdmv_dir,
+ &stream_info_bluray,
+ &stream_info_bdnav,
+#endif
+#if HAVE_LIBARCHIVE
+ &stream_info_libarchive,
+#endif
+ &stream_info_memory,
+ &stream_info_null,
+ &stream_info_mf,
+ &stream_info_edl,
+ &stream_info_file,
+ &stream_info_slice,
+ &stream_info_fd,
+ &stream_info_cb,
+};
+
+// Because of guarantees documented on STREAM_BUFFER_SIZE.
+// Half the buffer is used as forward buffer, the other for seek-back.
+#define STREAM_MIN_BUFFER_SIZE (STREAM_BUFFER_SIZE * 2)
+// Sort of arbitrary; keep *2 of it comfortably within integer limits.
+// Must be power of 2.
+#define STREAM_MAX_BUFFER_SIZE (512 * 1024 * 1024)
+
+struct stream_opts {
+ int64_t buffer_size;
+ bool load_unsafe_playlists;
+};
+
+#define OPT_BASE_STRUCT struct stream_opts
+
+const struct m_sub_options stream_conf = {
+ .opts = (const struct m_option[]){
+ {"stream-buffer-size", OPT_BYTE_SIZE(buffer_size),
+ M_RANGE(STREAM_MIN_BUFFER_SIZE, STREAM_MAX_BUFFER_SIZE)},
+ {"load-unsafe-playlists", OPT_BOOL(load_unsafe_playlists)},
+ {0}
+ },
+ .size = sizeof(struct stream_opts),
+ .defaults = &(const struct stream_opts){
+ .buffer_size = 128 * 1024,
+ },
+};
+
+// return -1 if not hex char
+static int hex2dec(char c)
+{
+ if (c >= '0' && c <= '9')
+ return c - '0';
+ if (c >= 'A' && c <= 'F')
+ return 10 + c - 'A';
+ if (c >= 'a' && c <= 'f')
+ return 10 + c - 'a';
+ return -1;
+}
+
+// Replace escape sequences in an URL (or a part of an URL)
+void mp_url_unescape_inplace(char *url)
+{
+ for (int len = strlen(url), i = 0, o = 0; i <= len;) {
+ if ((url[i] != '%') || (i > len - 3)) { // %NN can't start after len-3
+ url[o++] = url[i++];
+ continue;
+ }
+
+ int msd = hex2dec(url[i + 1]),
+ lsd = hex2dec(url[i + 2]);
+
+ if (msd >= 0 && lsd >= 0) {
+ url[o++] = 16 * msd + lsd;
+ i += 3;
+ } else {
+ url[o++] = url[i++];
+ url[o++] = url[i++];
+ url[o++] = url[i++];
+ }
+ }
+}
+
+static const char hex_digits[] = "0123456789ABCDEF";
+
+
+static const char url_default_ok[] = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "-._~";
+
+// Escape according to http://tools.ietf.org/html/rfc3986#section-2.1
+// Only unreserved characters are not escaped.
+// The argument ok (if not NULL) is as follows:
+// ok[0] != '~': additional characters that are not escaped
+// ok[0] == '~': do not escape anything but these characters
+// (can't override the unreserved characters, which are
+// never escaped)
+char *mp_url_escape(void *talloc_ctx, const char *url, const char *ok)
+{
+ char *rv = talloc_size(talloc_ctx, strlen(url) * 3 + 1);
+ char *out = rv;
+ bool negate = ok && ok[0] == '~';
+
+ for (char c; (c = *url); url++) {
+ bool as_is = negate ? !strchr(ok + 1, c)
+ : (strchr(url_default_ok, c) || (ok && strchr(ok, c)));
+ if (as_is) {
+ *out++ = c;
+ } else {
+ unsigned char v = c;
+ *out++ = '%';
+ *out++ = hex_digits[v / 16];
+ *out++ = hex_digits[v % 16];
+ }
+ }
+
+ *out = 0;
+ return rv;
+}
+
+static const char *match_proto(const char *url, const char *proto)
+{
+ int l = strlen(proto);
+ if (l > 0) {
+ if (strncasecmp(url, proto, l) == 0 && strncmp("://", url + l, 3) == 0)
+ return url + l + 3;
+ } else if (!mp_is_url(bstr0(url))) {
+ return url; // pure filenames
+ }
+ return NULL;
+}
+
+// src and new are both STREAM_ORIGIN_* values. This checks whether a stream
+// with flags "new" can be opened from the "src". On success, return
+// new origin, on incompatibility return 0.
+static int check_origin(int src, int new)
+{
+ switch (src) {
+ case STREAM_ORIGIN_DIRECT:
+ case STREAM_ORIGIN_UNSAFE:
+ // Allow anything, but constrain it to the new origin.
+ return new;
+ case STREAM_ORIGIN_FS:
+ // From unix FS, allow all but unsafe.
+ if (new == STREAM_ORIGIN_FS || new == STREAM_ORIGIN_NET)
+ return new;
+ break;
+ case STREAM_ORIGIN_NET:
+ // Allow only other network links.
+ if (new == STREAM_ORIGIN_NET)
+ return new;
+ break;
+ }
+ return 0;
+}
+
+// Read len bytes from the start position, and wrap around as needed. Limit the
+// actually read data to the size of the buffer. Return amount of copied bytes.
+// len: max bytes to copy to dst
+// pos: index into s->buffer[], e.g. s->buf_start is byte 0
+// returns: bytes copied to dst (limited by len and available buffered data)
+static int ring_copy(struct stream *s, void *dst, int len, int pos)
+{
+ assert(len >= 0);
+
+ if (pos < s->buf_start || pos > s->buf_end)
+ return 0;
+
+ int copied = 0;
+ len = MPMIN(len, s->buf_end - pos);
+
+ if (len && pos <= s->buffer_mask) {
+ int copy = MPMIN(len, s->buffer_mask + 1 - pos);
+ memcpy(dst, &s->buffer[pos], copy);
+ copied += copy;
+ len -= copy;
+ pos += copy;
+ }
+
+ if (len) {
+ memcpy((char *)dst + copied, &s->buffer[pos & s->buffer_mask], len);
+ copied += len;
+ }
+
+ return copied;
+}
+
+// Resize the current stream buffer. Uses a larger size if needed to keep data.
+// Does nothing if the size is adequate. Calling this with 0 ensures it uses the
+// default buffer size if possible.
+// The caller must check whether enough data was really allocated.
+// keep: keep at least [buf_end-keep, buf_end] (used for assert()s only)
+// new: new total size of buffer
+// returns: false if buffer allocation failed, true if reallocated or size ok
+static bool stream_resize_buffer(struct stream *s, int keep, int new)
+{
+ assert(keep >= s->buf_end - s->buf_cur);
+ assert(keep <= new);
+
+ new = MPMAX(new, s->requested_buffer_size);
+ new = MPMIN(new, STREAM_MAX_BUFFER_SIZE);
+ new = mp_round_next_power_of_2(new);
+
+ assert(keep <= new); // can't fail (if old buffer size was valid)
+
+ if (new == s->buffer_mask + 1)
+ return true;
+
+ int old_pos = s->buf_cur - s->buf_start;
+ int old_used_len = s->buf_end - s->buf_start;
+ int skip = old_used_len > new ? old_used_len - new : 0;
+
+ MP_DBG(s, "resize stream to %d bytes, drop %d bytes\n", new, skip);
+
+ void *nbuf = ta_alloc_size(s, new);
+ if (!nbuf)
+ return false; // oom; tolerate it, caller needs to check if required
+
+ int new_len = 0;
+ if (s->buffer)
+ new_len = ring_copy(s, nbuf, new, s->buf_start + skip);
+ assert(new_len == old_used_len - skip);
+ assert(old_pos >= skip); // "keep" too low
+ assert(old_pos - skip <= new_len);
+ s->buf_start = 0;
+ s->buf_cur = old_pos - skip;
+ s->buf_end = new_len;
+
+ ta_free(s->buffer);
+
+ s->buffer = nbuf;
+ s->buffer_mask = new - 1;
+
+ return true;
+}
+
+static int stream_create_instance(const stream_info_t *sinfo,
+ struct stream_open_args *args,
+ struct stream **ret)
+{
+ const char *url = args->url;
+ int flags = args->flags;
+
+ *ret = NULL;
+
+ const char *path = url;
+
+ if (flags & STREAM_LOCAL_FS_ONLY) {
+ if (!sinfo->local_fs)
+ return STREAM_NO_MATCH;
+ } else {
+ for (int n = 0; sinfo->protocols && sinfo->protocols[n]; n++) {
+ path = match_proto(url, sinfo->protocols[n]);
+ if (path)
+ break;
+ }
+
+ if (!path)
+ return STREAM_NO_MATCH;
+ }
+
+ stream_t *s = talloc_zero(NULL, stream_t);
+ s->global = args->global;
+ struct stream_opts *opts = mp_get_config_group(s, s->global, &stream_conf);
+ if (flags & STREAM_SILENT) {
+ s->log = mp_null_log;
+ } else {
+ s->log = mp_log_new(s, s->global->log, sinfo->name);
+ }
+ s->info = sinfo;
+ s->cancel = args->cancel;
+ s->url = talloc_strdup(s, url);
+ s->path = talloc_strdup(s, path);
+ s->mode = flags & (STREAM_READ | STREAM_WRITE);
+ s->requested_buffer_size = opts->buffer_size;
+
+ if (flags & STREAM_LESS_NOISE)
+ mp_msg_set_max_level(s->log, MSGL_WARN);
+
+ struct demux_opts *demux_opts = mp_get_config_group(s, s->global, &demux_conf);
+ s->access_references = demux_opts->access_references;
+ talloc_free(demux_opts);
+
+ MP_VERBOSE(s, "Opening %s\n", url);
+
+ if (strlen(url) > INT_MAX / 8) {
+ MP_ERR(s, "URL too large.\n");
+ talloc_free(s);
+ return STREAM_ERROR;
+ }
+
+ if ((s->mode & STREAM_WRITE) && !sinfo->can_write) {
+ MP_DBG(s, "No write access implemented.\n");
+ talloc_free(s);
+ return STREAM_NO_MATCH;
+ }
+
+ s->stream_origin = flags & STREAM_ORIGIN_MASK; // pass through by default
+ if (opts->load_unsafe_playlists) {
+ s->stream_origin = STREAM_ORIGIN_DIRECT;
+ } else if (sinfo->stream_origin) {
+ s->stream_origin = check_origin(s->stream_origin, sinfo->stream_origin);
+ }
+
+ if (!s->stream_origin) {
+ talloc_free(s);
+ return STREAM_UNSAFE;
+ }
+
+ int r = STREAM_UNSUPPORTED;
+ if (sinfo->open2) {
+ r = sinfo->open2(s, args);
+ } else if (!args->special_arg) {
+ r = (sinfo->open)(s);
+ }
+ if (r != STREAM_OK) {
+ talloc_free(s);
+ return r;
+ }
+
+ if (!stream_resize_buffer(s, 0, 0)) {
+ free_stream(s);
+ return STREAM_ERROR;
+ }
+
+ assert(s->seekable == !!s->seek);
+
+ if (s->mime_type)
+ MP_VERBOSE(s, "Mime-type: '%s'\n", s->mime_type);
+
+ MP_DBG(s, "Stream opened successfully.\n");
+
+ *ret = s;
+ return STREAM_OK;
+}
+
+int stream_create_with_args(struct stream_open_args *args, struct stream **ret)
+
+{
+ assert(args->url);
+
+ int r = STREAM_NO_MATCH;
+ *ret = NULL;
+
+ // Open stream proper
+ if (args->sinfo) {
+ r = stream_create_instance(args->sinfo, args, ret);
+ } else {
+ for (int i = 0; i < MP_ARRAY_SIZE(stream_list); i++) {
+ r = stream_create_instance(stream_list[i], args, ret);
+ if (r == STREAM_OK)
+ break;
+ if (r == STREAM_NO_MATCH || r == STREAM_UNSUPPORTED)
+ continue;
+ if (r == STREAM_UNSAFE)
+ continue;
+ break;
+ }
+ }
+
+ if (!*ret && !(args->flags & STREAM_SILENT) && !mp_cancel_test(args->cancel))
+ {
+ struct mp_log *log = mp_log_new(NULL, args->global->log, "!stream");
+
+ if (r == STREAM_UNSAFE) {
+ mp_err(log, "\nRefusing to load potentially unsafe URL from a playlist.\n"
+ "Use the --load-unsafe-playlists option to load it anyway.\n\n");
+ } else if (r == STREAM_NO_MATCH || r == STREAM_UNSUPPORTED) {
+ mp_err(log, "No protocol handler found to open URL %s\n", args->url);
+ mp_err(log, "The protocol is either unsupported, or was disabled "
+ "at compile-time.\n");
+ } else {
+ mp_err(log, "Failed to open %s.\n", args->url);
+ }
+
+ talloc_free(log);
+ }
+
+ return r;
+}
+
+struct stream *stream_create(const char *url, int flags,
+ struct mp_cancel *c, struct mpv_global *global)
+{
+ struct stream_open_args args = {
+ .global = global,
+ .cancel = c,
+ .flags = flags,
+ .url = url,
+ };
+ struct stream *s;
+ stream_create_with_args(&args, &s);
+ return s;
+}
+
+stream_t *open_output_stream(const char *filename, struct mpv_global *global)
+{
+ return stream_create(filename, STREAM_ORIGIN_DIRECT | STREAM_WRITE,
+ NULL, global);
+}
+
+// Read function bypassing the local stream buffer. This will not write into
+// s->buffer, but into buf[0..len] instead.
+// Returns 0 on error or EOF, and length of bytes read on success.
+// Partial reads are possible, even if EOF is not reached.
+static int stream_read_unbuffered(stream_t *s, void *buf, int len)
+{
+ assert(len >= 0);
+ if (len <= 0)
+ return 0;
+
+ int res = 0;
+ // we will retry even if we already reached EOF previously.
+ if (s->fill_buffer && !mp_cancel_test(s->cancel))
+ res = s->fill_buffer(s, buf, len);
+ if (res <= 0) {
+ s->eof = 1;
+ return 0;
+ }
+ assert(res <= len);
+ // When reading succeeded we are obviously not at eof.
+ s->eof = 0;
+ s->pos += res;
+ s->total_unbuffered_read_bytes += res;
+ return res;
+}
+
+// Ask for having at most "forward" bytes ready to read in the buffer.
+// To read everything, you may have to call this in a loop.
+// forward: desired amount of bytes in buffer after s->cur_pos
+// returns: progress (false on EOF or on OOM or if enough data was available)
+static bool stream_read_more(struct stream *s, int forward)
+{
+ assert(forward >= 0);
+
+ int forward_avail = s->buf_end - s->buf_cur;
+ if (forward_avail >= forward)
+ return false;
+
+ // Avoid that many small reads will lead to many low-level read calls.
+ forward = MPMAX(forward, s->requested_buffer_size / 2);
+ assert(forward_avail < forward);
+
+ // Keep guaranteed seek-back.
+ int buf_old = MPMIN(s->buf_cur - s->buf_start, s->requested_buffer_size / 2);
+
+ if (!stream_resize_buffer(s, buf_old + forward_avail, buf_old + forward))
+ return false;
+
+ int buf_alloc = s->buffer_mask + 1;
+
+ assert(s->buf_start <= s->buf_cur);
+ assert(s->buf_cur <= s->buf_end);
+ assert(s->buf_cur < buf_alloc * 2);
+ assert(s->buf_end < buf_alloc * 2);
+ assert(s->buf_start < buf_alloc);
+
+ // Note: read as much as possible, even if forward is much smaller. Do
+ // this because the stream buffer is supposed to set an approx. minimum
+ // read size on it.
+ int read = buf_alloc - (buf_old + forward_avail); // free buffer past end
+
+ int pos = s->buf_end & s->buffer_mask;
+ read = MPMIN(read, buf_alloc - pos);
+
+ // Note: if wrap-around happens, we need to make two calls. This may
+ // affect latency (e.g. waiting for new data on a socket), so do only
+ // 1 read call always.
+ read = stream_read_unbuffered(s, &s->buffer[pos], read);
+
+ s->buf_end += read;
+
+ // May have overwritten old data.
+ if (s->buf_end - s->buf_start >= buf_alloc) {
+ assert(s->buf_end >= buf_alloc);
+
+ s->buf_start = s->buf_end - buf_alloc;
+
+ assert(s->buf_start <= s->buf_cur);
+ assert(s->buf_cur <= s->buf_end);
+
+ if (s->buf_start >= buf_alloc) {
+ s->buf_start -= buf_alloc;
+ s->buf_cur -= buf_alloc;
+ s->buf_end -= buf_alloc;
+ }
+ }
+
+ // Must not have overwritten guaranteed old data.
+ assert(s->buf_cur - s->buf_start >= buf_old);
+
+ if (s->buf_cur < s->buf_end)
+ s->eof = 0;
+
+ return !!read;
+}
+
+// Read between 1..buf_size bytes of data, return how much data has been read.
+// Return 0 on EOF, error, or if buf_size was 0.
+int stream_read_partial(stream_t *s, void *buf, int buf_size)
+{
+ assert(s->buf_cur <= s->buf_end);
+ assert(buf_size >= 0);
+ if (s->buf_cur == s->buf_end && buf_size > 0) {
+ if (buf_size > (s->buffer_mask + 1) / 2) {
+ // Direct read if the buffer is too small anyway.
+ stream_drop_buffers(s);
+ return stream_read_unbuffered(s, buf, buf_size);
+ }
+ stream_read_more(s, 1);
+ }
+ int res = ring_copy(s, buf, buf_size, s->buf_cur);
+ s->buf_cur += res;
+ return res;
+}
+
+// Slow version of stream_read_char(); called by it if the buffer is empty.
+int stream_read_char_fallback(stream_t *s)
+{
+ uint8_t c;
+ return stream_read_partial(s, &c, 1) ? c : -256;
+}
+
+int stream_read(stream_t *s, void *mem, int total)
+{
+ int len = total;
+ while (len > 0) {
+ int read = stream_read_partial(s, mem, len);
+ if (read <= 0)
+ break; // EOF
+ mem = (char *)mem + read;
+ len -= read;
+ }
+ total -= len;
+ return total;
+}
+
+// Read ahead so that at least forward_size bytes are readable ahead. Returns
+// the actual forward amount available (restricted by EOF or buffer limits).
+int stream_peek(stream_t *s, int forward_size)
+{
+ while (stream_read_more(s, forward_size)) {}
+ return s->buf_end - s->buf_cur;
+}
+
+// Like stream_read(), but do not advance the current position. This may resize
+// the buffer to satisfy the read request.
+int stream_read_peek(stream_t *s, void *buf, int buf_size)
+{
+ stream_peek(s, buf_size);
+ return ring_copy(s, buf, buf_size, s->buf_cur);
+}
+
+int stream_write_buffer(stream_t *s, void *buf, int len)
+{
+ if (!s->write_buffer)
+ return -1;
+ int orig_len = len;
+ while (len) {
+ int w = s->write_buffer(s, buf, len);
+ if (w <= 0)
+ return -1;
+ s->pos += w;
+ buf = (char *)buf + w;
+ len -= w;
+ }
+ return orig_len;
+}
+
+// Drop len bytes form input, possibly reading more until all is skipped. If
+// EOF or an error was encountered before all could be skipped, return false,
+// otherwise return true.
+static bool stream_skip_read(struct stream *s, int64_t len)
+{
+ while (len > 0) {
+ unsigned int left = s->buf_end - s->buf_cur;
+ if (!left) {
+ if (!stream_read_more(s, 1))
+ return false;
+ continue;
+ }
+ unsigned skip = MPMIN(len, left);
+ s->buf_cur += skip;
+ len -= skip;
+ }
+ return true;
+}
+
+// Drop the internal buffer. Note that this will advance the stream position
+// (as seen by stream_tell()), because the real stream position is ahead of the
+// logical stream position by the amount of buffered but not yet read data.
+void stream_drop_buffers(stream_t *s)
+{
+ s->pos = stream_tell(s);
+ s->buf_start = s->buf_cur = s->buf_end = 0;
+ s->eof = 0;
+ stream_resize_buffer(s, 0, 0);
+}
+
+// Seek function bypassing the local stream buffer.
+static bool stream_seek_unbuffered(stream_t *s, int64_t newpos)
+{
+ if (newpos != s->pos) {
+ MP_VERBOSE(s, "stream level seek from %" PRId64 " to %" PRId64 "\n",
+ s->pos, newpos);
+
+ s->total_stream_seeks++;
+
+ if (newpos > s->pos && !s->seekable) {
+ MP_ERR(s, "Cannot seek forward in this stream\n");
+ return false;
+ }
+ if (newpos < s->pos && !s->seekable) {
+ MP_ERR(s, "Cannot seek backward in linear streams!\n");
+ return false;
+ }
+ if (s->seek(s, newpos) <= 0) {
+ int level = mp_cancel_test(s->cancel) ? MSGL_V : MSGL_ERR;
+ MP_MSG(s, level, "Seek failed (to %lld, size %lld)\n",
+ (long long)newpos, (long long)stream_get_size(s));
+ return false;
+ }
+ stream_drop_buffers(s);
+ s->pos = newpos;
+ }
+ return true;
+}
+
+bool stream_seek(stream_t *s, int64_t pos)
+{
+ MP_TRACE(s, "seek request from %" PRId64 " to %" PRId64 "\n",
+ stream_tell(s), pos);
+
+ s->eof = 0; // eof should be set only on read; seeking always clears it
+
+ if (pos < 0) {
+ MP_ERR(s, "Invalid seek to negative position %lld!\n", (long long)pos);
+ pos = 0;
+ }
+
+ if (pos <= s->pos) {
+ int64_t x = pos - (s->pos - (int)s->buf_end);
+ if (x >= (int)s->buf_start) {
+ s->buf_cur = x;
+ assert(s->buf_cur >= s->buf_start);
+ assert(s->buf_cur <= s->buf_end);
+ return true;
+ }
+ }
+
+ if (s->mode == STREAM_WRITE)
+ return s->seekable && s->seek(s, pos);
+
+ // Skip data instead of performing a seek in some cases.
+ if (pos >= s->pos &&
+ ((!s->seekable && s->fast_skip) ||
+ pos - s->pos <= s->requested_buffer_size))
+ {
+ return stream_skip_read(s, pos - stream_tell(s));
+ }
+
+ return stream_seek_unbuffered(s, pos);
+}
+
+// Like stream_seek(), but strictly prefer skipping data instead of failing, if
+// it's a forward-seek.
+bool stream_seek_skip(stream_t *s, int64_t pos)
+{
+ uint64_t cur_pos = stream_tell(s);
+
+ if (cur_pos == pos)
+ return true;
+
+ return !s->seekable && pos > cur_pos
+ ? stream_skip_read(s, pos - cur_pos)
+ : stream_seek(s, pos);
+}
+
+int stream_control(stream_t *s, int cmd, void *arg)
+{
+ return s->control ? s->control(s, cmd, arg) : STREAM_UNSUPPORTED;
+}
+
+// Return the current size of the stream, or a negative value if unknown.
+int64_t stream_get_size(stream_t *s)
+{
+ return s->get_size ? s->get_size(s) : -1;
+}
+
+void free_stream(stream_t *s)
+{
+ if (!s)
+ return;
+
+ if (s->close)
+ s->close(s);
+ talloc_free(s);
+}
+
+static const char *const bom[3] = {"\xEF\xBB\xBF", "\xFF\xFE", "\xFE\xFF"};
+
+// Return utf16 argument for stream_read_line
+int stream_skip_bom(struct stream *s)
+{
+ char buf[4];
+ int len = stream_read_peek(s, buf, sizeof(buf));
+ bstr data = {buf, len};
+ for (int n = 0; n < 3; n++) {
+ if (bstr_startswith0(data, bom[n])) {
+ stream_seek_skip(s, stream_tell(s) + strlen(bom[n]));
+ return n;
+ }
+ }
+ return -1; // default to 8 bit codepages
+}
+
+// Read the rest of the stream into memory (current pos to EOF), and return it.
+// talloc_ctx: used as talloc parent for the returned allocation
+// max_size: must be set to >0. If the file is larger than that, it is treated
+// as error. This is a minor robustness measure.
+// returns: stream contents, or .start/.len set to NULL on error
+// If the file was empty, but no error happened, .start will be non-NULL and
+// .len will be 0.
+// For convenience, the returned buffer is padded with a 0 byte. The padding
+// is not included in the returned length.
+struct bstr stream_read_complete(struct stream *s, void *talloc_ctx,
+ int max_size)
+{
+ if (max_size > 1000000000)
+ abort();
+
+ int bufsize;
+ int total_read = 0;
+ int padding = 1;
+ char *buf = NULL;
+ int64_t size = stream_get_size(s) - stream_tell(s);
+ if (size > max_size)
+ return (struct bstr){NULL, 0};
+ if (size > 0)
+ bufsize = size + padding;
+ else
+ bufsize = 1000;
+ while (1) {
+ buf = talloc_realloc_size(talloc_ctx, buf, bufsize);
+ int readsize = stream_read(s, buf + total_read, bufsize - total_read);
+ total_read += readsize;
+ if (total_read < bufsize)
+ break;
+ if (bufsize > max_size) {
+ talloc_free(buf);
+ return (struct bstr){NULL, 0};
+ }
+ bufsize = MPMIN(bufsize + (bufsize >> 1), max_size + padding);
+ }
+ buf = talloc_realloc_size(talloc_ctx, buf, total_read + padding);
+ memset(&buf[total_read], 0, padding);
+ return (struct bstr){buf, total_read};
+}
+
+struct bstr stream_read_file(const char *filename, void *talloc_ctx,
+ struct mpv_global *global, int max_size)
+{
+ struct bstr res = {0};
+ int flags = STREAM_ORIGIN_DIRECT | STREAM_READ | STREAM_LOCAL_FS_ONLY |
+ STREAM_LESS_NOISE;
+ stream_t *s = stream_create(filename, flags, NULL, global);
+ if (s) {
+ res = stream_read_complete(s, talloc_ctx, max_size);
+ free_stream(s);
+ }
+ return res;
+}
+
+char **stream_get_proto_list(void)
+{
+ char **list = NULL;
+ int num = 0;
+ for (int i = 0; i < MP_ARRAY_SIZE(stream_list); i++) {
+ const stream_info_t *stream_info = stream_list[i];
+
+ if (!stream_info->protocols)
+ continue;
+
+ for (int j = 0; stream_info->protocols[j]; j++) {
+ if (*stream_info->protocols[j] == '\0')
+ continue;
+
+ MP_TARRAY_APPEND(NULL, list, num,
+ talloc_strdup(NULL, stream_info->protocols[j]));
+ }
+ }
+ MP_TARRAY_APPEND(NULL, list, num, NULL);
+ return list;
+}
+
+void stream_print_proto_list(struct mp_log *log)
+{
+ int count = 0;
+
+ mp_info(log, "Protocols:\n\n");
+ char **list = stream_get_proto_list();
+ for (int i = 0; list[i]; i++) {
+ mp_info(log, " %s://\n", list[i]);
+ count++;
+ talloc_free(list[i]);
+ }
+ talloc_free(list);
+ mp_info(log, "\nTotal: %d protocols\n", count);
+}
+
+bool stream_has_proto(const char *proto)
+{
+ for (int i = 0; i < MP_ARRAY_SIZE(stream_list); i++) {
+ const stream_info_t *stream_info = stream_list[i];
+
+ for (int j = 0; stream_info->protocols && stream_info->protocols[j]; j++) {
+ if (strcmp(stream_info->protocols[j], proto) == 0)
+ return true;
+ }
+ }
+
+ return false;
+}
diff --git a/stream/stream.h b/stream/stream.h
new file mode 100644
index 0000000..423ba12
--- /dev/null
+++ b/stream/stream.h
@@ -0,0 +1,269 @@
+/*
+ * 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/>.
+ */
+
+#ifndef MPLAYER_STREAM_H
+#define MPLAYER_STREAM_H
+
+#include "common/msg.h"
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <inttypes.h>
+#include <sys/types.h>
+#include <fcntl.h>
+
+#include "misc/bstr.h"
+
+// Minimum guaranteed buffer and seek-back size. For any reads <= of this size,
+// it's guaranteed that you can seek back by <= of this size again.
+#define STREAM_BUFFER_SIZE 2048
+
+// flags for stream_open_ext (this includes STREAM_READ and STREAM_WRITE)
+
+// stream->mode
+#define STREAM_READ 0
+#define STREAM_WRITE (1 << 0)
+
+#define STREAM_SILENT (1 << 1)
+
+// Origin value for "security". This is an integer within the flags bit-field.
+#define STREAM_ORIGIN_DIRECT (1 << 2) // passed from cmdline or loadfile
+#define STREAM_ORIGIN_FS (2 << 2) // referenced from playlist on unix FS
+#define STREAM_ORIGIN_NET (3 << 2) // referenced from playlist on network
+#define STREAM_ORIGIN_UNSAFE (4 << 2) // from a grotesque source
+
+#define STREAM_ORIGIN_MASK (7 << 2) // for extracting origin value from flags
+
+#define STREAM_LOCAL_FS_ONLY (1 << 5) // stream_file only, no URLs
+#define STREAM_LESS_NOISE (1 << 6) // try to log errors only
+
+// end flags for stream_open_ext (the naming convention sucks)
+
+#define STREAM_UNSAFE -3
+#define STREAM_NO_MATCH -2
+#define STREAM_UNSUPPORTED -1
+#define STREAM_ERROR 0
+#define STREAM_OK 1
+
+enum stream_ctrl {
+ // Certain network protocols
+ STREAM_CTRL_AVSEEK,
+ STREAM_CTRL_HAS_AVSEEK,
+ STREAM_CTRL_GET_METADATA,
+
+ // Optical discs (internal interface between streams and demux_disc)
+ STREAM_CTRL_GET_TIME_LENGTH,
+ STREAM_CTRL_GET_DVD_INFO,
+ STREAM_CTRL_GET_DISC_NAME,
+ STREAM_CTRL_GET_NUM_CHAPTERS,
+ STREAM_CTRL_GET_CURRENT_TIME,
+ STREAM_CTRL_GET_CHAPTER_TIME,
+ STREAM_CTRL_SEEK_TO_TIME,
+ STREAM_CTRL_GET_ASPECT_RATIO,
+ STREAM_CTRL_GET_NUM_ANGLES,
+ STREAM_CTRL_GET_ANGLE,
+ STREAM_CTRL_SET_ANGLE,
+ STREAM_CTRL_GET_NUM_TITLES,
+ STREAM_CTRL_GET_TITLE_LENGTH, // double* (in: title number, out: len)
+ STREAM_CTRL_GET_LANG,
+ STREAM_CTRL_GET_CURRENT_TITLE,
+ STREAM_CTRL_SET_CURRENT_TITLE,
+};
+
+struct stream_lang_req {
+ int type; // STREAM_AUDIO, STREAM_SUB
+ int id;
+ char name[50];
+};
+
+struct stream_dvd_info_req {
+ unsigned int palette[16];
+ int num_subs;
+};
+
+// for STREAM_CTRL_AVSEEK
+struct stream_avseek {
+ int stream_index;
+ int64_t timestamp;
+ int flags;
+};
+
+struct stream;
+struct stream_open_args;
+typedef struct stream_info_st {
+ const char *name;
+ // opts is set from ->opts
+ int (*open)(struct stream *st);
+ // Alternative to open(). Only either open() or open2() can be set.
+ int (*open2)(struct stream *st, const struct stream_open_args *args);
+ const char *const *protocols;
+ bool can_write; // correctly checks for READ/WRITE modes
+ bool local_fs; // supports STREAM_LOCAL_FS_ONLY
+ int stream_origin; // 0 or set of STREAM_ORIGIN_*; if 0, the same origin
+ // is set, or the stream's open() function handles it
+} stream_info_t;
+
+typedef struct stream {
+ const struct stream_info_st *info;
+
+ // Read
+ int (*fill_buffer)(struct stream *s, void *buffer, int max_len);
+ // Write
+ int (*write_buffer)(struct stream *s, void *buffer, int len);
+ // Seek
+ int (*seek)(struct stream *s, int64_t pos);
+ // Total stream size in bytes (negative if unavailable)
+ int64_t (*get_size)(struct stream *s);
+ // Control
+ int (*control)(struct stream *s, int cmd, void *arg);
+ // Close
+ void (*close)(struct stream *s);
+
+ int64_t pos;
+ int eof; // valid only after read calls that returned a short result
+ int mode; //STREAM_READ or STREAM_WRITE
+ int stream_origin; // any STREAM_ORIGIN_*
+ void *priv; // used for DVD, TV, RTSP etc
+ char *url; // filename/url (possibly including protocol prefix)
+ char *path; // filename (url without protocol prefix)
+ char *mime_type; // when HTTP streaming is used
+ char *demuxer; // request demuxer to be used
+ char *lavf_type; // name of expected demuxer type for lavf
+ bool streaming : 1; // known to be a network stream if true
+ bool seekable : 1; // presence of general byte seeking support
+ bool fast_skip : 1; // consider stream fast enough to fw-seek by skipping
+ bool is_network : 1; // I really don't know what this is for
+ bool is_local_file : 1; // from the filesystem
+ bool is_directory : 1; // directory on the filesystem
+ bool access_references : 1; // open other streams
+ struct mp_log *log;
+ struct mpv_global *global;
+
+ struct mp_cancel *cancel; // cancellation notification
+
+ // Read statistic for fill_buffer calls. All bytes read by fill_buffer() are
+ // added to this. The user can reset this as needed.
+ uint64_t total_unbuffered_read_bytes;
+ // Seek statistics. The user can reset this as needed.
+ uint64_t total_stream_seeks;
+
+ // Buffer size requested by user; s->buffer may have a different size
+ int requested_buffer_size;
+
+ // This is a ring buffer. It is reset only on seeks (or when buffers are
+ // dropped). Otherwise old contents always stay valid.
+ // The valid buffer is from buf_start to buf_end; buf_end can be larger
+ // than the buffer size (requires wrap around). buf_cur is a value in the
+ // range [buf_start, buf_end].
+ // When reading more data from the stream, buf_start is advanced as old
+ // data is overwritten with new data.
+ // Example:
+ // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
+ // +===========================+---------------------------+
+ // + 05 06 07 08 | 01 02 03 04 + 05 06 07 08 | 01 02 03 04 +
+ // +===========================+---------------------------+
+ // ^ buf_start (4) | |
+ // | ^ buf_end (12 % 8 => 4)
+ // ^ buf_cur (9 % 8 => 1)
+ // Here, the entire 8 byte buffer is filled, i.e. buf_end - buf_start = 8.
+ // buffer_mask == 7, so (x & buffer_mask) == (x % buffer_size)
+ unsigned int buf_start; // index of oldest byte in buffer (is <= buffer_mask)
+ unsigned int buf_cur; // current read pos (can be > buffer_mask)
+ unsigned int buf_end; // end position (can be > buffer_mask)
+
+ unsigned int buffer_mask; // buffer_size-1, where buffer_size == 2**n
+ uint8_t *buffer;
+} stream_t;
+
+// Non-inline version of stream_read_char().
+int stream_read_char_fallback(stream_t *s);
+
+int stream_write_buffer(stream_t *s, void *buf, int len);
+
+inline static int stream_read_char(stream_t *s)
+{
+ return s->buf_cur < s->buf_end
+ ? s->buffer[(s->buf_cur++) & s->buffer_mask]
+ : stream_read_char_fallback(s);
+}
+
+int stream_skip_bom(struct stream *s);
+
+inline static int64_t stream_tell(stream_t *s)
+{
+ return s->pos + s->buf_cur - s->buf_end;
+}
+
+bool stream_seek_skip(stream_t *s, int64_t pos);
+bool stream_seek(stream_t *s, int64_t pos);
+int stream_read(stream_t *s, void *mem, int total);
+int stream_read_partial(stream_t *s, void *buf, int buf_size);
+int stream_peek(stream_t *s, int forward_size);
+int stream_read_peek(stream_t *s, void *buf, int buf_size);
+void stream_drop_buffers(stream_t *s);
+int64_t stream_get_size(stream_t *s);
+
+struct mpv_global;
+
+struct bstr stream_read_complete(struct stream *s, void *talloc_ctx,
+ int max_size);
+struct bstr stream_read_file(const char *filename, void *talloc_ctx,
+ struct mpv_global *global, int max_size);
+
+int stream_control(stream_t *s, int cmd, void *arg);
+void free_stream(stream_t *s);
+
+struct stream_open_args {
+ struct mpv_global *global;
+ struct mp_cancel *cancel; // aborting stream access (used directly)
+ const char *url;
+ int flags; // STREAM_READ etc.
+ const stream_info_t *sinfo; // NULL = autoprobe, otherwise force stream impl.
+ void *special_arg; // specific to impl., use only with sinfo
+};
+
+int stream_create_with_args(struct stream_open_args *args, struct stream **ret);
+struct stream *stream_create(const char *url, int flags,
+ struct mp_cancel *c, struct mpv_global *global);
+stream_t *open_output_stream(const char *filename, struct mpv_global *global);
+
+void mp_url_unescape_inplace(char *buf);
+char *mp_url_escape(void *talloc_ctx, const char *s, const char *ok);
+
+// stream_memory.c
+struct stream *stream_memory_open(struct mpv_global *global, void *data, int len);
+
+// stream_concat.c
+struct stream *stream_concat_open(struct mpv_global *global, struct mp_cancel *c,
+ struct stream **streams, int num_streams);
+
+// stream_file.c
+char *mp_file_url_to_filename(void *talloc_ctx, bstr url);
+char *mp_file_get_path(void *talloc_ctx, bstr url);
+
+// stream_lavf.c
+struct AVDictionary;
+void mp_setup_av_network_options(struct AVDictionary **dict,
+ const char *target_fmt,
+ struct mpv_global *global,
+ struct mp_log *log);
+
+void stream_print_proto_list(struct mp_log *log);
+char **stream_get_proto_list(void);
+bool stream_has_proto(const char *proto);
+
+#endif /* MPLAYER_STREAM_H */
diff --git a/stream/stream_avdevice.c b/stream/stream_avdevice.c
new file mode 100644
index 0000000..5046b21
--- /dev/null
+++ b/stream/stream_avdevice.c
@@ -0,0 +1,32 @@
+/*
+ * 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 "stream.h"
+
+static int open_f(stream_t *stream)
+{
+ stream->demuxer = "lavf";
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_avdevice = {
+ .name = "avdevice",
+ .open = open_f,
+ .protocols = (const char*const[]){ "avdevice", "av", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_bluray.c b/stream/stream_bluray.c
new file mode 100644
index 0000000..7771375
--- /dev/null
+++ b/stream/stream_bluray.c
@@ -0,0 +1,632 @@
+/*
+ * Copyright (C) 2010 Benjamin Zores <ben@geexbox.org>
+ *
+ * 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/>.
+ */
+
+/*
+ * Blu-ray parser/reader using libbluray
+ * Use 'git clone git://git.videolan.org/libbluray' to get it.
+ *
+ * TODO:
+ * - Add descrambled keys database support (KEYDB.cfg)
+ *
+ */
+
+#include <string.h>
+#include <strings.h>
+#include <assert.h>
+
+#include <libbluray/bluray.h>
+#include <libbluray/meta_data.h>
+#include <libbluray/overlay.h>
+#include <libbluray/keys.h>
+#include <libbluray/bluray-version.h>
+#include <libbluray/log_control.h>
+#include <libavutil/common.h>
+
+#include "config.h"
+#include "mpv_talloc.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "stream.h"
+#include "osdep/timer.h"
+#include "sub/osd.h"
+#include "sub/img_convert.h"
+#include "video/mp_image.h"
+
+#define BLURAY_SECTOR_SIZE 6144
+
+#define BLURAY_DEFAULT_ANGLE 0
+#define BLURAY_DEFAULT_CHAPTER 0
+#define BLURAY_PLAYLIST_TITLE -3
+#define BLURAY_DEFAULT_TITLE -2
+#define BLURAY_MENU_TITLE -1
+
+// 90khz ticks
+#define BD_TIMEBASE (90000)
+#define BD_TIME_TO_MP(x) ((x) / (double)(BD_TIMEBASE))
+#define BD_TIME_FROM_MP(x) ((uint64_t)(x * BD_TIMEBASE))
+
+// copied from aacs.h in libaacs
+#define AACS_ERROR_CORRUPTED_DISC -1 /* opening or reading of AACS files failed */
+#define AACS_ERROR_NO_CONFIG -2 /* missing config file */
+#define AACS_ERROR_NO_PK -3 /* no matching processing key */
+#define AACS_ERROR_NO_CERT -4 /* no valid certificate */
+#define AACS_ERROR_CERT_REVOKED -5 /* certificate has been revoked */
+#define AACS_ERROR_MMC_OPEN -6 /* MMC open failed (no MMC drive ?) */
+#define AACS_ERROR_MMC_FAILURE -7 /* MMC failed */
+#define AACS_ERROR_NO_DK -8 /* no matching device key */
+
+
+struct bluray_opts {
+ char *bluray_device;
+};
+
+#define OPT_BASE_STRUCT struct bluray_opts
+const struct m_sub_options stream_bluray_conf = {
+ .opts = (const struct m_option[]) {
+ {"device", OPT_STRING(bluray_device), .flags = M_OPT_FILE},
+ {0},
+ },
+ .size = sizeof(struct bluray_opts),
+};
+
+struct bluray_priv_s {
+ BLURAY *bd;
+ BLURAY_TITLE_INFO *title_info;
+ int num_titles;
+ int current_angle;
+ int current_title;
+ int current_playlist;
+
+ int cfg_title;
+ int cfg_playlist;
+ char *cfg_device;
+
+ bool use_nav;
+ struct bluray_opts *opts;
+ struct m_config_cache *opts_cache;
+};
+
+static void destruct(struct bluray_priv_s *priv)
+{
+ if (priv->title_info)
+ bd_free_title_info(priv->title_info);
+ bd_close(priv->bd);
+}
+
+inline static int play_playlist(struct bluray_priv_s *priv, int playlist)
+{
+ return bd_select_playlist(priv->bd, playlist);
+}
+
+inline static int play_title(struct bluray_priv_s *priv, int title)
+{
+ return bd_select_title(priv->bd, title);
+}
+
+static void bluray_stream_close(stream_t *s)
+{
+ destruct(s->priv);
+}
+
+static void handle_event(stream_t *s, const BD_EVENT *ev)
+{
+ struct bluray_priv_s *b = s->priv;
+ switch (ev->event) {
+ case BD_EVENT_MENU:
+ break;
+ case BD_EVENT_STILL:
+ break;
+ case BD_EVENT_STILL_TIME:
+ bd_read_skip_still(b->bd);
+ break;
+ case BD_EVENT_END_OF_TITLE:
+ break;
+ case BD_EVENT_PLAYLIST:
+ b->current_playlist = ev->param;
+ b->current_title = bd_get_current_title(b->bd);
+ if (b->title_info)
+ bd_free_title_info(b->title_info);
+ b->title_info = bd_get_playlist_info(b->bd, b->current_playlist,
+ b->current_angle);
+ break;
+ case BD_EVENT_TITLE:
+ if (ev->param == BLURAY_TITLE_FIRST_PLAY) {
+ b->current_title = bd_get_current_title(b->bd);
+ } else
+ b->current_title = ev->param;
+ if (b->title_info) {
+ bd_free_title_info(b->title_info);
+ b->title_info = NULL;
+ }
+ break;
+ case BD_EVENT_ANGLE:
+ b->current_angle = ev->param;
+ if (b->title_info) {
+ bd_free_title_info(b->title_info);
+ b->title_info = bd_get_playlist_info(b->bd, b->current_playlist,
+ b->current_angle);
+ }
+ break;
+ case BD_EVENT_POPUP:
+ break;
+#if BLURAY_VERSION >= BLURAY_VERSION_CODE(0, 5, 0)
+ case BD_EVENT_DISCONTINUITY:
+ break;
+#endif
+ default:
+ MP_TRACE(s, "Unhandled event: %d %d\n", ev->event, ev->param);
+ break;
+ }
+}
+
+static int bluray_stream_fill_buffer(stream_t *s, void *buf, int len)
+{
+ struct bluray_priv_s *b = s->priv;
+ BD_EVENT event;
+ while (bd_get_event(b->bd, &event))
+ handle_event(s, &event);
+ return bd_read(b->bd, buf, len);
+}
+
+static int bluray_stream_control(stream_t *s, int cmd, void *arg)
+{
+ struct bluray_priv_s *b = s->priv;
+
+ switch (cmd) {
+ case STREAM_CTRL_GET_NUM_CHAPTERS: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ *((unsigned int *) arg) = ti->chapter_count;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CHAPTER_TIME: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ int chapter = *(double *)arg;
+ double time = MP_NOPTS_VALUE;
+ if (chapter >= 0 || chapter < ti->chapter_count)
+ time = BD_TIME_TO_MP(ti->chapters[chapter].start);
+ if (time == MP_NOPTS_VALUE)
+ return STREAM_ERROR;
+ *(double *)arg = time;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SET_CURRENT_TITLE: {
+ const uint32_t title = *((unsigned int*)arg);
+ if (title >= b->num_titles || !play_title(b, title))
+ return STREAM_UNSUPPORTED;
+ b->current_title = title;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CURRENT_TITLE: {
+ *((unsigned int *) arg) = b->current_title;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_NUM_TITLES: {
+ *((unsigned int *)arg) = b->num_titles;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_TIME_LENGTH: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ *((double *) arg) = BD_TIME_TO_MP(ti->duration);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CURRENT_TIME: {
+ *((double *) arg) = BD_TIME_TO_MP(bd_tell_time(b->bd));
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SEEK_TO_TIME: {
+ double pts = *((double *) arg);
+ bd_seek_time(b->bd, BD_TIME_FROM_MP(pts));
+ stream_drop_buffers(s);
+ // API makes it hard to determine seeking success
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_NUM_ANGLES: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ *((int *) arg) = ti->angle_count;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_ANGLE: {
+ *((int *) arg) = b->current_angle;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SET_ANGLE: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (!ti)
+ return STREAM_UNSUPPORTED;
+ int angle = *((int *) arg);
+ if (angle < 0 || angle > ti->angle_count)
+ return STREAM_UNSUPPORTED;
+ b->current_angle = angle;
+ bd_seamless_angle_change(b->bd, angle);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_LANG: {
+ const BLURAY_TITLE_INFO *ti = b->title_info;
+ if (ti && ti->clip_count) {
+ struct stream_lang_req *req = arg;
+ BLURAY_STREAM_INFO *si = NULL;
+ int count = 0;
+ switch (req->type) {
+ case STREAM_AUDIO:
+ count = ti->clips[0].audio_stream_count;
+ si = ti->clips[0].audio_streams;
+ break;
+ case STREAM_SUB:
+ count = ti->clips[0].pg_stream_count;
+ si = ti->clips[0].pg_streams;
+ break;
+ }
+ for (int n = 0; n < count; n++) {
+ BLURAY_STREAM_INFO *i = &si[n];
+ if (i->pid == req->id) {
+ snprintf(req->name, sizeof(req->name), "%.4s", i->lang);
+ return STREAM_OK;
+ }
+ }
+ }
+ return STREAM_ERROR;
+ }
+ case STREAM_CTRL_GET_DISC_NAME: {
+ const struct meta_dl *meta = bd_get_meta(b->bd);
+ if (!meta || !meta->di_name || !meta->di_name[0])
+ break;
+ *(char**)arg = talloc_strdup(NULL, meta->di_name);
+ return STREAM_OK;
+ }
+ default:
+ break;
+ }
+
+ return STREAM_UNSUPPORTED;
+}
+
+static const char *aacs_strerr(int err)
+{
+ switch (err) {
+ case AACS_ERROR_CORRUPTED_DISC: return "opening or reading of AACS files failed";
+ case AACS_ERROR_NO_CONFIG: return "missing config file";
+ case AACS_ERROR_NO_PK: return "no matching processing key";
+ case AACS_ERROR_NO_CERT: return "no valid certificate";
+ case AACS_ERROR_CERT_REVOKED: return "certificate has been revoked";
+ case AACS_ERROR_MMC_OPEN: return "MMC open failed (maybe no MMC drive?)";
+ case AACS_ERROR_MMC_FAILURE: return "MMC failed";
+ case AACS_ERROR_NO_DK: return "no matching device key";
+ default: return "unknown error";
+ }
+}
+
+static bool check_disc_info(stream_t *s)
+{
+ struct bluray_priv_s *b = s->priv;
+ const BLURAY_DISC_INFO *info = bd_get_disc_info(b->bd);
+
+ // check Blu-ray
+ if (!info->bluray_detected) {
+ MP_ERR(s, "Given stream is not a Blu-ray.\n");
+ return false;
+ }
+
+ // check AACS
+ if (info->aacs_detected) {
+ if (!info->libaacs_detected) {
+ MP_ERR(s, "AACS encryption detected but cannot find libaacs.\n");
+ return false;
+ }
+ if (!info->aacs_handled) {
+ MP_ERR(s, "AACS error: %s\n", aacs_strerr(info->aacs_error_code));
+ return false;
+ }
+ }
+
+ // check BD+
+ if (info->bdplus_detected) {
+ if (!info->libbdplus_detected) {
+ MP_ERR(s, "BD+ encryption detected but cannot find libbdplus.\n");
+ return false;
+ }
+ if (!info->bdplus_handled) {
+ MP_ERR(s, "Cannot decrypt BD+ encryption.\n");
+ return false;
+ }
+ }
+
+ return true;
+}
+
+static void select_initial_title(stream_t *s, int title_guess) {
+ struct bluray_priv_s *b = s->priv;
+
+ if (b->cfg_title == BLURAY_PLAYLIST_TITLE) {
+ if (!play_playlist(b, b->cfg_playlist))
+ MP_WARN(s, "Couldn't start playlist '%05d'.\n", b->cfg_playlist);
+ b->current_title = bd_get_current_title(b->bd);
+ } else {
+ int title = -1;
+ if (b->cfg_title != BLURAY_DEFAULT_TITLE )
+ title = b->cfg_title;
+ else
+ title = title_guess;
+ if (title < 0)
+ return;
+
+ if (play_title(b, title))
+ b->current_title = title;
+ else {
+ MP_WARN(s, "Couldn't start title '%d'.\n", title);
+ b->current_title = bd_get_current_title(b->bd);
+ }
+ }
+}
+
+static int bluray_stream_open_internal(stream_t *s)
+{
+ struct bluray_priv_s *b = s->priv;
+
+ char *device = NULL;
+ /* find the requested device */
+ if (b->cfg_device && b->cfg_device[0]) {
+ device = b->cfg_device;
+ } else {
+ device = b->opts->bluray_device;
+ }
+
+ if (!device || !device[0]) {
+ MP_ERR(s, "No Blu-ray device/location was specified ...\n");
+ return STREAM_UNSUPPORTED;
+ }
+
+ if (!mp_msg_test(s->log, MSGL_DEBUG))
+ bd_set_debug_mask(0);
+
+ /* open device */
+ BLURAY *bd = bd_open(device, NULL);
+ if (!bd) {
+ MP_ERR(s, "Couldn't open Blu-ray device: %s\n", device);
+ return STREAM_UNSUPPORTED;
+ }
+ b->bd = bd;
+
+ if (!check_disc_info(s)) {
+ destruct(b);
+ return STREAM_UNSUPPORTED;
+ }
+
+ int title_guess = BLURAY_DEFAULT_TITLE;
+ if (b->use_nav) {
+ MP_FATAL(s, "BluRay menu support has been removed.\n");
+ return STREAM_ERROR;
+ } else {
+ /* check for available titles on disc */
+ b->num_titles = bd_get_titles(bd, TITLES_RELEVANT, 0);
+ if (!b->num_titles) {
+ MP_ERR(s, "Can't find any Blu-ray-compatible title here.\n");
+ destruct(b);
+ return STREAM_UNSUPPORTED;
+ }
+
+ MP_INFO(s, "List of available titles:\n");
+
+ /* parse titles information */
+ uint64_t max_duration = 0;
+ for (int i = 0; i < b->num_titles; i++) {
+ BLURAY_TITLE_INFO *ti = bd_get_title_info(bd, i, 0);
+ if (!ti)
+ continue;
+
+ char *time = mp_format_time(ti->duration / 90000, false);
+ MP_INFO(s, "idx: %3d duration: %s (playlist: %05d.mpls)\n",
+ i, time, ti->playlist);
+ talloc_free(time);
+
+ /* try to guess which title may contain the main movie */
+ if (ti->duration > max_duration) {
+ max_duration = ti->duration;
+ title_guess = i;
+ }
+
+ bd_free_title_info(ti);
+ }
+ }
+
+ // these should be set before any callback
+ b->current_angle = -1;
+ b->current_title = -1;
+
+ // initialize libbluray event queue
+ bd_get_event(bd, NULL);
+
+ select_initial_title(s, title_guess);
+
+ s->fill_buffer = bluray_stream_fill_buffer;
+ s->close = bluray_stream_close;
+ s->control = bluray_stream_control;
+ s->priv = b;
+ s->demuxer = "+disc";
+
+ MP_VERBOSE(s, "Blu-ray successfully opened.\n");
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_bdnav;
+
+static int bluray_stream_open(stream_t *s)
+{
+ struct bluray_priv_s *b = talloc_zero(s, struct bluray_priv_s);
+ s->priv = b;
+
+ struct m_config_cache *opts_cache =
+ m_config_cache_alloc(s, s->global, &stream_bluray_conf);
+
+ b->opts_cache = opts_cache;
+ b->opts = opts_cache->opts;
+
+ b->use_nav = s->info == &stream_info_bdnav;
+
+ bstr title, bdevice, rest = { .len = 0 };
+ bstr_split_tok(bstr0(s->path), "/", &title, &bdevice);
+
+ b->cfg_title = BLURAY_DEFAULT_TITLE;
+
+ if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) {
+ b->cfg_title = BLURAY_DEFAULT_TITLE;
+ } else if (bstr_equals0(title, "menu")) {
+ b->cfg_title = BLURAY_MENU_TITLE;
+ } else if (bstr_equals0(title, "mpls")) {
+ bstr_split_tok(bdevice, "/", &title, &bdevice);
+ long long pl = bstrtoll(title, &rest, 10);
+ if (rest.len) {
+ MP_ERR(s, "number expected: '%.*s'\n", BSTR_P(rest));
+ return STREAM_ERROR;
+ } else if (pl < 0 || 99999 < pl) {
+ MP_ERR(s, "invalid playlist: '%.*s', must be in the range 0-99999\n",
+ BSTR_P(title));
+ return STREAM_ERROR;
+ }
+ b->cfg_playlist = pl;
+ b->cfg_title = BLURAY_PLAYLIST_TITLE;
+ } else if (title.len) {
+ long long t = bstrtoll(title, &rest, 10);
+ if (rest.len) {
+ MP_ERR(s, "number expected: '%.*s'\n", BSTR_P(rest));
+ return STREAM_ERROR;
+ } else if (t < 0 || 99999 < t) {
+ MP_ERR(s, "invalid title: '%.*s', must be in the range 0-99999\n",
+ BSTR_P(title));
+ return STREAM_ERROR;
+ }
+ b->cfg_title = t;
+ }
+
+ b->cfg_device = bstrto0(b, bdevice);
+
+ return bluray_stream_open_internal(s);
+}
+
+const stream_info_t stream_info_bluray = {
+ .name = "bd",
+ .open = bluray_stream_open,
+ .protocols = (const char*const[]){ "bd", "br", "bluray", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
+
+const stream_info_t stream_info_bdnav = {
+ .name = "bdnav",
+ .open = bluray_stream_open,
+ .protocols = (const char*const[]){ "bdnav", "brnav", "bluraynav", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
+
+static bool check_bdmv(const char *path)
+{
+ if (strcasecmp(mp_basename(path), "MovieObject.bdmv"))
+ return false;
+
+ FILE *temp = fopen(path, "rb");
+ if (!temp)
+ return false;
+
+ char data[50] = {0};
+
+ fread(data, 50, 1, temp);
+ fclose(temp);
+
+ bstr bdata = {data, 50};
+
+ return bstr_startswith0(bdata, "MOBJ0100") || // AVCHD
+ bstr_startswith0(bdata, "MOBJ0200") || // Blu-ray
+ bstr_startswith0(bdata, "MOBJ0300"); // UHD BD
+}
+
+// Destructively remove the current trailing path component.
+static void remove_prefix(char *path)
+{
+ size_t len = strlen(path);
+#if HAVE_DOS_PATHS
+ const char *seps = "/\\";
+#else
+ const char *seps = "/";
+#endif
+ while (len > 0 && !strchr(seps, path[len - 1]))
+ len--;
+ while (len > 0 && strchr(seps, path[len - 1]))
+ len--;
+ path[len] = '\0';
+}
+
+static int bdmv_dir_stream_open(stream_t *stream)
+{
+ struct bluray_priv_s *priv = talloc_ptrtype(stream, priv);
+ stream->priv = priv;
+ *priv = (struct bluray_priv_s){
+ .cfg_title = BLURAY_DEFAULT_TITLE,
+ };
+
+ if (!stream->access_references)
+ goto unsupported;
+
+ char *path = mp_file_get_path(priv, bstr0(stream->url));
+ if (!path)
+ goto unsupported;
+
+ // We allow the path to point to a directory containing BDMV/, a
+ // directory containing MovieObject.bdmv, or that file itself.
+ if (!check_bdmv(path)) {
+ // On UNIX, just assume the filename has always this case.
+ char *npath = mp_path_join(priv, path, "MovieObject.bdmv");
+ if (!check_bdmv(npath)) {
+ npath = mp_path_join(priv, path, "BDMV/MovieObject.bdmv");
+ if (!check_bdmv(npath))
+ goto unsupported;
+ }
+ path = npath;
+ }
+
+ // Go up by 2 levels.
+ remove_prefix(path);
+ remove_prefix(path);
+ priv->cfg_device = path;
+ if (strlen(priv->cfg_device) <= 1)
+ goto unsupported;
+
+ MP_INFO(stream, "BDMV detected. Redirecting to bluray://\n");
+ return bluray_stream_open_internal(stream);
+
+unsupported:
+ talloc_free(priv);
+ stream->priv = NULL;
+ return STREAM_UNSUPPORTED;
+}
+
+const stream_info_t stream_info_bdmv_dir = {
+ .name = "bdmv/bluray",
+ .open = bdmv_dir_stream_open,
+ .protocols = (const char*const[]){ "file", "", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_cb.c b/stream/stream_cb.c
new file mode 100644
index 0000000..29e5563
--- /dev/null
+++ b/stream/stream_cb.c
@@ -0,0 +1,108 @@
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "common/global.h"
+#include "stream.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "player/client.h"
+#include "libmpv/stream_cb.h"
+#include "misc/thread_tools.h"
+
+struct priv {
+ mpv_stream_cb_info info;
+ struct mp_cancel *cancel;
+};
+
+static int fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ struct priv *p = s->priv;
+ return (int)p->info.read_fn(p->info.cookie, buffer, (size_t)max_len);
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+ return p->info.seek_fn(p->info.cookie, newpos) >= 0;
+}
+
+static int64_t get_size(stream_t *s)
+{
+ struct priv *p = s->priv;
+
+ if (p->info.size_fn) {
+ int64_t size = p->info.size_fn(p->info.cookie);
+ if (size >= 0)
+ return size;
+ }
+
+ return -1;
+}
+
+static void s_close(stream_t *s)
+{
+ struct priv *p = s->priv;
+ p->info.close_fn(p->info.cookie);
+}
+
+static int open_cb(stream_t *stream)
+{
+ struct priv *p = talloc_ptrtype(stream, p);
+ stream->priv = p;
+
+ bstr bproto = mp_split_proto(bstr0(stream->url), NULL);
+ char *proto = bstrto0(stream, bproto);
+
+ void *user_data;
+ mpv_stream_cb_open_ro_fn open_fn;
+
+ if (!mp_streamcb_lookup(stream->global, proto, &user_data, &open_fn))
+ return STREAM_UNSUPPORTED;
+
+ mpv_stream_cb_info info = {0};
+
+ int r = open_fn(user_data, stream->url, &info);
+ if (r < 0) {
+ if (r != MPV_ERROR_LOADING_FAILED)
+ MP_WARN(stream, "unknown error from user callback\n");
+ return STREAM_ERROR;
+ }
+
+ if (!info.read_fn || !info.close_fn) {
+ MP_FATAL(stream, "required read_fn or close_fn callbacks not set.\n");
+ return STREAM_ERROR;
+ }
+
+ p->info = info;
+
+ if (p->info.seek_fn && p->info.seek_fn(p->info.cookie, 0) >= 0) {
+ stream->seek = seek;
+ stream->seekable = true;
+ }
+ stream->fast_skip = true;
+ stream->fill_buffer = fill_buffer;
+ stream->get_size = get_size;
+ stream->close = s_close;
+
+ if (p->info.cancel_fn && stream->cancel) {
+ p->cancel = mp_cancel_new(p);
+ mp_cancel_set_parent(p->cancel, stream->cancel);
+ mp_cancel_set_cb(p->cancel, p->info.cancel_fn, p->info.cookie);
+ }
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_cb = {
+ .name = "stream_callback",
+ .open = open_cb,
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_cdda.c b/stream/stream_cdda.c
new file mode 100644
index 0000000..71ae461
--- /dev/null
+++ b/stream/stream_cdda.c
@@ -0,0 +1,369 @@
+/*
+ * Original author: Albeu
+ *
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+
+#include <cdio/cdio.h>
+
+// For cdio_cddap_version
+#pragma GCC diagnostic push
+#pragma GCC diagnostic ignored "-Wstrict-prototypes"
+#include <cdio/paranoia/cdda.h>
+#include <cdio/paranoia/paranoia.h>
+#pragma GCC diagnostic pop
+
+#include "common/msg.h"
+#include "config.h"
+#include "mpv_talloc.h"
+
+#include "stream.h"
+#include "options/m_option.h"
+#include "options/m_config.h"
+#include "options/options.h"
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+typedef struct cdda_params {
+ cdrom_drive_t *cd;
+ cdrom_paranoia_t *cdp;
+ int sector;
+ int start_sector;
+ int end_sector;
+ uint8_t *data;
+ size_t data_pos;
+
+ // options
+ char *cdda_device;
+ int speed;
+ int paranoia_mode;
+ int sector_size;
+ int search_overlap;
+ int toc_bias;
+ int toc_offset;
+ bool skip;
+ char *device;
+ int span[2];
+ bool cdtext;
+} cdda_priv;
+
+#define OPT_BASE_STRUCT struct cdda_params
+const struct m_sub_options stream_cdda_conf = {
+ .opts = (const m_option_t[]) {
+ {"device", OPT_STRING(cdda_device), .flags = M_OPT_FILE},
+ {"speed", OPT_INT(speed), M_RANGE(1, 100)},
+ {"paranoia", OPT_INT(paranoia_mode), M_RANGE(0, 2)},
+ {"sector-size", OPT_INT(sector_size), M_RANGE(1, 100)},
+ {"overlap", OPT_INT(search_overlap), M_RANGE(0, 75)},
+ {"toc-bias", OPT_INT(toc_bias),
+ .deprecation_message = "toc-bias is no longer used"},
+ {"toc-offset", OPT_INT(toc_offset)},
+ {"skip", OPT_BOOL(skip)},
+ {"span-a", OPT_INT(span[0])},
+ {"span-b", OPT_INT(span[1])},
+ {"cdtext", OPT_BOOL(cdtext)},
+ {0}
+ },
+ .size = sizeof(struct cdda_params),
+ .defaults = &(const struct cdda_params){
+ .search_overlap = -1,
+ .skip = true,
+ },
+};
+
+static const char *const cdtext_name[] = {
+ [CDTEXT_FIELD_ARRANGER] = "Arranger",
+ [CDTEXT_FIELD_COMPOSER] = "Composer",
+ [CDTEXT_FIELD_MESSAGE] = "Message",
+ [CDTEXT_FIELD_ISRC] = "ISRC",
+ [CDTEXT_FIELD_PERFORMER] = "Performer",
+ [CDTEXT_FIELD_SONGWRITER] = "Songwriter",
+ [CDTEXT_FIELD_TITLE] = "Title",
+ [CDTEXT_FIELD_UPC_EAN] = "UPC_EAN",
+};
+
+static void print_cdtext(stream_t *s, int track)
+{
+ cdda_priv* p = (cdda_priv*)s->priv;
+ if (!p->cdtext)
+ return;
+ cdtext_t *text = cdio_get_cdtext(p->cd->p_cdio);
+ int header = 0;
+ if (text) {
+ for (int i = 0; i < sizeof(cdtext_name) / sizeof(cdtext_name[0]); i++) {
+ const char *name = cdtext_name[i];
+ const char *value = cdtext_get_const(text, i, track);
+ if (name && value) {
+ if (!header)
+ MP_INFO(s, "CD-Text (%s):\n", track ? "track" : "CD");
+ header = 1;
+ MP_INFO(s, " %s: '%s'\n", name, value);
+ }
+ }
+ }
+}
+
+static void print_track_info(stream_t *s, int track)
+{
+ MP_INFO(s, "Switched to track %d\n", track);
+ print_cdtext(s, track);
+}
+
+static void cdparanoia_callback(long int inpos, paranoia_cb_mode_t function)
+{
+}
+
+static int fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ cdda_priv *p = (cdda_priv *)s->priv;
+
+ if (!p->data || p->data_pos >= CDIO_CD_FRAMESIZE_RAW) {
+ if ((p->sector < p->start_sector) || (p->sector > p->end_sector))
+ return 0;
+
+ p->data_pos = 0;
+ p->data = (uint8_t *)paranoia_read(p->cdp, cdparanoia_callback);
+ if (!p->data)
+ return 0;
+
+ p->sector++;
+ }
+
+ size_t copy = MPMIN(CDIO_CD_FRAMESIZE_RAW - p->data_pos, max_len);
+ memcpy(buffer, p->data + p->data_pos, copy);
+ p->data_pos += copy;
+ return copy;
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ cdda_priv *p = (cdda_priv *)s->priv;
+ int sec;
+ int current_track = 0, seeked_track = 0;
+ int seek_to_track = 0;
+ int i;
+
+ newpos += p->start_sector * CDIO_CD_FRAMESIZE_RAW;
+
+ sec = newpos / CDIO_CD_FRAMESIZE_RAW;
+ if (newpos < 0 || sec > p->end_sector) {
+ p->sector = p->end_sector + 1;
+ return 0;
+ }
+
+ for (i = 0; i < p->cd->tracks; i++) {
+ if (p->sector >= p->cd->disc_toc[i].dwStartSector
+ && p->sector < p->cd->disc_toc[i + 1].dwStartSector)
+ current_track = i;
+ if (sec >= p->cd->disc_toc[i].dwStartSector
+ && sec < p->cd->disc_toc[i + 1].dwStartSector)
+ {
+ seeked_track = i;
+ seek_to_track = sec == p->cd->disc_toc[i].dwStartSector;
+ }
+ }
+ if (current_track != seeked_track && !seek_to_track)
+ print_track_info(s, seeked_track + 1);
+
+ p->sector = sec;
+
+ paranoia_seek(p->cdp, sec, SEEK_SET);
+ return 1;
+}
+
+static void close_cdda(stream_t *s)
+{
+ cdda_priv *p = (cdda_priv *)s->priv;
+ paranoia_free(p->cdp);
+ cdda_close(p->cd);
+}
+
+static int get_track_by_sector(cdda_priv *p, unsigned int sector)
+{
+ int i;
+ for (i = p->cd->tracks; i >= 0; --i)
+ if (p->cd->disc_toc[i].dwStartSector <= sector)
+ break;
+ return i;
+}
+
+static int control(stream_t *stream, int cmd, void *arg)
+{
+ cdda_priv *p = stream->priv;
+ switch (cmd) {
+ case STREAM_CTRL_GET_NUM_CHAPTERS:
+ {
+ int start_track = get_track_by_sector(p, p->start_sector);
+ int end_track = get_track_by_sector(p, p->end_sector);
+ if (start_track == -1 || end_track == -1)
+ return STREAM_ERROR;
+ *(unsigned int *)arg = end_track + 1 - start_track;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CHAPTER_TIME:
+ {
+ int track = *(double *)arg;
+ int start_track = get_track_by_sector(p, p->start_sector);
+ int end_track = get_track_by_sector(p, p->end_sector);
+ track += start_track;
+ if (track > end_track)
+ return STREAM_ERROR;
+ int64_t sector = p->cd->disc_toc[track].dwStartSector;
+ int64_t pos = sector * CDIO_CD_FRAMESIZE_RAW;
+ // Assume standard audio CD: 44.1khz, 2 channels, s16 samples
+ *(double *)arg = pos / (44100.0 * 2 * 2);
+ return STREAM_OK;
+ }
+ }
+ return STREAM_UNSUPPORTED;
+}
+
+static int64_t get_size(stream_t *st)
+{
+ cdda_priv *p = st->priv;
+ return (p->end_sector + 1 - p->start_sector) * CDIO_CD_FRAMESIZE_RAW;
+}
+
+static int open_cdda(stream_t *st)
+{
+ st->priv = mp_get_config_group(st, st->global, &stream_cdda_conf);
+ cdda_priv *priv = st->priv;
+ cdda_priv *p = priv;
+ int mode = p->paranoia_mode;
+ int offset = p->toc_offset;
+ cdrom_drive_t *cdd = NULL;
+ int last_track;
+
+ if (st->path[0]) {
+ p->device = st->path;
+ } else if (p->cdda_device && p->cdda_device[0]) {
+ p->device = p->cdda_device;
+ } else {
+ p->device = DEFAULT_CDROM_DEVICE;
+ }
+
+#if defined(__NetBSD__)
+ cdd = cdda_identify_scsi(p->device, p->device, 0, NULL);
+#else
+ cdd = cdda_identify(p->device, 0, NULL);
+#endif
+
+ if (!cdd) {
+ MP_ERR(st, "Can't open CDDA device.\n");
+ return STREAM_ERROR;
+ }
+
+ cdda_verbose_set(cdd, CDDA_MESSAGE_FORGETIT, CDDA_MESSAGE_FORGETIT);
+
+ if (p->sector_size)
+ cdd->nsectors = p->sector_size;
+
+ if (cdda_open(cdd) != 0) {
+ MP_ERR(st, "Can't open disc.\n");
+ cdda_close(cdd);
+ return STREAM_ERROR;
+ }
+
+ priv->cd = cdd;
+
+ offset -= cdda_track_firstsector(cdd, 1);
+ if (offset) {
+ for (int n = 0; n < cdd->tracks + 1; n++)
+ cdd->disc_toc[n].dwStartSector += offset;
+ }
+
+ if (p->speed > 0)
+ cdda_speed_set(cdd, p->speed);
+
+ last_track = cdda_tracks(cdd);
+ if (p->span[0] > last_track)
+ p->span[0] = last_track;
+ if (p->span[1] < p->span[0])
+ p->span[1] = p->span[0];
+ if (p->span[1] > last_track)
+ p->span[1] = last_track;
+ if (p->span[0])
+ priv->start_sector = cdda_track_firstsector(cdd, p->span[0]);
+ else
+ priv->start_sector = cdda_disc_firstsector(cdd);
+
+ if (p->span[1])
+ priv->end_sector = cdda_track_lastsector(cdd, p->span[1]);
+ else
+ priv->end_sector = cdda_disc_lastsector(cdd);
+
+ priv->cdp = paranoia_init(cdd);
+ if (priv->cdp == NULL) {
+ cdda_close(cdd);
+ free(priv);
+ return STREAM_ERROR;
+ }
+
+ if (mode == 0)
+ mode = PARANOIA_MODE_DISABLE;
+ else if (mode == 1)
+ mode = PARANOIA_MODE_OVERLAP;
+ else
+ mode = PARANOIA_MODE_FULL;
+
+ if (p->skip)
+ mode &= ~PARANOIA_MODE_NEVERSKIP;
+ else
+ mode |= PARANOIA_MODE_NEVERSKIP;
+
+ if (p->search_overlap > 0)
+ mode |= PARANOIA_MODE_OVERLAP;
+ else if (p->search_overlap == 0)
+ mode &= ~PARANOIA_MODE_OVERLAP;
+
+ paranoia_modeset(priv->cdp, mode);
+
+ if (p->search_overlap > 0)
+ paranoia_overlapset(priv->cdp, p->search_overlap);
+
+ paranoia_seek(priv->cdp, priv->start_sector, SEEK_SET);
+ priv->sector = priv->start_sector;
+
+ st->priv = priv;
+
+ st->fill_buffer = fill_buffer;
+ st->seek = seek;
+ st->seekable = true;
+ st->control = control;
+ st->get_size = get_size;
+ st->close = close_cdda;
+
+ st->streaming = true;
+
+ st->demuxer = "+disc";
+
+ print_cdtext(st, 0);
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_cdda = {
+ .name = "cdda",
+ .open = open_cdda,
+ .protocols = (const char*const[]){"cdda", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_concat.c b/stream/stream_concat.c
new file mode 100644
index 0000000..d06bd4a
--- /dev/null
+++ b/stream/stream_concat.c
@@ -0,0 +1,179 @@
+/*
+ * 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 <libavutil/common.h>
+
+#include "common/common.h"
+#include "stream.h"
+
+struct priv {
+ struct stream **streams;
+ int num_streams;
+
+ int64_t size;
+
+ int cur; // streams[cur] is the stream for current stream.pos
+};
+
+static int fill_buffer(struct stream *s, void *buffer, int len)
+{
+ struct priv *p = s->priv;
+
+ while (1) {
+ int res = stream_read_partial(p->streams[p->cur], buffer, len);
+ if (res || p->cur == p->num_streams - 1)
+ return res;
+
+ p->cur += 1;
+ if (s->seekable)
+ stream_seek(p->streams[p->cur], 0);
+ }
+}
+
+static int seek(struct stream *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+
+ int64_t next_pos = 0;
+ int64_t base_pos = 0;
+
+ // Look for the last stream whose start position is <= newpos.
+ // Note that the last stream's size is essentially ignored. The last
+ // stream is allowed to have an unknown size.
+ for (int n = 0; n < p->num_streams; n++) {
+ if (next_pos > newpos)
+ break;
+
+ base_pos = next_pos;
+ p->cur = n;
+
+ int64_t size = stream_get_size(p->streams[n]);
+ if (size < 0)
+ break;
+
+ next_pos = base_pos + size;
+ }
+
+ int ok = stream_seek(p->streams[p->cur], newpos - base_pos);
+ s->pos = base_pos + stream_tell(p->streams[p->cur]);
+ return ok;
+}
+
+static int64_t get_size(struct stream *s)
+{
+ struct priv *p = s->priv;
+ return p->size;
+}
+
+static void s_close(struct stream *s)
+{
+ struct priv *p = s->priv;
+
+ for (int n = 0; n < p->num_streams; n++)
+ free_stream(p->streams[n]);
+}
+
+// Return the "worst" origin value of the two. cur can be 0 to mean unset.
+static int combine_origin(int cur, int new)
+{
+ if (cur == STREAM_ORIGIN_UNSAFE || new == STREAM_ORIGIN_UNSAFE)
+ return STREAM_ORIGIN_UNSAFE;
+ if (cur == STREAM_ORIGIN_NET || new == STREAM_ORIGIN_NET)
+ return STREAM_ORIGIN_NET;
+ if (cur == STREAM_ORIGIN_FS || new == STREAM_ORIGIN_FS)
+ return STREAM_ORIGIN_FS;
+ return new; // including cur==0
+}
+
+static int open2(struct stream *stream, const struct stream_open_args *args)
+{
+ struct priv *p = talloc_zero(stream, struct priv);
+ stream->priv = p;
+
+ stream->fill_buffer = fill_buffer;
+ stream->get_size = get_size;
+ stream->close = s_close;
+
+ stream->seekable = true;
+
+ struct priv *list = args->special_arg;
+ if (!list || !list->num_streams) {
+ MP_FATAL(stream, "No sub-streams.\n");
+ return STREAM_ERROR;
+ }
+
+ stream->stream_origin = 0;
+
+ for (int n = 0; n < list->num_streams; n++) {
+ struct stream *sub = list->streams[n];
+
+ int64_t size = stream_get_size(sub);
+ if (n != list->num_streams - 1 && size < 0) {
+ MP_WARN(stream, "Sub stream %d has unknown size.\n", n);
+ stream->seekable = false;
+ p->size = -1;
+ } else if (size >= 0 && p->size >= 0) {
+ p->size += size;
+ }
+
+ if (!sub->seekable)
+ stream->seekable = false;
+
+ stream->stream_origin =
+ combine_origin(stream->stream_origin, sub->stream_origin);
+
+ MP_TARRAY_APPEND(p, p->streams, p->num_streams, sub);
+ }
+
+ if (stream->seekable)
+ stream->seek = seek;
+
+ return STREAM_OK;
+}
+
+static const stream_info_t stream_info_concat = {
+ .name = "concat",
+ .open2 = open2,
+ .protocols = (const char*const[]){ "concat", NULL },
+};
+
+// Create a stream with a concatenated view on streams[]. Copies the streams
+// array. Takes over ownership of every stream passed to it (it will free them
+// if the concat stream is closed).
+// If an error happens, NULL is returned, and the streams are not freed.
+struct stream *stream_concat_open(struct mpv_global *global, struct mp_cancel *c,
+ struct stream **streams, int num_streams)
+{
+ // (struct priv is blatantly abused to pass in the stream list)
+ struct priv arg = {
+ .streams = streams,
+ .num_streams = num_streams,
+ };
+
+ struct stream_open_args sargs = {
+ .global = global,
+ .cancel = c,
+ .url = "concat://",
+ .flags = STREAM_READ | STREAM_SILENT | STREAM_ORIGIN_DIRECT,
+ .sinfo = &stream_info_concat,
+ .special_arg = &arg,
+ };
+
+ struct stream *s = NULL;
+ stream_create_with_args(&sargs, &s);
+ return s;
+}
diff --git a/stream/stream_dvb.c b/stream/stream_dvb.c
new file mode 100644
index 0000000..215e82c
--- /dev/null
+++ b/stream/stream_dvb.c
@@ -0,0 +1,1161 @@
+/*
+
+ dvbstream
+ (C) Dave Chapman <dave@dchapman.com> 2001, 2002.
+ (C) Rozhuk Ivan <rozhuk.im@gmail.com> 2016 - 2017
+
+ Original authors: Nico, probably Arpi
+
+ Some code based on dvbstream, 0.4.3-pre3 (CVS checkout),
+ http://sourceforge.net/projects/dvbtools/
+
+ Modified for use with MPlayer, for details see the changelog at
+ http://svn.mplayerhq.hu/mplayer/trunk/
+ $Id$
+
+ Copyright notice:
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program 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 General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+*/
+
+#include "config.h"
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/ioctl.h>
+#include <sys/time.h>
+#include <poll.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <errno.h>
+
+#include "osdep/io.h"
+#include "misc/ctype.h"
+#include "osdep/timer.h"
+
+#include "stream.h"
+#include "common/tags.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+#include "options/options.h"
+#include "options/path.h"
+#include "osdep/threads.h"
+
+#include "dvbin.h"
+#include "dvb_tune.h"
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#define CHANNEL_LINE_LEN 256
+
+#define OPT_BASE_STRUCT dvb_opts_t
+
+static dvb_state_t *global_dvb_state = NULL;
+static mp_static_mutex global_dvb_state_lock = MP_STATIC_MUTEX_INITIALIZER;
+
+const struct m_sub_options stream_dvb_conf = {
+ .opts = (const m_option_t[]) {
+ {"prog", OPT_STRING(cfg_prog), .flags = UPDATE_DVB_PROG},
+ {"card", OPT_INT(cfg_devno), M_RANGE(0, MAX_ADAPTERS-1)},
+ {"timeout", OPT_INT(cfg_timeout), M_RANGE(1, 30)},
+ {"file", OPT_STRING(cfg_file), .flags = M_OPT_FILE},
+ {"full-transponder", OPT_BOOL(cfg_full_transponder)},
+ {"channel-switch-offset", OPT_INT(cfg_channel_switch_offset),
+ .flags = UPDATE_DVB_PROG},
+ {0}
+ },
+ .size = sizeof(dvb_opts_t),
+ .defaults = &(const dvb_opts_t){
+ .cfg_prog = NULL,
+ .cfg_timeout = 30,
+ },
+};
+
+void dvbin_close(stream_t *stream);
+
+static fe_code_rate_t parse_fec(const char *cr)
+{
+ if (!strcmp(cr, "FEC_1_2")) {
+ return FEC_1_2;
+ } else if (!strcmp(cr, "FEC_2_3")) {
+ return FEC_2_3;
+ } else if (!strcmp(cr, "FEC_3_4")) {
+ return FEC_3_4;
+ } else if (!strcmp(cr, "FEC_4_5")) {
+ return FEC_4_5;
+ } else if (!strcmp(cr, "FEC_5_6")) {
+ return FEC_5_6;
+ } else if (!strcmp(cr, "FEC_6_7")) {
+ return FEC_6_7;
+ } else if (!strcmp(cr, "FEC_7_8")) {
+ return FEC_7_8;
+ } else if (!strcmp(cr, "FEC_8_9")) {
+ return FEC_8_9;
+ } else if (!strcmp(cr, "FEC_NONE")) {
+ return FEC_NONE;
+ }
+ return FEC_NONE;
+}
+
+static fe_modulation_t parse_vdr_modulation(const char** modstring)
+{
+ const static struct { const char *s; fe_modulation_t v; } table[] = {
+ { "16", QAM_16 },
+ { "32", QAM_32 },
+ { "64", QAM_64 },
+ { "128", QAM_128 },
+ { "256", QAM_256 },
+ { "998", QAM_AUTO },
+ { "2", QPSK },
+ { "5", PSK_8 },
+ { "6", APSK_16 },
+ { "7", APSK_32 },
+ { "10", VSB_8 },
+ { "11", VSB_16 },
+ { "12", DQPSK },
+ };
+ for (int i = 0; i < MP_ARRAY_SIZE(table); i++) {
+ if (!strncmp(*modstring, table[i].s, strlen(table[i].s))) {
+ *modstring += strlen(table[i].s);
+ return table[i].v;
+ }
+ }
+ return QAM_AUTO;
+}
+
+static void parse_vdr_par_string(const char *vdr_par_str, dvb_channel_t *ptr)
+{
+ //FIXME: There is more information in this parameter string, especially related
+ // to non-DVB-S reception.
+ if (!vdr_par_str[0])
+ return;
+ const char *vdr_par = &vdr_par_str[0];
+ while (vdr_par && *vdr_par) {
+ switch (mp_toupper(*vdr_par)) {
+ case 'H':
+ ptr->pol = 'H';
+ vdr_par++;
+ break;
+ case 'V':
+ ptr->pol = 'V';
+ vdr_par++;
+ break;
+ case 'S':
+ vdr_par++;
+ ptr->is_dvb_x2 = *vdr_par == '1';
+ vdr_par++;
+ break;
+ case 'P':
+ vdr_par++;
+ char *endptr = NULL;
+ errno = 0;
+ int n = strtol(vdr_par, &endptr, 10);
+ if (!errno && endptr != vdr_par) {
+ ptr->stream_id = n;
+ vdr_par = endptr;
+ }
+ break;
+ case 'I':
+ vdr_par++;
+ ptr->inv = (*vdr_par == '1') ? INVERSION_ON : INVERSION_OFF;
+ vdr_par++;
+ break;
+ case 'M':
+ vdr_par++;
+ ptr->mod = parse_vdr_modulation(&vdr_par);
+ break;
+ default:
+ vdr_par++;
+ }
+ }
+}
+
+static char *dvb_strtok_r(char *s, const char *sep, char **p)
+{
+ if (!s && !(s = *p))
+ return NULL;
+
+ /* Skip leading separators. */
+ s += strspn(s, sep);
+
+ /* s points at first non-separator, or end of string. */
+ if (!*s)
+ return *p = 0;
+
+ /* Move *p to next separator. */
+ *p = s + strcspn(s, sep);
+ if (**p) {
+ *(*p)++ = 0;
+ } else {
+ *p = 0;
+ }
+ return s;
+}
+
+static bool parse_pid_string(struct mp_log *log, char *pid_string,
+ dvb_channel_t *ptr)
+{
+ if (!pid_string[0])
+ return false;
+ int pcnt = 0;
+ /* These tokens also catch vdr-style PID lists.
+ * They can contain 123=deu@3,124=eng+jap@4;125
+ * 3 and 4 are codes for codec type, =langLeft+langRight is allowed,
+ * and ; may separate a dolby channel.
+ * With the numChars-test and the full token-list, all is handled
+ * gracefully.
+ */
+ const char *tokens = "+,;";
+ char *pidPart;
+ char *savePtr = NULL;
+ pidPart = dvb_strtok_r(pid_string, tokens, &savePtr);
+ while (pidPart != NULL) {
+ if (ptr->pids_cnt >= DMX_FILTER_SIZE - 1) {
+ mp_verbose(log, "Maximum number of PIDs for one channel "
+ "reached, ignoring further ones!\n");
+ break;
+ }
+ int numChars = 0;
+ int pid = 0;
+ pcnt += sscanf(pidPart, "%d%n", &pid, &numChars);
+ if (numChars > 0) {
+ ptr->pids[ptr->pids_cnt] = pid;
+ ptr->pids_cnt++;
+ }
+ pidPart = dvb_strtok_r(NULL, tokens, &savePtr);
+ }
+ return pcnt > 0;
+}
+
+static dvb_channels_list_t *dvb_get_channels(struct mp_log *log,
+ dvb_channels_list_t *list_add,
+ int cfg_full_transponder,
+ char *filename,
+ unsigned int frontend,
+ int delsys, unsigned int delsys_mask)
+{
+ dvb_channels_list_t *list = list_add;
+
+ if (!filename)
+ return list;
+
+ const char *cbl_conf =
+ "%d:%255[^:]:%d:%255[^:]:%255[^:]:%255[^:]:%255[^:]\n";
+ const char *sat_conf = "%d:%c:%d:%d:%255[^:]:%255[^:]\n";
+ const char *ter_conf =
+ "%d:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]:%255[^:]\n";
+ const char *atsc_conf = "%d:%255[^:]:%255[^:]:%255[^:]\n";
+ const char *vdr_conf =
+ "%d:%255[^:]:%255[^:]:%d:%255[^:]:%255[^:]:%255[^:]:%*255[^:]:%d:%*d:%*d:%*d\n%n";
+
+ mp_verbose(log, "Reading config file %s for type %s\n",
+ filename, get_dvb_delsys(delsys));
+ FILE *f = fopen(filename, "r");
+ if (!f) {
+ mp_fatal(log, "Can't open file %s\n", filename);
+ return list;
+ }
+
+ if (!list)
+ list = talloc_zero(NULL, dvb_channels_list_t);
+
+ while (!feof(f)) {
+ char line[CHANNEL_LINE_LEN];
+ if (!fgets(line, CHANNEL_LINE_LEN, f))
+ continue;
+
+ if (line[0] == '#' || strlen(line) == 0)
+ continue;
+
+ dvb_channel_t chn = {0};
+ dvb_channel_t *ptr = &chn;
+
+ char tmp_lcr[256], tmp_hier[256], inv[256], bw[256], cr[256], mod[256],
+ transm[256], gi[256], vpid_str[256], apid_str[256], tpid_str[256],
+ vdr_par_str[256], vdr_loc_str[256];
+
+ vpid_str[0] = apid_str[0] = tpid_str[0] = 0;
+ vdr_loc_str[0] = vdr_par_str[0] = 0;
+
+ char *colon = strchr(line, ':');
+ if (!colon)
+ continue;
+ int k = colon - line;
+ if (!k)
+ continue;
+ // In some modern VDR-style configs, channel name also has bouquet after ;.
+ // Parse that off, we ignore it.
+ char *bouquet_sep = strchr(line, ';');
+ {
+ int namelen = k;
+ if (bouquet_sep && bouquet_sep < colon)
+ namelen = bouquet_sep - line;
+ ptr->name = talloc_strndup(list, line, namelen);
+ }
+
+ k++;
+
+ ptr->pids_cnt = 0;
+ ptr->freq = 0;
+ ptr->service_id = -1;
+ ptr->is_dvb_x2 = false;
+ ptr->frontend = frontend;
+ ptr->delsys = delsys;
+ ptr->diseqc = 0;
+ ptr->stream_id = NO_STREAM_ID_FILTER;
+ ptr->inv = INVERSION_AUTO;
+ ptr->bw = BANDWIDTH_AUTO;
+ ptr->cr = FEC_AUTO;
+ ptr->cr_lp = FEC_AUTO;
+ ptr->mod = QAM_AUTO;
+ ptr->hier = HIERARCHY_AUTO;
+ ptr->gi = GUARD_INTERVAL_AUTO;
+ ptr->trans = TRANSMISSION_MODE_AUTO;
+
+ // Check if VDR-type channels.conf-line - then full line is consumed by the scan.
+ int fields, num_chars = 0;
+ fields = sscanf(&line[k], vdr_conf,
+ &ptr->freq, vdr_par_str, vdr_loc_str, &ptr->srate,
+ vpid_str, apid_str, tpid_str, &ptr->service_id,
+ &num_chars);
+
+ bool is_vdr_conf = (num_chars == strlen(&line[k]));
+
+ // Special case: DVB-T style ZAP config also has 13 columns.
+ // Most columns should have non-numeric content, but some channel list generators insert 0
+ // if a value is not used. However, INVERSION_* should always set after frequency.
+ if (is_vdr_conf && !strncmp(vdr_par_str, "INVERSION_", 10)) {
+ is_vdr_conf = false;
+ }
+
+ if (is_vdr_conf) {
+ // Modulation parsed here, not via old xine-parsing path.
+ mod[0] = '\0';
+ // It's a VDR-style config line.
+ parse_vdr_par_string(vdr_par_str, ptr);
+ // Frequency in VDR-style config files is in MHz for DVB-S,
+ // and may be in MHz, kHz or Hz for DVB-C and DVB-T.
+ // General rule to get useful units is to multiply by 1000 until value is larger than 1000000.
+ while (ptr->freq < 1000000U) {
+ ptr->freq *= 1000U;
+ }
+ // Symbol rate in VDR-style config files is divided by 1000.
+ ptr->srate *= 1000U;
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ /* Fix delsys value. */
+ if (ptr->is_dvb_x2) {
+ ptr->delsys = delsys = SYS_DVBT2;
+ } else {
+ ptr->delsys = delsys = SYS_DVBT;
+ }
+ if (!DELSYS_IS_SET(delsys_mask, delsys))
+ continue; /* Skip channel. */
+ mp_verbose(log, "VDR, %s, NUM: %d, NUM_FIELDS: %d, NAME: %s, "
+ "FREQ: %d, SRATE: %d, T2: %s\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS, fields,
+ ptr->name, ptr->freq, ptr->srate,
+ (delsys == SYS_DVBT2) ? "yes" : "no");
+ break;
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ case SYS_ISDBT:
+ mp_verbose(log, "VDR, %s, NUM: %d, NUM_FIELDS: %d, NAME: %s, "
+ "FREQ: %d, SRATE: %d\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS, fields,
+ ptr->name, ptr->freq, ptr->srate);
+ break;
+ case SYS_DVBS:
+ case SYS_DVBS2:
+ /* Fix delsys value. */
+ if (ptr->is_dvb_x2) {
+ ptr->delsys = delsys = SYS_DVBS2;
+ } else {
+ ptr->delsys = delsys = SYS_DVBS;
+ }
+ if (!DELSYS_IS_SET(delsys_mask, delsys))
+ continue; /* Skip channel. */
+
+ if (vdr_loc_str[0]) {
+ // In older vdr config format, this field contained the DISEQc information.
+ // If it is numeric, assume that's it.
+ int diseqc_info = 0;
+ int valid_digits = 0;
+ if (sscanf(vdr_loc_str, "%d%n", &diseqc_info,
+ &valid_digits) == 1)
+ {
+ if (valid_digits == strlen(vdr_loc_str)) {
+ ptr->diseqc = (unsigned int)diseqc_info;
+ if (ptr->diseqc > 4)
+ continue;
+ if (ptr->diseqc > 0)
+ ptr->diseqc--;
+ }
+ }
+ }
+
+ mp_verbose(log, "VDR, %s, NUM: %d, NUM_FIELDS: %d, NAME: %s, "
+ "FREQ: %d, SRATE: %d, POL: %c, DISEQC: %d, S2: %s, "
+ "StreamID: %d, SID: %d\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS,
+ fields, ptr->name, ptr->freq, ptr->srate, ptr->pol,
+ ptr->diseqc, (delsys == SYS_DVBS2) ? "yes" : "no",
+ ptr->stream_id, ptr->service_id);
+ break;
+ default:
+ break;
+ }
+ } else {
+ // ZAP style channel config file.
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ fields = sscanf(&line[k], ter_conf,
+ &ptr->freq, inv, bw, cr, tmp_lcr, mod,
+ transm, gi, tmp_hier, vpid_str, apid_str);
+ mp_verbose(log, "%s, NUM: %d, NUM_FIELDS: %d, NAME: %s, FREQ: %d\n",
+ get_dvb_delsys(delsys), list->NUM_CHANNELS,
+ fields, ptr->name, ptr->freq);
+ break;
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ fields = sscanf(&line[k], cbl_conf,
+ &ptr->freq, inv, &ptr->srate,
+ cr, mod, vpid_str, apid_str);
+ mp_verbose(log, "%s, NUM: %d, NUM_FIELDS: %d, NAME: %s, FREQ: %d, "
+ "SRATE: %d\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS, fields, ptr->name,
+ ptr->freq, ptr->srate);
+ break;
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ fields = sscanf(&line[k], atsc_conf,
+ &ptr->freq, mod, vpid_str, apid_str);
+ mp_verbose(log, "%s, NUM: %d, NUM_FIELDS: %d, NAME: %s, FREQ: %d\n",
+ get_dvb_delsys(delsys), list->NUM_CHANNELS,
+ fields, ptr->name, ptr->freq);
+ break;
+ case SYS_DVBS:
+ case SYS_DVBS2:
+ fields = sscanf(&line[k], sat_conf,
+ &ptr->freq, &ptr->pol, &ptr->diseqc, &ptr->srate,
+ vpid_str,
+ apid_str);
+ ptr->pol = mp_toupper(ptr->pol);
+ ptr->freq *= 1000U;
+ ptr->srate *= 1000U;
+ if (ptr->diseqc > 4)
+ continue;
+ if (ptr->diseqc > 0)
+ ptr->diseqc--;
+ mp_verbose(log, "%s, NUM: %d, NUM_FIELDS: %d, NAME: %s, FREQ: %d, "
+ "SRATE: %d, POL: %c, DISEQC: %d\n",
+ get_dvb_delsys(delsys),
+ list->NUM_CHANNELS, fields, ptr->name, ptr->freq,
+ ptr->srate, ptr->pol, ptr->diseqc);
+ break;
+ default:
+ break;
+ }
+ }
+
+ if (parse_pid_string(log, vpid_str, ptr))
+ fields++;
+ if (parse_pid_string(log, apid_str, ptr))
+ fields++;
+ /* If we do not know the service_id, PMT can not be extracted.
+ Teletext decoding will fail without PMT. */
+ if (ptr->service_id != -1) {
+ if (parse_pid_string(log, tpid_str, ptr))
+ fields++;
+ }
+
+
+ if (fields < 2 || ptr->pids_cnt == 0 || ptr->freq == 0 ||
+ strlen(ptr->name) == 0)
+ continue;
+
+ /* Add some PIDs which are mandatory in DVB,
+ * and contain human-readable helpful data. */
+
+ /* This is the STD, the service description table.
+ * It contains service names and such, ffmpeg decodes it. */
+ ptr->pids[ptr->pids_cnt] = 0x0011;
+ ptr->pids_cnt++;
+
+ /* This is the EIT, which contains EPG data.
+ * ffmpeg can not decode it (yet), but e.g. VLC
+ * shows what was recorded. */
+ ptr->pids[ptr->pids_cnt] = 0x0012;
+ ptr->pids_cnt++;
+
+ if (ptr->service_id != -1) {
+ /* We have the PMT-PID in addition.
+ This will be found later, when we tune to the channel.
+ Push back here to create the additional demux. */
+ ptr->pids[ptr->pids_cnt] = -1; // Placeholder.
+ ptr->pids_cnt++;
+ }
+
+ bool has8192 = false, has0 = false;
+ for (int cnt = 0; cnt < ptr->pids_cnt; cnt++) {
+ if (ptr->pids[cnt] == 8192)
+ has8192 = true;
+ if (ptr->pids[cnt] == 0)
+ has0 = true;
+ }
+
+ /* 8192 is the pseudo-PID for full TP dump,
+ enforce that if requested. */
+ if (!has8192 && cfg_full_transponder)
+ has8192 = 1;
+ if (has8192) {
+ ptr->pids[0] = 8192;
+ ptr->pids_cnt = 1;
+ } else if (!has0) {
+ ptr->pids[ptr->pids_cnt] = 0; //PID 0 is the PAT
+ ptr->pids_cnt++;
+ }
+
+ mp_verbose(log, " PIDS: ");
+ for (int cnt = 0; cnt < ptr->pids_cnt; cnt++)
+ mp_verbose(log, " %d ", ptr->pids[cnt]);
+ mp_verbose(log, "\n");
+
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ if (!strcmp(inv, "INVERSION_ON")) {
+ ptr->inv = INVERSION_ON;
+ } else if (!strcmp(inv, "INVERSION_OFF")) {
+ ptr->inv = INVERSION_OFF;
+ }
+
+ ptr->cr = parse_fec(cr);
+ }
+
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ case SYS_ATSC:
+ case SYS_DVBC_ANNEX_B:
+ if (!strcmp(mod, "QAM_128")) {
+ ptr->mod = QAM_128;
+ } else if (!strcmp(mod, "QAM_256")) {
+ ptr->mod = QAM_256;
+ } else if (!strcmp(mod, "QAM_64")) {
+ ptr->mod = QAM_64;
+ } else if (!strcmp(mod, "QAM_32")) {
+ ptr->mod = QAM_32;
+ } else if (!strcmp(mod, "QAM_16")) {
+ ptr->mod = QAM_16;
+ } else if (!strcmp(mod, "VSB_8") || !strcmp(mod, "8VSB")) {
+ ptr->mod = VSB_8;
+ } else if (!strcmp(mod, "VSB_16") || !strcmp(mod, "16VSB")) {
+ ptr->mod = VSB_16;
+ }
+ }
+
+ /* Modulation defines real delsys for ATSC:
+ Terrestrial (VSB) is SYS_ATSC, Cable (QAM) is SYS_DVBC_ANNEX_B. */
+ if (delsys == SYS_ATSC || delsys == SYS_DVBC_ANNEX_B) {
+ if (ptr->mod == VSB_8 || ptr->mod == VSB_16) {
+ delsys = SYS_ATSC;
+ } else {
+ delsys = SYS_DVBC_ANNEX_B;
+ }
+ if (!DELSYS_IS_SET(delsys_mask, delsys))
+ continue; /* Skip channel. */
+ mp_verbose(log, "Switched to delivery system for ATSC: %s (guessed from modulation)\n",
+ get_dvb_delsys(delsys));
+ }
+
+ switch (delsys) {
+ case SYS_DVBT:
+ case SYS_DVBT2:
+ case SYS_ISDBT:
+ if (!strcmp(bw, "BANDWIDTH_5_MHZ")) {
+ ptr->bw = BANDWIDTH_5_MHZ;
+ } else if (!strcmp(bw, "BANDWIDTH_6_MHZ")) {
+ ptr->bw = BANDWIDTH_6_MHZ;
+ } else if (!strcmp(bw, "BANDWIDTH_7_MHZ")) {
+ ptr->bw = BANDWIDTH_7_MHZ;
+ } else if (!strcmp(bw, "BANDWIDTH_8_MHZ")) {
+ ptr->bw = BANDWIDTH_8_MHZ;
+ } else if (!strcmp(bw, "BANDWIDTH_10_MHZ")) {
+ ptr->bw = BANDWIDTH_10_MHZ;
+ }
+
+
+ if (!strcmp(transm, "TRANSMISSION_MODE_2K")) {
+ ptr->trans = TRANSMISSION_MODE_2K;
+ } else if (!strcmp(transm, "TRANSMISSION_MODE_8K")) {
+ ptr->trans = TRANSMISSION_MODE_8K;
+ } else if (!strcmp(transm, "TRANSMISSION_MODE_AUTO")) {
+ ptr->trans = TRANSMISSION_MODE_AUTO;
+ }
+
+ if (!strcmp(gi, "GUARD_INTERVAL_1_32")) {
+ ptr->gi = GUARD_INTERVAL_1_32;
+ } else if (!strcmp(gi, "GUARD_INTERVAL_1_16")) {
+ ptr->gi = GUARD_INTERVAL_1_16;
+ } else if (!strcmp(gi, "GUARD_INTERVAL_1_8")) {
+ ptr->gi = GUARD_INTERVAL_1_8;
+ } else if (!strcmp(gi, "GUARD_INTERVAL_1_4")) {
+ ptr->gi = GUARD_INTERVAL_1_4;
+ }
+
+ ptr->cr_lp = parse_fec(tmp_lcr);
+
+ if (!strcmp(tmp_hier, "HIERARCHY_1")) {
+ ptr->hier = HIERARCHY_1;
+ } else if (!strcmp(tmp_hier, "HIERARCHY_2")) {
+ ptr->hier = HIERARCHY_2;
+ } else if (!strcmp(tmp_hier, "HIERARCHY_4")) {
+ ptr->hier = HIERARCHY_4;
+ } else if (!strcmp(tmp_hier, "HIERARCHY_NONE")) {
+ ptr->hier = HIERARCHY_NONE;
+ }
+ }
+
+ MP_TARRAY_APPEND(list, list->channels, list->NUM_CHANNELS, *ptr);
+ }
+
+ fclose(f);
+ if (list->NUM_CHANNELS == 0)
+ TA_FREEP(&list);
+
+ return list;
+}
+
+static int dvb_streaming_read(stream_t *stream, void *buffer, int size)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+ int pos = 0;
+ int tries = state->retry;
+ const int fd = state->dvr_fd;
+
+ MP_TRACE(stream, "dvb_streaming_read(%d)\n", size);
+
+ struct pollfd pfds[1];
+ pfds[0].fd = fd;
+ pfds[0].events = POLLIN | POLLPRI;
+
+ while (pos < size) {
+ int rk = read(fd, (char *)buffer + pos, (size - pos));
+ if (rk <= 0) {
+ if (pos || tries == 0)
+ break;
+ tries--;
+ if (poll(pfds, 1, 2000) <= 0) {
+ MP_ERR(stream, "dvb_streaming_read: failed with "
+ "errno %d when reading %d bytes\n", errno, size - pos);
+ errno = 0;
+ break;
+ }
+ continue;
+ }
+ pos += rk;
+ MP_TRACE(stream, "got %d bytes\n", pos);
+ }
+
+ if (!pos)
+ MP_ERR(stream, "dvb_streaming_read: returning 0 bytes\n");
+
+ // Check if config parameters have been updated.
+ dvb_update_config(stream);
+
+ return pos;
+}
+
+int dvb_set_channel(stream_t *stream, unsigned int adapter, unsigned int n)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ assert(adapter < state->adapters_count);
+ int devno = state->adapters[adapter].devno;
+ dvb_channels_list_t *new_list = state->adapters[adapter].list;
+ assert(n < new_list->NUM_CHANNELS);
+ dvb_channel_t *channel = &(new_list->channels[n]);
+
+ if (state->is_on) { //the fds are already open and we have to stop the demuxers
+ /* Remove all demuxes. */
+ dvb_fix_demuxes(priv, 0);
+
+ state->retry = 0;
+ // empty both the stream's and driver's buffer
+ char buf[4096];
+ while (dvb_streaming_read(stream, buf, sizeof(buf)) > 0) {}
+
+ if (state->cur_adapter != adapter ||
+ state->cur_frontend != channel->frontend) {
+ dvbin_close(stream);
+ if (!dvb_open_devices(priv, devno, channel->frontend, channel->pids_cnt)) {
+ MP_ERR(stream, "dvb_set_channel: couldn't open devices of adapter "
+ "%d\n", devno);
+ return 0;
+ }
+ } else {
+ // close all demux_fds with pos > pids required for the new channel
+ // or open other demux_fds if we have too few
+ if (!dvb_fix_demuxes(priv, channel->pids_cnt))
+ return 0;
+ }
+ } else {
+ if (!dvb_open_devices(priv, devno, channel->frontend, channel->pids_cnt)) {
+ MP_ERR(stream, "dvb_set_channel: couldn't open devices of adapter "
+ "%d\n", devno);
+ return 0;
+ }
+ }
+
+ state->retry = 5;
+ new_list->current = n;
+ MP_VERBOSE(stream, "dvb_set_channel: new channel name=\"%s\", adapter: %d, "
+ "channel: %d\n", channel->name, devno, n);
+
+ if (channel->freq != state->last_freq) {
+ if (!dvb_tune(priv, channel->delsys, channel->freq,
+ channel->pol, channel->srate, channel->diseqc,
+ channel->stream_id, channel->inv,
+ channel->mod, channel->gi,
+ channel->trans, channel->bw, channel->cr, channel->cr_lp,
+ channel->hier, priv->opts->cfg_timeout))
+ return 0;
+ }
+
+ state->is_on = true;
+ state->last_freq = channel->freq;
+ state->cur_adapter = adapter;
+ state->cur_frontend = channel->frontend;
+
+ if (channel->service_id != -1) {
+ /* We need the PMT-PID in addition.
+ If it has not yet beem resolved, do it now. */
+ for (int i = 0; i < channel->pids_cnt; i++) {
+ if (channel->pids[i] == -1) {
+ MP_VERBOSE(stream, "dvb_set_channel: PMT-PID for service %d "
+ "not resolved yet, parsing PAT...\n",
+ channel->service_id);
+ int pmt_pid = dvb_get_pmt_pid(priv, adapter, channel->service_id);
+ MP_VERBOSE(stream, "found PMT-PID: %d\n", pmt_pid);
+ channel->pids[i] = pmt_pid;
+ break;
+ }
+ }
+ }
+
+ // sets demux filters and restart the stream
+ for (int i = 0; i < channel->pids_cnt; i++) {
+ if (channel->pids[i] == -1) {
+ // In case PMT was not resolved, skip it here.
+ MP_ERR(stream, "dvb_set_channel: PMT-PID not found, "
+ "teletext decoding may fail.\n");
+ continue;
+ }
+ if (!dvb_set_ts_filt(priv, state->demux_fds[i], channel->pids[i],
+ DMX_PES_OTHER))
+ return 0;
+ }
+
+ return 1;
+}
+
+static int dvbin_stream_control(struct stream *s, int cmd, void *arg)
+{
+ dvb_priv_t *priv = s->priv;
+ dvb_state_t *state = priv->state;
+
+ if (state->cur_adapter >= state->adapters_count)
+ return STREAM_ERROR;
+ dvb_channels_list_t *list = state->adapters[state->cur_adapter].list;
+
+ switch (cmd) {
+ case STREAM_CTRL_GET_METADATA: {
+ struct mp_tags *metadata = talloc_zero(NULL, struct mp_tags);
+ char *progname = list->channels[list->current].name;
+ mp_tags_set_str(metadata, "title", progname);
+ *(struct mp_tags **)arg = metadata;
+ return STREAM_OK;
+ }
+ }
+ return STREAM_UNSUPPORTED;
+}
+
+void dvbin_close(stream_t *stream)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ if (state->switching_channel && state->is_on) {
+ // Prevent state destruction, reset channel-switch.
+ state->switching_channel = false;
+ mp_mutex_lock(&global_dvb_state_lock);
+ global_dvb_state->stream_used = false;
+ mp_mutex_unlock(&global_dvb_state_lock);
+ return;
+ }
+
+ for (int i = state->demux_fds_cnt - 1; i >= 0; i--) {
+ state->demux_fds_cnt--;
+ close(state->demux_fds[i]);
+ }
+ close(state->dvr_fd);
+ close(state->fe_fd);
+ state->fe_fd = state->dvr_fd = -1;
+
+ state->is_on = false;
+ state->cur_adapter = -1;
+ state->cur_frontend = -1;
+
+ mp_mutex_lock(&global_dvb_state_lock);
+ TA_FREEP(&global_dvb_state);
+ mp_mutex_unlock(&global_dvb_state_lock);
+}
+
+static int dvb_streaming_start(stream_t *stream, char *progname)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ if (!progname)
+ return 0;
+
+ dvb_channels_list_t *list = state->adapters[state->cur_adapter].list;
+ dvb_channel_t *channel = NULL;
+ int i;
+ for (i = 0; i < list->NUM_CHANNELS; i ++) {
+ if (!strcmp(list->channels[i].name, progname)) {
+ channel = &(list->channels[i]);
+ break;
+ }
+ }
+
+ if (!channel) {
+ MP_ERR(stream, "no such channel \"%s\"\n", progname);
+ return 0;
+ }
+ list->current = i;
+
+ // When switching channels, cfg_channel_switch_offset
+ // keeps the offset to the initially chosen channel.
+ list->current = (list->NUM_CHANNELS + list->current +
+ priv->opts->cfg_channel_switch_offset) % list->NUM_CHANNELS;
+ channel = &(list->channels[list->current]);
+ MP_INFO(stream, "Tuning to channel \"%s\"...\n", channel->name);
+ MP_VERBOSE(stream, "Program number %d: name=\"%s\", freq=%u\n", i,
+ channel->name, channel->freq);
+
+ if (!dvb_set_channel(stream, state->cur_adapter, list->current)) {
+ dvbin_close(stream);
+ return 0;
+ }
+
+ return 1;
+}
+
+void dvb_update_config(stream_t *stream)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ // Throttle the check to at maximum once every 0.1 s.
+ int now = (int)(mp_time_sec()*10);
+ if (now == priv->opts_check_time)
+ return;
+ priv->opts_check_time = now;
+
+ if (!m_config_cache_update(priv->opts_cache))
+ return;
+
+ // Re-parse stream path, if we have cfg parameters now,
+ // these should be preferred.
+ if (!dvb_parse_path(stream)) {
+ MP_ERR(stream, "error parsing DVB config, not tuning.");
+ return;
+ }
+
+ int r = dvb_streaming_start(stream, priv->prog);
+ if (r) {
+ // Stream will be pulled down after channel switch,
+ // persist state.
+ state->switching_channel = true;
+ }
+}
+
+static int dvb_open(stream_t *stream)
+{
+ dvb_priv_t *priv = NULL;
+
+ mp_mutex_lock(&global_dvb_state_lock);
+ if (global_dvb_state && global_dvb_state->stream_used) {
+ MP_ERR(stream, "DVB stream already in use, only one DVB stream can exist at a time!\n");
+ mp_mutex_unlock(&global_dvb_state_lock);
+ goto err_out;
+ }
+
+ // Need to re-get config in any case, not part of global state.
+ stream->priv = talloc_zero(stream, dvb_priv_t);
+ priv = stream->priv;
+ priv->opts_cache = m_config_cache_alloc(stream, stream->global, &stream_dvb_conf);
+ priv->opts = priv->opts_cache->opts;
+
+ dvb_state_t *state = dvb_get_state(stream);
+
+ priv->state = state;
+ priv->log = stream->log;
+ if (!state) {
+ MP_ERR(stream, "DVB configuration is empty\n");
+ mp_mutex_unlock(&global_dvb_state_lock);
+ goto err_out;
+ }
+
+ if (!dvb_parse_path(stream)) {
+ mp_mutex_unlock(&global_dvb_state_lock);
+ goto err_out;
+ }
+
+ state->stream_used = true;
+ mp_mutex_unlock(&global_dvb_state_lock);
+
+ if (!state->is_on) {
+ // State could be already initialized, for example, we just did a channel switch.
+ // The following setup only has to be done once.
+
+ state->cur_frontend = -1;
+
+ if (!dvb_streaming_start(stream, priv->prog))
+ goto err_out;
+ }
+
+ stream->fill_buffer = dvb_streaming_read;
+ stream->close = dvbin_close;
+ stream->control = dvbin_stream_control;
+ stream->streaming = true;
+ stream->demuxer = "lavf";
+ stream->lavf_type = "mpegts";
+
+ return STREAM_OK;
+
+err_out:
+ talloc_free(priv);
+ stream->priv = NULL;
+ return STREAM_ERROR;
+}
+
+int dvb_parse_path(stream_t *stream)
+{
+ dvb_priv_t *priv = stream->priv;
+ dvb_state_t *state = priv->state;
+
+ // Parse stream path. Common rule: cfg wins over stream path,
+ // since cfg may be changed at runtime.
+ bstr prog, devno;
+ if (!bstr_split_tok(bstr0(stream->path), "@", &devno, &prog)) {
+ prog = devno;
+ devno.len = 0;
+ }
+
+ if (priv->opts->cfg_devno != 0) {
+ priv->devno = priv->opts->cfg_devno;
+ } else if (devno.len) {
+ bstr r;
+ priv->devno = bstrtoll(devno, &r, 0);
+ if (r.len || priv->devno < 0 || priv->devno >= MAX_ADAPTERS) {
+ MP_ERR(stream, "invalid devno: '%.*s'\n", BSTR_P(devno));
+ return 0;
+ }
+ } else {
+ // Default to the default of cfg_devno.
+ priv->devno = priv->opts->cfg_devno;
+ }
+
+ // Current adapter is derived from devno.
+ state->cur_adapter = -1;
+ for (int i = 0; i < state->adapters_count; i++) {
+ if (state->adapters[i].devno == priv->devno) {
+ state->cur_adapter = i;
+ break;
+ }
+ }
+
+ if (state->cur_adapter == -1) {
+ MP_ERR(stream, "No configuration found for adapter %d!\n",
+ priv->devno);
+ return 0;
+ }
+
+ char *new_prog = NULL;
+ if (priv->opts->cfg_prog != NULL && strlen(priv->opts->cfg_prog) > 0) {
+ new_prog = talloc_strdup(priv, priv->opts->cfg_prog);
+ } else if (prog.len) {
+ new_prog = bstrto0(priv, prog);
+ } else {
+ // We use the first program from the channel list.
+ dvb_channels_list_t *list = state->adapters[state->cur_adapter].list;
+ if (!list) {
+ MP_ERR(stream, "No channel list available for adapter %d!\n", priv->devno);
+ return 0;
+ }
+ new_prog = talloc_strdup(priv, list->channels[0].name);
+ }
+ talloc_free(priv->prog);
+ priv->prog = new_prog;
+
+ MP_VERBOSE(stream, "dvb_config: prog=\"%s\", devno=%d\n",
+ priv->prog, priv->devno);
+ return 1;
+}
+
+dvb_state_t *dvb_get_state(stream_t *stream)
+{
+ dvb_priv_t *priv = stream->priv;
+ if (global_dvb_state)
+ return global_dvb_state;
+
+ struct mp_log *log = stream->log;
+ struct mpv_global *global = stream->global;
+
+ dvb_state_t *state = talloc_zero(NULL, dvb_state_t);
+ state->switching_channel = false;
+ state->is_on = false;
+ state->stream_used = true;
+ state->fe_fd = state->dvr_fd = -1;
+
+ for (unsigned int i = 0; i < MAX_ADAPTERS; i++) {
+ dvb_channels_list_t *list = NULL;
+ unsigned int delsys_mask[MAX_FRONTENDS];
+ for (unsigned int f = 0; f < MAX_FRONTENDS; f++) {
+ char filename[100];
+ snprintf(filename, sizeof(filename), "/dev/dvb/adapter%u/frontend%u", i, f);
+ int fd = open(filename, O_RDONLY | O_NONBLOCK | O_CLOEXEC);
+ if (fd < 0)
+ continue;
+
+ delsys_mask[f] = dvb_get_tuner_delsys_mask(fd, log);
+ delsys_mask[f] &= DELSYS_SUPP_MASK; /* Filter unsupported delivery systems. */
+ close(fd);
+ if (delsys_mask[f] == 0) {
+ mp_verbose(log, "Frontend device %s has no supported delivery systems.\n",
+ filename);
+ continue; /* Skip tuner. */
+ }
+
+ /* Create channel list for adapter. */
+ for (unsigned int delsys = 0; delsys < SYS_DVB__COUNT__; delsys++) {
+ if (!DELSYS_IS_SET(delsys_mask[f], delsys))
+ continue; /* Skip unsupported. */
+
+ mp_verbose(log, "Searching channel list for delivery system %s\n", get_dvb_delsys(delsys));
+ const char *conf_file_name;
+ switch (delsys) {
+ case SYS_DVBC_ANNEX_A:
+ case SYS_DVBC_ANNEX_C:
+ conf_file_name = "channels.conf.cbl";
+ break;
+ case SYS_ATSC:
+ conf_file_name = "channels.conf.atsc";
+ break;
+ case SYS_DVBT:
+ if (DELSYS_IS_SET(delsys_mask[f], SYS_DVBT2))
+ continue; /* Add all channels later with T2. */
+ conf_file_name = "channels.conf.ter";
+ break;
+ case SYS_DVBT2:
+ conf_file_name = "channels.conf.ter";
+ break;
+ case SYS_ISDBT:
+ conf_file_name = "channels.conf.isdbt";
+ break;
+ case SYS_DVBS:
+ if (DELSYS_IS_SET(delsys_mask[f], SYS_DVBS2))
+ continue; /* Add all channels later with S2. */
+ conf_file_name = "channels.conf.sat";
+ break;
+ case SYS_DVBS2:
+ conf_file_name = "channels.conf.sat";
+ break;
+ default:
+ continue;
+ }
+
+ void *talloc_ctx = NULL;
+ char *conf_file;
+ if (priv->opts->cfg_file && priv->opts->cfg_file[0]) {
+ conf_file = priv->opts->cfg_file;
+ } else {
+ talloc_ctx = talloc_new(NULL);
+ conf_file = mp_find_config_file(talloc_ctx, global, conf_file_name);
+ if (conf_file) {
+ mp_verbose(log, "Ignoring other channels.conf files.\n");
+ } else {
+ conf_file = mp_find_config_file(talloc_ctx, global,
+ "channels.conf");
+ }
+ }
+
+ list = dvb_get_channels(log, list, priv->opts->cfg_full_transponder,
+ conf_file, f, delsys, delsys_mask[f]);
+ talloc_free(talloc_ctx);
+ }
+ }
+ /* Add adapter with non zero channel list. */
+ if (!list)
+ continue;
+
+ dvb_adapter_config_t tmp = {
+ .devno = i,
+ .list = talloc_steal(state, list),
+ };
+ memcpy(&tmp.delsys_mask, delsys_mask, sizeof(delsys_mask));
+
+ MP_TARRAY_APPEND(state, state->adapters, state->adapters_count, tmp);
+
+ mp_verbose(log, "Added adapter with channels to state list, now %d.\n",
+ state->adapters_count);
+ }
+
+ if (state->adapters_count == 0)
+ TA_FREEP(&state);
+
+ global_dvb_state = state;
+ return state;
+}
+
+const stream_info_t stream_info_dvb = {
+ .name = "dvbin",
+ .open = dvb_open,
+ .protocols = (const char *const[]){ "dvb", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_dvdnav.c b/stream/stream_dvdnav.c
new file mode 100644
index 0000000..d858c51
--- /dev/null
+++ b/stream/stream_dvdnav.c
@@ -0,0 +1,719 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "config.h"
+
+#if !HAVE_GPL
+#error GPL only
+#endif
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <string.h>
+#include <strings.h>
+#include <errno.h>
+#include <assert.h>
+
+#ifdef __linux__
+#include <linux/cdrom.h>
+#include <scsi/sg.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/ioctl.h>
+#endif
+
+#include <dvdnav/dvdnav.h>
+#include <libavutil/common.h>
+#include <libavutil/intreadwrite.h>
+
+#include "osdep/io.h"
+
+#include "options/options.h"
+#include "common/msg.h"
+#include "input/input.h"
+#include "options/m_config.h"
+#include "options/path.h"
+#include "osdep/timer.h"
+#include "stream.h"
+#include "demux/demux.h"
+#include "video/out/vo.h"
+
+#define TITLE_MENU -1
+#define TITLE_LONGEST -2
+
+struct priv {
+ dvdnav_t *dvdnav; // handle to libdvdnav stuff
+ char *filename; // path
+ unsigned int duration; // in milliseconds
+ int mousex, mousey;
+ int title;
+ uint32_t spu_clut[16];
+ bool spu_clut_valid;
+ bool had_initial_vts;
+
+ int dvd_speed;
+
+ int track;
+ char *device;
+
+ struct dvd_opts *opts;
+};
+
+#define DNE(e) [e] = # e
+static const char *const mp_dvdnav_events[] = {
+ DNE(DVDNAV_BLOCK_OK),
+ DNE(DVDNAV_NOP),
+ DNE(DVDNAV_STILL_FRAME),
+ DNE(DVDNAV_SPU_STREAM_CHANGE),
+ DNE(DVDNAV_AUDIO_STREAM_CHANGE),
+ DNE(DVDNAV_VTS_CHANGE),
+ DNE(DVDNAV_CELL_CHANGE),
+ DNE(DVDNAV_NAV_PACKET),
+ DNE(DVDNAV_STOP),
+ DNE(DVDNAV_HIGHLIGHT),
+ DNE(DVDNAV_SPU_CLUT_CHANGE),
+ DNE(DVDNAV_HOP_CHANNEL),
+ DNE(DVDNAV_WAIT),
+};
+
+#define LOOKUP_NAME(array, i) \
+ (((i) >= 0 && (i) < MP_ARRAY_SIZE(array)) ? array[(i)] : "?")
+
+static void dvd_set_speed(stream_t *stream, char *device, unsigned speed)
+{
+#if defined(__linux__) && defined(SG_IO) && defined(GPCMD_SET_STREAMING)
+ int fd;
+ unsigned char buffer[28];
+ unsigned char cmd[12];
+ struct sg_io_hdr sghdr;
+ struct stat st;
+
+ memset(&st, 0, sizeof(st));
+
+ if (stat(device, &st) == -1) return;
+
+ if (!S_ISBLK(st.st_mode)) return; /* not a block device */
+
+ switch (speed) {
+ case 0: /* don't touch speed setting */
+ return;
+ case -1: /* restore default value */
+ MP_INFO(stream, "Restoring DVD speed... ");
+ break;
+ default: /* limit to <speed> KB/s */
+ // speed < 100 is multiple of DVD single speed (1350KB/s)
+ if (speed < 100)
+ speed *= 1350;
+ MP_INFO(stream, "Limiting DVD speed to %dKB/s... ", speed);
+ break;
+ }
+
+ memset(&sghdr, 0, sizeof(sghdr));
+ sghdr.interface_id = 'S';
+ sghdr.timeout = 5000;
+ sghdr.dxfer_direction = SG_DXFER_TO_DEV;
+ sghdr.dxfer_len = sizeof(buffer);
+ sghdr.dxferp = buffer;
+ sghdr.cmd_len = sizeof(cmd);
+ sghdr.cmdp = cmd;
+
+ memset(cmd, 0, sizeof(cmd));
+ cmd[0] = GPCMD_SET_STREAMING;
+ cmd[10] = sizeof(buffer);
+
+ memset(buffer, 0, sizeof(buffer));
+ /* first sector 0, last sector 0xffffffff */
+ AV_WB32(buffer + 8, 0xffffffff);
+ if (speed == -1)
+ buffer[0] = 4; /* restore default */
+ else {
+ /* <speed> kilobyte */
+ AV_WB32(buffer + 12, speed);
+ AV_WB32(buffer + 20, speed);
+ }
+ /* 1 second */
+ AV_WB16(buffer + 18, 1000);
+ AV_WB16(buffer + 26, 1000);
+
+ fd = open(device, O_RDWR | O_NONBLOCK | O_CLOEXEC);
+ if (fd == -1) {
+ MP_INFO(stream, "Couldn't open DVD device for writing, changing DVD speed needs write access.\n");
+ return;
+ }
+
+ if (ioctl(fd, SG_IO, &sghdr) < 0)
+ MP_INFO(stream, "failed\n");
+ else
+ MP_INFO(stream, "successful\n");
+
+ close(fd);
+#endif
+}
+
+// Check if this is likely to be an .ifo or similar file.
+static int dvd_probe(const char *path, const char *ext, const char *sig)
+{
+ if (!bstr_case_endswith(bstr0(path), bstr0(ext)))
+ return false;
+
+ FILE *temp = fopen(path, "rb");
+ if (!temp)
+ return false;
+
+ bool r = false;
+
+ char data[50];
+
+ assert(strlen(sig) <= sizeof(data));
+
+ if (fread(data, 50, 1, temp) == 1) {
+ if (memcmp(data, sig, strlen(sig)) == 0)
+ r = true;
+ }
+
+ fclose(temp);
+ return r;
+}
+
+/**
+ * \brief mp_dvdnav_lang_from_aid() returns the language corresponding to audio id 'aid'
+ * \param stream: - stream pointer
+ * \param sid: physical subtitle id
+ * \return 0 on error, otherwise language id
+ */
+static int mp_dvdnav_lang_from_aid(stream_t *stream, int aid)
+{
+ uint8_t lg;
+ uint16_t lang;
+ struct priv *priv = stream->priv;
+
+ if (aid < 0)
+ return 0;
+ lg = dvdnav_get_audio_logical_stream(priv->dvdnav, aid & 0x7);
+ if (lg == 0xff)
+ return 0;
+ lang = dvdnav_audio_stream_to_lang(priv->dvdnav, lg);
+ if (lang == 0xffff)
+ return 0;
+ return lang;
+}
+
+/**
+ * \brief mp_dvdnav_lang_from_sid() returns the language corresponding to subtitle id 'sid'
+ * \param stream: - stream pointer
+ * \param sid: physical subtitle id
+ * \return 0 on error, otherwise language id
+ */
+static int mp_dvdnav_lang_from_sid(stream_t *stream, int sid)
+{
+ uint8_t k;
+ uint16_t lang;
+ struct priv *priv = stream->priv;
+ if (sid < 0)
+ return 0;
+ for (k = 0; k < 32; k++)
+ if (dvdnav_get_spu_logical_stream(priv->dvdnav, k) == sid)
+ break;
+ if (k == 32)
+ return 0;
+ lang = dvdnav_spu_stream_to_lang(priv->dvdnav, k);
+ if (lang == 0xffff)
+ return 0;
+ return lang;
+}
+
+/**
+ * \brief mp_dvdnav_number_of_subs() returns the count of available subtitles
+ * \param stream: - stream pointer
+ * \return 0 on error, something meaningful otherwise
+ */
+static int mp_dvdnav_number_of_subs(stream_t *stream)
+{
+ struct priv *priv = stream->priv;
+ uint8_t lg, k, n = 0;
+
+ for (k = 0; k < 32; k++) {
+ lg = dvdnav_get_spu_logical_stream(priv->dvdnav, k);
+ if (lg == 0xff)
+ continue;
+ if (lg >= n)
+ n = lg + 1;
+ }
+ return n;
+}
+
+static int fill_buffer(stream_t *s, void *buf, int max_len)
+{
+ struct priv *priv = s->priv;
+ dvdnav_t *dvdnav = priv->dvdnav;
+
+ if (max_len < 2048) {
+ MP_FATAL(s, "Short read size. Data corruption will follow. Please "
+ "provide a patch.\n");
+ return -1;
+ }
+
+ while (1) {
+ int len = -1;
+ int event = DVDNAV_NOP;
+ if (dvdnav_get_next_block(dvdnav, buf, &event, &len) != DVDNAV_STATUS_OK)
+ {
+ MP_ERR(s, "Error getting next block from DVD %d (%s)\n",
+ event, dvdnav_err_to_string(dvdnav));
+ return 0;
+ }
+ if (event != DVDNAV_BLOCK_OK) {
+ const char *name = LOOKUP_NAME(mp_dvdnav_events, event);
+ MP_TRACE(s, "DVDNAV: event %s (%d).\n", name, event);
+ }
+ switch (event) {
+ case DVDNAV_BLOCK_OK:
+ return len;
+ case DVDNAV_STOP:
+ return 0;
+ case DVDNAV_NAV_PACKET: {
+ pci_t *pnavpci = dvdnav_get_current_nav_pci(dvdnav);
+ uint32_t start_pts = pnavpci->pci_gi.vobu_s_ptm;
+ MP_TRACE(s, "start pts = %"PRIu32"\n", start_pts);
+ break;
+ }
+ case DVDNAV_STILL_FRAME:
+ dvdnav_still_skip(dvdnav);
+ return 0;
+ case DVDNAV_WAIT:
+ dvdnav_wait_skip(dvdnav);
+ return 0;
+ case DVDNAV_HIGHLIGHT:
+ break;
+ case DVDNAV_VTS_CHANGE: {
+ int tit = 0, part = 0;
+ dvdnav_vts_change_event_t *vts_event =
+ (dvdnav_vts_change_event_t *)s->buffer;
+ MP_INFO(s, "DVDNAV, switched to title: %d\n",
+ vts_event->new_vtsN);
+ if (!priv->had_initial_vts) {
+ // dvdnav sends an initial VTS change before any data; don't
+ // cause a blocking wait for the player, because the player in
+ // turn can't initialize the demuxer without data.
+ priv->had_initial_vts = true;
+ break;
+ }
+ if (dvdnav_current_title_info(dvdnav, &tit, &part) == DVDNAV_STATUS_OK)
+ {
+ MP_VERBOSE(s, "DVDNAV, NEW TITLE %d\n", tit);
+ if (priv->title > 0 && tit != priv->title)
+ MP_WARN(s, "Requested title not found\n");
+ }
+ break;
+ }
+ case DVDNAV_CELL_CHANGE: {
+ dvdnav_cell_change_event_t *ev = (dvdnav_cell_change_event_t *)buf;
+
+ if (ev->pgc_length)
+ priv->duration = ev->pgc_length / 90;
+
+ break;
+ }
+ case DVDNAV_SPU_CLUT_CHANGE: {
+ memcpy(priv->spu_clut, buf, 16 * sizeof(uint32_t));
+ priv->spu_clut_valid = true;
+ break;
+ }
+ }
+ }
+ return 0;
+}
+
+static int control(stream_t *stream, int cmd, void *arg)
+{
+ struct priv *priv = stream->priv;
+ dvdnav_t *dvdnav = priv->dvdnav;
+ int tit, part;
+
+ switch (cmd) {
+ case STREAM_CTRL_GET_NUM_CHAPTERS: {
+ if (dvdnav_current_title_info(dvdnav, &tit, &part) != DVDNAV_STATUS_OK)
+ break;
+ if (dvdnav_get_number_of_parts(dvdnav, tit, &part) != DVDNAV_STATUS_OK)
+ break;
+ if (!part)
+ break;
+ *(unsigned int *)arg = part;
+ return 1;
+ }
+ case STREAM_CTRL_GET_CHAPTER_TIME: {
+ double *ch = arg;
+ int chapter = *ch;
+ if (dvdnav_current_title_info(dvdnav, &tit, &part) != DVDNAV_STATUS_OK)
+ break;
+ uint64_t *parts = NULL, duration = 0;
+ int n = dvdnav_describe_title_chapters(dvdnav, tit, &parts, &duration);
+ if (!parts)
+ break;
+ if (chapter < 0 || chapter + 1 > n)
+ break;
+ *ch = chapter > 0 ? parts[chapter - 1] / 90000.0 : 0;
+ free(parts);
+ return 1;
+ }
+ case STREAM_CTRL_GET_TIME_LENGTH: {
+ if (priv->duration) {
+ *(double *)arg = (double)priv->duration / 1000.0;
+ return 1;
+ }
+ break;
+ }
+ case STREAM_CTRL_GET_ASPECT_RATIO: {
+ uint8_t ar = dvdnav_get_video_aspect(dvdnav);
+ *(double *)arg = !ar ? 4.0 / 3.0 : 16.0 / 9.0;
+ return 1;
+ }
+ case STREAM_CTRL_GET_CURRENT_TIME: {
+ double tm;
+ tm = dvdnav_get_current_time(dvdnav) / 90000.0f;
+ if (tm != -1) {
+ *(double *)arg = tm;
+ return 1;
+ }
+ break;
+ }
+ case STREAM_CTRL_GET_NUM_TITLES: {
+ int32_t num_titles = 0;
+ if (dvdnav_get_number_of_titles(dvdnav, &num_titles) != DVDNAV_STATUS_OK)
+ break;
+ *((unsigned int*)arg)= num_titles;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_TITLE_LENGTH: {
+ int t = *(double *)arg;
+ int32_t num_titles = 0;
+ if (dvdnav_get_number_of_titles(dvdnav, &num_titles) != DVDNAV_STATUS_OK)
+ break;
+ if (t < 0 || t >= num_titles)
+ break;
+ uint64_t duration = 0;
+ uint64_t *parts = NULL;
+ dvdnav_describe_title_chapters(dvdnav, t + 1, &parts, &duration);
+ if (!parts)
+ break;
+ free(parts);
+ *(double *)arg = duration / 90000.0;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_CURRENT_TITLE: {
+ if (dvdnav_current_title_info(dvdnav, &tit, &part) != DVDNAV_STATUS_OK)
+ break;
+ *((unsigned int *) arg) = tit - 1;
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SET_CURRENT_TITLE: {
+ int title = *((unsigned int *) arg);
+ if (dvdnav_title_play(priv->dvdnav, title + 1) != DVDNAV_STATUS_OK)
+ break;
+ stream_drop_buffers(stream);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_SEEK_TO_TIME: {
+ double *args = arg;
+ double d = args[0]; // absolute target timestamp
+ int flags = args[1]; // from SEEK_* flags (demux.h)
+ if (flags & SEEK_HR)
+ d -= 10; // fudge offset; it's a hack, because fuck libdvd*
+ int64_t tm = (int64_t)(d * 90000);
+ if (tm < 0)
+ tm = 0;
+ if (priv->duration && tm >= (priv->duration * 90))
+ tm = priv->duration * 90 - 1;
+ uint32_t pos, len;
+ if (dvdnav_get_position(dvdnav, &pos, &len) != DVDNAV_STATUS_OK)
+ break;
+ MP_VERBOSE(stream, "seek to PTS %f (%"PRId64")\n", d, tm);
+ if (dvdnav_time_search(dvdnav, tm) != DVDNAV_STATUS_OK)
+ break;
+ stream_drop_buffers(stream);
+ d = dvdnav_get_current_time(dvdnav) / 90000.0f;
+ MP_VERBOSE(stream, "landed at: %f\n", d);
+ if (dvdnav_get_position(dvdnav, &pos, &len) == DVDNAV_STATUS_OK)
+ MP_VERBOSE(stream, "block: %lu\n", (unsigned long)pos);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_NUM_ANGLES: {
+ uint32_t curr, angles;
+ if (dvdnav_get_angle_info(dvdnav, &curr, &angles) != DVDNAV_STATUS_OK)
+ break;
+ *(int *)arg = angles;
+ return 1;
+ }
+ case STREAM_CTRL_GET_ANGLE: {
+ uint32_t curr, angles;
+ if (dvdnav_get_angle_info(dvdnav, &curr, &angles) != DVDNAV_STATUS_OK)
+ break;
+ *(int *)arg = curr;
+ return 1;
+ }
+ case STREAM_CTRL_SET_ANGLE: {
+ uint32_t curr, angles;
+ int new_angle = *(int *)arg;
+ if (dvdnav_get_angle_info(dvdnav, &curr, &angles) != DVDNAV_STATUS_OK)
+ break;
+ if (new_angle > angles || new_angle < 1)
+ break;
+ if (dvdnav_angle_change(dvdnav, new_angle) != DVDNAV_STATUS_OK)
+ return 1;
+ break;
+ }
+ case STREAM_CTRL_GET_LANG: {
+ struct stream_lang_req *req = arg;
+ int lang = 0;
+ switch (req->type) {
+ case STREAM_AUDIO:
+ lang = mp_dvdnav_lang_from_aid(stream, req->id);
+ break;
+ case STREAM_SUB:
+ lang = mp_dvdnav_lang_from_sid(stream, req->id);
+ break;
+ }
+ if (!lang)
+ break;
+ snprintf(req->name, sizeof(req->name), "%c%c", lang >> 8, lang);
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_DVD_INFO: {
+ struct stream_dvd_info_req *req = arg;
+ memset(req, 0, sizeof(*req));
+ req->num_subs = mp_dvdnav_number_of_subs(stream);
+ assert(sizeof(uint32_t) == sizeof(unsigned int));
+ memcpy(req->palette, priv->spu_clut, sizeof(req->palette));
+ return STREAM_OK;
+ }
+ case STREAM_CTRL_GET_DISC_NAME: {
+ const char *volume = NULL;
+ if (dvdnav_get_title_string(dvdnav, &volume) != DVDNAV_STATUS_OK)
+ break;
+ if (!volume || !volume[0])
+ break;
+ *(char**)arg = talloc_strdup(NULL, volume);
+ return STREAM_OK;
+ }
+ }
+
+ return STREAM_UNSUPPORTED;
+}
+
+static void stream_dvdnav_close(stream_t *s)
+{
+ struct priv *priv = s->priv;
+ dvdnav_close(priv->dvdnav);
+ priv->dvdnav = NULL;
+ if (priv->dvd_speed)
+ dvd_set_speed(s, priv->filename, -1);
+ if (priv->filename)
+ free(priv->filename);
+}
+
+static struct priv *new_dvdnav_stream(stream_t *stream, char *filename)
+{
+ struct priv *priv = stream->priv;
+ const char *title_str;
+
+ if (!filename)
+ return NULL;
+
+ if (!(priv->filename = strdup(filename)))
+ return NULL;
+
+ priv->dvd_speed = priv->opts->speed;
+ dvd_set_speed(stream, priv->filename, priv->dvd_speed);
+
+ if (dvdnav_open(&(priv->dvdnav), priv->filename) != DVDNAV_STATUS_OK) {
+ free(priv->filename);
+ priv->filename = NULL;
+ return NULL;
+ }
+
+ if (!priv->dvdnav)
+ return NULL;
+
+ dvdnav_set_readahead_flag(priv->dvdnav, 1);
+ if (dvdnav_set_PGC_positioning_flag(priv->dvdnav, 1) != DVDNAV_STATUS_OK)
+ MP_ERR(stream, "stream_dvdnav, failed to set PGC positioning\n");
+ /* report the title?! */
+ dvdnav_get_title_string(priv->dvdnav, &title_str);
+
+ return priv;
+}
+
+static int open_s_internal(stream_t *stream)
+{
+ struct priv *priv, *p;
+ priv = p = stream->priv;
+ char *filename;
+
+ p->opts = mp_get_config_group(stream, stream->global, &dvd_conf);
+
+ if (p->device && p->device[0])
+ filename = p->device;
+ else if (p->opts->device && p->opts->device[0])
+ filename = p->opts->device;
+ else
+ filename = DEFAULT_DVD_DEVICE;
+ if (!new_dvdnav_stream(stream, filename)) {
+ MP_ERR(stream, "Couldn't open DVD device: %s\n",
+ filename);
+ return STREAM_ERROR;
+ }
+
+ if (p->track == TITLE_LONGEST) { // longest
+ dvdnav_t *dvdnav = priv->dvdnav;
+ uint64_t best_length = 0;
+ int best_title = -1;
+ int32_t num_titles;
+ if (dvdnav_get_number_of_titles(dvdnav, &num_titles) == DVDNAV_STATUS_OK) {
+ MP_VERBOSE(stream, "List of available titles:\n");
+ for (int n = 1; n <= num_titles; n++) {
+ uint64_t *parts = NULL, duration = 0;
+ dvdnav_describe_title_chapters(dvdnav, n, &parts, &duration);
+ if (parts) {
+ if (duration > best_length) {
+ best_length = duration;
+ best_title = n;
+ }
+ if (duration > 90000) { // arbitrarily ignore <1s titles
+ char *time = mp_format_time(duration / 90000, false);
+ MP_VERBOSE(stream, "title: %3d duration: %s\n",
+ n - 1, time);
+ talloc_free(time);
+ }
+ free(parts);
+ }
+ }
+ }
+ p->track = best_title - 1;
+ MP_INFO(stream, "Selecting title %d.\n", p->track);
+ }
+
+ if (p->track >= 0) {
+ priv->title = p->track;
+ if (dvdnav_title_play(priv->dvdnav, p->track + 1) != DVDNAV_STATUS_OK) {
+ MP_FATAL(stream, "dvdnav_stream, couldn't select title %d, error '%s'\n",
+ p->track, dvdnav_err_to_string(priv->dvdnav));
+ return STREAM_UNSUPPORTED;
+ }
+ } else {
+ MP_FATAL(stream, "DVD menu support has been removed.\n");
+ return STREAM_ERROR;
+ }
+ if (p->opts->angle > 1)
+ dvdnav_angle_change(priv->dvdnav, p->opts->angle);
+
+ stream->fill_buffer = fill_buffer;
+ stream->control = control;
+ stream->close = stream_dvdnav_close;
+ stream->demuxer = "+disc";
+ stream->lavf_type = "mpeg";
+
+ return STREAM_OK;
+}
+
+static int open_s(stream_t *stream)
+{
+ struct priv *priv = talloc_zero(stream, struct priv);
+ stream->priv = priv;
+
+ bstr title, bdevice;
+ bstr_split_tok(bstr0(stream->path), "/", &title, &bdevice);
+
+ priv->track = TITLE_LONGEST;
+
+ if (bstr_equals0(title, "longest") || bstr_equals0(title, "first")) {
+ priv->track = TITLE_LONGEST;
+ } else if (bstr_equals0(title, "menu")) {
+ priv->track = TITLE_MENU;
+ } else if (title.len) {
+ bstr rest;
+ priv->track = bstrtoll(title, &rest, 10);
+ if (rest.len) {
+ MP_ERR(stream, "number expected: '%.*s'\n", BSTR_P(rest));
+ return STREAM_ERROR;
+ }
+ }
+
+ priv->device = bstrto0(priv, bdevice);
+
+ return open_s_internal(stream);
+}
+
+const stream_info_t stream_info_dvdnav = {
+ .name = "dvdnav",
+ .open = open_s,
+ .protocols = (const char*const[]){ "dvd", "dvdnav", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
+
+static bool check_ifo(const char *path)
+{
+ if (strcasecmp(mp_basename(path), "video_ts.ifo"))
+ return false;
+
+ return dvd_probe(path, ".ifo", "DVDVIDEO-VMG");
+}
+
+static int ifo_dvdnav_stream_open(stream_t *stream)
+{
+ struct priv *priv = talloc_zero(stream, struct priv);
+ stream->priv = priv;
+
+ if (!stream->access_references)
+ goto unsupported;
+
+ priv->track = TITLE_LONGEST;
+
+ char *path = mp_file_get_path(priv, bstr0(stream->url));
+ if (!path)
+ goto unsupported;
+
+ // We allow the path to point to a directory containing VIDEO_TS/, a
+ // directory containing VIDEO_TS.IFO, or that file itself.
+ if (!check_ifo(path)) {
+ // On UNIX, just assume the filename is always uppercase.
+ char *npath = mp_path_join(priv, path, "VIDEO_TS.IFO");
+ if (!check_ifo(npath)) {
+ npath = mp_path_join(priv, path, "VIDEO_TS/VIDEO_TS.IFO");
+ if (!check_ifo(npath))
+ goto unsupported;
+ }
+ path = npath;
+ }
+
+ priv->device = bstrto0(priv, mp_dirname(path));
+
+ MP_INFO(stream, ".IFO detected. Redirecting to dvd://\n");
+ return open_s_internal(stream);
+
+unsupported:
+ talloc_free(priv);
+ stream->priv = NULL;
+ return STREAM_UNSUPPORTED;
+}
+
+const stream_info_t stream_info_ifo_dvdnav = {
+ .name = "ifo_dvdnav",
+ .open = ifo_dvdnav_stream_open,
+ .protocols = (const char*const[]){ "file", "", NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_edl.c b/stream/stream_edl.c
new file mode 100644
index 0000000..94bbe58
--- /dev/null
+++ b/stream/stream_edl.c
@@ -0,0 +1,17 @@
+// Dummy stream implementation to enable demux_edl, which is in turn a
+// dummy demuxer implementation to enable tl_edl.
+
+#include "stream.h"
+
+static int s_open (struct stream *stream)
+{
+ stream->demuxer = "edl";
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_edl = {
+ .name = "edl",
+ .open = s_open,
+ .protocols = (const char*const[]){"edl", NULL},
+};
diff --git a/stream/stream_file.c b/stream/stream_file.c
new file mode 100644
index 0000000..4895a83
--- /dev/null
+++ b/stream/stream_file.c
@@ -0,0 +1,377 @@
+/*
+ * Original authors: Albeu, probably Arpi
+ *
+ * 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 "config.h"
+
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+
+#ifndef __MINGW32__
+#include <poll.h>
+#endif
+
+#include "osdep/io.h"
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/thread_tools.h"
+#include "stream.h"
+#include "options/m_option.h"
+#include "options/path.h"
+
+#if HAVE_BSD_FSTATFS
+#include <sys/param.h>
+#include <sys/mount.h>
+#endif
+
+#if HAVE_LINUX_FSTATFS
+#include <sys/vfs.h>
+#endif
+
+#ifdef _WIN32
+#include <windows.h>
+#include <winternl.h>
+#include <io.h>
+
+#ifndef FILE_REMOTE_DEVICE
+#define FILE_REMOTE_DEVICE (0x10)
+#endif
+#endif
+
+struct priv {
+ int fd;
+ bool close;
+ bool use_poll;
+ bool regular_file;
+ bool appending;
+ int64_t orig_size;
+ struct mp_cancel *cancel;
+};
+
+// Total timeout = RETRY_TIMEOUT * MAX_RETRIES
+#define RETRY_TIMEOUT 0.2
+#define MAX_RETRIES 10
+
+static int64_t get_size(stream_t *s)
+{
+ struct priv *p = s->priv;
+ struct stat st;
+ if (fstat(p->fd, &st) == 0) {
+ if (st.st_size <= 0 && !s->seekable)
+ st.st_size = -1;
+ if (st.st_size >= 0)
+ return st.st_size;
+ }
+ return -1;
+}
+
+static int fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ struct priv *p = s->priv;
+
+#ifndef __MINGW32__
+ if (p->use_poll) {
+ int c = mp_cancel_get_fd(p->cancel);
+ struct pollfd fds[2] = {
+ {.fd = p->fd, .events = POLLIN},
+ {.fd = c, .events = POLLIN},
+ };
+ poll(fds, c >= 0 ? 2 : 1, -1);
+ if (fds[1].revents & POLLIN)
+ return -1;
+ }
+#endif
+
+ for (int retries = 0; retries < MAX_RETRIES; retries++) {
+ int r = read(p->fd, buffer, max_len);
+ if (r > 0)
+ return r;
+
+ // Try to detect and handle files being appended during playback.
+ int64_t size = get_size(s);
+ if (p->regular_file && size > p->orig_size && !p->appending) {
+ MP_WARN(s, "File is apparently being appended to, will keep "
+ "retrying with timeouts.\n");
+ p->appending = true;
+ }
+
+ if (!p->appending || p->use_poll)
+ break;
+
+ if (mp_cancel_wait(p->cancel, RETRY_TIMEOUT))
+ break;
+ }
+
+ return 0;
+}
+
+static int write_buffer(stream_t *s, void *buffer, int len)
+{
+ struct priv *p = s->priv;
+ return write(p->fd, buffer, len);
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+ return lseek(p->fd, newpos, SEEK_SET) != (off_t)-1;
+}
+
+static void s_close(stream_t *s)
+{
+ struct priv *p = s->priv;
+ if (p->close)
+ close(p->fd);
+}
+
+// If url is a file:// URL, return the local filename, otherwise return NULL.
+char *mp_file_url_to_filename(void *talloc_ctx, bstr url)
+{
+ bstr proto = mp_split_proto(url, &url);
+ if (bstrcasecmp0(proto, "file") != 0)
+ return NULL;
+ char *filename = bstrto0(talloc_ctx, url);
+ mp_url_unescape_inplace(filename);
+#if HAVE_DOS_PATHS
+ // extract '/' from '/x:/path'
+ if (filename[0] == '/' && filename[1] && filename[2] == ':')
+ memmove(filename, filename + 1, strlen(filename)); // including \0
+#endif
+ return filename;
+}
+
+// Return talloc_strdup's filesystem path if local, otherwise NULL.
+// Unlike mp_file_url_to_filename(), doesn't return NULL if already local.
+char *mp_file_get_path(void *talloc_ctx, bstr url)
+{
+ if (mp_split_proto(url, &(bstr){0}).len) {
+ return mp_file_url_to_filename(talloc_ctx, url);
+ } else {
+ return bstrto0(talloc_ctx, url);
+ }
+}
+
+#if HAVE_BSD_FSTATFS
+static bool check_stream_network(int fd)
+{
+ struct statfs fs;
+ const char *stypes[] = { "afpfs", "nfs", "smbfs", "webdav", "osxfusefs",
+ "fuse", "fusefs.sshfs", "macfuse", NULL };
+ if (fstatfs(fd, &fs) == 0)
+ for (int i=0; stypes[i]; i++)
+ if (strcmp(stypes[i], fs.f_fstypename) == 0)
+ return true;
+ return false;
+
+}
+#elif HAVE_LINUX_FSTATFS
+static bool check_stream_network(int fd)
+{
+ struct statfs fs;
+ const uint32_t stypes[] = {
+ 0x5346414F /*AFS*/, 0x61756673 /*AUFS*/, 0x00C36400 /*CEPH*/,
+ 0xFF534D42 /*CIFS*/, 0x73757245 /*CODA*/, 0x19830326 /*FHGFS*/,
+ 0x65735546 /*FUSEBLK*/,0x65735543 /*FUSECTL*/,0x1161970 /*GFS*/,
+ 0x47504653 /*GPFS*/, 0x6B414653 /*KAFS*/, 0x0BD00BD0 /*LUSTRE*/,
+ 0x564C /*NCP*/, 0x6969 /*NFS*/, 0x6E667364 /*NFSD*/,
+ 0xAAD7AAEA /*PANFS*/, 0x50495045 /*PIPEFS*/, 0x517B /*SMB*/,
+ 0xBEEFDEAD /*SNFS*/, 0xBACBACBC /*VMHGFS*/, 0x7461636f /*OCFS2*/,
+ 0xFE534D42 /*SMB2*/, 0x61636673 /*ACFS*/, 0x013111A8 /*IBRIX*/,
+ 0
+ };
+ if (fstatfs(fd, &fs) == 0) {
+ for (int i=0; stypes[i]; i++) {
+ if (stypes[i] == fs.f_type)
+ return true;
+ }
+ }
+ return false;
+
+}
+#elif defined(_WIN32)
+static bool check_stream_network(int fd)
+{
+ NTSTATUS (NTAPI *pNtQueryVolumeInformationFile)(HANDLE,
+ PIO_STATUS_BLOCK, PVOID, ULONG, FS_INFORMATION_CLASS) = NULL;
+
+ // NtQueryVolumeInformationFile is an internal Windows function. It has
+ // been present since Windows XP, however this code should fail gracefully
+ // if it's removed from a future version of Windows.
+ HMODULE ntdll = GetModuleHandleW(L"ntdll.dll");
+ pNtQueryVolumeInformationFile = (NTSTATUS (NTAPI*)(HANDLE,
+ PIO_STATUS_BLOCK, PVOID, ULONG, FS_INFORMATION_CLASS))
+ GetProcAddress(ntdll, "NtQueryVolumeInformationFile");
+
+ if (!pNtQueryVolumeInformationFile)
+ return false;
+
+ HANDLE h = (HANDLE)_get_osfhandle(fd);
+ if (h == INVALID_HANDLE_VALUE)
+ return false;
+
+ FILE_FS_DEVICE_INFORMATION info = { 0 };
+ IO_STATUS_BLOCK io;
+ NTSTATUS status = pNtQueryVolumeInformationFile(h, &io, &info,
+ sizeof(info), FileFsDeviceInformation);
+ if (!NT_SUCCESS(status))
+ return false;
+
+ return info.DeviceType == FILE_DEVICE_NETWORK_FILE_SYSTEM ||
+ (info.Characteristics & FILE_REMOTE_DEVICE);
+}
+#else
+static bool check_stream_network(int fd)
+{
+ return false;
+}
+#endif
+
+static int open_f(stream_t *stream, const struct stream_open_args *args)
+{
+ struct priv *p = talloc_ptrtype(stream, p);
+ *p = (struct priv) {
+ .fd = -1,
+ };
+ stream->priv = p;
+ stream->is_local_file = true;
+
+ bool strict_fs = args->flags & STREAM_LOCAL_FS_ONLY;
+ bool write = stream->mode == STREAM_WRITE;
+ int m = O_CLOEXEC | (write ? O_RDWR | O_CREAT | O_TRUNC : O_RDONLY);
+
+ char *filename = stream->path;
+ char *url = "";
+ if (!strict_fs) {
+ char *fn = mp_file_url_to_filename(stream, bstr0(stream->url));
+ if (fn)
+ filename = stream->path = fn;
+ url = stream->url;
+ }
+
+ bool is_fdclose = strncmp(url, "fdclose://", 10) == 0;
+ if (strncmp(url, "fd://", 5) == 0 || is_fdclose) {
+ char *begin = strstr(stream->url, "://") + 3, *end = NULL;
+ p->fd = strtol(begin, &end, 0);
+ if (!end || end == begin || end[0]) {
+ MP_ERR(stream, "Invalid FD: %s\n", stream->url);
+ return STREAM_ERROR;
+ }
+ if (is_fdclose)
+ p->close = true;
+ } else if (!strict_fs && !strcmp(filename, "-")) {
+ if (!write) {
+ MP_INFO(stream, "Reading from stdin...\n");
+ p->fd = 0;
+ } else {
+ MP_INFO(stream, "Writing to stdout...\n");
+ p->fd = 1;
+ }
+ } else {
+ if (bstr_startswith0(bstr0(stream->url), "appending://"))
+ p->appending = true;
+
+ mode_t openmode = S_IRUSR | S_IWUSR;
+#ifndef __MINGW32__
+ openmode |= S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH;
+ if (!write)
+ m |= O_NONBLOCK;
+#endif
+ p->fd = open(filename, m | O_BINARY, openmode);
+ if (p->fd < 0) {
+ MP_ERR(stream, "Cannot open file '%s': %s\n",
+ filename, mp_strerror(errno));
+ return STREAM_ERROR;
+ }
+ p->close = true;
+ }
+
+ struct stat st;
+ if (fstat(p->fd, &st) == 0) {
+ if (S_ISDIR(st.st_mode)) {
+ stream->is_directory = true;
+ if (!(args->flags & STREAM_LESS_NOISE))
+ MP_INFO(stream, "This is a directory - adding to playlist.\n");
+ } else if (S_ISREG(st.st_mode)) {
+ p->regular_file = true;
+#ifndef __MINGW32__
+ // O_NONBLOCK has weird semantics on file locks; remove it.
+ int val = fcntl(p->fd, F_GETFL) & ~(unsigned)O_NONBLOCK;
+ fcntl(p->fd, F_SETFL, val);
+#endif
+ } else {
+ p->use_poll = true;
+ }
+ }
+
+#ifdef __MINGW32__
+ setmode(p->fd, O_BINARY);
+#endif
+
+ off_t len = lseek(p->fd, 0, SEEK_END);
+ lseek(p->fd, 0, SEEK_SET);
+ if (len != (off_t)-1) {
+ stream->seek = seek;
+ stream->seekable = true;
+ }
+
+ stream->fast_skip = true;
+ stream->fill_buffer = fill_buffer;
+ stream->write_buffer = write_buffer;
+ stream->get_size = get_size;
+ stream->close = s_close;
+
+ if (check_stream_network(p->fd)) {
+ stream->streaming = true;
+#if HAVE_COCOA
+ if (fcntl(p->fd, F_RDAHEAD, 0) < 0) {
+ MP_VERBOSE(stream, "Cannot disable read ahead on file '%s': %s\n",
+ filename, mp_strerror(errno));
+ }
+#endif
+ }
+
+ p->orig_size = get_size(stream);
+
+ p->cancel = mp_cancel_new(p);
+ if (stream->cancel)
+ mp_cancel_set_parent(p->cancel, stream->cancel);
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_file = {
+ .name = "file",
+ .open2 = open_f,
+ .protocols = (const char*const[]){ "file", "", "appending", NULL },
+ .can_write = true,
+ .local_fs = true,
+ .stream_origin = STREAM_ORIGIN_FS,
+};
+
+const stream_info_t stream_info_fd = {
+ .name = "fd",
+ .open2 = open_f,
+ .protocols = (const char*const[]){ "fd", "fdclose", NULL },
+ .can_write = true,
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+};
diff --git a/stream/stream_lavf.c b/stream/stream_lavf.c
new file mode 100644
index 0000000..c153ddd
--- /dev/null
+++ b/stream/stream_lavf.c
@@ -0,0 +1,457 @@
+/*
+ * 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 <libavformat/avformat.h>
+#include <libavformat/avio.h>
+#include <libavutil/opt.h>
+
+#include "options/path.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "common/tags.h"
+#include "common/av_common.h"
+#include "demux/demux.h"
+#include "misc/charset_conv.h"
+#include "misc/thread_tools.h"
+#include "stream.h"
+#include "options/m_config.h"
+#include "options/m_option.h"
+
+#include "cookies.h"
+
+#include "misc/bstr.h"
+#include "mpv_talloc.h"
+
+#define OPT_BASE_STRUCT struct stream_lavf_params
+struct stream_lavf_params {
+ char **avopts;
+ bool cookies_enabled;
+ char *cookies_file;
+ char *useragent;
+ char *referrer;
+ char **http_header_fields;
+ bool tls_verify;
+ char *tls_ca_file;
+ char *tls_cert_file;
+ char *tls_key_file;
+ double timeout;
+ char *http_proxy;
+};
+
+const struct m_sub_options stream_lavf_conf = {
+ .opts = (const m_option_t[]) {
+ {"stream-lavf-o", OPT_KEYVALUELIST(avopts)},
+ {"http-header-fields", OPT_STRINGLIST(http_header_fields)},
+ {"user-agent", OPT_STRING(useragent)},
+ {"referrer", OPT_STRING(referrer)},
+ {"cookies", OPT_BOOL(cookies_enabled)},
+ {"cookies-file", OPT_STRING(cookies_file), .flags = M_OPT_FILE},
+ {"tls-verify", OPT_BOOL(tls_verify)},
+ {"tls-ca-file", OPT_STRING(tls_ca_file), .flags = M_OPT_FILE},
+ {"tls-cert-file", OPT_STRING(tls_cert_file), .flags = M_OPT_FILE},
+ {"tls-key-file", OPT_STRING(tls_key_file), .flags = M_OPT_FILE},
+ {"network-timeout", OPT_DOUBLE(timeout), M_RANGE(0, DBL_MAX)},
+ {"http-proxy", OPT_STRING(http_proxy)},
+ {0}
+ },
+ .size = sizeof(struct stream_lavf_params),
+ .defaults = &(const struct stream_lavf_params){
+ .useragent = "libmpv",
+ .timeout = 60,
+ },
+};
+
+static const char *const http_like[] =
+ {"http", "https", "mmsh", "mmshttp", "httproxy", NULL};
+
+static int open_f(stream_t *stream);
+static struct mp_tags *read_icy(stream_t *stream);
+
+static int fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ AVIOContext *avio = s->priv;
+ int r = avio_read_partial(avio, buffer, max_len);
+ return (r <= 0) ? -1 : r;
+}
+
+static int write_buffer(stream_t *s, void *buffer, int len)
+{
+ AVIOContext *avio = s->priv;
+ avio_write(avio, buffer, len);
+ avio_flush(avio);
+ if (avio->error)
+ return -1;
+ return len;
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ AVIOContext *avio = s->priv;
+ if (avio_seek(avio, newpos, SEEK_SET) < 0) {
+ return 0;
+ }
+ return 1;
+}
+
+static int64_t get_size(stream_t *s)
+{
+ AVIOContext *avio = s->priv;
+ return avio_size(avio);
+}
+
+static void close_f(stream_t *stream)
+{
+ AVIOContext *avio = stream->priv;
+ /* NOTE: As of 2011 write streams must be manually flushed before close.
+ * Currently write_buffer() always flushes them after writing.
+ * avio_close() could return an error, but we have no way to return that
+ * with the current stream API.
+ */
+ if (avio)
+ avio_close(avio);
+}
+
+static int control(stream_t *s, int cmd, void *arg)
+{
+ AVIOContext *avio = s->priv;
+ switch(cmd) {
+ case STREAM_CTRL_AVSEEK: {
+ struct stream_avseek *c = arg;
+ int64_t r = avio_seek_time(avio, c->stream_index, c->timestamp, c->flags);
+ if (r >= 0) {
+ stream_drop_buffers(s);
+ return 1;
+ }
+ break;
+ }
+ case STREAM_CTRL_HAS_AVSEEK: {
+ // Starting at some point, read_seek is always available, and runtime
+ // behavior decides whether it exists or not. FFmpeg's API doesn't
+ // return anything helpful to determine seekability upfront, so here's
+ // a hardcoded whitelist. Not our fault.
+ // In addition we also have to jump through ridiculous hoops just to
+ // get the fucking protocol name.
+ const char *proto = NULL;
+ if (avio->av_class && avio->av_class->child_next) {
+ // This usually yields the URLContext (why does it even exist?),
+ // which holds the name of the actual protocol implementation.
+ void *child = avio->av_class->child_next(avio, NULL);
+ AVClass *cl = *(AVClass **)child;
+ if (cl && cl->item_name)
+ proto = cl->item_name(child);
+ }
+ static const char *const has_read_seek[] = {
+ "rtmp", "rtmpt", "rtmpe", "rtmpte", "rtmps", "rtmpts", "mmsh", 0};
+ for (int n = 0; has_read_seek[n]; n++) {
+ if (avio->read_seek && proto && strcmp(proto, has_read_seek[n]) == 0)
+ return 1;
+ }
+ break;
+ }
+ case STREAM_CTRL_GET_METADATA: {
+ *(struct mp_tags **)arg = read_icy(s);
+ if (!*(struct mp_tags **)arg)
+ break;
+ return 1;
+ }
+ }
+ return STREAM_UNSUPPORTED;
+}
+
+static int interrupt_cb(void *ctx)
+{
+ struct stream *stream = ctx;
+ return mp_cancel_test(stream->cancel);
+}
+
+static const char * const prefix[] = { "lavf://", "ffmpeg://" };
+
+void mp_setup_av_network_options(AVDictionary **dict, const char *target_fmt,
+ struct mpv_global *global, struct mp_log *log)
+{
+ void *temp = talloc_new(NULL);
+ struct stream_lavf_params *opts =
+ mp_get_config_group(temp, global, &stream_lavf_conf);
+
+ // HTTP specific options (other protocols ignore them)
+ if (opts->useragent)
+ av_dict_set(dict, "user_agent", opts->useragent, 0);
+ if (opts->cookies_enabled) {
+ char *file = opts->cookies_file;
+ if (file && file[0])
+ file = mp_get_user_path(temp, global, file);
+ char *cookies = cookies_lavf(temp, global, log, file);
+ if (cookies && cookies[0])
+ av_dict_set(dict, "cookies", cookies, 0);
+ }
+ av_dict_set(dict, "tls_verify", opts->tls_verify ? "1" : "0", 0);
+ if (opts->tls_ca_file)
+ av_dict_set(dict, "ca_file", opts->tls_ca_file, 0);
+ if (opts->tls_cert_file)
+ av_dict_set(dict, "cert_file", opts->tls_cert_file, 0);
+ if (opts->tls_key_file)
+ av_dict_set(dict, "key_file", opts->tls_key_file, 0);
+ char *cust_headers = talloc_strdup(temp, "");
+ if (opts->referrer) {
+ cust_headers = talloc_asprintf_append(cust_headers, "Referer: %s\r\n",
+ opts->referrer);
+ }
+ if (opts->http_header_fields) {
+ for (int n = 0; opts->http_header_fields[n]; n++) {
+ cust_headers = talloc_asprintf_append(cust_headers, "%s\r\n",
+ opts->http_header_fields[n]);
+ }
+ }
+ if (strlen(cust_headers))
+ av_dict_set(dict, "headers", cust_headers, 0);
+ av_dict_set(dict, "icy", "1", 0);
+ // So far, every known protocol uses microseconds for this
+ // Except rtsp.
+ if (opts->timeout > 0) {
+ if (target_fmt && strcmp(target_fmt, "rtsp") == 0) {
+ mp_verbose(log, "Broken FFmpeg RTSP API => not setting timeout.\n");
+ } else {
+ char buf[80];
+ snprintf(buf, sizeof(buf), "%lld", (long long)(opts->timeout * 1e6));
+ av_dict_set(dict, "timeout", buf, 0);
+ }
+ }
+ if (opts->http_proxy && opts->http_proxy[0])
+ av_dict_set(dict, "http_proxy", opts->http_proxy, 0);
+
+ mp_set_avdict(dict, opts->avopts);
+
+ talloc_free(temp);
+}
+
+// Escape http URLs with unescaped, invalid characters in them.
+// libavformat's http protocol does not do this, and a patch to add this
+// in a 100% safe case (spaces only) was rejected.
+static char *normalize_url(void *ta_parent, const char *filename)
+{
+ bstr proto = mp_split_proto(bstr0(filename), NULL);
+ for (int n = 0; http_like[n]; n++) {
+ if (bstr_equals0(proto, http_like[n]))
+ // Escape everything but reserved characters.
+ // Also don't double-scape, so include '%'.
+ return mp_url_escape(ta_parent, filename, ":/?#[]@!$&'()*+,;=%");
+ }
+ return (char *)filename;
+}
+
+static int open_f(stream_t *stream)
+{
+ AVIOContext *avio = NULL;
+ int res = STREAM_ERROR;
+ AVDictionary *dict = NULL;
+ void *temp = talloc_new(NULL);
+
+ stream->seek = NULL;
+ stream->seekable = false;
+
+ int flags = stream->mode == STREAM_WRITE ? AVIO_FLAG_WRITE : AVIO_FLAG_READ;
+
+ const char *filename = stream->url;
+ if (!filename) {
+ MP_ERR(stream, "No URL\n");
+ goto out;
+ }
+ for (int i = 0; i < sizeof(prefix) / sizeof(prefix[0]); i++)
+ if (!strncmp(filename, prefix[i], strlen(prefix[i])))
+ filename += strlen(prefix[i]);
+ if (!strncmp(filename, "rtsp:", 5) || !strncmp(filename, "rtsps:", 6)) {
+ /* This is handled as a special demuxer, without a separate
+ * stream layer. demux_lavf will do all the real work. Note
+ * that libavformat doesn't even provide a protocol entry for
+ * this (the rtsp demuxer's probe function checks for a "rtsp:"
+ * filename prefix), so it has to be handled specially here.
+ */
+ stream->demuxer = "lavf";
+ stream->lavf_type = "rtsp";
+ talloc_free(temp);
+ return STREAM_OK;
+ }
+
+ // Replace "mms://" with "mmsh://", so that most mms:// URLs just work.
+ // Replace "dav://" or "webdav://" with "http://" and "davs://" or "webdavs://" with "https://"
+ bstr b_filename = bstr0(filename);
+ if (bstr_eatstart0(&b_filename, "mms://") ||
+ bstr_eatstart0(&b_filename, "mmshttp://"))
+ {
+ filename = talloc_asprintf(temp, "mmsh://%.*s", BSTR_P(b_filename));
+ } else if (bstr_eatstart0(&b_filename, "dav://") || bstr_eatstart0(&b_filename, "webdav://"))
+ {
+ filename = talloc_asprintf(temp, "http://%.*s", BSTR_P(b_filename));
+ } else if (bstr_eatstart0(&b_filename, "davs://") || bstr_eatstart0(&b_filename, "webdavs://"))
+ {
+ filename = talloc_asprintf(temp, "https://%.*s", BSTR_P(b_filename));
+ }
+
+ av_dict_set(&dict, "reconnect", "1", 0);
+ av_dict_set(&dict, "reconnect_delay_max", "7", 0);
+
+ mp_setup_av_network_options(&dict, NULL, stream->global, stream->log);
+
+ AVIOInterruptCB cb = {
+ .callback = interrupt_cb,
+ .opaque = stream,
+ };
+
+ filename = normalize_url(stream, filename);
+
+ if (strncmp(filename, "rtmp", 4) == 0) {
+ stream->demuxer = "lavf";
+ stream->lavf_type = "flv";
+ // Setting timeout enables listen mode - force it to disabled.
+ av_dict_set(&dict, "timeout", "0", 0);
+ }
+
+ int err = avio_open2(&avio, filename, flags, &cb, &dict);
+ if (err < 0) {
+ if (err == AVERROR_PROTOCOL_NOT_FOUND)
+ MP_ERR(stream, "Protocol not found. Make sure"
+ " ffmpeg/Libav is compiled with networking support.\n");
+ goto out;
+ }
+
+ mp_avdict_print_unset(stream->log, MSGL_V, dict);
+
+ if (avio->av_class) {
+ uint8_t *mt = NULL;
+ if (av_opt_get(avio, "mime_type", AV_OPT_SEARCH_CHILDREN, &mt) >= 0) {
+ stream->mime_type = talloc_strdup(stream, mt);
+ av_free(mt);
+ }
+ }
+
+ stream->priv = avio;
+ stream->seekable = avio->seekable & AVIO_SEEKABLE_NORMAL;
+ stream->seek = stream->seekable ? seek : NULL;
+ stream->fill_buffer = fill_buffer;
+ stream->write_buffer = write_buffer;
+ stream->get_size = get_size;
+ stream->control = control;
+ stream->close = close_f;
+ // enable cache (should be avoided for files, but no way to detect this)
+ stream->streaming = true;
+ if (stream->info->stream_origin == STREAM_ORIGIN_NET)
+ stream->is_network = true;
+ res = STREAM_OK;
+
+out:
+ av_dict_free(&dict);
+ talloc_free(temp);
+ return res;
+}
+
+static struct mp_tags *read_icy(stream_t *s)
+{
+ AVIOContext *avio = s->priv;
+
+ if (!avio->av_class)
+ return NULL;
+
+ uint8_t *icy_header = NULL;
+ if (av_opt_get(avio, "icy_metadata_headers", AV_OPT_SEARCH_CHILDREN,
+ &icy_header) < 0)
+ icy_header = NULL;
+
+ uint8_t *icy_packet;
+ if (av_opt_get(avio, "icy_metadata_packet", AV_OPT_SEARCH_CHILDREN,
+ &icy_packet) < 0)
+ icy_packet = NULL;
+
+ // Send a metadata update only 1. on start, and 2. on a new metadata packet.
+ // To detect new packages, set the icy_metadata_packet to "-" once we've
+ // read it (a bit hacky, but works).
+
+ struct mp_tags *res = NULL;
+ if ((!icy_header || !icy_header[0]) && (!icy_packet || !icy_packet[0]))
+ goto done;
+
+ bstr packet = bstr0(icy_packet);
+ if (bstr_equals0(packet, "-"))
+ goto done;
+
+ res = talloc_zero(NULL, struct mp_tags);
+
+ bstr header = bstr0(icy_header);
+ while (header.len) {
+ bstr line = bstr_strip_linebreaks(bstr_getline(header, &header));
+ bstr name, val;
+ if (bstr_split_tok(line, ": ", &name, &val))
+ mp_tags_set_bstr(res, name, val);
+ }
+
+ bstr head = bstr0("StreamTitle='");
+ int i = bstr_find(packet, head);
+ if (i >= 0) {
+ packet = bstr_cut(packet, i + head.len);
+ int end = bstr_find(packet, bstr0("\';"));
+ packet = bstr_splice(packet, 0, end);
+
+ bool allocated = false;
+ struct demux_opts *opts = mp_get_config_group(NULL, s->global, &demux_conf);
+ const char *charset = mp_charset_guess(s, s->log, packet, opts->meta_cp, 0);
+ if (charset && !mp_charset_is_utf8(charset)) {
+ bstr conv = mp_iconv_to_utf8(s->log, packet, charset, 0);
+ if (conv.start && conv.start != packet.start) {
+ allocated = true;
+ packet = conv;
+ }
+ }
+ mp_tags_set_bstr(res, bstr0("icy-title"), packet);
+ talloc_free(opts);
+ if (allocated)
+ talloc_free(packet.start);
+ }
+
+ av_opt_set(avio, "icy_metadata_packet", "-", AV_OPT_SEARCH_CHILDREN);
+
+done:
+ av_free(icy_header);
+ av_free(icy_packet);
+ return res;
+}
+
+const stream_info_t stream_info_ffmpeg = {
+ .name = "ffmpeg",
+ .open = open_f,
+ .protocols = (const char *const[]){
+ "rtmp", "rtsp", "rtsps", "http", "https", "mms", "mmst", "mmsh", "mmshttp",
+ "rtp", "httpproxy", "rtmpe", "rtmps", "rtmpt", "rtmpte", "rtmpts", "srt",
+ "rist", "srtp", "gopher", "gophers", "data", "ipfs", "ipns", "dav",
+ "davs", "webdav", "webdavs",
+ NULL },
+ .can_write = true,
+ .stream_origin = STREAM_ORIGIN_NET,
+};
+
+// Unlike above, this is not marked as safe, and can contain protocols which
+// may do insecure things. (Such as "ffmpeg", which can access the "lavfi"
+// pseudo-demuxer, which in turn gives access to filters that can access the
+// local filesystem.)
+const stream_info_t stream_info_ffmpeg_unsafe = {
+ .name = "ffmpeg",
+ .open = open_f,
+ .protocols = (const char *const[]){
+ "lavf", "ffmpeg", "udp", "ftp", "tcp", "tls", "unix", "sftp", "md5",
+ "concat", "smb",
+ NULL },
+ .stream_origin = STREAM_ORIGIN_UNSAFE,
+ .can_write = true,
+};
diff --git a/stream/stream_libarchive.c b/stream/stream_libarchive.c
new file mode 100644
index 0000000..ff2d512
--- /dev/null
+++ b/stream/stream_libarchive.c
@@ -0,0 +1,623 @@
+/*
+ * 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 <archive.h>
+#include <archive_entry.h>
+
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "misc/thread_tools.h"
+#include "stream.h"
+
+#include "stream_libarchive.h"
+
+#define MP_ARCHIVE_FLAG_MAYBE_ZIP (MP_ARCHIVE_FLAG_PRIV << 0)
+#define MP_ARCHIVE_FLAG_MAYBE_RAR (MP_ARCHIVE_FLAG_PRIV << 1)
+#define MP_ARCHIVE_FLAG_MAYBE_VOLUMES (MP_ARCHIVE_FLAG_PRIV << 2)
+
+struct mp_archive_volume {
+ struct mp_archive *mpa;
+ int index; // volume number (starting with 0, mp_archive.primary_src)
+ struct stream *src; // NULL => not current volume, or 0 sized dummy stream
+ int64_t seek_to;
+ char *url;
+};
+
+static bool probe_rar(struct stream *s)
+{
+ static uint8_t rar_sig[] = {0x52, 0x61, 0x72, 0x21, 0x1a, 0x07};
+ uint8_t buf[6];
+ if (stream_read_peek(s, buf, sizeof(buf)) != sizeof(buf))
+ return false;
+ return memcmp(buf, rar_sig, 6) == 0;
+}
+
+static bool probe_multi_rar(struct stream *s)
+{
+ uint8_t hdr[14];
+ if (stream_read_peek(s, hdr, sizeof(hdr)) == sizeof(hdr)) {
+ // Look for rar mark head & main head (assume they're in order).
+ if (hdr[6] == 0x00 && hdr[7 + 2] == 0x73) {
+ int rflags = hdr[7 + 3] | (hdr[7 + 4] << 8);
+ return rflags & 0x100;
+ }
+ }
+ return false;
+}
+
+static bool probe_zip(struct stream *s)
+{
+ uint8_t p[4];
+ if (stream_read_peek(s, p, sizeof(p)) != sizeof(p))
+ return false;
+ // Lifted from libarchive, BSD license.
+ if (p[0] == 'P' && p[1] == 'K') {
+ if ((p[2] == '\001' && p[3] == '\002') ||
+ (p[2] == '\003' && p[3] == '\004') ||
+ (p[2] == '\005' && p[3] == '\006') ||
+ (p[2] == '\006' && p[3] == '\006') ||
+ (p[2] == '\007' && p[3] == '\010') ||
+ (p[2] == '0' && p[3] == '0'))
+ return true;
+ }
+ return false;
+}
+
+static int mp_archive_probe(struct stream *src)
+{
+ int flags = 0;
+ assert(stream_tell(src) == 0);
+ if (probe_zip(src))
+ flags |= MP_ARCHIVE_FLAG_MAYBE_ZIP;
+
+ if (probe_rar(src)) {
+ flags |= MP_ARCHIVE_FLAG_MAYBE_RAR;
+ if (probe_multi_rar(src))
+ flags |= MP_ARCHIVE_FLAG_MAYBE_VOLUMES;
+ }
+ return flags;
+}
+
+static bool volume_seek(struct mp_archive_volume *vol)
+{
+ if (!vol->src || vol->seek_to < 0)
+ return true;
+ bool r = stream_seek(vol->src, vol->seek_to);
+ vol->seek_to = -1;
+ return r;
+}
+
+static ssize_t read_cb(struct archive *arch, void *priv, const void **buffer)
+{
+ struct mp_archive_volume *vol = priv;
+ if (!vol->src)
+ return 0;
+ if (!volume_seek(vol))
+ return -1;
+ int res = stream_read_partial(vol->src, vol->mpa->buffer,
+ sizeof(vol->mpa->buffer));
+ *buffer = vol->mpa->buffer;
+ return MPMAX(res, 0);
+}
+
+// lazy seek to avoid problems with end seeking over http
+static int64_t seek_cb(struct archive *arch, void *priv,
+ int64_t offset, int whence)
+{
+ struct mp_archive_volume *vol = priv;
+ if (!vol->src)
+ return 0;
+ switch (whence) {
+ case SEEK_SET:
+ vol->seek_to = offset;
+ break;
+ case SEEK_CUR:
+ if (vol->seek_to < 0)
+ vol->seek_to = stream_tell(vol->src);
+ vol->seek_to += offset;
+ break;
+ case SEEK_END: ;
+ int64_t size = stream_get_size(vol->src);
+ if (size < 0)
+ return -1;
+ vol->seek_to = size + offset;
+ break;
+ default:
+ return -1;
+ }
+ return vol->seek_to;
+}
+
+static int64_t skip_cb(struct archive *arch, void *priv, int64_t request)
+{
+ struct mp_archive_volume *vol = priv;
+ if (!vol->src)
+ return request;
+ if (!volume_seek(vol))
+ return -1;
+ int64_t old = stream_tell(vol->src);
+ stream_seek_skip(vol->src, old + request);
+ return stream_tell(vol->src) - old;
+}
+
+static int open_cb(struct archive *arch, void *priv)
+{
+ struct mp_archive_volume *vol = priv;
+ vol->seek_to = -1;
+ if (!vol->src) {
+ // Avoid annoying warnings/latency for known dummy volumes.
+ if (vol->index >= vol->mpa->num_volumes)
+ return ARCHIVE_OK;
+ MP_INFO(vol->mpa, "Opening volume '%s'...\n", vol->url);
+ vol->src = stream_create(vol->url,
+ STREAM_READ |
+ vol->mpa->primary_src->stream_origin,
+ vol->mpa->primary_src->cancel,
+ vol->mpa->primary_src->global);
+ // We pretend that failure to open a stream means it was not found,
+ // we assume in turn means that the volume doesn't exist (since
+ // libarchive builds volumes as some sort of abstraction on top of its
+ // stream layer, and its rar code cannot access volumes or signal
+ // anything related to this). libarchive also encounters a fatal error
+ // when a volume could not be opened. However, due to the way volume
+ // support works, it is fine with 0-sized volumes, which we simulate
+ // whenever vol->src==NULL for an opened volume.
+ if (!vol->src) {
+ vol->mpa->num_volumes = MPMIN(vol->mpa->num_volumes, vol->index);
+ MP_INFO(vol->mpa, "Assuming the volume above was not needed.\n");
+ }
+ return ARCHIVE_OK;
+ }
+
+ // just rewind the primary stream
+ return stream_seek(vol->src, 0) ? ARCHIVE_OK : ARCHIVE_FATAL;
+}
+
+static void volume_close(struct mp_archive_volume *vol)
+{
+ // don't close the primary stream
+ if (vol->src && vol->src != vol->mpa->primary_src) {
+ free_stream(vol->src);
+ vol->src = NULL;
+ }
+}
+
+static int close_cb(struct archive *arch, void *priv)
+{
+ struct mp_archive_volume *vol = priv;
+ volume_close(vol);
+ return ARCHIVE_OK;
+}
+
+static void mp_archive_close(struct mp_archive *mpa)
+{
+ if (mpa && mpa->arch) {
+ archive_read_close(mpa->arch);
+ archive_read_free(mpa->arch);
+ mpa->arch = NULL;
+ }
+}
+
+// Supposedly we're not allowed to continue reading on FATAL returns. Otherwise
+// crashes and other UB is possible. Assume calling the close/free functions is
+// still ok. Return true if it was fatal and the archive was closed.
+static bool mp_archive_check_fatal(struct mp_archive *mpa, int r)
+{
+ if (r > ARCHIVE_FATAL)
+ return false;
+ MP_FATAL(mpa, "fatal error received - closing archive\n");
+ mp_archive_close(mpa);
+ return true;
+}
+
+void mp_archive_free(struct mp_archive *mpa)
+{
+ mp_archive_close(mpa);
+ if (mpa && mpa->locale)
+ freelocale(mpa->locale);
+ talloc_free(mpa);
+}
+
+static bool add_volume(struct mp_archive *mpa, struct stream *src,
+ const char* url, int index)
+{
+ struct mp_archive_volume *vol = talloc_zero(mpa, struct mp_archive_volume);
+ vol->index = index;
+ vol->mpa = mpa;
+ vol->src = src;
+ vol->url = talloc_strdup(vol, url);
+ locale_t oldlocale = uselocale(mpa->locale);
+ bool res = archive_read_append_callback_data(mpa->arch, vol) == ARCHIVE_OK;
+ uselocale(oldlocale);
+ return res;
+}
+
+static char *standard_volume_url(void *ctx, const char *format,
+ struct bstr base, int index)
+{
+ return talloc_asprintf(ctx, format, BSTR_P(base), index);
+}
+
+static char *old_rar_volume_url(void *ctx, const char *format,
+ struct bstr base, int index)
+{
+ return talloc_asprintf(ctx, format, BSTR_P(base),
+ 'r' + index / 100, index % 100);
+}
+
+struct file_pattern {
+ const char *match;
+ const char *format;
+ char *(*volume_url)(void *ctx, const char *format,
+ struct bstr base, int index);
+ int start;
+ int stop;
+ bool legacy;
+};
+
+static const struct file_pattern patterns[] = {
+ { ".part1.rar", "%.*s.part%.1d.rar", standard_volume_url, 2, 9 },
+ { ".part01.rar", "%.*s.part%.2d.rar", standard_volume_url, 2, 99 },
+ { ".part001.rar", "%.*s.part%.3d.rar", standard_volume_url, 2, 999 },
+ { ".part0001.rar", "%.*s.part%.4d.rar", standard_volume_url, 2, 9999 },
+ { ".rar", "%.*s.%c%.2d", old_rar_volume_url, 0, 99, true },
+ { ".001", "%.*s.%.3d", standard_volume_url, 2, 9999 },
+ { NULL, NULL, NULL, 0, 0 },
+};
+
+static bool find_volumes(struct mp_archive *mpa, int flags)
+{
+ struct bstr primary_url = bstr0(mpa->primary_src->url);
+
+ const struct file_pattern *pattern = patterns;
+ while (pattern->match) {
+ if (bstr_endswith0(primary_url, pattern->match))
+ break;
+ pattern++;
+ }
+
+ if (!pattern->match)
+ return true;
+ if (pattern->legacy && !(flags & MP_ARCHIVE_FLAG_MAYBE_VOLUMES))
+ return true;
+
+ struct bstr base = bstr_splice(primary_url, 0, -(int)strlen(pattern->match));
+ for (int i = pattern->start; i <= pattern->stop; i++) {
+ char* url = pattern->volume_url(mpa, pattern->format, base, i);
+
+ if (!add_volume(mpa, NULL, url, i + 1))
+ return false;
+ }
+
+ MP_WARN(mpa, "This appears to be a multi-volume archive.\n"
+ "Support is not very good due to libarchive limitations.\n"
+ "There are known cases of libarchive crashing mpv on these.\n"
+ "This is also an excessively inefficient and stupid way to distribute\n"
+ "media files. People creating them should rethink this.\n");
+
+ return true;
+}
+
+static struct mp_archive *mp_archive_new_raw(struct mp_log *log,
+ struct stream *src,
+ int flags, int max_volumes)
+{
+ struct mp_archive *mpa = talloc_zero(NULL, struct mp_archive);
+ mpa->log = log;
+ mpa->locale = newlocale(LC_CTYPE_MASK, "C.UTF-8", (locale_t)0);
+ if (!mpa->locale) {
+ mpa->locale = newlocale(LC_CTYPE_MASK, "", (locale_t)0);
+ if (!mpa->locale)
+ goto err;
+ }
+ mpa->arch = archive_read_new();
+ mpa->primary_src = src;
+ if (!mpa->arch)
+ goto err;
+
+ mpa->flags = flags;
+ mpa->num_volumes = max_volumes ? max_volumes : INT_MAX;
+
+ // first volume is the primary stream
+ if (!add_volume(mpa, src, src->url, 0))
+ goto err;
+
+ if (!(flags & MP_ARCHIVE_FLAG_NO_VOLUMES)) {
+ // try to open other volumes
+ if (!find_volumes(mpa, flags))
+ goto err;
+ }
+
+ locale_t oldlocale = uselocale(mpa->locale);
+
+ archive_read_support_format_rar(mpa->arch);
+ archive_read_support_format_rar5(mpa->arch);
+
+ // Exclude other formats if it's probably RAR, because other formats may
+ // behave suboptimal with multiple volumes exposed, such as opening every
+ // single volume by seeking at the end of the file.
+ if (!(flags & MP_ARCHIVE_FLAG_MAYBE_RAR)) {
+ archive_read_support_format_7zip(mpa->arch);
+ archive_read_support_format_iso9660(mpa->arch);
+ archive_read_support_filter_bzip2(mpa->arch);
+ archive_read_support_filter_gzip(mpa->arch);
+ archive_read_support_filter_xz(mpa->arch);
+ archive_read_support_format_zip_streamable(mpa->arch);
+
+ // This zip reader is normally preferable. However, it seeks to the end
+ // of the file, which may be annoying (HTTP reconnect, volume skipping),
+ // so use it only as last resort, or if it's relatively likely that it's
+ // really zip.
+ if (flags & (MP_ARCHIVE_FLAG_UNSAFE | MP_ARCHIVE_FLAG_MAYBE_ZIP))
+ archive_read_support_format_zip_seekable(mpa->arch);
+ }
+
+ archive_read_set_read_callback(mpa->arch, read_cb);
+ archive_read_set_skip_callback(mpa->arch, skip_cb);
+ archive_read_set_open_callback(mpa->arch, open_cb);
+ // Allow it to close a volume.
+ archive_read_set_close_callback(mpa->arch, close_cb);
+ if (mpa->primary_src->seekable)
+ archive_read_set_seek_callback(mpa->arch, seek_cb);
+ bool fail = archive_read_open1(mpa->arch) < ARCHIVE_OK;
+
+ uselocale(oldlocale);
+
+ if (fail)
+ goto err;
+
+ return mpa;
+
+err:
+ mp_archive_free(mpa);
+ return NULL;
+}
+
+struct mp_archive *mp_archive_new(struct mp_log *log, struct stream *src,
+ int flags, int max_volumes)
+{
+ flags |= mp_archive_probe(src);
+ return mp_archive_new_raw(log, src, flags, max_volumes);
+}
+
+// Iterate entries. The first call establishes the first entry. Returns false
+// if no entry found, otherwise returns true and sets mpa->entry/entry_filename.
+bool mp_archive_next_entry(struct mp_archive *mpa)
+{
+ mpa->entry = NULL;
+ talloc_free(mpa->entry_filename);
+ mpa->entry_filename = NULL;
+
+ if (!mpa->arch)
+ return false;
+
+ locale_t oldlocale = uselocale(mpa->locale);
+ bool success = false;
+
+ while (!mp_cancel_test(mpa->primary_src->cancel)) {
+ struct archive_entry *entry;
+ int r = archive_read_next_header(mpa->arch, &entry);
+ if (r == ARCHIVE_EOF)
+ break;
+ if (r < ARCHIVE_OK)
+ MP_ERR(mpa, "%s\n", archive_error_string(mpa->arch));
+ if (r < ARCHIVE_WARN) {
+ MP_FATAL(mpa, "could not read archive entry\n");
+ mp_archive_check_fatal(mpa, r);
+ break;
+ }
+ if (archive_entry_filetype(entry) != AE_IFREG)
+ continue;
+ // Some archives may have no filenames, or libarchive won't return some.
+ const char *fn = archive_entry_pathname(entry);
+ char buf[64];
+ if (!fn || bstr_validate_utf8(bstr0(fn)) < 0) {
+ snprintf(buf, sizeof(buf), "mpv_unknown#%d", mpa->entry_num);
+ fn = buf;
+ }
+ mpa->entry = entry;
+ mpa->entry_filename = talloc_strdup(mpa, fn);
+ mpa->entry_num += 1;
+ success = true;
+ break;
+ }
+
+ uselocale(oldlocale);
+
+ return success;
+}
+
+struct priv {
+ struct mp_archive *mpa;
+ bool broken_seek;
+ struct stream *src;
+ int64_t entry_size;
+ char *entry_name;
+};
+
+static int reopen_archive(stream_t *s)
+{
+ struct priv *p = s->priv;
+ s->pos = 0;
+ if (!p->mpa) {
+ p->mpa = mp_archive_new(s->log, p->src, MP_ARCHIVE_FLAG_UNSAFE, 0);
+ } else {
+ int flags = p->mpa->flags;
+ int num_volumes = p->mpa->num_volumes;
+ mp_archive_free(p->mpa);
+ p->mpa = mp_archive_new_raw(s->log, p->src, flags, num_volumes);
+ }
+
+ if (!p->mpa)
+ return STREAM_ERROR;
+
+ // Follows the same logic as demux_libarchive.c.
+ struct mp_archive *mpa = p->mpa;
+ while (mp_archive_next_entry(mpa)) {
+ if (strcmp(p->entry_name, mpa->entry_filename) == 0) {
+ locale_t oldlocale = uselocale(mpa->locale);
+ p->entry_size = -1;
+ if (archive_entry_size_is_set(mpa->entry))
+ p->entry_size = archive_entry_size(mpa->entry);
+ uselocale(oldlocale);
+ return STREAM_OK;
+ }
+ }
+
+ mp_archive_free(p->mpa);
+ p->mpa = NULL;
+ MP_ERR(s, "archive entry not found. '%s'\n", p->entry_name);
+ return STREAM_ERROR;
+}
+
+static int archive_entry_fill_buffer(stream_t *s, void *buffer, int max_len)
+{
+ struct priv *p = s->priv;
+ if (!p->mpa)
+ return 0;
+ locale_t oldlocale = uselocale(p->mpa->locale);
+ int r = archive_read_data(p->mpa->arch, buffer, max_len);
+ if (r < 0) {
+ MP_ERR(s, "%s\n", archive_error_string(p->mpa->arch));
+ if (mp_archive_check_fatal(p->mpa, r)) {
+ mp_archive_free(p->mpa);
+ p->mpa = NULL;
+ }
+ }
+ uselocale(oldlocale);
+ return r;
+}
+
+static int archive_entry_seek(stream_t *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+ if (p->mpa && !p->broken_seek) {
+ locale_t oldlocale = uselocale(p->mpa->locale);
+ int r = archive_seek_data(p->mpa->arch, newpos, SEEK_SET);
+ uselocale(oldlocale);
+ if (r >= 0)
+ return 1;
+ MP_WARN(s, "possibly unsupported seeking - switching to reopening\n");
+ p->broken_seek = true;
+ if (reopen_archive(s) < STREAM_OK)
+ return -1;
+ }
+ // libarchive can't seek in most formats.
+ if (newpos < s->pos) {
+ // Hack seeking backwards into working by reopening the archive and
+ // starting over.
+ MP_VERBOSE(s, "trying to reopen archive for performing seek\n");
+ if (reopen_archive(s) < STREAM_OK)
+ return -1;
+ }
+ if (newpos > s->pos) {
+ if (!p->mpa && reopen_archive(s) < STREAM_OK)
+ return -1;
+ // For seeking forwards, just keep reading data (there's no libarchive
+ // skip function either).
+ char buffer[4096];
+ while (newpos > s->pos) {
+ if (mp_cancel_test(s->cancel))
+ return -1;
+
+ int size = MPMIN(newpos - s->pos, sizeof(buffer));
+ locale_t oldlocale = uselocale(p->mpa->locale);
+ int r = archive_read_data(p->mpa->arch, buffer, size);
+ if (r <= 0) {
+ if (r == 0 && newpos > p->entry_size) {
+ MP_ERR(s, "demuxer trying to seek beyond end of archive "
+ "entry\n");
+ } else if (r == 0) {
+ MP_ERR(s, "end of archive entry reached while seeking\n");
+ } else {
+ MP_ERR(s, "%s\n", archive_error_string(p->mpa->arch));
+ }
+ uselocale(oldlocale);
+ if (mp_archive_check_fatal(p->mpa, r)) {
+ mp_archive_free(p->mpa);
+ p->mpa = NULL;
+ }
+ return -1;
+ }
+ uselocale(oldlocale);
+ s->pos += r;
+ }
+ }
+ return 1;
+}
+
+static void archive_entry_close(stream_t *s)
+{
+ struct priv *p = s->priv;
+ mp_archive_free(p->mpa);
+ free_stream(p->src);
+}
+
+static int64_t archive_entry_get_size(stream_t *s)
+{
+ struct priv *p = s->priv;
+ return p->entry_size;
+}
+
+static int archive_entry_open(stream_t *stream)
+{
+ struct priv *p = talloc_zero(stream, struct priv);
+ stream->priv = p;
+
+ if (!strchr(stream->path, '|'))
+ return STREAM_ERROR;
+
+ char *base = talloc_strdup(p, stream->path);
+ char *name = strchr(base, '|');
+ if (!name)
+ return STREAM_ERROR;
+ *name++ = '\0';
+ if (name[0] == '/')
+ name += 1;
+ p->entry_name = name;
+ mp_url_unescape_inplace(base);
+
+ p->src = stream_create(base, STREAM_READ | stream->stream_origin,
+ stream->cancel, stream->global);
+ if (!p->src) {
+ archive_entry_close(stream);
+ return STREAM_ERROR;
+ }
+
+ int r = reopen_archive(stream);
+ if (r < STREAM_OK) {
+ archive_entry_close(stream);
+ return r;
+ }
+
+ stream->fill_buffer = archive_entry_fill_buffer;
+ if (p->src->seekable) {
+ stream->seek = archive_entry_seek;
+ stream->seekable = true;
+ }
+ stream->close = archive_entry_close;
+ stream->get_size = archive_entry_get_size;
+ stream->streaming = true;
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_libarchive = {
+ .name = "libarchive",
+ .open = archive_entry_open,
+ .protocols = (const char*const[]){ "archive", NULL },
+};
diff --git a/stream/stream_libarchive.h b/stream/stream_libarchive.h
new file mode 100644
index 0000000..151e4ee
--- /dev/null
+++ b/stream/stream_libarchive.h
@@ -0,0 +1,35 @@
+#include <locale.h>
+#include "osdep/io.h"
+
+#ifdef __APPLE__
+# include <string.h>
+# include <xlocale.h>
+#endif
+
+struct mp_log;
+
+struct mp_archive {
+ locale_t locale;
+ struct mp_log *log;
+ struct archive *arch;
+ struct stream *primary_src;
+ char buffer[4096];
+ int flags;
+ int num_volumes; // INT_MAX if unknown (initial state)
+
+ // Current entry, as set by mp_archive_next_entry().
+ struct archive_entry *entry;
+ char *entry_filename;
+ int entry_num;
+};
+
+void mp_archive_free(struct mp_archive *mpa);
+
+#define MP_ARCHIVE_FLAG_UNSAFE (1 << 0)
+#define MP_ARCHIVE_FLAG_NO_VOLUMES (1 << 1)
+#define MP_ARCHIVE_FLAG_PRIV (1 << 2)
+
+struct mp_archive *mp_archive_new(struct mp_log *log, struct stream *src,
+ int flags, int max_volumes);
+
+bool mp_archive_next_entry(struct mp_archive *mpa);
diff --git a/stream/stream_memory.c b/stream/stream_memory.c
new file mode 100644
index 0000000..e4696a7
--- /dev/null
+++ b/stream/stream_memory.c
@@ -0,0 +1,100 @@
+/*
+ * 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 "common/common.h"
+#include "stream.h"
+
+struct priv {
+ bstr data;
+};
+
+static int fill_buffer(stream_t *s, void *buffer, int len)
+{
+ struct priv *p = s->priv;
+ bstr data = p->data;
+ if (s->pos < 0 || s->pos > data.len)
+ return 0;
+ len = MPMIN(len, data.len - s->pos);
+ memcpy(buffer, data.start + s->pos, len);
+ return len;
+}
+
+static int seek(stream_t *s, int64_t newpos)
+{
+ return 1;
+}
+
+static int64_t get_size(stream_t *s)
+{
+ struct priv *p = s->priv;
+ return p->data.len;
+}
+
+static int open2(stream_t *stream, const struct stream_open_args *args)
+{
+ stream->fill_buffer = fill_buffer;
+ stream->seek = seek;
+ stream->seekable = true;
+ stream->get_size = get_size;
+
+ struct priv *p = talloc_zero(stream, struct priv);
+ stream->priv = p;
+
+ // Initial data
+ bstr data = bstr0(stream->url);
+ bool use_hex = bstr_eatstart0(&data, "hex://");
+ if (!use_hex)
+ bstr_eatstart0(&data, "memory://");
+
+ if (args->special_arg)
+ data = *(bstr *)args->special_arg;
+
+ p->data = bstrdup(stream, data);
+
+ if (use_hex && !bstr_decode_hex(stream, p->data, &p->data)) {
+ MP_FATAL(stream, "Invalid data.\n");
+ return STREAM_ERROR;
+ }
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_memory = {
+ .name = "memory",
+ .open2 = open2,
+ .protocols = (const char*const[]){ "memory", "hex", NULL },
+};
+
+// The data is copied.
+// Caller may need to set stream.stream_origin correctly.
+struct stream *stream_memory_open(struct mpv_global *global, void *data, int len)
+{
+ assert(len >= 0);
+
+ struct stream_open_args sargs = {
+ .global = global,
+ .url = "memory://",
+ .flags = STREAM_READ | STREAM_SILENT | STREAM_ORIGIN_DIRECT,
+ .sinfo = &stream_info_memory,
+ .special_arg = &(bstr){data, len},
+ };
+
+ struct stream *s = NULL;
+ stream_create_with_args(&sargs, &s);
+ MP_HANDLE_OOM(s);
+ return s;
+}
diff --git a/stream/stream_mf.c b/stream/stream_mf.c
new file mode 100644
index 0000000..0160d7c
--- /dev/null
+++ b/stream/stream_mf.c
@@ -0,0 +1,41 @@
+/*
+ * stream layer for multiple files input, based on previous work from Albeu
+ *
+ * Copyright (C) 2006 Benjamin Zores
+ * Original author: Albeu
+ *
+ * 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 "stream.h"
+
+static int
+mf_stream_open (stream_t *stream)
+{
+ stream->demuxer = "mf";
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_mf = {
+ .name = "mf",
+ .open = mf_stream_open,
+ .protocols = (const char*const[]){ "mf", NULL },
+ .stream_origin = STREAM_ORIGIN_FS,
+};
diff --git a/stream/stream_null.c b/stream/stream_null.c
new file mode 100644
index 0000000..4027c1b
--- /dev/null
+++ b/stream/stream_null.c
@@ -0,0 +1,35 @@
+/*
+ * Original author: Albeu
+ *
+ * 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 "stream.h"
+
+static int open_s(stream_t *stream)
+{
+ return 1;
+}
+
+const stream_info_t stream_info_null = {
+ .name = "null",
+ .open = open_s,
+ .protocols = (const char*const[]){ "null", NULL },
+ .can_write = true,
+};
diff --git a/stream/stream_slice.c b/stream/stream_slice.c
new file mode 100644
index 0000000..c0dbeeb
--- /dev/null
+++ b/stream/stream_slice.c
@@ -0,0 +1,181 @@
+/*
+ * 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 <libavutil/common.h>
+#include <stdint.h>
+#include <string.h>
+#include <sys/types.h>
+#include <unistd.h>
+
+#include "common/common.h"
+#include "options/m_option.h"
+#include "options/path.h"
+#include "stream.h"
+
+struct priv {
+ int64_t slice_start;
+ int64_t slice_max_end; // 0 for no limit
+ struct stream *inner;
+};
+
+static int fill_buffer(struct stream *s, void *buffer, int len)
+{
+ struct priv *p = s->priv;
+
+ if (p->slice_max_end) {
+ // We don't simply use (s->pos >= size) to avoid early return
+ // if the file is still being appended to.
+ if (s->pos + p->slice_start >= p->slice_max_end)
+ return -1;
+ // Avoid rading beyond p->slice_max_end
+ len = MPMIN(len, p->slice_max_end - s->pos);
+ }
+
+ return stream_read_partial(p->inner, buffer, len);
+}
+
+static int seek(struct stream *s, int64_t newpos)
+{
+ struct priv *p = s->priv;
+ return stream_seek(p->inner, newpos + p->slice_start);
+}
+
+static int64_t get_size(struct stream *s)
+{
+ struct priv *p = s->priv;
+ int64_t size = stream_get_size(p->inner);
+ if (size <= 0)
+ return size;
+ if (size <= p->slice_start)
+ return 0;
+ if (p->slice_max_end)
+ size = MPMIN(size, p->slice_max_end);
+ return size - p->slice_start;
+}
+
+static void s_close(struct stream *s)
+{
+ struct priv *p = s->priv;
+ free_stream(p->inner);
+}
+
+static int parse_slice_range(stream_t *stream)
+{
+ struct priv *p = stream->priv;
+
+ struct bstr b_url = bstr0(stream->url);
+ struct bstr proto_with_range, inner_url;
+
+ bool has_at = bstr_split_tok(b_url, "@", &proto_with_range, &inner_url);
+
+ if (!has_at) {
+ MP_ERR(stream, "Expected slice://start[-end]@URL: '%s'\n", stream->url);
+ return STREAM_ERROR;
+ }
+
+ if (!inner_url.len) {
+ MP_ERR(stream, "URL expected to follow 'slice://start[-end]@': '%s'.\n", stream->url);
+ return STREAM_ERROR;
+ }
+ stream->path = bstrto0(stream, inner_url);
+
+ mp_split_proto(proto_with_range, &proto_with_range);
+ struct bstr range = proto_with_range;
+
+ struct bstr start, end;
+ bool has_end = bstr_split_tok(range, "-", &start, &end);
+
+ if (!start.len) {
+ MP_ERR(stream, "The byte range must have a start, and it can't be negative: '%s'\n", stream->url);
+ return STREAM_ERROR;
+ }
+
+ if (has_end && !end.len) {
+ MP_ERR(stream, "The byte range end can be omitted, but it can't be empty: '%s'\n", stream->url);
+ return STREAM_ERROR;
+ }
+
+ const struct m_option opt = {
+ .type = &m_option_type_byte_size,
+ };
+
+ if (m_option_parse(stream->log, &opt, bstr0("slice_start"), start, &p->slice_start) < 0)
+ return STREAM_ERROR;
+
+ bool max_end_is_offset = bstr_startswith0(end, "+");
+ if (has_end) {
+ if (m_option_parse(stream->log, &opt, bstr0("slice_max_end"), end, &p->slice_max_end) < 0)
+ return STREAM_ERROR;
+ }
+
+ if (max_end_is_offset)
+ p->slice_max_end += p->slice_start;
+
+ if (p->slice_max_end && p->slice_max_end < p->slice_start) {
+ MP_ERR(stream, "The byte range end (%"PRId64") can't be smaller than the start (%"PRId64"): '%s'\n",
+ p->slice_max_end,
+ p->slice_start,
+ stream->url);
+ return STREAM_ERROR;
+ }
+
+ return STREAM_OK;
+}
+
+static int open2(struct stream *stream, const struct stream_open_args *args)
+{
+ struct priv *p = talloc_zero(stream, struct priv);
+ stream->priv = p;
+
+ stream->fill_buffer = fill_buffer;
+ stream->seek = seek;
+ stream->get_size = get_size;
+ stream->close = s_close;
+
+ int parse_ret = parse_slice_range(stream);
+ if (parse_ret != STREAM_OK) {
+ return parse_ret;
+ }
+
+ struct stream_open_args args2 = *args;
+ args2.url = stream->path;
+ int inner_ret = stream_create_with_args(&args2, &p->inner);
+ if (inner_ret != STREAM_OK) {
+ return inner_ret;
+ }
+
+ if (!p->inner->seekable) {
+ MP_FATAL(stream, "Non-seekable stream '%s' can't be used with 'slice://'\n", p->inner->url);
+ free_stream(p->inner);
+ return STREAM_ERROR;
+ }
+
+ stream->seekable = 1;
+ stream->stream_origin = p->inner->stream_origin;
+
+ if (p->slice_start)
+ seek(stream, 0);
+
+ return STREAM_OK;
+}
+
+const stream_info_t stream_info_slice = {
+ .name = "slice",
+ .open2 = open2,
+ .protocols = (const char*const[]){ "slice", NULL },
+ .can_write = false,
+};