fix(player): 修复详情往返播放位置丢失
This commit is contained in:
1 parent
fe49c6d8b2
commit
759a02375a
16 files changed
+464
-5
No files matched your search
@@ -42,6 +42,7 @@ import 'core/data/services/favorite_controller.dart';
|
||||
import 'core/data/services/interaction_controller.dart';
|
||||
import 'core/data/services/locale_controller.dart';
|
||||
import 'core/data/services/event_tracker.dart';
|
||||
import 'core/data/services/playback_position_cache.dart';
|
||||
import 'core/data/services/playback_session.dart';
|
||||
import 'core/data/services/video_player_port.dart';
|
||||
import 'core/data/services/wallet_controller.dart';
|
||||
@@ -72,6 +73,7 @@ class LumaDramaApp extends StatefulWidget {
|
||||
this.watchHistoryRepository,
|
||||
this.favoriteRepository,
|
||||
this.playbackRepository,
|
||||
this.playbackPositionCache,
|
||||
this.commentRepository,
|
||||
this.interactionRepository,
|
||||
this.noticeRepository,
|
||||
@@ -91,6 +93,7 @@ class LumaDramaApp extends StatefulWidget {
|
||||
final WatchHistoryRepository? watchHistoryRepository;
|
||||
final FavoriteRepository? favoriteRepository;
|
||||
final PlaybackRepository? playbackRepository;
|
||||
final PlaybackPositionCache? playbackPositionCache;
|
||||
final CommentRepository? commentRepository;
|
||||
final InteractionRepository? interactionRepository;
|
||||
final NoticeRepository? noticeRepository;
|
||||
@@ -146,6 +149,9 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
||||
widget.playbackRepository ??
|
||||
HttpPlaybackRepository(PlaybackApi(_apiClient));
|
||||
|
||||
late final PlaybackPositionCache _playbackPositionCache =
|
||||
widget.playbackPositionCache ?? PlaybackPositionCache();
|
||||
|
||||
late final FavoriteController _favoriteController = FavoriteController(
|
||||
_favorites,
|
||||
);
|
||||
@@ -240,6 +246,7 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
||||
Provider<PlayerFactory>(create: (_) => widget.playerFactory),
|
||||
Provider<CatalogRepository>.value(value: _catalogRepository),
|
||||
Provider<PlaybackRepository>.value(value: _playback),
|
||||
Provider<PlaybackPositionCache>.value(value: _playbackPositionCache),
|
||||
Provider<WatchHistoryRepository>.value(value: _watchHistory),
|
||||
Provider<FavoriteRepository>.value(value: _favorites),
|
||||
Provider<CommentRepository>.value(value: _commentRepository),
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
const Duration _defaultMaxAge = Duration(hours: 12);
|
||||
|
||||
/// 进程内最近播放位置。
|
||||
class CachedPlaybackPosition {
|
||||
const CachedPlaybackPosition({
|
||||
required this.episodeId,
|
||||
required this.dramaId,
|
||||
required this.position,
|
||||
required this.duration,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
final String episodeId;
|
||||
final String dramaId;
|
||||
final Duration position;
|
||||
final Duration duration;
|
||||
final DateTime updatedAt;
|
||||
}
|
||||
|
||||
/// 按集缓存最近播放位置,仅当前进程有效。
|
||||
class PlaybackPositionCache {
|
||||
PlaybackPositionCache({
|
||||
DateTime Function()? now,
|
||||
this.maxAge = _defaultMaxAge,
|
||||
}) : _now = now ?? DateTime.now;
|
||||
|
||||
final DateTime Function() _now;
|
||||
final Duration maxAge;
|
||||
final Map<String, CachedPlaybackPosition> _positions = {};
|
||||
|
||||
/// 保存最近有效播放位置;零位置不会覆盖已有记录。
|
||||
void save({
|
||||
required String episodeId,
|
||||
required String dramaId,
|
||||
required Duration position,
|
||||
required Duration duration,
|
||||
}) {
|
||||
if (episodeId.isEmpty || dramaId.isEmpty || position <= Duration.zero) {
|
||||
return;
|
||||
}
|
||||
_positions[episodeId] = CachedPlaybackPosition(
|
||||
episodeId: episodeId,
|
||||
dramaId: dramaId,
|
||||
position: position,
|
||||
duration: duration,
|
||||
updatedAt: _now(),
|
||||
);
|
||||
}
|
||||
|
||||
/// 读取同剧同集且未过期的最近位置。
|
||||
CachedPlaybackPosition? read(String episodeId, {required String dramaId}) {
|
||||
final entry = _positions[episodeId];
|
||||
if (!_isValid(entry, dramaId)) return null;
|
||||
return entry;
|
||||
}
|
||||
|
||||
/// 读取指定短剧最近更新的有效位置。
|
||||
CachedPlaybackPosition? latestForDrama(String dramaId) {
|
||||
CachedPlaybackPosition? latest;
|
||||
for (final entry in _positions.values) {
|
||||
if (!_isValid(entry, dramaId)) continue;
|
||||
if (latest == null || !entry.updatedAt.isBefore(latest.updatedAt)) {
|
||||
latest = entry;
|
||||
}
|
||||
}
|
||||
return latest;
|
||||
}
|
||||
|
||||
bool _isValid(CachedPlaybackPosition? entry, String dramaId) {
|
||||
if (entry == null ||
|
||||
entry.dramaId != dramaId ||
|
||||
entry.position <= Duration.zero) {
|
||||
return false;
|
||||
}
|
||||
return _now().difference(entry.updatedAt) <= maxAge;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import '../../core/data/models/drama.dart';
|
||||
import '../../core/data/repositories/playback_repository.dart';
|
||||
import '../../core/data/repositories/watch_history_repository.dart';
|
||||
import '../../core/data/services/favorite_controller.dart';
|
||||
import '../../core/data/services/playback_position_cache.dart';
|
||||
import 'drama_detail_logic.dart';
|
||||
|
||||
class DramaDetailBinding {
|
||||
@@ -12,10 +13,12 @@ class DramaDetailBinding {
|
||||
required WatchHistoryRepository history,
|
||||
required PlaybackRepository playback,
|
||||
required FavoriteController favorites,
|
||||
required PlaybackPositionCache positionCache,
|
||||
}) => DramaDetailLogic(
|
||||
drama: drama,
|
||||
history: history,
|
||||
playback: playback,
|
||||
favorites: favorites,
|
||||
positionCache: positionCache,
|
||||
);
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import '../../core/data/repositories/watch_history_repository.dart';
|
||||
import '../../core/data/repositories/playback_repository.dart';
|
||||
import '../../core/data/services/favorite_controller.dart';
|
||||
import '../../core/data/services/download_controller.dart';
|
||||
import '../../core/data/services/playback_position_cache.dart';
|
||||
import '../../core/data/services/wallet_controller.dart';
|
||||
import '../player/player_view.dart';
|
||||
|
||||
@@ -20,6 +21,7 @@ class DramaDetailLogic extends ChangeNotifier {
|
||||
required this.history,
|
||||
required this.playback,
|
||||
required this.favorites,
|
||||
required this.positionCache,
|
||||
});
|
||||
|
||||
/// 先用列表卡片渲染,取到详情后换成带完整剧集的版本。
|
||||
@@ -27,6 +29,7 @@ class DramaDetailLogic extends ChangeNotifier {
|
||||
final WatchHistoryRepository history;
|
||||
final PlaybackRepository playback;
|
||||
final FavoriteController favorites;
|
||||
final PlaybackPositionCache positionCache;
|
||||
bool loading = false;
|
||||
bool failed = false;
|
||||
|
||||
@@ -71,6 +74,23 @@ class DramaDetailLogic extends ChangeNotifier {
|
||||
}
|
||||
|
||||
Future<void> loadContinue() async {
|
||||
final cached = positionCache.latestForDrama(drama.id);
|
||||
if (cached != null) {
|
||||
final index = _indexOf(cached.episodeId);
|
||||
if (index != null) {
|
||||
progress = WatchProgress(
|
||||
dramaId: cached.dramaId,
|
||||
episodeId: cached.episodeId,
|
||||
position: cached.position,
|
||||
duration: cached.duration,
|
||||
updatedAt: cached.updatedAt,
|
||||
);
|
||||
continueIndex = index;
|
||||
progressError = null;
|
||||
notify();
|
||||
return;
|
||||
}
|
||||
}
|
||||
try {
|
||||
final record = await history.progressFor(drama.id);
|
||||
if (_disposed) return;
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../../core/data/repositories/watch_history_repository.dart';
|
||||
import '../../core/data/services/catalog_controller.dart';
|
||||
import '../../core/data/services/favorite_controller.dart';
|
||||
import '../../core/data/services/interaction_controller.dart';
|
||||
import '../../core/data/services/playback_position_cache.dart';
|
||||
import '../../core/data/services/platform_share_service.dart';
|
||||
import '../../core/formatting/error_text.dart';
|
||||
import '../../core/formatting/localized_content.dart';
|
||||
@@ -64,6 +65,7 @@ class _DramaDetailPageState extends State<DramaDetailPage> {
|
||||
history: context.read<WatchHistoryRepository>(),
|
||||
playback: context.read<PlaybackRepository>(),
|
||||
favorites: context.read<FavoriteController>(),
|
||||
positionCache: context.read<PlaybackPositionCache>(),
|
||||
);
|
||||
_load();
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import '../../core/data/repositories/watch_history_repository.dart';
|
||||
import '../../core/data/repositories/playback_repository.dart';
|
||||
import '../../core/data/services/android_picture_in_picture.dart';
|
||||
import '../../core/data/services/download_controller.dart';
|
||||
import '../../core/data/services/playback_position_cache.dart';
|
||||
import '../../core/data/services/wallet_controller.dart';
|
||||
import 'player_logic.dart';
|
||||
|
||||
@@ -11,11 +12,13 @@ class PlayerBinding {
|
||||
PlayerLogic create({
|
||||
required WatchHistoryRepository history,
|
||||
required PlaybackRepository playback,
|
||||
required PlaybackPositionCache positionCache,
|
||||
WalletController? wallet,
|
||||
DownloadController? downloads,
|
||||
}) => PlayerLogic(
|
||||
history: history,
|
||||
playback: playback,
|
||||
positionCache: positionCache,
|
||||
pictureInPicture: AndroidPictureInPicture(),
|
||||
wallet: wallet,
|
||||
downloads: downloads,
|
||||
|
||||
@@ -17,6 +17,7 @@ import '../../core/data/services/android_picture_in_picture.dart';
|
||||
import '../../core/data/services/catalog_controller.dart';
|
||||
import '../../core/data/services/download_controller.dart';
|
||||
import '../../core/data/services/platform_share_service.dart';
|
||||
import '../../core/data/services/playback_position_cache.dart';
|
||||
import '../../core/data/services/playback_queue.dart';
|
||||
import '../../core/data/services/playback_session.dart';
|
||||
import '../../core/data/services/video_player_port.dart';
|
||||
@@ -40,8 +41,10 @@ class PlayerLogic extends ChangeNotifier {
|
||||
required this.pictureInPicture,
|
||||
this.wallet,
|
||||
this.downloads,
|
||||
PlaybackPositionCache? positionCache,
|
||||
Future<void> Function()? waitForFrame,
|
||||
}) : _waitForFrame =
|
||||
}) : positionCache = positionCache ?? PlaybackPositionCache(),
|
||||
_waitForFrame =
|
||||
waitForFrame ?? (() => WidgetsBinding.instance.endOfFrame);
|
||||
|
||||
final WatchHistoryRepository history;
|
||||
@@ -49,6 +52,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
final AndroidPictureInPicture pictureInPicture;
|
||||
final WalletController? wallet;
|
||||
final DownloadController? downloads;
|
||||
final PlaybackPositionCache positionCache;
|
||||
final Future<void> Function() _waitForFrame;
|
||||
|
||||
late PageController pages;
|
||||
@@ -351,9 +355,22 @@ class PlayerLogic extends ChangeNotifier {
|
||||
|
||||
/// 服务端续看位置:进播放页时取一次,用于"定位到上次那一集的那一秒"。
|
||||
Future<void> loadResumePoint() async {
|
||||
final dramaId = queue.entryAt(initialIndex).drama.id;
|
||||
final entry = queue.entryAt(initialIndex);
|
||||
final cached = positionCache.read(
|
||||
entry.episode.id,
|
||||
dramaId: entry.drama.id,
|
||||
);
|
||||
if (cached != null) {
|
||||
resumePoint = ResumePoint(
|
||||
episodeId: cached.episodeId,
|
||||
position: cached.position,
|
||||
);
|
||||
resumeLoaded = true;
|
||||
if (isMounted()) notify();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
resumePoint = await history.resumePointFor(dramaId);
|
||||
resumePoint = await history.resumePointFor(entry.drama.id);
|
||||
} catch (_) {
|
||||
// 拿不到续看位置不影响播放:从当前集开头看
|
||||
resumePoint = null;
|
||||
@@ -639,6 +656,18 @@ class PlayerLogic extends ChangeNotifier {
|
||||
await pendingWrite;
|
||||
// 服务端每剧只记一条:只有"记录里的那一集"才定位,其它集从头播
|
||||
if (resumePoint == null) await loadResumePoint();
|
||||
if (resumePositionFor(episode.id) == null) {
|
||||
final dramaId = _dramaIdForEpisode(episode.id);
|
||||
final cached = dramaId == null
|
||||
? null
|
||||
: positionCache.read(episode.id, dramaId: dramaId);
|
||||
if (cached != null) {
|
||||
resumePoint = ResumePoint(
|
||||
episodeId: cached.episodeId,
|
||||
position: cached.position,
|
||||
);
|
||||
}
|
||||
}
|
||||
final saved = resumePositionFor(episode.id);
|
||||
if (saved != null) {
|
||||
await player.controller.seekTo(
|
||||
@@ -667,6 +696,12 @@ class PlayerLogic extends ChangeNotifier {
|
||||
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) {
|
||||
completionProgressSaved = true;
|
||||
unawaited(
|
||||
@@ -750,6 +785,12 @@ class PlayerLogic extends ChangeNotifier {
|
||||
!player.controller.value.isInitialized) {
|
||||
return pendingWrite;
|
||||
}
|
||||
positionCache.save(
|
||||
episodeId: episode.id,
|
||||
dramaId: drama.id,
|
||||
position: player.controller.value.position,
|
||||
duration: player.controller.value.duration,
|
||||
);
|
||||
return queueProgress(
|
||||
drama,
|
||||
episode,
|
||||
@@ -851,6 +892,7 @@ class PlayerLogic extends ChangeNotifier {
|
||||
|
||||
Future<void> openDetail(BuildContext context) async {
|
||||
session.setOverlayOpen(true);
|
||||
await saveCurrentProgress();
|
||||
try {
|
||||
if (!context.mounted) return;
|
||||
await Navigator.of(context).push(
|
||||
@@ -865,6 +907,13 @@ class PlayerLogic extends ChangeNotifier {
|
||||
await saveCurrentProgress();
|
||||
}
|
||||
|
||||
String? _dramaIdForEpisode(String episodeId) {
|
||||
for (final entry in queue.entries) {
|
||||
if (entry.episode.id == episodeId) return entry.drama.id;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> openSeriesAt(
|
||||
BuildContext context,
|
||||
Drama drama,
|
||||
|
||||
@@ -11,6 +11,7 @@ import '../../core/data/repositories/comment_repository.dart';
|
||||
import '../../core/data/services/favorite_controller.dart';
|
||||
import '../../core/data/services/download_controller.dart';
|
||||
import '../../core/data/services/interaction_controller.dart';
|
||||
import '../../core/data/services/playback_position_cache.dart';
|
||||
import '../../core/data/services/playback_session.dart';
|
||||
import '../../core/data/services/video_player_port.dart';
|
||||
import '../../core/data/services/wallet_controller.dart';
|
||||
@@ -119,6 +120,7 @@ class _PlayerPageState extends State<PlayerPage>
|
||||
logic = PlayerBinding().create(
|
||||
history: context.read<WatchHistoryRepository>(),
|
||||
playback: context.read<PlaybackRepository>(),
|
||||
positionCache: context.read<PlaybackPositionCache>(),
|
||||
wallet: widget.wallet,
|
||||
downloads: widget.downloads,
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user