Add Linux/Windows support for audio renderer (audio frame capture)#1128
Add Linux/Windows support for audio renderer (audio frame capture)#1128alfredobs97 wants to merge 1 commit into
Conversation
| mutex_.lock(); | ||
| auto it = renderers_.find(rendererId); | ||
| if (it != renderers_.end()) { | ||
| it->second->RemoveSink(); | ||
| renderers_.erase(it); | ||
| } | ||
| mutex_.unlock(); |
There was a problem hiding this comment.
🔍 Potential use-after-free race between RemoveSink and concurrent OnData callback
In stopAudioRenderer (linux/livekit_plugin.cpp:446-452), the mutex is held while RemoveSink() is called and the renderer is erased from the map. However, if OnData is currently executing on the WebRTC audio thread when RemoveSink is called, the AudioRendererSink object could be destroyed (by renderers_.erase) while OnData is still accessing its members (e.g., format_, sink_). Whether this is safe depends on whether WebRTC's RemoveSink blocks until any in-flight OnData call completes. The existing VisualizerSink has the identical pattern (linux/livekit_plugin.cpp:365-369), so this is a pre-existing architectural concern.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Good catch to scrutinize — I verified this against both audio delivery paths used by the desktop binaries, and the pattern is safe: RemoveSink blocks until any in-flight OnData completes, so the sink cannot be destroyed while OnData is executing.
Remote tracks (pc/remote_audio_source.cc, upstream WebRTC and the webrtc-sdk fork that ships the desktop prebuilts): RemoteAudioSource::OnData holds sink_lock_ while iterating sinks and invoking sink->OnData(...), and RemoteAudioSource::RemoveSink acquires the same sink_lock_. RemoveSink therefore cannot return while a delivery is in flight, and no further callbacks occur after it returns.
Local tracks (webrtc-sdk/libwebrtc, src/internal/local_audio_track.h): identical discipline — AddSink, RemoveSink, and OnData all take the same sink_lock_, with sinks_ RTC_GUARDED_BY(sink_lock_).
Corroborating evidence: the wrapper itself relies on this guarantee. AudioTrackImpl::RemoveSink (src/rtc_audio_track_impl.h) destroys the AudioTrackSinkAdapter immediately after rtc_track_->RemoveSink(...) returns. If the guarantee did not hold, every existing consumer — including the VisualizerSink that has been shipping on Linux/Windows — would already be crashing on stop.
Holding the plugin mutex_ while RemoveSink blocks is also fine: OnData never touches the plugin mutex, so there is no lock-order cycle.
As noted, VisualizerSink uses the identical pattern, so this PR intentionally keeps the two classes consistent. There is a separate pre-existing (and unrelated) subtlety shared by both classes — sink_/on_listen_called_ are written on the platform thread and read on the audio thread without synchronization — which would be better addressed as a follow-up cleanup touching both sinks rather than diverging here.
Implements the startAudioRenderer / stopAudioRenderer method-channel
handlers on Linux and Windows, bringing desktop platforms to parity
with iOS/macOS/Android for AudioFrameCapture. Previously these calls
fell through to NotImplemented(), surfacing in Dart as
MissingPluginException.
- shared_cpp/audio_renderer.{h,cpp}: platform-agnostic conversion
pipeline (validate -> resample -> channel reduce -> int16/float32
encode), a direct port of the Android AudioRenderer/AudioResampler.
- linux/windows livekit_plugin.cpp: AudioRendererSink attaching to
tracks via libwebrtc AudioTrackSink, streaming converted frames to
Dart over io.livekit.audio.renderer/channel-<rendererId>.
e5951be to
b3d2e6e
Compare
Summary
AudioFrameCapture.start()currently fails on desktop Linux and Windows with:The Dart layer already routes desktop platforms to the native implementation (
audio_frame_capture_native.dart), but native handlers forstartAudioRenderer/stopAudioRendererexist only on iOS, macOS, and Android. The Linux and Windows plugins handle onlystartVisualizer/stopVisualizerand fall through toNotImplemented().This PR adds the missing handlers, bringing Linux and Windows to parity with iOS/macOS/Android for
AudioFrameCapture.Implementation
The low-level
AudioTrackSinkinfrastructure was already in use by the audio visualizer on both platforms, so the change is additive and self-contained — no changes toflutter_webrtcor the Dart layer are needed.shared_cpp/audio_renderer.{h,cpp}(new): platform-agnostic conversion pipeline, a direct port of the AndroidAudioRenderer/AudioResampler:int16orfloat32byteslinux/livekit_plugin.cpp,windows/livekit_plugin.cpp: newAudioRendererSinkclass, structurally cloned from the existingVisualizerSink(EventChannel +StreamHandlerFunctions,AddSink/RemoveSink, task-runner hop to the platform thread). Streams converted frames to Dart overio.livekit.audio.renderer/channel-<rendererId>. Unlike the visualizer, frames arriving before a subscriber attaches are dropped rather than queued, matching the Android renderer semantics for live audio.handleStartAudioRenderer/handleStopAudioRenderercontract: same argument validation and error codes (INVALID_ARGUMENT,No such track), idempotent start for an existingrendererId, and success on stop for unknown ids.shared_cpp/audio_renderer.cppadded to both platforms' sources.Testing
audio_frame_capture_native.dartconsumes (commonFormat,sampleRate,channels,dataasUint8List, plusframeLengthfor parity with Android).