Skip to content

Fix: call playback started in sound device callback (console mode)#4958

Open
chenghao-mou wants to merge 1 commit intomainfrom
fix/accurate-console-first-frame-timestamp
Open

Fix: call playback started in sound device callback (console mode)#4958
chenghao-mou wants to merge 1 commit intomainfrom
fix/accurate-console-first-frame-timestamp

Conversation

@chenghao-mou
Copy link
Member

Previously, the playback started is called when the first frame is captured. Now it is delayed until we have data consumed from the sound device callback. This improves the start timestamp by about 30~100ms.

@chenghao-mou chenghao-mou requested a review from a team February 26, 2026 13:02
Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

Comment on lines +259 to +261
self._playback_started_fired = True
t = _wall_time()
self._loop.call_soon_threadsafe(lambda: self.on_playback_started(created_at=t))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Stale on_playback_started can fire after on_playback_finished due to call_soon_threadsafe scheduling

When playback is interrupted, a stale on_playback_started event can fire after on_playback_finished for the same (or even a new) segment, because the event is scheduled asynchronously via call_soon_threadsafe from the sound device thread.

Race condition timeline
  1. Sound device thread (under audio_lock): _maybe_mark_playback_started() sets _playback_started_fired = True and calls self._loop.call_soon_threadsafe(lambda: self.on_playback_started(created_at=t)) — the callback is now queued on the event loop but not yet executed.
  2. Event loop thread: clear_buffer() runs, acquires audio_lock, resets _playback_started_fired = False, then sets _interrupted_ev.
  3. Event loop thread: _wait_for_playout resumes, calls self.on_playback_finished(...) at cli.py:245.
  4. Event loop thread: The stale on_playback_started callback from step 1 finally executes — after on_playback_finished.

This violates the expected playback_started → playback_finished ordering. If a new segment has started by step 4 and a new listener is registered, the stale event could set first_frame_fut in livekit-agents/livekit/agents/voice/generation.py:367-368 with an incorrect (old) timestamp.

The primary consumer in generation.py has a if not out.first_frame_fut.done() guard which limits the damage, and this only affects console mode, so practical impact is limited. A fix would be to add a generation counter or sequence ID so stale callbacks can be detected and dropped in the lambda.

Prompt for agents
In livekit-agents/livekit/agents/cli/cli.py, the _maybe_mark_playback_started method at line 255-261 schedules on_playback_started via call_soon_threadsafe, but the callback can execute after on_playback_finished if an interruption occurs between scheduling and execution.

To fix this, add a generation counter (e.g. self._playback_generation: int = 0) that increments on every reset (in clear_buffer and _wait_for_playout). Capture the current generation in _maybe_mark_playback_started and check it in the scheduled lambda:

    def _maybe_mark_playback_started(self) -> None:
        if self._playback_started_fired:
            return
        self._playback_started_fired = True
        t = _wall_time()
        gen = self._playback_generation
        self._loop.call_soon_threadsafe(lambda: self.on_playback_started(created_at=t) if self._playback_generation == gen else None)

Increment self._playback_generation wherever _playback_started_fired is reset to False (lines 199, 252).
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant