diff --git a/lib/screens/video_player_screen.dart b/lib/screens/video_player_screen.dart index 7d5a5826..2dd6e7ff 100644 --- a/lib/screens/video_player_screen.dart +++ b/lib/screens/video_player_screen.dart @@ -68,6 +68,11 @@ class VideoPlayerScreenState extends State { int _boxFitMode = 0; bool _isPinching = false; // Track if a pinch gesture is occurring + // Video cropping state for fill screen mode + Size? _playerSize; + Size? _videoSize; + Timer? _resizeDebounceTimer; + @override void initState() { super.initState(); @@ -108,6 +113,11 @@ class VideoPlayerScreenState extends State { appLogger.w('Failed to determine device type', error: e); _isPhone = false; // Default to tablet/desktop (all orientations) } + + // Update video filter when dependencies change (orientation, screen size, etc.) + WidgetsBinding.instance.addPostFrameCallback((_) { + _debouncedUpdateVideoFilter(); + }); } Future _initializePlayer() async { @@ -137,6 +147,7 @@ class VideoPlayerScreenState extends State { 'sub-border-color': settingsService.getSubtitleBorderColor(), 'sub-back-color': '#${(settingsService.getSubtitleBackgroundOpacity() * 255 / 100).toInt().toRadixString(16).padLeft(2, '0').toUpperCase()}${settingsService.getSubtitleBackgroundColor().replaceFirst('#', '')}', + 'sub-ass-override': 'yes', }, ), ); @@ -316,6 +327,8 @@ class VideoPlayerScreenState extends State { setState(() { _availableVersions = playbackData.availableVersions; }); + // Update video filter once dimensions are available + _updateVideoFilter(); } // Build list of external subtitle tracks for media_kit @@ -440,6 +453,7 @@ class VideoPlayerScreenState extends State { setState(() { _boxFitMode = (_boxFitMode + 1) % 3; }); + _updateVideoFilter(); } /// Toggle between contain and cover modes only (for pinch gesture) @@ -447,6 +461,7 @@ class VideoPlayerScreenState extends State { setState(() { _boxFitMode = _boxFitMode == 0 ? 1 : 0; }); + _updateVideoFilter(); } /// Get current BoxFit based on mode @@ -463,11 +478,183 @@ class VideoPlayerScreenState extends State { } } + /// Calculate crop parameters to match what BoxFit.cover will show + Map? _calculateCropParameters() { + if (_boxFitMode != 1 || _playerSize == null || _videoSize == null) { + return null; + } + + final playerAspectRatio = _playerSize!.width / _playerSize!.height; + final videoAspectRatio = _videoSize!.width / _videoSize!.height; + + // No cropping needed if aspect ratios are very similar + if ((playerAspectRatio - videoAspectRatio).abs() < 0.01) { + return null; + } + + int cropWidth, cropHeight, cropX, cropY; + + // BoxFit.cover scales the video to fill the container, cropping the excess + // We need to crop the video to match what will actually be visible + if (videoAspectRatio > playerAspectRatio) { + // Video is wider than player - BoxFit.cover will crop horizontally + // Scale video height to match player height, then crop the width + final scale = _playerSize!.height / _videoSize!.height; + + cropHeight = _videoSize!.height.toInt(); + cropWidth = (_playerSize!.width / scale).toInt(); + cropX = ((_videoSize!.width - cropWidth) / 2).toInt(); + cropY = 0; + } else { + // Video is taller than player - BoxFit.cover will crop vertically + // Scale video width to match player width, then crop the height + final scale = _playerSize!.width / _videoSize!.width; + + cropWidth = _videoSize!.width.toInt(); + cropHeight = (_playerSize!.height / scale).toInt(); + cropX = 0; + cropY = ((_videoSize!.height - cropHeight) / 2).toInt(); + } + + // Calculate subtitle margins to prevent text subtitles from appearing in cropped areas + // MPV subtitle coordinates use a normalized system where video height = 720 + const double subCoordinateHeight = 720.0; + const double baseMarginY = + 40.0; // Base margin from bottom edge (subtitle coordinates) + const double baseMarginX = + 20.0; // Base margin from sides (subtitle coordinates) + + double subMarginX = baseMarginX; + double subMarginY = baseMarginY; + double subScale = 1.0; // Default scale + + if (videoAspectRatio > playerAspectRatio) { + // Horizontal crop - need additional horizontal margins and scaling + // Convert pixel margin to subtitle coordinate system + final subCoordinateWidth = subCoordinateHeight * videoAspectRatio; + final cropMarginX = (cropX / _videoSize!.width) * subCoordinateWidth; + + // Calculate scale factor first + subScale = cropWidth / _videoSize!.width; + + // Apply margin accounting for scaling (scaled margins are effectively larger) + subMarginX = (baseMarginX + cropMarginX) / subScale; + } else { + // Vertical crop - need additional vertical margins and scaling + // Convert pixel margin to subtitle coordinate system + final cropMarginY = (cropY / _videoSize!.height) * subCoordinateHeight; + + // Calculate scale factor first + subScale = cropHeight / _videoSize!.height; + + // Apply margin accounting for scaling (scaled margins are effectively larger) + subMarginY = (baseMarginY + cropMarginY) / subScale; + } + + return { + 'width': cropWidth, + 'height': cropHeight, + 'x': cropX, + 'y': cropY, + 'subMarginX': subMarginX.round(), + 'subMarginY': subMarginY.round(), + 'subScale': subScale, + }; + } + + /// Get video dimensions from the currently selected media version + Size? _getCurrentVideoSize() { + if (_availableVersions.isEmpty || + widget.selectedMediaIndex >= _availableVersions.length) { + return null; + } + + final currentVersion = _availableVersions[widget.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 + void _updateVideoFilter() async { + if (player == null) return; + + try { + final nativePlayer = player!.platform as dynamic; + + if (_boxFitMode == 1) { + // Fill screen mode - apply crop filter + _videoSize = _getCurrentVideoSize(); + final cropParams = _calculateCropParameters(); + + 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 nativePlayer.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 nativePlayer.setProperty('sub-margin-x', subMarginX.toString()); + await nativePlayer.setProperty('sub-margin-y', subMarginY.toString()); + await nativePlayer.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 nativePlayer.setProperty('vf', ''); + await nativePlayer.setProperty('sub-margin-x', '20'); // Base margin + await nativePlayer.setProperty('sub-margin-y', '40'); // Base margin + await nativePlayer.setProperty('sub-scale', '1.0'); // Reset scale + } + } else { + // Other modes - clear video filter but apply base margins + appLogger.d( + 'Clearing video filter, applying base margins - BoxFit mode $_boxFitMode', + ); + await nativePlayer.setProperty('vf', ''); + await nativePlayer.setProperty('sub-margin-x', '20'); // Base margin + await nativePlayer.setProperty('sub-margin-y', '40'); // Base margin + await nativePlayer.setProperty('sub-scale', '1.0'); // Reset scale + } + } catch (e) { + appLogger.w('Failed to update video filter', error: e); + } + } + + /// Debounced version of _updateVideoFilter for resize events + void _debouncedUpdateVideoFilter() { + _resizeDebounceTimer?.cancel(); + _resizeDebounceTimer = Timer(const Duration(milliseconds: 50), () { + _updateVideoFilter(); + }); + } + @override void dispose() { // Stop progress tracking _progressTimer?.cancel(); + // Cancel debounce timer + _resizeDebounceTimer?.cancel(); + // Cancel stream subscriptions _playingSubscription?.cancel(); _completedSubscription?.cancel(); @@ -482,6 +669,19 @@ class VideoPlayerScreenState extends State { // Send final stopped state _sendProgress('stopped'); + // Clear video filter and reset subtitle margins before disposing player + try { + if (player != null) { + final nativePlayer = player!.platform as dynamic; + nativePlayer.setProperty('vf', ''); + nativePlayer.setProperty('sub-margin-x', '0'); + nativePlayer.setProperty('sub-margin-y', '0'); + nativePlayer.setProperty('sub-scale', '1.0'); + } + } catch (e) { + // Ignore errors during cleanup + } + // Restore system UI and orientation preferences (skip if navigating to another video) if (!_isReplacingWithVideo) { OrientationHelper.restoreSystemUI(); @@ -1378,19 +1578,46 @@ class VideoPlayerScreenState extends State { children: [ // Video player Center( - child: Video( - controller: controller!, - fit: _getCurrentBoxFit, - controls: (state) => plexVideoControlsBuilder( - player!, - widget.metadata, - onNext: _nextEpisode != null ? _playNext : null, - onPrevious: _previousEpisode != null ? _playPrevious : null, - availableVersions: _availableVersions, - selectedMediaIndex: widget.selectedMediaIndex, - boxFitMode: _boxFitMode, - onCycleBoxFitMode: _cycleBoxFitMode, - ), + child: LayoutBuilder( + builder: (context, constraints) { + // Update player size when layout changes + final newSize = Size( + constraints.maxWidth, + constraints.maxHeight, + ); + + // Check if size actually changed to avoid unnecessary updates + if (_playerSize == null || + (_playerSize!.width - newSize.width).abs() > 0.1 || + (_playerSize!.height - newSize.height).abs() > 0.1) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + setState(() { + _playerSize = newSize; + }); + // Use debounced update for resize events + _debouncedUpdateVideoFilter(); + } + }); + } + + return Video( + controller: controller!, + fit: _getCurrentBoxFit, + controls: (state) => plexVideoControlsBuilder( + player!, + widget.metadata, + onNext: _nextEpisode != null ? _playNext : null, + onPrevious: _previousEpisode != null + ? _playPrevious + : null, + availableVersions: _availableVersions, + selectedMediaIndex: widget.selectedMediaIndex, + boxFitMode: _boxFitMode, + onCycleBoxFitMode: _cycleBoxFitMode, + ), + ); + }, ), ), // Play Next Dialog diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 528496e2..3035b5b1 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -10,6 +10,7 @@ PODS: - FlutterMacOS - media_kit_video (0.0.1): - FlutterMacOS + - media_kit_libs_macos_video - os_media_controls (0.0.1): - FlutterMacOS - package_info_plus (0.0.1): @@ -92,8 +93,8 @@ SPEC CHECKSUMS: HotKey: 400beb7caa29054ea8d864c96f5ba7e5b4852277 hotkey_manager_macos: a4317849af96d2430fa89944d3c58977ca089fbe macos_window_utils: 23f54331a0fd51eea9e0ed347253bf48fd379d1d - media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 - media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 + media_kit_libs_macos_video: 69caff44badac986515a602bae1eb7f988a29f93 + media_kit_video: 71dcfc00fc7d2e7dbe43e60b5eb505f15ab6a88e os_media_controls: c07c04c4afdf59dda0a3f398457a46823c4ce0ed package_info_plus: f0052d280d17aa382b932f399edf32507174e870 path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880 diff --git a/pubspec.lock b/pubspec.lock index d49b164d..328c7b93 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -540,26 +540,26 @@ packages: dependency: "direct main" description: path: media_kit - ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" - resolved-ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" + ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + resolved-ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" url: "https://github.com/edde746/media-kit" source: git version: "1.2.1" media_kit_libs_android_video: - dependency: transitive + dependency: "direct overridden" description: path: "libs/android/media_kit_libs_android_video" - ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" - resolved-ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" + ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + resolved-ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" url: "https://github.com/edde746/media-kit" source: git version: "1.3.9" media_kit_libs_ios_video: - dependency: transitive + dependency: "direct overridden" description: path: "libs/ios/media_kit_libs_ios_video" - ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" - resolved-ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" + ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + resolved-ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" url: "https://github.com/edde746/media-kit" source: git version: "1.1.4" @@ -567,17 +567,17 @@ packages: dependency: transitive description: path: "libs/linux/media_kit_libs_linux" - ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" - resolved-ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" + ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + resolved-ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" url: "https://github.com/edde746/media-kit" source: git version: "1.2.1" media_kit_libs_macos_video: - dependency: transitive + dependency: "direct overridden" description: path: "libs/macos/media_kit_libs_macos_video" - ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" - resolved-ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" + ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + resolved-ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" url: "https://github.com/edde746/media-kit" source: git version: "1.1.5" @@ -585,8 +585,8 @@ packages: dependency: "direct main" description: path: "libs/universal/media_kit_libs_video" - ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" - resolved-ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" + ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + resolved-ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" url: "https://github.com/edde746/media-kit" source: git version: "1.0.7" @@ -594,27 +594,28 @@ packages: dependency: transitive description: path: "libs/windows/media_kit_libs_windows_video" - ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" - resolved-ref: "9782771486c0356b48c2e31e47365d1ed0b7fcb5" + ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + resolved-ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" url: "https://github.com/edde746/media-kit" source: git version: "1.0.12" media_kit_video: dependency: "direct main" description: - name: media_kit_video - sha256: "813858c3fe84eb46679eb698695f60665e2bfbef757766fac4d2e683f926e15a" - url: "https://pub.dev" - source: hosted + path: media_kit_video + ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + resolved-ref: "6bf03092e10e2f6ce19766ed290c0e6610be76f1" + url: "https://github.com/edde746/media-kit" + source: git version: "1.3.1" meta: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.16.0" mime: dependency: transitive description: @@ -1097,10 +1098,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.6" timing: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6f952caa..74002982 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,13 +37,33 @@ dependency_overrides: media_kit: git: url: https://github.com/edde746/media-kit - ref: 9782771486c0356b48c2e31e47365d1ed0b7fcb5 + ref: 6bf03092e10e2f6ce19766ed290c0e6610be76f1 path: media_kit + media_kit_video: + git: + url: https://github.com/edde746/media-kit + ref: 6bf03092e10e2f6ce19766ed290c0e6610be76f1 + path: media_kit_video media_kit_libs_video: git: url: https://github.com/edde746/media-kit - ref: 9782771486c0356b48c2e31e47365d1ed0b7fcb5 + ref: 6bf03092e10e2f6ce19766ed290c0e6610be76f1 path: libs/universal/media_kit_libs_video + media_kit_libs_macos_video: + git: + url: https://github.com/edde746/media-kit + ref: 6bf03092e10e2f6ce19766ed290c0e6610be76f1 + path: libs/macos/media_kit_libs_macos_video + media_kit_libs_ios_video: + git: + url: https://github.com/edde746/media-kit + ref: 6bf03092e10e2f6ce19766ed290c0e6610be76f1 + path: libs/ios/media_kit_libs_ios_video + media_kit_libs_android_video: + git: + url: https://github.com/edde746/media-kit + ref: 6bf03092e10e2f6ce19766ed290c0e6610be76f1 + path: libs/android/media_kit_libs_android_video dev_dependencies: flutter_test: