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