fix(player): 修复切换频道时复用已释放的视频控制器

This commit is contained in:
Zhou Weidong committed 2026-10-21 20:00:15 +08:00
1 parent 45a53180d9
commit f3ec12e3c3
6 files changed
+229 -6

No files matched your search

+115
View File
@@ -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();
}
}