fix(player): 使用高清首帧并稳定切集占位
This commit is contained in:
1 parent
0cb0ab0613
commit
2fef7470e0
10 files changed
+258
-33
No files matched your search
@@ -4,20 +4,42 @@ import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../../../core/config/app_config.dart';
|
||||
import '../../../core/data/services/sprite_thumbnail_axis.dart';
|
||||
import '../../../core/theme/app_colors.dart';
|
||||
import 'scrub_preview_card.dart';
|
||||
|
||||
/// 当前集雪碧图首帧在视频纹理未就绪时占位;空地址交还纯黑底。
|
||||
enum PlayerOpeningFallback { handoff, poster, thumbnails, black }
|
||||
|
||||
PlayerOpeningFallback playerOpeningFallback({
|
||||
String? posterUrl,
|
||||
String? thumbnailsUrl,
|
||||
}) {
|
||||
if (posterUrl?.trim().isNotEmpty ?? false) {
|
||||
return PlayerOpeningFallback.poster;
|
||||
}
|
||||
if (thumbnailsUrl?.trim().isNotEmpty ?? false) {
|
||||
return PlayerOpeningFallback.thumbnails;
|
||||
}
|
||||
return PlayerOpeningFallback.black;
|
||||
}
|
||||
|
||||
/// 当前集首帧在视频纹理未就绪时占位;空地址交还纯黑底。
|
||||
Widget? playerOpeningFrame(
|
||||
String? thumbnailsUrl, {
|
||||
String? posterUrl,
|
||||
required SpriteThumbnailAxisSource spriteSource,
|
||||
BoxFit fit = BoxFit.cover,
|
||||
}) {
|
||||
final url = thumbnailsUrl?.trim() ?? '';
|
||||
if (url.isEmpty) return null;
|
||||
final thumbnails = thumbnailsUrl?.trim() ?? '';
|
||||
final poster = posterUrl?.trim() ?? '';
|
||||
if (playerOpeningFallback(posterUrl: poster, thumbnailsUrl: thumbnails) ==
|
||||
PlayerOpeningFallback.black) {
|
||||
return null;
|
||||
}
|
||||
return PlayerOpeningFrame(
|
||||
thumbnailsUrl: url,
|
||||
thumbnailsUrl: thumbnails,
|
||||
posterUrl: poster,
|
||||
spriteSource: spriteSource,
|
||||
fit: fit,
|
||||
);
|
||||
@@ -29,9 +51,13 @@ class PlayerOpeningFrame extends StatefulWidget {
|
||||
super.key,
|
||||
required this.thumbnailsUrl,
|
||||
required this.spriteSource,
|
||||
this.posterUrl,
|
||||
this.fit = BoxFit.cover,
|
||||
});
|
||||
|
||||
/// 当前视频原始分辨率首帧地址,优先于雪碧图。
|
||||
final String? posterUrl;
|
||||
|
||||
/// 当前视频雪碧图 WebVTT 地址。
|
||||
final String thumbnailsUrl;
|
||||
|
||||
@@ -46,26 +72,29 @@ class PlayerOpeningFrame extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _PlayerOpeningFrameState extends State<PlayerOpeningFrame> {
|
||||
bool _posterFailed = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.spriteSource.addListener(_handleSpriteChanged);
|
||||
unawaited(
|
||||
widget.spriteSource.prepareAt(widget.thumbnailsUrl, Duration.zero),
|
||||
);
|
||||
_prepareSpriteIfNeeded();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant PlayerOpeningFrame oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.posterUrl != widget.posterUrl) {
|
||||
_posterFailed = false;
|
||||
}
|
||||
if (oldWidget.spriteSource != widget.spriteSource) {
|
||||
oldWidget.spriteSource.removeListener(_handleSpriteChanged);
|
||||
widget.spriteSource.addListener(_handleSpriteChanged);
|
||||
_prepareSpriteIfNeeded();
|
||||
}
|
||||
if (oldWidget.thumbnailsUrl != widget.thumbnailsUrl) {
|
||||
unawaited(
|
||||
widget.spriteSource.prepareAt(widget.thumbnailsUrl, Duration.zero),
|
||||
);
|
||||
if (oldWidget.posterUrl != widget.posterUrl ||
|
||||
oldWidget.thumbnailsUrl != widget.thumbnailsUrl) {
|
||||
_prepareSpriteIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,19 +108,56 @@ class _PlayerOpeningFrameState extends State<PlayerOpeningFrame> {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
bool get _hasPoster =>
|
||||
!_posterFailed && (widget.posterUrl?.trim().isNotEmpty ?? false);
|
||||
|
||||
void _prepareSpriteIfNeeded() {
|
||||
if (_hasPoster || widget.thumbnailsUrl.trim().isEmpty) return;
|
||||
unawaited(
|
||||
widget.spriteSource.prepareAt(widget.thumbnailsUrl, Duration.zero),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _handlePosterError() {
|
||||
if (!_posterFailed) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _posterFailed) return;
|
||||
setState(() => _posterFailed = true);
|
||||
_prepareSpriteIfNeeded();
|
||||
});
|
||||
}
|
||||
return _spriteOrBlack();
|
||||
}
|
||||
|
||||
Widget _spriteOrBlack() {
|
||||
final frame = widget.spriteSource.cachedFrameAt(
|
||||
widget.thumbnailsUrl,
|
||||
Duration.zero,
|
||||
);
|
||||
if (frame == null) return const SizedBox.expand();
|
||||
return SpriteCropPreview(
|
||||
key: const Key('player-opening-sprite'),
|
||||
image: frame.image,
|
||||
sourceRect: frame.sourceRect,
|
||||
fit: widget.fit,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
const ColoredBox(color: AppColors.mediaBlack),
|
||||
if (widget.spriteSource.cachedFrameAt(widget.thumbnailsUrl, Duration.zero)
|
||||
case final frame?)
|
||||
SpriteCropPreview(
|
||||
key: const Key('player-opening-sprite'),
|
||||
image: frame.image,
|
||||
sourceRect: frame.sourceRect,
|
||||
if (_hasPoster)
|
||||
Image.network(
|
||||
AppConfig.mediaUrl(widget.posterUrl!.trim()),
|
||||
key: const Key('player-opening-poster'),
|
||||
fit: widget.fit,
|
||||
),
|
||||
gaplessPlayback: true,
|
||||
errorBuilder: (_, _, _) => _handlePosterError(),
|
||||
)
|
||||
else
|
||||
_spriteOrBlack(),
|
||||
],
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user