第一次送礼物动画卡顿问题
This commit is contained in:
parent
dc6795e00e
commit
2e9bb9bddc
File diff suppressed because it is too large
Load Diff
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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)}';
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -186,7 +186,7 @@ showBottomInBottomDialog(
|
||||
Color? barrierColor,
|
||||
bool barrierDismissible = true,
|
||||
}) {
|
||||
showGeneralDialog(
|
||||
return showGeneralDialog(
|
||||
context: context,
|
||||
barrierLabel: '',
|
||||
barrierDismissible: barrierDismissible,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
#键值对存储
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user