diff options
Diffstat (limited to 'stream')
-rw-r--r-- | stream/cookies.c | 138 | ||||
-rw-r--r-- | stream/cookies.h | 31 | ||||
-rw-r--r-- | stream/dvb_tune.c | 652 | ||||
-rw-r--r-- | stream/dvb_tune.h | 42 | ||||
-rw-r--r-- | stream/dvbin.h | 143 | ||||
-rw-r--r-- | stream/stream.c | 900 | ||||
-rw-r--r-- | stream/stream.h | 269 | ||||
-rw-r--r-- | stream/stream_avdevice.c | 32 | ||||
-rw-r--r-- | stream/stream_bluray.c | 632 | ||||
-rw-r--r-- | stream/stream_cb.c | 108 | ||||
-rw-r--r-- | stream/stream_cdda.c | 369 | ||||
-rw-r--r-- | stream/stream_concat.c | 179 | ||||
-rw-r--r-- | stream/stream_dvb.c | 1161 | ||||
-rw-r--r-- | stream/stream_dvdnav.c | 719 | ||||
-rw-r--r-- | stream/stream_edl.c | 17 | ||||
-rw-r--r-- | stream/stream_file.c | 377 | ||||
-rw-r--r-- | stream/stream_lavf.c | 457 | ||||
-rw-r--r-- | stream/stream_libarchive.c | 623 | ||||
-rw-r--r-- | stream/stream_libarchive.h | 35 | ||||
-rw-r--r-- | stream/stream_memory.c | 100 | ||||
-rw-r--r-- | stream/stream_mf.c | 41 | ||||
-rw-r--r-- | stream/stream_null.c | 35 | ||||
-rw-r--r-- | stream/stream_slice.c | 181 |
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, +}; |