fix(exoplayer): external sub attach, non-ass render

close #895
This commit is contained in:
edde746
2026-04-20 13:14:32 +02:00
parent f96342bd81
commit 65e19706f8
2 changed files with 110 additions and 37 deletions
@@ -476,12 +476,17 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
// SurfaceFlinger-layer-backed overlay that eglPresentationTimeANDROID can
// vsync-pin. Z-order: default video SurfaceView < this MediaOverlay-flagged
// SurfaceView < Flutter TextureView in the window.
//
// Inserted at child index 0 so the SurfaceView's transparent punch runs BEFORE
// SubtitleView's built-in CanvasSubtitleOutput child renders non-ASS cues.
// Appending would punch away already-drawn SRT/VTT text.
var assSubtitleSurfaceView: AssSubtitleSurfaceView? = null
subtitleView?.let { sv ->
val assView = AssSubtitleSurfaceView(sv.context, handler)
assSubtitleSurfaceView = assView
sv.addView(
assView,
0,
FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
@@ -1408,22 +1413,69 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
}
fun addSubtitleTrack(uri: String, title: String?, language: String?, mimeType: String?, select: Boolean) {
val index = externalSubtitles.size
val subtitleConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(uri))
.setId("external_$index")
.setLabel(title ?: "External")
.setLanguage(language)
.setMimeType(mimeType ?: detectSubtitleMimeType(uri))
.build()
val existingIndex = externalSubtitleUris.indexOf(uri)
val isNew = existingIndex < 0
val index = if (isNew) externalSubtitles.size else existingIndex
val formatId = "external_$index"
externalSubtitles.add(subtitleConfig)
externalSubtitleUris.add(uri)
if (isNew) {
// SELECTION_FLAG_DEFAULT marks this as the preferred text track so ExoPlayer's
// natural selection picks it on prepare. Avoids pinning the selector to a
// specific TrackGroup override — if the URL 404s (e.g. stale Plex stream key),
// ExoPlayer falls back to another available track (e.g. embedded SRT) instead
// of leaving text disabled.
val selectionFlags = if (select) C.SELECTION_FLAG_DEFAULT else 0
val subtitleConfig = MediaItem.SubtitleConfiguration.Builder(Uri.parse(uri))
.setId(formatId)
.setLabel(title ?: "External")
.setLanguage(language)
.setMimeType(mimeType ?: detectSubtitleMimeType(uri))
.setSelectionFlags(selectionFlags)
.build()
externalSubtitles.add(subtitleConfig)
externalSubtitleUris.add(uri)
}
// Note: ExoPlayer won't see these until the media is reloaded.
// On Android, external subs are normally passed at open() time.
// This path is only reached if the Flutter layer calls addSubtitleTrack
// while ExoPlayer (not MPV fallback) is active.
emitTrackList()
// Media3 only picks up MediaItem.SubtitleConfiguration at prepare() time
// (tracking issue androidx/media #1649). When the caller wants this subtitle
// activated immediately (e.g. after OpenSubtitles download), rebuild the
// MediaItem and re-prepare with the position preserved.
val player = exoPlayer
val mediaUri = currentMediaUri
if (select && player != null && mediaUri != null && !currentMediaIsLive) {
if (isNew) {
val savedPosition = player.currentPosition
val savedPlayWhenReady = player.playWhenReady
// Clear any stale text-type override (pointing at a pre-reload TrackGroup)
// and re-enable the text type — mirrors the reset done in open(). Without
// this, a previously-selected sub's override would either block the new
// DEFAULT-flagged sub from winning or, if the new sub fails to load, keep
// the text renderer stuck with no selection.
trackSelector?.let { selector ->
selector.parameters = selector.buildUponParameters()
.clearOverridesOfType(C.TRACK_TYPE_TEXT)
.setTrackTypeDisabled(C.TRACK_TYPE_TEXT, false)
.build()
}
selectedSubtitleTrackId = null
val mediaItem = buildMediaItem(mediaUri)
player.setMediaItem(mediaItem, savedPosition)
player.prepare()
player.playWhenReady = savedPlayWhenReady
} else {
// Already attached — select the existing track via override.
val trackId = subtitleTrackGroupMap.entries
.firstOrNull { (_, group) -> group.getFormat(0).id == formatId }
?.key
if (trackId != null) {
selectSubtitleTrack(trackId)
}
}
}
if (isNew) emitTrackList()
}
private fun detectSubtitleMimeType(uri: String): String {
+44 -23
View File
@@ -2471,41 +2471,62 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
Future<void> _onSubtitleDownloaded() async {
if (!mounted) return;
// Wait for the server to finish downloading the subtitle file
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
try {
final client = _getClientForMetadata();
final data = await client.getVideoPlaybackData(widget.metadata.ratingKey);
if (!mounted || data.mediaInfo == null) return;
final token = client.config.token;
if (token == null) return;
// Find external subtitle tracks from the refreshed metadata
// Plex's OpenSubtitles download is asynchronous: the PUT returns immediately
// but the new stream entry shows up in metadata seconds later. Poll until it
// appears. Up to 15s matches what Plex-web tolerates before giving up.
// Snapshot what's already attached so we can identify the new download.
final existingUris = widget.player.state.tracks.subtitle.where((t) => t.uri != null).map((t) => t.uri!).toSet();
for (final plexTrack in data.mediaInfo!.subtitleTracks) {
if (!plexTrack.isExternal) continue;
final url = plexTrack.getSubtitleUrl(client.config.baseUrl, token);
if (url == null) continue;
// Skip tracks already loaded in the player
if (existingUris.any((uri) => uri.contains(plexTrack.key!))) continue;
final deadline = DateTime.now().add(const Duration(seconds: 15));
PlexSubtitleTrack? newTrack;
String? newUrl;
PlexMediaInfo? latestInfo;
await widget.player.addSubtitleTrack(
uri: url,
title: plexTrack.displayTitle ?? plexTrack.language ?? 'Downloaded',
language: plexTrack.languageCode,
select: true,
);
while (mounted && DateTime.now().isBefore(deadline)) {
await Future.delayed(const Duration(seconds: 2));
if (!mounted) return;
// Save the selection on the server so it persists across sessions
final partId = data.mediaInfo!.partId;
if (partId != null) {
await client.selectStreams(partId, subtitleStreamID: plexTrack.id);
try {
final data = await client.getVideoPlaybackData(widget.metadata.ratingKey);
if (!mounted) return;
if (data.mediaInfo == null) continue;
latestInfo = data.mediaInfo;
for (final plexTrack in data.mediaInfo!.subtitleTracks) {
if (!plexTrack.isExternal) continue;
final url = plexTrack.getSubtitleUrl(client.config.baseUrl, token);
if (url == null) continue;
if (existingUris.any((uri) => uri.contains(plexTrack.key!))) continue;
newTrack = plexTrack;
newUrl = url;
break;
}
if (newTrack != null) break;
} catch (e) {
appLogger.w('Subtitle download poll iteration failed', error: e);
}
}
if (!mounted || newTrack == null || newUrl == null) return;
await widget.player.addSubtitleTrack(
uri: newUrl,
title: newTrack.displayTitle ?? newTrack.language ?? 'Downloaded',
language: newTrack.languageCode,
select: true,
);
final partId = latestInfo?.partId;
if (partId != null) {
await client.selectStreams(partId, subtitleStreamID: newTrack.id);
}
} catch (e) {
appLogger.w('Failed to refresh subtitles after download', error: e);
}