libass-android lifecycle issue: Fixes #1049 (much of it) (#1362)

Fixed a bug with libass integration where the background thread
responsible for drawing the ASS subtitles (AssRenderThread) has been
shut down, but ExoPlayer is still actively running and trying to push
video time updates to it. Added fallback for software rendering on older
android versions that can have broken OpenGl implementation.This should
address much of issue #1049.

## Description
Essentially, debug logs showed repeated:
3145 3920 W MessageQueue: Handler (android.os.Handler) {4ba3c72} sending
message to a Handler on a dead thread
3145 3920 W MessageQueue: java.lang.IllegalStateException: Handler
(android.os.Handler) {4ba3c72} sending message to a Handler on a dead
thread

Essentially this was a lifecycle bug where the libass-android thread was
created, but was detaching from the window, shutting down when it does
so. It was still registered to ExoPlayer, but since there was no thread
to send to, it just repeatedly kept failing, never spinning back up to
display any subtitles. It seems just about any ui change can cause this
detachment, but in this case it seems it was right from the moment the
loading spinner shows up (making libass-android fail before video starts
playing).

Fixed by ensuring TextureView's visibility wasn't changed to invisible,
which ends up destroying the canvas that libass-android attaches itself
to. Hid the canvas using graphicsLayer (so the user can't see it, but
it's still alive and active). Also changed resizing logic to prevent
Android from seeing a 0x0 TextureView as "destroyed".

Since I discovered in testing how terribly my TV's OpenGl was handling
this, I added an additional fallback for Android versions < 9. Older
TV's can fallback to software rendering when using libass-android.

### Related issues
#### Issue #1049 : 
**Fixes:**
- the issue where libass-android (when enabled) fails to load any
subtitles in ExoPlayer DirectPlay.

**Not Fixed from issue #1049 :**
- Issues around accented characters.
- Any issues surrounding MPV (I have no way of testing, MPV never worked
on any player for me)
- Fallback mode (libass-android disabled) where subtitles aren't
rendering properly

**Added:**
- While testing, discovered older Android versions have trouble with
reliable OpenGl rendering. Thus I added a fallback for older Android
versions to use libass-android in software rendering mode
(AssRenderType.OVERLAY_CANVAS)

### Testing
- Tested and confirmed flawlessly to work on Android 9 TCL Smart TV
(AssRenderType.OVERLAY_CANVAS)
- Tested on Android 14 Chromecast (AssRenderType.OVERLAY_OPEN_GL).
Slightly less perfect (font not always a match, less fancy shadows) but
most ASS/SSA effects work well. May be worth testing other AssRenderType
options to compare features and performance on other machines.

## AI or LLM usage
Google Gemini Pro in tracing down the cause and fine-tuning the fixes.

---------

Co-authored-by: Damontecres <damontecres@gmail.com>
This commit is contained in:
dudecuda 2026-05-11 17:49:08 -04:00 committed by GitHub
parent 8b9b0ddec8
commit 96f94feac2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 20 additions and 7 deletions

View file

@ -106,7 +106,13 @@ class PlayerFactory
.setExtensionRendererMode(rendererMode)
val mediaSourceFactory =
if (useLibAss) {
assHandler = AssHandler(AssRenderType.OVERLAY_OPEN_GL)
val renderType =
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
AssRenderType.OVERLAY_CANVAS
} else {
AssRenderType.OVERLAY_OPEN_GL
}
assHandler = AssHandler(renderType)
val assSubtitleParserFactory = AssSubtitleParserFactory(assHandler)
renderersFactory = AssRenderersFactory(assHandler, renderersFactory)
DefaultMediaSourceFactory(

View file

@ -439,6 +439,9 @@ fun PlaybackPageContent(
val subtitleMaxSize by animateFloatAsState(if (controllerViewState.controlsVisible) .7f else 1f)
val isImageSubtitles = remember(cues) { cues.firstOrNull()?.bitmap != null }
var cueCount by remember { mutableIntStateOf(0) }
val subtitleVisible = skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled && !presentationState.coverSurface
AndroidView(
factory = { context ->
SubtitleView(context).apply {
@ -465,10 +468,6 @@ fun PlaybackPageContent(
}
},
update = { subtitleView ->
val subtitleVisible =
skipIndicatorDuration == 0L && currentItemPlayback.subtitleIndexEnabled && !presentationState.coverSurface
subtitleView.visibility = if (subtitleVisible) View.VISIBLE else View.INVISIBLE
subtitleView.setCues(cues)
if (cues.size > cueCount) {
// The output creates a painter for each cue, so need to apply the changes when the number of cues increases
@ -480,7 +479,8 @@ fun PlaybackPageContent(
(it as? AssSubtitleView)?.apply {
val resized =
layoutParams.let { it.width != playerSurfaceSize.width || it.height != playerSurfaceSize.height }
if (resized) {
if (resized && playerSurfaceSize.width > 0 && playerSurfaceSize.height > 0) {
Timber.v("Resizing AssSubtitleView: %s", playerSurfaceSize)
layoutParams =
FrameLayout
@ -501,7 +501,14 @@ fun PlaybackPageContent(
.align(Alignment.TopCenter)
.background(Color.Transparent)
.graphicsLayer {
alpha = if (isImageSubtitles) subtitleImageOpacity else 1f
alpha =
if (!subtitleVisible) {
0f
} else if (isImageSubtitles) {
subtitleImageOpacity
} else {
1f
}
},
)
}