Files
sync_replay_project/apps/desktop/lib/src/playback/playback_controller.dart
T
foxyz ff154c5030 Desktop session batch 09-12: frameless chrome, bisect removal, chat/idle fixes
Frameless (window_manager, hidden titlebar): chrome bar on welcome,
drag-to-move appbar + window buttons on connect, drag layer + window
buttons in session controls; min size 160x90; volume-scroll off for
drag; session maximize = fullscreen toggle. Verified on KDE Wayland.
Delete legacy/minimal/nosheet bisect screens; session routes direct
(black-screen fix verified incl. WAN).
Avatar: pipeline now 128px aspect-kept PNG (was cropped JPEG);
profile card centered 64px hover-to-change; member avatar drops
ResizeImage downscale. NOTE open: pipeline 512 KiB check vs server
64 KiB enforcement mismatch.
Fixes: idle warn/unready skipped while PLAYING; sendChat drops
blanks; one chat row per send (drop textless server feed echo);
chat field keeps focus after send. Playback libass true; sdk floor
3.13. Handoff 9-11: 9+10 fixed, 11 jumpy seek still open.
Docs: handoff/roadmap/protocol record all of the above; few-commits
preference going forward.
2026-09-12 11:42:18 +03:00

104 lines
2.6 KiB
Dart

import 'dart:io';
import 'package:flutter/widgets.dart';
import 'package:media_kit/media_kit.dart';
import '../sync/room_diff.dart';
class PlaybackController {
late final Player player;
bool _ready = false;
Future<void> init() async {
player = Player(
configuration: PlayerConfiguration(
bufferSize: 64 * 1024 * 1024,
libass: true,
vo: Platform.environment['SYNC_RELAY_VO'],
));
_ready = true;
await _pinBufferProps();
}
Future<void> _pin(String property, String value) async {
try {
final dynamic platform = player.platform;
await platform?.setProperty(property, value);
} catch (e, st) {
debugPrint('pin $property=$value failed: $e\n$st');
}
}
Future<void> _pinBufferProps() async {
await _pin('demuxer-readahead-secs', '10');
await _pin('cache-secs', '10');
final hwdec = Platform.environment['SYNC_RELAY_HWDEC'];
if (hwdec != null && hwdec.isNotEmpty) {
await _pin('hwdec', hwdec);
}
await _pin('stream-lavf-o',
'reconnect_streamed=1,reconnect_on_http_error=1,reconnect_delay_max=4');
}
Future<void> openUrl(String url) async {
if (!_ready) return;
try {
await player.open(Media(url), play: false);
} catch (e, st) {
debugPrint('open $url failed: $e\n$st');
rethrow;
}
}
double get positionSecs =>
player.state.position.inMilliseconds / 1000.0;
double get bufferedSecsAhead {
final ahead =
(player.state.buffer - player.state.position).inMilliseconds / 1000.0;
if (ahead.isNaN || ahead < 0) return 0;
return ahead;
}
double get durationSecs =>
player.state.duration.inMilliseconds / 1000.0;
Future<void> apply(List<PlayerAction> actions) async {
for (final action in actions) {
try {
switch (action) {
case SeekAction(:final position):
if ((positionSecs - position).abs() < 0.5) break;
await player.seek(Duration(milliseconds: (position * 1000).round()));
case TransportAction(:final playing):
if (playing) {
await player.play();
} else {
await player.pause();
}
case RateAction(:final rate):
await player.setRate(rate);
}
} catch (e, st) {
debugPrint('player action $action failed: $e\n$st');
}
}
}
Future<void> dispose() async {
_ready = false;
try {
await player.dispose();
} catch (_) {}
}
Future<void> stop() async {
if (!_ready) return;
try {
await player.stop();
} catch (e, st) {
debugPrint('player stop failed: $e\n$st');
}
}
}