Skip to content

Commit 38ee83e

Browse files
committed
sub: add subrandr renderer support
subrandr is a subtitle rendering library which aims to render SRV3 (YouTube) subtitles and WebVTT subtitles accurately. Currently in mpv WebVTT subs are rendered via ffmpeg conversion to ASS which throws away a lot of the style and completely disregards the WebVTT non-region-cue positioning algorithm. Furthermore if one wants to render some more complex SRV3 subtitles one has to resort to external converters since it's not even supported by ffmpeg. However subrandr is able to render SRV3 subtitles natively with support for the most commonly used features. It can render ruby text without relying on font metrics during conversion which is obviously fragile, and it can perform correct scaling using the exact calculations used by YouTube instead of making up ASS approximations. Similarly it follows the WebVTT spec for the features of WebVTT that it supports (mostly). It's not perfect of course and there's still many things it doesn't do or does wrong but those are things that can be incrementally improved outside of mpv.
1 parent 1f2bf81 commit 38ee83e

File tree

7 files changed

+342
-0
lines changed

7 files changed

+342
-0
lines changed

demux/demux.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,9 @@ extern const demuxer_desc_t demuxer_desc_directory;
6363
extern const demuxer_desc_t demuxer_desc_disc;
6464
extern const demuxer_desc_t demuxer_desc_rar;
6565
extern const demuxer_desc_t demuxer_desc_libarchive;
66+
#if HAVE_SUBRANDR
67+
extern const demuxer_desc_t demuxer_desc_textsub;
68+
#endif
6669
extern const demuxer_desc_t demuxer_desc_null;
6770
extern const demuxer_desc_t demuxer_desc_timeline;
6871

@@ -76,6 +79,9 @@ static const demuxer_desc_t *const demuxer_list[] = {
7679
&demuxer_desc_matroska,
7780
#if HAVE_LIBARCHIVE
7881
&demuxer_desc_libarchive,
82+
#endif
83+
#if HAVE_SUBRANDR
84+
&demuxer_desc_textsub,
7985
#endif
8086
&demuxer_desc_lavf,
8187
&demuxer_desc_mf,

demux/demux_textsub.c

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* This file is part of mpv.
3+
*
4+
* mpv is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU Lesser General Public
6+
* License as published by the Free Software Foundation; either
7+
* version 2.1 of the License, or (at your option) any later version.
8+
*
9+
* mpv is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public
15+
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#include <math.h>
19+
20+
#include <subrandr/subrandr.h>
21+
22+
#include "common/common.h"
23+
#include "demux/packet.h"
24+
#include "misc/bstr.h"
25+
#include "stream/stream.h"
26+
#include "demux.h"
27+
28+
struct textsub_ext {
29+
const char *ext;
30+
const char *codec;
31+
const char *codec_desc;
32+
};
33+
34+
static struct textsub_ext TEXT_FORMAT_EXTS[] = {
35+
{".vtt", "textsub/vtt", "WebVTT"},
36+
{".srv3", "textsub/srv3", "srv3"},
37+
{".ytt", "textsub/srv3", "srv3"},
38+
{NULL}
39+
};
40+
41+
static const int SUBRANDR_PROBE_SIZE = 128;
42+
43+
struct demux_textsub_priv {
44+
bstr content;
45+
bool exhausted;
46+
};
47+
48+
static int demux_open_textsub(struct demuxer *demuxer, enum demux_check check)
49+
{
50+
struct bstr filename = bstr0(demuxer->filename);
51+
const char *codec = NULL;
52+
const char *codec_desc = NULL;
53+
54+
for(struct textsub_ext *ext = TEXT_FORMAT_EXTS; ext->ext; ++ext)
55+
if(bstr_endswith0(filename, ext->ext)) {
56+
codec = ext->codec;
57+
codec_desc = ext->codec_desc;
58+
}
59+
60+
if(!codec) {
61+
int probe_size = stream_peek(demuxer->stream, SUBRANDR_PROBE_SIZE);
62+
uint8_t *probe_buffer = demuxer->stream->buffer;
63+
64+
sbr_subtitle_format fmt = sbr_probe_text((const char *)probe_buffer, (size_t)probe_size);
65+
if(fmt == SBR_SUBTITLE_FORMAT_SRV3) {
66+
codec = TEXT_FORMAT_EXTS[1].codec;
67+
codec_desc = TEXT_FORMAT_EXTS[1].codec_desc;
68+
} else if(fmt == SBR_SUBTITLE_FORMAT_WEBVTT) {
69+
codec = TEXT_FORMAT_EXTS[0].codec;
70+
codec_desc = TEXT_FORMAT_EXTS[0].codec_desc;
71+
}
72+
}
73+
74+
if(check != DEMUX_CHECK_REQUEST && !codec)
75+
return -1;
76+
77+
struct demux_textsub_priv *priv = talloc(demuxer, struct demux_textsub_priv);
78+
79+
priv->content = stream_read_complete(demuxer->stream, priv, 64 * 1024 * 1024);
80+
if(priv->content.start == NULL) {
81+
talloc_free(priv);
82+
return -1;
83+
}
84+
demuxer->priv = priv;
85+
86+
struct sh_stream *stream = demux_alloc_sh_stream(STREAM_SUB);
87+
stream->codec->codec = codec;
88+
stream->codec->codec_desc = codec_desc;
89+
demux_add_sh_stream(demuxer, stream);
90+
91+
demuxer->seekable = true;
92+
demuxer->fully_read = true;
93+
demux_close_stream(demuxer);
94+
95+
return 0;
96+
}
97+
98+
static bool demux_read_packet_textsub(struct demuxer *demuxer, struct demux_packet **packet)
99+
{
100+
struct demux_textsub_priv *priv = demuxer->priv;
101+
102+
if(priv->exhausted)
103+
return false;
104+
105+
*packet = new_demux_packet_from(demuxer->packet_pool, priv->content.start, priv->content.len);
106+
(*packet)->stream = 0;
107+
(*packet)->pts = 0.0;
108+
(*packet)->sub_duration = INFINITY;
109+
priv->exhausted = true;
110+
111+
return true;
112+
}
113+
114+
static void demux_seek_textsub(struct demuxer *demuxer, double seek_pts, int flags)
115+
{
116+
// We only ever emit one packet, no seeking needed or possible.
117+
}
118+
119+
static void demux_switched_tracks_textsub(struct demuxer *demuxer)
120+
{
121+
struct demux_textsub_priv *ctx = demuxer->priv;
122+
ctx->exhausted = false;
123+
}
124+
125+
126+
const struct demuxer_desc demuxer_desc_textsub = {
127+
.name = "textsub",
128+
.desc = "text subtitle demuxer",
129+
.open = demux_open_textsub,
130+
.read_packet = demux_read_packet_textsub,
131+
.seek = demux_seek_textsub,
132+
.switched_tracks = demux_switched_tracks_textsub
133+
};

meson.build

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,13 @@ if features['libdl']
357357
dependencies += libdl
358358
endif
359359

360+
subrandr = dependency('subrandr', version: ['>= 0.1.0', '<= 0.2.0'], required: false)
361+
features += {'subrandr': get_option('subrandr').require(subrandr.found()).allowed()}
362+
if features['subrandr']
363+
sources += files('demux/demux_textsub.c', 'sub/sd_sbr.c')
364+
dependencies += subrandr
365+
endif
366+
360367
# C11 atomics are mandatory but linking to the library is not always required.
361368
dependencies += cc.find_library('atomic', required: false)
362369

meson.options

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ option('pthread-debug', type: 'feature', value: 'disabled', description: 'pthrea
3030
option('rubberband', type: 'feature', value: 'auto', description: 'librubberband support')
3131
option('sdl2', type: 'feature', value: 'disabled', description: 'SDL2')
3232
option('sdl2-gamepad', type: 'feature', value: 'auto', description: 'SDL2 gamepad input')
33+
option('subrandr', type: 'feature', value: 'auto', description: 'subrandr support (SRV3 and WebVTT subtitle renderer)')
3334
option('uchardet', type: 'feature', value: 'auto', description: 'uchardet support')
3435
option('uwp', type: 'feature', value: 'disabled', description: 'Universal Windows Platform')
3536
option('vapoursynth', type: 'feature', value: 'auto', description: 'VapourSynth filter bridge')

options/options.c

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1086,13 +1086,15 @@ static const struct MPOpts mp_default_opts = {
10861086
"scc",
10871087
"smi",
10881088
"srt",
1089+
"srv3",
10891090
"ssa",
10901091
"sub",
10911092
"sup",
10921093
"utf",
10931094
"utf-8",
10941095
"utf8",
10951096
"vtt",
1097+
"ytt",
10961098
NULL
10971099
},
10981100

sub/dec_sub.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,15 @@
3636

3737
extern const struct sd_functions sd_ass;
3838
extern const struct sd_functions sd_lavc;
39+
#if HAVE_SUBRANDR
40+
extern const struct sd_functions sd_sbr;
41+
#endif
3942

4043
static const struct sd_functions *const sd_list[] = {
4144
&sd_lavc,
45+
#if HAVE_SUBRANDR
46+
&sd_sbr,
47+
#endif
4248
&sd_ass,
4349
NULL
4450
};

sub/sd_sbr.c

Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
/*
2+
* This file is part of mpv.
3+
*
4+
* mpv is free software; you can redistribute it and/or
5+
* modify it under the terms of the GNU Lesser General Public
6+
* License as published by the Free Software Foundation; either
7+
* version 2.1 of the License, or (at your option) any later version.
8+
*
9+
* mpv is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Lesser General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU Lesser General Public
15+
* License along with mpv. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
#include <assert.h>
19+
#include <math.h>
20+
#include <limits.h>
21+
22+
#include <libavutil/common.h>
23+
#include <subrandr/subrandr.h>
24+
25+
#include "mpv_talloc.h"
26+
27+
#include "options/m_config.h"
28+
#include "options/options.h"
29+
#include "common/common.h"
30+
#include "demux/packet_pool.h"
31+
#include "demux/stheader.h"
32+
#include "sub/osd.h"
33+
#include "video/mp_image.h"
34+
#include "sd.h"
35+
36+
struct sd_sbr_priv {
37+
struct sbr_library *sbr_library;
38+
struct sbr_renderer *sbr_renderer;
39+
struct sbr_subtitles *sbr_subtitles;
40+
struct mp_image_params video_params;
41+
struct mp_osd_res osd;
42+
struct sub_bitmaps *bitmaps;
43+
};
44+
45+
static void enable_output(struct sd *sd, bool enable)
46+
{
47+
struct sd_sbr_priv *ctx = sd->priv;
48+
if (enable == !!ctx->sbr_renderer)
49+
return;
50+
if (ctx->sbr_renderer) {
51+
sbr_renderer_destroy(ctx->sbr_renderer);
52+
ctx->sbr_renderer = NULL;
53+
} else {
54+
ctx->sbr_renderer = sbr_renderer_create(ctx->sbr_library);
55+
}
56+
}
57+
58+
static int init(struct sd *sd)
59+
{
60+
if(strcmp(sd->codec->codec, "textsub/srv3") && strcmp(sd->codec->codec, "textsub/vtt"))
61+
return -1;
62+
63+
struct sd_sbr_priv *ctx = talloc_zero(sd, struct sd_sbr_priv);
64+
sd->priv = ctx;
65+
66+
ctx->sbr_library = sbr_library_init();
67+
ctx->bitmaps = talloc_zero(ctx, struct sub_bitmaps);
68+
ctx->bitmaps->format = SUBBITMAP_BGRA;
69+
ctx->bitmaps->num_parts = 1;
70+
ctx->bitmaps->parts = talloc_zero(ctx->bitmaps, struct sub_bitmap);
71+
72+
enable_output(sd, true);
73+
74+
return 0;
75+
}
76+
77+
static void decode(struct sd *sd, struct demux_packet *packet)
78+
{
79+
struct sd_sbr_priv *ctx = sd->priv;
80+
81+
if(ctx->sbr_subtitles)
82+
sbr_subtitles_destroy(ctx->sbr_subtitles);
83+
84+
sbr_subtitle_format fmt = SBR_SUBTITLE_FORMAT_UNKOWN;
85+
if(!strcmp(sd->codec->codec, "textsub/srv3") )
86+
fmt = SBR_SUBTITLE_FORMAT_SRV3;
87+
else if(!strcmp(sd->codec->codec, "textsub/vtt"))
88+
fmt = SBR_SUBTITLE_FORMAT_WEBVTT;
89+
90+
ctx->sbr_subtitles = sbr_load_text(
91+
ctx->sbr_library,
92+
packet->buffer,
93+
packet->len,
94+
fmt,
95+
NULL
96+
);
97+
packet->sub_duration = packet->duration;
98+
}
99+
100+
static struct sub_bitmaps *get_bitmaps(struct sd *sd, struct mp_osd_res dim,
101+
int format, double pts)
102+
{
103+
struct sd_sbr_priv *ctx = sd->priv;
104+
struct mp_subtitle_opts *opts = sd->opts;
105+
106+
ctx->osd = dim;
107+
108+
if (pts == MP_NOPTS_VALUE || !ctx->sbr_renderer || !ctx->sbr_subtitles)
109+
return NULL;
110+
111+
if (opts->sub_forced_events_only)
112+
return NULL;
113+
114+
struct sbr_subtitle_context context = (sbr_subtitle_context) {
115+
.dpi = 72,
116+
.padding_top = (int32_t)dim.mt << 6,
117+
.padding_bottom = (int32_t)dim.mb << 6,
118+
.padding_left = (int32_t)dim.ml << 6,
119+
.padding_right = (int32_t)dim.mr << 6,
120+
.video_height = (int32_t)(dim.h - dim.mt - dim.mb) << 6,
121+
.video_width = (int32_t)(dim.w - dim.ml - dim.mr) << 6,
122+
};
123+
124+
unsigned t = lrint(pts * 1000);
125+
126+
struct sub_bitmaps *bitmaps = ctx->bitmaps;
127+
struct sub_bitmap *bitmap = bitmaps->parts;
128+
129+
bool size_did_change = bitmap->w != dim.w || bitmap->h != dim.h;
130+
if (size_did_change || sbr_renderer_did_change(ctx->sbr_renderer, &context, t)) {
131+
if(bitmaps->packed)
132+
talloc_free(bitmaps->packed);
133+
134+
bitmaps->packed = mp_image_alloc(IMGFMT_BGRA, dim.w, dim.h);
135+
bitmaps->packed_h = dim.h;
136+
bitmaps->packed_w = dim.w;
137+
bitmaps->packed->params.repr.alpha = PL_ALPHA_PREMULTIPLIED;
138+
++bitmaps->change_id;
139+
140+
bitmap->bitmap = bitmaps->packed->planes[0];
141+
bitmap->w = dim.w;
142+
bitmap->h = dim.h;
143+
bitmap->dw = dim.w;
144+
bitmap->dh = dim.h;
145+
// HACK: subrandr doesn't support stride, probably could add that though
146+
bitmaps->packed->stride[0] = dim.w * 4;
147+
bitmap->stride = (*bitmaps->packed).stride[0];
148+
149+
sbr_renderer_render(ctx->sbr_renderer, &context, ctx->sbr_subtitles, t, bitmap->bitmap, dim.w, dim.h);
150+
}
151+
152+
return sub_bitmaps_copy(NULL, bitmaps);
153+
}
154+
155+
static void uninit(struct sd *sd)
156+
{
157+
struct sd_sbr_priv *ctx = sd->priv;
158+
159+
if(ctx->sbr_renderer)
160+
sbr_renderer_destroy(ctx->sbr_renderer);
161+
if(ctx->sbr_subtitles)
162+
sbr_subtitles_destroy(ctx->sbr_subtitles);
163+
sbr_library_fini(ctx->sbr_library);
164+
}
165+
166+
static int control(struct sd *sd, enum sd_ctrl cmd, void *arg)
167+
{
168+
struct sd_sbr_priv *ctx = sd->priv;
169+
switch (cmd) {
170+
case SD_CTRL_SET_VIDEO_PARAMS:
171+
ctx->video_params = *(struct mp_image_params *)arg;
172+
return CONTROL_OK;
173+
default:
174+
return CONTROL_UNKNOWN;
175+
}
176+
}
177+
178+
const struct sd_functions sd_sbr = {
179+
.name = "subrandr",
180+
.accept_packets_in_advance = true,
181+
.init = init,
182+
.decode = decode,
183+
.get_bitmaps = get_bitmaps,
184+
.control = control,
185+
.select = enable_output,
186+
.uninit = uninit,
187+
};

0 commit comments

Comments
 (0)