fix(player): 记住拖动后的实际播放位置
This commit is contained in:
1 parent
47ca21406a
commit
5268adc999
8 files changed
+352
-67
No files matched your search
@@ -0,0 +1,21 @@
|
||||
# Seek 后播放位置保持
|
||||
|
||||
## 目标
|
||||
|
||||
- 拖动进度条后,缓存与观看记录立即使用 seek 完成后的真实位置。
|
||||
- 拖动后立即进详情/离开也不会回退到旧进度。
|
||||
- 越界位置 clamp,锁定/票据失败不写无效缓存。
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 增加播放位置 clamp 纯函数,缓存写入统一限制在 `0..duration`。
|
||||
2. VideoPlayerPort 暴露 seek 方法,PlayerLogic 等待 seek 完成后再读取 controller。
|
||||
3. PlayerLogic 统一当前播放快照,监听、缓存和上报共用同一份 position/duration。
|
||||
4. 增加 pending seek 屏障,saveCurrentProgress 与离开路径等待 seek 和上报完成。
|
||||
5. 补 seek 缓存、越界 clamp、seek 后进详情并返回测试。
|
||||
6. 执行 `dart analyze lib test` 与 `flutter test --no-pub`,回填 PROGRESS 后提交合并。
|
||||
|
||||
## 风险 / 待确认
|
||||
|
||||
- 片尾与完成逻辑继续沿用现有 controller/播放器行为。
|
||||
- 缓存仅在进程内,不改变服务端观看记录契约。
|
||||
@@ -359,3 +359,35 @@ $ flutter test --no-pub
|
||||
解读:缓存读写/过期、跨剧隔离、缓存命中去网络、服务端回落、详情 CTA 缓存集
|
||||
和详情往返同秒位置均通过;全量静态分析零告警,589 项测试全过。
|
||||
`pubspec.lock` 已还原,未启停服务,未操作真机。
|
||||
|
||||
## 2026-10-22 Seek 后播放位置保持
|
||||
|
||||
- 已从 `develop`(`47ca214`)创建 `feature/playback-seek-position`。
|
||||
- 新增播放位置 clamp;缓存、上报与详情返回统一使用 seek 完成后的 controller 位置。
|
||||
- PlayerLogic 增加 pending seek 屏障,saveCurrentProgress 与离开路径会等待 seek 和写入完成。
|
||||
- 监听器统一读取一次当前播放快照,避免旧位置覆盖新位置。
|
||||
- 已补 seek 目标缓存、越界 clamp、seek 后立即进详情并返回的位置测试。
|
||||
|
||||
执行命令与原始输出:
|
||||
|
||||
```text
|
||||
$ flutter test --no-pub test/playback/playback_position_cache_test.dart
|
||||
00:00 +5: All tests passed!
|
||||
|
||||
$ flutter test --no-pub test/player/player_logic_test.dart --plain-name should_cache_actual_position_after_seek_completes
|
||||
00:00 +1: All tests passed!
|
||||
|
||||
$ flutter test --no-pub test/player/player_open_detail_test.dart
|
||||
00:00 +1: All tests passed!
|
||||
|
||||
$ dart analyze lib test
|
||||
Analyzing lib, test...
|
||||
No issues found!
|
||||
|
||||
$ flutter test --no-pub
|
||||
00:49 +591: All tests passed!
|
||||
```
|
||||
|
||||
解读:seek 完成后缓存与上报使用实际位置,越界目标被 clamp;seek 后立即
|
||||
进详情并返回仍保持目标位置。全量静态分析零告警,591 项测试全过。
|
||||
`pubspec.lock` 已还原,未启停服务,未操作真机。
|
||||
@@ -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);
|
||||
|
||||
@@ -4,6 +4,42 @@ import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/services/playback_position_cache.dart';
|
||||
|
||||
void main() {
|
||||
test('should_clamp_invalid_or_out_of_range_position_when_caching', () {
|
||||
expect(
|
||||
clampPlaybackPosition(
|
||||
const Duration(seconds: -1),
|
||||
const Duration(seconds: 10),
|
||||
),
|
||||
Duration.zero,
|
||||
);
|
||||
expect(
|
||||
clampPlaybackPosition(
|
||||
const Duration(seconds: 12),
|
||||
const Duration(seconds: 10),
|
||||
),
|
||||
const Duration(seconds: 10),
|
||||
);
|
||||
expect(
|
||||
clampPlaybackPosition(
|
||||
const Duration(seconds: 5),
|
||||
const Duration(seconds: 10),
|
||||
),
|
||||
const Duration(seconds: 5),
|
||||
);
|
||||
|
||||
final cache = PlaybackPositionCache();
|
||||
cache.save(
|
||||
episodeId: 'episode-3',
|
||||
dramaId: 'drama-a',
|
||||
position: const Duration(minutes: 4),
|
||||
duration: const Duration(minutes: 3),
|
||||
);
|
||||
expect(
|
||||
cache.read('episode-3', dramaId: 'drama-a')?.position,
|
||||
const Duration(minutes: 3),
|
||||
);
|
||||
});
|
||||
|
||||
test('should_store_and_read_recent_position_when_drama_matches', () {
|
||||
final now = DateTime(2026, 10, 22, 12);
|
||||
final cache = PlaybackPositionCache(now: () => now);
|
||||
|
||||
@@ -218,6 +218,75 @@ void main() {
|
||||
logic.dispose();
|
||||
await _drain(logic);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'should_cache_actual_position_after_seek_completes',
|
||||
(tester) async {
|
||||
late BuildContext context;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: Builder(
|
||||
builder: (pageContext) {
|
||||
context = pageContext;
|
||||
return const Scaffold(body: SizedBox.expand());
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
final cache = PlaybackPositionCache();
|
||||
final history = FakeWatchHistory();
|
||||
final factory = _VideoRecordingFactory();
|
||||
late PlayerLogic logic;
|
||||
await tester.runAsync(() async {
|
||||
logic = _logic(history, positionCache: cache);
|
||||
logic.attach(
|
||||
PlayerPage(drama: _drama('signal'), initialIndex: 0),
|
||||
isMounted: () => true,
|
||||
);
|
||||
logic.start(
|
||||
factory: factory.create,
|
||||
coordinator: PlaybackCoordinator(),
|
||||
);
|
||||
await _drain(logic);
|
||||
});
|
||||
final player = logic.session.active! as _VideoRecordingPlayer;
|
||||
logic.beginScrub(player);
|
||||
|
||||
await tester.runAsync(
|
||||
() => logic.seekTo(
|
||||
context,
|
||||
player,
|
||||
const Duration(seconds: 42),
|
||||
),
|
||||
);
|
||||
await tester.runAsync(() => logic.pendingWrite);
|
||||
|
||||
expect(
|
||||
cache.read('signal-1', dramaId: 'signal')?.position,
|
||||
const Duration(seconds: 42),
|
||||
);
|
||||
expect(history.writes.last, 'signal-1|42000|false');
|
||||
|
||||
logic.beginScrub(player);
|
||||
await tester.runAsync(
|
||||
() => logic.seekTo(
|
||||
context,
|
||||
player,
|
||||
const Duration(minutes: 2),
|
||||
),
|
||||
);
|
||||
await tester.runAsync(() => logic.pendingWrite);
|
||||
|
||||
expect(player.controller.value.position, const Duration(minutes: 1));
|
||||
expect(
|
||||
cache.read('signal-1', dramaId: 'signal')?.position,
|
||||
const Duration(minutes: 1),
|
||||
);
|
||||
expect(history.writes.last, 'signal-1|60000|false');
|
||||
logic.dispose();
|
||||
await tester.pump();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
test('should_play_local_file_without_ticket_when_episode_is_downloaded',
|
||||
@@ -1257,6 +1326,18 @@ class _VideoRecordingPlayer extends VideoPlayerPort {
|
||||
@override
|
||||
Future<void> pause() async {}
|
||||
|
||||
@override
|
||||
Future<void> seekTo(Duration position) async {
|
||||
final current = controller.value;
|
||||
controller.value = VideoPlayerValue(
|
||||
duration: current.duration,
|
||||
size: current.size,
|
||||
position: position,
|
||||
isInitialized: current.isInitialized,
|
||||
isPlaying: current.isPlaying,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
if (disposed) return;
|
||||
|
||||
@@ -48,7 +48,7 @@ void main() {
|
||||
channel: const MethodChannel('test/player_open_detail/pip'),
|
||||
),
|
||||
);
|
||||
final factory = _RecordingFactory();
|
||||
final factory = _VideoRecordingFactory();
|
||||
logic.attach(
|
||||
PlayerPage(drama: drama, initialIndex: 0),
|
||||
isMounted: () => true,
|
||||
@@ -59,15 +59,8 @@ void main() {
|
||||
final active = logic.session.active;
|
||||
final index = logic.session.currentIndex;
|
||||
final player = factory.created.first;
|
||||
final playsBefore = player.plays;
|
||||
final tracked = VideoPlayerPort(drama.episodes.first);
|
||||
tracked.controller.value = const VideoPlayerValue(
|
||||
duration: Duration(minutes: 5),
|
||||
position: Duration(seconds: 37),
|
||||
isInitialized: true,
|
||||
);
|
||||
logic
|
||||
..trackedPlayer = tracked
|
||||
..trackedPlayer = player
|
||||
..trackedEpisode = drama.episodes.first
|
||||
..trackedDrama = drama;
|
||||
|
||||
@@ -96,22 +89,31 @@ void main() {
|
||||
),
|
||||
);
|
||||
|
||||
logic.beginScrub(player);
|
||||
await logic.seekTo(
|
||||
pageContext,
|
||||
player,
|
||||
const Duration(minutes: 1, seconds: 22),
|
||||
);
|
||||
|
||||
unawaited(logic.openDetail(pageContext));
|
||||
await tester.pump();
|
||||
await tester.pump(const Duration(milliseconds: 400));
|
||||
await _pumpSession(tester);
|
||||
await tester.runAsync(() => logic.session.playbackSettled);
|
||||
|
||||
expect(find.byType(DramaDetailPage), findsOneWidget);
|
||||
expect(logic.session.active, same(active));
|
||||
expect(logic.session.currentIndex, index);
|
||||
expect(logic.session.userWantsPlay, isTrue);
|
||||
expect(player.pauses, 1);
|
||||
expect(player.plays, playsBefore);
|
||||
expect(tracked.controller.value.position, const Duration(seconds: 37));
|
||||
expect(history.writes, ['signal-1|37000|false']);
|
||||
expect(
|
||||
player.controller.value.position,
|
||||
const Duration(minutes: 1, seconds: 22),
|
||||
);
|
||||
expect(history.writes, ['signal-1|82000|false', 'signal-1|82000|false']);
|
||||
expect(
|
||||
positionCache.read('signal-1', dramaId: 'signal')?.position,
|
||||
const Duration(seconds: 37),
|
||||
const Duration(minutes: 1, seconds: 22),
|
||||
);
|
||||
|
||||
await tester.pageBack();
|
||||
@@ -123,11 +125,14 @@ void main() {
|
||||
expect(logic.session.active, same(active));
|
||||
expect(logic.session.currentIndex, index);
|
||||
expect(logic.session.userWantsPlay, isTrue);
|
||||
expect(player.plays, playsBefore + 1);
|
||||
expect(tracked.controller.value.position, const Duration(seconds: 37));
|
||||
expect(
|
||||
player.controller.value.position,
|
||||
const Duration(minutes: 1, seconds: 22),
|
||||
);
|
||||
expect(history.writes, [
|
||||
'signal-1|37000|false',
|
||||
'signal-1|37000|false',
|
||||
'signal-1|82000|false',
|
||||
'signal-1|82000|false',
|
||||
'signal-1|82000|false',
|
||||
]);
|
||||
|
||||
logic
|
||||
@@ -136,7 +141,6 @@ void main() {
|
||||
..trackedDrama = null
|
||||
..dispose();
|
||||
await _pumpSession(tester);
|
||||
await tracked.dispose();
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -164,29 +168,64 @@ Future<void> _pumpSession(WidgetTester tester) async {
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordingFactory {
|
||||
final List<_RecordingPlayer> created = [];
|
||||
class _VideoRecordingFactory {
|
||||
final List<_VideoRecordingPlayer> created = [];
|
||||
|
||||
PlayerPort create(Episode episode) {
|
||||
final player = _RecordingPlayer();
|
||||
final player = _VideoRecordingPlayer(episode);
|
||||
created.add(player);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
class _RecordingPlayer implements PlayerPort {
|
||||
int plays = 0;
|
||||
int pauses = 0;
|
||||
class _VideoRecordingPlayer extends VideoPlayerPort {
|
||||
_VideoRecordingPlayer(super.episode);
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {}
|
||||
Future<void> initialize() async {
|
||||
controller.value = const VideoPlayerValue(
|
||||
duration: Duration(minutes: 5),
|
||||
size: Size(1280, 720),
|
||||
position: Duration(seconds: 1),
|
||||
isInitialized: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async => plays++;
|
||||
Future<void> play() async {
|
||||
_setPlaying(true);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> pause() async => pauses++;
|
||||
Future<void> pause() async {
|
||||
_setPlaying(false);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {}
|
||||
Future<void> seekTo(Duration position) async {
|
||||
final current = controller.value;
|
||||
controller.value = VideoPlayerValue(
|
||||
duration: current.duration,
|
||||
size: current.size,
|
||||
position: position,
|
||||
isInitialized: current.isInitialized,
|
||||
isPlaying: current.isPlaying,
|
||||
);
|
||||
}
|
||||
|
||||
void _setPlaying(bool playing) {
|
||||
final current = controller.value;
|
||||
controller.value = VideoPlayerValue(
|
||||
duration: current.duration,
|
||||
size: current.size,
|
||||
position: current.position,
|
||||
isInitialized: current.isInitialized,
|
||||
isPlaying: playing,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
await controller.dispose();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user