Files
mpv/stream/network.c
T
2026-10-08 02:13:29 +02:00

285 lines
9.0 KiB
C

/*
* 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 <float.h>
#include <string.h>
#include <libavformat/version.h>
#include "common/common.h"
#include "common/tags.h"
#include "demux/demux.h"
#include "misc/charset_conv.h"
#include "mpv_talloc.h"
#include "network.h"
#include "options/m_config.h"
#include "options/m_option.h"
#include "stream.h"
#define OPT_BASE_STRUCT struct mp_network_opts
const struct m_sub_options mp_network_conf = {
.opts = (const m_option_t[]) {
{"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 mp_network_opts),
.defaults = &(const struct mp_network_opts){
.useragent = "libmpv",
.timeout = 60,
#if HAVE_LIBCURL || LIBAVFORMAT_VERSION_MAJOR >= 63
.tls_verify = true,
#endif
},
};
struct mp_icy {
uint64_t metaint; // bytes between metadata blocks (0 = no ICY)
uint64_t data_read; // data bytes since last metadata block
enum {
ICY_DATA = 0,
ICY_LEN,
ICY_META,
} state;
size_t meta_pending; // bytes left in the current metadata block
size_t meta_pos; // bytes already accumulated in meta_buf
char meta_buf[255 * 16 + 1];
bstr headers; // accumulated "Icy-Name: value\n" lines
bstr packet; // last metadata payload
bool dirty; // new metadata to deliver
};
struct mp_icy *mp_icy_new(void *ta_parent)
{
return talloc_zero(ta_parent, struct mp_icy);
}
void mp_icy_reset(struct mp_icy *i)
{
i->metaint = 0;
i->data_read = 0;
i->state = ICY_DATA;
i->meta_pending = 0;
i->meta_pos = 0;
i->headers.len = 0;
i->packet.len = 0;
i->dirty = false;
}
void mp_icy_add_header(struct mp_icy *i, bstr line)
{
bstr name, val;
if (!bstr_split_tok(line, ":", &name, &val))
return;
if (!bstr_case_startswith(name, bstr0("Icy-")))
return;
val = bstr_strip(val);
if (bstrcasecmp0(name, "Icy-MetaInt") == 0) {
long long mi = bstrtoll(val, NULL, 10);
if (mi > 0)
i->metaint = mi;
return;
}
// This may look a bit weird, that we join headers again, but it's done
// to share common parse function with lavf format later.
bstr_xappend_asprintf(i, &i->headers, "%.*s: %.*s\n", BSTR_P(name), BSTR_P(val));
i->dirty = true;
}
bool mp_icy_active(const struct mp_icy *i)
{
return i->metaint > 0;
}
void mp_icy_process(struct mp_icy *i, const char *buf, size_t len,
mp_icy_write_fn write_cb, void *ctx)
{
if (!mp_icy_active(i)) {
if (len)
write_cb(ctx, buf, len);
return;
}
size_t pos = 0;
while (pos < len) {
switch (i->state) {
case ICY_DATA: {
size_t budget = i->metaint - i->data_read;
size_t take = MPMIN(len - pos, budget);
write_cb(ctx, buf + pos, take);
pos += take;
i->data_read += take;
if (i->data_read == i->metaint)
i->state = ICY_LEN;
break;
}
case ICY_LEN: {
uint8_t n = (uint8_t)buf[pos++];
if (n == 0) {
i->state = ICY_DATA;
i->data_read = 0;
} else {
i->meta_pending = (size_t)n * 16;
i->meta_pos = 0;
i->state = ICY_META;
}
break;
}
case ICY_META: {
size_t take = MPMIN(len - pos, i->meta_pending);
memcpy(i->meta_buf + i->meta_pos, buf + pos, take);
i->meta_pos += take;
i->meta_pending -= take;
pos += take;
if (i->meta_pending == 0) {
i->meta_buf[i->meta_pos] = '\0';
i->packet.len = 0;
bstr_xappend_asprintf(i, &i->packet, "%s\n", i->meta_buf);
i->dirty = true;
i->state = ICY_DATA;
i->data_read = 0;
}
break;
}
}
}
}
struct mp_tags *mp_icy_get_metadata(struct mp_icy *i, struct stream *s)
{
if (!i->dirty)
return NULL;
i->dirty = false;
return mp_parse_icy_metadata(s, i->headers, i->packet);
}
struct mp_tags *mp_parse_icy_metadata(struct stream *s, bstr headers, bstr packet)
{
if (!headers.len && !packet.len)
return NULL;
struct mp_tags *res = talloc_zero(NULL, struct mp_tags);
while (headers.len) {
bstr line = bstr_strip_linebreaks(bstr_getline(headers, &headers));
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("\';"));
if (end >= 0)
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);
}
return res;
}
static bstr header_to_utf8(void *tmp, struct stream *s, bstr text, const char *cp)
{
const char *charset = mp_charset_guess(tmp, s->log, text, cp, 0);
bstr conv = mp_iconv_to_utf8(s->log, text, charset, 0);
if (conv.start != text.start)
talloc_steal(tmp, conv.start);
return conv;
}
char *mp_parse_content_disposition(void *talloc_ctx, struct stream *s, bstr value)
{
void *tmp = talloc_new(NULL);
bstr plain = {0}, ext = {0};
// disposition-type *( ";" disposition-parm ). A quoted value
// may contain ";" or "=", so find its closing quote before splitting.
while (value.len) {
int eq = bstrchr(value, '=');
int semi = bstrchr(value, ';');
if (eq < 0 || (semi >= 0 && semi < eq)) {
if (semi < 0)
break;
value = bstr_cut(value, semi + 1);
continue;
}
bstr name = bstr_strip(bstr_splice(value, 0, eq));
value = bstr_lstrip(bstr_cut(value, eq + 1));
bstr val;
if (bstr_eatstart0(&value, "\"")) {
int end = bstrchr(value, '"');
val = end < 0 ? value : bstr_splice(value, 0, end);
value = bstr_cut(value, val.len);
semi = bstrchr(value, ';');
} else {
semi = bstrchr(value, ';');
val = bstr_strip(semi < 0 ? value : bstr_splice(value, 0, semi));
}
value = semi < 0 ? (bstr){0} : bstr_cut(value, semi + 1);
if (bstrcasecmp0(name, "filename*") == 0) {
ext = val;
} else if (bstrcasecmp0(name, "filename") == 0) {
plain = val;
}
}
bstr name = {0};
bstr charset, rest;
if (bstr_split_tok(ext, "'", &charset, &rest) &&
bstr_split_tok(rest, "'", &(bstr){0}, &rest))
{
bstr decoded = bstr0(mp_url_unescape(tmp, bstrto0(tmp, rest)));
name = header_to_utf8(tmp, s, decoded, bstrto0(tmp, charset));
}
if (!name.len && plain.len) {
struct demux_opts *opts = mp_get_config_group(tmp, s->global, &demux_conf);
name = header_to_utf8(tmp, s, plain, opts->meta_cp);
}
int sep = MPMAX(bstrrchr(name, '/'), bstrrchr(name, '\\'));
name = bstr_strip(bstr_cut(name, sep + 1));
char *res = name.len ? bstrto0(talloc_ctx, name) : NULL;
talloc_free(tmp);
return res;
}