diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index b572b809..5a29acc6 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -753,19 +753,6 @@ class VideoPlayerScreenState extends State _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(); diff --git a/lib/services/video_filter_manager.dart b/lib/services/video_filter_manager.dart index a0b0500c..bdb3a869 100644 --- a/lib/services/video_filter_manager.dart +++ b/lib/services/video_filter_manager.dart @@ -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? _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 _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();