fix(player): 雪碧图缓存补齐槽位预取量,清屏按钮贴近进度条
三个渲染槽位各预取 2 张雪碧图,容量 3 装不下会导致「重建→重新下载解码→ notify→重建」的死循环,真机持续发烫掉帧;容量恢复为 8 并只在未命中时预取。 首页清屏态的退出按钮由距底 56 收到 24,贴着进度条上方。
This commit is contained in:
1 parent
79463e65c2
commit
d5bcda54d3
2 files changed
+38
-11
No files matched your search
@@ -189,9 +189,10 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
_thumbnailService = PlaybackThumbnailService(
|
_thumbnailService = PlaybackThumbnailService(
|
||||||
sourceResolver: _resolveThumbnailSource,
|
sourceResolver: _resolveThumbnailSource,
|
||||||
);
|
);
|
||||||
// 雪碧图每张解码后是整块位图,保留 3 部剧足够擦洗回退,8 部在低端机
|
// 容量必须盖住「同时预取的数量」:渲染栈有上一集/当前集/下一集三个槽位,
|
||||||
// 容易把图片缓存顶满甚至触发 OOM。
|
// 每槽预取当前 cue + 1 个相邻 cue,约 6 张。容量小于这个数时,每次重建都会
|
||||||
_spriteAxisService = SpriteThumbnailAxisService(spriteCacheCapacity: 3)
|
// 把别处刚解码的雪碧图挤掉,形成「重建→重新下载解码→notify→重建」的死循环。
|
||||||
|
_spriteAxisService = SpriteThumbnailAxisService(spriteCacheCapacity: 8)
|
||||||
..addListener(_handleSpriteFrameChanged);
|
..addListener(_handleSpriteFrameChanged);
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
logic.start(
|
logic.start(
|
||||||
@@ -1013,19 +1014,19 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
final thumbnailReady =
|
final thumbnailReady =
|
||||||
thumbnailsUrl != null &&
|
thumbnailsUrl != null &&
|
||||||
_spriteAxisService.cachedFrameAt(thumbnailsUrl, Duration.zero) != null;
|
_spriteAxisService.cachedFrameAt(thumbnailsUrl, Duration.zero) != null;
|
||||||
if (!locked && !posterReady && thumbnailsUrl != null) {
|
// 只在没命中时预取:prepareAt 会触发下载解码并 notifyListeners -> 重建,
|
||||||
|
// 无条件调用会让每次重建都排一次异步任务。
|
||||||
|
if (!locked && !posterReady && !thumbnailReady && thumbnailsUrl != null) {
|
||||||
unawaited(_spriteAxisService.prepareAt(thumbnailsUrl, Duration.zero));
|
unawaited(_spriteAxisService.prepareAt(thumbnailsUrl, Duration.zero));
|
||||||
}
|
}
|
||||||
if (selected &&
|
if (selected &&
|
||||||
!locked &&
|
!locked &&
|
||||||
videoPort != null &&
|
videoPort != null &&
|
||||||
videoPort.controller.value.isInitialized) {
|
videoPort.controller.value.isInitialized) {
|
||||||
unawaited(
|
final position = videoPort.controller.value.position;
|
||||||
_spriteAxisService.prepareAt(
|
if (_spriteAxisService.cachedFrameAt(thumbnailsUrl, position) == null) {
|
||||||
thumbnailsUrl,
|
unawaited(_spriteAxisService.prepareAt(thumbnailsUrl, position));
|
||||||
videoPort.controller.value.position,
|
}
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
final pagePosition = indices.indexOf(index);
|
final pagePosition = indices.indexOf(index);
|
||||||
return PlayerRenderSlotData(
|
return PlayerRenderSlotData(
|
||||||
@@ -1286,7 +1287,8 @@ class _PlayerPageState extends State<PlayerPage>
|
|||||||
if (widget.homeMode && !_chromeVisible && !_boosting && !_scrubbing)
|
if (widget.homeMode && !_chromeVisible && !_boosting && !_scrubbing)
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 12,
|
right: 12,
|
||||||
bottom: 56,
|
// 进度条触控带 16pt + 8pt 间距,退出清屏按钮贴着进度条上方。
|
||||||
|
bottom: 24,
|
||||||
child: ExitClearScreenButton(
|
child: ExitClearScreenButton(
|
||||||
key: const Key('home-feed-exit-clear-screen'),
|
key: const Key('home-feed-exit-clear-screen'),
|
||||||
tooltip: l10n.exitClearScreen,
|
tooltip: l10n.exitClearScreen,
|
||||||
|
|||||||
@@ -159,6 +159,31 @@ void main() {
|
|||||||
expect(loader.spriteCalls, hasLength(6));
|
expect(loader.spriteCalls, hasLength(6));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('should_not_reload_three_slots_sprites_when_capacity_fits', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final loader = _FakeSpriteLoader(vtt: _spriteVtt);
|
||||||
|
// 播放页三个槽位各预取 2 张,容量必须 >= 6,否则每轮重建都会重新下载解码。
|
||||||
|
final service = SpriteThumbnailAxisService(
|
||||||
|
loader: loader,
|
||||||
|
spriteCacheCapacity: 8,
|
||||||
|
);
|
||||||
|
addTearDown(service.dispose);
|
||||||
|
const urls = [
|
||||||
|
'https://cdn.test/episode-0/thumbnails.vtt',
|
||||||
|
'https://cdn.test/episode-1/thumbnails.vtt',
|
||||||
|
'https://cdn.test/episode-2/thumbnails.vtt',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (var round = 0; round < 3; round++) {
|
||||||
|
for (final url in urls) {
|
||||||
|
await service.prepareAt(url, Duration.zero);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(loader.spriteCalls, hasLength(6));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('should_refresh_vtt_after_ten_minute_ttl', (tester) async {
|
testWidgets('should_refresh_vtt_after_ten_minute_ttl', (tester) async {
|
||||||
var now = DateTime(2026, 10, 22);
|
var now = DateTime(2026, 10, 22);
|
||||||
final loader = _FakeSpriteLoader(vtt: _spriteVtt);
|
final loader = _FakeSpriteLoader(vtt: _spriteVtt);
|
||||||
|
|||||||
Reference in new issue
Block a user