import 'dart:async'; import 'package:aves/model/entry.dart'; import 'package:aves/model/video_playback.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/format.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; abstract class AvesVideoController { final List _subscriptions = []; final AvesEntry _entry; final bool persistPlayback; AvesEntry get entry => _entry; static const resumeTimeSaveMinProgress = .05; static const resumeTimeSaveMaxProgress = .95; static const resumeTimeSaveMinDuration = Duration(minutes: 2); AvesVideoController(AvesEntry entry, {required this.persistPlayback}) : _entry = entry { entry.visualChangeNotifier.addListener(onVisualChanged); _subscriptions.add(statusStream.listen((event) => mediaSessionService.update(this))); } @mustCallSuper Future dispose() async { _subscriptions ..forEach((sub) => sub.cancel()) ..clear(); entry.visualChangeNotifier.removeListener(onVisualChanged); await _savePlaybackState(); } Future _savePlaybackState() async { final id = entry.id; if (!isReady || duration < resumeTimeSaveMinDuration.inMilliseconds) return; if (persistPlayback) { final _progress = progress; if (resumeTimeSaveMinProgress < _progress && _progress < resumeTimeSaveMaxProgress) { await metadataDb.addVideoPlayback({ VideoPlaybackRow( entryId: id, resumeTimeMillis: currentPosition, ) }); } else { await metadataDb.removeVideoPlayback({id}); } } } Future getResumeTime(BuildContext context) async { if (!persistPlayback) return null; final id = entry.id; final playback = await metadataDb.loadVideoPlayback(id); final resumeTime = playback?.resumeTimeMillis ?? 0; if (resumeTime == 0) return null; // clear on retrieval await metadataDb.removeVideoPlayback({id}); final resume = await showDialog( context: context, builder: (context) { return AvesDialog( content: Text(context.l10n.videoResumeDialogMessage(formatFriendlyDuration(Duration(milliseconds: resumeTime)))), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: Text(context.l10n.videoStartOverButtonLabel), ), TextButton( onPressed: () => Navigator.pop(context, true), child: Text(context.l10n.videoResumeButtonLabel), ), ], ); }, ); if (resume == null || !resume) return 0; return resumeTime; } void onVisualChanged(); Future play(); Future pause(); Future seekTo(int targetMillis); Future seekToProgress(double progress) => seekTo((duration * progress).toInt()); Listenable get playCompletedListenable; VideoStatus get status; Stream get statusStream; Stream get volumeStream; Stream get speedStream; bool get isReady; bool get isPlaying => status == VideoStatus.playing; int get duration; int get currentPosition; double get progress { final _duration = duration; return _duration != 0 ? currentPosition.toDouble() / _duration : 0; } Stream get positionStream; Stream get timedTextStream; ValueNotifier get canCaptureFrameNotifier; ValueNotifier get canMuteNotifier; ValueNotifier get canSetSpeedNotifier; ValueNotifier get canSelectStreamNotifier; ValueNotifier get sarNotifier; bool get isMuted; double get speed; double get minSpeed; double get maxSpeed; set speed(double speed); Future selectStream(StreamType type, StreamSummary? selected); Future getSelectedStream(StreamType type); List get streams; Future captureFrame(); Future mute(bool muted); Widget buildPlayerWidget(BuildContext context); } enum VideoStatus { idle, initialized, paused, playing, completed, error, } enum StreamType { video, audio, text } class StreamSummary { final StreamType type; final int? index, width, height; final String? codecName, language, title; const StreamSummary({ required this.type, required this.index, required this.codecName, required this.language, required this.title, required this.width, required this.height, }); @override String toString() => '$runtimeType#${shortHash(this)}{type: type, index: $index, codecName: $codecName, language: $language, title: $title, width: $width, height: $height}'; }