From 96f94feac26edfc269cb8101e3d2511617e5a55f Mon Sep 17 00:00:00 2001 From: dudecuda <94335388+dudecuda@users.noreply.github.com> Date: Mon, 11 May 2026 17:49:08 -0400 Subject: [PATCH] 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 --- .../wholphin/services/PlayerFactory.kt | 8 +++++++- .../wholphin/ui/playback/PlaybackPage.kt | 19 +++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt index da00fd3d..7f4886b4 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/services/PlayerFactory.kt @@ -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( diff --git a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt index e0e24f4c..5e748396 100644 --- a/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt +++ b/app/src/main/java/com/github/damontecres/wholphin/ui/playback/PlaybackPage.kt @@ -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 + } }, ) }