perf(player): 续看点加载与进度写盘改为并行且复用同一请求

进播放页与首次准备播放会各要一次续看点,同一次请求跑两遍还串在首帧前;
现在并发请求复用同一 Future,并与上一条进度写盘并行等待,少一个网络往返的起播延迟。
剧场网格海报按布局宽度限制解码尺寸,半屏卡片不再按原图解码。
This commit is contained in:
Vicky authored and trae-solo-agent committed 2026-10-29 11:25:47 +08:00
1 parent 0f777b19f2
commit 63191b4692
3 files changed
+51 -12

No files matched your search

+22 -9
View File
@@ -7,6 +7,7 @@ import '../../../core/data/models/drama.dart';
import '../../../core/formatting/count_format.dart'; import '../../../core/formatting/count_format.dart';
import '../../../core/theme/app_colors.dart'; import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_dimens.dart'; import '../../../core/theme/app_dimens.dart';
import '../../../core/widgets/remote_image.dart';
import '../../../l10n/app_localizations.dart'; import '../../../l10n/app_localizations.dart';
const int _columns = 2; const int _columns = 2;
@@ -218,16 +219,28 @@ class _DiscoverCoverImage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final url = AppConfig.mediaUrl(path); final url = AppConfig.mediaUrl(path);
if (url.isEmpty) return _placeholder(); if (url.isEmpty) return _placeholder();
return Image.network( return LayoutBuilder(
url, builder: (context, constraints) {
width: double.infinity, // 卡片只有半屏宽,按原图解码一张竖版海报就是数 MB,按布局宽度限一下。
height: double.infinity, final decodeWidth = constraints.hasBoundedWidth
fit: BoxFit.cover, ? resolveImageDecodePixels(
loadingBuilder: (context, child, progress) { constraints.maxWidth,
if (progress == null) return child; MediaQuery.devicePixelRatioOf(context),
return _placeholder(); )
: 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(),
); );
} }
+17 -3
View File
@@ -110,6 +110,9 @@ class PlayerLogic extends ChangeNotifier {
ResumePoint? resumePoint; ResumePoint? resumePoint;
bool resumeLoaded = false; bool resumeLoaded = false;
/// 续看点加载中的请求:进页与首次准备播放会同时触发,复用同一次请求。
Future<void>? _resumePointLoad;
/// 票据三态:加载中 / 失败(可重试)/ 被后端判定为不可播放 /// 票据三态:加载中 / 失败(可重试)/ 被后端判定为不可播放
bool ticketLoading = false; bool ticketLoading = false;
String? ticketError; String? ticketError;
@@ -512,7 +515,15 @@ class PlayerLogic extends ChangeNotifier {
} }
/// 服务端续看位置:进播放页时取一次,用于"定位到上次那一集的那一秒"。 /// 服务端续看位置:进播放页时取一次,用于"定位到上次那一集的那一秒"。
Future<void> loadResumePoint() async { Future<void> loadResumePoint() {
final inFlight = _resumePointLoad;
if (inFlight != null) return inFlight;
return _resumePointLoad = _loadResumePoint().whenComplete(() {
_resumePointLoad = null;
});
}
Future<void> _loadResumePoint() async {
final entry = queue.entryAt(initialIndex); final entry = queue.entryAt(initialIndex);
final cached = positionCache.read( final cached = positionCache.read(
entry.episode.id, entry.episode.id,
@@ -1281,9 +1292,12 @@ class PlayerLogic extends ChangeNotifier {
await _waitUntilFrameReady(player.controller); await _waitUntilFrameReady(player.controller);
return; return;
} }
await pendingWrite; // 上一条进度写盘与续看点拉取互不依赖,并行等,少一个网络往返的首帧延迟。
// 服务端每剧只记一条:只有"记录里的那一集"才定位,其它集从头播 // 服务端每剧只记一条:只有"记录里的那一集"才定位,其它集从头播
if (resumePoint == null) await loadResumePoint(); await Future.wait([
pendingWrite,
if (!resumeLoaded) loadResumePoint(),
]);
if (resumePositionFor(episode.id) == null) { if (resumePositionFor(episode.id) == null) {
final dramaId = _dramaIdForEpisode(episode.id); final dramaId = _dramaIdForEpisode(episode.id);
final cached = dramaId == null final cached = dramaId == null
+12
View File
@@ -158,6 +158,18 @@ void main() {
logic.dispose(); 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 { test('should_not_seek_when_position_is_zero', () async {
final history = FakeWatchHistory() final history = FakeWatchHistory()
..records['signal'] = const WatchProgress( ..records['signal'] = const WatchProgress(