Files
sm-emby-share/lib/features/player/services/danmaku/danmaku_feeder.dart
T
2026-07-14 11:11:36 +08:00

187 lines
4.9 KiB
Dart

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<DanmakuComment> _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<DanmakuComment> 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,
},
);
}
}