summaryrefslogtreecommitdiffstats
path: root/common
diff options
context:
space:
mode:
Diffstat (limited to 'common')
-rw-r--r--common/av_common.c404
-rw-r--r--common/av_common.h54
-rw-r--r--common/av_log.c215
-rw-r--r--common/av_log.h11
-rw-r--r--common/codecs.c107
-rw-r--r--common/codecs.h48
-rw-r--r--common/common.c413
-rw-r--r--common/common.h161
-rw-r--r--common/encode.h61
-rw-r--r--common/encode_lavc.c949
-rw-r--r--common/encode_lavc.h114
-rw-r--r--common/global.h15
-rw-r--r--common/meson.build8
-rw-r--r--common/msg.c1069
-rw-r--r--common/msg.h92
-rw-r--r--common/msg_control.h45
-rw-r--r--common/playlist.c413
-rw-r--r--common/playlist.h121
-rw-r--r--common/recorder.c422
-rw-r--r--common/recorder.h25
-rw-r--r--common/stats.c325
-rw-r--r--common/stats.h34
-rw-r--r--common/tags.c151
-rw-r--r--common/tags.h31
-rw-r--r--common/version.c23
-rw-r--r--common/version.h.in7
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