fix: preserve start position and subtitles in ExoPlayer→MPV fallback

This commit is contained in:
edde746
2026-03-15 11:32:25 +01:00
parent 360c3356cf
commit 0100b2a2ea
6 changed files with 44 additions and 27 deletions
@@ -143,6 +143,8 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
// Track state for event emission // Track state for event emission
private var lastPosition: Long = 0 private var lastPosition: Long = 0
/** Position to use for fallback: max of current position and pending start position. */
private val effectivePosition: Long get() = maxOf(lastPosition, pendingStartPositionMs)
private var lastDuration: Long = 0 private var lastDuration: Long = 0
private var lastBufferedPosition: Long = 0 private var lastBufferedPosition: Long = 0
private var positionUpdateRunnable: Runnable? = null private var positionUpdateRunnable: Runnable? = null
@@ -645,7 +647,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
delegate?.onFormatUnsupported( delegate?.onFormatUnsupported(
uri = currentMediaUri!!, uri = currentMediaUri!!,
headers = currentHeaders, headers = currentHeaders,
positionMs = lastPosition, positionMs = effectivePosition,
errorMessage = "Video track present but no decoder available" errorMessage = "Video track present but no decoder available"
) )
return return
@@ -671,7 +673,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
val handled = delegate?.onFormatUnsupported( val handled = delegate?.onFormatUnsupported(
uri = currentMediaUri!!, uri = currentMediaUri!!,
headers = currentHeaders, headers = currentHeaders,
positionMs = lastPosition, positionMs = effectivePosition,
errorMessage = error.message ?: "Unknown error" errorMessage = error.message ?: "Unknown error"
) ?: false ) ?: false
@@ -1069,7 +1071,7 @@ class ExoPlayerCore(private val activity: Activity) : Player.Listener {
delegate?.onFormatUnsupported( delegate?.onFormatUnsupported(
uri = uri, uri = uri,
headers = currentHeaders, headers = currentHeaders,
positionMs = lastPosition, positionMs = effectivePosition,
errorMessage = "Decoder hang: $decoderName accepted input but produced no output" errorMessage = "Decoder hang: $decoderName accepted input but produced no output"
) )
} }
@@ -211,8 +211,11 @@ class ExoPlayerPlugin : FlutterPlugin, MethodChannel.MethodCallHandler,
return return
} }
// New media = fresh slate for pending MPV properties // Only clear pending MPV properties when MPV is the active backend.
pendingMpvProperties.clear() // When ExoPlayer is active, keep them for potential ExoPlayer→MPV fallback.
if (usingMpvFallback) {
pendingMpvProperties.clear()
}
activity?.runOnUiThread { activity?.runOnUiThread {
if (usingMpvFallback) { if (usingMpvFallback) {
+10
View File
@@ -162,6 +162,16 @@ abstract class Player {
/// [args] - Command and arguments as a list of strings. /// [args] - Command and arguments as a list of strings.
Future<void> command(List<String> args); Future<void> command(List<String> args);
// ============================================
// Subtitle Fonts
// ============================================
/// Configure subtitle fonts for libass rendering.
///
/// Extracts a comprehensive Unicode font (Go Noto) to the cache directory
/// and sets `sub-fonts-dir` and `sub-font` properties.
Future<void> configureSubtitleFonts();
// ============================================ // ============================================
// Passthrough Mode (Audio) // Passthrough Mode (Audio)
// ============================================ // ============================================
+23
View File
@@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart' show protected;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../font_loader.dart';
import '../models.dart'; import '../models.dart';
import 'player.dart'; import 'player.dart';
import 'player_state.dart'; import 'player_state.dart';
@@ -550,6 +551,28 @@ abstract class PlayerBase with PlayerStreamControllersMixin implements Player {
// ignore: no-empty-block - base no-op, overridden by platform subclasses // ignore: no-empty-block - base no-op, overridden by platform subclasses
Future<void> setLogLevel(String level) async {} Future<void> setLogLevel(String level) async {}
// ============================================
// Subtitle Fonts
// ============================================
@override
Future<void> configureSubtitleFonts() async {
try {
final fontDir = await SubtitleFontLoader.loadSubtitleFont();
if (fontDir != null) {
await setProperty('sub-fonts-dir', fontDir);
await setProperty('sub-font', SubtitleFontLoader.fontName);
}
} catch (e) {
// Font configuration is not critical - continue without it
logController.add(PlayerLog(
prefix: 'fonts',
level: PlayerLogLevel.warn,
text: 'Failed to configure subtitle fonts: $e',
));
}
}
// ============================================ // ============================================
// Lifecycle // Lifecycle
// ============================================ // ============================================
-22
View File
@@ -2,7 +2,6 @@ import 'dart:io' show Platform;
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import '../font_loader.dart';
import '../models.dart'; import '../models.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import 'player_base.dart'; import 'player_base.dart';
@@ -55,9 +54,6 @@ class PlayerNative extends PlayerBase {
throw Exception('Failed to initialize player'); throw Exception('Failed to initialize player');
} }
// Configure subtitle fonts for libass support
await _configureSubtitleFonts();
// Subscribe to MPV properties // Subscribe to MPV properties
await observeProperty('time-pos', 'double'); await observeProperty('time-pos', 'double');
await observeProperty('duration', 'double'); await observeProperty('duration', 'double');
@@ -80,24 +76,6 @@ class PlayerNative extends PlayerBase {
} }
} }
/// Configures subtitle fonts for libass support.
/// Provides a comprehensive Unicode font (Go Noto) with CJK coverage to ensure
/// proper rendering of non-Latin characters in subtitles.
Future<void> _configureSubtitleFonts() async {
try {
final fontDir = await SubtitleFontLoader.loadSubtitleFont();
if (fontDir != null) {
// Configure MPV to use the extracted font for libass
await setProperty('config', 'yes');
await setProperty('sub-fonts-dir', fontDir);
await setProperty('sub-font', SubtitleFontLoader.fontName);
}
} catch (e) {
// Font configuration is not critical - continue without it
errorController.add('Failed to configure subtitle fonts: $e');
}
}
// ============================================ // ============================================
// Playback Control // Playback Control
// ============================================ // ============================================
+1
View File
@@ -413,6 +413,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> with WidgetsBindin
// Create player (on Android, uses ExoPlayer by default, MPV as fallback) // Create player (on Android, uses ExoPlayer by default, MPV as fallback)
player = Player(useExoPlayer: useExoPlayer); player = Player(useExoPlayer: useExoPlayer);
await player!.configureSubtitleFonts();
await player!.setProperty('sub-ass', 'yes'); // Enable libass await player!.setProperty('sub-ass', 'yes'); // Enable libass
if (Platform.isAndroid && useExoPlayer) { if (Platform.isAndroid && useExoPlayer) {
final tunneledPlayback = settingsService.getTunneledPlayback(); final tunneledPlayback = settingsService.getTunneledPlayback();