feat: audio output select for web. (#247)

* feat: audio output select for web.

* chore: Add comments for VideoTrackRenderer.

* update.
This commit is contained in:
CloudWebRTC
2023-03-06 10:53:45 +08:00
committed by GitHub
parent e3f78c2b4c
commit 70e6589981
7 changed files with 119 additions and 44 deletions
+38 -36
View File
@@ -317,43 +317,45 @@ class _ControlsWidgetState extends State<ControlsWidget> {
icon: const Icon(EvaIcons.micOff), icon: const Icon(EvaIcons.micOff),
tooltip: 'un-mute audio', tooltip: 'un-mute audio',
), ),
PopupMenuButton<MediaDevice>( if (!lkPlatformIs(PlatformType.web))
icon: const Icon(Icons.volume_up), PopupMenuButton<MediaDevice>(
itemBuilder: (BuildContext context) { icon: const Icon(Icons.volume_up),
return [ itemBuilder: (BuildContext context) {
const PopupMenuItem<MediaDevice>( return [
value: null, const PopupMenuItem<MediaDevice>(
child: ListTile( value: null,
leading: Icon( child: ListTile(
EvaIcons.speaker, leading: Icon(
color: Colors.white, EvaIcons.speaker,
), color: Colors.white,
title: Text('Select Audio Output'),
),
),
if (_audioOutputs != null)
..._audioOutputs!.map((device) {
return PopupMenuItem<MediaDevice>(
value: device,
child: ListTile(
leading: (device.deviceId ==
Hardware.instance.selectedAudioOutput?.deviceId)
? const Icon(
EvaIcons.checkmarkSquare,
color: Colors.white,
)
: const Icon(
EvaIcons.square,
color: Colors.white,
),
title: Text(device.label),
), ),
onTap: () => _selectAudioOutput(device), title: Text('Select Audio Output'),
); ),
}).toList() ),
]; if (_audioOutputs != null)
}, ..._audioOutputs!.map((device) {
), return PopupMenuItem<MediaDevice>(
value: device,
child: ListTile(
leading: (device.deviceId ==
Hardware
.instance.selectedAudioOutput?.deviceId)
? const Icon(
EvaIcons.checkmarkSquare,
color: Colors.white,
)
: const Icon(
EvaIcons.square,
color: Colors.white,
),
title: Text(device.label),
),
onTap: () => _selectAudioOutput(device),
);
}).toList()
];
},
),
if (participant.isCameraEnabled()) if (participant.isCameraEnabled())
PopupMenuButton<MediaDevice>( PopupMenuButton<MediaDevice>(
icon: const Icon(EvaIcons.video), icon: const Icon(EvaIcons.video),
+65 -5
View File
@@ -82,12 +82,20 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
VideoTrack? get activeVideoTrack; VideoTrack? get activeVideoTrack;
TrackPublication? get videoPublication; TrackPublication? get videoPublication;
TrackPublication? get firstAudioPublication; TrackPublication? get firstAudioPublication;
List<MediaDevice>? _audioOutputs;
MediaDevice? _selectedAudioDevice;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
widget.participant.addListener(_onParticipantChanged); widget.participant.addListener(_onParticipantChanged);
_onParticipantChanged(); _onParticipantChanged();
Hardware.instance.audioOutputs().then((value) {
setState(() {
_audioOutputs = value;
_selectedAudioDevice = _audioOutputs?.firstOrNull;
});
});
} }
@override @override
@@ -108,6 +116,14 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
// since the updated values are computed properties. // since the updated values are computed properties.
void _onParticipantChanged() => setState(() {}); void _onParticipantChanged() => setState(() {});
void _onSelectAudioOutput(MediaDevice device) {
var audioTrack = firstAudioPublication?.track as RemoteAudioTrack;
audioTrack.setAudioOutput(device.deviceId);
setState(() {
_selectedAudioDevice = device;
});
}
// Widgets to show above the info bar // Widgets to show above the info bar
List<Widget> extraWidgets(bool isScreenShare) => []; List<Widget> extraWidgets(bool isScreenShare) => [];
@@ -130,10 +146,8 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
InkWell( InkWell(
onTap: () => setState(() => _visible = !_visible), onTap: () => setState(() => _visible = !_visible),
child: activeVideoTrack != null && !activeVideoTrack!.muted child: activeVideoTrack != null && !activeVideoTrack!.muted
? VideoTrackRenderer( ? VideoTrackRenderer(activeVideoTrack!,
activeVideoTrack!, fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain)
fit: RTCVideoViewObjectFit.RTCVideoViewObjectFitContain,
)
: const NoVideoWidget(), : const NoVideoWidget(),
), ),
@@ -144,7 +158,9 @@ abstract class _ParticipantWidgetState<T extends ParticipantWidget>
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
...extraWidgets(widget.isScreenShare), ...extraWidgets(
widget.isScreenShare,
),
ParticipantInfoWidget( ParticipantInfoWidget(
title: widget.participant.name.isNotEmpty title: widget.participant.name.isNotEmpty
? '${widget.participant.name} (${widget.participant.identity})' ? '${widget.participant.name} (${widget.participant.identity})'
@@ -211,6 +227,13 @@ class _RemoteParticipantWidgetState
pub: firstAudioPublication!, pub: firstAudioPublication!,
icon: EvaIcons.volumeUp, icon: EvaIcons.volumeUp,
), ),
if (lkPlatformIs(PlatformType.web))
RemoteTrackAudioOutputSelectMenuWidget(
audioOutputs: _audioOutputs ?? [],
selected: _selectedAudioDevice,
onSelected: _onSelectAudioOutput,
icon: EvaIcons.speaker,
),
], ],
), ),
]; ];
@@ -253,3 +276,40 @@ class RemoteTrackPublicationMenuWidget extends StatelessWidget {
), ),
); );
} }
class RemoteTrackAudioOutputSelectMenuWidget extends StatelessWidget {
final IconData icon;
final List<MediaDevice> audioOutputs;
final MediaDevice? selected;
final Function(MediaDevice) onSelected;
const RemoteTrackAudioOutputSelectMenuWidget({
required this.audioOutputs,
required this.onSelected,
required this.selected,
required this.icon,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) => Material(
color: Colors.black.withOpacity(0.3),
child: PopupMenuButton<Function>(
tooltip: 'Select AudioOutput',
icon: Icon(icon),
onSelected: (value) => value(),
itemBuilder: (BuildContext context) => <PopupMenuEntry<Function>>[
...audioOutputs
.map((e) => PopupMenuItem(
child: ListTile(
trailing: (e.deviceId == selected?.deviceId)
? const Icon(Icons.check, color: Colors.white)
: null,
title: Text(e.label),
),
value: () => onSelected(e),
))
.toList(),
],
),
);
}
+1
View File
@@ -17,6 +17,7 @@ export 'src/proto/livekit_models.pb.dart' show TrackType, VideoQuality;
export 'src/publication/local.dart'; export 'src/publication/local.dart';
export 'src/publication/remote.dart'; export 'src/publication/remote.dart';
export 'src/publication/track_publication.dart'; export 'src/publication/track_publication.dart';
export 'src/support/platform.dart';
export 'src/track/local/audio.dart'; export 'src/track/local/audio.dart';
export 'src/track/local/local.dart'; export 'src/track/local/local.dart';
export 'src/track/local/video.dart'; export 'src/track/local/video.dart';
-3
View File
@@ -1,7 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'package:meta/meta.dart';
import 'platform/io.dart' if (dart.library.html) 'platform/web.dart'; import 'platform/io.dart' if (dart.library.html) 'platform/web.dart';
// Returns the current platform which works for both web and devices. // Returns the current platform which works for both web and devices.
@@ -9,7 +7,6 @@ PlatformType lkPlatform() => lkPlatformImplementation();
bool lkPlatformIs(PlatformType type) => lkPlatform() == type; bool lkPlatformIs(PlatformType type) => lkPlatform() == type;
@internal
bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST'); bool lkPlatformIsTest() => Platform.environment.containsKey('FLUTTER_TEST');
enum PlatformType { enum PlatformType {
+4
View File
@@ -43,4 +43,8 @@ class RemoteAudioTrack extends RemoteTrack
} }
return didStop; return didStop;
} }
Future<void> setAudioOutput(String deviceId) async {
audio.setAudioOutput(getCid(), deviceId);
}
} }
+4
View File
@@ -7,3 +7,7 @@ void startAudio(String id, rtc.MediaStreamTrack stream) {
void stopAudio(String id) { void stopAudio(String id) {
// do nothing // do nothing
} }
void setAudioOutput(String id, String deviceId) {
// do nothing
}
+7
View File
@@ -51,3 +51,10 @@ html.DivElement findOrCreateAudioContainer() {
html.document.body?.append(div); html.document.body?.append(div);
return div as html.DivElement; return div as html.DivElement;
} }
void setAudioOutput(String id, String deviceId) {
final audioElement = html.document.getElementById(audioPrefix + id);
if (audioElement is html.AudioElement) {
audioElement.setSinkId(deviceId);
}
}