fix: preserve start position and subtitles in ExoPlayer→MPV fallback
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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)
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -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
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -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
|
||||||
// ============================================
|
// ============================================
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user