From ea51bece7ecc4f8a84c617828985e1a9174d6c29 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Wed, 21 Apr 2021 19:18:38 +0900 Subject: [PATCH] video controller review --- lib/widgets/common/video/conductor.dart | 37 +++++ lib/widgets/common/video/controller.dart | 23 ++-- lib/widgets/common/video/fijkplayer.dart | 126 ++++++++++-------- .../viewer/entry_horizontal_pager.dart | 7 - lib/widgets/viewer/entry_vertical_pager.dart | 5 - lib/widgets/viewer/entry_viewer_page.dart | 12 +- lib/widgets/viewer/entry_viewer_stack.dart | 41 +++--- lib/widgets/viewer/overlay/video.dart | 61 ++------- .../viewer/visual/entry_page_view.dart | 12 +- lib/widgets/viewer/visual/video.dart | 2 +- 10 files changed, 166 insertions(+), 160 deletions(-) create mode 100644 lib/widgets/common/video/conductor.dart diff --git a/lib/widgets/common/video/conductor.dart b/lib/widgets/common/video/conductor.dart new file mode 100644 index 000000000..f4a4d7485 --- /dev/null +++ b/lib/widgets/common/video/conductor.dart @@ -0,0 +1,37 @@ +import 'package:aves/model/entry.dart'; +import 'package:aves/widgets/common/video/controller.dart'; +import 'package:aves/widgets/common/video/fijkplayer.dart'; +import 'package:fijkplayer/fijkplayer.dart'; + +class VideoConductor { + final List _controllers = []; + + static const maxControllerCount = 3; + + VideoConductor() { + FijkLog.setLevel(FijkLogLevel.Warn); + } + + Future dispose() async { + await Future.forEach(_controllers, (controller) => controller.dispose()); + _controllers.clear(); + } + + AvesVideoController getOrCreateController(AvesEntry entry) { + var controller = getController(entry); + if (controller != null) { + _controllers.remove(controller); + } else { + controller = IjkPlayerAvesVideoController(entry); + } + _controllers.insert(0, controller); + while (_controllers.length > maxControllerCount) { + _controllers.removeLast().dispose(); + } + return controller; + } + + AvesVideoController getController(AvesEntry entry) => _controllers.firstWhere((controller) => controller.entry == entry, orElse: () => null); + + void pauseAll() => _controllers.forEach((controller) => controller.pause()); +} diff --git a/lib/widgets/common/video/controller.dart b/lib/widgets/common/video/controller.dart index b6101af09..322ccf282 100644 --- a/lib/widgets/common/video/controller.dart +++ b/lib/widgets/common/video/controller.dart @@ -1,12 +1,17 @@ import 'package:aves/model/entry.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; abstract class AvesVideoController { - AvesVideoController(); + AvesEntry _entry; - void dispose(); + AvesEntry get entry => _entry; - Future setDataSource(String uri, {int startMillis = 0}); + AvesVideoController(AvesEntry entry) { + _entry = entry; + } + + Future dispose(); Future play(); @@ -14,11 +19,7 @@ abstract class AvesVideoController { Future seekTo(int targetMillis); - Future seekToProgress(double progress) async { - if (duration != null) { - await seekTo((duration * progress).toInt()); - } - } + Future seekToProgress(double progress) => seekTo((duration * progress).toInt()); Listenable get playCompletedListenable; @@ -26,7 +27,7 @@ abstract class AvesVideoController { Stream get statusStream; - bool get isPlayable; + bool get isReady; bool get isPlaying => status == VideoStatus.playing; @@ -34,11 +35,11 @@ abstract class AvesVideoController { int get currentPosition; - double get progress => duration == null ? 0 : (currentPosition ?? 0).toDouble() / duration; + double get progress => (currentPosition ?? 0).toDouble() / duration; Stream get positionStream; - Widget buildPlayerWidget(BuildContext context, AvesEntry entry); + Widget buildPlayerWidget(BuildContext context); } enum VideoStatus { diff --git a/lib/widgets/common/video/fijkplayer.dart b/lib/widgets/common/video/fijkplayer.dart index 53f891cfc..4f49f1441 100644 --- a/lib/widgets/common/video/fijkplayer.dart +++ b/lib/widgets/common/video/fijkplayer.dart @@ -23,7 +23,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { final ValueNotifier _selectedVideoStream = ValueNotifier(null); final ValueNotifier _selectedAudioStream = ValueNotifier(null); final ValueNotifier _selectedTextStream = ValueNotifier(null); - final ValueNotifier> _sar = ValueNotifier(Tuple2(1, 1)); + final ValueNotifier> _sar = ValueNotifier(null); Timer _initialPlayTimer; Stream get _valueStream => _valueStreamController.stream; @@ -31,15 +31,47 @@ class IjkPlayerAvesVideoController extends AvesVideoController { static const initialPlayDelay = Duration(milliseconds: 100); static const gifLikeVideoDurationThreshold = Duration(seconds: 10); - IjkPlayerAvesVideoController(AvesEntry entry) { - FijkLog.setLevel(FijkLogLevel.Warn); + IjkPlayerAvesVideoController(AvesEntry entry) : super(entry) { _instance = FijkPlayer(); + _startListening(); + } + @override + Future dispose() async { + _initialPlayTimer?.cancel(); + _stopListening(); + await _valueStreamController.close(); + await _instance.release(); + } + + void _startListening() { + _instance.addListener(_onValueChanged); + _subscriptions.add(_valueStream.where((value) => value.state == FijkState.completed).listen((_) => _completedNotifier.notifyListeners())); + } + + void _stopListening() { + _instance.removeListener(_onValueChanged); + _subscriptions + ..forEach((sub) => sub.cancel()) + ..clear(); + } + + Future _init({int startMillis = 0}) async { + _sar.value = Tuple2(1, 1); + _applyOptions(startMillis); + + // calling `setDataSource()` with `autoPlay` starts as soon as possible, but often yields initial artifacts + // so we introduce a small delay after the player is declared `prepared`, before playing + await _instance.setDataSourceUntilPrepared(entry.uri); + _initialPlayTimer = Timer(initialPlayDelay, play); + } + + void _applyOptions(int startMillis) { // FFmpeg options // cf https://github.com/Bilibili/ijkplayer/blob/master/ijkmedia/ijkplayer/ff_ffplay_options.h // cf https://www.jianshu.com/p/843c86a9e9ad - final option = FijkOption(); + final options = FijkOption(); // 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 @@ -62,42 +94,34 @@ class IjkPlayerAvesVideoController extends AvesVideoController { // `fastseek`: enable fast, but inaccurate seeks for some formats // in practice the flag seems ineffective, but harmless too - option.setFormatOption('fflags', 'fastseek'); + options.setFormatOption('fflags', 'fastseek'); // `enable-accurate-seek`: enable accurate seek, default: 0, in [0, 1] - option.setPlayerOption('enable-accurate-seek', accurateSeekEnabled ? 1 : 0); + options.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); + options.setPlayerOption('accurate-seek-timeout', 1000); // `framedrop`: drop frames when cpu is too slow, default: 0, in [-1, 120] - option.setPlayerOption('framedrop', 5); + options.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); + options.setPlayerOption('loop', loopEnabled ? -1 : 1); // `mediacodec-all-videos`: MediaCodec: enable all videos, default: 0, in [0, 1] - option.setPlayerOption('mediacodec-all-videos', hwAccelerationEnabled ? 1 : 0); + options.setPlayerOption('mediacodec-all-videos', hwAccelerationEnabled ? 1 : 0); + + // `seek-at-start`: set offset of player should be seeked, default: 0, in [0, INT_MAX] + options.setPlayerOption('seek-at-start', startMillis); + + // `cover-after-prepared`: show cover provided to `FijkView` when player is `prepared` without auto play, default: 0, in [0, 1] + options.setPlayerOption('cover-after-prepared', 0); // TODO TLAD try subs // `subtitle`: decode subtitle stream, default: 0, in [0, 1] // option.setPlayerOption('subtitle', 1); - _instance.applyOptions(option); - - _instance.addListener(_onValueChanged); - _subscriptions.add(_valueStream.where((value) => value.state == FijkState.completed).listen((_) => _completedNotifier.notifyListeners())); - } - - @override - void dispose() { - _initialPlayTimer?.cancel(); - _instance.removeListener(_onValueChanged); - _valueStreamController.close(); - _subscriptions - ..forEach((sub) => sub.cancel()) - ..clear(); - _instance.release(); + _instance.applyOptions(options); } void _fetchSelectedStreams() async { @@ -151,39 +175,33 @@ class IjkPlayerAvesVideoController extends AvesVideoController { _valueStreamController.add(_instance.value); } - // always start playing, 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, {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); + Future play() async { + if (isReady) { + await _instance.start(); + } else { + await _init(); } - // calling `setDataSource()` with `autoPlay` starts as soon as possible, but often yields initial artifacts - // so we introduce a small delay after the player is declared `prepared`, before playing - await _instance.setDataSourceUntilPrepared(uri); - _initialPlayTimer = Timer(initialPlayDelay, play); } @override - Future play() { - if (_instance.isPlayable()) { - _instance.start(); - } - return SynchronousFuture(null); - } - - @override - Future pause() { - if (_instance.isPlayable()) { + Future pause() async { + if (isReady) { _initialPlayTimer?.cancel(); - _instance.pause(); + await _instance.pause(); } - return SynchronousFuture(null); } @override - Future seekTo(int targetMillis) => _instance.seekTo(targetMillis); + Future seekTo(int targetMillis) async { + if (isReady) { + await _instance.seekTo(targetMillis); + } else { + // always start playing, even when seeking on uninitialized player, otherwise the texture is not updated + // as a workaround, pausing after a brief duration is possible, but fiddly + await _init(startMillis: targetMillis); + } + } @override Listenable get playCompletedListenable => _completedNotifier; @@ -195,10 +213,14 @@ class IjkPlayerAvesVideoController extends AvesVideoController { Stream get statusStream => _valueStream.map((value) => value.state.toAves); @override - bool get isPlayable => _instance.isPlayable(); + bool get isReady => _instance.isPlayable(); @override - int get duration => _instance.value.duration.inMilliseconds; + int get duration { + final controllerDuration = _instance.value.duration.inMilliseconds; + // use expected duration when controller duration is not set yet + return (controllerDuration == null || controllerDuration == 0) ? entry.durationMillis : controllerDuration; + } @override int get currentPosition => _instance.currentPos.inMilliseconds; @@ -207,7 +229,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController { Stream get positionStream => _instance.onCurrentPosUpdate.map((pos) => pos.inMilliseconds); @override - Widget buildPlayerWidget(BuildContext context, AvesEntry entry) { + Widget buildPlayerWidget(BuildContext context) { return ValueListenableBuilder>( valueListenable: _sar, builder: (context, sar, child) { @@ -272,7 +294,7 @@ extension ExtraIjkStatus on FijkState { extension ExtraFijkPlayer on FijkPlayer { Future setDataSourceUntilPrepared(String uri) async { - await setDataSource(uri, autoPlay: false); + await setDataSource(uri, autoPlay: false, showCover: false); final completer = Completer(); void onChange() { diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index f688df0c2..f338b8222 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -3,7 +3,6 @@ import 'package:aves/model/multipage.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/magnifier/pan/gesture_detector_scope.dart'; import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; -import 'package:aves/widgets/common/video/controller.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:flutter/material.dart'; @@ -14,7 +13,6 @@ class MultiEntryScroller extends StatefulWidget { final CollectionLens collection; final PageController pageController; final ValueChanged onPageChanged; - final List> videoControllers; final List> multiPageControllers; final void Function(String uri) onViewDisposed; @@ -22,7 +20,6 @@ class MultiEntryScroller extends StatefulWidget { this.collection, this.pageController, this.onPageChanged, - this.videoControllers, this.multiPageControllers, this.onViewDisposed, }); @@ -87,7 +84,6 @@ class _MultiEntryScrollerState extends State with AutomaticK mainEntry: entry, page: page, viewportSize: mqSize, - videoControllers: widget.videoControllers, onDisposed: () => widget.onViewDisposed?.call(entry.uri), ); }, @@ -104,12 +100,10 @@ class _MultiEntryScrollerState extends State with AutomaticK class SingleEntryScroller extends StatefulWidget { final AvesEntry entry; - final List> videoControllers; final List> multiPageControllers; const SingleEntryScroller({ this.entry, - this.videoControllers, this.multiPageControllers, }); @@ -158,7 +152,6 @@ class _SingleEntryScrollerState extends State with Automati mainEntry: entry, page: page, viewportSize: mqSize, - videoControllers: widget.videoControllers, ); }, ); diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index 1732bbe2a..5601e18f9 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -3,7 +3,6 @@ import 'dart:math'; import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/magnifier/pan/scroll_physics.dart'; -import 'package:aves/widgets/common/video/controller.dart'; import 'package:aves/widgets/viewer/entry_horizontal_pager.dart'; import 'package:aves/widgets/viewer/info/info_page.dart'; import 'package:aves/widgets/viewer/info/notifications.dart'; @@ -16,7 +15,6 @@ import 'package:tuple/tuple.dart'; class ViewerVerticalPageView extends StatefulWidget { final CollectionLens collection; final ValueNotifier entryNotifier; - final List> videoControllers; final List> multiPageControllers; final PageController horizontalPager, verticalPager; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; @@ -26,7 +24,6 @@ class ViewerVerticalPageView extends StatefulWidget { const ViewerVerticalPageView({ @required this.collection, @required this.entryNotifier, - @required this.videoControllers, @required this.multiPageControllers, @required this.verticalPager, @required this.horizontalPager, @@ -92,13 +89,11 @@ class _ViewerVerticalPageViewState extends State { collection: collection, pageController: widget.horizontalPager, onPageChanged: widget.onHorizontalPageChanged, - videoControllers: widget.videoControllers, multiPageControllers: widget.multiPageControllers, onViewDisposed: widget.onViewDisposed, ) : SingleEntryScroller( entry: entry, - videoControllers: widget.videoControllers, multiPageControllers: widget.multiPageControllers, ), NotificationListener( diff --git a/lib/widgets/viewer/entry_viewer_page.dart b/lib/widgets/viewer/entry_viewer_page.dart index f285d8292..0cefd2007 100644 --- a/lib/widgets/viewer/entry_viewer_page.dart +++ b/lib/widgets/viewer/entry_viewer_page.dart @@ -1,8 +1,10 @@ import 'package:aves/model/entry.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; +import 'package:aves/widgets/common/video/conductor.dart'; import 'package:aves/widgets/viewer/entry_viewer_stack.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class EntryViewerPage extends StatelessWidget { static const routeName = '/viewer'; @@ -20,9 +22,13 @@ class EntryViewerPage extends StatelessWidget { Widget build(BuildContext context) { return MediaQueryDataProvider( child: Scaffold( - body: EntryViewerStack( - collection: collection, - initialEntry: initialEntry, + body: Provider( + create: (context) => VideoConductor(), + dispose: (context, value) => value.dispose(), + child: EntryViewerStack( + collection: collection, + initialEntry: initialEntry, + ), ), backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black, resizeToAvoidBottomInset: false, diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index c8aa61092..55ed963a3 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -11,8 +11,8 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/utils/change_notifier.dart'; import 'package:aves/widgets/collection/collection_page.dart'; import 'package:aves/widgets/common/basic/insets.dart'; +import 'package:aves/widgets/common/video/conductor.dart'; import 'package:aves/widgets/common/video/controller.dart'; -import 'package:aves/widgets/common/video/fijkplayer.dart'; import 'package:aves/widgets/viewer/entry_action_delegate.dart'; import 'package:aves/widgets/viewer/entry_vertical_pager.dart'; import 'package:aves/widgets/viewer/hero.dart'; @@ -57,7 +57,6 @@ class _EntryViewerStackState extends State with SingleTickerPr Animation _bottomOverlayOffset; EdgeInsets _frozenViewInsets, _frozenViewPadding; EntryActionDelegate _actionDelegate; - final List> _videoControllers = []; final List> _multiPageControllers = []; final List>> _viewStateNotifiers = []; final ValueNotifier _heroInfoNotifier = ValueNotifier(null); @@ -128,8 +127,6 @@ class _EntryViewerStackState extends State with SingleTickerPr void dispose() { _overlayAnimationController.dispose(); _overlayVisible.removeListener(_onOverlayVisibleChange); - _videoControllers.forEach((kv) => kv.item2.dispose()); - _videoControllers.clear(); _multiPageControllers.forEach((kv) => kv.item2.dispose()); _multiPageControllers.clear(); _verticalPager.removeListener(_onVerticalPageControllerChange); @@ -198,7 +195,6 @@ class _EntryViewerStackState extends State with SingleTickerPr ViewerVerticalPageView( collection: collection, entryNotifier: _entryNotifier, - videoControllers: _videoControllers, multiPageControllers: _multiPageControllers, verticalPager: _verticalPager, horizontalPager: _horizontalPager, @@ -266,7 +262,7 @@ class _EntryViewerStackState extends State with SingleTickerPr Widget extraBottomOverlay; if (entry.isVideo) { - final videoController = _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; + final videoController = context.read().getController(entry); if (videoController != null) { extraBottomOverlay = VideoControlOverlay( entry: entry, @@ -506,14 +502,9 @@ class _EntryViewerStackState extends State with SingleTickerPr (_) => _.dispose(), ); if (entry.isVideo) { - _initViewSpecificController( - uri, - _videoControllers, - () => IjkPlayerAvesVideoController(entry), - (_) => _.dispose(), - ); + final controller = context.read().getOrCreateController(entry); if (settings.enableVideoAutoPlay) { - _playVideo(); + _playVideo(controller); } } if (entry.isMultipage) { @@ -528,19 +519,19 @@ class _EntryViewerStackState extends State with SingleTickerPr setState(() {}); } - Future _playVideo() async { - await Future.delayed(Duration(milliseconds: 300)); + Future _playVideo(AvesVideoController videoController) async { + // video decoding may fail or have initial artifacts when the player initializes + // during this widget initialization (because of the page transition and hero animation?) + // so we play after a delay for increased stability + await Future.delayed(Duration(milliseconds: 300) * timeDilation); - final entry = _entryNotifier.value; - if (entry == null) return; + await videoController.play(); - final videoController = _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; - if (videoController != null) { - if (videoController.isPlayable) { - await videoController.play(); - } else { - await videoController.setDataSource(entry.uri); - } + // playing controllers are paused when the entry changes, + // but the controller may still be preparing (not yet playing) when this happens + // so we make sure the current entry is still the same to keep playing + if (videoController.entry != _entryNotifier.value) { + await videoController.pause(); } } @@ -557,5 +548,5 @@ class _EntryViewerStackState extends State with SingleTickerPr } } - void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause()); + void _pauseVideoControllers() => context.read().pauseAll(); } diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index e39fbecd2..e44d316d6 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -34,7 +34,6 @@ class _VideoControlOverlayState extends State with SingleTi bool _playingOnDragStart = false; AnimationController _playPauseAnimation; final List _subscriptions = []; - double _seekTargetPercent; AvesEntry get entry => widget.entry; @@ -42,8 +41,6 @@ class _VideoControlOverlayState extends State with SingleTi AvesVideoController get controller => widget.controller; - bool get isPlayable => controller.isPlayable; - bool get isPlaying => controller.isPlaying; @override @@ -193,33 +190,6 @@ class _VideoControlOverlayState extends State with SingleTi } void _onStatusChange(VideoStatus status) { - if (status == VideoStatus.playing && _seekTargetPercent != null) { - _seekFromTarget(); - } - _updatePlayPauseIcon(); - } - - Future _togglePlayPause() async { - if (isPlaying) { - await controller.pause(); - } else { - await _play(); - } - } - - Future _play() async { - if (isPlayable) { - await controller.play(); - } else { - await controller.setDataSource(entry.uri); - } - - // hide overlay - await Future.delayed(Durations.iconAnimation); - ToggleOverlayNotification().dispatch(context); - } - - void _updatePlayPauseIcon() { final status = _playPauseAnimation.status; if (isPlaying && status != AnimationStatus.forward && status != AnimationStatus.completed) { _playPauseAnimation.forward(); @@ -228,28 +198,21 @@ class _VideoControlOverlayState extends State with SingleTi } } + Future _togglePlayPause() async { + if (isPlaying) { + await controller.pause(); + } else { + await controller.play(); + // hide overlay + await Future.delayed(Durations.iconAnimation); + ToggleOverlayNotification().dispatch(context); + } + } + void _seekFromTap(Offset globalPosition) async { final keyContext = _progressBarKey.currentContext; final RenderBox box = keyContext.findRenderObject(); final localPosition = box.globalToLocal(globalPosition); - _seekTargetPercent = (localPosition.dx / box.size.width); - - if (isPlayable) { - await _seekFromTarget(); - } else { - // 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.seekToProgress(_seekTargetPercent); - _seekTargetPercent = null; - } + await controller.seekToProgress(localPosition.dx / box.size.width); } } diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 78ad051eb..d16438158 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -15,6 +15,7 @@ import 'package:aves/widgets/common/magnifier/magnifier.dart'; import 'package:aves/widgets/common/magnifier/scale/scale_boundaries.dart'; import 'package:aves/widgets/common/magnifier/scale/scale_level.dart'; import 'package:aves/widgets/common/magnifier/scale/state.dart'; +import 'package:aves/widgets/common/video/conductor.dart'; import 'package:aves/widgets/common/video/controller.dart'; import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/overlay/notifications.dart'; @@ -27,14 +28,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; -import 'package:tuple/tuple.dart'; class EntryPageView extends StatefulWidget { final AvesEntry mainEntry; final AvesEntry entry; final SinglePageInfo page; final Size viewportSize; - final List> videoControllers; final VoidCallback onDisposed; static const decorationCheckSize = 20.0; @@ -44,7 +43,6 @@ class EntryPageView extends StatefulWidget { this.mainEntry, this.page, this.viewportSize, - @required this.videoControllers, this.onDisposed, }) : entry = mainEntry.getPageEntry(page) ?? mainEntry, super(key: key); @@ -195,7 +193,7 @@ class _EntryPageViewState extends State { } Widget _buildVideoView() { - final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2; + final videoController = context.read().getController(entry); if (videoController == null) return SizedBox(); return Stack( fit: StackFit.expand, @@ -210,11 +208,11 @@ class _EntryPageViewState extends State { StreamBuilder( stream: videoController.statusStream, builder: (context, snapshot) { - final showCover = videoController.isPlayable; + final showCover = !videoController.isReady; return IgnorePointer( - ignoring: showCover, + ignoring: !showCover, child: AnimatedOpacity( - opacity: showCover ? 0 : 1, + opacity: showCover ? 1 : 0, curve: Curves.easeInCirc, duration: Durations.viewerVideoPlayerTransition, child: GestureDetector( diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index 779015295..121c44b08 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -54,7 +54,7 @@ class _VideoViewState extends State { return StreamBuilder( stream: controller.statusStream, builder: (context, snapshot) { - return controller.isPlayable ? controller.buildPlayerWidget(context, entry) : SizedBox(); + return controller.isReady ? controller.buildPlayerWidget(context) : SizedBox(); }); }