diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-15 20:36:56 +0000 |
commit | 51de1d8436100f725f3576aefa24a2bd2057bc28 (patch) | |
tree | c6d1d5264b6d40a8d7ca34129f36b7d61e188af3 /sub | |
parent | Initial commit. (diff) | |
download | mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.tar.xz mpv-51de1d8436100f725f3576aefa24a2bd2057bc28.zip |
Adding upstream version 0.37.0.upstream/0.37.0
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'sub')
-rw-r--r-- | sub/ass_mp.c | 422 | ||||
-rw-r--r-- | sub/ass_mp.h | 65 | ||||
-rw-r--r-- | sub/dec_sub.c | 498 | ||||
-rw-r--r-- | sub/dec_sub.h | 62 | ||||
-rw-r--r-- | sub/draw_bmp.c | 1035 | ||||
-rw-r--r-- | sub/draw_bmp.h | 63 | ||||
-rw-r--r-- | sub/filter_jsre.c | 140 | ||||
-rw-r--r-- | sub/filter_regex.c | 89 | ||||
-rw-r--r-- | sub/filter_sdh.c | 482 | ||||
-rw-r--r-- | sub/img_convert.c | 128 | ||||
-rw-r--r-- | sub/img_convert.h | 23 | ||||
-rw-r--r-- | sub/lavc_conv.c | 293 | ||||
-rw-r--r-- | sub/meson.build | 6 | ||||
-rw-r--r-- | sub/osd.c | 559 | ||||
-rw-r--r-- | sub/osd.h | 247 | ||||
-rw-r--r-- | sub/osd_font.otf | bin | 0 -> 4460 bytes | |||
-rw-r--r-- | sub/osd_libass.c | 691 | ||||
-rw-r--r-- | sub/osd_state.h | 94 | ||||
-rw-r--r-- | sub/sd.h | 111 | ||||
-rw-r--r-- | sub/sd_ass.c | 1035 | ||||
-rw-r--r-- | sub/sd_lavc.c | 676 |
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, ¶ms, 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 Binary files differnew file mode 100644 index 0000000..70b9b21 --- /dev/null +++ b/sub/osd_font.otf 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, +}; |