diff --git a/DOCS/man/input.rst b/DOCS/man/input.rst index a4869969ed2dc..d813d912fa929 100644 --- a/DOCS/man/input.rst +++ b/DOCS/man/input.rst @@ -3951,6 +3951,12 @@ Property list somewhat weird form (apparently "hex BCD"), indicating the release version of the libass library linked to mpv. +``subrandr-version`` + The value of ``sbr_library_version()`` as a string in the format + ``..``, indicating the release version of the subrandr + library at runtime. This property is unavailable if mpv is not compiled + with subrandr enabled. + ``platform`` Returns a string describing what target platform mpv was built for. The value of this is dependent on what the underlying build system detects. Some of the diff --git a/ci/build-common.sh b/ci/build-common.sh index ef0a5f815ab81..2c3978aa62f38 100644 --- a/ci/build-common.sh +++ b/ci/build-common.sh @@ -3,4 +3,14 @@ common_args="--werror \ -Dtests=true \ " +build_subrandr() { + local target="$2" + local prefix="$1" + + git clone --depth=1 https://github.com/afishhh/subrandr.git + pushd subrandr + cargo xtask install ${target:+--target} $target --prefix "$prefix" + popd +} + export CFLAGS="$CFLAGS -Wno-error=deprecated -Wno-error=deprecated-declarations -U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3" diff --git a/ci/build-mingw64.sh b/ci/build-mingw64.sh index cd755b1a7f738..16366a8d09611 100755 --- a/ci/build-mingw64.sh +++ b/ci/build-mingw64.sh @@ -286,6 +286,11 @@ _luajit () { } _luajit_mark=lib/libluajit-5.1.a +_subrandr () { + RUSTFLAGS="-L$prefix_dir/lib" build_subrandr "$prefix_dir" "$RUST_TARGET" +} +_subrandr_mark=lib/libsubrandr.dll.a + for x in iconv zlib shaderc spirv-cross nv-headers dav1d lcms2; do build_if_missing $x done @@ -296,6 +301,9 @@ fi for x in ffmpeg libplacebo freetype fribidi harfbuzz libass luajit; do build_if_missing $x done +if [[ "$TARGET" != "i686-"* ]]; then + build_if_missing subrandr +fi ## mpv @@ -330,7 +338,7 @@ if [ "$2" = pack ]; then pushd artifact/tmp dlls=( libgcc_*.dll lib{ssp,stdc++,winpthread}-[0-9]*.dll # compiler runtime - av*.dll sw*.dll postproc-[0-9]*.dll lib{ass,freetype,fribidi,harfbuzz,iconv,placebo}-[0-9]*.dll + av*.dll sw*.dll {postproc,subrandr}-[0-9]*.dll lib{ass,freetype,fribidi,harfbuzz,iconv,placebo}-[0-9]*.dll lib{shaderc_shared,spirv-cross-c-shared,dav1d,lcms2}.dll zlib1.dll ) if [[ -f vulkan-1.dll ]]; then diff --git a/ci/build-msys2.sh b/ci/build-msys2.sh index 0bd74678e679e..13bb773a6e003 100755 --- a/ci/build-msys2.sh +++ b/ci/build-msys2.sh @@ -8,9 +8,18 @@ args=( -D{egl-angle-lib,egl-angle-win32,pdf-build,rubberband,win32-smtc}=enabled ) -[[ "$SYS" == "clang64" ]] && args+=( - -Db_sanitize=address,undefined -) +if [[ "$SYS" == "clang64" ]]; then + args+=( + -Db_sanitize=address,undefined + ) +else + # currently building with subrandr on clang64+asan + # causes a weird crash (https://github.com/msys2/MINGW-packages/issues/25267) + echo "::group::Building subrandr" + build_subrandr "/$SYS" + echo "::endgroup::" + args+=(-Dsubrandr=enabled) +fi meson setup build $common_args "${args[@]}" meson compile -C build diff --git a/ci/build-tumbleweed.sh b/ci/build-tumbleweed.sh index 557191a0f3b11..935d31dcd3d52 100755 --- a/ci/build-tumbleweed.sh +++ b/ci/build-tumbleweed.sh @@ -11,6 +11,7 @@ meson setup build $common_args $@ \ -Dlibarchive=enabled \ -Dmanpage-build=enabled \ -Dpipewire=enabled \ + -Dsubrandr=enabled \ -Dvulkan=enabled meson compile -C build ./build/mpv -v --no-config diff --git a/demux/demux.c b/demux/demux.c index 2bd88e8318562..480dc374f9c87 100644 --- a/demux/demux.c +++ b/demux/demux.c @@ -63,6 +63,9 @@ extern const demuxer_desc_t demuxer_desc_directory; extern const demuxer_desc_t demuxer_desc_disc; extern const demuxer_desc_t demuxer_desc_rar; extern const demuxer_desc_t demuxer_desc_libarchive; +#if HAVE_SUBRANDR +extern const demuxer_desc_t demuxer_desc_sbr; +#endif extern const demuxer_desc_t demuxer_desc_null; extern const demuxer_desc_t demuxer_desc_timeline; @@ -76,6 +79,9 @@ static const demuxer_desc_t *const demuxer_list[] = { &demuxer_desc_matroska, #if HAVE_LIBARCHIVE &demuxer_desc_libarchive, +#endif +#if HAVE_SUBRANDR + &demuxer_desc_sbr, #endif &demuxer_desc_lavf, &demuxer_desc_mf, diff --git a/demux/demux_sbr.c b/demux/demux_sbr.c new file mode 100644 index 0000000000000..0326b756175b4 --- /dev/null +++ b/demux/demux_sbr.c @@ -0,0 +1,173 @@ +/* + * 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 . + */ + +// This demuxer is specific to the `sd_sbr` subtitle driver and exists +// so that we can take subtitle files like .srv3 or .vtt in their +// full text form and pass them to subrandr in the subtitle driver. +// +// There are two reasons for this: +// - subrandr doesn't currently support parsing packetized streams like +// what is output by ffmpeg for WebVTT. +// - Demuxing .srv3 is not supported by ffmpeg, it also doesn't really have +// a standard packetized representation that ffmpeg could demux it to +// (that I know of). +// +// Note that for now this demuxer only recognizes srv3, this is to avoid +// regressing the behavior of other formats that were previously rendered +// with libass via ffmpeg conversion. + +#include +#include + +#include + +#include "common/common.h" +#include "demux/packet.h" +#include "misc/bstr.h" +#include "options/m_config.h" +#include "options/m_option.h" +#include "stream/stream.h" +#include "demux.h" + +#define OPT_BASE_STRUCT struct demux_sbr_opts +struct demux_sbr_opts { + int probesize; +}; + +const struct m_sub_options demux_sbr_conf = { + .opts = (const m_option_t[]) { + {"probesize", OPT_INT(probesize), M_RANGE(32, INT_MAX)}, + {0} + }, + .size = sizeof(struct demux_sbr_opts), + .defaults = &(const struct demux_sbr_opts){ + .probesize = 128, + }, + .change_flags = UPDATE_DEMUXER, +}; + +struct format_codec_info { + const char *codec; + const char *codec_desc; +}; + +static const struct format_codec_info fmt_to_codec[] = { + [SBR_SUBTITLE_FORMAT_UNKNOWN] = {NULL, NULL}, + [SBR_SUBTITLE_FORMAT_SRV3 ] = {"subrandr/srv3", "srv3"}, +}; + +struct sub_codec_ext { + const char *ext; + struct format_codec_info codec_info; +}; + +static const struct sub_codec_ext codec_exts[] = { + {".srv3", fmt_to_codec[SBR_SUBTITLE_FORMAT_SRV3]}, + {".ytt", fmt_to_codec[SBR_SUBTITLE_FORMAT_SRV3]}, + {NULL} +}; + +struct demux_sbr_priv { + bstr content; + bool exhausted; +}; + +static int demux_open_sbr(struct demuxer *demuxer, enum demux_check check) +{ + bstr filename = bstr0(demuxer->filename); + struct format_codec_info codec_info = {0}; + struct demux_sbr_opts *opts = mp_get_config_group(demuxer, demuxer->global, &demux_sbr_conf); + + for (const struct sub_codec_ext *ext = codec_exts; ext->ext; ++ext) { + if (bstr_endswith0(filename, ext->ext)) + codec_info = ext->codec_info; + } + + if (!codec_info.codec) { + int probe_size = stream_peek(demuxer->stream, opts->probesize); + uint8_t *probe_buffer = demuxer->stream->buffer; + + sbr_subtitle_format fmt = sbr_probe_text((const char *)probe_buffer, (size_t)probe_size); + if (fmt < MP_ARRAY_SIZE(fmt_to_codec)) + codec_info = fmt_to_codec[fmt]; + } + + if (check != DEMUX_CHECK_REQUEST && !codec_info.codec) + return -1; + + struct demux_sbr_priv *priv = talloc_zero(demuxer, struct demux_sbr_priv); + + priv->content = stream_read_complete(demuxer->stream, priv, 64 * 1024 * 1024); + if (priv->content.start == NULL) + return -1; + demuxer->priv = priv; + + struct sh_stream *stream = demux_alloc_sh_stream(STREAM_SUB); + stream->codec->codec = codec_info.codec; + stream->codec->codec_desc = codec_info.codec_desc; + demux_add_sh_stream(demuxer, stream); + + // Note that while in practice seeking on this stream is not possible, + // if `demuxer->seekable` is `false` then a warning is emitted when one + // tries to seek in the player which is undesirable. Therefore we mark + // it as seekable and make seeking a no-op instead. + demuxer->seekable = true; + demuxer->fully_read = true; + demux_close_stream(demuxer); + + return 0; +} + +static bool demux_read_packet_sbr(struct demuxer *demuxer, struct demux_packet **packet) +{ + struct demux_sbr_priv *priv = demuxer->priv; + + if (priv->exhausted) + return false; + + *packet = new_demux_packet_from(demuxer->packet_pool, priv->content.start, priv->content.len); + if (!*packet) + return true; + + (*packet)->stream = 0; + (*packet)->pts = 0.0; + (*packet)->sub_duration = INFINITY; + priv->exhausted = true; + + return true; +} + +static void demux_seek_sbr(struct demuxer *demuxer, double seek_pts, int flags) +{ + // We only ever emit one packet, no seeking needed or possible. +} + +static void demux_switched_tracks_sbr(struct demuxer *demuxer) +{ + struct demux_sbr_priv *ctx = demuxer->priv; + ctx->exhausted = false; +} + + +const struct demuxer_desc demuxer_desc_sbr = { + .name = "subrandr", + .desc = "subrandr text subtitle demuxer", + .open = demux_open_sbr, + .read_packet = demux_read_packet_sbr, + .seek = demux_seek_sbr, + .switched_tracks = demux_switched_tracks_sbr +}; diff --git a/meson.build b/meson.build index c63a7c464b380..95fb0323c2e16 100644 --- a/meson.build +++ b/meson.build @@ -358,6 +358,13 @@ if features['libdl'] dependencies += libdl endif +subrandr = dependency('subrandr', version: '>= 0.1.1', required: get_option('subrandr')) +features += {'subrandr': subrandr.found()} +if features['subrandr'] + sources += files('demux/demux_sbr.c', 'sub/sd_sbr.c') + dependencies += subrandr +endif + # C11 atomics are mandatory but linking to the library is not always required. dependencies += cc.find_library('atomic', required: false) diff --git a/meson.options b/meson.options index dae0a333ef71b..8839224185998 100644 --- a/meson.options +++ b/meson.options @@ -30,6 +30,7 @@ option('pthread-debug', type: 'feature', value: 'disabled', description: 'pthrea option('rubberband', type: 'feature', value: 'auto', description: 'librubberband support') option('sdl2', type: 'feature', value: 'disabled', description: 'SDL2') option('sdl2-gamepad', type: 'feature', value: 'auto', description: 'SDL2 gamepad input') +option('subrandr', type: 'feature', value: 'auto', description: 'subrandr support (SRV3 and WebVTT subtitle renderer)') option('uchardet', type: 'feature', value: 'auto', description: 'uchardet support') option('uwp', type: 'feature', value: 'disabled', description: 'Universal Windows Platform') option('vapoursynth', type: 'feature', value: 'auto', description: 'VapourSynth filter bridge') diff --git a/options/options.c b/options/options.c index 27c7f40313868..301c0593f6bb7 100644 --- a/options/options.c +++ b/options/options.c @@ -70,6 +70,9 @@ extern const struct m_sub_options demux_rawvideo_conf; extern const struct m_sub_options demux_playlist_conf; extern const struct m_sub_options demux_lavf_conf; extern const struct m_sub_options demux_mkv_conf; +#if HAVE_SUBRANDR +extern const struct m_sub_options demux_sbr_conf; +#endif extern const struct m_sub_options vd_lavc_conf; extern const struct m_sub_options ad_lavc_conf; extern const struct m_sub_options hwdec_conf; @@ -693,6 +696,9 @@ static const m_option_t mp_opts[] = { {"demuxer-rawvideo", OPT_SUBSTRUCT(demux_rawvideo, demux_rawvideo_conf)}, {"", OPT_SUBSTRUCT(demux_playlist, demux_playlist_conf)}, {"demuxer-mkv", OPT_SUBSTRUCT(demux_mkv, demux_mkv_conf)}, +#if HAVE_SUBRANDR + {"demuxer-sbr", OPT_SUBSTRUCT(demux_sbr, demux_sbr_conf)}, +#endif // ------------------------- subtitles options -------------------- @@ -1085,6 +1091,7 @@ static const struct MPOpts mp_default_opts = { "scc", "smi", "srt", + "srv3", "ssa", "sub", "sup", @@ -1092,6 +1099,7 @@ static const struct MPOpts mp_default_opts = { "utf-8", "utf8", "vtt", + "ytt", NULL }, diff --git a/options/options.h b/options/options.h index 6f16f50c25ecf..898285f51a3c1 100644 --- a/options/options.h +++ b/options/options.h @@ -365,6 +365,9 @@ typedef struct MPOpts { struct demux_playlist_opts *demux_playlist; struct demux_lavf_opts *demux_lavf; struct demux_mkv_opts *demux_mkv; +#if HAVE_SUBRANDR + struct demux_sbr_opts *demux_sbr; +#endif struct demux_opts *demux_opts; struct demux_cache_opts *demux_cache_opts; diff --git a/player/command.c b/player/command.c index ea0650d51997b..d12d4c596c027 100644 --- a/player/command.c +++ b/player/command.c @@ -25,7 +25,12 @@ #include #include +#include "config.h" // for HAVE_SUBRANDR + #include +#if HAVE_SUBRANDR +#include +#endif #include #include #include @@ -3748,6 +3753,19 @@ static int mp_property_libass_version(void *ctx, struct m_property *prop, return m_property_int64_ro(action, arg, ass_library_version()); } +static int mp_property_subrandr_version(void *ctx, struct m_property *prop, + int action, void *arg) +{ +#if HAVE_SUBRANDR + uint32_t major, minor, patch; + sbr_library_version(&major, &minor, &patch); + const char *result = mp_tprintf(33, "%" PRIu32 ".%" PRIu32 ".%" PRIu32, major, minor, patch); + return m_property_strdup_ro(action, arg, result); +#else + return M_PROPERTY_UNAVAILABLE; +#endif +} + static int mp_property_platform(void *ctx, struct m_property *prop, int action, void *arg) { @@ -4531,6 +4549,7 @@ static const struct m_property mp_properties_base[] = { {"mpv-configuration", mp_property_configuration}, {"ffmpeg-version", mp_property_ffmpeg}, {"libass-version", mp_property_libass_version}, + {"subrandr-version", mp_property_subrandr_version}, {"platform", mp_property_platform}, {"options", mp_property_options}, diff --git a/player/lua/ytdl_hook.lua b/player/lua/ytdl_hook.lua index ae5316a749de8..adcd612ae6597 100644 --- a/player/lua/ytdl_hook.lua +++ b/player/lua/ytdl_hook.lua @@ -88,6 +88,10 @@ local codec_map = { ["hev1%..*"] = "hevc", } +if mp.get_property_native("subrandr-version") ~= nil then + codec_map["srv3"] = "subrandr/srv3" +end + -- Codec name as reported by youtube-dl mapped to mpv internal codec names. -- Fun fact: mpv will not really use the codec, but will still try to initialize -- the codec on track selection (just to scrap it), meaning it's only a hint, diff --git a/sub/dec_sub.c b/sub/dec_sub.c index 118221a658ccd..3e32c86e02cfb 100644 --- a/sub/dec_sub.c +++ b/sub/dec_sub.c @@ -36,9 +36,15 @@ extern const struct sd_functions sd_ass; extern const struct sd_functions sd_lavc; +#if HAVE_SUBRANDR +extern const struct sd_functions sd_sbr; +#endif static const struct sd_functions *const sd_list[] = { &sd_lavc, +#if HAVE_SUBRANDR + &sd_sbr, +#endif &sd_ass, NULL }; diff --git a/sub/sd_sbr.c b/sub/sd_sbr.c new file mode 100644 index 0000000000000..b9dca5d4df291 --- /dev/null +++ b/sub/sd_sbr.c @@ -0,0 +1,236 @@ +/* + * 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 . + */ + +#include +#include + +#include +#include + +#include "mpv_talloc.h" + +#include "options/m_config.h" +#include "options/options.h" +#include "common/common.h" +#include "demux/packet_pool.h" +#include "demux/stheader.h" +#include "sub/osd.h" +#include "video/mp_image.h" +#include "sd.h" + +struct sd_sbr_priv { + struct sbr_library *sbr_library; + struct sbr_renderer *sbr_renderer; + struct sbr_subtitles *sbr_subtitles; + struct mp_osd_res osd; + struct sub_bitmaps *bitmaps; +}; + +static void enable_output(struct sd *sd, bool enable) +{ + struct sd_sbr_priv *ctx = sd->priv; + if (enable == !!ctx->sbr_renderer) + return; + if (ctx->sbr_renderer) { + sbr_renderer_destroy(ctx->sbr_renderer); + ctx->sbr_renderer = NULL; + } else { + ctx->sbr_renderer = sbr_renderer_create(ctx->sbr_library); + if (!ctx->sbr_renderer) { + const char *error = sbr_get_last_error_string(); + mp_err(sd->log, "Failed to create renderer: %s\n", error); + } + } +} + +static inline int mp_level_from_sbr_log_level(sbr_log_level level) +{ + switch (level) { + case SBR_LOG_LEVEL_TRACE: + return MSGL_TRACE; + case SBR_LOG_LEVEL_DEBUG: + return MSGL_DEBUG; + case SBR_LOG_LEVEL_INFO: // fallthrough + case SBR_LOG_LEVEL_WARN: + return MSGL_V; + case SBR_LOG_LEVEL_ERROR: // fallthrough + default: + return MSGL_WARN; + } +} + +static void mp_msg_sbr_log_callback(sbr_log_level level, + const char *source, size_t source_len, + const char *message, size_t message_len, + void *user_data) +{ + struct sd *sd = user_data; + int mp_level = mp_level_from_sbr_log_level(level); + + if (mp_msg_test(sd->log, mp_level)) { + if (source_len > 0) + mp_msg(sd->log, mp_level, "[%.*s] ", (int)source_len, source); + mp_msg(sd->log, mp_level, "%.*s\n", (int)message_len, message); + } +} + +static int init(struct sd *sd) +{ + if (!sd->codec->codec) + return -1; + if (strcmp(sd->codec->codec, "subrandr/srv3")) + return -1; + + sbr_library *library = sbr_library_init(); + if (!library) { + const char *error = sbr_get_last_error_string(); + mp_err(sd->log, "Failed to initialize library: %s\n", error); + return -1; + } + + struct sd_sbr_priv *ctx = talloc_zero(sd, struct sd_sbr_priv); + sd->priv = ctx; + + ctx->sbr_library = library; + sbr_library_set_log_callback(ctx->sbr_library, mp_msg_sbr_log_callback, sd); + + ctx->bitmaps = talloc_zero(ctx, struct sub_bitmaps); + ctx->bitmaps->format = SUBBITMAP_BGRA; + ctx->bitmaps->num_parts = 1; + ctx->bitmaps->parts = talloc_zero(ctx->bitmaps, struct sub_bitmap); + + enable_output(sd, true); + + return 0; +} + +static void decode(struct sd *sd, struct demux_packet *packet) +{ + struct sd_sbr_priv *ctx = sd->priv; + + if (ctx->sbr_subtitles) + sbr_subtitles_destroy(ctx->sbr_subtitles); + if (ctx->sbr_renderer) + sbr_renderer_set_subtitles(ctx->sbr_renderer, NULL); + + sbr_subtitle_format fmt = SBR_SUBTITLE_FORMAT_UNKNOWN; + if (!strcmp(sd->codec->codec, "subrandr/srv3") ) + fmt = SBR_SUBTITLE_FORMAT_SRV3; + + ctx->sbr_subtitles = sbr_load_text( + ctx->sbr_library, + packet->buffer, + packet->len, + fmt, + NULL + ); + packet->sub_duration = packet->duration; + + if (!ctx->sbr_subtitles) { + const char *error = sbr_get_last_error_string(); + mp_err(sd->log, "Failed to load subtitles: %s\n", error); + } +} + +static struct sub_bitmaps *get_bitmaps(struct sd *sd, struct mp_osd_res dim, + int format, double pts) +{ + struct sd_sbr_priv *ctx = sd->priv; + struct mp_subtitle_opts *opts = sd->opts; + + ctx->osd = dim; + + if (pts == MP_NOPTS_VALUE || !ctx->sbr_renderer || !ctx->sbr_subtitles) + return NULL; + + if (opts->sub_forced_events_only) + return NULL; + + struct sbr_subtitle_context context = (sbr_subtitle_context) { + .dpi = 72, + .padding_top = (int32_t)dim.mt << 6, + .padding_bottom = (int32_t)dim.mb << 6, + .padding_left = (int32_t)dim.ml << 6, + .padding_right = (int32_t)dim.mr << 6, + .video_height = (int32_t)(dim.h - dim.mt - dim.mb) << 6, + .video_width = (int32_t)(dim.w - dim.ml - dim.mr) << 6, + }; + + unsigned t = lrint(pts * 1000); + + struct sub_bitmaps *bitmaps = ctx->bitmaps; + struct sub_bitmap *bitmap = bitmaps->parts; + + bool size_did_change = bitmap->w != dim.w || bitmap->h != dim.h; + if (size_did_change || sbr_renderer_did_change(ctx->sbr_renderer, &context, t)) { + talloc_free(bitmaps->packed); + + bitmaps->packed = mp_image_alloc(IMGFMT_BGRA, dim.w, dim.h); + mp_require(bitmaps->packed); + bitmaps->packed_h = dim.h; + bitmaps->packed_w = dim.w; + bitmaps->packed->params.repr.alpha = PL_ALPHA_PREMULTIPLIED; + ++bitmaps->change_id; + + bitmap->bitmap = bitmaps->packed->planes[0]; + bitmap->w = dim.w; + bitmap->h = dim.h; + bitmap->dw = dim.w; + bitmap->dh = dim.h; + bitmap->stride = (*bitmaps->packed).stride[0]; + + sbr_renderer_set_subtitles(ctx->sbr_renderer, ctx->sbr_subtitles); + if (sbr_renderer_render(ctx->sbr_renderer, &context, t, bitmap->bitmap, + dim.w, dim.h, bitmap->stride >> 2) < 0) { + const char *error = sbr_get_last_error_string(); + mp_err(sd->log, "Failed to render frame: %s\n", error); + return NULL; + } + } + + return sub_bitmaps_copy(NULL, bitmaps); +} + +static void uninit(struct sd *sd) +{ + struct sd_sbr_priv *ctx = sd->priv; + + talloc_free(ctx->bitmaps->packed); + enable_output(sd, false); + if (ctx->sbr_subtitles) + sbr_subtitles_destroy(ctx->sbr_subtitles); + sbr_library_fini(ctx->sbr_library); +} + +static int control(struct sd *sd, enum sd_ctrl cmd, void *arg) +{ + switch (cmd) { + default: + return CONTROL_UNKNOWN; + } +} + +const struct sd_functions sd_sbr = { + .name = "subrandr", + .accept_packets_in_advance = true, + .init = init, + .decode = decode, + .get_bitmaps = get_bitmaps, + .control = control, + .select = enable_output, + .uninit = uninit, +};