fix(player): 优化上下滑切集黑屏
This commit is contained in:
1 parent
b83b5799e3
commit
222bacdeeb
10 files changed
+625
-122
No files matched your search
@@ -91,20 +91,63 @@ void main() {
|
||||
expect(session.preloadedIndex, 1);
|
||||
final loadingStates = <bool>[];
|
||||
session.addListener(() => loadingStates.add(session.loading));
|
||||
await session.select(1, preloadIndex: 2);
|
||||
await session.select(1, preloadIndex: 2, previousIndex: 0);
|
||||
await session.preloadSettled;
|
||||
expect(session.active, same(warm));
|
||||
expect(warm.initializeCalls, 1);
|
||||
expect(warm.playCalls, 1);
|
||||
expect(loadingStates, isNot(contains(true)));
|
||||
expect(activated, ['ep-0', 'ep-1']);
|
||||
expect(factory.peakLive, 2);
|
||||
expect(session.preparedPlayerAt(0), isNotNull);
|
||||
expect(session.preparedPlayerAt(2), isNotNull);
|
||||
expect(factory.peakLive, lessThanOrEqualTo(3));
|
||||
expect(factory.peakPlaying, 1);
|
||||
await session.close();
|
||||
expect(factory.live, 0);
|
||||
},
|
||||
);
|
||||
|
||||
test('should_prepare_previous_neighbor_when_selected', () async {
|
||||
final factory = _Factory();
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
factory: factory.create,
|
||||
canPlay: (_) => true,
|
||||
);
|
||||
await session.select(1, preloadIndex: 2, previousIndex: 0);
|
||||
await session.preloadSettled;
|
||||
expect(session.preparedPlayerAt(0), isNotNull);
|
||||
expect(session.preparedPlayerAt(2), isNotNull);
|
||||
expect(factory.players.map((player) => player.id).toSet(), {
|
||||
'ep-0',
|
||||
'ep-1',
|
||||
'ep-2',
|
||||
});
|
||||
expect(factory.peakLive, 3);
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('should_warm_previous_without_waiting_for_slow_next', () async {
|
||||
final nextGate = Completer<void>();
|
||||
final factory = _Factory(gates: {'ep-2': nextGate});
|
||||
final session = PlaybackSession(
|
||||
episodes: episodes,
|
||||
factory: factory.create,
|
||||
canPlay: (_) => true,
|
||||
);
|
||||
|
||||
await session.select(1, preloadIndex: 2, previousIndex: 0);
|
||||
await _tick();
|
||||
|
||||
expect(session.preparedPlayerAt(0), isNotNull);
|
||||
expect(session.preparedPlayerAt(2), isNull);
|
||||
|
||||
nextGate.complete();
|
||||
await session.preloadSettled;
|
||||
expect(session.preparedPlayerAt(2), isNotNull);
|
||||
await session.close();
|
||||
});
|
||||
|
||||
test('should_reuse_in_flight_preload_when_swiped_before_ready', () async {
|
||||
final gate = Completer<void>();
|
||||
final factory = _Factory(gates: {'ep-1': gate});
|
||||
@@ -124,7 +167,7 @@ void main() {
|
||||
await session.preloadSettled;
|
||||
expect(session.active, same(warm));
|
||||
expect(warm.initializeCalls, 1);
|
||||
expect(factory.peakLive, lessThanOrEqualTo(2));
|
||||
expect(factory.peakLive, lessThanOrEqualTo(3));
|
||||
expect(factory.peakPlaying, 1);
|
||||
await session.close();
|
||||
});
|
||||
@@ -149,7 +192,7 @@ void main() {
|
||||
expect(session.preloadedIndex, 2);
|
||||
expect(stale.playCalls, 0);
|
||||
expect(stale.disposed, isTrue);
|
||||
expect(factory.peakLive, lessThanOrEqualTo(2));
|
||||
expect(factory.peakLive, lessThanOrEqualTo(3));
|
||||
expect(factory.peakPlaying, 1);
|
||||
await session.close();
|
||||
});
|
||||
|
||||
@@ -594,8 +594,9 @@ void main() {
|
||||
await _drain(logic);
|
||||
|
||||
expect(logic.session.currentIndex, 2);
|
||||
// The last episode has no successor to warm.
|
||||
expect(factory.created, hasLength(1));
|
||||
// 最后一集没有下一集,但仍会预热上一集。
|
||||
expect(factory.created, hasLength(2));
|
||||
expect(logic.session.preparedPlayerAt(1), isNotNull);
|
||||
|
||||
logic.dispose();
|
||||
await _drain(logic);
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Author: fruit.com.cn
|
||||
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:fruitshorts/core/data/services/sprite_thumbnail_asset_loader.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/player_opening_frame.dart';
|
||||
|
||||
const String _spriteVtt = '''
|
||||
WEBVTT
|
||||
|
||||
00:00:00.000 --> 00:00:01.000
|
||||
sprite_001.jpg#xywh=0,0,160,90
|
||||
''';
|
||||
|
||||
void main() {
|
||||
test('should_return_no_opening_frame_when_thumbnails_url_is_empty', () {
|
||||
expect(playerOpeningFrame(null), isNull);
|
||||
expect(playerOpeningFrame(' '), isNull);
|
||||
});
|
||||
|
||||
testWidgets('should_load_and_reuse_first_sprite_frame', (tester) async {
|
||||
final loader = _FakeSpriteLoader();
|
||||
final cache = OpeningFrameCache();
|
||||
addTearDown(cache.clear);
|
||||
|
||||
final first = await cache.load(
|
||||
'https://cdn.test/thumbnails.vtt',
|
||||
loader: loader,
|
||||
);
|
||||
final second = await cache.load(
|
||||
'https://cdn.test/thumbnails.vtt',
|
||||
loader: loader,
|
||||
);
|
||||
|
||||
expect(first, isNotNull);
|
||||
expect(second, same(first));
|
||||
expect(loader.vttCalls, 1);
|
||||
expect(loader.spriteCalls, ['sprite_001.jpg']);
|
||||
});
|
||||
|
||||
testWidgets('should_render_decoded_opening_frame', (tester) async {
|
||||
final loader = _FakeSpriteLoader();
|
||||
final cache = OpeningFrameCache();
|
||||
addTearDown(cache.clear);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: PlayerOpeningFrame(
|
||||
thumbnailsUrl: 'https://cdn.test/thumbnails.vtt',
|
||||
loader: loader,
|
||||
cache: cache,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byKey(const Key('player-opening-sprite')), findsOneWidget);
|
||||
});
|
||||
}
|
||||
|
||||
class _FakeSpriteLoader implements SpriteThumbnailAssetLoader {
|
||||
int vttCalls = 0;
|
||||
final List<String> spriteCalls = [];
|
||||
|
||||
@override
|
||||
Future<String?> loadVtt(Uri uri) async {
|
||||
vttCalls++;
|
||||
return _spriteVtt;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<SpriteThumbnailDecodedImage?> loadSprite(Uri uri) async {
|
||||
spriteCalls.add(uri.pathSegments.last);
|
||||
return SpriteThumbnailDecodedImage(
|
||||
image: await _createImage(320, 180),
|
||||
originalWidth: 320,
|
||||
originalHeight: 180,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<ui.Image> _createImage(int width, int height) async {
|
||||
final recorder = ui.PictureRecorder();
|
||||
final canvas = Canvas(recorder);
|
||||
canvas.drawRect(
|
||||
Rect.fromLTWH(0, 0, width.toDouble(), height.toDouble()),
|
||||
Paint()..color = Colors.blue,
|
||||
);
|
||||
final picture = recorder.endRecording();
|
||||
final image = await picture.toImage(width, height);
|
||||
picture.dispose();
|
||||
return image;
|
||||
}
|
||||
@@ -13,16 +13,48 @@ void main() {
|
||||
await _pumpSurface(tester);
|
||||
|
||||
expect(_fallbackColor(tester), Colors.black);
|
||||
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
||||
expect(find.byKey(const Key('player-fallback-scrim')), findsNothing);
|
||||
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
||||
expect(_opacity(tester), 0);
|
||||
expect(find.byKey(const Key('player-debug-video')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_show_opening_frame_before_decode_is_ready', (
|
||||
tester,
|
||||
) async {
|
||||
await _pumpSurface(
|
||||
tester,
|
||||
openingFrame: const ColoredBox(
|
||||
key: Key('fake-opening'),
|
||||
color: Colors.red,
|
||||
),
|
||||
);
|
||||
|
||||
expect(find.byKey(const Key('player-opening-frame')), findsOneWidget);
|
||||
expect(find.byKey(const Key('fake-opening')), findsOneWidget);
|
||||
expect(_opacity(tester), 0);
|
||||
});
|
||||
|
||||
testWidgets('should_reveal_paused_frame_when_initialized', (tester) async {
|
||||
final controller = await _pumpSurface(tester);
|
||||
controller.value = const VideoPlayerValue(
|
||||
duration: Duration(minutes: 1),
|
||||
size: Size(1280, 720),
|
||||
position: Duration.zero,
|
||||
isInitialized: true,
|
||||
isPlaying: false,
|
||||
);
|
||||
await tester.pump();
|
||||
|
||||
expect(_opacity(tester), 1);
|
||||
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_fade_video_in_within_200ms_after_first_frame', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = await _pumpSurface(tester);
|
||||
final controller = await _pumpSurface(
|
||||
tester,
|
||||
openingFrame: const ColoredBox(color: Colors.red),
|
||||
);
|
||||
expect(_opacity(tester), 0);
|
||||
|
||||
controller.value = const VideoPlayerValue(
|
||||
@@ -35,6 +67,7 @@ void main() {
|
||||
await tester.pump();
|
||||
|
||||
expect(_opacity(tester), 1);
|
||||
expect(find.byKey(const Key('player-opening-frame')), findsNothing);
|
||||
expect(
|
||||
tester
|
||||
.widget<AnimatedOpacity>(
|
||||
@@ -49,9 +82,6 @@ void main() {
|
||||
expect(halfway, lessThan(1));
|
||||
await tester.pump(const Duration(milliseconds: 100));
|
||||
expect(_fadeValue(tester), 1);
|
||||
expect(_fallbackColor(tester), Colors.black);
|
||||
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
||||
expect(find.byKey(const Key('player-debug-video')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_show_video_error_above_retained_fallback', (
|
||||
@@ -65,9 +95,7 @@ void main() {
|
||||
expect(find.byKey(const Key('player-video-error')), findsOneWidget);
|
||||
expect(find.text('No route to host'), findsOneWidget);
|
||||
expect(_fallbackColor(tester), Colors.black);
|
||||
expect(find.byKey(const Key('player-fallback-cover')), findsNothing);
|
||||
expect(_opacity(tester), 0);
|
||||
expect(find.byKey(const Key('player-debug-video')), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('should_cover_new_video_only_while_handoff_is_held', (
|
||||
@@ -119,6 +147,7 @@ void main() {
|
||||
Future<VideoPlayerController> _pumpSurface(
|
||||
WidgetTester tester, {
|
||||
VideoPlayerValue? value,
|
||||
Widget? openingFrame,
|
||||
}) async {
|
||||
final controller = VideoPlayerController.networkUrl(
|
||||
Uri.parse('http://192.168.31.187:8081/files/video.mp4'),
|
||||
@@ -132,6 +161,7 @@ Future<VideoPlayerController> _pumpSurface(
|
||||
controller: controller,
|
||||
videoUrl: controller.dataSource,
|
||||
theater: false,
|
||||
openingFrame: openingFrame,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in new issue
Block a user