add
This commit is contained in:
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user