126 lines
3.8 KiB
Dart
126 lines
3.8 KiB
Dart
import 'dart:async';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:media_kit/media_kit.dart';
|
|
import 'package:media_kit_video/media_kit_video.dart';
|
|
import 'package:mirror_protocol/mirror_protocol.dart';
|
|
import 'display_state_provider.dart';
|
|
|
|
final playerProvider = Provider<PlayerService>((ref) {
|
|
final service = PlayerService(ref);
|
|
ref.onDispose(() => service.dispose());
|
|
return service;
|
|
});
|
|
|
|
final videoControllerProvider = Provider<VideoController>((ref) {
|
|
return ref.watch(playerProvider).videoController;
|
|
});
|
|
|
|
class PlayerService {
|
|
final Ref _ref;
|
|
late final Player _player;
|
|
late final VideoController _videoController;
|
|
StreamSubscription? _positionSub;
|
|
StreamSubscription? _playingSub;
|
|
StreamSubscription? _completedSub;
|
|
StreamSubscription? _durationSub;
|
|
Timer? _progressSaveTimer;
|
|
|
|
PlayerService(this._ref) {
|
|
_player = Player();
|
|
_videoController = VideoController(_player);
|
|
|
|
_positionSub = _player.stream.position.listen((position) {
|
|
_ref.read(displayStateProvider.notifier).updatePlaybackPosition(position);
|
|
});
|
|
|
|
_playingSub = _player.stream.playing.listen((playing) {
|
|
final state = _ref.read(displayStateProvider);
|
|
final current = state.playback;
|
|
if (current != null && current.isPlaying != playing) {
|
|
_ref.read(displayStateProvider.notifier).updatePlaybackPosition(
|
|
_player.state.position,
|
|
);
|
|
}
|
|
});
|
|
|
|
_durationSub = _player.stream.duration.listen((duration) {
|
|
final state = _ref.read(displayStateProvider);
|
|
final current = state.playback;
|
|
if (current != null && current.duration != duration) {
|
|
// Update duration if it wasn't known initially
|
|
}
|
|
});
|
|
|
|
_completedSub = _player.stream.completed.listen((completed) {
|
|
if (completed) {
|
|
_ref.read(displayStateProvider.notifier).onMediaCompleted();
|
|
}
|
|
});
|
|
}
|
|
|
|
VideoController get videoController => _videoController;
|
|
Player get player => _player;
|
|
|
|
Future<void> play(String url, {Duration? startPosition}) async {
|
|
await _player.open(Media(url));
|
|
if (startPosition != null && startPosition > Duration.zero) {
|
|
// Wait briefly for the player to initialize before seeking
|
|
await Future.delayed(const Duration(milliseconds: 500));
|
|
await _player.seek(startPosition);
|
|
}
|
|
_startProgressSaving();
|
|
}
|
|
|
|
Future<void> pause() async => await _player.pause();
|
|
|
|
Future<void> resume() async => await _player.play();
|
|
|
|
Future<void> seek(Duration position) async => await _player.seek(position);
|
|
|
|
Future<void> setVolume(double volume) async =>
|
|
await _player.setVolume(volume * 100);
|
|
|
|
Future<void> stop() async {
|
|
_stopProgressSaving();
|
|
await _player.stop();
|
|
}
|
|
|
|
void _startProgressSaving() {
|
|
_stopProgressSaving();
|
|
_progressSaveTimer = Timer.periodic(const Duration(seconds: 10), (_) {
|
|
final state = _ref.read(displayStateProvider);
|
|
final workout = state.workout;
|
|
final playback = state.playback;
|
|
|
|
if (workout != null &&
|
|
workout.phase == WorkoutPhase.playing &&
|
|
playback != null &&
|
|
playback.source == MediaSource.workout) {
|
|
final client = _ref.read(workoutApiClientProvider);
|
|
final percent = playback.duration.inSeconds > 0
|
|
? (playback.position.inSeconds / playback.duration.inSeconds * 100).round()
|
|
: 0;
|
|
client.updateProgress(
|
|
videoId: playback.mediaId,
|
|
position: playback.position.inSeconds,
|
|
percent: percent,
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
void _stopProgressSaving() {
|
|
_progressSaveTimer?.cancel();
|
|
_progressSaveTimer = null;
|
|
}
|
|
|
|
void dispose() {
|
|
_stopProgressSaving();
|
|
_positionSub?.cancel();
|
|
_playingSub?.cancel();
|
|
_completedSub?.cancel();
|
|
_durationSub?.cancel();
|
|
_player.dispose();
|
|
}
|
|
}
|