第一次送礼物动画卡顿问题

This commit is contained in:
roxy 2026-06-05 14:47:02 +08:00
parent dc6795e00e
commit 2e9bb9bddc
9 changed files with 1277 additions and 817 deletions

View File

@ -7,7 +7,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/modules/room_game/bridge/baishun_js_bridge.dart';
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
@ -51,7 +50,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
bool _didReceiveBridgeMessage = false;
bool _didFinishPageLoad = false;
bool _hasDeliveredLaunchConfig = false;
bool _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
int _bridgeInjectCount = 0;
String? _errorMessage;
@ -107,7 +105,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
onPageFinished: (String url) async {
_didFinishPageLoad = true;
_log('page_finished url=${_clip(url, 240)}');
_showWebView(reason: 'page_finished');
await _injectBridge(reason: 'page_finished');
},
onWebResourceError: (WebResourceError error) {
@ -145,7 +142,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
_didReceiveBridgeMessage = false;
_didFinishPageLoad = false;
_hasDeliveredLaunchConfig = false;
_shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
_bridgeInjectCount = 0;
_stopBridgeBootstrap(reason: 'prepare_page_load');
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
@ -177,7 +173,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
return;
}
_log('loading_fallback_fire isLoading=$_isLoading');
_showWebView(reason: 'loading_fallback');
setState(() {
_isLoading = false;
});
@ -331,7 +326,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
return;
}
_log('game_loaded received');
_showWebView(reason: 'game_loaded');
setState(() {
_isLoading = false;
_errorMessage = null;
@ -363,16 +357,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
}
}
void _showWebView({required String reason}) {
if (_shouldMountWebView || !mounted) {
return;
}
_log('mount_webview reason=$reason');
setState(() {
_shouldMountWebView = true;
});
}
Future<void> _reload() async {
if (!mounted) {
return;
@ -651,26 +635,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
return screenSize.width / ratio;
}
Widget _buildWebView() {
final params = PlatformWebViewWidgetCreationParams(
controller: _controller.platform,
gestureRecognizers: _webGestureRecognizers,
);
if (defaultTargetPlatform == TargetPlatform.android) {
return WebViewWidget.fromPlatformCreationParams(
params:
AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams(
params,
displayWithHybridComposition: true,
),
);
}
return WebViewWidget(
controller: _controller,
gestureRecognizers: _webGestureRecognizers,
);
}
@override
Widget build(BuildContext context) {
final topCrop = 28.w;
@ -700,14 +664,16 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
color: const Color(0xFF081915),
child: Stack(
children: [
if (_shouldMountWebView)
Positioned(
top: -topCrop,
left: 0,
right: 0,
bottom: bottomFrameHeight,
child: _buildWebView(),
Positioned(
top: -topCrop,
left: 0,
right: 0,
bottom: bottomFrameHeight,
child: WebViewWidget(
controller: _controller,
gestureRecognizers: _webGestureRecognizers,
),
),
if (bottomFrameHeight > 0)
Positioned(
left: 0,

View File

@ -6,7 +6,6 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:webview_flutter/webview_flutter.dart';
import 'package:webview_flutter_android/webview_flutter_android.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
import 'package:yumi/modules/room_game/bridge/leader_js_bridge.dart';
import 'package:yumi/modules/room_game/data/models/room_game_models.dart';
@ -51,7 +50,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
bool _isClosing = false;
bool _didReceiveBridgeMessage = false;
bool _didFinishPageLoad = false;
bool _shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
int _bridgeInjectCount = 0;
String? _errorMessage;
@ -107,7 +105,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
onPageFinished: (String url) async {
_didFinishPageLoad = true;
_log('page_finished url=${_clip(url, 240)}');
_showWebView(reason: 'page_finished');
await _injectBridge(reason: 'page_finished');
},
onWebResourceError: (WebResourceError error) {
@ -139,7 +136,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
void _prepareForPageLoad() {
_didReceiveBridgeMessage = false;
_didFinishPageLoad = false;
_shouldMountWebView = defaultTargetPlatform != TargetPlatform.android;
_bridgeInjectCount = 0;
_stopBridgeBootstrap(reason: 'prepare_page_load');
_bridgeBootstrapTimer = Timer.periodic(const Duration(milliseconds: 250), (
@ -159,7 +155,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
return;
}
_log('loading_fallback_fire');
_showWebView(reason: 'loading_fallback');
setState(() {
_isLoading = false;
});
@ -279,7 +274,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
if (!mounted) {
return;
}
_showWebView(reason: 'load_complete');
setState(() {
_isLoading = false;
_errorMessage = null;
@ -300,16 +294,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
}
}
void _showWebView({required String reason}) {
if (_shouldMountWebView || !mounted) {
return;
}
_log('mount_webview reason=$reason');
setState(() {
_shouldMountWebView = true;
});
}
Future<void> _reload() async {
if (!mounted) {
return;
@ -401,26 +385,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
return fallback;
}
Widget _buildWebView() {
final params = PlatformWebViewWidgetCreationParams(
controller: _controller.platform,
gestureRecognizers: _webGestureRecognizers,
);
if (defaultTargetPlatform == TargetPlatform.android) {
return WebViewWidget.fromPlatformCreationParams(
params:
AndroidWebViewWidgetCreationParams.fromPlatformWebViewWidgetCreationParams(
params,
displayWithHybridComposition: true,
),
);
}
return WebViewWidget(
controller: _controller,
gestureRecognizers: _webGestureRecognizers,
);
}
void _log(String message) {
if (kDebugMode) {
debugPrint('$_logPrefix $message');
@ -606,14 +570,16 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
color: const Color(0xFF081915),
child: Stack(
children: [
if (_shouldMountWebView)
Positioned(
left: 0,
right: 0,
top: 0,
bottom: bottomFrameHeight,
child: _buildWebView(),
Positioned(
left: 0,
right: 0,
top: 0,
bottom: bottomFrameHeight,
child: WebViewWidget(
controller: _controller,
gestureRecognizers: _webGestureRecognizers,
),
),
if (bottomFrameHeight > 0)
Positioned(
left: 0,

View File

@ -1,6 +1,7 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:dio/dio.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
import 'package:yumi/app/routes/sc_routes.dart';
import 'package:yumi/app/routes/sc_fluro_navigator.dart';
@ -104,49 +105,60 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
} else if (authType == SCAuthType.APPLE.name) {
this.authType = authType;
try {
_authDebugLog('apple sign-in start');
final isAvailable = await SignInWithApple.isAvailable();
_authDebugLog('apple sign-in availability=$isAvailable');
if (!isAvailable) {
SCTts.show("Sign in with Apple is not available on this device.");
return;
}
final appleCredential = await SignInWithApple.getAppleIDCredential(
scopes: [
AppleIDAuthorizationScopes.email,
AppleIDAuthorizationScopes.fullName, //
],
);
uid = appleCredential.userIdentifier ?? "";
SCLoadingManager.show();
if (uid.isNotEmpty) {
await DataPersistence.setPendingChannelAuth(authType, uid);
var user = await SCAccountRepository().loginForChannel(authType, uid);
if (!context.mounted) {
SCLoadingManager.hide();
return;
}
AccountStorage().setCurrentUser(user);
await DataPersistence.clearPendingChannelAuth();
if (!context.mounted) {
SCLoadingManager.hide();
return;
}
final canContinue = await SCVersionUtils.checkReview(
context: context,
);
if (!context.mounted || !canContinue) {
SCLoadingManager.hide();
return;
}
await Provider.of<RtcProvider>(
context,
listen: false,
).resetLocalRoomState(
fallbackRtmProvider: Provider.of<RtmProvider>(
context,
listen: false,
),
);
if (!context.mounted) {
return;
}
SCLoadingManager.hide();
notifyListeners();
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
_authDebugLog(
'apple credential success userIdentifier=${_maskForDebug(uid)} '
'hasAuthorizationCode=${appleCredential.authorizationCode.isNotEmpty} '
'hasIdentityToken=${(appleCredential.identityToken ?? "").isNotEmpty} '
'hasEmail=${(appleCredential.email ?? "").isNotEmpty}',
);
if (uid.isEmpty) {
SCTts.show("Apple authorization failed. Please try again.");
return;
}
SCLoadingManager.show();
await DataPersistence.setPendingChannelAuth(authType, uid);
var user = await SCAccountRepository().loginForChannel(authType, uid);
if (!context.mounted) {
SCLoadingManager.hide();
return;
}
AccountStorage().setCurrentUser(user);
await DataPersistence.clearPendingChannelAuth();
if (!context.mounted) {
SCLoadingManager.hide();
return;
}
final canContinue = await SCVersionUtils.checkReview(context: context);
if (!context.mounted || !canContinue) {
SCLoadingManager.hide();
return;
}
await Provider.of<RtcProvider>(
context,
listen: false,
).resetLocalRoomState(
fallbackRtmProvider: Provider.of<RtmProvider>(context, listen: false),
);
if (!context.mounted) {
return;
}
SCLoadingManager.hide();
notifyListeners();
SCNavigatorUtils.push(context, SCRoutes.home, replace: true);
} catch (e) {
SCLoadingManager.hide();
_showAuthError(e);
@ -162,13 +174,56 @@ class SocialChatAuthenticationManager extends ChangeNotifier {
}
void _showAuthError(Object error) {
_authDebugLog('authenticateUser error: $error');
if (error is DioException) {
return;
}
if (error is SignInWithAppleAuthorizationException) {
_showAppleAuthorizationError(error);
return;
}
if (error is SignInWithAppleNotSupportedException) {
SCTts.show("Sign in with Apple is not available on this device.");
return;
}
final message = error.toString().replaceFirst("Exception: ", "").trim();
if (message.isNotEmpty) {
SCTts.show(message);
}
}
void _showAppleAuthorizationError(
SignInWithAppleAuthorizationException error,
) {
if (error.code == AuthorizationErrorCode.canceled) {
return;
}
if (error.code == AuthorizationErrorCode.unknown) {
SCTts.show(
"Apple sign-in is not available. Please check the app signing profile.",
);
return;
}
SCTts.show("Apple authorization failed. Please try again.");
}
void _authDebugLog(String message) {
if (kDebugMode) {
debugPrint('[Auth][Apple] $message');
}
}
String _maskForDebug(String value) {
if (value.isEmpty) {
return '<empty>';
}
if (value.length <= 8) {
return '***';
}
return '${value.substring(0, 4)}...${value.substring(value.length - 4)}';
}
}

View File

@ -72,6 +72,45 @@ class SCAccountRepository implements SocialChatUserRepository {
}
}
void _authDebugLog(String message) {
if (kDebugMode) {
debugPrint('[Auth][API] $message');
}
}
Map<String, dynamic> _sanitizeAuthPayload(Map<String, dynamic> payload) {
return payload.map((key, value) {
final lowerKey = key.toLowerCase();
if (lowerKey.contains('openid') ||
lowerKey.contains('token') ||
lowerKey.contains('sign') ||
lowerKey.contains('pwd')) {
return MapEntry(key, _maskForDebug(value?.toString() ?? ''));
}
return MapEntry(key, value);
});
}
String _maskForDebug(String value) {
if (value.isEmpty) {
return '<empty>';
}
if (value.length <= 8) {
return '***';
}
return '${value.substring(0, 4)}...${value.substring(value.length - 4)}';
}
String _loginResultSummary(SocialChatLoginRes result) {
return jsonEncode({
'hasToken': (result.token ?? '').isNotEmpty,
'hasUserSig': (result.userSig ?? '').isNotEmpty,
'userId': result.userProfile?.id,
'account': result.userProfile?.account,
'nickname': result.userProfile?.userNickname,
});
}
String _cpPairSummary(CPRes pair) {
return '{relationType=${pair.relationType}, status=${pair.status}, '
'me=${pair.meUserId}/${pair.meAccount}, '
@ -198,11 +237,17 @@ class SCAccountRepository implements SocialChatUserRepository {
) async {
final data = <String, dynamic>{"authType": authType, "openId": openId};
data.addAll(await SCMobileLoginContext.loginBodyFields());
_authDebugLog(
'request POST /auth/account/login/channel params=${jsonEncode(_sanitizeAuthPayload(data))}',
);
final result = await http.post<SocialChatLoginRes>(
"e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44",
data: data,
fromJson: (json) => SocialChatLoginRes.fromJson(json),
);
_authDebugLog(
'response POST /auth/account/login/channel result=${_loginResultSummary(result)}',
);
return result;
}

View File

@ -186,7 +186,7 @@ showBottomInBottomDialog(
Color? barrierColor,
bool barrierDismissible = true,
}) {
showGeneralDialog(
return showGeneralDialog(
context: context,
barrierLabel: '',
barrierDismissible: barrierDismissible,

View File

@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
@ -17,12 +18,14 @@ class SCGiftMp4PlayRequest {
required this.sourceType,
required this.token,
required this.muted,
this.vapLayout,
});
final String path;
final SCGiftMp4SourceType sourceType;
final int token;
final bool muted;
final Map<String, Object?>? vapLayout;
}
class SCGiftMp4Controller extends ChangeNotifier {
@ -94,10 +97,10 @@ class SCGiftMp4Controller extends ChangeNotifier {
} catch (_) {}
}
Future<void> playAsset(String path) =>
Future<void> playAsset(String path) async =>
_play(path: path, sourceType: SCGiftMp4SourceType.asset);
Future<void> playFile(String path) =>
Future<void> playFile(String path) async =>
_play(path: path, sourceType: SCGiftMp4SourceType.file);
Future<void> _play({
@ -110,6 +113,10 @@ class SCGiftMp4Controller extends ChangeNotifier {
);
return;
}
final layout = await _parseVapLayout(path: path, sourceType: sourceType);
if (_disposed) {
return;
}
_unregisterActiveView();
_activeViewId = null;
_request = SCGiftMp4PlayRequest(
@ -117,6 +124,7 @@ class SCGiftMp4Controller extends ChangeNotifier {
sourceType: sourceType,
token: ++_nextToken,
muted: _muted,
vapLayout: layout,
);
debugPrint(
'[GiftFx][mp4] request token=$_nextToken source=${sourceType.name} path=$path',
@ -266,16 +274,21 @@ class _GiftMp4EffectViewState extends State<GiftMp4EffectView> {
return const SizedBox.shrink();
}
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
final creationParams = <String, Object>{
'path': request.path,
'sourceType': request.sourceType.name,
'token': request.token,
'muted': request.muted,
'renderMode': 'auto',
};
final vapLayout = request.vapLayout;
if (vapLayout != null) {
creationParams['vapLayout'] = vapLayout;
}
return AndroidView(
key: ValueKey('gift-mp4-${request.token}-${request.path}'),
viewType: 'gift_mp4_video_view',
creationParams: <String, Object>{
'path': request.path,
'sourceType': request.sourceType.name,
'token': request.token,
'muted': request.muted,
'renderMode': 'auto',
},
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated:
(viewId) =>
@ -283,15 +296,20 @@ class _GiftMp4EffectViewState extends State<GiftMp4EffectView> {
);
}
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.iOS) {
final creationParams = <String, Object>{
'path': request.path,
'sourceType': request.sourceType.name,
'token': request.token,
'muted': request.muted,
};
final vapLayout = request.vapLayout;
if (vapLayout != null) {
creationParams['vapLayout'] = vapLayout;
}
return UiKitView(
key: ValueKey('gift-mp4-${request.token}-${request.path}'),
viewType: 'gift_mp4_video_view',
creationParams: <String, Object>{
'path': request.path,
'sourceType': request.sourceType.name,
'token': request.token,
'muted': request.muted,
},
creationParams: creationParams,
creationParamsCodec: const StandardMessageCodec(),
onPlatformViewCreated:
(viewId) =>
@ -313,6 +331,176 @@ class _GiftMp4EffectViewState extends State<GiftMp4EffectView> {
}
}
Future<Map<String, Object?>?> _parseVapLayout({
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();
final jsonText = _extractVapcJson(bytes);
if (jsonText == null) {
return null;
}
final root = jsonDecode(jsonText);
if (root is! Map) {
return null;
}
final info = root['info'];
if (info is! Map) {
return null;
}
final videoWidth =
_readPositiveInt(info['videoW']) ??
_readPositiveInt(info['video_width']);
final videoHeight =
_readPositiveInt(info['videoH']) ??
_readPositiveInt(info['video_height']);
if (videoWidth == null || videoHeight == null) {
return null;
}
final color = _readFrame(info['rgbFrame']) ?? _readFrame(info['rgb_frame']);
final alpha = _readFrame(info['aFrame']) ?? _readFrame(info['a_frame']);
if (color == null || alpha == null) {
return null;
}
final contentWidth = _readPositiveInt(info['w']) ?? videoWidth;
final contentHeight = _readPositiveInt(info['h']) ?? videoHeight;
final layout = <String, Object?>{
'colorRect': _normalizeFrame(color, videoWidth, videoHeight),
'alphaRect': _normalizeFrame(alpha, videoWidth, videoHeight),
'hasAlphaMask': true,
'contentAspectRatio': contentWidth / contentHeight,
};
debugPrint('[GiftFx][mp4] parsed vapLayout path=$path layout=$layout');
return layout;
} catch (error) {
debugPrint('[GiftFx][mp4] parse vapLayout failed path=$path error=$error');
return null;
}
}
String? _extractVapcJson(Uint8List bytes) {
const marker = [0x76, 0x61, 0x70, 0x63]; // vapc
final markerIndex = _indexOfBytes(bytes, marker, 0);
if (markerIndex < 0) {
return null;
}
final searchEnd =
(markerIndex + marker.length + 512 * 1024).clamp(0, bytes.length).toInt();
for (var index = markerIndex + marker.length; index < searchEnd; index++) {
if (bytes[index] != 0x7b) {
continue;
}
final end = _findJsonObjectEnd(bytes, index, searchEnd);
if (end > index) {
return utf8.decode(bytes.sublist(index, end + 1));
}
}
return null;
}
int _indexOfBytes(Uint8List bytes, List<int> pattern, int start) {
if (pattern.isEmpty || bytes.length < pattern.length) {
return -1;
}
for (var index = start; index <= bytes.length - pattern.length; index++) {
var matches = true;
for (var patternIndex = 0; patternIndex < pattern.length; patternIndex++) {
if (bytes[index + patternIndex] != pattern[patternIndex]) {
matches = false;
break;
}
}
if (matches) {
return index;
}
}
return -1;
}
int _findJsonObjectEnd(Uint8List bytes, int start, int maxEnd) {
var depth = 0;
var inString = false;
var escaping = false;
for (var index = start; index < maxEnd; index++) {
final byte = bytes[index];
if (inString) {
if (escaping) {
escaping = false;
} else if (byte == 0x5c) {
escaping = true;
} else if (byte == 0x22) {
inString = false;
}
} else if (byte == 0x22) {
inString = true;
} else if (byte == 0x7b) {
depth += 1;
} else if (byte == 0x7d) {
depth -= 1;
if (depth == 0) {
return index;
}
}
}
return -1;
}
int? _readPositiveInt(Object? value) {
final resolved =
value is num
? value.toInt()
: value is String
? int.tryParse(value.trim())
: null;
return resolved != null && resolved > 0 ? resolved : null;
}
List<int>? _readFrame(Object? value) {
final values =
value is List
? value.map(_readPositiveInt).toList()
: value is String
? value.split(',').map((item) => int.tryParse(item.trim())).toList()
: null;
if (values == null ||
values.length < 4 ||
values[0] == null ||
values[1] == null ||
values[2] == null ||
values[3] == null ||
values[2]! <= 0 ||
values[3]! <= 0) {
return null;
}
return [values[0]!, values[1]!, values[2]!, values[3]!];
}
List<double> _normalizeFrame(
List<int> values,
int videoWidth,
int videoHeight,
) {
return <double>[
values[0] / videoWidth,
values[1] / videoHeight,
values[2] / videoWidth,
values[3] / videoHeight,
];
}
extension on String {
String removePrefix(String prefix) {
if (startsWith(prefix)) {
return substring(prefix.length);
}
return this;
}
}
class _FallbackMp4EffectVideo extends StatefulWidget {
const _FallbackMp4EffectVideo({
required this.request,

View File

@ -1781,7 +1781,7 @@ packages:
source: hosted
version: "4.4.2"
webview_flutter_android:
dependency: "direct main"
dependency: transitive
description:
name: webview_flutter_android
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"

View File

@ -55,10 +55,9 @@ dependencies:
flutter_cache_manager: ^3.0.0
#路由
fluro: 2.0.5
#权限请求
#权限请求
permission_handler: ^12.0.0+1
webview_flutter: 4.4.2
webview_flutter_android: 3.16.9
#跳转外链
url_launcher: ^6.3.1
#键值对存储