fix(player): 收紧图片与快速切集资源占用
This commit is contained in:
1 parent
2fef7470e0
commit
80c962a95c
10 files changed
+281
-30
No files matched your search
@@ -81,6 +81,8 @@ class PlaybackSession extends ChangeNotifier {
|
||||
// Includes initialization and asynchronous disposal, until native release ends.
|
||||
int get livePlayerCount => _live.length;
|
||||
int? get preloadedIndex => _next?.prepared == true ? _next?.index : null;
|
||||
int? get previousPreloadedIndex =>
|
||||
_previous?.prepared == true ? _previous?.index : null;
|
||||
Future<void> get playbackSettled => _operations;
|
||||
Future<void> get preloadSettled => _preloadTask;
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import '../../config/app_config.dart';
|
||||
import 'sprite_thumbnail_asset_loader.dart';
|
||||
|
||||
const Duration spriteVttCacheTtl = Duration(minutes: 10);
|
||||
const int spriteImageCacheCapacity = 2;
|
||||
const int spriteImageCacheCapacity = 1;
|
||||
|
||||
/// 单个 VTT cue 对应的 sprite 裁剪区域。
|
||||
class SpriteThumbnailCue {
|
||||
@@ -148,7 +148,6 @@ class SpriteThumbnailAxisService extends ChangeNotifier
|
||||
final Set<String> _failedSprites = {};
|
||||
bool _disposed = false;
|
||||
|
||||
@visibleForTesting
|
||||
int get spriteCacheSize => _spriteCache.length;
|
||||
|
||||
@visibleForTesting
|
||||
@@ -156,7 +155,7 @@ class SpriteThumbnailAxisService extends ChangeNotifier
|
||||
.map((url) => Uri.tryParse(url)?.pathSegments.lastOrNull ?? url)
|
||||
.toList();
|
||||
|
||||
/// 播放器就绪时预取当前 cue 与相邻 sprite,拖动帧不触发网络或解码。
|
||||
/// 播放器就绪时预取当前 cue;容量大于 1 时顺带预取相邻 sprite。
|
||||
@override
|
||||
Future<void> prepareAt(String? thumbnailsUrl, Duration position) async {
|
||||
final url = _normalizeUrl(thumbnailsUrl);
|
||||
@@ -269,6 +268,7 @@ class SpriteThumbnailAxisService extends ChangeNotifier
|
||||
if (current == null) return const [];
|
||||
final index = axis.cues.indexOf(current);
|
||||
final candidates = <SpriteThumbnailCue>[current];
|
||||
if (spriteCacheCapacity <= 1) return candidates;
|
||||
for (final direction in const [1, -1]) {
|
||||
for (var step = 1; step < axis.cues.length; step++) {
|
||||
final neighborIndex = index + direction * step;
|
||||
|
||||
@@ -5,6 +5,24 @@ import 'package:flutter/material.dart';
|
||||
import '../config/app_config.dart';
|
||||
import '../theme/app_colors.dart';
|
||||
|
||||
const int _minImageDecodePixels = 96;
|
||||
const int _maxImageDecodePixels = 2048;
|
||||
|
||||
/// 将布局逻辑尺寸换算成有限像素尺寸,兼顾小头像和全屏首帧。
|
||||
int resolveImageDecodePixels(double? logicalExtent, double devicePixelRatio) {
|
||||
final ratio = devicePixelRatio.isFinite && devicePixelRatio > 0
|
||||
? devicePixelRatio
|
||||
: 1.0;
|
||||
final extent =
|
||||
logicalExtent != null && logicalExtent.isFinite && logicalExtent > 0
|
||||
? logicalExtent
|
||||
: 120.0;
|
||||
return (extent * ratio)
|
||||
.ceil()
|
||||
.clamp(_minImageDecodePixels, _maxImageDecodePixels)
|
||||
.toInt();
|
||||
}
|
||||
|
||||
/// 封面图统一入口:后端给的是相对路径,这里补服务地址并处理加载失败。
|
||||
class RemoteImage extends StatelessWidget {
|
||||
const RemoteImage(
|
||||
@@ -17,6 +35,8 @@ class RemoteImage extends StatelessWidget {
|
||||
this.fallbackIconKey,
|
||||
this.fallbackAspectRatio,
|
||||
this.fallbackColor = AppColors.raised,
|
||||
this.decodeWidth,
|
||||
this.decodeHeight,
|
||||
});
|
||||
|
||||
final String path;
|
||||
@@ -27,21 +47,56 @@ class RemoteImage extends StatelessWidget {
|
||||
final Key? fallbackIconKey;
|
||||
final double? fallbackAspectRatio;
|
||||
final Color fallbackColor;
|
||||
final int? decodeWidth;
|
||||
final int? decodeHeight;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final url = AppConfig.mediaUrl(path);
|
||||
if (url.isEmpty) return _fallback(brand: showFallbackIcon);
|
||||
return Image.network(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => _fallback(brand: showFallbackIcon),
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) return child;
|
||||
return _fallback(brand: showFallbackIcon);
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final pixelRatio = MediaQuery.devicePixelRatioOf(context);
|
||||
final layoutWidth = constraints.hasBoundedWidth
|
||||
? constraints.maxWidth
|
||||
: width;
|
||||
final aspectRatio =
|
||||
fallbackAspectRatio != null &&
|
||||
fallbackAspectRatio!.isFinite &&
|
||||
fallbackAspectRatio! > 0
|
||||
? fallbackAspectRatio
|
||||
: null;
|
||||
final inferredHeight = constraints.hasBoundedHeight
|
||||
? constraints.maxHeight
|
||||
: height ??
|
||||
(layoutWidth != null && aspectRatio != null
|
||||
? layoutWidth / aspectRatio
|
||||
: null);
|
||||
final cacheWidth =
|
||||
decodeWidth ?? resolveImageDecodePixels(layoutWidth, pixelRatio);
|
||||
final cacheHeight =
|
||||
decodeHeight ??
|
||||
(inferredHeight == null
|
||||
? null
|
||||
: resolveImageDecodePixels(inferredHeight, pixelRatio));
|
||||
return Image.network(
|
||||
url,
|
||||
width: width,
|
||||
height: height,
|
||||
fit: fit,
|
||||
cacheWidth: cacheWidth
|
||||
.clamp(_minImageDecodePixels, _maxImageDecodePixels)
|
||||
.toInt(),
|
||||
cacheHeight: cacheHeight
|
||||
?.clamp(_minImageDecodePixels, _maxImageDecodePixels)
|
||||
.toInt(),
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => _fallback(brand: showFallbackIcon),
|
||||
frameBuilder: (context, child, frame, wasSynchronouslyLoaded) {
|
||||
if (wasSynchronouslyLoaded || frame != null) return child;
|
||||
return _fallback(brand: showFallbackIcon);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user