原生解析问题

This commit is contained in:
roxy 2026-06-08 10:16:56 +08:00
parent 2e9bb9bddc
commit 8ac9590a8f
7 changed files with 258 additions and 80 deletions

View File

@ -132,6 +132,8 @@ class GiftMp4VideoPlatformView(
}
}
private const val GIFT_MP4_VAPC_SCAN_BYTES = 512 * 1024
private class GiftMp4AlphaTextureView(context: Context) :
TextureView(context),
TextureView.SurfaceTextureListener {
@ -826,7 +828,7 @@ private data class GiftMp4NativeLayout(
val marker = "vapc".toByteArray(Charsets.US_ASCII)
val markerIndex = bytes.indexOf(marker, 0)
if (markerIndex < 0) return null
val searchEnd = min(bytes.size, markerIndex + marker.size + 512 * 1024)
val searchEnd = min(bytes.size, markerIndex + marker.size + GIFT_MP4_VAPC_SCAN_BYTES)
for (index in markerIndex + marker.size until searchEnd) {
if (bytes[index] != '{'.code.toByte()) continue
val end = findJsonObjectEnd(bytes, index, searchEnd)
@ -991,7 +993,21 @@ private data class GiftMp4ResolvedSource(
fun readBytes(): ByteArray? =
runCatching {
file.readBytes()
file.inputStream().use { input ->
val readLength = min(file.length(), GIFT_MP4_VAPC_SCAN_BYTES.toLong()).toInt()
if (readLength <= 0) {
return@use ByteArray(0)
}
val buffer = ByteArray(readLength)
val bytesRead = input.read(buffer)
if (bytesRead <= 0) {
ByteArray(0)
} else if (bytesRead == buffer.size) {
buffer
} else {
buffer.copyOf(bytesRead)
}
}
}.getOrNull()
fun close() = Unit

View File

@ -727,43 +727,38 @@ class _VoiceRoomPageState extends State<VoiceRoomPage>
targetGroupKey == null ||
_markGiftVisualBatchForPlayback(msg, targetGroupKey);
if (shouldPlayBatchVisuals) {
SCRoomEffectScheduler().scheduleDeferredEffect(
debugLabel: 'room_gift_ticker_${msg.type}_${msg.gift?.id ?? ""}',
action: () {
if (!mounted) {
return;
}
var giftModel = LGiftModel();
giftModel.labelId = _buildGiftTickerLabelId(msg, targetGroupKey);
giftModel.sendUserName = msg.user?.userNickname ?? "";
giftModel.sendToUserName = _resolveGiftTickerTargetName(
msg,
targetUserIds,
);
giftModel.sendUserPic = msg.user?.userAvatar ?? "";
giftModel.giftPic = msg.gift?.giftPhoto ?? "";
giftModel.giftCount = msg.number ?? 0;
giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg);
unawaited(
warmImageResource(
giftModel.sendUserPic,
logicalWidth: 26.w,
logicalHeight: 26.w,
),
);
unawaited(
warmImageResource(
giftModel.giftPic,
logicalWidth: 34.w,
logicalHeight: 34.w,
),
);
Provider.of<GiftAnimationManager>(
context,
listen: false,
).enqueueGiftAnimation(giftModel);
},
if (!mounted) {
return;
}
var giftModel = LGiftModel();
giftModel.labelId = _buildGiftTickerLabelId(msg, targetGroupKey);
giftModel.sendUserName = msg.user?.userNickname ?? "";
giftModel.sendToUserName = _resolveGiftTickerTargetName(
msg,
targetUserIds,
);
giftModel.sendUserPic = msg.user?.userAvatar ?? "";
giftModel.giftPic = msg.gift?.giftPhoto ?? "";
giftModel.giftCount = msg.number ?? 0;
giftModel.giftCountStepUnit = _resolveGiftCountStepUnit(msg);
unawaited(
warmImageResource(
giftModel.sendUserPic,
logicalWidth: 26.w,
logicalHeight: 26.w,
),
);
unawaited(
warmImageResource(
giftModel.giftPic,
logicalWidth: 34.w,
logicalHeight: 34.w,
),
);
Provider.of<GiftAnimationManager>(
context,
listen: false,
).enqueueGiftAnimation(giftModel);
} else {
debugPrint(
'[GiftFx][voice] skip gift ticker by visual batch dedupe '

View File

@ -338,11 +338,9 @@ class _RecentMicChangeSeatSnapshot {
class RealTimeCommunicationManager extends ChangeNotifier {
static const String _roomRocketPagLaunchDialogTag =
'showRoomRocketPagLaunchDialog';
static const List<Duration> _roomRocketGiftRefreshDelays = [
Duration(milliseconds: 700),
Duration(milliseconds: 1600),
Duration(milliseconds: 3200),
];
static const Duration _roomRocketGiftRefreshThrottleDelay = Duration(
milliseconds: 700,
);
static const List<Duration> _roomRocketPostLaunchRefreshDelays = [
Duration(milliseconds: 800),
Duration(seconds: 2),
@ -407,7 +405,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Timer? _roomEntryEffectTimer;
Timer? _roomRocketLaunchAnimationTimer;
ValueNotifier<String?>? _roomRocketLaunchTopAvatarNotifier;
final List<Timer> _roomRocketGiftRefreshTimers = [];
Timer? _roomRocketGiftRefreshTimer;
String? _roomRocketGiftRefreshRoomId;
bool _roomRocketGiftRefreshDirty = false;
bool _roomRocketGiftRefreshInFlight = false;
final List<Timer> _roomRocketPostLaunchRefreshTimers = [];
final List<Timer> _roomRocketEntryReadyFallbackTimers = [];
Timer? _roomRedPacketPresenceTimer;
@ -4122,25 +4123,63 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (resolvedRoomId.isEmpty) {
return;
}
_cancelRoomRocketGiftStatusRefresh();
for (final delay in _roomRocketGiftRefreshDelays) {
final timer = Timer(delay, () {
_roomRocketGiftRefreshTimers.removeWhere((item) => !item.isActive);
if (resolvedRoomId !=
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
return;
}
unawaited(refreshRoomRocketStatus(roomId: resolvedRoomId));
});
_roomRocketGiftRefreshTimers.add(timer);
_roomRocketGiftRefreshRoomId = resolvedRoomId;
_roomRocketGiftRefreshDirty = true;
if (_roomRocketGiftRefreshTimer != null || _roomRocketGiftRefreshInFlight) {
return;
}
_roomRocketGiftRefreshTimer = Timer(
_roomRocketGiftRefreshThrottleDelay,
() => unawaited(_runRoomRocketGiftStatusRefresh()),
);
}
void _schedulePendingRoomRocketGiftStatusRefreshIfNeeded() {
if (!_roomRocketGiftRefreshDirty || _roomRocketGiftRefreshTimer != null) {
return;
}
final pendingRoomId = (_roomRocketGiftRefreshRoomId ?? "").trim();
final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (pendingRoomId.isEmpty || pendingRoomId != currentRoomId) {
return;
}
_roomRocketGiftRefreshTimer = Timer(
_roomRocketGiftRefreshThrottleDelay,
() => unawaited(_runRoomRocketGiftStatusRefresh()),
);
}
Future<void> _runRoomRocketGiftStatusRefresh() async {
_roomRocketGiftRefreshTimer = null;
if (_roomRocketGiftRefreshInFlight) {
return;
}
final resolvedRoomId = (_roomRocketGiftRefreshRoomId ?? "").trim();
if (resolvedRoomId.isEmpty) {
_roomRocketGiftRefreshDirty = false;
return;
}
_roomRocketGiftRefreshDirty = false;
if (resolvedRoomId !=
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim()) {
_schedulePendingRoomRocketGiftStatusRefreshIfNeeded();
return;
}
_roomRocketGiftRefreshInFlight = true;
try {
await refreshRoomRocketStatus(roomId: resolvedRoomId);
} finally {
_roomRocketGiftRefreshInFlight = false;
_schedulePendingRoomRocketGiftStatusRefreshIfNeeded();
}
}
void _cancelRoomRocketGiftStatusRefresh() {
for (final timer in _roomRocketGiftRefreshTimers) {
timer.cancel();
}
_roomRocketGiftRefreshTimers.clear();
_roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null;
_roomRocketGiftRefreshRoomId = null;
_roomRocketGiftRefreshDirty = false;
}
void _scheduleRoomRocketEntryReadyFallback(String roomId) {
@ -4384,8 +4423,8 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
SCGiftVapSvgaManager().play(
animationUrl,
priority: SCGiftVapSvgaManager.entryEffectPriority + 100,
type: SCGiftVapSvgaManager.entryEffectType,
priority: SCGiftVapSvgaManager.rocketLaunchEffectPriority,
type: SCGiftVapSvgaManager.rocketLaunchEffectType,
);
}

View File

@ -5149,8 +5149,6 @@ class RealTimeMessagingManager extends ChangeNotifier {
RoomRocketLaunchBroadcastMessage launch,
) {
_clearRoomRocketLaunchCountdownForRoom(launch.roomId);
_pendingRoomRocketRewardPopups.remove(launch.roomId);
_recentRoomRocketRewardProbeTimes.remove(launch.roomId);
_markRoomRocketLaunchEffectConsumed(launch);
}
@ -6111,10 +6109,15 @@ class RealTimeMessagingManager extends ChangeNotifier {
currentContext,
listen: false,
);
rtcProvider.advanceRoomRocketStatusAfterLaunch(launch);
rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh(
roomId: launchRoomId,
);
if (_isCurrentVisibleVoiceRoom(launchRoomId)) {
_markRoomRocketRewardProbe(launch.roomId);
_playRoomRocketLaunchEffectsAndReward(launch);
} else {
rtcProvider.advanceRoomRocketStatusAfterLaunch(launch);
rtcProvider.scheduleRoomRocketPostLaunchStatusRefresh(
roomId: launchRoomId,
);
}
}
}
_scheduleRoomRocketStatusRefreshForCurrentRoom(launchRoomId);

View File

@ -85,16 +85,23 @@ class OverlayManager {
SCFloatingMessage message, {
required String debugLabel,
}) {
void enqueueNow() {
if (_isDisposed || _isSuppressed) {
return;
}
unawaited(_warmMessageImages(message));
_messageQueue.add(message);
_safeScheduleNext();
}
if (message.type == 1) {
enqueueNow();
return;
}
SCRoomEffectScheduler().scheduleDeferredEffect(
debugLabel: debugLabel,
action: () {
if (_isDisposed || _isSuppressed) {
return;
}
unawaited(_warmMessageImages(message));
_messageQueue.add(message);
_safeScheduleNext();
},
action: enqueueNow,
);
}

View File

@ -16,10 +16,13 @@ import 'package:yumi/shared/data_sources/sources/local/data_persistence.dart';
class SCGiftVapSvgaManager {
static const int giftEffectType = 0;
static const int entryEffectType = 1;
static const int rocketLaunchEffectType = 2;
static const int giftEffectPriority = 100;
static const int entryEffectPriority = 1000;
static const int rocketLaunchEffectPriority = 2000;
static const int _maxPendingGiftTaskCount = 24;
static const int _maxPendingEntryTaskCount = 5;
static const int _maxPendingRocketLaunchTaskCount = 3;
static const int _maxConsecutiveEntryTasksBeforeGift = 2;
static const Duration _taskWatchdogTimeout = Duration(seconds: 20);
static const Duration _entryTaskTtl = Duration(seconds: 6);
@ -40,6 +43,10 @@ class SCGiftVapSvgaManager {
final SCPriorityQueue<SCVapTask> _entryQueue = SCPriorityQueue<SCVapTask>(
(a, b) => a.compareTo(b), // SCVapTask compareTo
);
final SCPriorityQueue<SCVapTask> _rocketLaunchQueue =
SCPriorityQueue<SCVapTask>(
(a, b) => a.compareTo(b), // SCVapTask compareTo
);
SCGiftMp4Controller? _mp4Controller;
SVGAAnimationController? _rsc;
bool _play = false;
@ -97,6 +104,11 @@ class SCGiftVapSvgaManager {
if (type == entryEffectType) {
return priority > entryEffectPriority ? priority : entryEffectPriority;
}
if (type == rocketLaunchEffectType) {
return priority > rocketLaunchEffectPriority
? priority
: rocketLaunchEffectPriority;
}
if (priority != 0) {
return priority;
}
@ -105,10 +117,15 @@ class SCGiftVapSvgaManager {
bool _isEntryTask(SCVapTask task) => task.type == entryEffectType;
int get _pendingTaskCount => _giftQueue.length + _entryQueue.length;
bool _isRocketLaunchTask(SCVapTask task) =>
task.type == rocketLaunchEffectType;
int get _pendingTaskCount =>
_giftQueue.length + _entryQueue.length + _rocketLaunchQueue.length;
String get _queueSummary =>
'entry=${_entryQueue.length} gift=${_giftQueue.length}';
'rocket=${_rocketLaunchQueue.length} entry=${_entryQueue.length} '
'gift=${_giftQueue.length}';
bool _isControllerReady(SCVapTask task) {
if (_needsSvgaController(task.path)) {
@ -204,7 +221,19 @@ class SCGiftVapSvgaManager {
await _loadSvgaEntity(path);
return;
}
await _ensurePlayableFilePath(path);
final pathType = SCPathUtils.getPathType(path);
if (pathType == PathType.asset) {
await SCGiftMp4Controller.preloadVapLayout(
path: path,
sourceType: SCGiftMp4SourceType.asset,
);
return;
}
final playablePath = await _ensurePlayableFilePath(path);
await SCGiftMp4Controller.preloadVapLayout(
path: playablePath,
sourceType: SCGiftMp4SourceType.file,
);
}
Future<MovieEntity> _loadSvgaEntity(String path) async {
@ -328,6 +357,11 @@ class SCGiftVapSvgaManager {
}
void _enqueueTask(SCVapTask task) {
if (_isRocketLaunchTask(task)) {
_rocketLaunchQueue.add(task);
_trimRocketLaunchQueue();
return;
}
if (_isEntryTask(task)) {
_dropExpiredEntryTasks();
_entryQueue.add(task);
@ -338,6 +372,27 @@ class SCGiftVapSvgaManager {
_trimGiftQueue();
}
void _trimRocketLaunchQueue() {
while (_rocketLaunchQueue.length > _maxPendingRocketLaunchTaskCount) {
final removedTask = _removeRocketLaunchOverflowTask();
_completeDroppedQueuedTask(removedTask, 'trim rocket launch queue');
}
}
SCVapTask _removeRocketLaunchOverflowTask() {
final tasks = _rocketLaunchQueue.unorderedElements.cast<SCVapTask>();
var candidate = tasks.first;
for (final task in tasks.skip(1)) {
if (task.priority < candidate.priority ||
(task.priority == candidate.priority &&
task.createdAt.isBefore(candidate.createdAt))) {
candidate = task;
}
}
_rocketLaunchQueue.remove(candidate);
return candidate;
}
void _trimEntryQueue() {
while (_entryQueue.length > _maxPendingEntryTaskCount) {
final removedTask = _removeEntryOverflowTask();
@ -394,6 +449,9 @@ class SCGiftVapSvgaManager {
}
SCVapTask? _peekNextQueuedTask() {
if (_rocketLaunchQueue.isNotEmpty) {
return _rocketLaunchQueue.first;
}
_dropExpiredEntryTasks();
if (_entryQueue.isNotEmpty) {
final shouldYieldToGift =
@ -408,6 +466,11 @@ class SCGiftVapSvgaManager {
}
void _removeQueuedTask(SCVapTask task) {
if (_isRocketLaunchTask(task)) {
_rocketLaunchQueue.remove(task);
_consecutiveEntryTaskCount = 0;
return;
}
if (_isEntryTask(task)) {
_entryQueue.remove(task);
_consecutiveEntryTaskCount++;
@ -755,6 +818,7 @@ class SCGiftVapSvgaManager {
_cancelCurrentTaskWatchdog();
_giftQueue.clear();
_entryQueue.clear();
_rocketLaunchQueue.clear();
_preloadQueue.clear();
_queuedPreloadPaths.clear();
_activePreloadCount = 0;

View File

@ -53,6 +53,13 @@ class SCGiftMp4Controller extends ChangeNotifier {
SCGiftMp4PlayRequest? get request => _request;
static Future<void> preloadVapLayout({
required String path,
required SCGiftMp4SourceType sourceType,
}) async {
await _parseVapLayout(path: path, sourceType: sourceType);
}
static void _installEventsHandler() {
if (_eventsHandlerInstalled) {
return;
@ -331,15 +338,44 @@ class _GiftMp4EffectViewState extends State<GiftMp4EffectView> {
}
}
const int _vapLayoutScanBytes = 512 * 1024;
final Map<String, Map<String, Object?>?> _vapLayoutCache =
<String, Map<String, Object?>?>{};
final Map<String, Future<Map<String, Object?>?>> _vapLayoutTasks =
<String, Future<Map<String, Object?>?>>{};
Future<Map<String, Object?>?> _parseVapLayout({
required String path,
required SCGiftMp4SourceType sourceType,
}) async {
final cacheKey = '${sourceType.name}|$path';
if (_vapLayoutCache.containsKey(cacheKey)) {
return _vapLayoutCache[cacheKey];
}
final loadingTask = _vapLayoutTasks[cacheKey];
if (loadingTask != null) {
return loadingTask;
}
final future = _parseVapLayoutUncached(path: path, sourceType: sourceType);
_vapLayoutTasks[cacheKey] = future;
try {
final layout = await future;
_vapLayoutCache[cacheKey] = layout;
return layout;
} finally {
_vapLayoutTasks.remove(cacheKey);
}
}
Future<Map<String, Object?>?> _parseVapLayoutUncached({
required String path,
required SCGiftMp4SourceType sourceType,
}) async {
try {
final bytes =
sourceType == SCGiftMp4SourceType.asset
? (await rootBundle.load(path)).buffer.asUint8List()
: await File(path.removePrefix('file://')).readAsBytes();
: await _readFilePrefixBytes(path.removePrefix('file://'));
final jsonText = _extractVapcJson(bytes);
if (jsonText == null) {
return null;
@ -382,6 +418,22 @@ Future<Map<String, Object?>?> _parseVapLayout({
}
}
Future<Uint8List> _readFilePrefixBytes(String path) async {
final file = File(path);
final length = await file.length();
final readLength =
length < _vapLayoutScanBytes ? length.toInt() : _vapLayoutScanBytes;
if (readLength <= 0) {
return Uint8List(0);
}
final randomAccessFile = await file.open();
try {
return await randomAccessFile.read(readLength);
} finally {
await randomAccessFile.close();
}
}
String? _extractVapcJson(Uint8List bytes) {
const marker = [0x76, 0x61, 0x70, 0x63]; // vapc
final markerIndex = _indexOfBytes(bytes, marker, 0);
@ -389,7 +441,9 @@ String? _extractVapcJson(Uint8List bytes) {
return null;
}
final searchEnd =
(markerIndex + marker.length + 512 * 1024).clamp(0, bytes.length).toInt();
(markerIndex + marker.length + _vapLayoutScanBytes)
.clamp(0, bytes.length)
.toInt();
for (var index = markerIndex + marker.length; index < searchEnd; index++) {
if (bytes[index] != 0x7b) {
continue;