import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:flutter/foundation.dart' show debugPrint; import 'package:flutter/material.dart' show Colors; import '../../../../core/contracts/danmaku.dart'; import '../../../../core/media/danmaku_comment_pipeline.dart'; const double _kBackwardSeekThreshold = -1.0; const double _kForwardSeekThreshold = 5.0; const int _kMaxFeedPerTick = 10; const double _kMaxFeedLagSec = 1.0; enum DanmakuPlaybackState { detached, disabled, playing, frozen, seeking } class DanmakuFeeder { DanmakuController? _canvas; List _comments = const []; DanmakuPanelSettings _settings = const DanmakuPanelSettings(); int _feedIndex = 0; double _lastPosSec = -1; DanmakuPlaybackState _state = DanmakuPlaybackState.detached; double? _frozenPosSec; void attach(DanmakuController controller) { _canvas = controller; _state = _settings.enabled ? DanmakuPlaybackState.frozen : DanmakuPlaybackState.disabled; } void detach() { _canvas = null; _state = DanmakuPlaybackState.detached; } void setComments(List commentsSortedByTime) { _comments = commentsSortedByTime; _canvas?.clear(); _feedIndex = 0; _lastPosSec = -1; _frozenPosSec = null; } void setPanelSettings( DanmakuPanelSettings settings, { DanmakuOption? option, }) { _settings = settings; if (!_settings.enabled) { _state = DanmakuPlaybackState.disabled; } else if (_state == DanmakuPlaybackState.disabled) { _state = _canvas == null ? DanmakuPlaybackState.detached : DanmakuPlaybackState.frozen; } if (option != null) { try { _canvas?.updateOption(option); } catch (e) { debugPrint('[DanmakuFeeder] updateOption failed: $e; detaching canvas'); _canvas = null; _state = DanmakuPlaybackState.detached; } } } void onPosition(double posSec) { final canvas = _canvas; if (canvas == null || _comments.isEmpty || !_settings.enabled || _state != DanmakuPlaybackState.playing) { return; } final effectivePos = posSec - _settings.offset; final last = _lastPosSec; if (last >= 0 && _isTimelineJump(posSec - last)) { canvas.clear(); _feedIndex = lowerBoundByTime(_comments, effectivePos); } _lastPosSec = posSec; var fedThisTick = 0; while (_feedIndex < _comments.length && fedThisTick < _kMaxFeedPerTick) { final comment = _comments[_feedIndex]; if (comment.time > effectivePos) break; _feedIndex++; if (effectivePos - comment.time > _kMaxFeedLagSec) continue; canvas.addDanmaku(_toContentItem(comment)); fedThisTick++; } } void freezeAt(double posSec) { final canvas = _canvas; if (canvas == null) { _state = DanmakuPlaybackState.detached; return; } if (!_settings.enabled) { _state = DanmakuPlaybackState.disabled; return; } _state = DanmakuPlaybackState.frozen; _frozenPosSec = posSec; _lastPosSec = posSec; if (canvas.running) { canvas.pause(); } } void playFrom(double posSec) { final canvas = _canvas; if (canvas == null) { _state = DanmakuPlaybackState.detached; return; } if (!_settings.enabled) { _state = DanmakuPlaybackState.disabled; return; } final frozenPos = _frozenPosSec; if (_state == DanmakuPlaybackState.frozen && frozenPos != null && posSec > frozenPos) { final targetIndex = lowerBoundByTime( _comments, posSec - _settings.offset, ); if (targetIndex > _feedIndex) { _feedIndex = targetIndex; } } _lastPosSec = posSec; _frozenPosSec = null; _state = DanmakuPlaybackState.playing; if (!canvas.running) { canvas.resume(); } } void seekTo(double posSec, {bool clear = true}) { if (clear) _canvas?.clear(); _feedIndex = lowerBoundByTime(_comments, posSec - _settings.offset); _lastPosSec = posSec; _frozenPosSec = null; if (_canvas == null) { _state = DanmakuPlaybackState.detached; } else { _state = _settings.enabled ? DanmakuPlaybackState.seeking : DanmakuPlaybackState.disabled; } } bool _isTimelineJump(double deltaSec) { return deltaSec < _kBackwardSeekThreshold || deltaSec > _kForwardSeekThreshold; } DanmakuContentItem _toContentItem(DanmakuComment c) { final baseColor = _settings.colored ? c.color : Colors.white; final opacity = _settings.opacity.clamp(0.0, 1.0); final color = opacity >= 1.0 ? baseColor : baseColor.withValues(alpha: opacity); return DanmakuContentItem( c.text, color: color, type: switch (c.type) { 4 when !_settings.fixedToScroll => DanmakuItemType.bottom, 5 when !_settings.fixedToScroll => DanmakuItemType.top, _ => DanmakuItemType.scroll, }, ); } }