fix(Android): Render ASS subtitles on the native player - #1077
fix(Android): Render ASS subtitles on the native player#1077bilbofroggins wants to merge 1 commit into
Conversation
libass ships no font provider on Android, and ass-kt initialises the
renderer with
ass_set_fonts(renderer, NULL, "sans-serif", ASS_FONTPROVIDER_FONTCONFIG, NULL, 1)
requesting a provider that does not exist in that build and passing no
default font. libass therefore starts with zero fonts: every glyph lookup
fails, renderFrame() returns a frame containing no images, and
AssSubtitleParser only emits a cue from inside `frames?.images?.let { … }`.
The result is a subtitle track that is listed, selectable and never drawn,
with no error anywhere. SRT is unaffected because media3 renders it with
Android's own Typeface stack instead of libass.
Registering fonts is necessary but not sufficient. libass resolves a font
in a fixed order: the family the script asks for, then family_default,
then the provider's fallback hook, then a default font path. Script
families such as Arial are not present on the device, there is no
provider, and ass-kt cannot set a default font path, which leaves
family_default as the only reachable slot -- and it is hardcoded to
"sans-serif". AssFonts therefore rewrites the fallback font's sfnt name
table to declare that family. The replacement is shorter than the original
name, so the records are patched in place with no table resizing.
The fallback is assets/mp-font.ttf (Droid Sans Fallback), already shipped
for mpv's subtitleFontFile, so CJK is covered and the native player now
falls back to the same font as every other platform. Roboto and DroidSans
are registered under their real names so scripts naming them resolve
directly.
buildWithAssSupport() is inlined because it constructs the AssHandler
internally and never returns it, and that handler owns the Ass instance
fonts attach to. The wiring and render type are unchanged; a side benefit
is that the helper no longer silently replaces the configured
dataSourceFactory. ass-kt is declared explicitly because ass-media only
depends on it at runtime scope, leaving Ass off the compile classpath.
Verified on a Sony BRAVIA 4K VH2 (Android 12) with a direct-played mkv
carrying an embedded ASS track and no font attachments: libass now reports
`fontselect: (Arial, 700, 0) -> DroidSansFallback` and renders the
subtitles with their ASS colours and outlines intact.
Fixes DonutWare#919
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
This seems like a hacky-fix to what I'm not even sure is the exact problem. Ass fonts seem to render just fine on my Chromecast 4k. Is this specifically fixing external ass subtitles, or just fixing devices that are missing the required "default" fonts? |
The problem, at least for me, is not external ass subtitles - it's embedded ones. And I'm not sure about the device missing fonts but I think it's about how they are loaded. I'd be curious to know what type of media you are trying to play. Sometimes the fonts are included into the subtitles, which might be the difference we're seeing. I was testing with this video, which has english subs built-in: |
Problem
ASS/SSA subtitles never render on the Android TV / leanback player. The track appears in the picker, can be selected, and simply draws nothing — no error, no crash, nothing in logs. The same file plays with subtitles correctly on desktop, on web, and even in the same app on the same device in tablet layout.
Fixes #919.
Why only Android TV
PlayerOptions.available(lib/models/settings/video_player_settings.dart:177) returns onlynativePlayerin leanback mode, so TV playback runs throughVideoPlayerActivity→android/.../player/ExoPlayer.ktand renders ASS with libass viaio.github.peerless2012:ass-media. Every other platform uses mpv/mdk. Three different subtitle renderers, and only the libass-on-Android one was broken.Root cause
libass is built for Android without a font provider, and
ass-ktinitialises the renderer with:It requests the fontconfig provider, which does not exist in that build, and passes no default font path. libass therefore starts with zero fonts:
The failure is silent because of where it lands in the library.
AssSubtitleParser.parse()rasterises each event and only emits a cue from insideframes?.images?.let { … }. With no font,renderFrame()returns a frame containing no images, so that block never runs and not one cue is ever handed to ExoPlayer.SRT is unaffected: media3 renders it with Android's own
Typefacestack and never touches libass. That asymmetry is what makes the bug look mysterious from the outside.Why registering fonts isn't enough on its own
libass resolves a font in a fixed order (
ass_font_select,ass_fontselect.c):Arial,Nirmala UI, … not on the devicefamily_default"sans-serif", per the call aboveget_fallbackpath_defaultNULL, andass-ktcannot set itSteps 1, 3 and 4 are unreachable from app code, so a font registered as
Droid Sans Fallbackis loaded and then never looked at. Step 2 is the only lever — which means the fallback font has to literally be namedsans-serif.The fix
AssFonts.install()registers fonts throughAss.addFont()and rewrites the fallback font's sfntnametable so its family readssans-serif."sans-serif"is shorter than"Droid Sans Fallback", so the records are patched in place — no table resizing, no offset changes, file size unchanged.The fallback is
assets/mp-font.ttf(Droid Sans Fallback), already shipped for mpv'ssubtitleFontFile(lib/wrappers/players/base_player.dart:12), so CJK is covered and the native player now falls back to the same font as every other platform.RobotoandDroidSansare also registered under their real names so scripts naming them resolve at step 1.Two supporting changes:
buildWithAssSupport()is inlined inExoPlayer.kt. It constructs theAssHandlerinternally and never returns it, and that handler owns theAssinstance fonts attach to. Same wiring, same render type — and as a side benefit the helper no longer silently replaces the configureddataSourceFactorywith a plainDefaultDataSource.Factory.ass-ktis declared explicitly inbuild.gradle, becauseass-mediaonly depends on it at runtime scope, leavingAssoff the compile classpath.Testing
Verified on a Sony BRAVIA 4K VH2 (Android 12,
leanback_only, armeabi-v7a) with a direct-played mkv carrying an embedded ASS track and no font attachments — the case that was previously guaranteed to fail:Subtitles render with their ASS styling intact — the source's
{\c&H00D8FF&\3c&H054D8B&}shows up as gold text with a dark blue outline, so colours and outlines survive rather than degrading to plain text. An SRT control cut of the same file was checked before and after to confirm no regression on the non-libass path.Related, but deliberately not claimed
ExoPlayer.kt. This PR is deliberately narrower: one root cause, independently verified on hardware. Happy to rebase around whichever lands first.AssRenderType.LEGACYsizes libass's frame to the video dimensions rather than the display; moving to the overlay renderer would address it.VideoPlayerImplementation.open()only side-loads tracks withexternal=true, which is false for a stream embedded in the source. It is dropped before libass is involved. This fix is a prerequisite for that working, not a fix for it.Upstream notes
ass-kt: requesting a provider that does not exist in the Android build while passing no default font makes libass unusable unless the host app happens to register a font namedsans-serif. Worth filing upstream; this workaround can be dropped ifass_set_fontsever becomes configurable.ass-mediais pinned to 0.3.0. Font attachments embedded in an mkv also appear not to load on that version (a test file with a muxed font rendered nothing); Fix font loading when attachments precede tracks in MKV peerless2012/libass-android#63 "Fix font loading when attachments precede tracks in MKV" shipped in 0.4.0. A version bump is worth a separate PR.🤖 Generated with Claude Code