Add --no-video

Similar to --no-audio, add --no-video to play audio only.

Fixes #3842 <https://github.com/Genymobile/scrcpy/issues/3842>
PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
This commit is contained in:
Romain Vimont 2023-05-07 12:08:50 +02:00
parent e89e772c7c
commit 8c650e53cd
17 changed files with 243 additions and 96 deletions

View file

@ -37,6 +37,7 @@ _scrcpy() {
--no-key-repeat
--no-mipmaps
--no-power-on
--no-video
--otg
-p --port=
--power-off-on-close

View file

@ -43,6 +43,7 @@ arguments=(
'--no-key-repeat[Do not forward repeated key events when a key is held down]'
'--no-mipmaps[Disable the generation of mipmaps]'
'--no-power-on[Do not power on the device on start]'
'--no-video[Disable video forwarding]'
'--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]'
{-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]'
'--power-off-on-close[Turn the device screen off when closing scrcpy]'

View file

@ -225,6 +225,10 @@ If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically
.B \-\-no\-power\-on
Do not power on the device on start.
.TP
.B \-\-no\-video
Disable video forwarding.
.TP
.B \-\-otg
Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable.

View file

@ -73,6 +73,7 @@ enum {
OPT_AUDIO_BUFFER,
OPT_AUDIO_OUTPUT_BUFFER,
OPT_NO_DISPLAY,
OPT_NO_VIDEO,
};
struct sc_option {
@ -407,6 +408,11 @@ static const struct sc_option options[] = {
.longopt = "no-power-on",
.text = "Do not power on the device on start.",
},
{
.longopt_id = OPT_NO_VIDEO,
.longopt = "no-video",
.text = "Disable video forwarding.",
},
{
.longopt_id = OPT_OTG,
.longopt = "otg",
@ -1797,6 +1803,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NO_DOWNSIZE_ON_ERROR:
opts->downsize_on_error = false;
break;
case OPT_NO_VIDEO:
opts->video = false;
break;
case OPT_NO_AUDIO:
opts->audio = false;
break;
@ -2042,14 +2051,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
#endif
#ifdef HAVE_USB
if (!opts->mirror && opts->control && !opts->otg) {
if (!(opts->mirror && opts->video) && !opts->otg) {
#else
if (!opts->mirror && opts->control) {
if (!(opts->mirror && opts->video)) {
#endif
LOGD("Mirroring is disabled, force --no-control");
// If video mirroring is disabled and OTG are disabled, then there is
// no way to control the device.
opts->control = false;
}
if (!opts->video) {
// If video is disabled, then scrcpy must exit on audio failure.
opts->require_audio = true;
}
return true;
}

View file

@ -73,6 +73,7 @@ const struct scrcpy_options scrcpy_options_default = {
.cleanup = true,
.start_fps_counter = false,
.power_on = true,
.video = true,
.audio = true,
.require_audio = false,
.list_encoders = false,

View file

@ -156,6 +156,7 @@ struct scrcpy_options {
bool cleanup;
bool start_fps_counter;
bool power_on;
bool video;
bool audio;
bool require_audio;
bool list_encoders;

View file

@ -152,7 +152,7 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) {
static inline bool
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
// The video queue is empty
return true;
}
@ -176,7 +176,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
}
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
assert(recorder->stopped);
// If the recorder is stopped, don't process anything if there are not
// at least video packets
@ -184,7 +184,11 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
return false;
}
AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue);
AVPacket *video_pkt = NULL;
if (!sc_vecdeque_is_empty(&recorder->video_queue)) {
assert(recorder->video);
video_pkt = sc_vecdeque_pop(&recorder->video_queue);
}
AVPacket *audio_pkt = NULL;
if (!sc_vecdeque_is_empty(&recorder->audio_queue)) {
@ -196,6 +200,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
int ret = false;
if (video_pkt) {
if (video_pkt->pts != AV_NOPTS_VALUE) {
LOGE("The first video packet is not a config packet");
goto end;
@ -208,6 +213,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
if (!ok) {
goto end;
}
}
if (audio_pkt) {
if (audio_pkt->pts != AV_NOPTS_VALUE) {
@ -218,13 +224,13 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
assert(recorder->audio_stream_index >= 0);
AVStream *audio_stream =
recorder->ctx->streams[recorder->audio_stream_index];
ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
if (!ok) {
goto end;
}
}
ok = avformat_write_header(recorder->ctx, NULL) >= 0;
bool ok = avformat_write_header(recorder->ctx, NULL) >= 0;
if (!ok) {
LOGE("Failed to write header to %s", recorder->filename);
goto end;
@ -233,7 +239,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
ret = true;
end:
if (video_pkt) {
av_packet_free(&video_pkt);
}
if (audio_pkt) {
av_packet_free(&audio_pkt);
}
@ -263,7 +271,8 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped) {
if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) {
if (recorder->video && !video_pkt &&
!sc_vecdeque_is_empty(&recorder->video_queue)) {
// A new packet may be assigned to video_pkt and be processed
break;
}
@ -278,6 +287,11 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
// If stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping.
// If there is no video, then the video_queue will remain empty forever
// and video_pkt will always be NULL.
assert(recorder->video || (!video_pkt
&& sc_vecdeque_is_empty(&recorder->video_queue)));
// If there is no audio, then the audio_queue will remain empty forever
// and audio_pkt will always be NULL.
assert(recorder->audio || (!audio_pkt
@ -319,6 +333,9 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
if (!recorder->audio) {
assert(video_pkt);
pts_origin = video_pkt->pts;
} else if (!recorder->video) {
assert(audio_pkt);
pts_origin = audio_pkt->pts;
} else if (video_pkt && audio_pkt) {
pts_origin = MIN(video_pkt->pts, audio_pkt->pts);
} else if (recorder->stopped) {
@ -639,7 +656,7 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool audio,
enum sc_record_format format, bool video, bool audio,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
recorder->filename = strdup(filename);
if (!recorder->filename) {
@ -662,6 +679,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
goto error_queue_cond_destroy;
}
assert(video || audio);
recorder->video = video;
recorder->audio = audio;
sc_vecdeque_init(&recorder->video_queue);
@ -680,6 +699,7 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
recorder->cbs = cbs;
recorder->cbs_userdata = cbs_userdata;
if (video) {
static const struct sc_packet_sink_ops video_ops = {
.open = sc_recorder_video_packet_sink_open,
.close = sc_recorder_video_packet_sink_close,
@ -687,6 +707,7 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
};
recorder->video_packet_sink.ops = &video_ops;
}
if (audio) {
static const struct sc_packet_sink_ops audio_ops = {

View file

@ -27,6 +27,7 @@ struct sc_recorder {
* may access it without data races.
*/
bool audio;
bool video;
char *filename;
enum sc_record_format format;
@ -59,7 +60,7 @@ struct sc_recorder_callbacks {
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool audio,
enum sc_record_format format, bool video, bool audio,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
bool

View file

@ -345,6 +345,7 @@ scrcpy(struct scrcpy_options *options) {
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,
.video = options->video,
.audio = options->audio,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
@ -389,7 +390,7 @@ scrcpy(struct scrcpy_options *options) {
sdl_set_hints(options->render_driver);
// Initialize SDL video and audio in addition if mirroring is enabled
if (SDL_Init(SDL_INIT_VIDEO)) {
if (options->video && SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
}
@ -436,11 +437,13 @@ scrcpy(struct scrcpy_options *options) {
file_pusher_initialized = true;
}
if (options->video) {
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
.on_ended = sc_video_demuxer_on_ended,
};
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
&video_demuxer_cbs, NULL);
}
if (options->audio) {
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
@ -450,7 +453,7 @@ scrcpy(struct scrcpy_options *options) {
&audio_demuxer_cbs, options);
}
bool needs_video_decoder = options->mirror;
bool needs_video_decoder = options->mirror && options->video;
bool needs_audio_decoder = options->mirror && options->audio;
#ifdef HAVE_V4L2
needs_video_decoder |= !!options->v4l2_device;
@ -471,8 +474,8 @@ scrcpy(struct scrcpy_options *options) {
.on_ended = sc_recorder_on_ended,
};
if (!sc_recorder_init(&s->recorder, options->record_filename,
options->record_format, options->audio,
&recorder_cbs, NULL)) {
options->record_format, options->video,
options->audio, &recorder_cbs, NULL)) {
goto end;
}
recorder_initialized = true;
@ -482,8 +485,10 @@ scrcpy(struct scrcpy_options *options) {
}
recorder_started = true;
if (options->video) {
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
&s->recorder.video_packet_sink);
}
if (options->audio) {
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
&s->recorder.audio_packet_sink);
@ -671,11 +676,6 @@ aoa_hid_end:
.start_fps_counter = options->start_fps_counter,
};
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer, options->display_buffer,
@ -684,7 +684,14 @@ aoa_hid_end:
src = &s->display_buffer.frame_source;
}
if (options->video) {
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
if (options->audio) {
sc_audio_player_init(&s->audio_player, options->audio_buffer,
@ -713,12 +720,15 @@ aoa_hid_end:
}
#endif
// now we consumed the header values, the socket receives the video stream
// start the video demuxer
// Now that the header values have been consumed, the socket(s) will
// receive the stream(s). Start the demuxer(s).
if (options->video) {
if (!sc_demuxer_start(&s->video_demuxer)) {
goto end;
}
video_demuxer_started = true;
}
if (options->audio) {
if (!sc_demuxer_start(&s->audio_demuxer)) {

View file

@ -226,6 +226,9 @@ execute_server(struct sc_server *server,
ADD_PARAM("scid=%08x", params->scid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
if (!params->video) {
ADD_PARAM("video=false");
}
if (params->video_bit_rate) {
ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate);
}
@ -464,6 +467,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
const char *serial = server->serial;
assert(serial);
bool video = server->params.video;
bool audio = server->params.audio;
bool control = server->params.control;
@ -471,10 +475,13 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
sc_socket audio_socket = SC_SOCKET_NONE;
sc_socket control_socket = SC_SOCKET_NONE;
if (!tunnel->forward) {
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
if (video) {
video_socket =
net_accept_intr(&server->intr, tunnel->server_socket);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
}
if (audio) {
audio_socket =
@ -504,13 +511,20 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
unsigned attempts = 100;
sc_tick delay = SC_TICK_FROM_MS(100);
video_socket = connect_to_server(server, attempts, delay, tunnel_host,
tunnel_port);
if (video_socket == SC_SOCKET_NONE) {
sc_socket first_socket = connect_to_server(server, attempts, delay,
tunnel_host, tunnel_port);
if (first_socket == SC_SOCKET_NONE) {
goto fail;
}
if (video) {
video_socket = first_socket;
}
if (audio) {
if (!video) {
audio_socket = first_socket;
} else {
audio_socket = net_socket();
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
@ -521,10 +535,12 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail;
}
}
}
if (control) {
// we know that the device is listening, we don't need several
// attempts
if (!video && !audio) {
control_socket = first_socket;
} else {
control_socket = net_socket();
if (control_socket == SC_SOCKET_NONE) {
goto fail;
@ -536,18 +552,23 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
}
}
}
}
// we don't need the adb tunnel anymore
sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name);
sc_socket first_socket = video ? video_socket
: audio ? audio_socket
: control_socket;
// The sockets will be closed on stop if device_read_info() fails
bool ok = device_read_info(&server->intr, video_socket, info);
bool ok = device_read_info(&server->intr, first_socket, info);
if (!ok) {
goto fail;
}
assert(video_socket != SC_SOCKET_NONE);
assert(!video || video_socket != SC_SOCKET_NONE);
assert(!audio || audio_socket != SC_SOCKET_NONE);
assert(!control || control_socket != SC_SOCKET_NONE);
@ -931,8 +952,11 @@ run_server(void *data) {
sc_mutex_unlock(&server->mutex);
// Interrupt sockets to wake up socket blocking calls on the server
assert(server->video_socket != SC_SOCKET_NONE);
if (server->video_socket != SC_SOCKET_NONE) {
// There is no video_socket if --no-video is set
net_interrupt(server->video_socket);
}
if (server->audio_socket != SC_SOCKET_NONE) {
// There is no audio_socket if --no-audio is set

View file

@ -41,6 +41,7 @@ struct sc_server_params {
int8_t lock_video_orientation;
bool control;
uint32_t display_id;
bool video;
bool audio;
bool show_touches;
bool stay_awake;

View file

@ -24,6 +24,21 @@ To disable audio:
scrcpy --no-audio
```
## Audio only
To play audio only, disable the video:
```
scrcpy --no-video
```
Without video, the audio latency is typically not criticial, so it might be
interesting to add [buffering](#buffering) to minimize glitches:
```
scrcpy --no-video --audio-buffer=200
```
## Codec
The audio codec can be selected. The possible values are `opus` (default), `aac`

View file

@ -13,7 +13,11 @@ To record only the video:
scrcpy --no-audio --record=file.mp4
```
_It is currently not possible to record only the audio._
To record only the audio:
```bash
scrcpy --no-video --record=file.mp4
```
To disable mirroring while recording:

View file

@ -170,6 +170,16 @@ scrcpy --v4l2-sink=/dev/video2 --no-mirror
scrcpy --record=file.mkv --no-mirror
```
## No video
To disable video forwarding completely, so that only audio is forwarded:
```
scrcpy --no-video
```
## Video4Linux
See the dedicated [Video4Linux](v4l2.md) page.

View file

@ -41,7 +41,7 @@ public final class DesktopConnection implements Closeable {
controlInputStream = null;
controlOutputStream = null;
}
videoFd = videoSocket.getFileDescriptor();
videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null;
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
}
@ -60,29 +60,43 @@ public final class DesktopConnection implements Closeable {
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
}
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
public static DesktopConnection open(int scid, boolean tunnelForward, boolean video, boolean audio, boolean control, boolean sendDummyByte)
throws IOException {
String socketName = getSocketName(scid);
LocalSocket firstSocket = null;
LocalSocket videoSocket = null;
LocalSocket audioSocket = null;
LocalSocket controlSocket = null;
try {
if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
if (video) {
videoSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
firstSocket = videoSocket;
}
if (audio) {
audioSocket = localServerSocket.accept();
if (firstSocket == null) {
firstSocket = audioSocket;
}
}
if (control) {
controlSocket = localServerSocket.accept();
if (firstSocket == null) {
firstSocket = controlSocket;
}
}
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
firstSocket.getOutputStream().write(0);
}
}
} else {
if (video) {
videoSocket = connect(socketName);
}
if (audio) {
audioSocket = connect(socketName);
}
@ -106,10 +120,22 @@ public final class DesktopConnection implements Closeable {
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
}
private LocalSocket getFirstSocket() {
if (videoSocket != null) {
return videoSocket;
}
if (audioSocket != null) {
return audioSocket;
}
return controlSocket;
}
public void close() throws IOException {
if (videoSocket != null) {
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
videoSocket.close();
}
if (audioSocket != null) {
audioSocket.shutdownInput();
audioSocket.shutdownOutput();
@ -130,7 +156,8 @@ public final class DesktopConnection implements Closeable {
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
IO.writeFully(videoFd, buffer, 0, buffer.length);
FileDescriptor fd = getFirstSocket().getFileDescriptor();
IO.writeFully(fd, buffer, 0, buffer.length);
}
public FileDescriptor getVideoFd() {

View file

@ -9,6 +9,7 @@ public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG;
private int scid = -1; // 31-bit non-negative value, or -1
private boolean video = true;
private boolean audio = true;
private int maxSize;
private VideoCodec videoCodec = VideoCodec.H264;
@ -51,6 +52,10 @@ public class Options {
return scid;
}
public boolean getVideo() {
return video;
}
public boolean getAudio() {
return audio;
}
@ -200,6 +205,9 @@ public class Options {
case "log_level":
options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
break;
case "video":
options.video = Boolean.parseBoolean(value);
break;
case "audio":
options.audio = Boolean.parseBoolean(value);
break;

View file

@ -95,6 +95,7 @@ public final class Server {
int scid = options.getScid();
boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl();
boolean video = options.getVideo();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
@ -121,7 +122,7 @@ public final class Server {
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte);
DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte);
try {
if (options.getSendDeviceMeta()) {
connection.sendDeviceMeta(Device.getDeviceName());
@ -147,11 +148,13 @@ public final class Server {
asyncProcessors.add(audioRecorder);
}
if (video) {
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
options.getSendFrameMeta());
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
asyncProcessors.add(screenEncoder);
}
Completion completion = new Completion(asyncProcessors.size());
for (AsyncProcessor asyncProcessor : asyncProcessors) {