diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
commit | 51de1d8436100f725f3576aefa24a2bd2057bc28 (patch) | |
tree | c6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /common | |
parent | Initial commit. (diff) | |
download | mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip |
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'common')
-rw-r--r-- | common/av_common.c | 404 | ||||
-rw-r--r-- | common/av_common.h | 54 | ||||
-rw-r--r-- | common/av_log.c | 215 | ||||
-rw-r--r-- | common/av_log.h | 11 | ||||
-rw-r--r-- | common/codecs.c | 107 | ||||
-rw-r--r-- | common/codecs.h | 48 | ||||
-rw-r--r-- | common/common.c | 413 | ||||
-rw-r--r-- | common/common.h | 161 | ||||
-rw-r--r-- | common/encode.h | 61 | ||||
-rw-r--r-- | common/encode_lavc.c | 949 | ||||
-rw-r--r-- | common/encode_lavc.h | 114 | ||||
-rw-r--r-- | common/global.h | 15 | ||||
-rw-r--r-- | common/meson.build | 8 | ||||
-rw-r--r-- | common/msg.c | 1069 | ||||
-rw-r--r-- | common/msg.h | 92 | ||||
-rw-r--r-- | common/msg_control.h | 45 | ||||
-rw-r--r-- | common/playlist.c | 413 | ||||
-rw-r--r-- | common/playlist.h | 121 | ||||
-rw-r--r-- | common/recorder.c | 422 | ||||
-rw-r--r-- | common/recorder.h | 25 | ||||
-rw-r--r-- | common/stats.c | 325 | ||||
-rw-r--r-- | common/stats.h | 34 | ||||
-rw-r--r-- | common/tags.c | 151 | ||||
-rw-r--r-- | common/tags.h | 31 | ||||
-rw-r--r-- | common/version.c | 23 | ||||
-rw-r--r-- | common/version.h.in | 7 |
26 files changed, 5318 insertions, 0 deletions
diff --git a/common/av_common.c b/common/av_common.c new file mode 100644 index 0000000..5d07349 --- /dev/null +++ b/common/av_common.c @@ -0,0 +1,404 @@ +/* + * 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 <assert.h> +#include <math.h> +#include <limits.h> + +#include <libavutil/common.h> +#include <libavutil/log.h> +#include <libavutil/dict.h> +#include <libavutil/opt.h> +#include <libavutil/error.h> +#include <libavutil/cpu.h> +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> + +#include "config.h" + +#include "audio/chmap_avchannel.h" +#include "common/common.h" +#include "common/msg.h" +#include "demux/packet.h" +#include "demux/stheader.h" +#include "misc/bstr.h" +#include "video/fmt-conversion.h" +#include "av_common.h" +#include "codecs.h" + +enum AVMediaType mp_to_av_stream_type(int type) +{ + switch (type) { + case STREAM_VIDEO: return AVMEDIA_TYPE_VIDEO; + case STREAM_AUDIO: return AVMEDIA_TYPE_AUDIO; + case STREAM_SUB: return AVMEDIA_TYPE_SUBTITLE; + default: return AVMEDIA_TYPE_UNKNOWN; + } +} + +AVCodecParameters *mp_codec_params_to_av(const struct mp_codec_params *c) +{ + AVCodecParameters *avp = avcodec_parameters_alloc(); + if (!avp) + return NULL; + + // If we have lavf demuxer params, they overwrite by definition any others. + if (c->lav_codecpar) { + if (avcodec_parameters_copy(avp, c->lav_codecpar) < 0) + goto error; + return avp; + } + + avp->codec_type = mp_to_av_stream_type(c->type); + avp->codec_id = mp_codec_to_av_codec_id(c->codec); + avp->codec_tag = c->codec_tag; + if (c->extradata_size) { + uint8_t *extradata = c->extradata; + int size = c->extradata_size; + + if (avp->codec_id == AV_CODEC_ID_FLAC) { + // ffmpeg expects FLAC extradata to be just the STREAMINFO, + // so grab only that (and assume it'll be the first block) + if (size >= 8 && !memcmp(c->extradata, "fLaC", 4)) { + extradata += 8; + size = MPMIN(34, size - 8); // FLAC_STREAMINFO_SIZE + } + } + + avp->extradata = av_mallocz(size + AV_INPUT_BUFFER_PADDING_SIZE); + if (!avp->extradata) + goto error; + avp->extradata_size = size; + memcpy(avp->extradata, extradata, size); + } + avp->bits_per_coded_sample = c->bits_per_coded_sample; + + // Video only + avp->width = c->disp_w; + avp->height = c->disp_h; + + // Audio only + avp->sample_rate = c->samplerate; + avp->bit_rate = c->bitrate; + avp->block_align = c->block_align; + +#if !HAVE_AV_CHANNEL_LAYOUT + avp->channels = c->channels.num; + if (!mp_chmap_is_unknown(&c->channels)) + avp->channel_layout = mp_chmap_to_lavc(&c->channels); +#else + mp_chmap_to_av_layout(&avp->ch_layout, &c->channels); +#endif + + return avp; +error: + avcodec_parameters_free(&avp); + return NULL; +} + +// Set avctx codec headers for decoding. Returns <0 on failure. +int mp_set_avctx_codec_headers(AVCodecContext *avctx, const struct mp_codec_params *c) +{ + enum AVMediaType codec_type = avctx->codec_type; + enum AVCodecID codec_id = avctx->codec_id; + AVCodecParameters *avp = mp_codec_params_to_av(c); + if (!avp) + return -1; + + int r = avcodec_parameters_to_context(avctx, avp) < 0 ? -1 : 0; + avcodec_parameters_free(&avp); + + if (avctx->codec_type != AVMEDIA_TYPE_UNKNOWN) + avctx->codec_type = codec_type; + if (avctx->codec_id != AV_CODEC_ID_NONE) + avctx->codec_id = codec_id; + return r; +} + +// Pick a "good" timebase, which will be used to convert double timestamps +// back to fractions for passing them through libavcodec. +AVRational mp_get_codec_timebase(const struct mp_codec_params *c) +{ + AVRational tb = {c->native_tb_num, c->native_tb_den}; + if (tb.num < 1 || tb.den < 1) { + if (c->reliable_fps) + tb = av_inv_q(av_d2q(c->fps, 1000000)); + if (tb.num < 1 || tb.den < 1) + tb = AV_TIME_BASE_Q; + } + + // If the timebase is too coarse, raise its precision, or small adjustments + // to timestamps done between decoder and demuxer could be lost. + if (av_q2d(tb) > 0.001) { + AVRational r = av_div_q(tb, (AVRational){1, 1000}); + tb.den *= (r.num + r.den - 1) / r.den; + } + + av_reduce(&tb.num, &tb.den, tb.num, tb.den, INT_MAX); + + if (tb.num < 1 || tb.den < 1) + tb = AV_TIME_BASE_Q; + + return tb; +} + +static AVRational get_def_tb(AVRational *tb) +{ + return tb && tb->num > 0 && tb->den > 0 ? *tb : AV_TIME_BASE_Q; +} + +// Convert the mpv style timestamp (seconds as double) to a libavcodec style +// timestamp (integer units in a given timebase). +int64_t mp_pts_to_av(double mp_pts, AVRational *tb) +{ + AVRational b = get_def_tb(tb); + return mp_pts == MP_NOPTS_VALUE ? AV_NOPTS_VALUE : llrint(mp_pts / av_q2d(b)); +} + +// Inverse of mp_pts_to_av(). (The timebases must be exactly the same.) +double mp_pts_from_av(int64_t av_pts, AVRational *tb) +{ + AVRational b = get_def_tb(tb); + return av_pts == AV_NOPTS_VALUE ? MP_NOPTS_VALUE : av_pts * av_q2d(b); +} + +// Set dst from mpkt. Note that dst is not refcountable. +// mpkt can be NULL to generate empty packets (used to flush delayed data). +// Sets pts/dts using mp_pts_to_av(ts, tb). (Be aware of the implications.) +// Set duration field only if tb is set. +void mp_set_av_packet(AVPacket *dst, struct demux_packet *mpkt, AVRational *tb) +{ + dst->side_data = NULL; + dst->side_data_elems = 0; + dst->buf = NULL; + av_packet_unref(dst); + + dst->data = mpkt ? mpkt->buffer : NULL; + dst->size = mpkt ? mpkt->len : 0; + /* Some codecs (ZeroCodec, some cases of PNG) may want keyframe info + * from demuxer. */ + if (mpkt && mpkt->keyframe) + dst->flags |= AV_PKT_FLAG_KEY; + if (mpkt && mpkt->avpacket) { + dst->side_data = mpkt->avpacket->side_data; + dst->side_data_elems = mpkt->avpacket->side_data_elems; + if (dst->data == mpkt->avpacket->data) + dst->buf = mpkt->avpacket->buf; + dst->flags |= mpkt->avpacket->flags; + } + if (mpkt && tb && tb->num > 0 && tb->den > 0) + dst->duration = mpkt->duration / av_q2d(*tb); + dst->pts = mp_pts_to_av(mpkt ? mpkt->pts : MP_NOPTS_VALUE, tb); + dst->dts = mp_pts_to_av(mpkt ? mpkt->dts : MP_NOPTS_VALUE, tb); +} + +void mp_set_avcodec_threads(struct mp_log *l, AVCodecContext *avctx, int threads) +{ + if (threads == 0) { + threads = av_cpu_count(); + if (threads < 1) { + mp_warn(l, "Could not determine thread count to use, defaulting to 1.\n"); + threads = 1; + } else { + mp_verbose(l, "Detected %d logical cores.\n", threads); + if (threads > 1) + threads += 1; // extra thread for better load balancing + } + // Apparently some libavcodec versions have or had trouble with more + // than 16 threads, and/or print a warning when using > 16. + threads = MPMIN(threads, 16); + } + mp_verbose(l, "Requesting %d threads for decoding.\n", threads); + avctx->thread_count = threads; +} + +static void add_codecs(struct mp_decoder_list *list, enum AVMediaType type, + bool decoders) +{ + void *iter = NULL; + for (;;) { + const AVCodec *cur = av_codec_iterate(&iter); + if (!cur) + break; + if (av_codec_is_decoder(cur) == decoders && + (type == AVMEDIA_TYPE_UNKNOWN || cur->type == type)) + { + mp_add_decoder(list, mp_codec_from_av_codec_id(cur->id), + cur->name, cur->long_name); + } + } +} + +void mp_add_lavc_decoders(struct mp_decoder_list *list, enum AVMediaType type) +{ + add_codecs(list, type, true); +} + +// (Abuses the decoder list data structures.) +void mp_add_lavc_encoders(struct mp_decoder_list *list) +{ + add_codecs(list, AVMEDIA_TYPE_UNKNOWN, false); +} + +char **mp_get_lavf_demuxers(void) +{ + char **list = NULL; + void *iter = NULL; + int num = 0; + for (;;) { + const AVInputFormat *cur = av_demuxer_iterate(&iter); + if (!cur) + break; + MP_TARRAY_APPEND(NULL, list, num, talloc_strdup(NULL, cur->name)); + } + MP_TARRAY_APPEND(NULL, list, num, NULL); + return list; +} + +int mp_codec_to_av_codec_id(const char *codec) +{ + int id = AV_CODEC_ID_NONE; + if (codec) { + const AVCodecDescriptor *desc = avcodec_descriptor_get_by_name(codec); + if (desc) + id = desc->id; + if (id == AV_CODEC_ID_NONE) { + const AVCodec *avcodec = avcodec_find_decoder_by_name(codec); + if (avcodec) + id = avcodec->id; + } + } + return id; +} + +const char *mp_codec_from_av_codec_id(int codec_id) +{ + const char *name = NULL; + const AVCodecDescriptor *desc = avcodec_descriptor_get(codec_id); + if (desc) + name = desc->name; + if (!name) { + const AVCodec *avcodec = avcodec_find_decoder(codec_id); + if (avcodec) + name = avcodec->name; + } + return name; +} + +bool mp_codec_is_lossless(const char *codec) +{ + const AVCodecDescriptor *desc = + avcodec_descriptor_get(mp_codec_to_av_codec_id(codec)); + return desc && (desc->props & AV_CODEC_PROP_LOSSLESS); +} + +// kv is in the format as by OPT_KEYVALUELIST(): kv[0]=key0, kv[1]=val0, ... +// Copy them to the dict. +void mp_set_avdict(AVDictionary **dict, char **kv) +{ + for (int n = 0; kv && kv[n * 2]; n++) + av_dict_set(dict, kv[n * 2 + 0], kv[n * 2 + 1], 0); +} + +// For use with libav* APIs that take AVDictionaries of options. +// Print options remaining in the dict as unset. +void mp_avdict_print_unset(struct mp_log *log, int msgl, AVDictionary *dict) +{ + AVDictionaryEntry *t = NULL; + while ((t = av_dict_get(dict, "", t, AV_DICT_IGNORE_SUFFIX))) + mp_msg(log, msgl, "Could not set AVOption %s='%s'\n", t->key, t->value); +} + +// If the name starts with "@", try to interpret it as a number, and set *name +// to the name of the n-th parameter. +static void resolve_positional_arg(void *avobj, char **name) +{ + if (!*name || (*name)[0] != '@' || !avobj) + return; + + char *end = NULL; + int pos = strtol(*name + 1, &end, 10); + if (!end || *end) + return; + + const AVOption *opt = NULL; + int offset = -1; + while (1) { + opt = av_opt_next(avobj, opt); + if (!opt) + return; + // This is what libavfilter's parser does to skip aliases. + if (opt->offset != offset && opt->type != AV_OPT_TYPE_CONST) + pos--; + if (pos < 0) { + *name = (char *)opt->name; + return; + } + offset = opt->offset; + } +} + +// kv is in the format as by OPT_KEYVALUELIST(): kv[0]=key0, kv[1]=val0, ... +// Set these options on given avobj (using av_opt_set..., meaning avobj must +// point to a struct that has AVClass as first member). +// Options which fail to set (error or not found) are printed to log. +// Returns: >=0 success, <0 failed to set an option +int mp_set_avopts(struct mp_log *log, void *avobj, char **kv) +{ + return mp_set_avopts_pos(log, avobj, avobj, kv); +} + +// Like mp_set_avopts(), but the posargs argument is used to resolve positional +// arguments. If posargs==NULL, positional args are disabled. +int mp_set_avopts_pos(struct mp_log *log, void *avobj, void *posargs, char **kv) +{ + int success = 0; + for (int n = 0; kv && kv[n * 2]; n++) { + char *k = kv[n * 2 + 0]; + char *v = kv[n * 2 + 1]; + resolve_positional_arg(posargs, &k); + int r = av_opt_set(avobj, k, v, AV_OPT_SEARCH_CHILDREN); + if (r == AVERROR_OPTION_NOT_FOUND) { + mp_err(log, "AVOption '%s' not found.\n", k); + success = -1; + } else if (r < 0) { + char errstr[80]; + av_strerror(r, errstr, sizeof(errstr)); + mp_err(log, "Could not set AVOption %s='%s' (%s)\n", k, v, errstr); + success = -1; + } + } + return success; +} + +/** + * Must be used to free an AVPacket that was used with mp_set_av_packet(). + * + * We have a particular pattern where we "borrow" buffers and set them + * into an AVPacket to pass data to ffmpeg without extra copies. + * This applies to buf and side_data, so this function clears them before + * freeing. + */ +void mp_free_av_packet(AVPacket **pkt) +{ + if (*pkt) { + (*pkt)->side_data = NULL; + (*pkt)->side_data_elems = 0; + (*pkt)->buf = NULL; + } + av_packet_free(pkt); +} diff --git a/common/av_common.h b/common/av_common.h new file mode 100644 index 0000000..1f05e14 --- /dev/null +++ b/common/av_common.h @@ -0,0 +1,54 @@ +/* + * 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 MP_AVCOMMON_H +#define MP_AVCOMMON_H + +#include <inttypes.h> +#include <stdbool.h> + +#include <libavutil/avutil.h> +#include <libavutil/rational.h> +#include <libavcodec/avcodec.h> + +struct mp_decoder_list; +struct demux_packet; +struct mp_codec_params; +struct AVDictionary; +struct mp_log; + +enum AVMediaType mp_to_av_stream_type(int type); +AVCodecParameters *mp_codec_params_to_av(const struct mp_codec_params *c); +int mp_set_avctx_codec_headers(AVCodecContext *avctx, const struct mp_codec_params *c); +AVRational mp_get_codec_timebase(const struct mp_codec_params *c); +void mp_set_av_packet(AVPacket *dst, struct demux_packet *mpkt, AVRational *tb); +int64_t mp_pts_to_av(double mp_pts, AVRational *tb); +double mp_pts_from_av(int64_t av_pts, AVRational *tb); +void mp_set_avcodec_threads(struct mp_log *l, AVCodecContext *avctx, int threads); +void mp_add_lavc_decoders(struct mp_decoder_list *list, enum AVMediaType type); +void mp_add_lavc_encoders(struct mp_decoder_list *list); +char **mp_get_lavf_demuxers(void); +int mp_codec_to_av_codec_id(const char *codec); +const char *mp_codec_from_av_codec_id(int codec_id); +bool mp_codec_is_lossless(const char *codec); +void mp_set_avdict(struct AVDictionary **dict, char **kv); +void mp_avdict_print_unset(struct mp_log *log, int msgl, struct AVDictionary *d); +int mp_set_avopts(struct mp_log *log, void *avobj, char **kv); +int mp_set_avopts_pos(struct mp_log *log, void *avobj, void *posargs, char **kv); +void mp_free_av_packet(AVPacket **pkt); + +#endif diff --git a/common/av_log.c b/common/av_log.c new file mode 100644 index 0000000..b6bad04 --- /dev/null +++ b/common/av_log.c @@ -0,0 +1,215 @@ +/* + * av_log to mp_msg converter + * Copyright (C) 2006 Michael Niedermayer + * Copyright (C) 2009 Uoti Urpala + * + * 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 <stdio.h> +#include <stdbool.h> + +#include "av_log.h" +#include "common/common.h" +#include "common/global.h" +#include "common/msg.h" +#include "config.h" +#include "osdep/threads.h" + +#include <libavutil/avutil.h> +#include <libavutil/log.h> +#include <libavutil/version.h> + +#include <libavcodec/avcodec.h> +#include <libavcodec/version.h> +#include <libavformat/avformat.h> +#include <libavformat/version.h> +#include <libswresample/swresample.h> +#include <libswresample/version.h> +#include <libswscale/swscale.h> +#include <libswscale/version.h> +#include <libavfilter/avfilter.h> +#include <libavfilter/version.h> + +#if HAVE_LIBAVDEVICE +#include <libavdevice/avdevice.h> +#endif + +// Needed because the av_log callback does not provide a library-safe message +// callback. +static mp_static_mutex log_lock = MP_STATIC_MUTEX_INITIALIZER; +static struct mpv_global *log_mpv_instance; +static struct mp_log *log_root, *log_decaudio, *log_decvideo, *log_demuxer; +static bool log_print_prefix = true; + +static int av_log_level_to_mp_level(int av_level) +{ + if (av_level > AV_LOG_VERBOSE) + return MSGL_TRACE; + if (av_level > AV_LOG_INFO) + return MSGL_DEBUG; + if (av_level > AV_LOG_WARNING) + return MSGL_V; + if (av_level > AV_LOG_ERROR) + return MSGL_WARN; + if (av_level > AV_LOG_FATAL) + return MSGL_ERR; + return MSGL_FATAL; +} + +static struct mp_log *get_av_log(void *ptr) +{ + if (!ptr) + return log_root; + + AVClass *avc = *(AVClass **)ptr; + if (!avc) { + mp_warn(log_root, + "av_log callback called with bad parameters (NULL AVClass).\n" + "This is a bug in one of Libav/FFmpeg libraries used.\n"); + return log_root; + } + + if (!strcmp(avc->class_name, "AVCodecContext")) { + AVCodecContext *s = ptr; + if (s->codec) { + if (s->codec->type == AVMEDIA_TYPE_AUDIO) { + if (av_codec_is_decoder(s->codec)) + return log_decaudio; + } else if (s->codec->type == AVMEDIA_TYPE_VIDEO) { + if (av_codec_is_decoder(s->codec)) + return log_decvideo; + } + } + } + + if (!strcmp(avc->class_name, "AVFormatContext")) { + AVFormatContext *s = ptr; + if (s->iformat) + return log_demuxer; + } + + return log_root; +} + +static void mp_msg_av_log_callback(void *ptr, int level, const char *fmt, + va_list vl) +{ + AVClass *avc = ptr ? *(AVClass **)ptr : NULL; + int mp_level = av_log_level_to_mp_level(level); + + // Note: mp_log is thread-safe, but destruction of the log instances is not. + mp_mutex_lock(&log_lock); + + if (!log_mpv_instance) { + mp_mutex_unlock(&log_lock); + // Fallback to stderr + vfprintf(stderr, fmt, vl); + return; + } + + struct mp_log *log = get_av_log(ptr); + + if (mp_msg_test(log, mp_level)) { + char buffer[4096] = ""; + int pos = 0; + const char *prefix = avc ? avc->item_name(ptr) : NULL; + if (log_print_prefix && prefix) + pos = snprintf(buffer, sizeof(buffer), "%s: ", prefix); + log_print_prefix = fmt[strlen(fmt) - 1] == '\n'; + + pos = MPMIN(MPMAX(pos, 0), sizeof(buffer)); + vsnprintf(buffer + pos, sizeof(buffer) - pos, fmt, vl); + + mp_msg(log, mp_level, "%s", buffer); + } + + mp_mutex_unlock(&log_lock); +} + +void init_libav(struct mpv_global *global) +{ + mp_mutex_lock(&log_lock); + if (!log_mpv_instance) { + log_mpv_instance = global; + log_root = mp_log_new(NULL, global->log, "ffmpeg"); + log_decaudio = mp_log_new(log_root, log_root, "audio"); + log_decvideo = mp_log_new(log_root, log_root, "video"); + log_demuxer = mp_log_new(log_root, log_root, "demuxer"); + av_log_set_callback(mp_msg_av_log_callback); + } + mp_mutex_unlock(&log_lock); + + avformat_network_init(); + +#if HAVE_LIBAVDEVICE + avdevice_register_all(); +#endif +} + +void uninit_libav(struct mpv_global *global) +{ + mp_mutex_lock(&log_lock); + if (log_mpv_instance == global) { + av_log_set_callback(av_log_default_callback); + log_mpv_instance = NULL; + talloc_free(log_root); + } + mp_mutex_unlock(&log_lock); +} + +#define V(x) AV_VERSION_MAJOR(x), \ + AV_VERSION_MINOR(x), \ + AV_VERSION_MICRO(x) + +struct lib { + const char *name; + unsigned buildv; + unsigned runv; +}; + +void check_library_versions(struct mp_log *log, int v) +{ + const struct lib libs[] = { + {"libavutil", LIBAVUTIL_VERSION_INT, avutil_version()}, + {"libavcodec", LIBAVCODEC_VERSION_INT, avcodec_version()}, + {"libavformat", LIBAVFORMAT_VERSION_INT, avformat_version()}, + {"libswscale", LIBSWSCALE_VERSION_INT, swscale_version()}, + {"libavfilter", LIBAVFILTER_VERSION_INT, avfilter_version()}, + {"libswresample", LIBSWRESAMPLE_VERSION_INT, swresample_version()}, + }; + + mp_msg(log, v, "FFmpeg version: %s\n", av_version_info()); + mp_msg(log, v, "FFmpeg library versions:\n"); + + for (int n = 0; n < MP_ARRAY_SIZE(libs); n++) { + const struct lib *l = &libs[n]; + mp_msg(log, v, " %-15s %d.%d.%d", l->name, V(l->buildv)); + if (l->buildv != l->runv) + mp_msg(log, v, " (runtime %d.%d.%d)", V(l->runv)); + mp_msg(log, v, "\n"); + if (l->buildv > l->runv || + AV_VERSION_MAJOR(l->buildv) != AV_VERSION_MAJOR(l->runv)) + { + fprintf(stderr, "%s: %d.%d.%d -> %d.%d.%d\n", + l->name, V(l->buildv), V(l->runv)); + abort(); + } + } +} + +#undef V diff --git a/common/av_log.h b/common/av_log.h new file mode 100644 index 0000000..ae12838 --- /dev/null +++ b/common/av_log.h @@ -0,0 +1,11 @@ +#ifndef MP_AV_LOG_H +#define MP_AV_LOG_H + +#include <stdbool.h> + +struct mpv_global; +struct mp_log; +void init_libav(struct mpv_global *global); +void uninit_libav(struct mpv_global *global); +void check_library_versions(struct mp_log *log, int v); +#endif diff --git a/common/codecs.c b/common/codecs.c new file mode 100644 index 0000000..8101b50 --- /dev/null +++ b/common/codecs.c @@ -0,0 +1,107 @@ +/* + * 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 <assert.h> +#include "mpv_talloc.h" +#include "misc/bstr.h" +#include "common/msg.h" +#include "codecs.h" + +void mp_add_decoder(struct mp_decoder_list *list, const char *codec, + const char *decoder, const char *desc) +{ + struct mp_decoder_entry entry = { + .codec = talloc_strdup(list, codec), + .decoder = talloc_strdup(list, decoder), + .desc = talloc_strdup(list, desc), + }; + MP_TARRAY_APPEND(list, list->entries, list->num_entries, entry); +} + +// Add entry, but only if it's not yet on the list, and if the codec matches. +// If codec == NULL, don't compare codecs. +static void add_new(struct mp_decoder_list *to, struct mp_decoder_entry *entry, + const char *codec) +{ + if (!entry || (codec && strcmp(entry->codec, codec) != 0)) + return; + mp_add_decoder(to, entry->codec, entry->decoder, entry->desc); +} + +// Select a decoder from the given list for the given codec. The selection +// can be influenced by the selection string, which can specify a priority +// list of preferred decoders. +// This returns a list of decoders to try, with the preferred decoders first. +// The selection string corresponds to --vd/--ad directly, and has the +// following syntax: +// selection = [<entry> ("," <entry>)*] +// entry = <decoder> // prefer decoder +// entry = "-" <decoder> // exclude a decoder +// entry = "-" // don't add fallback decoders +// Forcing a decoder means it's added even if the codec mismatches. +struct mp_decoder_list *mp_select_decoders(struct mp_log *log, + struct mp_decoder_list *all, + const char *codec, + const char *selection) +{ + struct mp_decoder_list *list = talloc_zero(NULL, struct mp_decoder_list); + if (!codec) + codec = "unknown"; + bool stop = false; + bstr sel = bstr0(selection); + while (sel.len) { + bstr entry; + bstr_split_tok(sel, ",", &entry, &sel); + if (bstr_equals0(entry, "-")) { + mp_warn(log, "Excluding codecs is deprecated.\n"); + stop = true; + break; + } + for (int n = 0; n < all->num_entries; n++) { + struct mp_decoder_entry *cur = &all->entries[n]; + if (bstr_equals0(entry, cur->decoder)) + add_new(list, cur, codec); + } + } + if (!stop) { + // Add the remaining codecs which haven't been added yet + for (int n = 0; n < all->num_entries; n++) + add_new(list, &all->entries[n], codec); + } + return list; +} + +void mp_append_decoders(struct mp_decoder_list *list, struct mp_decoder_list *a) +{ + for (int n = 0; n < a->num_entries; n++) + add_new(list, &a->entries[n], NULL); +} + +void mp_print_decoders(struct mp_log *log, int msgl, const char *header, + struct mp_decoder_list *list) +{ + mp_msg(log, msgl, "%s\n", header); + for (int n = 0; n < list->num_entries; n++) { + struct mp_decoder_entry *entry = &list->entries[n]; + mp_msg(log, msgl, " %s", entry->decoder); + if (strcmp(entry->decoder, entry->codec) != 0) + mp_msg(log, msgl, " (%s)", entry->codec); + mp_msg(log, msgl, " - %s\n", entry->desc); + } + if (list->num_entries == 0) + mp_msg(log, msgl, " (no decoders)\n"); +} diff --git a/common/codecs.h b/common/codecs.h new file mode 100644 index 0000000..367e9f6 --- /dev/null +++ b/common/codecs.h @@ -0,0 +1,48 @@ +/* + * 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 MP_CODECS_H +#define MP_CODECS_H + +struct mp_log; + +struct mp_decoder_entry { + const char *codec; // name of the codec (e.g. "mp3") + const char *decoder; // decoder name (e.g. "mp3float") + const char *desc; // human readable description +}; + +struct mp_decoder_list { + struct mp_decoder_entry *entries; + int num_entries; +}; + +void mp_add_decoder(struct mp_decoder_list *list, const char *codec, + const char *decoder, const char *desc); + +struct mp_decoder_list *mp_select_decoders(struct mp_log *log, + struct mp_decoder_list *all, + const char *codec, + const char *selection); + +void mp_append_decoders(struct mp_decoder_list *list, struct mp_decoder_list *a); + +struct mp_log; +void mp_print_decoders(struct mp_log *log, int msgl, const char *header, + struct mp_decoder_list *list); + +#endif diff --git a/common/common.c b/common/common.c new file mode 100644 index 0000000..9f8230f --- /dev/null +++ b/common/common.c @@ -0,0 +1,413 @@ +/* + * 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 <stdarg.h> +#include <math.h> +#include <assert.h> + +#include <libavutil/common.h> +#include <libavutil/error.h> +#include <libavutil/mathematics.h> + +#include "mpv_talloc.h" +#include "misc/bstr.h" +#include "misc/ctype.h" +#include "common/common.h" +#include "osdep/strnlen.h" + +#define appendf(ptr, ...) \ + do {(*(ptr)) = talloc_asprintf_append_buffer(*(ptr), __VA_ARGS__);} while(0) + +// Return a talloc'ed string formatted according to the format string in fmt. +// On error, return NULL. +// Valid formats: +// %H, %h: hour (%H is padded with 0 to two digits) +// %M: minutes from 00-59 (hours are subtracted) +// %m: total minutes (includes hours, unlike %M) +// %S: seconds from 00-59 (minutes and hours are subtracted) +// %s: total seconds (includes hours and minutes) +// %f: like %s, but as float +// %T: milliseconds (000-999) +char *mp_format_time_fmt(const char *fmt, double time) +{ + if (time == MP_NOPTS_VALUE) + return talloc_strdup(NULL, "unknown"); + char *sign = time < 0 ? "-" : ""; + time = time < 0 ? -time : time; + long long int itime = time; + long long int h, m, tm, s; + int ms = lrint((time - itime) * 1000); + if (ms >= 1000) { + ms -= 1000; + itime += 1; + } + s = itime; + tm = s / 60; + h = s / 3600; + s -= h * 3600; + m = s / 60; + s -= m * 60; + char *res = talloc_strdup(NULL, ""); + while (*fmt) { + if (fmt[0] == '%') { + fmt++; + switch (fmt[0]) { + case 'h': appendf(&res, "%s%lld", sign, h); break; + case 'H': appendf(&res, "%s%02lld", sign, h); break; + case 'm': appendf(&res, "%s%lld", sign, tm); break; + case 'M': appendf(&res, "%02lld", m); break; + case 's': appendf(&res, "%s%lld", sign, itime); break; + case 'S': appendf(&res, "%02lld", s); break; + case 'T': appendf(&res, "%03d", ms); break; + case 'f': appendf(&res, "%f", time); break; + case '%': appendf(&res, "%s", "%"); break; + default: goto error; + } + fmt++; + } else { + appendf(&res, "%c", *fmt); + fmt++; + } + } + return res; +error: + talloc_free(res); + return NULL; +} + +char *mp_format_time(double time, bool fractions) +{ + return mp_format_time_fmt(fractions ? "%H:%M:%S.%T" : "%H:%M:%S", time); +} + +// Set rc to the union of rc and rc2 +void mp_rect_union(struct mp_rect *rc, const struct mp_rect *rc2) +{ + rc->x0 = MPMIN(rc->x0, rc2->x0); + rc->y0 = MPMIN(rc->y0, rc2->y0); + rc->x1 = MPMAX(rc->x1, rc2->x1); + rc->y1 = MPMAX(rc->y1, rc2->y1); +} + +// Returns whether or not a point is contained by rc +bool mp_rect_contains(struct mp_rect *rc, int x, int y) +{ + return rc->x0 <= x && x < rc->x1 && rc->y0 <= y && y < rc->y1; +} + +// Set rc to the intersection of rc and src. +// Return false if the result is empty. +bool mp_rect_intersection(struct mp_rect *rc, const struct mp_rect *rc2) +{ + rc->x0 = MPMAX(rc->x0, rc2->x0); + rc->y0 = MPMAX(rc->y0, rc2->y0); + rc->x1 = MPMIN(rc->x1, rc2->x1); + rc->y1 = MPMIN(rc->y1, rc2->y1); + + return rc->x1 > rc->x0 && rc->y1 > rc->y0; +} + +bool mp_rect_equals(const struct mp_rect *rc1, const struct mp_rect *rc2) +{ + return rc1->x0 == rc2->x0 && rc1->y0 == rc2->y0 && + rc1->x1 == rc2->x1 && rc1->y1 == rc2->y1; +} + +// Rotate mp_rect by 90 degrees increments +void mp_rect_rotate(struct mp_rect *rc, int w, int h, int rotation) +{ + rotation %= 360; + + if (rotation >= 180) { + rotation -= 180; + MPSWAP(int, rc->x0, rc->x1); + MPSWAP(int, rc->y0, rc->y1); + } + + if (rotation == 90) { + *rc = (struct mp_rect) { + .x0 = rc->y1, + .y0 = rc->x0, + .x1 = rc->y0, + .y1 = rc->x1, + }; + } + + if (rc->x1 < rc->x0) { + rc->x0 = w - rc->x0; + rc->x1 = w - rc->x1; + } + + if (rc->y1 < rc->y0) { + rc->y0 = h - rc->y0; + rc->y1 = h - rc->y1; + } +} + +// Compute rc1-rc2, put result in res_array, return number of rectangles in +// res_array. In the worst case, there are 4 rectangles, so res_array must +// provide that much storage space. +int mp_rect_subtract(const struct mp_rect *rc1, const struct mp_rect *rc2, + struct mp_rect res[4]) +{ + struct mp_rect rc = *rc1; + if (!mp_rect_intersection(&rc, rc2)) + return 0; + + int cnt = 0; + if (rc1->y0 < rc.y0) + res[cnt++] = (struct mp_rect){rc1->x0, rc1->y0, rc1->x1, rc.y0}; + if (rc1->x0 < rc.x0) + res[cnt++] = (struct mp_rect){rc1->x0, rc.y0, rc.x0, rc.y1}; + if (rc1->x1 > rc.x1) + res[cnt++] = (struct mp_rect){rc.x1, rc.y0, rc1->x1, rc.y1}; + if (rc1->y1 > rc.y1) + res[cnt++] = (struct mp_rect){rc1->x0, rc.y1, rc1->x1, rc1->y1}; + return cnt; +} + +// This works like snprintf(), except that it starts writing the first output +// character to str[strlen(str)]. This returns the number of characters the +// string would have *appended* assuming a large enough buffer, will make sure +// str is null-terminated, and will never write to str[size] or past. +// Example: +// int example(char *buf, size_t buf_size, double num, char *str) { +// int n = 0; +// n += mp_snprintf_cat(buf, size, "%f", num); +// n += mp_snprintf_cat(buf, size, "%s", str); +// return n; } +// Note how this can be chained with functions similar in style. +int mp_snprintf_cat(char *str, size_t size, const char *format, ...) +{ + size_t len = strnlen(str, size); + assert(!size || len < size); // str with no 0-termination is not allowed + int r; + va_list ap; + va_start(ap, format); + r = vsnprintf(str + len, size - len, format, ap); + va_end(ap); + return r; +} + +// Encode the unicode codepoint as UTF-8, and append to the end of the +// talloc'ed buffer. All guarantees bstr_xappend() give applies, such as +// implicit \0-termination for convenience. +void mp_append_utf8_bstr(void *talloc_ctx, struct bstr *buf, uint32_t codepoint) +{ + char data[8]; + uint8_t tmp; + char *output = data; + PUT_UTF8(codepoint, tmp, *output++ = tmp;); + bstr_xappend(talloc_ctx, buf, (bstr){data, output - data}); +} + +// Parse a C/JSON-style escape beginning at code, and append the result to *str +// using talloc. The input string (*code) must point to the first character +// after the initial '\', and after parsing *code is set to the first character +// after the current escape. +// On error, false is returned, and all input remains unchanged. +static bool mp_parse_escape(void *talloc_ctx, bstr *dst, bstr *code) +{ + if (code->len < 1) + return false; + char replace = 0; + switch (code->start[0]) { + case '"': replace = '"'; break; + case '\\': replace = '\\'; break; + case '/': replace = '/'; break; + case 'b': replace = '\b'; break; + case 'f': replace = '\f'; break; + case 'n': replace = '\n'; break; + case 'r': replace = '\r'; break; + case 't': replace = '\t'; break; + case 'e': replace = '\x1b'; break; + case '\'': replace = '\''; break; + } + if (replace) { + bstr_xappend(talloc_ctx, dst, (bstr){&replace, 1}); + *code = bstr_cut(*code, 1); + return true; + } + if (code->start[0] == 'x' && code->len >= 3) { + bstr num = bstr_splice(*code, 1, 3); + char c = bstrtoll(num, &num, 16); + if (num.len) + return false; + bstr_xappend(talloc_ctx, dst, (bstr){&c, 1}); + *code = bstr_cut(*code, 3); + return true; + } + if (code->start[0] == 'u' && code->len >= 5) { + bstr num = bstr_splice(*code, 1, 5); + uint32_t c = bstrtoll(num, &num, 16); + if (num.len) + return false; + if (c >= 0xd800 && c <= 0xdbff) { + if (code->len < 5 + 6 // udddd + \udddd + || code->start[5] != '\\' || code->start[6] != 'u') + return false; + *code = bstr_cut(*code, 5 + 1); + bstr num2 = bstr_splice(*code, 1, 5); + uint32_t c2 = bstrtoll(num2, &num2, 16); + if (num2.len || c2 < 0xdc00 || c2 > 0xdfff) + return false; + c = ((c - 0xd800) << 10) + 0x10000 + (c2 - 0xdc00); + } + mp_append_utf8_bstr(talloc_ctx, dst, c); + *code = bstr_cut(*code, 5); + return true; + } + return false; +} + +// Like mp_append_escaped_string, but set *dst to sliced *src if no escape +// sequences have to be parsed (i.e. no memory allocation is required), and +// if dst->start was NULL on function entry. +bool mp_append_escaped_string_noalloc(void *talloc_ctx, bstr *dst, bstr *src) +{ + bstr t = *src; + int cur = 0; + while (1) { + if (cur >= t.len || t.start[cur] == '"') { + *src = bstr_cut(t, cur); + t = bstr_splice(t, 0, cur); + if (dst->start == NULL) { + *dst = t; + } else { + bstr_xappend(talloc_ctx, dst, t); + } + return true; + } else if (t.start[cur] == '\\') { + bstr_xappend(talloc_ctx, dst, bstr_splice(t, 0, cur)); + t = bstr_cut(t, cur + 1); + cur = 0; + if (!mp_parse_escape(talloc_ctx, dst, &t)) + goto error; + } else { + cur++; + } + } +error: + return false; +} + +// src is expected to point to a C-style string literal, *src pointing to the +// first char after the starting '"'. It will append the contents of the literal +// to *dst (using talloc_ctx) until the first '"' or the end of *str is found. +// See bstr_xappend() how data is appended to *dst. +// On success, *src will either start with '"', or be empty. +// On error, return false, and *dst will contain the string until the first +// error, *src is not changed. +// Note that dst->start will be implicitly \0-terminated on successful return, +// and if it was NULL or \0-terminated before calling the function. +// As mentioned above, the caller is responsible for skipping the '"' chars. +bool mp_append_escaped_string(void *talloc_ctx, bstr *dst, bstr *src) +{ + if (mp_append_escaped_string_noalloc(talloc_ctx, dst, src)) { + // Guarantee copy (or allocation). + if (!dst->start || dst->start == src->start) { + bstr res = *dst; + *dst = (bstr){0}; + bstr_xappend(talloc_ctx, dst, res); + } + return true; + } + return false; +} + +// Behaves like strerror()/strerror_r(), but is thread- and GNU-safe. +char *mp_strerror_buf(char *buf, size_t buf_size, int errnum) +{ + // This handles the nasty details of calling the right function for us. + av_strerror(AVERROR(errnum), buf, buf_size); + return buf; +} + +char *mp_tag_str_buf(char *buf, size_t buf_size, uint32_t tag) +{ + if (buf_size < 1) + return buf; + buf[0] = '\0'; + for (int n = 0; n < 4; n++) { + uint8_t val = (tag >> (n * 8)) & 0xFF; + if (mp_isalnum(val) || val == '_' || val == ' ') { + mp_snprintf_cat(buf, buf_size, "%c", val); + } else { + mp_snprintf_cat(buf, buf_size, "[%d]", val); + } + } + return buf; +} + +char *mp_tprintf_buf(char *buf, size_t buf_size, const char *format, ...) +{ + va_list ap; + va_start(ap, format); + vsnprintf(buf, buf_size, format, ap); + va_end(ap); + return buf; +} + +char **mp_dup_str_array(void *tctx, char **s) +{ + char **r = NULL; + int num_r = 0; + for (int n = 0; s && s[n]; n++) + MP_TARRAY_APPEND(tctx, r, num_r, talloc_strdup(tctx, s[n])); + if (r) + MP_TARRAY_APPEND(tctx, r, num_r, NULL); + return r; +} + +// Return rounded down integer log 2 of v, i.e. position of highest set bit. +// mp_log2(0) == 0 +// mp_log2(1) == 0 +// mp_log2(31) == 4 +// mp_log2(32) == 5 +unsigned int mp_log2(uint32_t v) +{ +#if defined(__GNUC__) && __GNUC__ >= 4 + return v ? 31 - __builtin_clz(v) : 0; +#else + for (int x = 31; x >= 0; x--) { + if (v & (((uint32_t)1) << x)) + return x; + } + return 0; +#endif +} + +// If a power of 2, return it, otherwise return the next highest one, or 0. +// mp_round_next_power_of_2(65) == 128 +// mp_round_next_power_of_2(64) == 64 +// mp_round_next_power_of_2(0) == 1 +// mp_round_next_power_of_2(UINT32_MAX) == 0 +uint32_t mp_round_next_power_of_2(uint32_t v) +{ + if (!v) + return 1; + if (!(v & (v - 1))) + return v; + int l = mp_log2(v) + 1; + return l == 32 ? 0 : (uint32_t)1 << l; +} + +int mp_lcm(int x, int y) +{ + assert(x && y); + return x * (y / av_gcd(x, y)); +} diff --git a/common/common.h b/common/common.h new file mode 100644 index 0000000..ccdd94b --- /dev/null +++ b/common/common.h @@ -0,0 +1,161 @@ +/* + * 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_MPCOMMON_H +#define MPLAYER_MPCOMMON_H + +#include <assert.h> +#include <stddef.h> +#include <stdlib.h> +#include <stdbool.h> +#include <stdint.h> + +#include "osdep/compiler.h" +#include "mpv_talloc.h" + +// double should be able to represent this exactly +#define MP_NOPTS_VALUE (-0x1p+63) + +#define MP_CONCAT_(a, b) a ## b +#define MP_CONCAT(a, b) MP_CONCAT_(a, b) + +#define MPMAX(a, b) ((a) > (b) ? (a) : (b)) +#define MPMIN(a, b) ((a) > (b) ? (b) : (a)) +#define MPCLAMP(a, min, max) (((a) < (min)) ? (min) : (((a) > (max)) ? (max) : (a))) +#define MPSWAP(type, a, b) \ + do { type SWAP_tmp = b; b = a; a = SWAP_tmp; } while (0) +#define MP_ARRAY_SIZE(s) (sizeof(s) / sizeof((s)[0])) +#define MP_DIV_UP(x, y) (((x) + (y) - 1) / (y)) + +// align must be a power of two (align >= 1), x >= 0 +#define MP_ALIGN_UP(x, align) (((x) + (align) - 1) & ~((align) - 1)) +#define MP_ALIGN_DOWN(x, align) ((x) & ~((align) - 1)) +#define MP_IS_ALIGNED(x, align) (!((x) & ((align) - 1))) +#define MP_IS_POWER_OF_2(x) ((x) > 0 && !((x) & ((x) - 1))) + +// align to non power of two +#define MP_ALIGN_NPOT(x, align) ((align) ? MP_DIV_UP(x, align) * (align) : (x)) + +// Return "a", or if that is NOPTS, return "def". +#define MP_PTS_OR_DEF(a, def) ((a) == MP_NOPTS_VALUE ? (def) : (a)) +// If one of the values is NOPTS, always pick the other one. +#define MP_PTS_MIN(a, b) MPMIN(MP_PTS_OR_DEF(a, b), MP_PTS_OR_DEF(b, a)) +#define MP_PTS_MAX(a, b) MPMAX(MP_PTS_OR_DEF(a, b), MP_PTS_OR_DEF(b, a)) +// Return a+b, unless a is NOPTS. b must not be NOPTS. +#define MP_ADD_PTS(a, b) ((a) == MP_NOPTS_VALUE ? (a) : ((a) + (b))) + +#define CONTROL_OK 1 +#define CONTROL_TRUE 1 +#define CONTROL_FALSE 0 +#define CONTROL_UNKNOWN -1 +#define CONTROL_ERROR -2 +#define CONTROL_NA -3 + +enum stream_type { + STREAM_VIDEO, + STREAM_AUDIO, + STREAM_SUB, + STREAM_TYPE_COUNT, +}; + +enum video_sync { + VS_DEFAULT = 0, + VS_DISP_RESAMPLE, + VS_DISP_RESAMPLE_VDROP, + VS_DISP_RESAMPLE_NONE, + VS_DISP_TEMPO, + VS_DISP_ADROP, + VS_DISP_VDROP, + VS_DISP_NONE, + VS_NONE, +}; + +#define VS_IS_DISP(x) ((x) == VS_DISP_RESAMPLE || \ + (x) == VS_DISP_RESAMPLE_VDROP || \ + (x) == VS_DISP_RESAMPLE_NONE || \ + (x) == VS_DISP_TEMPO || \ + (x) == VS_DISP_ADROP || \ + (x) == VS_DISP_VDROP || \ + (x) == VS_DISP_NONE) + +extern const char mpv_version[]; +extern const char mpv_builddate[]; +extern const char mpv_copyright[]; + +char *mp_format_time(double time, bool fractions); +char *mp_format_time_fmt(const char *fmt, double time); + +struct mp_rect { + int x0, y0; + int x1, y1; +}; + +#define mp_rect_w(r) ((r).x1 - (r).x0) +#define mp_rect_h(r) ((r).y1 - (r).y0) + +void mp_rect_union(struct mp_rect *rc, const struct mp_rect *src); +bool mp_rect_intersection(struct mp_rect *rc, const struct mp_rect *rc2); +bool mp_rect_contains(struct mp_rect *rc, int x, int y); +bool mp_rect_equals(const struct mp_rect *rc1, const struct mp_rect *rc2); +int mp_rect_subtract(const struct mp_rect *rc1, const struct mp_rect *rc2, + struct mp_rect res_array[4]); +void mp_rect_rotate(struct mp_rect *rc, int w, int h, int rotation); + +unsigned int mp_log2(uint32_t v); +uint32_t mp_round_next_power_of_2(uint32_t v); +int mp_lcm(int x, int y); + +int mp_snprintf_cat(char *str, size_t size, const char *format, ...) + PRINTF_ATTRIBUTE(3, 4); + +struct bstr; + +void mp_append_utf8_bstr(void *talloc_ctx, struct bstr *buf, uint32_t codepoint); + +bool mp_append_escaped_string_noalloc(void *talloc_ctx, struct bstr *dst, + struct bstr *src); +bool mp_append_escaped_string(void *talloc_ctx, struct bstr *dst, + struct bstr *src); + +char *mp_strerror_buf(char *buf, size_t buf_size, int errnum); +#define mp_strerror(e) mp_strerror_buf((char[80]){0}, 80, e) + +char *mp_tag_str_buf(char *buf, size_t buf_size, uint32_t tag); +#define mp_tag_str(t) mp_tag_str_buf((char[22]){0}, 22, t) + +// Return a printf(format, ...) formatted string of the given SIZE. SIZE must +// be a compile time constant. The result is allocated on the stack and valid +// only within the current block scope. +#define mp_tprintf(SIZE, format, ...) \ + mp_tprintf_buf((char[SIZE]){0}, (SIZE), (format), __VA_ARGS__) +char *mp_tprintf_buf(char *buf, size_t buf_size, const char *format, ...) + PRINTF_ATTRIBUTE(3, 4); + +char **mp_dup_str_array(void *tctx, char **s); + +// We generally do not handle allocation failure of small malloc()s. This would +// create a large number of rarely tested code paths, which would probably +// regress and cause security issues. We prefer to fail fast. +// This macro generally behaves like an assert(), except it will make sure to +// kill the process even with NDEBUG. +#define MP_HANDLE_OOM(x) do { \ + assert(x); \ + if (!(x)) \ + abort(); \ + } while (0) + +#endif /* MPLAYER_MPCOMMON_H */ diff --git a/common/encode.h b/common/encode.h new file mode 100644 index 0000000..33d778c --- /dev/null +++ b/common/encode.h @@ -0,0 +1,61 @@ +/* + * Muxing/encoding API; ffmpeg specific implementation is in encode_lavc.*. + * + * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.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/>. + */ + +#ifndef MPLAYER_ENCODE_H +#define MPLAYER_ENCODE_H + +#include <stdbool.h> + +#include "demux/demux.h" + +struct mpv_global; +struct mp_log; +struct encode_lavc_context; + +struct encode_opts { + char *file; + char *format; + char **fopts; + char *vcodec; + char **vopts; + char *acodec; + char **aopts; + bool rawts; + bool copy_metadata; + char **set_metadata; + char **remove_metadata; +}; + +// interface for player core +struct encode_lavc_context *encode_lavc_init(struct mpv_global *global); +bool encode_lavc_free(struct encode_lavc_context *ctx); +void encode_lavc_discontinuity(struct encode_lavc_context *ctx); +bool encode_lavc_showhelp(struct mp_log *log, struct encode_opts *options); +int encode_lavc_getstatus(struct encode_lavc_context *ctx, char *buf, int bufsize, float relative_position); +bool encode_lavc_stream_type_ok(struct encode_lavc_context *ctx, + enum stream_type type); +void encode_lavc_expect_stream(struct encode_lavc_context *ctx, + enum stream_type type); +void encode_lavc_set_metadata(struct encode_lavc_context *ctx, + struct mp_tags *metadata); +bool encode_lavc_didfail(struct encode_lavc_context *ctx); // check if encoding failed + +#endif diff --git a/common/encode_lavc.c b/common/encode_lavc.c new file mode 100644 index 0000000..898545d --- /dev/null +++ b/common/encode_lavc.c @@ -0,0 +1,949 @@ +/* + * muxing using libavformat + * + * Copyright (C) 2010 Nicolas George <george@nsup.org> + * Copyright (C) 2011-2012 Rudolf Polzer <divVerent@xonotic.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/>. + */ + +#include <libavutil/avutil.h> +#include <libavutil/timestamp.h> + +#include "encode_lavc.h" +#include "common/av_common.h" +#include "common/global.h" +#include "common/msg.h" +#include "common/msg_control.h" +#include "options/m_config.h" +#include "options/m_option.h" +#include "options/options.h" +#include "osdep/timer.h" +#include "video/out/vo.h" +#include "mpv_talloc.h" +#include "stream/stream.h" + +struct encode_priv { + struct mp_log *log; + + // --- All fields are protected by encode_lavc_context.lock + + bool failed; + + struct mp_tags *metadata; + + AVFormatContext *muxer; + + bool header_written; // muxer was initialized + + struct mux_stream **streams; + int num_streams; + + // Statistics + double t0; + + long long abytes; + long long vbytes; + + unsigned int frames; + double audioseconds; +}; + +struct mux_stream { + int index; // index of this into p->streams[] + char name[80]; + struct encode_lavc_context *ctx; + enum AVMediaType codec_type; + AVRational encoder_timebase; // packet timestamps from encoder + AVStream *st; + void (*on_ready)(void *ctx); // when finishing muxer init + void *on_ready_ctx; +}; + +#define OPT_BASE_STRUCT struct encode_opts +const struct m_sub_options encode_config = { + .opts = (const m_option_t[]) { + {"o", OPT_STRING(file), .flags = CONF_NOCFG | CONF_PRE_PARSE | M_OPT_FILE}, + {"of", OPT_STRING(format)}, + {"ofopts", OPT_KEYVALUELIST(fopts), .flags = M_OPT_HAVE_HELP}, + {"ovc", OPT_STRING(vcodec)}, + {"ovcopts", OPT_KEYVALUELIST(vopts), .flags = M_OPT_HAVE_HELP}, + {"oac", OPT_STRING(acodec)}, + {"oacopts", OPT_KEYVALUELIST(aopts), .flags = M_OPT_HAVE_HELP}, + {"orawts", OPT_BOOL(rawts)}, + {"ocopy-metadata", OPT_BOOL(copy_metadata)}, + {"oset-metadata", OPT_KEYVALUELIST(set_metadata)}, + {"oremove-metadata", OPT_STRINGLIST(remove_metadata)}, + {0} + }, + .size = sizeof(struct encode_opts), + .defaults = &(const struct encode_opts){ + .copy_metadata = true, + }, +}; + +struct encode_lavc_context *encode_lavc_init(struct mpv_global *global) +{ + struct encode_lavc_context *ctx = talloc_ptrtype(NULL, ctx); + *ctx = (struct encode_lavc_context){ + .global = global, + .options = mp_get_config_group(ctx, global, &encode_config), + .priv = talloc_zero(ctx, struct encode_priv), + .log = mp_log_new(ctx, global->log, "encode"), + }; + mp_mutex_init(&ctx->lock); + + struct encode_priv *p = ctx->priv; + p->log = ctx->log; + + const char *filename = ctx->options->file; + + // STUPID STUPID STUPID STUPID avio + // does not support "-" as file name to mean stdin/stdout + // ffmpeg.c works around this too, the same way + if (!strcmp(filename, "-")) + filename = "pipe:1"; + + if (filename && ( + !strcmp(filename, "/dev/stdout") || + !strcmp(filename, "pipe:") || + !strcmp(filename, "pipe:1"))) + mp_msg_force_stderr(global, true); + + encode_lavc_discontinuity(ctx); + + p->muxer = avformat_alloc_context(); + MP_HANDLE_OOM(p->muxer); + + if (ctx->options->format && ctx->options->format[0]) { + ctx->oformat = av_guess_format(ctx->options->format, filename, NULL); + } else { + ctx->oformat = av_guess_format(NULL, filename, NULL); + } + + if (!ctx->oformat) { + MP_FATAL(ctx, "format not found\n"); + goto fail; + } + + p->muxer->oformat = ctx->oformat; + + p->muxer->url = av_strdup(filename); + MP_HANDLE_OOM(p->muxer->url); + + return ctx; + +fail: + p->failed = true; + encode_lavc_free(ctx); + return NULL; +} + +void encode_lavc_set_metadata(struct encode_lavc_context *ctx, + struct mp_tags *metadata) +{ + struct encode_priv *p = ctx->priv; + + mp_mutex_lock(&ctx->lock); + + if (ctx->options->copy_metadata) { + p->metadata = mp_tags_dup(ctx, metadata); + } else { + p->metadata = talloc_zero(ctx, struct mp_tags); + } + + if (ctx->options->set_metadata) { + char **kv = ctx->options->set_metadata; + // Set all user-provided metadata tags + for (int n = 0; kv[n * 2]; n++) { + MP_VERBOSE(ctx, "setting metadata value '%s' for key '%s'\n", + kv[n*2 + 0], kv[n*2 +1]); + mp_tags_set_str(p->metadata, kv[n*2 + 0], kv[n*2 +1]); + } + } + + if (ctx->options->remove_metadata) { + char **k = ctx->options->remove_metadata; + // Remove all user-provided metadata tags + for (int n = 0; k[n]; n++) { + MP_VERBOSE(ctx, "removing metadata key '%s'\n", k[n]); + mp_tags_remove_str(p->metadata, k[n]); + } + } + + mp_mutex_unlock(&ctx->lock); +} + +bool encode_lavc_free(struct encode_lavc_context *ctx) +{ + bool res = true; + if (!ctx) + return res; + + struct encode_priv *p = ctx->priv; + + if (!p->failed && !p->header_written) { + MP_FATAL(p, "no data written to target file\n"); + p->failed = true; + } + + if (!p->failed && p->header_written) { + if (av_write_trailer(p->muxer) < 0) + MP_ERR(p, "error writing trailer\n"); + + MP_INFO(p, "video: encoded %lld bytes\n", p->vbytes); + MP_INFO(p, "audio: encoded %lld bytes\n", p->abytes); + + MP_INFO(p, "muxing overhead %lld bytes\n", + (long long)(avio_size(p->muxer->pb) - p->vbytes - p->abytes)); + } + + if (avio_closep(&p->muxer->pb) < 0 && !p->failed) { + MP_ERR(p, "Closing file failed\n"); + p->failed = true; + } + + avformat_free_context(p->muxer); + + res = !p->failed; + + mp_mutex_destroy(&ctx->lock); + talloc_free(ctx); + + return res; +} + +// called locked +static void maybe_init_muxer(struct encode_lavc_context *ctx) +{ + struct encode_priv *p = ctx->priv; + + if (p->header_written || p->failed) + return; + + // Check if all streams were initialized yet. We need data to know the + // AVStream parameters, so we wait for data from _all_ streams before + // starting. + for (int n = 0; n < p->num_streams; n++) { + if (!p->streams[n]->st) + return; + } + + if (!(p->muxer->oformat->flags & AVFMT_NOFILE)) { + MP_INFO(p, "Opening output file: %s\n", p->muxer->url); + + if (avio_open(&p->muxer->pb, p->muxer->url, AVIO_FLAG_WRITE) < 0) { + MP_FATAL(p, "could not open '%s'\n", p->muxer->url); + goto failed; + } + } + + p->t0 = mp_time_sec(); + + MP_INFO(p, "Opening muxer: %s [%s]\n", + p->muxer->oformat->long_name, p->muxer->oformat->name); + + if (p->metadata) { + for (int i = 0; i < p->metadata->num_keys; i++) { + av_dict_set(&p->muxer->metadata, + p->metadata->keys[i], p->metadata->values[i], 0); + } + } + + AVDictionary *opts = NULL; + mp_set_avdict(&opts, ctx->options->fopts); + + if (avformat_write_header(p->muxer, &opts) < 0) { + MP_FATAL(p, "Failed to initialize muxer.\n"); + p->failed = true; + } else { + mp_avdict_print_unset(p->log, MSGL_WARN, opts); + } + + av_dict_free(&opts); + + if (p->failed) + goto failed; + + p->header_written = true; + + for (int n = 0; n < p->num_streams; n++) { + struct mux_stream *s = p->streams[n]; + + if (s->on_ready) + s->on_ready(s->on_ready_ctx); + } + + return; + +failed: + p->failed = true; +} + +// called locked +static struct mux_stream *find_mux_stream(struct encode_lavc_context *ctx, + enum AVMediaType codec_type) +{ + struct encode_priv *p = ctx->priv; + + for (int n = 0; n < p->num_streams; n++) { + struct mux_stream *s = p->streams[n]; + if (s->codec_type == codec_type) + return s; + } + + return NULL; +} + +void encode_lavc_expect_stream(struct encode_lavc_context *ctx, + enum stream_type type) +{ + struct encode_priv *p = ctx->priv; + + mp_mutex_lock(&ctx->lock); + + enum AVMediaType codec_type = mp_to_av_stream_type(type); + + // These calls are idempotent. + if (find_mux_stream(ctx, codec_type)) + goto done; + + if (p->header_written) { + MP_ERR(p, "Cannot add a stream during encoding.\n"); + p->failed = true; + goto done; + } + + struct mux_stream *dst = talloc_ptrtype(p, dst); + *dst = (struct mux_stream){ + .index = p->num_streams, + .ctx = ctx, + .codec_type = mp_to_av_stream_type(type), + }; + snprintf(dst->name, sizeof(dst->name), "%s", stream_type_name(type)); + MP_TARRAY_APPEND(p, p->streams, p->num_streams, dst); + +done: + mp_mutex_unlock(&ctx->lock); +} + +// Signal that you are ready to encode (you provide the codec params etc. too). +// This returns a muxing handle which you can use to add encodec packets. +// Can be called only once per stream. info is copied by callee as needed. +static void encode_lavc_add_stream(struct encoder_context *enc, + struct encode_lavc_context *ctx, + struct encoder_stream_info *info, + void (*on_ready)(void *ctx), + void *on_ready_ctx) +{ + struct encode_priv *p = ctx->priv; + + mp_mutex_lock(&ctx->lock); + + struct mux_stream *dst = find_mux_stream(ctx, info->codecpar->codec_type); + if (!dst) { + MP_ERR(p, "Cannot add a stream at runtime.\n"); + p->failed = true; + goto done; + } + if (dst->st) { + // Possibly via --gapless-audio, or explicitly recreating AO/VO. + MP_ERR(p, "Encoder was reinitialized; this is not allowed.\n"); + p->failed = true; + dst = NULL; + goto done; + } + + dst->st = avformat_new_stream(p->muxer, NULL); + MP_HANDLE_OOM(dst->st); + + dst->encoder_timebase = info->timebase; + dst->st->time_base = info->timebase; // lavf will change this on muxer init + // Some muxers (e.g. Matroska one) expect the sample_aspect_ratio to be + // set on the AVStream. + if (info->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) + dst->st->sample_aspect_ratio = info->codecpar->sample_aspect_ratio; + + if (avcodec_parameters_copy(dst->st->codecpar, info->codecpar) < 0) + MP_HANDLE_OOM(0); + + dst->on_ready = on_ready; + dst->on_ready_ctx = on_ready_ctx; + enc->mux_stream = dst; + + maybe_init_muxer(ctx); + +done: + mp_mutex_unlock(&ctx->lock); +} + +// Write a packet. This will take over ownership of `pkt` +static void encode_lavc_add_packet(struct mux_stream *dst, AVPacket *pkt) +{ + struct encode_lavc_context *ctx = dst->ctx; + struct encode_priv *p = ctx->priv; + + assert(dst->st); + + mp_mutex_lock(&ctx->lock); + + if (p->failed) + goto done; + + if (!p->header_written) { + MP_ERR(p, "Encoder trying to write packet before muxer was initialized.\n"); + p->failed = true; + goto done; + } + + pkt->stream_index = dst->st->index; + assert(dst->st == p->muxer->streams[pkt->stream_index]); + + av_packet_rescale_ts(pkt, dst->encoder_timebase, dst->st->time_base); + + switch (dst->st->codecpar->codec_type) { + case AVMEDIA_TYPE_VIDEO: + p->vbytes += pkt->size; + p->frames += 1; + break; + case AVMEDIA_TYPE_AUDIO: + p->abytes += pkt->size; + p->audioseconds += pkt->duration + * (double)dst->st->time_base.num + / (double)dst->st->time_base.den; + break; + } + + if (av_interleaved_write_frame(p->muxer, pkt) < 0) { + MP_ERR(p, "Writing packet failed.\n"); + p->failed = true; + } + + pkt = NULL; + +done: + mp_mutex_unlock(&ctx->lock); + if (pkt) + av_packet_unref(pkt); +} + +AVRational encoder_get_mux_timebase_unlocked(struct encoder_context *p) +{ + return p->mux_stream->st->time_base; +} + +void encode_lavc_discontinuity(struct encode_lavc_context *ctx) +{ + if (!ctx) + return; + + mp_mutex_lock(&ctx->lock); + ctx->discontinuity_pts_offset = MP_NOPTS_VALUE; + mp_mutex_unlock(&ctx->lock); +} + +static void encode_lavc_printoptions(struct mp_log *log, const void *obj, + const char *indent, const char *subindent, + const char *unit, int filter_and, + int filter_eq) +{ + const AVOption *opt = NULL; + char optbuf[32]; + while ((opt = av_opt_next(obj, opt))) { + // if flags are 0, it simply hasn't been filled in yet and may be + // potentially useful + if (opt->flags) + if ((opt->flags & filter_and) != filter_eq) + continue; + /* Don't print CONST's on level one. + * Don't print anything but CONST's on level two. + * Only print items from the requested unit. + */ + if (!unit && opt->type == AV_OPT_TYPE_CONST) { + continue; + } else if (unit && opt->type != AV_OPT_TYPE_CONST) { + continue; + } else if (unit && opt->type == AV_OPT_TYPE_CONST + && strcmp(unit, opt->unit)) + { + continue; + } else if (unit && opt->type == AV_OPT_TYPE_CONST) { + mp_info(log, "%s", subindent); + } else { + mp_info(log, "%s", indent); + } + + switch (opt->type) { + case AV_OPT_TYPE_FLAGS: + snprintf(optbuf, sizeof(optbuf), "%s=<flags>", opt->name); + break; + case AV_OPT_TYPE_INT: + snprintf(optbuf, sizeof(optbuf), "%s=<int>", opt->name); + break; + case AV_OPT_TYPE_INT64: + snprintf(optbuf, sizeof(optbuf), "%s=<int64>", opt->name); + break; + case AV_OPT_TYPE_DOUBLE: + snprintf(optbuf, sizeof(optbuf), "%s=<double>", opt->name); + break; + case AV_OPT_TYPE_FLOAT: + snprintf(optbuf, sizeof(optbuf), "%s=<float>", opt->name); + break; + case AV_OPT_TYPE_STRING: + snprintf(optbuf, sizeof(optbuf), "%s=<string>", opt->name); + break; + case AV_OPT_TYPE_RATIONAL: + snprintf(optbuf, sizeof(optbuf), "%s=<rational>", opt->name); + break; + case AV_OPT_TYPE_BINARY: + snprintf(optbuf, sizeof(optbuf), "%s=<binary>", opt->name); + break; + case AV_OPT_TYPE_CONST: + snprintf(optbuf, sizeof(optbuf), " [+-]%s", opt->name); + break; + default: + snprintf(optbuf, sizeof(optbuf), "%s", opt->name); + break; + } + optbuf[sizeof(optbuf) - 1] = 0; + mp_info(log, "%-32s ", optbuf); + if (opt->help) + mp_info(log, " %s", opt->help); + mp_info(log, "\n"); + if (opt->unit && opt->type != AV_OPT_TYPE_CONST) + encode_lavc_printoptions(log, obj, indent, subindent, opt->unit, + filter_and, filter_eq); + } +} + +bool encode_lavc_showhelp(struct mp_log *log, struct encode_opts *opts) +{ + bool help_output = false; +#define CHECKS(str) ((str) && \ + strcmp((str), "help") == 0 ? (help_output |= 1) : 0) +#define CHECKV(strv) ((strv) && (strv)[0] && \ + strcmp((strv)[0], "help") == 0 ? (help_output |= 1) : 0) + if (CHECKS(opts->format)) { + const AVOutputFormat *c = NULL; + void *iter = NULL; + mp_info(log, "Available output formats:\n"); + while ((c = av_muxer_iterate(&iter))) { + mp_info(log, " --of=%-13s %s\n", c->name, + c->long_name ? c->long_name : ""); + } + } + if (CHECKV(opts->fopts)) { + AVFormatContext *c = avformat_alloc_context(); + const AVOutputFormat *format = NULL; + mp_info(log, "Available output format ctx->options:\n"); + encode_lavc_printoptions(log, c, " --ofopts=", " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM, + AV_OPT_FLAG_ENCODING_PARAM); + av_free(c); + void *iter = NULL; + while ((format = av_muxer_iterate(&iter))) { + if (format->priv_class) { + mp_info(log, "Additionally, for --of=%s:\n", + format->name); + encode_lavc_printoptions(log, &format->priv_class, " --ofopts=", + " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM, + AV_OPT_FLAG_ENCODING_PARAM); + } + } + } + if (CHECKV(opts->vopts)) { + AVCodecContext *c = avcodec_alloc_context3(NULL); + const AVCodec *codec = NULL; + mp_info(log, "Available output video codec ctx->options:\n"); + encode_lavc_printoptions(log, + c, " --ovcopts=", " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_VIDEO_PARAM, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_VIDEO_PARAM); + av_free(c); + void *iter = NULL; + while ((codec = av_codec_iterate(&iter))) { + if (!av_codec_is_encoder(codec)) + continue; + if (codec->type != AVMEDIA_TYPE_VIDEO) + continue; + if (opts->vcodec && opts->vcodec[0] && + strcmp(opts->vcodec, codec->name) != 0) + continue; + if (codec->priv_class) { + mp_info(log, "Additionally, for --ovc=%s:\n", + codec->name); + encode_lavc_printoptions(log, + &codec->priv_class, " --ovcopts=", + " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_VIDEO_PARAM, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_VIDEO_PARAM); + } + } + } + if (CHECKV(opts->aopts)) { + AVCodecContext *c = avcodec_alloc_context3(NULL); + const AVCodec *codec = NULL; + mp_info(log, "Available output audio codec ctx->options:\n"); + encode_lavc_printoptions(log, + c, " --oacopts=", " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_AUDIO_PARAM, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_AUDIO_PARAM); + av_free(c); + void *iter = NULL; + while ((codec = av_codec_iterate(&iter))) { + if (!av_codec_is_encoder(codec)) + continue; + if (codec->type != AVMEDIA_TYPE_AUDIO) + continue; + if (opts->acodec && opts->acodec[0] && + strcmp(opts->acodec, codec->name) != 0) + continue; + if (codec->priv_class) { + mp_info(log, "Additionally, for --oac=%s:\n", + codec->name); + encode_lavc_printoptions(log, + &codec->priv_class, " --oacopts=", + " ", NULL, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_AUDIO_PARAM, + AV_OPT_FLAG_ENCODING_PARAM | + AV_OPT_FLAG_AUDIO_PARAM); + } + } + } + if (CHECKS(opts->vcodec)) { + const AVCodec *c = NULL; + void *iter = NULL; + mp_info(log, "Available output video codecs:\n"); + while ((c = av_codec_iterate(&iter))) { + if (!av_codec_is_encoder(c)) + continue; + if (c->type != AVMEDIA_TYPE_VIDEO) + continue; + mp_info(log, " --ovc=%-12s %s\n", c->name, + c->long_name ? c->long_name : ""); + } + } + if (CHECKS(opts->acodec)) { + const AVCodec *c = NULL; + void *iter = NULL; + mp_info(log, "Available output audio codecs:\n"); + while ((c = av_codec_iterate(&iter))) { + if (!av_codec_is_encoder(c)) + continue; + if (c->type != AVMEDIA_TYPE_AUDIO) + continue; + mp_info(log, " --oac=%-12s %s\n", c->name, + c->long_name ? c->long_name : ""); + } + } + return help_output; +} + +int encode_lavc_getstatus(struct encode_lavc_context *ctx, + char *buf, int bufsize, + float relative_position) +{ + if (!ctx) + return -1; + + struct encode_priv *p = ctx->priv; + + double now = mp_time_sec(); + float minutes, megabytes, fps, x; + float f = MPMAX(0.0001, relative_position); + + mp_mutex_lock(&ctx->lock); + + if (p->failed) { + snprintf(buf, bufsize, "(failed)\n"); + goto done; + } + + minutes = (now - p->t0) / 60.0 * (1 - f) / f; + megabytes = p->muxer->pb ? (avio_size(p->muxer->pb) / 1048576.0 / f) : 0; + fps = p->frames / (now - p->t0); + x = p->audioseconds / (now - p->t0); + if (p->frames) { + snprintf(buf, bufsize, "{%.1fmin %.1ffps %.1fMB}", + minutes, fps, megabytes); + } else if (p->audioseconds) { + snprintf(buf, bufsize, "{%.1fmin %.2fx %.1fMB}", + minutes, x, megabytes); + } else { + snprintf(buf, bufsize, "{%.1fmin %.1fMB}", + minutes, megabytes); + } + buf[bufsize - 1] = 0; + +done: + mp_mutex_unlock(&ctx->lock); + return 0; +} + +bool encode_lavc_didfail(struct encode_lavc_context *ctx) +{ + if (!ctx) + return false; + mp_mutex_lock(&ctx->lock); + bool fail = ctx->priv->failed; + mp_mutex_unlock(&ctx->lock); + return fail; +} + +static void encoder_destroy(void *ptr) +{ + struct encoder_context *p = ptr; + + av_packet_free(&p->pkt); + avcodec_parameters_free(&p->info.codecpar); + avcodec_free_context(&p->encoder); + free_stream(p->twopass_bytebuffer); +} + +static const AVCodec *find_codec_for(struct encode_lavc_context *ctx, + enum stream_type type, bool *used_auto) +{ + char *codec_name = type == STREAM_VIDEO + ? ctx->options->vcodec + : ctx->options->acodec; + enum AVMediaType codec_type = mp_to_av_stream_type(type); + const char *tname = stream_type_name(type); + + *used_auto = !(codec_name && codec_name[0]); + + const AVCodec *codec; + if (*used_auto) { + codec = avcodec_find_encoder(av_guess_codec(ctx->oformat, NULL, + ctx->options->file, NULL, + codec_type)); + } else { + codec = avcodec_find_encoder_by_name(codec_name); + if (!codec) + MP_FATAL(ctx, "codec '%s' not found.\n", codec_name); + } + + if (codec && codec->type != codec_type) { + MP_FATAL(ctx, "codec for %s has wrong media type\n", tname); + codec = NULL; + } + + return codec; +} + +// Return whether the stream type is "supposed" to work. +bool encode_lavc_stream_type_ok(struct encode_lavc_context *ctx, + enum stream_type type) +{ + // If a codec was forced, let it proceed to actual encoding, and then error + // if it doesn't work. (Worried that av_guess_codec() may return NULL for + // some formats where a specific codec works anyway.) + bool auto_codec; + return !!find_codec_for(ctx, type, &auto_codec) || !auto_codec; +} + +struct encoder_context *encoder_context_alloc(struct encode_lavc_context *ctx, + enum stream_type type, + struct mp_log *log) +{ + if (!ctx) { + mp_err(log, "the option --o (output file) must be specified\n"); + return NULL; + } + + struct encoder_context *p = talloc_ptrtype(NULL, p); + talloc_set_destructor(p, encoder_destroy); + *p = (struct encoder_context){ + .global = ctx->global, + .options = ctx->options, + .oformat = ctx->oformat, + .type = type, + .log = log, + .encode_lavc_ctx = ctx, + }; + + bool auto_codec; + const AVCodec *codec = find_codec_for(ctx, type, &auto_codec); + const char *tname = stream_type_name(type); + + if (!codec) { + if (auto_codec) + MP_FATAL(p, "codec for %s not found\n", tname); + goto fail; + } + + p->encoder = avcodec_alloc_context3(codec); + MP_HANDLE_OOM(p->encoder); + + return p; + +fail: + talloc_free(p); + return NULL; +} + +static void encoder_2pass_prepare(struct encoder_context *p) +{ + char *filename = talloc_asprintf(NULL, "%s-%s-pass1.log", + p->options->file, + stream_type_name(p->type)); + + if (p->encoder->flags & AV_CODEC_FLAG_PASS2) { + MP_INFO(p, "Reading 2-pass log: %s\n", filename); + struct stream *s = stream_create(filename, + STREAM_ORIGIN_DIRECT | STREAM_READ, + NULL, p->global); + if (s) { + struct bstr content = stream_read_complete(s, p, 1000000000); + if (content.start) { + p->encoder->stats_in = content.start; + } else { + MP_WARN(p, "could not read '%s', " + "disabling 2-pass encoding at pass 1\n", filename); + } + free_stream(s); + } else { + MP_WARN(p, "could not open '%s', " + "disabling 2-pass encoding at pass 2\n", filename); + p->encoder->flags &= ~(unsigned)AV_CODEC_FLAG_PASS2; + } + } + + if (p->encoder->flags & AV_CODEC_FLAG_PASS1) { + MP_INFO(p, "Writing to 2-pass log: %s\n", filename); + p->twopass_bytebuffer = open_output_stream(filename, p->global); + if (!p->twopass_bytebuffer) { + MP_WARN(p, "could not open '%s', " + "disabling 2-pass encoding at pass 1\n", filename); + p->encoder->flags &= ~(unsigned)AV_CODEC_FLAG_PASS1; + } + } + + talloc_free(filename); +} + +bool encoder_init_codec_and_muxer(struct encoder_context *p, + void (*on_ready)(void *ctx), void *ctx) +{ + assert(!avcodec_is_open(p->encoder)); + + char **copts = p->type == STREAM_VIDEO + ? p->options->vopts + : p->options->aopts; + + // Set these now, so the code below can read back parsed settings from it. + mp_set_avopts(p->log, p->encoder, copts); + + encoder_2pass_prepare(p); + + if (p->oformat->flags & AVFMT_GLOBALHEADER) + p->encoder->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + + MP_INFO(p, "Opening encoder: %s [%s]\n", + p->encoder->codec->long_name, p->encoder->codec->name); + + if (p->encoder->codec->capabilities & AV_CODEC_CAP_EXPERIMENTAL) { + p->encoder->strict_std_compliance = FF_COMPLIANCE_EXPERIMENTAL; + MP_WARN(p, "\n\n" + " ********************************************\n" + " **** Experimental codec selected! ****\n" + " ********************************************\n\n" + "This means the output file may be broken or bad.\n" + "Possible reasons, problems, workarounds:\n" + "- Codec implementation in ffmpeg/libav is not finished yet.\n" + " Try updating ffmpeg or libav.\n" + "- Bad picture quality, blocks, blurriness.\n" + " Experiment with codec settings to maybe still get the\n" + " desired quality output at the expense of bitrate.\n" + "- Broken files.\n" + " May not work at all, or break with other software.\n" + "- Slow compression.\n" + " Bear with it.\n" + "- Crashes.\n" + " Happens. Try varying options to work around.\n" + "If none of this helps you, try another codec in place of %s.\n\n", + p->encoder->codec->name); + } + + if (avcodec_open2(p->encoder, p->encoder->codec, NULL) < 0) { + MP_FATAL(p, "Could not initialize encoder.\n"); + goto fail; + } + + p->info.timebase = p->encoder->time_base; // (_not_ changed by enc. init) + p->info.codecpar = avcodec_parameters_alloc(); + MP_HANDLE_OOM(p->info.codecpar); + if (avcodec_parameters_from_context(p->info.codecpar, p->encoder) < 0) + goto fail; + + p->pkt = av_packet_alloc(); + MP_HANDLE_OOM(p->pkt); + + encode_lavc_add_stream(p, p->encode_lavc_ctx, &p->info, on_ready, ctx); + if (!p->mux_stream) + goto fail; + + return true; + +fail: + avcodec_close(p->encoder); + return false; +} + +bool encoder_encode(struct encoder_context *p, AVFrame *frame) +{ + int status = avcodec_send_frame(p->encoder, frame); + if (status < 0) { + if (frame && status == AVERROR_EOF) + MP_ERR(p, "new data after sending EOF to encoder\n"); + goto fail; + } + + AVPacket *packet = p->pkt; + for (;;) { + status = avcodec_receive_packet(p->encoder, packet); + if (status == AVERROR(EAGAIN)) + break; + if (status < 0 && status != AVERROR_EOF) + goto fail; + + if (p->twopass_bytebuffer && p->encoder->stats_out) { + stream_write_buffer(p->twopass_bytebuffer, p->encoder->stats_out, + strlen(p->encoder->stats_out)); + } + + if (status == AVERROR_EOF) + break; + + encode_lavc_add_packet(p->mux_stream, packet); + } + + return true; + +fail: + MP_ERR(p, "error encoding at %s\n", + frame ? av_ts2timestr(frame->pts, &p->encoder->time_base) : "EOF"); + return false; +} + +// vim: ts=4 sw=4 et diff --git a/common/encode_lavc.h b/common/encode_lavc.h new file mode 100644 index 0000000..8517726 --- /dev/null +++ b/common/encode_lavc.h @@ -0,0 +1,114 @@ +/* + * muxing using libavformat + * + * Copyright (C) 2011 Rudolf Polzer <divVerent@xonotic.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/>. + */ + +#ifndef MPLAYER_ENCODE_LAVC_H +#define MPLAYER_ENCODE_LAVC_H + + +#include <libavcodec/avcodec.h> +#include <libavformat/avformat.h> +#include <libavutil/avstring.h> +#include <libavutil/pixfmt.h> +#include <libavutil/opt.h> +#include <libavutil/mathematics.h> + +#include "common/common.h" +#include "encode.h" +#include "osdep/threads.h" +#include "video/csputils.h" + +struct encode_lavc_context { + // --- Immutable after init + struct mpv_global *global; + struct encode_opts *options; + struct mp_log *log; + struct encode_priv *priv; + const AVOutputFormat *oformat; + const char *filename; + + // All entry points must be guarded with the lock. Functions called by + // the playback core lock this automatically, but ao_lavc.c and vo_lavc.c + // must lock manually before accessing state. + mp_mutex lock; + + // anti discontinuity mode + double next_in_pts; + double discontinuity_pts_offset; +}; + +// --- interface for vo/ao drivers + +// Static information after encoder init. This never changes (even if there are +// dynamic runtime changes, they have to work over AVPacket side data). +// For use in encoder_context, most fields are copied from encoder_context.encoder +// by encoder_init_codec_and_muxer(). +struct encoder_stream_info { + AVRational timebase; // timebase used by the encoder (in frames/out packets) + AVCodecParameters *codecpar; +}; + +// The encoder parts for each stream (no muxing parts included). +// This is private to each stream. +struct encoder_context { + struct mpv_global *global; + struct encode_opts *options; + struct mp_log *log; + const AVOutputFormat *oformat; + + // (avoid using this) + struct encode_lavc_context *encode_lavc_ctx; + + enum stream_type type; + + // (different access restrictions before/after encoder init) + struct encoder_stream_info info; + AVCodecContext *encoder; + struct mux_stream *mux_stream; + + // (essentially private) + struct stream *twopass_bytebuffer; + AVPacket *pkt; +}; + +// Free with talloc_free(). (Keep in mind actual deinitialization requires +// sending a flush packet.) +// This can fail and return NULL. +struct encoder_context *encoder_context_alloc(struct encode_lavc_context *ctx, + enum stream_type type, + struct mp_log *log); + +// After setting your codec parameters on p->encoder, you call this to "open" +// the encoder. This also initializes p->mux_stream. Returns false on failure. +// on_ready is called as soon as the muxer has been initialized. Then you are +// allowed to write packets with encoder_encode(). +// Warning: the on_ready callback is called asynchronously, so you need to +// make sure to properly synchronize everything. +bool encoder_init_codec_and_muxer(struct encoder_context *p, + void (*on_ready)(void *ctx), void *ctx); + +// Encode the frame and write the packet. frame is ref'ed as need. +bool encoder_encode(struct encoder_context *p, AVFrame *frame); + +// Return muxer timebase (only available after on_ready() has been called). +// Caller needs to acquire encode_lavc_context.lock (or call it from on_ready). +AVRational encoder_get_mux_timebase_unlocked(struct encoder_context *p); + +#endif diff --git a/common/global.h b/common/global.h new file mode 100644 index 0000000..f95cf28 --- /dev/null +++ b/common/global.h @@ -0,0 +1,15 @@ +#ifndef MPV_MPV_H +#define MPV_MPV_H + +// This should be accessed by glue code only, never normal code. +// The only purpose of this is to make mpv library-safe. +// Think hard before adding new members. +struct mpv_global { + struct mp_log *log; + struct m_config_shadow *config; + struct mp_client_api *client_api; + char *configdir; + struct stats_base *stats; +}; + +#endif diff --git a/common/meson.build b/common/meson.build new file mode 100644 index 0000000..4bca5ea --- /dev/null +++ b/common/meson.build @@ -0,0 +1,8 @@ +version_h = vcs_tag( + command: ['git', 'describe', '--always', '--tags', '--dirty'], + input: 'version.h.in', + output: 'version.h', + replace_string: '@VERSION@', +) + +sources += version_h diff --git a/common/msg.c b/common/msg.c new file mode 100644 index 0000000..b14bd5a --- /dev/null +++ b/common/msg.c @@ -0,0 +1,1069 @@ +/* + * 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 <assert.h> +#include <stdarg.h> +#include <stdatomic.h> +#include <stdint.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <unistd.h> + +#include "mpv_talloc.h" + +#include "misc/bstr.h" +#include "common/common.h" +#include "common/global.h" +#include "misc/bstr.h" +#include "options/options.h" +#include "options/path.h" +#include "osdep/terminal.h" +#include "osdep/io.h" +#include "osdep/threads.h" +#include "osdep/timer.h" + +#include "libmpv/client.h" + +#include "msg.h" +#include "msg_control.h" + +// log buffer size (lines) for terminal level and logfile level +#define TERM_BUF 100 +#define FILE_BUF 100 + +// logfile lines to accumulate during init before we know the log file name. +// thousands of logfile lines during init can happen (especially with many +// scripts, big config, etc), so we set 5000. If it cycles and messages are +// overwritten, then the first (virtual) log line indicates how many were lost. +#define EARLY_FILE_BUF 5000 + +struct mp_log_root { + struct mpv_global *global; + mp_mutex lock; + mp_mutex log_file_lock; + mp_cond log_file_wakeup; + // --- protected by lock + char **msg_levels; + bool use_terminal; // make accesses to stderr/stdout + bool module; + bool show_time; + int blank_lines; // number of lines usable by status + int status_lines; // number of current status lines + bool color[STDERR_FILENO + 1]; + bool isatty[STDERR_FILENO + 1]; + int verbose; + bool really_quiet; + bool force_stderr; + struct mp_log_buffer **buffers; + int num_buffers; + struct mp_log_buffer *early_buffer; + struct mp_log_buffer *early_filebuffer; + FILE *stats_file; + bstr buffer; + bstr term_msg; + bstr term_msg_tmp; + bstr status_line; + struct mp_log *status_log; + bstr term_status_msg; + // --- must be accessed atomically + /* This is incremented every time the msglevels must be reloaded. + * (This is perhaps better than maintaining a globally accessible and + * synchronized mp_log tree.) */ + atomic_ulong reload_counter; + // --- owner thread only (caller of mp_msg_init() etc.) + char *log_path; + char *stats_path; + mp_thread log_file_thread; + // --- owner thread only, but frozen while log_file_thread is running + FILE *log_file; + struct mp_log_buffer *log_file_buffer; + // --- protected by log_file_lock + bool log_file_thread_active; // also termination signal for the thread + int module_indent; +}; + +struct mp_log { + struct mp_log_root *root; + const char *prefix; + const char *verbose_prefix; + int max_level; // minimum log level for this instance + int level; // minimum log level for any outputs + int terminal_level; // minimum log level for terminal output + atomic_ulong reload_counter; + bstr partial[MSGL_MAX + 1]; +}; + +struct mp_log_buffer { + struct mp_log_root *root; + mp_mutex lock; + // --- protected by lock + struct mp_log_buffer_entry **entries; // ringbuffer + int capacity; // total space in entries[] + int entry0; // first (oldest) entry index + int num_entries; // number of valid entries after entry0 + uint64_t dropped; // number of skipped entries + bool silent; + // --- immutable + void (*wakeup_cb)(void *ctx); + void *wakeup_cb_ctx; + int level; +}; + +static const struct mp_log null_log = {0}; +struct mp_log *const mp_null_log = (struct mp_log *)&null_log; + +static bool match_mod(const char *name, const char *mod) +{ + if (!strcmp(mod, "all")) + return true; + // Path prefix matches + bstr b = bstr0(name); + return bstr_eatstart0(&b, mod) && (bstr_eatstart0(&b, "/") || !b.len); +} + +static void update_loglevel(struct mp_log *log) +{ + struct mp_log_root *root = log->root; + mp_mutex_lock(&root->lock); + log->level = MSGL_STATUS + root->verbose; // default log level + if (root->really_quiet) + log->level = -1; + for (int n = 0; root->msg_levels && root->msg_levels[n * 2 + 0]; n++) { + if (match_mod(log->verbose_prefix, root->msg_levels[n * 2 + 0])) + log->level = mp_msg_find_level(root->msg_levels[n * 2 + 1]); + } + log->terminal_level = log->level; + for (int n = 0; n < log->root->num_buffers; n++) { + int buffer_level = log->root->buffers[n]->level; + if (buffer_level == MP_LOG_BUFFER_MSGL_LOGFILE) + buffer_level = MSGL_DEBUG; + if (buffer_level != MP_LOG_BUFFER_MSGL_TERM) + log->level = MPMAX(log->level, buffer_level); + } + if (log->root->log_file) + log->level = MPMAX(log->level, MSGL_DEBUG); + if (log->root->stats_file) + log->level = MPMAX(log->level, MSGL_STATS); + log->level = MPMIN(log->level, log->max_level); + atomic_store(&log->reload_counter, atomic_load(&log->root->reload_counter)); + mp_mutex_unlock(&root->lock); +} + +// Set (numerically) the maximum level that should still be output for this log +// instances. E.g. lev=MSGL_WARN => show only warnings and errors. +void mp_msg_set_max_level(struct mp_log *log, int lev) +{ + if (!log->root) + return; + mp_mutex_lock(&log->root->lock); + log->max_level = MPCLAMP(lev, -1, MSGL_MAX); + mp_mutex_unlock(&log->root->lock); + update_loglevel(log); +} + +// Get the current effective msg level. +// Thread-safety: see mp_msg(). +int mp_msg_level(struct mp_log *log) +{ + struct mp_log_root *root = log->root; + if (!root) + return -1; + if (atomic_load_explicit(&log->reload_counter, memory_order_relaxed) != + atomic_load_explicit(&root->reload_counter, memory_order_relaxed)) + { + update_loglevel(log); + } + return log->level; +} + +static inline int term_msg_fileno(struct mp_log_root *root, int lev) +{ + return (root->force_stderr || lev == MSGL_STATUS || lev == MSGL_FATAL || + lev == MSGL_ERR || lev == MSGL_WARN) ? STDERR_FILENO : STDOUT_FILENO; +} + +// Reposition cursor and clear lines for outputting the status line. In certain +// cases, like term OSD and subtitle display, the status can consist of +// multiple lines. +static void prepare_prefix(struct mp_log_root *root, bstr *out, int lev, int term_lines) +{ + int new_lines = lev == MSGL_STATUS ? term_lines : 0; + out->len = 0; + + if (!root->isatty[term_msg_fileno(root, lev)]) { + if (root->status_lines) + bstr_xappend(root, out, bstr0("\n")); + root->status_lines = new_lines; + return; + } + + // Set cursor state + if (new_lines && !root->status_lines) { + bstr_xappend(root, out, bstr0("\033[?25l")); + } else if (!new_lines && root->status_lines) { + bstr_xappend(root, out, bstr0("\033[?25h")); + } + + int line_skip = 0; + if (root->status_lines) { + // Clear previous status line + bstr_xappend(root, out, bstr0("\033[1K\r")); + bstr up_clear = bstr0("\033[A\033[K"); + for (int i = 1; i < root->status_lines; ++i) + bstr_xappend(root, out, up_clear); + // Reposition cursor after last message + line_skip = (new_lines ? new_lines : root->blank_lines) - root->status_lines; + line_skip = MPMIN(root->blank_lines - root->status_lines, line_skip); + if (line_skip) + bstr_xappend_asprintf(root, out, "\033[%dA", line_skip); + } else if (new_lines) { + line_skip = new_lines - root->blank_lines; + } + + if (line_skip < 0) { + // Reposition cursor to keep status line at the same line + line_skip = MPMIN(root->blank_lines, -line_skip); + if (line_skip) + bstr_xappend_asprintf(root, out, "\033[%dB", line_skip); + } + + root->blank_lines = MPMAX(0, root->blank_lines - term_lines); + root->status_lines = new_lines; + root->blank_lines += root->status_lines; +} + +void mp_msg_flush_status_line(struct mp_log *log) +{ + if (log->root) { + mp_mutex_lock(&log->root->lock); + if (log->root->status_lines) { + bstr term_msg = (bstr){0}; + prepare_prefix(log->root, &term_msg, MSGL_STATUS, 0); + if (term_msg.len) { + fprintf(stderr, "%.*s", BSTR_P(term_msg)); + talloc_free(term_msg.start); + } + } + mp_mutex_unlock(&log->root->lock); + } +} + +void mp_msg_set_term_title(struct mp_log *log, const char *title) +{ + if (log->root && title) { + // Lock because printf to terminal is not necessarily atomic. + mp_mutex_lock(&log->root->lock); + fprintf(stderr, "\e]0;%s\007", title); + mp_mutex_unlock(&log->root->lock); + } +} + +bool mp_msg_has_status_line(struct mpv_global *global) +{ + struct mp_log_root *root = global->log->root; + mp_mutex_lock(&root->lock); + bool r = root->status_lines > 0; + mp_mutex_unlock(&root->lock); + return r; +} + +static void set_term_color(void *talloc_ctx, bstr *text, int c) +{ + return c == -1 ? bstr_xappend(talloc_ctx, text, bstr0("\033[0m")) + : bstr_xappend_asprintf(talloc_ctx, text, + "\033[%d;3%dm", c >> 3, c & 7); +} + +static void set_msg_color(void *talloc_ctx, bstr *text, int lev) +{ + static const int v_colors[] = {9, 1, 3, -1, -1, 2, 8, 8, 8, -1}; + return set_term_color(talloc_ctx, text, v_colors[lev]); +} + +static void pretty_print_module(struct mp_log_root *root, bstr *text, + const char *prefix, int lev) +{ + size_t prefix_len = strlen(prefix); + root->module_indent = MPMAX(10, MPMAX(root->module_indent, prefix_len)); + bool color = root->color[term_msg_fileno(root, lev)]; + + // Use random color based on the name of the module + if (color) { + unsigned int mod = 0; + for (int i = 0; i < prefix_len; ++i) + mod = mod * 33 + prefix[i]; + set_term_color(root, text, (mod + 1) % 15 + 1); + } + + bstr_xappend_asprintf(root, text, "%*s", root->module_indent, prefix); + if (color) + set_term_color(root, text, -1); + bstr_xappend(root, text, bstr0(": ")); + if (color) + set_msg_color(root, text, lev); +} + +static bool test_terminal_level(struct mp_log *log, int lev) +{ + return lev <= log->terminal_level && log->root->use_terminal && + !(lev == MSGL_STATUS && terminal_in_background()); +} + +// This is very basic way to infer needed width for a string. +static int term_disp_width(bstr str, size_t start, size_t end) +{ + int width = 0; + bool escape = false; + + const char *line = str.start; + for (size_t i = start; i < end && i < str.len; ++i) { + if (escape) { + escape = !(line[i] >= '@' && line[i] <= '~'); + continue; + } + + if (line[i] == '\033' && line[i + 1] == '[') { + escape = true; + ++i; + continue; + } + + if (line[i] == '\n') + continue; + + width++; + + // Assume that everything before \r should be discarded for simplicity + if (line[i] == '\r') + width = 0; + } + + return width; +} + +static void append_terminal_line(struct mp_log *log, int lev, + bstr text, bstr *term_msg, int *line_w) +{ + struct mp_log_root *root = log->root; + + size_t start = term_msg->len; + + if (root->show_time) + bstr_xappend_asprintf(root, term_msg, "[%10.6f] ", mp_time_sec()); + + const char *log_prefix = (lev >= MSGL_V) || root->verbose || root->module + ? log->verbose_prefix : log->prefix; + if (log_prefix) { + if (root->module) { + pretty_print_module(root, term_msg, log_prefix, lev); + } else { + bstr_xappend_asprintf(root, term_msg, "[%s] ", log_prefix); + } + } + + bstr_xappend(root, term_msg, text); + *line_w = root->isatty[term_msg_fileno(root, lev)] + ? term_disp_width(*term_msg, start, term_msg->len) : 0; +} + +static struct mp_log_buffer_entry *log_buffer_read(struct mp_log_buffer *buffer) +{ + assert(buffer->num_entries); + struct mp_log_buffer_entry *res = buffer->entries[buffer->entry0]; + buffer->entry0 = (buffer->entry0 + 1) % buffer->capacity; + buffer->num_entries -= 1; + return res; +} + +static void write_msg_to_buffers(struct mp_log *log, int lev, bstr text) +{ + struct mp_log_root *root = log->root; + for (int n = 0; n < root->num_buffers; n++) { + struct mp_log_buffer *buffer = root->buffers[n]; + bool wakeup = false; + mp_mutex_lock(&buffer->lock); + int buffer_level = buffer->level; + if (buffer_level == MP_LOG_BUFFER_MSGL_TERM) + buffer_level = log->terminal_level; + if (buffer_level == MP_LOG_BUFFER_MSGL_LOGFILE) + buffer_level = MPMAX(log->terminal_level, MSGL_DEBUG); + if (lev <= buffer_level && lev != MSGL_STATUS) { + if (buffer->level == MP_LOG_BUFFER_MSGL_LOGFILE) { + // If the buffer is full, block until we can write again, + // unless there's no write thread (died, or early filebuffer) + bool dead = false; + while (buffer->num_entries == buffer->capacity && !dead) { + // Temporary unlock is OK; buffer->level is immutable, and + // buffer can't go away because the global log lock is held. + mp_mutex_unlock(&buffer->lock); + mp_mutex_lock(&root->log_file_lock); + if (root->log_file_thread_active) { + mp_cond_wait(&root->log_file_wakeup, + &root->log_file_lock); + } else { + dead = true; + } + mp_mutex_unlock(&root->log_file_lock); + mp_mutex_lock(&buffer->lock); + } + } + if (buffer->num_entries == buffer->capacity) { + struct mp_log_buffer_entry *skip = log_buffer_read(buffer); + talloc_free(skip); + buffer->dropped += 1; + } + struct mp_log_buffer_entry *entry = talloc_ptrtype(NULL, entry); + *entry = (struct mp_log_buffer_entry) { + .prefix = talloc_strdup(entry, log->verbose_prefix), + .level = lev, + .text = bstrdup0(entry, text), + }; + int pos = (buffer->entry0 + buffer->num_entries) % buffer->capacity; + buffer->entries[pos] = entry; + buffer->num_entries += 1; + if (buffer->wakeup_cb && !buffer->silent) + wakeup = true; + } + mp_mutex_unlock(&buffer->lock); + if (wakeup) + buffer->wakeup_cb(buffer->wakeup_cb_ctx); + } +} + +static void dump_stats(struct mp_log *log, int lev, bstr text) +{ + struct mp_log_root *root = log->root; + if (lev == MSGL_STATS && root->stats_file) + fprintf(root->stats_file, "%"PRId64" %.*s\n", mp_time_ns(), BSTR_P(text)); +} + +static void write_term_msg(struct mp_log *log, int lev, bstr text, bstr *out) +{ + struct mp_log_root *root = log->root; + bool print_term = test_terminal_level(log, lev); + int fileno = term_msg_fileno(root, lev); + int term_w = 0, term_h = 0; + if (print_term && root->isatty[fileno]) + terminal_get_size(&term_w, &term_h); + + out->len = 0; + + // Split away each line. Normally we require full lines; buffer partial + // lines if they happen. + root->term_msg_tmp.len = 0; + int term_msg_lines = 0; + + bstr str = text; + while (str.len) { + bstr line = bstr_getline(str, &str); + if (line.start[line.len - 1] != '\n') { + assert(str.len == 0); + str = line; + break; + } + + if (print_term) { + int line_w; + append_terminal_line(log, lev, line, &root->term_msg_tmp, &line_w); + term_msg_lines += (!line_w || !term_w) + ? 1 : (line_w + term_w - 1) / term_w; + } + write_msg_to_buffers(log, lev, line); + } + + if (lev == MSGL_STATUS && print_term) { + int line_w = 0; + if (str.len) + append_terminal_line(log, lev, str, &root->term_msg_tmp, &line_w); + term_msg_lines += !term_w ? (str.len ? 1 : 0) + : (line_w + term_w - 1) / term_w; + } else if (str.len) { + bstr_xappend(NULL, &log->partial[lev], str); + } + + if (print_term && (root->term_msg_tmp.len || lev == MSGL_STATUS)) { + prepare_prefix(root, out, lev, term_msg_lines); + if (root->color[fileno] && root->term_msg_tmp.len) { + set_msg_color(root, out, lev); + set_term_color(root, &root->term_msg_tmp, -1); + } + bstr_xappend(root, out, root->term_msg_tmp); + } +} + +void mp_msg_va(struct mp_log *log, int lev, const char *format, va_list va) +{ + if (!mp_msg_test(log, lev)) + return; // do not display + + struct mp_log_root *root = log->root; + + mp_mutex_lock(&root->lock); + + root->buffer.len = 0; + + if (log->partial[lev].len) + bstr_xappend(root, &root->buffer, log->partial[lev]); + log->partial[lev].len = 0; + + bstr_xappend_vasprintf(root, &root->buffer, format, va); + + // Remember last status message and restore it to ensure that it is + // always displayed + if (lev == MSGL_STATUS) { + root->status_log = log; + root->status_line.len = 0; + // Use bstr_xappend instead bstrdup to reuse allocated memory + if (root->buffer.len) + bstr_xappend(root, &root->status_line, root->buffer); + } + + if (lev == MSGL_STATS) { + dump_stats(log, lev, root->buffer); + } else if (lev == MSGL_STATUS && !test_terminal_level(log, lev)) { + /* discard */ + } else { + write_term_msg(log, lev, root->buffer, &root->term_msg); + + root->term_status_msg.len = 0; + if (lev != MSGL_STATUS && root->status_line.len && root->status_log && + test_terminal_level(root->status_log, MSGL_STATUS)) + { + write_term_msg(root->status_log, MSGL_STATUS, root->status_line, + &root->term_status_msg); + } + + int fileno = term_msg_fileno(root, lev); + FILE *stream = fileno == STDERR_FILENO ? stderr : stdout; + if (root->term_msg.len) { + if (root->term_status_msg.len) { + fprintf(stream, "%.*s%.*s", BSTR_P(root->term_msg), + BSTR_P(root->term_status_msg)); + } else { + fprintf(stream, "%.*s", BSTR_P(root->term_msg)); + } + fflush(stream); + } + } + + mp_mutex_unlock(&root->lock); +} + +static void destroy_log(void *ptr) +{ + struct mp_log *log = ptr; + // This is not managed via talloc itself, because mp_msg calls must be + // thread-safe, while talloc is not thread-safe. + for (int lvl = 0; lvl <= MSGL_MAX; ++lvl) + talloc_free(log->partial[lvl].start); +} + +// Create a new log context, which uses talloc_ctx as talloc parent, and parent +// as logical parent. +// The name is the prefix put before the output. It's usually prefixed by the +// parent's name. If the name starts with "/", the parent's name is not +// prefixed (except in verbose mode), and if it starts with "!", the name is +// not printed at all (except in verbose mode). +// If name is NULL, the parent's name/prefix is used. +// Thread-safety: fully thread-safe, but keep in mind that talloc is not (so +// talloc_ctx must be owned by the current thread). +struct mp_log *mp_log_new(void *talloc_ctx, struct mp_log *parent, + const char *name) +{ + assert(parent); + struct mp_log *log = talloc_zero(talloc_ctx, struct mp_log); + if (!parent->root) + return log; // same as null_log + talloc_set_destructor(log, destroy_log); + log->root = parent->root; + log->max_level = MSGL_MAX; + if (name) { + if (name[0] == '!') { + name = &name[1]; + } else if (name[0] == '/') { + name = &name[1]; + log->prefix = talloc_strdup(log, name); + } else { + log->prefix = parent->prefix + ? talloc_asprintf(log, "%s/%s", parent->prefix, name) + : talloc_strdup(log, name); + } + log->verbose_prefix = parent->prefix + ? talloc_asprintf(log, "%s/%s", parent->prefix, name) + : talloc_strdup(log, name); + if (log->prefix && !log->prefix[0]) + log->prefix = NULL; + if (!log->verbose_prefix[0]) + log->verbose_prefix = "global"; + } else { + log->prefix = talloc_strdup(log, parent->prefix); + log->verbose_prefix = talloc_strdup(log, parent->verbose_prefix); + } + return log; +} + +void mp_msg_init(struct mpv_global *global) +{ + assert(!global->log); + + struct mp_log_root *root = talloc_zero(NULL, struct mp_log_root); + *root = (struct mp_log_root){ + .global = global, + .reload_counter = 1, + }; + + mp_mutex_init(&root->lock); + mp_mutex_init(&root->log_file_lock); + mp_cond_init(&root->log_file_wakeup); + + struct mp_log dummy = { .root = root }; + struct mp_log *log = mp_log_new(root, &dummy, ""); + + global->log = log; +} + +static MP_THREAD_VOID log_file_thread(void *p) +{ + struct mp_log_root *root = p; + + mp_thread_set_name("log"); + + mp_mutex_lock(&root->log_file_lock); + + while (root->log_file_thread_active) { + struct mp_log_buffer_entry *e = + mp_msg_log_buffer_read(root->log_file_buffer); + if (e) { + mp_mutex_unlock(&root->log_file_lock); + fprintf(root->log_file, "[%8.3f][%c][%s] %s", + mp_time_sec(), + mp_log_levels[e->level][0], e->prefix, e->text); + fflush(root->log_file); + mp_mutex_lock(&root->log_file_lock); + talloc_free(e); + // Multiple threads might be blocked if the log buffer was full. + mp_cond_broadcast(&root->log_file_wakeup); + } else { + mp_cond_wait(&root->log_file_wakeup, &root->log_file_lock); + } + } + + mp_mutex_unlock(&root->log_file_lock); + + MP_THREAD_RETURN(); +} + +static void wakeup_log_file(void *p) +{ + struct mp_log_root *root = p; + + mp_mutex_lock(&root->log_file_lock); + mp_cond_broadcast(&root->log_file_wakeup); + mp_mutex_unlock(&root->log_file_lock); +} + +// Only to be called from the main thread. +static void terminate_log_file_thread(struct mp_log_root *root) +{ + bool wait_terminate = false; + + mp_mutex_lock(&root->log_file_lock); + if (root->log_file_thread_active) { + root->log_file_thread_active = false; + mp_cond_broadcast(&root->log_file_wakeup); + wait_terminate = true; + } + mp_mutex_unlock(&root->log_file_lock); + + if (wait_terminate) + mp_thread_join(root->log_file_thread); + + mp_msg_log_buffer_destroy(root->log_file_buffer); + root->log_file_buffer = NULL; + + if (root->log_file) + fclose(root->log_file); + root->log_file = NULL; +} + +// If opt is different from *current_path, update *current_path and return true. +// No lock must be held; passed values must be accessible without. +static bool check_new_path(struct mpv_global *global, char *opt, + char **current_path) +{ + void *tmp = talloc_new(NULL); + bool res = false; + + char *new_path = mp_get_user_path(tmp, global, opt); + if (!new_path) + new_path = ""; + + char *old_path = *current_path ? *current_path : ""; + if (strcmp(old_path, new_path) != 0) { + talloc_free(*current_path); + *current_path = NULL; + if (new_path[0]) + *current_path = talloc_strdup(NULL, new_path); + res = true; + } + + talloc_free(tmp); + + return res; +} + +void mp_msg_update_msglevels(struct mpv_global *global, struct MPOpts *opts) +{ + struct mp_log_root *root = global->log->root; + + mp_mutex_lock(&root->lock); + + root->verbose = opts->verbose; + root->really_quiet = opts->msg_really_quiet; + root->module = opts->msg_module; + root->use_terminal = opts->use_terminal; + root->show_time = opts->msg_time; + for (int i = STDOUT_FILENO; i <= STDERR_FILENO && root->use_terminal; ++i) { + root->isatty[i] = isatty(i); + root->color[i] = opts->msg_color && root->isatty[i]; + } + + m_option_type_msglevels.free(&root->msg_levels); + m_option_type_msglevels.copy(NULL, &root->msg_levels, &opts->msg_levels); + + atomic_fetch_add(&root->reload_counter, 1); + mp_mutex_unlock(&root->lock); + + if (check_new_path(global, opts->log_file, &root->log_path)) { + terminate_log_file_thread(root); + if (root->log_path) { + root->log_file = fopen(root->log_path, "wb"); + if (root->log_file) { + + // if a logfile is created and the early filebuf still exists, + // flush and destroy the early buffer + mp_mutex_lock(&root->lock); + struct mp_log_buffer *earlybuf = root->early_filebuffer; + if (earlybuf) + root->early_filebuffer = NULL; // but it still logs msgs + mp_mutex_unlock(&root->lock); + + if (earlybuf) { + // flush, destroy before creating the normal logfile buf, + // as once the new one is created (specifically, its write + // thread), then MSGL_LOGFILE messages become blocking, but + // the early logfile buf is without dequeue - can deadlock. + // note: timestamp is unknown, we use 0.000 as indication. + // note: new messages while iterating are still flushed. + struct mp_log_buffer_entry *e; + while ((e = mp_msg_log_buffer_read(earlybuf))) { + fprintf(root->log_file, "[%8.3f][%c][%s] %s", 0.0, + mp_log_levels[e->level][0], e->prefix, e->text); + talloc_free(e); + } + mp_msg_log_buffer_destroy(earlybuf); // + remove from root + } + + root->log_file_buffer = + mp_msg_log_buffer_new(global, FILE_BUF, MP_LOG_BUFFER_MSGL_LOGFILE, + wakeup_log_file, root); + root->log_file_thread_active = true; + if (mp_thread_create(&root->log_file_thread, log_file_thread, + root)) + { + root->log_file_thread_active = false; + terminate_log_file_thread(root); + } + } else { + mp_err(global->log, "Failed to open log file '%s'\n", + root->log_path); + } + } + } + + if (check_new_path(global, opts->dump_stats, &root->stats_path)) { + bool open_error = false; + + mp_mutex_lock(&root->lock); + if (root->stats_file) + fclose(root->stats_file); + root->stats_file = NULL; + if (root->stats_path) { + root->stats_file = fopen(root->stats_path, "wb"); + open_error = !root->stats_file; + } + mp_mutex_unlock(&root->lock); + + if (open_error) { + mp_err(global->log, "Failed to open stats file '%s'\n", + root->stats_path); + } + } +} + +void mp_msg_force_stderr(struct mpv_global *global, bool force_stderr) +{ + struct mp_log_root *root = global->log->root; + + mp_mutex_lock(&root->lock); + root->force_stderr = force_stderr; + mp_mutex_unlock(&root->lock); +} + +// Only to be called from the main thread. +bool mp_msg_has_log_file(struct mpv_global *global) +{ + struct mp_log_root *root = global->log->root; + + return !!root->log_file; +} + +void mp_msg_uninit(struct mpv_global *global) +{ + struct mp_log_root *root = global->log->root; + mp_msg_flush_status_line(global->log); + terminate_log_file_thread(root); + mp_msg_log_buffer_destroy(root->early_buffer); + mp_msg_log_buffer_destroy(root->early_filebuffer); + assert(root->num_buffers == 0); + if (root->stats_file) + fclose(root->stats_file); + talloc_free(root->stats_path); + talloc_free(root->log_path); + m_option_type_msglevels.free(&root->msg_levels); + mp_mutex_destroy(&root->lock); + mp_mutex_destroy(&root->log_file_lock); + mp_cond_destroy(&root->log_file_wakeup); + talloc_free(root); + global->log = NULL; +} + +// early logging store log messages before they have a known destination. +// there are two early log buffers which are similar logically, and both cease +// function (if still exist, independently) once the log destination is known, +// or mpv init is complete (typically, after all clients/scripts init is done). +// +// - "normal" early_buffer, holds early terminal-level logs, and is handed over +// to the first client which requests such log buffer, so that it sees older +// messages too. further clients which request a log buffer get a new one +// which accumulates messages starting at this point in time. +// +// - early_filebuffer - early log-file messages until a log file name is known. +// main cases where meaningful messages are accumulated before the filename +// is known are when log-file is set at mpv.conf, or from script/client init. +// once a file name is known, the early buffer is flushed and destroyed. +// unlike the "proper" log-file buffer, the early filebuffer is not backed by +// a write thread, and hence non-blocking (can overwrite old messages). +// it's also bigger than the actual file buffer (early: 5000, actual: 100). + +static void mp_msg_set_early_logging_raw(struct mpv_global *global, bool enable, + struct mp_log_buffer **root_logbuf, + int size, int level) +{ + struct mp_log_root *root = global->log->root; + mp_mutex_lock(&root->lock); + + if (enable != !!*root_logbuf) { + if (enable) { + mp_mutex_unlock(&root->lock); + struct mp_log_buffer *buf = + mp_msg_log_buffer_new(global, size, level, NULL, NULL); + mp_mutex_lock(&root->lock); + assert(!*root_logbuf); // no concurrent calls to this function + *root_logbuf = buf; + } else { + struct mp_log_buffer *buf = *root_logbuf; + *root_logbuf = NULL; + mp_mutex_unlock(&root->lock); + mp_msg_log_buffer_destroy(buf); + return; + } + } + + mp_mutex_unlock(&root->lock); +} + +void mp_msg_set_early_logging(struct mpv_global *global, bool enable) +{ + struct mp_log_root *root = global->log->root; + + mp_msg_set_early_logging_raw(global, enable, &root->early_buffer, + TERM_BUF, MP_LOG_BUFFER_MSGL_TERM); + + // normally MSGL_LOGFILE buffer gets a write thread, but not the early buf + mp_msg_set_early_logging_raw(global, enable, &root->early_filebuffer, + EARLY_FILE_BUF, MP_LOG_BUFFER_MSGL_LOGFILE); +} + +struct mp_log_buffer *mp_msg_log_buffer_new(struct mpv_global *global, + int size, int level, + void (*wakeup_cb)(void *ctx), + void *wakeup_cb_ctx) +{ + struct mp_log_root *root = global->log->root; + + mp_mutex_lock(&root->lock); + + if (level == MP_LOG_BUFFER_MSGL_TERM) { + size = TERM_BUF; + + // The first thing which creates a terminal-level log buffer gets the + // early log buffer, if it exists. This is supposed to enable a script + // to grab log messages from before it was initialized. It's OK that + // this works only for 1 script and only once. + if (root->early_buffer) { + struct mp_log_buffer *buffer = root->early_buffer; + root->early_buffer = NULL; + buffer->wakeup_cb = wakeup_cb; + buffer->wakeup_cb_ctx = wakeup_cb_ctx; + mp_mutex_unlock(&root->lock); + return buffer; + } + } + + assert(size > 0); + + struct mp_log_buffer *buffer = talloc_ptrtype(NULL, buffer); + *buffer = (struct mp_log_buffer) { + .root = root, + .level = level, + .entries = talloc_array(buffer, struct mp_log_buffer_entry *, size), + .capacity = size, + .wakeup_cb = wakeup_cb, + .wakeup_cb_ctx = wakeup_cb_ctx, + }; + + mp_mutex_init(&buffer->lock); + + MP_TARRAY_APPEND(root, root->buffers, root->num_buffers, buffer); + + atomic_fetch_add(&root->reload_counter, 1); + mp_mutex_unlock(&root->lock); + + return buffer; +} + +void mp_msg_log_buffer_set_silent(struct mp_log_buffer *buffer, bool silent) +{ + mp_mutex_lock(&buffer->lock); + buffer->silent = silent; + mp_mutex_unlock(&buffer->lock); +} + +void mp_msg_log_buffer_destroy(struct mp_log_buffer *buffer) +{ + if (!buffer) + return; + + struct mp_log_root *root = buffer->root; + + mp_mutex_lock(&root->lock); + + for (int n = 0; n < root->num_buffers; n++) { + if (root->buffers[n] == buffer) { + MP_TARRAY_REMOVE_AT(root->buffers, root->num_buffers, n); + goto found; + } + } + + MP_ASSERT_UNREACHABLE(); + +found: + + while (buffer->num_entries) + talloc_free(log_buffer_read(buffer)); + + mp_mutex_destroy(&buffer->lock); + talloc_free(buffer); + + atomic_fetch_add(&root->reload_counter, 1); + mp_mutex_unlock(&root->lock); +} + +// Return a queued message, or if the buffer is empty, NULL. +// Thread-safety: one buffer can be read by a single thread only. +struct mp_log_buffer_entry *mp_msg_log_buffer_read(struct mp_log_buffer *buffer) +{ + struct mp_log_buffer_entry *res = NULL; + + mp_mutex_lock(&buffer->lock); + + if (!buffer->silent && buffer->num_entries) { + if (buffer->dropped) { + res = talloc_ptrtype(NULL, res); + *res = (struct mp_log_buffer_entry) { + .prefix = "overflow", + .level = MSGL_FATAL, + .text = talloc_asprintf(res, + "log message buffer overflow: %"PRId64" messages skipped\n", + buffer->dropped), + }; + buffer->dropped = 0; + } else { + res = log_buffer_read(buffer); + } + } + + mp_mutex_unlock(&buffer->lock); + + return res; +} + +// Thread-safety: fully thread-safe, but keep in mind that the lifetime of +// log must be guaranteed during the call. +// Never call this from signal handlers. +void mp_msg(struct mp_log *log, int lev, const char *format, ...) +{ + va_list va; + va_start(va, format); + mp_msg_va(log, lev, format, va); + va_end(va); +} + +const char *const mp_log_levels[MSGL_MAX + 1] = { + [MSGL_FATAL] = "fatal", + [MSGL_ERR] = "error", + [MSGL_WARN] = "warn", + [MSGL_INFO] = "info", + [MSGL_STATUS] = "status", + [MSGL_V] = "v", + [MSGL_DEBUG] = "debug", + [MSGL_TRACE] = "trace", + [MSGL_STATS] = "stats", +}; + +const int mp_mpv_log_levels[MSGL_MAX + 1] = { + [MSGL_FATAL] = MPV_LOG_LEVEL_FATAL, + [MSGL_ERR] = MPV_LOG_LEVEL_ERROR, + [MSGL_WARN] = MPV_LOG_LEVEL_WARN, + [MSGL_INFO] = MPV_LOG_LEVEL_INFO, + [MSGL_STATUS] = 0, // never used + [MSGL_V] = MPV_LOG_LEVEL_V, + [MSGL_DEBUG] = MPV_LOG_LEVEL_DEBUG, + [MSGL_TRACE] = MPV_LOG_LEVEL_TRACE, + [MSGL_STATS] = 0, // never used +}; + +int mp_msg_find_level(const char *s) +{ + for (int n = 0; n < MP_ARRAY_SIZE(mp_log_levels); n++) { + if (mp_log_levels[n] && !strcasecmp(s, mp_log_levels[n])) + return n; + } + return -1; +} diff --git a/common/msg.h b/common/msg.h new file mode 100644 index 0000000..b0cec7b --- /dev/null +++ b/common/msg.h @@ -0,0 +1,92 @@ +/* + * 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_MP_MSG_H +#define MPLAYER_MP_MSG_H + +#include <stdarg.h> +#include <stdbool.h> +#include <stdlib.h> +#include <stdint.h> + +#include "osdep/compiler.h" + +struct mp_log; + +// A mp_log instance that never outputs anything. +extern struct mp_log *const mp_null_log; + +// Verbosity levels. +enum { + MSGL_FATAL, // only errors (difference to MSGL_ERR isn't too clear) + MSGL_ERR, // only errors + MSGL_WARN, // only warnings + MSGL_INFO, // what you normally see on the terminal + MSGL_STATUS, // exclusively for the playback status line (-quiet disables) + MSGL_V, // -v | slightly more information than default + MSGL_DEBUG, // -v -v | full debug information; this and numerically below + // should not produce "per frame" output + MSGL_TRACE, // -v -v -v | anything that might flood the terminal + MSGL_STATS, // dumping fine grained stats (--dump-stats) + + MSGL_MAX = MSGL_STATS, +}; + +struct mp_log *mp_log_new(void *talloc_ctx, struct mp_log *parent, + const char *name); + +void mp_msg(struct mp_log *log, int lev, const char *format, ...) + PRINTF_ATTRIBUTE(3, 4); +void mp_msg_va(struct mp_log *log, int lev, const char *format, va_list va); + +int mp_msg_level(struct mp_log *log); + +static inline bool mp_msg_test(struct mp_log *log, int lev) +{ + return lev <= mp_msg_level(log); +} + +void mp_msg_set_max_level(struct mp_log *log, int lev); + +// Convenience macros. +#define mp_fatal(log, ...) mp_msg(log, MSGL_FATAL, __VA_ARGS__) +#define mp_err(log, ...) mp_msg(log, MSGL_ERR, __VA_ARGS__) +#define mp_warn(log, ...) mp_msg(log, MSGL_WARN, __VA_ARGS__) +#define mp_info(log, ...) mp_msg(log, MSGL_INFO, __VA_ARGS__) +#define mp_verbose(log, ...) mp_msg(log, MSGL_V, __VA_ARGS__) +#define mp_dbg(log, ...) mp_msg(log, MSGL_DEBUG, __VA_ARGS__) +#define mp_trace(log, ...) mp_msg(log, MSGL_TRACE, __VA_ARGS__) + +// Convenience macros, typically called with a pointer to a context struct +// as first argument, which has a "struct mp_log *log;" member. + +#define MP_MSG(obj, lev, ...) mp_msg((obj)->log, lev, __VA_ARGS__) + +#define MP_FATAL(obj, ...) MP_MSG(obj, MSGL_FATAL, __VA_ARGS__) +#define MP_ERR(obj, ...) MP_MSG(obj, MSGL_ERR, __VA_ARGS__) +#define MP_WARN(obj, ...) MP_MSG(obj, MSGL_WARN, __VA_ARGS__) +#define MP_INFO(obj, ...) MP_MSG(obj, MSGL_INFO, __VA_ARGS__) +#define MP_VERBOSE(obj, ...) MP_MSG(obj, MSGL_V, __VA_ARGS__) +#define MP_DBG(obj, ...) MP_MSG(obj, MSGL_DEBUG, __VA_ARGS__) +#define MP_TRACE(obj, ...) MP_MSG(obj, MSGL_TRACE, __VA_ARGS__) + +// This is a bit special. See TOOLS/stats-conv.py what rules text passed +// to these functions should follow. Also see --dump-stats. +#define mp_stats(obj, ...) mp_msg(obj, MSGL_STATS, __VA_ARGS__) +#define MP_STATS(obj, ...) MP_MSG(obj, MSGL_STATS, __VA_ARGS__) + +#endif /* MPLAYER_MP_MSG_H */ diff --git a/common/msg_control.h b/common/msg_control.h new file mode 100644 index 0000000..e4da59e --- /dev/null +++ b/common/msg_control.h @@ -0,0 +1,45 @@ +#ifndef MP_MSG_CONTROL_H +#define MP_MSG_CONTROL_H + +#include <stdbool.h> +#include "common/msg.h" + +struct mpv_global; +struct MPOpts; +void mp_msg_init(struct mpv_global *global); +void mp_msg_uninit(struct mpv_global *global); +void mp_msg_update_msglevels(struct mpv_global *global, struct MPOpts *opts); +void mp_msg_force_stderr(struct mpv_global *global, bool force_stderr); +bool mp_msg_has_status_line(struct mpv_global *global); +bool mp_msg_has_log_file(struct mpv_global *global); +void mp_msg_set_early_logging(struct mpv_global *global, bool enable); + +void mp_msg_flush_status_line(struct mp_log *log); +void mp_msg_set_term_title(struct mp_log *log, const char *title); + +struct mp_log_buffer_entry { + char *prefix; + int level; + char *text; +}; + +// Use --msg-level option for log level of this log buffer +#define MP_LOG_BUFFER_MSGL_TERM (MSGL_MAX + 1) +// For --log-file; --msg-level, but at least MSGL_DEBUG +#define MP_LOG_BUFFER_MSGL_LOGFILE (MSGL_MAX + 2) + +struct mp_log_buffer; +struct mp_log_buffer *mp_msg_log_buffer_new(struct mpv_global *global, + int size, int level, + void (*wakeup_cb)(void *ctx), + void *wakeup_cb_ctx); +void mp_msg_log_buffer_destroy(struct mp_log_buffer *buffer); +struct mp_log_buffer_entry *mp_msg_log_buffer_read(struct mp_log_buffer *buffer); +void mp_msg_log_buffer_set_silent(struct mp_log_buffer *buffer, bool silent); + +int mp_msg_find_level(const char *s); + +extern const char *const mp_log_levels[MSGL_MAX + 1]; +extern const int mp_mpv_log_levels[MSGL_MAX + 1]; + +#endif diff --git a/common/playlist.c b/common/playlist.c new file mode 100644 index 0000000..c1636bc --- /dev/null +++ b/common/playlist.c @@ -0,0 +1,413 @@ +/* + * 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 <assert.h> +#include "playlist.h" +#include "common/common.h" +#include "common/global.h" +#include "common/msg.h" +#include "misc/random.h" +#include "mpv_talloc.h" +#include "options/path.h" + +#include "demux/demux.h" +#include "stream/stream.h" + +struct playlist_entry *playlist_entry_new(const char *filename) +{ + struct playlist_entry *e = talloc_zero(NULL, struct playlist_entry); + char *local_filename = mp_file_url_to_filename(e, bstr0(filename)); + e->filename = local_filename ? local_filename : talloc_strdup(e, filename); + e->stream_flags = STREAM_ORIGIN_DIRECT; + e->original_index = -1; + return e; +} + +void playlist_entry_add_param(struct playlist_entry *e, bstr name, bstr value) +{ + struct playlist_param p = {bstrdup(e, name), bstrdup(e, value)}; + MP_TARRAY_APPEND(e, e->params, e->num_params, p); +} + +void playlist_entry_add_params(struct playlist_entry *e, + struct playlist_param *params, + int num_params) +{ + for (int n = 0; n < num_params; n++) + playlist_entry_add_param(e, params[n].name, params[n].value); +} + +static void playlist_update_indexes(struct playlist *pl, int start, int end) +{ + start = MPMAX(start, 0); + end = end < 0 ? pl->num_entries : MPMIN(end, pl->num_entries); + + for (int n = start; n < end; n++) + pl->entries[n]->pl_index = n; +} + +void playlist_add(struct playlist *pl, struct playlist_entry *add) +{ + assert(add->filename); + MP_TARRAY_APPEND(pl, pl->entries, pl->num_entries, add); + add->pl = pl; + add->pl_index = pl->num_entries - 1; + add->id = ++pl->id_alloc; + talloc_steal(pl, add); +} + +void playlist_entry_unref(struct playlist_entry *e) +{ + e->reserved--; + if (e->reserved < 0) { + assert(!e->pl); + talloc_free(e); + } +} + +void playlist_remove(struct playlist *pl, struct playlist_entry *entry) +{ + assert(pl && entry->pl == pl); + + if (pl->current == entry) { + pl->current = playlist_entry_get_rel(entry, 1); + pl->current_was_replaced = true; + } + + MP_TARRAY_REMOVE_AT(pl->entries, pl->num_entries, entry->pl_index); + playlist_update_indexes(pl, entry->pl_index, -1); + + entry->pl = NULL; + entry->pl_index = -1; + ta_set_parent(entry, NULL); + + entry->removed = true; + playlist_entry_unref(entry); +} + +void playlist_clear(struct playlist *pl) +{ + for (int n = pl->num_entries - 1; n >= 0; n--) + playlist_remove(pl, pl->entries[n]); + assert(!pl->current); + pl->current_was_replaced = false; +} + +void playlist_clear_except_current(struct playlist *pl) +{ + for (int n = pl->num_entries - 1; n >= 0; n--) { + if (pl->entries[n] != pl->current) + playlist_remove(pl, pl->entries[n]); + } +} + +// Moves the entry so that it takes "at"'s place (or move to end, if at==NULL). +void playlist_move(struct playlist *pl, struct playlist_entry *entry, + struct playlist_entry *at) +{ + if (entry == at) + return; + + assert(entry && entry->pl == pl); + assert(!at || at->pl == pl); + + int index = at ? at->pl_index : pl->num_entries; + MP_TARRAY_INSERT_AT(pl, pl->entries, pl->num_entries, index, entry); + + int old_index = entry->pl_index; + if (old_index >= index) + old_index += 1; + MP_TARRAY_REMOVE_AT(pl->entries, pl->num_entries, old_index); + + playlist_update_indexes(pl, MPMIN(index - 1, old_index - 1), + MPMAX(index + 1, old_index + 1)); +} + +void playlist_add_file(struct playlist *pl, const char *filename) +{ + playlist_add(pl, playlist_entry_new(filename)); +} + +void playlist_populate_playlist_path(struct playlist *pl, const char *path) +{ + for (int n = 0; n < pl->num_entries; n++) { + struct playlist_entry *e = pl->entries[n]; + e->playlist_path = talloc_strdup(e, path); + } +} + +void playlist_shuffle(struct playlist *pl) +{ + for (int n = 0; n < pl->num_entries; n++) + pl->entries[n]->original_index = n; + for (int n = 0; n < pl->num_entries - 1; n++) { + size_t j = (size_t)((pl->num_entries - n) * mp_rand_next_double()); + MPSWAP(struct playlist_entry *, pl->entries[n], pl->entries[n + j]); + } + playlist_update_indexes(pl, 0, -1); +} + +#define CMP_INT(a, b) ((a) == (b) ? 0 : ((a) > (b) ? 1 : -1)) + +static int cmp_unshuffle(const void *a, const void *b) +{ + struct playlist_entry *ea = *(struct playlist_entry **)a; + struct playlist_entry *eb = *(struct playlist_entry **)b; + + if (ea->original_index >= 0 && ea->original_index != eb->original_index) + return CMP_INT(ea->original_index, eb->original_index); + return CMP_INT(ea->pl_index, eb->pl_index); +} + +void playlist_unshuffle(struct playlist *pl) +{ + if (pl->num_entries) + qsort(pl->entries, pl->num_entries, sizeof(pl->entries[0]), cmp_unshuffle); + playlist_update_indexes(pl, 0, -1); +} + +// (Explicitly ignores current_was_replaced.) +struct playlist_entry *playlist_get_first(struct playlist *pl) +{ + return pl->num_entries ? pl->entries[0] : NULL; +} + +// (Explicitly ignores current_was_replaced.) +struct playlist_entry *playlist_get_last(struct playlist *pl) +{ + return pl->num_entries ? pl->entries[pl->num_entries - 1] : NULL; +} + +struct playlist_entry *playlist_get_next(struct playlist *pl, int direction) +{ + assert(direction == -1 || direction == +1); + if (!pl->current) + return NULL; + assert(pl->current->pl == pl); + if (direction < 0) + return playlist_entry_get_rel(pl->current, -1); + return pl->current_was_replaced ? pl->current : + playlist_entry_get_rel(pl->current, 1); +} + +// (Explicitly ignores current_was_replaced.) +struct playlist_entry *playlist_entry_get_rel(struct playlist_entry *e, + int direction) +{ + assert(direction == -1 || direction == +1); + if (!e->pl) + return NULL; + return playlist_entry_from_index(e->pl, e->pl_index + direction); +} + +struct playlist_entry *playlist_get_first_in_next_playlist(struct playlist *pl, + int direction) +{ + struct playlist_entry *entry = playlist_get_next(pl, direction); + if (!entry) + return NULL; + + while (entry && entry->playlist_path && pl->current->playlist_path && + strcmp(entry->playlist_path, pl->current->playlist_path) == 0) + entry = playlist_entry_get_rel(entry, direction); + + if (direction < 0) + entry = playlist_get_first_in_same_playlist(entry, + pl->current->playlist_path); + + return entry; +} + +struct playlist_entry *playlist_get_first_in_same_playlist( + struct playlist_entry *entry, char *current_playlist_path) +{ + void *tmp = talloc_new(NULL); + + if (!entry || !entry->playlist_path) + goto exit; + + // Don't go to the beginning of the playlist when the current playlist-path + // starts with the previous playlist-path, e.g. with mpv --loop-playlist + // archive_dir/, which expands to archive_dir/{1..9}.zip, the current + // playlist path "archive_dir/1.zip" begins with the playlist-path + // "archive_dir/" of {2..9}.zip, so go to 9.zip instead of 2.zip. But + // playlist-prev-playlist from e.g. the directory "foobar" to the directory + // "foo" should still go to the first entry in "foo/", and this should all + // work whether mpv's arguments have trailing slashes or not, e.g. in the + // first example: + // mpv archive_dir results in the playlist-paths "archive_dir/1.zip" and + // "archive_dir" + // mpv archive_dir/ in "archive_dir/1.zip" and "archive_dir/" + // mpv archive_dir// in "archive_dir//1.zip" and "archive_dir//" + // Always adding a separator to entry->playlist_path to fix the foobar foo + // case would break the previous 2 cases instead. Stripping the separator + // from entry->playlist_path if present and appending it again makes this + // work in all cases. + char* playlist_path = talloc_strdup(tmp, entry->playlist_path); + mp_path_strip_trailing_separator(playlist_path); + if (bstr_startswith(bstr0(current_playlist_path), + bstr0(talloc_strdup_append(playlist_path, "/"))) +#if HAVE_DOS_PATHS + || + bstr_startswith(bstr0(current_playlist_path), + bstr0(talloc_strdup_append(playlist_path, "\\"))) +#endif + ) + goto exit; + + struct playlist_entry *prev = playlist_entry_get_rel(entry, -1); + + while (prev && prev->playlist_path && + strcmp(prev->playlist_path, entry->playlist_path) == 0) { + entry = prev; + prev = playlist_entry_get_rel(entry, -1); + } + +exit: + talloc_free(tmp); + return entry; +} + +void playlist_add_base_path(struct playlist *pl, bstr base_path) +{ + if (base_path.len == 0 || bstrcmp0(base_path, ".") == 0) + return; + for (int n = 0; n < pl->num_entries; n++) { + struct playlist_entry *e = pl->entries[n]; + if (!mp_is_url(bstr0(e->filename))) { + char *new_file = mp_path_join_bstr(e, base_path, bstr0(e->filename)); + talloc_free(e->filename); + e->filename = new_file; + } + } +} + +void playlist_set_stream_flags(struct playlist *pl, int flags) +{ + for (int n = 0; n < pl->num_entries; n++) + pl->entries[n]->stream_flags = flags; +} + +static int64_t playlist_transfer_entries_to(struct playlist *pl, int dst_index, + struct playlist *source_pl) +{ + assert(pl != source_pl); + struct playlist_entry *first = playlist_get_first(source_pl); + + int count = source_pl->num_entries; + MP_TARRAY_INSERT_N_AT(pl, pl->entries, pl->num_entries, dst_index, count); + + for (int n = 0; n < count; n++) { + struct playlist_entry *e = source_pl->entries[n]; + e->pl = pl; + e->pl_index = dst_index + n; + e->id = ++pl->id_alloc; + pl->entries[e->pl_index] = e; + talloc_steal(pl, e); + } + + playlist_update_indexes(pl, dst_index + count, -1); + source_pl->num_entries = 0; + + return first ? first->id : 0; +} + +// Move all entries from source_pl to pl, appending them after the current entry +// of pl. source_pl will be empty, and all entries have changed ownership to pl. +// Return the new ID of the first added entry within pl (0 if source_pl was +// empty). The IDs of all added entries increase by 1 each entry (you can +// predict the ID of the last entry). +int64_t playlist_transfer_entries(struct playlist *pl, struct playlist *source_pl) +{ + + int add_at = pl->num_entries; + if (pl->current) { + add_at = pl->current->pl_index + 1; + if (pl->current_was_replaced) + add_at += 1; + } + assert(add_at >= 0); + assert(add_at <= pl->num_entries); + + return playlist_transfer_entries_to(pl, add_at, source_pl); +} + +int64_t playlist_append_entries(struct playlist *pl, struct playlist *source_pl) +{ + return playlist_transfer_entries_to(pl, pl->num_entries, source_pl); +} + +// Return number of entries between list start and e. +// Return -1 if e is not on the list, or if e is NULL. +int playlist_entry_to_index(struct playlist *pl, struct playlist_entry *e) +{ + if (!e || e->pl != pl) + return -1; + return e->pl_index; +} + +int playlist_entry_count(struct playlist *pl) +{ + return pl->num_entries; +} + +// Return entry for which playlist_entry_to_index() would return index. +// Return NULL if not found. +struct playlist_entry *playlist_entry_from_index(struct playlist *pl, int index) +{ + return index >= 0 && index < pl->num_entries ? pl->entries[index] : NULL; +} + +struct playlist *playlist_parse_file(const char *file, struct mp_cancel *cancel, + struct mpv_global *global) +{ + struct mp_log *log = mp_log_new(NULL, global->log, "!playlist_parser"); + mp_verbose(log, "Parsing playlist file %s...\n", file); + + struct demuxer_params p = { + .force_format = "playlist", + .stream_flags = STREAM_ORIGIN_DIRECT, + }; + struct demuxer *d = demux_open_url(file, &p, cancel, global); + if (!d) { + talloc_free(log); + return NULL; + } + + struct playlist *ret = NULL; + if (d && d->playlist) { + ret = talloc_zero(NULL, struct playlist); + playlist_transfer_entries(ret, d->playlist); + if (d->filetype && strcmp(d->filetype, "hls") == 0) { + mp_warn(log, "This might be a HLS stream. For correct operation, " + "pass it to the player\ndirectly. Don't use --playlist.\n"); + } + } + demux_free(d); + + if (ret) { + mp_verbose(log, "Playlist successfully parsed\n"); + } else { + mp_err(log, "Error while parsing playlist\n"); + } + + if (ret && !ret->num_entries) + mp_warn(log, "Warning: empty playlist\n"); + + talloc_free(log); + return ret; +} diff --git a/common/playlist.h b/common/playlist.h new file mode 100644 index 0000000..aecd539 --- /dev/null +++ b/common/playlist.h @@ -0,0 +1,121 @@ +/* + * 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_PLAYLIST_H +#define MPLAYER_PLAYLIST_H + +#include <stdbool.h> +#include "misc/bstr.h" + +struct playlist_param { + bstr name, value; +}; + +struct playlist_entry { + // Invariant: (pl && pl->entries[pl_index] == this) || (!pl && pl_index < 0) + struct playlist *pl; + int pl_index; + + uint64_t id; + + char *filename; + char *playlist_path; + + struct playlist_param *params; + int num_params; + + char *title; + + // Used for unshuffling: the pl_index before it was shuffled. -1 => unknown. + int original_index; + + // Set to true if this playlist entry was selected while trying to go backwards + // in the playlist. If this is true and the playlist entry fails to play later, + // then mpv tries to go to the next previous entry. This flag is always cleared + // regardless if the attempt was successful or not. + bool playlist_prev_attempt : 1; + + // Set to true if not at least 1 frame (audio or video) could be played. + bool init_failed : 1; + // Entry was removed with playlist_remove (etc.), but not deallocated. + bool removed : 1; + // Additional refcount. Normally (reserved==0), the entry is owned by the + // playlist, and this can be used to keep the entry alive. + int reserved; + // Any flags from STREAM_ORIGIN_FLAGS. 0 if unknown. + // Used to reject loading of unsafe entries from external playlists. + int stream_flags; +}; + +struct playlist { + struct playlist_entry **entries; + int num_entries; + + // This provides some sort of stable iterator. If this entry is removed from + // the playlist, current is set to the next element (or NULL), and + // current_was_replaced is set to true. + struct playlist_entry *current; + bool current_was_replaced; + + uint64_t id_alloc; +}; + +void playlist_entry_add_param(struct playlist_entry *e, bstr name, bstr value); +void playlist_entry_add_params(struct playlist_entry *e, + struct playlist_param *params, + int params_count); + +struct playlist_entry *playlist_entry_new(const char *filename); + +void playlist_add(struct playlist *pl, struct playlist_entry *add); +void playlist_remove(struct playlist *pl, struct playlist_entry *entry); +void playlist_clear(struct playlist *pl); +void playlist_clear_except_current(struct playlist *pl); + +void playlist_move(struct playlist *pl, struct playlist_entry *entry, + struct playlist_entry *at); + +void playlist_add_file(struct playlist *pl, const char *filename); +void playlist_populate_playlist_path(struct playlist *pl, const char *path); +void playlist_shuffle(struct playlist *pl); +void playlist_unshuffle(struct playlist *pl); +struct playlist_entry *playlist_get_first(struct playlist *pl); +struct playlist_entry *playlist_get_last(struct playlist *pl); +struct playlist_entry *playlist_get_next(struct playlist *pl, int direction); +struct playlist_entry *playlist_entry_get_rel(struct playlist_entry *e, + int direction); +struct playlist_entry *playlist_get_first_in_next_playlist(struct playlist *pl, + int direction); +struct playlist_entry *playlist_get_first_in_same_playlist(struct playlist_entry *entry, + char *current_playlist_path); +void playlist_add_base_path(struct playlist *pl, bstr base_path); +void playlist_set_stream_flags(struct playlist *pl, int flags); +int64_t playlist_transfer_entries(struct playlist *pl, struct playlist *source_pl); +int64_t playlist_append_entries(struct playlist *pl, struct playlist *source_pl); + +int playlist_entry_to_index(struct playlist *pl, struct playlist_entry *e); +int playlist_entry_count(struct playlist *pl); +struct playlist_entry *playlist_entry_from_index(struct playlist *pl, int index); + +struct mp_cancel; +struct mpv_global; +struct playlist *playlist_parse_file(const char *file, struct mp_cancel *cancel, + struct mpv_global *global); + +void playlist_entry_unref(struct playlist_entry *e); + +#endif diff --git a/common/recorder.c b/common/recorder.c new file mode 100644 index 0000000..42ae7f8 --- /dev/null +++ b/common/recorder.c @@ -0,0 +1,422 @@ +/* + * 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 <math.h> + +#include <libavformat/avformat.h> + +#include "common/av_common.h" +#include "common/common.h" +#include "common/global.h" +#include "common/msg.h" +#include "demux/demux.h" +#include "demux/packet.h" +#include "demux/stheader.h" + +#include "recorder.h" + +// Maximum number of packets we buffer at most to attempt to resync streams. +// Essentially, this should be higher than the highest supported keyframe +// interval. +#define QUEUE_MAX_PACKETS 256 +// Number of packets we should buffer at least to determine timestamps (due to +// codec delay and frame reordering, and potentially lack of DTS). +// Keyframe flags can trigger this earlier. +#define QUEUE_MIN_PACKETS 16 + +struct mp_recorder { + struct mpv_global *global; + struct mp_log *log; + + struct mp_recorder_sink **streams; + int num_streams; + + bool opened; // mux context is valid + bool muxing; // we're currently recording (instead of preparing) + bool muxing_from_start; // no discontinuity at start + bool dts_warning; + + // The start timestamp of the currently recorded segment (the timestamp of + // the first packet of the incoming packet stream). + double base_ts; + // The output packet timestamp corresponding to base_ts. It's the timestamp + // of the first packet of the current segment written to the output. + double rebase_ts; + + AVFormatContext *mux; +}; + +struct mp_recorder_sink { + struct mp_recorder *owner; + struct sh_stream *sh; + AVStream *av_stream; + AVPacket *avpkt; + double max_out_pts; + bool discont; + bool proper_eof; + struct demux_packet **packets; + int num_packets; +}; + +static int add_stream(struct mp_recorder *priv, struct sh_stream *sh) +{ + enum AVMediaType av_type = mp_to_av_stream_type(sh->type); + int ret = -1; + AVCodecParameters *avp = NULL; + if (av_type == AVMEDIA_TYPE_UNKNOWN) + goto done; + + struct mp_recorder_sink *rst = talloc(priv, struct mp_recorder_sink); + *rst = (struct mp_recorder_sink) { + .owner = priv, + .sh = sh, + .av_stream = avformat_new_stream(priv->mux, NULL), + .avpkt = av_packet_alloc(), + .max_out_pts = MP_NOPTS_VALUE, + }; + + if (!rst->av_stream || !rst->avpkt) + goto done; + + avp = mp_codec_params_to_av(sh->codec); + if (!avp) + goto done; + + // Check if we get the same codec_id for the output format; + // otherwise clear it to have a chance at muxing + if (av_codec_get_id(priv->mux->oformat->codec_tag, + avp->codec_tag) != avp->codec_id) + avp->codec_tag = 0; + + // We don't know the delay, so make something up. If the format requires + // DTS, the result will probably be broken. FFmpeg provides nothing better + // yet (unless you demux with libavformat, which contains tons of hacks + // that try to determine a PTS). + if (!sh->codec->lav_codecpar) + avp->video_delay = 16; + + if (avp->codec_id == AV_CODEC_ID_NONE) + goto done; + + if (avcodec_parameters_copy(rst->av_stream->codecpar, avp) < 0) + goto done; + + ret = 0; + rst->av_stream->time_base = mp_get_codec_timebase(sh->codec); + + MP_TARRAY_APPEND(priv, priv->streams, priv->num_streams, rst); + +done: + if (avp) + avcodec_parameters_free(&avp); + return ret; +} + +struct mp_recorder *mp_recorder_create(struct mpv_global *global, + const char *target_file, + struct sh_stream **streams, + int num_streams, + struct demux_attachment **attachments, + int num_attachments) +{ + struct mp_recorder *priv = talloc_zero(NULL, struct mp_recorder); + + priv->global = global; + priv->log = mp_log_new(priv, global->log, "recorder"); + + if (!num_streams) { + MP_ERR(priv, "No streams.\n"); + goto error; + } + + priv->mux = avformat_alloc_context(); + if (!priv->mux) + goto error; + + priv->mux->oformat = av_guess_format(NULL, target_file, NULL); + if (!priv->mux->oformat) { + MP_ERR(priv, "Output format not found.\n"); + goto error; + } + + if (avio_open2(&priv->mux->pb, target_file, AVIO_FLAG_WRITE, NULL, NULL) < 0) { + MP_ERR(priv, "Failed opening output file.\n"); + goto error; + } + + for (int n = 0; n < num_streams; n++) { + if (add_stream(priv, streams[n]) < 0) { + MP_ERR(priv, "Can't mux one of the input streams.\n"); + goto error; + } + } + + if (!strcmp(priv->mux->oformat->name, "matroska")) { + // Only attach attachments (fonts) to matroska - mp4, nut, mpegts don't + // like them, and we find that out too late in the muxing process. + AVStream *a_stream = NULL; + for (int i = 0; i < num_attachments; ++i) { + a_stream = avformat_new_stream(priv->mux, NULL); + if (!a_stream) { + MP_ERR(priv, "Can't mux one of the attachments.\n"); + goto error; + } + struct demux_attachment *attachment = attachments[i]; + + a_stream->codecpar->codec_type = AVMEDIA_TYPE_ATTACHMENT; + + a_stream->codecpar->extradata = av_mallocz( + attachment->data_size + AV_INPUT_BUFFER_PADDING_SIZE + ); + if (!a_stream->codecpar->extradata) { + goto error; + } + memcpy(a_stream->codecpar->extradata, + attachment->data, attachment->data_size); + a_stream->codecpar->extradata_size = attachment->data_size; + + av_dict_set(&a_stream->metadata, "filename", attachment->name, 0); + av_dict_set(&a_stream->metadata, "mimetype", attachment->type, 0); + } + } + + // Not sure how to write this in a "standard" way. It appears only mkv + // and mp4 support this directly. + char version[200]; + snprintf(version, sizeof(version), "%s experimental stream recording " + "feature (can generate broken files - please report bugs)", + mpv_version); + av_dict_set(&priv->mux->metadata, "encoding_tool", version, 0); + + if (avformat_write_header(priv->mux, NULL) < 0) { + MP_ERR(priv, "Writing header failed.\n"); + goto error; + } + + priv->opened = true; + priv->muxing_from_start = true; + + priv->base_ts = MP_NOPTS_VALUE; + priv->rebase_ts = 0; + + MP_WARN(priv, "This is an experimental feature. Output files might be " + "broken or not play correctly with various players " + "(including mpv itself).\n"); + + return priv; + +error: + mp_recorder_destroy(priv); + return NULL; +} + +static void flush_packets(struct mp_recorder *priv) +{ + for (int n = 0; n < priv->num_streams; n++) { + struct mp_recorder_sink *rst = priv->streams[n]; + for (int i = 0; i < rst->num_packets; i++) + talloc_free(rst->packets[i]); + rst->num_packets = 0; + } +} + +static void mux_packet(struct mp_recorder_sink *rst, + struct demux_packet *pkt) +{ + struct mp_recorder *priv = rst->owner; + struct demux_packet mpkt = *pkt; + + double diff = priv->rebase_ts - priv->base_ts; + mpkt.pts = MP_ADD_PTS(mpkt.pts, diff); + mpkt.dts = MP_ADD_PTS(mpkt.dts, diff); + + rst->max_out_pts = MP_PTS_MAX(rst->max_out_pts, pkt->pts); + + mp_set_av_packet(rst->avpkt, &mpkt, &rst->av_stream->time_base); + + rst->avpkt->stream_index = rst->av_stream->index; + + if (rst->avpkt->duration < 0 && rst->sh->type != STREAM_SUB) + rst->avpkt->duration = 0; + + AVPacket *new_packet = av_packet_clone(rst->avpkt); + if (!new_packet) { + MP_ERR(priv, "Failed to allocate packet.\n"); + return; + } + + if (av_interleaved_write_frame(priv->mux, new_packet) < 0) + MP_ERR(priv, "Failed writing packet.\n"); + + av_packet_free(&new_packet); +} + +// Write all packets available in the stream queue +static void mux_packets(struct mp_recorder_sink *rst) +{ + struct mp_recorder *priv = rst->owner; + if (!priv->muxing || !rst->num_packets) + return; + + for (int n = 0; n < rst->num_packets; n++) { + mux_packet(rst, rst->packets[n]); + talloc_free(rst->packets[n]); + } + + rst->num_packets = 0; +} + +// If there was a discontinuity, check whether we can resume muxing (and from +// where). +static void check_restart(struct mp_recorder *priv) +{ + if (priv->muxing) + return; + + double min_ts = MP_NOPTS_VALUE; + double rebase_ts = 0; + for (int n = 0; n < priv->num_streams; n++) { + struct mp_recorder_sink *rst = priv->streams[n]; + int min_packets = rst->sh->type == STREAM_VIDEO ? QUEUE_MIN_PACKETS : 1; + + rebase_ts = MP_PTS_MAX(rebase_ts, rst->max_out_pts); + + if (rst->num_packets < min_packets) { + if (!rst->proper_eof && rst->sh->type != STREAM_SUB) + return; + continue; + } + + for (int i = 0; i < min_packets; i++) + min_ts = MP_PTS_MIN(min_ts, rst->packets[i]->pts); + } + + // Subtitle only stream (wait longer) or stream without any PTS (fuck it). + if (min_ts == MP_NOPTS_VALUE) + return; + + priv->rebase_ts = rebase_ts; + priv->base_ts = min_ts; + + for (int n = 0; n < priv->num_streams; n++) { + struct mp_recorder_sink *rst = priv->streams[n]; + rst->max_out_pts = min_ts; + } + + priv->muxing = true; + + if (!priv->muxing_from_start) + MP_WARN(priv, "Discontinuity at timestamp %f.\n", priv->rebase_ts); +} + +void mp_recorder_destroy(struct mp_recorder *priv) +{ + if (priv->opened) { + for (int n = 0; n < priv->num_streams; n++) { + struct mp_recorder_sink *rst = priv->streams[n]; + mux_packets(rst); + mp_free_av_packet(&rst->avpkt); + } + + if (av_write_trailer(priv->mux) < 0) + MP_ERR(priv, "Writing trailer failed.\n"); + } + + if (priv->mux) { + if (avio_closep(&priv->mux->pb) < 0) + MP_ERR(priv, "Closing file failed\n"); + + avformat_free_context(priv->mux); + } + + flush_packets(priv); + talloc_free(priv); +} + +// This is called on a seek, or when recording was started mid-stream. +void mp_recorder_mark_discontinuity(struct mp_recorder *priv) +{ + + for (int n = 0; n < priv->num_streams; n++) { + struct mp_recorder_sink *rst = priv->streams[n]; + mux_packets(rst); + rst->discont = true; + rst->proper_eof = false; + } + + flush_packets(priv); + priv->muxing = false; + priv->muxing_from_start = false; +} + +// Get a stream for writing. The pointer is valid until mp_recorder is +// destroyed. The stream ptr. is the same as one passed to +// mp_recorder_create() (returns NULL if it wasn't). +struct mp_recorder_sink *mp_recorder_get_sink(struct mp_recorder *r, + struct sh_stream *stream) +{ + for (int n = 0; n < r->num_streams; n++) { + struct mp_recorder_sink *rst = r->streams[n]; + if (rst->sh == stream) + return rst; + } + return NULL; +} + +// Pass a packet to the given stream. The function does not own the packet, but +// can create a new reference to it if it needs to retain it. Can be NULL to +// signal proper end of stream. +void mp_recorder_feed_packet(struct mp_recorder_sink *rst, + struct demux_packet *pkt) +{ + struct mp_recorder *priv = rst->owner; + + if (!pkt) { + rst->proper_eof = true; + check_restart(priv); + mux_packets(rst); + return; + } + + if (pkt->dts == MP_NOPTS_VALUE && !priv->dts_warning) { + // No, FFmpeg has no actually usable helpers to generate correct DTS. + // No, FFmpeg doesn't tell us which formats need DTS at all. + // No, we can not shut up the FFmpeg warning, which will follow. + MP_WARN(priv, "Source stream misses DTS on at least some packets!\n" + "If the target file format requires DTS, the written " + "file will be invalid.\n"); + priv->dts_warning = true; + } + + if (rst->discont && !pkt->keyframe) + return; + rst->discont = false; + + if (rst->num_packets >= QUEUE_MAX_PACKETS) { + MP_ERR(priv, "Stream %d has too many queued packets; dropping.\n", + rst->av_stream->index); + return; + } + + pkt = demux_copy_packet(pkt); + if (!pkt) + return; + MP_TARRAY_APPEND(rst, rst->packets, rst->num_packets, pkt); + + check_restart(priv); + mux_packets(rst); +} diff --git a/common/recorder.h b/common/recorder.h new file mode 100644 index 0000000..e86d978 --- /dev/null +++ b/common/recorder.h @@ -0,0 +1,25 @@ +#ifndef MP_RECORDER_H_ +#define MP_RECORDER_H_ + +struct mp_recorder; +struct mpv_global; +struct demux_packet; +struct sh_stream; +struct demux_attachment; +struct mp_recorder_sink; + +struct mp_recorder *mp_recorder_create(struct mpv_global *global, + const char *target_file, + struct sh_stream **streams, + int num_streams, + struct demux_attachment **demux_attachments, + int num_attachments); +void mp_recorder_destroy(struct mp_recorder *r); +void mp_recorder_mark_discontinuity(struct mp_recorder *r); + +struct mp_recorder_sink *mp_recorder_get_sink(struct mp_recorder *r, + struct sh_stream *stream); +void mp_recorder_feed_packet(struct mp_recorder_sink *s, + struct demux_packet *pkt); + +#endif diff --git a/common/stats.c b/common/stats.c new file mode 100644 index 0000000..c5f1e50 --- /dev/null +++ b/common/stats.c @@ -0,0 +1,325 @@ +#include <stdatomic.h> +#include <time.h> +#include <unistd.h> + +#include "common.h" +#include "global.h" +#include "misc/linked_list.h" +#include "misc/node.h" +#include "msg.h" +#include "options/m_option.h" +#include "osdep/threads.h" +#include "osdep/timer.h" +#include "stats.h" + +struct stats_base { + struct mpv_global *global; + + atomic_bool active; + + mp_mutex lock; + + struct { + struct stats_ctx *head, *tail; + } list; + + struct stat_entry **entries; + int num_entries; + + int64_t last_time; +}; + +struct stats_ctx { + struct stats_base *base; + const char *prefix; + + struct { + struct stats_ctx *prev, *next; + } list; + + struct stat_entry **entries; + int num_entries; +}; + +enum val_type { + VAL_UNSET = 0, + VAL_STATIC, + VAL_STATIC_SIZE, + VAL_INC, + VAL_TIME, + VAL_THREAD_CPU_TIME, +}; + +struct stat_entry { + char name[32]; + const char *full_name; // including stats_ctx.prefix + + enum val_type type; + double val_d; + int64_t val_rt; + int64_t val_th; + int64_t time_start_ns; + int64_t cpu_start_ns; + mp_thread_id thread_id; +}; + +#define IS_ACTIVE(ctx) \ + (atomic_load_explicit(&(ctx)->base->active, memory_order_relaxed)) + +static void stats_destroy(void *p) +{ + struct stats_base *stats = p; + + // All entries must have been destroyed before this. + assert(!stats->list.head); + + mp_mutex_destroy(&stats->lock); +} + +void stats_global_init(struct mpv_global *global) +{ + assert(!global->stats); + struct stats_base *stats = talloc_zero(global, struct stats_base); + ta_set_destructor(stats, stats_destroy); + mp_mutex_init(&stats->lock); + + global->stats = stats; + stats->global = global; +} + +static void add_stat(struct mpv_node *list, struct stat_entry *e, + const char *suffix, double num_val, char *text) +{ + struct mpv_node *ne = node_array_add(list, MPV_FORMAT_NODE_MAP); + + node_map_add_string(ne, "name", suffix ? + mp_tprintf(80, "%s/%s", e->full_name, suffix) : e->full_name); + node_map_add_double(ne, "value", num_val); + if (text) + node_map_add_string(ne, "text", text); +} + +static int cmp_entry(const void *p1, const void *p2) +{ + struct stat_entry **e1 = (void *)p1; + struct stat_entry **e2 = (void *)p2; + return strcmp((*e1)->full_name, (*e2)->full_name); +} + +void stats_global_query(struct mpv_global *global, struct mpv_node *out) +{ + struct stats_base *stats = global->stats; + assert(stats); + + mp_mutex_lock(&stats->lock); + + atomic_store(&stats->active, true); + + if (!stats->num_entries) { + for (struct stats_ctx *ctx = stats->list.head; ctx; ctx = ctx->list.next) + { + for (int n = 0; n < ctx->num_entries; n++) { + MP_TARRAY_APPEND(stats, stats->entries, stats->num_entries, + ctx->entries[n]); + } + } + if (stats->num_entries) { + qsort(stats->entries, stats->num_entries, sizeof(stats->entries[0]), + cmp_entry); + } + } + + node_init(out, MPV_FORMAT_NODE_ARRAY, NULL); + + int64_t now = mp_time_ns(); + if (stats->last_time) { + double t_ms = MP_TIME_NS_TO_MS(now - stats->last_time); + struct mpv_node *ne = node_array_add(out, MPV_FORMAT_NODE_MAP); + node_map_add_string(ne, "name", "poll-time"); + node_map_add_double(ne, "value", t_ms); + node_map_add_string(ne, "text", mp_tprintf(80, "%.2f ms", t_ms)); + + // Very dirty way to reset everything if the stats.lua page was probably + // closed. Not enough energy left for clean solution. Fuck it. + if (t_ms > 2000) { + for (int n = 0; n < stats->num_entries; n++) { + struct stat_entry *e = stats->entries[n]; + + e->cpu_start_ns = 0; + e->val_rt = e->val_th = 0; + if (e->type != VAL_THREAD_CPU_TIME) + e->type = 0; + } + } + } + stats->last_time = now; + + for (int n = 0; n < stats->num_entries; n++) { + struct stat_entry *e = stats->entries[n]; + + switch (e->type) { + case VAL_STATIC: + add_stat(out, e, NULL, e->val_d, NULL); + break; + case VAL_STATIC_SIZE: { + char *s = format_file_size(e->val_d); + add_stat(out, e, NULL, e->val_d, s); + talloc_free(s); + break; + } + case VAL_INC: + add_stat(out, e, NULL, e->val_d, NULL); + e->val_d = 0; + break; + case VAL_TIME: { + double t_cpu = MP_TIME_NS_TO_MS(e->val_th); + add_stat(out, e, "cpu", t_cpu, mp_tprintf(80, "%.2f ms", t_cpu)); + double t_rt = MP_TIME_NS_TO_MS(e->val_rt); + add_stat(out, e, "time", t_rt, mp_tprintf(80, "%.2f ms", t_rt)); + e->val_rt = e->val_th = 0; + break; + } + case VAL_THREAD_CPU_TIME: { + int64_t t = mp_thread_cpu_time_ns(e->thread_id); + if (!e->cpu_start_ns) + e->cpu_start_ns = t; + double t_msec = MP_TIME_NS_TO_MS(t - e->cpu_start_ns); + add_stat(out, e, NULL, t_msec, mp_tprintf(80, "%.2f ms", t_msec)); + e->cpu_start_ns = t; + break; + } + default: ; + } + } + + mp_mutex_unlock(&stats->lock); +} + +static void stats_ctx_destroy(void *p) +{ + struct stats_ctx *ctx = p; + + mp_mutex_lock(&ctx->base->lock); + LL_REMOVE(list, &ctx->base->list, ctx); + ctx->base->num_entries = 0; // invalidate + mp_mutex_unlock(&ctx->base->lock); +} + +struct stats_ctx *stats_ctx_create(void *ta_parent, struct mpv_global *global, + const char *prefix) +{ + struct stats_base *base = global->stats; + assert(base); + + struct stats_ctx *ctx = talloc_zero(ta_parent, struct stats_ctx); + ctx->base = base; + ctx->prefix = talloc_strdup(ctx, prefix); + ta_set_destructor(ctx, stats_ctx_destroy); + + mp_mutex_lock(&base->lock); + LL_APPEND(list, &base->list, ctx); + base->num_entries = 0; // invalidate + mp_mutex_unlock(&base->lock); + + return ctx; +} + +static struct stat_entry *find_entry(struct stats_ctx *ctx, const char *name) +{ + for (int n = 0; n < ctx->num_entries; n++) { + if (strcmp(ctx->entries[n]->name, name) == 0) + return ctx->entries[n]; + } + + struct stat_entry *e = talloc_zero(ctx, struct stat_entry); + snprintf(e->name, sizeof(e->name), "%s", name); + assert(strcmp(e->name, name) == 0); // make e->name larger and don't complain + + e->full_name = talloc_asprintf(e, "%s/%s", ctx->prefix, e->name); + + MP_TARRAY_APPEND(ctx, ctx->entries, ctx->num_entries, e); + ctx->base->num_entries = 0; // invalidate + + return e; +} + +static void static_value(struct stats_ctx *ctx, const char *name, double val, + enum val_type type) +{ + if (!IS_ACTIVE(ctx)) + return; + mp_mutex_lock(&ctx->base->lock); + struct stat_entry *e = find_entry(ctx, name); + e->val_d = val; + e->type = type; + mp_mutex_unlock(&ctx->base->lock); +} + +void stats_value(struct stats_ctx *ctx, const char *name, double val) +{ + static_value(ctx, name, val, VAL_STATIC); +} + +void stats_size_value(struct stats_ctx *ctx, const char *name, double val) +{ + static_value(ctx, name, val, VAL_STATIC_SIZE); +} + +void stats_time_start(struct stats_ctx *ctx, const char *name) +{ + MP_STATS(ctx->base->global, "start %s", name); + if (!IS_ACTIVE(ctx)) + return; + mp_mutex_lock(&ctx->base->lock); + struct stat_entry *e = find_entry(ctx, name); + e->cpu_start_ns = mp_thread_cpu_time_ns(mp_thread_current_id()); + e->time_start_ns = mp_time_ns(); + mp_mutex_unlock(&ctx->base->lock); +} + +void stats_time_end(struct stats_ctx *ctx, const char *name) +{ + MP_STATS(ctx->base->global, "end %s", name); + if (!IS_ACTIVE(ctx)) + return; + mp_mutex_lock(&ctx->base->lock); + struct stat_entry *e = find_entry(ctx, name); + if (e->time_start_ns) { + e->type = VAL_TIME; + e->val_rt += mp_time_ns() - e->time_start_ns; + e->val_th += mp_thread_cpu_time_ns(mp_thread_current_id()) - e->cpu_start_ns; + e->time_start_ns = 0; + } + mp_mutex_unlock(&ctx->base->lock); +} + +void stats_event(struct stats_ctx *ctx, const char *name) +{ + if (!IS_ACTIVE(ctx)) + return; + mp_mutex_lock(&ctx->base->lock); + struct stat_entry *e = find_entry(ctx, name); + e->val_d += 1; + e->type = VAL_INC; + mp_mutex_unlock(&ctx->base->lock); +} + +static void register_thread(struct stats_ctx *ctx, const char *name, + enum val_type type) +{ + mp_mutex_lock(&ctx->base->lock); + struct stat_entry *e = find_entry(ctx, name); + e->type = type; + e->thread_id = mp_thread_current_id(); + mp_mutex_unlock(&ctx->base->lock); +} + +void stats_register_thread_cputime(struct stats_ctx *ctx, const char *name) +{ + register_thread(ctx, name, VAL_THREAD_CPU_TIME); +} + +void stats_unregister_thread(struct stats_ctx *ctx, const char *name) +{ + register_thread(ctx, name, 0); +} diff --git a/common/stats.h b/common/stats.h new file mode 100644 index 0000000..c3e136e --- /dev/null +++ b/common/stats.h @@ -0,0 +1,34 @@ +#pragma once + +struct mpv_global; +struct mpv_node; +struct stats_ctx; + +void stats_global_init(struct mpv_global *global); +void stats_global_query(struct mpv_global *global, struct mpv_node *out); + +// stats_ctx can be free'd with ta_free(), or by using the ta_parent. +struct stats_ctx *stats_ctx_create(void *ta_parent, struct mpv_global *global, + const char *prefix); + +// A static numeric value. +void stats_value(struct stats_ctx *ctx, const char *name, double val); + +// Like stats_value(), but render as size in bytes. +void stats_size_value(struct stats_ctx *ctx, const char *name, double val); + +// Report the real time and CPU time in seconds between _start and _end calls +// as value, and report the average and number of all times. +void stats_time_start(struct stats_ctx *ctx, const char *name); +void stats_time_end(struct stats_ctx *ctx, const char *name); + +// Display number of events per poll period. +void stats_event(struct stats_ctx *ctx, const char *name); + +// Report the thread's CPU time. This needs to be called only once per thread. +// The current thread is assumed to stay valid until the stats_ctx is destroyed +// or stats_unregister_thread() is called, otherwise UB will occur. +void stats_register_thread_cputime(struct stats_ctx *ctx, const char *name); + +// Remove reference to the current thread. +void stats_unregister_thread(struct stats_ctx *ctx, const char *name); diff --git a/common/tags.c b/common/tags.c new file mode 100644 index 0000000..43f557d --- /dev/null +++ b/common/tags.c @@ -0,0 +1,151 @@ +/* + * 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 <stddef.h> +#include <limits.h> +#include <strings.h> +#include <assert.h> +#include <libavutil/dict.h> +#include "tags.h" +#include "misc/bstr.h" + +void mp_tags_set_str(struct mp_tags *tags, const char *key, const char *value) +{ + mp_tags_set_bstr(tags, bstr0(key), bstr0(value)); +} + +void mp_tags_set_bstr(struct mp_tags *tags, bstr key, bstr value) +{ + for (int n = 0; n < tags->num_keys; n++) { + if (bstrcasecmp0(key, tags->keys[n]) == 0) { + talloc_free(tags->values[n]); + tags->values[n] = bstrto0(tags, value); + return; + } + } + + MP_RESIZE_ARRAY(tags, tags->keys, tags->num_keys + 1); + MP_RESIZE_ARRAY(tags, tags->values, tags->num_keys + 1); + tags->keys[tags->num_keys] = bstrto0(tags, key); + tags->values[tags->num_keys] = bstrto0(tags, value); + tags->num_keys++; +} + +void mp_tags_remove_str(struct mp_tags *tags, const char *key) +{ + mp_tags_remove_bstr(tags, bstr0(key)); +} + +void mp_tags_remove_bstr(struct mp_tags *tags, bstr key) +{ + for (int n = 0; n < tags->num_keys; n++) { + if (bstrcasecmp0(key, tags->keys[n]) == 0) { + talloc_free(tags->keys[n]); + talloc_free(tags->values[n]); + int num_keys = tags->num_keys; // copy so it's only decremented once + MP_TARRAY_REMOVE_AT(tags->keys, num_keys, n); + MP_TARRAY_REMOVE_AT(tags->values, tags->num_keys, n); + } + } +} + +char *mp_tags_get_str(struct mp_tags *tags, const char *key) +{ + return mp_tags_get_bstr(tags, bstr0(key)); +} + +char *mp_tags_get_bstr(struct mp_tags *tags, bstr key) +{ + for (int n = 0; n < tags->num_keys; n++) { + if (bstrcasecmp0(key, tags->keys[n]) == 0) + return tags->values[n]; + } + return NULL; +} + +void mp_tags_clear(struct mp_tags *tags) +{ + *tags = (struct mp_tags){0}; + talloc_free_children(tags); +} + + + +struct mp_tags *mp_tags_dup(void *tparent, struct mp_tags *tags) +{ + struct mp_tags *new = talloc_zero(tparent, struct mp_tags); + mp_tags_replace(new, tags); + return new; +} + +void mp_tags_replace(struct mp_tags *dst, struct mp_tags *src) +{ + mp_tags_clear(dst); + MP_RESIZE_ARRAY(dst, dst->keys, src->num_keys); + MP_RESIZE_ARRAY(dst, dst->values, src->num_keys); + dst->num_keys = src->num_keys; + for (int n = 0; n < src->num_keys; n++) { + dst->keys[n] = talloc_strdup(dst, src->keys[n]); + dst->values[n] = talloc_strdup(dst, src->values[n]); + } +} + +// Return a copy of the tags, but containing only keys in list. Also forces +// the order and casing of the keys (for cosmetic reasons). +// A trailing '*' matches the rest. +struct mp_tags *mp_tags_filtered(void *tparent, struct mp_tags *tags, char **list) +{ + struct mp_tags *new = talloc_zero(tparent, struct mp_tags); + for (int n = 0; list && list[n]; n++) { + char *key = list[n]; + size_t keylen = strlen(key); + if (keylen >= INT_MAX) + continue; + bool prefix = keylen && key[keylen - 1] == '*'; + int matchlen = prefix ? keylen - 1 : keylen + 1; + for (int x = 0; x < tags->num_keys; x++) { + if (strncasecmp(tags->keys[x], key, matchlen) == 0) { + char skey[320]; + snprintf(skey, sizeof(skey), "%.*s%s", matchlen, key, + prefix ? tags->keys[x] + keylen - 1 : ""); + mp_tags_set_str(new, skey, tags->values[x]); + } + } + } + return new; +} + +void mp_tags_merge(struct mp_tags *tags, struct mp_tags *src) +{ + for (int n = 0; n < src->num_keys; n++) + mp_tags_set_str(tags, src->keys[n], src->values[n]); +} + +void mp_tags_copy_from_av_dictionary(struct mp_tags *tags, + struct AVDictionary *av_dict) +{ + AVDictionaryEntry *entry = NULL; + while ((entry = av_dict_get(av_dict, "", entry, AV_DICT_IGNORE_SUFFIX))) + mp_tags_set_str(tags, entry->key, entry->value); +} + +void mp_tags_move_from_av_dictionary(struct mp_tags *tags, + struct AVDictionary **av_dict_ptr) +{ + mp_tags_copy_from_av_dictionary(tags, *av_dict_ptr); + av_dict_free(av_dict_ptr); +} diff --git a/common/tags.h b/common/tags.h new file mode 100644 index 0000000..e7eb4b3 --- /dev/null +++ b/common/tags.h @@ -0,0 +1,31 @@ +#ifndef MP_TAGS_H +#define MP_TAGS_H + +#include <stdint.h> + +#include "misc/bstr.h" + +struct mp_tags { + char **keys; + char **values; + int num_keys; +}; + +void mp_tags_set_str(struct mp_tags *tags, const char *key, const char *value); +void mp_tags_set_bstr(struct mp_tags *tags, bstr key, bstr value); +void mp_tags_remove_str(struct mp_tags *tags, const char *key); +void mp_tags_remove_bstr(struct mp_tags *tags, bstr key); +char *mp_tags_get_str(struct mp_tags *tags, const char *key); +char *mp_tags_get_bstr(struct mp_tags *tags, bstr key); +void mp_tags_clear(struct mp_tags *tags); +struct mp_tags *mp_tags_dup(void *tparent, struct mp_tags *tags); +void mp_tags_replace(struct mp_tags *dst, struct mp_tags *src); +struct mp_tags *mp_tags_filtered(void *tparent, struct mp_tags *tags, char **list); +void mp_tags_merge(struct mp_tags *tags, struct mp_tags *src); +struct AVDictionary; +void mp_tags_copy_from_av_dictionary(struct mp_tags *tags, + struct AVDictionary *av_dict); +void mp_tags_move_from_av_dictionary(struct mp_tags *tags, + struct AVDictionary **av_dict_ptr); + +#endif diff --git a/common/version.c b/common/version.c new file mode 100644 index 0000000..228e3ee --- /dev/null +++ b/common/version.c @@ -0,0 +1,23 @@ +/* + * 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.h" +#include "version.h" + +const char mpv_version[] = "mpv " VERSION; +const char mpv_builddate[] = BUILDDATE; +const char mpv_copyright[] = MPVCOPYRIGHT; diff --git a/common/version.h.in b/common/version.h.in new file mode 100644 index 0000000..b09718f --- /dev/null +++ b/common/version.h.in @@ -0,0 +1,7 @@ +#define VERSION "@VERSION@" +#define MPVCOPYRIGHT "Copyright © 2000-2023 mpv/MPlayer/mplayer2 projects" +#ifndef NO_BUILD_TIMESTAMPS +#define BUILDDATE __DATE__ " " __TIME__ +#else +#define BUILDDATE "UNKNOWN" +#endif |