video controller review
This commit is contained in:
parent
8d1ab77aa9
commit
ea51bece7e
10 changed files with 166 additions and 160 deletions
37
lib/widgets/common/video/conductor.dart
Normal file
37
lib/widgets/common/video/conductor.dart
Normal file
|
@ -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<AvesVideoController> _controllers = [];
|
||||
|
||||
static const maxControllerCount = 3;
|
||||
|
||||
VideoConductor() {
|
||||
FijkLog.setLevel(FijkLogLevel.Warn);
|
||||
}
|
||||
|
||||
Future<void> 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());
|
||||
}
|
|
@ -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<void> setDataSource(String uri, {int startMillis = 0});
|
||||
AvesVideoController(AvesEntry entry) {
|
||||
_entry = entry;
|
||||
}
|
||||
|
||||
Future<void> dispose();
|
||||
|
||||
Future<void> play();
|
||||
|
||||
|
@ -14,11 +19,7 @@ abstract class AvesVideoController {
|
|||
|
||||
Future<void> seekTo(int targetMillis);
|
||||
|
||||
Future<void> seekToProgress(double progress) async {
|
||||
if (duration != null) {
|
||||
await seekTo((duration * progress).toInt());
|
||||
}
|
||||
}
|
||||
Future<void> seekToProgress(double progress) => seekTo((duration * progress).toInt());
|
||||
|
||||
Listenable get playCompletedListenable;
|
||||
|
||||
|
@ -26,7 +27,7 @@ abstract class AvesVideoController {
|
|||
|
||||
Stream<VideoStatus> 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<int> get positionStream;
|
||||
|
||||
Widget buildPlayerWidget(BuildContext context, AvesEntry entry);
|
||||
Widget buildPlayerWidget(BuildContext context);
|
||||
}
|
||||
|
||||
enum VideoStatus {
|
||||
|
|
|
@ -23,7 +23,7 @@ class IjkPlayerAvesVideoController extends AvesVideoController {
|
|||
final ValueNotifier<StreamSummary> _selectedVideoStream = ValueNotifier(null);
|
||||
final ValueNotifier<StreamSummary> _selectedAudioStream = ValueNotifier(null);
|
||||
final ValueNotifier<StreamSummary> _selectedTextStream = ValueNotifier(null);
|
||||
final ValueNotifier<Tuple2<int, int>> _sar = ValueNotifier(Tuple2(1, 1));
|
||||
final ValueNotifier<Tuple2<int, int>> _sar = ValueNotifier(null);
|
||||
Timer _initialPlayTimer;
|
||||
|
||||
Stream<FijkValue> 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<void> 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<void> _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<void> 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<void> 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<void> play() {
|
||||
if (_instance.isPlayable()) {
|
||||
_instance.start();
|
||||
}
|
||||
return SynchronousFuture(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() {
|
||||
if (_instance.isPlayable()) {
|
||||
Future<void> pause() async {
|
||||
if (isReady) {
|
||||
_initialPlayTimer?.cancel();
|
||||
_instance.pause();
|
||||
await _instance.pause();
|
||||
}
|
||||
return SynchronousFuture(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> seekTo(int targetMillis) => _instance.seekTo(targetMillis);
|
||||
Future<void> 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<VideoStatus> 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<int> get positionStream => _instance.onCurrentPosUpdate.map((pos) => pos.inMilliseconds);
|
||||
|
||||
@override
|
||||
Widget buildPlayerWidget(BuildContext context, AvesEntry entry) {
|
||||
Widget buildPlayerWidget(BuildContext context) {
|
||||
return ValueListenableBuilder<Tuple2<int, int>>(
|
||||
valueListenable: _sar,
|
||||
builder: (context, sar, child) {
|
||||
|
@ -272,7 +294,7 @@ extension ExtraIjkStatus on FijkState {
|
|||
|
||||
extension ExtraFijkPlayer on FijkPlayer {
|
||||
Future<void> setDataSourceUntilPrepared(String uri) async {
|
||||
await setDataSource(uri, autoPlay: false);
|
||||
await setDataSource(uri, autoPlay: false, showCover: false);
|
||||
|
||||
final completer = Completer();
|
||||
void onChange() {
|
||||
|
|
|
@ -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<int> onPageChanged;
|
||||
final List<Tuple2<String, AvesVideoController>> videoControllers;
|
||||
final List<Tuple2<String, MultiPageController>> 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<MultiEntryScroller> 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<MultiEntryScroller> with AutomaticK
|
|||
|
||||
class SingleEntryScroller extends StatefulWidget {
|
||||
final AvesEntry entry;
|
||||
final List<Tuple2<String, AvesVideoController>> videoControllers;
|
||||
final List<Tuple2<String, MultiPageController>> multiPageControllers;
|
||||
|
||||
const SingleEntryScroller({
|
||||
this.entry,
|
||||
this.videoControllers,
|
||||
this.multiPageControllers,
|
||||
});
|
||||
|
||||
|
@ -158,7 +152,6 @@ class _SingleEntryScrollerState extends State<SingleEntryScroller> with Automati
|
|||
mainEntry: entry,
|
||||
page: page,
|
||||
viewportSize: mqSize,
|
||||
videoControllers: widget.videoControllers,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
|
|
@ -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<AvesEntry> entryNotifier;
|
||||
final List<Tuple2<String, AvesVideoController>> videoControllers;
|
||||
final List<Tuple2<String, MultiPageController>> 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<ViewerVerticalPageView> {
|
|||
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(
|
||||
|
|
|
@ -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<VideoConductor>(
|
||||
create: (context) => VideoConductor(),
|
||||
dispose: (context, value) => value.dispose(),
|
||||
child: EntryViewerStack(
|
||||
collection: collection,
|
||||
initialEntry: initialEntry,
|
||||
),
|
||||
),
|
||||
backgroundColor: Navigator.canPop(context) ? Colors.transparent : Colors.black,
|
||||
resizeToAvoidBottomInset: false,
|
||||
|
|
|
@ -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<EntryViewerStack> with SingleTickerPr
|
|||
Animation<Offset> _bottomOverlayOffset;
|
||||
EdgeInsets _frozenViewInsets, _frozenViewPadding;
|
||||
EntryActionDelegate _actionDelegate;
|
||||
final List<Tuple2<String, AvesVideoController>> _videoControllers = [];
|
||||
final List<Tuple2<String, MultiPageController>> _multiPageControllers = [];
|
||||
final List<Tuple2<String, ValueNotifier<ViewState>>> _viewStateNotifiers = [];
|
||||
final ValueNotifier<HeroInfo> _heroInfoNotifier = ValueNotifier(null);
|
||||
|
@ -128,8 +127,6 @@ class _EntryViewerStackState extends State<EntryViewerStack> 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<EntryViewerStack> with SingleTickerPr
|
|||
ViewerVerticalPageView(
|
||||
collection: collection,
|
||||
entryNotifier: _entryNotifier,
|
||||
videoControllers: _videoControllers,
|
||||
multiPageControllers: _multiPageControllers,
|
||||
verticalPager: _verticalPager,
|
||||
horizontalPager: _horizontalPager,
|
||||
|
@ -266,7 +262,7 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
|
|||
|
||||
Widget extraBottomOverlay;
|
||||
if (entry.isVideo) {
|
||||
final videoController = _videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
||||
final videoController = context.read<VideoConductor>().getController(entry);
|
||||
if (videoController != null) {
|
||||
extraBottomOverlay = VideoControlOverlay(
|
||||
entry: entry,
|
||||
|
@ -506,14 +502,9 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
|
|||
(_) => _.dispose(),
|
||||
);
|
||||
if (entry.isVideo) {
|
||||
_initViewSpecificController<AvesVideoController>(
|
||||
uri,
|
||||
_videoControllers,
|
||||
() => IjkPlayerAvesVideoController(entry),
|
||||
(_) => _.dispose(),
|
||||
);
|
||||
final controller = context.read<VideoConductor>().getOrCreateController(entry);
|
||||
if (settings.enableVideoAutoPlay) {
|
||||
_playVideo();
|
||||
_playVideo(controller);
|
||||
}
|
||||
}
|
||||
if (entry.isMultipage) {
|
||||
|
@ -528,19 +519,19 @@ class _EntryViewerStackState extends State<EntryViewerStack> with SingleTickerPr
|
|||
setState(() {});
|
||||
}
|
||||
|
||||
Future<void> _playVideo() async {
|
||||
await Future.delayed(Duration(milliseconds: 300));
|
||||
Future<void> _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<EntryViewerStack> with SingleTickerPr
|
|||
}
|
||||
}
|
||||
|
||||
void _pauseVideoControllers() => _videoControllers.forEach((e) => e.item2.pause());
|
||||
void _pauseVideoControllers() => context.read<VideoConductor>().pauseAll();
|
||||
}
|
||||
|
|
|
@ -34,7 +34,6 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> with SingleTi
|
|||
bool _playingOnDragStart = false;
|
||||
AnimationController _playPauseAnimation;
|
||||
final List<StreamSubscription> _subscriptions = [];
|
||||
double _seekTargetPercent;
|
||||
|
||||
AvesEntry get entry => widget.entry;
|
||||
|
||||
|
@ -42,8 +41,6 @@ class _VideoControlOverlayState extends State<VideoControlOverlay> 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<VideoControlOverlay> with SingleTi
|
|||
}
|
||||
|
||||
void _onStatusChange(VideoStatus status) {
|
||||
if (status == VideoStatus.playing && _seekTargetPercent != null) {
|
||||
_seekFromTarget();
|
||||
}
|
||||
_updatePlayPauseIcon();
|
||||
}
|
||||
|
||||
Future<void> _togglePlayPause() async {
|
||||
if (isPlaying) {
|
||||
await controller.pause();
|
||||
} else {
|
||||
await _play();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _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<VideoControlOverlay> with SingleTi
|
|||
}
|
||||
}
|
||||
|
||||
Future<void> _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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Tuple2<String, AvesVideoController>> 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<EntryPageView> {
|
|||
}
|
||||
|
||||
Widget _buildVideoView() {
|
||||
final videoController = widget.videoControllers.firstWhere((kv) => kv.item1 == entry.uri, orElse: () => null)?.item2;
|
||||
final videoController = context.read<VideoConductor>().getController(entry);
|
||||
if (videoController == null) return SizedBox();
|
||||
return Stack(
|
||||
fit: StackFit.expand,
|
||||
|
@ -210,11 +208,11 @@ class _EntryPageViewState extends State<EntryPageView> {
|
|||
StreamBuilder<VideoStatus>(
|
||||
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(
|
||||
|
|
|
@ -54,7 +54,7 @@ class _VideoViewState extends State<VideoView> {
|
|||
return StreamBuilder<VideoStatus>(
|
||||
stream: controller.statusStream,
|
||||
builder: (context, snapshot) {
|
||||
return controller.isPlayable ? controller.buildPlayerWidget(context, entry) : SizedBox();
|
||||
return controller.isReady ? controller.buildPlayerWidget(context) : SizedBox();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue