hondavert-dev/lib/core/datalog/datalog_player.dart
2026-10-13 00:34:11 +00:00

120 lines
3.3 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import 'dart:async';
import 'dart:typed_data';
import 'datalog_db.dart';
enum PlaybackStatus { idle, playing, paused }
/// Loads stored frames from SQLite and replays them through a stream
/// at configurable speed. Consumers watch [frameStream] exactly like
/// the live BT stream — the parser layer is unaware of the difference.
class DatalogPlayer {
final DatalogDb _db;
List<StoredFrame> _frames = [];
int _position = 0; // index into _frames
double _speed = 1.0; // 1.0 = real-time, 2.0 = 2× faster
PlaybackStatus _status = PlaybackStatus.idle;
PlaybackStatus get status => _status;
int get position => _position;
int get totalFrames => _frames.length;
double get speed => _speed;
set speed(double v) => _speed = v.clamp(0.25, 8.0);
final StreamController<Uint8List> _controller =
StreamController<Uint8List>.broadcast();
Stream<Uint8List> get frameStream => _controller.stream;
/// Callback fired on each tick — lets UI update a progress indicator.
void Function(int position, int total)? onProgress;
Timer? _timer;
DatalogPlayer(this._db);
/// Load all frames for [sessionId] into memory.
Future<void> load(int sessionId) async {
await stop();
_frames = await _db.getFrames(sessionId);
_position = 0;
_status = PlaybackStatus.idle;
}
/// Start or resume playback.
void play() {
if (_status == PlaybackStatus.playing) return;
if (_frames.isEmpty) return;
_status = PlaybackStatus.playing;
_scheduleNext();
}
void pause() {
_timer?.cancel();
_timer = null;
if (_status == PlaybackStatus.playing) {
_status = PlaybackStatus.paused;
}
}
Future<void> stop() async {
_timer?.cancel();
_timer = null;
_position = 0;
_status = PlaybackStatus.idle;
}
/// Jump to a specific frame index.
void seek(int index) {
_position = index.clamp(0, _frames.length - 1);
onProgress?.call(_position, _frames.length);
}
void _scheduleNext() {
if (_position >= _frames.length) {
_status = PlaybackStatus.idle;
onProgress?.call(_frames.length, _frames.length);
return;
}
// Calculate delay to next frame using original timestamps
Duration delay = const Duration(milliseconds: 100); // default 10 Hz
if (_position + 1 < _frames.length) {
final gap = _frames[_position + 1].timestamp
.difference(_frames[_position].timestamp);
delay = Duration(
microseconds: (gap.inMicroseconds / _speed).round(),
);
// Clamp to 10ms–2s to handle gaps from pauses/reconnects
delay = delay.clamp(
const Duration(milliseconds: 10),
const Duration(seconds: 2),
);
}
_timer = Timer(delay, () {
if (_status != PlaybackStatus.playing) return;
if (_position < _frames.length) {
_controller.add(_frames[_position].data);
onProgress?.call(_position, _frames.length);
_position++;
}
_scheduleNext();
});
}
void dispose() {
_timer?.cancel();
_controller.close();
}
}
extension on Duration {
Duration clamp(Duration min, Duration max) {
if (this < min) return min;
if (this > max) return max;
return this;
}
}