From 5f02e1b8a21b5323adb643758409506ef0d2bbb6 Mon Sep 17 00:00:00 2001 From: Guido Cella Date: Thu, 9 Apr 2026 18:17:48 +0200 Subject: [PATCH] sub: add sub-lines property Return all subtitle lines in memory with text, start and end. I did not name this sub-all so it cannot be misinterpreted as referring to all subtitle tracks. Closes #12810. --- DOCS/interface-changes/sub-lines.txt | 1 + DOCS/man/input.rst | 23 ++++++++++++++ player/command.c | 45 ++++++++++++++++++++++++++-- sub/dec_sub.c | 21 +++++++++++++ sub/dec_sub.h | 14 +++++++++ sub/sd.h | 1 + sub/sd_ass.c | 39 ++++++++++++++++++++++++ 7 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 DOCS/interface-changes/sub-lines.txt diff --git a/DOCS/interface-changes/sub-lines.txt b/DOCS/interface-changes/sub-lines.txt new file mode 100644 index 0000000000..d31b2ee725 --- /dev/null +++ b/DOCS/interface-changes/sub-lines.txt @@ -0,0 +1 @@ +add `sub-lines` and `secondary-sub-lines` properties diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index 2975cfbeb3..0ebfc2123a 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -3290,6 +3290,29 @@ Property list ``secondary-sub-end`` Same as ``sub-end``, but for the secondary subtitles. +``sub-lines`` + The list of subtitle lines in memory. Not available if the subtitle is not + text-based (i.e. DVD/BD subtitles). + + When querying the property with the client API using ``MPV_FORMAT_NODE``, + or with Lua ``mp.get_property_native``, this will return a mpv_node with + the following contents: + + :: + + MPV_FORMAT_NODE_ARRAY + MPV_FORMAT_NODE_MAP (for each subtitle line) + "text" MPV_FORMAT_STRING + "start" MPV_FORMAT_DOUBLE + "end" MPV_FORMAT_DOUBLE + + ASS tags are stripped from ``text``. + + ``end`` is absent if unknown. + +``secondary-sub-lines`` + Same as ``sub-lines``, but for the secondary subtitles. + ``playlist-pos`` (RW) Current position on playlist. The first entry is on position 0. Writing to this property may start playback at the new position. diff --git a/player/command.c b/player/command.c index d5de2081ad..0a836b600d 100644 --- a/player/command.c +++ b/player/command.c @@ -3455,6 +3455,43 @@ static int mp_property_sub_end(void *ctx, struct m_property *prop, return property_time(action, arg, end); } +static int mp_property_sub_lines(void *ctx, struct m_property *prop, + int action, void *arg) +{ + MPContext *mpctx = ctx; + int sub_index = *(int *)prop->priv; + struct track *track = mpctx->current_track[sub_index][STREAM_SUB]; + struct dec_sub *sub = track ? track->d_sub : NULL; + if (!sub) + return M_PROPERTY_UNAVAILABLE; + + switch (action) { + case M_PROPERTY_GET_TYPE: + *(struct m_option *)arg = (struct m_option){.type = CONF_TYPE_NODE}; + return M_PROPERTY_OK; + case M_PROPERTY_GET: + case M_PROPERTY_GET_NODE: { + struct sub_lines *lines = sub_get_lines(sub); + if (!lines) + return M_PROPERTY_UNAVAILABLE; + + struct mpv_node *node = arg; + node_init(node, MPV_FORMAT_NODE_ARRAY, NULL); + for (int i = 0; i < lines->num_entries; i++) { + struct sub_line *line = &lines->entries[i]; + struct mpv_node *entry = node_array_add(node, MPV_FORMAT_NODE_MAP); + node_map_add_string(entry, "text", line->text); + node_map_add_double(entry, "start", line->start); + if (line->end != MP_NOPTS_VALUE) + node_map_add_double(entry, "end", line->end); + } + talloc_free(lines); + return M_PROPERTY_OK; + } + } + return M_PROPERTY_NOT_IMPLEMENTED; +} + static int mp_property_playlist_current_pos(void *ctx, struct m_property *prop, int action, void *arg) { @@ -4650,6 +4687,10 @@ static const struct m_property mp_properties_base[] = { .priv = (void *)&(const int){0}}, {"secondary-sub-end", mp_property_sub_end, .priv = (void *)&(const int){1}}, + {"sub-lines", mp_property_sub_lines, + .priv = (void *)&(const int){0}}, + {"secondary-sub-lines", mp_property_sub_lines, + .priv = (void *)&(const int){1}}, {"vf", mp_property_vf}, {"af", mp_property_af}, @@ -4734,8 +4775,8 @@ static const char *const *const mp_event_property_change[] = { "secondary-sub-text", "audio-bitrate", "video-bitrate", "sub-bitrate", "decoder-frame-drop-count", "frame-drop-count", "video-frame-info", "vf-metadata", "af-metadata", "sub-start", "sub-end", "secondary-sub-start", - "secondary-sub-end", "video-out-params", "video-dec-params", "video-params", - "deinterlace-active", "video-target-params"), + "secondary-sub-end", "sub-lines", "secondary-sub-lines", "video-out-params", + "video-dec-params", "video-params", "deinterlace-active", "video-target-params"), E(MP_EVENT_DURATION_UPDATE, "duration"), E(MPV_EVENT_VIDEO_RECONFIG, "video-out-params", "video-params", "video-format", "video-codec", "video-bitrate", "dwidth", "dheight", diff --git a/sub/dec_sub.c b/sub/dec_sub.c index 17a32c7f8e..cb18ea14f9 100644 --- a/sub/dec_sub.c +++ b/sub/dec_sub.c @@ -580,3 +580,24 @@ bool sub_is_secondary_visible(struct dec_sub *sub) mp_mutex_unlock(&sub->lock); return ret; } + +static int sub_line_cmp(const void *a, const void *b) +{ + const struct sub_line *la = a, *lb = b; + if (la->start < lb->start) return -1; + if (la->start > lb->start) return 1; + return 0; +} + +struct sub_lines *sub_get_lines(struct dec_sub *sub) +{ + mp_mutex_lock(&sub->lock); + struct sub_lines *res = NULL; + if (sub->sd->driver->get_lines) { + res = sub->sd->driver->get_lines(sub->sd); + qsort(res->entries, res->num_entries, sizeof(res->entries[0]), + sub_line_cmp); + } + mp_mutex_unlock(&sub->lock); + return res; +} diff --git a/sub/dec_sub.h b/sub/dec_sub.h index a6d6b585b8..d8f1539685 100644 --- a/sub/dec_sub.h +++ b/sub/dec_sub.h @@ -39,6 +39,17 @@ struct attachment_list { int num_entries; }; +struct sub_line { + char *text; + double start; + double end; +}; + +struct sub_lines { + struct sub_line *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); @@ -53,6 +64,9 @@ struct sub_bitmaps *sub_get_bitmaps(struct dec_sub *sub, struct mp_osd_res dim, 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); +// Return subtitle lines in memory. Call talloc_free() on the return value. +struct sub_lines *sub_get_lines(struct dec_sub *sub); + 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); diff --git a/sub/sd.h b/sub/sd.h index ccd472f9de..d7cea26525 100644 --- a/sub/sd.h +++ b/sub/sd.h @@ -52,6 +52,7 @@ struct sd_functions { 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); + struct sub_lines *(*get_lines)(struct sd *sd); }; // lavc_conv.c diff --git a/sub/sd_ass.c b/sub/sd_ass.c index fea4322694..d415106caf 100644 --- a/sub/sd_ass.c +++ b/sub/sd_ass.c @@ -1017,6 +1017,44 @@ static void uninit(struct sd *sd) talloc_free(ctx->copy_cache); } +static struct sub_lines *get_lines(struct sd *sd) +{ + struct sd_ass_priv *ctx = sd->priv; + ASS_Track *track = ctx->ass_track; + struct sub_lines *res = talloc_zero(NULL, struct sub_lines); + + for (int i = 0; i < track->n_events; i++) { + ASS_Event *event = &track->events[i]; + if (!event->Text) + continue; + + char *plain = NULL; + bstr result = sd_ass_to_plaintext(&plain, event->Text); + + // ASS subtitle lines can have many empty lines after stripping tags, + // but empty lines are useful in LRC. + if (is_whitespace_only(result)) { + talloc_free(plain); + if (!strcmp(sd->codec->codec, "ass")) + continue; + plain = talloc_strdup(res, ""); + } else { + talloc_steal(res, plain); + } + + struct sub_line line = { + .text = plain, + .start = event->Start / 1000.0, + .end = event->Duration == UNKNOWN_DURATION * 1000 + ? MP_NOPTS_VALUE + : (event->Start + event->Duration) / 1000.0, + }; + MP_TARRAY_APPEND(res, res->entries, res->num_entries, line); + } + + return res; +} + static int control(struct sd *sd, enum sd_ctrl cmd, void *arg) { struct sd_ass_priv *ctx = sd->priv; @@ -1072,6 +1110,7 @@ const struct sd_functions sd_ass = { .get_bitmaps = get_bitmaps, .get_text = get_text, .get_times = get_times, + .get_lines = get_lines, .control = control, .reset = reset, .select = enable_output,