594 lines
16 KiB
Dart
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;
|
|
}
|
|
}
|