diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d3f890e78..8d2b93dd5 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -138,6 +138,8 @@ "videoActionPlay": "Play", "videoActionReplay10": "Seek backward 10 seconds", "videoActionSkip10": "Seek forward 10 seconds", + "videoActionShowPreviousFrame": "Show previous frame", + "videoActionShowNextFrame": "Show next frame", "videoActionSelectStreams": "Select tracks", "videoActionSetSpeed": "Playback speed", "videoActionABRepeat": "A-B repeat", diff --git a/lib/theme/icons.dart b/lib/theme/icons.dart index 42cb7de5c..8cc01b0bb 100644 --- a/lib/theme/icons.dart +++ b/lib/theme/icons.dart @@ -131,6 +131,8 @@ class AIcons { static const repeat = Icons.repeat_outlined; static final repeatOff = MdiIcons.repeatOff; static const replay10 = Icons.replay_10_outlined; + static const previousFrame = Icons.skip_previous_outlined; + static const nextFrame = Icons.skip_next_outlined; static final resetBounds = MdiIcons.rayStartEnd; static const reverse = Icons.invert_colors_outlined; static const skip10 = Icons.forward_10_outlined; diff --git a/lib/view/src/actions/entry.dart b/lib/view/src/actions/entry.dart index 1c6809511..5b213d222 100644 --- a/lib/view/src/actions/entry.dart +++ b/lib/view/src/actions/entry.dart @@ -43,6 +43,8 @@ extension ExtraEntryActionView on EntryAction { l10n.videoActionPlay, EntryAction.videoReplay10 => l10n.videoActionReplay10, EntryAction.videoSkip10 => l10n.videoActionSkip10, + EntryAction.videoShowPreviousFrame => l10n.videoActionShowPreviousFrame, + EntryAction.videoShowNextFrame => l10n.videoActionShowNextFrame, // external EntryAction.edit => l10n.entryActionEdit, EntryAction.open || EntryAction.openVideo => l10n.entryActionOpen, @@ -118,6 +120,8 @@ extension ExtraEntryActionView on EntryAction { AIcons.play, EntryAction.videoReplay10 => AIcons.replay10, EntryAction.videoSkip10 => AIcons.skip10, + EntryAction.videoShowPreviousFrame => AIcons.previousFrame, + EntryAction.videoShowNextFrame => AIcons.nextFrame, // external EntryAction.edit => AIcons.edit, EntryAction.open || EntryAction.openVideo => AIcons.openOutside, diff --git a/lib/widgets/viewer/action/entry_action_delegate.dart b/lib/widgets/viewer/action/entry_action_delegate.dart index 688b3c64b..52b500e8d 100644 --- a/lib/widgets/viewer/action/entry_action_delegate.dart +++ b/lib/widgets/viewer/action/entry_action_delegate.dart @@ -102,6 +102,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.videoTogglePlay: case EntryAction.videoReplay10: case EntryAction.videoSkip10: + case EntryAction.videoShowPreviousFrame: + case EntryAction.videoShowNextFrame: case EntryAction.openVideo: return targetEntry.isPureVideo; case EntryAction.rotateScreen: @@ -242,6 +244,8 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix case EntryAction.videoTogglePlay: case EntryAction.videoReplay10: case EntryAction.videoSkip10: + case EntryAction.videoShowPreviousFrame: + case EntryAction.videoShowNextFrame: case EntryAction.openVideo: final controller = context.read().getController(targetEntry); if (controller != null) { diff --git a/lib/widgets/viewer/action/video_action_delegate.dart b/lib/widgets/viewer/action/video_action_delegate.dart index 65f96c45b..51a4ba19b 100644 --- a/lib/widgets/viewer/action/video_action_delegate.dart +++ b/lib/widgets/viewer/action/video_action_delegate.dart @@ -74,6 +74,10 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix await controller.seekTo(max(controller.currentPosition - 10000, 0)); case EntryAction.videoSkip10: await controller.seekTo(controller.currentPosition + 10000); + case EntryAction.videoShowPreviousFrame: + await controller.skipFrames(-1); + case EntryAction.videoShowNextFrame: + await controller.skipFrames(1); case EntryAction.openVideo: await appService.open(entry.uri, entry.mimeTypeAnySubtype, forceChooser: false).then((success) { if (!success) showNoMatchingAppDialog(context); diff --git a/plugins/aves_model/lib/src/actions/entry.dart b/plugins/aves_model/lib/src/actions/entry.dart index 3763f7dd1..fc1981a55 100644 --- a/plugins/aves_model/lib/src/actions/entry.dart +++ b/plugins/aves_model/lib/src/actions/entry.dart @@ -28,6 +28,8 @@ enum EntryAction { videoTogglePlay, videoReplay10, videoSkip10, + videoShowPreviousFrame, + videoShowNextFrame, // external edit, open, @@ -97,6 +99,8 @@ class EntryActions { EntryAction.videoTogglePlay, EntryAction.videoReplay10, EntryAction.videoSkip10, + EntryAction.videoShowPreviousFrame, + EntryAction.videoShowNextFrame, EntryAction.rotateCCW, EntryAction.rotateCW, EntryAction.flip, @@ -113,6 +117,8 @@ class EntryActions { EntryAction.videoToggleMute, EntryAction.videoSetSpeed, EntryAction.videoABRepeat, + EntryAction.videoShowPreviousFrame, + EntryAction.videoShowNextFrame, EntryAction.videoSelectStreams, EntryAction.videoSettings, EntryAction.lockViewer, diff --git a/plugins/aves_video/lib/src/controller.dart b/plugins/aves_video/lib/src/controller.dart index 8fac10917..1a9d4c24a 100644 --- a/plugins/aves_video/lib/src/controller.dart +++ b/plugins/aves_video/lib/src/controller.dart @@ -69,6 +69,8 @@ abstract class AvesVideoController with ABRepeatMixin { Future seekToProgress(double progress) => seekTo((duration * progress.clamp(0, 1)).toInt()); + Future skipFrames(int frameCount); + Listenable get playCompletedListenable; VideoStatus get status; diff --git a/plugins/aves_video_mpv/lib/src/controller.dart b/plugins/aves_video_mpv/lib/src/controller.dart index 65265323c..0b42e9734 100644 --- a/plugins/aves_video_mpv/lib/src/controller.dart +++ b/plugins/aves_video_mpv/lib/src/controller.dart @@ -59,6 +59,12 @@ class MpvVideoController extends AvesVideoController { title: entry.bestTitle ?? entry.uri, libass: false, logLevel: MPVLogLevel.warn, + protocolWhitelist: [ + ...const PlayerConfiguration().protocolWhitelist, + // Android `content` URIs are considered unsafe by default, + // as they are transferred via a custom `fd` protocol + 'fd', + ], ), ); _initController(); @@ -206,6 +212,15 @@ class MpvVideoController extends AvesVideoController { await _instance.seek(Duration(milliseconds: targetMillis)); } + @override + Future skipFrames(int frameCount) async { + if (frameCount > 0) { + await _instance.frameStep(); + } else if (frameCount < 0) { + await _instance.frameBackStep(); + } + } + @override Listenable get playCompletedListenable => _completedNotifier; diff --git a/plugins/aves_video_mpv/pubspec.lock b/plugins/aves_video_mpv/pubspec.lock index 58ce42bb4..7c8251cdf 100644 --- a/plugins/aves_video_mpv/pubspec.lock +++ b/plugins/aves_video_mpv/pubspec.lock @@ -179,10 +179,11 @@ packages: media_kit: dependency: "direct main" description: - name: media_kit - sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62" - url: "https://pub.dev" - source: hosted + path: media_kit + ref: frame-stepping + resolved-ref: "209221d605a0b2c34379e799a8e001cc359e6183" + url: "https://github.com/deckerst/media-kit.git" + source: git version: "1.1.11" media_kit_libs_android_video: dependency: "direct main" @@ -192,21 +193,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.6" - media_kit_native_event_loop: - dependency: "direct main" - description: - name: media_kit_native_event_loop - sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40" - url: "https://pub.dev" - source: hosted - version: "1.0.9" media_kit_video: dependency: "direct main" description: - name: media_kit_video - sha256: "2cc3b966679963ba25a4ce5b771e532a521ebde7c6aa20e9802bec95d9916c8f" - url: "https://pub.dev" - source: hosted + path: media_kit_video + ref: frame-stepping + resolved-ref: "209221d605a0b2c34379e799a8e001cc359e6183" + url: "https://github.com/deckerst/media-kit.git" + source: git version: "1.2.5" meta: dependency: transitive @@ -260,10 +254,10 @@ packages: dependency: transitive description: name: safe_local_storage - sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 + sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.1" screen_brightness: dependency: transitive description: @@ -377,10 +371,10 @@ packages: dependency: transitive description: name: uri_parser - sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" + sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.0.0" uuid: dependency: transitive description: diff --git a/plugins/aves_video_mpv/pubspec.yaml b/plugins/aves_video_mpv/pubspec.yaml index eb1c6374c..f54c0f2b5 100644 --- a/plugins/aves_video_mpv/pubspec.yaml +++ b/plugins/aves_video_mpv/pubspec.yaml @@ -17,10 +17,21 @@ dependencies: collection: media_kit: media_kit_libs_android_video: - media_kit_native_event_loop: media_kit_video: path: +dependency_overrides: + media_kit: + git: + url: https://github.com/deckerst/media-kit.git + ref: frame-stepping + path: media_kit + media_kit_video: + git: + url: https://github.com/deckerst/media-kit.git + ref: frame-stepping + path: media_kit_video + dev_dependencies: flutter_lints: diff --git a/pubspec.lock b/pubspec.lock index 5962e4c13..2a5080213 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -901,12 +901,13 @@ packages: source: hosted version: "7.0.7296" media_kit: - dependency: transitive + dependency: "direct overridden" description: - name: media_kit - sha256: "1f1deee148533d75129a6f38251ff8388e33ee05fc2d20a6a80e57d6051b7b62" - url: "https://pub.dev" - source: hosted + path: media_kit + ref: frame-stepping + resolved-ref: "209221d605a0b2c34379e799a8e001cc359e6183" + url: "https://github.com/deckerst/media-kit.git" + source: git version: "1.1.11" media_kit_libs_android_video: dependency: transitive @@ -916,21 +917,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.6" - media_kit_native_event_loop: - dependency: transitive - description: - name: media_kit_native_event_loop - sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40" - url: "https://pub.dev" - source: hosted - version: "1.0.9" media_kit_video: dependency: "direct overridden" description: - name: media_kit_video - sha256: "2cc3b966679963ba25a4ce5b771e532a521ebde7c6aa20e9802bec95d9916c8f" - url: "https://pub.dev" - source: hosted + path: media_kit_video + ref: frame-stepping + resolved-ref: "209221d605a0b2c34379e799a8e001cc359e6183" + url: "https://github.com/deckerst/media-kit.git" + source: git version: "1.2.5" meta: dependency: transitive @@ -1306,10 +1300,10 @@ packages: dependency: transitive description: name: safe_local_storage - sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 + sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "2.0.1" sanitize_html: dependency: transitive description: @@ -1322,10 +1316,10 @@ packages: dependency: "direct main" description: name: screen_brightness - sha256: "970d1cc68f8ed7bf3a539924a3cd51c06af138bbf345b6daa4af8ce4e8527ebf" + sha256: a43fdbccd5b90044f68057412dde7715cd7499a4c24f5d5da7e01ed4cf41e0af url: "https://pub.dev" source: hosted - version: "2.0.0+1" + version: "2.0.0+2" screen_brightness_android: dependency: transitive description: @@ -1664,10 +1658,10 @@ packages: dependency: transitive description: name: uri_parser - sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" + sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.0.0" url_launcher: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 37e1db18c..9689a8ee3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -120,8 +120,17 @@ dependencies: dependency_overrides: # media_kit_video v1.2.4 depends on a specific old version of screen_brightness - media_kit_video: ^1.0.0 screen_brightness: ^2.0.0 + media_kit: + git: + url: https://github.com/deckerst/media-kit.git + ref: frame-stepping + path: media_kit + media_kit_video: + git: + url: https://github.com/deckerst/media-kit.git + ref: frame-stepping + path: media_kit_video dev_dependencies: flutter_test: