Compare commits

..

26 Commits

Author SHA1 Message Date
Romain Vimont
381fe95867 Document virtual display and "start app" features
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
Romain Vimont
566b5be0f6 Add option to start an app by its name
By adding the '?' prefix, the app is searched by its name instead of its
package name (retrieving app names on the device may take some time):

    scrcpy --start-app=?firefox

An app matches if its label starts with the given name,
case-insensitive.

If '+' is also passed to force-stop the app before starting, then the
prefixes must be in that order:

    scrcpy --start-app=+?firefox

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
Romain Vimont
dd20efa41c Add option to force-stop app before starting
The previous commit introduced:

    scrcpy --start-app=name

By adding a '+' prefix, the app is stopped beforehand:

    scrcpy --start-app=+name

This may be useful to start a fresh app on a new virtual display:

    scrcpy --new-display --start-app=+org.mozilla.firefox

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
Romain Vimont
13ce277e1f Add --start-app
Add a command line option --start-app=name to start an Android app by
its package name. For example:

    scrcpy --start-app=org.mozilla.firefox

The app will be started on the correct target display:

    scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2024-10-28 18:18:36 +01:00
Romain Vimont
9c9d92fb1c Add --list-apps
Add an option to list all apps installed on the device:

    scrcpy --list-apps

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
Romain Vimont
408a388fc5 Reject --new-display for Android <= 10
Fail explicitly if a new virtual display is requested on an Android
version lower than 10.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:18:36 +01:00
Romain Vimont
98ed5eb643 Add virtual display feature
Add a feature to create a new (separate) virtual display instead of
mirroring the device screen:

    scrcpy --new-display=1920x1080
    scrcpy --new-display=1920x1080/420  # force 420 dpi
    scrcpy --new-display         # use the main display size and density
    scrcpy --new-display -m1920  # scaled to fit a max size of 1920
    scrcpy --new-display=/240    # use the main display size and 240 dpi

Fixes #1887 <https://github.com/Genymobile/scrcpy/issues/1887>
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Co-authored-by: anirudhb <anirudhb@users.noreply.github.com>
2024-10-28 18:18:34 +01:00
Romain Vimont
5d0e012a4c Add DPI to DisplayInfo
It will be useful to automatically set an appropriate DPI for new
virtual displays.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
d19396718e Inject display-related events to virtual display
Mouse and touch events must be sent to the virtual display id (used for
mirroring), other events (like key events) must be sent to the original
display id.

Fixes #4598 <https://github.com/Genymobile/scrcpy/issues/4598>
Fixes #5137 <https://github.com/Genymobile/scrcpy/issues/5137>
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>

Co-authored-by: nightmare <mengyanshou@gmail.com>
2024-10-28 18:13:55 +01:00
Romain Vimont
7024d38199 Send PositionMapper to Controller directly
When a new capture starts, send a new PositionMapper to the Controller
without using the global Device as an intermediate.

Now all Device methods are static.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
f1368d9a8f Introduce PositionMapper
Extract the function that converts coordinates from video space to
display space into a separate component.

It only requires the specific data it uses and does not need a full
ScreenInfo object (although it can be created from a ScreenInfo
instance).

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
d916429566 Move clipboard management to Controller
Continue to declutter the global Device.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
7cfefae5e1 Move implicit displayId to Controller
Remove from Device the functions using an implicit displayId.

Move them to Controller, which knows best which displayId it must use.

This will allow to properly dispatch events either to the origin display
or to the virtual display created for mirroring.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
b60e174780 Add capture prepare() step
Add a function called before each capture starts (before getSize() is
called).

This allows to compute the ScreenInfo instance once exactly when needed.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
5851b62580 Simplify virtual display video size
Do not use an unnecessary intermediate Rect object.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
12d5ca4d5e Move local variables in ScreenCapture
Do not initialize variables when they are not used.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
68e54d9b0b Refactor to call getSize() only once
Avoid to call capture.getSize() (provided by the SurfaceCapture
implementation) twice.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
5f0480c039 Ignore first displayFoldChanged event
An event is posted on registration to signal the initial state.

This had no impact when the listener was registered from Device (before
it was moved to ScreenCapture), because this first initial event was
already triggered when ScreenCapture started listening.

But now, it causes the first encoding to be reset immediately. To avoid
that, ignore the first event.

Refs <https://android.googlesource.com/platform/frameworks/base.git/+/refs/tags/android-15.0.0_r3/services/core/java/com/android/server/policy/DisplayFoldController.java#138>
PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
874eaec487 Move screen-related features out of Device.java
Move the code related to screen size and rotation/fold to ScreenCapture.

For now, keep the ScreenInfo instance in the Device class to communicate
with the Controller, but it will be removed by further commits.

PR #5370 <https://github.com/Genymobile/scrcpy/pull/5370>
2024-10-28 18:13:55 +01:00
Romain Vimont
14e5439dee Update mouse documentation about capture key
The mouse capture keys are not hardcoded anymore, they use the
configured shortcut modifiers.

Refs ff9fb5994d
2024-10-28 14:55:55 +01:00
Romain Vimont
a5844e198e Add GitHub Actions release workflow
Fixes #4490 <https://github.com/Genymobile/scrcpy/issues/4490>
PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:31:24 +01:00
Romain Vimont
2687d20280 Rework release.mk for CI
Make it possible to build scrcpy-server and Windows binaries in
parallel from different GitHub Actions workflows, and to package
everything as a final step.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:29:13 +01:00
Romain Vimont
9c0a328498 Build server without meson in release.mk
This avoids to install meson/ninja to build scrcpy-server on the CI.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:38 +01:00
Romain Vimont
02ef3d57ce Split client and server tests in release.mk
This will allow to run server tests separately on the CI.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:26 +01:00
Romain Vimont
538a32a539 Fix .PHONY in release.mk
The prepare-deps recipe does not exist anymore. It has been split into
prepare-deps-win32 and prepare-deps-win64.

PR #5306 <https://github.com/Genymobile/scrcpy/pull/5306>
2024-10-27 20:26:13 +01:00
Romain Vimont
9578aae34e Use explicit constant for @TargetApi 2024-10-22 19:47:56 +02:00
20 changed files with 385 additions and 258 deletions

147
.github/workflows/release.yml vendored Normal file
View 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 }}

View File

@@ -31,6 +31,7 @@ It focuses on:
Its features include:
- [audio forwarding](doc/audio.md) (Android 11+)
- [recording](doc/recording.md)
- [virtual display](doc/virtual_display.md)
- mirroring with [Android device screen off](doc/device.md#turn-screen-off)
- [copy-paste](doc/control.md#copy-paste) in both directions
- [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
```
- 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
file:
@@ -134,6 +141,7 @@ documented in the following pages:
- [Device](doc/device.md)
- [Window](doc/window.md)
- [Recording](doc/recording.md)
- [Virtual display](doc/virtual_displays.md)
- [Tunnels](doc/tunnels.md)
- [OTG](doc/otg.md)
- [Camera](doc/camera.md)

View File

@@ -326,8 +326,9 @@ Examples:
\-\-new\-display=1920x1080
\-\-new\-display=1920x1080/420
\-\-new\-display # default screen size and density
\-\-new\-display=240 # default screen size and 240 dpi
\-\-new\-display # main display size and density
\-\-new\-display -m1920 # scaled to fit a max size of 1920
\-\-new\-display=/240 # main display size and 240 dpi
.TP
.B \-\-no\-audio
@@ -497,7 +498,7 @@ Default is "lalt,lsuper" (left-Alt or left-Super).
.BI "\-\-start\-app " 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

View File

@@ -576,8 +576,9 @@ static const struct sc_option options[] = {
"Examples:\n"
" --new-display=1920x1080\n"
" --new-display=1920x1080/420 # force 420 dpi\n"
" --new-display # default screen size and density\n"
" --new-display=240 # default screen size and 240 dpi",
" --new-display # main display size and density\n"
" --new-display -m1920 # scaled to fit a max size of 1920\n"
" --new-display=/240 # main display size and 240 dpi",
},
{
.longopt_id = OPT_NO_AUDIO,
@@ -812,8 +813,8 @@ static const struct sc_option options[] = {
.argdesc = "name",
.text = "Start an Android app, by its exact package name.\n"
"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):\n"
"given name, case-insensitive (retrieving app names on the "
"device may take some time):\n"
" scrcpy --start-app=?firefox\n"
"Add a '+' prefix to force-stop before starting the app:\n"
" scrcpy --new-display --start-app=+org.mozilla.firefox\n"
@@ -2708,7 +2709,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
break;
case OPT_NEW_DISPLAY:
opts->new_display = optarg ? optarg : "auto";
opts->new_display = optarg ? optarg : "";
break;
case OPT_START_APP:
opts->start_app = optarg;
@@ -2893,6 +2894,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 (!opts->control) {
LOGE("--no-control is not allowed in OTG mode");
@@ -2963,11 +2983,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
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) {
LOGE("Cannot specify both --camera-id and --camera-facing");
return false;

View File

@@ -78,3 +78,48 @@ By default, on start, the device is powered on. To prevent this behavior:
```bash
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
```

View File

@@ -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
from the computer and appears on the Android device instead.
Special capture keys, either <kbd>Alt</kbd> or <kbd>Super</kbd>, toggle
(disable or enable) the mouse capture. Use one of them to give the control of
the mouse back to the computer.
The [shortcut mod](shortcuts.md) (either <kbd>Alt</kbd> or <kbd>Super</kbd> by
default) toggle (disable or enable) the mouse capture. Use one of them to give
the control of the mouse back to the computer.
### UHID

26
doc/virtual_display.md Normal file
View 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
```

View File

@@ -9,13 +9,12 @@
# the server to the device.
.PHONY: default clean \
test \
test test-client test-server \
build-server \
prepare-deps \
prepare-deps-win32 prepare-deps-win64 \
build-win32 build-win64 \
dist-win32 dist-win64 \
zip-win32 zip-win64 \
release
package release
GRADLE ?= ./gradlew
@@ -26,7 +25,7 @@ WIN64_BUILD_DIR := build-win64
VERSION ?= $(shell git describe --tags --exclude='*install-release' --always)
DIST := dist
ZIP := zip
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)
WIN64_TARGET_DIR := scrcpy-win64-$(VERSION)
WIN32_TARGET := $(WIN32_TARGET_DIR).zip
@@ -34,33 +33,28 @@ WIN64_TARGET := $(WIN64_TARGET_DIR).zip
RELEASE_DIR := release-$(VERSION)
release: clean test build-server zip-win32 zip-win64
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)/"
release: clean test build-server build-win32 build-win64 package
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)"
test:
test-client:
[ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \
meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address )
ninja -C "$(TEST_BUILD_DIR)"
test-server:
$(GRADLE) -p server check
test: test-client test-server
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
$(GRADLE) -p server assembleRelease
mkdir -p "$(SERVER_BUILD_DIR)/server"
cp server/build/outputs/apk/release/server-release-unsigned.apk \
"$(SERVER_BUILD_DIR)/server/scrcpy-server"
prepare-deps-win32:
@app/deps/adb.sh win32
@@ -86,6 +80,15 @@ build-win32: prepare-deps-win32
-Dcompile_server=false \
-Dportable=true
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
rm -rf "$(WIN64_BUILD_DIR)"
@@ -99,33 +102,40 @@ build-win64: prepare-deps-win64
-Dcompile_server=false \
-Dportable=true
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
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
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-win32:
mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)"
cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/"
cd "$(ZIP)"; \
zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)"
rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)"
zip-win64: dist-win64
cd "$(DIST)"; \
zip-win64:
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)"
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)/"

View File

@@ -524,8 +524,12 @@ public class Options {
}
private static NewDisplay parseNewDisplay(String newDisplay) {
// input format: "auto", or "<width>x<height>", or "<width>x<height>:<dpi>"
if ("auto".equals(newDisplay)) {
// Possible inputs:
// - "" (empty string)
// - "<width>x<height>/<dpi>"
// - "<width>x<height>"
// - "/<dpi>"
if (newDisplay.isEmpty()) {
return new NewDisplay();
}

View File

@@ -127,6 +127,11 @@ public final class Server {
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");
}
CleanUp cleanUp = null;
Thread initThread = null;

View File

@@ -102,7 +102,6 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
initPointers();
sender = new DeviceMessageSender(controlChannel);
// main display or any display on Android >= Q
supportsInputEvents = Device.supportsInputEvents(displayId);
if (!supportsInputEvents) {
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 old = this.displayData.getAndSet(data);
if (old == null) {
// The very first time the Controller is notified of a new virtual display
synchronized (displayDataAvailable) {
displayDataAvailable.notify();
}
@@ -348,7 +348,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
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")
DisplayData displayData = this.displayData.get();
assert displayData != null : "Cannot receive a touch event without a display";
@@ -462,7 +462,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener {
private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) {
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")
DisplayData displayData = this.displayData.get();
assert displayData != null : "Cannot receive a scroll event without a display";

View File

@@ -20,7 +20,9 @@ public final class PositionMapper {
}
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) {

View File

@@ -52,6 +52,7 @@ public final class Device {
}
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;
}
@@ -228,7 +229,7 @@ public final class Device {
private static List<ApplicationInfo> getLaunchableApps(PackageManager pm) {
List<ApplicationInfo> result = new ArrayList<>();
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);
}
}
@@ -248,7 +249,7 @@ public final class Device {
private static DeviceApp toApp(PackageManager pm, ApplicationInfo appInfo) {
String name = pm.getApplicationLabel(appInfo).toString();
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")
@@ -271,12 +272,10 @@ public final class Device {
PackageManager pm = FakeContext.get().getPackageManager();
for (ApplicationInfo appInfo : getLaunchableApps(pm)) {
if (appInfo.enabled) {
String name = pm.getApplicationLabel(appInfo).toString();
if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) {
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
result.add(new DeviceApp(appInfo.packageName, name, system, appInfo.enabled));
}
String name = pm.getApplicationLabel(appInfo).toString();
if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) {
boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
result.add(new DeviceApp(appInfo.packageName, name, system));
}
}

View File

@@ -5,13 +5,11 @@ public final class DeviceApp {
private final String packageName;
private final String name;
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.name = name;
this.system = system;
this.enabled = enabled;
}
public String getPackageName() {
@@ -25,8 +23,4 @@ public final class DeviceApp {
public boolean isSystem() {
return system;
}
public boolean isEnabled() {
return enabled;
}
}

View File

@@ -174,6 +174,7 @@ public final class LogUtils {
// 1. system flag (system apps are before non-system apps)
// 2. 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) -> {
// System apps first
int cmp = -Boolean.compare(thisApp.isSystem(), otherApp.isSystem());
@@ -207,9 +208,6 @@ public final class LogUtils {
builder.append("\n ").append(String.format("%" + column + "s", " "));
}
builder.append(" [").append(app.getPackageName()).append(']');
if (!app.isEnabled()) {
builder.append(" (disabled)");
}
}
return builder.toString();

View File

@@ -100,7 +100,7 @@ public class NewDisplayCapture extends SurfaceCapture {
virtualDisplay = ServiceManager.getDisplayManager()
.createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags);
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) {
Ln.e("Could not create display", e);
throw new AssertionError("Could not create display");

View File

@@ -2,19 +2,17 @@ package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AndroidVersions;
import com.genymobile.scrcpy.control.PositionMapper;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.IBinder;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher;
@@ -34,13 +32,6 @@ public class ScreenCapture extends SurfaceCapture {
private IBinder display;
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 IDisplayFoldListener displayFoldListener;
@@ -54,33 +45,47 @@ public class ScreenCapture extends SurfaceCapture {
@Override
public void init() {
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
registerDisplayListenerFallbacks();
if (displayId == 0) {
rotationWatcher = new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
requestReset();
}
};
ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId);
}
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 (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();
}
}
if (this.displayId == displayId) {
requestReset();
}
}, handler);
};
ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener);
}
}
@Override
public void prepare() {
public void prepare() throws ConfigurationException {
displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
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) {
@@ -101,17 +106,16 @@ public class ScreenCapture extends SurfaceCapture {
virtualDisplay = null;
}
Size displaySize = screenInfo.getVideoSize();
int virtualDisplayId;
PositionMapper positionMapper;
try {
Size videoSize = screenInfo.getVideoSize();
virtualDisplay = ServiceManager.getDisplayManager()
.createVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), displayId, surface);
.createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface);
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
positionMapper = new PositionMapper(displaySize, contentRect, 0);
positionMapper = new PositionMapper(videoSize, contentRect, 0);
Ln.d("Display: using DisplayManager API");
} catch (Exception displayManagerException) {
try {
@@ -142,11 +146,12 @@ public class ScreenCapture extends SurfaceCapture {
@Override
public void release() {
if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) {
unregisterDisplayListenerFallbacks();
if (rotationWatcher != null) {
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) {
SurfaceControl.destroyDisplay(display);
display = null;
@@ -186,45 +191,4 @@ public class ScreenCapture extends SurfaceCapture {
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;
}
}
}
}

View File

@@ -63,28 +63,6 @@ public final class ScreenInfo {
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) {
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
// The user requested to lock the video orientation to the current orientation

View File

@@ -45,7 +45,7 @@ public abstract class SurfaceCapture {
/**
* Called once before each capture starts, before {@link #getSize()}.
*/
public void prepare() {
public void prepare() throws ConfigurationException {
// empty by default
}

View File

@@ -9,40 +9,17 @@ import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.content.Context;
import android.hardware.display.VirtualDisplay;
import android.os.Handler;
import android.view.Display;
import android.view.Surface;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
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 Method createVirtualDisplayMethod;
@@ -160,50 +137,4 @@ public final class DisplayManager {
android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get());
return dm.createVirtualDisplay(name, width, height, dpi, surface, flags);
}
public DisplayListenerHandle registerDisplayListener(DisplayListener listener, Handler handler) {
try {
Class<?> displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener");
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 new DisplayListenerHandle(displayListenerProxy);
} catch (Exception e) {
// 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 {
Class<?> displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener");
manager.getClass().getMethod("unregisterDisplayListener", displayListenerClass).invoke(manager, listener.displayListenerProxy);
} catch (Exception e) {
Ln.e("Could not unregister display listener", e);
}
}
}