This commit is contained in:
macbook_max
2026-10-15 20:36:59 +08:00
commit 6bf949949c
7923 changed files with 1369582 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
import 'dart:async';
import 'package:media_kit/media_kit.dart';
import 'package:media_kit_video/media_kit_video.dart';
/// 一个播放槽位:Player + 视图控制器 + 当前绑定的视频
class PlayerSlot {
PlayerSlot(this.player);
final Player player;
late final VideoController controller = VideoController(player);
/// 当前槽位绑定的 videoId(-1 表示空槽)
int videoId = -1;
/// 是否已回调过 complete(每视频每次绑定只触发一次)
bool completeNotified = false;
}
/// Feed 播放器池:3 槽位循环复用,防止 PageView 无限列表下 OOM。
///
/// 策略:
/// - 窗口 [current-1, current, current+1] 映射到 index % 3 槽位
/// - 当前槽 play,相邻槽仅 open 预缓冲,出窗槽 stop 复位
/// - 每个槽位独立订阅进度流:position ≥ duration×95% 或 completed → 回调一次 complete
///
/// 构造接受 [playerFactory] 便于测试注入 fake(本机无播放器环境)。
class FeedPlayerPool {
FeedPlayerPool({
Player Function()? playerFactory,
this.onWatchComplete,
}) : _playerFactory = playerFactory;
final Player Function()? _playerFactory;
/// 完整观看回调:(videoId, watchDuration秒),由上层做去重与上报
final void Function(int videoId, int watchDurationSeconds)? onWatchComplete;
static const int slotCount = 3;
final List<PlayerSlot> _slots = [];
final List<StreamSubscription> _subs = [];
int _currentIndex = 0;
int get currentIndex => _currentIndex;
/// 初始化(创建 3 槽位并订阅各自进度流)
void init() {
for (var i = 0; i < slotCount; i++) {
final player = _playerFactory?.call() ?? Player();
final slot = PlayerSlot(player);
_slots.add(slot);
_hookSlot(slot);
}
}
/// 槽位号(PageView 页用 index % 3 取对应 controller 渲染)
int slotOf(int index) => index % slotCount;
PlayerSlot slotAt(int slotIndex) => _slots[slotIndex];
VideoController controllerAt(int slotIndex) => _slots[slotIndex].controller;
PlayerSlot get currentSlot => _slots[slotOf(_currentIndex)];
bool get isCurrentPlaying => currentSlot.player.state.playing;
/// 绑定窗口:index 为当前页;items 为已解析好播放地址的窗口数据源
void bindWindow(int index, List<({int videoId, String url})> items) {
_currentIndex = index;
// 1. 窗口内槽位按需加载
for (var offset = -1; offset <= 1; offset++) {
final i = index + offset;
if (i < 0 || i >= items.length) continue;
final slot = _slots[slotOf(i)];
final item = items[i];
if (slot.videoId == item.videoId) continue; // 已加载,避免重复 open
slot.videoId = item.videoId;
slot.completeNotified = false;
if (item.url.isNotEmpty) {
slot.player.open(Media(item.url), play: false);
}
}
// 2. 槽位状态:当前播、相邻预缓冲(保持 pause)、出窗复位
for (var s = 0; s < slotCount; s++) {
final slot = _slots[s];
if (s == slotOf(index)) continue;
if (slot.videoId == -1) continue;
if (_inWindow(slot.videoId, index, items)) {
slot.player.pause(); // 窗口内非当前:预缓冲不播放
} else {
slot.player.stop();
slot.videoId = -1; // 出窗复位
}
}
currentSlot.player.play();
}
bool _inWindow(int videoId, int index, List<({int videoId, String url})> items) {
for (var offset = -1; offset <= 1; offset++) {
final i = index + offset;
if (i >= 0 && i < items.length && items[i].videoId == videoId) return true;
}
return false;
}
/// 每槽位独立订阅:≥95% 或播完触发一次 complete 回调
void _hookSlot(PlayerSlot slot) {
var lastPosition = Duration.zero;
_subs.add(slot.player.stream.position.listen((pos) {
lastPosition = pos;
if (slot.videoId < 0 || slot.completeNotified) return;
// 仅当前槽才上报(预缓冲槽不应触发)
if (slot != currentSlot) return;
final dur = slot.player.state.duration;
if (dur.inMilliseconds > 0 &&
pos.inMilliseconds >= dur.inMilliseconds * 0.95) {
slot.completeNotified = true;
onWatchComplete?.call(slot.videoId, pos.inSeconds);
}
}));
_subs.add(slot.player.stream.completed.listen((done) {
if (!done) return;
if (slot.videoId < 0 || slot.completeNotified) return;
if (slot != currentSlot) return;
slot.completeNotified = true;
onWatchComplete?.call(slot.videoId, lastPosition.inSeconds);
}));
}
/// 暂停/继续/切换当前槽(手势与生命周期用)
void pauseCurrent() => currentSlot.player.pause();
void playCurrent() => currentSlot.player.play();
void toggleCurrent() => currentSlot.player.playOrPause();
/// 释放全部资源(Feed 页 dispose 时调用)
Future<void> dispose() async {
for (final sub in _subs) {
await sub.cancel();
}
_subs.clear();
for (final slot in _slots) {
await slot.player.dispose();
}
}
}