import 'dart:async'; import 'dart:math'; import 'package:aves/model/entry/entry.dart'; import 'package:aves/model/entry/extensions/props.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/widgets/viewer/video/db_playback_state_handler.dart'; import 'package:aves_model/aves_model.dart'; import 'package:aves_video/aves_video.dart'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:leak_tracker/leak_tracker.dart'; class VideoConductor { final CollectionLens? _collection; final List _controllers = []; final Map _subscriptions = {}; final PlaybackStateHandler _playbackStateHandler = DatabasePlaybackStateHandler(); final ValueNotifier playingVideoControllerNotifier = ValueNotifier(null); static const _defaultMaxControllerCount = 3; VideoConductor({CollectionLens? collection}) : _collection = collection { if (kFlutterMemoryAllocationsEnabled) { LeakTracking.dispatchObjectCreated( library: 'aves', className: '$VideoConductor', object: this, ); } } Future dispose() async { if (kFlutterMemoryAllocationsEnabled) { LeakTracking.dispatchObjectDisposed(object: this); } await _disposeAll(); playingVideoControllerNotifier.dispose(); _controllers.clear(); if (settings.keepScreenOn == KeepScreenOn.videoPlayback) { await windowService.keepScreenOn(false); } } Future getOrCreateController(AvesEntry entry, {int? maxControllerCount}) async { var controller = getController(entry); if (controller != null) { _controllers.remove(controller); } else { maxControllerCount = max(_defaultMaxControllerCount, maxControllerCount ?? 0); while (_controllers.length >= maxControllerCount) { await _disposeController(_controllers.removeLast()); } await deviceService.requestGarbageCollection(); controller = videoControllerFactory.buildController( entry, playbackStateHandler: _playbackStateHandler, settings: settings, ); _subscriptions[controller] = controller.statusStream.listen((event) => _onControllerStatusChanged(entry, controller!, event)); } _controllers.insert(0, controller); return controller; } AvesVideoController? getPlayingController() => _controllers.firstWhereOrNull((c) => c.isPlaying); AvesVideoController? getController(AvesEntry entry) { return _controllers.firstWhereOrNull((c) => c.entry.uri == entry.uri && c.entry.pageId == entry.pageId); } Future _onControllerStatusChanged(AvesEntry entry, AvesVideoController controller, VideoStatus status) async { bool canSkipToNext = false, canSkipToPrevious = false; final entries = _collection?.sortedEntries; if (entries != null) { final currentIndex = entries.indexOf(entry); if (currentIndex != -1) { bool isVideo(AvesEntry entry) => entry.isVideo; canSkipToPrevious = entries.take(currentIndex).lastWhereOrNull(isVideo) != null; canSkipToNext = entries.skip(currentIndex + 1).firstWhereOrNull(isVideo) != null; } } await mediaSessionService.update( entry: entry, controller: controller, canSkipToNext: canSkipToNext, canSkipToPrevious: canSkipToPrevious, ); if (settings.keepScreenOn == KeepScreenOn.videoPlayback) { await windowService.keepScreenOn(status == VideoStatus.playing); } playingVideoControllerNotifier.value = getPlayingController(); } Future _applyToAll(FutureOr Function(AvesVideoController controller) action) => Future.forEach(_controllers, action); Future _disposeAll() => _applyToAll(_disposeController); Future pauseAll() => _applyToAll((controller) => controller.pause()); Future muteAll(bool muted) => _applyToAll((controller) => controller.mute(muted)); Future _disposeController(AvesVideoController controller) async { await _subscriptions.remove(controller)?.cancel(); await controller.dispose(); } }