Skip to content

Commit 6365ec6

Browse files
authored
feat: Visualizer for web. (#718)
* feat: Visualizer for web. * wip. * done. * update. * dart run import_sorter:main --no-comments. * dart format. * export Visualizer and add VisualizerOptions. * support barCount configuration. * Fixed bug for visualizer restart, removed setting enableVisualizer from RoomOptions. * update. * cleanup. * rename. * fix. * fix. * fix.
1 parent a503e95 commit 6365ec6

23 files changed

+472
-116
lines changed

android/src/main/kotlin/io/livekit/plugin/LiveKitPlugin.kt

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ class LiveKitPlugin: FlutterPlugin, MethodCallHandler {
5050
@SuppressLint("SuspiciousIndentation")
5151
private fun handleStartVisualizer(@NonNull call: MethodCall, @NonNull result: Result) {
5252
val trackId = call.argument<String>("trackId")
53-
if (trackId == null) {
54-
result.error("INVALID_ARGUMENT", "trackId is required", null)
53+
val visualizerId = call.argument<String>("visualizerId")
54+
if (trackId == null || visualizerId == null) {
55+
result.error("INVALID_ARGUMENT", "trackId and visualizerId is required", null)
5556
return
5657
}
5758
var audioTrack: LKAudioTrack? = null
@@ -75,19 +76,21 @@ class LiveKitPlugin: FlutterPlugin, MethodCallHandler {
7576

7677
val visualizer = Visualizer(
7778
barCount = barCount, isCentered = isCentered,
78-
audioTrack = audioTrack, binaryMessenger = binaryMessenger!!)
79+
audioTrack = audioTrack, binaryMessenger = binaryMessenger!!,
80+
visualizerId = visualizerId)
7981

80-
processors[trackId] = visualizer
82+
processors[visualizerId] = visualizer
8183
result.success(null)
8284
}
8385

8486
private fun handleStopVisualizer(@NonNull call: MethodCall, @NonNull result: Result) {
8587
val trackId = call.argument<String>("trackId")
86-
if (trackId == null) {
87-
result.error("INVALID_ARGUMENT", "trackId is required", null)
88+
val visualizerId = call.argument<String>("visualizerId")
89+
if (trackId == null || visualizerId == null) {
90+
result.error("INVALID_ARGUMENT", "trackId and visualizerId is required", null)
8891
return
8992
}
90-
processors.entries.removeAll { (k, v) -> k == trackId }
93+
processors.entries.removeAll { (k, v) -> k == visualizerId }
9194
result.success(null)
9295
}
9396

android/src/main/kotlin/io/livekit/plugin/Visualizer.kt

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,16 +29,18 @@ class Visualizer(
2929
private var barCount: Int,
3030
private var isCentered: Boolean,
3131
audioTrack: LKAudioTrack,
32-
binaryMessenger: BinaryMessenger
32+
binaryMessenger: BinaryMessenger,
33+
visualizerId: String
3334
) : EventChannel.StreamHandler, AudioTrackSink {
3435
private var eventChannel: EventChannel? = null
3536
private var eventSink: EventChannel.EventSink? = null
3637
private var ffiAudioAnalyzer = FFTAudioAnalyzer()
3738
private var audioTrack: LKAudioTrack? = audioTrack
3839
private var amplitudes: FloatArray = FloatArray(0)
39-
private var bands: FloatArray = FloatArray(0)
40+
private var bands: FloatArray
4041
private var loPass: Int = 0
4142
private var hiPass: Int = 80
43+
4244
private var audioFormat = AudioFormat(16, 48000, 1)
4345

4446
fun stop() {
@@ -99,8 +101,9 @@ class Visualizer(
99101
}
100102

101103
init {
102-
eventChannel = EventChannel(binaryMessenger, "io.livekit.audio.visualizer/eventChannel-" + audioTrack.id())
104+
eventChannel = EventChannel(binaryMessenger, "io.livekit.audio.visualizer/eventChannel-" + audioTrack.id() + "-" + visualizerId)
103105
eventChannel?.setStreamHandler(this)
106+
bands = FloatArray(barCount)
104107
ffiAudioAnalyzer.configure(audioFormat)
105108
audioTrack.addSink(this)
106109
}

example/lib/pages/prejoin.dart

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,6 @@ class _PreJoinPageState extends State<PreJoinPage> {
131131
AudioCaptureOptions(
132132
deviceId: _selectedAudioDevice!.deviceId,
133133
),
134-
true, // enableVisualizer
135134
);
136135
await _audioTrack!.start();
137136
}
@@ -212,7 +211,6 @@ class _PreJoinPageState extends State<PreJoinPage> {
212211
screenShareEncoding: screenEncoding,
213212
),
214213
e2eeOptions: e2eeOptions,
215-
enableVisualizer: true,
216214
),
217215
);
218216
// Create a Listener before connecting

example/lib/widgets/participant_stats.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,9 @@ class _ParticipantStatsWidgetState extends State<ParticipantStatsWidget> {
5555
listener.on<VideoReceiverStatsEvent>((event) {
5656
Map<String, String> stats = {};
5757
setState(() {
58-
stats['rx'] = '${event.currentBitrate.toInt()} kpbs';
58+
if (!event.currentBitrate.isFinite && !event.currentBitrate.isNaN) {
59+
stats['rx'] = '${event.currentBitrate.toInt()} kpbs';
60+
}
5961
if (event.stats.mimeType != null) {
6062
stats['codec'] =
6163
'${event.stats.mimeType!.split('/')[1]}/${event.stats.clockRate}';

example/lib/widgets/sound_waveform.dart

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,15 +42,15 @@ import 'package:flutter/material.dart';
4242
import 'package:livekit_client/livekit_client.dart';
4343

4444
class SoundWaveformWidget extends StatefulWidget {
45-
final int count;
45+
final int barCount;
4646
final double width;
4747
final double minHeight;
4848
final double maxHeight;
4949
final int durationInMilliseconds;
5050
const SoundWaveformWidget({
5151
super.key,
5252
required this.audioTrack,
53-
this.count = 7,
53+
this.barCount = 5,
5454
this.width = 5,
5555
this.minHeight = 8,
5656
this.maxHeight = 100,
@@ -64,23 +64,32 @@ class SoundWaveformWidget extends StatefulWidget {
6464
class _SoundWaveformWidgetState extends State<SoundWaveformWidget>
6565
with TickerProviderStateMixin {
6666
late AnimationController controller;
67-
List<double> samples = [0, 0, 0, 0, 0, 0, 0];
68-
EventsListener<TrackEvent>? _listener;
67+
late List<double> samples;
68+
AudioVisualizer? _visualizer;
69+
EventsListener<AudioVisualizerEvent>? _listener;
6970

7071
void _startVisualizer(AudioTrack track) async {
71-
await _listener?.dispose();
72-
_listener = track.createListener();
72+
samples = List.filled(widget.barCount, 0);
73+
_visualizer ??= createVisualizer(track,
74+
options: AudioVisualizerOptions(barCount: widget.barCount));
75+
_listener ??= _visualizer?.createListener();
7376
_listener?.on<AudioVisualizerEvent>((e) {
7477
if (mounted) {
7578
setState(() {
7679
samples = e.event.map((e) => ((e as num) * 100).toDouble()).toList();
7780
});
7881
}
7982
});
83+
84+
await _visualizer!.start();
8085
}
8186

8287
void _stopVisualizer(AudioTrack track) async {
88+
await _visualizer?.stop();
89+
await _visualizer?.dispose();
90+
_visualizer = null;
8391
await _listener?.dispose();
92+
_listener = null;
8493
}
8594

8695
@override
@@ -106,7 +115,7 @@ class _SoundWaveformWidgetState extends State<SoundWaveformWidget>
106115

107116
@override
108117
Widget build(BuildContext context) {
109-
final count = widget.count;
118+
final count = widget.barCount;
110119
final minHeight = widget.minHeight;
111120
final maxHeight = widget.maxHeight;
112121
return AnimatedBuilder(

lib/livekit_client.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ export 'src/track/remote/remote.dart';
4545
export 'src/track/remote/video.dart';
4646
export 'src/track/track.dart';
4747
export 'src/track/processor.dart';
48+
export 'src/track/audio_visualizer.dart';
4849
export 'src/types/other.dart';
4950
export 'src/types/participant_permissions.dart';
5051
export 'src/types/video_dimensions.dart';

lib/src/core/room.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,6 @@ class Room extends DisposableChangeNotifier with EventsEmittable<RoomEvent> {
585585
trackSid,
586586
receiver: event.receiver,
587587
audioOutputOptions: roomOptions.defaultAudioOutputOptions,
588-
enableVisualizer: roomOptions.enableVisualizer,
589588
);
590589
} on TrackSubscriptionExceptionEvent catch (event) {
591590
logger.severe('addSubscribedMediaTrack() throwed ${event}');

lib/src/options.dart

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,10 @@ class RoomOptions {
115115
/// Options for end-to-end encryption.
116116
final E2EEOptions? e2eeOptions;
117117

118-
/// audio visualizer is disabled by default
119-
/// When enabled, the native layer will register an FFI audio analyzer
120-
/// and will emit AudioVisualizerEvent events from AudioTrack.
121-
/// You can use SoundWaveformWidget (example/lib/widgets/sound_waveform.dart)
122-
/// to display the audio wave. Or write a custom widget to visualize the audio
123-
/// wave.
124-
final bool enableVisualizer;
118+
/// deprecated, use [createVisualizer] instead
119+
/// please refer to example/lib/widgets/sound_waveform.dart
120+
@Deprecated('Use createVisualizer instead')
121+
final bool? enableVisualizer;
125122

126123
const RoomOptions({
127124
this.defaultCameraCaptureOptions = const CameraCaptureOptions(),
@@ -134,7 +131,7 @@ class RoomOptions {
134131
this.dynacast = false,
135132
this.stopLocalTrackOnUnpublish = true,
136133
this.e2eeOptions,
137-
this.enableVisualizer = false,
134+
this.enableVisualizer,
138135
});
139136

140137
RoomOptions copyWith({

lib/src/participant/local.dart

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -637,8 +637,7 @@ class LocalParticipant extends Participant<LocalTrackPublication> {
637637
} else if (source == TrackSource.microphone) {
638638
AudioCaptureOptions captureOptions =
639639
audioCaptureOptions ?? room.roomOptions.defaultAudioCaptureOptions;
640-
final track = await LocalAudioTrack.create(
641-
captureOptions, room.roomOptions.enableVisualizer);
640+
final track = await LocalAudioTrack.create(captureOptions);
642641
return await publishAudioTrack(track);
643642
} else if (source == TrackSource.screenShareVideo) {
644643
ScreenShareCaptureOptions captureOptions = screenShareCaptureOptions ??

lib/src/participant/remote.dart

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,6 @@ class RemoteParticipant extends Participant<RemoteTrackPublication> {
112112
String trackSid, {
113113
rtc.RTCRtpReceiver? receiver,
114114
AudioOutputOptions audioOutputOptions = const AudioOutputOptions(),
115-
bool? enableVisualizer,
116115
}) async {
117116
logger.fine('addSubscribedMediaTrack()');
118117

@@ -154,8 +153,8 @@ class RemoteParticipant extends Participant<RemoteTrackPublication> {
154153
RemoteVideoTrack(pub.source, stream, mediaTrack, receiver: receiver);
155154
} else if (pub.kind == TrackType.AUDIO) {
156155
// audio track
157-
track = RemoteAudioTrack(pub.source, stream, mediaTrack,
158-
receiver: receiver, enableVisualizer: enableVisualizer);
156+
track =
157+
RemoteAudioTrack(pub.source, stream, mediaTrack, receiver: receiver);
159158

160159
var listener = track.createListener();
161160
listener.on<AudioPlaybackStarted>((event) {

0 commit comments

Comments
 (0)