fix(player): 优化上下滑切集黑屏

This commit is contained in:
Zhou Weidong committed 2026-10-26 13:15:56 +08:00
1 parent b83b5799e3
commit 222bacdeeb
10 files changed
+625 -122

No files matched your search

@@ -0,0 +1,177 @@
// Author: fruit.com.cn
part of 'playback_session.dart';
/// 当前集加上一集、下一集,切集时最多三个解码器。
const int maxLivePlayers = 3;
/// 另一条播放路由必须等当前路由释放完全部原生播放器。
class PlaybackCoordinator {
PlaybackSession? _owner;
Future<void> _handoff = Future<void>.value();
Future<bool> _acquire(PlaybackSession session, bool Function() valid) {
final result = _handoff.then((_) async {
if (!valid()) return false;
if (_owner != session) {
final previous = _owner;
await previous?._releasePlayers();
if (previous != null && previous.livePlayerCount != 0) return false;
_owner = null;
if (!valid()) return false;
_owner = session;
}
return true;
});
_handoff = result.then<void>((_) {});
return result;
}
void _forget(PlaybackSession session) {
if (_owner == session && session.livePlayerCount == 0) _owner = null;
}
}
_PlayerSlot? _usableWarm(
PlaybackSession session,
_PlayerSlot? slot,
int index,
) {
if (slot == null || slot.index != index || slot.failure != null) return null;
if (session.isLocked) return null;
return slot;
}
/// 滑走的一集若仍是相邻集,就留着最后一帧,其余槽位释放。
Future<void> _parkNeighbors(
PlaybackSession session,
_PlayerSlot? old,
_PlayerSlot? warmNext,
_PlayerSlot? warmPrevious,
) async {
await _placeNeighbor(session, old);
await _placeNeighbor(session, warmNext);
await _placeNeighbor(session, warmPrevious);
}
Future<void> _placeNeighbor(PlaybackSession session, _PlayerSlot? slot) async {
if (slot == null || slot == session._active || slot.retiring) return;
if (session._next == null && slot.index == session._nextIndex) {
session._next = slot;
return;
}
if (session._previous == null && slot.index == session._previousIndex) {
session._previous = slot;
return;
}
await session._retire(slot);
}
/// 当前集已经出画后,静默准备上一集和下一集。
void _warmNeighbors(PlaybackSession session) {
final request = session._generation;
session._preloadTask = () async {
await Future.wait([
_warmOne(session, request, session._nextIndex, next: true),
_warmOne(session, request, session._previousIndex, next: false),
]);
}();
}
Future<void> _warmOne(
PlaybackSession session,
int request,
int? index, {
required bool next,
}) async {
final candidate = await _allocateNeighbor(
session,
request,
index,
next: next,
);
if (candidate == null) return;
await candidate.ready;
if (session._closed || !_stillHeld(session, candidate, next: next)) return;
if (candidate.failure == null) {
session._notify();
return;
}
await session._enqueue(() async {
if (!_stillHeld(session, candidate, next: next)) return;
_clearNeighbor(session, next: next);
await session._retire(candidate);
});
}
Future<_PlayerSlot?> _allocateNeighbor(
PlaybackSession session,
int request,
int? index, {
required bool next,
}) async {
_PlayerSlot? created;
await session._enqueue(() async {
if (session._closed || request != session._generation) return;
final allowed = _neighborAllowed(session, index);
final current = next ? session._next : session._previous;
if (!allowed || current?.index != index) {
_clearNeighbor(session, next: next);
await session._retire(current);
}
if (!_canAllocate(session, request, allowed: allowed, next: next)) {
return;
}
try {
created = session._create(index!);
} on StateError catch (error) {
if (kDebugMode) debugPrint('相邻集预热跳过: $error');
return;
}
if (next) {
session._next = created;
} else {
session._previous = created;
}
});
return created;
}
bool _neighborAllowed(PlaybackSession session, int? index) {
return index != null &&
index >= 0 &&
index < session.episodes.length &&
index != session._currentIndex &&
session.canPlay(session.episodes[index]);
}
bool _canAllocate(
PlaybackSession session,
int request, {
required bool allowed,
required bool next,
}) {
final held = next ? session._next : session._previous;
return session._visible &&
!session._overlayOpen &&
request == session._generation &&
session._active?.prepared == true &&
allowed &&
held == null;
}
bool _stillHeld(
PlaybackSession session,
_PlayerSlot slot, {
required bool next,
}) {
return (next ? session._next : session._previous) == slot;
}
void _clearNeighbor(PlaybackSession session, {required bool next}) {
if (next) {
session._next = null;
return;
}
session._previous = null;
}
+58 -100
View File
@@ -7,6 +7,8 @@ import '../models/drama.dart';
import '../models/player_port.dart';
import 'video_player_port.dart';
part 'playback_neighbors.dart';
/// 新纹理第一帧常是黑的,旧画面只盖住这一小段。
const Duration _qualityHandoffHold = Duration(milliseconds: 200);
@@ -14,37 +16,7 @@ typedef PlayerFactory = PlayerPort Function(Episode episode);
typedef PlayerPreparation =
Future<void> Function(Episode episode, PlayerPort player);
/// Shared by routes: the previous session releases all native players before
/// another session allocates. The app therefore has at most two, not two per page.
class PlaybackCoordinator {
PlaybackSession? _owner;
Future<void> _handoff = Future<void>.value();
Future<bool> _acquire(PlaybackSession session, bool Function() valid) {
final result = _handoff.then((_) async {
if (!valid()) return false;
if (_owner != session) {
final previous = _owner;
await previous?._releasePlayers();
// A failed native disposal cannot be treated as a free decoder slot.
if (previous != null && previous.livePlayerCount != 0) return false;
_owner = null;
if (!valid()) return false;
_owner = session;
}
return true;
});
_handoff = result.then<void>((_) {});
return result;
}
void _forget(PlaybackSession session) {
if (_owner == session && session.livePlayerCount == 0) _owner = null;
}
}
/// Owns current + one prepared successor. All allocation, playback and disposal
/// operations are serialized; initialization can run while current plays.
/// Owns current plus previous/next frames; allocation stays serialized.
class PlaybackSession extends ChangeNotifier {
PlaybackSession({
required this.episodes,
@@ -70,12 +42,14 @@ class PlaybackSession extends ChangeNotifier {
final Set<_PlayerSlot> _live = {};
_PlayerSlot? _active;
_PlayerSlot? _next;
_PlayerSlot? _previous;
_PlayerSlot? _handoff;
Future<void> _operations = Future<void>.value();
Future<void> _preloadTask = Future<void>.value();
int _generation = 0;
int _currentIndex = 0;
int? _nextIndex;
int? _previousIndex;
bool _selected = false;
bool _loading = false;
bool _closed = false;
@@ -120,7 +94,7 @@ class PlaybackSession extends ChangeNotifier {
/// during a swipe without starting audio or registering history listeners.
PlayerPort? preparedPlayerAt(int index) {
if (_closed) return null;
for (final slot in [_active, _next]) {
for (final slot in [_active, _next, _previous]) {
if (slot == null ||
slot.retiring ||
slot.index != index ||
@@ -142,24 +116,36 @@ class PlaybackSession extends ChangeNotifier {
}
/// 队列重排后把当前仍在播放的 slot 映射到新索引,不重新创建播放器。
void remapCurrentIndex(int index, {int? preloadIndex}) {
void remapCurrentIndex(int index, {int? preloadIndex, int? previousIndex}) {
if (_closed || index < 0 || index >= episodes.length) return;
final activeSlot = _active;
if (activeSlot == null || activeSlot.index != _currentIndex) {
_currentIndex = index;
_nextIndex = preloadIndex;
_rememberNeighbors(preloadIndex, previousIndex);
_notify();
return;
}
_generation++;
activeSlot.index = index;
_currentIndex = index;
_nextIndex = preloadIndex;
_rememberNeighbors(preloadIndex, previousIndex);
final staleNext = _next;
final stalePrevious = _previous;
_next = null;
_previous = null;
_notify();
unawaited(_enqueue(() async => _retire(staleNext)));
if (_visible && !_overlayOpen) _schedulePreload();
unawaited(
_enqueue(() async {
await _retire(staleNext);
await _retire(stalePrevious);
}),
);
if (_visible && !_overlayOpen) _warmNeighbors(this);
}
void _rememberNeighbors(int? preloadIndex, int? previousIndex) {
_nextIndex = preloadIndex;
_previousIndex = previousIndex;
}
Future<void> _enqueue(Future<void> Function() operation) {
@@ -176,7 +162,9 @@ class PlaybackSession extends ChangeNotifier {
}
_PlayerSlot _create(int index) {
if (_live.length >= 2) throw StateError('Playback resource limit reached');
if (_live.length >= maxLivePlayers) {
throw StateError('Playback resource limit reached');
}
final slot = _PlayerSlot(index, factory(episodes[index]));
_live.add(slot);
slot.ready = () async {
@@ -198,9 +186,14 @@ class PlaybackSession extends ChangeNotifier {
Future<void> _retire(_PlayerSlot? slot) async {
if (slot == null || !_live.contains(slot)) return;
slot.retiring = true;
final cleared = _active == slot || _next == slot || _handoff == slot;
final cleared =
_active == slot ||
_next == slot ||
_previous == slot ||
_handoff == slot;
if (_active == slot) _active = null;
if (_next == slot) _next = null;
if (_previous == slot) _previous = null;
if (_handoff == slot) _handoff = null;
if (cleared) _notify();
await waitForFrame?.call();
@@ -215,7 +208,11 @@ class PlaybackSession extends ChangeNotifier {
/// preloadIndex comes from the visible queue, so filtered categories and series
/// use exactly the same mechanism without warming an unrelated episode.
Future<void> select(int index, {int? preloadIndex}) async {
Future<void> select(
int index, {
int? preloadIndex,
int? previousIndex,
}) async {
if (_closed || index < 0 || index >= episodes.length) return;
// 切到另一集时,上一集按下的暂停不跟着走:滑过去就应该自动播放。
// 重复选中同一集(例如页面重新可见)不在这里恢复,尊重用户按下的暂停。
@@ -225,7 +222,7 @@ class PlaybackSession extends ChangeNotifier {
final request = ++_generation;
_selected = true;
_currentIndex = index;
_nextIndex = preloadIndex;
_rememberNeighbors(preloadIndex, previousIndex);
_error = null;
if (!_visible) {
_loading = false;
@@ -260,19 +257,22 @@ class PlaybackSession extends ChangeNotifier {
return;
}
if (old != null) await old.player.pause();
final warm = _next;
final warmNext = _next;
final warmPrevious = _previous;
_active = null;
_next = null;
final hit = warm?.index == index && warm?.failure == null && !isLocked;
if (hit) {
_previous = null;
final warm =
_usableWarm(this, warmNext, index) ??
_usableWarm(this, warmPrevious, index);
if (warm != null) {
target = _active = warm;
_loading = !warm!.prepared;
_loading = !warm.prepared;
_notify();
// Start a ready successor before awaiting slow disposal of the old one.
if (warm.prepared) await _start(warm, request);
}
await _retire(old);
if (!hit) await _retire(warm);
await _parkNeighbors(this, old, warmNext, warmPrevious);
if (_closed || request != _generation || isLocked) return;
if (target == null) {
target = _active = _create(index);
@@ -285,7 +285,7 @@ class PlaybackSession extends ChangeNotifier {
await slot.ready;
if (_closed || request != _generation || _active != slot) return;
await _enqueue(() => _start(slot, request));
if (!_closed && request == _generation) _schedulePreload();
if (!_closed && request == _generation) _warmNeighbors(this);
}
/// 后台准备新片源,成功后再替换当前播放器。失败时原播放器保持不动。
@@ -316,7 +316,7 @@ class PlaybackSession extends ChangeNotifier {
if (previous != null && _handoff == previous && _active == slot) {
await _enqueue(() => _retire(previous));
}
if (!_closed && request == _generation) _schedulePreload();
if (!_closed && request == _generation) _warmNeighbors(this);
return _active == slot && slot.failure == null;
}
@@ -422,54 +422,6 @@ class PlaybackSession extends ChangeNotifier {
slot.playing = shouldPlay;
}
void _schedulePreload() {
final request = _generation;
final index = _nextIndex;
_preloadTask = () async {
_PlayerSlot? candidate;
await _enqueue(() async {
if (_closed || request != _generation) return;
final allowed =
index != null &&
index >= 0 &&
index < episodes.length &&
index != _currentIndex &&
canPlay(episodes[index]);
if (!allowed || _next?.index != index) {
final stale = _next;
_next = null;
await _retire(stale);
}
if (!_visible ||
_overlayOpen ||
request != _generation ||
_active?.prepared != true ||
!allowed) {
return;
}
try {
candidate = _next ??= _create(index);
} catch (_) {
// Optional allocation failure must not interrupt the playing item.
}
});
final slot = candidate;
if (slot == null) return;
await slot.ready;
if (_closed || _next != slot) return;
if (slot.failure != null) {
// Optional work fails silently. A later explicit selection retries.
await _enqueue(() async {
if (_next != slot) return;
_next = null;
await _retire(slot);
});
} else {
_notify();
}
}();
}
void setAppResumed(bool value) {
_appResumed = value;
_syncPlayback();
@@ -494,18 +446,24 @@ class PlaybackSession extends ChangeNotifier {
void _syncPlayback() {
if (_closed) return;
if (_visible && _selected && active == null) {
unawaited(select(_currentIndex, preloadIndex: _nextIndex));
unawaited(
select(
_currentIndex,
preloadIndex: _nextIndex,
previousIndex: _previousIndex,
),
);
return;
}
final slot = _active;
if (slot != null) unawaited(_enqueue(() => _applyPlayback(slot)));
if (_visible && !_overlayOpen && !_loading) _schedulePreload();
if (_visible && !_overlayOpen && !_loading) _warmNeighbors(this);
}
Future<void> _releasePlayers() {
_generation++;
return _enqueue(() async {
_active = _next = _handoff = null;
_active = _next = _previous = _handoff = null;
_loading = false;
_notify();
for (final slot in _live.toList()) {