diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index a954fd6c..c0bca144 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -2,6 +2,7 @@ _scrcpy() { local cur prev words cword local opts=" --always-on-top + --angle --audio-bit-rate= --audio-buffer= --audio-codec= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 154ddef0..552fd4b9 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -9,6 +9,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' + '--angle=[Rotate the video content by a custom angle, in degrees]' '--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 29b53d98..91e766d6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -19,6 +19,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP .B \-\-always\-on\-top Make scrcpy window always on top (above other windows). +.TP +.BI "\-\-angle " degrees +Rotate the video content by a custom angle, in degrees counter-clockwise. + .TP .BI "\-\-audio\-bit\-rate " value Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). diff --git a/app/src/cli.c b/app/src/cli.c index 7ecf054f..d6eb3881 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -108,6 +108,7 @@ enum { OPT_START_APP, OPT_SCREEN_OFF_TIMEOUT, OPT_CAPTURE_ORIENTATION, + OPT_ANGLE, }; struct sc_option { @@ -149,6 +150,13 @@ static const struct sc_option options[] = { .longopt = "always-on-top", .text = "Make scrcpy window always on top (above other windows).", }, + { + .longopt_id = OPT_ANGLE, + .longopt = "angle", + .argdesc = "degrees", + .text = "Rotate the video content by a custom angle, in degrees " + "counter-clockwise.", + }, { .longopt_id = OPT_AUDIO_BIT_RATE, .longopt = "audio-bit-rate", @@ -2691,6 +2699,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_ANGLE: + opts->angle = optarg; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 69f8f64d..adc7ba0c 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -107,6 +107,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_dup = false, .new_display = NULL, .start_app = NULL, + .angle = NULL, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 945fcdf7..0692276e 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -247,6 +247,7 @@ struct scrcpy_options { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server + const char *angle; // float to be parsed by the server enum sc_orientation capture_orientation; enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5528910a..48befb1d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -428,6 +428,7 @@ scrcpy(struct scrcpy_options *options) { .video_bit_rate = options->video_bit_rate, .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, + .angle = options->angle, .screen_off_timeout = options->screen_off_timeout, .capture_orientation = options->capture_orientation, .capture_orientation_lock = options->capture_orientation_lock, diff --git a/app/src/server.c b/app/src/server.c index 9c12500e..9c81a7f6 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -274,6 +274,10 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps); } + if (params->angle) { + VALIDATE_STRING(params->angle); + ADD_PARAM("angle=%s", params->angle); + } if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED || params->capture_orientation != SC_ORIENTATION_0) { if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) { diff --git a/app/src/server.h b/app/src/server.h index 20d998e9..9d46b354 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -45,6 +45,7 @@ struct sc_server_params { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server + const char *angle; // float to be parsed by the server sc_tick screen_off_timeout; enum sc_orientation capture_orientation; enum sc_orientation_lock capture_orientation_lock; diff --git a/doc/video.md b/doc/video.md index aab8dbe8..67804ba8 100644 --- a/doc/video.md +++ b/doc/video.md @@ -158,6 +158,17 @@ to the MP4 or MKV target file. Flipping is not supported, so only the 4 first values are allowed when recording. +## Angle + +To rotate the video content by a custom angle (in degrees, counter-clockwise): + +``` +scrcpy --angle=23 +``` + +The center of rotation is the center of the visible area (after cropping). + + ## Crop The device screen may be cropped to mirror only part of the screen. diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index e1b3b9af..6a59fbe7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -34,6 +34,7 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; + private float angle; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -127,6 +128,10 @@ public class Options { return maxFps; } + public float getAngle() { + return angle; + } + public boolean isTunnelForward() { return tunnelForward; } @@ -349,6 +354,9 @@ public class Options { case "max_fps": options.maxFps = parseFloat("max_fps", value); break; + case "angle": + options.angle = parseFloat("angle", value); + break; case "tunnel_forward": options.tunnelForward = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 2886966f..ebccd035 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -62,6 +62,7 @@ public class CameraCapture extends SurfaceCapture { private final boolean highSpeed; private final Rect crop; private final Orientation captureOrientation; + private final float angle; private String cameraId; private Size captureSize; @@ -88,6 +89,7 @@ public class CameraCapture extends SurfaceCapture { this.crop = options.getCrop(); this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -131,6 +133,8 @@ public class CameraCapture extends SurfaceCapture { filter.addOrientation(captureOrientation); } + filter.addAngle(angle); + transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 16c46edf..412eb850 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -56,6 +56,7 @@ public class NewDisplayCapture extends DisplayCapture { private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; + private final float angle; private VirtualDisplay virtualDisplay; private Size videoSize; @@ -74,6 +75,7 @@ public class NewDisplayCapture extends DisplayCapture { this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -126,6 +128,7 @@ public class NewDisplayCapture extends DisplayCapture { } filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); + filter.addAngle(angle); eventTransform = filter.getInverseTransform(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index ef82c87b..794040ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -38,6 +38,7 @@ public class ScreenCapture extends DisplayCapture { private final Rect crop; private Orientation.Lock captureOrientationLock; private Orientation captureOrientation; + private final float angle; private DisplayInfo displayInfo; private Size videoSize; @@ -68,6 +69,7 @@ public class ScreenCapture extends DisplayCapture { this.captureOrientation = options.getCaptureOrientation(); assert captureOrientationLock != null; assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -126,6 +128,7 @@ public class ScreenCapture extends DisplayCapture { boolean locked = captureOrientationLock != Orientation.Lock.Unlocked; filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation); + filter.addAngle(angle); transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 05170930..79e6fcbc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -89,4 +89,11 @@ public class VideoFilter { } addOrientation(captureOrientation); } + + public void addAngle(double ccwAngle) { + if (ccwAngle == 0) { + return; + } + transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform); + } }