summaryrefslogtreecommitdiffstats
path: root/sub
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 20:36:56 +0000
commit51de1d8436100f725f3576aefa24a2bd2057bc28 (patch)
treec6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /sub
parentInitial commit. (diff)
downloadmpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz
mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sub')
-rw-r--r--sub/ass_mp.c422
-rw-r--r--sub/ass_mp.h65
-rw-r--r--sub/dec_sub.c498
-rw-r--r--sub/dec_sub.h62
-rw-r--r--sub/draw_bmp.c1035
-rw-r--r--sub/draw_bmp.h63
-rw-r--r--sub/filter_jsre.c140
-rw-r--r--sub/filter_regex.c89
-rw-r--r--sub/filter_sdh.c482
-rw-r--r--sub/img_convert.c128
-rw-r--r--sub/img_convert.h23
-rw-r--r--sub/lavc_conv.c293
-rw-r--r--sub/meson.build6
-rw-r--r--sub/osd.c559
-rw-r--r--sub/osd.h247
-rw-r--r--sub/osd_font.otfbin0 -> 4460 bytes
-rw-r--r--sub/osd_libass.c691
-rw-r--r--sub/osd_state.h94
-rw-r--r--sub/sd.h111
-rw-r--r--sub/sd_ass.c1035
-rw-r--r--sub/sd_lavc.c676
21 files changed, 6719 insertions, 0 deletions
diff --git a/sub/ass_mp.c b/sub/ass_mp.c
new file mode 100644
index 0000000..634681f
--- /dev/null
+++ b/sub/ass_mp.c
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com>
+ *
+ * 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 <inttypes.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdarg.h>
+#include <stdbool.h>
+#include <assert.h>
+#include <math.h>
+
+#include <ass/ass.h>
+#include <ass/ass_types.h>
+
+#include "common/common.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "options/path.h"
+#include "ass_mp.h"
+#include "img_convert.h"
+#include "osd.h"
+#include "stream/stream.h"
+#include "options/options.h"
+#include "video/out/bitmap_packer.h"
+#include "video/mp_image.h"
+
+// res_y should be track->PlayResY
+// It determines scaling of font sizes and more.
+void mp_ass_set_style(ASS_Style *style, double res_y,
+ const struct osd_style_opts *opts)
+{
+ if (!style)
+ return;
+
+ if (opts->font) {
+ if (!style->FontName || strcmp(style->FontName, opts->font) != 0) {
+ free(style->FontName);
+ style->FontName = strdup(opts->font);
+ }
+ }
+
+ // libass_font_size = FontSize * (window_height / res_y)
+ // scale translates parameters from PlayResY=720 to res_y
+ double scale = res_y / 720.0;
+
+ style->FontSize = opts->font_size * scale;
+ style->PrimaryColour = MP_ASS_COLOR(opts->color);
+ style->SecondaryColour = style->PrimaryColour;
+ style->OutlineColour = MP_ASS_COLOR(opts->border_color);
+ if (opts->back_color.a) {
+ style->BackColour = MP_ASS_COLOR(opts->back_color);
+ style->BorderStyle = 4; // opaque box
+ } else {
+ style->BackColour = MP_ASS_COLOR(opts->shadow_color);
+ style->BorderStyle = 1; // outline
+ }
+ style->Outline = opts->border_size * scale;
+ style->Shadow = opts->shadow_offset * scale;
+ style->Spacing = opts->spacing * scale;
+ style->MarginL = opts->margin_x * scale;
+ style->MarginR = style->MarginL;
+ style->MarginV = opts->margin_y * scale;
+ style->ScaleX = 1.;
+ style->ScaleY = 1.;
+ style->Alignment = 1 + (opts->align_x + 1) + (opts->align_y + 2) % 3 * 4;
+#ifdef ASS_JUSTIFY_LEFT
+ style->Justify = opts->justify;
+#endif
+ style->Blur = opts->blur;
+ style->Bold = opts->bold;
+ style->Italic = opts->italic;
+}
+
+void mp_ass_configure_fonts(ASS_Renderer *priv, struct osd_style_opts *opts,
+ struct mpv_global *global, struct mp_log *log)
+{
+ void *tmp = talloc_new(NULL);
+ char *default_font = mp_find_config_file(tmp, global, "subfont.ttf");
+ char *config = mp_find_config_file(tmp, global, "fonts.conf");
+
+ if (default_font && !mp_path_exists(default_font))
+ default_font = NULL;
+
+ int font_provider = ASS_FONTPROVIDER_AUTODETECT;
+ if (opts->font_provider == 1)
+ font_provider = ASS_FONTPROVIDER_NONE;
+ if (opts->font_provider == 2)
+ font_provider = ASS_FONTPROVIDER_FONTCONFIG;
+
+ mp_verbose(log, "Setting up fonts...\n");
+ ass_set_fonts(priv, default_font, opts->font, font_provider, config, 1);
+ mp_verbose(log, "Done.\n");
+
+ talloc_free(tmp);
+}
+
+static const int map_ass_level[] = {
+ MSGL_ERR, // 0 "FATAL errors"
+ MSGL_WARN,
+ MSGL_INFO,
+ MSGL_V,
+ MSGL_V,
+ MSGL_DEBUG, // 5 application recommended level
+ MSGL_TRACE,
+ MSGL_TRACE, // 7 "verbose DEBUG"
+};
+
+static void message_callback(int level, const char *format, va_list va, void *ctx)
+{
+ struct mp_log *log = ctx;
+ if (!log)
+ return;
+ level = map_ass_level[level];
+ mp_msg_va(log, level, format, va);
+ // libass messages lack trailing \n
+ mp_msg(log, level, "\n");
+}
+
+ASS_Library *mp_ass_init(struct mpv_global *global,
+ struct osd_style_opts *opts, struct mp_log *log)
+{
+ char *path = opts->fonts_dir && opts->fonts_dir[0] ?
+ mp_get_user_path(NULL, global, opts->fonts_dir) :
+ mp_find_config_file(NULL, global, "fonts");
+ mp_dbg(log, "ASS library version: 0x%x (runtime 0x%x)\n",
+ (unsigned)LIBASS_VERSION, ass_library_version());
+ ASS_Library *priv = ass_library_init();
+ if (!priv)
+ abort();
+ ass_set_message_cb(priv, message_callback, log);
+ if (path)
+ ass_set_fonts_dir(priv, path);
+ talloc_free(path);
+ return priv;
+}
+
+void mp_ass_flush_old_events(ASS_Track *track, long long ts)
+{
+ int n = 0;
+ for (; n < track->n_events; n++) {
+ if ((track->events[n].Start + track->events[n].Duration) >= ts)
+ break;
+ ass_free_event(track, n);
+ track->n_events--;
+ }
+ for (int i = 0; n > 0 && i < track->n_events; i++) {
+ track->events[i] = track->events[i+n];
+ }
+}
+
+static void draw_ass_rgba(unsigned char *src, int src_w, int src_h,
+ int src_stride, unsigned char *dst, size_t dst_stride,
+ int dst_x, int dst_y, uint32_t color)
+{
+ const unsigned int r = (color >> 24) & 0xff;
+ const unsigned int g = (color >> 16) & 0xff;
+ const unsigned int b = (color >> 8) & 0xff;
+ const unsigned int a = 0xff - (color & 0xff);
+
+ dst += dst_y * dst_stride + dst_x * 4;
+
+ for (int y = 0; y < src_h; y++, dst += dst_stride, src += src_stride) {
+ uint32_t *dstrow = (uint32_t *) dst;
+ for (int x = 0; x < src_w; x++) {
+ const unsigned int v = src[x];
+ int rr = (r * a * v);
+ int gg = (g * a * v);
+ int bb = (b * a * v);
+ int aa = a * v;
+ uint32_t dstpix = dstrow[x];
+ unsigned int dstb = dstpix & 0xFF;
+ unsigned int dstg = (dstpix >> 8) & 0xFF;
+ unsigned int dstr = (dstpix >> 16) & 0xFF;
+ unsigned int dsta = (dstpix >> 24) & 0xFF;
+ dstb = (bb + dstb * (255 * 255 - aa)) / (255 * 255);
+ dstg = (gg + dstg * (255 * 255 - aa)) / (255 * 255);
+ dstr = (rr + dstr * (255 * 255 - aa)) / (255 * 255);
+ dsta = (aa * 255 + dsta * (255 * 255 - aa)) / (255 * 255);
+ dstrow[x] = dstb | (dstg << 8) | (dstr << 16) | (dsta << 24);
+ }
+ }
+}
+
+struct mp_ass_packer {
+ struct sub_bitmap *cached_parts; // only for the array memory
+ struct mp_image *cached_img;
+ struct sub_bitmaps cached_subs;
+ bool cached_subs_valid;
+ struct sub_bitmap rgba_imgs[MP_SUB_BB_LIST_MAX];
+ struct bitmap_packer *packer;
+};
+
+// Free with talloc_free().
+struct mp_ass_packer *mp_ass_packer_alloc(void *ta_parent)
+{
+ struct mp_ass_packer *p = talloc_zero(ta_parent, struct mp_ass_packer);
+ p->packer = talloc_zero(p, struct bitmap_packer);
+ return p;
+}
+
+static bool pack(struct mp_ass_packer *p, struct sub_bitmaps *res, int imgfmt)
+{
+ packer_set_size(p->packer, res->num_parts);
+
+ for (int n = 0; n < res->num_parts; n++)
+ p->packer->in[n] = (struct pos){res->parts[n].w, res->parts[n].h};
+
+ if (p->packer->count == 0 || packer_pack(p->packer) < 0)
+ return false;
+
+ struct pos bb[2];
+ packer_get_bb(p->packer, bb);
+
+ res->packed_w = bb[1].x;
+ res->packed_h = bb[1].y;
+
+ if (!p->cached_img || p->cached_img->w < res->packed_w ||
+ p->cached_img->h < res->packed_h ||
+ p->cached_img->imgfmt != imgfmt)
+ {
+ talloc_free(p->cached_img);
+ p->cached_img = mp_image_alloc(imgfmt, p->packer->w, p->packer->h);
+ if (!p->cached_img) {
+ packer_reset(p->packer);
+ return false;
+ }
+ talloc_steal(p, p->cached_img);
+ }
+
+ if (!mp_image_make_writeable(p->cached_img)) {
+ packer_reset(p->packer);
+ return false;
+ }
+
+ res->packed = p->cached_img;
+
+ for (int n = 0; n < res->num_parts; n++) {
+ struct sub_bitmap *b = &res->parts[n];
+ struct pos pos = p->packer->result[n];
+
+ b->src_x = pos.x;
+ b->src_y = pos.y;
+ }
+
+ return true;
+}
+
+static bool pack_libass(struct mp_ass_packer *p, struct sub_bitmaps *res)
+{
+ if (!pack(p, res, IMGFMT_Y8))
+ return false;
+
+ for (int n = 0; n < res->num_parts; n++) {
+ struct sub_bitmap *b = &res->parts[n];
+
+ int stride = res->packed->stride[0];
+ void *pdata =
+ (uint8_t *)res->packed->planes[0] + b->src_y * stride + b->src_x;
+ memcpy_pic(pdata, b->bitmap, b->w, b->h, stride, b->stride);
+
+ b->bitmap = pdata;
+ b->stride = stride;
+ }
+
+ return true;
+}
+
+static bool pack_rgba(struct mp_ass_packer *p, struct sub_bitmaps *res)
+{
+ struct mp_rect bb_list[MP_SUB_BB_LIST_MAX];
+ int num_bb = mp_get_sub_bb_list(res, bb_list, MP_SUB_BB_LIST_MAX);
+
+ struct sub_bitmaps imgs = {
+ .change_id = res->change_id,
+ .format = SUBBITMAP_BGRA,
+ .parts = p->rgba_imgs,
+ .num_parts = num_bb,
+ };
+
+ for (int n = 0; n < imgs.num_parts; n++) {
+ imgs.parts[n].w = bb_list[n].x1 - bb_list[n].x0;
+ imgs.parts[n].h = bb_list[n].y1 - bb_list[n].y0;
+ }
+
+ if (!pack(p, &imgs, IMGFMT_BGRA))
+ return false;
+
+ for (int n = 0; n < num_bb; n++) {
+ struct mp_rect bb = bb_list[n];
+ struct sub_bitmap *b = &imgs.parts[n];
+
+ b->x = bb.x0;
+ b->y = bb.y0;
+ b->w = b->dw = bb.x1 - bb.x0;
+ b->h = b->dh = bb.y1 - bb.y0;
+ b->stride = imgs.packed->stride[0];
+ b->bitmap = (uint8_t *)imgs.packed->planes[0] +
+ b->stride * b->src_y + b->src_x * 4;
+
+ memset_pic(b->bitmap, 0, b->w * 4, b->h, b->stride);
+
+ for (int i = 0; i < res->num_parts; i++) {
+ struct sub_bitmap *s = &res->parts[i];
+
+ // Assume mp_get_sub_bb_list() never splits sub bitmaps
+ // So we don't clip/adjust the size of the sub bitmap
+ if (s->x > bb.x1 || s->x + s->w < bb.x0 ||
+ s->y > bb.y1 || s->y + s->h < bb.y0)
+ continue;
+
+ draw_ass_rgba(s->bitmap, s->w, s->h, s->stride,
+ b->bitmap, b->stride,
+ s->x - bb.x0, s->y - bb.y0,
+ s->libass.color);
+ }
+ }
+
+ *res = imgs;
+ return true;
+}
+
+// Pack the contents of image_lists[0] to image_lists[num_image_lists-1] into
+// a single image, and make *out point to it. *out is completely overwritten.
+// If libass reported any change, image_lists_changed must be set (it then
+// repacks all images). preferred_osd_format can be set to a desired
+// sub_bitmap_format. Currently, only SUBBITMAP_LIBASS is supported.
+void mp_ass_packer_pack(struct mp_ass_packer *p, ASS_Image **image_lists,
+ int num_image_lists, bool image_lists_changed,
+ int preferred_osd_format, struct sub_bitmaps *out)
+{
+ int format = preferred_osd_format == SUBBITMAP_BGRA ? SUBBITMAP_BGRA
+ : SUBBITMAP_LIBASS;
+
+ if (p->cached_subs_valid && !image_lists_changed &&
+ p->cached_subs.format == format)
+ {
+ *out = p->cached_subs;
+ return;
+ }
+
+ *out = (struct sub_bitmaps){.change_id = 1};
+ p->cached_subs_valid = false;
+
+ struct sub_bitmaps res = {
+ .change_id = image_lists_changed,
+ .format = SUBBITMAP_LIBASS,
+ .parts = p->cached_parts,
+ };
+
+ for (int n = 0; n < num_image_lists; n++) {
+ for (struct ass_image *img = image_lists[n]; img; img = img->next) {
+ if (img->w == 0 || img->h == 0)
+ continue;
+ MP_TARRAY_GROW(p, p->cached_parts, res.num_parts);
+ res.parts = p->cached_parts;
+ struct sub_bitmap *b = &res.parts[res.num_parts];
+ b->bitmap = img->bitmap;
+ b->stride = img->stride;
+ b->libass.color = img->color;
+ b->dw = b->w = img->w;
+ b->dh = b->h = img->h;
+ b->x = img->dst_x;
+ b->y = img->dst_y;
+ res.num_parts++;
+ }
+ }
+
+ bool r = false;
+ if (format == SUBBITMAP_BGRA) {
+ r = pack_rgba(p, &res);
+ } else {
+ r = pack_libass(p, &res);
+ }
+
+ if (!r)
+ return;
+
+ *out = res;
+ p->cached_subs = res;
+ p->cached_subs.change_id = 0;
+ p->cached_subs_valid = true;
+}
+
+// Set *out_rc to [x0, y0, x1, y1] of the graphical bounding box in script
+// coordinates.
+// Set it to [inf, inf, -inf, -inf] if empty.
+void mp_ass_get_bb(ASS_Image *image_list, ASS_Track *track,
+ struct mp_osd_res *res, double *out_rc)
+{
+ double rc[4] = {INFINITY, INFINITY, -INFINITY, -INFINITY};
+
+ for (ASS_Image *img = image_list; img; img = img->next) {
+ if (img->w == 0 || img->h == 0)
+ continue;
+ rc[0] = MPMIN(rc[0], img->dst_x);
+ rc[1] = MPMIN(rc[1], img->dst_y);
+ rc[2] = MPMAX(rc[2], img->dst_x + img->w);
+ rc[3] = MPMAX(rc[3], img->dst_y + img->h);
+ }
+
+ double scale = track->PlayResY / (double)MPMAX(res->h, 1);
+ if (scale > 0) {
+ for (int i = 0; i < 4; i++)
+ out_rc[i] = rc[i] * scale;
+ }
+}
diff --git a/sub/ass_mp.h b/sub/ass_mp.h
new file mode 100644
index 0000000..dc83e31
--- /dev/null
+++ b/sub/ass_mp.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2006 Evgeniy Stepanov <eugeni.stepanov@gmail.com>
+ *
+ * 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_ASS_MP_H
+#define MPLAYER_ASS_MP_H
+
+#include <stdint.h>
+#include <stdbool.h>
+
+#include <ass/ass.h>
+#include <ass/ass_types.h>
+
+// These PlayResX and PlayResY values are arbitrary and taken from lavc.
+// lavc assumes these values when converting to ass generally. Moreover, these
+// values are also used by default in VSFilter, so it isn't that arbitrary.
+#define MP_ASS_FONT_PLAYRESX 384
+#define MP_ASS_FONT_PLAYRESY 288
+
+#define MP_ASS_RGBA(r, g, b, a) \
+ (((unsigned)(r) << 24) | ((g) << 16) | ((b) << 8) | (0xFF - (a)))
+
+// m_color argument
+#define MP_ASS_COLOR(c) MP_ASS_RGBA((c).r, (c).g, (c).b, (c).a)
+
+struct MPOpts;
+struct mpv_global;
+struct mp_osd_res;
+struct osd_style_opts;
+struct mp_log;
+
+void mp_ass_flush_old_events(ASS_Track *track, long long ts);
+void mp_ass_set_style(ASS_Style *style, double res_y,
+ const struct osd_style_opts *opts);
+
+void mp_ass_configure_fonts(ASS_Renderer *priv, struct osd_style_opts *opts,
+ struct mpv_global *global, struct mp_log *log);
+ASS_Library *mp_ass_init(struct mpv_global *global,
+ struct osd_style_opts *opts, struct mp_log *log);
+
+struct sub_bitmaps;
+struct mp_ass_packer;
+struct mp_ass_packer *mp_ass_packer_alloc(void *ta_parent);
+void mp_ass_packer_pack(struct mp_ass_packer *p, ASS_Image **image_lists,
+ int num_image_lists, bool changed,
+ int preferred_osd_format, struct sub_bitmaps *out);
+void mp_ass_get_bb(ASS_Image *image_list, ASS_Track *track,
+ struct mp_osd_res *res, double *out_rc);
+
+#endif /* MPLAYER_ASS_MP_H */
diff --git a/sub/dec_sub.c b/sub/dec_sub.c
new file mode 100644
index 0000000..18d826e
--- /dev/null
+++ b/sub/dec_sub.c
@@ -0,0 +1,498 @@
+/*
+ * 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 <stdbool.h>
+#include <string.h>
+#include <math.h>
+#include <assert.h>
+
+#include "demux/demux.h"
+#include "sd.h"
+#include "dec_sub.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/recorder.h"
+#include "misc/dispatch.h"
+#include "osdep/threads.h"
+
+extern const struct sd_functions sd_ass;
+extern const struct sd_functions sd_lavc;
+
+static const struct sd_functions *const sd_list[] = {
+ &sd_lavc,
+ &sd_ass,
+ NULL
+};
+
+struct dec_sub {
+ mp_mutex lock;
+
+ struct mp_log *log;
+ struct mpv_global *global;
+ struct mp_subtitle_opts *opts;
+ struct m_config_cache *opts_cache;
+
+ struct mp_recorder_sink *recorder_sink;
+
+ struct attachment_list *attachments;
+
+ struct sh_stream *sh;
+ int play_dir;
+ int order;
+ double last_pkt_pts;
+ bool preload_attempted;
+ double video_fps;
+ double sub_speed;
+
+ struct mp_codec_params *codec;
+ double start, end;
+
+ double last_vo_pts;
+ struct sd *sd;
+
+ struct demux_packet *new_segment;
+ struct demux_packet *cached_pkts[2];
+};
+
+static void update_subtitle_speed(struct dec_sub *sub)
+{
+ struct mp_subtitle_opts *opts = sub->opts;
+ sub->sub_speed = 1.0;
+
+ if (sub->video_fps > 0 && sub->codec->frame_based > 0) {
+ MP_VERBOSE(sub, "Frame based format, dummy FPS: %f, video FPS: %f\n",
+ sub->codec->frame_based, sub->video_fps);
+ sub->sub_speed *= sub->codec->frame_based / sub->video_fps;
+ }
+
+ if (opts->sub_fps && sub->video_fps)
+ sub->sub_speed *= opts->sub_fps / sub->video_fps;
+
+ sub->sub_speed *= opts->sub_speed;
+}
+
+// Return the subtitle PTS used for a given video PTS.
+static double pts_to_subtitle(struct dec_sub *sub, double pts)
+{
+ struct mp_subtitle_opts *opts = sub->opts;
+
+ if (pts != MP_NOPTS_VALUE)
+ pts = (pts * sub->play_dir - opts->sub_delay) / sub->sub_speed;
+
+ return pts;
+}
+
+static double pts_from_subtitle(struct dec_sub *sub, double pts)
+{
+ struct mp_subtitle_opts *opts = sub->opts;
+
+ if (pts != MP_NOPTS_VALUE)
+ pts = (pts * sub->sub_speed + opts->sub_delay) * sub->play_dir;
+
+ return pts;
+}
+
+static void wakeup_demux(void *ctx)
+{
+ struct mp_dispatch_queue *q = ctx;
+ mp_dispatch_interrupt(q);
+}
+
+void sub_destroy(struct dec_sub *sub)
+{
+ if (!sub)
+ return;
+ demux_set_stream_wakeup_cb(sub->sh, NULL, NULL);
+ if (sub->sd) {
+ sub_reset(sub);
+ sub->sd->driver->uninit(sub->sd);
+ }
+ talloc_free(sub->sd);
+ mp_mutex_destroy(&sub->lock);
+ talloc_free(sub);
+}
+
+static struct sd *init_decoder(struct dec_sub *sub)
+{
+ for (int n = 0; sd_list[n]; n++) {
+ const struct sd_functions *driver = sd_list[n];
+ struct sd *sd = talloc(NULL, struct sd);
+ *sd = (struct sd){
+ .global = sub->global,
+ .log = mp_log_new(sd, sub->log, driver->name),
+ .opts = sub->opts,
+ .driver = driver,
+ .attachments = sub->attachments,
+ .codec = sub->codec,
+ .preload_ok = true,
+ };
+
+ if (sd->driver->init(sd) >= 0)
+ return sd;
+
+ talloc_free(sd);
+ }
+
+ MP_ERR(sub, "Could not find subtitle decoder for format '%s'.\n",
+ sub->codec->codec);
+ return NULL;
+}
+
+// Thread-safety of the returned object: all functions are thread-safe,
+// except sub_get_bitmaps() and sub_get_text(). Decoder backends (sd_*)
+// do not need to acquire locks.
+// Ownership of attachments goes to the callee, and is released with
+// talloc_free() (even on failure).
+struct dec_sub *sub_create(struct mpv_global *global, struct track *track,
+ struct attachment_list *attachments, int order)
+{
+ assert(track->stream && track->stream->type == STREAM_SUB);
+
+ struct dec_sub *sub = talloc(NULL, struct dec_sub);
+ *sub = (struct dec_sub){
+ .log = mp_log_new(sub, global->log, "sub"),
+ .global = global,
+ .opts_cache = m_config_cache_alloc(sub, global, &mp_subtitle_sub_opts),
+ .sh = track->stream,
+ .codec = track->stream->codec,
+ .attachments = talloc_steal(sub, attachments),
+ .play_dir = 1,
+ .order = order,
+ .last_pkt_pts = MP_NOPTS_VALUE,
+ .last_vo_pts = MP_NOPTS_VALUE,
+ .start = MP_NOPTS_VALUE,
+ .end = MP_NOPTS_VALUE,
+ };
+ sub->opts = sub->opts_cache->opts;
+ mp_mutex_init_type(&sub->lock, MP_MUTEX_RECURSIVE);
+
+ sub->sd = init_decoder(sub);
+ if (sub->sd) {
+ update_subtitle_speed(sub);
+ return sub;
+ }
+
+ sub_destroy(sub);
+ return NULL;
+}
+
+// Called locked.
+static void update_segment(struct dec_sub *sub)
+{
+ if (sub->new_segment && sub->last_vo_pts != MP_NOPTS_VALUE &&
+ sub->last_vo_pts >= sub->new_segment->start)
+ {
+ MP_VERBOSE(sub, "Switch segment: %f at %f\n", sub->new_segment->start,
+ sub->last_vo_pts);
+
+ sub->codec = sub->new_segment->codec;
+ sub->start = sub->new_segment->start;
+ sub->end = sub->new_segment->end;
+ struct sd *new = init_decoder(sub);
+ if (new) {
+ sub->sd->driver->uninit(sub->sd);
+ talloc_free(sub->sd);
+ sub->sd = new;
+ update_subtitle_speed(sub);
+ sub_control(sub, SD_CTRL_SET_TOP, &sub->order);
+ } else {
+ // We'll just keep the current decoder, and feed it possibly
+ // invalid data (not our fault if it crashes or something).
+ MP_ERR(sub, "Can't change to new codec.\n");
+ }
+ sub->sd->driver->decode(sub->sd, sub->new_segment);
+ talloc_free(sub->new_segment);
+ sub->new_segment = NULL;
+ }
+}
+
+bool sub_can_preload(struct dec_sub *sub)
+{
+ bool r;
+ mp_mutex_lock(&sub->lock);
+ r = sub->sd->driver->accept_packets_in_advance && !sub->preload_attempted;
+ mp_mutex_unlock(&sub->lock);
+ return r;
+}
+
+void sub_preload(struct dec_sub *sub)
+{
+ mp_mutex_lock(&sub->lock);
+
+ struct mp_dispatch_queue *demux_waiter = mp_dispatch_create(NULL);
+ demux_set_stream_wakeup_cb(sub->sh, wakeup_demux, demux_waiter);
+
+ sub->preload_attempted = true;
+
+ for (;;) {
+ struct demux_packet *pkt = NULL;
+ int r = demux_read_packet_async(sub->sh, &pkt);
+ if (r == 0) {
+ mp_dispatch_queue_process(demux_waiter, INFINITY);
+ continue;
+ }
+ if (!pkt)
+ break;
+ sub->sd->driver->decode(sub->sd, pkt);
+ talloc_free(pkt);
+ }
+
+ demux_set_stream_wakeup_cb(sub->sh, NULL, NULL);
+ talloc_free(demux_waiter);
+
+ mp_mutex_unlock(&sub->lock);
+}
+
+static bool is_new_segment(struct dec_sub *sub, struct demux_packet *p)
+{
+ return p->segmented &&
+ (p->start != sub->start || p->end != sub->end || p->codec != sub->codec);
+}
+
+// Read packets from the demuxer stream passed to sub_create(). Return true if
+// enough packets were read, false if the player should wait until the demuxer
+// signals new packets available (and then should retry).
+bool sub_read_packets(struct dec_sub *sub, double video_pts, bool force)
+{
+ bool r = true;
+ mp_mutex_lock(&sub->lock);
+ video_pts = pts_to_subtitle(sub, video_pts);
+ while (1) {
+ bool read_more = true;
+ if (sub->sd->driver->accepts_packet)
+ read_more = sub->sd->driver->accepts_packet(sub->sd, video_pts);
+
+ if (!read_more)
+ break;
+
+ if (sub->new_segment && sub->new_segment->start < video_pts) {
+ sub->last_vo_pts = video_pts;
+ update_segment(sub);
+ }
+
+ if (sub->new_segment)
+ break;
+
+ // (Use this mechanism only if sub_delay matters to avoid corner cases.)
+ double min_pts = sub->opts->sub_delay < 0 || force ? video_pts : MP_NOPTS_VALUE;
+
+ struct demux_packet *pkt;
+ int st = demux_read_packet_async_until(sub->sh, min_pts, &pkt);
+ // Note: "wait" (st==0) happens with non-interleaved streams only, and
+ // then we should stop the playloop until a new enough packet has been
+ // seen (or the subtitle decoder's queue is full). This usually does not
+ // happen for interleaved subtitle streams, which never return "wait"
+ // when reading, unless min_pts is set.
+ if (st <= 0) {
+ r = st < 0 || (sub->last_pkt_pts != MP_NOPTS_VALUE &&
+ sub->last_pkt_pts > video_pts);
+ break;
+ }
+
+ if (sub->recorder_sink)
+ mp_recorder_feed_packet(sub->recorder_sink, pkt);
+
+
+ // Update cached packets
+ if (sub->cached_pkts[0]) {
+ if (sub->cached_pkts[1])
+ talloc_free(sub->cached_pkts[1]);
+ sub->cached_pkts[1] = sub->cached_pkts[0];
+ }
+ sub->cached_pkts[0] = pkt;
+
+ sub->last_pkt_pts = pkt->pts;
+
+ if (is_new_segment(sub, pkt)) {
+ sub->new_segment = demux_copy_packet(pkt);
+ // Note that this can be delayed to a much later point in time.
+ update_segment(sub);
+ break;
+ }
+
+ if (!(sub->preload_attempted && sub->sd->preload_ok))
+ sub->sd->driver->decode(sub->sd, pkt);
+ }
+ mp_mutex_unlock(&sub->lock);
+ return r;
+}
+
+// Redecode both cached packets if needed.
+// Used with UPDATE_SUB_HARD and UPDATE_SUB_FILT.
+void sub_redecode_cached_packets(struct dec_sub *sub)
+{
+ mp_mutex_lock(&sub->lock);
+ if (sub->cached_pkts[0])
+ sub->sd->driver->decode(sub->sd, sub->cached_pkts[0]);
+ if (sub->cached_pkts[1])
+ sub->sd->driver->decode(sub->sd, sub->cached_pkts[1]);
+ mp_mutex_unlock(&sub->lock);
+}
+
+// Unref sub_bitmaps.rc to free the result. May return NULL.
+struct sub_bitmaps *sub_get_bitmaps(struct dec_sub *sub, struct mp_osd_res dim,
+ int format, double pts)
+{
+ mp_mutex_lock(&sub->lock);
+
+ pts = pts_to_subtitle(sub, pts);
+
+ sub->last_vo_pts = pts;
+ update_segment(sub);
+
+ struct sub_bitmaps *res = NULL;
+
+ if (!(sub->end != MP_NOPTS_VALUE && pts >= sub->end) &&
+ sub->sd->driver->get_bitmaps)
+ res = sub->sd->driver->get_bitmaps(sub->sd, dim, format, pts);
+
+ mp_mutex_unlock(&sub->lock);
+ return res;
+}
+
+// The returned string is talloc'ed.
+char *sub_get_text(struct dec_sub *sub, double pts, enum sd_text_type type)
+{
+ mp_mutex_lock(&sub->lock);
+ char *text = NULL;
+
+ pts = pts_to_subtitle(sub, pts);
+
+ sub->last_vo_pts = pts;
+ update_segment(sub);
+
+ if (sub->sd->driver->get_text)
+ text = sub->sd->driver->get_text(sub->sd, pts, type);
+ mp_mutex_unlock(&sub->lock);
+ return text;
+}
+
+char *sub_ass_get_extradata(struct dec_sub *sub)
+{
+ if (strcmp(sub->sd->codec->codec, "ass") != 0)
+ return NULL;
+ char *extradata = sub->sd->codec->extradata;
+ int extradata_size = sub->sd->codec->extradata_size;
+ return talloc_strndup(NULL, extradata, extradata_size);
+}
+
+struct sd_times sub_get_times(struct dec_sub *sub, double pts)
+{
+ mp_mutex_lock(&sub->lock);
+ struct sd_times res = { .start = MP_NOPTS_VALUE, .end = MP_NOPTS_VALUE };
+
+ pts = pts_to_subtitle(sub, pts);
+
+ sub->last_vo_pts = pts;
+ update_segment(sub);
+
+ if (sub->sd->driver->get_times)
+ res = sub->sd->driver->get_times(sub->sd, pts);
+
+ mp_mutex_unlock(&sub->lock);
+ return res;
+}
+
+void sub_reset(struct dec_sub *sub)
+{
+ mp_mutex_lock(&sub->lock);
+ if (sub->sd->driver->reset)
+ sub->sd->driver->reset(sub->sd);
+ sub->last_pkt_pts = MP_NOPTS_VALUE;
+ sub->last_vo_pts = MP_NOPTS_VALUE;
+ TA_FREEP(&sub->cached_pkts[0]);
+ TA_FREEP(&sub->cached_pkts[1]);
+ TA_FREEP(&sub->new_segment);
+ mp_mutex_unlock(&sub->lock);
+}
+
+void sub_select(struct dec_sub *sub, bool selected)
+{
+ mp_mutex_lock(&sub->lock);
+ if (sub->sd->driver->select)
+ sub->sd->driver->select(sub->sd, selected);
+ mp_mutex_unlock(&sub->lock);
+}
+
+int sub_control(struct dec_sub *sub, enum sd_ctrl cmd, void *arg)
+{
+ int r = CONTROL_UNKNOWN;
+ mp_mutex_lock(&sub->lock);
+ bool propagate = false;
+ switch (cmd) {
+ case SD_CTRL_SET_VIDEO_DEF_FPS:
+ sub->video_fps = *(double *)arg;
+ update_subtitle_speed(sub);
+ break;
+ case SD_CTRL_SUB_STEP: {
+ double *a = arg;
+ double arg2[2] = {a[0], a[1]};
+ arg2[0] = pts_to_subtitle(sub, arg2[0]);
+ if (sub->sd->driver->control)
+ r = sub->sd->driver->control(sub->sd, cmd, arg2);
+ if (r == CONTROL_OK)
+ a[0] = pts_from_subtitle(sub, arg2[0]);
+ break;
+ }
+ case SD_CTRL_UPDATE_OPTS: {
+ int flags = (uintptr_t)arg;
+ if (m_config_cache_update(sub->opts_cache))
+ update_subtitle_speed(sub);
+ propagate = true;
+ if (flags & UPDATE_SUB_HARD) {
+ // forget about the previous preload because
+ // UPDATE_SUB_HARD will cause a sub reinit
+ // that clears all preloaded sub packets
+ sub->preload_attempted = false;
+ }
+ break;
+ }
+ default:
+ propagate = true;
+ }
+ if (propagate && sub->sd->driver->control)
+ r = sub->sd->driver->control(sub->sd, cmd, arg);
+ mp_mutex_unlock(&sub->lock);
+ return r;
+}
+
+void sub_set_recorder_sink(struct dec_sub *sub, struct mp_recorder_sink *sink)
+{
+ mp_mutex_lock(&sub->lock);
+ sub->recorder_sink = sink;
+ mp_mutex_unlock(&sub->lock);
+}
+
+void sub_set_play_dir(struct dec_sub *sub, int dir)
+{
+ mp_mutex_lock(&sub->lock);
+ sub->play_dir = dir;
+ mp_mutex_unlock(&sub->lock);
+}
+
+bool sub_is_primary_visible(struct dec_sub *sub)
+{
+ return !!sub->opts->sub_visibility;
+}
+
+bool sub_is_secondary_visible(struct dec_sub *sub)
+{
+ return !!sub->opts->sec_sub_visibility;
+}
diff --git a/sub/dec_sub.h b/sub/dec_sub.h
new file mode 100644
index 0000000..9de6760
--- /dev/null
+++ b/sub/dec_sub.h
@@ -0,0 +1,62 @@
+#ifndef MPLAYER_DEC_SUB_H
+#define MPLAYER_DEC_SUB_H
+
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "player/core.h"
+#include "osd.h"
+
+struct sh_stream;
+struct mpv_global;
+struct demux_packet;
+struct mp_recorder_sink;
+struct dec_sub;
+struct sd;
+
+enum sd_ctrl {
+ SD_CTRL_SUB_STEP,
+ SD_CTRL_SET_VIDEO_PARAMS,
+ SD_CTRL_SET_TOP,
+ SD_CTRL_SET_VIDEO_DEF_FPS,
+ SD_CTRL_UPDATE_OPTS,
+};
+
+enum sd_text_type {
+ SD_TEXT_TYPE_PLAIN,
+ SD_TEXT_TYPE_ASS,
+};
+
+struct sd_times {
+ double start;
+ double end;
+};
+
+struct attachment_list {
+ struct demux_attachment *entries;
+ int num_entries;
+};
+
+struct dec_sub *sub_create(struct mpv_global *global, struct track *track,
+ struct attachment_list *attachments, int order);
+void sub_destroy(struct dec_sub *sub);
+
+bool sub_can_preload(struct dec_sub *sub);
+void sub_preload(struct dec_sub *sub);
+void sub_redecode_cached_packets(struct dec_sub *sub);
+bool sub_read_packets(struct dec_sub *sub, double video_pts, bool force);
+struct sub_bitmaps *sub_get_bitmaps(struct dec_sub *sub, struct mp_osd_res dim,
+ int format, double pts);
+char *sub_get_text(struct dec_sub *sub, double pts, enum sd_text_type type);
+char *sub_ass_get_extradata(struct dec_sub *sub);
+struct sd_times sub_get_times(struct dec_sub *sub, double pts);
+void sub_reset(struct dec_sub *sub);
+void sub_select(struct dec_sub *sub, bool selected);
+void sub_set_recorder_sink(struct dec_sub *sub, struct mp_recorder_sink *sink);
+void sub_set_play_dir(struct dec_sub *sub, int dir);
+bool sub_is_primary_visible(struct dec_sub *sub);
+bool sub_is_secondary_visible(struct dec_sub *sub);
+
+int sub_control(struct dec_sub *sub, enum sd_ctrl cmd, void *arg);
+
+#endif
diff --git a/sub/draw_bmp.c b/sub/draw_bmp.c
new file mode 100644
index 0000000..58db162
--- /dev/null
+++ b/sub/draw_bmp.c
@@ -0,0 +1,1035 @@
+/*
+ * 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 <stdbool.h>
+#include <assert.h>
+#include <math.h>
+#include <inttypes.h>
+
+#include "common/common.h"
+#include "draw_bmp.h"
+#include "img_convert.h"
+#include "video/mp_image.h"
+#include "video/repack.h"
+#include "video/sws_utils.h"
+#include "video/img_format.h"
+#include "video/csputils.h"
+
+const bool mp_draw_sub_formats[SUBBITMAP_COUNT] = {
+ [SUBBITMAP_LIBASS] = true,
+ [SUBBITMAP_BGRA] = true,
+};
+
+struct part {
+ int change_id;
+ // Sub-bitmaps scaled to final sizes.
+ int num_imgs;
+ struct mp_image **imgs;
+};
+
+// Must be a power of 2. Height is 1, but mark_rect() effectively operates on
+// multiples of chroma sized macro-pixels. (E.g. 4:2:0 -> every second line is
+// the same as the previous one, and x0%2==x1%2==0.)
+#define SLICE_W 256u
+
+// Whether to scale in tiles. Faster, but can't use correct chroma position.
+// Should be a runtime option. SLICE_W is used as tile width. The tile size
+// should probably be small; too small or too big will cause overhead when
+// scaling.
+#define SCALE_IN_TILES 1
+#define TILE_H 4u
+
+struct slice {
+ uint16_t x0, x1;
+};
+
+struct mp_draw_sub_cache
+{
+ struct mpv_global *global;
+
+ // Possibly cached parts. Also implies what's in the video_overlay.
+ struct part parts[MAX_OSD_PARTS];
+ int64_t change_id;
+
+ struct mp_image_params params; // target image params
+
+ int w, h; // like params.w/h, but rounded up to chroma
+ unsigned align_x, align_y; // alignment for all video pixels
+
+ struct mp_image *rgba_overlay; // all OSD in RGBA
+ struct mp_image *video_overlay; // rgba_overlay converted to video colorspace
+ struct mp_image *alpha_overlay; // alpha plane ref. to video_overlay
+ struct mp_image *calpha_overlay; // alpha_overlay scaled to chroma plane size
+
+ unsigned s_w; // number of slices per line
+ struct slice *slices; // slices[y * s_w + x / SLICE_W]
+ bool any_osd;
+
+ struct mp_sws_context *rgba_to_overlay; // scaler for rgba -> video csp.
+ struct mp_sws_context *alpha_to_calpha; // scaler for overlay -> calpha
+ bool scale_in_tiles;
+
+ struct mp_sws_context *sub_scale; // scaler for SUBBITMAP_BGRA
+
+ struct mp_repack *overlay_to_f32; // convert video_overlay to float
+ struct mp_image *overlay_tmp; // slice in float32
+
+ struct mp_repack *calpha_to_f32; // convert video_overlay to float
+ struct mp_image *calpha_tmp; // slice in float32
+
+ struct mp_repack *video_to_f32; // convert video to float
+ struct mp_repack *video_from_f32; // convert float back to video
+ struct mp_image *video_tmp; // slice in float32
+
+ struct mp_sws_context *premul; // video -> premultiplied video
+ struct mp_sws_context *unpremul; // reverse
+ struct mp_image *premul_tmp;
+
+ // Function that works on the _f32 data.
+ void (*blend_line)(void *dst, void *src, void *src_a, int w);
+
+ struct mp_image res_overlay; // returned by mp_draw_sub_overlay()
+};
+
+static void blend_line_f32(void *dst, void *src, void *src_a, int w)
+{
+ float *dst_f = dst;
+ float *src_f = src;
+ float *src_a_f = src_a;
+
+ for (int x = 0; x < w; x++)
+ dst_f[x] = src_f[x] + dst_f[x] * (1.0f - src_a_f[x]);
+}
+
+static void blend_line_u8(void *dst, void *src, void *src_a, int w)
+{
+ uint8_t *dst_i = dst;
+ uint8_t *src_i = src;
+ uint8_t *src_a_i = src_a;
+
+ for (int x = 0; x < w; x++)
+ dst_i[x] = src_i[x] + dst_i[x] * (255u - src_a_i[x]) / 255u;
+}
+
+static void blend_slice(struct mp_draw_sub_cache *p)
+{
+ struct mp_image *ov = p->overlay_tmp;
+ struct mp_image *ca = p->calpha_tmp;
+ struct mp_image *vid = p->video_tmp;
+
+ for (int plane = 0; plane < vid->num_planes; plane++) {
+ int xs = vid->fmt.xs[plane];
+ int ys = vid->fmt.ys[plane];
+ int h = (1 << vid->fmt.chroma_ys) - (1 << ys) + 1;
+ int cw = mp_chroma_div_up(vid->w, xs);
+ for (int y = 0; y < h; y++) {
+ p->blend_line(mp_image_pixel_ptr_ny(vid, plane, 0, y),
+ mp_image_pixel_ptr_ny(ov, plane, 0, y),
+ xs || ys ? mp_image_pixel_ptr_ny(ca, 0, 0, y)
+ : mp_image_pixel_ptr_ny(ov, ov->num_planes - 1, 0, y),
+ cw);
+ }
+ }
+}
+
+static bool blend_overlay_with_video(struct mp_draw_sub_cache *p,
+ struct mp_image *dst)
+{
+ if (!repack_config_buffers(p->video_to_f32, 0, p->video_tmp, 0, dst, NULL))
+ return false;
+ if (!repack_config_buffers(p->video_from_f32, 0, dst, 0, p->video_tmp, NULL))
+ return false;
+
+ int xs = dst->fmt.chroma_xs;
+ int ys = dst->fmt.chroma_ys;
+
+ for (int y = 0; y < dst->h; y += p->align_y) {
+ struct slice *line = &p->slices[y * p->s_w];
+
+ for (int sx = 0; sx < p->s_w; sx++) {
+ struct slice *s = &line[sx];
+
+ int w = s->x1 - s->x0;
+ if (w <= 0)
+ continue;
+ int x = sx * SLICE_W + s->x0;
+
+ assert(MP_IS_ALIGNED(x, p->align_x));
+ assert(MP_IS_ALIGNED(w, p->align_x));
+ assert(x + w <= p->w);
+
+ repack_line(p->overlay_to_f32, 0, 0, x, y, w);
+ repack_line(p->video_to_f32, 0, 0, x, y, w);
+ if (p->calpha_to_f32)
+ repack_line(p->calpha_to_f32, 0, 0, x >> xs, y >> ys, w >> xs);
+
+ blend_slice(p);
+
+ repack_line(p->video_from_f32, x, y, 0, 0, w);
+ }
+ }
+
+ return true;
+}
+
+static bool convert_overlay_part(struct mp_draw_sub_cache *p,
+ int x0, int y0, int w, int h)
+{
+ struct mp_image src = *p->rgba_overlay;
+ struct mp_image dst = *p->video_overlay;
+
+ mp_image_crop(&src, x0, y0, x0 + w, y0 + h);
+ mp_image_crop(&dst, x0, y0, x0 + w, y0 + h);
+
+ if (mp_sws_scale(p->rgba_to_overlay, &dst, &src) < 0)
+ return false;
+
+ if (p->calpha_overlay) {
+ src = *p->alpha_overlay;
+ dst = *p->calpha_overlay;
+
+ int xs = p->video_overlay->fmt.chroma_xs;
+ int ys = p->video_overlay->fmt.chroma_ys;
+ mp_image_crop(&src, x0, y0, x0 + w, y0 + h);
+ mp_image_crop(&dst, x0 >> xs, y0 >> ys, (x0 + w) >> xs, (y0 + h) >> ys);
+
+ if (mp_sws_scale(p->alpha_to_calpha, &dst, &src) < 0)
+ return false;
+ }
+
+ return true;
+}
+
+static bool convert_to_video_overlay(struct mp_draw_sub_cache *p)
+{
+ if (!p->video_overlay)
+ return true;
+
+ if (p->scale_in_tiles) {
+ int t_h = p->rgba_overlay->h / TILE_H;
+ for (int ty = 0; ty < t_h; ty++) {
+ for (int sx = 0; sx < p->s_w; sx++) {
+ struct slice *s = &p->slices[ty * TILE_H * p->s_w + sx];
+ bool pixels_set = false;
+ for (int y = 0; y < TILE_H; y++) {
+ if (s[0].x0 < s[0].x1) {
+ pixels_set = true;
+ break;
+ }
+ s += p->s_w;
+ }
+ if (!pixels_set)
+ continue;
+ if (!convert_overlay_part(p, sx * SLICE_W, ty * TILE_H,
+ SLICE_W, TILE_H))
+ return false;
+ }
+ }
+ } else {
+ if (!convert_overlay_part(p, 0, 0, p->rgba_overlay->w, p->rgba_overlay->h))
+ return false;
+ }
+
+ return true;
+}
+
+// Mark the given rectangle of pixels as possibly non-transparent.
+// The rectangle must have been pre-clipped.
+static void mark_rect(struct mp_draw_sub_cache *p, int x0, int y0, int x1, int y1)
+{
+ x0 = MP_ALIGN_DOWN(x0, p->align_x);
+ y0 = MP_ALIGN_DOWN(y0, p->align_y);
+ x1 = MP_ALIGN_UP(x1, p->align_x);
+ y1 = MP_ALIGN_UP(y1, p->align_y);
+
+ assert(x0 >= 0 && x0 <= x1 && x1 <= p->w);
+ assert(y0 >= 0 && y0 <= y1 && y1 <= p->h);
+
+ const int sx0 = x0 / SLICE_W;
+ const int sx1 = MPMIN(x1 / SLICE_W, p->s_w - 1);
+
+ for (int y = y0; y < y1; y++) {
+ struct slice *line = &p->slices[y * p->s_w];
+
+ struct slice *s0 = &line[sx0];
+ struct slice *s1 = &line[sx1];
+
+ s0->x0 = MPMIN(s0->x0, x0 % SLICE_W);
+ s1->x1 = MPMAX(s1->x1, ((x1 - 1) % SLICE_W) + 1);
+
+ if (s0 != s1) {
+ s0->x1 = SLICE_W;
+ s1->x0 = 0;
+
+ for (int x = sx0 + 1; x < sx1; x++) {
+ struct slice *s = &line[x];
+ s->x0 = 0;
+ s->x1 = SLICE_W;
+ }
+ }
+
+ // Ensure the very last slice does not extend
+ // beyond the total width.
+ struct slice *last_s = &line[p->s_w - 1];
+ last_s->x1 = MPMIN(p->w - ((p->s_w - 1) * SLICE_W), last_s->x1);
+
+ p->any_osd = true;
+ }
+}
+
+static void draw_ass_rgba(uint8_t *dst, ptrdiff_t dst_stride,
+ uint8_t *src, ptrdiff_t src_stride,
+ int w, int h, uint32_t color)
+{
+ const unsigned int r = (color >> 24) & 0xff;
+ const unsigned int g = (color >> 16) & 0xff;
+ const unsigned int b = (color >> 8) & 0xff;
+ const unsigned int a = 0xff - (color & 0xff);
+
+ for (int y = 0; y < h; y++) {
+ uint32_t *dstrow = (uint32_t *) dst;
+ for (int x = 0; x < w; x++) {
+ const unsigned int v = src[x];
+ unsigned int aa = a * v;
+ uint32_t dstpix = dstrow[x];
+ unsigned int dstb = dstpix & 0xFF;
+ unsigned int dstg = (dstpix >> 8) & 0xFF;
+ unsigned int dstr = (dstpix >> 16) & 0xFF;
+ unsigned int dsta = (dstpix >> 24) & 0xFF;
+ dstb = (v * b * a + dstb * (255 * 255 - aa)) / (255 * 255);
+ dstg = (v * g * a + dstg * (255 * 255 - aa)) / (255 * 255);
+ dstr = (v * r * a + dstr * (255 * 255 - aa)) / (255 * 255);
+ dsta = (aa * 255 + dsta * (255 * 255 - aa)) / (255 * 255);
+ dstrow[x] = dstb | (dstg << 8) | (dstr << 16) | (dsta << 24);
+ }
+ dst += dst_stride;
+ src += src_stride;
+ }
+}
+
+static void render_ass(struct mp_draw_sub_cache *p, struct sub_bitmaps *sb)
+{
+ assert(sb->format == SUBBITMAP_LIBASS);
+
+ for (int i = 0; i < sb->num_parts; i++) {
+ struct sub_bitmap *s = &sb->parts[i];
+
+ draw_ass_rgba(mp_image_pixel_ptr(p->rgba_overlay, 0, s->x, s->y),
+ p->rgba_overlay->stride[0], s->bitmap, s->stride,
+ s->w, s->h, s->libass.color);
+
+ mark_rect(p, s->x, s->y, s->x + s->w, s->y + s->h);
+ }
+}
+
+static void draw_rgba(uint8_t *dst, ptrdiff_t dst_stride,
+ uint8_t *src, ptrdiff_t src_stride, int w, int h)
+{
+ for (int y = 0; y < h; y++) {
+ uint32_t *srcrow = (uint32_t *)src;
+ uint32_t *dstrow = (uint32_t *)dst;
+ for (int x = 0; x < w; x++) {
+ uint32_t srcpix = srcrow[x];
+ uint32_t dstpix = dstrow[x];
+ unsigned int srcb = srcpix & 0xFF;
+ unsigned int srcg = (srcpix >> 8) & 0xFF;
+ unsigned int srcr = (srcpix >> 16) & 0xFF;
+ unsigned int srca = (srcpix >> 24) & 0xFF;
+ unsigned int dstb = dstpix & 0xFF;
+ unsigned int dstg = (dstpix >> 8) & 0xFF;
+ unsigned int dstr = (dstpix >> 16) & 0xFF;
+ unsigned int dsta = (dstpix >> 24) & 0xFF;
+ dstb = srcb + dstb * (255 * 255 - srca) / (255 * 255);
+ dstg = srcg + dstg * (255 * 255 - srca) / (255 * 255);
+ dstr = srcr + dstr * (255 * 255 - srca) / (255 * 255);
+ dsta = srca + dsta * (255 * 255 - srca) / (255 * 255);
+ dstrow[x] = dstb | (dstg << 8) | (dstr << 16) | (dsta << 24);
+ }
+ dst += dst_stride;
+ src += src_stride;
+ }
+}
+
+static bool render_rgba(struct mp_draw_sub_cache *p, struct part *part,
+ struct sub_bitmaps *sb)
+{
+ assert(sb->format == SUBBITMAP_BGRA);
+
+ if (part->change_id != sb->change_id) {
+ for (int n = 0; n < part->num_imgs; n++)
+ talloc_free(part->imgs[n]);
+ part->num_imgs = sb->num_parts;
+ MP_TARRAY_GROW(p, part->imgs, part->num_imgs);
+ for (int n = 0; n < part->num_imgs; n++)
+ part->imgs[n] = NULL;
+
+ part->change_id = sb->change_id;
+ }
+
+ for (int i = 0; i < sb->num_parts; i++) {
+ struct sub_bitmap *s = &sb->parts[i];
+
+ // Clipping is rare but necessary.
+ int sx0 = s->x;
+ int sy0 = s->y;
+ int sx1 = s->x + s->dw;
+ int sy1 = s->y + s->dh;
+
+ int x0 = MPCLAMP(sx0, 0, p->w);
+ int y0 = MPCLAMP(sy0, 0, p->h);
+ int x1 = MPCLAMP(sx1, 0, p->w);
+ int y1 = MPCLAMP(sy1, 0, p->h);
+
+ int dw = x1 - x0;
+ int dh = y1 - y0;
+ if (dw <= 0 || dh <= 0)
+ continue;
+
+ // We clip the source instead of the scaled image, because that might
+ // avoid excessive memory usage when applying a ridiculous scale factor,
+ // even if that stretches it to up to 1 pixel due to integer rounding.
+ int sx = 0;
+ int sy = 0;
+ int sw = s->w;
+ int sh = s->h;
+ if (x0 != sx0 || y0 != sy0 || x1 != sx1 || y1 != sy1) {
+ double fx = s->dw / (double)s->w;
+ double fy = s->dh / (double)s->h;
+ sx = MPCLAMP((x0 - sx0) / fx, 0, s->w);
+ sy = MPCLAMP((y0 - sy0) / fy, 0, s->h);
+ sw = MPCLAMP(dw / fx, 1, s->w);
+ sh = MPCLAMP(dh / fy, 1, s->h);
+ }
+
+ assert(sx >= 0 && sw > 0 && sx + sw <= s->w);
+ assert(sy >= 0 && sh > 0 && sy + sh <= s->h);
+
+ ptrdiff_t s_stride = s->stride;
+ void *s_ptr = (char *)s->bitmap + s_stride * sy + sx * 4;
+
+ if (dw != sw || dh != sh) {
+ struct mp_image *scaled = part->imgs[i];
+
+ if (!scaled) {
+ struct mp_image src_img = {0};
+ mp_image_setfmt(&src_img, IMGFMT_BGRA);
+ mp_image_set_size(&src_img, sw, sh);
+ src_img.planes[0] = s_ptr;
+ src_img.stride[0] = s_stride;
+ src_img.params.alpha = MP_ALPHA_PREMUL;
+
+ scaled = mp_image_alloc(IMGFMT_BGRA, dw, dh);
+ if (!scaled)
+ return false;
+ part->imgs[i] = talloc_steal(p, scaled);
+ mp_image_copy_attributes(scaled, &src_img);
+
+ if (mp_sws_scale(p->sub_scale, scaled, &src_img) < 0)
+ return false;
+ }
+
+ assert(scaled->w == dw);
+ assert(scaled->h == dh);
+
+ s_stride = scaled->stride[0];
+ s_ptr = scaled->planes[0];
+ }
+
+ draw_rgba(mp_image_pixel_ptr(p->rgba_overlay, 0, x0, y0),
+ p->rgba_overlay->stride[0], s_ptr, s_stride, dw, dh);
+
+ mark_rect(p, x0, y0, x1, y1);
+ }
+
+ return true;
+}
+
+static bool render_sb(struct mp_draw_sub_cache *p, struct sub_bitmaps *sb)
+{
+ struct part *part = &p->parts[sb->render_index];
+
+ switch (sb->format) {
+ case SUBBITMAP_LIBASS:
+ render_ass(p, sb);
+ return true;
+ case SUBBITMAP_BGRA:
+ return render_rgba(p, part, sb);
+ }
+
+ return false;
+}
+
+static void clear_rgba_overlay(struct mp_draw_sub_cache *p)
+{
+ assert(p->rgba_overlay->imgfmt == IMGFMT_BGRA);
+
+ for (int y = 0; y < p->rgba_overlay->h; y++) {
+ uint32_t *px = mp_image_pixel_ptr(p->rgba_overlay, 0, 0, y);
+ struct slice *line = &p->slices[y * p->s_w];
+
+ for (int sx = 0; sx < p->s_w; sx++) {
+ struct slice *s = &line[sx];
+
+ // Ensure this final slice doesn't extend beyond the width of p->s_w
+ if (s->x1 == SLICE_W && sx == p->s_w - 1 && y == p->rgba_overlay->h - 1)
+ s->x1 = MPMIN(p->w - ((p->s_w - 1) * SLICE_W), s->x1);
+
+ if (s->x0 <= s->x1) {
+ memset(px + s->x0, 0, (s->x1 - s->x0) * 4);
+ *s = (struct slice){SLICE_W, 0};
+ }
+
+ px += SLICE_W;
+ }
+ }
+
+ p->any_osd = false;
+}
+
+static struct mp_sws_context *alloc_scaler(struct mp_draw_sub_cache *p)
+{
+ struct mp_sws_context *s = mp_sws_alloc(p);
+ mp_sws_enable_cmdline_opts(s, p->global);
+ return s;
+}
+
+static void init_general(struct mp_draw_sub_cache *p)
+{
+ p->sub_scale = alloc_scaler(p);
+
+ p->s_w = MP_ALIGN_UP(p->rgba_overlay->w, SLICE_W) / SLICE_W;
+
+ p->slices = talloc_zero_array(p, struct slice, p->s_w * p->rgba_overlay->h);
+
+ mp_image_clear(p->rgba_overlay, 0, 0, p->w, p->h);
+ clear_rgba_overlay(p);
+}
+
+static bool reinit_to_video(struct mp_draw_sub_cache *p)
+{
+ struct mp_image_params *params = &p->params;
+ mp_image_params_guess_csp(params);
+
+ bool need_premul = params->alpha != MP_ALPHA_PREMUL &&
+ (mp_imgfmt_get_desc(params->imgfmt).flags & MP_IMGFLAG_ALPHA);
+
+ // Intermediate format for video_overlay. Requirements:
+ // - same subsampling as video
+ // - uses video colorspace
+ // - has alpha
+ // - repacker support (to the format used in p->blend_line)
+ // - probably 8 bit per component rather than something wasteful or strange
+ struct mp_regular_imgfmt vfdesc = {0};
+
+ int rflags = REPACK_CREATE_EXPAND_8BIT;
+ bool use_shortcut = false;
+
+ p->video_to_f32 = mp_repack_create_planar(params->imgfmt, false, rflags);
+ talloc_steal(p, p->video_to_f32);
+ if (!p->video_to_f32)
+ return false;
+ mp_get_regular_imgfmt(&vfdesc, mp_repack_get_format_dst(p->video_to_f32));
+ assert(vfdesc.num_planes); // must have succeeded
+
+ if (params->color.space == MP_CSP_RGB && vfdesc.num_planes >= 3) {
+ use_shortcut = true;
+
+ if (vfdesc.component_type == MP_COMPONENT_TYPE_UINT &&
+ vfdesc.component_size == 1 && vfdesc.component_pad == 0)
+ p->blend_line = blend_line_u8;
+ }
+
+ // If no special blender is available, blend in float.
+ if (!p->blend_line) {
+ TA_FREEP(&p->video_to_f32);
+
+ rflags |= REPACK_CREATE_PLANAR_F32;
+
+ p->video_to_f32 = mp_repack_create_planar(params->imgfmt, false, rflags);
+ talloc_steal(p, p->video_to_f32);
+ if (!p->video_to_f32)
+ return false;
+
+ mp_get_regular_imgfmt(&vfdesc, mp_repack_get_format_dst(p->video_to_f32));
+ assert(vfdesc.component_type == MP_COMPONENT_TYPE_FLOAT);
+
+ p->blend_line = blend_line_f32;
+ }
+
+ p->scale_in_tiles = SCALE_IN_TILES;
+
+ int vid_f32_fmt = mp_repack_get_format_dst(p->video_to_f32);
+
+ p->video_from_f32 = mp_repack_create_planar(params->imgfmt, true, rflags);
+ talloc_steal(p, p->video_from_f32);
+ if (!p->video_from_f32)
+ return false;
+
+ assert(mp_repack_get_format_dst(p->video_to_f32) ==
+ mp_repack_get_format_src(p->video_from_f32));
+
+ int overlay_fmt = 0;
+ if (use_shortcut) {
+ // No point in doing anything fancy.
+ overlay_fmt = IMGFMT_BGRA;
+ p->scale_in_tiles = false;
+ } else {
+ struct mp_regular_imgfmt odesc = vfdesc;
+ // Just use 8 bit as well (should be fine, may use less memory).
+ odesc.component_type = MP_COMPONENT_TYPE_UINT;
+ odesc.component_size = 1;
+ odesc.component_pad = 0;
+
+ // Ensure there's alpha.
+ if (odesc.planes[odesc.num_planes - 1].components[0] != 4) {
+ if (odesc.num_planes >= 4)
+ return false; // wat
+ odesc.planes[odesc.num_planes++] =
+ (struct mp_regular_imgfmt_plane){1, {4}};
+ }
+
+ overlay_fmt = mp_find_regular_imgfmt(&odesc);
+ p->scale_in_tiles = odesc.chroma_xs || odesc.chroma_ys;
+ }
+ if (!overlay_fmt)
+ return false;
+
+ p->overlay_to_f32 = mp_repack_create_planar(overlay_fmt, false, rflags);
+ talloc_steal(p, p->overlay_to_f32);
+ if (!p->overlay_to_f32)
+ return false;
+
+ int render_fmt = mp_repack_get_format_dst(p->overlay_to_f32);
+
+ struct mp_regular_imgfmt ofdesc = {0};
+ mp_get_regular_imgfmt(&ofdesc, render_fmt);
+
+ if (ofdesc.planes[ofdesc.num_planes - 1].components[0] != 4)
+ return false;
+
+ // The formats must be the same, minus possible lack of alpha in vfdesc.
+ if (ofdesc.num_planes != vfdesc.num_planes &&
+ ofdesc.num_planes - 1 != vfdesc.num_planes)
+ return false;
+ for (int n = 0; n < vfdesc.num_planes; n++) {
+ if (vfdesc.planes[n].components[0] != ofdesc.planes[n].components[0])
+ return false;
+ }
+
+ p->align_x = mp_repack_get_align_x(p->video_to_f32);
+ p->align_y = mp_repack_get_align_y(p->video_to_f32);
+
+ assert(p->align_x >= mp_repack_get_align_x(p->overlay_to_f32));
+ assert(p->align_y >= mp_repack_get_align_y(p->overlay_to_f32));
+
+ if (p->align_x > SLICE_W || p->align_y > TILE_H)
+ return false;
+
+ p->w = MP_ALIGN_UP(params->w, p->align_x);
+ int slice_h = p->align_y;
+ p->h = MP_ALIGN_UP(params->h, slice_h);
+
+ // Size of the overlay. If scaling in tiles, round up to tiles, so we don't
+ // need to reinit the scale for right/bottom tiles.
+ int w = p->w;
+ int h = p->h;
+ if (p->scale_in_tiles) {
+ w = MP_ALIGN_UP(w, SLICE_W);
+ h = MP_ALIGN_UP(h, TILE_H);
+ }
+
+ p->rgba_overlay = talloc_steal(p, mp_image_alloc(IMGFMT_BGRA, w, h));
+ p->overlay_tmp = talloc_steal(p, mp_image_alloc(render_fmt, SLICE_W, slice_h));
+ p->video_tmp = talloc_steal(p, mp_image_alloc(vid_f32_fmt, SLICE_W, slice_h));
+ if (!p->rgba_overlay || !p->overlay_tmp || !p->video_tmp)
+ return false;
+
+ mp_image_params_guess_csp(&p->rgba_overlay->params);
+ p->rgba_overlay->params.alpha = MP_ALPHA_PREMUL;
+
+ p->overlay_tmp->params.color = params->color;
+ p->video_tmp->params.color = params->color;
+
+ if (p->rgba_overlay->imgfmt == overlay_fmt) {
+ if (!repack_config_buffers(p->overlay_to_f32, 0, p->overlay_tmp,
+ 0, p->rgba_overlay, NULL))
+ return false;
+ } else {
+ // Generally non-RGB.
+ p->video_overlay = talloc_steal(p, mp_image_alloc(overlay_fmt, w, h));
+ if (!p->video_overlay)
+ return false;
+
+ p->video_overlay->params.color = params->color;
+ p->video_overlay->params.chroma_location = params->chroma_location;
+ p->video_overlay->params.alpha = MP_ALPHA_PREMUL;
+
+ if (p->scale_in_tiles)
+ p->video_overlay->params.chroma_location = MP_CHROMA_CENTER;
+
+ p->rgba_to_overlay = alloc_scaler(p);
+ p->rgba_to_overlay->allow_zimg = true;
+ if (!mp_sws_supports_formats(p->rgba_to_overlay,
+ p->video_overlay->imgfmt, p->rgba_overlay->imgfmt))
+ return false;
+
+ if (!repack_config_buffers(p->overlay_to_f32, 0, p->overlay_tmp,
+ 0, p->video_overlay, NULL))
+ return false;
+
+ // Setup a scaled alpha plane if chroma-subsampling is present.
+ int xs = p->video_overlay->fmt.chroma_xs;
+ int ys = p->video_overlay->fmt.chroma_ys;
+ if (xs || ys) {
+ // Require float so format selection becomes simpler (maybe).
+ assert(rflags & REPACK_CREATE_PLANAR_F32);
+
+ // For extracting the alpha plane, construct a gray format that is
+ // compatible with the alpha one.
+ struct mp_regular_imgfmt odesc = {0};
+ mp_get_regular_imgfmt(&odesc, overlay_fmt);
+ assert(odesc.component_size);
+ int aplane = odesc.num_planes - 1;
+ assert(odesc.planes[aplane].num_components == 1);
+ assert(odesc.planes[aplane].components[0] == 4);
+ struct mp_regular_imgfmt cadesc = odesc;
+ cadesc.num_planes = 1;
+ cadesc.planes[0] = (struct mp_regular_imgfmt_plane){1, {1}};
+ cadesc.chroma_xs = cadesc.chroma_ys = 0;
+
+ int calpha_fmt = mp_find_regular_imgfmt(&cadesc);
+ if (!calpha_fmt)
+ return false;
+
+ // Unscaled alpha plane from p->video_overlay.
+ p->alpha_overlay = talloc_zero(p, struct mp_image);
+ mp_image_setfmt(p->alpha_overlay, calpha_fmt);
+ mp_image_set_size(p->alpha_overlay, w, h);
+ p->alpha_overlay->planes[0] = p->video_overlay->planes[aplane];
+ p->alpha_overlay->stride[0] = p->video_overlay->stride[aplane];
+
+ // Full range gray always has the same range as alpha.
+ p->alpha_overlay->params.color.levels = MP_CSP_LEVELS_PC;
+ mp_image_params_guess_csp(&p->alpha_overlay->params);
+
+ p->calpha_overlay =
+ talloc_steal(p, mp_image_alloc(calpha_fmt, w >> xs, h >> ys));
+ if (!p->calpha_overlay)
+ return false;
+ p->calpha_overlay->params.color = p->alpha_overlay->params.color;
+
+ p->calpha_to_f32 = mp_repack_create_planar(calpha_fmt, false, rflags);
+ talloc_steal(p, p->calpha_to_f32);
+ if (!p->calpha_to_f32)
+ return false;
+
+ int af32_fmt = mp_repack_get_format_dst(p->calpha_to_f32);
+ p->calpha_tmp = talloc_steal(p, mp_image_alloc(af32_fmt, SLICE_W, 1));
+ if (!p->calpha_tmp)
+ return false;
+
+ if (!repack_config_buffers(p->calpha_to_f32, 0, p->calpha_tmp,
+ 0, p->calpha_overlay, NULL))
+ return false;
+
+ p->alpha_to_calpha = alloc_scaler(p);
+ if (!mp_sws_supports_formats(p->alpha_to_calpha,
+ calpha_fmt, calpha_fmt))
+ return false;
+ }
+ }
+
+ if (need_premul) {
+ p->premul = alloc_scaler(p);
+ p->unpremul = alloc_scaler(p);
+ p->premul_tmp = mp_image_alloc(params->imgfmt, params->w, params->h);
+ talloc_steal(p, p->premul_tmp);
+ if (!p->premul_tmp)
+ return false;
+ mp_image_set_params(p->premul_tmp, params);
+ p->premul_tmp->params.alpha = MP_ALPHA_PREMUL;
+
+ // Only zimg supports this.
+ p->premul->force_scaler = MP_SWS_ZIMG;
+ p->unpremul->force_scaler = MP_SWS_ZIMG;
+ }
+
+ init_general(p);
+
+ return true;
+}
+
+static bool reinit_to_overlay(struct mp_draw_sub_cache *p)
+{
+ p->align_x = 1;
+ p->align_y = 1;
+
+ p->w = p->params.w;
+ p->h = p->params.h;
+
+ p->rgba_overlay = talloc_steal(p, mp_image_alloc(IMGFMT_BGRA, p->w, p->h));
+ if (!p->rgba_overlay)
+ return false;
+
+ mp_image_params_guess_csp(&p->rgba_overlay->params);
+ p->rgba_overlay->params.alpha = MP_ALPHA_PREMUL;
+
+ // Some non-sense with the intention to somewhat isolate the returned image.
+ mp_image_setfmt(&p->res_overlay, p->rgba_overlay->imgfmt);
+ mp_image_set_size(&p->res_overlay, p->rgba_overlay->w, p->rgba_overlay->h);
+ mp_image_copy_attributes(&p->res_overlay, p->rgba_overlay);
+ p->res_overlay.planes[0] = p->rgba_overlay->planes[0];
+ p->res_overlay.stride[0] = p->rgba_overlay->stride[0];
+
+ init_general(p);
+
+ // Mark all dirty (for full reinit of user state).
+ for (int y = 0; y < p->rgba_overlay->h; y++) {
+ for (int sx = 0; sx < p->s_w; sx++)
+ p->slices[y * p->s_w + sx] = (struct slice){0, SLICE_W};
+ }
+
+ return true;
+}
+
+static bool check_reinit(struct mp_draw_sub_cache *p,
+ struct mp_image_params *params, bool to_video)
+{
+ if (!mp_image_params_equal(&p->params, params) || !p->rgba_overlay) {
+ talloc_free_children(p);
+ *p = (struct mp_draw_sub_cache){.global = p->global, .params = *params};
+ if (!(to_video ? reinit_to_video(p) : reinit_to_overlay(p))) {
+ talloc_free_children(p);
+ *p = (struct mp_draw_sub_cache){.global = p->global};
+ return false;
+ }
+ }
+ return true;
+}
+
+char *mp_draw_sub_get_dbg_info(struct mp_draw_sub_cache *p)
+{
+ assert(p);
+
+ return talloc_asprintf(NULL,
+ "align=%d:%d ov=%-7s, ov_f=%s, v_f=%s, a=%s, ca=%s, ca_f=%s",
+ p->align_x, p->align_y,
+ mp_imgfmt_to_name(p->video_overlay ? p->video_overlay->imgfmt : 0),
+ mp_imgfmt_to_name(p->overlay_tmp->imgfmt),
+ mp_imgfmt_to_name(p->video_tmp->imgfmt),
+ mp_imgfmt_to_name(p->alpha_overlay ? p->alpha_overlay->imgfmt : 0),
+ mp_imgfmt_to_name(p->calpha_overlay ? p->calpha_overlay->imgfmt : 0),
+ mp_imgfmt_to_name(p->calpha_tmp ? p->calpha_tmp->imgfmt : 0));
+}
+
+struct mp_draw_sub_cache *mp_draw_sub_alloc(void *ta_parent, struct mpv_global *g)
+{
+ struct mp_draw_sub_cache *c = talloc_zero(ta_parent, struct mp_draw_sub_cache);
+ c->global = g;
+ return c;
+}
+
+// For tests.
+struct mp_draw_sub_cache *mp_draw_sub_alloc_test(struct mp_image *dst)
+{
+ struct mp_draw_sub_cache *c = talloc_zero(NULL, struct mp_draw_sub_cache);
+ reinit_to_video(c);
+ return c;
+}
+
+bool mp_draw_sub_bitmaps(struct mp_draw_sub_cache *p, struct mp_image *dst,
+ struct sub_bitmap_list *sbs_list)
+{
+ bool ok = false;
+
+ // dst must at least be as large as the bounding box, or you may get memory
+ // corruption.
+ assert(dst->w >= sbs_list->w);
+ assert(dst->h >= sbs_list->h);
+
+ if (!check_reinit(p, &dst->params, true))
+ return false;
+
+ if (p->change_id != sbs_list->change_id) {
+ p->change_id = sbs_list->change_id;
+
+ clear_rgba_overlay(p);
+
+ for (int n = 0; n < sbs_list->num_items; n++) {
+ if (!render_sb(p, sbs_list->items[n]))
+ goto done;
+ }
+
+ if (!convert_to_video_overlay(p))
+ goto done;
+ }
+
+ if (p->any_osd) {
+ struct mp_image *target = dst;
+ if (p->premul_tmp) {
+ if (mp_sws_scale(p->premul, p->premul_tmp, dst) < 0)
+ goto done;
+ target = p->premul_tmp;
+ }
+
+ if (!blend_overlay_with_video(p, target))
+ goto done;
+
+ if (target != dst) {
+ if (mp_sws_scale(p->unpremul, dst, p->premul_tmp) < 0)
+ goto done;
+ }
+ }
+
+ ok = true;
+
+done:
+ return ok;
+}
+
+// Bounding boxes for mp_draw_sub_overlay() API. For simplicity, each rectangle
+// covers a fixed tile on the screen, starts out empty, but is not extended
+// beyond the tile. In the simplest case, there's only 1 rect/tile for everything.
+struct rc_grid {
+ unsigned w, h; // size in grid tiles
+ unsigned r_w, r_h; // size of a grid tile in pixels
+ struct mp_rect *rcs; // rcs[x * w + y]
+};
+
+static void init_rc_grid(struct rc_grid *gr, struct mp_draw_sub_cache *p,
+ struct mp_rect *rcs, int max_rcs)
+{
+ *gr = (struct rc_grid){ .w = max_rcs ? 1 : 0, .h = max_rcs ? 1 : 0,
+ .rcs = rcs, .r_w = p->s_w * SLICE_W, .r_h = p->h, };
+
+ // Dumb iteration to figure out max. size because I'm stupid.
+ bool more = true;
+ while (more) {
+ more = false;
+ if (gr->r_h >= 128) {
+ if (gr->w * gr->h * 2 > max_rcs)
+ break;
+ gr->h *= 2;
+ gr->r_h = (p->h + gr->h - 1) / gr->h;
+ more = true;
+ }
+ if (gr->r_w >= SLICE_W * 2) {
+ if (gr->w * gr->h * 2 > max_rcs)
+ break;
+ gr->w *= 2;
+ gr->r_w = (p->s_w + gr->w - 1) / gr->w * SLICE_W;
+ more = true;
+ }
+ }
+
+ assert(gr->r_h * gr->h >= p->h);
+ assert(!(gr->r_w & (SLICE_W - 1)));
+ assert(gr->r_w * gr->w >= p->w);
+
+ // Init with empty (degenerate) rectangles.
+ for (int y = 0; y < gr->h; y++) {
+ for (int x = 0; x < gr->w; x++) {
+ struct mp_rect *rc = &gr->rcs[y * gr->w + x];
+ rc->x1 = x * gr->r_w;
+ rc->y1 = y * gr->r_h;
+ rc->x0 = rc->x1 + gr->r_w;
+ rc->y0 = rc->y1 + gr->r_h;
+ }
+ }
+}
+
+// Extend given grid with contents of p->slices.
+static void mark_rcs(struct mp_draw_sub_cache *p, struct rc_grid *gr)
+{
+ for (int y = 0; y < p->h; y++) {
+ struct slice *line = &p->slices[y * p->s_w];
+ struct mp_rect *rcs = &gr->rcs[y / gr->r_h * gr->w];
+
+ for (int sx = 0; sx < p->s_w; sx++) {
+ struct slice *s = &line[sx];
+ if (s->x0 < s->x1) {
+ unsigned xpos = sx * SLICE_W;
+ struct mp_rect *rc = &rcs[xpos / gr->r_w];
+ rc->y0 = MPMIN(rc->y0, y);
+ rc->y1 = MPMAX(rc->y1, y + 1);
+ rc->x0 = MPMIN(rc->x0, xpos + s->x0);
+ // Ensure this does not extend beyond the total width
+ rc->x1 = MPCLAMP(xpos + s->x1, rc->x1, p->w);
+ }
+ }
+ }
+}
+
+// Remove empty RCs, and return rc count.
+static int return_rcs(struct rc_grid *gr)
+{
+ int num = 0, cnt = gr->w * gr->h;
+ for (int n = 0; n < cnt; n++) {
+ struct mp_rect *rc = &gr->rcs[n];
+ if (rc->x0 < rc->x1 && rc->y0 < rc->y1)
+ gr->rcs[num++] = *rc;
+ }
+ return num;
+}
+
+struct mp_image *mp_draw_sub_overlay(struct mp_draw_sub_cache *p,
+ struct sub_bitmap_list *sbs_list,
+ struct mp_rect *act_rcs,
+ int max_act_rcs,
+ int *num_act_rcs,
+ struct mp_rect *mod_rcs,
+ int max_mod_rcs,
+ int *num_mod_rcs)
+{
+ *num_act_rcs = 0;
+ *num_mod_rcs = 0;
+
+ struct mp_image_params params = {.w = sbs_list->w, .h = sbs_list->h};
+ if (!check_reinit(p, &params, false))
+ return NULL;
+
+ struct rc_grid gr_act, gr_mod;
+ init_rc_grid(&gr_act, p, act_rcs, max_act_rcs);
+ init_rc_grid(&gr_mod, p, mod_rcs, max_mod_rcs);
+
+ if (p->change_id != sbs_list->change_id) {
+ p->change_id = sbs_list->change_id;
+
+ mark_rcs(p, &gr_mod);
+
+ clear_rgba_overlay(p);
+
+ for (int n = 0; n < sbs_list->num_items; n++) {
+ if (!render_sb(p, sbs_list->items[n])) {
+ p->change_id = 0;
+ return NULL;
+ }
+ }
+
+ mark_rcs(p, &gr_mod);
+ }
+
+ mark_rcs(p, &gr_act);
+
+ *num_act_rcs = return_rcs(&gr_act);
+ *num_mod_rcs = return_rcs(&gr_mod);
+
+ return &p->res_overlay;
+}
+
+// vim: ts=4 sw=4 et tw=80
diff --git a/sub/draw_bmp.h b/sub/draw_bmp.h
new file mode 100644
index 0000000..fda7797
--- /dev/null
+++ b/sub/draw_bmp.h
@@ -0,0 +1,63 @@
+#ifndef MPLAYER_DRAW_BMP_H
+#define MPLAYER_DRAW_BMP_H
+
+#include "osd.h"
+
+struct mp_rect;
+struct mp_image;
+struct mpv_global;
+struct mp_draw_sub_cache;
+
+struct mp_draw_sub_cache *mp_draw_sub_alloc(void *ta_parent, struct mpv_global *g);
+
+// Only for use in tests.
+struct mp_draw_sub_cache *mp_draw_sub_alloc_test(struct mp_image *dst);
+
+// Render the sub-bitmaps in sbs_list to dst. sbs_list must have been rendered
+// for an OSD resolution equivalent to dst's size (UB if not).
+// Warning: if dst is a format with alpha, and dst is not set to MP_ALPHA_PREMUL
+// (not done by default), this will be extremely slow.
+// Warning: the caller is responsible for ensuring that dst is writable.
+// cache: allocated instance; caches non-changing OSD parts etc.
+// dst: image to draw to
+// sbs_list: source sub-bitmaps
+// returns: success
+bool mp_draw_sub_bitmaps(struct mp_draw_sub_cache *cache, struct mp_image *dst,
+ struct sub_bitmap_list *sbs_list);
+
+char *mp_draw_sub_get_dbg_info(struct mp_draw_sub_cache *c);
+
+// Return a RGBA overlay with subtitles. The returned image uses IMGFMT_BGRA and
+// premultiplied alpha, and the size specified by sbs_list.w/h.
+// This can return a list of active (act_) and modified (mod_) rectangles.
+// Active rectangles are regions that contain visible OSD pixels. Modified
+// rectangles are regions that were changed since the last call. This function
+// always makes the act region a subset of the mod region. Rectangles within a
+// list never overlap with rectangles within the same list.
+// If num_mod_rcs==0 is returned, this function guarantees that the act region
+// did not change since the last call.
+// If the user-provided lists are too small (max_*_rcs too small), multiple
+// rectangles are merged until they fit in the list.
+// You can pass max_act_rcs=0, which implies you render the whole overlay.
+// cache: allocated instance; keeps track of changed regions
+// sbs_list: source sub-bitmaps
+// act_rcs: caller allocated list of non-transparent rectangles
+// max_act_rcs: number of allocated items in act_rcs
+// num_act_rcs: set to the number of valid items in act_rcs
+// mod_rcs, max_mod_rcs, num_mod_rcs: modified rectangles
+// returns: internal OSD overlay owned by cache, NULL on error
+// read only, valid until the next call on cache
+struct mp_image *mp_draw_sub_overlay(struct mp_draw_sub_cache *cache,
+ struct sub_bitmap_list *sbs_list,
+ struct mp_rect *act_rcs,
+ int max_act_rcs,
+ int *num_act_rcs,
+ struct mp_rect *mod_rcs,
+ int max_mod_rcs,
+ int *num_mod_rcs);
+
+extern const bool mp_draw_sub_formats[SUBBITMAP_COUNT];
+
+#endif /* MPLAYER_DRAW_BMP_H */
+
+// vim: ts=4 sw=4 et tw=80
diff --git a/sub/filter_jsre.c b/sub/filter_jsre.c
new file mode 100644
index 0000000..f956000
--- /dev/null
+++ b/sub/filter_jsre.c
@@ -0,0 +1,140 @@
+#include <stdio.h>
+#include <sys/types.h>
+
+#include <mujs.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/bstr.h"
+#include "options/options.h"
+#include "sd.h"
+
+
+// p_NAME are protected functions (never throw) which interact with the JS VM.
+// return 0 on successful interaction, not-0 on (caught) js-error.
+// on error: stack is the same as on entry + an error value
+
+// js: global[n] = new RegExp(str, flags)
+static int p_regcomp(js_State *J, int n, const char *str, int flags)
+{
+ if (js_try(J))
+ return 1;
+
+ js_pushnumber(J, n); // n
+ js_newregexp(J, str, flags); // n regex
+ js_setglobal(J, js_tostring(J, -2)); // n (and global[n] is the regex)
+ js_pop(J, 1);
+
+ js_endtry(J);
+ return 0;
+}
+
+// js: found = global[n].test(text)
+static int p_regexec(js_State *J, int n, const char *text, int *found)
+{
+ if (js_try(J))
+ return 1;
+
+ js_pushnumber(J, n); // n
+ js_getglobal(J, js_tostring(J, -1)); // n global[n]
+ js_getproperty(J, -1, "test"); // n global[n] global[n].test
+ js_rot2(J); // n global[n].test global[n] (n, test(), and its `this')
+ js_pushstring(J, text); // n global[n].test global[n] text
+ js_call(J, 1); // n test-result
+ *found = js_toboolean(J, -1);
+ js_pop(J, 2); // the result and n
+
+ js_endtry(J);
+ return 0;
+}
+
+// protected. caller should pop the error after using the result string.
+static const char *get_err(js_State *J)
+{
+ return js_trystring(J, -1, "unknown error");
+}
+
+
+struct priv {
+ js_State *J;
+ int num_regexes;
+ int offset;
+};
+
+static void destruct_priv(void *p)
+{
+ js_freestate(((struct priv *)p)->J);
+}
+
+static bool jsre_init(struct sd_filter *ft)
+{
+ if (strcmp(ft->codec, "ass") != 0)
+ return false;
+
+ if (!ft->opts->rf_enable)
+ return false;
+
+ if (!(ft->opts->jsre_items && ft->opts->jsre_items[0]))
+ return false;
+
+ struct priv *p = talloc_zero(ft, struct priv);
+ ft->priv = p;
+
+ p->J = js_newstate(0, 0, JS_STRICT);
+ if (!p->J) {
+ MP_ERR(ft, "jsre: VM init error\n");
+ return false;
+ }
+ talloc_set_destructor(p, destruct_priv);
+
+ for (int n = 0; ft->opts->jsre_items[n]; n++) {
+ char *item = ft->opts->jsre_items[n];
+
+ int err = p_regcomp(p->J, p->num_regexes, item, JS_REGEXP_I | JS_REGEXP_M);
+ if (err) {
+ MP_ERR(ft, "jsre: %s -- '%s'\n", get_err(p->J), item);
+ js_pop(p->J, 1);
+ continue;
+ }
+
+ p->num_regexes += 1;
+ }
+
+ if (!p->num_regexes)
+ return false;
+
+ p->offset = sd_ass_fmt_offset(ft->event_format);
+ return true;
+}
+
+static struct demux_packet *jsre_filter(struct sd_filter *ft,
+ struct demux_packet *pkt)
+{
+ struct priv *p = ft->priv;
+ char *text = bstrto0(NULL, sd_ass_pkt_text(ft, pkt, p->offset));
+ bool drop = false;
+
+ if (ft->opts->rf_plain)
+ sd_ass_to_plaintext(text, strlen(text), text);
+
+ for (int n = 0; n < p->num_regexes; n++) {
+ int found, err = p_regexec(p->J, n, text, &found);
+ if (err == 0 && found) {
+ int level = ft->opts->rf_warn ? MSGL_WARN : MSGL_V;
+ MP_MSG(ft, level, "jsre: regex %d => drop: '%s'\n", n, text);
+ drop = true;
+ break;
+ } else if (err) {
+ MP_WARN(ft, "jsre: test regex %d: %s.\n", n, get_err(p->J));
+ js_pop(p->J, 1);
+ }
+ }
+
+ talloc_free(text);
+ return drop ? NULL : pkt;
+}
+
+const struct sd_filter_functions sd_filter_jsre = {
+ .init = jsre_init,
+ .filter = jsre_filter,
+};
diff --git a/sub/filter_regex.c b/sub/filter_regex.c
new file mode 100644
index 0000000..8e29991
--- /dev/null
+++ b/sub/filter_regex.c
@@ -0,0 +1,89 @@
+#include <regex.h>
+#include <sys/types.h>
+
+#include "common/common.h"
+#include "common/msg.h"
+#include "misc/bstr.h"
+#include "options/options.h"
+#include "sd.h"
+
+struct priv {
+ int offset;
+ regex_t *regexes;
+ int num_regexes;
+};
+
+static bool rf_init(struct sd_filter *ft)
+{
+ if (strcmp(ft->codec, "ass") != 0)
+ return false;
+
+ if (!ft->opts->rf_enable)
+ return false;
+
+ struct priv *p = talloc_zero(ft, struct priv);
+ ft->priv = p;
+
+ for (int n = 0; ft->opts->rf_items && ft->opts->rf_items[n]; n++) {
+ char *item = ft->opts->rf_items[n];
+
+ MP_TARRAY_GROW(p, p->regexes, p->num_regexes);
+ regex_t *preg = &p->regexes[p->num_regexes];
+
+ int err = regcomp(preg, item, REG_ICASE | REG_EXTENDED | REG_NOSUB | REG_NEWLINE);
+ if (err) {
+ char errbuf[512];
+ regerror(err, preg, errbuf, sizeof(errbuf));
+ MP_ERR(ft, "Regular expression error: '%s'\n", errbuf);
+ continue;
+ }
+
+ p->num_regexes += 1;
+ }
+
+ if (!p->num_regexes)
+ return false;
+
+ p->offset = sd_ass_fmt_offset(ft->event_format);
+ return true;
+}
+
+static void rf_uninit(struct sd_filter *ft)
+{
+ struct priv *p = ft->priv;
+
+ for (int n = 0; n < p->num_regexes; n++)
+ regfree(&p->regexes[n]);
+}
+
+static struct demux_packet *rf_filter(struct sd_filter *ft,
+ struct demux_packet *pkt)
+{
+ struct priv *p = ft->priv;
+ char *text = bstrto0(NULL, sd_ass_pkt_text(ft, pkt, p->offset));
+ bool drop = false;
+
+ if (ft->opts->rf_plain)
+ sd_ass_to_plaintext(text, strlen(text), text);
+
+ for (int n = 0; n < p->num_regexes; n++) {
+ int err = regexec(&p->regexes[n], text, 0, NULL, 0);
+ if (err == 0) {
+ int level = ft->opts->rf_warn ? MSGL_WARN : MSGL_V;
+ MP_MSG(ft, level, "Matching regex %d => drop: '%s'\n", n, text);
+ drop = true;
+ break;
+ } else if (err != REG_NOMATCH) {
+ MP_WARN(ft, "Error on regexec() on regex %d.\n", n);
+ }
+ }
+
+ talloc_free(text);
+ return drop ? NULL : pkt;
+}
+
+const struct sd_filter_functions sd_filter_regex = {
+ .init = rf_init,
+ .uninit = rf_uninit,
+ .filter = rf_filter,
+};
diff --git a/sub/filter_sdh.c b/sub/filter_sdh.c
new file mode 100644
index 0000000..69fca9f
--- /dev/null
+++ b/sub/filter_sdh.c
@@ -0,0 +1,482 @@
+/*
+ * 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 <stdlib.h>
+#include <string.h>
+#include <limits.h>
+#include <stddef.h>
+
+#include "misc/ctype.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "options/options.h"
+#include "sd.h"
+
+// Filter for removing subtitle additions for deaf or hard-of-hearing (SDH)
+// This is for English, but may in part work for others too.
+// The intention is that it can always be active so may not remove
+// all SDH parts.
+// It is for filtering ASS encoded subtitles
+
+struct buffer {
+ char *string;
+ int length;
+ int pos;
+};
+
+static void init_buf(struct buffer *buf, int length)
+{
+ buf->string = talloc_size(NULL, length);
+ buf->pos = 0;
+ buf->length = length;
+}
+
+static inline int append(struct sd_filter *sd, struct buffer *buf, char c)
+{
+ if (buf->pos >= 0 && buf->pos < buf->length) {
+ buf->string[buf->pos++] = c;
+ } else {
+ // ensure that terminating \0 is always written
+ if (c == '\0')
+ buf->string[buf->length - 1] = c;
+ }
+ return c;
+}
+
+
+// copy ass override tags, if they exist att current position,
+// from source string to destination buffer stopping at first
+// character following last sequence of '{text}'
+//
+// Parameters:
+// rpp read pointer pointer to source string, updated on return
+// buf write buffer
+//
+// on return the read pointer is updated to the position after
+// the tags.
+static void copy_ass(struct sd_filter *sd, char **rpp, struct buffer *buf)
+{
+ char *rp = *rpp;
+
+ while (rp[0] == '{') {
+ while (*rp) {
+ char tmp = append(sd, buf, rp[0]);
+ rp++;
+ if (tmp == '}')
+ break;
+ }
+ }
+ *rpp = rp;
+
+ return;
+}
+
+static bool skip_bracketed(struct sd_filter *sd, char **rpp, struct buffer *buf);
+
+// check for speaker label, like MAN:
+// normal subtitles may include mixed case text with : after so
+// only upper case is accepted and lower case l which for some
+// looks like upper case I unless filter_harder - then
+// lower case is also acceptable
+//
+// Parameters:
+// rpp read pointer pointer to source string, updated on return
+// buf write buffer
+//
+// scan in source string and copy ass tags to destination string
+// skipping speaker label if it exists
+//
+// if no label was found read pointer and write position in buffer
+// will be unchanged
+// otherwise they point to next position after label and next write position
+static void skip_speaker_label(struct sd_filter *sd, char **rpp, struct buffer *buf)
+{
+ int filter_harder = sd->opts->sub_filter_SDH_harder;
+ char *rp = *rpp;
+ int old_pos = buf->pos;
+
+ copy_ass(sd, &rp, buf);
+ // copy any leading "- "
+ if (rp[0] == '-') {
+ append(sd, buf, rp[0]);
+ rp++;
+ }
+ copy_ass(sd, &rp, buf);
+ while (rp[0] == ' ') {
+ append(sd, buf, rp[0]);
+ rp++;
+ copy_ass(sd, &rp, buf);
+ }
+ // skip past valid data searching for :
+ while (*rp && rp[0] != ':') {
+ if (rp[0] == '{') {
+ copy_ass(sd, &rp, buf);
+ } else if (rp[0] == '[') {
+ // not uncommon with [xxxx]: which should also be skipped
+ if (!skip_bracketed(sd, &rp, buf)) {
+ buf->pos = old_pos;
+ return;
+ }
+ } else if ((mp_isalpha(rp[0]) &&
+ (filter_harder || mp_isupper(rp[0]) || rp[0] == 'l')) ||
+ mp_isdigit(rp[0]) ||
+ rp[0] == ' ' || rp[0] == '\'' ||
+ (filter_harder && (rp[0] == '(' || rp[0] == ')')) ||
+ rp[0] == '#' || rp[0] == '.' || rp[0] == ',') {
+ rp++;
+ } else {
+ buf->pos = old_pos;
+ return;
+ }
+ }
+ if (!*rp) {
+ // : was not found
+ buf->pos = old_pos;
+ return;
+ }
+ rp++; // skip :
+ copy_ass(sd, &rp, buf);
+ if (!*rp) {
+ // end of data
+ } else if (rp[0] == '\\' && rp[1] == 'N') {
+ // line end follows - skip it as line is empty
+ rp += 2;
+ } else if (rp[0] == ' ') {
+ while (rp[0] == ' ') {
+ rp++;
+ }
+ if (rp[0] == '\\' && rp[1] == 'N') {
+ // line end follows - skip it as line is empty
+ rp += 2;
+ }
+ } else {
+ // non space follows - no speaker label
+ buf->pos = old_pos;
+ return;
+ }
+ *rpp = rp;
+
+ return;
+}
+
+// check for bracketed text, like [SOUND]
+// and skip it while preserving ass tags
+// any characters are allowed, brackets are seldom used in normal text
+//
+// Parameters:
+// rpp read pointer pointer to source string, updated on return
+// buf write buffer
+//
+// scan in source string
+// the first character in source string must by the starting '['
+// and copy ass tags to destination string but
+// skipping bracketed text if it looks like SDH
+//
+// return true if bracketed text was removed.
+// if not valid SDH read pointer and write buffer position will be unchanged
+// otherwise they point to next position after text and next write position
+static bool skip_bracketed(struct sd_filter *sd, char **rpp, struct buffer *buf)
+{
+ char *rp = *rpp;
+ int old_pos = buf->pos;
+
+ rp++; // skip past '['
+ // skip past valid data searching for ]
+ while (*rp && rp[0] != ']') {
+ if (rp[0] == '{') {
+ copy_ass(sd, &rp, buf);
+ } else {
+ rp++;
+ }
+ }
+ if (!*rp) {
+ // ] was not found
+ buf->pos = old_pos;
+ return false;
+ }
+ rp++; // skip ]
+ // skip trailing spaces
+ while (rp[0] == ' ') {
+ rp++;
+ }
+ *rpp = rp;
+
+ return true;
+}
+
+// check for parenthesized text, like (SOUND)
+// and skip it while preserving ass tags
+// normal subtitles may include mixed case text in parentheses so
+// only upper case is accepted and lower case l which for some
+// looks like upper case I but if requested harder filtering
+// both upper and lower case is accepted
+//
+// Parameters:
+// rpp read pointer pointer to source string, updated on return
+// buf write buffer
+//
+// scan in source string
+// the first character in source string must be the starting '('
+// and copy ass tags to destination string but
+// skipping parenthesized text if it looks like SDH
+//
+// return true if parenthesized text was removed.
+// if not valid SDH read pointer and write buffer position will be unchanged
+// otherwise they point to next position after text and next write position
+static bool skip_parenthesized(struct sd_filter *sd, char **rpp, struct buffer *buf)
+{
+ int filter_harder = sd->opts->sub_filter_SDH_harder;
+ char *rp = *rpp;
+ int old_pos = buf->pos;
+
+ rp++; // skip past '('
+ // skip past valid data searching for )
+ bool only_digits = true;
+ while (*rp && rp[0] != ')') {
+ if (rp[0] == '{') {
+ copy_ass(sd, &rp, buf);
+ } else if ((mp_isalpha(rp[0]) &&
+ (filter_harder || mp_isupper(rp[0]) || rp[0] == 'l')) ||
+ mp_isdigit(rp[0]) ||
+ rp[0] == ' ' || rp[0] == '\'' || rp[0] == '#' ||
+ rp[0] == '.' || rp[0] == ',' ||
+ rp[0] == '-' || rp[0] == '"' || rp[0] == '\\') {
+ if (!mp_isdigit(rp[0]))
+ only_digits = false;
+ rp++;
+ } else {
+ buf->pos = old_pos;
+ return false;
+ }
+ }
+ if (!*rp) {
+ // ) was not found
+ buf->pos = old_pos;
+ return false;
+ }
+ if (only_digits) {
+ // number within parentheses is probably not SDH
+ buf->pos = old_pos;
+ return false;
+ }
+ rp++; // skip )
+ // skip trailing spaces
+ while (rp[0] == ' ') {
+ rp++;
+ }
+ *rpp = rp;
+
+ return true;
+}
+
+// remove leading hyphen and following spaces in write buffer
+//
+// Parameters:
+// start_pos start position i buffer
+// buf buffer to remove in
+//
+// when removing characters the following are moved back
+//
+static void remove_leading_hyphen_space(struct sd_filter *sd, int start_pos,
+ struct buffer *buf)
+{
+ int old_pos = buf->pos;
+ if (start_pos < 0 || start_pos >= old_pos)
+ return;
+ append(sd, buf, '\0'); // \0 terminate for reading
+
+ // move past leading ass tags
+ while (buf->string[start_pos] == '{') {
+ while (buf->string[start_pos] && buf->string[start_pos] != '}') {
+ start_pos++;
+ }
+ if (buf->string[start_pos])
+ start_pos++; // skip past '}'
+ }
+
+ // if there is not a leading '-' no removing will be done
+ if (buf->string[start_pos] != '-') {
+ buf->pos = old_pos;
+ return;
+ }
+
+ char *rp = &buf->string[start_pos]; // read from here
+ buf->pos = start_pos; // start writing here
+ rp++; // skip '-'
+ copy_ass(sd, &rp, buf);
+ while (rp[0] == ' ') {
+ rp++; // skip ' '
+ copy_ass(sd, &rp, buf);
+ }
+ while (*rp) {
+ // copy the rest
+ append(sd, buf, rp[0]);
+ rp++;
+ }
+}
+
+// Filter ASS formatted string for SDH
+//
+// Parameters:
+// data ASS line
+// length length of ASS line
+// toff Text offset from data. required: 0 <= toff <= length
+//
+// Returns a talloc allocated string with filtered ASS data (may be the same
+// content as original if no SDH was found) which must be released
+// by caller using talloc_free.
+//
+// Returns NULL if filtering resulted in all of ASS data being removed so no
+// subtitle should be output
+static char *filter_SDH(struct sd_filter *sd, char *data, int length, ptrdiff_t toff)
+{
+ struct buffer writebuf;
+ struct buffer *buf = &writebuf;
+ init_buf(buf, length + 1); // with room for terminating '\0'
+
+ // pre-text headers into buf, rp is the (null-terminated) remaining text
+ char *ass = talloc_strndup(NULL, data, length), *rp = ass;
+ while (rp - ass < toff)
+ append(sd, buf, *rp++);
+
+ bool contains_text = false; // true if non SDH text was found
+ bool line_with_text = false; // if last line contained text
+ int wp_line_start = buf->pos; // write pos to start of last line
+ int wp_line_end = buf->pos; // write pos to end of previous line with text (\N)
+
+ // go through the lines in the text
+ // they are separated by \N
+ while (*rp) {
+ line_with_text = false;
+ wp_line_start = buf->pos;
+
+ // skip any speaker label
+ skip_speaker_label(sd, &rp, buf);
+
+ // go through the rest of the line looking for SDH in () or []
+ while (*rp && !(rp[0] == '\\' && rp[1] == 'N')) {
+ copy_ass(sd, &rp, buf);
+ if (rp[0] == '[') {
+ if (!skip_bracketed(sd, &rp, buf)) {
+ append(sd, buf, rp[0]);
+ rp++;
+ line_with_text = true;
+ }
+ } else if (rp[0] == '(') {
+ if (!skip_parenthesized(sd, &rp, buf)) {
+ append(sd, buf, rp[0]);
+ rp++;
+ line_with_text = true;
+ }
+ } else if (*rp && rp[0] != '\\') {
+ if ((rp[0] > 32 && rp[0] < 127 && rp[0] != '-') ||
+ (unsigned char)rp[0] >= 0xC0)
+ {
+ line_with_text = true;
+ }
+ append(sd, buf, rp[0]);
+ rp++;
+ } else if (rp[0] == '\\' && rp[1] != 'N') {
+ append(sd, buf, rp[0]);
+ rp++;
+ }
+ }
+ // either end of data or ASS line end defined by separating \N
+ if (*rp) {
+ // ASS line end
+ if (line_with_text) {
+ contains_text = true;
+ wp_line_end = buf->pos;
+ append(sd, buf, rp[0]); // copy backslash
+ append(sd, buf, rp[1]); // copy N
+ rp += 2; // move read pointer past \N
+ } else {
+ // no text in line, remove leading hyphen and spaces
+ remove_leading_hyphen_space(sd, wp_line_start, buf);
+ // and join with next line
+ rp += 2; // move read pointer past \N
+ }
+ }
+ }
+ // if no normal text in last line - remove last line
+ // by moving write pointer to start of last line
+ if (!line_with_text) {
+ buf->pos = wp_line_end;
+ } else {
+ contains_text = true;
+ }
+ talloc_free(ass);
+
+ if (contains_text) {
+ // the ASS data contained normal text after filtering
+ append(sd, buf, '\0'); // '\0' terminate
+ return buf->string;
+ } else {
+ // all data removed by filtering
+ talloc_free(buf->string);
+ return NULL;
+ }
+}
+
+static bool sdh_init(struct sd_filter *ft)
+{
+ if (strcmp(ft->codec, "ass") != 0)
+ return false;
+
+ if (!ft->opts->sub_filter_SDH)
+ return false;
+
+ if (!ft->event_format) {
+ MP_VERBOSE(ft, "SDH filtering not possible - format missing\n");
+ return false;
+ }
+
+ return true;
+}
+
+static struct demux_packet *sdh_filter(struct sd_filter *ft,
+ struct demux_packet *pkt)
+{
+ bstr text = sd_ass_pkt_text(ft, pkt, sd_ass_fmt_offset(ft->event_format));
+ if (!text.start || !text.len || pkt->len >= INT_MAX)
+ return pkt; // we don't touch it
+
+ ptrdiff_t toff = text.start - pkt->buffer;
+ char *line = filter_SDH(ft, (char *)pkt->buffer, (int)pkt->len, toff);
+ if (!line)
+ return NULL;
+ if (0 == bstrcmp0((bstr){(char *)pkt->buffer, pkt->len}, line)) {
+ talloc_free(line);
+ return pkt; // unmodified, no need to allocate new packet
+ }
+
+ // Stupidly, this copies it again. One could possibly allocate the packet
+ // for writing in the first place (new_demux_packet()) and use
+ // demux_packet_shorten().
+ struct demux_packet *npkt = new_demux_packet_from(line, strlen(line));
+ if (npkt)
+ demux_packet_copy_attribs(npkt, pkt);
+
+ talloc_free(line);
+ return npkt;
+}
+
+const struct sd_filter_functions sd_filter_sdh = {
+ .init = sdh_init,
+ .filter = sdh_filter,
+};
diff --git a/sub/img_convert.c b/sub/img_convert.c
new file mode 100644
index 0000000..a70bb0a
--- /dev/null
+++ b/sub/img_convert.c
@@ -0,0 +1,128 @@
+/*
+ * 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 <string.h>
+#include <assert.h>
+#include <limits.h>
+
+#include "mpv_talloc.h"
+
+#include "common/common.h"
+#include "img_convert.h"
+#include "osd.h"
+#include "video/img_format.h"
+#include "video/mp_image.h"
+#include "video/sws_utils.h"
+
+void mp_blur_rgba_sub_bitmap(struct sub_bitmap *d, double gblur)
+{
+ struct mp_image *tmp1 = mp_image_alloc(IMGFMT_BGRA, d->w, d->h);
+ if (tmp1) { // on OOM, skip region
+ struct mp_image s = {0};
+ mp_image_setfmt(&s, IMGFMT_BGRA);
+ mp_image_set_size(&s, d->w, d->h);
+ s.stride[0] = d->stride;
+ s.planes[0] = d->bitmap;
+
+ mp_image_copy(tmp1, &s);
+
+ mp_image_sw_blur_scale(&s, tmp1, gblur);
+ }
+ talloc_free(tmp1);
+}
+
+bool mp_sub_bitmaps_bb(struct sub_bitmaps *imgs, struct mp_rect *out_bb)
+{
+ struct mp_rect bb = {INT_MAX, INT_MAX, INT_MIN, INT_MIN};
+ for (int n = 0; n < imgs->num_parts; n++) {
+ struct sub_bitmap *p = &imgs->parts[n];
+ bb.x0 = MPMIN(bb.x0, p->x);
+ bb.y0 = MPMIN(bb.y0, p->y);
+ bb.x1 = MPMAX(bb.x1, p->x + p->dw);
+ bb.y1 = MPMAX(bb.y1, p->y + p->dh);
+ }
+
+ // avoid degenerate bounding box if empty
+ bb.x0 = MPMIN(bb.x0, bb.x1);
+ bb.y0 = MPMIN(bb.y0, bb.y1);
+
+ *out_bb = bb;
+
+ return bb.x0 < bb.x1 && bb.y0 < bb.y1;
+}
+
+// Merge bounding rectangles if they're closer than the given amount of pixels.
+// Avoids having too many rectangles due to spacing between letter.
+#define MERGE_RC_PIXELS 50
+
+static void remove_intersecting_rcs(struct mp_rect *list, int *count)
+{
+ int M = MERGE_RC_PIXELS;
+ bool changed = true;
+ while (changed) {
+ changed = false;
+ for (int a = 0; a < *count; a++) {
+ struct mp_rect *rc_a = &list[a];
+ for (int b = *count - 1; b > a; b--) {
+ struct mp_rect *rc_b = &list[b];
+ if (rc_a->x0 - M <= rc_b->x1 && rc_a->x1 + M >= rc_b->x0 &&
+ rc_a->y0 - M <= rc_b->y1 && rc_a->y1 + M >= rc_b->y0)
+ {
+ mp_rect_union(rc_a, rc_b);
+ MP_TARRAY_REMOVE_AT(list, *count, b);
+ changed = true;
+ }
+ }
+ }
+ }
+}
+
+// Cluster the given subrectangles into a small numbers of bounding rectangles,
+// and store them into list. E.g. when subtitles and toptitles are visible at
+// the same time, there should be two bounding boxes, so that the video between
+// the text is left untouched (need to resample less pixels -> faster).
+// Returns number of rectangles added to out_rc_list (<= rc_list_count)
+// NOTE: some callers assume that sub bitmaps are never split or partially
+// covered by returned rectangles.
+int mp_get_sub_bb_list(struct sub_bitmaps *sbs, struct mp_rect *out_rc_list,
+ int rc_list_count)
+{
+ int M = MERGE_RC_PIXELS;
+ int num_rc = 0;
+ for (int n = 0; n < sbs->num_parts; n++) {
+ struct sub_bitmap *sb = &sbs->parts[n];
+ struct mp_rect bb = {sb->x, sb->y, sb->x + sb->dw, sb->y + sb->dh};
+ bool intersects = false;
+ for (int r = 0; r < num_rc; r++) {
+ struct mp_rect *rc = &out_rc_list[r];
+ if ((bb.x0 - M <= rc->x1 && bb.x1 + M >= rc->x0 &&
+ bb.y0 - M <= rc->y1 && bb.y1 + M >= rc->y0) ||
+ num_rc == rc_list_count)
+ {
+ mp_rect_union(rc, &bb);
+ intersects = true;
+ break;
+ }
+ }
+ if (!intersects) {
+ out_rc_list[num_rc++] = bb;
+ remove_intersecting_rcs(out_rc_list, &num_rc);
+ }
+ }
+ remove_intersecting_rcs(out_rc_list, &num_rc);
+ return num_rc;
+}
diff --git a/sub/img_convert.h b/sub/img_convert.h
new file mode 100644
index 0000000..e03c155
--- /dev/null
+++ b/sub/img_convert.h
@@ -0,0 +1,23 @@
+#ifndef MPLAYER_SUB_IMG_CONVERT_H
+#define MPLAYER_SUB_IMG_CONVERT_H
+
+#include <stdbool.h>
+
+struct sub_bitmaps;
+struct sub_bitmap;
+struct mp_rect;
+
+// Sub postprocessing
+void mp_blur_rgba_sub_bitmap(struct sub_bitmap *d, double gblur);
+
+bool mp_sub_bitmaps_bb(struct sub_bitmaps *imgs, struct mp_rect *out_bb);
+
+// Intentionally limit the maximum number of bounding rects to something low.
+// This prevents the algorithm from degrading to O(N^2).
+// Most subtitles yield a very low number of bounding rects (<5).
+#define MP_SUB_BB_LIST_MAX 15
+
+int mp_get_sub_bb_list(struct sub_bitmaps *sbs, struct mp_rect *out_rc_list,
+ int rc_list_count);
+
+#endif
diff --git a/sub/lavc_conv.c b/sub/lavc_conv.c
new file mode 100644
index 0000000..532e91d
--- /dev/null
+++ b/sub/lavc_conv.c
@@ -0,0 +1,293 @@
+/*
+ * 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 <assert.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/common.h>
+#include <libavutil/opt.h>
+
+#include "mpv_talloc.h"
+#include "common/msg.h"
+#include "common/av_common.h"
+#include "demux/stheader.h"
+#include "misc/bstr.h"
+#include "sd.h"
+
+struct lavc_conv {
+ struct mp_log *log;
+ AVCodecContext *avctx;
+ AVPacket *avpkt;
+ AVPacket *avpkt_vtt;
+ char *codec;
+ char *extradata;
+ AVSubtitle cur;
+ char **cur_list;
+};
+
+static const char *get_lavc_format(const char *format)
+{
+ // For the hack involving parse_webvtt().
+ if (format && strcmp(format, "webvtt-webm") == 0)
+ format = "webvtt";
+ // Most text subtitles are srt/html style anyway.
+ if (format && strcmp(format, "text") == 0)
+ format = "subrip";
+ return format;
+}
+
+// Disable style definitions generated by the libavcodec converter.
+// We always want the user defined style instead.
+static void disable_styles(bstr header)
+{
+ bstr style = bstr0("\nStyle: ");
+ while (header.len) {
+ int n = bstr_find(header, style);
+ if (n < 0)
+ break;
+ header.start[n + 1] = '#'; // turn into a comment
+ header = bstr_cut(header, n + style.len);
+ }
+}
+
+struct lavc_conv *lavc_conv_create(struct mp_log *log,
+ const struct mp_codec_params *mp_codec)
+{
+ struct lavc_conv *priv = talloc_zero(NULL, struct lavc_conv);
+ priv->log = log;
+ priv->cur_list = talloc_array(priv, char*, 0);
+ priv->codec = talloc_strdup(priv, mp_codec->codec);
+ AVCodecContext *avctx = NULL;
+ AVDictionary *opts = NULL;
+ const char *fmt = get_lavc_format(priv->codec);
+ const AVCodec *codec = avcodec_find_decoder(mp_codec_to_av_codec_id(fmt));
+ if (!codec)
+ goto error;
+ avctx = avcodec_alloc_context3(codec);
+ if (!avctx)
+ goto error;
+ if (mp_set_avctx_codec_headers(avctx, mp_codec) < 0)
+ goto error;
+
+ priv->avpkt = av_packet_alloc();
+ priv->avpkt_vtt = av_packet_alloc();
+ if (!priv->avpkt || !priv->avpkt_vtt)
+ goto error;
+
+#if LIBAVCODEC_VERSION_MAJOR < 59
+ av_dict_set(&opts, "sub_text_format", "ass", 0);
+#endif
+ av_dict_set(&opts, "flags2", "+ass_ro_flush_noop", 0);
+ if (strcmp(priv->codec, "eia_608") == 0)
+ av_dict_set(&opts, "real_time", "1", 0);
+ if (avcodec_open2(avctx, codec, &opts) < 0)
+ goto error;
+ av_dict_free(&opts);
+ // Documented as "set by libavcodec", but there is no other way
+ avctx->time_base = (AVRational) {1, 1000};
+ avctx->pkt_timebase = avctx->time_base;
+ avctx->sub_charenc_mode = FF_SUB_CHARENC_MODE_IGNORE;
+ priv->avctx = avctx;
+ priv->extradata = talloc_strndup(priv, avctx->subtitle_header,
+ avctx->subtitle_header_size);
+ disable_styles(bstr0(priv->extradata));
+ return priv;
+
+ error:
+ MP_FATAL(priv, "Could not open libavcodec subtitle converter\n");
+ av_dict_free(&opts);
+ av_free(avctx);
+ mp_free_av_packet(&priv->avpkt);
+ mp_free_av_packet(&priv->avpkt_vtt);
+ talloc_free(priv);
+ return NULL;
+}
+
+char *lavc_conv_get_extradata(struct lavc_conv *priv)
+{
+ return priv->extradata;
+}
+
+// FFmpeg WebVTT packets are pre-parsed in some way. The FFmpeg Matroska
+// demuxer does this on its own. In order to free our demuxer_mkv.c from
+// codec-specific crud, we do this here.
+// Copied from libavformat/matroskadec.c (FFmpeg 818ebe9 / 2013-08-19)
+// License: LGPL v2.1 or later
+// Author header: The FFmpeg Project
+// Modified in some ways.
+static int parse_webvtt(AVPacket *in, AVPacket *pkt)
+{
+ uint8_t *id, *settings, *text, *buf;
+ int id_len, settings_len, text_len;
+ uint8_t *p, *q;
+ int err;
+
+ uint8_t *data = in->data;
+ int data_len = in->size;
+
+ if (data_len <= 0)
+ return AVERROR_INVALIDDATA;
+
+ p = data;
+ q = data + data_len;
+
+ id = p;
+ id_len = -1;
+ while (p < q) {
+ if (*p == '\r' || *p == '\n') {
+ id_len = p - id;
+ if (*p == '\r')
+ p++;
+ break;
+ }
+ p++;
+ }
+
+ if (p >= q || *p != '\n')
+ return AVERROR_INVALIDDATA;
+ p++;
+
+ settings = p;
+ settings_len = -1;
+ while (p < q) {
+ if (*p == '\r' || *p == '\n') {
+ settings_len = p - settings;
+ if (*p == '\r')
+ p++;
+ break;
+ }
+ p++;
+ }
+
+ if (p >= q || *p != '\n')
+ return AVERROR_INVALIDDATA;
+ p++;
+
+ text = p;
+ text_len = q - p;
+ while (text_len > 0) {
+ const int len = text_len - 1;
+ const uint8_t c = p[len];
+ if (c != '\r' && c != '\n')
+ break;
+ text_len = len;
+ }
+
+ if (text_len <= 0)
+ return AVERROR_INVALIDDATA;
+
+ err = av_new_packet(pkt, text_len);
+ if (err < 0)
+ return AVERROR(err);
+
+ memcpy(pkt->data, text, text_len);
+
+ if (id_len > 0) {
+ buf = av_packet_new_side_data(pkt,
+ AV_PKT_DATA_WEBVTT_IDENTIFIER,
+ id_len);
+ if (buf == NULL) {
+ av_packet_unref(pkt);
+ return AVERROR(ENOMEM);
+ }
+ memcpy(buf, id, id_len);
+ }
+
+ if (settings_len > 0) {
+ buf = av_packet_new_side_data(pkt,
+ AV_PKT_DATA_WEBVTT_SETTINGS,
+ settings_len);
+ if (buf == NULL) {
+ av_packet_unref(pkt);
+ return AVERROR(ENOMEM);
+ }
+ memcpy(buf, settings, settings_len);
+ }
+
+ pkt->pts = in->pts;
+ pkt->duration = in->duration;
+ return 0;
+}
+
+// Return a NULL-terminated list of ASS event lines and have
+// the AVSubtitle display PTS and duration set to input
+// double variables.
+char **lavc_conv_decode(struct lavc_conv *priv, struct demux_packet *packet,
+ double *sub_pts, double *sub_duration)
+{
+ AVCodecContext *avctx = priv->avctx;
+ AVPacket *curr_pkt = priv->avpkt;
+ int ret, got_sub;
+ int num_cur = 0;
+
+ avsubtitle_free(&priv->cur);
+
+ mp_set_av_packet(priv->avpkt, packet, &avctx->time_base);
+ if (priv->avpkt->pts < 0)
+ priv->avpkt->pts = 0;
+
+ if (strcmp(priv->codec, "webvtt-webm") == 0) {
+ if (parse_webvtt(priv->avpkt, priv->avpkt_vtt) < 0) {
+ MP_ERR(priv, "Error parsing subtitle\n");
+ goto done;
+ }
+ curr_pkt = priv->avpkt_vtt;
+ }
+
+ ret = avcodec_decode_subtitle2(avctx, &priv->cur, &got_sub, curr_pkt);
+ if (ret < 0) {
+ MP_ERR(priv, "Error decoding subtitle\n");
+ } else if (got_sub) {
+ *sub_pts = packet->pts + mp_pts_from_av(priv->cur.start_display_time,
+ &avctx->time_base);
+ *sub_duration = priv->cur.end_display_time == UINT32_MAX ?
+ UINT32_MAX :
+ mp_pts_from_av(priv->cur.end_display_time -
+ priv->cur.start_display_time,
+ &avctx->time_base);
+
+ for (int i = 0; i < priv->cur.num_rects; i++) {
+ if (priv->cur.rects[i]->w > 0 && priv->cur.rects[i]->h > 0)
+ MP_WARN(priv, "Ignoring bitmap subtitle.\n");
+ char *ass_line = priv->cur.rects[i]->ass;
+ if (!ass_line)
+ continue;
+ MP_TARRAY_APPEND(priv, priv->cur_list, num_cur, ass_line);
+ }
+ }
+
+done:
+ av_packet_unref(priv->avpkt_vtt);
+ MP_TARRAY_APPEND(priv, priv->cur_list, num_cur, NULL);
+ return priv->cur_list;
+}
+
+void lavc_conv_reset(struct lavc_conv *priv)
+{
+ avcodec_flush_buffers(priv->avctx);
+}
+
+void lavc_conv_uninit(struct lavc_conv *priv)
+{
+ avsubtitle_free(&priv->cur);
+ avcodec_free_context(&priv->avctx);
+ mp_free_av_packet(&priv->avpkt);
+ mp_free_av_packet(&priv->avpkt_vtt);
+ talloc_free(priv);
+}
diff --git a/sub/meson.build b/sub/meson.build
new file mode 100644
index 0000000..867f218
--- /dev/null
+++ b/sub/meson.build
@@ -0,0 +1,6 @@
+osd_font = custom_target('osd_font.otf',
+ input: join_paths(source_root, 'sub', 'osd_font.otf'),
+ output: 'osd_font.otf.inc',
+ command: [file2string, '@INPUT@', '@OUTPUT@'],
+)
+sources += osd_font
diff --git a/sub/osd.c b/sub/osd.c
new file mode 100644
index 0000000..9d6926d
--- /dev/null
+++ b/sub/osd.c
@@ -0,0 +1,559 @@
+/*
+ * This file is part of mpv.
+ *
+ * mpv is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 2.1 of the License, or (at your option) any later version.
+ *
+ * mpv is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with mpv. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <assert.h>
+
+#include <libavutil/common.h>
+
+#include "common/common.h"
+
+#include "stream/stream.h"
+
+#include "osdep/timer.h"
+
+#include "mpv_talloc.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "common/global.h"
+#include "common/msg.h"
+#include "common/stats.h"
+#include "player/client.h"
+#include "player/command.h"
+#include "osd.h"
+#include "osd_state.h"
+#include "dec_sub.h"
+#include "img_convert.h"
+#include "draw_bmp.h"
+#include "video/mp_image.h"
+#include "video/mp_image_pool.h"
+
+#define OPT_BASE_STRUCT struct osd_style_opts
+static const m_option_t style_opts[] = {
+ {"font", OPT_STRING(font)},
+ {"font-size", OPT_FLOAT(font_size), M_RANGE(1, 9000)},
+ {"color", OPT_COLOR(color)},
+ {"border-color", OPT_COLOR(border_color)},
+ {"shadow-color", OPT_COLOR(shadow_color)},
+ {"back-color", OPT_COLOR(back_color)},
+ {"border-size", OPT_FLOAT(border_size)},
+ {"shadow-offset", OPT_FLOAT(shadow_offset)},
+ {"spacing", OPT_FLOAT(spacing), M_RANGE(-10, 10)},
+ {"margin-x", OPT_INT(margin_x), M_RANGE(0, 300)},
+ {"margin-y", OPT_INT(margin_y), M_RANGE(0, 600)},
+ {"align-x", OPT_CHOICE(align_x,
+ {"left", -1}, {"center", 0}, {"right", +1})},
+ {"align-y", OPT_CHOICE(align_y,
+ {"top", -1}, {"center", 0}, {"bottom", +1})},
+ {"blur", OPT_FLOAT(blur), M_RANGE(0, 20)},
+ {"bold", OPT_BOOL(bold)},
+ {"italic", OPT_BOOL(italic)},
+ {"justify", OPT_CHOICE(justify,
+ {"auto", 0}, {"left", 1}, {"center", 2}, {"right", 3})},
+ {"font-provider", OPT_CHOICE(font_provider,
+ {"auto", 0}, {"none", 1}, {"fontconfig", 2}), .flags = UPDATE_SUB_HARD},
+ {"fonts-dir", OPT_STRING(fonts_dir),
+ .flags = M_OPT_FILE | UPDATE_SUB_HARD},
+ {0}
+};
+
+const struct m_sub_options osd_style_conf = {
+ .opts = style_opts,
+ .size = sizeof(struct osd_style_opts),
+ .defaults = &(const struct osd_style_opts){
+ .font = "sans-serif",
+ .font_size = 55,
+ .color = {255, 255, 255, 255},
+ .border_color = {0, 0, 0, 255},
+ .shadow_color = {240, 240, 240, 128},
+ .border_size = 3,
+ .shadow_offset = 0,
+ .margin_x = 25,
+ .margin_y = 22,
+ .align_x = -1,
+ .align_y = -1,
+ },
+ .change_flags = UPDATE_OSD,
+};
+
+const struct m_sub_options sub_style_conf = {
+ .opts = style_opts,
+ .size = sizeof(struct osd_style_opts),
+ .defaults = &(const struct osd_style_opts){
+ .font = "sans-serif",
+ .font_size = 55,
+ .color = {255, 255, 255, 255},
+ .border_color = {0, 0, 0, 255},
+ .shadow_color = {240, 240, 240, 128},
+ .border_size = 3,
+ .shadow_offset = 0,
+ .margin_x = 25,
+ .margin_y = 22,
+ .align_x = 0,
+ .align_y = 1,
+ },
+ .change_flags = UPDATE_OSD,
+};
+
+bool osd_res_equals(struct mp_osd_res a, struct mp_osd_res b)
+{
+ return a.w == b.w && a.h == b.h && a.ml == b.ml && a.mt == b.mt
+ && a.mr == b.mr && a.mb == b.mb
+ && a.display_par == b.display_par;
+}
+
+struct osd_state *osd_create(struct mpv_global *global)
+{
+ assert(MAX_OSD_PARTS >= OSDTYPE_COUNT);
+
+ struct osd_state *osd = talloc_zero(NULL, struct osd_state);
+ *osd = (struct osd_state) {
+ .opts_cache = m_config_cache_alloc(osd, global, &mp_osd_render_sub_opts),
+ .global = global,
+ .log = mp_log_new(osd, global->log, "osd"),
+ .force_video_pts = MP_NOPTS_VALUE,
+ .stats = stats_ctx_create(osd, global, "osd"),
+ };
+ mp_mutex_init(&osd->lock);
+ osd->opts = osd->opts_cache->opts;
+
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct osd_object *obj = talloc(osd, struct osd_object);
+ *obj = (struct osd_object) {
+ .type = n,
+ .text = talloc_strdup(obj, ""),
+ .progbar_state = {.type = -1},
+ .vo_change_id = 1,
+ };
+ osd->objs[n] = obj;
+ }
+
+ osd->objs[OSDTYPE_SUB]->is_sub = true;
+ osd->objs[OSDTYPE_SUB2]->is_sub = true;
+
+ osd_init_backend(osd);
+ return osd;
+}
+
+void osd_free(struct osd_state *osd)
+{
+ if (!osd)
+ return;
+ osd_destroy_backend(osd);
+ talloc_free(osd->objs[OSDTYPE_EXTERNAL2]->external2);
+ mp_mutex_destroy(&osd->lock);
+ talloc_free(osd);
+}
+
+void osd_set_text(struct osd_state *osd, const char *text)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *osd_obj = osd->objs[OSDTYPE_OSD];
+ if (!text)
+ text = "";
+ if (strcmp(osd_obj->text, text) != 0) {
+ talloc_free(osd_obj->text);
+ osd_obj->text = talloc_strdup(osd_obj, text);
+ osd_obj->osd_changed = true;
+ osd->want_redraw_notification = true;
+ }
+ mp_mutex_unlock(&osd->lock);
+}
+
+void osd_set_sub(struct osd_state *osd, int index, struct dec_sub *dec_sub)
+{
+ mp_mutex_lock(&osd->lock);
+ if (index >= 0 && index < 2) {
+ struct osd_object *obj = osd->objs[OSDTYPE_SUB + index];
+ obj->sub = dec_sub;
+ obj->vo_change_id += 1;
+ }
+ osd->want_redraw_notification = true;
+ mp_mutex_unlock(&osd->lock);
+}
+
+bool osd_get_render_subs_in_filter(struct osd_state *osd)
+{
+ mp_mutex_lock(&osd->lock);
+ bool r = osd->render_subs_in_filter;
+ mp_mutex_unlock(&osd->lock);
+ return r;
+}
+
+void osd_set_render_subs_in_filter(struct osd_state *osd, bool s)
+{
+ mp_mutex_lock(&osd->lock);
+ if (osd->render_subs_in_filter != s) {
+ osd->render_subs_in_filter = s;
+
+ int change_id = 0;
+ for (int n = 0; n < MAX_OSD_PARTS; n++)
+ change_id = MPMAX(change_id, osd->objs[n]->vo_change_id);
+ for (int n = 0; n < MAX_OSD_PARTS; n++)
+ osd->objs[n]->vo_change_id = change_id + 1;
+ }
+ mp_mutex_unlock(&osd->lock);
+}
+
+void osd_set_force_video_pts(struct osd_state *osd, double video_pts)
+{
+ atomic_store(&osd->force_video_pts, video_pts);
+}
+
+double osd_get_force_video_pts(struct osd_state *osd)
+{
+ return atomic_load(&osd->force_video_pts);
+}
+
+void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *osd_obj = osd->objs[OSDTYPE_OSD];
+ osd_obj->progbar_state.type = s->type;
+ osd_obj->progbar_state.value = s->value;
+ osd_obj->progbar_state.num_stops = s->num_stops;
+ MP_TARRAY_GROW(osd_obj, osd_obj->progbar_state.stops, s->num_stops);
+ if (s->num_stops) {
+ memcpy(osd_obj->progbar_state.stops, s->stops,
+ sizeof(osd_obj->progbar_state.stops[0]) * s->num_stops);
+ }
+ osd_obj->osd_changed = true;
+ osd->want_redraw_notification = true;
+ mp_mutex_unlock(&osd->lock);
+}
+
+void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL2];
+ talloc_free(obj->external2);
+ obj->external2 = sub_bitmaps_copy(NULL, imgs);
+ obj->vo_change_id += 1;
+ osd->want_redraw_notification = true;
+ mp_mutex_unlock(&osd->lock);
+}
+
+static void check_obj_resize(struct osd_state *osd, struct mp_osd_res res,
+ struct osd_object *obj)
+{
+ if (!osd_res_equals(res, obj->vo_res)) {
+ obj->vo_res = res;
+ obj->osd_changed = true;
+ mp_client_broadcast_event_external(osd->global->client_api,
+ MP_EVENT_WIN_RESIZE, NULL);
+ }
+}
+
+// Optional. Can be called for faster reaction of OSD-generating scripts like
+// osc.lua. This can achieve that the resize happens first, so that the OSD is
+// generated at the correct resolution the first time the resized frame is
+// rendered. Since the OSD doesn't (and can't) wait for the script, this
+// increases the time in which the script can react, and also gets rid of the
+// unavoidable redraw delay (though it will still be racy).
+// Unnecessary for anything else.
+void osd_resize(struct osd_state *osd, struct mp_osd_res res)
+{
+ mp_mutex_lock(&osd->lock);
+ int types[] = {OSDTYPE_OSD, OSDTYPE_EXTERNAL, OSDTYPE_EXTERNAL2, -1};
+ for (int n = 0; types[n] >= 0; n++)
+ check_obj_resize(osd, res, osd->objs[types[n]]);
+ mp_mutex_unlock(&osd->lock);
+}
+
+static struct sub_bitmaps *render_object(struct osd_state *osd,
+ struct osd_object *obj,
+ struct mp_osd_res osdres, double video_pts,
+ const bool sub_formats[SUBBITMAP_COUNT])
+{
+ int format = SUBBITMAP_LIBASS;
+ if (!sub_formats[format] || osd->opts->force_rgba_osd)
+ format = SUBBITMAP_BGRA;
+
+ struct sub_bitmaps *res = NULL;
+
+ check_obj_resize(osd, osdres, obj);
+
+ if (obj->type == OSDTYPE_SUB) {
+ if (obj->sub && sub_is_primary_visible(obj->sub))
+ res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts);
+ } else if (obj->type == OSDTYPE_SUB2) {
+ if (obj->sub && sub_is_secondary_visible(obj->sub))
+ res = sub_get_bitmaps(obj->sub, obj->vo_res, format, video_pts);
+ } else if (obj->type == OSDTYPE_EXTERNAL2) {
+ if (obj->external2 && obj->external2->format) {
+ res = sub_bitmaps_copy(NULL, obj->external2); // need to be owner
+ obj->external2->change_id = 0;
+ }
+ } else {
+ res = osd_object_get_bitmaps(osd, obj, format);
+ }
+
+ if (obj->vo_had_output != !!res) {
+ obj->vo_had_output = !!res;
+ obj->vo_change_id += 1;
+ }
+
+ if (res) {
+ obj->vo_change_id += res->change_id;
+
+ res->render_index = obj->type;
+ res->change_id = obj->vo_change_id;
+ }
+
+ return res;
+}
+
+// Render OSD to a list of bitmap and return it. The returned object is
+// refcounted. Typically you should hold it only for a short time, and then
+// release it.
+// draw_flags is a bit field of OSD_DRAW_* constants
+struct sub_bitmap_list *osd_render(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ const bool formats[SUBBITMAP_COUNT])
+{
+ mp_mutex_lock(&osd->lock);
+
+ struct sub_bitmap_list *list = talloc_zero(NULL, struct sub_bitmap_list);
+ list->change_id = 1;
+ list->w = res.w;
+ list->h = res.h;
+
+ double force_video_pts = atomic_load(&osd->force_video_pts);
+ if (force_video_pts != MP_NOPTS_VALUE)
+ video_pts = force_video_pts;
+
+ if (draw_flags & OSD_DRAW_SUB_FILTER)
+ draw_flags |= OSD_DRAW_SUB_ONLY;
+
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct osd_object *obj = osd->objs[n];
+
+ // Object is drawn into the video frame itself; don't draw twice
+ if (osd->render_subs_in_filter && obj->is_sub &&
+ !(draw_flags & OSD_DRAW_SUB_FILTER))
+ continue;
+ if ((draw_flags & OSD_DRAW_SUB_ONLY) && !obj->is_sub)
+ continue;
+ if ((draw_flags & OSD_DRAW_OSD_ONLY) && obj->is_sub)
+ continue;
+
+ char *stat_type_render = obj->is_sub ? "sub-render" : "osd-render";
+ stats_time_start(osd->stats, stat_type_render);
+
+ struct sub_bitmaps *imgs =
+ render_object(osd, obj, res, video_pts, formats);
+
+ stats_time_end(osd->stats, stat_type_render);
+
+ if (imgs && imgs->num_parts > 0) {
+ if (formats[imgs->format]) {
+ talloc_steal(list, imgs);
+ MP_TARRAY_APPEND(list, list->items, list->num_items, imgs);
+ imgs = NULL;
+ } else {
+ MP_ERR(osd, "Can't render OSD part %d (format %d).\n",
+ obj->type, imgs->format);
+ }
+ }
+
+ list->change_id += obj->vo_change_id;
+
+ talloc_free(imgs);
+ }
+
+ // If this is called with OSD_DRAW_SUB_ONLY or OSD_DRAW_OSD_ONLY set, assume
+ // it will always draw the complete OSD by doing multiple osd_draw() calls.
+ // OSD_DRAW_SUB_FILTER on the other hand is an evil special-case, and we
+ // must not reset the flag when it happens.
+ if (!(draw_flags & OSD_DRAW_SUB_FILTER))
+ osd->want_redraw_notification = false;
+
+ mp_mutex_unlock(&osd->lock);
+ return list;
+}
+
+// Warning: this function should be considered legacy. Use osd_render() instead.
+void osd_draw(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ const bool formats[SUBBITMAP_COUNT],
+ void (*cb)(void *ctx, struct sub_bitmaps *imgs), void *cb_ctx)
+{
+ struct sub_bitmap_list *list =
+ osd_render(osd, res, video_pts, draw_flags, formats);
+
+ stats_time_start(osd->stats, "draw");
+
+ for (int n = 0; n < list->num_items; n++)
+ cb(cb_ctx, list->items[n]);
+
+ stats_time_end(osd->stats, "draw");
+
+ talloc_free(list);
+}
+
+// Calls mp_image_make_writeable() on the dest image if something is drawn.
+// draw_flags as in osd_render().
+void osd_draw_on_image(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags, struct mp_image *dest)
+{
+ osd_draw_on_image_p(osd, res, video_pts, draw_flags, NULL, dest);
+}
+
+// Like osd_draw_on_image(), but if dest needs to be copied to make it
+// writeable, allocate images from the given pool. (This is a minor
+// optimization to reduce "real" image sized memory allocations.)
+void osd_draw_on_image_p(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ struct mp_image_pool *pool, struct mp_image *dest)
+{
+ struct sub_bitmap_list *list =
+ osd_render(osd, res, video_pts, draw_flags, mp_draw_sub_formats);
+
+ if (!list->num_items) {
+ talloc_free(list);
+ return;
+ }
+
+ if (!mp_image_pool_make_writeable(pool, dest))
+ return; // on OOM, skip
+
+ // Need to lock for the dumb osd->draw_cache thing.
+ mp_mutex_lock(&osd->lock);
+
+ if (!osd->draw_cache)
+ osd->draw_cache = mp_draw_sub_alloc(osd, osd->global);
+
+ stats_time_start(osd->stats, "draw-bmp");
+
+ if (!mp_draw_sub_bitmaps(osd->draw_cache, dest, list))
+ MP_WARN(osd, "Failed rendering OSD.\n");
+ talloc_steal(osd, osd->draw_cache);
+
+ stats_time_end(osd->stats, "draw-bmp");
+
+ mp_mutex_unlock(&osd->lock);
+
+ talloc_free(list);
+}
+
+// Setup the OSD resolution to render into an image with the given parameters.
+// The interesting part about this is that OSD has to compensate the aspect
+// ratio if the image does not have a 1:1 pixel aspect ratio.
+struct mp_osd_res osd_res_from_image_params(const struct mp_image_params *p)
+{
+ return (struct mp_osd_res) {
+ .w = p->w,
+ .h = p->h,
+ .display_par = p->p_h / (double)p->p_w,
+ };
+}
+
+// Typically called to react to OSD style changes.
+void osd_changed(struct osd_state *osd)
+{
+ mp_mutex_lock(&osd->lock);
+ osd->objs[OSDTYPE_OSD]->osd_changed = true;
+ osd->want_redraw_notification = true;
+ // Done here for a lack of a better place.
+ m_config_cache_update(osd->opts_cache);
+ mp_mutex_unlock(&osd->lock);
+}
+
+bool osd_query_and_reset_want_redraw(struct osd_state *osd)
+{
+ mp_mutex_lock(&osd->lock);
+ bool r = osd->want_redraw_notification;
+ osd->want_redraw_notification = false;
+ mp_mutex_unlock(&osd->lock);
+ return r;
+}
+
+struct mp_osd_res osd_get_vo_res(struct osd_state *osd)
+{
+ mp_mutex_lock(&osd->lock);
+ // Any OSDTYPE is fine; but it mustn't be a subtitle one (can have lower res.)
+ struct mp_osd_res res = osd->objs[OSDTYPE_OSD]->vo_res;
+ mp_mutex_unlock(&osd->lock);
+ return res;
+}
+
+// Position the subbitmaps in imgs on the screen. Basically, this fits the
+// subtitle canvas (of size frame_w x frame_h) onto the screen, such that it
+// fills the whole video area (especially if the video is magnified, e.g. on
+// fullscreen). If compensate_par is >0, adjust the way the subtitles are
+// "stretched" on the screen, and letter-box the result. If compensate_par
+// is <0, strictly letter-box the subtitles. If it is 0, stretch them.
+void osd_rescale_bitmaps(struct sub_bitmaps *imgs, int frame_w, int frame_h,
+ struct mp_osd_res res, double compensate_par)
+{
+ int vidw = res.w - res.ml - res.mr;
+ int vidh = res.h - res.mt - res.mb;
+ double xscale = (double)vidw / frame_w;
+ double yscale = (double)vidh / frame_h;
+ if (compensate_par < 0) {
+ assert(res.display_par);
+ compensate_par = xscale / yscale / res.display_par;
+ }
+ if (compensate_par > 0)
+ xscale /= compensate_par;
+ int cx = vidw / 2 - (int)(frame_w * xscale) / 2;
+ int cy = vidh / 2 - (int)(frame_h * yscale) / 2;
+ for (int i = 0; i < imgs->num_parts; i++) {
+ struct sub_bitmap *bi = &imgs->parts[i];
+ bi->x = (int)(bi->x * xscale) + cx + res.ml;
+ bi->y = (int)(bi->y * yscale) + cy + res.mt;
+ bi->dw = (int)(bi->w * xscale + 0.5);
+ bi->dh = (int)(bi->h * yscale + 0.5);
+ }
+}
+
+// Copy *in and return a new allocation of it. Free with talloc_free(). This
+// will contain a refcounted copy of the image data.
+//
+// in->packed must be set and must be a refcounted image, unless there is no
+// data (num_parts==0).
+//
+// p_cache: if not NULL, then this points to a struct sub_bitmap_copy_cache*
+// variable. The function may set this to an allocation and may later
+// read it. You have to free it with talloc_free() when done.
+// in: valid struct, or NULL (in this case it also returns NULL)
+// returns: new copy, or NULL if there was no data in the input
+struct sub_bitmaps *sub_bitmaps_copy(struct sub_bitmap_copy_cache **p_cache,
+ struct sub_bitmaps *in)
+{
+ if (!in || !in->num_parts)
+ return NULL;
+
+ struct sub_bitmaps *res = talloc(NULL, struct sub_bitmaps);
+ *res = *in;
+
+ // Note: the p_cache thing is a lie and unused.
+
+ // The bitmaps being refcounted is essential for performance, and for
+ // not invalidating in->parts[*].bitmap pointers.
+ assert(in->packed && in->packed->bufs[0]);
+
+ res->packed = mp_image_new_ref(res->packed);
+ talloc_steal(res, res->packed);
+
+ res->parts = NULL;
+ MP_RESIZE_ARRAY(res, res->parts, res->num_parts);
+ memcpy(res->parts, in->parts, sizeof(res->parts[0]) * res->num_parts);
+
+ return res;
+}
diff --git a/sub/osd.h b/sub/osd.h
new file mode 100644
index 0000000..39a88ea
--- /dev/null
+++ b/sub/osd.h
@@ -0,0 +1,247 @@
+/*
+ * 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_SUB_H
+#define MPLAYER_SUB_H
+
+#include <stddef.h>
+#include <stdbool.h>
+#include <stdint.h>
+
+#include "options/m_option.h"
+
+// NOTE: VOs must support at least SUBBITMAP_BGRA.
+enum sub_bitmap_format {
+ SUBBITMAP_EMPTY = 0,// no bitmaps; always has num_parts==0
+ SUBBITMAP_LIBASS, // A8, with a per-surface blend color (libass.color)
+ SUBBITMAP_BGRA, // IMGFMT_BGRA (MSB=A, LSB=B), scaled, premultiplied alpha
+
+ SUBBITMAP_COUNT
+};
+
+struct sub_bitmap {
+ void *bitmap;
+ int stride;
+ // Note: not clipped, going outside the screen area is allowed
+ // (except for SUBBITMAP_LIBASS, which is always clipped)
+ int w, h;
+ int x, y;
+ int dw, dh;
+
+ // If the containing struct sub_bitmaps has the packed field set, then this
+ // is the position within the source. (Strictly speaking this is redundant
+ // with the bitmap pointer.)
+ int src_x, src_y;
+
+ struct {
+ uint32_t color;
+ } libass;
+};
+
+struct sub_bitmaps {
+ // For VO cache state (limited by MAX_OSD_PARTS)
+ int render_index;
+
+ enum sub_bitmap_format format;
+
+ struct sub_bitmap *parts;
+ int num_parts;
+
+ // Packed representation of the bitmap data. If non-NULL, then the
+ // parts[].bitmap pointer points into the image data here (and stride will
+ // correspond to packed->stride[0]).
+ // SUBBITMAP_BGRA: IMGFMT_BGRA (exact match)
+ // SUBBITMAP_LIBASS: IMGFMT_Y8 (not the same, but compatible layout)
+ // Other formats have this set to NULL.
+ struct mp_image *packed;
+
+ // Bounding box for the packed image. All parts will be within the bounding
+ // box. (The origin of the box is at (0,0).)
+ int packed_w, packed_h;
+
+ int change_id; // Incremented on each change (0 is never used)
+};
+
+struct sub_bitmap_list {
+ // Combined change_id - of any of the existing items change (even if they
+ // e.g. go away and are removed from items[]), this is incremented.
+ int64_t change_id;
+
+ // Bounding box for rendering. It's notable that SUBBITMAP_LIBASS images are
+ // always within these bounds, while SUBBITMAP_BGRA is not necessarily.
+ int w, h;
+
+ // Sorted by sub_bitmaps.render_index. Unused parts are not in the array,
+ // and you cannot index items[] with render_index.
+ struct sub_bitmaps **items;
+ int num_items;
+};
+
+struct sub_bitmap_copy_cache;
+struct sub_bitmaps *sub_bitmaps_copy(struct sub_bitmap_copy_cache **cache,
+ struct sub_bitmaps *in);
+
+struct mp_osd_res {
+ int w, h; // screen dimensions, including black borders
+ int mt, mb, ml, mr; // borders (top, bottom, left, right)
+ double display_par;
+};
+
+bool osd_res_equals(struct mp_osd_res a, struct mp_osd_res b);
+
+// 0 <= sub_bitmaps.render_index < MAX_OSD_PARTS
+#define MAX_OSD_PARTS 5
+
+// Start of OSD symbols in osd_font.pfb
+#define OSD_CODEPOINTS 0xE000
+
+// OSD symbols. osd_font.pfb has them starting from codepoint OSD_CODEPOINTS.
+// Symbols with a value >= 32 are normal unicode codepoints.
+enum mp_osd_font_codepoints {
+ OSD_PLAY = 0x01,
+ OSD_PAUSE = 0x02,
+ OSD_STOP = 0x03,
+ OSD_REW = 0x04,
+ OSD_FFW = 0x05,
+ OSD_CLOCK = 0x06,
+ OSD_CONTRAST = 0x07,
+ OSD_SATURATION = 0x08,
+ OSD_VOLUME = 0x09,
+ OSD_BRIGHTNESS = 0x0A,
+ OSD_HUE = 0x0B,
+ OSD_BALANCE = 0x0C,
+ OSD_PANSCAN = 0x50,
+
+ OSD_PB_START = 0x10,
+ OSD_PB_0 = 0x11,
+ OSD_PB_END = 0x12,
+ OSD_PB_1 = 0x13,
+};
+
+
+// Never valid UTF-8, so we expect it's free for use.
+// Specially interpreted by osd_libass.c, in order to allow/escape ASS tags.
+#define OSD_ASS_0 "\xFD"
+#define OSD_ASS_1 "\xFE"
+
+struct osd_style_opts {
+ char *font;
+ float font_size;
+ struct m_color color;
+ struct m_color border_color;
+ struct m_color shadow_color;
+ struct m_color back_color;
+ float border_size;
+ float shadow_offset;
+ float spacing;
+ int margin_x;
+ int margin_y;
+ int align_x;
+ int align_y;
+ float blur;
+ bool bold;
+ bool italic;
+ int justify;
+ int font_provider;
+ char *fonts_dir;
+};
+
+extern const struct m_sub_options osd_style_conf;
+extern const struct m_sub_options sub_style_conf;
+
+struct osd_state;
+struct osd_object;
+struct mpv_global;
+struct dec_sub;
+
+struct osd_state *osd_create(struct mpv_global *global);
+void osd_changed(struct osd_state *osd);
+void osd_free(struct osd_state *osd);
+
+bool osd_query_and_reset_want_redraw(struct osd_state *osd);
+
+void osd_set_text(struct osd_state *osd, const char *text);
+void osd_set_sub(struct osd_state *osd, int index, struct dec_sub *dec_sub);
+
+bool osd_get_render_subs_in_filter(struct osd_state *osd);
+void osd_set_render_subs_in_filter(struct osd_state *osd, bool s);
+void osd_set_force_video_pts(struct osd_state *osd, double video_pts);
+double osd_get_force_video_pts(struct osd_state *osd);
+
+struct osd_progbar_state {
+ int type; // <0: disabled, 1-255: symbol, else: no symbol
+ float value; // range 0.0-1.0
+ float *stops; // used for chapter indicators (0.0-1.0 each)
+ int num_stops;
+};
+void osd_set_progbar(struct osd_state *osd, struct osd_progbar_state *s);
+
+void osd_set_external2(struct osd_state *osd, struct sub_bitmaps *imgs);
+
+enum mp_osd_draw_flags {
+ OSD_DRAW_SUB_FILTER = (1 << 0),
+ OSD_DRAW_SUB_ONLY = (1 << 1),
+ OSD_DRAW_OSD_ONLY = (1 << 2),
+};
+
+void osd_draw(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ const bool formats[SUBBITMAP_COUNT],
+ void (*cb)(void *ctx, struct sub_bitmaps *imgs), void *cb_ctx);
+
+struct sub_bitmap_list *osd_render(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ const bool formats[SUBBITMAP_COUNT]);
+
+struct mp_image;
+void osd_draw_on_image(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags, struct mp_image *dest);
+
+struct mp_image_pool;
+void osd_draw_on_image_p(struct osd_state *osd, struct mp_osd_res res,
+ double video_pts, int draw_flags,
+ struct mp_image_pool *pool, struct mp_image *dest);
+
+void osd_resize(struct osd_state *osd, struct mp_osd_res res);
+
+struct mp_image_params;
+struct mp_osd_res osd_res_from_image_params(const struct mp_image_params *p);
+
+struct mp_osd_res osd_get_vo_res(struct osd_state *osd);
+
+void osd_rescale_bitmaps(struct sub_bitmaps *imgs, int frame_w, int frame_h,
+ struct mp_osd_res res, double compensate_par);
+
+struct osd_external_ass {
+ void *owner; // unique pointer (NULL is also allowed)
+ int64_t id;
+ int format;
+ char *data;
+ int res_x, res_y;
+ int z;
+ bool hidden;
+
+ double *out_rc; // hack to pass boundary rect, [x0, y0, x1, y1]
+};
+
+// defined in osd_libass.c and osd_dummy.c
+void osd_set_external(struct osd_state *osd, struct osd_external_ass *ov);
+void osd_set_external_remove_owner(struct osd_state *osd, void *owner);
+void osd_get_text_size(struct osd_state *osd, int *out_screen_h, int *out_font_h);
+void osd_get_function_sym(char *buffer, size_t buffer_size, int osd_function);
+
+#endif /* MPLAYER_SUB_H */
diff --git a/sub/osd_font.otf b/sub/osd_font.otf
new file mode 100644
index 0000000..70b9b21
--- /dev/null
+++ b/sub/osd_font.otf
Binary files differ
diff --git a/sub/osd_libass.c b/sub/osd_libass.c
new file mode 100644
index 0000000..a3b19c9
--- /dev/null
+++ b/sub/osd_libass.c
@@ -0,0 +1,691 @@
+/*
+ * 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <assert.h>
+
+#include "mpv_talloc.h"
+#include "misc/bstr.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "osd.h"
+#include "osd_state.h"
+
+static const char osd_font_pfb[] =
+#include "sub/osd_font.otf.inc"
+;
+
+#include "sub/ass_mp.h"
+#include "options/options.h"
+
+
+#define ASS_USE_OSD_FONT "{\\fnmpv-osd-symbols}"
+
+static void append_ass(struct ass_state *ass, struct mp_osd_res *res,
+ ASS_Image **img_list, bool *changed);
+
+void osd_init_backend(struct osd_state *osd)
+{
+}
+
+static void create_ass_renderer(struct osd_state *osd, struct ass_state *ass)
+{
+ if (ass->render)
+ return;
+
+ ass->log = mp_log_new(NULL, osd->log, "libass");
+ ass->library = mp_ass_init(osd->global, osd->opts->osd_style, ass->log);
+ ass_add_font(ass->library, "mpv-osd-symbols", (void *)osd_font_pfb,
+ sizeof(osd_font_pfb) - 1);
+
+ ass->render = ass_renderer_init(ass->library);
+ if (!ass->render)
+ abort();
+
+ mp_ass_configure_fonts(ass->render, osd->opts->osd_style,
+ osd->global, ass->log);
+ ass_set_pixel_aspect(ass->render, 1.0);
+}
+
+static void destroy_ass_renderer(struct ass_state *ass)
+{
+ if (ass->track)
+ ass_free_track(ass->track);
+ ass->track = NULL;
+ if (ass->render)
+ ass_renderer_done(ass->render);
+ ass->render = NULL;
+ if (ass->library)
+ ass_library_done(ass->library);
+ ass->library = NULL;
+ talloc_free(ass->log);
+ ass->log = NULL;
+}
+
+static void destroy_external(struct osd_external *ext)
+{
+ destroy_ass_renderer(&ext->ass);
+ talloc_free(ext);
+}
+
+void osd_destroy_backend(struct osd_state *osd)
+{
+ for (int n = 0; n < MAX_OSD_PARTS; n++) {
+ struct osd_object *obj = osd->objs[n];
+ destroy_ass_renderer(&obj->ass);
+ for (int i = 0; i < obj->num_externals; i++)
+ destroy_external(obj->externals[i]);
+ obj->num_externals = 0;
+ }
+}
+
+static void update_playres(struct ass_state *ass, struct mp_osd_res *vo_res)
+{
+ ASS_Track *track = ass->track;
+ int old_res_x = track->PlayResX;
+ int old_res_y = track->PlayResY;
+
+ ass->vo_res = *vo_res;
+
+ double aspect = 1.0 * vo_res->w / MPMAX(vo_res->h, 1);
+ if (vo_res->display_par > 0)
+ aspect = aspect / vo_res->display_par;
+
+ track->PlayResY = ass->res_y ? ass->res_y : MP_ASS_FONT_PLAYRESY;
+ track->PlayResX = ass->res_x ? ass->res_x : track->PlayResY * aspect;
+
+ // Force libass to clear its internal cache - it doesn't check for
+ // PlayRes changes itself.
+ if (old_res_x != track->PlayResX || old_res_y != track->PlayResY)
+ ass_set_frame_size(ass->render, 1, 1);
+}
+
+static void create_ass_track(struct osd_state *osd, struct osd_object *obj,
+ struct ass_state *ass)
+{
+ create_ass_renderer(osd, ass);
+
+ ASS_Track *track = ass->track;
+ if (!track)
+ track = ass->track = ass_new_track(ass->library);
+
+ track->track_type = TRACK_TYPE_ASS;
+ track->Timer = 100.;
+ track->WrapStyle = 1; // end-of-line wrapping instead of smart wrapping
+ track->Kerning = true;
+ track->ScaledBorderAndShadow = true;
+#if LIBASS_VERSION >= 0x01600010
+ ass_track_set_feature(track, ASS_FEATURE_WRAP_UNICODE, 1);
+#endif
+ update_playres(ass, &obj->vo_res);
+}
+
+static int find_style(ASS_Track *track, const char *name, int def)
+{
+ for (int n = 0; n < track->n_styles; n++) {
+ if (track->styles[n].Name && strcmp(track->styles[n].Name, name) == 0)
+ return n;
+ }
+ return def;
+}
+
+// Find a given style, or add it if it's missing.
+static ASS_Style *get_style(struct ass_state *ass, char *name)
+{
+ ASS_Track *track = ass->track;
+ if (!track)
+ return NULL;
+
+ int sid = find_style(track, name, -1);
+ if (sid >= 0)
+ return &track->styles[sid];
+
+ sid = ass_alloc_style(track);
+ ASS_Style *style = &track->styles[sid];
+ style->Name = strdup(name);
+ // Set to neutral base direction, as opposed to VSFilter LTR default
+ style->Encoding = -1;
+ return style;
+}
+
+static ASS_Event *add_osd_ass_event(ASS_Track *track, const char *style,
+ const char *text)
+{
+ int n = ass_alloc_event(track);
+ ASS_Event *event = track->events + n;
+ event->Start = 0;
+ event->Duration = 100;
+ event->Style = find_style(track, style, 0);
+ event->ReadOrder = n;
+ assert(event->Text == NULL);
+ if (text)
+ event->Text = strdup(text);
+ return event;
+}
+
+static void clear_ass(struct ass_state *ass)
+{
+ if (ass->track)
+ ass_flush_events(ass->track);
+}
+
+void osd_get_function_sym(char *buffer, size_t buffer_size, int osd_function)
+{
+ // 0xFF is never valid UTF-8, so we can use it to escape OSD symbols.
+ // (Same trick as OSD_ASS_0/OSD_ASS_1.)
+ snprintf(buffer, buffer_size, "\xFF%c", osd_function);
+}
+
+static void mangle_ass(bstr *dst, const char *in)
+{
+ const char *start = in;
+ bool escape_ass = true;
+ while (*in) {
+ // As used by osd_get_function_sym().
+ if (in[0] == '\xFF' && in[1]) {
+ bstr_xappend(NULL, dst, bstr0(ASS_USE_OSD_FONT));
+ mp_append_utf8_bstr(NULL, dst, OSD_CODEPOINTS + in[1]);
+ bstr_xappend(NULL, dst, bstr0("{\\r}"));
+ in += 2;
+ continue;
+ }
+ if (*in == OSD_ASS_0[0] || *in == OSD_ASS_1[0]) {
+ escape_ass = *in == OSD_ASS_1[0];
+ in += 1;
+ continue;
+ }
+ if (escape_ass && *in == '{')
+ bstr_xappend(NULL, dst, bstr0("\\"));
+ // Libass will strip leading whitespace
+ if (in[0] == ' ' && (in == start || in[-1] == '\n')) {
+ bstr_xappend(NULL, dst, bstr0("\\h"));
+ in += 1;
+ continue;
+ }
+ bstr_xappend(NULL, dst, (bstr){(char *)in, 1});
+ // Break ASS escapes with U+2060 WORD JOINER
+ if (escape_ass && *in == '\\')
+ mp_append_utf8_bstr(NULL, dst, 0x2060);
+ in++;
+ }
+}
+
+static ASS_Event *add_osd_ass_event_escaped(ASS_Track *track, const char *style,
+ const char *text)
+{
+ bstr buf = {0};
+ mangle_ass(&buf, text);
+ ASS_Event *e = add_osd_ass_event(track, style, buf.start);
+ talloc_free(buf.start);
+ return e;
+}
+
+static ASS_Style *prepare_osd_ass(struct osd_state *osd, struct osd_object *obj)
+{
+ struct mp_osd_render_opts *opts = osd->opts;
+
+ create_ass_track(osd, obj, &obj->ass);
+
+ struct osd_style_opts font = *opts->osd_style;
+ font.font_size *= opts->osd_scale;
+
+ double playresy = obj->ass.track->PlayResY;
+ // Compensate for libass and mp_ass_set_style scaling the font etc.
+ if (!opts->osd_scale_by_window)
+ playresy *= 720.0 / obj->vo_res.h;
+
+ ASS_Style *style = get_style(&obj->ass, "OSD");
+ mp_ass_set_style(style, playresy, &font);
+ return style;
+}
+
+static void update_osd_text(struct osd_state *osd, struct osd_object *obj)
+{
+
+ if (!obj->text[0])
+ return;
+
+ prepare_osd_ass(osd, obj);
+ add_osd_ass_event_escaped(obj->ass.track, "OSD", obj->text);
+}
+
+void osd_get_text_size(struct osd_state *osd, int *out_screen_h, int *out_font_h)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *obj = osd->objs[OSDTYPE_OSD];
+ ASS_Style *style = prepare_osd_ass(osd, obj);
+ *out_screen_h = obj->ass.track->PlayResY - style->MarginV;
+ *out_font_h = style->FontSize;
+ mp_mutex_unlock(&osd->lock);
+}
+
+// align: -1 .. +1
+// frame: size of the containing area
+// obj: size of the object that should be positioned inside the area
+// margin: min. distance from object to frame (as long as -1 <= align <= +1)
+static float get_align(float align, float frame, float obj, float margin)
+{
+ frame -= margin * 2;
+ return margin + frame / 2 - obj / 2 + (frame - obj) / 2 * align;
+}
+
+struct ass_draw {
+ int scale;
+ char *text;
+};
+
+static void ass_draw_start(struct ass_draw *d)
+{
+ d->scale = MPMAX(d->scale, 1);
+ d->text = talloc_asprintf_append(d->text, "{\\p%d}", d->scale);
+}
+
+static void ass_draw_stop(struct ass_draw *d)
+{
+ d->text = talloc_strdup_append(d->text, "{\\p0}");
+}
+
+static void ass_draw_c(struct ass_draw *d, float x, float y)
+{
+ int ix = round(x * (1 << (d->scale - 1)));
+ int iy = round(y * (1 << (d->scale - 1)));
+ d->text = talloc_asprintf_append(d->text, " %d %d", ix, iy);
+}
+
+static void ass_draw_append(struct ass_draw *d, const char *t)
+{
+ d->text = talloc_strdup_append(d->text, t);
+}
+
+static void ass_draw_move_to(struct ass_draw *d, float x, float y)
+{
+ ass_draw_append(d, " m");
+ ass_draw_c(d, x, y);
+}
+
+static void ass_draw_line_to(struct ass_draw *d, float x, float y)
+{
+ ass_draw_append(d, " l");
+ ass_draw_c(d, x, y);
+}
+
+static void ass_draw_rect_ccw(struct ass_draw *d, float x0, float y0,
+ float x1, float y1)
+{
+ ass_draw_move_to(d, x0, y0);
+ ass_draw_line_to(d, x0, y1);
+ ass_draw_line_to(d, x1, y1);
+ ass_draw_line_to(d, x1, y0);
+}
+
+static void ass_draw_rect_cw(struct ass_draw *d, float x0, float y0,
+ float x1, float y1)
+{
+ ass_draw_move_to(d, x0, y0);
+ ass_draw_line_to(d, x1, y0);
+ ass_draw_line_to(d, x1, y1);
+ ass_draw_line_to(d, x0, y1);
+}
+
+static void ass_draw_reset(struct ass_draw *d)
+{
+ talloc_free(d->text);
+ d->text = NULL;
+}
+
+static void get_osd_bar_box(struct osd_state *osd, struct osd_object *obj,
+ float *o_x, float *o_y, float *o_w, float *o_h,
+ float *o_border)
+{
+ struct mp_osd_render_opts *opts = osd->opts;
+
+ create_ass_track(osd, obj, &obj->ass);
+ ASS_Track *track = obj->ass.track;
+
+ ASS_Style *style = get_style(&obj->ass, "progbar");
+ if (!style) {
+ *o_x = *o_y = *o_w = *o_h = *o_border = 0;
+ return;
+ }
+
+ mp_ass_set_style(style, track->PlayResY, opts->osd_style);
+
+ if (osd->opts->osd_style->back_color.a) {
+ // override the default osd opaque-box into plain outline. Otherwise
+ // the opaque box is not aligned with the bar (even without shadow),
+ // and each bar ass event gets its own opaque box - breaking the bar.
+ style->BackColour = MP_ASS_COLOR(opts->osd_style->shadow_color);
+ style->BorderStyle = 1; // outline
+ }
+
+ *o_w = track->PlayResX * (opts->osd_bar_w / 100.0);
+ *o_h = track->PlayResY * (opts->osd_bar_h / 100.0);
+
+ float base_size = 0.03125;
+ style->Outline *= *o_h / track->PlayResY / base_size;
+ // So that the chapter marks have space between them
+ style->Outline = MPMIN(style->Outline, *o_h / 5.2);
+ // So that the border is not 0
+ style->Outline = MPMAX(style->Outline, *o_h / 32.0);
+ // Rendering with shadow is broken (because there's more than one shape)
+ style->Shadow = 0;
+
+ style->Alignment = 5;
+
+ *o_border = style->Outline;
+
+ *o_x = get_align(opts->osd_bar_align_x, track->PlayResX, *o_w, *o_border);
+ *o_y = get_align(opts->osd_bar_align_y, track->PlayResY, *o_h, *o_border);
+}
+
+static void update_progbar(struct osd_state *osd, struct osd_object *obj)
+{
+ if (obj->progbar_state.type < 0)
+ return;
+
+ float px, py, width, height, border;
+ get_osd_bar_box(osd, obj, &px, &py, &width, &height, &border);
+
+ ASS_Track *track = obj->ass.track;
+
+ float sx = px - border * 2 - height / 4; // includes additional spacing
+ float sy = py + height / 2;
+
+ bstr buf = bstr0(talloc_asprintf(NULL, "{\\an6\\pos(%f,%f)}", sx, sy));
+
+ if (obj->progbar_state.type == 0 || obj->progbar_state.type >= 256) {
+ // no sym
+ } else if (obj->progbar_state.type >= 32) {
+ mp_append_utf8_bstr(NULL, &buf, obj->progbar_state.type);
+ } else {
+ bstr_xappend(NULL, &buf, bstr0(ASS_USE_OSD_FONT));
+ mp_append_utf8_bstr(NULL, &buf, OSD_CODEPOINTS + obj->progbar_state.type);
+ bstr_xappend(NULL, &buf, bstr0("{\\r}"));
+ }
+
+ add_osd_ass_event(track, "progbar", buf.start);
+ talloc_free(buf.start);
+
+ struct ass_draw *d = &(struct ass_draw) { .scale = 4 };
+
+ if (osd->opts->osd_style->back_color.a) {
+ // the bar style always ignores the --osd-back-color config - it messes
+ // up the bar. draw an artificial box at the original back color.
+ struct m_color bc = osd->opts->osd_style->back_color;
+ d->text = talloc_asprintf_append(d->text,
+ "{\\pos(%f,%f)\\bord0\\1a&H%02X\\1c&H%02X%02X%02X&}",
+ px, py, 255 - bc.a, (int)bc.b, (int)bc.g, (int)bc.r);
+
+ ass_draw_start(d);
+ ass_draw_rect_cw(d, -border, -border, width + border, height + border);
+ ass_draw_stop(d);
+ add_osd_ass_event(track, "progbar", d->text);
+ ass_draw_reset(d);
+ }
+
+ // filled area
+ d->text = talloc_asprintf_append(d->text, "{\\bord0\\pos(%f,%f)}", px, py);
+ ass_draw_start(d);
+ float pos = obj->progbar_state.value * width - border / 2;
+ ass_draw_rect_cw(d, 0, 0, pos, height);
+ ass_draw_stop(d);
+ add_osd_ass_event(track, "progbar", d->text);
+ ass_draw_reset(d);
+
+ // position marker
+ d->text = talloc_asprintf_append(d->text, "{\\bord%f\\pos(%f,%f)}",
+ border / 2, px, py);
+ ass_draw_start(d);
+ ass_draw_move_to(d, pos + border / 2, 0);
+ ass_draw_line_to(d, pos + border / 2, height);
+ ass_draw_stop(d);
+ add_osd_ass_event(track, "progbar", d->text);
+ ass_draw_reset(d);
+
+ d->text = talloc_asprintf_append(d->text, "{\\pos(%f,%f)}", px, py);
+ ass_draw_start(d);
+
+ // the box
+ ass_draw_rect_cw(d, -border, -border, width + border, height + border);
+
+ // the "hole"
+ ass_draw_rect_ccw(d, 0, 0, width, height);
+
+ // chapter marks
+ for (int n = 0; n < obj->progbar_state.num_stops; n++) {
+ float s = obj->progbar_state.stops[n] * width;
+ float dent = border * 1.3;
+
+ if (s > dent && s < width - dent) {
+ ass_draw_move_to(d, s + dent, 0);
+ ass_draw_line_to(d, s, dent);
+ ass_draw_line_to(d, s - dent, 0);
+
+ ass_draw_move_to(d, s - dent, height);
+ ass_draw_line_to(d, s, height - dent);
+ ass_draw_line_to(d, s + dent, height);
+ }
+ }
+
+ ass_draw_stop(d);
+ add_osd_ass_event(track, "progbar", d->text);
+ ass_draw_reset(d);
+}
+
+static void update_osd(struct osd_state *osd, struct osd_object *obj)
+{
+ obj->osd_changed = false;
+ clear_ass(&obj->ass);
+ update_osd_text(osd, obj);
+ update_progbar(osd, obj);
+}
+
+static void update_external(struct osd_state *osd, struct osd_object *obj,
+ struct osd_external *ext)
+{
+ bstr t = bstr0(ext->ov.data);
+ ext->ass.res_x = ext->ov.res_x;
+ ext->ass.res_y = ext->ov.res_y;
+ create_ass_track(osd, obj, &ext->ass);
+
+ clear_ass(&ext->ass);
+
+ int resy = ext->ass.track->PlayResY;
+ mp_ass_set_style(get_style(&ext->ass, "OSD"), resy, osd->opts->osd_style);
+
+ // Some scripts will reference this style name with \r tags.
+ const struct osd_style_opts *def = osd_style_conf.defaults;
+ mp_ass_set_style(get_style(&ext->ass, "Default"), resy, def);
+
+ while (t.len) {
+ bstr line;
+ bstr_split_tok(t, "\n", &line, &t);
+ if (line.len) {
+ char *tmp = bstrdup0(NULL, line);
+ add_osd_ass_event(ext->ass.track, "OSD", tmp);
+ talloc_free(tmp);
+ }
+ }
+}
+
+static int cmp_zorder(const void *pa, const void *pb)
+{
+ const struct osd_external *a = *(struct osd_external **)pa;
+ const struct osd_external *b = *(struct osd_external **)pb;
+ return a->ov.z == b->ov.z ? 0 : (a->ov.z > b->ov.z ? 1 : -1);
+}
+
+void osd_set_external(struct osd_state *osd, struct osd_external_ass *ov)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL];
+ bool zorder_changed = false;
+ int index = -1;
+
+ for (int n = 0; n < obj->num_externals; n++) {
+ struct osd_external *e = obj->externals[n];
+ if (e->ov.id == ov->id && e->ov.owner == ov->owner) {
+ index = n;
+ break;
+ }
+ }
+
+ if (index < 0) {
+ if (!ov->format)
+ goto done;
+ struct osd_external *new = talloc_zero(NULL, struct osd_external);
+ new->ov.owner = ov->owner;
+ new->ov.id = ov->id;
+ MP_TARRAY_APPEND(obj, obj->externals, obj->num_externals, new);
+ index = obj->num_externals - 1;
+ zorder_changed = true;
+ }
+
+ struct osd_external *entry = obj->externals[index];
+
+ if (!ov->format) {
+ if (!entry->ov.hidden) {
+ obj->changed = true;
+ osd->want_redraw_notification = true;
+ }
+ destroy_external(entry);
+ MP_TARRAY_REMOVE_AT(obj->externals, obj->num_externals, index);
+ goto done;
+ }
+
+ if (!entry->ov.hidden || !ov->hidden) {
+ obj->changed = true;
+ osd->want_redraw_notification = true;
+ }
+
+ entry->ov.format = ov->format;
+ if (!entry->ov.data)
+ entry->ov.data = talloc_strdup(entry, "");
+ entry->ov.data[0] = '\0'; // reuse memory allocation
+ entry->ov.data = talloc_strdup_append(entry->ov.data, ov->data);
+ entry->ov.res_x = ov->res_x;
+ entry->ov.res_y = ov->res_y;
+ zorder_changed |= entry->ov.z != ov->z;
+ entry->ov.z = ov->z;
+ entry->ov.hidden = ov->hidden;
+
+ update_external(osd, obj, entry);
+
+ if (zorder_changed) {
+ qsort(obj->externals, obj->num_externals, sizeof(obj->externals[0]),
+ cmp_zorder);
+ }
+
+ if (ov->out_rc) {
+ struct mp_osd_res vo_res = entry->ass.vo_res;
+ // Defined fallback if VO has not drawn this yet
+ if (vo_res.w < 1 || vo_res.h < 1) {
+ vo_res = (struct mp_osd_res){
+ .w = entry->ov.res_x,
+ .h = entry->ov.res_y,
+ .display_par = 1,
+ };
+ // According to osd-overlay command description.
+ if (vo_res.w < 1)
+ vo_res.w = 1280;
+ if (vo_res.h < 1)
+ vo_res.h = 720;
+ }
+
+ ASS_Image *img_list = NULL;
+ append_ass(&entry->ass, &vo_res, &img_list, NULL);
+
+ mp_ass_get_bb(img_list, entry->ass.track, &vo_res, ov->out_rc);
+ }
+
+done:
+ mp_mutex_unlock(&osd->lock);
+}
+
+void osd_set_external_remove_owner(struct osd_state *osd, void *owner)
+{
+ mp_mutex_lock(&osd->lock);
+ struct osd_object *obj = osd->objs[OSDTYPE_EXTERNAL];
+ for (int n = obj->num_externals - 1; n >= 0; n--) {
+ struct osd_external *e = obj->externals[n];
+ if (e->ov.owner == owner) {
+ destroy_external(e);
+ MP_TARRAY_REMOVE_AT(obj->externals, obj->num_externals, n);
+ obj->changed = true;
+ osd->want_redraw_notification = true;
+ }
+ }
+ mp_mutex_unlock(&osd->lock);
+}
+
+static void append_ass(struct ass_state *ass, struct mp_osd_res *res,
+ ASS_Image **img_list, bool *changed)
+{
+ if (!ass->render || !ass->track) {
+ *img_list = NULL;
+ return;
+ }
+
+ update_playres(ass, res);
+
+ ass_set_frame_size(ass->render, res->w, res->h);
+ ass_set_pixel_aspect(ass->render, res->display_par);
+
+ int ass_changed;
+ *img_list = ass_render_frame(ass->render, ass->track, 0, &ass_changed);
+
+ ass->changed |= ass_changed;
+
+ if (changed) {
+ *changed |= ass->changed;
+ ass->changed = false;
+ }
+}
+
+struct sub_bitmaps *osd_object_get_bitmaps(struct osd_state *osd,
+ struct osd_object *obj, int format)
+{
+ if (obj->type == OSDTYPE_OSD && obj->osd_changed)
+ update_osd(osd, obj);
+
+ if (!obj->ass_packer)
+ obj->ass_packer = mp_ass_packer_alloc(obj);
+
+ MP_TARRAY_GROW(obj, obj->ass_imgs, obj->num_externals + 1);
+
+ append_ass(&obj->ass, &obj->vo_res, &obj->ass_imgs[0], &obj->changed);
+ for (int n = 0; n < obj->num_externals; n++) {
+ if (obj->externals[n]->ov.hidden) {
+ update_playres(&obj->externals[n]->ass, &obj->vo_res);
+ obj->ass_imgs[n + 1] = NULL;
+ } else {
+ append_ass(&obj->externals[n]->ass, &obj->vo_res,
+ &obj->ass_imgs[n + 1], &obj->changed);
+ }
+ }
+
+ struct sub_bitmaps out_imgs = {0};
+ mp_ass_packer_pack(obj->ass_packer, obj->ass_imgs, obj->num_externals + 1,
+ obj->changed, format, &out_imgs);
+
+ obj->changed = false;
+
+ return sub_bitmaps_copy(&obj->copy_cache, &out_imgs);
+}
diff --git a/sub/osd_state.h b/sub/osd_state.h
new file mode 100644
index 0000000..9bb48f8
--- /dev/null
+++ b/sub/osd_state.h
@@ -0,0 +1,94 @@
+#ifndef MP_OSD_STATE_H_
+#define MP_OSD_STATE_H_
+
+#include <stdatomic.h>
+
+#include "osd.h"
+#include "osdep/threads.h"
+
+enum mp_osdtype {
+ OSDTYPE_SUB,
+ OSDTYPE_SUB2, // IDs must be numerically successive
+
+ OSDTYPE_OSD,
+
+ OSDTYPE_EXTERNAL,
+ OSDTYPE_EXTERNAL2,
+
+ OSDTYPE_COUNT
+};
+
+struct ass_state {
+ struct mp_log *log;
+ struct ass_track *track;
+ struct ass_renderer *render;
+ struct ass_library *library;
+ int res_x, res_y;
+ bool changed;
+ struct mp_osd_res vo_res; // last known value
+};
+
+struct osd_object {
+ int type; // OSDTYPE_*
+ bool is_sub;
+
+ // OSDTYPE_OSD
+ bool osd_changed;
+ char *text;
+ struct osd_progbar_state progbar_state;
+
+ // OSDTYPE_SUB/OSDTYPE_SUB2
+ struct dec_sub *sub;
+
+ // OSDTYPE_EXTERNAL
+ struct osd_external **externals;
+ int num_externals;
+
+ // OSDTYPE_EXTERNAL2
+ struct sub_bitmaps *external2;
+
+ // VO cache state
+ int vo_change_id;
+ struct mp_osd_res vo_res;
+ bool vo_had_output;
+
+ // Internally used by osd_libass.c
+ bool changed;
+ struct ass_state ass;
+ struct mp_ass_packer *ass_packer;
+ struct sub_bitmap_copy_cache *copy_cache;
+ struct ass_image **ass_imgs;
+};
+
+struct osd_external {
+ struct osd_external_ass ov;
+ struct ass_state ass;
+};
+
+struct osd_state {
+ mp_mutex lock;
+
+ struct osd_object *objs[MAX_OSD_PARTS];
+
+ bool render_subs_in_filter;
+ _Atomic double force_video_pts;
+
+ bool want_redraw;
+ bool want_redraw_notification;
+
+ struct m_config_cache *opts_cache;
+ struct mp_osd_render_opts *opts;
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct stats_ctx *stats;
+
+ struct mp_draw_sub_cache *draw_cache;
+};
+
+// defined in osd_libass.c
+struct sub_bitmaps *osd_object_get_bitmaps(struct osd_state *osd,
+ struct osd_object *obj, int format);
+void osd_init_backend(struct osd_state *osd);
+void osd_destroy_backend(struct osd_state *osd);
+
+#endif
diff --git a/sub/sd.h b/sub/sd.h
new file mode 100644
index 0000000..11a90fe
--- /dev/null
+++ b/sub/sd.h
@@ -0,0 +1,111 @@
+#ifndef MPLAYER_SD_H
+#define MPLAYER_SD_H
+
+#include "dec_sub.h"
+#include "demux/packet.h"
+#include "misc/bstr.h"
+
+// up to 210 ms overlaps or gaps are removed
+#define SUB_GAP_THRESHOLD 0.210
+// don't change timings if durations are smaller
+#define SUB_GAP_KEEP 0.4
+// slight offset when sub seeking or sub stepping
+#define SUB_SEEK_OFFSET 0.01
+
+struct sd {
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct mp_subtitle_opts *opts;
+
+ const struct sd_functions *driver;
+ void *priv;
+
+ struct attachment_list *attachments;
+ struct mp_codec_params *codec;
+
+ // Set to false as soon as the decoder discards old subtitle events.
+ // (only needed if sd_functions.accept_packets_in_advance == false)
+ bool preload_ok;
+};
+
+struct sd_functions {
+ const char *name;
+ bool accept_packets_in_advance;
+ int (*init)(struct sd *sd);
+ void (*decode)(struct sd *sd, struct demux_packet *packet);
+ void (*reset)(struct sd *sd);
+ void (*select)(struct sd *sd, bool selected);
+ void (*uninit)(struct sd *sd);
+
+ bool (*accepts_packet)(struct sd *sd, double pts); // implicit default if NULL: true
+ int (*control)(struct sd *sd, enum sd_ctrl cmd, void *arg);
+
+ struct sub_bitmaps *(*get_bitmaps)(struct sd *sd, struct mp_osd_res dim,
+ int format, double pts);
+ char *(*get_text)(struct sd *sd, double pts, enum sd_text_type type);
+ struct sd_times (*get_times)(struct sd *sd, double pts);
+};
+
+// lavc_conv.c
+struct lavc_conv;
+struct lavc_conv *lavc_conv_create(struct mp_log *log,
+ const struct mp_codec_params *mp_codec);
+char *lavc_conv_get_extradata(struct lavc_conv *priv);
+char **lavc_conv_decode(struct lavc_conv *priv, struct demux_packet *packet,
+ double *sub_pts, double *sub_duration);
+void lavc_conv_reset(struct lavc_conv *priv);
+void lavc_conv_uninit(struct lavc_conv *priv);
+
+struct sd_filter {
+ struct mpv_global *global;
+ struct mp_log *log;
+ struct mp_sub_filter_opts *opts;
+ const struct sd_filter_functions *driver;
+
+ void *priv;
+
+ // Static codec parameters. Set by sd; cannot be changed by filter.
+ char *codec;
+ char *event_format;
+};
+
+struct sd_filter_functions {
+ bool (*init)(struct sd_filter *ft);
+
+ // Filter an ASS event (usually in the Matroska format, but event_format
+ // can be used to determine details).
+ // Returning NULL is interpreted as dropping the event completely.
+ // Returning pkt makes it no-op.
+ // If the returned packet is not pkt or NULL, it must have been properly
+ // allocated.
+ // pkt is owned by the caller (and freed by the caller when needed).
+ // Note: as by normal demux_packet rules, you must not modify any fields in
+ // it, or the data referenced by it. You must create a new demux_packet
+ // when modifying data.
+ struct demux_packet *(*filter)(struct sd_filter *ft,
+ struct demux_packet *pkt);
+
+ void (*uninit)(struct sd_filter *ft);
+};
+
+extern const struct sd_filter_functions sd_filter_sdh;
+extern const struct sd_filter_functions sd_filter_regex;
+extern const struct sd_filter_functions sd_filter_jsre;
+
+
+// convenience utils for filters with ass codec
+
+// num commas to skip at an ass-event before the "Text" field (always last)
+// (doesn't change, can be retrieved once on filter init)
+int sd_ass_fmt_offset(const char *event_format);
+
+// the event (pkt->buffer) "Text" content according to the calculated offset.
+// on malformed event: warns and returns (bstr){NULL,0}
+bstr sd_ass_pkt_text(struct sd_filter *ft, struct demux_packet *pkt, int offset);
+
+// convert \0-terminated "Text" (ass) content to plaintext, possibly in-place.
+// result.start is out, result.len is MIN(out_siz, strlen(in)) or smaller.
+// if there's room: out[result.len] is set to \0. out == in is allowed.
+bstr sd_ass_to_plaintext(char *out, size_t out_siz, const char *in);
+
+#endif
diff --git a/sub/sd_ass.c b/sub/sd_ass.c
new file mode 100644
index 0000000..6742f6f
--- /dev/null
+++ b/sub/sd_ass.c
@@ -0,0 +1,1035 @@
+/*
+ * 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 <assert.h>
+#include <string.h>
+#include <math.h>
+#include <limits.h>
+
+#include <libavutil/common.h>
+#include <ass/ass.h>
+
+#include "mpv_talloc.h"
+
+#include "config.h"
+#include "options/m_config.h"
+#include "options/options.h"
+#include "common/common.h"
+#include "common/msg.h"
+#include "demux/demux.h"
+#include "video/csputils.h"
+#include "video/mp_image.h"
+#include "dec_sub.h"
+#include "ass_mp.h"
+#include "sd.h"
+
+struct sd_ass_priv {
+ struct ass_library *ass_library;
+ struct ass_renderer *ass_renderer;
+ struct ass_track *ass_track;
+ struct ass_track *shadow_track; // for --sub-ass=no rendering
+ bool ass_configured;
+ bool is_converted;
+ struct lavc_conv *converter;
+ struct sd_filter **filters;
+ int num_filters;
+ bool clear_once;
+ bool on_top;
+ struct mp_ass_packer *packer;
+ struct sub_bitmap_copy_cache *copy_cache;
+ char last_text[500];
+ struct mp_image_params video_params;
+ struct mp_image_params last_params;
+ struct mp_osd_res osd;
+ int64_t *seen_packets;
+ int num_seen_packets;
+ bool duration_unknown;
+};
+
+static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts);
+static void fill_plaintext(struct sd *sd, double pts);
+
+static const struct sd_filter_functions *const filters[] = {
+ // Note: list order defines filter order.
+ &sd_filter_sdh,
+#if HAVE_POSIX
+ &sd_filter_regex,
+#endif
+#if HAVE_JAVASCRIPT
+ &sd_filter_jsre,
+#endif
+ NULL,
+};
+
+// Add default styles, if the track does not have any styles yet.
+// Apply style overrides if the user provides any.
+static void mp_ass_add_default_styles(ASS_Track *track, struct mp_subtitle_opts *opts)
+{
+ if (opts->ass_styles_file && opts->ass_style_override)
+ ass_read_styles(track, opts->ass_styles_file, NULL);
+
+ if (track->n_styles == 0) {
+ if (!track->PlayResY) {
+ track->PlayResX = MP_ASS_FONT_PLAYRESX;
+ track->PlayResY = MP_ASS_FONT_PLAYRESY;
+ }
+ track->Kerning = true;
+ int sid = ass_alloc_style(track);
+ track->default_style = sid;
+ ASS_Style *style = track->styles + sid;
+ style->Name = strdup("Default");
+ mp_ass_set_style(style, track->PlayResY, opts->sub_style);
+ }
+
+ if (opts->ass_style_override)
+ ass_process_force_style(track);
+}
+
+static const char *const font_mimetypes[] = {
+ "application/x-truetype-font",
+ "application/vnd.ms-opentype",
+ "application/x-font-ttf",
+ "application/x-font", // probably incorrect
+ "application/font-sfnt",
+ "font/collection",
+ "font/otf",
+ "font/sfnt",
+ "font/ttf",
+ NULL
+};
+
+static const char *const font_exts[] = {".ttf", ".ttc", ".otf", ".otc", NULL};
+
+static bool attachment_is_font(struct mp_log *log, struct demux_attachment *f)
+{
+ if (!f->name || !f->type || !f->data || !f->data_size)
+ return false;
+ for (int n = 0; font_mimetypes[n]; n++) {
+ if (strcmp(font_mimetypes[n], f->type) == 0)
+ return true;
+ }
+ // fallback: match against file extension
+ char *ext = strlen(f->name) > 4 ? f->name + strlen(f->name) - 4 : "";
+ for (int n = 0; font_exts[n]; n++) {
+ if (strcasecmp(ext, font_exts[n]) == 0) {
+ mp_warn(log, "Loading font attachment '%s' with MIME type %s. "
+ "Assuming this is a broken Matroska file, which was "
+ "muxed without setting a correct font MIME type.\n",
+ f->name, f->type);
+ return true;
+ }
+ }
+ return false;
+}
+
+static void add_subtitle_fonts(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ struct mp_subtitle_opts *opts = sd->opts;
+ if (!opts->ass_enabled || !opts->use_embedded_fonts || !sd->attachments)
+ return;
+ for (int i = 0; i < sd->attachments->num_entries; i++) {
+ struct demux_attachment *f = &sd->attachments->entries[i];
+ if (attachment_is_font(sd->log, f))
+ ass_add_font(ctx->ass_library, f->name, f->data, f->data_size);
+ }
+}
+
+static void filters_destroy(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+
+ for (int n = 0; n < ctx->num_filters; n++) {
+ struct sd_filter *ft = ctx->filters[n];
+ if (ft->driver->uninit)
+ ft->driver->uninit(ft);
+ talloc_free(ft);
+ }
+ ctx->num_filters = 0;
+}
+
+static void filters_init(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+
+ filters_destroy(sd);
+
+ for (int n = 0; filters[n]; n++) {
+ struct sd_filter *ft = talloc_ptrtype(ctx, ft);
+ *ft = (struct sd_filter){
+ .global = sd->global,
+ .log = sd->log,
+ .opts = mp_get_config_group(ft, sd->global, &mp_sub_filter_opts),
+ .driver = filters[n],
+ .codec = "ass",
+ .event_format = ctx->ass_track->event_format,
+ };
+ if (ft->driver->init(ft)) {
+ MP_TARRAY_APPEND(ctx, ctx->filters, ctx->num_filters, ft);
+ } else {
+ talloc_free(ft);
+ }
+ }
+}
+
+static void enable_output(struct sd *sd, bool enable)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ if (enable == !!ctx->ass_renderer)
+ return;
+ if (ctx->ass_renderer) {
+ ass_renderer_done(ctx->ass_renderer);
+ ctx->ass_renderer = NULL;
+ } else {
+ ctx->ass_renderer = ass_renderer_init(ctx->ass_library);
+
+ mp_ass_configure_fonts(ctx->ass_renderer, sd->opts->sub_style,
+ sd->global, sd->log);
+ }
+}
+
+static void assobjects_init(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ struct mp_subtitle_opts *opts = sd->opts;
+
+ ctx->ass_library = mp_ass_init(sd->global, sd->opts->sub_style, sd->log);
+ ass_set_extract_fonts(ctx->ass_library, opts->use_embedded_fonts);
+
+ add_subtitle_fonts(sd);
+
+ if (opts->ass_style_override)
+ ass_set_style_overrides(ctx->ass_library, opts->ass_style_override_list);
+
+ ctx->ass_track = ass_new_track(ctx->ass_library);
+ ctx->ass_track->track_type = TRACK_TYPE_ASS;
+
+ ctx->shadow_track = ass_new_track(ctx->ass_library);
+ ctx->shadow_track->PlayResX = MP_ASS_FONT_PLAYRESX;
+ ctx->shadow_track->PlayResY = MP_ASS_FONT_PLAYRESY;
+ mp_ass_add_default_styles(ctx->shadow_track, opts);
+
+ char *extradata = sd->codec->extradata;
+ int extradata_size = sd->codec->extradata_size;
+ if (ctx->converter) {
+ extradata = lavc_conv_get_extradata(ctx->converter);
+ extradata_size = extradata ? strlen(extradata) : 0;
+ }
+ if (extradata)
+ ass_process_codec_private(ctx->ass_track, extradata, extradata_size);
+
+ mp_ass_add_default_styles(ctx->ass_track, opts);
+
+#if LIBASS_VERSION >= 0x01302000
+ ass_set_check_readorder(ctx->ass_track, sd->opts->sub_clear_on_seek ? 0 : 1);
+#endif
+
+ enable_output(sd, true);
+}
+
+static void assobjects_destroy(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+
+ ass_free_track(ctx->ass_track);
+ ass_free_track(ctx->shadow_track);
+ enable_output(sd, false);
+ ass_library_done(ctx->ass_library);
+}
+
+static int init(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = talloc_zero(sd, struct sd_ass_priv);
+ sd->priv = ctx;
+
+ // Note: accept "null" as alias for "ass", so EDL delay_open subtitle
+ // streams work.
+ if (strcmp(sd->codec->codec, "ass") != 0 &&
+ strcmp(sd->codec->codec, "null") != 0)
+ {
+ ctx->is_converted = true;
+ ctx->converter = lavc_conv_create(sd->log, sd->codec);
+ if (!ctx->converter)
+ return -1;
+
+ if (strcmp(sd->codec->codec, "eia_608") == 0)
+ ctx->duration_unknown = 1;
+ }
+
+ assobjects_init(sd);
+ filters_init(sd);
+
+ ctx->packer = mp_ass_packer_alloc(ctx);
+
+ return 0;
+}
+
+// Note: pkt is not necessarily a fully valid refcounted packet.
+static void filter_and_add(struct sd *sd, struct demux_packet *pkt)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ struct demux_packet *orig_pkt = pkt;
+
+ for (int n = 0; n < ctx->num_filters; n++) {
+ struct sd_filter *ft = ctx->filters[n];
+ struct demux_packet *npkt = ft->driver->filter(ft, pkt);
+ if (pkt != npkt && pkt != orig_pkt)
+ talloc_free(pkt);
+ pkt = npkt;
+ if (!pkt)
+ return;
+ }
+
+ ass_process_chunk(ctx->ass_track, pkt->buffer, pkt->len,
+ llrint(pkt->pts * 1000),
+ llrint(pkt->duration * 1000));
+
+ if (pkt != orig_pkt)
+ talloc_free(pkt);
+}
+
+// Test if the packet with the given file position (used as unique ID) was
+// already consumed. Return false if the packet is new (and add it to the
+// internal list), and return true if it was already seen.
+static bool check_packet_seen(struct sd *sd, int64_t pos)
+{
+ struct sd_ass_priv *priv = sd->priv;
+ int a = 0;
+ int b = priv->num_seen_packets;
+ while (a < b) {
+ int mid = a + (b - a) / 2;
+ int64_t val = priv->seen_packets[mid];
+ if (pos == val)
+ return true;
+ if (pos > val) {
+ a = mid + 1;
+ } else {
+ b = mid;
+ }
+ }
+ MP_TARRAY_INSERT_AT(priv, priv->seen_packets, priv->num_seen_packets, a, pos);
+ return false;
+}
+
+#define UNKNOWN_DURATION (INT_MAX / 1000)
+
+static void decode(struct sd *sd, struct demux_packet *packet)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Track *track = ctx->ass_track;
+ if (ctx->converter) {
+ if (!sd->opts->sub_clear_on_seek && packet->pos >= 0 &&
+ check_packet_seen(sd, packet->pos))
+ return;
+
+ double sub_pts = 0;
+ double sub_duration = 0;
+ char **r = lavc_conv_decode(ctx->converter, packet, &sub_pts,
+ &sub_duration);
+ if (sd->opts->sub_stretch_durations ||
+ packet->duration < 0 || sub_duration == UINT32_MAX) {
+ if (!ctx->duration_unknown) {
+ MP_WARN(sd, "Subtitle with unknown duration.\n");
+ ctx->duration_unknown = true;
+ }
+ sub_duration = UNKNOWN_DURATION;
+ }
+
+ for (int n = 0; r && r[n]; n++) {
+ struct demux_packet pkt2 = {
+ .pts = sub_pts,
+ .duration = sub_duration,
+ .buffer = r[n],
+ .len = strlen(r[n]),
+ };
+ filter_and_add(sd, &pkt2);
+ }
+ if (ctx->duration_unknown) {
+ for (int n = track->n_events - 2; n >= 0; n--) {
+ if (track->events[n].Duration == UNKNOWN_DURATION * 1000) {
+ if (track->events[n].Start != track->events[n + 1].Start) {
+ track->events[n].Duration = track->events[n + 1].Start -
+ track->events[n].Start;
+ } else {
+ track->events[n].Duration = track->events[n + 1].Duration;
+ }
+ }
+ }
+ }
+ } else {
+ // Note that for this packet format, libass has an internal mechanism
+ // for discarding duplicate (already seen) packets.
+ filter_and_add(sd, packet);
+ }
+}
+
+static void configure_ass(struct sd *sd, struct mp_osd_res *dim,
+ bool converted, ASS_Track *track)
+{
+ struct mp_subtitle_opts *opts = sd->opts;
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Renderer *priv = ctx->ass_renderer;
+
+ ass_set_frame_size(priv, dim->w, dim->h);
+ ass_set_margins(priv, dim->mt, dim->mb, dim->ml, dim->mr);
+
+ bool set_use_margins = false;
+ float set_sub_pos = 0.0f;
+ float set_line_spacing = 0;
+ float set_font_scale = 1;
+ int set_hinting = 0;
+ bool set_scale_with_window = false;
+ bool set_scale_by_window = true;
+ bool total_override = false;
+ // With forced overrides, apply the --sub-* specific options
+ if (converted || opts->ass_style_override == 3) { // 'force'
+ set_scale_with_window = opts->sub_scale_with_window;
+ set_use_margins = opts->sub_use_margins;
+ set_scale_by_window = opts->sub_scale_by_window;
+ total_override = true;
+ } else {
+ set_scale_with_window = opts->ass_scale_with_window;
+ set_use_margins = opts->ass_use_margins;
+ }
+ if (converted || opts->ass_style_override) {
+ set_sub_pos = 100.0f - opts->sub_pos;
+ set_line_spacing = opts->ass_line_spacing;
+ set_hinting = opts->ass_hinting;
+ set_font_scale = opts->sub_scale;
+ }
+ if (set_scale_with_window) {
+ int vidh = dim->h - (dim->mt + dim->mb);
+ set_font_scale *= dim->h / (float)MPMAX(vidh, 1);
+ }
+ if (!set_scale_by_window) {
+ double factor = dim->h / 720.0;
+ if (factor != 0.0)
+ set_font_scale /= factor;
+ }
+ ass_set_use_margins(priv, set_use_margins);
+ ass_set_line_position(priv, set_sub_pos);
+ ass_set_shaper(priv, opts->ass_shaper);
+ int set_force_flags = 0;
+ if (total_override)
+ set_force_flags |= ASS_OVERRIDE_BIT_STYLE | ASS_OVERRIDE_BIT_SELECTIVE_FONT_SCALE;
+ if (opts->ass_style_override == 4) // 'scale'
+ set_force_flags |= ASS_OVERRIDE_BIT_SELECTIVE_FONT_SCALE;
+ if (converted)
+ set_force_flags |= ASS_OVERRIDE_BIT_ALIGNMENT;
+#ifdef ASS_JUSTIFY_AUTO
+ if ((converted || opts->ass_style_override) && opts->ass_justify)
+ set_force_flags |= ASS_OVERRIDE_BIT_JUSTIFY;
+#endif
+ ass_set_selective_style_override_enabled(priv, set_force_flags);
+ ASS_Style style = {0};
+ mp_ass_set_style(&style, MP_ASS_FONT_PLAYRESY, opts->sub_style);
+ ass_set_selective_style_override(priv, &style);
+ free(style.FontName);
+ if (converted && track->default_style < track->n_styles) {
+ mp_ass_set_style(track->styles + track->default_style,
+ track->PlayResY, opts->sub_style);
+ }
+ ass_set_font_scale(priv, set_font_scale);
+ ass_set_hinting(priv, set_hinting);
+ ass_set_line_spacing(priv, set_line_spacing);
+#if LIBASS_VERSION >= 0x01600010
+ if (converted)
+ ass_track_set_feature(track, ASS_FEATURE_WRAP_UNICODE, 1);
+#endif
+ if (converted) {
+ bool override_playres = true;
+ char **ass_style_override_list = opts->ass_style_override_list;
+ for (int i = 0; ass_style_override_list && ass_style_override_list[i]; i++) {
+ if (bstr_find0(bstr0(ass_style_override_list[i]), "PlayResX") >= 0)
+ override_playres = false;
+ }
+
+ // srt to ass conversion from ffmpeg has fixed PlayResX of 384 with an
+ // aspect of 4:3. Starting with libass f08f8ea5 (pre 0.17) PlayResX
+ // affects shadow and border widths, among others, so to render borders
+ // and shadows correctly, we adjust PlayResX according to the DAR.
+ // But PlayResX also affects margins, so we adjust those too.
+ // This should ensure basic srt-to-ass ffmpeg conversion has correct
+ // borders, but there could be other issues with some srt extensions
+ // and/or different source formats which would be exposed over time.
+ // Make these adjustments only if the user didn't set PlayResX.
+ if (override_playres) {
+ int vidw = dim->w - (dim->ml + dim->mr);
+ int vidh = dim->h - (dim->mt + dim->mb);
+ track->PlayResX = track->PlayResY * (double)vidw / MPMAX(vidh, 1);
+ // ffmpeg and mpv use a default PlayResX of 384 when it is not known,
+ // this comes from VSFilter.
+ double fix_margins = track->PlayResX / (double)MP_ASS_FONT_PLAYRESX;
+ track->styles->MarginL = round(track->styles->MarginL * fix_margins);
+ track->styles->MarginR = round(track->styles->MarginR * fix_margins);
+ }
+ }
+}
+
+static bool has_overrides(char *s)
+{
+ if (!s)
+ return false;
+ return strstr(s, "\\pos") || strstr(s, "\\move") || strstr(s, "\\clip") ||
+ strstr(s, "\\iclip") || strstr(s, "\\org") || strstr(s, "\\p");
+}
+
+#define END(ev) ((ev)->Start + (ev)->Duration)
+
+static long long find_timestamp(struct sd *sd, double pts)
+{
+ struct sd_ass_priv *priv = sd->priv;
+ if (pts == MP_NOPTS_VALUE)
+ return 0;
+
+ long long ts = llrint(pts * 1000);
+
+ if (!sd->opts->sub_fix_timing || sd->opts->ass_style_override == 0)
+ return ts;
+
+ // Try to fix small gaps and overlaps.
+ ASS_Track *track = priv->ass_track;
+ int threshold = SUB_GAP_THRESHOLD * 1000;
+ int keep = SUB_GAP_KEEP * 1000;
+
+ // Find the "current" event.
+ ASS_Event *ev[2] = {0};
+ int n_ev = 0;
+ for (int n = 0; n < track->n_events; n++) {
+ ASS_Event *event = &track->events[n];
+ if (ts >= event->Start - threshold && ts <= END(event) + threshold) {
+ if (n_ev >= MP_ARRAY_SIZE(ev))
+ return ts; // multiple overlaps - give up (probably complex subs)
+ ev[n_ev++] = event;
+ }
+ }
+
+ if (n_ev != 2)
+ return ts;
+
+ // Simple/minor heuristic against destroying typesetting.
+ if (ev[0]->Style != ev[1]->Style || has_overrides(ev[0]->Text) ||
+ has_overrides(ev[1]->Text))
+ return ts;
+
+ // Sort by start timestamps.
+ if (ev[0]->Start > ev[1]->Start)
+ MPSWAP(ASS_Event*, ev[0], ev[1]);
+
+ // We want to fix partial overlaps only.
+ if (END(ev[0]) >= END(ev[1]))
+ return ts;
+
+ if (ev[0]->Duration < keep || ev[1]->Duration < keep)
+ return ts;
+
+ // Gap between the events -> move ts to show the end of the first event.
+ if (ts >= END(ev[0]) && ts < ev[1]->Start && END(ev[0]) < ev[1]->Start &&
+ END(ev[0]) + threshold >= ev[1]->Start)
+ return END(ev[0]) - 1;
+
+ // Overlap -> move ts to the (exclusive) end of the first event.
+ // Relies on the fact that the ASS_Renderer has no overlap registered, even
+ // if there is one. This happens to work because we never render the
+ // overlapped state, and libass never resolves a collision.
+ if (ts >= ev[1]->Start && ts <= END(ev[0]) && END(ev[0]) > ev[1]->Start &&
+ END(ev[0]) <= ev[1]->Start + threshold)
+ return END(ev[0]);
+
+ return ts;
+}
+
+#undef END
+
+static struct sub_bitmaps *get_bitmaps(struct sd *sd, struct mp_osd_res dim,
+ int format, double pts)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ struct mp_subtitle_opts *opts = sd->opts;
+ bool no_ass = !opts->ass_enabled || ctx->on_top ||
+ opts->ass_style_override == 5;
+ bool converted = ctx->is_converted || no_ass;
+ ASS_Track *track = no_ass ? ctx->shadow_track : ctx->ass_track;
+ ASS_Renderer *renderer = ctx->ass_renderer;
+ struct sub_bitmaps *res = &(struct sub_bitmaps){0};
+
+ // Always update the osd_res
+ struct mp_osd_res old_osd = ctx->osd;
+ ctx->osd = dim;
+
+ if (pts == MP_NOPTS_VALUE || !renderer)
+ goto done;
+
+ // Currently no supported text sub formats support a distinction between forced
+ // and unforced lines, so we just assume everything's unforced and discard everything.
+ // If we ever see a format that makes this distinction, we can add support here.
+ if (opts->sub_forced_events_only)
+ goto done;
+
+ double scale = dim.display_par;
+ if (!converted && (!opts->ass_style_override ||
+ opts->ass_vsfilter_aspect_compat))
+ {
+ // Let's use the original video PAR for vsfilter compatibility:
+ double par = ctx->video_params.p_w / (double)ctx->video_params.p_h;
+ if (isnormal(par))
+ scale *= par;
+ }
+ if (!ctx->ass_configured || !osd_res_equals(old_osd, ctx->osd)) {
+ configure_ass(sd, &dim, converted, track);
+ ctx->ass_configured = true;
+ }
+ ass_set_pixel_aspect(renderer, scale);
+ if (!converted && (!opts->ass_style_override ||
+ opts->ass_vsfilter_blur_compat))
+ {
+ ass_set_storage_size(renderer, ctx->video_params.w, ctx->video_params.h);
+ } else {
+ ass_set_storage_size(renderer, 0, 0);
+ }
+ long long ts = find_timestamp(sd, pts);
+ if (ctx->duration_unknown && pts != MP_NOPTS_VALUE) {
+ mp_ass_flush_old_events(track, ts);
+ ctx->num_seen_packets = 0;
+ sd->preload_ok = false;
+ }
+
+ if (no_ass)
+ fill_plaintext(sd, pts);
+
+ int changed;
+ ASS_Image *imgs = ass_render_frame(renderer, track, ts, &changed);
+ mp_ass_packer_pack(ctx->packer, &imgs, 1, changed, format, res);
+
+done:
+ // mangle_colors() modifies the color field, so copy the thing _before_.
+ res = sub_bitmaps_copy(&ctx->copy_cache, res);
+
+ if (!converted && res)
+ mangle_colors(sd, res);
+
+ return res;
+}
+
+struct buf {
+ char *start;
+ int size;
+ int len;
+};
+
+static void append(struct buf *b, char c)
+{
+ if (b->len < b->size) {
+ b->start[b->len] = c;
+ b->len++;
+ }
+}
+
+static void ass_to_plaintext(struct buf *b, const char *in)
+{
+ bool in_tag = false;
+ const char *open_tag_pos = NULL;
+ bool in_drawing = false;
+ while (*in) {
+ if (in_tag) {
+ if (in[0] == '}') {
+ in += 1;
+ in_tag = false;
+ } else if (in[0] == '\\' && in[1] == 'p') {
+ in += 2;
+ // Skip text between \pN and \p0 tags. A \p without a number
+ // is the same as \p0, and leading 0s are also allowed.
+ in_drawing = false;
+ while (in[0] >= '0' && in[0] <= '9') {
+ if (in[0] != '0')
+ in_drawing = true;
+ in += 1;
+ }
+ } else {
+ in += 1;
+ }
+ } else {
+ if (in[0] == '\\' && (in[1] == 'N' || in[1] == 'n')) {
+ in += 2;
+ append(b, '\n');
+ } else if (in[0] == '\\' && in[1] == 'h') {
+ in += 2;
+ append(b, ' ');
+ } else if (in[0] == '{') {
+ open_tag_pos = in;
+ in += 1;
+ in_tag = true;
+ } else {
+ if (!in_drawing)
+ append(b, in[0]);
+ in += 1;
+ }
+ }
+ }
+ // A '{' without a closing '}' is always visible.
+ if (in_tag) {
+ while (*open_tag_pos)
+ append(b, *open_tag_pos++);
+ }
+}
+
+// Empty string counts as whitespace. Reads s[len-1] even if there are \0s.
+static bool is_whitespace_only(char *s, int len)
+{
+ for (int n = 0; n < len; n++) {
+ if (s[n] != ' ' && s[n] != '\t')
+ return false;
+ }
+ return true;
+}
+
+static char *get_text_buf(struct sd *sd, double pts, enum sd_text_type type)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Track *track = ctx->ass_track;
+
+ if (pts == MP_NOPTS_VALUE)
+ return NULL;
+ long long ipts = find_timestamp(sd, pts);
+
+ struct buf b = {ctx->last_text, sizeof(ctx->last_text) - 1};
+
+ for (int i = 0; i < track->n_events; ++i) {
+ ASS_Event *event = track->events + i;
+ if (ipts >= event->Start && ipts < event->Start + event->Duration) {
+ if (event->Text) {
+ int start = b.len;
+ if (type == SD_TEXT_TYPE_PLAIN) {
+ ass_to_plaintext(&b, event->Text);
+ } else {
+ char *t = event->Text;
+ while (*t)
+ append(&b, *t++);
+ }
+ if (is_whitespace_only(&b.start[start], b.len - start)) {
+ b.len = start;
+ } else {
+ append(&b, '\n');
+ }
+ }
+ }
+ }
+
+ b.start[b.len] = '\0';
+
+ if (b.len > 0 && b.start[b.len - 1] == '\n')
+ b.start[b.len - 1] = '\0';
+
+ return ctx->last_text;
+}
+
+static char *get_text(struct sd *sd, double pts, enum sd_text_type type)
+{
+ return talloc_strdup(NULL, get_text_buf(sd, pts, type));
+}
+
+static struct sd_times get_times(struct sd *sd, double pts)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Track *track = ctx->ass_track;
+ struct sd_times res = { .start = MP_NOPTS_VALUE, .end = MP_NOPTS_VALUE };
+
+ if (pts == MP_NOPTS_VALUE)
+ return res;
+
+ long long ipts = find_timestamp(sd, pts);
+
+ for (int i = 0; i < track->n_events; ++i) {
+ ASS_Event *event = track->events + i;
+ if (ipts >= event->Start && ipts < event->Start + event->Duration) {
+ double start = event->Start / 1000.0;
+ double end = event->Duration == UNKNOWN_DURATION ?
+ MP_NOPTS_VALUE : (event->Start + event->Duration) / 1000.0;
+
+ if (res.start == MP_NOPTS_VALUE || res.start > start)
+ res.start = start;
+
+ if (res.end == MP_NOPTS_VALUE || res.end < end)
+ res.end = end;
+ }
+ }
+
+ return res;
+}
+
+static void fill_plaintext(struct sd *sd, double pts)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ ASS_Track *track = ctx->shadow_track;
+
+ ass_flush_events(track);
+
+ char *text = get_text_buf(sd, pts, SD_TEXT_TYPE_PLAIN);
+ if (!text)
+ return;
+
+ bstr dst = {0};
+
+ if (ctx->on_top)
+ bstr_xappend(NULL, &dst, bstr0("{\\a6}"));
+
+ while (*text) {
+ if (*text == '{')
+ bstr_xappend(NULL, &dst, bstr0("\\"));
+ bstr_xappend(NULL, &dst, (bstr){text, 1});
+ // Break ASS escapes with U+2060 WORD JOINER
+ if (*text == '\\')
+ mp_append_utf8_bstr(NULL, &dst, 0x2060);
+ text++;
+ }
+
+ if (!dst.start)
+ return;
+
+ int n = ass_alloc_event(track);
+ ASS_Event *event = track->events + n;
+ event->Start = 0;
+ event->Duration = INT_MAX;
+ event->Style = track->default_style;
+ event->Text = strdup(dst.start);
+
+ talloc_free(dst.start);
+}
+
+static void reset(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ if (sd->opts->sub_clear_on_seek || ctx->duration_unknown || ctx->clear_once) {
+ ass_flush_events(ctx->ass_track);
+ ctx->num_seen_packets = 0;
+ sd->preload_ok = false;
+ ctx->clear_once = false;
+ }
+ if (ctx->converter)
+ lavc_conv_reset(ctx->converter);
+}
+
+static void uninit(struct sd *sd)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+
+ filters_destroy(sd);
+ if (ctx->converter)
+ lavc_conv_uninit(ctx->converter);
+ assobjects_destroy(sd);
+ talloc_free(ctx->copy_cache);
+}
+
+static int control(struct sd *sd, enum sd_ctrl cmd, void *arg)
+{
+ struct sd_ass_priv *ctx = sd->priv;
+ switch (cmd) {
+ case SD_CTRL_SUB_STEP: {
+ double *a = arg;
+ long long ts = llrint(a[0] * 1000.0);
+ long long res = ass_step_sub(ctx->ass_track, ts, a[1]);
+ if (!res)
+ return false;
+ // Try to account for overlapping durations
+ a[0] += res / 1000.0 + SUB_SEEK_OFFSET;
+ return true;
+ }
+ case SD_CTRL_SET_VIDEO_PARAMS:
+ ctx->video_params = *(struct mp_image_params *)arg;
+ return CONTROL_OK;
+ case SD_CTRL_SET_TOP:
+ ctx->on_top = *(bool *)arg;
+ return CONTROL_OK;
+ case SD_CTRL_UPDATE_OPTS: {
+ int flags = (uintptr_t)arg;
+ if (flags & UPDATE_SUB_FILT) {
+ filters_destroy(sd);
+ filters_init(sd);
+ ctx->clear_once = true; // allow reloading on seeks
+ reset(sd);
+ }
+ if (flags & UPDATE_SUB_HARD) {
+ // ass_track will be recreated, so clear duplicate cache
+ ctx->clear_once = true;
+ reset(sd);
+ assobjects_destroy(sd);
+ assobjects_init(sd);
+ }
+ ctx->ass_configured = false; // ass always needs to be reconfigured
+ return CONTROL_OK;
+ }
+ default:
+ return CONTROL_UNKNOWN;
+ }
+}
+
+const struct sd_functions sd_ass = {
+ .name = "ass",
+ .accept_packets_in_advance = true,
+ .init = init,
+ .decode = decode,
+ .get_bitmaps = get_bitmaps,
+ .get_text = get_text,
+ .get_times = get_times,
+ .control = control,
+ .reset = reset,
+ .select = enable_output,
+ .uninit = uninit,
+};
+
+// Disgusting hack for (xy-)vsfilter color compatibility.
+static void mangle_colors(struct sd *sd, struct sub_bitmaps *parts)
+{
+ struct mp_subtitle_opts *opts = sd->opts;
+ struct sd_ass_priv *ctx = sd->priv;
+ enum mp_csp csp = 0;
+ enum mp_csp_levels levels = 0;
+ if (opts->ass_vsfilter_color_compat == 0) // "no"
+ return;
+ bool force_601 = opts->ass_vsfilter_color_compat == 3;
+ ASS_Track *track = ctx->ass_track;
+ static const int ass_csp[] = {
+ [YCBCR_BT601_TV] = MP_CSP_BT_601,
+ [YCBCR_BT601_PC] = MP_CSP_BT_601,
+ [YCBCR_BT709_TV] = MP_CSP_BT_709,
+ [YCBCR_BT709_PC] = MP_CSP_BT_709,
+ [YCBCR_SMPTE240M_TV] = MP_CSP_SMPTE_240M,
+ [YCBCR_SMPTE240M_PC] = MP_CSP_SMPTE_240M,
+ };
+ static const int ass_levels[] = {
+ [YCBCR_BT601_TV] = MP_CSP_LEVELS_TV,
+ [YCBCR_BT601_PC] = MP_CSP_LEVELS_PC,
+ [YCBCR_BT709_TV] = MP_CSP_LEVELS_TV,
+ [YCBCR_BT709_PC] = MP_CSP_LEVELS_PC,
+ [YCBCR_SMPTE240M_TV] = MP_CSP_LEVELS_TV,
+ [YCBCR_SMPTE240M_PC] = MP_CSP_LEVELS_PC,
+ };
+ int trackcsp = track->YCbCrMatrix;
+ if (force_601)
+ trackcsp = YCBCR_BT601_TV;
+ // NONE is a bit random, but the intention is: don't modify colors.
+ if (trackcsp == YCBCR_NONE)
+ return;
+ if (trackcsp < sizeof(ass_csp) / sizeof(ass_csp[0]))
+ csp = ass_csp[trackcsp];
+ if (trackcsp < sizeof(ass_levels) / sizeof(ass_levels[0]))
+ levels = ass_levels[trackcsp];
+ if (trackcsp == YCBCR_DEFAULT) {
+ csp = MP_CSP_BT_601;
+ levels = MP_CSP_LEVELS_TV;
+ }
+ // Unknown colorspace (either YCBCR_UNKNOWN, or a valid value unknown to us)
+ if (!csp || !levels)
+ return;
+
+ struct mp_image_params params = ctx->video_params;
+
+ if (force_601) {
+ params.color = (struct mp_colorspace){
+ .space = MP_CSP_BT_709,
+ .levels = MP_CSP_LEVELS_TV,
+ };
+ }
+
+ if ((csp == params.color.space && levels == params.color.levels) ||
+ params.color.space == MP_CSP_RGB) // Even VSFilter doesn't mangle on RGB video
+ return;
+
+ bool basic_conv = params.color.space == MP_CSP_BT_709 &&
+ params.color.levels == MP_CSP_LEVELS_TV &&
+ csp == MP_CSP_BT_601 &&
+ levels == MP_CSP_LEVELS_TV;
+
+ // With "basic", only do as much as needed for basic compatibility.
+ if (opts->ass_vsfilter_color_compat == 1 && !basic_conv)
+ return;
+
+ if (params.color.space != ctx->last_params.color.space ||
+ params.color.levels != ctx->last_params.color.levels)
+ {
+ int msgl = basic_conv ? MSGL_V : MSGL_WARN;
+ ctx->last_params = params;
+ MP_MSG(sd, msgl, "mangling colors like vsfilter: "
+ "RGB -> %s %s -> %s %s -> RGB\n",
+ m_opt_choice_str(mp_csp_names, csp),
+ m_opt_choice_str(mp_csp_levels_names, levels),
+ m_opt_choice_str(mp_csp_names, params.color.space),
+ m_opt_choice_str(mp_csp_names, params.color.levels));
+ }
+
+ // Conversion that VSFilter would use
+ struct mp_csp_params vs_params = MP_CSP_PARAMS_DEFAULTS;
+ vs_params.color.space = csp;
+ vs_params.color.levels = levels;
+ struct mp_cmat vs_yuv2rgb, vs_rgb2yuv;
+ mp_get_csp_matrix(&vs_params, &vs_yuv2rgb);
+ mp_invert_cmat(&vs_rgb2yuv, &vs_yuv2rgb);
+
+ // Proper conversion to RGB
+ struct mp_csp_params rgb_params = MP_CSP_PARAMS_DEFAULTS;
+ rgb_params.color = params.color;
+ struct mp_cmat vs2rgb;
+ mp_get_csp_matrix(&rgb_params, &vs2rgb);
+
+ for (int n = 0; n < parts->num_parts; n++) {
+ struct sub_bitmap *sb = &parts->parts[n];
+ uint32_t color = sb->libass.color;
+ int r = (color >> 24u) & 0xff;
+ int g = (color >> 16u) & 0xff;
+ int b = (color >> 8u) & 0xff;
+ int a = 0xff - (color & 0xff);
+ int rgb[3] = {r, g, b}, yuv[3];
+ mp_map_fixp_color(&vs_rgb2yuv, 8, rgb, 8, yuv);
+ mp_map_fixp_color(&vs2rgb, 8, yuv, 8, rgb);
+ sb->libass.color = MP_ASS_RGBA(rgb[0], rgb[1], rgb[2], a);
+ }
+}
+
+int sd_ass_fmt_offset(const char *evt_fmt)
+{
+ // "Text" is always last (as it's arbitrary content in buf), e.g. format:
+ // "Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text"
+ int n = 0;
+ while (evt_fmt && (evt_fmt = strchr(evt_fmt, ',')))
+ evt_fmt++, n++;
+ return n-1; // buffer is without the format's Start/End, with ReadOrder
+}
+
+bstr sd_ass_pkt_text(struct sd_filter *ft, struct demux_packet *pkt, int offset)
+{
+ // e.g. pkt->buffer ("4" is ReadOrder): "4,0,Default,,0,0,0,,fifth line"
+ bstr txt = {(char *)pkt->buffer, pkt->len}, t0 = txt;
+ while (offset-- > 0) {
+ int n = bstrchr(txt, ',');
+ if (n < 0) { // shouldn't happen
+ MP_WARN(ft, "Malformed event '%.*s'\n", BSTR_P(t0));
+ return (bstr){NULL, 0};
+ }
+ txt = bstr_cut(txt, n+1);
+ }
+ return txt;
+}
+
+bstr sd_ass_to_plaintext(char *out, size_t out_siz, const char *in)
+{
+ struct buf b = {out, out_siz, 0};
+ ass_to_plaintext(&b, in);
+ if (b.len < out_siz)
+ out[b.len] = 0;
+ return (bstr){out, b.len};
+}
diff --git a/sub/sd_lavc.c b/sub/sd_lavc.c
new file mode 100644
index 0000000..30aa641
--- /dev/null
+++ b/sub/sd_lavc.c
@@ -0,0 +1,676 @@
+/*
+ * 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 <assert.h>
+#include <math.h>
+
+#include <libavcodec/avcodec.h>
+#include <libavutil/common.h>
+#include <libavutil/intreadwrite.h>
+#include <libavutil/opt.h>
+
+#include "mpv_talloc.h"
+#include "common/msg.h"
+#include "common/av_common.h"
+#include "demux/stheader.h"
+#include "options/options.h"
+#include "video/mp_image.h"
+#include "video/out/bitmap_packer.h"
+#include "img_convert.h"
+#include "sd.h"
+#include "dec_sub.h"
+
+#define MAX_QUEUE 4
+
+struct sub {
+ bool valid;
+ AVSubtitle avsub;
+ struct sub_bitmap *inbitmaps;
+ int count;
+ struct mp_image *data;
+ int bound_w, bound_h;
+ int src_w, src_h;
+ double pts;
+ double endpts;
+ int64_t id;
+};
+
+struct seekpoint {
+ double pts;
+ double endpts;
+};
+
+struct sd_lavc_priv {
+ AVCodecContext *avctx;
+ AVPacket *avpkt;
+ AVRational pkt_timebase;
+ struct sub subs[MAX_QUEUE]; // most recent event first
+ struct sub_bitmap *outbitmaps;
+ struct sub_bitmap *prevret;
+ int prevret_num;
+ int64_t displayed_id;
+ int64_t new_id;
+ struct mp_image_params video_params;
+ double current_pts;
+ struct seekpoint *seekpoints;
+ int num_seekpoints;
+ struct bitmap_packer *packer;
+};
+
+static int init(struct sd *sd)
+{
+ enum AVCodecID cid = mp_codec_to_av_codec_id(sd->codec->codec);
+
+ // Supported codecs must be known to decode to paletted bitmaps
+ switch (cid) {
+ case AV_CODEC_ID_DVB_SUBTITLE:
+ case AV_CODEC_ID_DVB_TELETEXT:
+ case AV_CODEC_ID_HDMV_PGS_SUBTITLE:
+ case AV_CODEC_ID_XSUB:
+ case AV_CODEC_ID_DVD_SUBTITLE:
+ break;
+ default:
+ return -1;
+ }
+
+ struct sd_lavc_priv *priv = talloc_zero(NULL, struct sd_lavc_priv);
+ AVCodecContext *ctx = NULL;
+ const AVCodec *sub_codec = avcodec_find_decoder(cid);
+ if (!sub_codec)
+ goto error;
+ ctx = avcodec_alloc_context3(sub_codec);
+ if (!ctx)
+ goto error;
+ priv->avpkt = av_packet_alloc();
+ if (!priv->avpkt)
+ goto error;
+ if (mp_set_avctx_codec_headers(ctx, sd->codec) < 0)
+ goto error;
+ priv->pkt_timebase = mp_get_codec_timebase(sd->codec);
+ ctx->pkt_timebase = priv->pkt_timebase;
+ if (avcodec_open2(ctx, sub_codec, NULL) < 0)
+ goto error;
+ priv->avctx = ctx;
+ sd->priv = priv;
+ priv->displayed_id = -1;
+ priv->current_pts = MP_NOPTS_VALUE;
+ priv->packer = talloc_zero(priv, struct bitmap_packer);
+ return 0;
+
+ error:
+ MP_FATAL(sd, "Could not open libavcodec subtitle decoder\n");
+ avcodec_free_context(&ctx);
+ mp_free_av_packet(&priv->avpkt);
+ talloc_free(priv);
+ return -1;
+}
+
+static void clear_sub(struct sub *sub)
+{
+ sub->count = 0;
+ sub->pts = MP_NOPTS_VALUE;
+ sub->endpts = MP_NOPTS_VALUE;
+ if (sub->valid)
+ avsubtitle_free(&sub->avsub);
+ sub->valid = false;
+}
+
+static void alloc_sub(struct sd_lavc_priv *priv)
+{
+ clear_sub(&priv->subs[MAX_QUEUE - 1]);
+ struct sub tmp = priv->subs[MAX_QUEUE - 1];
+ for (int n = MAX_QUEUE - 1; n > 0; n--)
+ priv->subs[n] = priv->subs[n - 1];
+ priv->subs[0] = tmp;
+ // clear only some fields; the memory allocs can be reused
+ priv->subs[0].valid = false;
+ priv->subs[0].count = 0;
+ priv->subs[0].src_w = 0;
+ priv->subs[0].src_h = 0;
+ priv->subs[0].id = priv->new_id++;
+}
+
+static void convert_pal(uint32_t *colors, size_t count, bool gray)
+{
+ for (int n = 0; n < count; n++) {
+ uint32_t c = colors[n];
+ uint32_t b = c & 0xFF;
+ uint32_t g = (c >> 8) & 0xFF;
+ uint32_t r = (c >> 16) & 0xFF;
+ uint32_t a = (c >> 24) & 0xFF;
+ if (gray)
+ r = g = b = (r + g + b) / 3;
+ // from straight to pre-multiplied alpha
+ b = b * a / 255;
+ g = g * a / 255;
+ r = r * a / 255;
+ colors[n] = b | (g << 8) | (r << 16) | (a << 24);
+ }
+}
+
+// Initialize sub from sub->avsub.
+static void read_sub_bitmaps(struct sd *sd, struct sub *sub)
+{
+ struct mp_subtitle_opts *opts = sd->opts;
+ struct sd_lavc_priv *priv = sd->priv;
+ AVSubtitle *avsub = &sub->avsub;
+
+ MP_TARRAY_GROW(priv, sub->inbitmaps, avsub->num_rects);
+
+ packer_set_size(priv->packer, avsub->num_rects);
+
+ // If we blur, we want a transparent region around the bitmap data to
+ // avoid "cut off" artifacts on the borders.
+ bool apply_blur = opts->sub_gauss != 0.0f;
+ int extend = apply_blur ? 5 : 0;
+ // Assume consumers may use bilinear scaling on it (2x2 filter)
+ int padding = 1 + extend;
+
+ priv->packer->padding = padding;
+
+ // For the sake of libswscale, which in some cases takes sub-rects as
+ // source images, and wants 16 byte start pointer and stride alignment.
+ int align = 4;
+
+ for (int i = 0; i < avsub->num_rects; i++) {
+ struct AVSubtitleRect *r = avsub->rects[i];
+ struct sub_bitmap *b = &sub->inbitmaps[sub->count];
+
+ if (r->type != SUBTITLE_BITMAP) {
+ MP_ERR(sd, "unsupported subtitle type from libavcodec\n");
+ continue;
+ }
+ if (!(r->flags & AV_SUBTITLE_FLAG_FORCED) && opts->sub_forced_events_only)
+ continue;
+ if (r->w <= 0 || r->h <= 0)
+ continue;
+
+ b->bitmap = r; // save for later (dumb hack to avoid more complexity)
+
+ priv->packer->in[sub->count] = (struct pos){r->w + (align - 1), r->h};
+ sub->count++;
+ }
+
+ priv->packer->count = sub->count;
+
+ if (packer_pack(priv->packer) < 0) {
+ MP_ERR(sd, "Unable to pack subtitle bitmaps.\n");
+ sub->count = 0;
+ }
+
+ if (!sub->count)
+ return;
+
+ struct pos bb[2];
+ packer_get_bb(priv->packer, bb);
+
+ sub->bound_w = bb[1].x;
+ sub->bound_h = bb[1].y;
+
+ if (!sub->data || sub->data->w < sub->bound_w || sub->data->h < sub->bound_h) {
+ talloc_free(sub->data);
+ sub->data = mp_image_alloc(IMGFMT_BGRA, priv->packer->w, priv->packer->h);
+ if (!sub->data) {
+ sub->count = 0;
+ return;
+ }
+ talloc_steal(priv, sub->data);
+ }
+
+ if (!mp_image_make_writeable(sub->data)) {
+ sub->count = 0;
+ return;
+ }
+
+ for (int i = 0; i < sub->count; i++) {
+ struct sub_bitmap *b = &sub->inbitmaps[i];
+ struct pos pos = priv->packer->result[i];
+ struct AVSubtitleRect *r = b->bitmap;
+ uint8_t **data = r->data;
+ int *linesize = r->linesize;
+ b->w = r->w;
+ b->h = r->h;
+ b->x = r->x;
+ b->y = r->y;
+
+ // Choose such that the extended start position is aligned.
+ pos.x = MP_ALIGN_UP(pos.x - extend, align) + extend;
+
+ b->src_x = pos.x;
+ b->src_y = pos.y;
+ b->stride = sub->data->stride[0];
+ b->bitmap = sub->data->planes[0] + pos.y * b->stride + pos.x * 4;
+
+ sub->src_w = MPMAX(sub->src_w, b->x + b->w);
+ sub->src_h = MPMAX(sub->src_h, b->y + b->h);
+
+ assert(r->nb_colors > 0);
+ assert(r->nb_colors <= 256);
+ uint32_t pal[256] = {0};
+ memcpy(pal, data[1], r->nb_colors * 4);
+ convert_pal(pal, 256, opts->sub_gray);
+
+ for (int y = -padding; y < b->h + padding; y++) {
+ uint32_t *out = (uint32_t*)((char*)b->bitmap + y * b->stride);
+ int start = 0;
+ for (int x = -padding; x < 0; x++)
+ out[x] = 0;
+ if (y >= 0 && y < b->h) {
+ uint8_t *in = data[0] + y * linesize[0];
+ for (int x = 0; x < b->w; x++)
+ *out++ = pal[*in++];
+ start = b->w;
+ }
+ for (int x = start; x < b->w + padding; x++)
+ *out++ = 0;
+ }
+
+ b->bitmap = (char*)b->bitmap - extend * b->stride - extend * 4;
+ b->src_x -= extend;
+ b->src_y -= extend;
+ b->x -= extend;
+ b->y -= extend;
+ b->w += extend * 2;
+ b->h += extend * 2;
+
+ if (apply_blur)
+ mp_blur_rgba_sub_bitmap(b, opts->sub_gauss);
+ }
+}
+
+static void decode(struct sd *sd, struct demux_packet *packet)
+{
+ struct mp_subtitle_opts *opts = sd->opts;
+ struct sd_lavc_priv *priv = sd->priv;
+ AVCodecContext *ctx = priv->avctx;
+ double pts = packet->pts;
+ double endpts = MP_NOPTS_VALUE;
+ AVSubtitle sub;
+
+ if (pts == MP_NOPTS_VALUE)
+ MP_WARN(sd, "Subtitle with unknown start time.\n");
+
+ mp_set_av_packet(priv->avpkt, packet, &priv->pkt_timebase);
+
+ if (ctx->codec_id == AV_CODEC_ID_DVB_TELETEXT) {
+ char page[4];
+ snprintf(page, sizeof(page), "%d", opts->teletext_page);
+ av_opt_set(ctx, "txt_page", page, AV_OPT_SEARCH_CHILDREN);
+ }
+
+ int got_sub;
+ int res = avcodec_decode_subtitle2(ctx, &sub, &got_sub, priv->avpkt);
+ if (res < 0 || !got_sub)
+ return;
+
+ if (sub.pts != AV_NOPTS_VALUE)
+ pts = sub.pts / (double)AV_TIME_BASE;
+
+ if (pts != MP_NOPTS_VALUE) {
+ if (sub.end_display_time > sub.start_display_time &&
+ sub.end_display_time != UINT32_MAX)
+ {
+ endpts = pts + sub.end_display_time / 1000.0;
+ }
+ pts += sub.start_display_time / 1000.0;
+
+ // set end time of previous sub
+ struct sub *prev = &priv->subs[0];
+ if (prev->valid) {
+ if (prev->endpts == MP_NOPTS_VALUE || prev->endpts > pts)
+ prev->endpts = pts;
+
+ if (opts->sub_fix_timing && pts - prev->endpts <= SUB_GAP_THRESHOLD)
+ prev->endpts = pts;
+
+ for (int n = 0; n < priv->num_seekpoints; n++) {
+ if (priv->seekpoints[n].pts == prev->pts) {
+ priv->seekpoints[n].endpts = prev->endpts;
+ break;
+ }
+ }
+ }
+
+ // This subtitle packet only signals the end of subtitle display.
+ if (!sub.num_rects) {
+ avsubtitle_free(&sub);
+ return;
+ }
+ }
+
+ alloc_sub(priv);
+ struct sub *current = &priv->subs[0];
+
+ current->valid = true;
+ current->pts = pts;
+ current->endpts = endpts;
+ current->avsub = sub;
+
+ read_sub_bitmaps(sd, current);
+
+ if (pts != MP_NOPTS_VALUE) {
+ for (int n = 0; n < priv->num_seekpoints; n++) {
+ if (priv->seekpoints[n].pts == pts)
+ goto skip;
+ }
+ // Set arbitrary limit as safe-guard against insane files.
+ if (priv->num_seekpoints >= 10000)
+ MP_TARRAY_REMOVE_AT(priv->seekpoints, priv->num_seekpoints, 0);
+ MP_TARRAY_APPEND(priv, priv->seekpoints, priv->num_seekpoints,
+ (struct seekpoint){.pts = pts, .endpts = endpts});
+ skip: ;
+ }
+}
+
+static struct sub *get_current(struct sd_lavc_priv *priv, double pts)
+{
+ struct sub *current = NULL;
+ for (int n = 0; n < MAX_QUEUE; n++) {
+ struct sub *sub = &priv->subs[n];
+ if (!sub->valid)
+ continue;
+ if (pts == MP_NOPTS_VALUE ||
+ ((sub->pts == MP_NOPTS_VALUE || pts + 1e-6 >= sub->pts) &&
+ (sub->endpts == MP_NOPTS_VALUE || pts + 1e-6 < sub->endpts)))
+ {
+ // Ignore "trailing" subtitles with unknown length after 1 minute.
+ if (sub->endpts == MP_NOPTS_VALUE && pts >= sub->pts + 60)
+ break;
+ current = sub;
+ break;
+ }
+ }
+ return current;
+}
+
+static struct sub_bitmaps *get_bitmaps(struct sd *sd, struct mp_osd_res d,
+ int format, double pts)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+ struct mp_subtitle_opts *opts = sd->opts;
+
+ priv->current_pts = pts;
+
+ struct sub *current = get_current(priv, pts);
+
+ if (!current)
+ return NULL;
+
+ MP_TARRAY_GROW(priv, priv->outbitmaps, current->count);
+ for (int n = 0; n < current->count; n++)
+ priv->outbitmaps[n] = current->inbitmaps[n];
+
+ struct sub_bitmaps *res = &(struct sub_bitmaps){0};
+ res->parts = priv->outbitmaps;
+ res->num_parts = current->count;
+ if (priv->displayed_id != current->id)
+ res->change_id++;
+ priv->displayed_id = current->id;
+ res->packed = current->data;
+ res->packed_w = current->bound_w;
+ res->packed_h = current->bound_h;
+ res->format = SUBBITMAP_BGRA;
+
+ double video_par = 0;
+ if (priv->avctx->codec_id == AV_CODEC_ID_DVD_SUBTITLE &&
+ opts->stretch_dvd_subs)
+ {
+ // For DVD subs, try to keep the subtitle PAR at display PAR.
+ double par = priv->video_params.p_w / (double)priv->video_params.p_h;
+ if (isnormal(par))
+ video_par = par;
+ }
+ if (priv->avctx->codec_id == AV_CODEC_ID_HDMV_PGS_SUBTITLE)
+ video_par = -1;
+ if (opts->stretch_image_subs)
+ d.ml = d.mr = d.mt = d.mb = 0;
+ int w = priv->avctx->width;
+ int h = priv->avctx->height;
+ if (w <= 0 || h <= 0 || opts->image_subs_video_res) {
+ w = priv->video_params.w;
+ h = priv->video_params.h;
+ }
+ if (current->src_w > w || current->src_h > h) {
+ w = MPMAX(priv->video_params.w, current->src_w);
+ h = MPMAX(priv->video_params.h, current->src_h);
+ }
+
+ if (opts->sub_pos != 100.0f && opts->ass_style_override) {
+ float offset = (100.0f - opts->sub_pos) / 100.0f * h;
+
+ for (int n = 0; n < res->num_parts; n++) {
+ struct sub_bitmap *sub = &res->parts[n];
+
+ // Decide by heuristic whether this is a sub-title or something
+ // else (top-title, covering whole screen).
+ if (sub->y < h / 2)
+ continue;
+
+ // Allow moving up the subtitle, but only until it clips.
+ sub->y = MPMAX(sub->y - offset, 0);
+ sub->y = MPMIN(sub->y + sub->h, h) - sub->h;
+ }
+ }
+
+ osd_rescale_bitmaps(res, w, h, d, video_par);
+
+ if (opts->sub_scale != 1.0 && opts->ass_style_override) {
+ for (int n = 0; n < res->num_parts; n++) {
+ struct sub_bitmap *sub = &res->parts[n];
+
+ float shit = (opts->sub_scale - 1.0f) / 2;
+
+ // Fortunately VO isn't supposed to give a FUCKING FUCK about
+ // whether the sub might e.g. go outside of the screen.
+ sub->x -= sub->dw * shit;
+ sub->y -= sub->dh * shit;
+ sub->dw += sub->dw * shit * 2;
+ sub->dh += sub->dh * shit * 2;
+ }
+ }
+
+ if (priv->prevret_num != res->num_parts)
+ res->change_id++;
+
+ if (!res->change_id) {
+ assert(priv->prevret_num == res->num_parts);
+ for (int n = 0; n < priv->prevret_num; n++) {
+ struct sub_bitmap *a = &res->parts[n];
+ struct sub_bitmap *b = &priv->prevret[n];
+
+ if (a->x != b->x || a->y != b->y ||
+ a->dw != b->dw || a->dh != b->dh)
+ {
+ res->change_id++;
+ break;
+ }
+ }
+ }
+
+ priv->prevret_num = res->num_parts;
+ MP_TARRAY_GROW(priv, priv->prevret, priv->prevret_num);
+ memcpy(priv->prevret, res->parts, res->num_parts * sizeof(priv->prevret[0]));
+
+ return sub_bitmaps_copy(NULL, res);
+}
+
+static struct sd_times get_times(struct sd *sd, double pts)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+ struct sd_times res = { .start = MP_NOPTS_VALUE, .end = MP_NOPTS_VALUE };
+
+ if (pts == MP_NOPTS_VALUE)
+ return res;
+
+ struct sub *current = get_current(priv, pts);
+
+ if (!current)
+ return res;
+
+ res.start = current->pts;
+ res.end = current->endpts;
+
+ return res;
+}
+
+static bool accepts_packet(struct sd *sd, double min_pts)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+
+ double pts = priv->current_pts;
+ if (min_pts != MP_NOPTS_VALUE) {
+ // guard against bogus rendering PTS in the future.
+ if (pts == MP_NOPTS_VALUE || min_pts < pts)
+ pts = min_pts;
+ // Heuristic: we assume rendering cannot lag behind more than 1 second
+ // behind decoding.
+ if (pts + 1 < min_pts)
+ pts = min_pts;
+ }
+
+ int last_needed = -1;
+ for (int n = 0; n < MAX_QUEUE; n++) {
+ struct sub *sub = &priv->subs[n];
+ if (!sub->valid)
+ continue;
+ if (pts == MP_NOPTS_VALUE ||
+ ((sub->pts == MP_NOPTS_VALUE || sub->pts >= pts) ||
+ (sub->endpts == MP_NOPTS_VALUE || pts < sub->endpts)))
+ {
+ last_needed = n;
+ }
+ }
+ // We can accept a packet if it wouldn't overflow the fixed subtitle queue.
+ // We assume that get_bitmaps() never decreases the PTS.
+ return last_needed + 1 < MAX_QUEUE;
+}
+
+static void reset(struct sd *sd)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+
+ for (int n = 0; n < MAX_QUEUE; n++)
+ clear_sub(&priv->subs[n]);
+ // lavc might not do this right for all codecs; may need close+reopen
+ avcodec_flush_buffers(priv->avctx);
+
+ priv->current_pts = MP_NOPTS_VALUE;
+}
+
+static void uninit(struct sd *sd)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+
+ for (int n = 0; n < MAX_QUEUE; n++)
+ clear_sub(&priv->subs[n]);
+ avcodec_free_context(&priv->avctx);
+ mp_free_av_packet(&priv->avpkt);
+ talloc_free(priv);
+}
+
+static int compare_seekpoint(const void *pa, const void *pb)
+{
+ const struct seekpoint *a = pa, *b = pb;
+ return a->pts == b->pts ? 0 : (a->pts < b->pts ? -1 : +1);
+}
+
+// taken from ass_step_sub(), libass (ISC)
+static double step_sub(struct sd *sd, double now, int movement)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+ int best = -1;
+ double target = now;
+ int direction = (movement > 0 ? 1 : -1) * !!movement;
+
+ if (priv->num_seekpoints == 0)
+ return MP_NOPTS_VALUE;
+
+ qsort(priv->seekpoints, priv->num_seekpoints, sizeof(priv->seekpoints[0]),
+ compare_seekpoint);
+
+ do {
+ int closest = -1;
+ double closest_time = 0;
+ for (int i = 0; i < priv->num_seekpoints; i++) {
+ struct seekpoint *p = &priv->seekpoints[i];
+ double start = p->pts;
+ if (direction < 0) {
+ double end = p->endpts == MP_NOPTS_VALUE ? INFINITY : p->endpts;
+ if (end < target) {
+ if (closest < 0 || end > closest_time) {
+ closest = i;
+ closest_time = end;
+ }
+ }
+ } else if (direction > 0) {
+ if (start > target) {
+ if (closest < 0 || start < closest_time) {
+ closest = i;
+ closest_time = start;
+ }
+ }
+ } else {
+ if (start < target) {
+ if (closest < 0 || start >= closest_time) {
+ closest = i;
+ closest_time = start;
+ }
+ }
+ }
+ }
+ if (closest < 0)
+ break;
+ target = closest_time + direction;
+ best = closest;
+ movement -= direction;
+ } while (movement);
+
+ return best < 0 ? now : priv->seekpoints[best].pts;
+}
+
+static int control(struct sd *sd, enum sd_ctrl cmd, void *arg)
+{
+ struct sd_lavc_priv *priv = sd->priv;
+ switch (cmd) {
+ case SD_CTRL_SUB_STEP: {
+ double *a = arg;
+ double res = step_sub(sd, a[0], a[1]);
+ if (res == MP_NOPTS_VALUE)
+ return false;
+ a[0] = res;
+ return true;
+ }
+ case SD_CTRL_SET_VIDEO_PARAMS:
+ priv->video_params = *(struct mp_image_params *)arg;
+ return CONTROL_OK;
+ default:
+ return CONTROL_UNKNOWN;
+ }
+}
+
+const struct sd_functions sd_lavc = {
+ .name = "lavc",
+ .init = init,
+ .decode = decode,
+ .get_bitmaps = get_bitmaps,
+ .get_times = get_times,
+ .accepts_packet = accepts_packet,
+ .control = control,
+ .reset = reset,
+ .uninit = uninit,
+};