Skip to content

Commit 5f1bf88

Browse files
fix the timeout issue and a few bugs related to the SimpleRoom example
1 parent a444821 commit 5f1bf88

8 files changed

Lines changed: 192 additions & 49 deletions

File tree

client-sdk-rust

examples/simple_room/main.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ int main(int argc, char *argv[]) {
319319
<< " Creation time (ms): " << info.creation_time << "\n";
320320

321321
// Setup Audio Source / Track
322-
auto audioSource = std::make_shared<AudioSource>(44100, 1, 10);
322+
auto audioSource = std::make_shared<AudioSource>(44100, 1, 0);
323323
auto audioTrack =
324324
LocalAudioTrack::createLocalAudioTrack("micTrack", audioSource);
325325

@@ -385,6 +385,8 @@ int main(int argc, char *argv[]) {
385385
// Shutdown the audio / video capture threads.
386386
media.stopMic();
387387
media.stopCamera();
388+
media.stopSpeaker();
389+
media.shutdownRenderer();
388390

389391
// Drain any queued tasks that might still try to update the renderer /
390392
// speaker

examples/simple_room/sdl_media_manager.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ SDLMediaManager::~SDLMediaManager() {
3131
stopMic();
3232
stopCamera();
3333
stopSpeaker();
34+
shutdownRenderer();
3435
}
3536

3637
bool SDLMediaManager::ensureSDLInit(Uint32 flags) {

examples/simple_room/sdl_video_renderer.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222

2323
using namespace livekit;
2424

25+
constexpr int kMaxFPS = 60;
26+
2527
SDLVideoRenderer::SDLVideoRenderer() = default;
2628

2729
SDLVideoRenderer::~SDLVideoRenderer() { shutdown(); }
@@ -95,6 +97,16 @@ void SDLVideoRenderer::render() {
9597
return;
9698
}
9799

100+
// Throttle rendering to kMaxFPS
101+
const auto now = std::chrono::steady_clock::now();
102+
if (last_render_time_.time_since_epoch().count() != 0) {
103+
const auto min_interval = std::chrono::microseconds(1'000'000 / kMaxFPS);
104+
if (now - last_render_time_ < min_interval) {
105+
return;
106+
}
107+
}
108+
last_render_time_ = now;
109+
98110
// 3) Read a frame from VideoStream (blocking until one is available)
99111
livekit::VideoFrameEvent vfe;
100112
bool gotFrame = stream_->read(vfe);

examples/simple_room/sdl_video_renderer.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,4 +49,5 @@ class SDLVideoRenderer {
4949
std::shared_ptr<livekit::VideoStream> stream_;
5050
int width_ = 0;
5151
int height_ = 0;
52+
std::chrono::steady_clock::time_point last_render_time_{};
5253
};

include/livekit/audio_source.h

Lines changed: 51 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,34 @@ class AudioSource {
4141
* @param sample_rate Sample rate in Hz.
4242
* @param num_channels Number of channels.
4343
* @param queue_size_ms Max buffer duration for the internal queue in ms.
44+
*
45+
* Buffering behavior:
46+
* -------------------
47+
* - queue_size_ms == 0 (recommended for real-time capture):
48+
* Disables internal buffering entirely. Audio frames are forwarded
49+
* directly to WebRTC sinks and consumed synchronously.
50+
*
51+
* This mode is optimized for real-time audio capture driven by hardware
52+
* media callbacks (e.g. microphone capture). The caller is expected to
53+
* provide fixed-size real-time frames (typically 10 ms per call).
54+
*
55+
* Because the native side consumes frames immediately, this mode
56+
* minimizes latency and jitter and is the best choice for live capture
57+
* scenarios.
58+
*
59+
* - queue_size_ms > 0 (buffered / blocking mode):
60+
* Enables an internal queue that buffers audio up to the specified
61+
* duration. Frames are accumulated and flushed asynchronously once the buffer
62+
* reaches its threshold.
63+
*
64+
* This mode is intended for non-real-time producers (e.g. TTS engines,
65+
* file-based audio, or agents generating audio faster or slower than
66+
* real-time). The buffering layer smooths timing and allows the audio to
67+
* be streamed out in real time even if the producer is bursty.
68+
*
69+
* queue_size_ms must be a multiple of 10.
4470
*/
45-
AudioSource(int sample_rate, int num_channels, int queue_size_ms = 1000);
71+
AudioSource(int sample_rate, int num_channels, int queue_size_ms = 0);
4672
virtual ~AudioSource() = default;
4773

4874
AudioSource(const AudioSource &) = delete;
@@ -86,19 +112,32 @@ class AudioSource {
86112
* callback arrives (recommended for production unless the caller needs
87113
* explicit timeout control).
88114
*
89-
* Notes:
90-
* - This is a blocking call.
91-
* - timeout_ms == 0 (infinite wait) is the safest mode because it
92-
* guarantees the callback completes before the function returns, which in
93-
* turn guarantees that the audio buffer lifetime is fully protected. The
94-
* caller does not need to manage or extend the frame lifetime manually.
115+
* Blocking semantics:
116+
* The blocking behavior of this call depends on the buffering mode selected
117+
* at construction time:
118+
*
119+
* - queue_size_ms == 0 (real-time capture mode):
120+
* Frames are consumed synchronously by the native layer. The FFI callback
121+
* is invoked immediately as part of the capture call, so this function
122+
* returns quickly.
123+
*
124+
* This mode relies on the caller being paced by a real-time media
125+
* callback (e.g. audio hardware interrupt / capture thread). It provides the
126+
* lowest possible latency and is ideal for live microphone capture.
127+
*
128+
* - queue_size_ms > 0 (buffered / non-real-time mode):
129+
* Frames are queued internally and flushed asynchronously. This function
130+
* will block until the buffered audio corresponding to this frame has
131+
* been consumed by the native side and the FFI callback fires.
95132
*
96-
* - May throw std::runtime_error if:
97-
* • the FFI reports an error
133+
* This mode is best suited for non-real-time audio producers (such as TTS
134+
* engines or agents) that generate audio independently of real-time
135+
* pacing, while still streaming audio out in real time.
98136
*
99-
* - The underlying FFI request *must* eventually produce a callback for
100-
* each frame. If the FFI layer is misbehaving or the event loop is stalled,
101-
* a timeout may occur in bounded-wait mode.
137+
* Safety notes:
138+
* May throw std::runtime_error if:
139+
* - the FFI reports an error
140+
* - a timeout occurs in bounded-wait mode
102141
*/
103142
void captureFrame(const AudioFrame &frame, int timeout_ms = 20);
104143

src/ffi_client.cpp

Lines changed: 113 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,93 @@ inline void logAndThrow(const std::string &error_msg) {
4444
throw std::runtime_error(error_msg);
4545
}
4646

47+
std::optional<FfiClient::AsyncId> ExtractAsyncId(const proto::FfiEvent &event) {
48+
using E = proto::FfiEvent;
49+
switch (event.message_case()) {
50+
case E::kConnect:
51+
return event.connect().async_id();
52+
case E::kDisconnect:
53+
return event.disconnect().async_id();
54+
case E::kDispose:
55+
return event.dispose().async_id();
56+
case E::kPublishTrack:
57+
return event.publish_track().async_id();
58+
case E::kUnpublishTrack:
59+
return event.unpublish_track().async_id();
60+
case E::kPublishData:
61+
return event.publish_data().async_id();
62+
case E::kPublishTranscription:
63+
return event.publish_transcription().async_id();
64+
case E::kCaptureAudioFrame:
65+
return event.capture_audio_frame().async_id();
66+
case E::kSetLocalMetadata:
67+
return event.set_local_metadata().async_id();
68+
case E::kSetLocalName:
69+
return event.set_local_name().async_id();
70+
case E::kSetLocalAttributes:
71+
return event.set_local_attributes().async_id();
72+
case E::kGetStats:
73+
return event.get_stats().async_id();
74+
case E::kGetSessionStats:
75+
return event.get_session_stats().async_id();
76+
case E::kPublishSipDtmf:
77+
return event.publish_sip_dtmf().async_id();
78+
case E::kChatMessage:
79+
return event.chat_message().async_id();
80+
case E::kPerformRpc:
81+
return event.perform_rpc().async_id();
82+
83+
// low-level data stream callbacks
84+
case E::kSendStreamHeader:
85+
return event.send_stream_header().async_id();
86+
case E::kSendStreamChunk:
87+
return event.send_stream_chunk().async_id();
88+
case E::kSendStreamTrailer:
89+
return event.send_stream_trailer().async_id();
90+
91+
// high-level
92+
case E::kByteStreamReaderReadAll:
93+
return event.byte_stream_reader_read_all().async_id();
94+
case E::kByteStreamReaderWriteToFile:
95+
return event.byte_stream_reader_write_to_file().async_id();
96+
case E::kByteStreamOpen:
97+
return event.byte_stream_open().async_id();
98+
case E::kByteStreamWriterWrite:
99+
return event.byte_stream_writer_write().async_id();
100+
case E::kByteStreamWriterClose:
101+
return event.byte_stream_writer_close().async_id();
102+
case E::kSendFile:
103+
return event.send_file().async_id();
104+
105+
case E::kTextStreamReaderReadAll:
106+
return event.text_stream_reader_read_all().async_id();
107+
case E::kTextStreamOpen:
108+
return event.text_stream_open().async_id();
109+
case E::kTextStreamWriterWrite:
110+
return event.text_stream_writer_write().async_id();
111+
case E::kTextStreamWriterClose:
112+
return event.text_stream_writer_close().async_id();
113+
case E::kSendText:
114+
return event.send_text().async_id();
115+
case E::kSendBytes:
116+
return event.send_bytes().async_id();
117+
118+
// NOT async completion:
119+
case E::kRoomEvent:
120+
case E::kTrackEvent:
121+
case E::kVideoStreamEvent:
122+
case E::kAudioStreamEvent:
123+
case E::kByteStreamReaderEvent:
124+
case E::kTextStreamReaderEvent:
125+
case E::kRpcMethodInvocation:
126+
case E::kLogs:
127+
case E::kPanic:
128+
case E::MESSAGE_NOT_SET:
129+
default:
130+
return std::nullopt;
131+
}
132+
}
133+
47134
} // namespace
48135

49136
FfiClient::~FfiClient() {
@@ -77,7 +164,7 @@ bool FfiClient::isInitialized() const noexcept {
77164
FfiClient::ListenerId
78165
FfiClient::AddListener(const FfiClient::Listener &listener) {
79166
std::lock_guard<std::mutex> guard(lock_);
80-
FfiClient::ListenerId id = nextListenerId++;
167+
FfiClient::ListenerId id = next_listener_id++;
81168
listeners_[id] = listener;
82169
return id;
83170
}
@@ -117,34 +204,33 @@ FfiClient::sendRequest(const proto::FfiRequest &request) const {
117204
}
118205

119206
void FfiClient::PushEvent(const proto::FfiEvent &event) const {
120-
std::vector<std::unique_ptr<PendingBase>> to_complete;
207+
std::unique_ptr<PendingBase> to_complete;
208+
std::vector<Listener> listeners_copy;
121209
{
122210
std::lock_guard<std::mutex> guard(lock_);
123-
for (auto it = pending_.begin(); it != pending_.end();) {
124-
if ((*it)->matches(event)) {
125-
to_complete.push_back(std::move(*it));
126-
it = pending_.erase(it);
127-
} else {
128-
++it;
211+
212+
// Complete pending future if this event is a callback with async_id
213+
if (auto async_id = ExtractAsyncId(event)) {
214+
auto it = pending_by_id_.find(*async_id);
215+
if (it != pending_by_id_.end() && it->second &&
216+
it->second->matches(event)) {
217+
to_complete = std::move(it->second);
218+
pending_by_id_.erase(it);
129219
}
130220
}
131-
}
132-
133-
// Run handlers outside lock
134-
for (auto &p : to_complete) {
135-
p->complete(event);
136-
}
137221

138-
// Notify listeners. Note, we copy the listeners here to avoid calling into
139-
// the listeners under the lock, which could potentially cause deadlock.
140-
std::vector<Listener> listeners_copy;
141-
{
142-
std::lock_guard<std::mutex> guard(lock_);
222+
// Snapshot listeners
143223
listeners_copy.reserve(listeners_.size());
144-
for (auto &[_, listener] : listeners_) {
145-
listeners_copy.push_back(listener);
224+
for (const auto &kv : listeners_) {
225+
listeners_copy.push_back(kv.second);
146226
}
147227
}
228+
// Run handler outside lock
229+
if (to_complete) {
230+
to_complete->complete(event);
231+
}
232+
233+
// Notify listeners outside lock
148234
for (auto &listener : listeners_copy) {
149235
listener(event);
150236
}
@@ -158,22 +244,19 @@ void LivekitFfiCallback(const uint8_t *buf, size_t len) {
158244
}
159245

160246
FfiClient::AsyncId FfiClient::generateAsyncId() {
161-
return nextAsyncId_.fetch_add(1, std::memory_order_relaxed);
247+
return next_async_id_.fetch_add(1, std::memory_order_relaxed);
162248
}
163249

164250
bool FfiClient::cancelPendingByAsyncId(AsyncId async_id) {
165251
std::unique_ptr<PendingBase> to_cancel;
166252
{
167253
std::lock_guard<std::mutex> guard(lock_);
168-
for (auto it = pending_.begin(); it != pending_.end(); ++it) {
169-
if ((*it)->async_id == async_id) {
170-
to_cancel = std::move(*it);
171-
pending_.erase(it);
172-
break;
173-
}
254+
auto it = pending_by_id_.find(async_id);
255+
if (it != pending_by_id_.end()) {
256+
to_cancel = std::move(it->second);
257+
pending_by_id_.erase(it);
174258
}
175259
}
176-
177260
if (to_cancel) {
178261
to_cancel->cancel();
179262
return true;
@@ -192,7 +275,7 @@ std::future<T> FfiClient::registerAsync(
192275
pending->handler = std::move(handler);
193276
{
194277
std::lock_guard<std::mutex> guard(lock_);
195-
pending_.push_back(std::move(pending));
278+
pending_by_id_.emplace(async_id, std::move(pending));
196279
}
197280
return fut;
198281
}

src/ffi_client.h

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,12 @@ class FfiClient {
169169
}
170170

171171
void cancel() override {
172-
promise.set_exception(std::make_exception_ptr(
173-
std::runtime_error("Async operation cancelled")));
172+
try {
173+
promise.set_exception(std::make_exception_ptr(
174+
std::runtime_error("Async operation cancelled")));
175+
} catch (const std::future_error &) {
176+
// already satisfied
177+
}
174178
}
175179
};
176180

@@ -187,10 +191,11 @@ class FfiClient {
187191
bool cancelPendingByAsyncId(AsyncId async_id);
188192

189193
std::unordered_map<ListenerId, Listener> listeners_;
190-
ListenerId nextListenerId = 1;
194+
std::atomic<ListenerId> next_listener_id{1};
191195
mutable std::mutex lock_;
192-
mutable std::vector<std::unique_ptr<PendingBase>> pending_;
193-
std::atomic<AsyncId> nextAsyncId_{1};
196+
mutable std::unordered_map<AsyncId, std::unique_ptr<PendingBase>>
197+
pending_by_id_;
198+
std::atomic<AsyncId> next_async_id_{1};
194199

195200
void PushEvent(const proto::FfiEvent &event) const;
196201
friend void LivekitFfiCallback(const uint8_t *buf, size_t len);

0 commit comments

Comments
 (0)