Add virtual display feature
Add a feature to create a new (separate) virtual display instead of mirroring the device screen: scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display # use the main display size and density scrcpy --new-display -m1920 # scaled to fit a max size of 1920 scrcpy --new-display=/240 # use the main display size and 240 dpi Fixes #1887 <https://github.com/Genymobile/scrcpy/issues/1887> PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370> Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> Co-authored-by: anirudhb <anirudhb@users.noreply.github.com>
This commit is contained in:
parent
5d0e012a4c
commit
98ed5eb643
@ -46,6 +46,8 @@ _scrcpy() {
|
|||||||
--mouse-bind=
|
--mouse-bind=
|
||||||
-n --no-control
|
-n --no-control
|
||||||
-N --no-playback
|
-N --no-playback
|
||||||
|
--new-display
|
||||||
|
--new-display=
|
||||||
--no-audio
|
--no-audio
|
||||||
--no-audio-playback
|
--no-audio-playback
|
||||||
--no-cleanup
|
--no-cleanup
|
||||||
|
@ -52,6 +52,7 @@ arguments=(
|
|||||||
'--mouse-bind=[Configure bindings of secondary clicks]'
|
'--mouse-bind=[Configure bindings of secondary clicks]'
|
||||||
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
|
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
|
||||||
{-N,--no-playback}'[Disable video and audio playback]'
|
{-N,--no-playback}'[Disable video and audio playback]'
|
||||||
|
'--new-display=[Create a new display]'
|
||||||
'--no-audio[Disable audio forwarding]'
|
'--no-audio[Disable audio forwarding]'
|
||||||
'--no-audio-playback[Disable audio playback]'
|
'--no-audio-playback[Disable audio playback]'
|
||||||
'--no-cleanup[Disable device cleanup actions on exit]'
|
'--no-cleanup[Disable device cleanup actions on exit]'
|
||||||
|
12
app/scrcpy.1
12
app/scrcpy.1
@ -314,6 +314,18 @@ Disable device control (mirror the device in read\-only).
|
|||||||
.B \-N, \-\-no\-playback
|
.B \-N, \-\-no\-playback
|
||||||
Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR).
|
Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]]
|
||||||
|
Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI, and \fB\-\-max\-size\fR is considered.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
\-\-new\-display=1920x1080
|
||||||
|
\-\-new\-display=1920x1080/420
|
||||||
|
\-\-new\-display # main display size and density
|
||||||
|
\-\-new\-display -m1920 # scaled to fit a max size of 1920
|
||||||
|
\-\-new\-display=/240 # main display size and 240 dpi
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.B \-\-no\-audio
|
.B \-\-no\-audio
|
||||||
Disable audio forwarding.
|
Disable audio forwarding.
|
||||||
|
@ -102,6 +102,7 @@ enum {
|
|||||||
OPT_NO_MOUSE_HOVER,
|
OPT_NO_MOUSE_HOVER,
|
||||||
OPT_AUDIO_DUP,
|
OPT_AUDIO_DUP,
|
||||||
OPT_GAMEPAD,
|
OPT_GAMEPAD,
|
||||||
|
OPT_NEW_DISPLAY,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_option {
|
struct sc_option {
|
||||||
@ -557,6 +558,21 @@ static const struct sc_option options[] = {
|
|||||||
.text = "Disable video and audio playback on the computer (equivalent "
|
.text = "Disable video and audio playback on the computer (equivalent "
|
||||||
"to --no-video-playback --no-audio-playback).",
|
"to --no-video-playback --no-audio-playback).",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_NEW_DISPLAY,
|
||||||
|
.longopt = "new-display",
|
||||||
|
.argdesc = "[<width>x<height>][/<dpi>]",
|
||||||
|
.optional_arg = true,
|
||||||
|
.text = "Create a new display with the specified resolution and "
|
||||||
|
"density. If not provided, they default to the main display "
|
||||||
|
"dimensions and DPI, and --max-size is considered.\n"
|
||||||
|
"Examples:\n"
|
||||||
|
" --new-display=1920x1080\n"
|
||||||
|
" --new-display=1920x1080/420 # force 420 dpi\n"
|
||||||
|
" --new-display # main display size and density\n"
|
||||||
|
" --new-display -m1920 # scaled to fit a max size of 1920\n"
|
||||||
|
" --new-display=/240 # main display size and 240 dpi",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_NO_AUDIO,
|
.longopt_id = OPT_NO_AUDIO,
|
||||||
.longopt = "no-audio",
|
.longopt = "no-audio",
|
||||||
@ -2668,6 +2684,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case OPT_NEW_DISPLAY:
|
||||||
|
opts->new_display = optarg ? optarg : "";
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
@ -2848,6 +2867,25 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts->new_display) {
|
||||||
|
if (opts->video_source != SC_VIDEO_SOURCE_DISPLAY) {
|
||||||
|
LOGE("--new-display is only available with --video-source=display");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!opts->video) {
|
||||||
|
LOGE("--new-display is incompatible with --no-video");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts->max_size && opts->new_display[0] != '\0'
|
||||||
|
&& opts->new_display[0] != '/') {
|
||||||
|
// An explicit size is defined (not "" nor "/<dpi>")
|
||||||
|
LOGE("Cannot specify both --new-display size and -m/--max-size");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (otg) {
|
if (otg) {
|
||||||
if (!opts->control) {
|
if (!opts->control) {
|
||||||
LOGE("--no-control is not allowed in OTG mode");
|
LOGE("--no-control is not allowed in OTG mode");
|
||||||
@ -2954,6 +2992,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts->display_id != 0 && opts->new_display) {
|
||||||
|
LOGE("Cannot specify both --display-id and --new-display");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
|
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
|
||||||
// Select the audio source according to the video source
|
// Select the audio source according to the video source
|
||||||
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
|
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
|
||||||
|
@ -103,6 +103,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.window = true,
|
.window = true,
|
||||||
.mouse_hover = true,
|
.mouse_hover = true,
|
||||||
.audio_dup = false,
|
.audio_dup = false,
|
||||||
|
.new_display = NULL,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_orientation
|
enum sc_orientation
|
||||||
|
@ -308,6 +308,7 @@ struct scrcpy_options {
|
|||||||
bool window;
|
bool window;
|
||||||
bool mouse_hover;
|
bool mouse_hover;
|
||||||
bool audio_dup;
|
bool audio_dup;
|
||||||
|
const char *new_display; // [<width>x<height>][/<dpi>] parsed by the server
|
||||||
};
|
};
|
||||||
|
|
||||||
extern const struct scrcpy_options scrcpy_options_default;
|
extern const struct scrcpy_options scrcpy_options_default;
|
||||||
|
@ -431,6 +431,7 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
.lock_video_orientation = options->lock_video_orientation,
|
.lock_video_orientation = options->lock_video_orientation,
|
||||||
.control = options->control,
|
.control = options->control,
|
||||||
.display_id = options->display_id,
|
.display_id = options->display_id,
|
||||||
|
.new_display = options->new_display,
|
||||||
.video = options->video,
|
.video = options->video,
|
||||||
.audio = options->audio,
|
.audio = options->audio,
|
||||||
.audio_dup = options->audio_dup,
|
.audio_dup = options->audio_dup,
|
||||||
|
@ -355,6 +355,10 @@ execute_server(struct sc_server *server,
|
|||||||
// By default, power_on is true
|
// By default, power_on is true
|
||||||
ADD_PARAM("power_on=false");
|
ADD_PARAM("power_on=false");
|
||||||
}
|
}
|
||||||
|
if (params->new_display) {
|
||||||
|
VALIDATE_STRING(params->new_display);
|
||||||
|
ADD_PARAM("new_display=%s", params->new_display);
|
||||||
|
}
|
||||||
if (params->list & SC_OPTION_LIST_ENCODERS) {
|
if (params->list & SC_OPTION_LIST_ENCODERS) {
|
||||||
ADD_PARAM("list_encoders=true");
|
ADD_PARAM("list_encoders=true");
|
||||||
}
|
}
|
||||||
|
@ -48,6 +48,7 @@ struct sc_server_params {
|
|||||||
int8_t lock_video_orientation;
|
int8_t lock_video_orientation;
|
||||||
bool control;
|
bool control;
|
||||||
uint32_t display_id;
|
uint32_t display_id;
|
||||||
|
const char *new_display;
|
||||||
bool video;
|
bool video;
|
||||||
bool audio;
|
bool audio;
|
||||||
bool audio_dup;
|
bool audio_dup;
|
||||||
|
@ -139,8 +139,10 @@ public final class CleanUp {
|
|||||||
|
|
||||||
if (Device.isScreenOn()) {
|
if (Device.isScreenOn()) {
|
||||||
if (powerOffScreen) {
|
if (powerOffScreen) {
|
||||||
Ln.i("Power off screen");
|
if (displayId != Device.DISPLAY_ID_NONE) {
|
||||||
Device.powerOffScreen(displayId);
|
Ln.i("Power off screen");
|
||||||
|
Device.powerOffScreen(displayId);
|
||||||
|
}
|
||||||
} else if (restoreNormalPowerMode) {
|
} else if (restoreNormalPowerMode) {
|
||||||
Ln.i("Restoring normal power mode");
|
Ln.i("Restoring normal power mode");
|
||||||
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
|
||||||
|
@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
|
|||||||
|
|
||||||
import com.genymobile.scrcpy.audio.AudioCodec;
|
import com.genymobile.scrcpy.audio.AudioCodec;
|
||||||
import com.genymobile.scrcpy.audio.AudioSource;
|
import com.genymobile.scrcpy.audio.AudioSource;
|
||||||
|
import com.genymobile.scrcpy.device.NewDisplay;
|
||||||
import com.genymobile.scrcpy.device.Size;
|
import com.genymobile.scrcpy.device.Size;
|
||||||
import com.genymobile.scrcpy.util.CodecOption;
|
import com.genymobile.scrcpy.util.CodecOption;
|
||||||
import com.genymobile.scrcpy.util.Ln;
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
@ -54,6 +55,8 @@ public class Options {
|
|||||||
private boolean cleanup = true;
|
private boolean cleanup = true;
|
||||||
private boolean powerOn = true;
|
private boolean powerOn = true;
|
||||||
|
|
||||||
|
private NewDisplay newDisplay;
|
||||||
|
|
||||||
private boolean listEncoders;
|
private boolean listEncoders;
|
||||||
private boolean listDisplays;
|
private boolean listDisplays;
|
||||||
private boolean listCameras;
|
private boolean listCameras;
|
||||||
@ -205,6 +208,10 @@ public class Options {
|
|||||||
return powerOn;
|
return powerOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public NewDisplay getNewDisplay() {
|
||||||
|
return newDisplay;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean getList() {
|
public boolean getList() {
|
||||||
return listEncoders || listDisplays || listCameras || listCameraSizes;
|
return listEncoders || listDisplays || listCameras || listCameraSizes;
|
||||||
}
|
}
|
||||||
@ -418,6 +425,9 @@ public class Options {
|
|||||||
case "camera_high_speed":
|
case "camera_high_speed":
|
||||||
options.cameraHighSpeed = Boolean.parseBoolean(value);
|
options.cameraHighSpeed = Boolean.parseBoolean(value);
|
||||||
break;
|
break;
|
||||||
|
case "new_display":
|
||||||
|
options.newDisplay = parseNewDisplay(value);
|
||||||
|
break;
|
||||||
case "send_device_meta":
|
case "send_device_meta":
|
||||||
options.sendDeviceMeta = Boolean.parseBoolean(value);
|
options.sendDeviceMeta = Boolean.parseBoolean(value);
|
||||||
break;
|
break;
|
||||||
@ -504,4 +514,36 @@ public class Options {
|
|||||||
throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\"");
|
throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\"");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static NewDisplay parseNewDisplay(String newDisplay) {
|
||||||
|
// Possible inputs:
|
||||||
|
// - "" (empty string)
|
||||||
|
// - "<width>x<height>/<dpi>"
|
||||||
|
// - "<width>x<height>"
|
||||||
|
// - "/<dpi>"
|
||||||
|
if (newDisplay.isEmpty()) {
|
||||||
|
return new NewDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] tokens = newDisplay.split("/");
|
||||||
|
|
||||||
|
Size size;
|
||||||
|
if (!tokens[0].isEmpty()) {
|
||||||
|
size = parseSize(tokens[0]);
|
||||||
|
} else {
|
||||||
|
size = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int dpi;
|
||||||
|
if (tokens.length >= 2) {
|
||||||
|
dpi = Integer.parseInt(tokens[1]);
|
||||||
|
if (dpi <= 0) {
|
||||||
|
throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dpi = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NewDisplay(size, dpi);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,12 +12,14 @@ import com.genymobile.scrcpy.control.Controller;
|
|||||||
import com.genymobile.scrcpy.device.ConfigurationException;
|
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||||
import com.genymobile.scrcpy.device.DesktopConnection;
|
import com.genymobile.scrcpy.device.DesktopConnection;
|
||||||
import com.genymobile.scrcpy.device.Device;
|
import com.genymobile.scrcpy.device.Device;
|
||||||
|
import com.genymobile.scrcpy.device.NewDisplay;
|
||||||
import com.genymobile.scrcpy.device.Streamer;
|
import com.genymobile.scrcpy.device.Streamer;
|
||||||
import com.genymobile.scrcpy.util.Ln;
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
import com.genymobile.scrcpy.util.LogUtils;
|
import com.genymobile.scrcpy.util.LogUtils;
|
||||||
import com.genymobile.scrcpy.util.Settings;
|
import com.genymobile.scrcpy.util.Settings;
|
||||||
import com.genymobile.scrcpy.util.SettingsException;
|
import com.genymobile.scrcpy.util.SettingsException;
|
||||||
import com.genymobile.scrcpy.video.CameraCapture;
|
import com.genymobile.scrcpy.video.CameraCapture;
|
||||||
|
import com.genymobile.scrcpy.video.NewDisplayCapture;
|
||||||
import com.genymobile.scrcpy.video.ScreenCapture;
|
import com.genymobile.scrcpy.video.ScreenCapture;
|
||||||
import com.genymobile.scrcpy.video.SurfaceCapture;
|
import com.genymobile.scrcpy.video.SurfaceCapture;
|
||||||
import com.genymobile.scrcpy.video.SurfaceEncoder;
|
import com.genymobile.scrcpy.video.SurfaceEncoder;
|
||||||
@ -128,8 +130,11 @@ public final class Server {
|
|||||||
CleanUp cleanUp = null;
|
CleanUp cleanUp = null;
|
||||||
Thread initThread = null;
|
Thread initThread = null;
|
||||||
|
|
||||||
|
NewDisplay newDisplay = options.getNewDisplay();
|
||||||
|
int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE;
|
||||||
|
|
||||||
if (options.getCleanup()) {
|
if (options.getCleanup()) {
|
||||||
cleanUp = CleanUp.configure(options.getDisplayId());
|
cleanUp = CleanUp.configure(displayId);
|
||||||
initThread = startInitThread(options, cleanUp);
|
initThread = startInitThread(options, cleanUp);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,7 +159,7 @@ public final class Server {
|
|||||||
|
|
||||||
if (control) {
|
if (control) {
|
||||||
ControlChannel controlChannel = connection.getControlChannel();
|
ControlChannel controlChannel = connection.getControlChannel();
|
||||||
controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
|
controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn());
|
||||||
asyncProcessors.add(controller);
|
asyncProcessors.add(controller);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -184,8 +189,13 @@ public final class Server {
|
|||||||
options.getSendFrameMeta());
|
options.getSendFrameMeta());
|
||||||
SurfaceCapture surfaceCapture;
|
SurfaceCapture surfaceCapture;
|
||||||
if (options.getVideoSource() == VideoSource.DISPLAY) {
|
if (options.getVideoSource() == VideoSource.DISPLAY) {
|
||||||
surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(),
|
if (newDisplay != null) {
|
||||||
options.getLockVideoOrientation());
|
surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize());
|
||||||
|
} else {
|
||||||
|
assert displayId != Device.DISPLAY_ID_NONE;
|
||||||
|
surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(),
|
||||||
|
options.getLockVideoOrientation());
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
|
surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
|
||||||
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
|
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
|
||||||
|
@ -40,6 +40,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
* In order to make events work correctly in all cases:
|
* In order to make events work correctly in all cases:
|
||||||
* - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates);
|
* - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates);
|
||||||
* - displayId must be used for other events (like key events).
|
* - displayId must be used for other events (like key events).
|
||||||
|
*
|
||||||
|
* If a new separate virtual display is created (using --new-display), then displayId == Device.DISPLAY_ID_NONE. In that case, all events are
|
||||||
|
* sent to the virtual display id.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
private static final class DisplayData {
|
private static final class DisplayData {
|
||||||
@ -151,7 +154,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
|
|
||||||
private void control() throws IOException {
|
private void control() throws IOException {
|
||||||
// on start, power on the device
|
// on start, power on the device
|
||||||
if (powerOn && !Device.isScreenOn()) {
|
if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) {
|
||||||
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
|
||||||
|
|
||||||
// dirty hack
|
// dirty hack
|
||||||
@ -270,7 +273,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_ROTATE_DEVICE:
|
case ControlMessage.TYPE_ROTATE_DEVICE:
|
||||||
Device.rotateDevice(displayId);
|
Device.rotateDevice(getActionDisplayId());
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_UHID_CREATE:
|
case ControlMessage.TYPE_UHID_CREATE:
|
||||||
getUhidManager().open(msg.getId(), msg.getText(), msg.getData());
|
getUhidManager().open(msg.getId(), msg.getText(), msg.getData());
|
||||||
@ -305,8 +308,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
if (events == null) {
|
if (events == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int actionDisplayId = getActionDisplayId();
|
||||||
for (KeyEvent event : events) {
|
for (KeyEvent event : events) {
|
||||||
if (!Device.injectEvent(event, displayId, Device.INJECT_MODE_ASYNC)) {
|
if (!Device.injectEvent(event, actionDisplayId, Device.INJECT_MODE_ASYNC)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -543,10 +548,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) {
|
private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) {
|
||||||
return Device.injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode);
|
return Device.injectKeyEvent(action, keyCode, repeat, metaState, getActionDisplayId(), injectMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean pressReleaseKeycode(int keyCode, int injectMode) {
|
private boolean pressReleaseKeycode(int keyCode, int injectMode) {
|
||||||
return Device.pressReleaseKeycode(keyCode, displayId, injectMode);
|
return Device.pressReleaseKeycode(keyCode, getActionDisplayId(), injectMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getActionDisplayId() {
|
||||||
|
if (displayId != Device.DISPLAY_ID_NONE) {
|
||||||
|
// Real screen mirrored, use the source display id
|
||||||
|
return displayId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Virtual display created by --new-display, use the virtualDisplayId
|
||||||
|
DisplayData data = displayData.get();
|
||||||
|
if (data == null) {
|
||||||
|
// If no virtual display id is initialized yet, use the main display id
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return data.virtualDisplayId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,8 @@ import android.view.KeyEvent;
|
|||||||
|
|
||||||
public final class Device {
|
public final class Device {
|
||||||
|
|
||||||
|
public static final int DISPLAY_ID_NONE = -1;
|
||||||
|
|
||||||
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
|
||||||
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
|
||||||
|
|
||||||
@ -159,6 +161,8 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean powerOffScreen(int displayId) {
|
public static boolean powerOffScreen(int displayId) {
|
||||||
|
assert displayId != DISPLAY_ID_NONE;
|
||||||
|
|
||||||
if (!isScreenOn()) {
|
if (!isScreenOn()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -169,6 +173,8 @@ public final class Device {
|
|||||||
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
|
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
|
||||||
*/
|
*/
|
||||||
public static void rotateDevice(int displayId) {
|
public static void rotateDevice(int displayId) {
|
||||||
|
assert displayId != DISPLAY_ID_NONE;
|
||||||
|
|
||||||
WindowManager wm = ServiceManager.getWindowManager();
|
WindowManager wm = ServiceManager.getWindowManager();
|
||||||
|
|
||||||
boolean accelerometerRotation = !wm.isRotationFrozen(displayId);
|
boolean accelerometerRotation = !wm.isRotationFrozen(displayId);
|
||||||
@ -187,6 +193,8 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static int getCurrentRotation(int displayId) {
|
private static int getCurrentRotation(int displayId) {
|
||||||
|
assert displayId != DISPLAY_ID_NONE;
|
||||||
|
|
||||||
if (displayId == 0) {
|
if (displayId == 0) {
|
||||||
return ServiceManager.getWindowManager().getRotation();
|
return ServiceManager.getWindowManager().getRotation();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,31 @@
|
|||||||
|
package com.genymobile.scrcpy.device;
|
||||||
|
|
||||||
|
public final class NewDisplay {
|
||||||
|
private Size size;
|
||||||
|
private int dpi;
|
||||||
|
|
||||||
|
public NewDisplay() {
|
||||||
|
// Auto size and dpi
|
||||||
|
}
|
||||||
|
|
||||||
|
public NewDisplay(Size size, int dpi) {
|
||||||
|
this.size = size;
|
||||||
|
this.dpi = dpi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Size getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getDpi() {
|
||||||
|
return dpi;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasExplicitSize() {
|
||||||
|
return size != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasExplicitDpi() {
|
||||||
|
return dpi != 0;
|
||||||
|
}
|
||||||
|
}
|
@ -21,6 +21,10 @@ public final class Size {
|
|||||||
return height;
|
return height;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getMax() {
|
||||||
|
return Math.max(width, height);
|
||||||
|
}
|
||||||
|
|
||||||
public Size rotate() {
|
public Size rotate() {
|
||||||
return new Size(height, width);
|
return new Size(height, width);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,146 @@
|
|||||||
|
package com.genymobile.scrcpy.video;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.AndroidVersions;
|
||||||
|
import com.genymobile.scrcpy.control.PositionMapper;
|
||||||
|
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||||
|
import com.genymobile.scrcpy.device.NewDisplay;
|
||||||
|
import com.genymobile.scrcpy.device.Size;
|
||||||
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
|
import android.graphics.Rect;
|
||||||
|
import android.hardware.display.DisplayManager;
|
||||||
|
import android.hardware.display.VirtualDisplay;
|
||||||
|
import android.os.Build;
|
||||||
|
import android.view.Surface;
|
||||||
|
|
||||||
|
public class NewDisplayCapture extends SurfaceCapture {
|
||||||
|
|
||||||
|
// Internal fields copied from android.hardware.display.DisplayManager
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 1 << 9;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1 << 10;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP = 1 << 11;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED = 1 << 12;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED = 1 << 13;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_OWN_FOCUS = 1 << 14;
|
||||||
|
private static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15;
|
||||||
|
|
||||||
|
private final VirtualDisplayListener vdListener;
|
||||||
|
private final NewDisplay newDisplay;
|
||||||
|
|
||||||
|
private Size mainDisplaySize;
|
||||||
|
private int mainDisplayDpi;
|
||||||
|
private int maxSize; // only used if newDisplay.getSize() != null
|
||||||
|
|
||||||
|
private VirtualDisplay virtualDisplay;
|
||||||
|
private Size size;
|
||||||
|
private int dpi;
|
||||||
|
|
||||||
|
public NewDisplayCapture(VirtualDisplayListener vdListener, NewDisplay newDisplay, int maxSize) {
|
||||||
|
this.vdListener = vdListener;
|
||||||
|
this.newDisplay = newDisplay;
|
||||||
|
this.maxSize = maxSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void init() {
|
||||||
|
size = newDisplay.getSize();
|
||||||
|
dpi = newDisplay.getDpi();
|
||||||
|
if (size == null || dpi == 0) {
|
||||||
|
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0);
|
||||||
|
if (displayInfo != null) {
|
||||||
|
mainDisplaySize = displayInfo.getSize();
|
||||||
|
mainDisplayDpi = displayInfo.getDpi();
|
||||||
|
} else {
|
||||||
|
Ln.w("Main display not found, fallback to 1920x1080 240dpi");
|
||||||
|
mainDisplaySize = new Size(1920, 1080);
|
||||||
|
mainDisplayDpi = 240;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void prepare() {
|
||||||
|
if (!newDisplay.hasExplicitSize()) {
|
||||||
|
size = ScreenInfo.computeVideoSize(mainDisplaySize.getWidth(), mainDisplaySize.getHeight(), maxSize);
|
||||||
|
}
|
||||||
|
if (!newDisplay.hasExplicitDpi()) {
|
||||||
|
dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void start(Surface surface) {
|
||||||
|
if (virtualDisplay != null) {
|
||||||
|
virtualDisplay.release();
|
||||||
|
virtualDisplay = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int virtualDisplayId;
|
||||||
|
try {
|
||||||
|
int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC
|
||||||
|
| DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
|
||||||
|
| VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH
|
||||||
|
| VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT
|
||||||
|
| VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL
|
||||||
|
| VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS;
|
||||||
|
if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) {
|
||||||
|
flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED
|
||||||
|
| VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
|
||||||
|
| VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
|
||||||
|
| VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED;
|
||||||
|
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
|
||||||
|
flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS
|
||||||
|
| VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
virtualDisplay = ServiceManager.getDisplayManager()
|
||||||
|
.createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags);
|
||||||
|
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||||
|
Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Ln.e("Could not create display", e);
|
||||||
|
throw new AssertionError("Could not create display");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vdListener != null) {
|
||||||
|
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||||
|
Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight());
|
||||||
|
PositionMapper positionMapper = new PositionMapper(size, contentRect, 0);
|
||||||
|
vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void release() {
|
||||||
|
if (virtualDisplay != null) {
|
||||||
|
virtualDisplay.release();
|
||||||
|
virtualDisplay = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized Size getSize() {
|
||||||
|
return size;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public synchronized boolean setMaxSize(int newMaxSize) {
|
||||||
|
if (newDisplay.hasExplicitSize()) {
|
||||||
|
// Cannot retry with a different size if the display size was explicitly provided
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
maxSize = newMaxSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int scaleDpi(Size initialSize, int initialDpi, Size size) {
|
||||||
|
int den = initialSize.getMax();
|
||||||
|
int num = size.getMax();
|
||||||
|
return initialDpi * num / den;
|
||||||
|
}
|
||||||
|
}
|
@ -90,7 +90,7 @@ public final class ScreenInfo {
|
|||||||
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
|
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Size computeVideoSize(int w, int h, int maxSize) {
|
public static Size computeVideoSize(int w, int h, int maxSize) {
|
||||||
// Compute the video size and the padding of the content inside this video.
|
// Compute the video size and the padding of the content inside this video.
|
||||||
// Principle:
|
// Principle:
|
||||||
// - scale down the great side of the screen to maxSize (if necessary);
|
// - scale down the great side of the screen to maxSize (if necessary);
|
||||||
|
@ -1,15 +1,18 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.FakeContext;
|
||||||
import com.genymobile.scrcpy.device.DisplayInfo;
|
import com.genymobile.scrcpy.device.DisplayInfo;
|
||||||
import com.genymobile.scrcpy.device.Size;
|
import com.genymobile.scrcpy.device.Size;
|
||||||
import com.genymobile.scrcpy.util.Command;
|
import com.genymobile.scrcpy.util.Command;
|
||||||
import com.genymobile.scrcpy.util.Ln;
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.content.Context;
|
||||||
import android.hardware.display.VirtualDisplay;
|
import android.hardware.display.VirtualDisplay;
|
||||||
import android.view.Display;
|
import android.view.Display;
|
||||||
import android.view.Surface;
|
import android.view.Surface;
|
||||||
|
|
||||||
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
@ -126,4 +129,12 @@ public final class DisplayManager {
|
|||||||
Method method = getCreateVirtualDisplayMethod();
|
Method method = getCreateVirtualDisplayMethod();
|
||||||
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
|
return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public VirtualDisplay createNewVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags) throws Exception {
|
||||||
|
Constructor<android.hardware.display.DisplayManager> ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor(
|
||||||
|
Context.class);
|
||||||
|
ctor.setAccessible(true);
|
||||||
|
android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get());
|
||||||
|
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user