fix: don't crop subtitles
This commit is contained in:
@@ -68,6 +68,11 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
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<VideoPlayerScreen> {
|
||||
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<void> _initializePlayer() async {
|
||||
@@ -137,6 +147,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
'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<VideoPlayerScreen> {
|
||||
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<VideoPlayerScreen> {
|
||||
setState(() {
|
||||
_boxFitMode = (_boxFitMode + 1) % 3;
|
||||
});
|
||||
_updateVideoFilter();
|
||||
}
|
||||
|
||||
/// Toggle between contain and cover modes only (for pinch gesture)
|
||||
@@ -447,6 +461,7 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
setState(() {
|
||||
_boxFitMode = _boxFitMode == 0 ? 1 : 0;
|
||||
});
|
||||
_updateVideoFilter();
|
||||
}
|
||||
|
||||
/// Get current BoxFit based on mode
|
||||
@@ -463,11 +478,183 @@ class VideoPlayerScreenState extends State<VideoPlayerScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate crop parameters to match what BoxFit.cover will show
|
||||
Map<String, dynamic>? _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<VideoPlayerScreen> {
|
||||
// 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<VideoPlayerScreen> {
|
||||
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
|
||||
|
||||
+3
-2
@@ -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
|
||||
|
||||
+26
-25
@@ -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:
|
||||
|
||||
+22
-2
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user