diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 27448baf40..339a819a84 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -11,6 +11,7 @@ _scrcpy() { --audio-output-buffer= -b --video-bit-rate= --camera-id= + --camera-facing= --camera-size= --crop= -d --select-usb @@ -104,6 +105,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'output mic' -- "$cur")) return ;; + --camera-facing) + COMPREPLY=($(compgen -W 'front back external' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 58c3ccccf6..c92f0ac1fa 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -18,6 +18,7 @@ arguments=( '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--camera-id=[Specify the camera id to mirror]' + '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-size=[Specify an explicit camera capture size]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index b108d675de..d2fb3ad578 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -81,6 +81,12 @@ Specify the device camera id to mirror. The available camera ids can be listed by \-\-list\-cameras. +.TP +.BI "\-\-camera\-facing " facing +Select the device camera by its facing direction. + +Possible values are "front", "back" and "external". + .TP .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. diff --git a/app/src/cli.c b/app/src/cli.c index cac547302b..96b8c26dba 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -86,6 +86,7 @@ enum { OPT_LIST_CAMERA_SIZES, OPT_CAMERA_ID, OPT_CAMERA_SIZE, + OPT_CAMERA_FACING, }; struct sc_option { @@ -210,6 +211,13 @@ static const struct sc_option options[] = { "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_SIZE, .longopt = "camera-size", @@ -1700,6 +1708,34 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { 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_time_limit(const char *s, sc_tick *tick) { long value; @@ -2100,6 +2136,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_CAMERA_SIZE: opts->camera_size = optarg; break; + case OPT_CAMERA_FACING: + if (!parse_camera_facing(optarg, &opts->camera_facing)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2199,6 +2240,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], 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) { LOGE("Camera size must be specified by --camera-size"); return false; @@ -2208,7 +2254,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGI("Camera video source: control disabled"); opts->control = false; } - } else if (opts->camera_id || opts->camera_size) { + } else if (opts->camera_id + || opts->camera_facing != SC_CAMERA_FACING_ANY + || opts->camera_size) { LOGE("Camera options are only available with --video-source=camera"); return false; } diff --git a/app/src/options.c b/app/src/options.c index 96741a7d99..2adb432370 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -21,6 +21,7 @@ const struct scrcpy_options scrcpy_options_default = { .record_format = SC_RECORD_FORMAT_AUTO, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, + .camera_facing = SC_CAMERA_FACING_ANY, .port_range = { .first = DEFAULT_LOCAL_PORT_RANGE_FIRST, .last = DEFAULT_LOCAL_PORT_RANGE_LAST, diff --git a/app/src/options.h b/app/src/options.h index afc6aa494c..1e783bae0b 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -55,6 +55,13 @@ enum sc_audio_source { SC_AUDIO_SOURCE_MIC, }; +enum sc_camera_facing { + SC_CAMERA_FACING_ANY, + SC_CAMERA_FACING_FRONT, + SC_CAMERA_FACING_BACK, + SC_CAMERA_FACING_EXTERNAL, +}; + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts @@ -133,6 +140,7 @@ struct scrcpy_options { enum sc_record_format record_format; enum sc_keyboard_input_mode keyboard_input_mode; enum sc_mouse_input_mode mouse_input_mode; + enum sc_camera_facing camera_facing; struct sc_port_range port_range; uint32_t tunnel_host; uint16_t tunnel_port; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d51d573b97..54e794e05e 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -353,6 +353,7 @@ scrcpy(struct scrcpy_options *options) { .audio_codec = options->audio_codec, .video_source = options->video_source, .audio_source = options->audio_source, + .camera_facing = options->camera_facing, .crop = options->crop, .port_range = options->port_range, .tunnel_host = options->tunnel_host, diff --git a/app/src/server.c b/app/src/server.c index 81a371ae38..7f8a492682 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -183,6 +183,20 @@ sc_server_get_codec_name(enum sc_codec codec) { } } +static const char * +sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) { + switch (camera_facing) { + case SC_CAMERA_FACING_FRONT: + return "front"; + case SC_CAMERA_FACING_BACK: + return "back"; + case SC_CAMERA_FACING_EXTERNAL: + return "external"; + default: + return NULL; + } +} + static sc_pid execute_server(struct sc_server *server, const struct sc_server_params *params) { @@ -285,6 +299,10 @@ execute_server(struct sc_server *server, if (params->camera_size) { ADD_PARAM("camera_size=%s", params->camera_size); } + if (params->camera_facing != SC_CAMERA_FACING_ANY) { + ADD_PARAM("camera_facing=%s", + sc_server_get_camera_facing_name(params->camera_facing)); + } if (params->show_touches) { ADD_PARAM("show_touches=true"); } diff --git a/app/src/server.h b/app/src/server.h index 92c5f22ef7..786aea5c30 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -28,6 +28,7 @@ struct sc_server_params { enum sc_codec audio_codec; enum sc_video_source video_source; enum sc_audio_source audio_source; + enum sc_camera_facing camera_facing; const char *crop; const char *video_codec_options; const char *audio_codec_options; diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index 5aadbae722..949eb34394 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -6,6 +6,7 @@ import android.annotation.TargetApi; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCaptureSession; +import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraDevice; import android.hardware.camera2.CameraManager; import android.hardware.camera2.CaptureFailure; @@ -28,6 +29,7 @@ public class CameraCapture extends SurfaceCapture { private final String explicitCameraId; + private final CameraFacing cameraFacing; private final Size explicitSize; private HandlerThread cameraThread; @@ -37,8 +39,9 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, Size explicitSize) { + public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize) { this.explicitCameraId = explicitCameraId; + this.cameraFacing = cameraFacing; this.explicitSize = explicitSize; } @@ -50,7 +53,7 @@ public void init() throws IOException { cameraExecutor = new HandlerExecutor(cameraHandler); try { - String cameraId = selectCamera(explicitCameraId); + String cameraId = selectCamera(explicitCameraId, cameraFacing); if (cameraId == null) { throw new IOException("No matching camera found"); } @@ -62,7 +65,7 @@ public void init() throws IOException { } } - private static String selectCamera(String explicitCameraId) throws CameraAccessException { + private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException { if (explicitCameraId != null) { return explicitCameraId; } @@ -70,8 +73,22 @@ private static String selectCamera(String explicitCameraId) throws CameraAccessE CameraManager cameraManager = ServiceManager.getCameraManager(); String[] cameraIds = cameraManager.getCameraIdList(); - // Use the first one - return cameraIds.length > 0 ? cameraIds[0] : null; + if (cameraFacing == null) { + // Use the first one + return cameraIds.length > 0 ? cameraIds[0] : null; + } + + for (String cameraId : cameraIds) { + CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId); + + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); + if (cameraFacing.value() == facing) { + return cameraId; + } + } + + // Not found + return null; } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java b/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java new file mode 100644 index 0000000000..b7e8daa5e9 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/CameraFacing.java @@ -0,0 +1,33 @@ +package com.genymobile.scrcpy; + +import android.annotation.SuppressLint; +import android.hardware.camera2.CameraCharacteristics; + +public enum CameraFacing { + FRONT("front", CameraCharacteristics.LENS_FACING_FRONT), + BACK("back", CameraCharacteristics.LENS_FACING_BACK), + @SuppressLint("InlinedApi") // introduced in API 23 + EXTERNAL("external", CameraCharacteristics.LENS_FACING_EXTERNAL); + + private final String name; + private final int value; + + CameraFacing(String name, int value) { + this.name = name; + this.value = value; + } + + int value() { + return value; + } + + static CameraFacing findByName(String name) { + for (CameraFacing facing : CameraFacing.values()) { + if (name.equals(facing.name)) { + return facing; + } + } + + return null; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 2366f9c88f..11af3cca7e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -26,6 +26,7 @@ public class Options { private int displayId; private String cameraId; private Size cameraSize; + private CameraFacing cameraFacing; private boolean showTouches; private boolean stayAwake; private List videoCodecOptions; @@ -126,6 +127,10 @@ public Size getCameraSize() { return cameraSize; } + public CameraFacing getCameraFacing() { + return cameraFacing; + } + public boolean getShowTouches() { return showTouches; } @@ -360,6 +365,15 @@ public static Options parse(String... args) { options.cameraSize = parseSize(value); } break; + case "camera_facing": + if (!value.isEmpty()) { + CameraFacing facing = CameraFacing.findByName(value); + if (facing == null) { + throw new IllegalArgumentException("Camera facing " + value + " not supported"); + } + options.cameraFacing = facing; + } + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index e43b9f0a63..1a93323a8f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -137,7 +137,7 @@ private static void scrcpy(Options options) throws IOException, ConfigurationExc if (options.getVideoSource() == VideoSource.DISPLAY) { surfaceCapture = new ScreenCapture(device); } else { - surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraSize()); + surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize()); } SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());