You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
scrcpy/app/src/cli.c

2865 lines
87 KiB
C

#include "cli.h"
#include <assert.h>
#include <getopt.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include "options.h"
#include "util/log.h"
#include "util/net.h"
#include "util/str.h"
#include "util/strbuf.h"
#include "util/term.h"
#define STR_IMPL_(x) #x
#define STR(x) STR_IMPL_(x)
enum {
OPT_BIT_RATE = 1000,
OPT_WINDOW_TITLE,
OPT_PUSH_TARGET,
OPT_ALWAYS_ON_TOP,
OPT_CROP,
OPT_RECORD_FORMAT,
OPT_PREFER_TEXT,
OPT_WINDOW_X,
OPT_WINDOW_Y,
OPT_WINDOW_WIDTH,
OPT_WINDOW_HEIGHT,
OPT_WINDOW_BORDERLESS,
OPT_MAX_FPS,
OPT_LOCK_VIDEO_ORIENTATION,
OPT_DISPLAY,
OPT_DISPLAY_ID,
OPT_ROTATION,
OPT_RENDER_DRIVER,
OPT_NO_MIPMAPS,
OPT_CODEC_OPTIONS,
OPT_VIDEO_CODEC_OPTIONS,
OPT_FORCE_ADB_FORWARD,
OPT_DISABLE_SCREENSAVER,
OPT_SHORTCUT_MOD,
OPT_NO_KEY_REPEAT,
OPT_FORWARD_ALL_CLICKS,
OPT_LEGACY_PASTE,
OPT_ENCODER,
OPT_VIDEO_ENCODER,
OPT_POWER_OFF_ON_CLOSE,
OPT_V4L2_SINK,
OPT_DISPLAY_BUFFER,
OPT_V4L2_BUFFER,
OPT_TUNNEL_HOST,
OPT_TUNNEL_PORT,
OPT_NO_CLIPBOARD_AUTOSYNC,
OPT_TCPIP,
OPT_RAW_KEY_EVENTS,
OPT_NO_DOWNSIZE_ON_ERROR,
OPT_OTG,
OPT_NO_CLEANUP,
OPT_PRINT_FPS,
OPT_NO_POWER_ON,
OPT_CODEC,
OPT_VIDEO_CODEC,
OPT_NO_AUDIO,
OPT_AUDIO_BIT_RATE,
OPT_AUDIO_CODEC,
OPT_AUDIO_CODEC_OPTIONS,
OPT_AUDIO_ENCODER,
OPT_LIST_ENCODERS,
OPT_LIST_DISPLAYS,
OPT_REQUIRE_AUDIO,
OPT_AUDIO_BUFFER,
OPT_AUDIO_OUTPUT_BUFFER,
OPT_NO_DISPLAY,
OPT_NO_VIDEO,
OPT_NO_AUDIO_PLAYBACK,
OPT_NO_VIDEO_PLAYBACK,
OPT_VIDEO_SOURCE,
OPT_AUDIO_SOURCE,
OPT_KILL_ADB_ON_CLOSE,
OPT_TIME_LIMIT,
OPT_PAUSE_ON_EXIT,
OPT_LIST_CAMERAS,
OPT_LIST_CAMERA_SIZES,
OPT_CAMERA_ID,
OPT_CAMERA_SIZE,
OPT_CAMERA_FACING,
OPT_CAMERA_AR,
OPT_CAMERA_FPS,
OPT_CAMERA_HIGH_SPEED,
OPT_DISPLAY_ORIENTATION,
OPT_RECORD_ORIENTATION,
OPT_ORIENTATION,
OPT_KEYBOARD,
OPT_MOUSE,
};
struct sc_option {
char shortopt;
int longopt_id; // either shortopt or longopt_id is non-zero
const char *longopt;
// no argument: argdesc == NULL && !optional_arg
// optional argument: argdesc != NULL && optional_arg
// required argument: argdesc != NULL && !optional_arg
const char *argdesc;
bool optional_arg;
const char *text; // if NULL, the option does not appear in the help
};
#define MAX_EQUIVALENT_SHORTCUTS 3
struct sc_shortcut {
const char *shortcuts[MAX_EQUIVALENT_SHORTCUTS + 1];
const char *text;
};
struct sc_envvar {
const char *name;
const char *text;
};
struct sc_exit_status {
unsigned value;
const char *text;
};
struct sc_getopt_adapter {
char *optstring;
struct option *longopts;
};
static const struct sc_option options[] = {
{
.longopt_id = OPT_ALWAYS_ON_TOP,
.longopt = "always-on-top",
.text = "Make scrcpy window always on top (above other windows).",
},
{
.longopt_id = OPT_AUDIO_BIT_RATE,
.longopt = "audio-bit-rate",
.argdesc = "value",
.text = "Encode the audio at the given bit rate, expressed in bits/s. "
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
"Default is 128K (128000).",
},
{
.longopt_id = OPT_AUDIO_BUFFER,
.longopt = "audio-buffer",
.argdesc = "ms",
.text = "Configure the audio buffering delay (in milliseconds).\n"
"Lower values decrease the latency, but increase the "
"likelyhood of buffer underrun (causing audio glitches).\n"
"Default is 50.",
},
{
.longopt_id = OPT_AUDIO_CODEC,
.longopt = "audio-codec",
.argdesc = "name",
.text = "Select an audio codec (opus, aac, flac or raw).\n"
"Default is opus.",
},
{
.longopt_id = OPT_AUDIO_CODEC_OPTIONS,
.longopt = "audio-codec-options",
.argdesc = "key[:type]=value[,...]",
.text = "Set a list of comma-separated key:type=value options for the "
"device audio encoder.\n"
"The possible values for 'type' are 'int' (default), 'long', "
"'float' and 'string'.\n"
"The list of possible codec options is available in the "
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_AUDIO_ENCODER,
.longopt = "audio-encoder",
.argdesc = "name",
.text = "Use a specific MediaCodec audio encoder (depending on the "
"codec provided by --audio-codec).\n"
"The available encoders can be listed by --list-encoders.",
},
{
.longopt_id = OPT_AUDIO_SOURCE,
.longopt = "audio-source",
.argdesc = "source",
.text = "Select the audio source (output or mic).\n"
"Default is output.",
},
{
.longopt_id = OPT_AUDIO_OUTPUT_BUFFER,
.longopt = "audio-output-buffer",
.argdesc = "ms",
.text = "Configure the size of the SDL audio output buffer (in "
"milliseconds).\n"
"If you get \"robotic\" audio playback, you should test with "
"a higher value (10). Do not change this setting otherwise.\n"
"Default is 5.",
},
{
.shortopt = 'b',
.longopt = "video-bit-rate",
.argdesc = "value",
.text = "Encode the video at the given bit rate, expressed in bits/s. "
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
"Default is 8M (8000000).",
},
{
// deprecated
.longopt_id = OPT_BIT_RATE,
.longopt = "bit-rate",
.argdesc = "value",
},
{
.longopt_id = OPT_CAMERA_AR,
.longopt = "camera-ar",
.argdesc = "ar",
.text = "Select the camera size by its aspect ratio (+/- 10%).\n"
"Possible values are \"sensor\" (use the camera sensor aspect "
"ratio), \"<num>:<den>\" (e.g. \"4:3\") or \"<value>\" (e.g. "
"\"1.6\")."
},
{
.longopt_id = OPT_CAMERA_ID,
.longopt = "camera-id",
.argdesc = "id",
.text = "Specify the device camera id to mirror.\n"
"The available camera ids can be listed by:\n"
" scrcpy --list-cameras",
},
{
.longopt_id = OPT_CAMERA_FACING,
.longopt = "camera-facing",
.argdesc = "facing",
.text = "Select the device camera by its facing direction.\n"
"Possible values are \"front\", \"back\" and \"external\".",
},
{
.longopt_id = OPT_CAMERA_HIGH_SPEED,
.longopt = "camera-high-speed",
.text = "Enable high-speed camera capture mode.\n"
"This mode is restricted to specific resolutions and frame "
"rates, listed by --list-camera-sizes.",
},
{
.longopt_id = OPT_CAMERA_SIZE,
.longopt = "camera-size",
.argdesc = "<width>x<height>",
.text = "Specify an explicit camera capture size.",
},
{
.longopt_id = OPT_CAMERA_FPS,
.longopt = "camera-fps",
.argdesc = "value",
.text = "Specify the camera capture frame rate.\n"
"If not specified, Android's default frame rate (30 fps) is "
"used.",
},
{
// Not really deprecated (--codec has never been released), but without
// declaring an explicit --codec option, getopt_long() partial matching
// behavior would consider --codec to be equivalent to --codec-options,
// which would be confusing.
.longopt_id = OPT_CODEC,
.longopt = "codec",
.argdesc = "value",
},
{
// deprecated
.longopt_id = OPT_CODEC_OPTIONS,
.longopt = "codec-options",
.argdesc = "key[:type]=value[,...]",
},
{
.longopt_id = OPT_CROP,
.longopt = "crop",
.argdesc = "width:height:x:y",
.text = "Crop the device screen on the server.\n"
"The values are expressed in the device natural orientation "
"(typically, portrait for a phone, landscape for a tablet). "
"Any --max-size value is computed on the cropped size.",
},
{
.shortopt = 'd',
.longopt = "select-usb",
.text = "Use USB device (if there is exactly one, like adb -d).\n"
"Also see -e (--select-tcpip).",
},
{
.longopt_id = OPT_DISABLE_SCREENSAVER,
.longopt = "disable-screensaver",
.text = "Disable screensaver while scrcpy is running.",
},
{
// deprecated
.longopt_id = OPT_DISPLAY,
.longopt = "display",
.argdesc = "id",
},
{
.longopt_id = OPT_DISPLAY_BUFFER,
.longopt = "display-buffer",
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before displaying. "
"This increases latency to compensate for jitter.\n"
"Default is 0 (no buffering).",
},
{
.longopt_id = OPT_DISPLAY_ID,
.longopt = "display-id",
.argdesc = "id",
.text = "Specify the device display id to mirror.\n"
"The available display ids can be listed by:\n"
" scrcpy --list-displays\n"
"Default is 0.",
},
{
.longopt_id = OPT_DISPLAY_ORIENTATION,
.longopt = "display-orientation",
.argdesc = "value",
.text = "Set the initial display orientation.\n"
"Possible values are 0, 90, 180, 270, flip0, flip90, flip180 "
"and flip270. The number represents the clockwise rotation "
"in degrees; the \"flip\" keyword applies a horizontal flip "
"before the rotation.\n"
"Default is 0.",
},
{
.shortopt = 'e',
.longopt = "select-tcpip",
.text = "Use TCP/IP device (if there is exactly one, like adb -e).\n"
"Also see -d (--select-usb).",
},
{
// deprecated
.longopt_id = OPT_ENCODER,
.longopt = "encoder",
.argdesc = "name",
},
{
.shortopt = 'f',
.longopt = "fullscreen",
.text = "Start in fullscreen.",
},
{
.longopt_id = OPT_FORCE_ADB_FORWARD,
.longopt = "force-adb-forward",
.text = "Do not attempt to use \"adb reverse\" to connect to the "
"device.",
},
{
.longopt_id = OPT_FORWARD_ALL_CLICKS,
.longopt = "forward-all-clicks",
.text = "By default, right-click triggers BACK (or POWER on) and "
"middle-click triggers HOME. This option disables these "
"shortcuts and forwards the clicks to the device instead.",
},
{
.shortopt = 'h',
.longopt = "help",
.text = "Print this help.",
},
{
.longopt_id = OPT_KEYBOARD,
.longopt = "keyboard",
.argdesc = "mode",
.text = "Select how to send keyboard inputs to the device.\n"
"Possible values are \"disabled\", \"sdk\", \"uhid\" and "
"\"aoa\".\n"
"\"disabled\" does not send keyboard inputs to the device.\n"
"\"sdk\" uses the Android system API to deliver keyboard "
"events to applications.\n"
"\"uhid\" simulates a physical HID keyboard using the Linux "
"UHID kernel module on the device.\n"
"\"aoa\" simulates a physical keyboard using the AOAv2 "
"protocol. It may only work over USB.\n"
"For \"uhid\" and \"aoa\", the keyboard layout must be "
"configured (once and for all) on the device, via Settings -> "
"System -> Languages and input -> Physical keyboard. This "
"settings page can be started directly using the shortcut "
"MOD+k (except in OTG mode) or by executing: `adb shell am "
"start -a android.settings.HARD_KEYBOARD_SETTINGS`.\n"
"This option is only available when a HID keyboard is enabled "
"(or a physical keyboard is connected).\n"
"Also see --mouse.",
},
{
.longopt_id = OPT_KILL_ADB_ON_CLOSE,
.longopt = "kill-adb-on-close",
.text = "Kill adb when scrcpy terminates.",
},
{
// deprecated
.shortopt = 'K',
.longopt = "hid-keyboard",
},
{
.longopt_id = OPT_LEGACY_PASTE,
.longopt = "legacy-paste",
.text = "Inject computer clipboard text as a sequence of key events "
"on Ctrl+v (like MOD+Shift+v).\n"
"This is a workaround for some devices not behaving as "
"expected when setting the device clipboard programmatically.",
},
{
.longopt_id = OPT_LIST_CAMERAS,
.longopt = "list-cameras",
.text = "List device cameras.",
},
{
.longopt_id = OPT_LIST_CAMERA_SIZES,
.longopt = "list-camera-sizes",
.text = "List the valid camera capture sizes.",
},
{
.longopt_id = OPT_LIST_DISPLAYS,
.longopt = "list-displays",
.text = "List device displays.",
},
{
.longopt_id = OPT_LIST_ENCODERS,
.longopt = "list-encoders",
.text = "List video and audio encoders available on the device.",
},
{
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation",
.argdesc = "value",
.optional_arg = true,
.text = "Lock capture video orientation to value.\n"
"Possible values are \"unlocked\", \"initial\" (locked to the "
"initial orientation), 0, 90, 180 and 270. The values "
"represent the clockwise rotation from the natural device "
"orientation, in degrees.\n"
"Default is \"unlocked\".\n"
"Passing the option without argument is equivalent to passing "
"\"initial\".",
},
{
.shortopt = 'm',
.longopt = "max-size",
.argdesc = "value",
.text = "Limit both the width and height of the video to value. The "
"other dimension is computed so that the device aspect-ratio "
"is preserved.\n"
"Default is 0 (unlimited).",
},
{
// deprecated
.shortopt = 'M',
.longopt = "hid-mouse",
},
{
.longopt_id = OPT_MAX_FPS,
.longopt = "max-fps",
.argdesc = "value",
.text = "Limit the frame rate of screen capture (officially supported "
"since Android 10, but may work on earlier versions).",
},
{
.longopt_id = OPT_MOUSE,
.longopt = "mouse",
.argdesc = "mode",
.text = "Select how to send mouse inputs to the device.\n"
"Possible values are \"disabled\", \"sdk\", \"uhid\" and "
"\"aoa\".\n"
"\"disabled\" does not send mouse inputs to the device.\n"
"\"sdk\" uses the Android system API to deliver mouse events"
"to applications.\n"
"\"uhid\" simulates a physical HID mouse using the Linux UHID "
"kernel module on the device.\n"
"\"aoa\" simulates a physical mouse using the AOAv2 protocol. "
"It may only work over USB.\n"
"In \"uhid\" and \"aoa\" modes, the computer mouse is captured "
"to control the device directly (relative mouse mode).\n"
"LAlt, LSuper or RSuper toggle the capture mode, to give "
"control of the mouse back to the computer.\n"
"Also see --keyboard.",
},
{
.shortopt = 'n',
.longopt = "no-control",
.text = "Disable device control (mirror the device in read-only).",
},
{
.shortopt = 'N',
.longopt = "no-playback",
.text = "Disable video and audio playback on the computer (equivalent "
"to --no-video-playback --no-audio-playback).",
},
{
.longopt_id = OPT_NO_AUDIO,
.longopt = "no-audio",
.text = "Disable audio forwarding.",
},
{
.longopt_id = OPT_NO_AUDIO_PLAYBACK,
.longopt = "no-audio-playback",
.text = "Disable audio playback on the computer.",
},
{
.longopt_id = OPT_NO_CLEANUP,
.longopt = "no-cleanup",
.text = "By default, scrcpy removes the server binary from the device "
"and restores the device state (show touches, stay awake and "
"power mode) on exit.\n"
"This option disables this cleanup."
},
{
.longopt_id = OPT_NO_CLIPBOARD_AUTOSYNC,
.longopt = "no-clipboard-autosync",
.text = "By default, scrcpy automatically synchronizes the computer "
"clipboard to the device clipboard before injecting Ctrl+v, "
"and the device clipboard to the computer clipboard whenever "
"it changes.\n"
"This option disables this automatic synchronization."
},
{
.longopt_id = OPT_NO_DOWNSIZE_ON_ERROR,
.longopt = "no-downsize-on-error",
.text = "By default, on MediaCodec error, scrcpy automatically tries "
"again with a lower definition.\n"
"This option disables this behavior.",
},
{
// deprecated
.longopt_id = OPT_NO_DISPLAY,
.longopt = "no-display",
},
{
.longopt_id = OPT_NO_KEY_REPEAT,
.longopt = "no-key-repeat",
.text = "Do not forward repeated key events when a key is held down.",
},
{
.longopt_id = OPT_NO_MIPMAPS,
.longopt = "no-mipmaps",
.text = "If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then "
"mipmaps are automatically generated to improve downscaling "
"quality. This option disables the generation of mipmaps.",
},
{
.longopt_id = OPT_NO_POWER_ON,
.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_NO_VIDEO_PLAYBACK,
.longopt = "no-video-playback",
.text = "Disable video playback on the computer.",
},
{
.longopt_id = OPT_ORIENTATION,
.longopt = "orientation",
.argdesc = "value",
.text = "Same as --display-orientation=value "
"--record-orientation=value.",
},
{
.longopt_id = OPT_OTG,
.longopt = "otg",
.text = "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.\n"
"In this mode, adb (USB debugging) is not necessary, and "
"mirroring is disabled.\n"
"LAlt, LSuper or RSuper toggle the mouse capture mode, to give "
"control of the mouse back to the computer.\n"
"Keyboard and mouse may be disabled separately using"
"--keyboard=disabled and --mouse=disabled.\n"
"It may only work over USB.\n"
"See --keyboard and --mouse.",
},
{
.shortopt = 'p',
.longopt = "port",
.argdesc = "port[:port]",
.text = "Set the TCP port (range) used by the client to listen.\n"
"Default is " STR(DEFAULT_LOCAL_PORT_RANGE_FIRST) ":"
STR(DEFAULT_LOCAL_PORT_RANGE_LAST) ".",
},
{
.longopt_id = OPT_PAUSE_ON_EXIT,
.longopt = "pause-on-exit",
.argdesc = "mode",
.optional_arg = true,
.text = "Configure pause on exit. Possible values are \"true\" (always "
"pause on exit), \"false\" (never pause on exit) and "
"\"if-error\" (pause only if an error occured).\n"
"This is useful to prevent the terminal window from "
"automatically closing, so that error messages can be read.\n"
"Default is \"false\".\n"
"Passing the option without argument is equivalent to passing "
"\"true\".",
},
{
.longopt_id = OPT_POWER_OFF_ON_CLOSE,
.longopt = "power-off-on-close",
.text = "Turn the device screen off when closing scrcpy.",
},
{
.longopt_id = OPT_PREFER_TEXT,
.longopt = "prefer-text",
.text = "Inject alpha characters and space as text events instead of "
"key events.\n"
"This avoids issues when combining multiple keys to enter a "
"special character, but breaks the expected behavior of alpha "
"keys in games (typically WASD).",
},
{
.longopt_id = OPT_PRINT_FPS,
.longopt = "print-fps",
.text = "Start FPS counter, to print framerate logs to the console. "
"It can be started or stopped at any time with MOD+i.",
},
{
.longopt_id = OPT_PUSH_TARGET,
.longopt = "push-target",
.argdesc = "path",
.text = "Set the target directory for pushing files to the device by "
"drag & drop. It is passed as is to \"adb push\".\n"
"Default is \"/sdcard/Download/\".",
},
{
.shortopt = 'r',
.longopt = "record",
.argdesc = "file.mp4",
.text = "Record screen to file.\n"
"The format is determined by the --record-format option if "
"set, or by the file extension.",
},
{
.longopt_id = OPT_RAW_KEY_EVENTS,
.longopt = "raw-key-events",
.text = "Inject key events for all input keys, and ignore text events."
},
{
.longopt_id = OPT_RECORD_FORMAT,
.longopt = "record-format",
.argdesc = "format",
.text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac "
"or wav).",
},
{
.longopt_id = OPT_RECORD_ORIENTATION,
.longopt = "record-orientation",
.argdesc = "value",
.text = "Set the record orientation.\n"
"Possible values are 0, 90, 180 and 270. The number represents "
"the clockwise rotation in degrees.\n"
"Default is 0.",
},
{
.longopt_id = OPT_RENDER_DRIVER,
.longopt = "render-driver",
.argdesc = "name",
.text = "Request SDL to use the given render driver (this is just a "
"hint).\n"
"Supported names are currently \"direct3d\", \"opengl\", "
"\"opengles2\", \"opengles\", \"metal\" and \"software\".\n"
"<https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>",
},
{
.longopt_id = OPT_REQUIRE_AUDIO,
.longopt = "require-audio",
.text = "By default, scrcpy mirrors only the video when audio capture "
"fails on the device. This option makes scrcpy fail if audio "
"is enabled but does not work."
},
{
// deprecated
.longopt_id = OPT_ROTATION,
.longopt = "rotation",
.argdesc = "value",
},
{
.shortopt = 's',
.longopt = "serial",
.argdesc = "serial",
.text = "The device serial number. Mandatory only if several devices "
"are connected to adb.",
},
{
.shortopt = 'S',
.longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.",
},
{
.longopt_id = OPT_SHORTCUT_MOD,
.longopt = "shortcut-mod",
.argdesc = "key[+...][,...]",
.text = "Specify the modifiers to use for scrcpy shortcuts.\n"
"Possible keys are \"lctrl\", \"rctrl\", \"lalt\", \"ralt\", "
"\"lsuper\" and \"rsuper\".\n"
"A shortcut can consist in several keys, separated by '+'. "
"Several shortcuts can be specified, separated by ','.\n"
"For example, to use either LCtrl+LAlt or LSuper for scrcpy "
"shortcuts, pass \"lctrl+lalt,lsuper\".\n"
"Default is \"lalt,lsuper\" (left-Alt or left-Super).",
},
{
.shortopt = 't',
.longopt = "show-touches",
.text = "Enable \"show touches\" on start, restore the initial value "
"on exit.\n"
"It only shows physical touches (not clicks from scrcpy).",
},
{
.longopt_id = OPT_TCPIP,
.longopt = "tcpip",
.argdesc = "ip[:port]",
.optional_arg = true,
.text = "Configure and reconnect the device over TCP/IP.\n"
"If a destination address is provided, then scrcpy connects to "
"this address before starting. The device must listen on the "
"given TCP port (default is 5555).\n"
"If no destination address is provided, then scrcpy attempts "
"to find the IP address of the current device (typically "
"connected over USB), enables TCP/IP mode, then connects to "
"this address before starting.",
},
{
.longopt_id = OPT_TIME_LIMIT,
.longopt = "time-limit",
.argdesc = "seconds",
.text = "Set the maximum mirroring time, in seconds.",
},
{
.longopt_id = OPT_TUNNEL_HOST,
.longopt = "tunnel-host",
.argdesc = "ip",
.text = "Set the IP address of the adb tunnel to reach the scrcpy "
"server. This option automatically enables "
"--force-adb-forward.\n"
"Default is localhost.",
},
{
.longopt_id = OPT_TUNNEL_PORT,
.longopt = "tunnel-port",
.argdesc = "port",
.text = "Set the TCP port of the adb tunnel to reach the scrcpy "
"server. This option automatically enables "
"--force-adb-forward.\n"
"Default is 0 (not forced): the local port used for "
"establishing the tunnel will be used.",
},
{
.shortopt = 'v',
.longopt = "version",
.text = "Print the version of scrcpy.",
},
{
.shortopt = 'V',
.longopt = "verbosity",
.argdesc = "value",
.text = "Set the log level (verbose, debug, info, warn or error).\n"
#ifndef NDEBUG
"Default is debug.",
#else
"Default is info.",
#endif
},
{
.longopt_id = OPT_V4L2_SINK,
.longopt = "v4l2-sink",
.argdesc = "/dev/videoN",
.text = "Output to v4l2loopback device.\n"
"It requires to lock the video orientation (see "
"--lock-video-orientation).\n"
"This feature is only available on Linux.",
},
{
.longopt_id = OPT_V4L2_BUFFER,
.longopt = "v4l2-buffer",
.argdesc = "ms",
.text = "Add a buffering delay (in milliseconds) before pushing "
"frames. This increases latency to compensate for jitter.\n"
"This option is similar to --display-buffer, but specific to "
"V4L2 sink.\n"
"Default is 0 (no buffering).\n"
"This option is only available on Linux.",
},
{
.longopt_id = OPT_VIDEO_CODEC,
.longopt = "video-codec",
.argdesc = "name",
.text = "Select a video codec (h264, h265 or av1).\n"
"Default is h264.",
},
{
.longopt_id = OPT_VIDEO_CODEC_OPTIONS,
.longopt = "video-codec-options",
.argdesc = "key[:type]=value[,...]",
.text = "Set a list of comma-separated key:type=value options for the "
"device video encoder.\n"
"The possible values for 'type' are 'int' (default), 'long', "
"'float' and 'string'.\n"
"The list of possible codec options is available in the "
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_VIDEO_ENCODER,
.longopt = "video-encoder",
.argdesc = "name",
.text = "Use a specific MediaCodec video encoder (depending on the "
"codec provided by --video-codec).\n"
"The available encoders can be listed by --list-encoders.",
},
{
.longopt_id = OPT_VIDEO_SOURCE,
.longopt = "video-source",
.argdesc = "source",
.text = "Select the video source (display or camera).\n"
"Camera mirroring requires Android 12+.\n"
"Default is display.",
},
{
.shortopt = 'w',
.longopt = "stay-awake",
.text = "Keep the device on while scrcpy is running, when the device "
"is plugged in.",
},
{
.longopt_id = OPT_WINDOW_BORDERLESS,
.longopt = "window-borderless",
.text = "Disable window decorations (display borderless window)."
},
{
.longopt_id = OPT_WINDOW_TITLE,
.longopt = "window-title",
.argdesc = "text",
.text = "Set a custom window title.",
},
{
.longopt_id = OPT_WINDOW_X,
.longopt = "window-x",
.argdesc = "value",
.text = "Set the initial window horizontal position.\n"
"Default is \"auto\".",
},
{
.longopt_id = OPT_WINDOW_Y,
.longopt = "window-y",
.argdesc = "value",
.text = "Set the initial window vertical position.\n"
"Default is \"auto\".",
},
{
.longopt_id = OPT_WINDOW_WIDTH,
.longopt = "window-width",
.argdesc = "value",
.text = "Set the initial window width.\n"
"Default is 0 (automatic).",
},
{
.longopt_id = OPT_WINDOW_HEIGHT,
.longopt = "window-height",
.argdesc = "value",
.text = "Set the initial window height.\n"
"Default is 0 (automatic).",
},
};
static const struct sc_shortcut shortcuts[] = {
{
.shortcuts = { "MOD+f" },
.text = "Switch fullscreen mode",
},
{
.shortcuts = { "MOD+Left" },
.text = "Rotate display left",
},
{
.shortcuts = { "MOD+Right" },
.text = "Rotate display right",
},
{
.shortcuts = { "MOD+Shift+Left", "MOD+Shift+Right" },
.text = "Flip display horizontally",
},
{
.shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" },
.text = "Flip display vertically",
},
{
.shortcuts = { "MOD+g" },
.text = "Resize window to 1:1 (pixel-perfect)",
},
{
.shortcuts = { "MOD+w", "Double-click on black borders" },
.text = "Resize window to remove black borders",
},
{
.shortcuts = { "MOD+h", "Middle-click" },
.text = "Click on HOME",
},
{
.shortcuts = {
"MOD+b",
"MOD+Backspace",
"Right-click (when screen is on)",
},
.text = "Click on BACK",
},
{
.shortcuts = { "MOD+s", "4th-click" },
.text = "Click on APP_SWITCH",
},
{
.shortcuts = { "MOD+m" },
.text = "Click on MENU",
},
{
.shortcuts = { "MOD+Up" },
.text = "Click on VOLUME_UP",
},
{
.shortcuts = { "MOD+Down" },
.text = "Click on VOLUME_DOWN",
},
{
.shortcuts = { "MOD+p" },
.text = "Click on POWER (turn screen on/off)",
},
{
.shortcuts = { "Right-click (when screen is off)" },
.text = "Power on",
},
{
.shortcuts = { "MOD+o" },
.text = "Turn device screen off (keep mirroring)",
},
{
.shortcuts = { "MOD+Shift+o" },
.text = "Turn device screen on",
},
{
.shortcuts = { "MOD+r" },
.text = "Rotate device screen",
},
{
.shortcuts = { "MOD+n", "5th-click" },
.text = "Expand notification panel",
},
{
.shortcuts = { "MOD+Shift+n" },
.text = "Collapse notification panel",
},
{
.shortcuts = { "MOD+c" },
.text = "Copy to clipboard (inject COPY keycode, Android >= 7 only)",
},
{
.shortcuts = { "MOD+x" },
.text = "Cut to clipboard (inject CUT keycode, Android >= 7 only)",
},
{
.shortcuts = { "MOD+v" },
.text = "Copy computer clipboard to device, then paste (inject PASTE "
"keycode, Android >= 7 only)",
},
{
.shortcuts = { "MOD+Shift+v" },
.text = "Inject computer clipboard text as a sequence of key events",
},
{
.shortcuts = { "MOD+k" },
.text = "Open keyboard settings on the device (for HID keyboard only)",
},
{
.shortcuts = { "MOD+i" },
.text = "Enable/disable FPS counter (print frames/second in logs)",
},
{
.shortcuts = { "Ctrl+click-and-move" },
.text = "Pinch-to-zoom and rotate from the center of the screen",
},
{
.shortcuts = { "Shift+click-and-move" },
.text = "Tilt (slide vertically with two fingers)",
},
{
.shortcuts = { "Drag & drop APK file" },
.text = "Install APK from computer",
},
{
.shortcuts = { "Drag & drop non-APK file" },
.text = "Push file to device (see --push-target)",
},
};
static const struct sc_envvar envvars[] = {
{
.name = "ADB",
.text = "Path to adb executable",
},
{
.name = "ANDROID_SERIAL",
.text = "Device serial to use if no selector (-s, -d, -e or "
"--tcpip=<addr>) is specified",
},
{
.name = "SCRCPY_ICON_PATH",
.text = "Path to the program icon",
},
{
.name = "SCRCPY_SERVER_PATH",
.text = "Path to the server binary",
},
};
static const struct sc_exit_status exit_statuses[] = {
{
.value = 0,
.text = "Normal program termination",
},
{
.value = 1,
.text = "Start failure",
},
{
.value = 2,
.text = "Device disconnected while running",
},
};
static char *
sc_getopt_adapter_create_optstring(void) {
struct sc_strbuf buf;
if (!sc_strbuf_init(&buf, 64)) {
return false;
}
for (size_t i = 0; i < ARRAY_LEN(options); ++i) {
const struct sc_option *opt = &options[i];
if (opt->shortopt) {
if (!sc_strbuf_append_char(&buf, opt->shortopt)) {
goto error;
}
// If there is an argument, add ':'
if (opt->argdesc) {
if (!sc_strbuf_append_char(&buf, ':')) {
goto error;
}
// If the argument is optional, add another ':'
if (opt->optional_arg && !sc_strbuf_append_char(&buf, ':')) {
goto error;
}
}
}
}
return buf.s;
error:
free(buf.s);
return NULL;
}
static struct option *
sc_getopt_adapter_create_longopts(void) {
struct option *longopts =
malloc((ARRAY_LEN(options) + 1) * sizeof(*longopts));
if (!longopts) {
LOG_OOM();
return NULL;
}
size_t out_idx = 0;
for (size_t i = 0; i < ARRAY_LEN(options); ++i) {
const struct sc_option *in = &options[i];
// If longopt_id is set, then longopt must be set
assert(!in->longopt_id || in->longopt);
if (!in->longopt) {
// The longopts array must only contain long options
continue;
}
struct option *out = &longopts[out_idx++];
out->name = in->longopt;
if (!in->argdesc) {
assert(!in->optional_arg);
out->has_arg = no_argument;
} else if (in->optional_arg) {
out->has_arg = optional_argument;
} else {
out->has_arg = required_argument;
}
out->flag = NULL;
// Either shortopt or longopt_id is set, but not both
assert(!!in->shortopt ^ !!in->longopt_id);
out->val = in->shortopt ? in->shortopt : in->longopt_id;
}
// The array must be terminated by a NULL item
longopts[out_idx] = (struct option) {0};
return longopts;
}
static bool
sc_getopt_adapter_init(struct sc_getopt_adapter *adapter) {
adapter->optstring = sc_getopt_adapter_create_optstring();
if (!adapter->optstring) {
return false;
}
adapter->longopts = sc_getopt_adapter_create_longopts();
if (!adapter->longopts) {
free(adapter->optstring);
return false;
}
return true;
}
static void
sc_getopt_adapter_destroy(struct sc_getopt_adapter *adapter) {
free(adapter->optstring);
free(adapter->longopts);
}
static void
print_option_usage_header(const struct sc_option *opt) {
struct sc_strbuf buf;
if (!sc_strbuf_init(&buf, 64)) {
goto error;
}
bool ok = true;
(void) ok; // only used for assertions
if (opt->shortopt) {
ok = sc_strbuf_append_char(&buf, '-');
assert(ok);
ok = sc_strbuf_append_char(&buf, opt->shortopt);
assert(ok);
if (opt->longopt) {
ok = sc_strbuf_append_staticstr(&buf, ", ");
assert(ok);
}
}
if (opt->longopt) {
ok = sc_strbuf_append_staticstr(&buf, "--");
assert(ok);
if (!sc_strbuf_append_str(&buf, opt->longopt)) {
goto error;
}
}
if (opt->argdesc) {
if (opt->optional_arg && !sc_strbuf_append_char(&buf, '[')) {
goto error;
}
if (!sc_strbuf_append_char(&buf, '=')) {
goto error;
}
if (!sc_strbuf_append_str(&buf, opt->argdesc)) {
goto error;
}
if (opt->optional_arg && !sc_strbuf_append_char(&buf, ']')) {
goto error;
}
}
printf("\n %s\n", buf.s);
free(buf.s);
return;
error:
printf("<ERROR>\n");
}
static void
print_option_usage(const struct sc_option *opt, unsigned cols) {
assert(cols > 8); // sc_str_wrap_lines() requires indent < columns
if (!opt->text) {
// Option not documented in help (for example because it is deprecated)
return;
}
print_option_usage_header(opt);
char *text = sc_str_wrap_lines(opt->text, cols, 8);
if (!text) {
printf("<ERROR>\n");
return;
}
printf("%s\n", text);
free(text);
}
static void
print_shortcuts_intro(unsigned cols) {
char *intro = sc_str_wrap_lines(
"In the following list, MOD is the shortcut modifier. By default, it's "
"(left) Alt or (left) Super, but it can be configured by "
"--shortcut-mod (see above).", cols, 4);
if (!intro) {
printf("<ERROR>\n");
return;
}
printf("\n%s\n", intro);
free(intro);
}
static void
print_shortcut(const struct sc_shortcut *shortcut, unsigned cols) {
assert(cols > 8); // sc_str_wrap_lines() requires indent < columns
assert(shortcut->shortcuts[0]); // At least one shortcut
assert(shortcut->text);
printf("\n");
unsigned i = 0;
while (shortcut->shortcuts[i]) {
printf(" %s\n", shortcut->shortcuts[i]);
++i;
}
char *text = sc_str_wrap_lines(shortcut->text, cols, 8);
if (!text) {
printf("<ERROR>\n");
return;
}
printf("%s\n", text);
free(text);
}
static void
print_envvar(const struct sc_envvar *envvar, unsigned cols) {
assert(cols > 8); // sc_str_wrap_lines() requires indent < columns
assert(envvar->name);
assert(envvar->text);
printf("\n %s\n", envvar->name);
char *text = sc_str_wrap_lines(envvar->text, cols, 8);
if (!text) {
printf("<ERROR>\n");
return;
}
printf("%s\n", text);
free(text);
}
static void
print_exit_status(const struct sc_exit_status *status, unsigned cols) {
assert(cols > 8); // sc_str_wrap_lines() requires indent < columns
assert(status->text);
// The text starts at 9: 4 ident spaces, 3 chars for numeric value, 2 spaces
char *text = sc_str_wrap_lines(status->text, cols, 9);
if (!text) {
printf("<ERROR>\n");
return;
}
assert(strlen(text) >= 9); // Contains at least the initial identation
// text + 9 to remove the initial indentation
printf(" %3d %s\n", status->value, text + 9);
free(text);
}
void
scrcpy_print_usage(const char *arg0) {
#define SC_TERM_COLS_DEFAULT 80
unsigned cols;
if (!isatty(STDERR_FILENO)) {
// Not a tty
cols = SC_TERM_COLS_DEFAULT;
} else {
bool ok = sc_term_get_size(NULL, &cols);
if (!ok) {
// Could not get the terminal size
cols = SC_TERM_COLS_DEFAULT;
}
if (cols < 20) {
// Do not accept a too small value
cols = 20;
}
}
printf("Usage: %s [options]\n\n"
"Options:\n", arg0);
for (size_t i = 0; i < ARRAY_LEN(options); ++i) {
print_option_usage(&options[i], cols);
}
// Print shortcuts section
printf("\nShortcuts:\n");
print_shortcuts_intro(cols);
for (size_t i = 0; i < ARRAY_LEN(shortcuts); ++i) {
print_shortcut(&shortcuts[i], cols);
}
// Print environment variables section
printf("\nEnvironment variables:\n");
for (size_t i = 0; i < ARRAY_LEN(envvars); ++i) {
print_envvar(&envvars[i], cols);
}
printf("\nExit status:\n\n");
for (size_t i = 0; i < ARRAY_LEN(exit_statuses); ++i) {
print_exit_status(&exit_statuses[i], cols);
}
}
static bool
parse_integer_arg(const char *s, long *out, bool accept_suffix, long min,
long max, const char *name) {
long value;
bool ok;
if (accept_suffix) {
ok = sc_str_parse_integer_with_suffix(s, &value);
} else {
ok = sc_str_parse_integer(s, &value);
}
if (!ok) {
LOGE("Could not parse %s: %s", name, s);
return false;
}
if (value < min || value > max) {
LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)",
name, value, min, max);
return false;
}
*out = value;
return true;
}
static size_t
parse_integers_arg(const char *s, const char sep, size_t max_items, long *out,
long min, long max, const char *name) {
size_t count = sc_str_parse_integers(s, sep, max_items, out);
if (!count) {
LOGE("Could not parse %s: %s", name, s);
return 0;
}
for (size_t i = 0; i < count; ++i) {
long value = out[i];
if (value < min || value > max) {
LOGE("Could not parse %s: value (%ld) out-of-range (%ld; %ld)",
name, value, min, max);
return 0;
}
}
return count;
}
static bool
parse_bit_rate(const char *s, uint32_t *bit_rate) {
long value;
// long may be 32 bits (it is the case on mingw), so do not use more than
// 31 bits (long is signed)
bool ok = parse_integer_arg(s, &value, true, 0, 0x7FFFFFFF, "bit-rate");
if (!ok) {
return false;
}
*bit_rate = (uint32_t) value;
return true;
}
static bool
parse_max_size(const char *s, uint16_t *max_size) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max size");
if (!ok) {
return false;
}
*max_size = (uint16_t) value;
return true;
}
static bool
parse_max_fps(const char *s, uint16_t *max_fps) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "max fps");
if (!ok) {
return false;
}
*max_fps = (uint16_t) value;
return true;
}
static bool
parse_buffering_time(const char *s, sc_tick *tick) {
long value;
// In practice, buffering time should not exceed a few seconds.
// Limit it to some arbitrary value (1 hour) to prevent 32-bit overflow
// when multiplied by the audio sample size and the number of samples per
// millisecond.
bool ok = parse_integer_arg(s, &value, false, 0, 60 * 60 * 1000,
"buffering time");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_MS(value);
return true;
}
static bool
parse_audio_output_buffer(const char *s, sc_tick *tick) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 1000,
"audio output buffer");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_MS(value);
return true;
}
static bool
parse_lock_video_orientation(const char *s,
enum sc_lock_video_orientation *lock_mode) {
if (!s || !strcmp(s, "initial")) {
// Without argument, lock the initial orientation
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
return true;
}
if (!strcmp(s, "unlocked")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED;
return true;
}
if (!strcmp(s, "0")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_0;
return true;
}
if (!strcmp(s, "90")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
return true;
}
if (!strcmp(s, "180")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
return true;
}
if (!strcmp(s, "270")) {
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
return true;
}
if (!strcmp(s, "1")) {
LOGW("--lock-video-orientation=1 is deprecated, use "
"--lock-video-orientation=270 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_270;
return true;
}
if (!strcmp(s, "2")) {
LOGW("--lock-video-orientation=2 is deprecated, use "
"--lock-video-orientation=180 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_180;
return true;
}
if (!strcmp(s, "3")) {
LOGW("--lock-video-orientation=3 is deprecated, use "
"--lock-video-orientation=90 instead.");
*lock_mode = SC_LOCK_VIDEO_ORIENTATION_90;
return true;
}
LOGE("Unsupported --lock-video-orientation value: %s (expected initial, "
"unlocked, 0, 90, 180 or 270).", s);
return false;
}
static bool
parse_rotation(const char *s, uint8_t *rotation) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation");
if (!ok) {
return false;
}
*rotation = (uint8_t) value;
return true;
}
static bool
parse_orientation(const char *s, enum sc_orientation *orientation) {
if (!strcmp(s, "0")) {
*orientation = SC_ORIENTATION_0;
return true;
}
if (!strcmp(s, "90")) {
*orientation = SC_ORIENTATION_90;
return true;
}
if (!strcmp(s, "180")) {
*orientation = SC_ORIENTATION_180;
return true;
}
if (!strcmp(s, "270")) {
*orientation = SC_ORIENTATION_270;
return true;
}
if (!strcmp(s, "flip0")) {
*orientation = SC_ORIENTATION_FLIP_0;
return true;
}
if (!strcmp(s, "flip90")) {
*orientation = SC_ORIENTATION_FLIP_90;
return true;
}
if (!strcmp(s, "flip180")) {
*orientation = SC_ORIENTATION_FLIP_180;
return true;
}
if (!strcmp(s, "flip270")) {
*orientation = SC_ORIENTATION_FLIP_270;
return true;
}
LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, "
"flip90, flip180 or flip270)", optarg);
return false;
}
static bool
parse_window_position(const char *s, int16_t *position) {
// special value for "auto"
static_assert(SC_WINDOW_POSITION_UNDEFINED == -0x8000, "unexpected value");
if (!strcmp(s, "auto")) {
*position = SC_WINDOW_POSITION_UNDEFINED;
return true;
}
long value;
bool ok = parse_integer_arg(s, &value, false, -0x7FFF, 0x7FFF,
"window position");
if (!ok) {
return false;
}
*position = (int16_t) value;
return true;
}
static bool
parse_window_dimension(const char *s, uint16_t *dimension) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF,
"window dimension");
if (!ok) {
return false;
}
*dimension = (uint16_t) value;
return true;
}
static bool
parse_port_range(const char *s, struct sc_port_range *port_range) {
long values[2];
size_t count = parse_integers_arg(s, ':', 2, values, 0, 0xFFFF, "port");
if (!count) {
return false;
}
uint16_t v0 = (uint16_t) values[0];
if (count == 1) {
port_range->first = v0;
port_range->last = v0;
return true;
}
assert(count == 2);
uint16_t v1 = (uint16_t) values[1];
if (v0 < v1) {
port_range->first = v0;
port_range->last = v1;
} else {
port_range->first = v1;
port_range->last = v0;
}
return true;
}
static bool
parse_display_id(const char *s, uint32_t *display_id) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "display id");
if (!ok) {
return false;
}
*display_id = (uint32_t) value;
return true;
}
static bool
parse_log_level(const char *s, enum sc_log_level *log_level) {
if (!strcmp(s, "verbose")) {
*log_level = SC_LOG_LEVEL_VERBOSE;
return true;
}
if (!strcmp(s, "debug")) {
*log_level = SC_LOG_LEVEL_DEBUG;
return true;
}
if (!strcmp(s, "info")) {
*log_level = SC_LOG_LEVEL_INFO;
return true;
}
if (!strcmp(s, "warn")) {
*log_level = SC_LOG_LEVEL_WARN;
return true;
}
if (!strcmp(s, "error")) {
*log_level = SC_LOG_LEVEL_ERROR;
return true;
}
LOGE("Could not parse log level: %s", s);
return false;
}
// item is a list of mod keys separated by '+' (e.g. "lctrl+lalt")
// returns a bitwise-or of SC_SHORTCUT_MOD_* constants (or 0 on error)
static unsigned
parse_shortcut_mods_item(const char *item, size_t len) {
unsigned mod = 0;
for (;;) {
char *plus = strchr(item, '+');
// strchr() does not consider the "len" parameter, to it could find an
// occurrence too far in the string (there is no strnchr())
bool has_plus = plus && plus < item + len;
assert(!has_plus || plus > item);
size_t key_len = has_plus ? (size_t) (plus - item) : len;
#define STREQ(literal, s, len) \
((sizeof(literal)-1 == len) && !memcmp(literal, s, len))
if (STREQ("lctrl", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LCTRL;
} else if (STREQ("rctrl", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RCTRL;
} else if (STREQ("lalt", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LALT;
} else if (STREQ("ralt", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RALT;
} else if (STREQ("lsuper", item, key_len)) {
mod |= SC_SHORTCUT_MOD_LSUPER;
} else if (STREQ("rsuper", item, key_len)) {
mod |= SC_SHORTCUT_MOD_RSUPER;
} else {
LOGE("Unknown modifier key: %.*s "
"(must be one of: lctrl, rctrl, lalt, ralt, lsuper, rsuper)",
(int) key_len, item);
return 0;
}
#undef STREQ
if (!has_plus) {
break;
}
item = plus + 1;
assert(len >= key_len + 1);
len -= key_len + 1;
}
return mod;
}
static bool
parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
unsigned count = 0;
unsigned current = 0;
// LCtrl+LAlt or RCtrl or LCtrl+RSuper: "lctrl+lalt,rctrl,lctrl+rsuper"
for (;;) {
char *comma = strchr(s, ',');
if (comma && count == SC_MAX_SHORTCUT_MODS - 1) {
assert(count < SC_MAX_SHORTCUT_MODS);
LOGW("Too many shortcut modifiers alternatives");
return false;
}
assert(!comma || comma > s);
size_t limit = comma ? (size_t) (comma - s) : strlen(s);
unsigned mod = parse_shortcut_mods_item(s, limit);
if (!mod) {
LOGE("Invalid modifier keys: %.*s", (int) limit, s);
return false;
}
mods->data[current++] = mod;
++count;
if (!comma) {
break;
}
s = comma + 1;
}
mods->count = count;
return true;
}
#ifdef SC_TEST
// expose the function to unit-tests
bool
sc_parse_shortcut_mods(const char *s, struct sc_shortcut_mods *mods) {
return parse_shortcut_mods(s, mods);
}
#endif
static enum sc_record_format
get_record_format(const char *name) {
if (!strcmp(name, "mp4")) {
return SC_RECORD_FORMAT_MP4;
}
if (!strcmp(name, "mkv")) {
return SC_RECORD_FORMAT_MKV;
}
if (!strcmp(name, "m4a")) {
return SC_RECORD_FORMAT_M4A;
}
if (!strcmp(name, "mka")) {
return SC_RECORD_FORMAT_MKA;
}
if (!strcmp(name, "opus")) {
return SC_RECORD_FORMAT_OPUS;
}
if (!strcmp(name, "aac")) {
return SC_RECORD_FORMAT_AAC;
}
if (!strcmp(name, "flac")) {
return SC_RECORD_FORMAT_FLAC;
}
if (!strcmp(name, "wav")) {
return SC_RECORD_FORMAT_WAV;
}
return 0;
}
static bool
parse_record_format(const char *optarg, enum sc_record_format *format) {
enum sc_record_format fmt = get_record_format(optarg);
if (!fmt) {
LOGE("Unsupported record format: %s (expected mp4, mkv, m4a, mka, "
"opus, aac, flac or wav)", optarg);
return false;
}
*format = fmt;
return true;
}
static bool
parse_ip(const char *optarg, uint32_t *ipv4) {
return net_parse_ipv4(optarg, ipv4);
}
static bool
parse_port(const char *optarg, uint16_t *port) {
long value;
if (!parse_integer_arg(optarg, &value, false, 0, 0xFFFF, "port")) {
return false;
}
*port = (uint16_t) value;
return true;
}
static enum sc_record_format
guess_record_format(const char *filename) {
const char *dot = strrchr(filename, '.');
if (!dot) {
return 0;
}
const char *ext = dot + 1;
return get_record_format(ext);
}
static bool
parse_video_codec(const char *optarg, enum sc_codec *codec) {
if (!strcmp(optarg, "h264")) {
*codec = SC_CODEC_H264;
return true;
}
if (!strcmp(optarg, "h265")) {
*codec = SC_CODEC_H265;
return true;
}
if (!strcmp(optarg, "av1")) {
*codec = SC_CODEC_AV1;
return true;
}
LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg);
return false;
}
static bool
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
if (!strcmp(optarg, "opus")) {
*codec = SC_CODEC_OPUS;
return true;
}
if (!strcmp(optarg, "aac")) {
*codec = SC_CODEC_AAC;
return true;
}
if (!strcmp(optarg, "flac")) {
*codec = SC_CODEC_FLAC;
return true;
}
if (!strcmp(optarg, "raw")) {
*codec = SC_CODEC_RAW;
return true;
}
LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)",
optarg);
return false;
}
static bool
parse_video_source(const char *optarg, enum sc_video_source *source) {
if (!strcmp(optarg, "display")) {
*source = SC_VIDEO_SOURCE_DISPLAY;
return true;
}
if (!strcmp(optarg, "camera")) {
*source = SC_VIDEO_SOURCE_CAMERA;
return true;
}
LOGE("Unsupported video source: %s (expected display or camera)", optarg);
return false;
}
static bool
parse_audio_source(const char *optarg, enum sc_audio_source *source) {
if (!strcmp(optarg, "mic")) {
*source = SC_AUDIO_SOURCE_MIC;
return true;
}
if (!strcmp(optarg, "output")) {
*source = SC_AUDIO_SOURCE_OUTPUT;
return true;
}
LOGE("Unsupported audio source: %s (expected output or mic)", optarg);
return false;
}
static bool
parse_camera_facing(const char *optarg, enum sc_camera_facing *facing) {
if (!strcmp(optarg, "front")) {
*facing = SC_CAMERA_FACING_FRONT;
return true;
}
if (!strcmp(optarg, "back")) {
*facing = SC_CAMERA_FACING_BACK;
return true;
}
if (!strcmp(optarg, "external")) {
*facing = SC_CAMERA_FACING_EXTERNAL;
return true;
}
if (*optarg == '\0') {
// Empty string is a valid value (equivalent to not passing the option)
*facing = SC_CAMERA_FACING_ANY;
return true;
}
LOGE("Unsupported camera facing: %s (expected front, back or external)",
optarg);
return false;
}
static bool
parse_camera_fps(const char *s, uint16_t *camera_fps) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0xFFFF, "camera fps");
if (!ok) {
return false;
}
*camera_fps = (uint16_t) value;
return true;
}
static bool
parse_keyboard(const char *optarg, enum sc_keyboard_input_mode *mode) {
if (!strcmp(optarg, "disabled")) {
*mode = SC_KEYBOARD_INPUT_MODE_DISABLED;
return true;
}
if (!strcmp(optarg, "sdk")) {
*mode = SC_KEYBOARD_INPUT_MODE_SDK;
return true;
}
if (!strcmp(optarg, "uhid")) {
*mode = SC_KEYBOARD_INPUT_MODE_UHID;
return true;
}
if (!strcmp(optarg, "aoa")) {
#ifdef HAVE_USB
*mode = SC_KEYBOARD_INPUT_MODE_AOA;
return true;
#else
LOGE("--keyboard=aoa is disabled.");
return false;
#endif
}
LOGE("Unsupported keyboard: %s (expected disabled, sdk, uhid and aoa)",
optarg);
return false;
}
static bool
parse_mouse(const char *optarg, enum sc_mouse_input_mode *mode) {
if (!strcmp(optarg, "disabled")) {
*mode = SC_MOUSE_INPUT_MODE_DISABLED;
return true;
}
if (!strcmp(optarg, "sdk")) {
*mode = SC_MOUSE_INPUT_MODE_SDK;
return true;
}
if (!strcmp(optarg, "uhid")) {
*mode = SC_MOUSE_INPUT_MODE_UHID;
return true;
}
if (!strcmp(optarg, "aoa")) {
#ifdef HAVE_USB
*mode = SC_MOUSE_INPUT_MODE_AOA;
return true;
#else
LOGE("--mouse=aoa is disabled.");
return false;
#endif
}
LOGE("Unsupported mouse: %s (expected disabled, sdk, uhid or aoa)", optarg);
return false;
}
static bool
parse_time_limit(const char *s, sc_tick *tick) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "time limit");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_SEC(value);
return true;
}
static bool
parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) {
if (!s || !strcmp(s, "true")) {
*pause_on_exit = SC_PAUSE_ON_EXIT_TRUE;
return true;
}
if (!strcmp(s, "false")) {
*pause_on_exit = SC_PAUSE_ON_EXIT_FALSE;
return true;
}
if (!strcmp(s, "if-error")) {
*pause_on_exit = SC_PAUSE_ON_EXIT_IF_ERROR;
return true;
}
LOGE("Unsupported pause on exit mode: %s "
"(expected true, false or if-error)", optarg);
return false;
}
static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) {
struct scrcpy_options *opts = &args->opts;
optind = 0; // reset to start from the first argument in tests
int c;
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
switch (c) {
case OPT_BIT_RATE:
LOGE("--bit-rate has been removed, "
"use --video-bit-rate or --audio-bit-rate.");
return false;
case 'b':
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
return false;
}
break;
case OPT_AUDIO_BIT_RATE:
if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) {
return false;
}
break;
case OPT_CROP:
opts->crop = optarg;
break;
case OPT_DISPLAY:
LOGW("--display is deprecated, use --display-id instead.");
// fall through
case OPT_DISPLAY_ID:
if (!parse_display_id(optarg, &opts->display_id)) {
return false;
}
break;
case 'd':
opts->select_usb = true;
break;
case 'e':
opts->select_tcpip = true;
break;
case 'f':
opts->fullscreen = true;
break;
case OPT_RECORD_FORMAT:
if (!parse_record_format(optarg, &opts->record_format)) {
return false;
}
break;
case 'h':
args->help = true;
break;
case 'K':
#ifdef HAVE_USB
LOGW("-K/--hid-keyboard is deprecated, use --keyboard=aoa "
"instead.");
opts->keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_AOA;
break;
#else
LOGE("HID over AOA (-K/--hid-keyboard) is disabled.");
return false;
#endif
case OPT_KEYBOARD:
if (!parse_keyboard(optarg, &opts->keyboard_input_mode)) {
return false;
}
break;
case OPT_MAX_FPS:
if (!parse_max_fps(optarg, &opts->max_fps)) {
return false;
}
break;
case 'm':
if (!parse_max_size(optarg, &opts->max_size)) {
return false;
}
break;
case 'M':
#ifdef HAVE_USB
LOGW("-M/--hid-mouse is deprecated, use --mouse=aoa instead.");
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
break;
#else
LOGE("HID over AOA (-M/--hid-mouse) is disabled.");
return false;
#endif
case OPT_MOUSE:
if (!parse_mouse(optarg, &opts->mouse_input_mode)) {
return false;
}
break;
case OPT_LOCK_VIDEO_ORIENTATION:
if (!parse_lock_video_orientation(optarg,
&opts->lock_video_orientation)) {
return false;
}
break;
case OPT_TUNNEL_HOST:
if (!parse_ip(optarg, &opts->tunnel_host)) {
return false;
}
break;
case OPT_TUNNEL_PORT:
if (!parse_port(optarg, &opts->tunnel_port)) {
return false;
}
break;
case 'n':
opts->control = false;
break;
case OPT_NO_DISPLAY:
LOGW("--no-display is deprecated, use --no-playback instead.");
// fall through
case 'N':
opts->video_playback = false;
opts->audio_playback = false;
break;
case OPT_NO_VIDEO_PLAYBACK:
opts->video_playback = false;
break;
case OPT_NO_AUDIO_PLAYBACK:
opts->audio_playback = false;
break;
case 'p':
if (!parse_port_range(optarg, &opts->port_range)) {
return false;
}
break;
case 'r':
opts->record_filename = optarg;
break;
case 's':
opts->serial = optarg;
break;
case 'S':
opts->turn_screen_off = true;
break;
case 't':
opts->show_touches = true;
break;
case OPT_ALWAYS_ON_TOP:
opts->always_on_top = true;
break;
case 'v':
args->version = true;
break;
case 'V':
if (!parse_log_level(optarg, &opts->log_level)) {
return false;
}
break;
case 'w':
opts->stay_awake = true;
break;
case OPT_WINDOW_TITLE:
opts->window_title = optarg;
break;
case OPT_WINDOW_X:
if (!parse_window_position(optarg, &opts->window_x)) {
return false;
}
break;
case OPT_WINDOW_Y:
if (!parse_window_position(optarg, &opts->window_y)) {
return false;
}
break;
case OPT_WINDOW_WIDTH:
if (!parse_window_dimension(optarg, &opts->window_width)) {
return false;
}
break;
case OPT_WINDOW_HEIGHT:
if (!parse_window_dimension(optarg, &opts->window_height)) {
return false;
}
break;
case OPT_WINDOW_BORDERLESS:
opts->window_borderless = true;
break;
case OPT_PUSH_TARGET:
opts->push_target = optarg;
break;
case OPT_PREFER_TEXT:
if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) {
LOGE("--prefer-text is incompatible with --raw-key-events");
return false;
}
opts->key_inject_mode = SC_KEY_INJECT_MODE_TEXT;
break;
case OPT_RAW_KEY_EVENTS:
if (opts->key_inject_mode != SC_KEY_INJECT_MODE_MIXED) {
LOGE("--prefer-text is incompatible with --raw-key-events");
return false;
}
opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW;
break;
case OPT_ROTATION:
LOGW("--rotation is deprecated, use --display-orientation "
"instead.");
uint8_t rotation;
if (!parse_rotation(optarg, &rotation)) {
return false;
}
assert(rotation <= 3);
switch (rotation) {
case 0:
opts->display_orientation = SC_ORIENTATION_0;
break;
case 1:
// rotation 1 was 90° counterclockwise, but orientation
// is expressed clockwise
opts->display_orientation = SC_ORIENTATION_270;
break;
case 2:
opts->display_orientation = SC_ORIENTATION_180;
break;
case 3:
// rotation 3 was 270° counterclockwise, but orientation
// is expressed clockwise
opts->display_orientation = SC_ORIENTATION_90;
break;
}
break;
case OPT_DISPLAY_ORIENTATION:
if (!parse_orientation(optarg, &opts->display_orientation)) {
return false;
}
break;
case OPT_RECORD_ORIENTATION:
if (!parse_orientation(optarg, &opts->record_orientation)) {
return false;
}
break;
case OPT_ORIENTATION: {
enum sc_orientation orientation;
if (!parse_orientation(optarg, &orientation)) {
return false;
}
opts->display_orientation = orientation;
opts->record_orientation = orientation;
break;
}
case OPT_RENDER_DRIVER:
opts->render_driver = optarg;
break;
case OPT_NO_MIPMAPS:
opts->mipmaps = false;
break;
case OPT_NO_KEY_REPEAT:
opts->forward_key_repeat = false;
break;
case OPT_CODEC_OPTIONS:
LOGE("--codec-options has been removed, "
"use --video-codec-options or --audio-codec-options.");
return false;
case OPT_VIDEO_CODEC_OPTIONS:
opts->video_codec_options = optarg;
break;
case OPT_AUDIO_CODEC_OPTIONS:
opts->audio_codec_options = optarg;
break;
case OPT_ENCODER:
LOGE("--encoder has been removed, "
"use --video-encoder or --audio-encoder.");
return false;
case OPT_VIDEO_ENCODER:
opts->video_encoder = optarg;
break;
case OPT_AUDIO_ENCODER:
opts->audio_encoder = optarg;
break;
case OPT_FORCE_ADB_FORWARD:
opts->force_adb_forward = true;
break;
case OPT_DISABLE_SCREENSAVER:
opts->disable_screensaver = true;
break;
case OPT_SHORTCUT_MOD:
if (!parse_shortcut_mods(optarg, &opts->shortcut_mods)) {
return false;
}
break;
case OPT_FORWARD_ALL_CLICKS:
opts->forward_all_clicks = true;
break;
case OPT_LEGACY_PASTE:
opts->legacy_paste = true;
break;
case OPT_POWER_OFF_ON_CLOSE:
opts->power_off_on_close = true;
break;
case OPT_DISPLAY_BUFFER:
if (!parse_buffering_time(optarg, &opts->display_buffer)) {
return false;
}
break;
case OPT_NO_CLIPBOARD_AUTOSYNC:
opts->clipboard_autosync = false;
break;
case OPT_TCPIP:
opts->tcpip = true;
opts->tcpip_dst = optarg;
break;
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;
case OPT_NO_CLEANUP:
opts->cleanup = false;
break;
case OPT_NO_POWER_ON:
opts->power_on = false;
break;
case OPT_PRINT_FPS:
opts->start_fps_counter = true;
break;
case OPT_CODEC:
LOGE("--codec has been removed, "
"use --video-codec or --audio-codec.");
return false;
case OPT_VIDEO_CODEC:
if (!parse_video_codec(optarg, &opts->video_codec)) {
return false;
}
break;
case OPT_AUDIO_CODEC:
if (!parse_audio_codec(optarg, &opts->audio_codec)) {
return false;
}
break;
case OPT_OTG:
#ifdef HAVE_USB
opts->otg = true;
break;
#else
LOGE("OTG mode (--otg) is disabled.");
return false;
#endif
case OPT_V4L2_SINK:
#ifdef HAVE_V4L2
opts->v4l2_device = optarg;
break;
#else
LOGE("V4L2 (--v4l2-sink) is disabled (or unsupported on this "
"platform).");
return false;
#endif
case OPT_V4L2_BUFFER:
#ifdef HAVE_V4L2
if (!parse_buffering_time(optarg, &opts->v4l2_buffer)) {
return false;
}
break;
#else
LOGE("V4L2 (--v4l2-buffer) is disabled (or unsupported on this "
"platform).");
return false;
#endif
case OPT_LIST_ENCODERS:
opts->list |= SC_OPTION_LIST_ENCODERS;
break;
case OPT_LIST_DISPLAYS:
opts->list |= SC_OPTION_LIST_DISPLAYS;
break;
case OPT_LIST_CAMERAS:
opts->list |= SC_OPTION_LIST_CAMERAS;
break;
case OPT_LIST_CAMERA_SIZES:
opts->list |= SC_OPTION_LIST_CAMERA_SIZES;
break;
case OPT_REQUIRE_AUDIO:
opts->require_audio = true;
break;
case OPT_AUDIO_BUFFER:
if (!parse_buffering_time(optarg, &opts->audio_buffer)) {
return false;
}
break;
case OPT_AUDIO_OUTPUT_BUFFER:
if (!parse_audio_output_buffer(optarg,
&opts->audio_output_buffer)) {
return false;
}
break;
case OPT_VIDEO_SOURCE:
if (!parse_video_source(optarg, &opts->video_source)) {
return false;
}
break;
case OPT_AUDIO_SOURCE:
if (!parse_audio_source(optarg, &opts->audio_source)) {
return false;
}
break;
case OPT_KILL_ADB_ON_CLOSE:
opts->kill_adb_on_close = true;
break;
case OPT_TIME_LIMIT:
if (!parse_time_limit(optarg, &opts->time_limit)) {
return false;
}
break;
case OPT_PAUSE_ON_EXIT:
if (!parse_pause_on_exit(optarg, &args->pause_on_exit)) {
return false;
}
break;
case OPT_CAMERA_AR:
opts->camera_ar = optarg;
break;
case OPT_CAMERA_ID:
opts->camera_id = optarg;
break;
case OPT_CAMERA_SIZE:
opts->camera_size = optarg;
break;
case OPT_CAMERA_FACING:
if (!parse_camera_facing(optarg, &opts->camera_facing)) {
return false;
}
break;
case OPT_CAMERA_FPS:
if (!parse_camera_fps(optarg, &opts->camera_fps)) {
return false;
}
break;
case OPT_CAMERA_HIGH_SPEED:
opts->camera_high_speed = true;
break;
default:
// getopt prints the error message on stderr
return false;
}
}
int index = optind;
if (index < argc) {
LOGE("Unexpected additional argument: %s", argv[index]);
return false;
}
// If a TCP/IP address is provided, then tcpip must be enabled
assert(opts->tcpip || !opts->tcpip_dst);
unsigned selectors = !!opts->serial
+ !!opts->tcpip_dst
+ opts->select_tcpip
+ opts->select_usb;
if (selectors > 1) {
LOGE("At most one device selector option may be passed, among:\n"
" --serial (-s)\n"
" --select-usb (-d)\n"
" --select-tcpip (-e)\n"
" --tcpip=<addr> (with an argument)");
return false;
}
bool otg = false;
bool v4l2 = false;
#ifdef HAVE_USB
otg = opts->otg;
#endif
#ifdef HAVE_V4L2
v4l2 = !!opts->v4l2_device;
#endif
if (!opts->video) {
opts->video_playback = false;
// Do not power on the device on start if video capture is disabled
opts->power_on = false;
}
if (!opts->audio) {
opts->audio_playback = false;
}
if (opts->video && !opts->video_playback && !opts->record_filename
&& !v4l2) {
LOGI("No video playback, no recording, no V4L2 sink: video disabled");
opts->video = false;
}
if (opts->audio && !opts->audio_playback && !opts->record_filename) {
LOGI("No audio playback, no recording: audio disabled");
opts->audio = false;
}
if (!opts->video && !opts->audio && !otg) {
LOGE("No video, no audio, no OTG: nothing to do");
return false;
}
if (!opts->video && !otg) {
// If video is disabled, then scrcpy must exit on audio failure.
opts->require_audio = true;
}
if (opts->audio_playback && opts->audio_buffer == -1) {
if (opts->audio_codec == SC_CODEC_FLAC) {
// Use 50 ms audio buffer by default, but use a higher value for FLAC,
// which is not low latency (the default encoder produces blocks of
// 4096 samples, which represent ~85.333ms).
LOGI("FLAC audio: audio buffer increased to 120 ms (use "
"--audio-buffer to set a custom value)");
opts->audio_buffer = SC_TICK_FROM_MS(120);
} else {
opts->audio_buffer = SC_TICK_FROM_MS(50);
}
}
#ifdef HAVE_V4L2
if (v4l2) {
if (opts->lock_video_orientation ==
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
LOGI("Video orientation is locked for v4l2 sink. "
"See --lock-video-orientation.");
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
}
// V4L2 could not handle size change.
// Do not log because downsizing on error is the default behavior,
// not an explicit request from the user.
opts->downsize_on_error = false;
}
if (opts->v4l2_buffer && !opts->v4l2_device) {
LOGE("V4L2 buffer value without V4L2 sink\n");
return false;
}
#endif
if (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AUTO) {
opts->keyboard_input_mode = otg ? SC_KEYBOARD_INPUT_MODE_AOA
: SC_KEYBOARD_INPUT_MODE_SDK;
}
if (opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AUTO) {
opts->mouse_input_mode = otg ? SC_MOUSE_INPUT_MODE_AOA
: SC_MOUSE_INPUT_MODE_SDK;
}
if (otg) {
enum sc_keyboard_input_mode kmode = opts->keyboard_input_mode;
if (kmode != SC_KEYBOARD_INPUT_MODE_AOA
&& kmode != SC_KEYBOARD_INPUT_MODE_DISABLED) {
LOGE("In OTG mode, --keyboard only supports aoa or disabled.");
return false;
}
enum sc_mouse_input_mode mmode = opts->mouse_input_mode;
if (mmode != SC_MOUSE_INPUT_MODE_AOA
&& mmode != SC_MOUSE_INPUT_MODE_DISABLED) {
LOGE("In OTG mode, --mouse only supports aoa or disabled.");
return false;
}
if (kmode == SC_KEYBOARD_INPUT_MODE_DISABLED
&& mmode == SC_MOUSE_INPUT_MODE_DISABLED) {
LOGE("Could not disable both keyboard and mouse in OTG mode.");
return false;
}
}
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
LOGI("Tunnel host/port is set, "
"--force-adb-forward automatically enabled.");
opts->force_adb_forward = true;
}
if (opts->video_source == SC_VIDEO_SOURCE_CAMERA) {
if (opts->display_id) {
LOGE("--display-id is only available with --video-source=display");
return false;
}
if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) {
LOGE("Could not specify both --camera-id and --camera-facing");
return false;
}
if (opts->camera_size) {
if (opts->max_size) {
LOGE("Could not specify both --camera-size and -m/--max-size");
return false;
}
if (opts->camera_ar) {
LOGE("Could not specify both --camera-size and --camera-ar");
return false;
}
}
if (opts->camera_high_speed && !opts->camera_fps) {
LOGE("--camera-high-speed requires an explicit --camera-fps value");
return false;
}
if (opts->control) {
LOGI("Camera video source: control disabled");
opts->control = false;
}
} else if (opts->camera_id
|| opts->camera_ar
|| opts->camera_facing != SC_CAMERA_FACING_ANY
|| opts->camera_fps
|| opts->camera_high_speed
|| opts->camera_size) {
LOGE("Camera options are only available with --video-source=camera");
return false;
}
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
} else {
opts->audio_source = SC_AUDIO_SOURCE_MIC;
LOGI("Camera video source: microphone audio source selected");
}
}
if (opts->record_format && !opts->record_filename) {
LOGE("Record format specified without recording");
return false;
}
if (opts->record_filename) {
if (!opts->record_format) {
opts->record_format = guess_record_format(opts->record_filename);
if (!opts->record_format) {
LOGE("No format specified for \"%s\" "
"(try with --record-format=mkv)",
opts->record_filename);
return false;
}
}
if (opts->record_orientation != SC_ORIENTATION_0) {
if (sc_orientation_is_mirror(opts->record_orientation)) {
LOGE("Record orientation only supports rotation, not "
"flipping: %s",
sc_orientation_get_name(opts->record_orientation));
return false;
}
}
if (opts->video
&& sc_record_format_is_audio_only(opts->record_format)) {
LOGE("Audio container does not support video stream");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_OPUS
&& opts->audio_codec != SC_CODEC_OPUS) {
LOGE("Recording to OPUS file requires an OPUS audio stream "
"(try with --audio-codec=opus)");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_AAC
&& opts->audio_codec != SC_CODEC_AAC) {
LOGE("Recording to AAC file requires an AAC audio stream "
"(try with --audio-codec=aac)");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_FLAC
&& opts->audio_codec != SC_CODEC_FLAC) {
LOGE("Recording to FLAC file requires a FLAC audio stream "
"(try with --audio-codec=flac)");
return false;
}
if (opts->record_format == SC_RECORD_FORMAT_WAV
&& opts->audio_codec != SC_CODEC_RAW) {
LOGE("Recording to WAV file requires a RAW audio stream "
"(try with --audio-codec=raw)");
return false;
}
if ((opts->record_format == SC_RECORD_FORMAT_MP4 ||
opts->record_format == SC_RECORD_FORMAT_M4A)
&& opts->audio_codec == SC_CODEC_RAW) {
LOGE("Recording to MP4 container does not support RAW audio");
return false;
}
}
if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) {
LOGW("--audio-bit-rate is ignored for FLAC audio codec");
}
if (opts->audio_codec == SC_CODEC_RAW) {
if (opts->audio_bit_rate) {
LOGW("--audio-bit-rate is ignored for raw audio codec");
}
if (opts->audio_codec_options) {
LOGW("--audio-codec-options is ignored for raw audio codec");
}
if (opts->audio_encoder) {
LOGW("--audio-encoder is ignored for raw audio codec");
}
}
if (!opts->control) {
if (opts->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
return false;
}
if (opts->stay_awake) {
LOGE("Could not request to stay awake if control is disabled");
return false;
}
if (opts->show_touches) {
LOGE("Could not request to show touches if control is disabled");
return false;
}
if (opts->power_off_on_close) {
LOGE("Could not request power off on close if control is disabled");
return false;
}
}
# ifdef _WIN32
if (!otg && (opts->keyboard_input_mode == SC_KEYBOARD_INPUT_MODE_AOA
|| opts->mouse_input_mode == SC_MOUSE_INPUT_MODE_AOA)) {
LOGE("On Windows, it is not possible to open a USB device already open "
"by another process (like adb).");
LOGE("Therefore, --keyboard=aoa and --mouse=aoa may only work in OTG"
"mode (--otg).");
return false;
}
# endif
if (otg) {
// OTG mode is compatible with only very few options.
// Only report obvious errors.
if (opts->record_filename) {
LOGE("OTG mode: could not record");
return false;
}
if (opts->turn_screen_off) {
LOGE("OTG mode: could not turn screen off");
return false;
}
if (opts->stay_awake) {
LOGE("OTG mode: could not stay awake");
return false;
}
if (opts->show_touches) {
LOGE("OTG mode: could not request to show touches");
return false;
}
if (opts->power_off_on_close) {
LOGE("OTG mode: could not request power off on close");
return false;
}
if (opts->display_id) {
LOGE("OTG mode: could not select display");
return false;
}
if (v4l2) {
LOGE("OTG mode: could not sink to V4L2 device");
return false;
}
}
return true;
}
static enum sc_pause_on_exit
sc_get_pause_on_exit(int argc, char *argv[]) {
// Read arguments backwards so that the last --pause-on-exit is considered
// (same behavior as getopt())
for (int i = argc - 1; i >= 1; --i) {
const char *arg = argv[i];
// Starts with "--pause-on-exit"
if (!strncmp("--pause-on-exit", arg, 15)) {
if (arg[15] == '\0') {
// No argument
return SC_PAUSE_ON_EXIT_TRUE;
}
if (arg[15] != '=') {
// Invalid parameter, ignore
return SC_PAUSE_ON_EXIT_FALSE;
}
const char *value = &arg[16];
if (!strcmp(value, "true")) {
return SC_PAUSE_ON_EXIT_TRUE;
}
if (!strcmp(value, "if-error")) {
return SC_PAUSE_ON_EXIT_IF_ERROR;
}
// Set to false, inclusing when the value is invalid
return SC_PAUSE_ON_EXIT_FALSE;
}
}
return SC_PAUSE_ON_EXIT_FALSE;
}
bool
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]) {
struct sc_getopt_adapter adapter;
if (!sc_getopt_adapter_init(&adapter)) {
LOGW("Could not create getopt adapter");
return false;
}
bool ret = parse_args_with_getopt(args, argc, argv, adapter.optstring,
adapter.longopts);
sc_getopt_adapter_destroy(&adapter);
if (!ret && args->pause_on_exit == SC_PAUSE_ON_EXIT_FALSE) {
// Check if "--pause-on-exit" is present in the arguments list, because
// it must be taken into account even if command line parsing failed
args->pause_on_exit = sc_get_pause_on_exit(argc, argv);
}
return ret;
}