Skip to content

fix(Android): Render ASS subtitles on the native player - #1077

Open
bilbofroggins wants to merge 1 commit into
DonutWare:developfrom
bilbofroggins:fix/android-tv-ass-subtitle-fonts
Open

fix(Android): Render ASS subtitles on the native player#1077
bilbofroggins wants to merge 1 commit into
DonutWare:developfrom
bilbofroggins:fix/android-tv-ass-subtitle-fonts

Conversation

@bilbofroggins

Copy link
Copy Markdown

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 only nativePlayer in leanback mode, so TV playback runs through VideoPlayerActivityandroid/.../player/ExoPlayer.kt and renders ASS with libass via io.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-kt initialises the renderer with:

// lib_ass_kt/src/main/cpp/AssKt.c
ass_set_fonts(assRenderer, NULL, "sans-serif", ASS_FONTPROVIDER_FONTCONFIG, NULL, 1);

It requests the fontconfig provider, which does not exist in that build, and passes no default font path. libass therefore starts with zero fonts:

W SubtitleRenderer: can't find selected font provider
W SubtitleRenderer: fontselect: failed to find any fallback with glyph 0x0 for font: (Arial, 700, 0)

The failure is silent because of where it lands in the library. AssSubtitleParser.parse() rasterises each event and only emits a cue from inside frames?.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 Typeface stack 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):

# Step Result here
1 family the script asks for Arial, Nirmala UI, … not on the device
2 family_default "sans-serif", per the call above
3 provider's get_fallback no provider exists
4 path_default NULL, and ass-kt cannot set it

Steps 1, 3 and 4 are unreachable from app code, so a font registered as Droid Sans Fallback is loaded and then never looked at. Step 2 is the only lever — which means the fallback font has to literally be named sans-serif.

The fix

AssFonts.install() registers fonts through Ass.addFont() and rewrites the fallback font's sfnt name table so its family reads sans-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's subtitleFontFile (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. Roboto and DroidSans are also registered under their real names so scripts naming them resolve at step 1.

Two supporting changes:

  • buildWithAssSupport() is inlined in ExoPlayer.kt. It constructs the AssHandler internally and never returns it, and that handler owns the Ass instance fonts attach to. Same wiring, same render type — and as a side benefit the helper no longer silently replaces the configured dataSourceFactory with a plain DefaultDataSource.Factory.
  • ass-kt is declared explicitly in build.gradle, because ass-media only depends on it at runtime scope, leaving Ass off 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:

fontselect: Using default font family: (Arial, 700, 0) -> DroidSansFallback
fontselect: (Nirmala UI, 700, 0) -> DroidSansFallback

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

Upstream notes

  • The real defect is in 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 named sans-serif. Worth filing upstream; this workaround can be dropped if ass_set_fonts ever becomes configurable.
  • ass-media is 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

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>
@PartyDonut

Copy link
Copy Markdown
Collaborator

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?

@bilbofroggins

Copy link
Copy Markdown
Author

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:
yt-dlp 'https://archive.org/details/os-038-k-38-cyber-soldier-polygon/OS001_K01+-+Pok%C3%A9mon!+I+Choose+You!.mkv'

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

🐛 External ASS subtitles aren't playing on Android TV

2 participants