Skip to content

Cannot customize the push voice stream #213

Closed

Description

@vipzipp

Describe the bug
I am unable to hear clear sound on the device using CustomAudioSource.pushAudio, and it often reports errors

To Reproduce
Steps to reproduce the behavior:
The audio stream returned by calling TTS
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
audio = ByteUtils.concat(audio, data);
// play tts audio
MediaManager bean = ApplicationContextUtil.getBean(MediaManager.class);
CustomAudioSource customAudioSource = bean.getCustomAudioSource();

            customAudioSource.pushAudio(audio, 16, 16000, 1, 160);

Expected behavior
I expect the streaming voice to be played normally

Screenshots
If applicable, add screenshots to help explain your problem.

Desktop (please complete the following information):

  • OS: [e.g. iOS] macos
  • Version [e.g. 22] 15.6.1 (24G90)

Additional context
I hope to use WebRTC Java to implement a JavaScript request for Java, and then Java calls ASR, LLM synthesizes TTS speech stream, and then plays it, similar to a robot

Activity

  1. vipzipp commented on Sep 22, 2025

    @vipzipp
    Author

    Frequently prompt this error
    Image

  2. devopvoid commented on Oct 19, 2025

    @devopvoid
    Owner

    Duplicate of #118.

    You are not alone running into this issue. The problem here is that you are using the CustomAudioSource from multiple threads and then you are running into race conditions as the error states. To handle the CustomAudioSource properly, please wrap its usage into a single thread.

  3. ziyoushu commented on Oct 24, 2025

    @ziyoushu

    Duplicate of #118.

    You are not alone running into this issue. The problem here is that you are using the CustomAudioSource from multiple threads and then you are running into race conditions as the error states. To handle the CustomAudioSource properly, please wrap its usage into a single thread.

    Thanks! I redirected to the link to see the content. I had also added synchronized before and only came to seek advice after it was not resolved. I noticed that the issue # 118 has not been resolved, and several developers have encountered this problem @devopvoid

  4. devopvoid commented on Sep 13, 2026

    @devopvoid
    Owner

    Following up on this — I looked closer and my earlier "wrap it in a single thread" answer was incomplete. There's an actual native bug here, not just a caller-side threading issue.

    CustomAudioSource (webrtc-jni/src/main/cpp/src/media/audio/CustomAudioSource.cpp) keeps its registered sinks in a plain std::vector<webrtc::AudioTrackSinkInterface*> sinks_ with zero synchronization: the locking that should protect it is commented out in AddSink, RemoveSink, and PushAudioData (//webrtc::CritScope lock(&crit_);), and the crit_ member itself doesn't even exist — it's commented out in the header too.

    AddSink/RemoveSink are called by WebRTC's own internals whenever a track backed by this source gets attached to (or detached from) a peer connection's send stream — not by your code. So even if you perfectly single-thread your own pushAudio() calls, PushAudioData iterating sinks_ can still race against WebRTC's internal thread mutating that same vector via AddSink/RemoveSink the moment a connection attaches. That matches both the race_checker.RaceDetected() crash in #118 and why adding synchronized around your own calls here didn't fix it — you can't synchronize against a thread you don't control from the Java side.

    This needs an actual fix on the native side (restoring proper locking around sinks_), not a usage workaround. Keeping this open and tracking it as a real bug rather than user error.

  5. devopvoid commented on Sep 13, 2026

    @devopvoid
    Owner

    I dug into the actual native source and found something beyond the threading advice earlier in this thread: CustomAudioSource's sinks_ vector has no synchronization at all — the CritScope locks in AddSink/RemoveSink/PushAudioData, and the mutex member itself, are all commented out.

    That matters here because AddSink/RemoveSink are called by WebRTC's own thread as a track attaches to/detaches from a peer connection, not by your code — so no amount of synchronizing your own calls to pushAudio() prevents the race. This is a genuine native bug.

    Opened #285 with a fix (restores the lock with webrtc::Mutex) plus a regression test.

  6. added a commit that references this issue on Sep 13, 2026
    da75ca2
  7. devopvoid commented on Sep 16, 2026

    @devopvoid
    Owner

    The cause of this is now understood and fixed on fix/adm-capture-gate. Full write-up in #217.

    The unclear audio you were hearing was the microphone being mixed into your TTS stream. The factory's AudioDeviceModule feeds captured audio into every audio sender of that factory, including one backed by a CustomAudioSource, so both arrive at the same send stream. The same double feed is what aborts the process when the timing lines up. My earlier answer, to wrap the usage into a single thread, was wrong: the second thread is WebRTC's own capture thread, which you cannot synchronise against from Java.

    On the branch, a factory that sends pushed audio never opens the recording device, so nothing is mixed into your stream.

    Two things about pushAudio that matter for your code specifically. Each call carries one chunk, and 10 ms is the size WebRTC works with, so push 160 frames at a time rather than concatenating into a growing buffer and pushing that repeatedly. And only 16-bit samples are accepted, since WebRTC reads the data as 16-bit PCM whatever you declare. Both are validated now instead of failing obscurely or reading past the end of the array.

    @vipzipp @ziyoushu tagging you since you both ran into this.

  8. devopvoid commented on Sep 19, 2026

    @devopvoid
    Owner

    The unclear sound has a concrete cause, the same one behind #217.

    When a CustomAudioSource track is sent, WebRTC also starts the factory's AudioDeviceModule recording and feeds the captured microphone frames into every send stream of the factory. The encoder then receives microphone frames interleaved with the pushed audio. When the two feeders overlap exactly, the process aborts instead.

    #289 fixes this: a factory that sends pushed audio never opens the recording device. Note also that pushAudio() only ever accepted 16-bit PCM, despite the docs listing 8 and 32 bits; it now rejects other widths with an IllegalArgumentException.

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

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions