diff options
Diffstat (limited to 'sub/osd_libass.c')
-rw-r--r-- | sub/osd_libass.c | 691 |
1 files changed, 691 insertions, 0 deletions
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); +} |