refactor: simplify fill mode

This commit is contained in:
edde746
2025-12-03 18:31:52 +01:00
parent b1dd33ad7a
commit a71354d9c5
2 changed files with 15 additions and 190 deletions
-13
View File
@@ -753,19 +753,6 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen>
_mediaControlsManager?.clear();
_mediaControlsManager?.dispose();
// Clear video filter and reset subtitle margins before disposing player
try {
if (player != null) {
player!.setProperty('vf', '');
player!.setProperty('sub-margin-x', '0');
player!.setProperty('sub-margin-y', '0');
player!.setProperty('sub-scale', '1.0');
}
} catch (e) {
// Non-critical: Cleanup operations during disposal
appLogger.d('Error during player cleanup in dispose', error: e);
}
// Restore system UI and orientation preferences (skip if navigating to another video)
if (!_isReplacingWithVideo) {
OrientationHelper.restoreSystemUI();
+15 -177
View File
@@ -27,8 +27,6 @@ class VideoFilterManager {
/// Current player viewport size
Size? _playerSize;
/// Current video dimensions
Size? _videoSize;
/// Debounced video filter update with leading edge execution
late final Debounce _debouncedUpdateVideoFilter;
@@ -89,187 +87,34 @@ class VideoFilterManager {
}
}
/// Calculates crop parameters for "fill screen" mode (BoxFit.cover) to eliminate letterboxing.
///
/// This method is only active when [_boxFitMode] == 1 (cover mode). It determines how to
/// crop the video to completely fill the player area while maintaining aspect ratio.
///
/// **How it works:**
/// 1. Compares video aspect ratio vs player aspect ratio
/// 2. Crops the dimension that would create letterboxing:
/// - Wide video (16:9) on tall player (4:3): crops left/right sides
/// - Tall video (4:3) on wide player (16:9): crops top/bottom
/// 3. Centers the crop within the video
/// 4. Calculates subtitle margin adjustments to keep subtitles visible
///
/// **Subtitle positioning:**
/// MPV uses a 720p reference coordinate system for subtitle positioning.
/// When cropping zooms the video, subtitles need larger margins to avoid
/// being cropped or appearing too close to edges.
///
/// Returns `null` if:
/// - Not in cover mode (_boxFitMode != 1)
/// - Player or video size is unknown
/// - Aspect ratios are too similar (< 0.01 difference) - no crop needed
///
/// Returns a map containing:
/// - `width`, `height`: Dimensions of the cropped area in video pixels
/// - `x`, `y`: Crop offset from video's top-left corner in pixels
/// - `subMarginX`, `subMarginY`: Subtitle margins in MPV coordinate space (720p reference)
/// - `subScale`: Subtitle scaling factor (currently always 1.0)
Map<String, dynamic>? _calculateCropParameters() {
// Only calculate for cover mode with known dimensions
if (_boxFitMode != 1 || _playerSize == null || _videoSize == null) {
return null;
}
final playerAspect = _playerSize!.width / _playerSize!.height;
final videoAspect = _videoSize!.width / _videoSize!.height;
// No cropping needed if aspect ratios are very similar
if ((playerAspect - videoAspect).abs() < 0.01) return null;
late final int cropW, cropH, cropX, cropY;
if (videoAspect > playerAspect) {
// Video is wider than player - crop left/right sides
// Example: 16:9 video in 4:3 player
final scale = _playerSize!.height / _videoSize!.height;
cropH = _videoSize!.height.toInt();
cropW = (_playerSize!.width / scale).toInt();
cropX = ((_videoSize!.width - cropW) ~/ 2); // Center horizontally
cropY = 0;
} else {
// Video is taller than player - crop top/bottom
// Example: 4:3 video in 16:9 player (most common case)
final scale = _playerSize!.width / _videoSize!.width;
cropW = _videoSize!.width.toInt();
cropH = (_playerSize!.height / scale).toInt();
cropX = 0;
cropY = ((_videoSize!.height - cropH) ~/ 2); // Center vertically
}
// Subtitle positioning constants
/// MPV's subtitle coordinate system height (720p reference)
const double kSubCoord = 720.0;
/// Base horizontal subtitle margin to prevent edge clipping
const double baseX = 20.0;
/// Base vertical subtitle margin, tuned to position subtitles
/// comfortably above the bottom while avoiding overscan areas
const double baseY = 45.0;
// Calculate additional margin needed due to cropping
// When we crop, the visible area is "zoomed in", so subtitles need
// proportionally larger margins to maintain the same visual distance from edges
double extraX = cropX > 0
? (cropX / _videoSize!.width) * kSubCoord * videoAspect
: 0.0;
double extraY = cropY > 0 ? (cropY / _videoSize!.height) * kSubCoord : 0.0;
// Apply additional margin (never reduce below base)
int marginX = (baseX + extraX).round();
int marginY = (baseY + extraY).round();
return {
'width': cropW,
'height': cropH,
'x': cropX,
'y': cropY,
'subMarginX': marginX,
'subMarginY': marginY,
'subScale': 1.0,
};
}
/// Get video dimensions from the currently selected media version
Size? _getCurrentVideoSize() {
if (availableVersions.isEmpty ||
selectedMediaIndex >= availableVersions.length) {
return null;
}
final currentVersion = availableVersions[selectedMediaIndex];
if (currentVersion.width != null && currentVersion.height != null) {
return Size(
currentVersion.width!.toDouble(),
currentVersion.height!.toDouble(),
);
}
return null;
}
/// Update the video filter based on current crop mode
/// Update the video scaling and positioning based on current display mode
void updateVideoFilter() async {
try {
// Clear all video filters and manual scaling first
await player.setProperty('video-aspect-override', 'no');
if (_boxFitMode == 1) {
// Cover mode - apply crop filter to fill screen while maintaining aspect ratio
_videoSize = _getCurrentVideoSize();
final cropParams = _calculateCropParameters();
// Reset aspect override (may have been set by stretch mode)
await player.setProperty('video-aspect-override', 'no');
if (cropParams != null) {
final cropFilter =
'crop=${cropParams['width']}:${cropParams['height']}:${cropParams['x']}:${cropParams['y']}';
appLogger.d(
'Applying video filter: $cropFilter (player: $_playerSize, video: $_videoSize)',
);
// Apply crop filter
await player.setProperty('vf', cropFilter);
// Apply subtitle margins and scaling to compensate for crop zoom
final subMarginX = cropParams['subMarginX']!;
final subMarginY = cropParams['subMarginY']!;
final subScale = cropParams['subScale']!;
appLogger.d(
'Applying subtitle properties - margins: x=$subMarginX, y=$subMarginY, scale=$subScale',
);
await player.setProperty('sub-margin-x', subMarginX.toString());
await player.setProperty('sub-margin-y', subMarginY.toString());
await player.setProperty('sub-scale', subScale.toString());
} else {
// Clear filter but apply base margins if no cropping needed
appLogger.d(
'Clearing video filter - aspect ratios similar, applying base margins (player: $_playerSize, video: $_videoSize)',
);
await player.setProperty('vf', '');
await _applyBaseSubtitleMargins();
}
// Cover mode - use panscan to fill screen while maintaining aspect ratio
await player.setProperty('panscan', '1.0');
appLogger.d('Cover mode: panscan=1.0 (player: $_playerSize)');
} else if (_boxFitMode == 2) {
// Stretch/fill mode - override aspect ratio to match player
appLogger.d(
'Applying stretch mode - BoxFit mode $_boxFitMode (player: $_playerSize)',
);
await player.setProperty('vf', '');
// Override video aspect ratio to match player aspect ratio (stretches video)
// Fill/stretch mode - override aspect ratio to match player (stretches video)
await player.setProperty('panscan', '0');
if (_playerSize != null) {
final playerAspect = _playerSize!.width / _playerSize!.height;
await player.setProperty(
'video-aspect-override',
playerAspect.toString(),
);
appLogger.d(
'Stretch mode: aspect-override=$playerAspect (player: $_playerSize)',
);
}
await _applyBaseSubtitleMargins();
} else {
// Contain mode (0) - clear video filter and reset aspect ratio
appLogger.d(
'Clearing video filter, applying base margins - BoxFit mode $_boxFitMode',
);
await player.setProperty('vf', '');
await player.setProperty(
'video-aspect-override',
'no',
); // Reset to original aspect
await _applyBaseSubtitleMargins();
// Contain mode (0) - letterbox/pillarbox, no scaling
await player.setProperty('panscan', '0');
appLogger.d('Contain mode: panscan=0 (player: $_playerSize)');
}
} catch (e) {
appLogger.w('Failed to update video filter', error: e);
@@ -281,13 +126,6 @@ class VideoFilterManager {
/// subsequent calls within 50ms are debounced.
void debouncedUpdateVideoFilter() => _debouncedUpdateVideoFilter();
/// Apply base subtitle margins (used when no custom crop margins needed)
Future<void> _applyBaseSubtitleMargins() async {
await player.setProperty('sub-margin-x', '20');
await player.setProperty('sub-margin-y', '40');
await player.setProperty('sub-scale', '1.0');
}
/// Clean up resources
void dispose() {
_debouncedUpdateVideoFilter.cancel();