feat: audio output select for web. (#247)
* feat: audio output select for web. * chore: Add comments for VideoTrackRenderer. * update.
This commit is contained in:
@@ -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),
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -43,4 +43,8 @@ class RemoteAudioTrack extends RemoteTrack
|
|||||||
}
|
}
|
||||||
return didStop;
|
return didStop;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> setAudioOutput(String deviceId) async {
|
||||||
|
audio.setAudioOutput(getCid(), deviceId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user