麦位bug

This commit is contained in:
roxy 2026-04-29 09:56:01 +08:00
parent 78c3a95160
commit 1b4d0c40c6
8 changed files with 624 additions and 249 deletions

View File

@ -502,7 +502,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = S9X2AJ2US9;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -511,7 +511,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.2;
MARKETING_VERSION = 1.2.5;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -693,7 +693,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -702,7 +702,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.2;
MARKETING_VERSION = 1.2.5;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
@ -722,7 +722,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerRelease.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 6;
CURRENT_PROJECT_VERSION = 7;
DEVELOPMENT_TEAM = F33K8VUZ62;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@ -731,7 +731,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.2;
MARKETING_VERSION = 1.2.5;
PRODUCT_BUNDLE_IDENTIFIER = com.org.yumiparty;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";

View File

@ -1,5 +1,3 @@
import 'dart:convert';
import 'package:flutter/material.dart';
const double roomGameDesignWidth = 750;
@ -64,79 +62,115 @@ String resolveRoomGameScreenMode(
return fullScreen ? 'full' : 'half';
}
String buildRoomGameNativeFitScript({
required double visibleHeight,
required double expectedHeight,
}) {
final options = jsonEncode(<String, dynamic>{
'visibleHeight': _roundMetric(visibleHeight),
'expectedHeight': _roundMetric(expectedHeight),
});
return '''
(function() {
const options = $options;
const fitKey = '__roomGameNativeViewportFit';
function applyFit() {
try {
const visibleHeight = Number(options.visibleHeight) || window.innerHeight || 0;
const expectedHeight = Number(options.expectedHeight) || 0;
if (!document.body || !visibleHeight || !expectedHeight) {
return;
}
const rawScale = visibleHeight / expectedHeight;
const scale = Math.min(1, rawScale);
window[fitKey] = {
scale: scale,
visibleHeight: visibleHeight,
expectedHeight: expectedHeight,
compact: scale < 0.999
};
if (scale >= 0.999) {
document.documentElement.style.overflow = '';
document.body.style.overflow = '';
document.body.style.transform = '';
document.body.style.transformOrigin = '';
document.body.style.width = '';
document.body.style.minHeight = '';
document.documentElement.removeAttribute('data-room-game-native-fit');
return;
}
document.documentElement.setAttribute('data-room-game-native-fit', 'true');
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
document.body.style.transform = 'scale(' + scale + ')';
document.body.style.transformOrigin = 'top left';
document.body.style.width = (100 / scale) + '%';
document.body.style.minHeight = expectedHeight + 'px';
} catch (_) {}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', applyFit, { once: true });
} else {
applyFit();
}
[50, 250, 800, 1600, 3000].forEach(function(delay) {
window.setTimeout(applyFit, delay);
});
if (!window.__roomGameNativeFitResizeReady) {
window.__roomGameNativeFitResizeReady = true;
window.addEventListener('resize', applyFit, true);
window.addEventListener('orientationchange', function() {
window.setTimeout(applyFit, 120);
}, true);
}
})();
''';
}
double _roundMetric(double value) {
if (!value.isFinite) {
double resolveRoomGameCompatibilityBottomHeight(BuildContext context) {
final mediaQuery = MediaQuery.maybeOf(context);
if (mediaQuery == null) {
return 0;
}
return double.parse(value.toStringAsFixed(2));
final isClassicScreen = mediaQuery.padding.bottom <= 0.5;
if (!isClassicScreen) {
return 0;
}
return (mediaQuery.size.width * 0.19).clamp(56.0, 88.0).toDouble();
}
class RoomGameCompatibilityBottomFrame extends StatelessWidget {
const RoomGameCompatibilityBottomFrame({super.key, required this.height});
final double height;
@override
Widget build(BuildContext context) {
if (height <= 0) {
return const SizedBox.shrink();
}
return SizedBox(
height: height,
width: double.infinity,
child: CustomPaint(painter: _RoomGameBottomFramePainter()),
);
}
}
class _RoomGameBottomFramePainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final radius = Radius.circular(size.height * 0.18);
final frameRect = RRect.fromRectAndCorners(
Offset.zero & size,
topLeft: radius,
topRight: radius,
);
final backgroundPaint =
Paint()
..shader = const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[Color(0xFF0E392E), Color(0xFF082A23)],
).createShader(Offset.zero & size);
canvas.drawRRect(frameRect, backgroundPaint);
canvas.save();
canvas.clipRRect(frameRect);
final patternPaint =
Paint()
..color = const Color(0xFF2C5B4A).withValues(alpha: 0.20)
..style = PaintingStyle.stroke
..strokeWidth = 1.5;
final motifWidth = size.width / 8;
final motifHeight = size.height / 2.4;
for (double x = -motifWidth; x < size.width + motifWidth; x += motifWidth) {
for (
double y = -motifHeight * 0.2;
y < size.height + motifHeight;
y += motifHeight * 0.72
) {
final rect = Rect.fromCenter(
center: Offset(x + motifWidth * 0.52, y + motifHeight * 0.55),
width: motifWidth * 0.9,
height: motifHeight,
);
canvas.drawArc(rect, -0.35, 4.5, false, patternPaint);
canvas.drawArc(
rect.translate(motifWidth * 0.34, 0),
2.75,
3.6,
false,
patternPaint,
);
}
}
canvas.restore();
final borderPaint =
Paint()
..color = const Color(0xFFE9D58A)
..style = PaintingStyle.stroke
..strokeWidth = 3;
final innerBorderPaint =
Paint()
..color = const Color(0xFF8F7136).withValues(alpha: 0.75)
..style = PaintingStyle.stroke
..strokeWidth = 1;
canvas.drawRRect(frameRect.deflate(1.5), borderPaint);
canvas.drawRRect(frameRect.deflate(5), innerBorderPaint);
final highlightPaint =
Paint()
..shader = const LinearGradient(
colors: <Color>[
Color(0x00F6E8A7),
Color(0x99F6E8A7),
Color(0x00F6E8A7),
],
).createShader(Rect.fromLTWH(0, 0, size.width, 3));
canvas.drawRect(Rect.fromLTWH(0, 0, size.width, 3), highlightPaint);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -106,7 +106,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
_didFinishPageLoad = true;
_log('page_finished url=${_clip(url, 240)}');
await _injectBridge(reason: 'page_finished');
await _applyNativeViewportFit(reason: 'page_finished');
},
onWebResourceError: (WebResourceError error) {
_log(
@ -274,7 +273,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
jsCallbackPath: jsCallbackPath,
),
);
await _applyNativeViewportFit(reason: 'send_config');
} catch (error) {
_log('send_config_error error=${_clip(error.toString(), 300)}');
}
@ -332,7 +330,6 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
_isLoading = false;
_errorMessage = null;
});
await _applyNativeViewportFit(reason: 'game_loaded');
break;
case BaishunBridgeActions.gameRecharge:
_log('game_recharge open_wallet');
@ -636,35 +633,12 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
return screenSize.width / ratio;
}
Future<void> _applyNativeViewportFit({String reason = 'manual'}) async {
final topCrop = 28.w;
final visibleHeight = _calculateWebViewHeight(context) + topCrop;
final expectedHeight = _calculatePreferredWebViewHeight(context);
if (expectedHeight <= visibleHeight + 1) {
return;
}
_log(
'native_fit reason=$reason visible=${visibleHeight.toStringAsFixed(1)} '
'expected=${expectedHeight.toStringAsFixed(1)}',
);
try {
await _controller.runJavaScript(
buildRoomGameNativeFitScript(
visibleHeight: visibleHeight,
expectedHeight: expectedHeight,
),
);
} catch (error) {
_log(
'native_fit_error reason=$reason error=${_clip(error.toString(), 300)}',
);
}
}
@override
Widget build(BuildContext context) {
final topCrop = 28.w;
final webViewHeight = _calculateWebViewHeight(context);
final bottomFrameHeight = resolveRoomGameCompatibilityBottomHeight(context);
final dialogHeight = webViewHeight + bottomFrameHeight;
return PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) async {
@ -684,7 +658,7 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
),
child: Container(
width: ScreenUtil().screenWidth,
height: webViewHeight,
height: dialogHeight,
color: const Color(0xFF081915),
child: Stack(
children: [
@ -692,12 +666,22 @@ class _BaishunGamePageState extends State<BaishunGamePage> {
top: -topCrop,
left: 0,
right: 0,
bottom: 0,
bottom: bottomFrameHeight,
child: WebViewWidget(
controller: _controller,
gestureRecognizers: _webGestureRecognizers,
),
),
if (bottomFrameHeight > 0)
Positioned(
left: 0,
right: 0,
bottom: 0,
height: bottomFrameHeight,
child: RoomGameCompatibilityBottomFrame(
height: bottomFrameHeight,
),
),
if (_errorMessage != null) _buildErrorState(),
if (_isLoading && _errorMessage == null)
const IgnorePointer(

View File

@ -219,7 +219,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
await _controller.runJavaScript(
LeaderJsBridge.bootstrapScript(launchPayload: _buildLaunchPayload()),
);
await _applyNativeViewportFit(reason: reason);
} catch (error) {
_log(
'inject_bridge_error reason=$reason error=${_clip(error.toString(), 300)}',
@ -387,35 +386,6 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
return fallback;
}
Future<void> _applyNativeViewportFit({String reason = 'manual'}) async {
final screenMode = _resolveScreenMode();
if (screenMode == 'full') {
return;
}
final headerHeight = 44.w;
final visibleHeight = _calculateBodyHeight(context, headerHeight);
final expectedHeight = _calculatePreferredBodyHeight(context);
if (expectedHeight <= visibleHeight + 1) {
return;
}
_log(
'native_fit reason=$reason visible=${visibleHeight.toStringAsFixed(1)} '
'expected=${expectedHeight.toStringAsFixed(1)}',
);
try {
await _controller.runJavaScript(
buildRoomGameNativeFitScript(
visibleHeight: visibleHeight,
expectedHeight: expectedHeight,
),
);
} catch (error) {
_log(
'native_fit_error reason=$reason error=${_clip(error.toString(), 300)}',
);
}
}
void _log(String message) {
debugPrint('$_logPrefix $message');
}
@ -576,6 +546,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
Widget build(BuildContext context) {
final headerHeight = 44.w;
final bodyHeight = _calculateBodyHeight(context, headerHeight);
final bottomFrameHeight = resolveRoomGameCompatibilityBottomHeight(context);
return PopScope(
canPop: false,
onPopInvokedWithResult: (bool didPop, Object? result) async {
@ -595,7 +566,7 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
),
child: Container(
width: ScreenUtil().screenWidth,
height: bodyHeight + headerHeight,
height: bodyHeight + headerHeight + bottomFrameHeight,
color: const Color(0xFF081915),
child: Stack(
children: [
@ -610,12 +581,22 @@ class _LeaderGamePageState extends State<LeaderGamePage> {
left: 0,
right: 0,
top: headerHeight,
bottom: 0,
bottom: bottomFrameHeight,
child: WebViewWidget(
controller: _controller,
gestureRecognizers: _webGestureRecognizers,
),
),
if (bottomFrameHeight > 0)
Positioned(
left: 0,
right: 0,
bottom: 0,
height: bottomFrameHeight,
child: RoomGameCompatibilityBottomFrame(
height: bottomFrameHeight,
),
),
if (_errorMessage != null) _buildErrorState(),
if (_isLoading && _errorMessage == null)
const IgnorePointer(

View File

@ -57,16 +57,25 @@ class RealTimeCommunicationManager extends ChangeNotifier {
static const Duration _giftTriggeredMicRefreshMinInterval = Duration(
milliseconds: 900,
);
static const Duration _agoraJoinTimeout = Duration(seconds: 8);
static const Duration _agoraDisconnectedGracePeriod = Duration(seconds: 12);
bool needUpDataUserInfo = false;
bool _roomVisualEffectsEnabled = false;
bool _isExitingCurrentVoiceRoomSession = false;
bool _isHandlingAgoraRoomFailure = false;
bool _agoraJoined = false;
bool _agoraRoleIsBroadcaster = false;
RoomStartupStatus _roomStartupStatus = RoomStartupStatus.idle;
RoomStartupFailureType _roomStartupFailureType = RoomStartupFailureType.none;
int _roomStartupFailureToken = 0;
bool _isHandlingRoomStartupFailure = false;
Timer? _micListPollingTimer;
Timer? _onlineUsersPollingTimer;
Timer? _joinAgoraTimeoutTimer;
Timer? _agoraDisconnectedCleanupTimer;
Completer<void>? _joinAgoraCompleter;
String? _joiningChannelId;
bool _isRefreshingMicList = false;
bool _isRefreshingOnlineUsers = false;
bool _disableMicListRefreshForCurrentSession = false;
@ -580,6 +589,96 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return false;
}
bool _isCurrentAgoraChannel(String? channelId) {
final normalizedChannelId = (channelId ?? "").trim();
final currentRoomId =
(currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (normalizedChannelId.isEmpty) {
return false;
}
return normalizedChannelId == currentRoomId ||
normalizedChannelId == (_joiningChannelId ?? "").trim();
}
void _cancelAgoraJoinWaiter([Object? error]) {
_joinAgoraTimeoutTimer?.cancel();
_joinAgoraTimeoutTimer = null;
final completer = _joinAgoraCompleter;
_joinAgoraCompleter = null;
if (error != null && completer != null && !completer.isCompleted) {
completer.completeError(error);
}
}
void _cancelAgoraDisconnectedCleanup() {
_agoraDisconnectedCleanupTimer?.cancel();
_agoraDisconnectedCleanupTimer = null;
}
void _resetAgoraTracking({bool clearJoiningChannel = true}) {
_cancelAgoraJoinWaiter();
_cancelAgoraDisconnectedCleanup();
_agoraJoined = false;
_agoraRoleIsBroadcaster = false;
if (clearJoiningChannel) {
_joiningChannelId = null;
}
_resetLocalAudioRuntimeTracking();
}
bool get _canReportMicActive {
return _agoraJoined && _agoraRoleIsBroadcaster && isOnMai();
}
bool get _joinAgoraCompleterIsPending {
final completer = _joinAgoraCompleter;
return completer != null && !completer.isCompleted;
}
void _resyncHeartbeatFromAgoraState() {
final roomId = currenRoom?.roomProfile?.roomProfile?.id ?? "";
final canReportMicActive = _canReportMicActive;
_syncRoomHeartbeatState(
roomId: roomId,
isOnMic: canReportMicActive,
shouldRunAnchorHeartbeat: canReportMicActive,
);
}
bool _isFatalAgoraError(ErrorCodeType err) {
return err == ErrorCodeType.errInvalidArgument ||
err == ErrorCodeType.errNotReady ||
err == ErrorCodeType.errNotSupported ||
err == ErrorCodeType.errRefused ||
err == ErrorCodeType.errInvalidState ||
err == ErrorCodeType.errNoPermission ||
err == ErrorCodeType.errJoinChannelRejected ||
err == ErrorCodeType.errInvalidAppId ||
err == ErrorCodeType.errInvalidChannelName ||
err == ErrorCodeType.errTokenExpired ||
err == ErrorCodeType.errInvalidToken ||
err == ErrorCodeType.errConnectionLost;
}
bool _isFatalConnectionReason(ConnectionChangedReasonType reason) {
return reason ==
ConnectionChangedReasonType.connectionChangedBannedByServer ||
reason == ConnectionChangedReasonType.connectionChangedJoinFailed ||
reason == ConnectionChangedReasonType.connectionChangedInvalidAppId ||
reason ==
ConnectionChangedReasonType.connectionChangedInvalidChannelName ||
reason == ConnectionChangedReasonType.connectionChangedInvalidToken ||
reason == ConnectionChangedReasonType.connectionChangedTokenExpired ||
reason ==
ConnectionChangedReasonType.connectionChangedRejectedByServer ||
reason == ConnectionChangedReasonType.connectionChangedSameUidLogin ||
reason ==
ConnectionChangedReasonType.connectionChangedTooManyBroadcasters ||
reason ==
ConnectionChangedReasonType
.connectionChangedLicenseValidationFailure;
}
void _syncSelfMicRuntimeState() {
final currentUserId = AccountStorage().getCurrentUser()?.userProfile?.id;
if ((currentUserId ?? "").isEmpty) {
@ -597,6 +696,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (currentUserMic == null) {
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
_agoraRoleIsBroadcaster = false;
_syncRoomHeartbeatState(
roomId: roomId,
isOnMic: false,
@ -610,23 +710,16 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
_preferredSelfMicIndex = currentUserMic.micIndex;
final canReportMicActive = _canReportMicActive;
_syncRoomHeartbeatState(
roomId: roomId,
isOnMic: true,
shouldRunAnchorHeartbeat: true,
isOnMic: canReportMicActive,
shouldRunAnchorHeartbeat: canReportMicActive,
);
if ((currentUserMic.micMute ?? false) || isMic) {
_applyLocalAudioRuntimeState(
clientRole: ClientRoleType.clientRoleAudience,
muted: true,
);
return;
}
_applyLocalAudioRuntimeState(
clientRole: ClientRoleType.clientRoleBroadcaster,
muted: false,
muted: (currentUserMic.micMute ?? false) || isMic,
);
}
@ -641,6 +734,19 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return;
}
if (!_agoraJoined) {
if (_lastScheduledVoiceLiveRoomId != null ||
_lastScheduledAnchorRoomId != null) {
SCHeartbeatUtils.scheduleHeartbeat(
SCHeartbeatStatus.ONLINE.name,
false,
);
SCHeartbeatUtils.cancelAnchorTimer();
}
_resetHeartbeatTracking();
return;
}
final voiceLiveChanged =
_lastScheduledVoiceLiveRoomId != normalizedRoomId ||
_lastScheduledVoiceLiveOnMic != isOnMic;
@ -692,6 +798,9 @@ class RealTimeCommunicationManager extends ChangeNotifier {
required ClientRoleType clientRole,
required bool muted,
}) {
if (clientRole != ClientRoleType.clientRoleBroadcaster) {
_agoraRoleIsBroadcaster = false;
}
if (engine == null) {
_lastAppliedClientRole = null;
_lastAppliedLocalAudioMuted = null;
@ -818,7 +927,83 @@ class RealTimeCommunicationManager extends ChangeNotifier {
});
}
Future<void> _cleanupAgoraRoomState({bool clearMicSeats = false}) async {
_resetAgoraTracking();
try {
await engine?.leaveChannel();
} catch (error) {
debugPrint('[Agora] leave channel during cleanup failed: $error');
}
if (clearMicSeats) {
roomWheatMap.clear();
notifyListeners();
}
_resyncHeartbeatFromAgoraState();
}
Future<void> _handleAgoraJoinFailed(
Object error, {
required bool exitRoom,
}) async {
debugPrint('[Agora] join failed: $error');
await _cleanupAgoraRoomState(clearMicSeats: true);
if (!exitRoom || _isExitingCurrentVoiceRoomSession) {
return;
}
SCTts.show("Join room fail");
unawaited(exitCurrentVoiceRoomSession(false));
}
Future<void> _handleAgoraDisconnected(Object reason) async {
if (_isHandlingAgoraRoomFailure || _isExitingCurrentVoiceRoomSession) {
return;
}
_isHandlingAgoraRoomFailure = true;
debugPrint('[Agora] disconnected, leave room: $reason');
SCTts.show("Voice connection failed");
await _cleanupAgoraRoomState(clearMicSeats: true);
await exitCurrentVoiceRoomSession(false);
}
void _scheduleAgoraDisconnectedCleanup(Object reason) {
if (_isExitingCurrentVoiceRoomSession) {
return;
}
_agoraDisconnectedCleanupTimer?.cancel();
_agoraDisconnectedCleanupTimer = Timer(_agoraDisconnectedGracePeriod, () {
_agoraDisconnectedCleanupTimer = null;
if (!_agoraJoined && !_isExitingCurrentVoiceRoomSession) {
unawaited(_handleAgoraDisconnected(reason));
}
});
}
Future<void> joinAgoraVoiceChannel({bool throwOnError = false}) async {
final channelId = (currenRoom?.roomProfile?.roomProfile?.id ?? "").trim();
if (channelId.isEmpty) {
final error = StateError('Agora channel id is empty');
await _handleAgoraJoinFailed(error, exitRoom: !throwOnError);
if (throwOnError) {
throw error;
}
return;
}
_cancelAgoraJoinWaiter();
_cancelAgoraDisconnectedCleanup();
_joiningChannelId = channelId;
_agoraJoined = false;
_agoraRoleIsBroadcaster = false;
final joinCompleter = Completer<void>();
_joinAgoraCompleter = joinCompleter;
_joinAgoraTimeoutTimer = Timer(_agoraJoinTimeout, () {
if (!joinCompleter.isCompleted) {
joinCompleter.completeError(
TimeoutException('Agora join timeout', _agoraJoinTimeout),
);
}
});
try {
final rtcEngine = await _initAgoraRtcEngine();
rtcEngine.setAudioProfile(
@ -832,15 +1017,15 @@ class RealTimeCommunicationManager extends ChangeNotifier {
);
await rtcEngine.disableVideo();
var rtcToken = await SCAccountRepository().getRtcToken(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
channelId,
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
isPublisher: false,
);
await rtcEngine.joinChannel(
token: rtcToken.rtcToken ?? "",
channelId: currenRoom?.roomProfile?.roomProfile?.id ?? "",
channelId: channelId,
uid: _resolveAgoraUidForCurrentUser(),
options: ChannelMediaOptions(
options: const ChannelMediaOptions(
//
autoSubscribeVideo: false,
//
@ -854,15 +1039,122 @@ class RealTimeCommunicationManager extends ChangeNotifier {
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
),
);
await joinCompleter.future;
_syncSelfMicRuntimeState();
rtcEngine.muteAllRemoteAudioStreams(roomIsMute);
} catch (e) {
await _handleAgoraJoinFailed(e, exitRoom: !throwOnError);
if (throwOnError) {
rethrow;
}
SCTts.show("Join room fail");
exitCurrentVoiceRoomSession(false);
print('加入失败:${e.runtimeType},${e.toString()}');
} finally {
if (identical(_joinAgoraCompleter, joinCompleter)) {
_cancelAgoraJoinWaiter();
}
}
}
Future<bool> _switchAgoraToBroadcaster({
required String publisherToken,
required bool muted,
}) async {
if (!_agoraJoined || engine == null) {
return false;
}
try {
if (publisherToken.trim().isNotEmpty) {
await engine?.renewToken(publisherToken);
}
await engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
if (!muted) {
adjustRecordingSignalVolume(100);
}
await engine?.muteLocalAudioStream(muted);
_lastAppliedClientRole = ClientRoleType.clientRoleBroadcaster;
_lastAppliedLocalAudioMuted = muted;
_agoraRoleIsBroadcaster = true;
_resyncHeartbeatFromAgoraState();
return true;
} catch (error) {
debugPrint('[Agora] switch to broadcaster failed: $error');
_agoraRoleIsBroadcaster = false;
_resyncHeartbeatFromAgoraState();
return false;
}
}
Future<bool> _switchAgoraToAudience() async {
try {
await engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
await engine?.muteLocalAudioStream(true);
_lastAppliedClientRole = ClientRoleType.clientRoleAudience;
_lastAppliedLocalAudioMuted = true;
_agoraRoleIsBroadcaster = false;
_resyncHeartbeatFromAgoraState();
return true;
} catch (error) {
debugPrint('[Agora] switch to audience failed: $error');
return false;
}
}
Future<void> setSelfMicMutedFromBottom(bool muted) async {
isMic = muted;
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
final seatIndex = userOnMaiInIndex(currentUserId);
if (seatIndex < 0) {
notifyListeners();
return;
}
final seat = roomWheatMap[seatIndex];
final seatMuted = seat?.micMute ?? false;
if (seatMuted || muted) {
if (_agoraJoined && isOnMai()) {
await _switchAgoraToBroadcaster(
publisherToken: seat?.roomToken ?? "",
muted: true,
);
}
} else {
await _switchAgoraToBroadcaster(
publisherToken: seat?.roomToken ?? "",
muted: false,
);
}
notifyListeners();
}
Future<void> handleSelfMicRemovedByRemote({
bool refreshMicList = true,
}) async {
final currentUserId =
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "").trim();
if (currentUserId.isEmpty) {
await _switchAgoraToAudience();
return;
}
final currentSeatIndex = userOnMaiInIndex(currentUserId);
isMic = true;
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
if (currentSeatIndex > -1) {
_startSelfMicReleaseGuard(sourceIndex: currentSeatIndex);
} else {
_clearSelfMicReleaseGuard();
}
_clearUserFromSeats(currentUserId);
_agoraRoleIsBroadcaster = false;
_syncSelfMicRuntimeState();
notifyListeners();
await _switchAgoraToAudience();
if (refreshMicList) {
requestMicrophoneListRefresh(
notifyIfUnchanged: false,
minInterval: const Duration(milliseconds: 350),
);
}
}
@ -878,6 +1170,119 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_rtcEngineEventHandler = RtcEngineEventHandler(
onError: (ErrorCodeType err, String msg) {
print('rtc错误$err');
final error = 'Agora error: $err $msg';
if (!_agoraJoined) {
final completer = _joinAgoraCompleter;
if (completer != null && !completer.isCompleted) {
completer.completeError(StateError(error));
} else {
unawaited(_handleAgoraJoinFailed(error, exitRoom: true));
}
return;
}
if (_isFatalAgoraError(err)) {
unawaited(_handleAgoraDisconnected(error));
}
},
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
print('rtc 自己加入 ${connection.channelId} ${connection.localUid}');
final joinedChannelId = connection.channelId ?? "";
if (!_isCurrentAgoraChannel(joinedChannelId)) {
return;
}
_agoraJoined = true;
_agoraRoleIsBroadcaster = false;
_cancelAgoraDisconnectedCleanup();
_joinAgoraTimeoutTimer?.cancel();
_joinAgoraTimeoutTimer = null;
final completer = _joinAgoraCompleter;
if (completer != null && !completer.isCompleted) {
completer.complete();
}
_resyncHeartbeatFromAgoraState();
},
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
if (_isCurrentAgoraChannel(connection.channelId)) {
final completer = _joinAgoraCompleter;
if (completer != null && !completer.isCompleted) {
completer.completeError(
StateError('Agora left channel before join success'),
);
}
_resetAgoraTracking();
_resyncHeartbeatFromAgoraState();
}
},
onClientRoleChanged: (
RtcConnection connection,
ClientRoleType oldRole,
ClientRoleType newRole,
ClientRoleOptions newRoleOptions,
) {
if (!_isCurrentAgoraChannel(connection.channelId)) {
return;
}
_agoraRoleIsBroadcaster =
newRole == ClientRoleType.clientRoleBroadcaster;
_resyncHeartbeatFromAgoraState();
},
onClientRoleChangeFailed: (
RtcConnection connection,
ClientRoleChangeFailedReason reason,
ClientRoleType currentRole,
) {
if (!_isCurrentAgoraChannel(connection.channelId)) {
return;
}
_agoraRoleIsBroadcaster =
currentRole == ClientRoleType.clientRoleBroadcaster;
_resyncHeartbeatFromAgoraState();
},
onConnectionStateChanged: (
RtcConnection connection,
ConnectionStateType state,
ConnectionChangedReasonType reason,
) {
if (!_isCurrentAgoraChannel(connection.channelId)) {
return;
}
if (state == ConnectionStateType.connectionStateConnected &&
reason ==
ConnectionChangedReasonType.connectionChangedRejoinSuccess) {
_agoraJoined = true;
_cancelAgoraDisconnectedCleanup();
_resyncHeartbeatFromAgoraState();
return;
}
if (reason ==
ConnectionChangedReasonType.connectionChangedLeaveChannel) {
_resetAgoraTracking();
_resyncHeartbeatFromAgoraState();
return;
}
if (state == ConnectionStateType.connectionStateFailed ||
_isFatalConnectionReason(reason)) {
_agoraJoined = false;
_agoraRoleIsBroadcaster = false;
_resyncHeartbeatFromAgoraState();
if (_joinAgoraCompleterIsPending) {
_joinAgoraCompleter?.completeError(
StateError('Agora connection failed before join: $reason'),
);
} else {
unawaited(
_handleAgoraDisconnected('Agora connection failed: $reason'),
);
}
return;
}
if (state == ConnectionStateType.connectionStateDisconnected ||
state == ConnectionStateType.connectionStateReconnecting) {
_agoraJoined = false;
_agoraRoleIsBroadcaster = false;
_resyncHeartbeatFromAgoraState();
_scheduleAgoraDisconnectedCleanup(reason);
}
},
onAudioMixingStateChanged: (
AudioMixingStateType state,
@ -895,9 +1300,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
break;
}
},
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
print('rtc 自己加入 ${connection.channelId} ${connection.localUid}');
},
onUserJoined: (connection, remoteUid, elapsed) {
print('rtc用户 $remoteUid 加入了频道');
},
@ -912,7 +1314,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
var rtcToken = await SCAccountRepository().getRtcToken(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
AccountStorage().getCurrentUser()?.userProfile?.id ?? "",
isPublisher: isOnMai(),
isPublisher: _agoraRoleIsBroadcaster,
);
engine?.renewToken(rtcToken.rtcToken ?? "");
},
@ -940,6 +1342,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} catch (e) {
print('rtc销毁前离开频道出错: $e');
}
_resetAgoraTracking();
try {
await rtcEngine.release();
@ -1207,8 +1610,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
),
);
await _bootstrapRoomHeartbeatState();
///
retrieveMicrophoneList();
fetchOnlineUsersList();
@ -1222,6 +1623,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
failureType = RoomStartupFailureType.rtc;
await joinAgoraVoiceChannel(throwOnError: true);
await _bootstrapRoomHeartbeatState();
_setRoomStartupReady();
Msg joinMsg = Msg(
@ -1421,7 +1823,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
SCHeartbeatUtils.scheduleHeartbeat(SCHeartbeatStatus.ONLINE.name, false);
SCHeartbeatUtils.cancelAnchorTimer();
rtmProvider?.cleanRoomData();
await engine?.leaveChannel();
await _cleanupAgoraRoomState();
await Future.delayed(Duration(milliseconds: 100));
await rtmProvider?.quitGroup(
currenRoom!.roomProfile?.roomProfile?.roomAccount ?? "",
@ -1454,7 +1856,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (groupId.isNotEmpty) {
await rtmProvider?.quitGroup(groupId);
}
await engine?.leaveChannel();
await _cleanupAgoraRoomState();
await Future.delayed(const Duration(milliseconds: 100));
} catch (e) {
print('rtc清理本地房间状态出错: $e');
@ -1487,7 +1889,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
if (groupId.isNotEmpty) {
await rtmProvider?.quitGroup(groupId);
}
await engine?.leaveChannel();
await _cleanupAgoraRoomState(clearMicSeats: true);
await Future.delayed(const Duration(milliseconds: 100));
if (roomId.isNotEmpty) {
await SCAccountRepository().quitRoom(roomId);
@ -1508,13 +1910,14 @@ class RealTimeCommunicationManager extends ChangeNotifier {
void _clearData() {
_stopRoomStatePolling();
_resetHeartbeatTracking();
_resetLocalAudioRuntimeTracking();
_resetAgoraTracking();
_disableMicListRefreshForCurrentSession = false;
_disableOnlineUsersRefreshForCurrentSession = false;
_isSelfMicActionInFlight = false;
_roomStartupStatus = RoomStartupStatus.idle;
_roomStartupFailureType = RoomStartupFailureType.none;
_isHandlingRoomStartupFailure = false;
_isHandlingAgoraRoomFailure = false;
_pendingMicListRefresh = false;
_pendingMicListRefreshNotifyIfUnchanged = false;
_seatEmojiEventVersions.clear();
@ -1719,22 +2122,32 @@ class RealTimeCommunicationManager extends ChangeNotifier {
inviterId: inviterId,
);
///
engine?.renewToken(micGoUpRes.roomToken ?? "");
if (!micGoUpRes.micMute!) {
if (!isMic) {
engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
adjustRecordingSignalVolume(100);
await engine?.muteLocalAudioStream(false);
}
} else {
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
await engine?.muteLocalAudioStream(true);
}
SCHeartbeatUtils.scheduleAnchorHeartbeat(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
);
final targetIndex = micGoUpRes.micIndex ?? index;
final switched = await _switchAgoraToBroadcaster(
publisherToken: micGoUpRes.roomToken ?? "",
muted: (micGoUpRes.micMute ?? false) || isMic,
);
if (!switched) {
await _switchAgoraToAudience();
try {
await SCChatRoomRepository().micGoDown(
currenRoom?.roomProfile?.roomProfile?.id ?? "",
targetIndex,
);
} catch (downError) {
debugPrint(
'[Agora] release backend mic after broadcaster switch failed: $downError',
);
}
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
_clearUserFromSeats(myUser?.id);
_syncSelfMicRuntimeState();
notifyListeners();
_refreshMicListSilently(notifyIfUnchanged: true);
SCTts.show('Failed to put on the microphone, Agora switch failed');
return;
}
final previousSelfSeatIndex =
myUser != null ? userOnMaiInIndex(myUser.id ?? "") : -1;
final isSeatSwitching =
@ -1806,7 +2219,6 @@ class RealTimeCommunicationManager extends ChangeNotifier {
final currentUserSeatIndex = userOnMaiInIndex(currentUserId);
if (roomWheatMap[index]?.user?.id == currentUserId) {
isMic = true;
engine?.muteLocalAudioStream(true);
}
_clearSelfMicSwitchGuard(clearPreferredIndex: true);
if (currentUserSeatIndex > -1) {
@ -1816,9 +2228,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
}
SCHeartbeatUtils.cancelAnchorTimer();
///
engine?.renewToken("");
engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
await _switchAgoraToAudience();
_clearUserFromSeats(currentUserId);
_syncSelfMicRuntimeState();
notifyListeners();
@ -2054,14 +2464,10 @@ class RealTimeCommunicationManager extends ChangeNotifier {
true,
);
if (isOnMaiInIndex(index)) {
Provider.of<RtcProvider>(
context!,
listen: false,
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
Provider.of<RtcProvider>(
context!,
listen: false,
).engine?.muteLocalAudioStream(true);
await _switchAgoraToBroadcaster(
publisherToken: roomWheatMap[index]?.roomToken ?? "",
muted: true,
);
}
var mic = roomWheatMap[index];
if (mic != null) {
@ -2079,17 +2485,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
false,
);
if (isOnMaiInIndex(index)) {
if (!Provider.of<RtcProvider>(context!, listen: false).isMic) {
if (!isMic) {
if (roomWheatMap[index]?.user != null) {
adjustRecordingSignalVolume(100);
Provider.of<RtcProvider>(
context!,
listen: false,
).engine?.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
Provider.of<RtcProvider>(
context!,
listen: false,
).engine?.muteLocalAudioStream(false);
await _switchAgoraToBroadcaster(
publisherToken: roomWheatMap[index]?.roomToken ?? "",
muted: false,
);
}
}
}

View File

@ -2,7 +2,6 @@ import 'dart:async';
import 'dart:collection';
import 'dart:convert';
import 'dart:io';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:extended_image/extended_image.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@ -304,16 +303,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
if (memberList.isNotEmpty) {
if (memberList.first.userID ==
AccountStorage().getCurrentUser()?.userProfile?.id) {
Provider.of<RealTimeCommunicationManager>(
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
);
unawaited(
rtcProvider.handleSelfMicRemovedByRemote(refreshMicList: false),
);
///退
Provider.of<RealTimeCommunicationManager>(
context,
listen: false,
).exitCurrentVoiceRoomSession(false).whenComplete(() {
rtcProvider.exitCurrentVoiceRoomSession(false).whenComplete(() {
SCRoomUtils.closeAllDialogs();
SmartDialog.show(
tag: "showConfirmDialog",
@ -1395,16 +1394,16 @@ class RealTimeMessagingManager extends ChangeNotifier {
}
if (msg.type == SCRoomMsgType.killXiaMai) {
///
if (msg.msg == AccountStorage().getCurrentUser()?.userProfile?.id) {
Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).engine?.setClientRole(role: ClientRoleType.clientRoleAudience);
}
Provider.of<RealTimeCommunicationManager>(
final rtcProvider = Provider.of<RealTimeCommunicationManager>(
context!,
listen: false,
).retrieveMicrophoneList(notifyIfUnchanged: false);
);
if (msg.msg == AccountStorage().getCurrentUser()?.userProfile?.id) {
unawaited(
rtcProvider.handleSelfMicRemovedByRemote(refreshMicList: false),
);
}
rtcProvider.retrieveMicrophoneList(notifyIfUnchanged: false);
return;
}

View File

@ -1,4 +1,5 @@
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
@ -13,7 +14,6 @@ import 'package:yumi/ui_kit/widgets/room/room_menu_dialog.dart';
import 'package:yumi/ui_kit/widgets/room/room_msg_input.dart';
import 'package:yumi/ui_kit/components/text/sc_text.dart';
import 'package:yumi/shared/tools/sc_room_utils.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/services/audio/rtc_manager.dart';
import '../../../app/routes/sc_fluro_navigator.dart';
@ -340,32 +340,8 @@ class _RoomBottomWidgetState extends State<RoomBottomWidget> {
final provider = context.read<RtcProvider>();
setState(() {
provider.isMic = !provider.isMic;
provider.roomWheatMap.forEach((k, v) {
final seat = provider.micAtIndexForDisplay(k);
if ((seat?.user?.id ?? "").trim() ==
(AccountStorage().getCurrentUser()?.userProfile?.id ?? "")
.trim() &&
!(seat?.micMute ?? true)) {
if (!provider.isMic) {
provider.engine?.adjustRecordingSignalVolume(100);
provider.engine?.setClientRole(
role: ClientRoleType.clientRoleBroadcaster,
);
provider.engine?.muteLocalAudioStream(false);
} else {
if (provider.isMusicPlaying) {
provider.engine?.adjustRecordingSignalVolume(0);
} else {
provider.engine?.setClientRole(
role: ClientRoleType.clientRoleAudience,
);
provider.engine?.muteLocalAudioStream(provider.isMic);
}
}
}
});
});
unawaited(provider.setSelfMicMutedFromBottom(provider.isMic));
},
child: RoomBottomCircleAction(
child: Image.asset(

View File

@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.2.2+6
version: 1.2.5+7
environment: