fix(player): 优化封面过渡与换集起播
This commit is contained in:
1 parent
516593499c
commit
04f034817c
6 files changed
+65
-19
No files matched your search
@@ -41,7 +41,6 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
final Future<void> Function()? waitForFrame;
|
final Future<void> Function()? waitForFrame;
|
||||||
final PlaybackCoordinator _coordinator;
|
final PlaybackCoordinator _coordinator;
|
||||||
final Set<_PlayerSlot> _live = {};
|
final Set<_PlayerSlot> _live = {};
|
||||||
final Set<String> _pausedEpisodeIds = {};
|
|
||||||
_PlayerSlot? _active;
|
_PlayerSlot? _active;
|
||||||
_PlayerSlot? _next;
|
_PlayerSlot? _next;
|
||||||
_PlayerSlot? _previous;
|
_PlayerSlot? _previous;
|
||||||
@@ -207,9 +206,9 @@ class PlaybackSession extends ChangeNotifier {
|
|||||||
int? previousIndex,
|
int? previousIndex,
|
||||||
}) async {
|
}) async {
|
||||||
if (_closed || index < 0 || index >= episodes.length) return;
|
if (_closed || index < 0 || index >= episodes.length) return;
|
||||||
// 新集默认起播;用户手动暂停过的同一集滑回时继续尊重暂停。
|
// 切换到不同剧集时恢复播放;仍停留在同一集时保留用户暂停。
|
||||||
if (!_selected || index != _currentIndex) {
|
if (!_selected || index != _currentIndex) {
|
||||||
_userWantsPlay = !_pausedEpisodeIds.contains(episodes[index].id);
|
_userWantsPlay = true;
|
||||||
}
|
}
|
||||||
final request = ++_generation;
|
final request = ++_generation;
|
||||||
_selected = true;
|
_selected = true;
|
||||||
|
|||||||
@@ -37,16 +37,8 @@ extension PlaybackSessionControls on PlaybackSession {
|
|||||||
_syncPlayback();
|
_syncPlayback();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 记录用户播放意图;手动暂停的剧集滑回时不再自动起播。
|
/// 记录用户播放意图;切换剧集时会在 [PlaybackSession.select] 中恢复播放。
|
||||||
void setUserWantsPlay(bool value) {
|
void setUserWantsPlay(bool value) {
|
||||||
if (episodes.isNotEmpty) {
|
|
||||||
final episodeId = episodes[_currentIndex].id;
|
|
||||||
if (value) {
|
|
||||||
_pausedEpisodeIds.remove(episodeId);
|
|
||||||
} else {
|
|
||||||
_pausedEpisodeIds.add(episodeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_userWantsPlay = value;
|
_userWantsPlay = value;
|
||||||
_syncPlayback();
|
_syncPlayback();
|
||||||
_notify();
|
_notify();
|
||||||
|
|||||||
@@ -64,9 +64,12 @@ class _PlayerVideoSurfaceState extends State<PlayerVideoSurface> {
|
|||||||
key: Key('player-fallback-background'),
|
key: Key('player-fallback-background'),
|
||||||
color: AppColors.mediaBlack,
|
color: AppColors.mediaBlack,
|
||||||
),
|
),
|
||||||
if (!reveal && widget.openingFrame != null)
|
if (widget.openingFrame != null)
|
||||||
KeyedSubtree(
|
AnimatedOpacity(
|
||||||
key: const Key('player-opening-frame'),
|
key: const Key('player-opening-frame'),
|
||||||
|
opacity: reveal ? 0 : 1,
|
||||||
|
duration: handoff != null ? Duration.zero : _videoFadeDuration,
|
||||||
|
curve: Curves.easeOut,
|
||||||
child: widget.openingFrame!,
|
child: widget.openingFrame!,
|
||||||
),
|
),
|
||||||
AnimatedOpacity(
|
AnimatedOpacity(
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ void main() {
|
|||||||
await session.close();
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should_keep_manual_pause_when_returning_to_same_episode', () async {
|
test('should_auto_play_previous_after_switching_from_paused_episode', () async {
|
||||||
final factory = _Factory();
|
final factory = _Factory();
|
||||||
final session = PlaybackSession(
|
final session = PlaybackSession(
|
||||||
episodes: episodes,
|
episodes: episodes,
|
||||||
@@ -195,13 +195,16 @@ void main() {
|
|||||||
factory.players.firstWhere((player) => player.id == 'ep-2').playing,
|
factory.players.firstWhere((player) => player.id == 'ep-2').playing,
|
||||||
isTrue,
|
isTrue,
|
||||||
);
|
);
|
||||||
|
session.setUserWantsPlay(false);
|
||||||
|
await session.playbackSettled;
|
||||||
|
expect(session.userWantsPlay, isFalse);
|
||||||
|
|
||||||
await session.select(1, preloadIndex: 2, previousIndex: 0);
|
await session.select(1, preloadIndex: 2, previousIndex: 0);
|
||||||
await session.preloadSettled;
|
await session.preloadSettled;
|
||||||
expect(session.userWantsPlay, isFalse);
|
expect(session.userWantsPlay, isTrue);
|
||||||
expect(
|
expect(
|
||||||
factory.players.firstWhere((player) => player.id == 'ep-1').playing,
|
factory.players.firstWhere((player) => player.id == 'ep-1').playing,
|
||||||
isFalse,
|
isTrue,
|
||||||
);
|
);
|
||||||
await session.close();
|
await session.close();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -83,6 +83,34 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byKey(const Key('player-opening-poster')), findsOneWidget);
|
expect(find.byKey(const Key('player-opening-poster')), findsOneWidget);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_reuse_decoded_sprite_when_parent_rebuilds', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final loader = _FakeSpriteLoader();
|
||||||
|
final cache = OpeningFrameCache();
|
||||||
|
addTearDown(cache.clear);
|
||||||
|
Widget build() => MaterialApp(
|
||||||
|
home: PlayerOpeningFrame(
|
||||||
|
thumbnailsUrl: 'https://cdn.test/thumbnails.vtt',
|
||||||
|
posterUrl: 'https://cdn.test/poster.jpg',
|
||||||
|
loader: loader,
|
||||||
|
cache: cache,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(build());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(loader.vttCalls, 1);
|
||||||
|
expect(loader.spriteCalls, hasLength(1));
|
||||||
|
|
||||||
|
await tester.pumpWidget(build());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(loader.vttCalls, 1);
|
||||||
|
expect(loader.spriteCalls, hasLength(1));
|
||||||
|
expect(find.byKey(const Key('player-opening-sprite')), findsOneWidget);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FakeSpriteLoader implements SpriteThumbnailAssetLoader {
|
class _FakeSpriteLoader implements SpriteThumbnailAssetLoader {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
||||||
expect(find.byKey(const Key('fake-opening')), findsOneWidget);
|
expect(find.byKey(const Key('fake-opening')), findsOneWidget);
|
||||||
|
expect(_openingOpacity(tester), 1);
|
||||||
expect(_opacity(tester), 0);
|
expect(_opacity(tester), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,7 +49,7 @@ void main() {
|
|||||||
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_fade_video_in_within_200ms_after_first_frame', (
|
testWidgets('should_cross_fade_video_over_opening_frame', (
|
||||||
tester,
|
tester,
|
||||||
) async {
|
) async {
|
||||||
final controller = await _pumpSurface(
|
final controller = await _pumpSurface(
|
||||||
@@ -67,7 +68,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(_opacity(tester), 1);
|
expect(_opacity(tester), 1);
|
||||||
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
||||||
expect(
|
expect(
|
||||||
tester
|
tester
|
||||||
.widget<AnimatedOpacity>(
|
.widget<AnimatedOpacity>(
|
||||||
@@ -80,8 +81,12 @@ void main() {
|
|||||||
final halfway = _fadeValue(tester);
|
final halfway = _fadeValue(tester);
|
||||||
expect(halfway, greaterThan(0));
|
expect(halfway, greaterThan(0));
|
||||||
expect(halfway, lessThan(1));
|
expect(halfway, lessThan(1));
|
||||||
|
final openingHalfway = _openingFadeValue(tester);
|
||||||
|
expect(openingHalfway, greaterThan(0));
|
||||||
|
expect(openingHalfway, lessThan(1));
|
||||||
await tester.pump(const Duration(milliseconds: 100));
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
expect(_fadeValue(tester), 1);
|
expect(_fadeValue(tester), 1);
|
||||||
|
expect(_openingFadeValue(tester), 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('should_show_video_error_above_retained_fallback', (
|
testWidgets('should_show_video_error_above_retained_fallback', (
|
||||||
@@ -182,6 +187,14 @@ double _opacity(WidgetTester tester) {
|
|||||||
.opacity;
|
.opacity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _openingOpacity(WidgetTester tester) {
|
||||||
|
return tester
|
||||||
|
.widget<AnimatedOpacity>(
|
||||||
|
find.byKey(const Key('player-opening-frame')),
|
||||||
|
)
|
||||||
|
.opacity;
|
||||||
|
}
|
||||||
|
|
||||||
double _fadeValue(WidgetTester tester) {
|
double _fadeValue(WidgetTester tester) {
|
||||||
final fade = find.descendant(
|
final fade = find.descendant(
|
||||||
of: find.byKey(const Key('player-video-opacity')),
|
of: find.byKey(const Key('player-video-opacity')),
|
||||||
@@ -189,3 +202,11 @@ double _fadeValue(WidgetTester tester) {
|
|||||||
);
|
);
|
||||||
return tester.widget<FadeTransition>(fade).opacity.value;
|
return tester.widget<FadeTransition>(fade).opacity.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
double _openingFadeValue(WidgetTester tester) {
|
||||||
|
final fade = find.descendant(
|
||||||
|
of: find.byKey(const Key('player-opening-frame')),
|
||||||
|
matching: find.byType(FadeTransition),
|
||||||
|
);
|
||||||
|
return tester.widget<FadeTransition>(fade).opacity.value;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user