fix(player): 记住拖动后的实际播放位置
This commit is contained in:
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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user