Compare commits
36 Commits
display_ch
...
video_buff
Author | SHA1 | Date | |
---|---|---|---|
c29ecd0314 | |||
d62fa8880e | |||
1f6634ea87 | |||
58ba00fa06 | |||
569c37cec1 | |||
58a0fbbf2e | |||
acff5b005c | |||
5474ae6bd6 | |||
2c25fd7a80 | |||
ce21f515e3 | |||
381fe95867 | |||
566b5be0f6 | |||
dd20efa41c | |||
13ce277e1f | |||
9c9d92fb1c | |||
408a388fc5 | |||
98ed5eb643 | |||
5d0e012a4c | |||
d19396718e | |||
7024d38199 | |||
f1368d9a8f | |||
d916429566 | |||
7cfefae5e1 | |||
b60e174780 | |||
5851b62580 | |||
12d5ca4d5e | |||
68e54d9b0b | |||
5f0480c039 | |||
874eaec487 | |||
14e5439dee | |||
a5844e198e | |||
2687d20280 | |||
9c0a328498 | |||
02ef3d57ce | |||
538a32a539 | |||
9578aae34e |
147
.github/workflows/release.yml
vendored
Normal file
147
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
name:
|
||||||
|
description: 'Version name (default is ref name)'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-scrcpy-server:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GRADLE: gradle # use native gradle instead of ./gradlew in release.mk
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup JDK
|
||||||
|
uses: actions/setup-java@v4
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: '17'
|
||||||
|
|
||||||
|
- name: Test scrcpy-server
|
||||||
|
run: make -f release.mk test-server
|
||||||
|
|
||||||
|
- name: Build scrcpy-server
|
||||||
|
run: make -f release.mk build-server
|
||||||
|
|
||||||
|
- name: Upload scrcpy-server artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: scrcpy-server
|
||||||
|
path: build-server/server/scrcpy-server
|
||||||
|
|
||||||
|
test-client:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||||
|
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||||
|
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
meson setup d -Db_sanitize=address,undefined
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: |
|
||||||
|
meson test -Cd
|
||||||
|
|
||||||
|
build-win32:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||||
|
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||||
|
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||||
|
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
||||||
|
|
||||||
|
- name: Workaround for old meson version run by Github Actions
|
||||||
|
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
|
||||||
|
|
||||||
|
- name: Build scrcpy win32
|
||||||
|
run: make -f release.mk build-win32
|
||||||
|
|
||||||
|
- name: Upload build-win32 artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-win32-intermediate
|
||||||
|
path: build-win32/dist/
|
||||||
|
|
||||||
|
build-win64:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
||||||
|
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
||||||
|
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
||||||
|
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
||||||
|
|
||||||
|
- name: Workaround for old meson version run by Github Actions
|
||||||
|
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
|
||||||
|
|
||||||
|
- name: Build scrcpy win64
|
||||||
|
run: make -f release.mk build-win64
|
||||||
|
|
||||||
|
- name: Upload build-win64 artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-win64-intermediate
|
||||||
|
path: build-win64/dist/
|
||||||
|
|
||||||
|
package:
|
||||||
|
needs:
|
||||||
|
- build-scrcpy-server
|
||||||
|
- build-win32
|
||||||
|
- build-win64
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# $VERSION is used by release.mk
|
||||||
|
VERSION: ${{ github.event.inputs.name || github.ref_name }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download scrcpy-server
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: scrcpy-server
|
||||||
|
path: build-server/server/
|
||||||
|
|
||||||
|
- name: Download build-win32
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-win32-intermediate
|
||||||
|
path: build-win32/dist/
|
||||||
|
|
||||||
|
- name: Download build-win64
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: build-win64-intermediate
|
||||||
|
path: build-win64/dist/
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: make -f release.mk package
|
||||||
|
|
||||||
|
- name: Upload release artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: scrcpy-release-${{ env.VERSION }}
|
||||||
|
path: release-${{ env.VERSION }}
|
@ -31,6 +31,7 @@ It focuses on:
|
|||||||
Its features include:
|
Its features include:
|
||||||
- [audio forwarding](doc/audio.md) (Android 11+)
|
- [audio forwarding](doc/audio.md) (Android 11+)
|
||||||
- [recording](doc/recording.md)
|
- [recording](doc/recording.md)
|
||||||
|
- [virtual display](doc/virtual_display.md)
|
||||||
- mirroring with [Android device screen off](doc/device.md#turn-screen-off)
|
- mirroring with [Android device screen off](doc/device.md#turn-screen-off)
|
||||||
- [copy-paste](doc/control.md#copy-paste) in both directions
|
- [copy-paste](doc/control.md#copy-paste) in both directions
|
||||||
- [configurable quality](doc/video.md)
|
- [configurable quality](doc/video.md)
|
||||||
@ -91,6 +92,12 @@ Here are just some common examples.
|
|||||||
scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version
|
scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Start VLC in a new virtual display (separate from the device display):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
|
||||||
|
```
|
||||||
|
|
||||||
- Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4
|
- Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4
|
||||||
file:
|
file:
|
||||||
|
|
||||||
@ -134,6 +141,7 @@ documented in the following pages:
|
|||||||
- [Device](doc/device.md)
|
- [Device](doc/device.md)
|
||||||
- [Window](doc/window.md)
|
- [Window](doc/window.md)
|
||||||
- [Recording](doc/recording.md)
|
- [Recording](doc/recording.md)
|
||||||
|
- [Virtual display](doc/virtual_displays.md)
|
||||||
- [Tunnels](doc/tunnels.md)
|
- [Tunnels](doc/tunnels.md)
|
||||||
- [OTG](doc/otg.md)
|
- [OTG](doc/otg.md)
|
||||||
- [Camera](doc/camera.md)
|
- [Camera](doc/camera.md)
|
||||||
|
@ -20,7 +20,6 @@ _scrcpy() {
|
|||||||
--crop=
|
--crop=
|
||||||
-d --select-usb
|
-d --select-usb
|
||||||
--disable-screensaver
|
--disable-screensaver
|
||||||
--display-buffer=
|
|
||||||
--display-id=
|
--display-id=
|
||||||
--display-orientation=
|
--display-orientation=
|
||||||
-e --select-tcpip
|
-e --select-tcpip
|
||||||
@ -90,6 +89,7 @@ _scrcpy() {
|
|||||||
--v4l2-sink=
|
--v4l2-sink=
|
||||||
-v --version
|
-v --version
|
||||||
-V --verbosity=
|
-V --verbosity=
|
||||||
|
--video-buffer=
|
||||||
--video-codec=
|
--video-codec=
|
||||||
--video-codec-options=
|
--video-codec-options=
|
||||||
--video-encoder=
|
--video-encoder=
|
||||||
@ -191,7 +191,6 @@ _scrcpy() {
|
|||||||
|--camera-size \
|
|--camera-size \
|
||||||
|--crop \
|
|--crop \
|
||||||
|--display-id \
|
|--display-id \
|
||||||
|--display-buffer \
|
|
||||||
|--max-fps \
|
|--max-fps \
|
||||||
|-m|--max-size \
|
|-m|--max-size \
|
||||||
|-p|--port \
|
|-p|--port \
|
||||||
@ -201,6 +200,7 @@ _scrcpy() {
|
|||||||
|--tunnel-port \
|
|--tunnel-port \
|
||||||
|--v4l2-buffer \
|
|--v4l2-buffer \
|
||||||
|--v4l2-sink \
|
|--v4l2-sink \
|
||||||
|
|--video-buffer \
|
||||||
|--video-codec-options \
|
|--video-codec-options \
|
||||||
|--video-encoder \
|
|--video-encoder \
|
||||||
|--tcpip \
|
|--tcpip \
|
||||||
|
@ -27,7 +27,6 @@ arguments=(
|
|||||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
||||||
{-d,--select-usb}'[Use USB device]'
|
{-d,--select-usb}'[Use USB device]'
|
||||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
||||||
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
|
|
||||||
'--display-id=[Specify the display id to mirror]'
|
'--display-id=[Specify the display id to mirror]'
|
||||||
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
||||||
{-e,--select-tcpip}'[Use TCP/IP device]'
|
{-e,--select-tcpip}'[Use TCP/IP device]'
|
||||||
@ -92,6 +91,7 @@ arguments=(
|
|||||||
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
|
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
|
||||||
{-v,--version}'[Print the version of scrcpy]'
|
{-v,--version}'[Print the version of scrcpy]'
|
||||||
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
|
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
|
||||||
|
'--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]'
|
||||||
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
|
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
|
||||||
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
|
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
|
||||||
'--video-encoder=[Use a specific MediaCodec video encoder]'
|
'--video-encoder=[Use a specific MediaCodec video encoder]'
|
||||||
|
23
app/scrcpy.1
23
app/scrcpy.1
@ -139,12 +139,6 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
|
|||||||
.BI "\-\-disable\-screensaver"
|
.BI "\-\-disable\-screensaver"
|
||||||
Disable screensaver while scrcpy is running.
|
Disable screensaver while scrcpy is running.
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-display\-buffer " ms
|
|
||||||
Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
|
|
||||||
|
|
||||||
Default is 0 (no buffering).
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-display\-id " id
|
.BI "\-\-display\-id " id
|
||||||
Specify the device display id to mirror.
|
Specify the device display id to mirror.
|
||||||
@ -326,8 +320,9 @@ Examples:
|
|||||||
|
|
||||||
\-\-new\-display=1920x1080
|
\-\-new\-display=1920x1080
|
||||||
\-\-new\-display=1920x1080/420
|
\-\-new\-display=1920x1080/420
|
||||||
\-\-new\-display # default screen size and density
|
\-\-new\-display # main display size and density
|
||||||
\-\-new\-display=240 # default screen size and 240 dpi
|
\-\-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
|
||||||
@ -497,7 +492,7 @@ Default is "lalt,lsuper" (left-Alt or left-Super).
|
|||||||
.BI "\-\-start\-app " name
|
.BI "\-\-start\-app " name
|
||||||
Start an Android app, by its exact package name.
|
Start an Android app, by its exact package name.
|
||||||
|
|
||||||
Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (it may take some time to retrieve the app names on the device):
|
Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time):
|
||||||
|
|
||||||
scrcpy --start-app=?firefox
|
scrcpy --start-app=?firefox
|
||||||
|
|
||||||
@ -559,7 +554,15 @@ It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\f
|
|||||||
.BI "\-\-v4l2-buffer " ms
|
.BI "\-\-v4l2-buffer " ms
|
||||||
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
|
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
|
||||||
|
|
||||||
This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
|
This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink.
|
||||||
|
|
||||||
|
Default is 0 (no buffering).
|
||||||
|
|
||||||
|
.TP
|
||||||
|
.BI "\-\-video\-buffer " ms
|
||||||
|
Add a buffering delay (in milliseconds) before displaying video frames.
|
||||||
|
|
||||||
|
This increases latency to compensate for jitter.
|
||||||
|
|
||||||
Default is 0 (no buffering).
|
Default is 0 (no buffering).
|
||||||
|
|
||||||
|
@ -50,6 +50,7 @@ enum {
|
|||||||
OPT_POWER_OFF_ON_CLOSE,
|
OPT_POWER_OFF_ON_CLOSE,
|
||||||
OPT_V4L2_SINK,
|
OPT_V4L2_SINK,
|
||||||
OPT_DISPLAY_BUFFER,
|
OPT_DISPLAY_BUFFER,
|
||||||
|
OPT_VIDEO_BUFFER,
|
||||||
OPT_V4L2_BUFFER,
|
OPT_V4L2_BUFFER,
|
||||||
OPT_TUNNEL_HOST,
|
OPT_TUNNEL_HOST,
|
||||||
OPT_TUNNEL_PORT,
|
OPT_TUNNEL_PORT,
|
||||||
@ -321,12 +322,10 @@ static const struct sc_option options[] = {
|
|||||||
.argdesc = "id",
|
.argdesc = "id",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
// deprecated
|
||||||
.longopt_id = OPT_DISPLAY_BUFFER,
|
.longopt_id = OPT_DISPLAY_BUFFER,
|
||||||
.longopt = "display-buffer",
|
.longopt = "display-buffer",
|
||||||
.argdesc = "ms",
|
.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_id = OPT_DISPLAY_ID,
|
||||||
@ -576,8 +575,9 @@ static const struct sc_option options[] = {
|
|||||||
"Examples:\n"
|
"Examples:\n"
|
||||||
" --new-display=1920x1080\n"
|
" --new-display=1920x1080\n"
|
||||||
" --new-display=1920x1080/420 # force 420 dpi\n"
|
" --new-display=1920x1080/420 # force 420 dpi\n"
|
||||||
" --new-display # default screen size and density\n"
|
" --new-display # main display size and density\n"
|
||||||
" --new-display=240 # default screen size and 240 dpi",
|
" --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,
|
||||||
@ -812,8 +812,8 @@ static const struct sc_option options[] = {
|
|||||||
.argdesc = "name",
|
.argdesc = "name",
|
||||||
.text = "Start an Android app, by its exact package name.\n"
|
.text = "Start an Android app, by its exact package name.\n"
|
||||||
"Add a '?' prefix to select an app whose name starts with the "
|
"Add a '?' prefix to select an app whose name starts with the "
|
||||||
"given name, case-insensitive (it may take some time to "
|
"given name, case-insensitive (retrieving app names on the "
|
||||||
"retrieve the app names on the device):\n"
|
"device may take some time):\n"
|
||||||
" scrcpy --start-app=?firefox\n"
|
" scrcpy --start-app=?firefox\n"
|
||||||
"Add a '+' prefix to force-stop before starting the app:\n"
|
"Add a '+' prefix to force-stop before starting the app:\n"
|
||||||
" scrcpy --new-display --start-app=+org.mozilla.firefox\n"
|
" scrcpy --new-display --start-app=+org.mozilla.firefox\n"
|
||||||
@ -897,11 +897,20 @@ static const struct sc_option options[] = {
|
|||||||
.argdesc = "ms",
|
.argdesc = "ms",
|
||||||
.text = "Add a buffering delay (in milliseconds) before pushing "
|
.text = "Add a buffering delay (in milliseconds) before pushing "
|
||||||
"frames. This increases latency to compensate for jitter.\n"
|
"frames. This increases latency to compensate for jitter.\n"
|
||||||
"This option is similar to --display-buffer, but specific to "
|
"This option is similar to --video-buffer, but specific to "
|
||||||
"V4L2 sink.\n"
|
"V4L2 sink.\n"
|
||||||
"Default is 0 (no buffering).\n"
|
"Default is 0 (no buffering).\n"
|
||||||
"This option is only available on Linux.",
|
"This option is only available on Linux.",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
.longopt_id = OPT_VIDEO_BUFFER,
|
||||||
|
.longopt = "video-buffer",
|
||||||
|
.argdesc = "ms",
|
||||||
|
.text = "Add a buffering delay (in milliseconds) before displaying "
|
||||||
|
"video frames.\n"
|
||||||
|
"This increases latency to compensate for jitter.\n"
|
||||||
|
"Default is 0 (no buffering).",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_VIDEO_CODEC,
|
.longopt_id = OPT_VIDEO_CODEC,
|
||||||
.longopt = "video-codec",
|
.longopt = "video-codec",
|
||||||
@ -2548,7 +2557,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->power_off_on_close = true;
|
opts->power_off_on_close = true;
|
||||||
break;
|
break;
|
||||||
case OPT_DISPLAY_BUFFER:
|
case OPT_DISPLAY_BUFFER:
|
||||||
if (!parse_buffering_time(optarg, &opts->display_buffer)) {
|
LOGW("--display-buffer is deprecated, use --video-buffer "
|
||||||
|
"instead.");
|
||||||
|
// fall through
|
||||||
|
case OPT_VIDEO_BUFFER:
|
||||||
|
if (!parse_buffering_time(optarg, &opts->video_buffer)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -2708,7 +2721,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case OPT_NEW_DISPLAY:
|
case OPT_NEW_DISPLAY:
|
||||||
opts->new_display = optarg ? optarg : "auto";
|
opts->new_display = optarg ? optarg : "";
|
||||||
break;
|
break;
|
||||||
case OPT_START_APP:
|
case OPT_START_APP:
|
||||||
opts->start_app = optarg;
|
opts->start_app = optarg;
|
||||||
@ -2811,7 +2824,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||||
LOGI("Video orientation is locked for v4l2 sink. "
|
LOGI("Video orientation is locked for v4l2 sink. "
|
||||||
"See --lock-video-orientation.");
|
"See --lock-video-orientation.");
|
||||||
opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL;
|
opts->lock_video_orientation =
|
||||||
|
SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO;
|
||||||
}
|
}
|
||||||
|
|
||||||
// V4L2 could not handle size change.
|
// V4L2 could not handle size change.
|
||||||
@ -2821,7 +2835,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opts->v4l2_buffer && !opts->v4l2_device) {
|
if (opts->v4l2_buffer && !opts->v4l2_device) {
|
||||||
LOGE("V4L2 buffer value without V4L2 sink\n");
|
LOGE("V4L2 buffer value without V4L2 sink");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
@ -2840,8 +2854,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
if (otg) {
|
if (otg) {
|
||||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
|
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA;
|
||||||
} else if (!opts->video_playback) {
|
} else if (!opts->video_playback) {
|
||||||
LOGI("No video mirroring, mouse mode switched to UHID");
|
LOGI("No video mirroring, SDK mouse disabled");
|
||||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID;
|
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED;
|
||||||
} else {
|
} else {
|
||||||
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
|
opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK;
|
||||||
}
|
}
|
||||||
@ -2893,6 +2907,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");
|
||||||
@ -2963,11 +2996,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts->new_display) {
|
|
||||||
LOGE("--new-display is only available with --video-source=display");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) {
|
if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) {
|
||||||
LOGE("Cannot specify both --camera-id and --camera-facing");
|
LOGE("Cannot specify both --camera-id and --camera-facing");
|
||||||
return false;
|
return false;
|
||||||
|
@ -22,9 +22,6 @@
|
|||||||
#define MOTIONEVENT_ACTION_LABEL(value) \
|
#define MOTIONEVENT_ACTION_LABEL(value) \
|
||||||
ENUM_TO_LABEL(android_motionevent_action_labels, value)
|
ENUM_TO_LABEL(android_motionevent_action_labels, value)
|
||||||
|
|
||||||
#define SCREEN_POWER_MODE_LABEL(value) \
|
|
||||||
ENUM_TO_LABEL(screen_power_mode_labels, value)
|
|
||||||
|
|
||||||
static const char *const android_keyevent_action_labels[] = {
|
static const char *const android_keyevent_action_labels[] = {
|
||||||
"down",
|
"down",
|
||||||
"up",
|
"up",
|
||||||
@ -47,14 +44,6 @@ static const char *const android_motionevent_action_labels[] = {
|
|||||||
"btn-release",
|
"btn-release",
|
||||||
};
|
};
|
||||||
|
|
||||||
static const char *const screen_power_mode_labels[] = {
|
|
||||||
"off",
|
|
||||||
"doze",
|
|
||||||
"normal",
|
|
||||||
"doze-suspend",
|
|
||||||
"suspend",
|
|
||||||
};
|
|
||||||
|
|
||||||
static const char *const copy_key_labels[] = {
|
static const char *const copy_key_labels[] = {
|
||||||
"none",
|
"none",
|
||||||
"copy",
|
"copy",
|
||||||
@ -158,8 +147,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
|||||||
size_t len = write_string(&buf[10], msg->set_clipboard.text,
|
size_t len = write_string(&buf[10], msg->set_clipboard.text,
|
||||||
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
|
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
|
||||||
return 10 + len;
|
return 10 + len;
|
||||||
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
|
||||||
buf[1] = msg->set_screen_power_mode.mode;
|
buf[1] = msg->set_display_power.on;
|
||||||
return 2;
|
return 2;
|
||||||
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
|
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
|
||||||
sc_write16be(&buf[1], msg->uhid_create.id);
|
sc_write16be(&buf[1], msg->uhid_create.id);
|
||||||
@ -268,9 +257,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
|||||||
msg->set_clipboard.paste ? "paste" : "nopaste",
|
msg->set_clipboard.paste ? "paste" : "nopaste",
|
||||||
msg->set_clipboard.text);
|
msg->set_clipboard.text);
|
||||||
break;
|
break;
|
||||||
case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE:
|
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
|
||||||
LOG_CMSG("power mode %s",
|
LOG_CMSG("display power %s",
|
||||||
SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode));
|
msg->set_display_power.on ? "on" : "off");
|
||||||
break;
|
break;
|
||||||
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
LOG_CMSG("expand notification panel");
|
LOG_CMSG("expand notification panel");
|
||||||
|
@ -35,7 +35,7 @@ enum sc_control_msg_type {
|
|||||||
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
|
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
|
||||||
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
||||||
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
||||||
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
|
||||||
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
|
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
|
||||||
SC_CONTROL_MSG_TYPE_UHID_CREATE,
|
SC_CONTROL_MSG_TYPE_UHID_CREATE,
|
||||||
SC_CONTROL_MSG_TYPE_UHID_INPUT,
|
SC_CONTROL_MSG_TYPE_UHID_INPUT,
|
||||||
@ -44,12 +44,6 @@ enum sc_control_msg_type {
|
|||||||
SC_CONTROL_MSG_TYPE_START_APP,
|
SC_CONTROL_MSG_TYPE_START_APP,
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_screen_power_mode {
|
|
||||||
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
|
|
||||||
SC_SCREEN_POWER_MODE_OFF = 0,
|
|
||||||
SC_SCREEN_POWER_MODE_NORMAL = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum sc_copy_key {
|
enum sc_copy_key {
|
||||||
SC_COPY_KEY_NONE,
|
SC_COPY_KEY_NONE,
|
||||||
SC_COPY_KEY_COPY,
|
SC_COPY_KEY_COPY,
|
||||||
@ -95,8 +89,8 @@ struct sc_control_msg {
|
|||||||
bool paste;
|
bool paste;
|
||||||
} set_clipboard;
|
} set_clipboard;
|
||||||
struct {
|
struct {
|
||||||
enum sc_screen_power_mode mode;
|
bool on;
|
||||||
} set_screen_power_mode;
|
} set_display_power;
|
||||||
struct {
|
struct {
|
||||||
uint16_t id;
|
uint16_t id;
|
||||||
const char *name; // pointer to static data
|
const char *name; // pointer to static data
|
||||||
|
@ -203,13 +203,12 @@ set_device_clipboard(struct sc_input_manager *im, bool paste,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
set_screen_power_mode(struct sc_input_manager *im,
|
set_display_power(struct sc_input_manager *im, bool on) {
|
||||||
enum sc_screen_power_mode mode) {
|
|
||||||
assert(im->controller);
|
assert(im->controller);
|
||||||
|
|
||||||
struct sc_control_msg msg;
|
struct sc_control_msg msg;
|
||||||
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
|
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
|
||||||
msg.set_screen_power_mode.mode = mode;
|
msg.set_display_power.on = on;
|
||||||
|
|
||||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||||
LOGW("Could not request 'set screen power mode'");
|
LOGW("Could not request 'set screen power mode'");
|
||||||
@ -415,10 +414,8 @@ sc_input_manager_process_key(struct sc_input_manager *im,
|
|||||||
return;
|
return;
|
||||||
case SDLK_o:
|
case SDLK_o:
|
||||||
if (control && !repeat && down && !paused) {
|
if (control && !repeat && down && !paused) {
|
||||||
enum sc_screen_power_mode mode = shift
|
bool on = shift;
|
||||||
? SC_SCREEN_POWER_MODE_NORMAL
|
set_display_power(im, on);
|
||||||
: SC_SCREEN_POWER_MODE_OFF;
|
|
||||||
set_screen_power_mode(im, mode);
|
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
case SDLK_z:
|
case SDLK_z:
|
||||||
|
@ -58,7 +58,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.window_width = 0,
|
.window_width = 0,
|
||||||
.window_height = 0,
|
.window_height = 0,
|
||||||
.display_id = 0,
|
.display_id = 0,
|
||||||
.display_buffer = 0,
|
.video_buffer = 0,
|
||||||
.audio_buffer = -1, // depends on the audio format,
|
.audio_buffer = -1, // depends on the audio format,
|
||||||
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
||||||
.time_limit = 0,
|
.time_limit = 0,
|
||||||
|
@ -134,6 +134,8 @@ enum sc_lock_video_orientation {
|
|||||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
||||||
// lock the current orientation when scrcpy starts
|
// lock the current orientation when scrcpy starts
|
||||||
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
|
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
|
||||||
|
// like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically
|
||||||
|
SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3,
|
||||||
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
|
SC_LOCK_VIDEO_ORIENTATION_0 = 0,
|
||||||
SC_LOCK_VIDEO_ORIENTATION_90 = 3,
|
SC_LOCK_VIDEO_ORIENTATION_90 = 3,
|
||||||
SC_LOCK_VIDEO_ORIENTATION_180 = 2,
|
SC_LOCK_VIDEO_ORIENTATION_180 = 2,
|
||||||
@ -259,7 +261,7 @@ struct scrcpy_options {
|
|||||||
uint16_t window_width;
|
uint16_t window_width;
|
||||||
uint16_t window_height;
|
uint16_t window_height;
|
||||||
uint32_t display_id;
|
uint32_t display_id;
|
||||||
sc_tick display_buffer;
|
sc_tick video_buffer;
|
||||||
sc_tick audio_buffer;
|
sc_tick audio_buffer;
|
||||||
sc_tick audio_output_buffer;
|
sc_tick audio_output_buffer;
|
||||||
sc_tick time_limit;
|
sc_tick time_limit;
|
||||||
|
@ -53,7 +53,7 @@ struct scrcpy {
|
|||||||
struct sc_decoder video_decoder;
|
struct sc_decoder video_decoder;
|
||||||
struct sc_decoder audio_decoder;
|
struct sc_decoder audio_decoder;
|
||||||
struct sc_recorder recorder;
|
struct sc_recorder recorder;
|
||||||
struct sc_delay_buffer display_buffer;
|
struct sc_delay_buffer video_buffer;
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
struct sc_v4l2_sink v4l2_sink;
|
struct sc_v4l2_sink v4l2_sink;
|
||||||
struct sc_delay_buffer v4l2_buffer;
|
struct sc_delay_buffer v4l2_buffer;
|
||||||
@ -815,11 +815,11 @@ aoa_complete:
|
|||||||
|
|
||||||
if (options->video_playback) {
|
if (options->video_playback) {
|
||||||
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
||||||
if (options->display_buffer) {
|
if (options->video_buffer) {
|
||||||
sc_delay_buffer_init(&s->display_buffer,
|
sc_delay_buffer_init(&s->video_buffer,
|
||||||
options->display_buffer, true);
|
options->video_buffer, true);
|
||||||
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
|
sc_frame_source_add_sink(src, &s->video_buffer.frame_sink);
|
||||||
src = &s->display_buffer.frame_source;
|
src = &s->video_buffer.frame_source;
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
||||||
@ -873,11 +873,11 @@ aoa_complete:
|
|||||||
// everything is set up
|
// everything is set up
|
||||||
if (options->control && options->turn_screen_off) {
|
if (options->control && options->turn_screen_off) {
|
||||||
struct sc_control_msg msg;
|
struct sc_control_msg msg;
|
||||||
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
|
msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER;
|
||||||
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
|
msg.set_display_power.on = false;
|
||||||
|
|
||||||
if (!sc_controller_push_msg(&s->controller, &msg)) {
|
if (!sc_controller_push_msg(&s->controller, &msg)) {
|
||||||
LOGW("Could not request 'set screen power mode'");
|
LOGW("Could not request 'set display power'");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -289,11 +289,11 @@ static void test_serialize_set_clipboard_long(void) {
|
|||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||||
}
|
}
|
||||||
|
|
||||||
static void test_serialize_set_screen_power_mode(void) {
|
static void test_serialize_set_display_power(void) {
|
||||||
struct sc_control_msg msg = {
|
struct sc_control_msg msg = {
|
||||||
.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
|
||||||
.set_screen_power_mode = {
|
.set_display_power = {
|
||||||
.mode = SC_SCREEN_POWER_MODE_NORMAL,
|
.on = true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -302,8 +302,8 @@ static void test_serialize_set_screen_power_mode(void) {
|
|||||||
assert(size == 2);
|
assert(size == 2);
|
||||||
|
|
||||||
const uint8_t expected[] = {
|
const uint8_t expected[] = {
|
||||||
SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE,
|
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
|
||||||
0x02, // SC_SCREEN_POWER_MODE_NORMAL
|
0x01, // true
|
||||||
};
|
};
|
||||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||||
}
|
}
|
||||||
@ -423,7 +423,7 @@ int main(int argc, char *argv[]) {
|
|||||||
test_serialize_get_clipboard();
|
test_serialize_get_clipboard();
|
||||||
test_serialize_set_clipboard();
|
test_serialize_set_clipboard();
|
||||||
test_serialize_set_clipboard_long();
|
test_serialize_set_clipboard_long();
|
||||||
test_serialize_set_screen_power_mode();
|
test_serialize_set_display_power();
|
||||||
test_serialize_rotate_device();
|
test_serialize_rotate_device();
|
||||||
test_serialize_uhid_create();
|
test_serialize_uhid_create();
|
||||||
test_serialize_uhid_input();
|
test_serialize_uhid_input();
|
||||||
|
@ -170,7 +170,7 @@ latency (for both [video](video.md#buffering) and audio) might be preferable to
|
|||||||
avoid glitches and smooth the playback:
|
avoid glitches and smooth the playback:
|
||||||
|
|
||||||
```
|
```
|
||||||
scrcpy --display-buffer=200 --audio-buffer=200
|
scrcpy --video-buffer=200 --audio-buffer=200
|
||||||
```
|
```
|
||||||
|
|
||||||
It is also possible to configure another audio buffer (the audio output buffer),
|
It is also possible to configure another audio buffer (the audio output buffer),
|
||||||
|
@ -21,9 +21,9 @@ the client and on the server.
|
|||||||
If video is enabled, then the server sends a raw video stream (H.264 by default)
|
If video is enabled, then the server sends a raw video stream (H.264 by default)
|
||||||
of the device screen, with some additional headers for each packet. The client
|
of the device screen, with some additional headers for each packet. The client
|
||||||
decodes the video frames, and displays them as soon as possible, without
|
decodes the video frames, and displays them as soon as possible, without
|
||||||
buffering (unless `--display-buffer=delay` is specified) to minimize latency.
|
buffering (unless `--video-buffer=delay` is specified) to minimize latency. The
|
||||||
The client is not aware of the device rotation (which is handled by the server),
|
client is not aware of the device rotation (which is handled by the server), it
|
||||||
it just knows the dimensions of the video frames it receives.
|
just knows the dimensions of the video frames it receives.
|
||||||
|
|
||||||
Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS
|
Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS
|
||||||
by default) of the device audio output (or the microphone if
|
by default) of the device audio output (or the microphone if
|
||||||
|
@ -18,6 +18,21 @@ The initial state is restored when _scrcpy_ is closed.
|
|||||||
If the device is not plugged in (i.e. only connected over TCP/IP),
|
If the device is not plugged in (i.e. only connected over TCP/IP),
|
||||||
`--stay-awake` has no effect (this is the Android behavior).
|
`--stay-awake` has no effect (this is the Android behavior).
|
||||||
|
|
||||||
|
This changes the value of [`stay_on_while_plugged_in`], setting which can be
|
||||||
|
changed manually:
|
||||||
|
|
||||||
|
[`stay_on_while_plugged_in`]: https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN
|
||||||
|
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# get the current show_touches value
|
||||||
|
adb shell settings get global stay_on_while_plugged_in
|
||||||
|
# enable for AC/USB/wireless chargers
|
||||||
|
adb shell settings put global stay_on_while_plugged_in 7
|
||||||
|
# disable
|
||||||
|
adb shell settings put global stay_on_while_plugged_in 0
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Turn screen off
|
## Turn screen off
|
||||||
|
|
||||||
@ -46,6 +61,15 @@ scrcpy --turn-screen-off --stay-awake
|
|||||||
scrcpy -Sw # short version
|
scrcpy -Sw # short version
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Since Android 15, it is possible to change this setting manually:
|
||||||
|
|
||||||
|
```
|
||||||
|
# turn screen off (0 for main display)
|
||||||
|
adb shell cmd display power-off 0
|
||||||
|
# turn screen on
|
||||||
|
adb shell cmd display power-on 0
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Show touches
|
## Show touches
|
||||||
|
|
||||||
@ -62,6 +86,16 @@ scrcpy -t # short version
|
|||||||
|
|
||||||
Note that it only shows _physical_ touches (by a finger on the device).
|
Note that it only shows _physical_ touches (by a finger on the device).
|
||||||
|
|
||||||
|
It is possible to change this setting manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# get the current show_touches value
|
||||||
|
adb shell settings get system show_touches
|
||||||
|
# enable show_touches
|
||||||
|
adb shell settings put system show_touches 1
|
||||||
|
# disable show_touches
|
||||||
|
adb shell settings put system show_touches 0
|
||||||
|
```
|
||||||
|
|
||||||
## Power off on close
|
## Power off on close
|
||||||
|
|
||||||
@ -78,3 +112,48 @@ By default, on start, the device is powered on. To prevent this behavior:
|
|||||||
```bash
|
```bash
|
||||||
scrcpy --no-power-on
|
scrcpy --no-power-on
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Start Android app
|
||||||
|
|
||||||
|
To list the Android apps installed on the device:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --list-apps
|
||||||
|
```
|
||||||
|
|
||||||
|
An app, selected by its package name, can be launched on start:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --start-app=org.mozilla.firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
This feature can be used to run an app in a [virtual
|
||||||
|
display](virtual_display.md):
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
|
||||||
|
```
|
||||||
|
|
||||||
|
The app can be optionally forced-stop before being started, by adding a `+`
|
||||||
|
prefix:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --start-app=+org.mozilla.firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
For convenience, it is also possible to select an app by its name, by adding a
|
||||||
|
`?` prefix:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --start-app=?firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
But retrieving app names may take some time (sometimes several seconds), so
|
||||||
|
passing the package name is recommended.
|
||||||
|
|
||||||
|
The `+` and `?` prefixes can be combined (in that order):
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --start-app=+?firefox
|
||||||
|
```
|
||||||
|
@ -34,9 +34,9 @@ Two modes allow to simulate a physical HID mouse on the device.
|
|||||||
In these modes, the computer mouse is "captured": the mouse pointer disappears
|
In these modes, the computer mouse is "captured": the mouse pointer disappears
|
||||||
from the computer and appears on the Android device instead.
|
from the computer and appears on the Android device instead.
|
||||||
|
|
||||||
Special capture keys, either <kbd>Alt</kbd> or <kbd>Super</kbd>, toggle
|
The [shortcut mod](shortcuts.md) (either <kbd>Alt</kbd> or <kbd>Super</kbd> by
|
||||||
(disable or enable) the mouse capture. Use one of them to give the control of
|
default) toggle (disable or enable) the mouse capture. Use one of them to give
|
||||||
the mouse back to the computer.
|
the control of the mouse back to the computer.
|
||||||
|
|
||||||
|
|
||||||
### UHID
|
### UHID
|
||||||
|
@ -189,15 +189,15 @@ The configuration is available independently for the display,
|
|||||||
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
|
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --display-buffer=50 # add 50ms buffering for display
|
scrcpy --video-buffer=50 # add 50ms buffering for video playback
|
||||||
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
|
|
||||||
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
|
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
|
||||||
|
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
|
||||||
```
|
```
|
||||||
|
|
||||||
They can be applied simultaneously:
|
They can be applied simultaneously:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --display-buffer=50 --v4l2-buffer=300
|
scrcpy --video-buffer=50 --v4l2-buffer=300
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
26
doc/virtual_display.md
Normal file
26
doc/virtual_display.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Virtual display
|
||||||
|
|
||||||
|
## New display
|
||||||
|
|
||||||
|
To mirror a new virtual display instead of the device screen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Start app
|
||||||
|
|
||||||
|
On some devices, a launcher is available in the virtual display.
|
||||||
|
|
||||||
|
When no launcher is available, the virtual display is empty. In that case, you
|
||||||
|
must [start an Android app](device.md#start-android-app).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
|
||||||
|
```
|
104
release.mk
104
release.mk
@ -9,13 +9,12 @@
|
|||||||
# the server to the device.
|
# the server to the device.
|
||||||
|
|
||||||
.PHONY: default clean \
|
.PHONY: default clean \
|
||||||
test \
|
test test-client test-server \
|
||||||
build-server \
|
build-server \
|
||||||
prepare-deps \
|
prepare-deps-win32 prepare-deps-win64 \
|
||||||
build-win32 build-win64 \
|
build-win32 build-win64 \
|
||||||
dist-win32 dist-win64 \
|
|
||||||
zip-win32 zip-win64 \
|
zip-win32 zip-win64 \
|
||||||
release
|
package release
|
||||||
|
|
||||||
GRADLE ?= ./gradlew
|
GRADLE ?= ./gradlew
|
||||||
|
|
||||||
@ -26,7 +25,7 @@ WIN64_BUILD_DIR := build-win64
|
|||||||
|
|
||||||
VERSION ?= $(shell git describe --tags --exclude='*install-release' --always)
|
VERSION ?= $(shell git describe --tags --exclude='*install-release' --always)
|
||||||
|
|
||||||
DIST := dist
|
ZIP := zip
|
||||||
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
|
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
|
||||||
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
|
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
|
||||||
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
|
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
|
||||||
@ -34,33 +33,28 @@ WIN64_TARGET := $(WIN64_TARGET_DIR).zip
|
|||||||
|
|
||||||
RELEASE_DIR := release-$(VERSION)
|
RELEASE_DIR := release-$(VERSION)
|
||||||
|
|
||||||
release: clean test build-server zip-win32 zip-win64
|
release: clean test build-server build-win32 build-win64 package
|
||||||
mkdir -p "$(RELEASE_DIR)"
|
|
||||||
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
|
|
||||||
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
|
|
||||||
cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
|
|
||||||
cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)"
|
|
||||||
cd "$(RELEASE_DIR)" && \
|
|
||||||
sha256sum "scrcpy-server-$(VERSION)" \
|
|
||||||
"scrcpy-win32-$(VERSION).zip" \
|
|
||||||
"scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt
|
|
||||||
@echo "Release generated in $(RELEASE_DIR)/"
|
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
$(GRADLE) clean
|
$(GRADLE) clean
|
||||||
rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
|
rm -rf "$(ZIP)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \
|
||||||
"$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
|
"$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)"
|
||||||
|
|
||||||
test:
|
test-client:
|
||||||
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
|
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
|
||||||
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
|
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
|
||||||
ninja -C "$(TEST_BUILD_DIR)"
|
ninja -C "$(TEST_BUILD_DIR)"
|
||||||
|
|
||||||
|
test-server:
|
||||||
$(GRADLE) -p server check
|
$(GRADLE) -p server check
|
||||||
|
|
||||||
|
test: test-client test-server
|
||||||
|
|
||||||
build-server:
|
build-server:
|
||||||
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
|
$(GRADLE) -p server assembleRelease
|
||||||
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
|
mkdir -p "$(SERVER_BUILD_DIR)/server"
|
||||||
ninja -C "$(SERVER_BUILD_DIR)"
|
cp server/build/outputs/apk/release/server-release-unsigned.apk \
|
||||||
|
"$(SERVER_BUILD_DIR)/server/scrcpy-server"
|
||||||
|
|
||||||
prepare-deps-win32:
|
prepare-deps-win32:
|
||||||
@app/deps/adb.sh win32
|
@app/deps/adb.sh win32
|
||||||
@ -86,6 +80,15 @@ build-win32: prepare-deps-win32
|
|||||||
-Dcompile_server=false \
|
-Dcompile_server=false \
|
||||||
-Dportable=true
|
-Dportable=true
|
||||||
ninja -C "$(WIN32_BUILD_DIR)"
|
ninja -C "$(WIN32_BUILD_DIR)"
|
||||||
|
# Group intermediate outputs into a 'dist' directory
|
||||||
|
mkdir -p "$(WIN32_BUILD_DIR)/dist"
|
||||||
|
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(WIN32_BUILD_DIR)/dist/"
|
||||||
|
cp app/data/scrcpy-console.bat "$(WIN32_BUILD_DIR)/dist/"
|
||||||
|
cp app/data/scrcpy-noconsole.vbs "$(WIN32_BUILD_DIR)/dist/"
|
||||||
|
cp app/data/icon.png "$(WIN32_BUILD_DIR)/dist/"
|
||||||
|
cp app/data/open_a_terminal_here.bat "$(WIN32_BUILD_DIR)/dist/"
|
||||||
|
cp app/deps/work/install/win32/bin/*.dll "$(WIN32_BUILD_DIR)/dist/"
|
||||||
|
cp app/deps/work/install/win32/bin/adb.exe "$(WIN32_BUILD_DIR)/dist/"
|
||||||
|
|
||||||
build-win64: prepare-deps-win64
|
build-win64: prepare-deps-win64
|
||||||
rm -rf "$(WIN64_BUILD_DIR)"
|
rm -rf "$(WIN64_BUILD_DIR)"
|
||||||
@ -99,33 +102,40 @@ build-win64: prepare-deps-win64
|
|||||||
-Dcompile_server=false \
|
-Dcompile_server=false \
|
||||||
-Dportable=true
|
-Dportable=true
|
||||||
ninja -C "$(WIN64_BUILD_DIR)"
|
ninja -C "$(WIN64_BUILD_DIR)"
|
||||||
|
# Group intermediate outputs into a 'dist' directory
|
||||||
|
mkdir -p "$(WIN64_BUILD_DIR)/dist"
|
||||||
|
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(WIN64_BUILD_DIR)/dist/"
|
||||||
|
cp app/data/scrcpy-console.bat "$(WIN64_BUILD_DIR)/dist/"
|
||||||
|
cp app/data/scrcpy-noconsole.vbs "$(WIN64_BUILD_DIR)/dist/"
|
||||||
|
cp app/data/icon.png "$(WIN64_BUILD_DIR)/dist/"
|
||||||
|
cp app/data/open_a_terminal_here.bat "$(WIN64_BUILD_DIR)/dist/"
|
||||||
|
cp app/deps/work/install/win64/bin/*.dll "$(WIN64_BUILD_DIR)/dist/"
|
||||||
|
cp app/deps/work/install/win64/bin/adb.exe "$(WIN64_BUILD_DIR)/dist/"
|
||||||
|
|
||||||
dist-win32: build-server build-win32
|
zip-win32:
|
||||||
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
|
mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)"
|
||||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/"
|
||||||
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cd "$(ZIP)"; \
|
||||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/"
|
|
||||||
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/"
|
|
||||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
|
|
||||||
cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
|
||||||
cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
|
||||||
|
|
||||||
dist-win64: build-server build-win64
|
|
||||||
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
|
|
||||||
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
|
|
||||||
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
|
||||||
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
|
|
||||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/"
|
|
||||||
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/"
|
|
||||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
|
|
||||||
cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
|
||||||
cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
|
||||||
|
|
||||||
zip-win32: dist-win32
|
|
||||||
cd "$(DIST)"; \
|
|
||||||
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
|
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
|
||||||
|
rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)"
|
||||||
|
|
||||||
zip-win64: dist-win64
|
zip-win64:
|
||||||
cd "$(DIST)"; \
|
mkdir -p "$(ZIP)/$(WIN64_TARGET_DIR)"
|
||||||
|
cp -r "$(WIN64_BUILD_DIR)/dist/." "$(ZIP)/$(WIN64_TARGET_DIR)/"
|
||||||
|
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN64_TARGET_DIR)/"
|
||||||
|
cd "$(ZIP)"; \
|
||||||
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
|
zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)"
|
||||||
|
rm -rf "$(ZIP)/$(WIN64_TARGET_DIR)"
|
||||||
|
|
||||||
|
package: zip-win32 zip-win64
|
||||||
|
mkdir -p "$(RELEASE_DIR)"
|
||||||
|
cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \
|
||||||
|
"$(RELEASE_DIR)/scrcpy-server-$(VERSION)"
|
||||||
|
cp "$(ZIP)/$(WIN32_TARGET)" "$(RELEASE_DIR)"
|
||||||
|
cp "$(ZIP)/$(WIN64_TARGET)" "$(RELEASE_DIR)"
|
||||||
|
cd "$(RELEASE_DIR)" && \
|
||||||
|
sha256sum "scrcpy-server-$(VERSION)" \
|
||||||
|
"scrcpy-win32-$(VERSION).zip" \
|
||||||
|
"scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt
|
||||||
|
@echo "Release generated in $(RELEASE_DIR)/"
|
||||||
|
@ -19,7 +19,7 @@ public final class CleanUp {
|
|||||||
private static final int MSG_TYPE_MASK = 0b11;
|
private static final int MSG_TYPE_MASK = 0b11;
|
||||||
private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
|
private static final int MSG_TYPE_RESTORE_STAY_ON = 0;
|
||||||
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
|
private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1;
|
||||||
private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2;
|
private static final int MSG_TYPE_RESTORE_DISPLAY_POWER = 2;
|
||||||
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
|
private static final int MSG_TYPE_POWER_OFF_SCREEN = 3;
|
||||||
|
|
||||||
private static final int MSG_PARAM_SHIFT = 2;
|
private static final int MSG_PARAM_SHIFT = 2;
|
||||||
@ -63,8 +63,8 @@ public final class CleanUp {
|
|||||||
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
|
return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean setRestoreNormalPowerMode(boolean restoreOnExit) {
|
public boolean setRestoreDisplayPower(boolean restoreOnExit) {
|
||||||
return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0);
|
return sendMessage(MSG_TYPE_RESTORE_DISPLAY_POWER, restoreOnExit ? 1 : 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
|
public boolean setPowerOffScreen(boolean powerOffScreenOnExit) {
|
||||||
@ -86,7 +86,7 @@ public final class CleanUp {
|
|||||||
|
|
||||||
int restoreStayOn = -1;
|
int restoreStayOn = -1;
|
||||||
boolean disableShowTouches = false;
|
boolean disableShowTouches = false;
|
||||||
boolean restoreNormalPowerMode = false;
|
boolean restoreDisplayPower = false;
|
||||||
boolean powerOffScreen = false;
|
boolean powerOffScreen = false;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@ -102,8 +102,8 @@ public final class CleanUp {
|
|||||||
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
|
case MSG_TYPE_DISABLE_SHOW_TOUCHES:
|
||||||
disableShowTouches = param != 0;
|
disableShowTouches = param != 0;
|
||||||
break;
|
break;
|
||||||
case MSG_TYPE_RESTORE_NORMAL_POWER_MODE:
|
case MSG_TYPE_RESTORE_DISPLAY_POWER:
|
||||||
restoreNormalPowerMode = param != 0;
|
restoreDisplayPower = param != 0;
|
||||||
break;
|
break;
|
||||||
case MSG_TYPE_POWER_OFF_SCREEN:
|
case MSG_TYPE_POWER_OFF_SCREEN:
|
||||||
powerOffScreen = param != 0;
|
powerOffScreen = param != 0;
|
||||||
@ -137,15 +137,13 @@ public final class CleanUp {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Device.isScreenOn()) {
|
if (Device.isScreenOn() && displayId != Device.DISPLAY_ID_NONE) {
|
||||||
if (powerOffScreen) {
|
if (powerOffScreen) {
|
||||||
if (displayId != Device.DISPLAY_ID_NONE) {
|
|
||||||
Ln.i("Power off screen");
|
Ln.i("Power off screen");
|
||||||
Device.powerOffScreen(displayId);
|
Device.powerOffScreen(displayId);
|
||||||
}
|
} else if (restoreDisplayPower) {
|
||||||
} else if (restoreNormalPowerMode) {
|
Ln.i("Restoring display power");
|
||||||
Ln.i("Restoring normal power mode");
|
Device.setDisplayPower(displayId, true);
|
||||||
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.Device;
|
||||||
import com.genymobile.scrcpy.device.NewDisplay;
|
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;
|
||||||
@ -31,7 +32,7 @@ public class Options {
|
|||||||
private int videoBitRate = 8000000;
|
private int videoBitRate = 8000000;
|
||||||
private int audioBitRate = 128000;
|
private int audioBitRate = 128000;
|
||||||
private float maxFps;
|
private float maxFps;
|
||||||
private int lockVideoOrientation = -1;
|
private int lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED;
|
||||||
private boolean tunnelForward;
|
private boolean tunnelForward;
|
||||||
private Rect crop;
|
private Rect crop;
|
||||||
private boolean control = true;
|
private boolean control = true;
|
||||||
@ -253,6 +254,10 @@ public class Options {
|
|||||||
return sendCodecMeta;
|
return sendCodecMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void resetLockVideoOrientation() {
|
||||||
|
this.lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED;
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("MethodLength")
|
@SuppressWarnings("MethodLength")
|
||||||
public static Options parse(String... args) {
|
public static Options parse(String... args) {
|
||||||
if (args.length < 1) {
|
if (args.length < 1) {
|
||||||
@ -524,8 +529,12 @@ public class Options {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static NewDisplay parseNewDisplay(String newDisplay) {
|
private static NewDisplay parseNewDisplay(String newDisplay) {
|
||||||
// input format: "auto", or "<width>x<height>", or "<width>x<height>:<dpi>"
|
// Possible inputs:
|
||||||
if ("auto".equals(newDisplay)) {
|
// - "" (empty string)
|
||||||
|
// - "<width>x<height>/<dpi>"
|
||||||
|
// - "<width>x<height>"
|
||||||
|
// - "/<dpi>"
|
||||||
|
if (newDisplay.isEmpty()) {
|
||||||
return new NewDisplay();
|
return new NewDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,6 +127,28 @@ public final class Server {
|
|||||||
throw new ConfigurationException("Camera mirroring is not supported");
|
throw new ConfigurationException("Camera mirroring is not supported");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) {
|
||||||
|
Ln.e("New virtual display is not supported before Android 10");
|
||||||
|
throw new ConfigurationException("New virtual display is not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) {
|
||||||
|
int lockVideoOrientation = options.getLockVideoOrientation();
|
||||||
|
if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) {
|
||||||
|
if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) {
|
||||||
|
Ln.e("--lock-video-orientation is broken on Android >= 14: <https://github.com/Genymobile/scrcpy/issues/4011>");
|
||||||
|
throw new ConfigurationException("--lock-video-orientation is broken on Android >= 14");
|
||||||
|
} else {
|
||||||
|
// If the flag has been set automatically (because v4l2 sink is enabled), do not fail
|
||||||
|
Ln.w("--lock-video-orientation is ignored on Android >= 14: <https://github.com/Genymobile/scrcpy/issues/4011>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (options.getCrop() != null) {
|
||||||
|
Ln.e("--crop is broken on Android >= 14: <https://github.com/Genymobile/scrcpy/issues/4162>");
|
||||||
|
throw new ConfigurationException("Crop is not broken on Android >= 14");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CleanUp cleanUp = null;
|
CleanUp cleanUp = null;
|
||||||
Thread initThread = null;
|
Thread initThread = null;
|
||||||
|
|
||||||
|
@ -17,7 +17,7 @@ public final class ControlMessage {
|
|||||||
public static final int TYPE_COLLAPSE_PANELS = 7;
|
public static final int TYPE_COLLAPSE_PANELS = 7;
|
||||||
public static final int TYPE_GET_CLIPBOARD = 8;
|
public static final int TYPE_GET_CLIPBOARD = 8;
|
||||||
public static final int TYPE_SET_CLIPBOARD = 9;
|
public static final int TYPE_SET_CLIPBOARD = 9;
|
||||||
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
|
public static final int TYPE_SET_DISPLAY_POWER = 10;
|
||||||
public static final int TYPE_ROTATE_DEVICE = 11;
|
public static final int TYPE_ROTATE_DEVICE = 11;
|
||||||
public static final int TYPE_UHID_CREATE = 12;
|
public static final int TYPE_UHID_CREATE = 12;
|
||||||
public static final int TYPE_UHID_INPUT = 13;
|
public static final int TYPE_UHID_INPUT = 13;
|
||||||
@ -34,7 +34,7 @@ public final class ControlMessage {
|
|||||||
private int type;
|
private int type;
|
||||||
private String text;
|
private String text;
|
||||||
private int metaState; // KeyEvent.META_*
|
private int metaState; // KeyEvent.META_*
|
||||||
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
|
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_*
|
||||||
private int keycode; // KeyEvent.KEYCODE_*
|
private int keycode; // KeyEvent.KEYCODE_*
|
||||||
private int actionButton; // MotionEvent.BUTTON_*
|
private int actionButton; // MotionEvent.BUTTON_*
|
||||||
private int buttons; // MotionEvent.BUTTON_*
|
private int buttons; // MotionEvent.BUTTON_*
|
||||||
@ -49,6 +49,7 @@ public final class ControlMessage {
|
|||||||
private long sequence;
|
private long sequence;
|
||||||
private int id;
|
private int id;
|
||||||
private byte[] data;
|
private byte[] data;
|
||||||
|
private boolean on;
|
||||||
|
|
||||||
private ControlMessage() {
|
private ControlMessage() {
|
||||||
}
|
}
|
||||||
@ -116,13 +117,10 @@ public final class ControlMessage {
|
|||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static ControlMessage createSetDisplayPower(boolean on) {
|
||||||
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
|
|
||||||
*/
|
|
||||||
public static ControlMessage createSetScreenPowerMode(int mode) {
|
|
||||||
ControlMessage msg = new ControlMessage();
|
ControlMessage msg = new ControlMessage();
|
||||||
msg.type = TYPE_SET_SCREEN_POWER_MODE;
|
msg.type = TYPE_SET_DISPLAY_POWER;
|
||||||
msg.action = mode;
|
msg.on = on;
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -234,4 +232,8 @@ public final class ControlMessage {
|
|||||||
public byte[] getData() {
|
public byte[] getData() {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean getOn() {
|
||||||
|
return on;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -39,8 +39,8 @@ public class ControlMessageReader {
|
|||||||
return parseGetClipboard();
|
return parseGetClipboard();
|
||||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||||
return parseSetClipboard();
|
return parseSetClipboard();
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||||
return parseSetScreenPowerMode();
|
return parseSetDisplayPower();
|
||||||
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
|
||||||
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
|
||||||
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
case ControlMessage.TYPE_COLLAPSE_PANELS:
|
||||||
@ -134,9 +134,9 @@ public class ControlMessageReader {
|
|||||||
return ControlMessage.createSetClipboard(sequence, text, paste);
|
return ControlMessage.createSetClipboard(sequence, text, paste);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseSetScreenPowerMode() throws IOException {
|
private ControlMessage parseSetDisplayPower() throws IOException {
|
||||||
int mode = dis.readUnsignedByte();
|
boolean on = dis.readBoolean();
|
||||||
return ControlMessage.createSetScreenPowerMode(mode);
|
return ControlMessage.createSetDisplayPower(on);
|
||||||
}
|
}
|
||||||
|
|
||||||
private ControlMessage parseUhidCreate() throws IOException {
|
private ControlMessage parseUhidCreate() throws IOException {
|
||||||
|
@ -91,7 +91,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
|
||||||
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
|
||||||
|
|
||||||
private boolean keepPowerModeOff;
|
private boolean keepDisplayPowerOff;
|
||||||
|
|
||||||
public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
|
public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) {
|
||||||
this.displayId = displayId;
|
this.displayId = displayId;
|
||||||
@ -102,7 +102,6 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
initPointers();
|
initPointers();
|
||||||
sender = new DeviceMessageSender(controlChannel);
|
sender = new DeviceMessageSender(controlChannel);
|
||||||
|
|
||||||
// main display or any display on Android >= Q
|
|
||||||
supportsInputEvents = Device.supportsInputEvents(displayId);
|
supportsInputEvents = Device.supportsInputEvents(displayId);
|
||||||
if (!supportsInputEvents) {
|
if (!supportsInputEvents) {
|
||||||
Ln.w("Input events are not supported for secondary displays before Android 10");
|
Ln.w("Input events are not supported for secondary displays before Android 10");
|
||||||
@ -137,6 +136,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
DisplayData data = new DisplayData(virtualDisplayId, positionMapper);
|
DisplayData data = new DisplayData(virtualDisplayId, positionMapper);
|
||||||
DisplayData old = this.displayData.getAndSet(data);
|
DisplayData old = this.displayData.getAndSet(data);
|
||||||
if (old == null) {
|
if (old == null) {
|
||||||
|
// The very first time the Controller is notified of a new virtual display
|
||||||
synchronized (displayDataAvailable) {
|
synchronized (displayDataAvailable) {
|
||||||
displayDataAvailable.notify();
|
displayDataAvailable.notify();
|
||||||
}
|
}
|
||||||
@ -270,16 +270,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
case ControlMessage.TYPE_SET_CLIPBOARD:
|
case ControlMessage.TYPE_SET_CLIPBOARD:
|
||||||
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
|
||||||
break;
|
break;
|
||||||
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
|
case ControlMessage.TYPE_SET_DISPLAY_POWER:
|
||||||
if (supportsInputEvents) {
|
if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) {
|
||||||
int mode = msg.getAction();
|
boolean on = msg.getOn();
|
||||||
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
|
boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on);
|
||||||
if (setPowerModeOk) {
|
if (setDisplayPowerOk) {
|
||||||
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
|
keepDisplayPowerOff = !on;
|
||||||
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
|
Ln.i("Device display turned " + (on ? "on" : "off"));
|
||||||
if (cleanUp != null) {
|
if (cleanUp != null) {
|
||||||
boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL;
|
boolean mustRestoreOnExit = !on;
|
||||||
cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit);
|
cleanUp.setRestoreDisplayPower(mustRestoreOnExit);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -310,8 +310,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
private boolean injectKeycode(int action, int keycode, int repeat, int metaState) {
|
||||||
if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
|
if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) {
|
||||||
schedulePowerModeOff();
|
assert displayId != Device.DISPLAY_ID_NONE;
|
||||||
|
scheduleDisplayPowerOff(displayId);
|
||||||
}
|
}
|
||||||
return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
|
return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
|
||||||
}
|
}
|
||||||
@ -348,7 +349,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
||||||
long now = SystemClock.uptimeMillis();
|
long now = SystemClock.uptimeMillis();
|
||||||
|
|
||||||
// it hides the field on purpose, to read it from the atomic once
|
// it hides the field on purpose, to read it with atomic access
|
||||||
@SuppressWarnings("checkstyle:HiddenField")
|
@SuppressWarnings("checkstyle:HiddenField")
|
||||||
DisplayData displayData = this.displayData.get();
|
DisplayData displayData = this.displayData.get();
|
||||||
assert displayData != null : "Cannot receive a touch event without a display";
|
assert displayData != null : "Cannot receive a touch event without a display";
|
||||||
@ -462,7 +463,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
|
private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
|
||||||
long now = SystemClock.uptimeMillis();
|
long now = SystemClock.uptimeMillis();
|
||||||
|
|
||||||
// it hides the field on purpose, to read it from the atomic once
|
// it hides the field on purpose, to read it with atomic access
|
||||||
@SuppressWarnings("checkstyle:HiddenField")
|
@SuppressWarnings("checkstyle:HiddenField")
|
||||||
DisplayData displayData = this.displayData.get();
|
DisplayData displayData = this.displayData.get();
|
||||||
assert displayData != null : "Cannot receive a scroll event without a display";
|
assert displayData != null : "Cannot receive a scroll event without a display";
|
||||||
@ -488,12 +489,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Schedule a call to set power mode to off after a small delay.
|
* Schedule a call to set display power to off after a small delay.
|
||||||
*/
|
*/
|
||||||
private static void schedulePowerModeOff() {
|
private static void scheduleDisplayPowerOff(int displayId) {
|
||||||
EXECUTOR.schedule(() -> {
|
EXECUTOR.schedule(() -> {
|
||||||
Ln.i("Forcing screen off");
|
Ln.i("Forcing display off");
|
||||||
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
|
Device.setDisplayPower(displayId, false);
|
||||||
}, 200, TimeUnit.MILLISECONDS);
|
}, 200, TimeUnit.MILLISECONDS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -509,8 +510,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (keepPowerModeOff) {
|
if (keepDisplayPowerOff) {
|
||||||
schedulePowerModeOff();
|
assert displayId != Device.DISPLAY_ID_NONE;
|
||||||
|
scheduleDisplayPowerOff(displayId);
|
||||||
}
|
}
|
||||||
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,9 @@ public final class PositionMapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static PositionMapper from(ScreenInfo screenInfo) {
|
public static PositionMapper from(ScreenInfo screenInfo) {
|
||||||
return new PositionMapper(screenInfo.getUnlockedVideoSize(), screenInfo.getContentRect(), screenInfo.getVideoRotation());
|
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
|
||||||
|
Size videoSize = screenInfo.getUnlockedVideoSize();
|
||||||
|
return new PositionMapper(videoSize, screenInfo.getContentRect(), screenInfo.getVideoRotation());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int reverseRotation(int rotation) {
|
private static int reverseRotation(int rotation) {
|
||||||
|
@ -42,6 +42,8 @@ public final class Device {
|
|||||||
|
|
||||||
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
|
||||||
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
|
||||||
|
// like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically
|
||||||
|
public static final int LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3;
|
||||||
|
|
||||||
private Device() {
|
private Device() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
@ -52,6 +54,7 @@ public final class Device {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean supportsInputEvents(int displayId) {
|
public static boolean supportsInputEvents(int displayId) {
|
||||||
|
// main display or any display on Android >= 10
|
||||||
return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
|
return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,10 +128,13 @@ public final class Device {
|
|||||||
return clipboardManager.setText(text);
|
return clipboardManager.setText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public static boolean setDisplayPower(int displayId, boolean on) {
|
||||||
* @param mode one of the {@code POWER_MODE_*} constants
|
assert displayId != Device.DISPLAY_ID_NONE;
|
||||||
*/
|
|
||||||
public static boolean setScreenPowerMode(int mode) {
|
if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) {
|
||||||
|
return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on);
|
||||||
|
}
|
||||||
|
|
||||||
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
|
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10;
|
||||||
|
|
||||||
if (applyToMultiPhysicalDisplays
|
if (applyToMultiPhysicalDisplays
|
||||||
@ -141,6 +147,7 @@ public final class Device {
|
|||||||
applyToMultiPhysicalDisplays = false;
|
applyToMultiPhysicalDisplays = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int mode = on ? POWER_MODE_NORMAL : POWER_MODE_OFF;
|
||||||
if (applyToMultiPhysicalDisplays) {
|
if (applyToMultiPhysicalDisplays) {
|
||||||
// On Android 14, these internal methods have been moved to DisplayControl
|
// On Android 14, these internal methods have been moved to DisplayControl
|
||||||
boolean useDisplayControl =
|
boolean useDisplayControl =
|
||||||
@ -228,7 +235,7 @@ public final class Device {
|
|||||||
private static List<ApplicationInfo> getLaunchableApps(PackageManager pm) {
|
private static List<ApplicationInfo> getLaunchableApps(PackageManager pm) {
|
||||||
List<ApplicationInfo> result = new ArrayList<>();
|
List<ApplicationInfo> result = new ArrayList<>();
|
||||||
for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) {
|
for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) {
|
||||||
if (getLaunchIntent(pm, appInfo.packageName) != null) {
|
if (appInfo.enabled && getLaunchIntent(pm, appInfo.packageName) != null) {
|
||||||
result.add(appInfo);
|
result.add(appInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -248,7 +255,7 @@ public final class Device {
|
|||||||
private static DeviceApp toApp(PackageManager pm, ApplicationInfo appInfo) {
|
private static DeviceApp toApp(PackageManager pm, ApplicationInfo appInfo) {
|
||||||
String name = pm.getApplicationLabel(appInfo).toString();
|
String name = pm.getApplicationLabel(appInfo).toString();
|
||||||
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
||||||
return new DeviceApp(appInfo.packageName, name, system, appInfo.enabled);
|
return new DeviceApp(appInfo.packageName, name, system);
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("QueryPermissionsNeeded")
|
@SuppressLint("QueryPermissionsNeeded")
|
||||||
@ -271,12 +278,10 @@ public final class Device {
|
|||||||
|
|
||||||
PackageManager pm = FakeContext.get().getPackageManager();
|
PackageManager pm = FakeContext.get().getPackageManager();
|
||||||
for (ApplicationInfo appInfo : getLaunchableApps(pm)) {
|
for (ApplicationInfo appInfo : getLaunchableApps(pm)) {
|
||||||
if (appInfo.enabled) {
|
|
||||||
String name = pm.getApplicationLabel(appInfo).toString();
|
String name = pm.getApplicationLabel(appInfo).toString();
|
||||||
if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) {
|
if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) {
|
||||||
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
|
||||||
result.add(new DeviceApp(appInfo.packageName, name, system, appInfo.enabled));
|
result.add(new DeviceApp(appInfo.packageName, name, system));
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -5,13 +5,11 @@ public final class DeviceApp {
|
|||||||
private final String packageName;
|
private final String packageName;
|
||||||
private final String name;
|
private final String name;
|
||||||
private final boolean system;
|
private final boolean system;
|
||||||
private final boolean enabled;
|
|
||||||
|
|
||||||
public DeviceApp(String packageName, String name, boolean system, boolean enabled) {
|
public DeviceApp(String packageName, String name, boolean system) {
|
||||||
this.packageName = packageName;
|
this.packageName = packageName;
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.system = system;
|
this.system = system;
|
||||||
this.enabled = enabled;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPackageName() {
|
public String getPackageName() {
|
||||||
@ -25,8 +23,4 @@ public final class DeviceApp {
|
|||||||
public boolean isSystem() {
|
public boolean isSystem() {
|
||||||
return system;
|
return system;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEnabled() {
|
|
||||||
return enabled;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,5 @@
|
|||||||
package com.genymobile.scrcpy.util;
|
package com.genymobile.scrcpy.util;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.audio.AudioCodec;
|
|
||||||
import com.genymobile.scrcpy.video.VideoCodec;
|
|
||||||
|
|
||||||
import android.media.MediaCodecInfo;
|
import android.media.MediaCodecInfo;
|
||||||
import android.media.MediaCodecList;
|
import android.media.MediaCodecList;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
@ -13,24 +10,6 @@ import java.util.List;
|
|||||||
|
|
||||||
public final class CodecUtils {
|
public final class CodecUtils {
|
||||||
|
|
||||||
public static final class DeviceEncoder {
|
|
||||||
private final Codec codec;
|
|
||||||
private final MediaCodecInfo info;
|
|
||||||
|
|
||||||
DeviceEncoder(Codec codec, MediaCodecInfo info) {
|
|
||||||
this.codec = codec;
|
|
||||||
this.info = info;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Codec getCodec() {
|
|
||||||
return codec;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MediaCodecInfo getInfo() {
|
|
||||||
return info;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private CodecUtils() {
|
private CodecUtils() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
@ -47,7 +26,7 @@ public final class CodecUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) {
|
public static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) {
|
||||||
List<MediaCodecInfo> result = new ArrayList<>();
|
List<MediaCodecInfo> result = new ArrayList<>();
|
||||||
for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) {
|
for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) {
|
||||||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) {
|
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) {
|
||||||
@ -56,26 +35,4 @@ public final class CodecUtils {
|
|||||||
}
|
}
|
||||||
return result.toArray(new MediaCodecInfo[result.size()]);
|
return result.toArray(new MediaCodecInfo[result.size()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<DeviceEncoder> listVideoEncoders() {
|
|
||||||
List<DeviceEncoder> encoders = new ArrayList<>();
|
|
||||||
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
|
||||||
for (VideoCodec codec : VideoCodec.values()) {
|
|
||||||
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) {
|
|
||||||
encoders.add(new DeviceEncoder(codec, info));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return encoders;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static List<DeviceEncoder> listAudioEncoders() {
|
|
||||||
List<DeviceEncoder> encoders = new ArrayList<>();
|
|
||||||
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
|
||||||
for (AudioCodec codec : AudioCodec.values()) {
|
|
||||||
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) {
|
|
||||||
encoders.add(new DeviceEncoder(codec, info));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return encoders;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,19 +1,26 @@
|
|||||||
package com.genymobile.scrcpy.util;
|
package com.genymobile.scrcpy.util;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.AndroidVersions;
|
||||||
|
import com.genymobile.scrcpy.audio.AudioCodec;
|
||||||
import com.genymobile.scrcpy.device.Device;
|
import com.genymobile.scrcpy.device.Device;
|
||||||
import com.genymobile.scrcpy.device.DeviceApp;
|
import com.genymobile.scrcpy.device.DeviceApp;
|
||||||
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.video.VideoCodec;
|
||||||
import com.genymobile.scrcpy.wrappers.DisplayManager;
|
import com.genymobile.scrcpy.wrappers.DisplayManager;
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
|
import android.annotation.TargetApi;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.hardware.camera2.CameraAccessException;
|
import android.hardware.camera2.CameraAccessException;
|
||||||
import android.hardware.camera2.CameraCharacteristics;
|
import android.hardware.camera2.CameraCharacteristics;
|
||||||
import android.hardware.camera2.CameraManager;
|
import android.hardware.camera2.CameraManager;
|
||||||
import android.hardware.camera2.params.StreamConfigurationMap;
|
import android.hardware.camera2.params.StreamConfigurationMap;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
|
import android.media.MediaCodecInfo;
|
||||||
|
import android.media.MediaCodecList;
|
||||||
|
import android.os.Build;
|
||||||
import android.util.Range;
|
import android.util.Range;
|
||||||
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
@ -28,32 +35,54 @@ public final class LogUtils {
|
|||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String buildVideoEncoderListMessage() {
|
private static String buildEncoderListMessage(String type, Codec[] codecs) {
|
||||||
StringBuilder builder = new StringBuilder("List of video encoders:");
|
StringBuilder builder = new StringBuilder("List of ").append(type).append(" encoders:");
|
||||||
List<CodecUtils.DeviceEncoder> videoEncoders = CodecUtils.listVideoEncoders();
|
MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
|
||||||
if (videoEncoders.isEmpty()) {
|
for (Codec codec : codecs) {
|
||||||
builder.append("\n (none)");
|
MediaCodecInfo[] encoders = CodecUtils.getEncoders(codecList, codec.getMimeType());
|
||||||
} else {
|
for (MediaCodecInfo info : encoders) {
|
||||||
for (CodecUtils.DeviceEncoder encoder : videoEncoders) {
|
int lineStart = builder.length();
|
||||||
builder.append("\n --video-codec=").append(encoder.getCodec().getName());
|
builder.append("\n --").append(type).append("-codec=").append(codec.getName());
|
||||||
builder.append(" --video-encoder=").append(encoder.getInfo().getName());
|
builder.append(" --").append(type).append("-encoder=").append(info.getName());
|
||||||
|
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
|
||||||
|
int lineLength = builder.length() - lineStart;
|
||||||
|
final int column = 70;
|
||||||
|
if (lineLength < column) {
|
||||||
|
int padding = column - lineLength;
|
||||||
|
builder.append(String.format("%" + padding + "s", " "));
|
||||||
|
}
|
||||||
|
builder.append(" (").append(getHwCodecType(info)).append(')');
|
||||||
|
if (info.isVendor()) {
|
||||||
|
builder.append(" [vendor]");
|
||||||
|
}
|
||||||
|
if (info.isAlias()) {
|
||||||
|
builder.append(" (alias for ").append(info.getCanonicalName()).append(')');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static String buildVideoEncoderListMessage() {
|
||||||
|
return buildEncoderListMessage("video", VideoCodec.values());
|
||||||
|
}
|
||||||
|
|
||||||
public static String buildAudioEncoderListMessage() {
|
public static String buildAudioEncoderListMessage() {
|
||||||
StringBuilder builder = new StringBuilder("List of audio encoders:");
|
return buildEncoderListMessage("audio", AudioCodec.values());
|
||||||
List<CodecUtils.DeviceEncoder> audioEncoders = CodecUtils.listAudioEncoders();
|
|
||||||
if (audioEncoders.isEmpty()) {
|
|
||||||
builder.append("\n (none)");
|
|
||||||
} else {
|
|
||||||
for (CodecUtils.DeviceEncoder encoder : audioEncoders) {
|
|
||||||
builder.append("\n --audio-codec=").append(encoder.getCodec().getName());
|
|
||||||
builder.append(" --audio-encoder=").append(encoder.getInfo().getName());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@TargetApi(AndroidVersions.API_29_ANDROID_10)
|
||||||
|
private static String getHwCodecType(MediaCodecInfo info) {
|
||||||
|
if (info.isSoftwareOnly()) {
|
||||||
|
return "sw";
|
||||||
}
|
}
|
||||||
return builder.toString();
|
if (info.isHardwareAccelerated()) {
|
||||||
|
return "hw";
|
||||||
|
}
|
||||||
|
return "hybrid";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String buildDisplayListMessage() {
|
public static String buildDisplayListMessage() {
|
||||||
@ -174,6 +203,7 @@ public final class LogUtils {
|
|||||||
// 1. system flag (system apps are before non-system apps)
|
// 1. system flag (system apps are before non-system apps)
|
||||||
// 2. name
|
// 2. name
|
||||||
// 3. package name
|
// 3. package name
|
||||||
|
// Comparator.comparing() was introduced in API 24, so it cannot be used here to simplify the code
|
||||||
Collections.sort(apps, (thisApp, otherApp) -> {
|
Collections.sort(apps, (thisApp, otherApp) -> {
|
||||||
// System apps first
|
// System apps first
|
||||||
int cmp = -Boolean.compare(thisApp.isSystem(), otherApp.isSystem());
|
int cmp = -Boolean.compare(thisApp.isSystem(), otherApp.isSystem());
|
||||||
@ -207,9 +237,6 @@ public final class LogUtils {
|
|||||||
builder.append("\n ").append(String.format("%" + column + "s", " "));
|
builder.append("\n ").append(String.format("%" + column + "s", " "));
|
||||||
}
|
}
|
||||||
builder.append(" [").append(app.getPackageName()).append(']');
|
builder.append(" [").append(app.getPackageName()).append(']');
|
||||||
if (!app.isEnabled()) {
|
|
||||||
builder.append(" (disabled)");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return builder.toString();
|
return builder.toString();
|
||||||
|
@ -100,7 +100,7 @@ public class NewDisplayCapture extends SurfaceCapture {
|
|||||||
virtualDisplay = ServiceManager.getDisplayManager()
|
virtualDisplay = ServiceManager.getDisplayManager()
|
||||||
.createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags);
|
.createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags);
|
||||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||||
Ln.i("New display id: " + virtualDisplayId);
|
Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")");
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Ln.e("Could not create display", e);
|
Ln.e("Could not create display", e);
|
||||||
throw new AssertionError("Could not create display");
|
throw new AssertionError("Could not create display");
|
||||||
|
@ -2,19 +2,17 @@ package com.genymobile.scrcpy.video;
|
|||||||
|
|
||||||
import com.genymobile.scrcpy.AndroidVersions;
|
import com.genymobile.scrcpy.AndroidVersions;
|
||||||
import com.genymobile.scrcpy.control.PositionMapper;
|
import com.genymobile.scrcpy.control.PositionMapper;
|
||||||
|
import com.genymobile.scrcpy.device.ConfigurationException;
|
||||||
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.Ln;
|
import com.genymobile.scrcpy.util.Ln;
|
||||||
import com.genymobile.scrcpy.util.LogUtils;
|
import com.genymobile.scrcpy.util.LogUtils;
|
||||||
import com.genymobile.scrcpy.wrappers.DisplayManager;
|
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
import com.genymobile.scrcpy.wrappers.SurfaceControl;
|
||||||
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.hardware.display.VirtualDisplay;
|
import android.hardware.display.VirtualDisplay;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.HandlerThread;
|
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.IDisplayFoldListener;
|
import android.view.IDisplayFoldListener;
|
||||||
import android.view.IRotationWatcher;
|
import android.view.IRotationWatcher;
|
||||||
@ -34,13 +32,6 @@ public class ScreenCapture extends SurfaceCapture {
|
|||||||
private IBinder display;
|
private IBinder display;
|
||||||
private VirtualDisplay virtualDisplay;
|
private VirtualDisplay virtualDisplay;
|
||||||
|
|
||||||
private DisplayManager.DisplayListenerHandle displayListenerHandle;
|
|
||||||
private HandlerThread handlerThread;
|
|
||||||
|
|
||||||
// On Android 14, the DisplayListener may be broken (it never send events). This is fixed in recent Android 14 upgrades, but we can't really know.
|
|
||||||
// So register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from DisplayListener (which proves
|
|
||||||
// that it works).
|
|
||||||
private boolean displayListenerWorks; // only accessed from the display listener thread
|
|
||||||
private IRotationWatcher rotationWatcher;
|
private IRotationWatcher rotationWatcher;
|
||||||
private IDisplayFoldListener displayFoldListener;
|
private IDisplayFoldListener displayFoldListener;
|
||||||
|
|
||||||
@ -54,33 +45,47 @@ public class ScreenCapture extends SurfaceCapture {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void init() {
|
public void init() {
|
||||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
if (displayId == 0) {
|
||||||
registerDisplayListenerFallbacks();
|
rotationWatcher = new IRotationWatcher.Stub() {
|
||||||
}
|
@Override
|
||||||
|
public void onRotationChanged(int rotation) {
|
||||||
handlerThread = new HandlerThread("DisplayListener");
|
|
||||||
handlerThread.start();
|
|
||||||
Handler handler = new Handler(handlerThread.getLooper());
|
|
||||||
displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> {
|
|
||||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
|
||||||
if (!displayListenerWorks) {
|
|
||||||
// On the first display listener event, we know it works, we can unregister the fallbacks
|
|
||||||
displayListenerWorks = true;
|
|
||||||
unregisterDisplayListenerFallbacks();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (this.displayId == displayId) {
|
|
||||||
requestReset();
|
requestReset();
|
||||||
}
|
}
|
||||||
}, handler);
|
};
|
||||||
|
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
|
||||||
|
displayFoldListener = new IDisplayFoldListener.Stub() {
|
||||||
|
|
||||||
|
private boolean first = true;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDisplayFoldChanged(int displayId, boolean folded) {
|
||||||
|
if (first) {
|
||||||
|
// An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding.
|
||||||
|
first = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ScreenCapture.this.displayId != displayId) {
|
||||||
|
// Ignore events related to other display ids
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
requestReset();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void prepare() {
|
public void prepare() throws ConfigurationException {
|
||||||
displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||||
if (displayInfo == null) {
|
if (displayInfo == null) {
|
||||||
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
|
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
|
||||||
throw new AssertionError("Display " + display + " not found");
|
throw new ConfigurationException("Unknown display id: " + displayId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
|
||||||
@ -101,17 +106,16 @@ public class ScreenCapture extends SurfaceCapture {
|
|||||||
virtualDisplay = null;
|
virtualDisplay = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Size displaySize = screenInfo.getVideoSize();
|
|
||||||
|
|
||||||
int virtualDisplayId;
|
int virtualDisplayId;
|
||||||
PositionMapper positionMapper;
|
PositionMapper positionMapper;
|
||||||
try {
|
try {
|
||||||
|
Size videoSize = screenInfo.getVideoSize();
|
||||||
virtualDisplay = ServiceManager.getDisplayManager()
|
virtualDisplay = ServiceManager.getDisplayManager()
|
||||||
.createVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), displayId, surface);
|
.createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface);
|
||||||
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
virtualDisplayId = virtualDisplay.getDisplay().getDisplayId();
|
||||||
Rect contentRect = new Rect(0, 0, displaySize.getWidth(), displaySize.getHeight());
|
Rect contentRect = new Rect(0, 0, videoSize.getWidth(), videoSize.getHeight());
|
||||||
// The position are relative to the virtual display, not the original display
|
// The position are relative to the virtual display, not the original display
|
||||||
positionMapper = new PositionMapper(displaySize, contentRect, 0);
|
positionMapper = new PositionMapper(videoSize, contentRect, 0);
|
||||||
Ln.d("Display: using DisplayManager API");
|
Ln.d("Display: using DisplayManager API");
|
||||||
} catch (Exception displayManagerException) {
|
} catch (Exception displayManagerException) {
|
||||||
try {
|
try {
|
||||||
@ -142,11 +146,12 @@ public class ScreenCapture extends SurfaceCapture {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void release() {
|
public void release() {
|
||||||
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
|
if (rotationWatcher != null) {
|
||||||
unregisterDisplayListenerFallbacks();
|
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
|
||||||
|
}
|
||||||
|
if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) {
|
||||||
|
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
|
||||||
}
|
}
|
||||||
handlerThread.quitSafely();
|
|
||||||
ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle);
|
|
||||||
if (display != null) {
|
if (display != null) {
|
||||||
SurfaceControl.destroyDisplay(display);
|
SurfaceControl.destroyDisplay(display);
|
||||||
display = null;
|
display = null;
|
||||||
@ -186,45 +191,4 @@ public class ScreenCapture extends SurfaceCapture {
|
|||||||
SurfaceControl.closeTransaction();
|
SurfaceControl.closeTransaction();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerDisplayListenerFallbacks() {
|
|
||||||
if (displayId == 0) {
|
|
||||||
rotationWatcher = new IRotationWatcher.Stub() {
|
|
||||||
@Override
|
|
||||||
public void onRotationChanged(int rotation) {
|
|
||||||
Ln.i("=== rotation");
|
|
||||||
requestReset();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
|
|
||||||
displayFoldListener = new IDisplayFoldListener.Stub() {
|
|
||||||
@Override
|
|
||||||
public void onDisplayFoldChanged(int displayId, boolean folded) {
|
|
||||||
if (ScreenCapture.this.displayId != displayId) {
|
|
||||||
// Ignore events related to other display ids
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestReset();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void unregisterDisplayListenerFallbacks() {
|
|
||||||
synchronized (this) {
|
|
||||||
if (rotationWatcher != null) {
|
|
||||||
ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher);
|
|
||||||
rotationWatcher = null;
|
|
||||||
}
|
|
||||||
if (displayFoldListener != null) {
|
|
||||||
// Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14)
|
|
||||||
ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener);
|
|
||||||
displayFoldListener = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -63,30 +63,8 @@ public final class ScreenInfo {
|
|||||||
return unlockedVideoSize.rotate();
|
return unlockedVideoSize.rotate();
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getDeviceRotation() {
|
|
||||||
return deviceRotation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ScreenInfo withDeviceRotation(int newDeviceRotation) {
|
|
||||||
if (newDeviceRotation == deviceRotation) {
|
|
||||||
return this;
|
|
||||||
}
|
|
||||||
// true if changed between portrait and landscape
|
|
||||||
boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0;
|
|
||||||
Rect newContentRect;
|
|
||||||
Size newUnlockedVideoSize;
|
|
||||||
if (orientationChanged) {
|
|
||||||
newContentRect = flipRect(contentRect);
|
|
||||||
newUnlockedVideoSize = unlockedVideoSize.rotate();
|
|
||||||
} else {
|
|
||||||
newContentRect = contentRect;
|
|
||||||
newUnlockedVideoSize = unlockedVideoSize;
|
|
||||||
}
|
|
||||||
return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) {
|
public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) {
|
||||||
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
|
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL || lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) {
|
||||||
// The user requested to lock the video orientation to the current orientation
|
// The user requested to lock the video orientation to the current orientation
|
||||||
lockedVideoOrientation = rotation;
|
lockedVideoOrientation = rotation;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ public abstract class SurfaceCapture {
|
|||||||
/**
|
/**
|
||||||
* Called once before each capture starts, before {@link #getSize()}.
|
* Called once before each capture starts, before {@link #getSize()}.
|
||||||
*/
|
*/
|
||||||
public void prepare() {
|
public void prepare() throws ConfigurationException {
|
||||||
// empty by default
|
// empty by default
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
package com.genymobile.scrcpy.wrappers;
|
package com.genymobile.scrcpy.wrappers;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.AndroidVersions;
|
||||||
import com.genymobile.scrcpy.FakeContext;
|
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;
|
||||||
@ -7,44 +8,23 @@ 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.annotation.TargetApi;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.hardware.display.VirtualDisplay;
|
import android.hardware.display.VirtualDisplay;
|
||||||
import android.os.Handler;
|
|
||||||
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.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.lang.reflect.Proxy;
|
|
||||||
import java.util.regex.Matcher;
|
import java.util.regex.Matcher;
|
||||||
import java.util.regex.Pattern;
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
public final class DisplayManager {
|
public final class DisplayManager {
|
||||||
|
|
||||||
// android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED
|
|
||||||
public static final long EVENT_FLAG_DISPLAY_CHANGED = 1L << 2;
|
|
||||||
|
|
||||||
public interface DisplayListener {
|
|
||||||
/**
|
|
||||||
* Called whenever the properties of a logical {@link android.view.Display},
|
|
||||||
* such as size and density, have changed.
|
|
||||||
*
|
|
||||||
* @param displayId The id of the logical display that changed.
|
|
||||||
*/
|
|
||||||
void onDisplayChanged(int displayId);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static final class DisplayListenerHandle {
|
|
||||||
private final Object displayListenerProxy;
|
|
||||||
private DisplayListenerHandle(Object displayListenerProxy) {
|
|
||||||
this.displayListenerProxy = displayListenerProxy;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
|
private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal
|
||||||
private Method createVirtualDisplayMethod;
|
private Method createVirtualDisplayMethod;
|
||||||
|
private Method requestDisplayPowerMethod;
|
||||||
|
|
||||||
static DisplayManager create() {
|
static DisplayManager create() {
|
||||||
try {
|
try {
|
||||||
@ -161,49 +141,21 @@ public final class DisplayManager {
|
|||||||
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags);
|
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags);
|
||||||
}
|
}
|
||||||
|
|
||||||
public DisplayListenerHandle registerDisplayListener(DisplayListener listener, Handler handler) {
|
private Method getRequestDisplayPowerMethod() throws NoSuchMethodException {
|
||||||
try {
|
if (requestDisplayPowerMethod == null) {
|
||||||
Class<?> displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener");
|
requestDisplayPowerMethod = manager.getClass().getMethod("requestDisplayPower", int.class, boolean.class);
|
||||||
Object displayListenerProxy = Proxy.newProxyInstance(
|
|
||||||
ClassLoader.getSystemClassLoader(),
|
|
||||||
new Class[] {displayListenerClass},
|
|
||||||
(proxy, method, args) -> {
|
|
||||||
if ("onDisplayChanged".equals(method.getName())) {
|
|
||||||
listener.onDisplayChanged((int) args[0]);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
try {
|
|
||||||
manager.getClass()
|
|
||||||
.getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class, String.class)
|
|
||||||
.invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED, FakeContext.PACKAGE_NAME);
|
|
||||||
} catch (NoSuchMethodException e) {
|
|
||||||
try {
|
|
||||||
manager.getClass()
|
|
||||||
.getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class)
|
|
||||||
.invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED);
|
|
||||||
} catch (NoSuchMethodException e2) {
|
|
||||||
manager.getClass()
|
|
||||||
.getMethod("registerDisplayListener", displayListenerClass, Handler.class)
|
|
||||||
.invoke(manager, displayListenerProxy, handler);
|
|
||||||
}
|
}
|
||||||
|
return requestDisplayPowerMethod;
|
||||||
}
|
}
|
||||||
|
|
||||||
return new DisplayListenerHandle(displayListenerProxy);
|
@TargetApi(AndroidVersions.API_35_ANDROID_15)
|
||||||
} catch (Exception e) {
|
public boolean requestDisplayPower(int displayId, boolean on) {
|
||||||
// Rotation and screen size won't be updated, not a fatal error
|
|
||||||
Ln.e("Could not register display listener", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unregisterDisplayListener(DisplayListenerHandle listener) {
|
|
||||||
try {
|
try {
|
||||||
Class<?> displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener");
|
Method method = getRequestDisplayPowerMethod();
|
||||||
manager.getClass().getMethod("unregisterDisplayListener", displayListenerClass).invoke(manager, listener.displayListenerProxy);
|
return (boolean) method.invoke(manager, displayId, on);
|
||||||
} catch (Exception e) {
|
} catch (ReflectiveOperationException e) {
|
||||||
Ln.e("Could not unregister display listener", e);
|
Ln.e("Could not invoke method", e);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,5 @@
|
|||||||
package com.genymobile.scrcpy.control;
|
package com.genymobile.scrcpy.control;
|
||||||
|
|
||||||
import com.genymobile.scrcpy.device.Device;
|
|
||||||
|
|
||||||
import android.view.KeyEvent;
|
import android.view.KeyEvent;
|
||||||
import android.view.MotionEvent;
|
import android.view.MotionEvent;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
@ -285,19 +283,19 @@ public class ControlMessageReaderTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testParseSetScreenPowerMode() throws IOException {
|
public void testParseSetDisplayPower() throws IOException {
|
||||||
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
ByteArrayOutputStream bos = new ByteArrayOutputStream();
|
||||||
DataOutputStream dos = new DataOutputStream(bos);
|
DataOutputStream dos = new DataOutputStream(bos);
|
||||||
dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE);
|
dos.writeByte(ControlMessage.TYPE_SET_DISPLAY_POWER);
|
||||||
dos.writeByte(Device.POWER_MODE_NORMAL);
|
dos.writeBoolean(true);
|
||||||
byte[] packet = bos.toByteArray();
|
byte[] packet = bos.toByteArray();
|
||||||
|
|
||||||
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
ByteArrayInputStream bis = new ByteArrayInputStream(packet);
|
||||||
ControlMessageReader reader = new ControlMessageReader(bis);
|
ControlMessageReader reader = new ControlMessageReader(bis);
|
||||||
|
|
||||||
ControlMessage event = reader.read();
|
ControlMessage event = reader.read();
|
||||||
Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType());
|
Assert.assertEquals(ControlMessage.TYPE_SET_DISPLAY_POWER, event.getType());
|
||||||
Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction());
|
Assert.assertTrue(event.getOn());
|
||||||
|
|
||||||
Assert.assertEquals(-1, bis.read()); // EOS
|
Assert.assertEquals(-1, bis.read()); // EOS
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user