fix(player): 记住拖动后的实际播放位置

This commit is contained in:
Zhou Weidong committed 2026-10-22 10:41:01 +08:00
1 parent 47ca21406a
commit 5268adc999
8 files changed
+352 -67

No files matched your search

@@ -2,6 +2,15 @@
const Duration _defaultMaxAge = Duration(hours: 12);
/// 将播放位置限制在当前媒体时长内。
Duration clampPlaybackPosition(Duration position, Duration duration) {
if (position <= Duration.zero || duration <= Duration.zero) {
return Duration.zero;
}
if (position >= duration) return duration;
return position;
}
/// 进程内最近播放位置。
class CachedPlaybackPosition {
const CachedPlaybackPosition({
@@ -37,13 +46,14 @@ class PlaybackPositionCache {
required Duration position,
required Duration duration,
}) {
if (episodeId.isEmpty || dramaId.isEmpty || position <= Duration.zero) {
final safePosition = clampPlaybackPosition(position, duration);
if (episodeId.isEmpty || dramaId.isEmpty || safePosition <= Duration.zero) {
return;
}
_positions[episodeId] = CachedPlaybackPosition(
episodeId: episodeId,
dramaId: dramaId,
position: position,
position: safePosition,
duration: duration,
updatedAt: _now(),
);
@@ -61,6 +61,9 @@ class VideoPlayerPort implements PlayerPort {
if (!_disposed && controller.value.isInitialized) await controller.pause();
}
/// 跳转到指定播放位置。
Future<void> seekTo(Duration position) => controller.seekTo(position);
@override
Future<void> dispose() async {
if (_disposed) return;
+99 -36
View File
@@ -34,6 +34,12 @@ import 'player_view.dart';
import 'widgets/drama_comments_sheet.dart';
import 'widgets/episode_picker_sheet.dart';
typedef _PlaybackSnapshot = ({
Duration position,
Duration duration,
bool completed,
});
class PlayerLogic extends ChangeNotifier {
PlayerLogic({
required this.history,
@@ -119,6 +125,7 @@ class PlayerLogic extends ChangeNotifier {
bool _deferredFeedNotifyScheduled = false;
int _feedJumpSerial = 0;
Future<void> _feedUpdateFuture = Future<void>.value();
Future<void>? _pendingSeek;
/// [start] 是否已经跑过:没跑过时 session/pages 还是 late 未初始化,
/// dispose 不能直接碰它们(例如页面构建失败或纯逻辑测试)。
@@ -693,29 +700,24 @@ class PlayerLogic extends ChangeNotifier {
var completionHandled = false;
var completionProgressSaved = false;
player.controller.addListener(() {
if (session.active != player || !player.controller.value.isInitialized) {
return;
}
positionCache.save(
episodeId: episode.id,
dramaId: drama.id,
position: player.controller.value.position,
duration: player.controller.value.duration,
);
if (player.controller.value.isCompleted && !completionProgressSaved) {
if (session.active != player) return;
final snapshot = _readPlaybackSnapshot(player);
if (snapshot == null) return;
_cachePlaybackPosition(drama, episode, snapshot);
if (snapshot.completed && !completionProgressSaved) {
completionProgressSaved = true;
unawaited(
queueProgress(
drama,
episode,
player.controller.value.duration,
duration: player.controller.value.duration,
snapshot.duration,
duration: snapshot.duration,
finished: true,
),
);
}
if (!completionHandled &&
player.controller.value.isCompleted &&
snapshot.completed &&
session.canAutoAdvance &&
session.currentIndex == index) {
completionHandled = true;
@@ -723,7 +725,12 @@ class PlayerLogic extends ChangeNotifier {
}
if (!shouldReportProgress(DateTime.now())) return;
unawaited(
queueProgress(drama, episode, player.controller.value.position),
queueProgress(
drama,
episode,
snapshot.position,
duration: snapshot.duration,
),
);
});
}
@@ -775,27 +782,63 @@ class PlayerLogic extends ChangeNotifier {
return pendingWrite;
}
Future<void> saveCurrentProgress() {
Future<void> saveCurrentProgress() async {
final pendingSeek = _pendingSeek;
if (pendingSeek != null) await pendingSeek;
await _saveCurrentProgressNow();
}
Future<void> _saveCurrentProgressNow() {
final player = trackedPlayer;
final episode = trackedEpisode;
final drama = trackedDrama;
if (player == null ||
episode == null ||
drama == null ||
!player.controller.value.isInitialized) {
return pendingWrite;
if (player == null || episode == null || drama == null) return pendingWrite;
final snapshot = _readPlaybackSnapshot(player);
if (snapshot == null) return pendingWrite;
return _persistPlaybackSnapshot(drama, episode, snapshot);
}
_PlaybackSnapshot? _readPlaybackSnapshot(VideoPlayerPort player) {
final value = player.controller.value;
if (!value.isInitialized || value.duration <= Duration.zero) return null;
return (
position: clampPlaybackPosition(value.position, value.duration),
duration: value.duration,
completed: value.isCompleted,
);
}
void _cachePlaybackPosition(
Drama drama,
Episode episode,
_PlaybackSnapshot snapshot,
) {
if (session.isLocked ||
ticketError != null ||
blockedEpisodeId == episode.id) {
return;
}
positionCache.save(
episodeId: episode.id,
dramaId: drama.id,
position: player.controller.value.position,
duration: player.controller.value.duration,
position: snapshot.position,
duration: snapshot.duration,
);
}
Future<void> _persistPlaybackSnapshot(
Drama drama,
Episode episode,
_PlaybackSnapshot snapshot, {
bool finished = false,
}) {
_cachePlaybackPosition(drama, episode, snapshot);
return queueProgress(
drama,
episode,
player.controller.value.position,
duration: player.controller.value.duration,
snapshot.position,
duration: snapshot.duration,
finished: finished,
);
}
@@ -856,23 +899,44 @@ class PlayerLogic extends ChangeNotifier {
BuildContext context,
VideoPlayerPort player,
Duration position,
) {
final operation = _performSeek(context, player, position);
_pendingSeek = operation;
return operation.whenComplete(() {
if (identical(_pendingSeek, operation)) _pendingSeek = null;
});
}
Future<void> _performSeek(
BuildContext context,
VideoPlayerPort player,
Duration position,
) async {
if (scrubbingPlayer != player || session.active != player) return;
try {
await session.playbackSettled;
if (scrubbingPlayer != player || session.active != player) return;
await player.controller.seekTo(position);
if (session.active == player) {
// 暂停时拖动进度后直接恢复播放,避免画面停在旧位置。
resumeAfterSeek();
await saveCurrentProgress();
}
final duration = player.controller.value.duration;
final target = clampPlaybackPosition(position, duration);
if (target <= Duration.zero) return;
await player.seekTo(target);
if (session.active != player) return;
final snapshot = _readPlaybackSnapshot(player);
if (snapshot == null) return;
final entry = queue.entryAt(session.currentIndex);
final save = _persistPlaybackSnapshot(
entry.drama,
entry.episode,
snapshot,
);
// 暂停时拖动进度后直接恢复播放,避免画面停在旧位置。
resumeAfterSeek();
await save;
} catch (_) {
if (context.mounted && session.active == player) {
final l10n = AppLocalizations.of(context)!;
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(l10n.seekFailed)));
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(l10n.seekFailed)));
}
} finally {
if (scrubbingPlayer == player) cancelScrub();
@@ -1013,9 +1077,8 @@ class PlayerLogic extends ChangeNotifier {
await session.playbackSettled;
await saveCurrentProgress();
if (!context.mounted) return;
final action = await Navigator.of(
context,
).push<MyPageAction>(MaterialPageRoute(builder: (_) => const MyPage()));
final action = await Navigator.of(context)
.push<MyPageAction>(MaterialPageRoute(builder: (_) => const MyPage()));
if (!context.mounted) return;
if (action == MyPageAction.discover) {
await showDiscover(context);