fix(player): 修复切换频道时复用已释放的视频控制器
This commit is contained in:
1 parent
45a53180d9
commit
f3ec12e3c3
6 files changed
+229
-6
No files matched your search
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:video_player/video_player.dart';
|
||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||
import 'package:fruitshorts/core/data/models/play_ticket.dart';
|
||||
import 'package:fruitshorts/core/data/models/player_port.dart';
|
||||
@@ -11,8 +12,10 @@ import 'package:fruitshorts/core/data/services/android_picture_in_picture.dart';
|
||||
import 'package:fruitshorts/core/data/services/download_controller.dart';
|
||||
import 'package:fruitshorts/core/data/services/download_storage.dart';
|
||||
import 'package:fruitshorts/core/data/services/playback_session.dart';
|
||||
import 'package:fruitshorts/core/data/services/video_player_port.dart';
|
||||
import 'package:fruitshorts/pages/player/player_logic.dart';
|
||||
import 'package:fruitshorts/pages/player/player_view.dart';
|
||||
import 'package:fruitshorts/pages/player/widgets/player_video_surface.dart';
|
||||
|
||||
import '../support/fake_watch_history.dart';
|
||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||
@@ -45,10 +48,12 @@ AndroidPictureInPicture _pip() =>
|
||||
PlayerLogic _logic(
|
||||
FakeWatchHistory history, {
|
||||
PlaybackRepository? playback,
|
||||
Future<void> Function()? waitForFrame,
|
||||
}) => PlayerLogic(
|
||||
history: history,
|
||||
playback: playback ?? FakePlaybackRepository(),
|
||||
pictureInPicture: _pip(),
|
||||
waitForFrame: waitForFrame ?? () async {},
|
||||
);
|
||||
|
||||
/// Drains the session's serialized work so a test never ends mid-disposal.
|
||||
@@ -681,6 +686,49 @@ void main() {
|
||||
logic.dispose();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'should_unmount_old_controller_before_disposing_it_when_feed_changes',
|
||||
(tester) async {
|
||||
final factory = _VideoRecordingFactory();
|
||||
final initial = [_drama('old')];
|
||||
final logic = _logic(FakeWatchHistory());
|
||||
logic.attach(
|
||||
PlayerPage(
|
||||
drama: initial.first,
|
||||
initialIndex: 0,
|
||||
homeMode: true,
|
||||
feedDramas: initial,
|
||||
),
|
||||
isMounted: () => true,
|
||||
);
|
||||
await tester.runAsync(() async {
|
||||
logic.start(
|
||||
factory: factory.create,
|
||||
coordinator: PlaybackCoordinator(),
|
||||
);
|
||||
await _drain(logic);
|
||||
});
|
||||
final oldPlayer = factory.created.single;
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(home: _SessionVideoSurface(logic: logic)),
|
||||
);
|
||||
await tester.pump();
|
||||
expect(find.byType(VideoPlayer), findsOneWidget);
|
||||
|
||||
logic.updateFeed([_drama('new')], resetToFirst: true);
|
||||
for (var frame = 0; frame < 5; frame++) {
|
||||
await tester.pump();
|
||||
await tester.runAsync(pumpEventQueue);
|
||||
}
|
||||
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(oldPlayer.disposed, isTrue);
|
||||
expect(factory.created.length, greaterThanOrEqualTo(2));
|
||||
logic.dispose();
|
||||
await tester.pump();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
group('lifecycle', () {
|
||||
@@ -889,3 +937,70 @@ class _RecordingPlayer implements PlayerPort {
|
||||
onDispose();
|
||||
}
|
||||
}
|
||||
|
||||
class _SessionVideoSurface extends StatelessWidget {
|
||||
const _SessionVideoSurface({required this.logic});
|
||||
|
||||
final PlayerLogic logic;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ListenableBuilder(
|
||||
listenable: logic,
|
||||
builder: (context, _) => AnimatedBuilder(
|
||||
animation: logic.session,
|
||||
builder: (context, _) {
|
||||
final port = logic.session.active;
|
||||
final video = port is VideoPlayerPort ? port : null;
|
||||
return Scaffold(
|
||||
body: PlayerVideoSurface(
|
||||
controller: video?.controller,
|
||||
videoUrl: video?.controller.dataSource ?? '',
|
||||
theater: false,
|
||||
onTap: () {},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoRecordingFactory {
|
||||
final List<_VideoRecordingPlayer> created = [];
|
||||
|
||||
PlayerPort create(Episode episode) {
|
||||
final player = _VideoRecordingPlayer(episode);
|
||||
created.add(player);
|
||||
return player;
|
||||
}
|
||||
}
|
||||
|
||||
class _VideoRecordingPlayer extends VideoPlayerPort {
|
||||
_VideoRecordingPlayer(super.episode);
|
||||
|
||||
bool disposed = false;
|
||||
|
||||
@override
|
||||
Future<void> initialize() async {
|
||||
controller.value = const VideoPlayerValue(
|
||||
duration: Duration(minutes: 1),
|
||||
size: Size(1280, 720),
|
||||
position: Duration(seconds: 1),
|
||||
isInitialized: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> play() async {}
|
||||
|
||||
@override
|
||||
Future<void> pause() async {}
|
||||
|
||||
@override
|
||||
Future<void> dispose() async {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
await controller.dispose();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user