yumi-flutter/lib/services/audio/trtc_room_rtc_engine_adapter.dart
2026-05-16 01:04:49 +08:00

594 lines
16 KiB
Dart

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<TencentRoomTrtcCloud> 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<RoomTrtcCloud> 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<RoomTrtcCloud> Function() _cloudFactory;
final RoomRtcVoiceVolumeListener? onVoiceVolume;
final RoomMusicMixingStateListener? onAudioMixingStateChanged;
final RoomRtcRoomErrorListener? onRoomError;
final RoomRtcRoomExitListener? onRoomExit;
final Duration enterRoomTimeout;
RoomTrtcCloud? _cloud;
RoomTrtcAudioEffectManager? _audioEffectManager;
TRTCCloudListener? _listener;
Completer<void>? _enterRoomCompleter;
Completer<void>? _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<void> prewarm() async {}
Future<RoomTrtcCloud> _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<void> 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<void>();
_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<bool> 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<bool> 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<void> 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<void> pauseAudioMixing() async {
_musicManager?.pausePlayMusic(_roomMusicId);
onAudioMixingStateChanged?.call(
RoomRtcAudioMixingState.paused,
RoomRtcAudioMixingReason.ok,
);
}
@override
Future<void> resumeAudioMixing() async {
_musicManager?.resumePlayMusic(_roomMusicId);
onAudioMixingStateChanged?.call(
RoomRtcAudioMixingState.playing,
RoomRtcAudioMixingReason.ok,
);
}
@override
Future<void> stopAudioMixing() async {
_musicManager?.stopPlayMusic(_roomMusicId);
onAudioMixingStateChanged?.call(
RoomRtcAudioMixingState.stopped,
RoomRtcAudioMixingReason.stoppedByUser,
);
}
@override
Future<void> setAudioMixingPosition(int positionMs) async {
_musicManager?.seekMusicToPosInTime(_roomMusicId, positionMs);
}
@override
Future<int> getAudioMixingPosition() async {
return _musicManager?.getMusicCurrentPosInMS(_roomMusicId) ?? 0;
}
@override
Future<int> getAudioMixingDuration() async {
final path = (_currentMusicPath ?? '').trim();
if (path.isEmpty) {
return 0;
}
return _musicManager?.getMusicDurationInMS(path) ?? 0;
}
@override
Future<void> setAudioMixingVolume(int volume) async {
final safeVolume = volume.clamp(0, 100).toInt();
_musicManager?.setMusicPlayoutVolume(_roomMusicId, safeVolume);
_musicManager?.setMusicPublishVolume(_roomMusicId, safeVolume);
}
@override
Future<void> 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<void>();
_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<void> 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;
}
}