Skip to content

Conversation

@Cabbache
Copy link

This is a WIP based on the poc by @yume-chan to add the feature requested in #3880

@Cabbache Cabbache force-pushed the forward-client-microphone branch from 9d10730 to df655e4 Compare October 19, 2025 16:58
@Cabbache Cabbache changed the title [Draft] Forward client microphone [Draft] Forward client audio Oct 21, 2025
@Cabbache Cabbache marked this pull request as draft October 23, 2025 10:39

int read_mic(void *data) {
sc_socket mic_socket = *(sc_socket*)data;
const char *input_format_name = "alsa";
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess this is only available on Linux? Can SDL be used instead?

Copy link
Author

@Cabbache Cabbache Nov 3, 2025

Choose a reason for hiding this comment

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

@yume-chan it should work on other platforms if you change alsa to something appropriate? We might want input_format_name to be passed as an argument.

It's possible to list the available input devices with

const AVInputFormat *fmt = NULL;
fmt = av_input_audio_device_next(NULL);
while (fmt) {
  printf("%s: %s\n",
    fmt->name,
    fmt->long_name ? fmt->long_name : "(no description)"
   );
   fmt = av_input_audio_device_next(fmt);
}

This is what I see on linux:

alsa: ALSA audio input
jack: JACK Audio Connection Kit
lavfi: Libavfilter virtual input device
oss: OSS (Open Sound System) capture
pulse: Pulse audio input

@Hideman85
Copy link

Looking forward to this feature, would be really good in phone call experience while working for instance.

@Cabbache Cabbache force-pushed the forward-client-microphone branch 2 times, most recently from 7b052b2 to 8e1da04 Compare December 8, 2025 04:25
@Cabbache
Copy link
Author

Cabbache commented Dec 8, 2025

It is working and somewhat stable on high latency. Tested from a laptop running linux and an android 16 device.

The new arguments are like this:

<     --client-audio-source=source
<         Inject audio into the device microphone from a device or file.
<         The source can be:
<           - A device name (e.g., "Microphone", "default")
<           - A file path prefixed with "file://" (e.g.,
<         "file:///path/to/audio.mp3")
<         Supported file formats: MP3, OGG, WAV, FLAC, etc.
< 

<     --list-audio-sources
<         List available audio input sources on the client computer.

This is not enabled by default so you have to explicitly add the argument, e.g
scrcpy --client-audio-source default

@Cabbache Cabbache force-pushed the forward-client-microphone branch from 8e1da04 to f565df6 Compare December 14, 2025 09:04
@Cabbache Cabbache changed the title [Draft] Forward client audio Forward client audio Dec 20, 2025
@Cabbache Cabbache marked this pull request as ready for review December 20, 2025 14:48
@JunaidQrysh
Copy link

JunaidQrysh commented Dec 22, 2025

@Cabbache i am getting these errors the moment i answer a phone call. Though i can use any recorder app on android to record using pc mic

> scrcpy --client-audio-source default
scrcpy 3.3.3 <https://github.com/Genymobile/scrcpy>
INFO: ADB device found:
INFO:     --> (tcpip)  10.159.163.21:5555              device  CPH2585
/usr/local/share/scrcpy/scrcpy-server: 1 file pushed, 0 skipped. 161.1 MB/s (93012 bytes in 0.001s)
[server] INFO: Device: [OnePlus] OnePlus CPH2585 (Android 16)
INFO: Renderer: opengl
INFO: OpenGL version: 4.6 (Compatibility Profile) Mesa 25.3.2-arch1.1
INFO: Trilinear filtering enabled
INFO: Texture: 1080x2376
INFO: Recording audio with Opus encoding...
[server] ERROR: Audio capture error
java.io.IOException: Could not read audio: -6
        at com.genymobile.scrcpy.audio.AudioEncoder.inputThread(AudioEncoder.java:110)
        at com.genymobile.scrcpy.audio.AudioEncoder.lambda$encode$1$com-genymobile-scrcpy-audio-AudioEncoder(AudioEncoder.java:240)
        at com.genymobile.scrcpy.audio.AudioEncoder$$ExternalSyntheticLambda2.run(D8$$SyntheticClass:0)
        at java.lang.Thread.run(Thread.java:1563)
WARN: [FFmpeg] Queue input is backward in time

and it also does not work with
scrcpy --audio-source=voice-call --client-audio-source default

@Cabbache
Copy link
Author

Cabbache commented Dec 22, 2025

@JunaidQrysh Hmm I haven't tested this thoroughly, let me see if I can replicate it.

@Cabbache Cabbache force-pushed the forward-client-microphone branch from de939a0 to fe310e2 Compare December 24, 2025 09:48
@Cabbache
Copy link
Author

Cabbache commented Dec 24, 2025

@JunaidQrysh I also see similar errors, but the audio still worked.

[server] ERROR: Audio capture error
java.io.IOException: Could not read audio: 0
	at com.genymobile.scrcpy.audio.AudioEncoder.inputThread(AudioEncoder.java:110)
	at com.genymobile.scrcpy.audio.AudioEncoder.lambda$encode$1$com-genymobile-scrcpy-audio-AudioEncoder(AudioEncoder.java:240)
	at com.genymobile.scrcpy.audio.AudioEncoder$$ExternalSyntheticLambda0.run(D8$$SyntheticClass:0)
	at java.lang.Thread.run(Thread.java:1365)

edit: I've tested only with --client-audio-source default (i.e without --audio-source=voice-call)

@JunaidQrysh
Copy link

@Cabbache the audio and client mic should work in a voice call. I get those errors when i make a voice call using scrcpy --client-audio-source default and dont get those when using scrcpy --audio-source=voice-call --client-audio-source default but client mic does not get forwarded then, losing the point of client-audio-source.

@JunaidQrysh
Copy link

@Cabbache i have found that scrcpy --audio-source=voice-call-downlink --client-audio-source default works with no errors and even pc mic is forwarded but only to the phone and not to the person i am in call with. I can say this because i have voice recording enabled while in call and it records audio from my pc mic but the person i am in call with does not hear it.

@diffray-bot
Copy link

Changes Summary

This PR implements client-side microphone audio forwarding to Android devices, allowing users to inject audio from their computer's microphone (or audio files) into the Android device. The feature includes a new audio decoder (Opus format), audio injector using Android's AudioPolicy APIs, command-line options for audio source selection, and a utility to list available audio sources.

Type: feature

Components Affected: Client-side audio capture (C), Server-side audio injection (Java), Command-line interface, Socket management for microphone stream, Audio decoding pipeline, Options/configuration system

Files Changed
File Summary Change Impact
app/src/client_audio.c New module implementing client-side microphone audio capture using FFmpeg/libav libraries, with support for device enumeration and file-based audio sources. 🔴
app/src/client_audio.h Header file defining microphone parameters structure and public API for listing audio sources and running the microphone capture thread. 🔴
app/src/cli.c Added command-line options --client-audio-source and --list-audio-sources to support microphone forwarding feature. ✏️ 🟡
app/src/cli.h Added list_audio_sources field to scrcpy_cli_args structure. ✏️ 🟢
app/src/options.h Added client_audio_source field to scrcpy_options structure for storing the audio source configuration. ✏️ 🟢
app/src/options.c Updated options parsing to handle client_audio_source field. ✏️ 🟢
app/src/main.c Added initialization of avdevice for microphone support and handling of --list-audio-sources flag; moved avdevice registration outside of HAVE_V4L2 ifdef. ✏️ 🟡
app/src/server.c Added client_mic_socket management, connection setup, TCP_NODELAY optimization, and proper cleanup for client audio streaming. ✏️ 🔴
app/src/server.h Added client_audio boolean flag to server_params and client_mic_socket field to server structure. ✏️ 🟡
app/src/scrcpy.c Added microphone thread creation and management, passing client_audio_source to the microphone capture module. ✏️ 🔴
app/meson.build Added client_audio.c source file to the build configuration. ✏️ 🟢
app/tests/test_cli.c Updated test structures to initialize list_audio_sources field in all test cases. ✏️ 🟢
...n/java/com/genymobile/scrcpy/AudioInjector.java New Java class implementing audio injection into Android device microphone using AudioPolicy and reflection APIs, supporting various microphone input modes. 🔴
...rc/main/java/com/genymobile/scrcpy/Options.java Added client_audio boolean option and getter method for configuration. ✏️ 🟡
...src/main/java/com/genymobile/scrcpy/Server.java Integrated audio decoder and injector into main server loop; retrieves client audio socket and processes audio stream through decoder and injector pipeline. ✏️ 🔴
...a/com/genymobile/scrcpy/audio/AudioDecoder.java New audio decoder handling Opus format decompression, configuring MediaCodec with proper CSD (codec-specific data) headers. 🔴
...genymobile/scrcpy/device/DesktopConnection.java Added clientAudioSocket connection handling, accepting/connecting to microphone socket, and providing accessor method. ✏️ 🔴
Architecture Impact
  • New Patterns: Audio streaming pipeline (client capture → encoding → transmission → decoding → injection), Thread-based async processing (microphone capture runs in separate thread), Socket-based IPC for audio stream transport, Android reflection-based API access (AudioPolicy)
  • Dependencies: added: libavdevice for audio device enumeration and capture, added: libswresample for audio format conversion, added: Android MediaCodec for audio decoding (server-side)
  • Coupling: New coupling between main scrcpy flow and microphone thread; audio socket now treated as peer to video/audio/control sockets in server initialization. Client audio processing is tightly integrated into Server.java main loop.
  • Breaking Changes: Changed validation logic: --no-video --no-audio --no-control without microphone is now invalid (was previously invalid, now includes --no-client-audio in the check), avdevice registration now unconditional (moved from HAVE_V4L2 conditional) - may have minor platform impact

Risk Areas: Android AudioPolicy API use via reflection: Fragile across Android versions, relies on private APIs that may change or be restricted on newer Android versions, Audio format/codec assumptions: Hardcoded Opus format and stereo 48kHz configuration may not work universally; limited flexibility for different audio formats, Buffer management: PipedInputStream/PipedOutputStream with 500KB buffer - potential blocking/deadlock if producer/consumer rates mismatch, Error handling in audio decoder: Limited error recovery in AudioDecoder.start() and audio injection pipeline - exceptions logged but may silently fail, Platform-specific audio capture: Uses different FFmpeg input formats (WASAPI/dshow on Windows, avfoundation on macOS, ALSA on Linux) - testing burden across platforms, Memory/resource lifecycle: Multiple concurrent streams (video, audio, microphone) - potential for resource exhaustion with long-running sessions, Socket connection ordering: Complex socket management logic in server.c for client_audio_socket - subtle bugs possible in connection establishment or cleanup

Suggestions
  • Add comprehensive error handling and recovery in the audio decoder - currently exceptions may silently fail the audio injection
  • Consider making audio format/codec configurable rather than hardcoding Opus stereo 48kHz
  • Add logging/debugging facilities for audio injection pipeline to help diagnose issues across different Android versions
  • Consider version detection for Android AudioPolicy APIs to provide graceful fallback on older devices
  • Add integration tests for the microphone forwarding feature with different audio sources and formats
  • Document the private API dependency and version compatibility matrix for AudioPolicy
  • Review buffer sizes (500KB PipedInputStream) under various load conditions to ensure no deadlocks
  • Consider adding audio format conversion capabilities if supporting multiple formats is desired

Full review in progress... | Powered by diffray

try {
decoder.stop();
decoder.release();
} catch (Exception e) {}

Choose a reason for hiding this comment

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

🟠 HIGH - Empty catch block without exception logging
Agent: spring

Category: bug

Description:
The catch block at line 130 is empty - it catches Exception but does nothing with it. This silently swallows critical decoder cleanup errors without any diagnostic information.

Suggestion:
Log the exception using Ln.e() before the cleanup logic: catch (Exception e) { Ln.e("Error stopping decoder", e); }

Why this matters: Swallowed errors hide bugs and make debugging difficult.

Confidence: 85%
Rule: java_avoid_empty_catch_blocks
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +108 to +116
ByteBuffer outputBuffer = decoder.getOutputBuffer(outputIndex);
if (bufferInfo.size > 0) {
byte[] pcmData = new byte[bufferInfo.size];
outputBuffer.position(bufferInfo.offset);
outputBuffer.limit(bufferInfo.offset + bufferInfo.size);
outputBuffer.get(pcmData);
pcmOutput.write(pcmData);
pcmOutput.flush();
}

Choose a reason for hiding this comment

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

🟠 HIGH - Potential null dereference of outputBuffer
Agent: bugs

Category: bug

Description:
decoder.getOutputBuffer(outputIndex) can return null. The code uses outputBuffer without null checking, even though size is checked.

Suggestion:
Add a null check: if (outputBuffer == null) { decoder.releaseOutputBuffer(...); continue; } before using outputBuffer.

Why this matters: NullPointerException is the most common runtime exception in Java.

Confidence: 75%
Rule: bug_null_pointer_java
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 196 to 197
return audioFd;
}

Choose a reason for hiding this comment

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

🟠 HIGH - Potential null dereference in getFirstSocket
Agent: bugs

Category: bug

Description:
getFirstSocket() result is immediately used to get a FileDescriptor without null checking. If getFirstSocket() returns null, calling getFileDescriptor() on null will cause a NullPointerException.

Suggestion:
Check if the result is not null: LocalSocket socket = getFirstSocket(); if (socket == null) throw new IOException("No sockets available"); FileDescriptor fd = socket.getFileDescriptor();

Why this matters: NullPointerException is the most common runtime exception in Java.

Confidence: 80%
Rule: bug_null_pointer_java
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +90 to +92
ByteBuffer inputBuffer = decoder.getInputBuffer(inputIndex);
inputBuffer.clear();
inputBuffer.put(opusData);

Choose a reason for hiding this comment

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

🟠 HIGH - Potential null dereference of inputBuffer
Agent: bugs

Category: bug

Description:
decoder.getInputBuffer(inputIndex) can return null, but the code calls inputBuffer.clear() and inputBuffer.put() without null checking first.

Suggestion:
Add a null check: if (inputBuffer == null) { continue; } before calling methods on inputBuffer.

Why this matters: NullPointerException is the most common runtime exception in Java.

Confidence: 75%
Rule: bug_null_pointer_java
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +150 to +164
new Thread(() -> {
byte[] audioBuffer = new byte[4096];
while (true) {
try {
int bytesRead = pis.read(audioBuffer);
if (bytesRead <= 0) {
break;
}
audioTrack.write(audioBuffer, 0, bytesRead);
} catch (Exception e) {
Ln.e("Audio injection error", e);
break;
}
}
}, "client-audio-injector").start();

Choose a reason for hiding this comment

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

🟡 MEDIUM - PipedInputStream not closed in new thread
Agent: bugs

Category: bug

Description:
The PipedInputStream 'pis' passed to the audio injector thread is never explicitly closed. The thread reads from it in an infinite loop, but there's no guarantee it will be closed if the thread exits.

Suggestion:
Add a try-finally block to close pis after the while loop exits, or wrap the read loop in try-with-resources if the stream should be owned by this thread.

Why this matters: Memory leaks degrade performance and can crash the application.

Confidence: 70%
Rule: java_resource_leak
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

return clientAudioSocket;
}

public void shutdown() throws IOException {

Choose a reason for hiding this comment

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

🟡 MEDIUM - Incomplete shutdown() for clientAudioSocket
Agent: refactoring

Category: bug

Description:
The shutdown() method shuts down video, audio, and control sockets but NOT clientAudioSocket. Additionally, close() method (lines 167-177) also omits clientAudioSocket. This leaves the socket in an inconsistent state.

Suggestion:
Add shutdown for clientAudioSocket: if (clientAudioSocket != null) { clientAudioSocket.shutdownInput(); clientAudioSocket.shutdownOutput(); } and also add clientAudioSocket.close() in the close() method.

Confidence: 95%
Rule: quality_law_of_demeter
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

return clientAudioSocket;
}

public void shutdown() throws IOException {

Choose a reason for hiding this comment

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

🟠 HIGH - Missing error handling for socket shutdown operations
Agent: spring

Category: bug

Description:
The shutdown() method calls shutdownInput() and shutdownOutput() on sockets without try-catch. These throw IOException which propagates and may leave sockets in inconsistent state.

Suggestion:
Either wrap each socket shutdown in individual try-catch to ensure all sockets are shutdown even if one fails, or document that caller must handle partial cleanup.

Confidence: 75%
Rule: java_add_try_catch_blocks_for_external_servic
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines 139 to 150
private LocalSocket getFirstSocket() {
if (videoSocket != null) {
return videoSocket;
}
if (audioSocket != null) {
return audioSocket;
}
return controlSocket;
if (controlSocket != null) {
return controlSocket;
}
return clientAudioSocket;
}

Choose a reason for hiding this comment

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

🟡 MEDIUM - Missing null check in getFirstSocket()
Agent: refactoring

Category: quality

Description:
The getFirstSocket() method returns clientAudioSocket without null check if all other sockets are null. Line 187 calls getFirstSocket().getFileDescriptor() which would NPE if all sockets are null.

Suggestion:
Either throw IllegalStateException if all sockets are null, or add defensive null check in sendDeviceMeta(). Document the contract clearly.

Confidence: 70%
Rule: quality_law_of_demeter
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +81 to +97
int packetSize = ((sizeBuffer[0] & 0xFF) << 24) |
((sizeBuffer[1] & 0xFF) << 16) |
((sizeBuffer[2] & 0xFF) << 8) |
(sizeBuffer[3] & 0xFF);

if (packetSize > 0 && packetSize <= 100000) {
byte[] opusData = new byte[packetSize];
int actualRead = bis.read(opusData);
if (actualRead == packetSize) {
ByteBuffer inputBuffer = decoder.getInputBuffer(inputIndex);
inputBuffer.clear();
inputBuffer.put(opusData);
decoder.queueInputBuffer(inputIndex, 0, packetSize, presentationTime, 0);
// Increment presentation time (20ms per frame at 48kHz = 960 samples)
presentationTime += 20000; // microseconds
}
}

Choose a reason for hiding this comment

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

🟡 MEDIUM - Large memory allocation from untrusted network data
Agent: security

Category: security

Description:
Lines 81-84 parse 32-bit big-endian integer from network. Limit of 100KB (line 86) is reasonable but allocates new byte array per packet without pooling, which could cause memory pressure under high load.

Suggestion:
Consider using a reusable buffer pool for common packet sizes, or add rate limiting for allocation frequency.

Confidence: 60%
Rule: c_buffer_overflow
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

Comment on lines +33 to +44
//private final FileDescriptor micFd;

private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket, LocalSocket clientAudioSocket) throws IOException {
this.videoSocket = videoSocket;
this.audioSocket = audioSocket;
this.controlSocket = controlSocket;
this.clientAudioSocket = clientAudioSocket;

videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null;
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
controlChannel = controlSocket != null ? new ControlChannel(controlSocket) : null;
//micFd = clientAudioSocket != null ? clientAudioSocket.getFileDescriptor() : null;

Choose a reason for hiding this comment

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

🔵 LOW - Commented-out code for microphone socket
Agent: refactoring

Category: quality

Description:
Lines 33 and 44 have commented-out code for micFd. This indicates incomplete implementation that should be cleaned up or completed.

Suggestion:
Remove commented-out code entirely, or add TODO comment explaining the planned implementation.

Confidence: 80%
Rule: quality_law_of_demeter
Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

@diffray-bot
Copy link

Review Summary

Free public review - Want AI code reviews on your PRs? Check out diffray.ai

Validated 40 issues: 22 kept, 18 filtered

Issues Found: 22

💬 See 21 individual line comment(s) for details.

📊 8 unique issue type(s) across 22 location(s)

📋 Full issue list (click to expand)

🟠 HIGH - Array bounds violation on audio stream access (2 occurrences)

Agent: security

Category: security

Why this matters: Buffer overflows allow arbitrary code execution.

📍 View all locations
File Description Suggestion Confidence
app/src/client_audio.c:169-171 Line 171 accesses fmt_ctx->streams[audio_stream_index] with audio_stream_index hardcoded to 0, but t... Validate audio_stream_index: if (fmt_ctx->nb_streams == 0 || fmt_ctx->streams[audio_stream_index] ... 85%
server/src/main/java/com/genymobile/scrcpy/audio/AudioDecoder.java:81-97 Lines 81-84 parse 32-bit big-endian integer from network. Limit of 100KB (line 86) is reasonable but... Consider using a reusable buffer pool for common packet sizes, or add rate limiting for allocation f... 60%

Rule: c_buffer_overflow


🟠 HIGH - Potential null dereference of outputBuffer (3 occurrences)

Agent: bugs

Category: bug

Why this matters: NullPointerException is the most common runtime exception in Java.

📍 View all locations
File Description Suggestion Confidence
server/src/main/java/com/genymobile/scrcpy/audio/AudioDecoder.java:108-116 decoder.getOutputBuffer(outputIndex) can return null. The code uses outputBuffer without null checki... Add a null check: if (outputBuffer == null) { decoder.releaseOutputBuffer(...); continue; } before u... 75%
server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java:196-197 getFirstSocket() result is immediately used to get a FileDescriptor without null checking. If getFir... Check if the result is not null: LocalSocket socket = getFirstSocket(); if (socket == null) throw ne... 80%
server/src/main/java/com/genymobile/scrcpy/audio/AudioDecoder.java:90-92 decoder.getInputBuffer(inputIndex) can return null, but the code calls inputBuffer.clear() and input... Add a null check: if (inputBuffer == null) { continue; } before calling methods on inputBuffer. 75%

Rule: bug_null_pointer_java


🟠 HIGH - Empty catch block without exception logging

Agent: spring

Category: bug

Why this matters: Swallowed errors hide bugs and make debugging difficult.

File: server/src/main/java/com/genymobile/scrcpy/audio/AudioDecoder.java:130

Description: The catch block at line 130 is empty - it catches Exception but does nothing with it. This silently swallows critical decoder cleanup errors without any diagnostic information.

Suggestion: Log the exception using Ln.e() before the cleanup logic: catch (Exception e) { Ln.e("Error stopping decoder", e); }

Confidence: 85%

Rule: java_avoid_empty_catch_blocks


🟡 MEDIUM - PipedInputStream not closed in new thread (2 occurrences)

Agent: bugs

Category: bug

Why this matters: Memory leaks degrade performance and can crash the application.

📍 View all locations
File Description Suggestion Confidence
server/src/main/java/com/genymobile/scrcpy/AudioInjector.java:150-164 The PipedInputStream 'pis' passed to the audio injector thread is never explicitly closed. The threa... Add a try-finally block to close pis after the while loop exits, or wrap the read loop in try-with-r... 70%
server/src/main/java/com/genymobile/scrcpy/Server.java:173-177 PipedOutputStream 'pos' and PipedInputStream 'pis' created at lines 173-174 are not in try-with-reso... Wrap the creation and usage in a try-with-resources block or add proper cleanup in a finally block. 75%

Rule: java_resource_leak


🟡 MEDIUM - Timing control logic embedded in encoding loop

Agent: architecture

Category: quality

Why this matters: Multi-purpose code is hard to test, modify, and reuse.

File: app/src/client_audio.c:349-365

Description: Lines 350-365 embed timing control (start_time tracking, frame duration calculation, sleep logic) directly in the transmission section. This mixes playback timing with encoding responsibility.

Suggestion: Extract timing control into a separate function or helper. Let the main loop call a timing function to wait until the next frame.

Confidence: 65%

Rule: arch_srp_violation


🟡 MEDIUM - Overly broad exception handling in client audio setup (2 occurrences)

Agent: spring

Category: quality

📍 View all locations
File Description Suggestion Confidence
server/src/main/java/com/genymobile/scrcpy/Server.java:168-181 Lines 168-181 wrap the entire client audio injection setup in a broad try-catch(Exception e), which ... Consider catching more specific exceptions (IOException, etc.) for better error handling and debuggi... 65%
server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java:152 The shutdown() method calls shutdownInput() and shutdownOutput() on sockets without try-catch. These... Either wrap each socket shutdown in individual try-catch to ensure all sockets are shutdown even if ... 75%

Rule: java_add_try_catch_blocks_for_external_servic


🟡 MEDIUM - Excessive nesting depth (4 levels) in main audio loop (6 occurrences)

Agent: refactoring

Category: quality

📍 View all locations
File Description Suggestion Confidence
app/src/client_audio.c:280-370 The main loop contains 4 levels of nested while loops: should_stop -> receive_frame -> fifo_size -> ... Extract inner logic into helper functions like process_decoded_frame() and send_encoded_frames() to ... 70%
server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java:152 The close() method closes video, audio, and control sockets but does NOT close clientAudioSocket. Th... Add closure for clientAudioSocket: if (clientAudioSocket != null) { clientAudioSocket.close(); } 95%
app/src/client_audio.c:324-439 Lines 324-366 (main encoding loop) and lines 397-439 (flush loop) contain similar code for reading F... Extract the packet sending logic into a helper function: static bool send_encoded_packet(sc_socket s... 75%
server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java:152 The shutdown() method shuts down video, audio, and control sockets but NOT clientAudioSocket. Additi... Add shutdown for clientAudioSocket: if (clientAudioSocket != null) { clientAudioSocket.shutdownInput... 95%
server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java:139-150 The getFirstSocket() method returns clientAudioSocket without null check if all other sockets are nu... Either throw IllegalStateException if all sockets are null, or add defensive null check in sendDevic... 70%
server/src/main/java/com/genymobile/scrcpy/device/DesktopConnection.java:33-44 Lines 33 and 44 have commented-out code for micFd. This indicates incomplete implementation that sho... Remove commented-out code entirely, or add TODO comment explaining the planned implementation. 80%

Rule: quality_law_of_demeter


🔵 LOW - Empty catch block for setTargetMixRole - intentional (5 occurrences)

Agent: bugs

Category: bug

Why this matters: Swallowed errors hide bugs and make debugging difficult.

📍 View all locations
File Description Suggestion Confidence
server/src/main/java/com/genymobile/scrcpy/AudioInjector.java:60-61 Catch block catches Exception but does nothing. While intentional for API compatibility, the catch b... The catch is intentional per comment on line 54, but variable could be named 'ignored' to be clearer... 60%
server/src/main/java/com/genymobile/scrcpy/Options.java:421-427 The case "audio_encoder" is missing a break statement, causing fall-through to the next case "power_... Add 'break;' after line 424 to prevent fall-through to the next case. 98%
app/src/client_audio.c:298-299 If avcodec_send_packet() returns < 0, the code just continues without logging. This silently ignores... Log the error before continuing: if (ret < 0) { LOGD("Failed to send packet to decoder"); continue; ... 80%
app/src/client_audio.c:307-308 If out_samples <= 0, the code silently continues. This could mean resampling failed or produced no o... Log the condition: if (out_samples <= 0) { LOGD("Resampling produced no output or failed"); continue... 75%
app/src/client_audio.c:321-322 If avcodec_send_frame() returns < 0, the code silently continues. Encoding failed but the error is n... Log the error: if (ret < 0) { LOGD("Failed to send frame to encoder"); continue; } 80%

Rule: bug_empty_catch


ℹ️ 1 issue(s) outside PR diff (click to expand)

These issues were found in lines not modified in this PR.

🟠 HIGH - Missing break statement in switch case

Agent: bugs

Category: bug

File: server/src/main/java/com/genymobile/scrcpy/Options.java:421-427

Description: The case "audio_encoder" is missing a break statement, causing fall-through to the next case "power_off_on_close". Setting audio_encoder will incorrectly also set powerOffScreenOnClose.

Suggestion: Add 'break;' after line 424 to prevent fall-through to the next case.

Confidence: 98%

Rule: bug_empty_catch



Review ID: 7dceb3ee-2843-4df2-91e1-e1a6f1855ffd
Rate it 👍 or 👎 to improve future reviews | Powered by diffray

@rom1v
Copy link
Collaborator

rom1v commented Dec 28, 2025

Why is this thread spammed with a bot review? Nobody asked for it AFAIK.

@Cabbache
Copy link
Author

Weird, I didn't ask for it either.

@Cabbache Cabbache force-pushed the forward-client-microphone branch from fe310e2 to 28113de Compare January 1, 2026 22:51
@anotheruserofgithub
Copy link

anotheruserofgithub commented Jan 2, 2026

Well, I don't really like these tools, but I must admit it actually found a real bug outside this PR, see #6439 (comment), at the bottom: "1 issue(s) outside PR diff (click to expand)". Commit f9960e9 is the culprit. Any static analyzer should give a warning, though, AI is not needed for that AFAIK.

rom1v added a commit that referenced this pull request Jan 2, 2026
@rom1v
Copy link
Collaborator

rom1v commented Jan 2, 2026

Thank you, fixed: cda4387

@rom1v
Copy link
Collaborator

rom1v commented Jan 2, 2026

Well, I don't really like these tools, but I must admit it actually found a real bug outside this PR

AI tools can be great for reviews, but I don't want them to be intrusive. I don't want an unsolicited bot posting a ton of AI-generated review comments.

What could be useful is a tool or website where I can push a branch or a PR and get an explanation of possible issues (like a powerful static analyzer), without adding noise to the PR thread.

@Cabbache
Copy link
Author

Cabbache commented Jan 2, 2026

@JunaidQrysh I can confirm that scrcpy --audio-source=voice-call-downlink --client-audio-source default works best. The other person being called can hear me and I can hear them. I have not tested with recording. Are you recording with the device running scrcpy or the other one on the call?

In reality, --audio-source=voice-call-uplink --client-audio-source <xxx> do not make sense / are useless to be with each other because even if it worked, they would just make the pc/laptop replay the audio from it's own microphone. I think it's best to simply disallow them from being used with each other.

@pierl2000
Copy link

Hi Cabbache,
I test this PR with a Samsung Galaxy S23, I was able to use the Samsung voice recorder with mp3 played from scrcpy using the following command
scrcpy --audio-source=voice-call-downlink --client-audio-source file:///home/test//loop001.mp3
it worked well. Take note that the voice recorder need to be started first then we can start the scrcpy

I try to use scrcpy into an active phone call using the the main Samsung phone app which is:
adb shell dumpsys window | grep 'mFocusedApp'
mFocusedApp=ActivityRecord{100612507 u0 com.samsung.android.incallui/com.android.incallui.call.InCallActivity t32564}

but doesn't work. ( I tried with android 14 15 16)
into logcat I see this message
adb shell -n logcat | grep -ai 'audio|sound|snd|scrcpy|voice'
1-06 10:09:19.369 1671 22055 E AHAL: AudioStream: configurePalOutputStream: 3860: failed to open stream.
01-06 10:09:19.369 1671 22055 D AHAL: AudioStream: ~AutoPerfLock: 196: (release) perf_lock_handle: 0x0, count: 1
01-06 10:09:19.369 1671 22055 D AHAL: AudioStream: ~AutoPerfLock: 200: Releasing perf_lock_handle: 0x0
01-06 10:09:19.369 1671 22055 E AHAL: AudioStream: onWriteError: 3828: write error -22 usecase(2: low-latency-playback)
01-06 10:09:19.369 1671 22055 D AHAL: AudioStream: Standby: 2447: Enter
01-06 10:09:19.369 1671 22055 D AHAL: AudioStream: Standby: 2585: Exit ret: 0
01-06 10:09:19.390 1671 22055 D AHAL: AudioStream: AutoPerfLock: 186: (Acquired) perf_lock_handle: 0x0, count: 1
01-06 10:09:19.390 1671 22055 I AHAL: AudioStream: Open: 3298: Enter: OutPrimary usecase(2: low-latency-playback)
01-06 10:09:19.390 1671 22055 D AHAL: AudioStream: Open: 3304: no_of_devices 1
01-06 10:09:19.390 1671 22055 I AHAL: AudioStream: Open: 3318: flags_:0x0, mAndroidOutDevices 65536
01-06 10:09:19.390 1671 22055 D AHAL: AudioStream: Open: 3383: halInputFormat 1 halOutputFormat 1 palformat 1
01-06 10:09:19.390 1671 22055 D AHAL: AudioStream: Open: 3462: channels 2 samplerate 48000 format id 1, stream type 8 stream bitwidth 16
01-06 10:09:19.390 1671 22055 D AHAL: AudioStream: Open: 3463: msample_rate 0 mchannels 0 mNoOfOutDevices 1
01-06 10:09:19.391 1671 22055 E AHAL: AudioStream: Open: 3492: Pal Stream Open Error (ffffffea)
01-06 10:09:19.391 1671 22055 D AHAL: AudioStream: Open: 3672: Exit ret: -22

into partial dumpsys audio I see this difference:

active phone call, scrcpy plays mp3 on phone A and we do not hear the mp3 on the B phone side neither A side
adb shell dumpsys audio | grep -A5 route
Audio routes:
mMainType=0x0
mBluetoothName=null

Other state:
mUseVolumeGroupAliases=false

  • route flags=0x2
    rate=48000Hz
    encoding=2
    channels=0xC
    ignore playback capture opt out=false
    allow voice communication capture=false
    --
    Communication route clients:
    [CommunicationRouteClient: mAttributionSource: AttributionSource { uid = 1000, packageName = com.android.server.telecom, attributionTag = null, token = android.os.Binder@1811f2d, deviceId = 0, next = null } mDevice: AudioDeviceAttributes: role:output type:earpiece addr: name:SM-S911W profiles:[{ENCODING_PCM_16BIT, sampling rates=[48000], channel masks=0x04, encapsulation type=0}] descriptors:[] mIsPrivileged: true mPlaybackActive: false mRecordingActive: false mDisabled: false]

    Computed Preferred communication device: AudioDeviceAttributes: role:output type:earpiece addr: name:SM-S911W profiles:[{ENCODING_PCM_16BIT, sampling rates=[48000], channel masks=0x04, encapsulation type=0}] descriptors:[]

    Applied Preferred communication device: AudioDeviceAttributes: role:output type:earpiece addr: name:SM-S911W profiles:[{ENCODING_PCM_16BIT, sampling rates=[48000], channel masks=0x04, encapsulation type=0}] descriptors:[]

Samsung voice recorder, scrcpy play mp3, it works we see the audio db graphic and we can play it.
adb shell dumpsys audio | grep -A5 route
Audio routes:
mMainType=0x0
mBluetoothName=null

Other state:
mUseVolumeGroupAliases=false

  • route flags=0x2
    rate=48000Hz
    encoding=2
    channels=0xC
    ignore playback capture opt out=false
    allow voice communication capture=false
    --
    Communication route clients:

    Computed Preferred communication device: null

    Applied Preferred communication device: null
    Active communication device: AudioDeviceAttributes: role:output type:earpiece addr: name:SM-S911W profiles:[{ENCODING_PCM_16BIT, sampling rates=[48000], channel masks=0x04, encapsulation type=0}] descriptors:[]

I would like to help if possible,
Could you please give me information about what I should investigate?
I start to look into AudioInjector.java
Thank you

@Cabbache Cabbache force-pushed the forward-client-microphone branch from b11bf8b to cdc664b Compare January 6, 2026 19:06
@Cabbache
Copy link
Author

Cabbache commented Jan 6, 2026

Thanks for the report @pierl2000. This looks like a vendor specific issue related to Qualcomm PAL (Pal Stream Open Error (ffffffea)). Also see this.

Can you test these changes? https://github.com/Cabbache/scrcpy/tree/qualcomm-pal-test

@pierl2000
Copy link

Hi Cabbache,
Same issue with your new branch. see below with the new debug details.
Thank you
l@debian:~/dev/scrcpy-qualcomm-pal-test/scrcpy$ scrcpy --audio-source=voice-call-downlink --client-audio-source file:///home/test/loop001.mp3
scrcpy 3.3.4 https://github.com/Genymobile/scrcpy
INFO: ADB device found:
INFO: --> (usb) XYZ device SM_S911W
/usr/local/share/scrcpy/scrcpy-server: 1 file pushed, 0 skipped. 105.1 MB/s (93948 bytes in 0.001s)
[server] INFO: Device: [samsung] samsung SM-S911W (Android 16)
[server] INFO: Call state: 2 (OFFHOOK=2)
[server] INFO: In call: true
[server] WARN: Could not enable call redirection on AudioMix: android.media.audiopolicy.AudioMix$Builder.setCallRedirection [boolean]
INFO: Renderer: opengl
INFO: OpenGL version: 4.6 (Compatibility Profile) Mesa 25.0.7-2
INFO: Trilinear filtering enabled
INFO: Recording audio with Opus encoding...
INFO: File will loop continuously. Press Ctrl+C to stop.
WARN: [FFmpeg] Could not update timestamps for skipped samples.
[server] INFO: AudioTrack state: 1 (INITIALIZED=1)
[server] INFO: AudioTrack playback state: 3 (PLAYING=3)
[server] INFO: successfull: true
INFO: Texture: 1080x2336
[server] WARN: Could not get initial audio timestamp
WARN: [FFmpeg] Could not update timestamps for discarded samples.
WARN: [FFmpeg] Could not update timestamps for skipped samples.
WARN: [FFmpeg] Queue input is backward in time
^CWARN: [FFmpeg] 1 frames left in the queue on closing

adb shell -n logcat | grep -ai 'audio|sound|snd|scrcpy|voice|mix'

01-06 19:52:07.805 1767 2003 I SDM : HWDeviceDRM::UpdateMixerAttributes: Mixer WxH 1080x2340-1 for Peripheral
01-06 19:52:07.824 1671 22055 D AHAL: AudioStream: AutoPerfLock: 186: (Acquired) perf_lock_handle: 0x0, count: 1
01-06 19:52:07.824 1671 22055 I AHAL: AudioStream: Open: 3298: Enter: OutPrimary usecase(2: low-latency-playback)
01-06 19:52:07.824 1671 22055 D AHAL: AudioStream: Open: 3304: no_of_devices 1
01-06 19:52:07.824 1671 22055 I AHAL: AudioStream: Open: 3318: flags_:0x0, mAndroidOutDevices 65536
01-06 19:52:07.824 1671 22055 D AHAL: AudioStream: Open: 3383: halInputFormat 1 halOutputFormat 1 palformat 1
01-06 19:52:07.824 1671 22055 D AHAL: AudioStream: Open: 3462: channels 2 samplerate 48000 format id 1, stream type 8 stream bitwidth 16
01-06 19:52:07.824 1671 22055 D AHAL: AudioStream: Open: 3463: msample_rate 0 mchannels 0 mNoOfOutDevices 1
01-06 19:52:07.825 1671 22055 E AHAL: AudioStream: Open: 3492: Pal Stream Open Error (ffffffea)
01-06 19:52:07.825 1671 22055 D AHAL: AudioStream: Open: 3672: Exit ret: -22
01-06 19:52:07.825 1671 22055 E AHAL: AudioStream: configurePalOutputStream: 3860: failed to open stream.
01-06 19:52:07.825 1671 22055 D AHAL: AudioStream: ~AutoPerfLock: 196: (release) perf_lock_handle: 0x0, count: 1
01-06 19:52:07.825 1671 22055 D AHAL: AudioStream: ~AutoPerfLock: 200: Releasing perf_lock_handle: 0x0
01-06 19:52:07.825 1671 22055 E AHAL: AudioStream: onWriteError: 3828: write error -22 usecase(2: low-latency-playback)
01-06 19:52:07.825 1671 22055 D AHAL: AudioStream: Standby: 2447: Enter
01-06 19:52:07.825 1671 22055 D AHAL: AudioStream: Standby: 2585: Exit ret: 0
01-06 19:52:07.846 1671 22055 D AHAL: AudioStream: AutoPerfLock: 186: (Acquired) perf_lock_handle: 0x0, count: 1
01-06 19:52:07.846 1671 22055 I AHAL: AudioStream: Open: 3298: Enter: OutPrimary usecase(2: low-latency-playback)
01-06 19:52:07.846 1671 22055 D AHAL: AudioStream: Open: 3304: no_of_devices 1
01-06 19:52:07.846 1671 22055 I AHAL: AudioStream: Open: 3318: flags_:0x0, mAndroidOutDevices 65536
01-06 19:52:07.846 1671 22055 D AHAL: AudioStream: Open: 3383: halInputFormat 1 halOutputFormat 1 palformat 1
01-06 19:52:07.846 1671 22055 D AHAL: AudioStream: Open: 3462: channels 2 samplerate 48000 format id 1, stream type 8 stream bitwidth 16
01-06 19:52:07.846 1671 22055 D AHAL: AudioStream: Open: 3463: msample_rate 0 mchannels 0 mNoOfOutDevices 1
01-06 19:52:07.847 1671 22055 E AHAL: AudioStream: Open: 3492: Pal Stream Open Error (ffffffea)
01-06 19:52:07.847 1671 22055 D AHAL: AudioStream: Open: 3672: Exit ret: -22
01-06 19:52:07.847 1671 22055 E AHAL: AudioStream: configurePalOutputStream: 3860: failed to open stream.
01-06 19:52:07.847 1671 22055 D AHAL: AudioStream: ~AutoPerfLock: 196: (release) perf_lock_handle: 0x0, count: 1
01-06 19:52:07.847 1671 22055 D AHAL: AudioStream: ~AutoPerfLock: 200: Releasing perf_lock_handle: 0x0
01-06 19:52:07.847 1671 22055 E AHAL: AudioStream: onWriteError: 3828: write error -22 usecase(2: low-latency-playback)
01-06 19:52:07.847 1671 22055 D AHAL: AudioStream: Standby: 2447: Enter
01-06 19:52:07.847 1671 22055 D AHAL: AudioStream: Standby: 2585: Exit ret: 0

@trombettafrank
Copy link

Hi @Cabbache

I tried this code on "Device: [Xiaomi] Redmi 24116RACCG (Android 14)" (Redmi note 14 pro 4g) and it worked! I needed to re-run it after the call started but that's fine.

Also the sound gets echoed in a disturbing way.

Thanks!

@Cabbache
Copy link
Author

Thanks for the feedback @trombettafrank.

A bit unrelated but I got my hands on Device: [samsung] samsung SM-A720F (Android 14) (exynos 7880) and it does not seem to work there. So we have issues with both snapdragon and exynos samsung devices.

@Cabbache Cabbache force-pushed the forward-client-microphone branch 2 times, most recently from cdc664b to 8b8582c Compare January 16, 2026 08:32
@almog
Copy link

almog commented Jan 22, 2026

Tried on Mac x Android 16 (after commenting out HAVE_V4L2 conditional statements), getting these errors:

➜  scrcpy git:(forward-client-microphone) ✗ ./run x -V debug --audio-source=voice-call-downlink --client-audio-source default
scrcpy 3.3.4 <https://github.com/Genymobile/scrcpy>
INFO: ADB device found:
INFO:     -->   (usb)  R3CY90E031P                     device  SM_S938B
DEBUG: Device serial: R3CY90E031P
DEBUG: Using SCRCPY_SERVER_PATH: x/server/scrcpy-server
x/server/scrcpy-server: 1 file pushed. 9.4 MB/s (95008 bytes in 0.010s)
[server] INFO: Device: [samsung] samsung SM-S938B (Android 16)
DEBUG: Server connected
DEBUG: Starting controller thread
DEBUG: Starting receiver thread
[server] DEBUG: Using audio encoder: 'c2.android.opus.encoder'
[server] DEBUG: Using video encoder: 'c2.qti.avc.encoder'
[server] DEBUG: Display: using DisplayManager API
DEBUG: Using SCRCPY_ICON_PATH: app/data/icon.png
INFO: Renderer: metal
DEBUG: Trilinear filtering disabled (not an OpenGL renderer)
DEBUG: Demuxer 'video': starting thread
DEBUG: Demuxer 'audio': starting thread
DEBUG: Using audio format: avfoundation, device: default
2026-01-22 19:04:01.748 scrcpy[20943:74960092] WARNING: Add NSCameraUseContinuityCameraDeviceType to your Info.plist to use AVCaptureDeviceTypeContinuityCamera.
INFO: Texture: 720x1560
ERROR: [FFmpeg] Video device not found
ERROR: Could not open audio device 'default'
DEBUG: Microphone socket closed

Not sure if this information is of any help but I'd love to provide more feedback/output if you'd like me to test it further.
Thanks!

@Cabbache
Copy link
Author

@almog can you share the output of ./run x --list-client-audio-sources?

@almog
Copy link

almog commented Jan 22, 2026

@almog can you share the output of ./run x --list-client-audio-sources?

Hey @Cabbache, I forgot to mention that I tried that too and it failed:

➜  scrcpy git:(forward-client-microphone) ✗ ./run x --list-client-audio-sources
scrcpy 3.3.4 <https://github.com/Genymobile/scrcpy>
2026-01-22 20:10:02.790 scrcpy[25294:75055574] Audio input format: avfoundation (AVFoundation input device)
2026-01-22 20:10:02.791 scrcpy[25294:75055574] Available audio sources:
2026-01-22 20:10:02.792 scrcpy[25294:75055574] ERROR: Could not list audio devices (error code: -78)
2026-01-22 20:10:02.792 scrcpy[25294:75055574] Note: You can still use device names directly with --client-audio-source
2026-01-22 20:10:02.792 scrcpy[25294:75055574] Common device names:
2026-01-22 20:10:02.792 scrcpy[25294:75055574]   - ":0" (default microphone)
2026-01-22 20:10:02.792 scrcpy[25294:75055574]   - Try running 'ffmpeg -f avfoundation -list_devices true -i ""' to see available devices

Running ffmpeg directly (as suggested in the previous output) produces a this list of devices:

➜  scrcpy git:(forward-client-microphone) ✗ ffmpeg -f avfoundation -list_devices true -i ""
ffmpeg version 8.0.1 Copyright (c) 2000-2025 the FFmpeg developers
  built with Apple clang version 16.0.0 (clang-1600.0.26.6)
  configuration: --prefix=/usr/local/Cellar/ffmpeg/8.0.1_1 --enable-shared --enable-pthreads --enable-version3 --cc=clang --host-cflags= --host-ldflags= --enable-ffplay --enable-gpl --enable-libsvtav1 --enable-libopus --enable-libx264 --enable-libmp3lame --enable-libdav1d --enable-libvpx --enable-libx265 --enable-videotoolbox --enable-audiotoolbox
  libavutil      60.  8.100 / 60.  8.100
  libavcodec     62. 11.100 / 62. 11.100
  libavformat    62.  3.100 / 62.  3.100
  libavdevice    62.  1.100 / 62.  1.100
  libavfilter    11.  4.100 / 11.  4.100
  libswscale      9.  1.100 /  9.  1.100
  libswresample   6.  1.100 /  6.  1.100
2026-01-22 20:12:25.270 ffmpeg[25722:75061074] WARNING: Add NSCameraUseContinuityCameraDeviceType to your Info.plist to use AVCaptureDeviceTypeContinuityCamera.
[AVFoundation indev @ 0x7fb3ac904b40] AVFoundation video devices:
[AVFoundation indev @ 0x7fb3ac904b40] [0] Capture screen 0
[AVFoundation indev @ 0x7fb3ac904b40] AVFoundation audio devices:
[AVFoundation indev @ 0x7fb3ac904b40] [0] ONE
[AVFoundation indev @ 0x7fb3ac904b40] [1] ZoomAudioDevice
[AVFoundation indev @ 0x7fb3ac904b40] [2] BlackHole 2ch
[AVFoundation indev @ 0x7fb3ac904b40] [3] BlackHole 16ch
[AVFoundation indev @ 0x7fb3ac904b40] [4] Aggregate Device
[AVFoundation indev @ 0x7fb3ac904b40] [5] NDI Audio
[AVFoundation indev @ 0x7fb3ac904b40] [6] Scarlett Solo USB
[AVFoundation indev @ 0x7fb3ac904b40] [7] Aggregate: output + input (scarlett)
[in#0 @ 0x7fb3aca04600] Error opening input: Input/output error
Error opening input file .
Error opening input files: Input/output error

@Cabbache
Copy link
Author

@almog did you try :0 or ONE instead of default?

I havent tested with avfoundation.

@almog
Copy link

almog commented Jan 22, 2026

@almog did you try :0 or ONE instead of default?

@Cabbache Yes, but now I noticed that you prefixed the zero with colon (:0) is that intentional?
At any rate, I get different results for each of these possible options:

  1. '0' (the device that's listed as One, which is in fact an input device):
➜  scrcpy git:(forward-client-microphone) ✗ ./run x  --audio-source=voice-call-downlink --client-audio-source 0
scrcpy 3.3.4 <https://github.com/Genymobile/scrcpy>
INFO: ADB device found:
INFO:     -->   (usb)  R3CY90E031P                     device  SM_S938B
x/server/scrcpy-server: 1 file pushed. 16.2 MB/s (95008 bytes in 0.006s)
[server] INFO: Device: [samsung] samsung SM-S938B (Android 16)
INFO: Renderer: metal
2026-01-22 21:05:44.270 scrcpy[28795:75114276] WARNING: Add NSCameraUseContinuityCameraDeviceType to your Info.plist to use AVCaptureDeviceTypeContinuityCamera.
objc[28795]: class `NSKVONotifying_AVCaptureScreenInput' not linked into application
objc[28795]: class `NSKVONotifying_AVCaptureScreenInput' not linked into application
WARN: [FFmpeg] Configuration of video device failed, falling back to default.
ERROR: [FFmpeg] Selected pixel format (yuv420p) is not supported by the input device.
ERROR: [FFmpeg] Supported pixel formats:
ERROR: [FFmpeg]   uyvy422
ERROR: [FFmpeg]   yuyv422
ERROR: [FFmpeg]   nv12
ERROR: [FFmpeg]   0rgb
ERROR: [FFmpeg]   bgr0
WARN: [FFmpeg] Overriding selected pixel format to use uyvy422 instead.
WARN: [FFmpeg] Stream #0: not enough frames to estimate rate; consider increasing probesize
ERROR: [FFmpeg] Requested input sample format -1 is invalid
ERROR: Could not initialize resampler
INFO: Texture: 720x1560
  1. Using :0 (or colon any other number really):
➜  scrcpy git:(forward-client-microphone) ✗ ./run x  --audio-source=voice-call-downlink --client-audio-source :0
scrcpy 3.3.4 <https://github.com/Genymobile/scrcpy>
INFO: ADB device found:
INFO:     -->   (usb)  R3CY90E031P                     device  SM_S938B
x/server/scrcpy-server: 1 file pushed. 14.3 MB/s (95008 bytes in 0.006s)
[server] INFO: Device: [samsung] samsung SM-S938B (Android 16)
INFO: Renderer: metal
2026-01-22 21:06:25.062 scrcpy[28817:75114954] WARNING: Add NSCameraUseContinuityCameraDeviceType to your Info.plist to use AVCaptureDeviceTypeContinuityCamera.
INFO: Texture: 720x1560
INFO: Recording audio with Opus encoding...

No error here but the device doesn't seem to register any audio input either.

  1. No colon + any other device digit, let's say 6, which is another input device:
➜  scrcpy git:(forward-client-microphone) ✗ ./run x  --audio-source=voice-call-downlink --client-audio-source 6
scrcpy 3.3.4 <https://github.com/Genymobile/scrcpy>
INFO: ADB device found:
INFO:     -->   (usb)  R3CY90E031P                     device  SM_S938B
x/server/scrcpy-server: 1 file pushed. 13.7 MB/s (95008 bytes in 0.007s)
[server] INFO: Device: [samsung] samsung SM-S938B (Android 16)
INFO: Renderer: metal
2026-01-22 21:07:44.709 scrcpy[28853:75115907] WARNING: Add NSCameraUseContinuityCameraDeviceType to your Info.plist to use AVCaptureDeviceTypeContinuityCamera.
ERROR: [FFmpeg] Invalid device index
ERROR: Could not open audio device '6'
[server] WARN: Could not get initial audio timestamp
INFO: Texture: 720x1560

Notice that in case 3 the error is different error than for the zero device (no colon).

Not sure if any of the information I provide here is helpful in any way.

@Cabbache Cabbache force-pushed the forward-client-microphone branch from 8b8582c to 50734c9 Compare January 30, 2026 20:38
@Cabbache
Copy link
Author

Cabbache commented Jan 30, 2026

I was able to get a Mac to test with and replicated some of the issues @almog reported. I got it to work with a Google Pixel device.

@almog
Copy link

almog commented Jan 31, 2026

Thank you @Cabbache! I can confirm it works now on my Mac, only issues I see right now (that were mentioned by others) are:

  1. Recording on the android device cannot be started after scrcpy is already running.
  2. Input device must be named (e.g. --client-audio-source :6, default does not work yet).

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.

10 participants