From 63191b4692c2bfe752e56fd9992e4af2c4c683a1 Mon Sep 17 00:00:00 2001 From: Vicky <10212746+haifeng-jiang@user.noreply.gitee.com> Date: Thu, 29 Oct 2026 11:09:11 +0800 Subject: [PATCH] =?UTF-8?q?perf(player):=20=E7=BB=AD=E7=9C=8B=E7=82=B9?= =?UTF-8?q?=E5=8A=A0=E8=BD=BD=E4=B8=8E=E8=BF=9B=E5=BA=A6=E5=86=99=E7=9B=98?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=B9=B6=E8=A1=8C=E4=B8=94=E5=A4=8D=E7=94=A8?= =?UTF-8?q?=E5=90=8C=E4=B8=80=E8=AF=B7=E6=B1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 进播放页与首次准备播放会各要一次续看点,同一次请求跑两遍还串在首帧前; 现在并发请求复用同一 Future,并与上一条进度写盘并行等待,少一个网络往返的起播延迟。 剧场网格海报按布局宽度限制解码尺寸,半屏卡片不再按原图解码。 --- lib/pages/discover/widgets/drama_grid.dart | 31 +++++++++++++++------- lib/pages/player/player_logic.dart | 20 +++++++++++--- test/playback/progress_throttle_test.dart | 12 +++++++++ 3 files changed, 51 insertions(+), 12 deletions(-) diff --git a/lib/pages/discover/widgets/drama_grid.dart b/lib/pages/discover/widgets/drama_grid.dart index 17a2c12..9e52536 100644 --- a/lib/pages/discover/widgets/drama_grid.dart +++ b/lib/pages/discover/widgets/drama_grid.dart @@ -7,6 +7,7 @@ import '../../../core/data/models/drama.dart'; import '../../../core/formatting/count_format.dart'; import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_dimens.dart'; +import '../../../core/widgets/remote_image.dart'; import '../../../l10n/app_localizations.dart'; const int _columns = 2; @@ -218,16 +219,28 @@ class _DiscoverCoverImage extends StatelessWidget { Widget build(BuildContext context) { final url = AppConfig.mediaUrl(path); if (url.isEmpty) return _placeholder(); - return Image.network( - url, - width: double.infinity, - height: double.infinity, - fit: BoxFit.cover, - loadingBuilder: (context, child, progress) { - if (progress == null) return child; - return _placeholder(); + return LayoutBuilder( + builder: (context, constraints) { + // 卡片只有半屏宽,按原图解码一张竖版海报就是数 MB,按布局宽度限一下。 + final decodeWidth = constraints.hasBoundedWidth + ? resolveImageDecodePixels( + constraints.maxWidth, + MediaQuery.devicePixelRatioOf(context), + ) + : null; + return Image.network( + url, + width: double.infinity, + height: double.infinity, + fit: BoxFit.cover, + cacheWidth: decodeWidth, + loadingBuilder: (context, child, progress) { + if (progress == null) return child; + return _placeholder(); + }, + errorBuilder: (_, _, _) => _placeholder(), + ); }, - errorBuilder: (_, _, _) => _placeholder(), ); } diff --git a/lib/pages/player/player_logic.dart b/lib/pages/player/player_logic.dart index 027a00c..66c841a 100644 --- a/lib/pages/player/player_logic.dart +++ b/lib/pages/player/player_logic.dart @@ -110,6 +110,9 @@ class PlayerLogic extends ChangeNotifier { ResumePoint? resumePoint; bool resumeLoaded = false; + /// 续看点加载中的请求:进页与首次准备播放会同时触发,复用同一次请求。 + Future? _resumePointLoad; + /// 票据三态:加载中 / 失败(可重试)/ 被后端判定为不可播放 bool ticketLoading = false; String? ticketError; @@ -512,7 +515,15 @@ class PlayerLogic extends ChangeNotifier { } /// 服务端续看位置:进播放页时取一次,用于"定位到上次那一集的那一秒"。 - Future loadResumePoint() async { + Future loadResumePoint() { + final inFlight = _resumePointLoad; + if (inFlight != null) return inFlight; + return _resumePointLoad = _loadResumePoint().whenComplete(() { + _resumePointLoad = null; + }); + } + + Future _loadResumePoint() async { final entry = queue.entryAt(initialIndex); final cached = positionCache.read( entry.episode.id, @@ -1281,9 +1292,12 @@ class PlayerLogic extends ChangeNotifier { await _waitUntilFrameReady(player.controller); return; } - await pendingWrite; + // 上一条进度写盘与续看点拉取互不依赖,并行等,少一个网络往返的首帧延迟。 // 服务端每剧只记一条:只有"记录里的那一集"才定位,其它集从头播 - if (resumePoint == null) await loadResumePoint(); + await Future.wait([ + pendingWrite, + if (!resumeLoaded) loadResumePoint(), + ]); if (resumePositionFor(episode.id) == null) { final dramaId = _dramaIdForEpisode(episode.id); final cached = dramaId == null diff --git a/test/playback/progress_throttle_test.dart b/test/playback/progress_throttle_test.dart index 7bfb62a..86c04ee 100644 --- a/test/playback/progress_throttle_test.dart +++ b/test/playback/progress_throttle_test.dart @@ -158,6 +158,18 @@ void main() { logic.dispose(); }); + test('should_share_one_request_when_resume_point_loads_concurrently', () async { + final history = FakeWatchHistory()..saved = const Duration(seconds: 20); + final logic = _logic(history, drama: _drama('signal')); + + // 进页与首次准备播放会同时要续看点,只应发一次请求。 + await Future.wait([logic.loadResumePoint(), logic.loadResumePoint()]); + + expect(logic.resumeLoaded, isTrue); + expect(history.reads, ['signal']); + logic.dispose(); + }); + test('should_not_seek_when_position_is_zero', () async { final history = FakeWatchHistory() ..records['signal'] = const WatchProgress(