#962 video: A-B repeat

This commit is contained in:
Thibault Deckers 2024-03-31 00:17:37 +01:00
parent 8555322460
commit 46aef919be
16 changed files with 572 additions and 124 deletions

View file

@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file.
- Collection: support for Fairphone burst pattern - Collection: support for Fairphone burst pattern
- Collection: allow using tags/make/model when bulk renaming - Collection: allow using tags/make/model when bulk renaming
- Video: A-B repeat
- Settings: hidden items can be toggled - Settings: hidden items can be toggled
### Changed ### Changed

View file

@ -63,6 +63,7 @@
"actionRemove": "Remove", "actionRemove": "Remove",
"resetTooltip": "Reset", "resetTooltip": "Reset",
"saveTooltip": "Save", "saveTooltip": "Save",
"stopTooltip": "Stop",
"pickTooltip": "Pick", "pickTooltip": "Pick",
"doubleBackExitMessage": "Tap “back” again to exit.", "doubleBackExitMessage": "Tap “back” again to exit.",
@ -127,6 +128,10 @@
"videoActionSkip10": "Seek forward 10 seconds", "videoActionSkip10": "Seek forward 10 seconds",
"videoActionSelectStreams": "Select tracks", "videoActionSelectStreams": "Select tracks",
"videoActionSetSpeed": "Playback speed", "videoActionSetSpeed": "Playback speed",
"videoActionABRepeat": "A-B repeat",
"videoRepeatActionSetStart": "Set start",
"videoRepeatActionSetEnd": "Set end",
"viewerActionSettings": "Settings", "viewerActionSettings": "Settings",
"viewerActionLock": "Lock viewer", "viewerActionLock": "Lock viewer",

View file

@ -119,7 +119,10 @@ class AIcons {
static const pause = Icons.pause; static const pause = Icons.pause;
static const print = Icons.print_outlined; static const print = Icons.print_outlined;
static const refresh = Icons.refresh_outlined; static const refresh = Icons.refresh_outlined;
static const repeat = Icons.repeat_outlined;
static final repeatOff = MdiIcons.repeatOff;
static const replay10 = Icons.replay_10_outlined; static const replay10 = Icons.replay_10_outlined;
static final resetBounds = MdiIcons.rayStartEnd;
static const reverse = Icons.invert_colors_outlined; static const reverse = Icons.invert_colors_outlined;
static const skip10 = Icons.forward_10_outlined; static const skip10 = Icons.forward_10_outlined;
static const reset = Icons.restart_alt_outlined; static const reset = Icons.restart_alt_outlined;
@ -131,6 +134,8 @@ class AIcons {
static const select = Icons.select_all_outlined; static const select = Icons.select_all_outlined;
static const setAs = Icons.wallpaper_outlined; static const setAs = Icons.wallpaper_outlined;
static final setCover = MdiIcons.imageEditOutline; static final setCover = MdiIcons.imageEditOutline;
static final setEnd = MdiIcons.rayEnd;
static final setStart = MdiIcons.rayStart;
static const share = Icons.share_outlined; static const share = Icons.share_outlined;
static const show = Icons.visibility_outlined; static const show = Icons.visibility_outlined;
static final showFullscreen = MdiIcons.arrowExpand; static final showFullscreen = MdiIcons.arrowExpand;

View file

@ -36,6 +36,7 @@ extension ExtraEntryActionView on EntryAction {
l10n.videoActionMute, l10n.videoActionMute,
EntryAction.videoSelectStreams => l10n.videoActionSelectStreams, EntryAction.videoSelectStreams => l10n.videoActionSelectStreams,
EntryAction.videoSetSpeed => l10n.videoActionSetSpeed, EntryAction.videoSetSpeed => l10n.videoActionSetSpeed,
EntryAction.videoABRepeat => l10n.videoActionABRepeat,
EntryAction.videoSettings => l10n.viewerActionSettings, EntryAction.videoSettings => l10n.viewerActionSettings,
EntryAction.videoTogglePlay => EntryAction.videoTogglePlay =>
// different data depending on toggle state // different data depending on toggle state
@ -110,6 +111,7 @@ extension ExtraEntryActionView on EntryAction {
AIcons.mute, AIcons.mute,
EntryAction.videoSelectStreams => AIcons.streams, EntryAction.videoSelectStreams => AIcons.streams,
EntryAction.videoSetSpeed => AIcons.speed, EntryAction.videoSetSpeed => AIcons.speed,
EntryAction.videoABRepeat => AIcons.repeat,
EntryAction.videoSettings => AIcons.videoSettings, EntryAction.videoSettings => AIcons.videoSettings,
EntryAction.videoTogglePlay => EntryAction.videoTogglePlay =>
// different data depending on toggle state // different data depending on toggle state

View file

@ -94,6 +94,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
return !settings.useTvLayout && targetEntry.isPureVideo; return !settings.useTvLayout && targetEntry.isPureVideo;
case EntryAction.videoSelectStreams: case EntryAction.videoSelectStreams:
case EntryAction.videoSetSpeed: case EntryAction.videoSetSpeed:
case EntryAction.videoABRepeat:
case EntryAction.videoSettings: case EntryAction.videoSettings:
case EntryAction.videoTogglePlay: case EntryAction.videoTogglePlay:
case EntryAction.videoReplay10: case EntryAction.videoReplay10:
@ -229,6 +230,7 @@ class EntryActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
case EntryAction.videoToggleMute: case EntryAction.videoToggleMute:
case EntryAction.videoSelectStreams: case EntryAction.videoSelectStreams:
case EntryAction.videoSetSpeed: case EntryAction.videoSetSpeed:
case EntryAction.videoABRepeat:
case EntryAction.videoSettings: case EntryAction.videoSettings:
case EntryAction.videoTogglePlay: case EntryAction.videoTogglePlay:
case EntryAction.videoReplay10: case EntryAction.videoReplay10:

View file

@ -64,6 +64,8 @@ class VideoActionDelegate with FeedbackMixin, PermissionAwareMixin, SizeAwareMix
await _showStreamSelectionDialog(context, controller); await _showStreamSelectionDialog(context, controller);
case EntryAction.videoSetSpeed: case EntryAction.videoSetSpeed:
await _showSpeedDialog(context, controller); await _showSpeedDialog(context, controller);
case EntryAction.videoABRepeat:
controller.toggleABRepeat();
case EntryAction.videoSettings: case EntryAction.videoSettings:
await _showSettings(context, controller); await _showSettings(context, controller);
case EntryAction.videoTogglePlay: case EntryAction.videoTogglePlay:

View file

@ -0,0 +1,78 @@
import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:aves_video/aves_video.dart';
import 'package:flutter/material.dart';
class VideoABRepeatOverlay extends StatefulWidget {
final AvesVideoController? controller;
final Animation<double> scale;
const VideoABRepeatOverlay({
super.key,
required this.controller,
required this.scale,
});
@override
State<StatefulWidget> createState() => _VideoABRepeatOverlayState();
}
class _VideoABRepeatOverlayState extends State<VideoABRepeatOverlay> {
Animation<double> get scale => widget.scale;
AvesVideoController? get controller => widget.controller;
ValueNotifier<ABRepeat?> get abRepeatNotifier => controller?.abRepeatNotifier ?? ValueNotifier(null);
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return ValueListenableBuilder<ABRepeat?>(
valueListenable: abRepeatNotifier,
builder: (context, abRepeat, child) {
if (abRepeat == null) return const SizedBox();
Widget boundButton;
if (abRepeat.start == null) {
boundButton = IconButton(
icon: Icon(AIcons.setStart),
onPressed: controller?.setABRepeatStart,
tooltip: l10n.videoRepeatActionSetStart,
);
} else if (abRepeat.end == null) {
boundButton = IconButton(
icon: Icon(AIcons.setEnd),
onPressed: controller?.setABRepeatEnd,
tooltip: l10n.videoRepeatActionSetEnd,
);
} else {
boundButton = IconButton(
icon: Icon(AIcons.resetBounds),
onPressed: controller?.resetABRepeat,
tooltip: l10n.resetTooltip,
);
}
return Row(
children: [
const Spacer(),
OverlayButton(
scale: scale,
child: boundButton,
),
const SizedBox(width: 8),
OverlayButton(
scale: scale,
child: IconButton(
icon: Icon(AIcons.repeatOff),
onPressed: () => controller?.toggleABRepeat(),
tooltip: l10n.stopTooltip,
),
),
],
);
},
);
}
}

View file

@ -37,6 +37,8 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
bool get isPlaying => controller?.isPlaying ?? false; bool get isPlaying => controller?.isPlaying ?? false;
ValueNotifier<ABRepeat?> get abRepeatNotifier => controller?.abRepeatNotifier ?? ValueNotifier(null);
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final blurred = settings.enableBlurEffect; final blurred = settings.enableBlurEffect;
@ -69,8 +71,7 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minHeight: kMinInteractiveDimension), constraints: const BoxConstraints(minHeight: kMinInteractiveDimension),
child: Container( child: Container(
alignment: Alignment.center, padding: const EdgeInsets.symmetric(horizontal: 16),
padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred), color: Themes.overlayBackgroundColor(brightness: theme.brightness, blurred: blurred),
border: AvesBorder.border(context), border: AvesBorder.border(context),
@ -80,62 +81,80 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
data: MediaQuery.of(context).copyWith( data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.noScaling, textScaler: TextScaler.noScaling,
), ),
child: Column( child: ValueListenableBuilder<ABRepeat?>(
key: _progressBarKey, valueListenable: abRepeatNotifier,
mainAxisSize: MainAxisSize.min, builder: (context, abRepeat, child) {
children: [ return Stack(
Row( fit: StackFit.passthrough,
children: [ children: [
StreamBuilder<int>( if (abRepeat != null) ...[
stream: positionStream, _buildABRepeatMark(context, abRepeat.start),
builder: (context, snapshot) { _buildABRepeatMark(context, abRepeat.end),
// do not use stream snapshot because it is obsolete when switching between videos ],
final position = controller?.currentPosition.floor() ?? 0; Container(
return Text( key: _progressBarKey,
formatFriendlyDuration(Duration(milliseconds: position)), alignment: Alignment.center,
style: textStyle, padding: const EdgeInsets.symmetric(vertical: 4),
strutStyle: strutStyle, child: Column(
); mainAxisSize: MainAxisSize.min,
}), children: [
const Spacer(), Row(
Text( children: [
formatFriendlyDuration(Duration(milliseconds: controller?.duration ?? 0)), StreamBuilder<int>(
style: textStyle, stream: positionStream,
strutStyle: strutStyle, builder: (context, snapshot) {
// do not use stream snapshot because it is obsolete when switching between videos
final position = controller?.currentPosition.floor() ?? 0;
return Text(
formatFriendlyDuration(Duration(milliseconds: position)),
style: textStyle,
strutStyle: strutStyle,
);
}),
const Spacer(),
Text(
formatFriendlyDuration(Duration(milliseconds: controller?.duration ?? 0)),
style: textStyle,
strutStyle: strutStyle,
),
],
),
ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Directionality(
// force directionality for `LinearProgressIndicator`
textDirection: TextDirection.ltr,
child: StreamBuilder<int>(
stream: positionStream,
builder: (context, snapshot) {
// do not use stream snapshot because it is obsolete when switching between videos
var progress = controller?.progress ?? 0.0;
if (!progress.isFinite) progress = 0.0;
return LinearProgressIndicator(
value: progress,
backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2),
);
}),
),
),
Row(
children: [
_buildSpeedIndicator(),
_buildMuteIndicator(),
Text(
// fake text below to match the height of the text above and center the whole thing
'',
style: textStyle,
strutStyle: strutStyle,
),
],
),
],
),
), ),
], ],
), );
ClipRRect( },
borderRadius: const BorderRadius.all(Radius.circular(4)),
child: Directionality(
// force directionality for `LinearProgressIndicator`
textDirection: TextDirection.ltr,
child: StreamBuilder<int>(
stream: positionStream,
builder: (context, snapshot) {
// do not use stream snapshot because it is obsolete when switching between videos
var progress = controller?.progress ?? 0.0;
if (!progress.isFinite) progress = 0.0;
return LinearProgressIndicator(
value: progress,
backgroundColor: Theme.of(context).colorScheme.onSurface.withOpacity(.2),
);
}),
),
),
Row(
children: [
_buildSpeedIndicator(),
_buildMuteIndicator(),
Text(
// fake text below to match the height of the text above and center the whole thing
'',
style: textStyle,
strutStyle: strutStyle,
),
],
),
],
), ),
), ),
), ),
@ -145,6 +164,20 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
); );
} }
Widget _buildABRepeatMark(BuildContext context, int? position) {
if (controller == null || position == null) return const SizedBox();
return Positioned(
left: _progressToDx(position / controller!.duration),
top: 0,
bottom: 0,
child: Container(
decoration: BoxDecoration(
border: Border(left: AvesBorder.straightSide(context, width: 2)),
),
),
);
}
Widget _buildSpeedIndicator() => StreamBuilder<double>( Widget _buildSpeedIndicator() => StreamBuilder<double>(
stream: controller?.speedStream ?? Stream.value(1.0), stream: controller?.speedStream ?? Stream.value(1.0),
builder: (context, snapshot) { builder: (context, snapshot) {
@ -175,11 +208,20 @@ class _VideoProgressBarState extends State<VideoProgressBar> {
}, },
); );
RenderBox? _getProgressBarRenderBox() {
return _progressBarKey.currentContext?.findRenderObject() as RenderBox?;
}
void _seekFromTap(Offset globalPosition) async { void _seekFromTap(Offset globalPosition) async {
if (controller == null) return; final box = _getProgressBarRenderBox();
final keyContext = _progressBarKey.currentContext!; if (controller == null || box == null) return;
final box = keyContext.findRenderObject() as RenderBox;
final localPosition = box.globalToLocal(globalPosition); final dx = box.globalToLocal(globalPosition).dx;
await controller!.seekToProgress(localPosition.dx / box.size.width); await controller!.seekToProgress(dx / box.size.width);
}
double? _progressToDx(double progress) {
final box = _getProgressBarRenderBox();
return box == null ? null : progress * box.size.width;
} }
} }

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/entry.dart';
import 'package:aves/view/view.dart'; import 'package:aves/view/view.dart';
import 'package:aves/widgets/common/identity/buttons/overlay_button.dart'; import 'package:aves/widgets/common/identity/buttons/overlay_button.dart';
import 'package:aves/widgets/viewer/overlay/video/ab_repeat.dart';
import 'package:aves/widgets/viewer/overlay/video/controls.dart'; import 'package:aves/widgets/viewer/overlay/video/controls.dart';
import 'package:aves/widgets/viewer/overlay/video/progress_bar.dart'; import 'package:aves/widgets/viewer/overlay/video/progress_bar.dart';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
@ -64,19 +65,28 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
); );
} }
return Row( return Column(
children: [ children: [
Expanded( VideoABRepeatOverlay(
child: VideoProgressBar(
controller: controller,
scale: scale,
),
),
VideoControlRow(
entry: entry,
controller: controller, controller: controller,
scale: scale, scale: scale,
onActionSelected: widget.onActionSelected, ),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: VideoProgressBar(
controller: controller,
scale: scale,
),
),
VideoControlRow(
entry: entry,
controller: controller,
scale: scale,
onActionSelected: widget.onActionSelected,
),
],
), ),
], ],
); );

View file

@ -22,6 +22,7 @@ enum EntryAction {
videoCaptureFrame, videoCaptureFrame,
videoSelectStreams, videoSelectStreams,
videoSetSpeed, videoSetSpeed,
videoABRepeat,
videoToggleMute, videoToggleMute,
videoSettings, videoSettings,
videoTogglePlay, videoTogglePlay,
@ -90,6 +91,7 @@ class EntryActions {
EntryAction.videoCaptureFrame, EntryAction.videoCaptureFrame,
EntryAction.videoSelectStreams, EntryAction.videoSelectStreams,
EntryAction.videoSetSpeed, EntryAction.videoSetSpeed,
EntryAction.videoABRepeat,
EntryAction.videoToggleMute, EntryAction.videoToggleMute,
EntryAction.videoSettings, EntryAction.videoSettings,
EntryAction.videoTogglePlay, EntryAction.videoTogglePlay,
@ -110,6 +112,7 @@ class EntryActions {
EntryAction.videoCaptureFrame, EntryAction.videoCaptureFrame,
EntryAction.videoToggleMute, EntryAction.videoToggleMute,
EntryAction.videoSetSpeed, EntryAction.videoSetSpeed,
EntryAction.videoABRepeat,
EntryAction.videoSelectStreams, EntryAction.videoSelectStreams,
EntryAction.videoSettings, EntryAction.videoSettings,
EntryAction.lockViewer, EntryAction.lockViewer,

View file

@ -1,5 +1,6 @@
library aves_video; library aves_video;
export 'src/ab_repeat.dart';
export 'src/controller.dart'; export 'src/controller.dart';
export 'src/metadata.dart'; export 'src/metadata.dart';
export 'src/settings/subtitles.dart'; export 'src/settings/subtitles.dart';

View file

@ -0,0 +1,33 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
class ABRepeat {
final int? start, end;
ABRepeat({this.start, this.end});
ABRepeat sanitize() {
if (start != null && end != null && start! > end!) {
return ABRepeat(start: end, end: start);
}
return ABRepeat(start: start, end: end);
}
int clamp(int position) => (start != null && end != null) ? position.clamp(start!, end!) : position;
}
mixin ABRepeatMixin {
int get currentPosition;
ValueNotifier<ABRepeat?> abRepeatNotifier = ValueNotifier(null);
void toggleABRepeat() => _setAbRepeat(abRepeatNotifier.value != null ? null : ABRepeat());
void resetABRepeat() => _setAbRepeat(ABRepeat());
void setABRepeatStart() => _setAbRepeat(ABRepeat(start: currentPosition, end: abRepeatNotifier.value?.end));
void setABRepeatEnd() => _setAbRepeat(ABRepeat(start: abRepeatNotifier.value?.start, end: currentPosition));
void _setAbRepeat(ABRepeat? v) => abRepeatNotifier.value = v?.sanitize();
}

View file

@ -15,7 +15,7 @@ abstract class AvesVideoControllerFactory {
}); });
} }
abstract class AvesVideoController { abstract class AvesVideoController with ABRepeatMixin {
final AvesEntryBase _entry; final AvesEntryBase _entry;
final PlaybackStateHandler playbackStateHandler; final PlaybackStateHandler playbackStateHandler;
final VideoSettings settings; final VideoSettings settings;
@ -96,6 +96,7 @@ abstract class AvesVideoController {
int get duration; int get duration;
@override
int get currentPosition; int get currentPosition;
double get progress { double get progress {

View file

@ -3,6 +3,7 @@ import 'dart:async';
import 'package:aves_model/aves_model.dart'; import 'package:aves_model/aves_model.dart';
import 'package:aves_utils/aves_utils.dart'; import 'package:aves_utils/aves_utils.dart';
import 'package:aves_video/aves_video.dart'; import 'package:aves_video/aves_video.dart';
import 'package:aves_video_mpv/src/tracks.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -12,7 +13,7 @@ import 'package:media_kit_video/media_kit_video.dart';
class MpvVideoController extends AvesVideoController { class MpvVideoController extends AvesVideoController {
late Player _instance; late Player _instance;
late VideoStatus _status; late VideoStatus _status;
bool _firstFrameRendered = false; bool _firstFrameRendered = false, _abRepeatSeeking = false;
final ValueNotifier<VideoController?> _controllerNotifier = ValueNotifier(null); final ValueNotifier<VideoController?> _controllerNotifier = ValueNotifier(null);
final List<StreamSubscription> _subscriptions = []; final List<StreamSubscription> _subscriptions = [];
final StreamController<VideoStatus> _statusStreamController = StreamController.broadcast(); final StreamController<VideoStatus> _statusStreamController = StreamController.broadcast();
@ -82,22 +83,41 @@ class MpvVideoController extends AvesVideoController {
void _startListening() { void _startListening() {
_subscriptions.add(statusStream.listen((v) => _status = v)); _subscriptions.add(statusStream.listen((v) => _status = v));
_subscriptions.add(_instance.stream.completed.listen((v) {
if (v) { final playerStream = _instance.stream;
_subscriptions.add(playerStream.completed.listen((completed) {
if (completed) {
_statusStreamController.add(VideoStatus.completed); _statusStreamController.add(VideoStatus.completed);
_completedNotifier.notify(); _completedNotifier.notify();
} }
})); }));
_subscriptions.add(_instance.stream.playing.listen((v) { _subscriptions.add(playerStream.playing.listen((playing) {
if (status == VideoStatus.idle) return; if (status == VideoStatus.idle) return;
_statusStreamController.add(v ? VideoStatus.playing : VideoStatus.paused); _statusStreamController.add(playing ? VideoStatus.playing : VideoStatus.paused);
})); }));
_subscriptions.add(_instance.stream.subtitle.listen((v) => _timedTextStreamController.add(v.isEmpty ? null : v[0]))); _subscriptions.add(playerStream.position.listen((v) {
_subscriptions.add(_instance.stream.videoParams.listen((v) => sarNotifier.value = v.par)); final abRepeat = abRepeatNotifier.value;
_subscriptions.add(_instance.stream.log.listen((v) => debugPrint('libmpv log: $v'))); if (abRepeat != null && status == VideoStatus.playing) {
_subscriptions.add(_instance.stream.error.listen((v) => debugPrint('libmpv error: $v'))); final start = abRepeat.start;
_subscriptions.add(settings.updateStream.where((event) => event.key == SettingKeys.enableVideoHardwareAccelerationKey).listen((_) => _initController())); final end = abRepeat.end;
_subscriptions.add(settings.updateStream.where((event) => event.key == SettingKeys.videoLoopModeKey).listen((_) => _applyLoop())); if (start != null && end != null) {
if (v.inMilliseconds < end) {
_abRepeatSeeking = false;
} else if (!_abRepeatSeeking) {
_abRepeatSeeking = true;
_instance.seek(Duration(milliseconds: start));
}
}
}
}));
_subscriptions.add(playerStream.subtitle.listen((v) => _timedTextStreamController.add(v.isEmpty ? null : v[0])));
_subscriptions.add(playerStream.videoParams.listen((v) => sarNotifier.value = v.par));
_subscriptions.add(playerStream.log.listen((v) => debugPrint('libmpv log: $v')));
_subscriptions.add(playerStream.error.listen((v) => debugPrint('libmpv error: $v')));
final settingsStream = settings.updateStream;
_subscriptions.add(settingsStream.where((event) => event.key == SettingKeys.enableVideoHardwareAccelerationKey).listen((_) => _initController()));
_subscriptions.add(settingsStream.where((event) => event.key == SettingKeys.videoLoopModeKey).listen((_) => _applyLoop()));
} }
void _stopListening() { void _stopListening() {
@ -160,6 +180,7 @@ class MpvVideoController extends AvesVideoController {
// and `PlayerConfiguration.ready` hook is useless. // and `PlayerConfiguration.ready` hook is useless.
await Future.delayed(const Duration(milliseconds: 500)); await Future.delayed(const Duration(milliseconds: 500));
} }
targetMillis = abRepeatNotifier.value?.clamp(targetMillis) ?? targetMillis;
await _instance.seek(Duration(milliseconds: targetMillis)); await _instance.seek(Duration(milliseconds: targetMillis));
} }
@ -355,45 +376,3 @@ class MpvVideoController extends AvesVideoController {
} }
} }
} }
extension ExtraVideoTrack on VideoTrack {
MediaStreamSummary toAves(int index) {
return MediaStreamSummary(
type: MediaStreamType.video,
index: index,
codecName: null,
language: language,
title: title,
width: null,
height: null,
);
}
}
extension ExtraAudioTrack on AudioTrack {
MediaStreamSummary toAves(int index) {
return MediaStreamSummary(
type: MediaStreamType.audio,
index: index,
codecName: null,
language: language,
title: title,
width: null,
height: null,
);
}
}
extension ExtraSubtitleTrack on SubtitleTrack {
MediaStreamSummary toAves(int index) {
return MediaStreamSummary(
type: MediaStreamType.text,
index: index,
codecName: null,
language: language,
title: title,
width: null,
height: null,
);
}
}

View file

@ -0,0 +1,44 @@
import 'package:aves_video/aves_video.dart';
import 'package:media_kit/media_kit.dart';
extension ExtraVideoTrack on VideoTrack {
MediaStreamSummary toAves(int index) {
return MediaStreamSummary(
type: MediaStreamType.video,
index: index,
codecName: null,
language: language,
title: title,
width: null,
height: null,
);
}
}
extension ExtraAudioTrack on AudioTrack {
MediaStreamSummary toAves(int index) {
return MediaStreamSummary(
type: MediaStreamType.audio,
index: index,
codecName: null,
language: language,
title: title,
width: null,
height: null,
);
}
}
extension ExtraSubtitleTrack on SubtitleTrack {
MediaStreamSummary toAves(int index) {
return MediaStreamSummary(
type: MediaStreamType.text,
index: index,
codecName: null,
language: language,
title: title,
width: null,
height: null,
);
}
}

View file

@ -1,4 +1,18 @@
{ {
"ar": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"be": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"bn": [ "bn": [
"itemCount", "itemCount",
"columnCount", "columnCount",
@ -7,6 +21,7 @@
"timeDays", "timeDays",
"focalLength", "focalLength",
"resetTooltip", "resetTooltip",
"stopTooltip",
"doubleBackExitMessage", "doubleBackExitMessage",
"sourceStateLocatingCountries", "sourceStateLocatingCountries",
"sourceStateLocatingPlaces", "sourceStateLocatingPlaces",
@ -62,6 +77,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -659,7 +677,15 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"ca": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"ckb": [ "ckb": [
"stopTooltip",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionShowCountryStates", "chipActionShowCountryStates",
"entryActionFlip", "entryActionFlip",
@ -668,6 +694,9 @@
"entryActionCast", "entryActionCast",
"videoActionCaptureFrame", "videoActionCaptureFrame",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"slideshowActionResume", "slideshowActionResume",
@ -1199,6 +1228,10 @@
], ],
"cs": [ "cs": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"collectionActionSetHome", "collectionActionSetHome",
"setHomeCustomCollection", "setHomeCustomCollection",
"settingsThumbnailShowHdrIcon" "settingsThumbnailShowHdrIcon"
@ -1233,6 +1266,7 @@
"actionRemove", "actionRemove",
"resetTooltip", "resetTooltip",
"saveTooltip", "saveTooltip",
"stopTooltip",
"pickTooltip", "pickTooltip",
"doubleBackExitMessage", "doubleBackExitMessage",
"doNotAskAgain", "doNotAskAgain",
@ -1292,6 +1326,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -1889,8 +1926,19 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"de": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"el": [ "el": [
"stopTooltip",
"entryActionCast", "entryActionCast",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"castDialogTitle", "castDialogTitle",
"aboutDataUsageSectionTitle", "aboutDataUsageSectionTitle",
"aboutDataUsageData", "aboutDataUsageData",
@ -1906,7 +1954,25 @@
"settingsViewerShowHistogram" "settingsViewerShowHistogram"
], ],
"es": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"eu": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"fa": [ "fa": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"coordinateFormatDms", "coordinateFormatDms",
"coordinateDms", "coordinateDms",
"coordinateDmsNorth", "coordinateDmsNorth",
@ -2346,11 +2412,15 @@
"fi": [ "fi": [
"focalLength", "focalLength",
"clearTooltip", "clearTooltip",
"stopTooltip",
"chipActionFilterIn", "chipActionFilterIn",
"entryActionSetAs", "entryActionSetAs",
"entryActionCast", "entryActionCast",
"videoActionUnmute", "videoActionUnmute",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"filterTypeRawLabel", "filterTypeRawLabel",
"filterTypeSphericalVideoLabel", "filterTypeSphericalVideoLabel",
"filterTypeGeotiffLabel", "filterTypeGeotiffLabel",
@ -2887,10 +2957,18 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"fr": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"gl": [ "gl": [
"columnCount", "columnCount",
"saveCopyButtonLabel", "saveCopyButtonLabel",
"applyTooltip", "applyTooltip",
"stopTooltip",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
@ -2899,6 +2977,9 @@
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryActionCast", "entryActionCast",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"entryInfoActionExportMetadata", "entryInfoActionExportMetadata",
@ -3465,6 +3546,7 @@
"actionRemove", "actionRemove",
"resetTooltip", "resetTooltip",
"saveTooltip", "saveTooltip",
"stopTooltip",
"pickTooltip", "pickTooltip",
"doubleBackExitMessage", "doubleBackExitMessage",
"doNotAskAgain", "doNotAskAgain",
@ -3524,6 +3606,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -4122,6 +4207,7 @@
], ],
"hi": [ "hi": [
"stopTooltip",
"sourceStateCataloguing", "sourceStateCataloguing",
"sourceStateLocatingCountries", "sourceStateLocatingCountries",
"sourceStateLocatingPlaces", "sourceStateLocatingPlaces",
@ -4171,6 +4257,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -4768,7 +4857,32 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"hu": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"id": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"is": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"it": [ "it": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"collectionActionSetHome", "collectionActionSetHome",
"setHomeCustomCollection", "setHomeCustomCollection",
"settingsThumbnailShowHdrIcon" "settingsThumbnailShowHdrIcon"
@ -4776,8 +4890,12 @@
"ja": [ "ja": [
"applyTooltip", "applyTooltip",
"stopTooltip",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -4844,6 +4962,7 @@
"timeMinutes", "timeMinutes",
"timeDays", "timeDays",
"focalLength", "focalLength",
"stopTooltip",
"chipActionGoToCountryPage", "chipActionGoToCountryPage",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionGoToTagPage", "chipActionGoToTagPage",
@ -4894,6 +5013,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -5491,16 +5613,27 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"ko": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"lt": [ "lt": [
"columnCount", "columnCount",
"saveCopyButtonLabel", "saveCopyButtonLabel",
"applyTooltip", "applyTooltip",
"stopTooltip",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"entryActionCast", "entryActionCast",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -5613,6 +5746,7 @@
"actionRemove", "actionRemove",
"resetTooltip", "resetTooltip",
"saveTooltip", "saveTooltip",
"stopTooltip",
"pickTooltip", "pickTooltip",
"doubleBackExitMessage", "doubleBackExitMessage",
"doNotAskAgain", "doNotAskAgain",
@ -5672,6 +5806,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -6270,7 +6407,11 @@
], ],
"my": [ "my": [
"stopTooltip",
"entryActionCast", "entryActionCast",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"accessibilityAnimationsRemove", "accessibilityAnimationsRemove",
"accessibilityAnimationsKeep", "accessibilityAnimationsKeep",
"overlayHistogramLuminance", "overlayHistogramLuminance",
@ -6385,7 +6526,11 @@
], ],
"nb": [ "nb": [
"stopTooltip",
"entryActionCast", "entryActionCast",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -6412,12 +6557,16 @@
"nl": [ "nl": [
"columnCount", "columnCount",
"stopTooltip",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
"entryActionShareImageOnly", "entryActionShareImageOnly",
"entryActionShareVideoOnly", "entryActionShareVideoOnly",
"entryActionCast", "entryActionCast",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -6500,8 +6649,12 @@
], ],
"nn": [ "nn": [
"stopTooltip",
"sourceStateCataloguing", "sourceStateCataloguing",
"entryActionCast", "entryActionCast",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"accessibilityAnimationsKeep", "accessibilityAnimationsKeep",
"overlayHistogramNone", "overlayHistogramNone",
"overlayHistogramRGB", "overlayHistogramRGB",
@ -6566,6 +6719,7 @@
"actionRemove", "actionRemove",
"resetTooltip", "resetTooltip",
"saveTooltip", "saveTooltip",
"stopTooltip",
"pickTooltip", "pickTooltip",
"doubleBackExitMessage", "doubleBackExitMessage",
"doNotAskAgain", "doNotAskAgain",
@ -6621,6 +6775,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -7154,12 +7311,37 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"pl": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"pt": [ "pt": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"collectionActionSetHome", "collectionActionSetHome",
"setHomeCustomCollection", "setHomeCustomCollection",
"settingsThumbnailShowHdrIcon" "settingsThumbnailShowHdrIcon"
], ],
"ro": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"ru": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"sat": [ "sat": [
"welcomeOptional", "welcomeOptional",
"welcomeTermsToggle", "welcomeTermsToggle",
@ -7187,6 +7369,7 @@
"actionRemove", "actionRemove",
"resetTooltip", "resetTooltip",
"saveTooltip", "saveTooltip",
"stopTooltip",
"pickTooltip", "pickTooltip",
"doubleBackExitMessage", "doubleBackExitMessage",
"doNotAskAgain", "doNotAskAgain",
@ -7246,6 +7429,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -7843,6 +8029,13 @@
"filePickerUseThisFolder" "filePickerUseThisFolder"
], ],
"sk": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"sl": [ "sl": [
"itemCount", "itemCount",
"columnCount", "columnCount",
@ -7868,6 +8061,7 @@
"actionRemove", "actionRemove",
"resetTooltip", "resetTooltip",
"saveTooltip", "saveTooltip",
"stopTooltip",
"pickTooltip", "pickTooltip",
"doubleBackExitMessage", "doubleBackExitMessage",
"doNotAskAgain", "doNotAskAgain",
@ -7927,6 +8121,9 @@
"videoActionSkip10", "videoActionSkip10",
"videoActionSelectStreams", "videoActionSelectStreams",
"videoActionSetSpeed", "videoActionSetSpeed",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionSettings", "viewerActionSettings",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
@ -8525,6 +8722,10 @@
], ],
"sv": [ "sv": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"widgetOpenPageViewer", "widgetOpenPageViewer",
"rootDirectoryDescription", "rootDirectoryDescription",
"restrictedAccessDialogMessage", "restrictedAccessDialogMessage",
@ -8841,12 +9042,16 @@
"applyButtonLabel", "applyButtonLabel",
"saveCopyButtonLabel", "saveCopyButtonLabel",
"applyTooltip", "applyTooltip",
"stopTooltip",
"chipActionGoToPlacePage", "chipActionGoToPlacePage",
"chipActionLock", "chipActionLock",
"chipActionShowCountryStates", "chipActionShowCountryStates",
"chipActionCreateVault", "chipActionCreateVault",
"chipActionConfigureVault", "chipActionConfigureVault",
"entryActionCast", "entryActionCast",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd",
"viewerActionLock", "viewerActionLock",
"viewerActionUnlock", "viewerActionUnlock",
"editorActionTransform", "editorActionTransform",
@ -9234,5 +9439,40 @@
"filePickerOpenFrom", "filePickerOpenFrom",
"filePickerNoItems", "filePickerNoItems",
"filePickerUseThisFolder" "filePickerUseThisFolder"
],
"tr": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"uk": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"vi": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"zh": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
],
"zh_Hant": [
"stopTooltip",
"videoActionABRepeat",
"videoRepeatActionSetStart",
"videoRepeatActionSetEnd"
] ]
} }