From 63de9674684a3561999a7567c9cd33743bdeedd4 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Thu, 1 Apr 2021 09:21:51 +0900 Subject: [PATCH] video: controller switch prep --- lib/widgets/common/video/fijkplayer.dart | 119 +++++++++++++++ .../common/video/flutter_ijkplayer.dart | 144 ++++++++++++++++++ lib/widgets/common/video/video.dart | 73 +++++++++ .../viewer/entry_horizontal_pager.dart | 10 +- lib/widgets/viewer/entry_vertical_pager.dart | 6 +- lib/widgets/viewer/entry_viewer_stack.dart | 8 +- lib/widgets/viewer/overlay/video.dart | 69 ++++----- .../viewer/visual/entry_page_view.dart | 8 +- lib/widgets/viewer/visual/video.dart | 55 ++----- pubspec.yaml | 5 +- 10 files changed, 396 insertions(+), 101 deletions(-) create mode 100644 lib/widgets/common/video/fijkplayer.dart create mode 100644 lib/widgets/common/video/flutter_ijkplayer.dart create mode 100644 lib/widgets/common/video/video.dart diff --git a/lib/widgets/common/video/fijkplayer.dart b/lib/widgets/common/video/fijkplayer.dart new file mode 100644 index 000000000..f095c3029 --- /dev/null +++ b/lib/widgets/common/video/fijkplayer.dart @@ -0,0 +1,119 @@ +// import 'dart:async'; +// +// import 'package:aves/model/entry.dart'; +// import 'package:aves/utils/change_notifier.dart'; +// import 'package:aves/widgets/common/video/video.dart'; +// import 'package:fijkplayer/fijkplayer.dart'; +// import 'package:flutter/material.dart'; +// +// class FijkPlayerAvesVideoController extends AvesVideoController { +// FijkPlayer _instance; +// final List _subscriptions = []; +// final StreamController _valueStreamController = StreamController.broadcast(); +// final AChangeNotifier _playFinishNotifier = AChangeNotifier(); +// +// Stream get _valueStream => _valueStreamController.stream; +// +// FijkPlayerAvesVideoController() { +// _instance = FijkPlayer(); +// _instance.addListener(_onValueChanged); +// _subscriptions.add(_valueStream.where((value) => value.completed).listen((_) => _playFinishNotifier.notifyListeners())); +// } +// +// @override +// void dispose() { +// _instance.removeListener(_onValueChanged); +// _valueStreamController.close(); +// _subscriptions +// ..forEach((sub) => sub.cancel()) +// ..clear(); +// _instance.release(); +// } +// +// void _onValueChanged() => _valueStreamController.add(_instance.value); +// +// // 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; +// +// @override +// Future play() => _instance.start(); +// +// @override +// Future pause() => _instance.pause(); +// +// @override +// Future seekTo(int targetMillis) => _instance.seekTo(targetMillis); +// +// @override +// Future seekToProgress(double progress) => _instance.seekTo((duration * progress).toInt()); +// +// @override +// Listenable get playCompletedListenable => _playFinishNotifier; +// +// @override +// VideoStatus get status => _instance.state.toAves; +// +// @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); +// +// // 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 => _instance.isPlayable(); +// +// @override +// int get duration => _instance.value.duration.inMilliseconds; +// +// @override +// int get currentPosition => _instance.currentPos.inMilliseconds; +// +// @override +// Stream get positionStream => _instance.onCurrentPosUpdate.map((pos) => pos.inMilliseconds); +// +// @override +// Widget buildPlayerWidget(AvesEntry entry) => FijkView( +// player: _instance, +// panelBuilder: (player, data, context, viewSize, texturePos) => SizedBox(), +// color: Colors.transparent, +// ); +// } +// +// extension ExtraIjkStatus on FijkState { +// VideoStatus get toAves { +// switch (this) { +// case FijkState.idle: +// return VideoStatus.idle; +// case FijkState.initialized: +// return VideoStatus.initialized; +// case FijkState.asyncPreparing: +// return VideoStatus.preparing; +// case FijkState.prepared: +// return VideoStatus.prepared; +// case FijkState.started: +// return VideoStatus.playing; +// case FijkState.paused: +// return VideoStatus.paused; +// case FijkState.completed: +// return VideoStatus.completed; +// case FijkState.stopped: +// return VideoStatus.stopped; +// case FijkState.end: +// return VideoStatus.disposed; +// case FijkState.error: +// return VideoStatus.error; +// } +// return VideoStatus.idle; +// } +// } diff --git a/lib/widgets/common/video/flutter_ijkplayer.dart b/lib/widgets/common/video/flutter_ijkplayer.dart new file mode 100644 index 000000000..4d5f259f6 --- /dev/null +++ b/lib/widgets/common/video/flutter_ijkplayer.dart @@ -0,0 +1,144 @@ +import 'dart:async'; + +import 'package:aves/model/entry.dart'; +import 'package:aves/utils/change_notifier.dart'; +import 'package:aves/widgets/common/video/video.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; + +class FlutterIjkPlayerAvesVideoController extends AvesVideoController { + IjkMediaController _instance; + final List _subscriptions = []; + final AChangeNotifier _playFinishNotifier = AChangeNotifier(); + + FlutterIjkPlayerAvesVideoController() { + _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; + + @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(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/video.dart b/lib/widgets/common/video/video.dart new file mode 100644 index 000000000..08d498c37 --- /dev/null +++ b/lib/widgets/common/video/video.dart @@ -0,0 +1,73 @@ +import 'package:aves/model/entry.dart'; +// import 'package:aves/widgets/common/video/fijkplayer.dart'; +import 'package:aves/widgets/common/video/flutter_ijkplayer.dart'; +import 'package:flutter/material.dart'; + +abstract class AvesVideoController { + AvesVideoController(); + + factory AvesVideoController.flutterIjkPlayer() => FlutterIjkPlayerAvesVideoController(); + + // factory AvesVideoController.fijkPlayer() => FijkPlayerAvesVideoController(); + + void dispose(); + + Future setDataSource(String uri); + + Future refreshVideoInfo(); + + Future play(); + + Future pause(); + + Future seekTo(int targetMillis); + + Future seekToProgress(double progress); + + Listenable get playCompletedListenable; + + VideoStatus get status; + + Stream get statusStream; + + bool get isPlayable; + + 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); + + Stream get positionStream; + + Widget buildPlayerWidget(AvesEntry entry); +} + +class AvesVideoInfo { + // in millis + int duration, currentPosition; + + AvesVideoInfo({ + this.duration, + this.currentPosition, + }); +} + +enum VideoStatus { + idle, + initialized, + preparing, + prepared, + playing, + paused, + completed, + stopped, + disposed, + error, +} diff --git a/lib/widgets/viewer/entry_horizontal_pager.dart b/lib/widgets/viewer/entry_horizontal_pager.dart index d5766ffeb..7760eab22 100644 --- a/lib/widgets/viewer/entry_horizontal_pager.dart +++ b/lib/widgets/viewer/entry_horizontal_pager.dart @@ -3,10 +3,10 @@ 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/video.dart'; import 'package:aves/widgets/viewer/multipage.dart'; import 'package:aves/widgets/viewer/visual/entry_page_view.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -15,7 +15,7 @@ class MultiEntryScroller extends StatefulWidget { final PageController pageController; final ValueChanged onPageChanged; final VoidCallback onTap; - final List> videoControllers; + final List> videoControllers; final List> multiPageControllers; final void Function(String uri) onViewDisposed; @@ -89,7 +89,7 @@ class _MultiEntryScrollerState extends State with AutomaticK mainEntry: entry, page: page, viewportSize: mqSize, - onTap: (_) => widget.onTap?.call(), + onTap: widget.onTap == null ? null : (_) => widget.onTap(), videoControllers: widget.videoControllers, onDisposed: () => widget.onViewDisposed?.call(entry.uri), ); @@ -108,7 +108,7 @@ class _MultiEntryScrollerState extends State with AutomaticK class SingleEntryScroller extends StatefulWidget { final AvesEntry entry; final VoidCallback onTap; - final List> videoControllers; + final List> videoControllers; final List> multiPageControllers; const SingleEntryScroller({ @@ -163,7 +163,7 @@ class _SingleEntryScrollerState extends State with Automati mainEntry: entry, page: page, viewportSize: mqSize, - onTap: (_) => widget.onTap?.call(), + onTap: widget.onTap == null ? null : (_) => widget.onTap(), videoControllers: widget.videoControllers, ); }, diff --git a/lib/widgets/viewer/entry_vertical_pager.dart b/lib/widgets/viewer/entry_vertical_pager.dart index d8441475e..db60c230a 100644 --- a/lib/widgets/viewer/entry_vertical_pager.dart +++ b/lib/widgets/viewer/entry_vertical_pager.dart @@ -3,6 +3,7 @@ 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/video.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'; @@ -10,13 +11,12 @@ import 'package:aves/widgets/viewer/multipage.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:tuple/tuple.dart'; class ViewerVerticalPageView extends StatefulWidget { final CollectionLens collection; final ValueNotifier entryNotifier; - final List> videoControllers; + final List> videoControllers; final List> multiPageControllers; final PageController horizontalPager, verticalPager; final void Function(int page) onVerticalPageChanged, onHorizontalPageChanged; @@ -32,7 +32,7 @@ class ViewerVerticalPageView extends StatefulWidget { @required this.horizontalPager, @required this.onVerticalPageChanged, @required this.onHorizontalPageChanged, - @required this.onImageTap, + this.onImageTap, @required this.onImagePageRequested, @required this.onViewDisposed, }); diff --git a/lib/widgets/viewer/entry_viewer_stack.dart b/lib/widgets/viewer/entry_viewer_stack.dart index f4bfe6799..876df08e2 100644 --- a/lib/widgets/viewer/entry_viewer_stack.dart +++ b/lib/widgets/viewer/entry_viewer_stack.dart @@ -11,6 +11,7 @@ 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/video.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'; @@ -25,7 +26,6 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -55,7 +55,7 @@ class _EntryViewerStackState extends State with SingleTickerPr Animation _bottomOverlayOffset; EdgeInsets _frozenViewInsets, _frozenViewPadding; EntryActionDelegate _actionDelegate; - final List> _videoControllers = []; + final List> _videoControllers = []; final List> _multiPageControllers = []; final List>> _viewStateNotifiers = []; final ValueNotifier _heroInfoNotifier = ValueNotifier(null); @@ -496,10 +496,10 @@ class _EntryViewerStackState extends State with SingleTickerPr (_) => _.dispose(), ); if (entry.isVideo) { - _initViewSpecificController( + _initViewSpecificController( uri, _videoControllers, - () => IjkMediaController(), + () => AvesVideoController.flutterIjkPlayer(), (_) => _.dispose(), ); } diff --git a/lib/widgets/viewer/overlay/video.dart b/lib/widgets/viewer/overlay/video.dart index 78bed2dfb..fbbd43fb1 100644 --- a/lib/widgets/viewer/overlay/video.dart +++ b/lib/widgets/viewer/overlay/video.dart @@ -8,13 +8,13 @@ import 'package:aves/utils/time_utils.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; +import 'package:aves/widgets/common/video/video.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; class VideoControlOverlay extends StatefulWidget { final AvesEntry entry; - final IjkMediaController controller; + final AvesVideoController controller; final Animation scale; const VideoControlOverlay({ @@ -42,18 +42,11 @@ class _VideoControlOverlayState extends State with SingleTi Animation get scale => widget.scale; - IjkMediaController get controller => widget.controller; + AvesVideoController get controller => widget.controller; - // `videoInfo` is never null (even if `toString` prints `null`) - // check presence with `hasData` instead - VideoInfo get videoInfo => controller.videoInfo; + bool get isPlayable => controller.isPlayable; - // 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) - bool get isInitialized => videoInfo.hasData; - - bool get isPlaying => controller.ijkStatus == IjkStatus.playing; + bool get isPlaying => controller.isPlaying; @override void initState() { @@ -80,10 +73,10 @@ class _VideoControlOverlayState extends State with SingleTi } void _registerWidget(VideoControlOverlay widget) { - _subscriptions.add(widget.controller.ijkStatusStream.listen(_onStatusChange)); - _subscriptions.add(widget.controller.textureIdStream.listen(_onTextureIdChange)); - _onStatusChange(widget.controller.ijkStatus); - _onTextureIdChange(widget.controller.textureId); + _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) { @@ -95,18 +88,18 @@ class _VideoControlOverlayState extends State with SingleTi @override Widget build(BuildContext context) { - return StreamBuilder( - stream: controller.ijkStatusStream, + return StreamBuilder( + stream: controller.statusStream, builder: (context, snapshot) { // do not use stream snapshot because it is obsolete when switching between videos - final status = controller.ijkStatus; + final status = controller.status; return TooltipTheme( data: TooltipTheme.of(context).copyWith( preferBelow: false, ), child: Row( mainAxisAlignment: MainAxisAlignment.end, - children: status == IjkStatus.error + children: status == VideoStatus.error ? [ OverlayButton( scale: scale, @@ -171,22 +164,22 @@ class _VideoControlOverlayState extends State with SingleTi children: [ Row( children: [ - StreamBuilder( - stream: controller.videoInfoStream, + StreamBuilder( + stream: controller.positionStream, builder: (context, snapshot) { // do not use stream snapshot because it is obsolete when switching between videos - final position = videoInfo.currentPosition?.floor() ?? 0; - return Text(formatDuration(Duration(seconds: position))); + final position = controller.currentPosition?.floor() ?? 0; + return Text(formatDuration(Duration(milliseconds: position))); }), Spacer(), Text(entry.durationText), ], ), - StreamBuilder( - stream: controller.videoInfoStream, + StreamBuilder( + stream: controller.positionStream, builder: (context, snapshot) { // do not use stream snapshot because it is obsolete when switching between videos - var progress = videoInfo.progress; + var progress = controller.progress; if (!progress.isFinite) progress = 0.0; return LinearProgressIndicator(value: progress); }), @@ -199,7 +192,7 @@ class _VideoControlOverlayState extends State with SingleTi } void _startTimer() { - if (controller.textureId == null) return; + if (!controller.isVideoReady) return; _progressTimer?.cancel(); _progressTimer = Timer.periodic(Durations.videoProgressTimerInterval, (_) => controller.refreshVideoInfo()); } @@ -208,16 +201,16 @@ class _VideoControlOverlayState extends State with SingleTi _progressTimer?.cancel(); } - void _onTextureIdChange(int textureId) { - if (textureId != null) { + void _onVideoReadinessChanged(bool isVideoReady) { + if (isVideoReady) { _startTimer(); } else { _stopTimer(); } } - void _onStatusChange(IjkStatus status) { - if (status == IjkStatus.playing && _seekTargetPercent != null) { + void _onStatusChange(VideoStatus status) { + if (status == VideoStatus.playing && _seekTargetPercent != null) { _seekFromTarget(); } _updatePlayPauseIcon(); @@ -226,10 +219,10 @@ class _VideoControlOverlayState extends State with SingleTi Future _playPause() async { if (isPlaying) { await controller.pause(); - } else if (isInitialized) { + } else if (isPlayable) { await controller.play(); } else { - await controller.setDataSource(DataSource.photoManagerUrl(entry.uri), autoPlay: true); + await controller.setDataSource(entry.uri); } } @@ -248,19 +241,17 @@ class _VideoControlOverlayState extends State with SingleTi final localPosition = box.globalToLocal(globalPosition); _seekTargetPercent = (localPosition.dx / box.size.width); - if (isInitialized) { + if (isPlayable) { await _seekFromTarget(); } else { - // autoplay when seeking on uninitialized player, otherwise the texture is not updated - // as a workaround, pausing after a brief duration is possible, but fiddly - await controller.setDataSource(DataSource.photoManagerUrl(entry.uri), autoPlay: true); + await controller.setDataSource(entry.uri); } } 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 (videoInfo.duration == null) { + if (controller.duration == null) { await controller.refreshVideoInfo(); } else { await controller.seekToProgress(_seekTargetPercent); diff --git a/lib/widgets/viewer/visual/entry_page_view.dart b/lib/widgets/viewer/visual/entry_page_view.dart index 784956cb2..0be786967 100644 --- a/lib/widgets/viewer/visual/entry_page_view.dart +++ b/lib/widgets/viewer/visual/entry_page_view.dart @@ -12,6 +12,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/video.dart'; import 'package:aves/widgets/viewer/hero.dart'; import 'package:aves/widgets/viewer/visual/error.dart'; import 'package:aves/widgets/viewer/visual/raster.dart'; @@ -20,7 +21,6 @@ import 'package:aves/widgets/viewer/visual/vector.dart'; import 'package:aves/widgets/viewer/visual/video.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -31,7 +31,7 @@ class EntryPageView extends StatefulWidget { final SinglePageInfo page; final Size viewportSize; final MagnifierTapCallback onTap; - final List> videoControllers; + final List> videoControllers; final VoidCallback onDisposed; static const decorationCheckSize = 20.0; @@ -138,7 +138,7 @@ class _EntryPageViewState extends State { } child ??= ErrorView( entry: entry, - onTap: () => onTap?.call(null), + onTap: onTap == null ? null : () => onTap(null), ); return child; }, @@ -221,7 +221,7 @@ class _EntryPageViewState extends State { initialScale: initialScale, scaleStateCycle: scaleStateCycle, applyScale: applyScale, - onTap: (c, d, s, childPosition) => onTap?.call(childPosition), + onTap: onTap == null ? null : (c, d, s, childPosition) => onTap(childPosition), child: child, ); } diff --git a/lib/widgets/viewer/visual/video.dart b/lib/widgets/viewer/visual/video.dart index aac61c5e4..fb8dec25c 100644 --- a/lib/widgets/viewer/visual/video.dart +++ b/lib/widgets/viewer/visual/video.dart @@ -1,16 +1,15 @@ -import 'dart:async'; import 'dart:ui'; import 'package:aves/model/entry.dart'; import 'package:aves/model/entry_images.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/widgets/collection/collection_page.dart'; +import 'package:aves/widgets/common/video/video.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_ijkplayer/flutter_ijkplayer.dart'; class VideoView extends StatefulWidget { final AvesEntry entry; - final IjkMediaController controller; + final AvesVideoController controller; const VideoView({ Key key, @@ -23,11 +22,9 @@ class VideoView extends StatefulWidget { } class _VideoViewState extends State { - final List _subscriptions = []; - AvesEntry get entry => widget.entry; - IjkMediaController get controller => widget.controller; + AvesVideoController get controller => widget.controller; @override void initState() { @@ -49,56 +46,24 @@ class _VideoViewState extends State { } void _registerWidget(VideoView widget) { - _subscriptions.add(widget.controller.playFinishStream.listen(_onPlayFinish)); + widget.controller.playCompletedListenable.addListener(_onPlayCompleted); } void _unregisterWidget(VideoView widget) { - _subscriptions - ..forEach((sub) => sub.cancel()) - ..clear(); + widget.controller.playCompletedListenable.removeListener(_onPlayCompleted); } - bool isPlayable(IjkStatus status) => controller != null && [IjkStatus.prepared, IjkStatus.playing, IjkStatus.pause, IjkStatus.complete].contains(status); + bool isPlayable(VideoStatus status) => controller != null && [VideoStatus.prepared, VideoStatus.playing, VideoStatus.paused, VideoStatus.completed].contains(status); @override Widget build(BuildContext context) { if (controller == null) return SizedBox(); - return StreamBuilder( - stream: widget.controller.ijkStatusStream, + return StreamBuilder( + stream: widget.controller.statusStream, builder: (context, snapshot) { final status = snapshot.data; return isPlayable(status) - ? IjkPlayer( - mediaController: controller, - 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, - ) + ? controller.buildPlayerWidget(entry) : Image( image: entry.getBestThumbnail(settings.getTileExtent(CollectionPage.routeName)), fit: BoxFit.contain, @@ -106,5 +71,5 @@ class _VideoViewState extends State { }); } - void _onPlayFinish(IjkMediaController controller) => controller.seekTo(0); + void _onPlayCompleted() => controller.seekTo(0); } diff --git a/pubspec.yaml b/pubspec.yaml index 03095a28a..7896035e6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,6 +40,7 @@ dependencies: firebase_analytics: firebase_crashlytics: flutter_highlight: +# fijkplayer: flutter_ijkplayer: # path: ../flutter_ijkplayer git: @@ -120,13 +121,15 @@ flutter: # - does not support AVI/XVID, AC3 # - cannot play if only the video or audio stream is supported -# fijkplayer (as of v0.7.1, backed by IJKPlayer & ffmpeg): +# fijkplayer (as of v0.8.7, backed by IJKPlayer & ffmpeg): # - support content URIs # - does not support XVID, AC3 (by default, but possible by custom build) # - can play if only the video or audio stream is supported # - crash when calling `seekTo` for some files (e.g. TED talk videos) +# - no edge smear (with default build) # flutter_ijkplayer (as of v0.3.5+1, backed by IJKPlayer & ffmpeg): # - support content URIs (`DataSource.photoManagerUrl` from v0.3.6, but need fork to support content URIs on Android