- datalog_session.dart: DatalogSession model with toMap/fromMap, duration helpers - datalog_db.dart: SQLite schema (sessions + frames tables), batch insertFrames, getSessions, getFrames, deleteSession, closeSession - datalog_recorder.dart: listens to raw frame stream, buffers PendingFrames, flushes to DB every 500ms via Timer, finalises session on stop() - datalog_player.dart: loads StoredFrames from DB, replays at original timing adjusted by speed multiplier (0.25x–8x), play/pause/stop/seek, onProgress cb - datalog_provider.dart: datalogDbProvider, datalogRecorderProvider, datalogPlayerProvider singletons; RecordingNotifier (idle/recording state); sessionListProvider FutureProvider; PlaybackNotifier with full playback state - pubspec.yaml: added path ^1.9.0 dependency
120 lines
3.2 KiB
Dart
120 lines
3.2 KiB
Dart
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;
|
||
}
|
||
}
|