Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/models/settings/video_player_settings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ enum VideoHotKeys {
takeScreenshot,
takeScreenshotClean,
toggleSubtitles,
subtitleOffsetBackward,
subtitleOffsetForward,
exit;

const VideoHotKeys();
Expand Down Expand Up @@ -63,6 +65,8 @@ enum VideoHotKeys {
VideoHotKeys.takeScreenshot => context.localized.takeScreenshot,
VideoHotKeys.takeScreenshotClean => context.localized.takeScreenshotClean,
VideoHotKeys.toggleSubtitles => context.localized.toggleSubtitles,
VideoHotKeys.subtitleOffsetBackward => 'Subtitle backward',
VideoHotKeys.subtitleOffsetForward => 'Subtitle forward',
VideoHotKeys.exit => context.localized.exit,
};
}
Expand Down Expand Up @@ -312,6 +316,10 @@ Map<VideoHotKeys, KeyCombination> get _defaultVideoHotKeys => {
VideoHotKeys.takeScreenshotClean =>
KeyCombination(key: LogicalKeyboardKey.keyG, modifier: LogicalKeyboardKey.controlLeft),
VideoHotKeys.toggleSubtitles => KeyCombination(key: LogicalKeyboardKey.keyT),
VideoHotKeys.subtitleOffsetBackward =>
KeyCombination(key: LogicalKeyboardKey.arrowLeft, modifier: LogicalKeyboardKey.controlLeft),
VideoHotKeys.subtitleOffsetForward =>
KeyCombination(key: LogicalKeyboardKey.arrowRight, modifier: LogicalKeyboardKey.controlLeft),
VideoHotKeys.exit => KeyCombination(key: LogicalKeyboardKey.escape),
},
};
2 changes: 2 additions & 0 deletions lib/models/settings/video_player_settings.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions lib/providers/settings/video_player_settings_provider.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ final videoPlayerSettingsProvider =
return VideoPlayerSettingsProviderNotifier(ref);
});

final subtitleTimingOffsetProvider = StateProvider<Duration>((ref) => Duration.zero);

final playbackRateProvider = StateProvider<double>((ref) => 1.0);

class VideoPlayerSettingsProviderNotifier extends StateNotifier<VideoPlayerSettingsModel> {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import 'package:flutter/material.dart';

import 'package:async/async.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';

import 'package:fladder/providers/settings/video_player_settings_provider.dart';

class VideoPlayerSubtitleOffsetIndicator extends ConsumerStatefulWidget {
const VideoPlayerSubtitleOffsetIndicator({super.key});

@override
ConsumerState<ConsumerStatefulWidget> createState() => _VideoPlayerSubtitleOffsetIndicatorState();
}

class _VideoPlayerSubtitleOffsetIndicatorState extends ConsumerState<VideoPlayerSubtitleOffsetIndicator> {
late Duration currentOffset = ref.read(subtitleTimingOffsetProvider);

bool showIndicator = false;
late final timer = RestartableTimer(const Duration(seconds: 1), () {
setState(() {
showIndicator = false;
});
});

@override
void dispose() {
showIndicator = false;
timer.cancel();
super.dispose();
}

@override
Widget build(BuildContext context) {
ref.listen(
subtitleTimingOffsetProvider,
(previous, next) {
if (previous == next) return;
setState(() {
showIndicator = true;
currentOffset = next;
});
timer.reset();
},
);

return IgnorePointer(
child: AnimatedOpacity(
duration: const Duration(milliseconds: 250),
opacity: showIndicator ? 1 : 0,
child: Center(
child: Container(
decoration: BoxDecoration(
color: Colors.black.withValues(alpha: 0.85),
borderRadius: BorderRadius.circular(16),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.textalign_left),
Text(_subtitleDelayLabel(currentOffset)),
],
),
),
),
),
),
);
}
}

String _subtitleDelayLabel(Duration offset) {
final absMilliseconds = offset.inMilliseconds.abs();
final sign = offset.inMilliseconds >= 0 ? '+' : '-';
return 'Subtitle $sign${absMilliseconds}ms';
}
14 changes: 14 additions & 0 deletions lib/screens/video_player/video_player_controls.dart
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import 'package:fladder/screens/video_player/components/video_player_quality_con
import 'package:fladder/screens/video_player/components/video_player_screenshot_indicator.dart';
import 'package:fladder/screens/video_player/components/video_player_seek_indicator.dart';
import 'package:fladder/screens/video_player/components/video_player_speed_indicator.dart';
import 'package:fladder/screens/video_player/components/video_player_subtitle_offset_indicator.dart';
import 'package:fladder/screens/video_player/components/video_player_volume_indicator.dart';
import 'package:fladder/screens/video_player/components/video_progress_bar.dart';
import 'package:fladder/screens/video_player/components/video_volume_slider.dart';
Expand Down Expand Up @@ -182,6 +183,7 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
),
VideoPlayerSeekIndicator(controller: _seekController),
const VideoPlayerVolumeIndicator(),
const VideoPlayerSubtitleOffsetIndicator(),
const VideoPlayerBrightnessIndicator(),
const VideoPlayerSpeedIndicator(),
const VideoPlayerScreenshotIndicator(),
Expand Down Expand Up @@ -1000,6 +1002,8 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
final mediaSegments = ref.read(playBackModel.select((value) => value?.mediaSegments));
final position = ref.read(mediaPlaybackProvider).position;
final playing = ref.read(mediaPlaybackProvider.select((value) => value.playing));
final subtitlesEnabled = ref.read(playBackModel)?.mediaStreams?.defaultSubStreamIndex != null &&
ref.read(playBackModel)!.mediaStreams!.defaultSubStreamIndex != -1;

MediaSegment? segment = mediaSegments?.atPosition(position);

Expand Down Expand Up @@ -1064,6 +1068,16 @@ class _DesktopControlsState extends ConsumerState<DesktopControls> {
case VideoHotKeys.toggleSubtitles:
_toggleSubtitles();
return true;
case VideoHotKeys.subtitleOffsetBackward:
if (!subtitlesEnabled) return false;
ref.read(subtitleTimingOffsetProvider.notifier).update((state) => state + const Duration(milliseconds: -250));
ref.read(videoPlayerProvider).shiftSubtitleOffset(const Duration(milliseconds: -250));
return true;
case VideoHotKeys.subtitleOffsetForward:
if (!subtitlesEnabled) return false;
ref.read(subtitleTimingOffsetProvider.notifier).update((state) => state + const Duration(milliseconds: 250));
ref.read(videoPlayerProvider).shiftSubtitleOffset(const Duration(milliseconds: 250));
return true;
case VideoHotKeys.seekForwardInstant:
final seekForwardSeconds =
ref.read(userProvider.select((value) => value?.userSettings?.skipForwardDuration.inSeconds ?? 30));
Expand Down
2 changes: 2 additions & 0 deletions lib/wrappers/media_control_wrapper.dart
Original file line number Diff line number Diff line change
Expand Up @@ -553,6 +553,8 @@ class MediaControlsWrapper extends BaseAudioHandler implements VideoPlayerContro
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel) async =>
await _player?.setSubtitleTrack(model, playbackModel) ?? -1;

Future<void> shiftSubtitleOffset(Duration offset) async => _player?.shiftSubtitleOffset(offset);

Future<void> setVolume(double volume) async => _player?.setVolume(volume);

@override
Expand Down
1 change: 1 addition & 0 deletions lib/wrappers/players/base_player.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ abstract class BasePlayer {
Future<Uint8List?> takeScreenshot();
Future<int> setSubtitleTrack(SubStreamModel? model, PlaybackModel playbackModel);
Future<int> setAudioTrack(AudioStreamModel? model, PlaybackModel playbackModel);
Future<void> shiftSubtitleOffset(Duration offset) async {}
void applySubtitleSettings(SubtitleSettingsModel settings) {}

Uri? isValidUrl(String input) {
Expand Down
10 changes: 10 additions & 0 deletions lib/wrappers/players/lib_mpv.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class LibMPV extends BasePlayer {
mpv.Player? _player;
VideoController? _controller;
String _currentSubtitleCodec = '';
Duration _subtitleOffset = Duration.zero;

final StreamController<PlayerState> _stateController = StreamController.broadcast();
@override
Expand Down Expand Up @@ -526,6 +527,15 @@ class LibMPV extends BasePlayer {
)
: null;

@override
Future<void> shiftSubtitleOffset(Duration offset) async {
if (_player?.platform is! mpv.NativePlayer) return;

_subtitleOffset += offset;
final nativePlayer = _player!.platform as dynamic;
await nativePlayer.setProperty('sub-delay', (_subtitleOffset.inMilliseconds / 1000).toStringAsFixed(3));
}

@override
Future<void> setVolume(double volume) async {
_fadeTimer?.cancel();
Expand Down
Loading