From 52e4f96f7b815a62480dd78a0671f0ee6c5dd373 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Fri, 9 Apr 2021 11:08:16 +0900 Subject: [PATCH] video: added loop mode setting, fixed pause-seek position update, cleanup --- lib/l10n/app_en.arb | 11 ++ lib/l10n/app_ko.arb | 6 + lib/model/settings/enums.dart | 2 + lib/model/settings/settings.dart | 5 + lib/model/settings/video_loop_mode.dart | 35 +++++ lib/widgets/common/video/controller.dart | 32 +--- lib/widgets/common/video/fijkplayer.dart | 95 ++++++------ .../common/video/flutter_ijkplayer.dart | 144 ------------------ .../common/video/flutter_vlc_player.dart | 14 +- lib/widgets/common/video/video_player.dart | 14 +- lib/widgets/settings/settings_page.dart | 18 +++ lib/widgets/viewer/overlay/video.dart | 33 +--- lib/widgets/viewer/visual/video.dart | 1 + pubspec.lock | 2 +- 14 files changed, 139 insertions(+), 273 deletions(-) create mode 100644 lib/model/settings/video_loop_mode.dart delete mode 100644 lib/widgets/common/video/flutter_ijkplayer.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 9bb119024..71663228b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -115,6 +115,13 @@ "coordinateFormatDecimal": "Decimal degrees", "@coordinateFormatDecimal": {}, + "videoLoopModeNever": "Never", + "@videoLoopModeNever": {}, + "videoLoopModeShortOnly": "Short videos only", + "@videoLoopModeShortOnly": {}, + "videoLoopModeAlways": "Always", + "@videoLoopModeAlways": {}, + "mapStyleGoogleNormal": "Google Maps", "@mapStyleGoogleNormal": {}, "mapStyleGoogleHybrid": "Google Maps (Hybrid)", @@ -544,6 +551,10 @@ "@settingsVideoShowVideos": {}, "settingsVideoEnableHardwareAcceleration": "Enable hardware acceleration", "@settingsVideoEnableHardwareAcceleration": {}, + "settingsVideoLoopModeTile": "Loop mode", + "@settingsVideoLoopModeTile": {}, + "settingsVideoLoopModeTitle": "Loop Mode", + "@settingsVideoLoopModeTitle": {}, "settingsSectionPrivacy": "Privacy", "@settingsSectionPrivacy": {}, diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index a3b110136..670a2fa5c 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -59,6 +59,10 @@ "coordinateFormatDms": "도분초", "coordinateFormatDecimal": "소수점", + "videoLoopModeNever": "반복 안 함", + "videoLoopModeShortOnly": "짧은 동영상만 반복", + "videoLoopModeAlways": "항상 반복", + "mapStyleGoogleNormal": "구글 지도", "mapStyleGoogleHybrid": "구글 지도 (위성)", "mapStyleGoogleTerrain": "구글 지도 (지형)", @@ -253,6 +257,8 @@ "settingsSectionVideo": "동영상", "settingsVideoShowVideos": "미디어에 동영상 표시", "settingsVideoEnableHardwareAcceleration": "하드웨어 가속 사용", + "settingsVideoLoopModeTile": "반복 모드", + "settingsVideoLoopModeTitle": "반복 모드", "settingsSectionPrivacy": "개인정보 보호", "settingsEnableAnalytics": "진단 데이터 보내기", diff --git a/lib/model/settings/enums.dart b/lib/model/settings/enums.dart index c6d32dde6..c65fb738c 100644 --- a/lib/model/settings/enums.dart +++ b/lib/model/settings/enums.dart @@ -8,3 +8,5 @@ enum HomePageSetting { collection, albums } enum EntryMapStyle { googleNormal, googleHybrid, googleTerrain, osmHot, stamenToner, stamenWatercolor } enum KeepScreenOn { never, viewerOnly, always } + +enum VideoLoopMode { never, shortOnly, always } diff --git a/lib/model/settings/settings.dart b/lib/model/settings/settings.dart index 28783e845..b7a9de9da 100644 --- a/lib/model/settings/settings.dart +++ b/lib/model/settings/settings.dart @@ -51,6 +51,7 @@ class Settings extends ChangeNotifier { // video static const isVideoHardwareAccelerationEnabledKey = 'video_hwaccel_mediacodec'; + static const videoLoopModeKey = 'video_loop'; // info static const infoMapStyleKey = 'info_map_style'; @@ -232,6 +233,10 @@ class Settings extends ChangeNotifier { bool get isVideoHardwareAccelerationEnabled => getBoolOrDefault(isVideoHardwareAccelerationEnabledKey, true); + VideoLoopMode get videoLoopMode => getEnumOrDefault(videoLoopModeKey, VideoLoopMode.shortOnly, VideoLoopMode.values); + + set videoLoopMode(VideoLoopMode newValue) => setAndNotify(videoLoopModeKey, newValue.toString()); + // info EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); diff --git a/lib/model/settings/video_loop_mode.dart b/lib/model/settings/video_loop_mode.dart new file mode 100644 index 000000000..ec114540c --- /dev/null +++ b/lib/model/settings/video_loop_mode.dart @@ -0,0 +1,35 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/widgets/common/extensions/build_context.dart'; +import 'package:flutter/widgets.dart'; + +import 'enums.dart'; + +extension ExtraVideoLoopMode on VideoLoopMode { + String getName(BuildContext context) { + switch (this) { + case VideoLoopMode.never: + return context.l10n.videoLoopModeNever; + case VideoLoopMode.shortOnly: + return context.l10n.videoLoopModeShortOnly; + case VideoLoopMode.always: + return context.l10n.videoLoopModeAlways; + default: + return toString(); + } + } + + static const shortVideoThreshold = Duration(seconds: 30); + + bool shouldLoop(AvesEntry entry) { + switch (this) { + case VideoLoopMode.never: + return false; + case VideoLoopMode.shortOnly: + if (entry.durationMillis == null) return false; + return entry.durationMillis < shortVideoThreshold.inMilliseconds; + case VideoLoopMode.always: + return true; + } + return false; + } +} diff --git a/lib/widgets/common/video/controller.dart b/lib/widgets/common/video/controller.dart index 518aa74a8..b6101af09 100644 --- a/lib/widgets/common/video/controller.dart +++ b/lib/widgets/common/video/controller.dart @@ -6,9 +6,7 @@ abstract class AvesVideoController { void dispose(); - Future setDataSource(String uri); - - Future refreshVideoInfo(); + Future setDataSource(String uri, {int startMillis = 0}); Future play(); @@ -16,7 +14,11 @@ abstract class AvesVideoController { Future seekTo(int targetMillis); - Future seekToProgress(double progress); + Future seekToProgress(double progress) async { + if (duration != null) { + await seekTo((duration * progress).toInt()); + } + } Listenable get playCompletedListenable; @@ -28,40 +30,22 @@ abstract class AvesVideoController { bool get isPlaying => status == VideoStatus.playing; - bool get isVideoReady; - - Stream get isVideoReadyStream; - int get duration; int get currentPosition; - double get progress => (currentPosition ?? 0).toDouble() / (duration ?? 1); + double get progress => duration == null ? 0 : (currentPosition ?? 0).toDouble() / duration; Stream get positionStream; Widget buildPlayerWidget(BuildContext context, AvesEntry entry); } -class AvesVideoInfo { - // in millis - int duration, currentPosition; - - AvesVideoInfo({ - this.duration, - this.currentPosition, - }); -} - enum VideoStatus { idle, initialized, - preparing, - prepared, - playing, paused, + playing, completed, - stopped, - disposed, error, } diff --git a/lib/widgets/common/video/fijkplayer.dart b/lib/widgets/common/video/fijkplayer.dart index b2eae99fe..857bc69f9 100644 --- a/lib/widgets/common/video/fijkplayer.dart +++ b/lib/widgets/common/video/fijkplayer.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:aves/model/entry.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/settings/video_loop_mode.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/common/video/controller.dart'; import 'package:fijkplayer/fijkplayer.dart'; @@ -12,7 +13,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { FijkPlayer _instance; final List _subscriptions = []; final StreamController _valueStreamController = StreamController.broadcast(); - final AChangeNotifier _playFinishNotifier = AChangeNotifier(); + final AChangeNotifier _completedNotifier = AChangeNotifier(); Offset _macroBlockCrop = Offset.zero; Stream get _valueStream => _valueStreamController.stream; @@ -25,41 +26,45 @@ class IjkPlayerAvesVideoController extends AvesVideoController { // cf https://www.jianshu.com/p/843c86a9e9ad final option = FijkOption(); - // `fastseek`: enable fast, but inaccurate seeks for some formats - option.setFormatOption('fflags', 'fastseek'); - // `enable-accurate-seek`: enable accurate seek, default: 0, in [0, 1] - option.setPlayerOption('enable-accurate-seek', 1); - // `framedrop`: drop frames when cpu is too slow, default: 0, in [-1, 120] - option.setPlayerOption('framedrop', 5); - final _hwAccelerationEnabled = settings.isVideoHardwareAccelerationEnabled; - if (_hwAccelerationEnabled) { - // crop HW acceleration macroblock misalignment for videos with dimensions that do not fit 16x + // when accurate seek is enabled and seeking fails, it takes time (cf `accurate-seek-timeout`) to acknowledge the error and proceed + // failure seems to happen when pause-seeking videos with an audio stream, whatever container or video stream + // player cannot be dynamically set to use accurate seek only when playing + const accurateSeekEnabled = false; + + // when HW acceleration is enabled, videos with dimensions that do not fit 16x macroblocks need cropping + // TODO TLAD HW codecs sometimes fail when seek-starting some videos, e.g. MP2TS/h264(HDPR) + final hwAccelerationEnabled = settings.isVideoHardwareAccelerationEnabled; + if (hwAccelerationEnabled) { final s = entry.displaySize % 16 * -1 % 16; _macroBlockCrop = Offset(s.width, s.height); } + + final loopEnabled = settings.videoLoopMode.shouldLoop(entry); + + // `fastseek`: enable fast, but inaccurate seeks for some formats + // in practice the flag seems ineffective, but harmless too + option.setFormatOption('fflags', 'fastseek'); + + // `enable-accurate-seek`: enable accurate seek, default: 0, in [0, 1] + option.setPlayerOption('enable-accurate-seek', accurateSeekEnabled ? 1 : 0); + + // `accurate-seek-timeout`: accurate seek timeout, default: 5000 ms, in [0, 5000] + option.setPlayerOption('accurate-seek-timeout', 1000); + + // `framedrop`: drop frames when cpu is too slow, default: 0, in [-1, 120] + option.setPlayerOption('framedrop', 5); + + // `loop`: set number of times the playback shall be looped, default: 1, in [INT_MIN, INT_MAX] + option.setPlayerOption('loop', loopEnabled ? -1 : 1); + // `mediacodec-all-videos`: MediaCodec: enable all videos, default: 0, in [0, 1] - // TODO TLAD enabling `mediacodec-all-videos` randomly fails to render some videos, e.g. MP2TS/h264(HDPR) - option.setPlayerOption('mediacodec-all-videos', _hwAccelerationEnabled ? 1 : 0); - - // option.setPlayerOption('analyzemaxduration', 200 * 1024); - // option.setPlayerOption('analyzeduration', 200 * 1024); - // option.setPlayerOption('probesize', 1024 * 1024); - - // CJL options - // option.setPlayerOption('reconnect', 5); - // option.setPlayerOption('mediacodec', 1); - // option.setPlayerOption('packet-buffering', 1); - // option.setPlayerOption('soundtouch', 1); - // option.setPlayerOption('start-on-prepared', 1); - - // TODO TLAD check looping - // option.setPlayerOption('loop', 42); + option.setPlayerOption('mediacodec-all-videos', hwAccelerationEnabled ? 1 : 0); _instance.applyOptions(option); _instance.addListener(_onValueChanged); - _subscriptions.add(_valueStream.where((value) => value.completed).listen((_) => _playFinishNotifier.notifyListeners())); + _subscriptions.add(_valueStream.where((value) => value.state == FijkState.completed).listen((_) => _completedNotifier.notifyListeners())); } @override @@ -77,10 +82,13 @@ class IjkPlayerAvesVideoController extends AvesVideoController { // enable autoplay, even when seeking on uninitialized player, otherwise the texture is not updated // as a workaround, pausing after a brief duration is possible, but fiddly @override - Future setDataSource(String uri) => _instance.setDataSource(uri, autoPlay: true); - - @override - Future refreshVideoInfo() => null; + Future setDataSource(String uri, {int startMillis = 0}) async { + if (startMillis > 0) { + // `seek-at-start`: set offset of player should be seeked, default: 0, in [0, INT_MAX] + await _instance.setOption(FijkOption.playerCategory, 'seek-at-start', startMillis); + } + await _instance.setDataSource(uri, autoPlay: true); + } @override Future play() => _instance.start(); @@ -92,10 +100,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { Future seekTo(int targetMillis) => _instance.seekTo(targetMillis); @override - Future seekToProgress(double progress) => _instance.seekTo((duration * progress).toInt()); - - @override - Listenable get playCompletedListenable => _playFinishNotifier; + Listenable get playCompletedListenable => _completedNotifier; @override VideoStatus get status => _instance.state.toAves; @@ -103,12 +108,6 @@ class IjkPlayerAvesVideoController extends AvesVideoController { @override Stream get statusStream => _valueStream.map((value) => value.state.toAves); - @override - bool get isVideoReady => _instance.value.videoRenderStart; - - @override - Stream get isVideoReadyStream => _valueStream.map((value) => value.videoRenderStart); - @override bool get isPlayable => _instance.isPlayable(); @@ -141,23 +140,19 @@ extension ExtraIjkStatus on FijkState { VideoStatus get toAves { switch (this) { case FijkState.idle: + case FijkState.end: + case FijkState.stopped: return VideoStatus.idle; case FijkState.initialized: - return VideoStatus.initialized; case FijkState.asyncPreparing: - return VideoStatus.preparing; + return VideoStatus.initialized; case FijkState.prepared: - return VideoStatus.prepared; - case FijkState.started: - return VideoStatus.playing; case FijkState.paused: return VideoStatus.paused; + case FijkState.started: + return VideoStatus.playing; case FijkState.completed: return VideoStatus.completed; - case FijkState.stopped: - return VideoStatus.stopped; - case FijkState.end: - return VideoStatus.disposed; case FijkState.error: return VideoStatus.error; } diff --git a/lib/widgets/common/video/flutter_ijkplayer.dart b/lib/widgets/common/video/flutter_ijkplayer.dart deleted file mode 100644 index c27852efc..000000000 --- a/lib/widgets/common/video/flutter_ijkplayer.dart +++ /dev/null @@ -1,144 +0,0 @@ -// import 'dart:async'; -// -// import 'package:aves/model/entry.dart'; -// import 'package:aves/utils/change_notifier.dart'; -// import 'package:aves/widgets/common/video/controller.dart'; -// import 'package:flutter/material.dart'; -// import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; -// -// class IjkPlayerAvesVideoController extends AvesVideoController { -// IjkMediaController _instance; -// final List _subscriptions = []; -// final AChangeNotifier _playFinishNotifier = AChangeNotifier(); -// -// IjkPlayerAvesVideoController() { -// _instance = IjkMediaController(); -// _subscriptions.add(_instance.playFinishStream.listen((_) => _playFinishNotifier.notifyListeners())); -// } -// -// @override -// void dispose() { -// _subscriptions -// ..forEach((sub) => sub.cancel()) -// ..clear(); -// _instance?.dispose(); -// } -// -// // enable autoplay, even when seeking on uninitialized player, otherwise the texture is not updated -// // as a workaround, pausing after a brief duration is possible, but fiddly -// @override -// Future setDataSource(String uri) => _instance.setDataSource(DataSource.photoManagerUrl(uri), autoPlay: true); -// -// @override -// Future refreshVideoInfo() => _instance.refreshVideoInfo(); -// -// @override -// Future play() => _instance.play(); -// -// @override -// Future pause() => _instance.pause(); -// -// @override -// Future seekTo(int targetMillis) => _instance.seekTo(targetMillis / 1000.0); -// -// @override -// Future seekToProgress(double progress) => _instance.seekToProgress(progress); -// -// @override -// Listenable get playCompletedListenable => _playFinishNotifier; -// -// @override -// VideoStatus get status => _instance.ijkStatus.toAves; -// -// @override -// Stream get statusStream => _instance.ijkStatusStream.map((status) => status.toAves); -// -// // we check whether video info is ready instead of checking for `noDatasource` status, -// // as the controller could also be uninitialized with the `pause` status -// // (e.g. when switching between video entries without playing them the first time) -// @override -// bool get isPlayable => _videoInfo.hasData && [VideoStatus.prepared, VideoStatus.playing, VideoStatus.paused, VideoStatus.completed].contains(status); -// -// @override -// bool get isVideoReady => _instance.textureId != null; -// -// @override -// Stream get isVideoReadyStream => _instance.textureIdStream.map((id) => id != null); -// -// // `videoInfo` is never null (even if `toString` prints `null`) -// // check presence with `hasData` instead -// VideoInfo get _videoInfo => _instance.videoInfo; -// -// @override -// int get duration => _videoInfo.durationMillis; -// -// @override -// int get currentPosition => _videoInfo.currentPositionMillis; -// -// @override -// Stream get positionStream => _instance.videoInfoStream.map((info) => info.currentPositionMillis); -// -// @override -// Widget buildPlayerWidget(BuildContext context, AvesEntry entry) => IjkPlayer( -// mediaController: _instance, -// controllerWidgetBuilder: (controller) => SizedBox.shrink(), -// statusWidgetBuilder: (context, controller, status) => SizedBox.shrink(), -// textureBuilder: (context, controller, info) { -// var id = controller.textureId; -// var child = id != null -// ? Texture( -// textureId: id, -// ) -// : Container( -// color: Colors.black, -// ); -// -// final degree = entry.rotationDegrees ?? 0; -// if (degree != 0) { -// child = RotatedBox( -// quarterTurns: degree ~/ 90, -// child: child, -// ); -// } -// -// return Center( -// child: AspectRatio( -// aspectRatio: entry.displayAspectRatio, -// child: child, -// ), -// ); -// }, -// backgroundColor: Colors.transparent, -// ); -// } -// -// extension ExtraVideoInfo on VideoInfo { -// int get durationMillis => duration == null ? null : (duration * 1000).toInt(); -// -// int get currentPositionMillis => currentPosition == null ? null : (currentPosition * 1000).toInt(); -// } -// -// extension ExtraIjkStatus on IjkStatus { -// VideoStatus get toAves { -// switch (this) { -// case IjkStatus.noDatasource: -// return VideoStatus.idle; -// case IjkStatus.preparing: -// return VideoStatus.preparing; -// case IjkStatus.prepared: -// return VideoStatus.prepared; -// case IjkStatus.playing: -// return VideoStatus.playing; -// case IjkStatus.pause: -// return VideoStatus.paused; -// case IjkStatus.complete: -// return VideoStatus.completed; -// case IjkStatus.disposed: -// return VideoStatus.disposed; -// case IjkStatus.setDatasourceFail: -// case IjkStatus.error: -// return VideoStatus.error; -// } -// return VideoStatus.idle; -// } -// } diff --git a/lib/widgets/common/video/flutter_vlc_player.dart b/lib/widgets/common/video/flutter_vlc_player.dart index 424f8a46f..6dfdfcefa 100644 --- a/lib/widgets/common/video/flutter_vlc_player.dart +++ b/lib/widgets/common/video/flutter_vlc_player.dart @@ -21,7 +21,7 @@ // VlcAvesVideoController(); // // @override -// Future setDataSource(String uri) async { +// Future setDataSource(String uri, {int startMillis = 0}) async { // _instance = VlcPlayerController.file( // File(uri), // ); @@ -49,9 +49,6 @@ // void _onValueChanged() => _valueStreamController.add(_instance.value); // // @override -// Future refreshVideoInfo() => null; -// -// @override // Future play() => _instance.play(); // // @override @@ -61,9 +58,6 @@ // Future seekTo(int targetMillis) => _instance.seekTo(Duration(milliseconds: targetMillis)); // // @override -// Future seekToProgress(double progress) => _instance.seekTo(Duration(milliseconds: (duration * progress).toInt())); -// -// @override // Listenable get playCompletedListenable => _playFinishNotifier; // // @override @@ -73,12 +67,6 @@ // Stream get statusStream => _valueStream.map((value) => value.toAves); // // @override -// bool get isVideoReady => _instance != null && _instance.value.isInitialized && !_instance.value.hasError; -// -// @override -// Stream get isVideoReadyStream => _valueStream.map((value) => value.isInitialized && !value.hasError); -// -// @override // bool get isPlayable => _instance != null; // // @override diff --git a/lib/widgets/common/video/video_player.dart b/lib/widgets/common/video/video_player.dart index efd614d85..6e0fb35af 100644 --- a/lib/widgets/common/video/video_player.dart +++ b/lib/widgets/common/video/video_player.dart @@ -18,7 +18,7 @@ // VideoPlayerAvesVideoController(); // // @override -// Future setDataSource(String uri) async { +// Future setDataSource(String uri, {int startMillis = 0}) async { // _instance = VideoPlayerController.network(uri); // _instance.addListener(_onValueChanged); // _subscriptions.add(_valueStream.where((value) => value.position > value.duration).listen((_) => _playFinishNotifier.notifyListeners())); @@ -40,9 +40,6 @@ // void _onValueChanged() => _valueStreamController.add(_instance.value); // // @override -// Future refreshVideoInfo() => null; -// -// @override // Future play() => _instance.play(); // // @override @@ -52,9 +49,6 @@ // Future seekTo(int targetMillis) => _instance.seekTo(Duration(milliseconds: targetMillis)); // // @override -// Future seekToProgress(double progress) => _instance.seekTo(Duration(milliseconds: (duration * progress).toInt())); -// -// @override // Listenable get playCompletedListenable => _playFinishNotifier; // // @override @@ -64,12 +58,6 @@ // Stream get statusStream => _valueStream.map((value) => value.toAves); // // @override -// bool get isVideoReady => _instance != null && _instance.value.isInitialized && !_instance.value.hasError; -// -// @override -// Stream get isVideoReadyStream => _valueStream.map((value) => value.isInitialized && !value.hasError); -// -// @override // bool get isPlayable => _instance != null && _instance.value.isInitialized && !_instance.value.hasError; // // @override diff --git a/lib/widgets/settings/settings_page.dart b/lib/widgets/settings/settings_page.dart index d041de0ec..7b5a02a5d 100644 --- a/lib/widgets/settings/settings_page.dart +++ b/lib/widgets/settings/settings_page.dart @@ -4,6 +4,7 @@ import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/settings.dart'; +import 'package:aves/model/settings/video_loop_mode.dart'; import 'package:aves/model/source/collection_source.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -244,6 +245,23 @@ class _SettingsPageState extends State { onChanged: (v) => settings.isVideoHardwareAccelerationEnabled = v, title: Text(context.l10n.settingsVideoEnableHardwareAcceleration), ), + ListTile( + title: Text(context.l10n.settingsVideoLoopModeTile), + subtitle: Text(settings.videoLoopMode.getName(context)), + onTap: () async { + final value = await showDialog( + context: context, + builder: (context) => AvesSelectionDialog( + initialValue: settings.videoLoopMode, + options: Map.fromEntries(VideoLoopMode.values.map((v) => MapEntry(v, v.getName(context)))), + title: context.l10n.settingsVideoLoopModeTitle, + ), + ); + if (value != null) { + settings.videoLoopMode = value; + } + }, + ), ], ); } diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index 9ac0a908f..59451765b 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -35,9 +35,6 @@ class _VideoControlOverlayState extends State with SingleTi final List _subscriptions = []; double _seekTargetPercent; - // video info is not refreshed by default, so we use a timer to do so - Timer _progressTimer; - AvesEntry get entry => widget.entry; Animation get scale => widget.scale; @@ -74,16 +71,13 @@ class _VideoControlOverlayState extends State with SingleTi void _registerWidget(VideoControlOverlay widget) { _subscriptions.add(widget.controller.statusStream.listen(_onStatusChange)); - _subscriptions.add(widget.controller.isVideoReadyStream.listen(_onVideoReadinessChanged)); _onStatusChange(widget.controller.status); - _onVideoReadinessChanged(widget.controller.isVideoReady); } void _unregisterWidget(VideoControlOverlay widget) { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); - _stopTimer(); } @override @@ -194,24 +188,6 @@ class _VideoControlOverlayState extends State with SingleTi ); } - void _startTimer() { - if (!controller.isVideoReady) return; - _progressTimer?.cancel(); - _progressTimer = Timer.periodic(Durations.videoProgressTimerInterval, (_) => controller.refreshVideoInfo()); - } - - void _stopTimer() { - _progressTimer?.cancel(); - } - - void _onVideoReadinessChanged(bool isVideoReady) { - if (isVideoReady) { - _startTimer(); - } else { - _stopTimer(); - } - } - void _onStatusChange(VideoStatus status) { if (status == VideoStatus.playing && _seekTargetPercent != null) { _seekFromTarget(); @@ -247,16 +223,17 @@ class _VideoControlOverlayState extends State with SingleTi if (isPlayable) { await _seekFromTarget(); } else { - await controller.setDataSource(entry.uri); + // controller duration is not set yet, so we use the expected duration instead + final seekTargetMillis = (entry.durationMillis * _seekTargetPercent).toInt(); + await controller.setDataSource(entry.uri, startMillis: seekTargetMillis); + _seekTargetPercent = null; } } Future _seekFromTarget() async { // `seekToProgress` is not safe as it can be called when the `duration` is not set yet // so we make sure the video info is up to date first - if (controller.duration == null) { - await controller.refreshVideoInfo(); - } else { + if (controller.duration != null) { await controller.seekToProgress(_seekTargetPercent); _seekTargetPercent = null; } diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 730db595b..488142ce8 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -68,5 +68,6 @@ class _VideoViewState extends State { }); } + // not called when looping void _onPlayCompleted() => controller.seekTo(0); } diff --git a/pubspec.lock b/pubspec.lock index c9333c8e3..0dcb3dc5e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -211,7 +211,7 @@ packages: description: path: "." ref: aves - resolved-ref: "7171c3ede20f407b523c18692572cbcd12acc169" + resolved-ref: a4640923c7c6141ff543f676a8cb7d2fe8b0ffba url: "git://github.com/deckerst/fijkplayer.git" source: git version: "0.8.7"