fix(player): 路由覆盖时暂停播放
This commit is contained in:
1 parent
911f13f464
commit
699fd427b8
5 files changed
+141
-2
No files matched your search
@@ -44,6 +44,7 @@ import 'core/data/services/locale_controller.dart';
|
|||||||
import 'core/data/services/playback_session.dart';
|
import 'core/data/services/playback_session.dart';
|
||||||
import 'core/data/services/video_player_port.dart';
|
import 'core/data/services/video_player_port.dart';
|
||||||
import 'core/data/services/wallet_controller.dart';
|
import 'core/data/services/wallet_controller.dart';
|
||||||
|
import 'core/navigation/app_route_observer.dart';
|
||||||
import 'core/platform/haptics.dart';
|
import 'core/platform/haptics.dart';
|
||||||
import 'core/theme/app_theme.dart';
|
import 'core/theme/app_theme.dart';
|
||||||
import 'core/theme/app_colors.dart';
|
import 'core/theme/app_colors.dart';
|
||||||
@@ -266,6 +267,7 @@ class _LumaDramaAppState extends State<LumaDramaApp> {
|
|||||||
onGenerateTitle: (context) => AppLocalizations.of(context)!.appName,
|
onGenerateTitle: (context) => AppLocalizations.of(context)!.appName,
|
||||||
supportedLocales: AppLocalizations.supportedLocales,
|
supportedLocales: AppLocalizations.supportedLocales,
|
||||||
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
localizationsDelegates: AppLocalizations.localizationsDelegates,
|
||||||
|
navigatorObservers: [appRouteObserver],
|
||||||
localeResolutionCallback: (deviceLocale, _) =>
|
localeResolutionCallback: (deviceLocale, _) =>
|
||||||
resolveAppLocale(deviceLocale),
|
resolveAppLocale(deviceLocale),
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
/// 全局页面路由观察者:供播放器在路由被覆盖时暂停。
|
||||||
|
final RouteObserver<PageRoute<dynamic>> appRouteObserver =
|
||||||
|
RouteObserver<PageRoute<dynamic>>();
|
||||||
@@ -289,6 +289,20 @@ class PlayerLogic extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 当前播放器页面被其它 PageRoute 覆盖时暂停并保存进度。
|
||||||
|
void onRouteCovered() {
|
||||||
|
if (!_started) return;
|
||||||
|
session.setRouteVisible(false);
|
||||||
|
cancelScrub();
|
||||||
|
unawaited(saveCurrentProgress());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 覆盖播放器的 PageRoute 返回后恢复当前 Tab 的播放状态。
|
||||||
|
void onRouteRevealed() {
|
||||||
|
if (!_started || !isMounted()) return;
|
||||||
|
session.setRouteVisible(active);
|
||||||
|
}
|
||||||
|
|
||||||
void onPageUpdated(PlayerPage previous) {
|
void onPageUpdated(PlayerPage previous) {
|
||||||
if (previous.active != active) {
|
if (previous.active != active) {
|
||||||
session.setRouteVisible(active && visibleIndices.isNotEmpty);
|
session.setRouteVisible(active && visibleIndices.isNotEmpty);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import '../../core/data/services/playback_session.dart';
|
|||||||
import '../../core/data/services/video_player_port.dart';
|
import '../../core/data/services/video_player_port.dart';
|
||||||
import '../../core/data/services/wallet_controller.dart';
|
import '../../core/data/services/wallet_controller.dart';
|
||||||
import '../../core/formatting/error_text.dart';
|
import '../../core/formatting/error_text.dart';
|
||||||
|
import '../../core/navigation/app_route_observer.dart';
|
||||||
import '../../core/platform/haptics.dart';
|
import '../../core/platform/haptics.dart';
|
||||||
import '../../core/theme/app_colors.dart';
|
import '../../core/theme/app_colors.dart';
|
||||||
import '../../core/theme/app_theme.dart';
|
import '../../core/theme/app_theme.dart';
|
||||||
@@ -86,8 +87,10 @@ class PlayerPage extends StatefulWidget {
|
|||||||
State<PlayerPage> createState() => _PlayerPageState();
|
State<PlayerPage> createState() => _PlayerPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
class _PlayerPageState extends State<PlayerPage>
|
||||||
|
with WidgetsBindingObserver, RouteAware {
|
||||||
late final PlayerLogic logic;
|
late final PlayerLogic logic;
|
||||||
|
PageRoute<dynamic>? _subscribedRoute;
|
||||||
bool _chromeVisible = true;
|
bool _chromeVisible = true;
|
||||||
double _playbackSpeed = 1.0;
|
double _playbackSpeed = 1.0;
|
||||||
bool _boosting = false;
|
bool _boosting = false;
|
||||||
@@ -121,14 +124,35 @@ class _PlayerPageState extends State<PlayerPage> with WidgetsBindingObserver {
|
|||||||
logic.onPageUpdated(oldWidget);
|
logic.onPageUpdated(oldWidget);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didChangeDependencies() {
|
||||||
|
super.didChangeDependencies();
|
||||||
|
final route = ModalRoute.of(context);
|
||||||
|
if (route is! PageRoute<dynamic> || route == _subscribedRoute) return;
|
||||||
|
if (_subscribedRoute != null) appRouteObserver.unsubscribe(this);
|
||||||
|
_subscribedRoute = route;
|
||||||
|
appRouteObserver.subscribe(this, route);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||||
logic.onLifecycle(state);
|
logic.onLifecycle(state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPushNext() {
|
||||||
|
logic.onRouteCovered();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didPopNext() {
|
||||||
|
logic.onRouteRevealed();
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
|
appRouteObserver.unsubscribe(this);
|
||||||
logic.dispose();
|
logic.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
// Author: fruit.com.cn
|
// Author: fruit.com.cn
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
@@ -10,11 +12,14 @@ import 'package:fruitshorts/app.dart';
|
|||||||
import 'package:fruitshorts/core/data/models/drama.dart';
|
import 'package:fruitshorts/core/data/models/drama.dart';
|
||||||
import 'package:fruitshorts/core/data/models/interaction.dart';
|
import 'package:fruitshorts/core/data/models/interaction.dart';
|
||||||
import 'package:fruitshorts/core/data/models/play_ticket.dart';
|
import 'package:fruitshorts/core/data/models/play_ticket.dart';
|
||||||
|
import 'package:fruitshorts/core/data/models/player_port.dart';
|
||||||
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
import 'package:fruitshorts/core/data/repositories/favorite_repository.dart';
|
||||||
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
import 'package:fruitshorts/core/data/repositories/playback_repository.dart';
|
||||||
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
import 'package:fruitshorts/core/data/repositories/watch_history_repository.dart';
|
||||||
|
import 'package:fruitshorts/core/data/services/playback_session.dart';
|
||||||
import 'package:fruitshorts/core/theme/app_colors.dart';
|
import 'package:fruitshorts/core/theme/app_colors.dart';
|
||||||
import 'package:fruitshorts/core/widgets/remote_image.dart';
|
import 'package:fruitshorts/core/widgets/remote_image.dart';
|
||||||
|
import 'package:fruitshorts/pages/auth/auth_view.dart';
|
||||||
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
|
import 'package:fruitshorts/pages/player/widgets/player_center_toggle.dart';
|
||||||
import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart';
|
import 'package:fruitshorts/pages/player/widgets/seek_progress_bar.dart';
|
||||||
|
|
||||||
@@ -61,6 +66,72 @@ void main() {
|
|||||||
_expectPlayerNavigationBar(tester);
|
_expectPlayerNavigationBar(tester);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_pause_and_resume_player_when_page_route_covers_it', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final players = <_RecordingPlayer>[];
|
||||||
|
await _pumpApp(
|
||||||
|
tester,
|
||||||
|
playerFactory: (_) {
|
||||||
|
final player = _RecordingPlayer();
|
||||||
|
players.add(player);
|
||||||
|
return player;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
await _openSeriesPlayer(tester);
|
||||||
|
final player = players.firstWhere((item) => item.playing);
|
||||||
|
expect(player.playing, isTrue);
|
||||||
|
|
||||||
|
final navigator = tester.state<NavigatorState>(
|
||||||
|
find.byType(Navigator).first,
|
||||||
|
);
|
||||||
|
unawaited(
|
||||||
|
navigator.push<void>(
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (_) => const Scaffold(key: Key('test-covering-route')),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('test-covering-route')), findsOneWidget);
|
||||||
|
expect(player.playing, isFalse);
|
||||||
|
|
||||||
|
navigator.pop();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(player.playing, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('should_pause_feed_when_auth_page_is_pushed_and_resume_on_pop', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final players = <_RecordingPlayer>[];
|
||||||
|
await _pumpApp(
|
||||||
|
tester,
|
||||||
|
playerFactory: (_) {
|
||||||
|
final player = _RecordingPlayer();
|
||||||
|
players.add(player);
|
||||||
|
return player;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final player = players.firstWhere((item) => item.playing);
|
||||||
|
expect(player.playing, isTrue);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('rail-save')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('Sign In'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(AuthPage), findsOneWidget);
|
||||||
|
expect(player.playing, isFalse);
|
||||||
|
|
||||||
|
await tester.pageBack();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(player.playing, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_fall_back_to_black_when_poster_is_empty', (tester) async {
|
testWidgets('should_fall_back_to_black_when_poster_is_empty', (tester) async {
|
||||||
await _pumpApp(
|
await _pumpApp(
|
||||||
tester,
|
tester,
|
||||||
@@ -1071,6 +1142,7 @@ Future<void> _pumpApp(
|
|||||||
FakeCatalogRepository? catalog,
|
FakeCatalogRepository? catalog,
|
||||||
FakePlaybackRepository? playback,
|
FakePlaybackRepository? playback,
|
||||||
FakeInteractionRepository? interactions,
|
FakeInteractionRepository? interactions,
|
||||||
|
PlayerFactory? playerFactory,
|
||||||
}) async {
|
}) async {
|
||||||
tester.view.physicalSize = size;
|
tester.view.physicalSize = size;
|
||||||
tester.view.devicePixelRatio = 1;
|
tester.view.devicePixelRatio = 1;
|
||||||
@@ -1079,7 +1151,7 @@ Future<void> _pumpApp(
|
|||||||
final repository = catalog ?? FakeCatalogRepository();
|
final repository = catalog ?? FakeCatalogRepository();
|
||||||
await tester.pumpWidget(
|
await tester.pumpWidget(
|
||||||
LumaDramaApp(
|
LumaDramaApp(
|
||||||
playerFactory: createWidgetPlayer,
|
playerFactory: playerFactory ?? createWidgetPlayer,
|
||||||
catalogRepository: repository,
|
catalogRepository: repository,
|
||||||
categoryRepository: FakeCategoryRepository(catalog: repository),
|
categoryRepository: FakeCategoryRepository(catalog: repository),
|
||||||
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
watchHistoryRepository: InMemoryWatchHistoryRepository(),
|
||||||
@@ -1094,6 +1166,26 @@ Future<void> _pumpApp(
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _RecordingPlayer implements PlayerPort {
|
||||||
|
bool playing = false;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> initialize() async {}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> play() async {
|
||||||
|
playing = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> pause() async {
|
||||||
|
playing = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {}
|
||||||
|
}
|
||||||
|
|
||||||
const _lockedDrama = Drama(
|
const _lockedDrama = Drama(
|
||||||
id: 'locked-demo',
|
id: 'locked-demo',
|
||||||
title: 'Locked Demo',
|
title: 'Locked Demo',
|
||||||
|
|||||||
Reference in new issue
Block a user