Skip to content

Commit 9adbe5d

Browse files
authored
fix: respecting default audio output setting (#608)
1 parent d25a9a1 commit 9adbe5d

File tree

10 files changed

+118
-8
lines changed

10 files changed

+118
-8
lines changed

packages/stream_video/lib/src/call/call.dart

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,8 +270,7 @@ class Call {
270270

271271
set connectOptions(CallConnectOptions connectOptions) {
272272
final status = _status.value;
273-
if (status == _ConnectionStatus.connecting ||
274-
status == _ConnectionStatus.connected) {
273+
if (status == _ConnectionStatus.connected) {
275274
_logger.w(
276275
() => '[setConnectOptions] rejected (connectOptions must be'
277276
' set before invoking `connect`)',
@@ -976,6 +975,17 @@ class Call {
976975
return [...?_session?.getTracks(trackIdPrefix)];
977976
}
978977

978+
void _setDefaultConnectOptions(CallSettings settings) {
979+
connectOptions = connectOptions.copyWith(
980+
camera: TrackOption.fromSetting(
981+
enabled: settings.video.cameraDefaultOn,
982+
),
983+
microphone: TrackOption.fromSetting(
984+
enabled: settings.audio.micDefaultOn,
985+
),
986+
);
987+
}
988+
979989
Future<void> _applyConnectOptions() async {
980990
_logger.d(() => '[applyConnectOptions] connectOptions: $_connectOptions');
981991
await _applyCameraOption(_connectOptions.camera);
@@ -1192,11 +1202,26 @@ class Call {
11921202
custom: custom,
11931203
);
11941204

1205+
final mediaDevicesResult =
1206+
await RtcMediaDeviceNotifier.instance.enumerateDevices();
1207+
final mediaDevices = mediaDevicesResult.fold(
1208+
success: (success) => success.data,
1209+
failure: (failure) => <RtcMediaDevice>[],
1210+
);
1211+
11951212
return response.fold(
11961213
success: (it) {
1214+
_setDefaultConnectOptions(it.data.data.metadata.settings);
1215+
11971216
_stateManager.lifecycleCallCreated(
11981217
CallCreated(it.data.data),
11991218
ringing: ringing,
1219+
audioOutputs: mediaDevices
1220+
.where((d) => d.kind == RtcMediaDeviceKind.audioOutput)
1221+
.toList(),
1222+
audioInputs: mediaDevices
1223+
.where((d) => d.kind == RtcMediaDeviceKind.audioInput)
1224+
.toList(),
12001225
);
12011226
_logger.v(() => '[getOrCreate] completed: ${it.data}');
12021227
return it;

packages/stream_video/lib/src/call/call_connect_options.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ class CallConnectOptions with EquatableMixin {
1818
TrackOption? camera,
1919
TrackOption? microphone,
2020
TrackOption? screenShare,
21-
Duration? dropTimeout,
2221
}) {
2322
return CallConnectOptions(
2423
camera: camera ?? this.camera,
@@ -43,6 +42,9 @@ class CallConnectOptions with EquatableMixin {
4342
abstract class TrackOption with EquatableMixin {
4443
const TrackOption();
4544

45+
factory TrackOption.fromSetting({required bool enabled}) =>
46+
enabled ? TrackOption.enabled() : TrackOption.disabled();
47+
4648
factory TrackOption.enabled() {
4749
return TrackEnabled._instance;
4850
}

packages/stream_video/lib/src/call/session/call_session.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,15 @@ class CallSession extends Disposable {
167167
if (CurrentPlatform.isIos) {
168168
await rtcManager?.setAppleAudioConfiguration();
169169
}
170+
171+
//FIXME: This is a temporary fix for the issue where the audio output device is not set correctly
172+
// we should remove the delay and figure out why it's not setting the device without it
173+
unawaited(
174+
Future.delayed(const Duration(milliseconds: 250), () async {
175+
await _applyCurrentAudioOutputDevice();
176+
}),
177+
);
178+
170179
_logger.v(() => '[start] completed');
171180
return const Result.success(none);
172181
} catch (e, stk) {

packages/stream_video/lib/src/call/state/mixins/state_lifecycle_mixin.dart

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import 'package:state_notifier/state_notifier.dart';
2+
import 'package:collection/collection.dart';
23

34
import '../../../../stream_video.dart';
45
import '../../../action/internal/lifecycle_action.dart';
@@ -101,7 +102,26 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
101102
void lifecycleCallCreated(
102103
CallCreated stage, {
103104
bool ringing = false,
105+
List<RtcMediaDevice>? audioOutputs,
106+
List<RtcMediaDevice>? audioInputs,
104107
}) {
108+
final defaultAudioOutput = audioOutputs?.firstWhereOrNull((device) {
109+
if (stage.data.metadata.settings.audio.defaultDevice ==
110+
AudioSettingsRequestDefaultDeviceEnum.speaker) {
111+
return device.id.equalsIgnoreCase(
112+
AudioSettingsRequestDefaultDeviceEnum.speaker.value,
113+
);
114+
}
115+
116+
return !device.id.equalsIgnoreCase(
117+
AudioSettingsRequestDefaultDeviceEnum.speaker.value,
118+
);
119+
});
120+
121+
final defaultAudioInput = audioInputs
122+
?.firstWhereOrNull((d) => d.label == defaultAudioOutput?.label) ??
123+
audioInputs?.firstOrNull;
124+
105125
_logger.d(() => '[lifecycleCallCreated] ringing: $ringing, state: $state');
106126
state = state.copyWith(
107127
status: stage.data.toCallStatus(state: state, ringing: ringing),
@@ -118,6 +138,8 @@ mixin StateLifecycleMixin on StateNotifier<CallState> {
118138
isBackstage: stage.data.metadata.details.backstage,
119139
isBroadcasting: stage.data.metadata.details.broadcasting,
120140
isRecording: stage.data.metadata.details.recording,
141+
audioOutputDevice: defaultAudioOutput,
142+
audioInputDevice: defaultAudioInput,
121143
);
122144
}
123145

@@ -361,3 +383,7 @@ extension on CallRingingData {
361383
}
362384
}
363385
}
386+
387+
extension on String {
388+
bool equalsIgnoreCase(String other) => toUpperCase() == other.toUpperCase();
389+
}

packages/stream_video/lib/src/coordinator/open_api/open_api_extensions.dart

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import 'package:collection/collection.dart';
22

33
import '../../../../open_api/video/coordinator/api.dart' as open;
4+
import '../../../stream_video.dart';
45
import '../../errors/video_error.dart';
56
import '../../logger/stream_log.dart';
67
import '../../models/call_cid.dart';
@@ -176,6 +177,9 @@ extension CallSettingsExt on open.CallSettingsResponse {
176177
accessRequestEnabled: audio.accessRequestEnabled,
177178
opusDtxEnabled: audio.opusDtxEnabled,
178179
redundantCodingEnabled: audio.redundantCodingEnabled,
180+
defaultDevice: audio.defaultDevice.toDomain(),
181+
micDefaultOn: audio.micDefaultOn,
182+
speakerDefaultOn: audio.speakerDefaultOn,
179183
),
180184
video: StreamVideoSettings(
181185
accessRequestEnabled: video.accessRequestEnabled,
@@ -208,6 +212,16 @@ extension CallSettingsExt on open.CallSettingsResponse {
208212
}
209213
}
210214

215+
extension on open.AudioSettingsDefaultDeviceEnum {
216+
AudioSettingsRequestDefaultDeviceEnum toDomain() {
217+
if (this == open.AudioSettingsDefaultDeviceEnum.speaker) {
218+
return AudioSettingsRequestDefaultDeviceEnum.speaker;
219+
} else {
220+
return AudioSettingsRequestDefaultDeviceEnum.earpiece;
221+
}
222+
}
223+
}
224+
211225
extension on open.TranscriptionSettingsModeEnum {
212226
TranscriptionSettingsMode toDomain() {
213227
if (this == open.TranscriptionSettingsModeEnum.autoOn) {

packages/stream_video/lib/src/models/call_settings.dart

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,24 @@ class StreamAudioSettings extends MediaSettings {
7777
this.opusDtxEnabled = false,
7878
this.redundantCodingEnabled = false,
7979
this.defaultDevice = AudioSettingsRequestDefaultDeviceEnum.speaker,
80+
this.micDefaultOn = true,
81+
this.speakerDefaultOn = true,
8082
});
8183

8284
final bool opusDtxEnabled;
8385
final bool redundantCodingEnabled;
8486
final AudioSettingsRequestDefaultDeviceEnum defaultDevice;
87+
final bool micDefaultOn;
88+
final bool speakerDefaultOn;
8589

8690
@override
8791
List<Object?> get props => [
8892
accessRequestEnabled,
8993
opusDtxEnabled,
9094
redundantCodingEnabled,
95+
defaultDevice,
96+
micDefaultOn,
97+
speakerDefaultOn,
9198
];
9299

93100
AudioSettingsRequest toOpenDto() {
@@ -96,6 +103,8 @@ class StreamAudioSettings extends MediaSettings {
96103
accessRequestEnabled: accessRequestEnabled,
97104
opusDtxEnabled: opusDtxEnabled,
98105
redundantCodingEnabled: redundantCodingEnabled,
106+
micDefaultOn: micDefaultOn,
107+
speakerDefaultOn: speakerDefaultOn,
99108
);
100109
}
101110
}
@@ -104,20 +113,24 @@ class StreamVideoSettings extends MediaSettings {
104113
const StreamVideoSettings({
105114
super.accessRequestEnabled = false,
106115
this.enabled = false,
116+
this.cameraDefaultOn = true,
107117
});
108118

109119
final bool enabled;
120+
final bool cameraDefaultOn;
110121

111122
@override
112123
List<Object?> get props => [
113124
accessRequestEnabled,
114125
enabled,
126+
cameraDefaultOn,
115127
];
116128

117129
VideoSettingsRequest toOpenDto() {
118130
return VideoSettingsRequest(
119131
enabled: enabled,
120132
accessRequestEnabled: accessRequestEnabled,
133+
cameraDefaultOn: cameraDefaultOn,
121134
);
122135
}
123136
}

packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device.dart

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,17 +26,19 @@ class RtcMediaDevice with EquatableMixin {
2626
required this.id,
2727
required this.label,
2828
required this.kind,
29+
this.groupId,
2930
});
3031

3132
final String id;
3233
final String label;
34+
final String? groupId;
3335
final RtcMediaDeviceKind kind;
3436

3537
@override
3638
String toString() {
37-
return 'RtcMediaDevice{id: $id, label: $label, kind: $kind}';
39+
return 'RtcMediaDevice{id: $id, label: $label, groupId: $groupId, kind: $kind}';
3840
}
3941

4042
@override
41-
List<Object?> get props => [id, kind, label];
43+
List<Object?> get props => [id, kind, groupId, label];
4244
}

packages/stream_video/lib/src/webrtc/rtc_media_device/rtc_media_device_notifier.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class RtcMediaDeviceNotifier {
3737
return RtcMediaDevice(
3838
id: it.deviceId,
3939
label: it.label,
40+
groupId: it.groupId,
4041
kind: RtcMediaDeviceKind.fromAlias(it.kind),
4142
);
4243
});

packages/stream_video_flutter/lib/src/call_screen/call_container.dart

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class StreamCallContainer extends StatefulWidget {
4747
const StreamCallContainer({
4848
super.key,
4949
required this.call,
50-
this.callConnectOptions = const CallConnectOptions(),
50+
this.callConnectOptions,
5151
this.onBackPressed,
5252
this.onLeaveCallTap,
5353
this.onAcceptCallTap,
@@ -62,7 +62,7 @@ class StreamCallContainer extends StatefulWidget {
6262
final Call call;
6363

6464
/// Options used while connecting to the call.
65-
final CallConnectOptions callConnectOptions;
65+
final CallConnectOptions? callConnectOptions;
6666

6767
/// The action to perform when the back button is pressed.
6868
final VoidCallback? onBackPressed;
@@ -161,7 +161,9 @@ class _StreamCallContainerState extends State<StreamCallContainer> {
161161
Future<void> _connect() async {
162162
try {
163163
_logger.d(() => '[connect] no args');
164-
call.connectOptions = widget.callConnectOptions;
164+
if (widget.callConnectOptions != null) {
165+
call.connectOptions = widget.callConnectOptions!;
166+
}
165167
final result = await call.join();
166168
_logger.v(() => '[connect] completed: $result');
167169
} catch (e) {

packages/stream_video_flutter/lib/src/call_screen/lobby_video.dart

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@ class _StreamLobbyVideoState extends State<StreamLobbyVideo> {
4141
RtcLocalAudioTrack? _microphoneTrack;
4242
RtcLocalCameraTrack? _cameraTrack;
4343

44+
@override
45+
void initState() {
46+
super.initState();
47+
48+
Future.delayed(Duration.zero, () {
49+
final callSettings = widget.call.state.value.settings;
50+
if (callSettings.audio.micDefaultOn) {
51+
toggleMicrophone();
52+
}
53+
54+
if (callSettings.video.cameraDefaultOn) {
55+
toggleCamera();
56+
}
57+
});
58+
}
59+
4460
Future<void> toggleCamera() async {
4561
if (_cameraTrack != null) {
4662
await _cameraTrack?.stop();

0 commit comments

Comments
 (0)