refactor: M2-Call-Pilot – Module call_signaling + voice_channel mit Fassaden (Entscheidung 2)
- CallSignalingService/callSignalingProvider und VoiceChannelService/ voiceChannelProvider als einzige oeffentliche Schnittstellen - voip_manager.dart / livekit_call_manager.dart in ihre Module verschoben, Logik unangetastet (nur Klassen-Koepfe, @override, Imports, Provider-Namen) - Alle Konsumenten entkoppelt; Kompositions-Punkt matrix_client.dart als dokumentierte Ausnahme. analyze sauber, 29 Tests gruen, Windows-Start ok. Call-Klick-/Geraetetest steht aus (PROGRESS.md)
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:pyramid/features/call_signaling/voip_manager.dart';
|
||||
|
||||
/// Öffentliche Schnittstelle des Moduls `call_signaling` – natives
|
||||
/// Matrix-1:1-VoIP (`m.call.*`-Events, direkte WebRTC-Peer-Connection,
|
||||
/// kein SFU). Schnitt nach `docs/M2_MODULE_SCHNITT.md`.
|
||||
///
|
||||
/// Andere Module (UI, Settings, Shell) konsumieren AUSSCHLIESSLICH diese
|
||||
/// Schnittstelle über [callSignalingProvider] – nie `voip_manager.dart`
|
||||
/// direkt. Einzige Ausnahme ist der Kompositions-Punkt in
|
||||
/// `matrix_client.dart`, der die Implementierung konstruiert.
|
||||
///
|
||||
/// Bewusste Abweichungen vom Entwurf im Doc (Verhaltensgleichheit vor
|
||||
/// Schönheit, heiliger Call-Pfad):
|
||||
/// * ChangeNotifier statt Streams – die gesamte UI hängt heute an
|
||||
/// `ref.watch(...)` mit notifyListeners; ein Stream-Umbau wäre eine
|
||||
/// Verhaltensänderung ohne Not.
|
||||
/// * [currentCall] bleibt als matrix-SDK-`CallSession` sichtbar (die UI
|
||||
/// braucht state/room/answer/reject), ebenso die WebRTC-Renderer –
|
||||
/// das Doc sieht Letzteres ausdrücklich vor.
|
||||
abstract class CallSignalingService extends ChangeNotifier {
|
||||
/// Der laufende 1:1-Call (null = keiner). UI liest state/room und
|
||||
/// beantwortet eingehende Calls über [safeAction] + answer()/reject().
|
||||
CallSession? get currentCall;
|
||||
|
||||
rtc.RTCVideoRenderer get localRenderer;
|
||||
rtc.RTCVideoRenderer get remoteRenderer;
|
||||
|
||||
bool get isMicMuted;
|
||||
bool get isCameraMuted;
|
||||
bool get isScreensharing;
|
||||
bool get isRemoteScreensharing;
|
||||
bool get isDeafened;
|
||||
|
||||
/// Sende-Qualität ('sd' | 'hd' | 'fhd' | '4k').
|
||||
String get currentQualityKey;
|
||||
|
||||
Future<void> startCall(String roomId, {bool video});
|
||||
Future<void> hangup();
|
||||
Future<void> toggleMic();
|
||||
Future<void> toggleCamera();
|
||||
Future<void> toggleDeafen();
|
||||
|
||||
/// Auf Windows öffnet ein [context] den Quellen-Picker fürs Screensharing.
|
||||
Future<void> toggleScreenSharing([BuildContext? context]);
|
||||
|
||||
Future<void> changeQuality(String key);
|
||||
|
||||
/// Wendet die in den Einstellungen gewählte Ausgabelautstärke auf den
|
||||
/// Remote-Stream an. Ohne [value] wird der Pref-Wert gelesen.
|
||||
Future<void> applyOutputVolume([double? value]);
|
||||
|
||||
/// Führt eine Call-Aktion (answer/reject/…) fehlertolerant aus und stößt
|
||||
/// danach die interne Zustands-/Renderer-Aktualisierung an.
|
||||
Future<void> safeAction(Future<dynamic> Function() action);
|
||||
}
|
||||
|
||||
/// Einziger Zugriffsweg der UI auf das 1:1-VoIP-Modul. Die Instanz wird beim
|
||||
/// Client-Aufbau in `matrix_client.dart` erzeugt (existiert also, sobald die
|
||||
/// eingeloggte Shell steht).
|
||||
final callSignalingProvider =
|
||||
ChangeNotifierProvider<CallSignalingService>((ref) {
|
||||
return PyramidVoipManager.instance;
|
||||
});
|
||||
@@ -0,0 +1,561 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_webrtc/flutter_webrtc.dart' as rtc;
|
||||
import 'package:matrix/matrix.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:webrtc_interface/webrtc_interface.dart';
|
||||
import 'package:pyramid/core/ice_servers.dart';
|
||||
import 'package:pyramid/features/call_signaling/call_signaling_service.dart';
|
||||
import 'package:pyramid/widgets/screen_share_picker.dart';
|
||||
|
||||
/// Implementierung von [CallSignalingService] (Modul `call_signaling`).
|
||||
/// Wird ausschließlich vom Kompositions-Punkt (`matrix_client.dart`)
|
||||
/// konstruiert; alle anderen gehen über `callSignalingProvider`.
|
||||
class PyramidVoipManager extends CallSignalingService
|
||||
implements WebRTCDelegate {
|
||||
static PyramidVoipManager? _instance;
|
||||
static PyramidVoipManager get instance => _instance!;
|
||||
|
||||
final Client client;
|
||||
late final VoIP voip;
|
||||
@override
|
||||
CallSession? currentCall;
|
||||
|
||||
@override
|
||||
final rtc.RTCVideoRenderer localRenderer = rtc.RTCVideoRenderer();
|
||||
@override
|
||||
final rtc.RTCVideoRenderer remoteRenderer = rtc.RTCVideoRenderer();
|
||||
bool _renderersInitialized = false;
|
||||
|
||||
bool _isMicMuted = false;
|
||||
bool _isCameraMuted = true;
|
||||
bool _isScreensharing = false;
|
||||
bool _isRemoteScreensharing = false;
|
||||
bool _isDeafened = false;
|
||||
bool _isUpdating = false;
|
||||
String? _selectedSourceId;
|
||||
String _currentQualityKey = 'hd';
|
||||
|
||||
@override
|
||||
bool get isMicMuted => _isMicMuted;
|
||||
@override
|
||||
bool get isCameraMuted => _isCameraMuted;
|
||||
@override
|
||||
bool get isScreensharing => _isScreensharing;
|
||||
@override
|
||||
bool get isRemoteScreensharing => _isRemoteScreensharing;
|
||||
@override
|
||||
bool get isDeafened => _isDeafened;
|
||||
@override
|
||||
String get currentQualityKey => _currentQualityKey;
|
||||
|
||||
PyramidVoipManager(this.client) {
|
||||
_instance = this;
|
||||
voip = VoIP(client, this);
|
||||
// Suppress speakerphone call — not supported on Windows/Desktop
|
||||
try {
|
||||
(voip as dynamic).onConfigSpeakerphone = (bool _) async {};
|
||||
} catch (_) {}
|
||||
_ensureRenderers();
|
||||
}
|
||||
|
||||
Future<void> _ensureRenderers() async {
|
||||
if (_renderersInitialized) return;
|
||||
try {
|
||||
await localRenderer.initialize();
|
||||
await remoteRenderer.initialize();
|
||||
_renderersInitialized = true;
|
||||
} catch (e) {
|
||||
debugPrint('[VOIP] Renderer init error: $e');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
MediaDevices get mediaDevices => MediaDevicesWrapper(rtc.navigator.mediaDevices, this);
|
||||
|
||||
@override
|
||||
bool get isWeb => kIsWeb;
|
||||
|
||||
@override
|
||||
Future<RTCPeerConnection> createPeerConnection(
|
||||
Map<String, dynamic> configuration, [
|
||||
Map<String, dynamic> constraints = const {},
|
||||
]) async {
|
||||
// Zusätzliche ICE-Server (STUN/TURN aus ice.json) zu den vom Homeserver
|
||||
// gelieferten mergen — ohne STUN scheitern Calls außerhalb des LANs,
|
||||
// weil der Server hinter CGNAT hängt.
|
||||
try {
|
||||
final extra = await IceServers.get();
|
||||
final merged = Map<String, dynamic>.from(configuration);
|
||||
final existing =
|
||||
(merged['iceServers'] as List?)?.cast<Map<String, dynamic>>() ?? [];
|
||||
merged['iceServers'] = [...existing, ...extra];
|
||||
configuration = merged;
|
||||
} catch (e) {
|
||||
debugPrint('[VOIP] ICE server merge error: $e');
|
||||
}
|
||||
return rtc.createPeerConnection(configuration, constraints);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleNewCall(CallSession call) async {
|
||||
debugPrint('[VOIP] Incoming call ${call.callId} (type: ${call.type})');
|
||||
await _ensureRenderers();
|
||||
currentCall = call;
|
||||
_isCameraMuted = call.type == CallType.kVoice;
|
||||
_setupCallListeners(call);
|
||||
_updateInternalStates();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Timer? _renderRefreshTimer;
|
||||
void _setupCallListeners(CallSession call) {
|
||||
call.onCallStateChanged.stream.listen((state) {
|
||||
debugPrint('[VOIP] State: $state');
|
||||
_updateInternalStates();
|
||||
|
||||
if (state == CallState.kConnected) {
|
||||
if (!_isCameraMuted) {
|
||||
debugPrint('[VOIP] Ensuring camera is unmuted on connect');
|
||||
call.setLocalVideoMuted(false);
|
||||
}
|
||||
}
|
||||
_updateRenderers();
|
||||
});
|
||||
|
||||
_renderRefreshTimer?.cancel();
|
||||
_renderRefreshTimer = Timer.periodic(
|
||||
const Duration(milliseconds: 1000), // Faster polling to catch feeds
|
||||
(_) => _updateRenderers(),
|
||||
);
|
||||
|
||||
_updateRenderers();
|
||||
}
|
||||
|
||||
void _updateInternalStates() {
|
||||
if (currentCall == null) return;
|
||||
final dynamic c = currentCall;
|
||||
try {
|
||||
_isMicMuted = c.isMicrophoneMuted == true;
|
||||
_isCameraMuted = c.isLocalVideoMuted == true;
|
||||
} catch (_) {}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _updateRenderers() async {
|
||||
if (currentCall == null || _isUpdating) return;
|
||||
_isUpdating = true;
|
||||
|
||||
try {
|
||||
await _ensureRenderers();
|
||||
final dynamic c = currentCall;
|
||||
rtc.MediaStream? remote;
|
||||
rtc.MediaStream? local;
|
||||
|
||||
rtc.MediaStream? extract(dynamic obj) {
|
||||
if (obj == null) return null;
|
||||
if (obj is rtc.MediaStream) return obj;
|
||||
try { return obj.stream as rtc.MediaStream?; } catch (_) {}
|
||||
return null;
|
||||
}
|
||||
|
||||
final List<dynamic> feeds = [];
|
||||
try {
|
||||
final dynamic sdkFeeds = c.getFeeds();
|
||||
if (sdkFeeds is Iterable) feeds.addAll(sdkFeeds.toList());
|
||||
} catch (_) {}
|
||||
|
||||
bool remoteScreenFound = false;
|
||||
bool localScreenFound = false;
|
||||
|
||||
for (final dynamic f in feeds) {
|
||||
final s = extract(f);
|
||||
if (s == null) continue;
|
||||
|
||||
bool isLocal = false;
|
||||
try { isLocal = f.isLocal == true || f.userId == client.userID; } catch (_) {}
|
||||
|
||||
String purpose = 'm.usermedia';
|
||||
try { purpose = f.purpose ?? 'm.usermedia'; } catch (_) {}
|
||||
|
||||
if (isLocal) {
|
||||
if (local == null || purpose == 'm.screenshare') {
|
||||
local = s;
|
||||
if (purpose == 'm.screenshare') localScreenFound = true;
|
||||
}
|
||||
} else {
|
||||
if (remote == null || purpose == 'm.screenshare') {
|
||||
remote = s;
|
||||
if (purpose == 'm.screenshare') remoteScreenFound = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isScreensharing = localScreenFound;
|
||||
_isRemoteScreensharing = remoteScreenFound;
|
||||
|
||||
if (remote == null) {
|
||||
try { remote = extract(c.remoteUserMediaStream); } catch (_) {}
|
||||
}
|
||||
if (remote == null) {
|
||||
try { remote = extract(c.remoteStream); } catch (_) {}
|
||||
}
|
||||
if (local == null) {
|
||||
try { local = extract(c.localUserMediaStream); } catch (_) {}
|
||||
}
|
||||
|
||||
// Ausgabelautstärke (Settings-Slider) auf den Remote-Stream anwenden —
|
||||
// nur einmal pro Stream, _updateRenderers läuft auch per Timer.
|
||||
if (remote != null && remote.id != _volumeAppliedStreamId) {
|
||||
_volumeAppliedStreamId = remote.id;
|
||||
_remoteAudioStream = remote;
|
||||
applyOutputVolume();
|
||||
}
|
||||
|
||||
// Final check for tracks before assigning
|
||||
bool changed = false;
|
||||
if (remote != null && remote.getVideoTracks().isNotEmpty) {
|
||||
if (remoteRenderer.srcObject?.id != remote.id) {
|
||||
debugPrint('[VOIP] Assigning Remote Renderer: ${remote.id} (${remote.getVideoTracks().length} video tracks)');
|
||||
remoteRenderer.srcObject = remote;
|
||||
changed = true;
|
||||
}
|
||||
} else if (remoteRenderer.srcObject != null) {
|
||||
remoteRenderer.srcObject = null;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (local != null && local.getVideoTracks().isNotEmpty) {
|
||||
if (localRenderer.srcObject?.id != local.id) {
|
||||
debugPrint('[VOIP] Assigning Local Renderer: ${local.id} (${local.getVideoTracks().length} video tracks)');
|
||||
localRenderer.srcObject = local;
|
||||
changed = true;
|
||||
}
|
||||
} else if (localRenderer.srcObject != null) {
|
||||
localRenderer.srcObject = null;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (changed) notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint('[VOIP] Error in _updateRenderers: $e');
|
||||
} finally {
|
||||
_isUpdating = false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleCallEnded(CallSession session) async {
|
||||
if (currentCall?.callId == session.callId) {
|
||||
_renderRefreshTimer?.cancel();
|
||||
_renderRefreshTimer = null;
|
||||
localRenderer.srcObject = null;
|
||||
remoteRenderer.srcObject = null;
|
||||
_remoteAudioStream = null;
|
||||
_volumeAppliedStreamId = null;
|
||||
currentCall = null;
|
||||
_isScreensharing = false;
|
||||
_isRemoteScreensharing = false;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
// ── Ausgabelautstärke (Settings-Slider "voice_output_volume") ─────────────
|
||||
rtc.MediaStream? _remoteAudioStream;
|
||||
String? _volumeAppliedStreamId;
|
||||
|
||||
/// Wendet die in den Einstellungen gewählte Ausgabelautstärke auf den
|
||||
/// Remote-Audio-Stream an. Ohne [value] wird der Pref-Wert gelesen.
|
||||
@override
|
||||
Future<void> applyOutputVolume([double? value]) async {
|
||||
var v = value;
|
||||
if (v == null) {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
v = prefs.getDouble('voice_output_volume') ?? 1.0;
|
||||
} catch (_) {
|
||||
v = 1.0;
|
||||
}
|
||||
}
|
||||
final stream = _remoteAudioStream;
|
||||
if (stream == null) return;
|
||||
for (final track in stream.getAudioTracks()) {
|
||||
try {
|
||||
await rtc.Helper.setVolume(v.clamp(0.0, 2.0), track);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleMissedCall(CallSession session) async {
|
||||
if (currentCall?.callId == session.callId) {
|
||||
currentCall = null;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> handleNewGroupCall(GroupCallSession groupCall) async {}
|
||||
@override
|
||||
Future<void> handleGroupCallEnded(GroupCallSession groupCall) async {}
|
||||
@override
|
||||
bool get canHandleNewCall => currentCall == null;
|
||||
@override
|
||||
Future<void> playRingtone() async {}
|
||||
@override
|
||||
Future<void> stopRingtone() async {}
|
||||
@override
|
||||
EncryptionKeyProvider? get keyProvider => null;
|
||||
@override
|
||||
Future<void> registerListeners(CallSession session) async {}
|
||||
|
||||
@override
|
||||
Future<void> safeAction(Future<dynamic> Function() action) async {
|
||||
try {
|
||||
await action();
|
||||
await Future.delayed(const Duration(milliseconds: 1000));
|
||||
} catch (e) {
|
||||
debugPrint('[VOIP] Action error: $e');
|
||||
}
|
||||
_updateInternalStates();
|
||||
_updateRenderers();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> toggleMic() async {
|
||||
if (currentCall == null) return;
|
||||
_isMicMuted = !_isMicMuted;
|
||||
notifyListeners();
|
||||
try {
|
||||
await currentCall!.setMicrophoneMuted(_isMicMuted);
|
||||
} catch (e) {
|
||||
_isMicMuted = !_isMicMuted;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> toggleCamera() async {
|
||||
if (currentCall == null) return;
|
||||
_isCameraMuted = !_isCameraMuted;
|
||||
notifyListeners();
|
||||
try {
|
||||
await currentCall!.setLocalVideoMuted(_isCameraMuted);
|
||||
for (var i = 0; i < 5; i++) {
|
||||
await Future.delayed(Duration(milliseconds: 300 + (i * 200)));
|
||||
await _updateRenderers();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('[VOIP] toggleCamera error: $e');
|
||||
_isCameraMuted = !_isCameraMuted;
|
||||
notifyListeners();
|
||||
}
|
||||
_updateInternalStates();
|
||||
await _updateRenderers();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> toggleScreenSharing([BuildContext? context]) async {
|
||||
if (currentCall == null) return;
|
||||
final call = currentCall!;
|
||||
|
||||
if (_isScreensharing) {
|
||||
await safeAction(() => call.setScreensharingEnabled(false));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows && context != null) {
|
||||
final selectedSource = await ScreenSharePicker.show(context);
|
||||
if (selectedSource == null) return;
|
||||
_selectedSourceId = selectedSource.id;
|
||||
} else {
|
||||
_selectedSourceId = null;
|
||||
}
|
||||
|
||||
await safeAction(() async {
|
||||
await call.setScreensharingEnabled(true);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> toggleDeafen() async {
|
||||
_isDeafened = !_isDeafened;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> hangup() async {
|
||||
if (currentCall == null) return;
|
||||
await safeAction(() async {
|
||||
await currentCall!.hangup(reason: CallErrorCode.userHangup);
|
||||
currentCall = null;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> startCall(String roomId, {bool video = false}) async {
|
||||
final room = client.getRoomById(roomId);
|
||||
if (room == null) return;
|
||||
await _ensureRenderers();
|
||||
_isCameraMuted = !video;
|
||||
|
||||
debugPrint('[VOIP] Starting call in room $roomId (video=$video)');
|
||||
final call = await voip.inviteToCall(room, video ? CallType.kVideo : CallType.kVoice);
|
||||
currentCall = call;
|
||||
|
||||
_setupCallListeners(call);
|
||||
|
||||
if (video) {
|
||||
await Future.delayed(const Duration(milliseconds: 800));
|
||||
await call.setLocalVideoMuted(false);
|
||||
await _updateRenderers();
|
||||
}
|
||||
|
||||
_updateInternalStates();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> changeQuality(String key) async {
|
||||
_currentQualityKey = key;
|
||||
if (currentCall == null) {
|
||||
notifyListeners();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_isCameraMuted) {
|
||||
await toggleCamera();
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
await toggleCamera();
|
||||
}
|
||||
|
||||
if (_isScreensharing) {
|
||||
final String? oldSource = _selectedSourceId;
|
||||
await safeAction(() => currentCall!.setScreensharingEnabled(false));
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
_selectedSourceId = oldSource;
|
||||
await safeAction(() => currentCall!.setScreensharingEnabled(true));
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
class MediaDevicesWrapper extends MediaDevices {
|
||||
final MediaDevices _delegate;
|
||||
final PyramidVoipManager _manager;
|
||||
MediaDevicesWrapper(this._delegate, this._manager);
|
||||
|
||||
@override
|
||||
Future<rtc.MediaStream> getUserMedia(Map<String, dynamic> constraints) async {
|
||||
final Map<String, dynamic> improvedConstraints = Map.from(constraints);
|
||||
|
||||
final String? sourceId = _manager._selectedSourceId;
|
||||
if (sourceId != null) {
|
||||
if (improvedConstraints['video'] is! Map) {
|
||||
improvedConstraints['video'] = {};
|
||||
}
|
||||
|
||||
final v = Map<String, dynamic>.from(improvedConstraints['video']);
|
||||
v['mandatory'] ??= {};
|
||||
v['mandatory']['chromeMediaSource'] = 'desktop';
|
||||
v['mandatory']['chromeMediaSourceId'] = sourceId;
|
||||
improvedConstraints['video'] = v;
|
||||
}
|
||||
|
||||
// In den Einstellungen gewählte Geräte (voice_*_device Prefs) anwenden —
|
||||
// nur wenn kein Screenshare-Source gesetzt ist (sonst nicht anfassen).
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final mic = prefs.getString('voice_mic_device');
|
||||
final cam = prefs.getString('voice_camera_device');
|
||||
final speaker = prefs.getString('voice_speaker_device');
|
||||
if (mic != null &&
|
||||
improvedConstraints['audio'] != null &&
|
||||
improvedConstraints['audio'] != false) {
|
||||
final a = improvedConstraints['audio'] is Map
|
||||
? Map<String, dynamic>.from(improvedConstraints['audio'])
|
||||
: <String, dynamic>{};
|
||||
a['deviceId'] = mic;
|
||||
a['optional'] = [
|
||||
...?(a['optional'] as List?),
|
||||
{'sourceId': mic},
|
||||
];
|
||||
improvedConstraints['audio'] = a;
|
||||
}
|
||||
if (cam != null &&
|
||||
sourceId == null &&
|
||||
improvedConstraints['video'] != null &&
|
||||
improvedConstraints['video'] != false) {
|
||||
final v = improvedConstraints['video'] is Map
|
||||
? Map<String, dynamic>.from(improvedConstraints['video'])
|
||||
: <String, dynamic>{};
|
||||
v['deviceId'] = cam;
|
||||
improvedConstraints['video'] = v;
|
||||
}
|
||||
if (speaker != null) {
|
||||
rtc.Helper.selectAudioOutput(speaker).catchError((_) {});
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
if (improvedConstraints['video'] != null && improvedConstraints['video'] != false) {
|
||||
final String q = _manager.currentQualityKey;
|
||||
int width = 1280;
|
||||
int height = 720;
|
||||
|
||||
if (q == 'sd') { width = 640; height = 360; }
|
||||
else if (q == 'fhd') { width = 1920; height = 1080; }
|
||||
else if (q == '4k') { width = 3840; height = 2160; }
|
||||
|
||||
if (improvedConstraints['video'] is bool) {
|
||||
improvedConstraints['video'] = {
|
||||
'width': {'ideal': width},
|
||||
'height': {'ideal': height},
|
||||
'frameRate': {'ideal': 30},
|
||||
};
|
||||
} else if (improvedConstraints['video'] is Map) {
|
||||
final v = Map<String, dynamic>.from(improvedConstraints['video']);
|
||||
v['width'] = {'ideal': width};
|
||||
v['height'] = {'ideal': height};
|
||||
improvedConstraints['video'] = v;
|
||||
}
|
||||
}
|
||||
return _delegate.getUserMedia(improvedConstraints);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<rtc.MediaStream> getDisplayMedia(Map<String, dynamic> constraints) async {
|
||||
final String? sourceId = _manager._selectedSourceId;
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.windows && sourceId != null) {
|
||||
final desktopConstraints = {
|
||||
'video': {
|
||||
'mandatory': {
|
||||
'chromeMediaSource': 'desktop',
|
||||
'chromeMediaSourceId': sourceId,
|
||||
},
|
||||
'optional': [],
|
||||
},
|
||||
'audio': false,
|
||||
};
|
||||
return _delegate.getDisplayMedia(desktopConstraints);
|
||||
}
|
||||
return _delegate.getDisplayMedia(constraints);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<MediaDeviceInfo>> enumerateDevices() => _delegate.enumerateDevices();
|
||||
@override
|
||||
MediaTrackSupportedConstraints getSupportedConstraints() => _delegate.getSupportedConstraints();
|
||||
@override
|
||||
set ondevicechange(Function(dynamic event)? callback) => _delegate.ondevicechange = callback;
|
||||
@override
|
||||
Function(dynamic event)? get ondevicechange => _delegate.ondevicechange;
|
||||
// Erzwungener Override der abstrakten MediaDevices-Klasse, die getSources()
|
||||
// selbst als deprecated markiert (Ersatz: enumerateDevices()) aber weiterhin verlangt.
|
||||
@override
|
||||
// ignore: deprecated_member_use
|
||||
Future<List<dynamic>> getSources() => _delegate.getSources();
|
||||
@override
|
||||
Future<MediaDeviceInfo> selectAudioOutput([AudioOutputOptions? options]) => _delegate.selectAudioOutput(options);
|
||||
}
|
||||
Reference in New Issue
Block a user