import 'dart:async'; import 'package:tencent_rtc_sdk/trtc_cloud.dart'; import 'package:tencent_rtc_sdk/trtc_cloud_def.dart'; import 'package:tencent_rtc_sdk/trtc_cloud_listener.dart'; import 'package:tencent_rtc_sdk/tx_audio_effect_manager.dart'; import 'room_rtc_engine_adapter.dart'; import 'room_rtc_types.dart'; abstract class RoomTrtcCloud { void registerListener(TRTCCloudListener listener); void unRegisterListener(TRTCCloudListener listener); void setDefaultStreamRecvMode(bool autoRecvAudio, bool autoRecvVideo); void enableAudioVolumeEvaluation( bool enable, TRTCAudioVolumeEvaluateParams params, ); void enterRoom(TRTCParams param, TRTCAppScene scene); void exitRoom(); void switchRole(TRTCRoleType role); void startLocalAudio(TRTCAudioQuality quality); void stopLocalAudio(); void muteLocalAudio(bool mute); void muteAllRemoteAudio(bool mute); RoomTrtcAudioEffectManager getAudioEffectManager(); } abstract class RoomTrtcAudioEffectManager { void setMusicObserver(int musicId, TXMusicPlayObserver? observer); void startPlayMusic(AudioMusicParam musicParam); void stopPlayMusic(int id); void pausePlayMusic(int id); void resumePlayMusic(int id); void setMusicPublishVolume(int id, int volume); void setMusicPlayoutVolume(int id, int volume); int getMusicCurrentPosInMS(int id); int getMusicDurationInMS(String path); void seekMusicToPosInTime(int id, int pts); } class TencentRoomTrtcCloud implements RoomTrtcCloud { TencentRoomTrtcCloud._(this._cloud); static Future sharedInstance() async { return TencentRoomTrtcCloud._(await TRTCCloud.sharedInstance()); } static void destroySharedInstance() { TRTCCloud.destroySharedInstance(); } final TRTCCloud _cloud; @override void registerListener(TRTCCloudListener listener) { _cloud.registerListener(listener); } @override void unRegisterListener(TRTCCloudListener listener) { _cloud.unRegisterListener(listener); } @override void setDefaultStreamRecvMode(bool autoRecvAudio, bool autoRecvVideo) { _cloud.setDefaultStreamRecvMode(autoRecvAudio, autoRecvVideo); } @override void enableAudioVolumeEvaluation( bool enable, TRTCAudioVolumeEvaluateParams params, ) { _cloud.enableAudioVolumeEvaluation(enable, params); } @override void enterRoom(TRTCParams param, TRTCAppScene scene) { _cloud.enterRoom(param, scene); } @override void exitRoom() { _cloud.exitRoom(); } @override void switchRole(TRTCRoleType role) { _cloud.switchRole(role); } @override void startLocalAudio(TRTCAudioQuality quality) { _cloud.startLocalAudio(quality); } @override void stopLocalAudio() { _cloud.stopLocalAudio(); } @override void muteLocalAudio(bool mute) { _cloud.muteLocalAudio(mute); } @override void muteAllRemoteAudio(bool mute) { _cloud.muteAllRemoteAudio(mute); } @override RoomTrtcAudioEffectManager getAudioEffectManager() { return TencentRoomTrtcAudioEffectManager(_cloud.getAudioEffectManager()); } } class TencentRoomTrtcAudioEffectManager implements RoomTrtcAudioEffectManager { const TencentRoomTrtcAudioEffectManager(this._manager); final TXAudioEffectManager _manager; @override void setMusicObserver(int musicId, TXMusicPlayObserver? observer) { _manager.setMusicObserver(musicId, observer); } @override void startPlayMusic(AudioMusicParam musicParam) { _manager.startPlayMusic(musicParam); } @override void stopPlayMusic(int id) { _manager.stopPlayMusic(id); } @override void pausePlayMusic(int id) { _manager.pausePlayMusic(id); } @override void resumePlayMusic(int id) { _manager.resumePlayMusic(id); } @override void setMusicPublishVolume(int id, int volume) { _manager.setMusicPublishVolume(id, volume); } @override void setMusicPlayoutVolume(int id, int volume) { _manager.setMusicPlayoutVolume(id, volume); } @override int getMusicCurrentPosInMS(int id) { return _manager.getMusicCurrentPosInMS(id); } @override int getMusicDurationInMS(String path) { return _manager.getMusicDurationInMS(path); } @override void seekMusicToPosInTime(int id, int pts) { _manager.seekMusicToPosInTime(id, pts); } } class TrtcRoomRtcEngineAdapter implements RoomRtcEngineAdapter { TrtcRoomRtcEngineAdapter({ Future Function()? cloudFactory, this.onVoiceVolume, this.onAudioMixingStateChanged, this.onRoomError, this.onRoomExit, this.enterRoomTimeout = const Duration(seconds: 8), }) : _cloudFactory = cloudFactory ?? (() => TencentRoomTrtcCloud.sharedInstance()); static const int _roomMusicId = 1; final Future Function() _cloudFactory; final RoomRtcVoiceVolumeListener? onVoiceVolume; final RoomMusicMixingStateListener? onAudioMixingStateChanged; final RoomRtcRoomErrorListener? onRoomError; final RoomRtcRoomExitListener? onRoomExit; final Duration enterRoomTimeout; RoomTrtcCloud? _cloud; RoomTrtcAudioEffectManager? _audioEffectManager; TRTCCloudListener? _listener; Completer? _enterRoomCompleter; Completer? _exitRoomCompleter; String? _localUserId; String? _currentMusicPath; bool _isEntered = false; bool _isLocalAudioStarted = false; bool _expectingLocalExit = false; @override RoomRtcEngineKind get kind => RoomRtcEngineKind.trtc; @override bool get isInRoom => _isEntered; @override Future prewarm() async {} Future _ensureCloud() async { final existing = _cloud; if (existing != null) { return existing; } final cloud = await _cloudFactory(); final listener = _createListener(); cloud.registerListener(listener); _listener = listener; _cloud = cloud; return cloud; } TRTCCloudListener _createListener() { return TRTCCloudListener( onEnterRoom: (result) { final completer = _enterRoomCompleter; if (result >= 0) { _isEntered = true; if (completer != null && !completer.isCompleted) { completer.complete(); } return; } final error = StateError('TRTC enter room failed: $result'); if (completer != null && !completer.isCompleted) { completer.completeError(error); } else { onRoomError?.call(error); } }, onError: (errCode, errMsg) { final error = StateError('TRTC error: $errCode $errMsg'); final completer = _enterRoomCompleter; if (completer != null && !completer.isCompleted) { completer.completeError(error); return; } if (_isEntered) { onRoomError?.call(error); } }, onExitRoom: (reason) { final wasEntered = _isEntered; _isEntered = false; _isLocalAudioStarted = false; final completer = _exitRoomCompleter; if (completer != null && !completer.isCompleted) { completer.complete(); } final exitReason = _mapExitReason(reason); if (wasEntered && !_expectingLocalExit && exitReason != RoomRtcExitReason.local) { onRoomExit?.call(exitReason); } _expectingLocalExit = false; }, onUserVoiceVolume: (userVolumes, totalVolume) { final localUserId = (_localUserId ?? '').trim(); final mapped = userVolumes .map((item) { final normalizedUserId = item.userId.trim().isEmpty ? localUserId : item.userId.trim(); return RoomRtcUserVolume( userId: normalizedUserId, volume: item.volume, isLocalUser: localUserId.isNotEmpty && normalizedUserId == localUserId, ); }) .where((item) => item.userId.isNotEmpty) .toList(growable: false); onVoiceVolume?.call(mapped, totalVolume); }, ); } RoomRtcExitReason _mapExitReason(int reason) { switch (reason) { case 0: return RoomRtcExitReason.local; case 1: return RoomRtcExitReason.kicked; case 2: return RoomRtcExitReason.dismissed; default: return RoomRtcExitReason.unknown; } } @override Future enterRoomAsAudience(RoomRtcEnterRoomConfig config) async { if (!config.hasTrtcCredential) { final error = StateError('TRTC room credential is invalid'); if (config.throwOnError) { throw error; } onRoomError?.call(error); return; } final cloud = await _ensureCloud(); _localUserId = config.identity.trtcUserId; _isEntered = false; _isLocalAudioStarted = false; _expectingLocalExit = false; final joinCompleter = Completer(); _enterRoomCompleter = joinCompleter; try { cloud.setDefaultStreamRecvMode(true, false); cloud.enableAudioVolumeEvaluation( true, TRTCAudioVolumeEvaluateParams(interval: 500), ); cloud.enterRoom( TRTCParams( sdkAppId: config.trtcSdkAppId, userId: config.identity.trtcUserId, userSig: config.trtcUserSig.trim(), roomId: 0, strRoomId: config.identity.trtcStrRoomId, role: TRTCRoleType.audience, ), TRTCAppScene.voiceChatRoom, ); await joinCompleter.future.timeout(enterRoomTimeout); } catch (error) { try { cloud.exitRoom(); } catch (_) {} if (config.throwOnError) { rethrow; } onRoomError?.call(error); } finally { if (identical(_enterRoomCompleter, joinCompleter)) { _enterRoomCompleter = null; } } } @override Future switchToAnchor(RoomRtcAnchorConfig config) async { final cloud = _cloud; if (!_isEntered || cloud == null) { return false; } try { cloud.switchRole(TRTCRoleType.anchor); cloud.startLocalAudio(TRTCAudioQuality.defaultMode); cloud.muteLocalAudio(config.muted); _isLocalAudioStarted = true; return true; } catch (_) { return false; } } @override Future switchToAudience() async { final cloud = _cloud; if (cloud == null) { return false; } try { cloud.switchRole(TRTCRoleType.audience); cloud.muteLocalAudio(true); if (_isLocalAudioStarted) { cloud.stopLocalAudio(); } _isLocalAudioStarted = false; return true; } catch (_) { return false; } } @override void muteAllRemoteAudio(bool muted) { _cloud?.muteAllRemoteAudio(muted); } RoomTrtcAudioEffectManager? get _musicManager { final existing = _audioEffectManager; if (existing != null) { return existing; } final cloud = _cloud; if (cloud == null) { return null; } return _audioEffectManager = cloud.getAudioEffectManager(); } @override Future startAudioMixing(RoomRtcAudioMixingConfig config) async { final path = config.filePath.trim(); if (path.isEmpty) { return; } final manager = _musicManager; if (manager == null) { throw StateError('TRTC audio effect manager is not ready'); } _currentMusicPath = path; manager.setMusicObserver(_roomMusicId, _createMusicObserver()); manager.startPlayMusic( AudioMusicParam( id: _roomMusicId, path: path, loopCount: _audioMixingCycleToTrtcLoopCount(config.cycle), publish: !config.loopback, startTimeMS: config.startPos, ), ); } int _audioMixingCycleToTrtcLoopCount(int cycle) { if (cycle < 0) { return 1000000; } if (cycle <= 1) { return 0; } return cycle - 1; } TXMusicPlayObserver _createMusicObserver() { return TXMusicPlayObserver( onStart: (id, errorCode) { if (id != _roomMusicId) { return; } if (errorCode == 0) { onAudioMixingStateChanged?.call( RoomRtcAudioMixingState.playing, RoomRtcAudioMixingReason.ok, ); } else { onAudioMixingStateChanged?.call( RoomRtcAudioMixingState.failed, RoomRtcAudioMixingReason.failed, ); } }, onPlayProgress: (_, __, ___) {}, onComplete: (id, errorCode) { if (id != _roomMusicId) { return; } onAudioMixingStateChanged?.call( errorCode == 0 ? RoomRtcAudioMixingState.stopped : RoomRtcAudioMixingState.failed, errorCode == 0 ? RoomRtcAudioMixingReason.allLoopsCompleted : RoomRtcAudioMixingReason.failed, ); }, ); } @override Future pauseAudioMixing() async { _musicManager?.pausePlayMusic(_roomMusicId); onAudioMixingStateChanged?.call( RoomRtcAudioMixingState.paused, RoomRtcAudioMixingReason.ok, ); } @override Future resumeAudioMixing() async { _musicManager?.resumePlayMusic(_roomMusicId); onAudioMixingStateChanged?.call( RoomRtcAudioMixingState.playing, RoomRtcAudioMixingReason.ok, ); } @override Future stopAudioMixing() async { _musicManager?.stopPlayMusic(_roomMusicId); onAudioMixingStateChanged?.call( RoomRtcAudioMixingState.stopped, RoomRtcAudioMixingReason.stoppedByUser, ); } @override Future setAudioMixingPosition(int positionMs) async { _musicManager?.seekMusicToPosInTime(_roomMusicId, positionMs); } @override Future getAudioMixingPosition() async { return _musicManager?.getMusicCurrentPosInMS(_roomMusicId) ?? 0; } @override Future getAudioMixingDuration() async { final path = (_currentMusicPath ?? '').trim(); if (path.isEmpty) { return 0; } return _musicManager?.getMusicDurationInMS(path) ?? 0; } @override Future setAudioMixingVolume(int volume) async { final safeVolume = volume.clamp(0, 100).toInt(); _musicManager?.setMusicPlayoutVolume(_roomMusicId, safeVolume); _musicManager?.setMusicPublishVolume(_roomMusicId, safeVolume); } @override Future leaveRoom() async { final cloud = _cloud; if (cloud == null) { _isEntered = false; _isLocalAudioStarted = false; return; } try { await stopAudioMixing(); } catch (_) {} try { if (_isLocalAudioStarted) { cloud.stopLocalAudio(); } cloud.enableAudioVolumeEvaluation( false, TRTCAudioVolumeEvaluateParams(interval: 0), ); } catch (_) {} if (!_isEntered) { _isLocalAudioStarted = false; return; } final exitCompleter = Completer(); _exitRoomCompleter = exitCompleter; _expectingLocalExit = true; try { cloud.exitRoom(); await exitCompleter.future.timeout(const Duration(seconds: 3)); } catch (_) { _isEntered = false; _isLocalAudioStarted = false; _expectingLocalExit = false; } finally { if (identical(_exitRoomCompleter, exitCompleter)) { _exitRoomCompleter = null; } } } @override Future release() async { final cloud = _cloud; final listener = _listener; await leaveRoom(); try { if (cloud != null && listener != null) { cloud.unRegisterListener(listener); } TencentRoomTrtcCloud.destroySharedInstance(); } catch (_) {} _listener = null; _cloud = null; _audioEffectManager = null; _currentMusicPath = null; _localUserId = null; _isEntered = false; _isLocalAudioStarted = false; _expectingLocalExit = false; } }