Files
mirror-os-flutter/apps/display/lib/providers/player_provider.dart
T
2026-07-11 21:51:34 -05:00

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();
}
}