Compare commits

...

109 commits
v2.1.2 ... dev

Author SHA1 Message Date
Leonardo Chen
02ffb264c9 fix: windows build error
Some checks failed
Windows / Build (push) Has been cancelled
MacOS / Build (push) Has been cancelled
Ubuntu / Build (push) Has been cancelled
2025-04-10 08:42:47 +08:00
barry
314bd4f613 ui: update i18
Some checks failed
MacOS / Build (push) Has been cancelled
Ubuntu / Build (push) Has been cancelled
Windows / Build (push) Has been cancelled
2025-03-09 15:51:50 +08:00
barry
6f0f9447da feat: add for QuickAssistant 2025-03-09 15:46:12 +08:00
barry
cfe79c7d5a feat: config support set language 2025-03-09 15:40:06 +08:00
barry
dbf25166ea feat: add for QuickAssistant 2025-03-09 15:27:31 +08:00
Liu Jinchang
f5cccac0df feat: add IP history record feature
- Implement IP address history storage
- Add mechanism to record connection timestamps
- Ensure unique IP entries in history

Log: Add functionality to store and track IP address history

Issue: https://github.com/barry-ran/QtScrcpy/issues/1075
2025-03-06 14:18:13 +08:00
rankun
224f04ffa0 feat: add star badge 2025-03-04 17:21:22 +08:00
barry
a8d3609c19 feat: ad for QuickMirror
Some checks failed
MacOS / Build (push) Has been cancelled
Ubuntu / Build (push) Has been cancelled
Windows / Build (push) Has been cancelled
2025-02-21 22:25:18 +08:00
rankun
790f422f99 fix: not found adb on windows
Some checks are pending
MacOS / Build (push) Waiting to run
Ubuntu / Build (push) Waiting to run
Windows / Build (push) Waiting to run
2025-02-21 13:28:25 +08:00
re2zero
c1faff820d fix: #1117 Fix phone window show blank if run with Qt6
It needs to bind every time when GL paint on Qt6, and it works on Qt5 too.

Log: Fix phone window show blank if run with Qt6.
2025-02-21 11:11:11 +08:00
barry
5fa18219b6 fix: qt6 build error 2025-02-21 11:11:11 +08:00
barry
8196e46648 feat: update QtScrcpyCore 2025-02-21 11:11:11 +08:00
rankun
f863a91f94 feat: mac support arm64 2025-02-21 11:11:11 +08:00
rankun
ae1523b2a0 chore: add build annotation 2025-02-21 11:11:11 +08:00
re2zero
96fc6bfdf7 feat: Enable Qt6 build
Enable build with Qt6 and compat Qt5, the user can select which Qt version by configuration parameter.

Log: Enable Qt6 build.
2025-02-21 11:11:11 +08:00
barry
c1cb2dad3b fix: package x64 failed on mac arm 2025-02-21 11:11:11 +08:00
barry
7d1fdf4965 fix: record&screenshot failed on wireless device
Some checks failed
MacOS / Build (push) Has been cancelled
Ubuntu / Build (push) Has been cancelled
Windows / Build (push) Has been cancelled
2025-01-11 02:42:38 +08:00
rankun
f143c50596 feat: update config.ini server 3.1 2025-01-11 02:42:38 +08:00
rankun
97be13ec82 feat: update scrcpy-server 3.1 2025-01-11 02:42:38 +08:00
AbdAlrahman Ghanem
072d6c7c6c docs: add ScrcpyKeyMapper tool to key mapping documentation 2025-01-11 02:42:38 +08:00
AbdAlrahman Ghanem
49378f540c feat: add configurable drag speed and delay
Added new features to drag functionality:
- Added startDelay parameter to specify delay before drag movement
- Added dragSpeed parameter to control movement speed
- Updated documentation in both English and Chinese

This allows users to fine-tune both the timing and speed of drag movements.
2025-01-11 02:42:38 +08:00
Barry
ec5f413a38
Merge pull request #1073 from barry-ran/dev
Some checks failed
MacOS / Build (push) Has been cancelled
Ubuntu / Build (push) Has been cancelled
Windows / Build (push) Has been cancelled
sync dev
2024-12-07 17:26:04 +08:00
barry
16d693a618 fix: mac only run macos14 2024-12-07 17:04:00 +08:00
barry
8e8df04213 refactor: 适配3.0.2的电源接口 2024-12-07 12:25:47 +08:00
Barry
0a9871290f
Merge pull request #1071 from barry-ran/dev
Some checks are pending
MacOS / Build (push) Waiting to run
Ubuntu / Build (push) Waiting to run
Windows / Build (push) Waiting to run
sync dev
2024-12-07 00:37:30 +08:00
barry
723b4efb0f fix: wireles connect btn too big
Some checks are pending
MacOS / Build (push) Waiting to run
Ubuntu / Build (push) Waiting to run
Windows / Build (push) Waiting to run
2024-12-07 00:24:44 +08:00
barry
a60c2ee906 chore: update ci action 2024-12-07 00:05:37 +08:00
barry
43f5747cfe chore: update ci mac 14 2024-12-07 00:02:18 +08:00
barry
9e613986bf chore: update upload-artifact v4 2024-12-06 23:51:41 +08:00
barry
7c0b32de9a feat: add star history 2024-12-06 23:49:07 +08:00
barry
d0d6356f37 fix: 适配3.0.2的锁定采集方向 2024-12-06 23:43:53 +08:00
barry
9fa4b6672c fix: 录制最后一帧pts不对 2024-12-06 22:25:28 +08:00
barry
1496b767b7 feat: 更新scrcpy-server 3.0.2 2024-12-06 22:03:30 +08:00
Fanxing
5354ae0173
feat: Option to show/hide the toolbar when connecting a device. (#1042)
Some checks are pending
MacOS / Build (push) Waiting to run
Ubuntu / Build (push) Waiting to run
Windows / Build (push) Waiting to run
* Option to show/hide the toolbar when connecting a device.

* Modify the code as required.
2024-12-06 16:33:15 +08:00
Barry
076f9ba4c7
Merge pull request #1000 from teohhanhui/patch-1
Fix outdated link to GitHub Actions workflow runs
2024-08-03 14:03:15 +08:00
Teoh Han Hui
8baa406bc6
Fix outdated link to GitHub Actions workflow runs 2024-07-09 17:12:45 +08:00
Barry
05bfbe6e5c
docs: update README_zh.md 2024-05-26 16:53:01 +08:00
rankun
bc2687df7c feat: update core 2024-05-11 15:40:11 +08:00
Barry
ae1851abec Merge pull request #944 from barry-ran/dev
sync dev
2024-04-08 15:07:49 +08:00
Barry
971e94b682 feat: update server 2.4 2024-04-08 15:07:00 +08:00
Barry
e7125d80b7 chore: remove server code 2024-04-08 15:06:48 +08:00
Barry
6147963a1c Merge pull request #943 from barry-ran/dev
merge dev
2024-04-08 14:43:27 +08:00
Barry
4f50092fb1 fix: build error on version repeat 2024-04-08 14:39:25 +08:00
Barry
44575ff658 fix: build error with xcode 2024-04-08 14:17:09 +08:00
Barry
9bfe67455b fix: build error with version repeat 2024-04-08 14:16:48 +08:00
Barry
5b7b54bad9 fix: QtScrcpyCore mac编译错误 2024-04-08 14:14:34 +08:00
Barry
1aa191764e fix: record failed 2024-04-08 13:16:09 +08:00
Barry
5537a15a48 Merge pull request #909 from barry-ran/dev
sync
2024-02-25 17:58:52 +08:00
barry
00a4882263 feat: update ci 2024-02-25 17:56:37 +08:00
barry
1107980188 feat: aad quickmirror 2024-02-25 16:45:36 +08:00
barry
e07a69737e feat: 更新scrcpy-server 2.1.1 2024-02-25 16:32:11 +08:00
barry
332e36972b feat: add quickmirror des 2023-10-23 01:35:51 +08:00
Barry
035dc51d46 Merge pull request #770 from FrzMtrsprt/kill_tray_message
fix: don't show tray message twice
2023-03-20 12:07:27 +08:00
FrzMtrsprt
9a3cfc62f9 fix: build 2023-02-26 00:31:33 +08:00
FrzMtrsprt
4aa8cebcd9 fix: save tray message shown status 2023-02-26 00:22:43 +08:00
FrzMtrsprt
a09a59b52d fix: don't show tray message twice 2023-02-25 23:53:24 +08:00
Barry
e701d959ca Merge pull request #715 from barry-ran/dev
sync dev
2022-10-31 10:39:15 +08:00
rankun
587c1f5872 fix: config path error on mac 2022-10-30 19:25:26 +08:00
Barry
f1e24fe81a Merge pull request #706 from barry-ran/fix/sanxing_bug
fix: samsung error stack corruption detected
2022-10-21 12:16:43 +08:00
Barry
bdcd9c227f fix: samsung error stack corruption detected 2022-10-21 11:44:58 +08:00
Barry
0d0a7edf27 Merge pull request #701 from barry-ran/dev
sync dev
2022-10-18 10:11:34 +08:00
Barry
3658ebbef0 docs: update readme 2022-10-18 10:10:11 +08:00
Barry
1f776d842f docs: update readme 2022-10-18 10:07:39 +08:00
Barry
4b0e42c285 fix: server start failed on samsung 2022-10-18 10:03:41 +08:00
Barry
79a184145e Merge pull request #690 from FrzMtrsprt/dark_border
Enable dark window border on Windows
2022-10-17 11:16:24 +08:00
Barry
fa943f75a3 Merge pull request #699 from barry-ran/dev
fix: mac read client version is 1.21
2022-10-17 11:10:08 +08:00
Barry
8c0b3d4870 fix: mac read client version is 1.21 2022-10-17 10:45:09 +08:00
FrzMtrsprt
ab4fea7b03 Merge branch 'dark_border' of https://github.com/FrzMtrsprt/QtScrcpy into dark_border 2022-10-16 21:30:47 +08:00
FrzMtrsprt
5a31a8fa48 Merge branch 'barry-ran:dev' into dark_border 2022-10-16 21:30:18 +08:00
FrzMtrsprt
bd51fa19a0 Fix build error 2022-10-16 21:29:47 +08:00
Barry
2dc9189473 Merge pull request #696 from barry-ran/dev
merge dev
2022-10-16 19:26:27 +08:00
Barry
01bbff8fe4 fix: build error 2022-10-16 19:09:10 +08:00
Barry
c0ac2de19e feat: update server 1.24 2022-10-16 14:29:59 +08:00
FrzMtrsprt
f5380bc514 Enable dark window border on Windows 2022-10-07 12:20:14 +08:00
Barry
ca55caa0f0 Merge pull request #666 from re2zero/dev
feat: enable back to original size after exit fullscreen.
2022-09-14 10:03:50 +08:00
re2zero
ef2e822c13 Merge branch 'barry-ran:dev' into dev 2022-09-13 19:05:46 +08:00
Barry
b44edf076a fix: drag ui delay recv video 2022-09-12 21:53:53 +08:00
Barry
c237a17b06 feat: update server source code 2022-09-12 10:35:19 +08:00
Barry
3fd25c367f fix: record failed 2022-09-12 10:17:46 +08:00
Barry
5a20373c88 Merge pull request #656 from UjhhgtgTeams/dev
Set up Linux Github Actions Build & Add Linux Build Instructions & Update Readme
2022-09-12 10:15:37 +08:00
Ujhhgtg
45fbbbf813 scrcpy-server: update to 1.24
Signed-off-by: Ujhhgtg <feyxiexzf@gmail.com>
2022-08-22 15:06:10 +08:00
Ujhhgtg
ab3541a8a8 gh-actions: remove gen-ver as it causes problems 2022-08-22 14:56:50 +08:00
Ujhhgtg
cb73720d74 ci: fix a problem caused by renaming 2022-08-22 14:28:28 +08:00
Ujhhgtg
5e328a7be7 docs + ci: update readme and rename linux build script as i have tested it on many platforms 2022-08-22 14:16:53 +08:00
YangWu
56b7c03748 feat: enable back to original size after exit fullscreen.
After exit fullscreen, it still show as fullscreen with title, this make user feel unwell. Record the normal size and then recover will be beter.

Log: support recover size.
2022-08-18 10:07:45 +08:00
Ujhhgtg
1af89bcb04 gh-actions: use relwithdebinfo for actions and release for releases 2022-08-08 12:43:53 +08:00
Ujhhgtg
c7e0727e5d ci / gh-actions : update linux build script
The script is tested on my computer with zsh, qt 5.15.2
2022-08-08 11:48:39 +08:00
Ujhhgtg
eb3579482d docs: update chinese readme 2022-08-06 13:25:45 +00:00
Ujhhgtg
b66b1b6e71 docs: update readme again 2022-08-06 13:25:35 +00:00
Ujhhgtg
2e2423c6a1 docs: update readme 2022-08-06 13:12:46 +00:00
Ujhhgtg
e8d073dfae gh-actions: fix & update actions 2022-08-06 13:00:09 +00:00
Ujhhgtg
26b0c896ad gh-actions: try to standardize the process 2022-08-06 12:51:14 +00:00
Ujhhgtg
90dfcbd075 gh-actions: fix grammar 2022-08-06 12:32:39 +00:00
Ujhhgtg
29e7654c38 gh-actions: make artifacts' name more clear 2022-08-06 12:30:26 +00:00
Ujhhgtg
21f67a0aa5 gh-actions: update qt 2022-08-06 12:26:58 +00:00
Ujhhgtg
ab4175489d [Github Actions] Capture linux build artifacts 2022-08-06 12:17:24 +00:00
Barry
d1d2f0454f Merge pull request #640 from barry-ran/dev
sync
2022-07-10 14:53:56 +08:00
Barry
e253e63b00 Merge pull request #638 from Zeroo28/patch-1
Improve documentation
2022-07-10 09:23:05 +08:00
Zeroo
86d20e653b Merge branch 'patch-1' of https://github.com/Zeroo28/QtScrcpy into patch-1 2022-07-06 08:34:28 +07:00
Zeroo
0158707fb5 docs: fixes grammar mistakes and typos
_I have used Grammarly to fixed them_
2022-07-06 08:32:34 +07:00
Zeroo
d6896e30df revert(vcs): removed .vscode directory from .gitignore 2022-07-06 08:26:46 +07:00
Zeroo
d956c1fa68 Revert "docs(readme): Fixed 'build' category link"
This reverts commit b1dbbfcdb0.
2022-06-29 16:32:09 +07:00
Zeroo
44e815f373 chore: removed .vscode directory
Removed vscode's config directory from version control.
2022-06-29 14:16:38 +07:00
Zeroo
6bddc2de95 docs(readme): Fixed 'build' category link 2022-06-29 13:39:32 +07:00
Barry
2a4093a487 Merge pull request #628 from barry-ran/dev
sync
2022-06-20 13:27:06 +08:00
Barry
d5e915d404 Merge pull request #621 from barry-ran/dev
refactor: add QtScrcpyCore submodule
2022-06-09 07:40:35 +08:00
Barry
bb43261872 Merge pull request #600 from barry-ran/dev
feat: adjust ui
2022-04-09 17:35:25 +08:00
Barry
44d2825a0d Merge pull request #562 from barry-ran/dev
Dev
2022-01-16 22:34:31 +08:00
Barry
fc400fabd4 Merge pull request #545 from barry-ran/dev
Dev
2022-01-09 17:11:41 +08:00
96 changed files with 1185 additions and 5694 deletions

View file

@ -13,12 +13,23 @@ on:
jobs:
build:
name: Build
runs-on: macos-10.15
# install-qt-action在arm上执行macdeployqt会报parse otool错误所以在intel mac上执行:
# 用qt6时在arm mac上编译arm和intel都没有问题
# qt5+intel mac编译intel没问题
# qt5+arm mac编译intel会报错
# https://github.com/actions/runner-images?tab=readme-ov-file#available-images
runs-on: macos-13
strategy:
matrix:
qt-ver: [5.15.1]
qt-arch-install: [clang_64]
clang-arch: [x64]
qt-ver: [5.15.2, 6.5.3]
# 配置qt-ver的额外设置qt-arch-installbuild-arch
include:
- qt-ver: 5.15.2
qt-arch-install: clang_64
build-arch: x64
- qt-ver: 6.5.3
qt-arch-install: arm64
build-arch: arm64
env:
target-name: QtScrcpy
qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }}
@ -26,15 +37,23 @@ jobs:
steps:
- name: Cache Qt
id: cache-qt
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }}
- name: Install Qt
uses: jurplel/install-qt-action@v2.13.0
- name: Install Qt5
if: startsWith(matrix.qt-ver, '5.')
uses: jurplel/install-qt-action@v4.1.1
with:
version: ${{ matrix.qt-ver }}
cached: ${{ steps.cache-qt.outputs.cache-hit }}
- name: Install Qt6
if: startsWith(matrix.qt-ver, '6.')
uses: jurplel/install-qt-action@v4.1.1
with:
version: ${{ matrix.qt-ver }}
modules: qtmultimedia
cached: ${{ steps.cache-qt.outputs.cache-hit }}
- uses: actions/checkout@v2
with:
fetch-depth: 0
@ -46,7 +65,7 @@ jobs:
ENV_QT_PATH: ${{ env.qt-install-path }}
run: |
python ci/generate-version.py
ci/mac/build_for_mac.sh RelWithDebInfo
ci/mac/build_for_mac.sh RelWithDebInfo ${{ matrix.build-arch }}
# 获取ref最后一个/后的内容
- name: Get the version
shell: bash
@ -58,17 +77,17 @@ jobs:
id: package
env:
ENV_QT_PATH: ${{ env.qt-install-path }}
publish_name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.clang-arch }}-${{ steps.get-version.outputs.version }}
publish_name: ${{ env.target-name }}-${{ env.plantform-des }}-${{ matrix.build-arch }}-Qt${{matrix.qt-ver}}-${{ steps.get-version.outputs.version }}
run: |
ci/mac/publish_for_mac.sh ../build
ci/mac/publish_for_mac.sh ../build ${{ matrix.build-arch }}
ci/mac/package_for_mac.sh
mv ci/build/QtScrcpy.app ci/build/${{ env.publish_name }}.app
mv ci/build/QtScrcpy.dmg ci/build/${{ env.publish_name }}.dmg
echo "::set-output name=package-name::${{ env.publish_name }}"
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.package-name }}.zip
path: ci/build/${{ steps.package.outputs.package-name }}.app
path: ci/build/${{ steps.package.outputs.package-name }}.dmg
# Upload to release
- name: Upload Release
if: startsWith(github.ref, 'refs/tags/')

View file

@ -1,5 +1,4 @@
name: Ubuntu
# Qt官方没有linux平台的x86包
on:
push:
paths:
@ -19,7 +18,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-22.04]
qt-ver: [5.15.1]
qt-ver: [5.15.2]
qt-arch-install: [gcc_64]
gcc-arch: [x64]
env:
@ -27,27 +26,56 @@ jobs:
qt-install-path: ${{ github.workspace }}/${{ matrix.qt-ver }}
plantform-des: ubuntu
steps:
- name: Cache Qt
id: cache-qt
uses: actions/cache@v1
with:
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }}
- name: Install Qt
uses: jurplel/install-qt-action@v2.13.0
uses: jurplel/install-qt-action@v4.1.1
with:
version: ${{ matrix.qt-ver }}
cached: ${{ steps.cache-qt.outputs.cache-hit }}
- name: Ubuntu install GL library
- name: Cache Qt
id: cache-qt
uses: actions/cache@v4
with:
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch-install }}
- name: Install GL library
run: sudo apt-get install -y libglew-dev libglfw3-dev
- uses: actions/checkout@v2
with:
fetch-depth: 0
submodules: 'true'
ssh-key: ${{ secrets.BOT_SSH_KEY }}
- name: Build Ubuntu
- name: Build RelWithDebInfo
env:
ENV_QT_PATH: ${{ env.qt-install-path }}
run: |
python ci/generate-version.py
ci/linux/build_for_ubuntu.sh RelWithDebInfo
ci/linux/build_for_linux.sh "RelWithDebInfo"
- name: Upload RelWithDebInfo
uses: actions/upload-artifact@v4
with:
name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}-RelWithDebInfo
path: output/x64/RelWithDebInfo/*
- name: Build Release
env:
ENV_QT_PATH: ${{ env.qt-install-path }}
run: |
ci/linux/build_for_linux.sh "Release"
- name: Upload Release
uses: actions/upload-artifact@v4
with:
name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}-Release
path: output/x64/Release/*
- name: Install the zip utility
run: |
sudo apt install zip -y
- name: Zip the Artifacts
run: |
zip -r QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip output/x64/Release
- name: Upload to Releases
if: startsWith(github.ref, 'refs/tags/')
uses: svenstaro/upload-release-action@2.3.0
with:
file: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip
asset_name: QtScrcpy-${{ matrix.os }}-${{ matrix.qt-arch-install }}.zip
repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }}
overwrite: true

View file

@ -24,7 +24,7 @@ jobs:
# 矩阵配置 https://help.github.com/en/actions/reference/workflow-syntax-for-github-actions#jobsjob_idstrategymatrix
strategy:
matrix:
qt-ver: [5.15.1]
qt-ver: [5.15.2]
qt-arch: [win64_msvc2019_64, win32_msvc2019]
# 配置qt-arch的额外设置msvc-archqt-arch-install
include:
@ -47,14 +47,14 @@ jobs:
steps:
- name: Cache Qt
id: cache-qt
uses: actions/cache@v1
uses: actions/cache@v4
with:
path: ${{ env.qt-install-path }}/${{ matrix.qt-arch-install }}
key: ${{ runner.os }}/${{ matrix.qt-ver }}/${{ matrix.qt-arch }}
# 安装Qt
- name: Install Qt
# 使用外部action。这个action专门用来安装Qt
uses: jurplel/install-qt-action@v2.13.0
uses: jurplel/install-qt-action@v4.1.1
with:
# Version of Qt to install
version: ${{ matrix.qt-ver }}
@ -100,7 +100,7 @@ jobs:
echo "::set-output name=package-name::${{ env.publish_name }}"
# 上传artifacts
# https://help.github.com/en/actions/configuring-and-managing-workflows/persisting-workflow-data-using-artifacts
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v4
with:
name: ${{ steps.package.outputs.package-name }}.zip
path: ci\build\${{ steps.package.outputs.package-name }}

3
.gitignore vendored
View file

@ -13,4 +13,5 @@
build-*
*.DS_Store
userdata.ini
Info_Mac.plist
Info_Mac.plist
/ci/build_temp

View file

@ -11,7 +11,7 @@ cmake_minimum_required(VERSION 3.19 FATAL_ERROR)
# QC Custom config
set(QC_PROJECT_NAME "QtScrcpy")
# Read version numbers from file
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/version QC_FILE_VERSION)
file(STRINGS ${CMAKE_CURRENT_SOURCE_DIR}/appversion QC_FILE_VERSION)
set(QC_PROJECT_VERSION ${QC_FILE_VERSION})
# Project declare
@ -26,6 +26,19 @@ if(CMAKE_SIZEOF_VOID_P EQUAL 8)
else()
set(QC_CPU_ARCH x86)
endif()
# MacOS
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# mac default arch arm64
if(NOT CMAKE_OSX_ARCHITECTURES)
set(CMAKE_OSX_ARCHITECTURES arm64)
endif()
if (CMAKE_OSX_ARCHITECTURES MATCHES "arm64")
set(QC_CPU_ARCH arm64)
endif()
endif()
message(STATUS "[${PROJECT_NAME}] CPU_ARCH:${QC_CPU_ARCH}")
# CMake set
@ -52,8 +65,9 @@ if (MSVC)
add_compile_options(/W3 /WX /wd4566)
# avoid warning C4819
add_compile_options(-source-charset:utf-8)
#add_compile_options(/utf-8)
#add_compile_options(-source-charset:utf-8)
# /utf-8 will set source charset and execution charset to utf-8, so we don't need to set source-charset:utf-8
add_compile_options(/utf-8)
# ensure we use minimal "windows.h" lib without the crazy min max macros
add_compile_definitions(NOMINMAX WIN32_LEAN_AND_MEAN)
@ -75,18 +89,49 @@ endif()
# Qt
#
# Find Qt version
if (NOT QT_DESIRED_VERSION)
find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core)
message(" >>> Found Qt version: ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}.${QT_VERSION_PATCH}")
set(QT_DESIRED_VERSION ${QT_VERSION_MAJOR})
endif()
set(CMAKE_AUTOUIC ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTORCC ON)
find_package(QT NAMES Qt6 Qt5 COMPONENTS Widgets Network Multimedia REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS Widgets Network Multimedia REQUIRED)
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(QT NAMES Qt6 Qt5 COMPONENTS X11Extras REQUIRED)
find_package(Qt${QT_VERSION_MAJOR} COMPONENTS X11Extras REQUIRED)
set(qt_required_components Widgets Network Multimedia)
if (QT_DESIRED_VERSION MATCHES 6)
# list(APPEND qt_required_components Core5Compat)
list(APPEND qt_required_components OpenGL)
list(APPEND qt_required_components OpenGLWidgets)
else()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
list(APPEND qt_required_components X11Extras )
endif()
endif()
message(STATUS "[${PROJECT_NAME}] Qt version is: ${QT_VERSION_MAJOR}.${QT_VERSION_MINOR}")
find_package(Qt${QT_DESIRED_VERSION} REQUIRED COMPONENTS ${qt_required_components})
set(LINK_LIBS
Qt${QT_DESIRED_VERSION}::Widgets
Qt${QT_DESIRED_VERSION}::Network
Qt${QT_DESIRED_VERSION}::Multimedia
)
if (QT_DESIRED_VERSION MATCHES 6)
# list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::Core5Compat)
list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::GuiPrivate)
list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::OpenGL)
list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::OpenGLWidgets)
else()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
list(APPEND LINK_LIBS Qt${QT_DESIRED_VERSION}::X11Extras)
endif()
endif()
message(STATUS "[${PROJECT_NAME}] Qt version is: ${QT_DESIRED_VERSION}")
#
# Sources
@ -149,6 +194,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES}
util/mousetap/winmousetap.h
util/mousetap/winmousetap.cpp
util/winutils.h
util/winutils.cpp
)
endif()
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
@ -161,6 +208,8 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
set(QC_UTIL_SOURCES ${QC_UTIL_SOURCES}
util/mousetap/cocoamousetap.h
util/mousetap/cocoamousetap.mm
util/path.h
util/path.mm
)
endif()
source_group(util FILES ${QC_UTIL_SOURCES})
@ -259,6 +308,9 @@ endif()
# MacOS
if(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# qt6 need 10.15 or later
set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15")
# copy bundle file
get_target_property(MACOS_BUNDLE_PATH ${PROJECT_NAME} RUNTIME_OUTPUT_DIRECTORY)
set(MACOS_BUNDLE_PATH ${MACOS_BUNDLE_PATH}/${PROJECT_NAME}.app/Contents)
@ -308,8 +360,6 @@ if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
find_package(Threads REQUIRED)
target_link_libraries(${PROJECT_NAME} PRIVATE
# qx11
Qt${QT_VERSION_MAJOR}::X11Extras
# xcb https://doc.qt.io/qt-5/linux-requirements.html
xcb
# pthread
@ -327,8 +377,6 @@ add_subdirectory(QtScrcpyCore)
# Qt
target_link_libraries(${PROJECT_NAME} PRIVATE
Qt${QT_VERSION_MAJOR}::Widgets
Qt${QT_VERSION_MAJOR}::Network
Qt${QT_VERSION_MAJOR}::Multimedia
${LINK_LIBS}
QtScrcpyCore
)

@ -1 +1 @@
Subproject commit 3004e63935fe8a3e57b91e117a91c1a6aa68ae42
Subproject commit 19e1ba8fb5c59c5a85c3c6a79967fab4c84739c7

View file

@ -4,11 +4,23 @@
#include <QTime>
#include <QElapsedTimer>
#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
#include <QAudioSink>
#include <QAudioDevice>
#include <QMediaDevices>
#endif
#include "audiooutput.h"
AudioOutput::AudioOutput(QObject *parent)
: QObject(parent)
{
m_running = false;
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
m_audioOutput = nullptr;
#else
m_audioSink = nullptr;
#endif
connect(&m_sndcpy, &QProcess::readyReadStandardOutput, this, [this]() {
qInfo() << QString("AudioOutput::") << QString(m_sndcpy.readAllStandardOutput());
});
@ -69,15 +81,10 @@ bool AudioOutput::runSndcpyProcess(const QString &serial, int port, bool wait)
}
#ifdef Q_OS_WIN32
QStringList params;
params << serial;
params << QString("%1").arg(port);
QStringList params{serial, QString::number(port)};
m_sndcpy.start("sndcpy.bat", params);
#else
QStringList params;
params << "sndcpy.sh";
params << serial;
params << QString("%1").arg(port);
QStringList params{"sndcpy.sh", serial, QString::number(port)};
m_sndcpy.start("bash", params);
#endif
@ -86,11 +93,11 @@ bool AudioOutput::runSndcpyProcess(const QString &serial, int port, bool wait)
}
if (!m_sndcpy.waitForStarted()) {
qWarning() << "AudioOutput::start sndcpy.bat failed";
qWarning() << "AudioOutput::start sndcpy process failed";
return false;
}
if (!m_sndcpy.waitForFinished()) {
qWarning() << "AudioOutput::sndcpy.bat crashed";
qWarning() << "AudioOutput::sndcpy process crashed";
return false;
}
@ -99,6 +106,7 @@ bool AudioOutput::runSndcpyProcess(const QString &serial, int port, bool wait)
void AudioOutput::startAudioOutput()
{
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
if (m_audioOutput) {
return;
}
@ -110,8 +118,8 @@ void AudioOutput::startAudioOutput()
format.setCodec("audio/pcm");
format.setByteOrder(QAudioFormat::LittleEndian);
format.setSampleType(QAudioFormat::SignedInt);
QAudioDeviceInfo info(QAudioDeviceInfo::defaultOutputDevice());
if (!info.isFormatSupported(format)) {
qWarning() << "AudioOutput::audio format not supported, cannot play audio.";
return;
@ -123,17 +131,47 @@ void AudioOutput::startAudioOutput()
});
m_audioOutput->setBufferSize(48000*2*15/1000 * 20);
m_outputDevice = m_audioOutput->start();
#else
if (m_audioSink) {
return;
}
QAudioFormat format;
format.setSampleRate(48000);
format.setChannelCount(2);
format.setSampleFormat(QAudioFormat::Int16);
QAudioDevice defaultDevice = QMediaDevices::defaultAudioOutput();
if (!defaultDevice.isFormatSupported(format)) {
qWarning() << "AudioOutput::audio format not supported, cannot play audio.";
return;
}
m_audioSink = new QAudioSink(defaultDevice, format, this);
m_outputDevice = m_audioSink->start();
if (!m_outputDevice) {
qWarning() << "AudioOutput::audio output device not available, cannot play audio.";
delete m_audioSink;
m_audioSink = nullptr;
return;
}
#endif
}
void AudioOutput::stopAudioOutput()
{
if (!m_audioOutput) {
return;
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
if (m_audioOutput) {
m_audioOutput->stop();
delete m_audioOutput;
m_audioOutput = nullptr;
}
m_audioOutput->stop();
delete m_audioOutput;
m_audioOutput = nullptr;
#else
if (m_audioSink) {
m_audioSink->stop();
delete m_audioSink;
m_audioSink = nullptr;
}
#endif
m_outputDevice = nullptr;
}
void AudioOutput::startRecvData(int port)
@ -165,7 +203,7 @@ void AudioOutput::startRecvData(int port)
m_buffer.reserve(recv);
}
qint64 count = audioSocket->read(m_buffer.data(), audioSocket->bytesAvailable());
qint64 count = audioSocket->read(m_buffer.data(), recv);
m_outputDevice->write(m_buffer.data(), count);
});
connect(audioSocket, &QTcpSocket::stateChanged, audioSocket, [](QAbstractSocket::SocketState state) {

View file

@ -6,6 +6,7 @@
#include <QPointer>
#include <QVector>
class QAudioSink;
class QAudioOutput;
class QIODevice;
class AudioOutput : public QObject
@ -30,12 +31,16 @@ signals:
void connectTo(int port);
private:
QAudioOutput* m_audioOutput = nullptr;
QPointer<QIODevice> m_outputDevice;
QThread m_workerThread;
QProcess m_sndcpy;
QVector<char> m_buffer;
bool m_running = false;
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QAudioOutput* m_audioOutput = nullptr;
#else
QAudioSink *m_audioSink = nullptr;
#endif
};
#endif // AUDIOOUTPUT_H

View file

@ -262,7 +262,7 @@ void GroupController::postCut()
}
}
void GroupController::setScreenPowerMode(bool open)
void GroupController::setDisplayPower(bool on)
{
for (const auto& serial : m_devices) {
if (true == isHost(serial)) {
@ -273,7 +273,7 @@ void GroupController::setScreenPowerMode(bool open)
continue;
}
device->setScreenPowerMode(open);
device->setDisplayPower(on);
}
}

View file

@ -31,7 +31,7 @@ private:
void postVolumeDown() override;
void postCopy() override;
void postCut() override;
void setScreenPowerMode(bool open) override;
void setDisplayPower(bool on) override;
void expandNotificationPanel() override;
void collapsePanel() override;
void postBackOrScreenOn(bool down) override;

View file

@ -55,10 +55,12 @@ int main(int argc, char *argv[])
QApplication::setAttribute(Qt::AA_UseDesktopOpenGL);
}
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
#if (QT_VERSION >= QT_VERSION_CHECK(5,14,0))
QGuiApplication::setHighDpiScaleFactorRoundingPolicy(Qt::HighDpiScaleFactorRoundingPolicy::PassThrough);
#endif
#endif
QSurfaceFormat varFormat = QSurfaceFormat::defaultFormat();
@ -113,6 +115,14 @@ int main(int argc, char *argv[])
"following address:");
qInfo() << QString("QtScrcpy %1 <https://github.com/barry-ran/QtScrcpy>").arg(QCoreApplication::applicationVersion());
qInfo() << QObject::tr("If you need more professional batch control mirror software, you can try the following software:");
qInfo() << QString(QObject::tr("QuickMirror") + " <https://lrbnfell4p.feishu.cn/drive/folder/KviYfz5uFlpUT8dXgdjccmfUnse>");
qInfo() << QObject::tr("If you need more professional game keymap mirror software, you can try the following software:");
qInfo() << QString(QObject::tr("QuickAssistant") + " <https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh>");
qInfo() << QObject::tr("You can contact me with telegram <https://t.me/+Ylf_5V_rDCMyODQ1>");
int ret = a.exec();
delete g_mainDlg;
@ -127,7 +137,13 @@ void installTranslator()
static QTranslator translator;
QLocale locale;
QLocale::Language language = locale.language();
//language = QLocale::English;
if (Config::getInstance().getLanguage() == "zh_CN") {
language = QLocale::Chinese;
} else if (Config::getInstance().getLanguage() == "en_US") {
language = QLocale::English;
}
QString languagePath = ":/i18n/";
switch (language) {
case QLocale::Chinese:
@ -139,7 +155,10 @@ void installTranslator()
break;
}
translator.load(languagePath);
auto loaded = translator.load(languagePath);
if (!loaded) {
qWarning() << "Failed to load translation file:" << languagePath;
}
qApp->installTranslator(&translator);
}

View file

@ -157,6 +157,8 @@ void QYUVOpenGLWidget::initializeGL()
void QYUVOpenGLWidget::paintGL()
{
m_shaderProgram.bind();
if (m_needUpdate) {
deInitTextures();
initTextures();
@ -175,6 +177,8 @@ void QYUVOpenGLWidget::paintGL()
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
}
m_shaderProgram.release();
}
void QYUVOpenGLWidget::resizeGL(int width, int height)

Binary file not shown.

View file

@ -31,6 +31,10 @@
<source>select path</source>
<translation>select path</translation>
</message>
<message>
<source>Clear History</source>
<translation>Clear History</translation>
</message>
</context>
<context>
<name>QObject</name>
@ -38,6 +42,26 @@
<source>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</source>
<translation>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</translation>
</message>
<message>
<source>QuickMirror</source>
<translation>QuickMirror</translation>
</message>
<message>
<source>If you need more professional batch control mirror software, you can try the following software:</source>
<translation>If you need more professional batch control mirror software, you can try the following software:</translation>
</message>
<message>
<source>If you need more professional game keymap mirror software, you can try the following software:</source>
<translation>If you need more professional game keymap mirror software, you can try the following software:</translation>
</message>
<message>
<source>QuickAssistant</source>
<translation>QuickAssistant</translation>
</message>
<message>
<source>You can contact me with telegram &lt;https://t.me/+Ylf_5V_rDCMyODQ1&gt;</source>
<translation>You can contact me with telegram &lt;https://t.me/+Ylf_5V_rDCMyODQ1&gt;</translation>
</message>
</context>
<context>
<name>ToolForm</name>
@ -127,10 +151,6 @@
<source>Start Config</source>
<translation>Start Config</translation>
</message>
<message>
<source>record save path:</source>
<translation>record save path:</translation>
</message>
<message>
<source>select path</source>
<translation>select path</translation>
@ -289,5 +309,13 @@
<source>auto update</source>
<translation>auto update</translation>
</message>
<message>
<source>show toolbar</source>
<translation>show toolbar</translation>
</message>
<message>
<source>record save path:</source>
<translation>record save path:</translation>
</message>
</context>
</TS>

Binary file not shown.

View file

@ -31,6 +31,10 @@
<source>select path</source>
<translation></translation>
</message>
<message>
<source>Clear History</source>
<translation></translation>
</message>
</context>
<context>
<name>QObject</name>
@ -38,6 +42,26 @@
<source>This software is completely open source and free. Use it at your own risk. You can download it at the following address:</source>
<translation>使</translation>
</message>
<message>
<source>QuickMirror</source>
<translation></translation>
</message>
<message>
<source>If you need more professional batch control mirror software, you can try the following software:</source>
<translation></translation>
</message>
<message>
<source>If you need more professional game keymap mirror software, you can try the following software:</source>
<translation></translation>
</message>
<message>
<source>QuickAssistant</source>
<translation></translation>
</message>
<message>
<source>You can contact me with telegram &lt;https://t.me/+Ylf_5V_rDCMyODQ1&gt;</source>
<translation>QQ群联系我 &lt;901736468&gt;</translation>
</message>
</context>
<context>
<name>ToolForm</name>
@ -127,10 +151,6 @@
<source>Start Config</source>
<translation></translation>
</message>
<message>
<source>record save path:</source>
<translation></translation>
</message>
<message>
<source>select path</source>
<translation></translation>
@ -289,5 +309,13 @@
<source>auto update</source>
<translation></translation>
</message>
<message>
<source>show toolbar</source>
<translation></translation>
</message>
<message>
<source>record save path:</source>
<translation></translation>
</message>
</context>
</TS>

View file

@ -2,6 +2,7 @@
#include <QFile>
#include <QFileDialog>
#include <QKeyEvent>
#include <QRandomGenerator>
#include <QTime>
#include <QTimer>
@ -11,6 +12,10 @@
#include "videoform.h"
#include "../groupcontroller/groupcontroller.h"
#ifdef Q_OS_WIN32
#include "../util/winutils.h"
#endif
QString s_keyMapPath = "";
const QString &getKeyMapPath()
@ -77,21 +82,21 @@ Dialog::Dialog(QWidget *parent) : QWidget(parent), ui(new Ui::Widget)
log = "ip not find, connect to wifi?";
break;
}
ui->deviceIpEdt->setText(ip);
ui->deviceIpEdt->setEditText(ip);
} else if (args.contains("ifconfig") && args.contains("wlan0")) {
QString ip = m_adb.getDeviceIPFromStdOut();
if (ip.isEmpty()) {
log = "ip not find, connect to wifi?";
break;
}
ui->deviceIpEdt->setText(ip);
ui->deviceIpEdt->setEditText(ip);
} else if (args.contains("ip -o a")) {
QString ip = m_adb.getDeviceIPByIpFromStdOut();
if (ip.isEmpty()) {
log = "ip not find, connect to wifi?";
break;
}
ui->deviceIpEdt->setText(ip);
ui->deviceIpEdt->setEditText(ip);
}
break;
}
@ -137,6 +142,10 @@ void Dialog::initUI()
setWindowTitle(Config::getInstance().getTitle());
#ifdef Q_OS_WIN32
WinUtils::setDarkBorderToWindow((HWND)this->winId(), true);
#endif
ui->bitRateEdit->setValidator(new QIntValidator(1, 99999, this));
ui->maxSizeBox->addItem("640");
@ -155,6 +164,16 @@ void Dialog::initUI()
ui->lockOrientationBox->addItem("180");
ui->lockOrientationBox->addItem("270");
ui->lockOrientationBox->setCurrentIndex(0);
// 加载IP历史记录
loadIpHistory();
// 为deviceIpEdt添加右键菜单
if (ui->deviceIpEdt->lineEdit()) {
ui->deviceIpEdt->lineEdit()->setContextMenuPolicy(Qt::CustomContextMenu);
connect(ui->deviceIpEdt->lineEdit(), &QWidget::customContextMenuRequested,
this, &Dialog::showIpEditMenu);
}
}
void Dialog::updateBootConfig(bool toView)
@ -186,6 +205,7 @@ void Dialog::updateBootConfig(bool toView)
ui->stayAwakeCheck->setChecked(config.keepAlive);
ui->useSingleModeCheck->setChecked(config.simpleMode);
ui->autoUpdatecheckBox->setChecked(config.autoUpdateDevice);
ui->showToolbar->setChecked(config.showToolbar);
} else {
UserBootConfig config;
@ -204,6 +224,14 @@ void Dialog::updateBootConfig(bool toView)
config.keepAlive = ui->stayAwakeCheck->isChecked();
config.simpleMode = ui->useSingleModeCheck->isChecked();
config.autoUpdateDevice = ui->autoUpdatecheckBox->isChecked();
config.showToolbar = ui->showToolbar->isChecked();
// 保存当前IP到历史记录
QString currentIp = ui->deviceIpEdt->currentText().trimmed();
if (!currentIp.isEmpty()) {
saveIpHistory(currentIp);
}
Config::getInstance().setUserBootConfig(config);
}
}
@ -264,10 +292,13 @@ void Dialog::slotActivated(QSystemTrayIcon::ActivationReason reason)
void Dialog::closeEvent(QCloseEvent *event)
{
this->hide();
m_hideIcon->showMessage(tr("Notice"),
tr("Hidden here!"),
QSystemTrayIcon::Information,
3000);
if (!Config::getInstance().getTrayMessageShown()) {
Config::getInstance().setTrayMessageShown(true);
m_hideIcon->showMessage(tr("Notice"),
tr("Hidden here!"),
QSystemTrayIcon::Information,
3000);
}
event->ignore();
}
@ -284,7 +315,7 @@ void Dialog::on_startServerBtn_clicked()
{
outLog("start server...", false);
// this is ok that "native" toUshort is 0
// this is ok that "original" toUshort is 0
quint16 videoSize = ui->maxSizeBox->currentText().trimmed().toUShort();
qsc::DeviceParams params;
params.serial = ui->serialBox->currentText().trimmed();
@ -296,8 +327,12 @@ void Dialog::on_startServerBtn_clicked()
params.useReverse = ui->useReverseCheck->isChecked();
params.display = !ui->notDisplayCheck->isChecked();
params.renderExpiredFrames = Config::getInstance().getRenderExpiredFrames();
params.lockVideoOrientation = ui->lockOrientationBox->currentIndex() - 1;
if (ui->lockOrientationBox->currentIndex() > 0) {
params.captureOrientationLock = 1;
params.captureOrientation = (ui->lockOrientationBox->currentIndex() - 1) * 90;
}
params.stayAwake = ui->stayAwakeCheck->isChecked();
params.recordFile = ui->recordScreenCheck->isChecked();
params.recordPath = ui->recordPathEdt->text().trimmed();
params.recordFileFormat = ui->formatBox->currentText().trimmed();
params.serverLocalPath = getServerPath();
@ -308,6 +343,7 @@ void Dialog::on_startServerBtn_clicked()
params.logLevel = Config::getInstance().getLogLevel();
params.codecOptions = Config::getInstance().getCodecOptions();
params.codecName = Config::getInstance().getCodecName();
params.scid = QRandomGenerator::global()->bounded(1, 10000) & 0x7FFFFFFF;
qsc::IDeviceManage::getInstance().connectDevice(params);
}
@ -324,7 +360,7 @@ void Dialog::on_wirelessConnectBtn_clicked()
if (checkAdbRun()) {
return;
}
QString addr = ui->deviceIpEdt->text().trimmed();
QString addr = ui->deviceIpEdt->currentText().trimmed();
if (!ui->devicePortEdt->text().isEmpty()) {
addr += ":";
addr += ui->devicePortEdt->text().trimmed();
@ -336,6 +372,12 @@ void Dialog::on_wirelessConnectBtn_clicked()
return;
}
// 保存IP历史记录 - 只保存IP部分,不包含端口
QString ip = addr.split(":").first();
if (!ip.isEmpty()) {
saveIpHistory(ip);
}
outLog("wireless connect...", false);
QStringList adbArgs;
adbArgs << "connect";
@ -433,20 +475,23 @@ void Dialog::onDeviceConnected(bool success, const QString &serial, const QStrin
if (!success) {
return;
}
auto videoForm = new VideoForm(ui->framelessCheck->isChecked(), Config::getInstance().getSkin());
auto videoForm = new VideoForm(ui->framelessCheck->isChecked(), Config::getInstance().getSkin(), ui->showToolbar->isChecked());
videoForm->setSerial(serial);
qsc::IDeviceManage::getInstance().getDevice(serial)->setUserData(static_cast<void*>(videoForm));
qsc::IDeviceManage::getInstance().getDevice(serial)->registerDeviceObserver(videoForm);
videoForm->showFPS(ui->fpsCheck->isChecked());
if (ui->alwaysTopCheck->isChecked()) {
videoForm->staysOnTop();
}
#ifndef Q_OS_WIN32
// must be show before updateShowSize
videoForm->show();
#endif
QString name = Config::getInstance().getNickName(serial);
if (name.isEmpty()) {
name = Config::getInstance().getTitle();
@ -464,6 +509,11 @@ void Dialog::onDeviceConnected(bool success, const QString &serial, const QStrin
videoForm->setGeometry(rc);
}
#ifdef Q_OS_WIN32
// windows是show太早可以看到resize的过程
QTimer::singleShot(200, videoForm, [videoForm](){videoForm->show();});
#endif
GroupController::instance().addDevice(serial);
}
@ -488,7 +538,7 @@ void Dialog::on_wirelessDisConnectBtn_clicked()
if (checkAdbRun()) {
return;
}
QString addr = ui->deviceIpEdt->text().trimmed();
QString addr = ui->deviceIpEdt->currentText().trimmed();
outLog("wireless disconnect...", false);
QStringList adbArgs;
adbArgs << "disconnect";
@ -590,9 +640,18 @@ void Dialog::on_usbConnectBtn_clicked()
int Dialog::findDeviceFromeSerialBox(bool wifi)
{
QRegExp regIP("\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\:([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])\\b");
QString regStr = "\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\:([0-9]|[1-9]\\d|[1-9]\\d{2}|[1-9]\\d{3}|[1-5]\\d{4}|6[0-4]\\d{3}|65[0-4]\\d{2}|655[0-2]\\d|6553[0-5])\\b";
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QRegExp regIP(regStr);
#else
QRegularExpression regIP(regStr);
#endif
for (int i = 0; i < ui->serialBox->count(); ++i) {
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
bool isWifi = regIP.exactMatch(ui->serialBox->itemText(i));
#else
bool isWifi = regIP.match(ui->serialBox->itemText(i)).hasMatch();
#endif
bool found = wifi ? isWifi : !isWifi;
if (found) {
return i;
@ -730,3 +789,45 @@ void Dialog::on_autoUpdatecheckBox_toggled(bool checked)
m_autoUpdatetimer.stop();
}
}
void Dialog::loadIpHistory()
{
QStringList ipList = Config::getInstance().getIpHistory();
ui->deviceIpEdt->clear();
ui->deviceIpEdt->addItems(ipList);
ui->deviceIpEdt->setContentsMargins(0, 0, 0, 0);
if (ui->deviceIpEdt->lineEdit()) {
ui->deviceIpEdt->lineEdit()->setMaxLength(128);
ui->deviceIpEdt->lineEdit()->setPlaceholderText("192.168.0.1");
}
}
void Dialog::saveIpHistory(const QString &ip)
{
if (ip.isEmpty()) {
return;
}
Config::getInstance().saveIpHistory(ip);
// 更新ComboBox
loadIpHistory();
ui->deviceIpEdt->setCurrentText(ip);
}
void Dialog::showIpEditMenu(const QPoint &pos)
{
QMenu *menu = ui->deviceIpEdt->lineEdit()->createStandardContextMenu();
menu->addSeparator();
QAction *clearHistoryAction = new QAction(tr("Clear History"), menu);
connect(clearHistoryAction, &QAction::triggered, this, [this]() {
Config::getInstance().clearIpHistory();
loadIpHistory();
});
menu->addAction(clearHistoryAction);
menu->exec(ui->deviceIpEdt->lineEdit()->mapToGlobal(pos));
delete menu;
}

View file

@ -67,6 +67,8 @@ private slots:
void on_autoUpdatecheckBox_toggled(bool checked);
void showIpEditMenu(const QPoint &pos);
private:
bool checkAdbRun();
void initUI();
@ -78,6 +80,8 @@ private:
int findDeviceFromeSerialBox(bool wifi);
quint32 getBitRate();
const QString &getServerPath();
void loadIpHistory();
void saveIpHistory(const QString &ip);
protected:
void closeEvent(QCloseEvent *event);

View file

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>1293</width>
<height>454</height>
<height>502</height>
</rect>
</property>
<property name="windowTitle">
@ -255,7 +255,7 @@
</sizepolicy>
</property>
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
<enum>Qt::NoFocus</enum>
</property>
<property name="documentTitle">
<string/>
@ -626,6 +626,80 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="4">
<widget class="QCheckBox" name="fpsCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>show fps</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="notDisplayCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>background record</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="alwaysTopCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>always on top</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="recordScreenCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>record screen</string>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="useReverseCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>reverse connection</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="closeScreenCheck">
<property name="sizePolicy">
@ -652,80 +726,6 @@
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QCheckBox" name="alwaysTopCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>always on top</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QCheckBox" name="notDisplayCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>background record</string>
</property>
<property name="checkable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="3">
<widget class="QCheckBox" name="useReverseCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>reverse connection</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QCheckBox" name="recordScreenCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>record screen</string>
</property>
</widget>
</item>
<item row="0" column="4">
<widget class="QCheckBox" name="fpsCheck">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>show fps</string>
</property>
</widget>
</item>
<item row="1" column="4">
<widget class="QCheckBox" name="stayAwakeCheck">
<property name="sizePolicy">
@ -739,6 +739,13 @@
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QCheckBox" name="showToolbar">
<property name="text">
<string>show toolbar</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
@ -1052,22 +1059,25 @@
<number>5</number>
</property>
<item>
<widget class="QLineEdit" name="deviceIpEdt">
<widget class="QComboBox" name="deviceIpEdt">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<property name="minimumSize">
<size>
<width>200</width>
<height>0</height>
</size>
</property>
<property name="editable">
<bool>true</bool>
</property>
<property name="currentText">
<string/>
</property>
<property name="maxLength">
<number>128</number>
</property>
<property name="placeholderText">
<string notr="true">192.168.0.1</string>
</property>
</widget>
</item>
<item>
@ -1140,7 +1150,7 @@
<item>
<spacer name="verticalSpacer">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<sizepolicy hsizetype="Minimum" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
@ -1148,10 +1158,13 @@
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Expanding</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>

View file

@ -68,7 +68,11 @@ void ToolForm::updateGroupControl()
void ToolForm::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton) {
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
m_dragPosition = event->globalPos() - frameGeometry().topLeft();
#else
m_dragPosition = event->globalPosition().toPoint() - frameGeometry().topLeft();
#endif
event->accept();
}
}
@ -81,7 +85,11 @@ void ToolForm::mouseReleaseEvent(QMouseEvent *event)
void ToolForm::mouseMoveEvent(QMouseEvent *event)
{
if (event->buttons() & Qt::LeftButton) {
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
move(event->globalPos() - m_dragPosition);
#else
move(event->globalPosition().toPoint() - m_dragPosition);
#endif
event->accept();
}
}
@ -186,7 +194,7 @@ void ToolForm::on_closeScreenBtn_clicked()
if (!device) {
return;
}
device->setScreenPowerMode(false);
device->setDisplayPower(false);
}
void ToolForm::on_expandNotifyBtn_clicked()
@ -221,5 +229,5 @@ void ToolForm::on_openScreenBtn_clicked()
if (!device) {
return;
}
device->setScreenPowerMode(true);
device->setDisplayPower(true);
}

View file

@ -1,4 +1,4 @@
#include <QDesktopWidget>
// #include <QDesktopWidget>
#include <QFileInfo>
#include <QLabel>
#include <QMessageBox>
@ -13,6 +13,10 @@
#include <QWindow>
#include <QtWidgets/QHBoxLayout>
#if defined(Q_OS_WIN32)
#include <Windows.h>
#endif
#include "config.h"
#include "iconhelper.h"
#include "qyuvopenglwidget.h"
@ -21,13 +25,14 @@
#include "ui_videoform.h"
#include "videoform.h"
VideoForm::VideoForm(bool framelessWindow, bool skin, QWidget *parent) : QWidget(parent), ui(new Ui::videoForm), m_skin(skin)
VideoForm::VideoForm(bool framelessWindow, bool skin, bool showToolbar, QWidget *parent) : QWidget(parent), ui(new Ui::videoForm), m_skin(skin)
{
ui->setupUi(this);
initUI();
installShortcut();
updateShowSize(size());
bool vertical = size().height() > size().width();
this->show_toolbar = showToolbar;
if (m_skin) {
updateStyleSheet(vertical);
}
@ -289,7 +294,7 @@ void VideoForm::installShortcut()
if (!device) {
return;
}
emit device->setScreenPowerMode(false);
emit device->setDisplayPower(false);
});
// expandNotificationPanel
@ -362,21 +367,18 @@ void VideoForm::installShortcut()
QRect VideoForm::getScreenRect()
{
QRect screenRect;
QWidget *win = window();
if (!win) {
return screenRect;
}
QWindow *winHandle = win->windowHandle();
QScreen *screen = QGuiApplication::primaryScreen();
if (winHandle) {
screen = winHandle->screen();
}
if (!screen) {
return screenRect;
QWidget *win = window();
if (win) {
QWindow *winHandle = win->windowHandle();
if (winHandle) {
screen = winHandle->screen();
}
}
screenRect = screen->availableGeometry();
if (screen) {
screenRect = screen->availableGeometry();
}
return screenRect;
}
@ -467,6 +469,8 @@ void VideoForm::switchFullScreen()
}
showNormal();
// back to normal size.
resize(m_normalSize);
// fullscreen window will move (0,0). qt bug?
move(m_fullScreenBeforePos);
@ -477,7 +481,7 @@ void VideoForm::switchFullScreen()
if (m_skin) {
updateStyleSheet(m_frameSize.height() > m_frameSize.width());
}
showToolForm(true);
showToolForm(this->show_toolbar);
#ifdef Q_OS_WIN32
::SetThreadExecutionState(ES_CONTINUOUS);
#endif
@ -487,6 +491,9 @@ void VideoForm::switchFullScreen()
ui->keepRatioWidget->setWidthHeightRatio(-1.0f);
}
// record current size before fullscreen, it will be used to rollback size after exit fullscreen.
m_normalSize = size();
m_fullScreenBeforePos = pos();
// 这种临时增加标题栏再全屏的方案会导致收不到mousemove事件导致setmousetrack失效
// mac fullscreen must show title bar
@ -508,6 +515,9 @@ void VideoForm::switchFullScreen()
bool VideoForm::isHost()
{
if (!m_toolForm) {
return false;
}
return m_toolForm->isHost();
}
@ -563,23 +573,32 @@ void VideoForm::mousePressEvent(QMouseEvent *event)
}
}
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QPointF localPos = event->localPos();
QPointF globalPos = event->globalPos();
#else
QPointF localPos = event->position();
QPointF globalPos = event->globalPosition();
#endif
if (m_videoWidget->geometry().contains(event->pos())) {
if (!device) {
return;
}
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
emit device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint());
QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers());
emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size());
// debug keymap pos
if (event->button() == Qt::LeftButton) {
qreal x = event->localPos().x() / m_videoWidget->size().width();
qreal y = event->localPos().y() / m_videoWidget->size().height();
qreal x = localPos.x() / m_videoWidget->size().width();
qreal y = localPos.y() / m_videoWidget->size().height();
QString posTip = QString(R"("pos": {"x": %1, "y": %2})").arg(x).arg(y);
qInfo() << posTip.toStdString().c_str();
}
} else {
if (event->button() == Qt::LeftButton) {
m_dragPosition = event->globalPos() - frameGeometry().topLeft();
m_dragPosition = globalPos.toPoint() - frameGeometry().topLeft();
event->accept();
}
}
@ -592,9 +611,15 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event)
if (!device) {
return;
}
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QPointF localPos = event->localPos();
QPointF globalPos = event->globalPos();
#else
QPointF localPos = event->position();
QPointF globalPos = event->globalPosition();
#endif
// local check
QPointF local = event->localPos();
QPointF local = m_videoWidget->mapFrom(this, localPos.toPoint());
if (local.x() < 0) {
local.setX(0);
}
@ -607,8 +632,8 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event)
if (local.y() > m_videoWidget->height()) {
local.setY(m_videoWidget->height());
}
event->setLocalPos(local);
emit device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
QMouseEvent newEvent(event->type(), local, globalPos, event->button(), event->buttons(), event->modifiers());
emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size());
} else {
m_dragPosition = QPoint(0, 0);
}
@ -616,16 +641,24 @@ void VideoForm::mouseReleaseEvent(QMouseEvent *event)
void VideoForm::mouseMoveEvent(QMouseEvent *event)
{
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QPointF localPos = event->localPos();
QPointF globalPos = event->globalPos();
#else
QPointF localPos = event->position();
QPointF globalPos = event->globalPosition();
#endif
auto device = qsc::IDeviceManage::getInstance().getDevice(m_serial);
if (m_videoWidget->geometry().contains(event->pos())) {
if (!device) {
return;
}
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
emit device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint());
QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers());
emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size());
} else if (!m_dragPosition.isNull()) {
if (event->buttons() & Qt::LeftButton) {
move(event->globalPos() - m_dragPosition);
move(globalPos.toPoint() - m_dragPosition);
event->accept();
}
}
@ -648,8 +681,16 @@ void VideoForm::mouseDoubleClickEvent(QMouseEvent *event)
if (!device) {
return;
}
event->setLocalPos(m_videoWidget->mapFrom(this, event->localPos().toPoint()));
emit device->mouseEvent(event, m_videoWidget->frameSize(), m_videoWidget->size());
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
QPointF localPos = event->localPos();
QPointF globalPos = event->globalPos();
#else
QPointF localPos = event->position();
QPointF globalPos = event->globalPosition();
#endif
QPointF mappedPos = m_videoWidget->mapFrom(this, localPos.toPoint());
QMouseEvent newEvent(event->type(), mappedPos, globalPos, event->button(), event->buttons(), event->modifiers());
emit device->mouseEvent(&newEvent, m_videoWidget->frameSize(), m_videoWidget->size());
}
}
@ -705,7 +746,11 @@ void VideoForm::paintEvent(QPaintEvent *paint)
{
Q_UNUSED(paint)
QStyleOption opt;
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
opt.init(this);
#else
opt.initFrom(this);
#endif
QPainter p(this);
style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this);
}
@ -713,8 +758,10 @@ void VideoForm::paintEvent(QPaintEvent *paint)
void VideoForm::showEvent(QShowEvent *event)
{
Q_UNUSED(event)
if (!isFullScreen()) {
showToolForm();
if (!isFullScreen() && this->show_toolbar) {
QTimer::singleShot(500, this, [this](){
showToolForm(this->show_toolbar);
});
}
}

View file

@ -19,7 +19,7 @@ class VideoForm : public QWidget, public qsc::DeviceObserver
{
Q_OBJECT
public:
explicit VideoForm(bool framelessWindow = false, bool skin = true, QWidget *parent = 0);
explicit VideoForm(bool framelessWindow = false, bool skin = true, bool showToolBar = true, QWidget *parent = 0);
~VideoForm();
void staysOnTop(bool top = true);
@ -32,7 +32,6 @@ public:
void removeBlackRect();
void showFPS(bool show);
void switchFullScreen();
bool isHost();
private:
@ -79,11 +78,15 @@ private:
//inside member
QSize m_frameSize;
QSize m_normalSize;
QPoint m_dragPosition;
float m_widthHeightRatio = 0.5f;
bool m_skin = true;
QPoint m_fullScreenBeforePos;
QString m_serial;
//Whether to display the toolbar when connecting a device.
bool show_toolbar = true;
};
#endif // VIDEOFORM_H

View file

@ -4,10 +4,16 @@
#include <QDebug>
#include "config.h"
#ifdef Q_OS_OSX
#include "path.h"
#endif
#define GROUP_COMMON "common"
// config
#define COMMON_LANGUAGE_KEY "Language"
#define COMMON_LANGUAGE_DEF "Auto"
#define COMMON_TITLE_KEY "WindowTitle"
#define COMMON_TITLE_DEF QCoreApplication::applicationName()
@ -15,13 +21,13 @@
#define COMMON_PUSHFILE_DEF "/sdcard/"
#define COMMON_SERVER_VERSION_KEY "ServerVersion"
#define COMMON_SERVER_VERSION_DEF "1.21"
#define COMMON_SERVER_VERSION_DEF "3.1"
#define COMMON_SERVER_PATH_KEY "ServerPath"
#define COMMON_SERVER_PATH_DEF "/data/local/tmp/scrcpy-server.jar"
#define COMMON_MAX_FPS_KEY "MaxFps"
#define COMMON_MAX_FPS_DEF 60
#define COMMON_MAX_FPS_DEF 0
#define COMMON_DESKTOP_OPENGL_KEY "UseDesktopOpenGL"
#define COMMON_DESKTOP_OPENGL_DEF -1
@ -90,6 +96,12 @@
#define COMMON_AUTO_UPDATE_DEVICE_KEY "AutoUpdateDevice"
#define COMMON_AUTO_UPDATE_DEVICE_DEF true
#define COMMON_TRAY_MESSAGE_SHOWN_KEY "TrayMessageShown"
#define COMMON_TRAY_MESSAGE_SHOWN_DEF false
#define COMMON_SHOW_TOOLBAR_KEY "showToolbar"
#define COMMON_SHOW_TOOLBAR_DEF true
// device config
#define SERIAL_WINDOW_RECT_KEY_X "WindowRectX"
#define SERIAL_WINDOW_RECT_KEY_Y "WindowRectY"
@ -99,15 +111,21 @@
#define SERIAL_NICK_NAME_KEY "NickName"
#define SERIAL_NICK_NAME_DEF "Phone"
// IP history
#define IP_HISTORY_KEY "IpHistory"
#define IP_HISTORY_DEF ""
#define IP_HISTORY_MAX 10
QString Config::s_configPath = "";
Config::Config(QObject *parent) : QObject(parent)
{
m_settings = new QSettings(getConfigPath() + "/config.ini", QSettings::IniFormat);
m_settings->setIniCodec("UTF-8");
m_userData = new QSettings(getConfigPath() + "/userdata.ini", QSettings::IniFormat);
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
m_settings->setIniCodec("UTF-8");
m_userData->setIniCodec("UTF-8");
#endif
qDebug()<<m_userData->childGroups();
}
@ -125,7 +143,15 @@ const QString &Config::getConfigPath()
QFileInfo fileInfo(s_configPath);
if (s_configPath.isEmpty() || !fileInfo.isDir()) {
// default application dir
// mac系统当从finder打开app时默认工作目录不再是可执行程序的目录了而是"/"
// 而Qt的获取工作目录的api都依赖QCoreApplication的初始化所以使用mac api获取当前目录
#ifdef Q_OS_OSX
// get */QtScrcpy.app path
s_configPath = Path::GetCurrentPath();
s_configPath += "/Contents/MacOS/config";
#else
s_configPath = "config";
#endif
}
}
return s_configPath;
@ -149,6 +175,7 @@ void Config::setUserBootConfig(const UserBootConfig &config)
m_userData->setValue(COMMON_KEEP_ALIVE_KEY, config.keepAlive);
m_userData->setValue(COMMON_SIMPLE_MODE_KEY, config.simpleMode);
m_userData->setValue(COMMON_AUTO_UPDATE_DEVICE_KEY, config.autoUpdateDevice);
m_userData->setValue(COMMON_SHOW_TOOLBAR_KEY, config.showToolbar);
m_userData->endGroup();
m_userData->sync();
}
@ -172,10 +199,28 @@ UserBootConfig Config::getUserBootConfig()
config.keepAlive = m_userData->value(COMMON_KEEP_ALIVE_KEY, COMMON_KEEP_ALIVE_DEF).toBool();
config.simpleMode = m_userData->value(COMMON_SIMPLE_MODE_KEY, COMMON_SIMPLE_MODE_DEF).toBool();
config.autoUpdateDevice = m_userData->value(COMMON_AUTO_UPDATE_DEVICE_KEY, COMMON_AUTO_UPDATE_DEVICE_DEF).toBool();
config.showToolbar =m_userData->value(COMMON_SHOW_TOOLBAR_KEY,COMMON_SHOW_TOOLBAR_DEF).toBool();
m_userData->endGroup();
return config;
}
void Config::setTrayMessageShown(bool shown)
{
m_userData->beginGroup(GROUP_COMMON);
m_userData->setValue(COMMON_TRAY_MESSAGE_SHOWN_KEY, shown);
m_userData->endGroup();
m_userData->sync();
}
bool Config::getTrayMessageShown()
{
bool shown;
m_userData->beginGroup(GROUP_COMMON);
shown = m_userData->value(COMMON_TRAY_MESSAGE_SHOWN_KEY, COMMON_TRAY_MESSAGE_SHOWN_DEF).toBool();
m_userData->endGroup();
return shown;
}
void Config::setRect(const QString &serial, const QRect &rc)
{
m_userData->beginGroup(serial);
@ -227,7 +272,7 @@ QString Config::getServerVersion()
int Config::getMaxFps()
{
int fps = 60;
int fps = 0;
m_settings->beginGroup(GROUP_COMMON);
fps = m_settings->value(COMMON_MAX_FPS_KEY, COMMON_MAX_FPS_DEF).toInt();
m_settings->endGroup();
@ -327,6 +372,15 @@ void Config::deleteGroup(const QString &serial)
m_userData->remove(serial);
}
QString Config::getLanguage()
{
QString language;
m_settings->beginGroup(GROUP_COMMON);
language = m_settings->value(COMMON_LANGUAGE_KEY, COMMON_LANGUAGE_DEF).toString();
m_settings->endGroup();
return language;
}
QString Config::getTitle()
{
QString title;
@ -335,3 +389,34 @@ QString Config::getTitle()
m_settings->endGroup();
return title;
}
void Config::saveIpHistory(const QString &ip)
{
QStringList ipList = getIpHistory();
// 移除已存在的相同IP避免重复
ipList.removeAll(ip);
// 将新IP添加到开头
ipList.prepend(ip);
// 限制历史记录数量
while (ipList.size() > IP_HISTORY_MAX) {
ipList.removeLast();
}
m_userData->setValue(IP_HISTORY_KEY, ipList);
m_userData->sync();
}
QStringList Config::getIpHistory()
{
QStringList ipList = m_userData->value(IP_HISTORY_KEY, IP_HISTORY_DEF).toStringList();
return ipList;
}
void Config::clearIpHistory()
{
m_userData->remove(IP_HISTORY_KEY);
m_userData->sync();
}

View file

@ -22,6 +22,7 @@ struct UserBootConfig
bool keepAlive = false;
bool simpleMode = false;
bool autoUpdateDevice = true;
bool showToolbar = true;
};
class QSettings;
@ -33,6 +34,7 @@ public:
static Config &getInstance();
// config
QString getLanguage();
QString getTitle();
QString getServerVersion();
int getMaxFps();
@ -50,6 +52,8 @@ public:
// user data:common
void setUserBootConfig(const UserBootConfig &config);
UserBootConfig getUserBootConfig();
void setTrayMessageShown(bool shown);
bool getTrayMessageShown();
// user data:device
void setNickName(const QString &serial, const QString &name);
@ -59,6 +63,11 @@ public:
void deleteGroup(const QString &serial);
// IP history methods
void saveIpHistory(const QString &ip);
QStringList getIpHistory();
void clearIpHistory();
private:
explicit Config(QObject *parent = nullptr);
const QString &getConfigPath();

View file

@ -1,4 +1,10 @@
#include <QX11Info>
#include <QtGlobal>
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
#include <QtX11Extras/QX11Info>
#else
#include <QtGui/private/qtx11extras_p.h>
#endif
#include <xcb/xproto.h>
#include <stdlib.h>

6
QtScrcpy/util/path.h Normal file
View file

@ -0,0 +1,6 @@
#pragma once
class Path {
public:
static const char* GetCurrentPath();
};

7
QtScrcpy/util/path.mm Normal file
View file

@ -0,0 +1,7 @@
#include "path.h"
#import <Cocoa/Cocoa.h>
const char* Path::GetCurrentPath() {
return [[[NSBundle mainBundle] bundlePath] UTF8String];
}

View file

@ -0,0 +1,28 @@
#include <QDebug>
#include <Windows.h>
#include <dwmapi.h>
#pragma comment(lib, "dwmapi")
#include "winutils.h"
enum : WORD
{
DwmwaUseImmersiveDarkMode = 20,
DwmwaUseImmersiveDarkModeBefore20h1 = 19
};
WinUtils::WinUtils(){};
WinUtils::~WinUtils(){};
// Set dark border to window
// Reference: qt/qtbase.git/tree/src/plugins/platforms/windows/qwindowswindow.cpp
bool WinUtils::setDarkBorderToWindow(const HWND &hwnd, const bool &d)
{
const BOOL darkBorder = d ? TRUE : FALSE;
const bool ok = SUCCEEDED(DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkMode, &darkBorder, sizeof(darkBorder)))
|| SUCCEEDED(DwmSetWindowAttribute(hwnd, DwmwaUseImmersiveDarkModeBefore20h1, &darkBorder, sizeof(darkBorder)));
if (!ok)
qWarning("%s: Unable to set dark window border.", __FUNCTION__);
return ok;
}

16
QtScrcpy/util/winutils.h Normal file
View file

@ -0,0 +1,16 @@
#ifndef WINUTILS_H
#define WINUTILS_H
#include <QApplication>
#include <Windows.h>
class WinUtils
{
public:
WinUtils();
~WinUtils();
static bool setDarkBorderToWindow(const HWND &hwnd, const bool &d);
};
#endif // WINUTILS_H

178
README.md
View file

@ -1,27 +1,28 @@
# QtScrcpy
[![Financial Contributors on Open Collective](https://opencollective.com/QtScrcpy/all/badge.svg?label=financial+contributors)](https://opencollective.com/QtScrcpy)
[![Financial Contributors to Open Collective](https://opencollective.com/QtScrcpy/all/badge.svg?label=financial+contributors)](https://opencollective.com/QtScrcpy)
![Windows](https://github.com/barry-ran/QtScrcpy/workflows/Windows/badge.svg)
![MacOS](https://github.com/barry-ran/QtScrcpy/workflows/MacOS/badge.svg)
![Ubuntu](https://github.com/barry-ran/QtScrcpy/workflows/Ubuntu/badge.svg)
![license](https://img.shields.io/badge/license-Apache2.0-blue.svg)
![release](https://img.shields.io/github/v/release/barry-ran/QtScrcpy.svg)
![star](https://img.shields.io/github/stars/barry-ran/QtScrcpy.svg)
[中文介绍](README_zh.md)
[中文用户?点我查看中文介绍](README_zh.md)
QtScrcpy connects to Android devices via USB (or via TCP/IP) for display and control. It does NOT require the root privileges.
QtScrcpy supports displaying and controlling Android devices via USB or over network. It does NOT require root privileges.
It supports three major platforms: GNU/Linux, Windows and MacOS.
It supports three major platforms: GNU/Linux, Windows and macOS.
It focuses on:
- **lightness** (native, displays only the device screen)
- **performance** (30~60fps)
- **lightness** (displays only the device screen)
- **performance** (30~60 fps)
- **quality** (1920×1080 or above)
- **low latency** ([35~70ms][lowlatency])
- **low startup time** (~1 second to display the first image)
- **non-intrusiveness** (nothing is left installed on the device)
- **low startup time** (only about 1 second to display the first frame)
- **non-intrusiveness** (nothing will be installed on the device)
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
@ -31,37 +32,73 @@ It focuses on:
![linux](screenshot/linux-en.png)
## Customized key mapping
You can write your own script to map keyboard and mouse actions to touches and clicks of the mobile phone according to your needs. [Here](docs/KeyMapDes.md) are the rules.
## The author has developed a more professional screen casting software called `QuickMirror`
QuickMirror function&features:
- Equipment screen casting&control: batch screen casting, individual control, batch control
- Group management
- WiFi screen mirroring/OTG screen mirroring
- Adb shell shortcut command
- File transfer, apk installation
- Multiple screen mirroring: In OTG mirroring mode, with low resolution and smoothness settings, a single computer can manage 500+phones simultaneously
- Low latency: USB screen mirroring 1080p latency is within 30ms, which is lower than all screen mirroring software on the market in terms of latency at the same resolution and smoothness
- Low CPU usage: pure C++development, high-performance GPU video rendering
- High resolution: adjustable, maximum support for native resolution of Android terminals
- Perfect Chinese input: Supports Xianyu app, supports Samsung phones
- The free version can cast up to 10 screens, with unlimited functionality (except for automatic screen mirroring)
- QuickMirror tutorial: https://lrbnfell4p.feishu.cn/docx/EMkvdfIvDowy3UxsXUCcpPV8nDh
- QuickMirror Telegram communication group: https://t.me/+Ylf_5V_rDCMyODQ1
- Preview of QuickMirror Interface:
![quickmirror](docs/image/quickmirror.png)
A script for "PUBG mobile" and TikTok mapping is provided by default. Once enabled, you can play the game with your keyboard and mouse as the PC version. or you can use up/down/left/right direction keys to simulate up/down/left/right sliding. You can also write your own mapping files for other games according to [writing rules](docs/KeyMapDes.md). The default key mapping is as follows:
## Mapping Keys
You can write your script to map keyboard and mouse actions to touches and clicks of the mobile phone according to your needs. [Here](docs/KeyMapDes.md) are the script writing rules.
![game](screenshot/game.jpg)
Script for TikTok and some other games are provided by default. Once enabled, you can play the game with your keyboard and mouse. The default key mapping for PUBG Mobile is as follows:
[Here is a video demonstration of playing "PUBG mobile"](http://mp.weixin.qq.com/mp/video?__biz=MzU1NTg5MjYyNw==&mid=100000015&sn=3e301fdc5a364bd16d6207fa674bc8b3&vid=wxv_968792362971430913&idx=1&vidsn=eec329cc13c3e24c187dc9b4d5eb8760&fromid=1&scene=20&xtrack=1&clicktime=1567346543&sessionid=1567346375&subscene=92&ascene=0&fasttmpl_type=0&fasttmpl_fullversion=4730859-zh_CN-zip&fasttmpl_flag=0&realreporttime=1567346543910#wechat_redirect)
![game](screenshot/game.png)
Here is the instruction of adding new customized mapping files.
Instruction for adding new customized mapping files.
- Write a customized script and put it in the `keymap` directory
- Click `refresh script` to check whether it can be found
- Click `refresh script` to show it
- Select your script
- Connect your phone, start service and click `apply`
- Press `~` key (left side of the number key 1) to switch to the custom mapping mode (It can be changed in the script as `switchkey`)
- Connect to your phone, start service and click `apply`
- Press `~` key (the SwitchKey in the key map script) to switch to custom mapping mode
- Press the ~ key again to switch back to normal mode
- (For PUBG and similar games) If you want to drive cars with WASD, you need to check the `single rocker mode` in the game setting.
- (For games such as PUBG Mobile) If you want to move vehicles with the STEER_WHEEL keys, you need to set the move mode to `single rocker mode`.
If you don't know how to manually write mapping rules, you can also use the `QuickAssistant` developed by the author
QuickAssistant Features&Functions:
- Play Android mobile games smoothly through keyboard and mouse
- Interface based editing of key mapping script
- Support pausing the computer screen and using only keyboard and mouse operations
- Screenshot&Recording of Mobile Screen
- Simple batch control
- Android 11+supports playing mobile audio on computers (under development...)
- Mobile app installation free
- Fast and instant connection
- Low latency: USB screen mirroring 1080p latency is within 30ms, which is lower than all screen mirroring software on the market in terms of latency at the same resolution and smoothness
- Low CPU usage: pure C++development, high-performance GPU video rendering
- High resolution: adjustable, maximum support for native resolution of Android terminals
- Telegram Grouphttps://t.me/+EnQNmb47C_liYmRl
- [QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh)
## Group control
You can control all your phones at the same time.
![](docs/image/group-control.gif)
![group-control-demo](docs/image/group-control.gif)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=barry-ran/QtScrcpy&type=Date)](https://star-history.com/#barry-ran/QtScrcpy&Date)
## Thanks
QtScrcpy is based on [Genymobile's](https://github.com/Genymobile) [scrcpy](https://github.com/Genymobile/scrcpy) project. Thanks
QtScrcpy is based on [Genymobile](https://github.com/Genymobile)'s [scrcpy](https://github.com/Genymobile/scrcpy) project. Thanks a lot!
The difference between QtScrcpy and the original scrcpy is as follows:
keys|scrcpy|QtScrcpy
key points|scrcpy|QtScrcpy
--|:--:|:--:
ui|sdl|qt
video encode|ffmpeg|ffmpeg
@ -81,19 +118,19 @@ build|meson+gradle|qmake or CMake
## Learn
If you are interested in it and want to learn how it works but do not know how to get started, you can choose to purchase my recorded video lessons.
It details the development architecture and the development process of the entire software, and help you develop QtScrcpy from scratch.
It details the development architecture and the development process of the entire software and helps you develop QtScrcpy from scratch.
Course introduction[https://blog.csdn.net/rankun1/article/details/87970523](https://blog.csdn.net/rankun1/article/details/87970523)
You can join my QQ group for QtScrcpy and exchange ideas with like-minded friends.
You can join Telegram Group for QtScrcpy and exchange ideas with like-minded friends.
QQ Group number901736468
Telegram Grouphttps://t.me/+EnQNmb47C_liYmRl
## Requirements
Android API >= 21 (Android 5.0).
Make sure you enabled [adb debugging][enable-adb] on your device(s).
Make sure you have enabled [ADB debugging][enable-adb] on your device(s).
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
@ -104,63 +141,70 @@ Make sure you enabled [adb debugging][enable-adb] on your device(s).
[github-download]: https://github.com/barry-ran/QtScrcpy/releases
### Windows
For Windows, for simplicity, prebuilt archives with all the dependencies (including adb) are available:
On Windows, for simplicity, prebuilt archives with all the dependencies (including ADB) are available at Releases:
- [`QtScrcpy`][github-download]
or you can [build it by yourself](##Build)
or you can [build it yourself](#Build)
### Mac OS
For Mac OS, for simplicity, prebuilt archives with all the dependencies (including adb) are available:
On Mac OS, for simplicity, prebuilt archives with all the dependencies (including ADB) are available at Releases:
- [`QtScrcpy`][github-download]
or you can [build it by yourself](##Build)
or you can [build it yourself](#Build)
### Linux
you can [build it by yourself](##Build)(just ubuntu test)
For Arch Linux Users, you can use AUR to install: `yay -Syu qtscrcpy` (may be outdated; maintainer: [yochananmarqos](https://aur.archlinux.org/account/yochananmarqos))
For users in other distros, you can use the prebuilt archives from Releases:
- [`QtScrcpy`][github-download]
or you can get it at [GitHub Actions](https://github.com/barry-ran/QtScrcpy/actions/workflows/ubuntu.yml), in branch `dev` and download the latest artifact.
or you can [build it yourself](#Build) (not recommended, get it in Actions if you can)
## Run
Connect to your Android device on your computer, then run the program and click `USB connect` or `WiFi connect`
### Wireless connection steps (ensure that the mobile phone and PC are in the same LAN):
### Wireless connection steps (ensure that the mobile phone and PC are on the same LAN):
1. Enable USB debugging in developer options on the Android device
2. Connect the Android device to computer via USB
2. Connect the Android device to the computer via USB
3. Click update device, and you will see that the device number is updated
4. Click get device IP
5. Click start adbd
6. Click wireless connect
7. Click update device again, and another device with IP address will be found. Select this device.
7. Click update device again, and another device with an IP address will be found. Select this device.
8. Click start service
Note: it is not necessary to keep you Android device connected via USB after you start adbd.
Note: it is not necessary to keep your Android device connected via USB after you start adbd.
## Interface button introduction
- Start config: function parameter settings before starting the service
You can set the bit rate, resolution, recording format, and video save path of the local recorded video.
You can set the bit rate, resolution, recording format, and video save path of the locally recorded video.
- Background record: the Android device screen is not displayed after starting the service. It is recorded in background.
- Always on top: the video window for Android device will be kept on the top
- Background record: the Android device screen is not displayed after starting the service. It is recorded in the background.
- Always on top: the video window for Android devices will be kept on the top
- Close screen: automatically turn off the Android device screen to save power after starting the service
- Reverse connection: service startup mode. You can uncheck it if you experience connection failure with message `more than one device`
- Reverse connection: service startup mode. You can uncheck it if you experience connection failure with a message `more than one device`
- Refresh devices: Refresh the currently connected device
- Start service: connect to the Android device
- Stop service: disconnect from Android device
- Stop service: disconnect from the Android device
- Stop all services: disconnect all connected Android devices
- Get device IP: Get the IP address of the Android device and update it to the "Wireless" area for the ease of wireless connection setting.
- Start adbd: Start the adbd service of the Android device. You must start it before the wireless connection.
- Wireless connect: Connect to Android devices wirelessly
- Wireless disconnect: Disconnect wirelessly connected Android devices
- adb command: execute customized adb commands (blocking commands are not supported now, such as shell)
- adb command: execute customized ADB commands (blocking commands are not supported now, such as a shell)
## The main function
- Display Android device screens in real time
- Display Android device screens in real-time
- Real-time mouse and keyboard control of Android devices
- Screen recording
- Screenshot to png
@ -176,10 +220,9 @@ Note: it is not necessary to keep you Android device connected via USB after you
It is possible to synchronize clipboards between the computer and the device, in
both directions:
- `Ctrl`+`c` copies the device clipboard to the computer clipboard;
- `Ctrl`+`Shift`+`v` copies the computer clipboard to the device clipboard;
- `Ctrl`+`v` _pastes_ the computer clipboard as a sequence of text events (but
breaks non-ASCII characters).
- `Ctrl + c` copies the device clipboard to the computer clipboard;
- `Ctrl + Shift + v` copies the computer clipboard to the device clipboard;
- `Ctrl + v` _pastes_ the computer clipboard as a sequence of text events (non-ASCII characters does not yet work).
- Group control
- Sync device speaker sound to the computer (based on [sndcpy](https://github.com/rom1v/sndcpy), Android 10+ only)
@ -222,39 +265,44 @@ _³Only on Android >= 7._
[DEVELOP](docs/DEVELOP.md)
Everyone is welcome to maintain this project and contribute your own code, but please follow these requirements:
1. pr please mention the dev branch, not the master branch
2. Please rebase dev before mentioning pr
3. pr please submit on the principle of a small number of times (a small function point is recommended to mention a pr)
4. Please keep the code style consistent with the existing style
1. Please open PRs to the dev branch instead of the master branch
2. Please rebase the original project before opening PRs
3. Please submit PRs on the principle of "small amounts, many times" (one PR for a change is recommended)
4. Please keep the code style consistent with the existing style.
## Why develop QtScrcpy?
There are several reasons listed as below according to importance (high to low).
1. In the process of learning Qt, I need a real project to try
2. I have some background skill about audio and video and I am interested at them
There are several reasons listed below according to importance (high to low).
1. In the process of learning Qt, I need a real project to try.
2. I have some background skills in audio and video and I am interested in them.
3. I have some Android development skills. But I have used it for a long time. I want to consolidate it.
4. I found scrcpy and decided to re-make it with the new technology stack (C++ + Qt + Opengl + ffmpeg)
4. I found scrcpy and decided to re-make it with the new technology stack (C++ + Qt + Opengl + FFmpeg).
## Build
All the dependencies are provided and it is easy to compile.
### PC client
1. Set up the Qt development environment on the target platform.
Qt version>=5.12 (use MSVC 2019 on Windows)
2. Clone the project (git clone --recursive git@github.com:barry-ran/QtScrcpy.git)
3. Open the project root directory `CMakeLists.txt` with QtCreator
4. Compile and run
### QtScrcpy
#### Non-Arch Linux Users
1. Set up the Qt development environment with the official Qt installer or third-party tools such as [aqt](https://github.com/miurahr/aqtinstall) on the target platform.
Qt version bigger than 5.12 is required. (use MSVC 2019 on Windows)
2. Clone the project with `git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git`
3. For Windows, open CMakeLists.txt with QtCreator and compile Release
4. For Linux, directly run `./ci/linux/build_for_linux.sh "Release"`
Note: compiled artifacts are located at `output/x64/Release`
### Android (If you do not have special requirements, you can directly use the built-in scrcpy-server.jar)
#### Arch Linux Users
1. Install packages: `base-devel cmake qt5-base qt5-multimedia qt5-x11extras` (`qtcreator` is recommended)
2. Clone the project with `git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git`
3. Run `./ci/linux/build_for_linux.sh "Release"`
1. Set up an Android development environment on the target platform
### Scrcpy-Server
1. Set up Android development environment on the target platform
2. Open server project in project root with Android Studio
3. The first time you open it, if you do not have the corresponding version of gradle, you will be prompted to find gradle, whether to upgrade gradle and create it. Select Cancel. After canceling, you will be prompted to select the location of the existing gradle. You can also cancel it (it will download automatically).
4. Edit the code as needed, but of course you do nt need to.
3. The first time you open it, if you do not have the corresponding version of Gradle, you will be prompted to find Gradle, whether to upgrade Gradle or create it. Select Cancel. After cancelling, you will be prompted to select the location of existing Gradle. Cancel it too and it will download automatically.
4. After compiling the apk, rename it to scrcpy-server and replace QtScrcpy/QtScrcpyCore/src/third_party/scrcpy-server.
## Licence
Since it is based on scrcpy, respect its Licence
Since it is based on scrcpy, it uses the same license as scrcpy
Copyright (C) 2025 Rankun
@ -274,7 +322,7 @@ Since it is based on scrcpy, respect its Licence
[Barry CSDN](https://blog.csdn.net/rankun1)
An ordinary programmer, working mainly in C++ for desktop client development, graduated from Shandong for more than a year of steel simulation education software, and later moved to Shanghai to work in security, online education related fields, familiar with audio and video. I have an understanding of audio and video fields such as voice calls, live education, video conferencing and other related solutions. I also have experience in Android, Linux server and other kinds of development.
An ordinary programmer, working mainly in C++ for desktop client development, graduated from Shandong for more than a year of steel simulation education software, and later moved to Shanghai to work in security, online education-related fields, familiar with audio and video. I have an understanding of audio and video fields such as voice calls, live education, video conferencing and other related solutions. I also have experience in Android, Linux servers and other kinds of development.
## Contributors

View file

@ -6,12 +6,14 @@
![license](https://img.shields.io/badge/license-Apache2.0-blue.svg)
![release](https://img.shields.io/github/v/release/barry-ran/QtScrcpy.svg)
![star](https://img.shields.io/github/stars/barry-ran/QtScrcpy.svg)
![star](https://gitcode.com/barry-ran/QtScrcpy/star/badge.svg)
[English introduction](README.md)
[Speaks English? Click me for English introduction.](README.md)
QtScrcpy可以通过USB(或通过TCP/IP)连接Android设备并进行显示和控制。不需要root权限。
QtScrcpy 可以通过 USB / 网络连接Android设备并进行显示和控制。无需root权限。
同时支持GNU/LinuxWindows和MacOS三大主流桌面平台
同时支持 GNU/Linux Windows 和 MacOS 三大主流桌面平台。
它专注于:
@ -19,7 +21,7 @@ QtScrcpy可以通过USB(或通过TCP/IP)连接Android设备并进行显示和
- **性能** (30~60fps)
- **质量** (1920×1080以上)
- **低延迟** ([35~70ms][低延迟])
- **快速启动** (1s内就可以看到第一帧图像)
- **快速启动** (1s 内就可以看到第一帧图像)
- **非侵入性** (不在设备上安装任何软件)
[低延迟]: https://github.com/Genymobile/scrcpy/pull/646
@ -30,43 +32,81 @@ QtScrcpy可以通过USB(或通过TCP/IP)连接Android设备并进行显示和
![linux](screenshot/linux-zh.png)
## 作者开发了更加专业的投屏软件`极限投屏`
极限投屏功能&特点:
- 设备投屏&控制:批量投屏、单个控制、批量控制
- 分组管理
- wifi投屏/OTG投屏
- adb shell快捷指令
- 文件传输、apk安装
- 投屏数量多在OTG投屏模式设置分辨率和流畅度为低的情况下单台电脑可以同时管理500+台手机
- 低延迟usb投屏1080p延迟在30ms以内在相同分辨率流畅度情况下比市面上所有投屏软件延迟都低
- cpu占用率低纯C++开发高性能GPU视频渲染
- 高分辨率:可调节,最大支持安卓终端的原生分辨率
- 完美中文输入支持闲鱼app支持三星手机
- 免费版最多投屏10台功能无限制(除了自动重新投屏)
- 极限投屏使用教程https://lrbnfell4p.feishu.cn/docx/QRMhd9nImorAGgxVLlmczxSdnYf
- 极限投屏qq交流群822464342
- 极限投屏界面预览:
![quickmirror](docs/image/quickmirror.png)
## 自定义按键映射
可以根据需要自己编写脚本将PC键盘按键映射为手机的触摸点击编写规则在[这里](docs/KeyMapDes_zh.md)。
可以根据需要,自己编写脚本将键盘按键映射为手机的触摸点击,编写规则在[这里](docs/KeyMapDes_zh.md)。
默认自带了针对和平精英手游和抖音进行键鼠映射的映射脚本,开启平精英手游后可以用键鼠像玩端游一样玩和平精英手游,开启抖音映射以后可以使用上下左右方向键模拟上下左右滑动,你也可以按照[编写规则](docs/KeyMapDes_zh.md)编写其他游戏的映射文件,默认按键映射如下:
![game](screenshot/game.jpg)
[这里有玩和平精英的视频演示](http://mp.weixin.qq.com/mp/video?__biz=MzU1NTg5MjYyNw==&mid=100000015&sn=3e301fdc5a364bd16d6207fa674bc8b3&vid=wxv_968792362971430913&idx=1&vidsn=eec329cc13c3e24c187dc9b4d5eb8760&fromid=1&scene=20&xtrack=1&clicktime=1567346543&sessionid=1567346375&subscene=92&ascene=0&fasttmpl_type=0&fasttmpl_fullversion=4730859-zh_CN-zip&fasttmpl_flag=0&realreporttime=1567346543910#wechat_redirect)
![game](screenshot/game.png)
自定义按键映射操作方法如下:
- 编写自定义脚本放入keymap目录
- 编写自定义脚本放入 keymap 目录
- 点击刷新脚本,确保脚本可以被检测到
- 选择需要的脚本
- 连接手机并启动服务之后,点击应用脚本
- 按~键数字键1左边切换为自定义映射模式即可体验具体按什么键要看你按键脚本定义的switchKey
- 按`~`(即脚本中定义的 SwitchKey键切换为自定义映射模式即可启用
- 再次按~键切换为正常控制模式
- 要想wasd控制开车记得在载具设置中设置为单摇杆模式
- (对于和平精英等游戏)若想使用方向盘控制载具,记得在载具设置中设置为单摇杆模式
## 群控
如果不会自己手写映射规则,也可以去使用作者开发的`极限手游助手`
极限手游助手功能&特点:
- 通过键盘鼠标畅玩安卓手机游戏
- 按键映射脚本界面化编辑
- 支持暂停电脑端画面,只使用键鼠操作
- 截图&录制手机画面
- 简单批量控制
- 安卓11+支持电脑播放手机音频(开发中...
- 手机端免安装App
- 极速秒连接
- 低延迟usb投屏1080p延迟在30ms以内在相同分辨率流畅度情况下比市面上所有投屏软件延迟都低
- cpu占用率低纯C++开发高性能GPU视频渲染
- 高分辨率:可调节,最大支持安卓终端的原生分辨率
- [QQ交流群901736468](https://qm.qq.com/q/wRJJaWLWc8)
- [极限手游助手说明文档](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh)
## 批量操作
你可以同时控制所有的手机
## Star历史
[![Star History Chart](https://api.star-history.com/svg?repos=barry-ran/QtScrcpy&type=Date)](https://star-history.com/#barry-ran/QtScrcpy&Date)
![gc](docs/image/group-control.gif)
## 感谢
基于[Genymobile](https://github.com/Genymobile)的[scrcpy](https://github.com/Genymobile/scrcpy)项目进行复刻重构非常感谢。QtScrcpy和原版scrcpy区别如下
基于[Genymobile](https://github.com/Genymobile)的[scrcpy](https://github.com/Genymobile/scrcpy)项目进行复刻,重构,非常感谢。
## 比较
QtScrcpy 和 Scrcpy 区别如下:
关键点|scrcpy|QtScrcpy
--|:--:|:--:
界面|sdl|qt
视频解码|ffmpeg|ffmpeg
视频渲染|sdl|opengl
跨平台基础设施|自己封装|Qt提供
跨平台基础设施|自己封装|Qt
编程语言|C|C++
编程方式|同步|异步
按键映射|不支持自定义|支持自定义按键映射
编译方式|meson+gradle|qmake or CMake
编译方式|Meson+Gradle|CMake
- 使用Qt可以非常容易的定制自己的界面
- 基于Qt的信号槽机制的异步编程提高性能
@ -76,111 +116,116 @@ QtScrcpy可以通过USB(或通过TCP/IP)连接Android设备并进行显示和
## 学习它
如果你对它感兴趣,想学习它的实现原理而又感觉无从下手,可以选择购买我录制的视频课程,
里面详细介绍了整个软件的开发架构以及开发流程带你从无到有的开发QtScrcpy
里面详细介绍了整个软件的开发架构以及开发流程,带你从无到有的开发 QtScrcpy
课程介绍:[https://blog.csdn.net/rankun1/article/details/87970523](https://blog.csdn.net/rankun1/article/details/87970523)
或者你也可以加入我的QtScrcpy QQ群和志同道合的朋友一块互相交流技术
或者你也可以加入我的 QtScrcpy QQ 群,和志同道合的朋友一块互相交流技术:
QQ群号901736468
## 要求
Android部分至少需要API 21Android 5.0)。
Android 部分至少需要 API 21Android 5.0)。
您要确保在Android设备上[启用adb调试][enable-adb]。
您要确保在 Android 设备上[启用adb调试][enable-adb]。
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
## 下载这个软件
## 下载
[gitee-download]: https://gitee.com/Barryda/QtScrcpy/releases
[github-download]: https://github.com/barry-ran/QtScrcpy/releases
### Windows
Windows平台你可以直接使用我编译好的可执行程序:
Windows 平台,你可以直接使用我编译好的可执行程序:
- [国内下载][gitee-download]
- [国外下载][github-download]
你也可以[自己编译](##如何编译)
你也可以[自己编译](##编译)
### Mac OS
Mac OS平台你可以直接使用我编译好的可执行程序:
Mac OS 平台,你可以直接使用我编译好的可执行程序:
- [国内下载][gitee-download]
- [国外下载][github-download]
你也可以[自己编译](##如何编译)
你也可以[自己编译](##编译)
### Linux
目前只提供了windows和mac平台的可执行程序如果需要linux平台的可执行程序
对于 Arch Linux 用户,可以使用 AUR 安装:`yay -Syu qtscrcpy`(可能版本并非最新;维护者:[yochananmarqos](https://aur.archlinux.org/account/yochananmarqos)
您通常需要[自己编译](##如何编译)。别担心,这并不难。
其他发行版的用户可以直接使用我编译好的可执行程序:
目前只在ubuntu上测试过
- [国外下载][github-download]
你也可以从 [GitHub Actions](https://github.com/UjhhgtgTeams/QtScrcpy/actions/workflows/ubuntu.yml) 获取最新的自动编译好的软件
当然,你也可以[自己编译](##编译)(不推荐,需要准备环境)
目前只在 Ubuntu 和 Arch Linux 上测试过编译过程
## 运行
在你的电脑上接入Android设备然后运行程序点击`一键USB连接`或者`一键WIFI连接`
在你的电脑上接入Android设备然后运行程序点击 `一键USB连接` 或者 `一键WIFI连接`
### 无线连接步骤(保证手机和电脑在同一个局域网):
1. 安卓手机端在开发者选项中打开usb调试
2. 通过usb连接安卓手机到电脑
3. 点击刷新设备,会看到有设备号更新出来
4. 点击获取设备IP
5. 点击启动adbd
6. 无线连接
7. 再次点击刷新设备发现多出了一个IP地址开头的设备选择这个设备
8. 启动服务
### 无线连接步骤
1. 将手机和电脑连接到同一局域网
2. 安卓手机端在开发者选项中打开 USB 调试
3. 通过 USB 连接安卓手机到电脑
4. 点击刷新设备,会看到有设备号更新出来
5. 点击获取设备 IP
6. 点击启动 adbd
7. 无线连接
8. 再次点击刷新设备,发现多出了一个 IP 地址开头的设备,选择这个设备
9. 启动服务
备注:启动adbd以后不用再连着usb线了以后连接断开都不再需要除非安卓adbd停了需要重新启动
备注:启动 adbd 以后无需继续连接 USB 线,以后连接断开都不再需要,除非 adbd 停止运行
## 界面按钮介绍:
## 界面解释
- 启动配置:启动服务前的功能参数设置
分别可以设置本地录制视频的比特率、分辨率、录制格式、录像保存路径等。
- 仅后台录制:启动服务不现实界面只是录制Android设备屏幕
- 窗口置顶Android设备视频窗口置顶显示
- 自动息屏启动服务以后自动关闭Android设备屏幕节省电量
- 使用reverse服务启动模式出现服务启动失败报错more than one device可以去掉这个勾选尝试连接
- 仅后台录制:启动服务不显示界面,只录制 Android 设备屏幕
- 窗口置顶Android 设备显示窗口置顶
- 自动息屏:启动服务以后,自动关闭 Android 设备屏幕节省电量
- 使用 Reverse服务启动模式出现服务启动失败报错 "more than one device" 可以去掉这个勾选尝试连接
- 刷新设备列表:刷新当前连接的设备
- 启动服务连接到Android设备
- 停止服务断开与Android设备的连接
- 停止所有服务断开所有已连接的Android设备
- 获取设备ip获取到Android设备的ip地址更新到“无线”区域中,方便进行无线连接
- 启动adbd启动Android设备的adbd服务无线连接之前必须要启动
- 无线连接使用无线方式连接Android设备
- 无线断开断开无线方式连接的Android设备
- adb命令行:方便执行自定义adb命令目前不支持阻塞命令例如shell
- 启动服务:连接到 Android 设备
- 停止服务:断开与 Android 设备的连接
- 停止所有服务:断开所有已连接的 Android 设备
- 获取设备ip获取到 Android 设备的 IP 地址,更新到无线区域中,方便进行无线连接
- 启动adbd启动 Android 设备的 adbd 服务,无线连接之前,必须要启动
- 无线连接:使用无线方式连接 Android 设备
- 无线断开:断开无线方式连接的 Android 设备
- 命令行:执行自定义 adb 命令目前不支持阻塞命令例如shell
## 主要功能
- 实时显示Android设备屏幕
## 功能
- 实时显示 Android 设备屏幕
- 实时键鼠控制Android设备
- 屏幕录制
- 截图为png
- 截图
- 无线连接
- 支持多台设备连接
- 多设备连接与批量操作
- 全屏显示
- 窗口置顶
- 安装apk拖拽apk到视频窗口即可安装
- 传输文件拖拽文件到视频窗口即可发送文件到Android设备
- 后台录制:只录制,不显示界面
- 复制粘贴
在计算机和设备之间双向同步剪贴板:
- `Ctrl` + `c`将设备剪贴板复制到计算机剪贴板;
- `Ctrl` + `Shift` + `v`将计算机剪贴板复制到设备剪贴板;
- `Ctrl` +`v` 将计算机剪贴板作为一系列文本事件发送到设备不支持非ASCII字符
- 群控
- 同步设备扬声器声音到电脑(基于[sndcpy](https://github.com/rom1v/sndcpy)仅支持安卓10+
- 安装 apk拖拽apk到显示窗口即可安装
- 传输文件:拖拽文件到显示窗口即可发送文件到 Android 设备
- 后台录制:只录制屏幕,不显示界面
- 剪贴板同步:
在计算机和设备之间同步剪贴板:
- `Ctrl + c`将设备剪贴板复制到计算机剪贴板;
- `Ctrl + Shift + v`将计算机剪贴板复制到设备剪贴板;
- `Ctrl + v` 将计算机剪贴板作为一系列文本事件发送到设备不支持非ASCII字符
- 同步设备扬声器声音到电脑(基于[sndcpy](https://github.com/rom1v/sndcpy)仅支持安卓10级以上目前不推荐使用可使用蓝牙连接替代
## 快捷键
@ -216,41 +261,49 @@ Mac OS平台你可以直接使用我编译好的可执行程序:
[常见问题说明](docs/FAQ.md)
## 开发者
[开发相关](docs/DEVELOP.md)
[开发相关](docs/DEVELOP.md)
欢迎大家一起维护这个项目,贡献自己的代码,不过请遵循下几点要求:
1. pr请提到dev分支不要提到master分支
2. 提pr之前请先rebase dev
3. pr请以少量多次的原则提交建议一个小的功能点提一个pr
4. 代码风格请保持和有风格一致
欢迎大家一起维护这个项目,贡献自己的代码,不过请遵循下几点要求:
1. PR 请推向 dev 分支,不要推向 master 分支
2. 提交 PR 之前请先变基原项目
3. PR 请以少量多次的原则提交(即一个功能点提交一个 PR
4. 代码风格请保持和有风格一致
## 为什么开发QtScrcpy
## 为什么开发 QtScrcpy
综合起来有以下几个原因,比重从大到小排列:
1. 学习Qt的过程中需要一个项目实战一下
2. 本身具有音视频相关技能,对音视频很感兴趣
3. 本身具有Android开发技能好久没用有点生疏需要巩固一下
4. 发现了scrcpy决定用新的技术栈C++ + Qt + Opengl + ffmpeg复刻一下
3. 本身具有 Android 开发技能,好久没用有点生疏,需要巩固一下
4. 发现了 Scrcpy决定用新的技术栈C++ + Qt + Opengl + FFmpeg进行复刻
## 如何编译
## 编译
尽量提供了所有依赖资源,方便傻瓜式编译。
### PC端
1. 在目标平台上搭建Qt开发环境
Qt版本>=5.12在Windows上使用MSVC 2019
2. 克隆该项目git clone --recursive git@github.com:barry-ran/QtScrcpy.git
3. 使用QtCreator打开项目根目录`CMakeLists.txt`
4. 编译,运行
### QtScrcpy
#### 非 Arch Linux
1. 使用官方 Qt Installer 或非官方工具(如 [aqt](https://github.com/miurahr/aqtinstall)在目标平台上搭建Qt开发环境。
需要 5.12 以上版本 Qt在 Windows 上使用 MSVC 2019
2. 克隆该项目:`git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git`
3. Windows 使用 QtCreator 打开项目下 CMakeLists.txt 并编译 Release
4. Linux 用终端执行 `./ci/linux/build_for_linux.sh "Release"`
注:编译结果位于 `output/x64/Release`
### Android端 没有修改需求的话直接使用自带的scrcpy-server即可
1. 目标平台上搭建Android开发环境
2. 使用Android Studio打开项目根目录中的server项目
3. 第一次打开如果你没有对应版本的gradle会提示找不到gradle是否升级gradle并创建选择取消取消后会弹出选择已有gradle的位置同样取消即可会自动下载
4. 按需编辑代码即可,当然也可以不编辑
4. 编译出apk以后改名为scrcpy-server并替换third_party/scrcpy-server即可
#### Arch Linux
1. 安装以下包:`qt5-base qt5-multimedia qt5-x11extras`(推荐安装 `qtcreator`
2. 克隆该项目:`git clone --recurse-submodules git@github.com:barry-ran/QtScrcpy.git`
3. 用终端执行 `./ci/linux/build_for_linux.sh "Release"`
注:编译结果位于 `output/x64/Release`
### Scrcpy-Server
1. 目标平台上搭建 Android 开发环境
2. 使用 Android Studio 打开项目根目录中的 server
3. 第一次打开时,如果你没有对应版本的 GradleStudio 会提示找不到 Gradle是否升级 Gradle 并创建,选择取消,取消后会提示选择 Gradle 的位置同样取消即可。Studio 会随后自动下载。
4. 按需编辑代码
5. 编译出 apk 以后改名为 scrcpy-server 并替换 `third_party/scrcpy-server` 即可
## Licence
由于是复刻的scrcpy尊重它的Licence
由于是复刻的 Scrcpy尊重它的 Licence
Copyright (C) 2025 Rankun
@ -268,6 +321,6 @@ Qt版本>=5.12在Windows上使用MSVC 2019
## 关于作者
[BarryCSDN](https://blog.csdn.net/rankun1)
[BarryCSDN](https://blog.csdn.net/rankun1)
一枚普通的程序员工作中主要使用C++进行桌面客户端开发一毕业在山东做过一年多钢铁仿真教育软件后来转战上海先后从事安防在线教育相关领域工作对音视频比较熟悉对音视频领域如语音通话直播教育视频会议等相关解决方案有所了解。同时具有AndroidLinux服务器等开发经验。
一枚普通的程序员,工作中主要使用 C++ 进行桌面客户端开发一毕业在山东做过一年多钢铁仿真教育软件后来转战上海先后从事安防在线教育相关领域工作对音视频比较熟悉对音视频领域如语音通话直播教育视频会议等相关解决方案有所了解。同时具有AndroidLinux服务器等开发经验。

View file

@ -13,7 +13,7 @@ if __name__ == '__main__':
# print('get tag:', tag)
version = str(tag[1:])
version_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "../QtScrcpy/version"))
version_file = os.path.abspath(os.path.join(os.path.dirname(__file__), "../QtScrcpy/appversion"))
file=open(version_file, 'w')
file.write(version)
file.close()

67
ci/linux/build_for_linux.sh Executable file
View file

@ -0,0 +1,67 @@
echo ---------------------------------------------------------------
echo Check \& Set Environment Variables
echo ---------------------------------------------------------------
# Get Qt path
# ENV_QT_PATH example: /home/barry/Qt5.9.6/5.9.6
echo Current ENV_QT_PATH: $ENV_QT_PATH
echo Current directory: $(pwd)
# Set variables
qt_cmake_path=$ENV_QT_PATH/gcc_64/lib/cmake/Qt5
export PATH=$qt_gcc_path/bin:$PATH
# Remember working directory
old_cd=$(pwd)
# Set working dir to the script's path
cd $(dirname "$0")/.../
echo
echo
echo ---------------------------------------------------------------
echo Check Build Parameters
echo ---------------------------------------------------------------
echo Possible build modes: Debug/Release/MinSizeRel/RelWithDebInfo
build_mode="$1"
if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then
echo "error: unknown build mode, exiting......"
exit 1
fi
echo Current build mode: $build_mode
echo
echo
echo ---------------------------------------------------------------
echo CMake Build Begins
echo ---------------------------------------------------------------
# Remove output folder
output_path=./output
if [ -d "$output_path" ]; then
rm -rf $output_path
fi
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode"
cmake $cmake_params .
if [ $? -ne 0 ] ;then
echo "error: CMake failed, exiting......"
exit 1
fi
cmake --build . --config "$build_mode" -j8
if [ $? -ne 0 ] ;then
echo "error: CMake build failed, exiting......"
exit 1
fi
echo
echo
echo ---------------------------------------------------------------
echo CMake Build Succeeded
echo ---------------------------------------------------------------
# Resume current directory
cd $old_cd
exit 0

View file

@ -1,85 +0,0 @@
echo
echo
echo ---------------------------------------------------------------
echo check ENV
echo ---------------------------------------------------------------
# 从环境变量获取必要参数
# 例如 /home/barry/Qt5.9.6/5.9.6
echo ENV_QT_PATH $ENV_QT_PATH
qt_cmake_path=$ENV_QT_PATH/gcc_64/lib/cmake/Qt5
# 获取绝对路径,保证其他目录执行此脚本依然正确
{
cd $(dirname "$0")
script_path=$(pwd)
cd -
} &> /dev/null # disable output
# 设置当前目录cd的目录影响接下来执行程序的工作目录
old_cd=$(pwd)
cd $(dirname "$0")
# 启动参数声明
build_mode=RelWithDebInfo
echo
echo
echo ---------------------------------------------------------------
echo check build param[Debug/Release/MinSizeRel/RelWithDebInfo]
echo ---------------------------------------------------------------
# 编译参数检查
build_mode=$(echo $1)
if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinSizeRel" && $build_mode != "RelWithDebInfo" ]]; then
echo "error: unkonow build mode -- $1"
exit 1
fi
# 提示
echo current build mode: $build_mode
# 环境变量设置
#export PATH=$qt_gcc_path/bin:$PATH
echo
echo
echo ---------------------------------------------------------------
echo begin cmake build
echo ---------------------------------------------------------------
# 删除输出目录
output_path=$script_path../../output
if [ -d "$output_path" ]; then
rm -rf $output_path
fi
# 删除临时目录
build_path=$script_path/../build_temp
if [ -d "$build_path" ]; then
rm -rf $build_path
fi
mkdir $build_path
cd $build_path
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode"
cmake $cmake_params ../..
if [ $? -ne 0 ] ;then
echo "cmake failed"
exit 1
fi
cmake --build . --config $build_mode -j8
if [ $? -ne 0 ] ;then
echo "cmake build failed"
exit 1
fi
echo
echo
echo ---------------------------------------------------------------
echo finish!!!
echo ---------------------------------------------------------------
# 恢复当前目录
cd $old_cd
exit 0

View file

@ -8,7 +8,6 @@ echo ---------------------------------------------------------------
# 从环境变量获取必要参数
# 例如 /Users/barry/Qt5.12.5/5.12.5
echo ENV_QT_PATH $ENV_QT_PATH
qt_cmake_path=$ENV_QT_PATH/clang_64/lib/cmake/Qt5
# 获取绝对路径,保证其他目录执行此脚本依然正确
{
@ -22,6 +21,7 @@ cd $(dirname "$0")
# 启动参数声明
build_mode=RelWithDebInfo
cpu_arch=arm64
echo
echo
@ -36,8 +36,30 @@ if [[ $build_mode != "Release" && $build_mode != "Debug" && $build_mode != "MinS
exit 1
fi
echo
echo
echo ---------------------------------------------------------------
echo check cpu arch[x64/arm64]
echo ---------------------------------------------------------------
cpu_arch=$(echo $2)
if [[ $cpu_arch != "x64" && $cpu_arch != "arm64" ]]; then
echo "error: unkonow cpu mode -- $2"
exit 1
fi
# 提示
echo current build mode: $build_mode
echo current cpu mode: $cpu_arch
cmake_arch=x86_64
if [ $cpu_arch == "x64" ]; then
qt_cmake_path=$ENV_QT_PATH/clang_64/lib/cmake/Qt5
cmake_arch=x86_64
else
qt_cmake_path=$ENV_QT_PATH/macos/lib/cmake/Qt6
cmake_arch=arm64
fi
echo
echo
@ -58,7 +80,7 @@ fi
mkdir $build_path
cd $build_path
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode -G Xcode"
cmake_params="-DCMAKE_PREFIX_PATH=$qt_cmake_path -DCMAKE_BUILD_TYPE=$build_mode -DCMAKE_OSX_ARCHITECTURES=$cmake_arch"
cmake $cmake_params ../..
if [ $? -ne 0 ] ;then
echo "cmake failed"

View file

@ -7,7 +7,6 @@ echo ---------------------------------------------------------------
# 从环境变量获取必要参数
# 例如 /Users/barry/Qt5.12.5/5.12.5
echo ENV_QT_PATH $ENV_QT_PATH
qt_clang_path=$ENV_QT_PATH/clang_64
# 获取绝对路径,保证其他目录执行此脚本依然正确
{
@ -21,6 +20,27 @@ cd $(dirname "$0")
# 启动参数声明
publish_dir=$1
cpu_arch=$2
echo
echo
echo ---------------------------------------------------------------
echo check cpu arch[x64/arm64]
echo ---------------------------------------------------------------
if [[ $cpu_arch != "x64" && $cpu_arch != "arm64" ]]; then
echo "error: unkonow cpu mode -- $2"
exit 1
fi
# 提示
echo current cpu mode: $cpu_arch
if [ $cpu_arch == "x64" ]; then
qt_clang_path=$ENV_QT_PATH/clang_64
else
qt_clang_path=$ENV_QT_PATH/macos
fi
# 提示
echo current publish dir: $publish_dir
@ -30,7 +50,7 @@ keymap_path=$script_path/../../keymap
# config_path=$script_path/../../config
publish_path=$script_path/$publish_dir
release_path=$script_path/../../output/x64/RelWithDebInfo
release_path=$script_path/../../output/$cpu_arch/RelWithDebInfo
export PATH=$qt_clang_path/bin:$PATH

View file

@ -1,16 +1,18 @@
[common]
# 语言 Auto=自动zh_CN=简体中文en_US=English
Language=Auto
# 窗口标题
WindowTitle=QtScrcpy
# 推送到安卓设备的文件保存路径(必须以/结尾)
PushFilePath=/sdcard/
# 最大fps仅支持Android 10以上
MaxFps=60
MaxFps=0
# 是否渲染过期视频帧(跳过过期视频帧意味着更低的延迟)
RenderExpiredFrames=0
# 视频解码方式:-1 自动0 软解1 dx硬解2 opengl硬解
UseDesktopOpenGL=-1
# scrcpy-server的版本号不要修改
ServerVersion=1.21
ServerVersion=3.1
# scrcpy-server推送到安卓设备的路径
ServerPath=/data/local/tmp/scrcpy-server.jar
# 自定义adb路径例如D:/android/tools/adb.exe
@ -23,5 +25,5 @@ CodecOptions=""
# 例如 CodecName="OMX.qcom.video.encoder.avc"
CodecName=""
# Set the log level (debug, info, warn, error)
LogLevel=info
# Set the log level (verbose, debug, info, warn, error)
LogLevel=verbose

View file

@ -60,6 +60,8 @@ Description of the unique attributes of different key mapping types:
-key The key code to be mapped
-startPos Simulate the start position of touch drag
-endPos Simulate the end position of touch drag
-dragSpeed Speed of the drag movement (range 0-1, default 1.0). Higher values result in faster movements
-startDelay Optional delay in milliseconds to wait after the initial touch before starting the drag movement
-KMT_STEER_WHEEL
-centerPos steering wheel center point
@ -71,3 +73,22 @@ Description of the unique attributes of different key mapping types:
-rightOffset After pressing the right direction key, drag it to the right offset of the center to the right of the centerPos position
-upOffset After pressing the up arrow key, drag it to the upper offset position horizontally relative to the centerPos position
-downOffset Press the down arrow key and drag it to the downOffset position horizontally relative to the centerPos position
## Visual Key Mapping Tool
1. Just use [QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh)
![game](../screenshot/game.png)
2. A web-based GUI tool is available to help you create and manage key mappings visually: [ScrcpyKeyMapper](https://github.com/w4po/ScrcpyKeyMapper)
![ScrcpyKeyMapper Screenshot](https://raw.githubusercontent.com/w4po/ScrcpyKeyMapper/main/assets/screenshot.png)
You can use this tool to:
- Create key mappings visually
- Test your mappings in real-time
- Export mappings as JSON files
- Import existing mappings for editing
Try it online: [ScrcpyKeyMapper Web App](https://w4po.github.io/ScrcpyKeyMapper)

View file

@ -60,6 +60,8 @@
- key 要映射的按键码
- startPos 模拟触摸拖动的开始位置
- endPos 模拟触摸拖动的结束位置
- dragSpeed 拖动移动的速度范围0-1默认1.0)。数值越大,移动越快
- startDelay 可选的延迟时间(毫秒),在开始拖动移动之前等待指定的时间
- KMT_STEER_WHEEL
- centerPos 方向盘中心点
@ -72,5 +74,19 @@
- upOffset 按下上方向键后模拟拖动到相对centerPos位置水平偏上upOffset处
- downOffset 按下下方向键后模拟拖动到相对centerPos位置水平偏下downOffset处
## 可视化按键映射工具
1. 直接使用[QuickAssistant](https://lrbnfell4p.feishu.cn/drive/folder/Hqckfxj5el1Wjpd9uezcX71lnBh)
![game](../screenshot/game.png)
2. 还有一个基于Web的GUI工具可以帮助你直观地创建和管理按键映射[ScrcpyKeyMapper](https://github.com/w4po/ScrcpyKeyMapper)
![ScrcpyKeyMapper截图](https://raw.githubusercontent.com/w4po/ScrcpyKeyMapper/main/assets/screenshot.png)
你可以使用这个工具来:
- 直观地创建按键映射
- 实时测试你的映射
- 导出映射为JSON文件
- 导入现有映射进行编辑
在线试用:[ScrcpyKeyMapper网页应用](https://w4po.github.io/ScrcpyKeyMapper)

BIN
docs/image/quickmirror.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

BIN
screenshot/game.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 823 KiB

8
server/.gitignore vendored
View file

@ -1,8 +0,0 @@
*.iml
.gradle
/local.properties
/.idea/
.DS_Store
/build
/captures
.externalNativeBuild

View file

@ -1,26 +0,0 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 31
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 31
versionCode 12100
versionName "1.21"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation 'junit:junit:4.13.1'
}
apply from: "$project.rootDir/config/android-checkstyle.gradle"

View file

@ -1,88 +0,0 @@
#!/usr/bin/env bash
#
# This script generates the scrcpy binary "manually" (without gradle).
#
# Adapt Android platform and build tools versions (via ANDROID_PLATFORM and
# ANDROID_BUILD_TOOLS environment variables).
#
# Then execute:
#
# BUILD_DIR=my_build_dir ./build_without_gradle.sh
set -e
SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=1.21
PLATFORM_VERSION=31
PLATFORM=${ANDROID_PLATFORM:-$PLATFORM_VERSION}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-31.0.0}
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
CLASSES_DIR="$BUILD_DIR/classes"
SERVER_DIR=$(dirname "$0")
SERVER_BINARY=scrcpy-server
ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar"
echo "Platform: android-$PLATFORM"
echo "Build-tools: $BUILD_TOOLS"
echo "Build dir: $BUILD_DIR"
rm -rf "$CLASSES_DIR" "$BUILD_DIR/$SERVER_BINARY" classes.dex
mkdir -p "$CLASSES_DIR/com/genymobile/scrcpy"
<< EOF cat > "$CLASSES_DIR/com/genymobile/scrcpy/BuildConfig.java"
package com.genymobile.scrcpy;
public final class BuildConfig {
public static final boolean DEBUG = $SCRCPY_DEBUG;
public static final String VERSION_NAME = "$SCRCPY_VERSION_NAME";
}
EOF
echo "Generating java from aidl..."
cd "$SERVER_DIR/src/main/aidl"
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/view/IRotationWatcher.aidl
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/aidl" -o"$CLASSES_DIR" \
android/content/IOnPrimaryClipChangedListener.aidl
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_JAR" -cp "$CLASSES_DIR" -d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java
echo "Dexing..."
cd "$CLASSES_DIR"
if [[ $PLATFORM_VERSION -lt 31 ]]
then
# use dx
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/dx" --dex \
--output "$BUILD_DIR/classes.dex" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
echo "Archiving..."
cd "$BUILD_DIR"
jar cvf "$SERVER_BINARY" classes.dex
rm -rf classes.dex classes
else
# use d8
"$ANDROID_HOME/build-tools/$BUILD_TOOLS/d8" --classpath "$ANDROID_JAR" \
--output "$BUILD_DIR/classes.zip" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
cd "$BUILD_DIR"
mv classes.zip "$SERVER_BINARY"
rm -rf classes
fi
echo "Server generated in $BUILD_DIR/$SERVER_BINARY"

View file

@ -1,28 +0,0 @@
apply plugin: 'checkstyle'
check.dependsOn 'checkstyle'
checkstyle {
toolVersion = '6.19'
}
task checkstyle(type: Checkstyle) {
description = "Check Java style with Checkstyle"
configFile = rootProject.file("config/checkstyle/checkstyle.xml")
source = javaSources()
classpath = files()
ignoreFailures = true
}
def javaSources() {
def files = []
android.sourceSets.each { sourceSet ->
sourceSet.java.each { javaSource ->
javaSource.getSrcDirs().each {
if (it.exists()) {
files.add(it)
}
}
}
}
return files
}

View file

@ -1,25 +0,0 @@
# It may be useful to use a prebuilt server, so that no Android SDK is required
# to build. If the 'prebuilt_server' option is set, just copy the file as is.
prebuilt_server = get_option('prebuilt_server')
if prebuilt_server == ''
custom_target('scrcpy-server',
# gradle is responsible for tracking source changes
build_by_default: true,
build_always_stale: true,
output: 'scrcpy-server',
command: [find_program('./scripts/build-wrapper.sh'), meson.current_source_dir(), '@OUTPUT@', get_option('buildtype')],
console: true,
install: true,
install_dir: 'share/scrcpy')
else
if not prebuilt_server.startswith('/')
# relative path needs some trick
prebuilt_server = meson.source_root() + '/' + prebuilt_server
endif
custom_target('scrcpy-server-prebuilt',
input: prebuilt_server,
output: 'scrcpy-server',
command: ['cp', '@INPUT@', '@OUTPUT@'],
install: true,
install_dir: 'share/scrcpy')
endif

View file

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -1,2 +0,0 @@
<!-- not a real Android application, it is run by app_process manually -->
<manifest package="com.genymobile.scrcpy"/>

View file

@ -1,24 +0,0 @@
/**
* Copyright (c) 2008, The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.content;
/**
* {@hide}
*/
oneway interface IOnPrimaryClipChangedListener {
void dispatchPrimaryClipChanged();
}

View file

@ -1,25 +0,0 @@
/* //device/java/android/android/hardware/ISensorListener.aidl
**
** Copyright 2008, The Android Open Source Project
**
** Licensed under the Apache License, Version 2.0 (the "License");
** you may not use this file except in compliance with the License.
** You may obtain a copy of the License at
**
** http://www.apache.org/licenses/LICENSE-2.0
**
** Unless required by applicable law or agreed to in writing, software
** distributed under the License is distributed on an "AS IS" BASIS,
** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
** See the License for the specific language governing permissions and
** limitations under the License.
*/
package android.view;
/**
* {@hide}
*/
interface IRotationWatcher {
oneway void onRotationChanged(int rotation);
}

View file

@ -1,197 +0,0 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Base64;
import java.io.File;
import java.io.IOException;
/**
* Handle the cleanup of scrcpy, even if the main process is killed.
* <p>
* This is useful to restore some state when scrcpy is closed, even on device disconnection (which kills the scrcpy process).
*/
public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
// A simple struct to be passed from the main process to the cleanup process
public static class Config implements Parcelable {
public static final Creator<Config> CREATOR = new Creator<Config>() {
@Override
public Config createFromParcel(Parcel in) {
return new Config(in);
}
@Override
public Config[] newArray(int size) {
return new Config[size];
}
};
private static final int FLAG_DISABLE_SHOW_TOUCHES = 1;
private static final int FLAG_RESTORE_NORMAL_POWER_MODE = 2;
private static final int FLAG_POWER_OFF_SCREEN = 4;
private int displayId;
// Restore the value (between 0 and 7), -1 to not restore
// <https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN>
private int restoreStayOn = -1;
private boolean disableShowTouches;
private boolean restoreNormalPowerMode;
private boolean powerOffScreen;
public Config() {
// Default constructor, the fields are initialized by CleanUp.configure()
}
protected Config(Parcel in) {
displayId = in.readInt();
restoreStayOn = in.readInt();
byte options = in.readByte();
disableShowTouches = (options & FLAG_DISABLE_SHOW_TOUCHES) != 0;
restoreNormalPowerMode = (options & FLAG_RESTORE_NORMAL_POWER_MODE) != 0;
powerOffScreen = (options & FLAG_POWER_OFF_SCREEN) != 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(displayId);
dest.writeInt(restoreStayOn);
byte options = 0;
if (disableShowTouches) {
options |= FLAG_DISABLE_SHOW_TOUCHES;
}
if (restoreNormalPowerMode) {
options |= FLAG_RESTORE_NORMAL_POWER_MODE;
}
if (powerOffScreen) {
options |= FLAG_POWER_OFF_SCREEN;
}
dest.writeByte(options);
}
private boolean hasWork() {
return disableShowTouches || restoreStayOn != -1 || restoreNormalPowerMode || powerOffScreen;
}
@Override
public int describeContents() {
return 0;
}
byte[] serialize() {
Parcel parcel = Parcel.obtain();
writeToParcel(parcel, 0);
byte[] bytes = parcel.marshall();
parcel.recycle();
return bytes;
}
static Config deserialize(byte[] bytes) {
Parcel parcel = Parcel.obtain();
parcel.unmarshall(bytes, 0, bytes.length);
parcel.setDataPosition(0);
return CREATOR.createFromParcel(parcel);
}
static Config fromBase64(String base64) {
byte[] bytes = Base64.decode(base64, Base64.NO_WRAP);
return deserialize(bytes);
}
String toBase64() {
byte[] bytes = serialize();
return Base64.encodeToString(bytes, Base64.NO_WRAP);
}
}
private CleanUp() {
// not instantiable
}
public static void configure(int displayId, int restoreStayOn, boolean disableShowTouches, boolean restoreNormalPowerMode, boolean powerOffScreen)
throws IOException {
Config config = new Config();
config.displayId = displayId;
config.disableShowTouches = disableShowTouches;
config.restoreStayOn = restoreStayOn;
config.restoreNormalPowerMode = restoreNormalPowerMode;
config.powerOffScreen = powerOffScreen;
if (config.hasWork()) {
startProcess(config);
} else {
// There is no additional clean up to do when scrcpy dies
unlinkSelf();
}
}
private static void startProcess(Config config) throws IOException {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH);
builder.start();
}
private static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
}
}
public static void main(String... args) {
unlinkSelf();
try {
// Wait for the server to die
System.in.read();
} catch (IOException e) {
// Expected when the server is dead
}
Ln.i("Cleaning up");
Config config = Config.fromBase64(args[0]);
if (config.disableShowTouches || config.restoreStayOn != -1) {
ServiceManager serviceManager = new ServiceManager();
Settings settings = new Settings(serviceManager);
if (config.disableShowTouches) {
Ln.i("Disabling \"show touches\"");
try {
settings.putValue(Settings.TABLE_SYSTEM, "show_touches", "0");
} catch (SettingsException e) {
Ln.e("Could not restore \"show_touches\"", e);
}
}
if (config.restoreStayOn != -1) {
Ln.i("Restoring \"stay awake\"");
try {
settings.putValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(config.restoreStayOn));
} catch (SettingsException e) {
Ln.e("Could not restore \"stay_on_while_plugged_in\"", e);
}
}
}
if (Device.isScreenOn()) {
if (config.powerOffScreen) {
Ln.i("Power off screen");
Device.powerOffScreen(config.displayId);
} else if (config.restoreNormalPowerMode) {
Ln.i("Restoring normal power mode");
Device.setScreenPowerMode(Device.POWER_MODE_NORMAL);
}
}
}
}

View file

@ -1,112 +0,0 @@
package com.genymobile.scrcpy;
import java.util.ArrayList;
import java.util.List;
public class CodecOption {
private String key;
private Object value;
public CodecOption(String key, Object value) {
this.key = key;
this.value = value;
}
public String getKey() {
return key;
}
public Object getValue() {
return value;
}
public static List<CodecOption> parse(String codecOptions) {
if (codecOptions.isEmpty()) {
return null;
}
List<CodecOption> result = new ArrayList<>();
boolean escape = false;
StringBuilder buf = new StringBuilder();
for (char c : codecOptions.toCharArray()) {
switch (c) {
case '\\':
if (escape) {
buf.append('\\');
escape = false;
} else {
escape = true;
}
break;
case ',':
if (escape) {
buf.append(',');
escape = false;
} else {
// This comma is a separator between codec options
String codecOption = buf.toString();
result.add(parseOption(codecOption));
// Clear buf
buf.setLength(0);
}
break;
default:
buf.append(c);
break;
}
}
if (buf.length() > 0) {
String codecOption = buf.toString();
result.add(parseOption(codecOption));
}
return result;
}
private static CodecOption parseOption(String option) {
int equalSignIndex = option.indexOf('=');
if (equalSignIndex == -1) {
throw new IllegalArgumentException("'=' expected");
}
String keyAndType = option.substring(0, equalSignIndex);
if (keyAndType.length() == 0) {
throw new IllegalArgumentException("Key may not be null");
}
String key;
String type;
int colonIndex = keyAndType.indexOf(':');
if (colonIndex != -1) {
key = keyAndType.substring(0, colonIndex);
type = keyAndType.substring(colonIndex + 1);
} else {
key = keyAndType;
type = "int"; // assume int by default
}
Object value;
String valueString = option.substring(equalSignIndex + 1);
switch (type) {
case "int":
value = Integer.parseInt(valueString);
break;
case "long":
value = Long.parseLong(valueString);
break;
case "float":
value = Float.parseFloat(valueString);
break;
case "string":
value = valueString;
break;
default:
throw new IllegalArgumentException("Invalid codec option type (int, long, float, str): " + type);
}
return new CodecOption(key, value);
}
}

View file

@ -1,33 +0,0 @@
package com.genymobile.scrcpy;
import java.io.IOException;
import java.util.Arrays;
import java.util.Scanner;
public final class Command {
private Command() {
// not instantiable
}
public static void exec(String... cmd) throws IOException, InterruptedException {
Process process = Runtime.getRuntime().exec(cmd);
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
}
}
public static String execReadLine(String... cmd) throws IOException, InterruptedException {
String result = null;
Process process = Runtime.getRuntime().exec(cmd);
Scanner scanner = new Scanner(process.getInputStream());
if (scanner.hasNextLine()) {
result = scanner.nextLine();
}
int exitCode = process.waitFor();
if (exitCode != 0) {
throw new IOException("Command " + Arrays.toString(cmd) + " returned with value " + exitCode);
}
return result;
}
}

View file

@ -1,181 +0,0 @@
package com.genymobile.scrcpy;
/**
* Union of all supported event types, identified by their {@code type}.
*/
public final class ControlMessage {
public static final int TYPE_INJECT_KEYCODE = 0;
public static final int TYPE_INJECT_TEXT = 1;
public static final int TYPE_INJECT_TOUCH_EVENT = 2;
public static final int TYPE_INJECT_SCROLL_EVENT = 3;
public static final int TYPE_BACK_OR_SCREEN_ON = 4;
public static final int TYPE_EXPAND_NOTIFICATION_PANEL = 5;
public static final int TYPE_EXPAND_SETTINGS_PANEL = 6;
public static final int TYPE_COLLAPSE_PANELS = 7;
public static final int TYPE_GET_CLIPBOARD = 8;
public static final int TYPE_SET_CLIPBOARD = 9;
public static final int TYPE_SET_SCREEN_POWER_MODE = 10;
public static final int TYPE_ROTATE_DEVICE = 11;
public static final long SEQUENCE_INVALID = 0;
public static final int COPY_KEY_NONE = 0;
public static final int COPY_KEY_COPY = 1;
public static final int COPY_KEY_CUT = 2;
private int type;
private String text;
private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_*
private int buttons; // MotionEvent.BUTTON_*
private long pointerId;
private float pressure;
private Position position;
private int hScroll;
private int vScroll;
private int copyKey;
private boolean paste;
private int repeat;
private long sequence;
private ControlMessage() {
}
public static ControlMessage createInjectKeycode(int action, int keycode, int repeat, int metaState) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_KEYCODE;
msg.action = action;
msg.keycode = keycode;
msg.repeat = repeat;
msg.metaState = metaState;
return msg;
}
public static ControlMessage createInjectText(String text) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TEXT;
msg.text = text;
return msg;
}
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TOUCH_EVENT;
msg.action = action;
msg.pointerId = pointerId;
msg.pressure = pressure;
msg.position = position;
msg.buttons = buttons;
return msg;
}
public static ControlMessage createInjectScrollEvent(Position position, int hScroll, int vScroll) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_SCROLL_EVENT;
msg.position = position;
msg.hScroll = hScroll;
msg.vScroll = vScroll;
return msg;
}
public static ControlMessage createBackOrScreenOn(int action) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_BACK_OR_SCREEN_ON;
msg.action = action;
return msg;
}
public static ControlMessage createGetClipboard(int copyKey) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_GET_CLIPBOARD;
msg.copyKey = copyKey;
return msg;
}
public static ControlMessage createSetClipboard(long sequence, String text, boolean paste) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_CLIPBOARD;
msg.sequence = sequence;
msg.text = text;
msg.paste = paste;
return msg;
}
/**
* @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants
*/
public static ControlMessage createSetScreenPowerMode(int mode) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_SET_SCREEN_POWER_MODE;
msg.action = mode;
return msg;
}
public static ControlMessage createEmpty(int type) {
ControlMessage msg = new ControlMessage();
msg.type = type;
return msg;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
public int getMetaState() {
return metaState;
}
public int getAction() {
return action;
}
public int getKeycode() {
return keycode;
}
public int getButtons() {
return buttons;
}
public long getPointerId() {
return pointerId;
}
public float getPressure() {
return pressure;
}
public Position getPosition() {
return position;
}
public int getHScroll() {
return hScroll;
}
public int getVScroll() {
return vScroll;
}
public int getCopyKey() {
return copyKey;
}
public boolean getPaste() {
return paste;
}
public int getRepeat() {
return repeat;
}
public long getSequence() {
return sequence;
}
}

View file

@ -1,212 +0,0 @@
package com.genymobile.scrcpy;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
static final int GET_CLIPBOARD_LENGTH = 1;
static final int SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH = 9;
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 14; // type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
public static final int INJECT_TEXT_MAX_LENGTH = 300;
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
public ControlMessageReader() {
// invariant: the buffer is always in "get" mode
buffer.limit(0);
}
public boolean isFull() {
return buffer.remaining() == rawBuffer.length;
}
public void readFrom(InputStream input) throws IOException {
if (isFull()) {
throw new IllegalStateException("Buffer full, call next() to consume");
}
buffer.compact();
int head = buffer.position();
int r = input.read(rawBuffer, head, rawBuffer.length - head);
if (r == -1) {
throw new EOFException("Controller socket closed");
}
buffer.position(head + r);
buffer.flip();
}
public ControlMessage next() {
if (!buffer.hasRemaining()) {
return null;
}
int savedPosition = buffer.position();
int type = buffer.get();
ControlMessage msg;
switch (type) {
case ControlMessage.TYPE_INJECT_KEYCODE:
msg = parseInjectKeycode();
break;
case ControlMessage.TYPE_INJECT_TEXT:
msg = parseInjectText();
break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
msg = parseInjectTouchEvent();
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
msg = parseInjectScrollEvent();
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
msg = parseBackOrScreenOnEvent();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
msg = parseGetClipboard();
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
msg = parseSetClipboard();
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
msg = parseSetScreenPowerMode();
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
case ControlMessage.TYPE_COLLAPSE_PANELS:
case ControlMessage.TYPE_ROTATE_DEVICE:
msg = ControlMessage.createEmpty(type);
break;
default:
Ln.w("Unknown event type: " + type);
msg = null;
break;
}
if (msg == null) {
// failure, reset savedPosition
buffer.position(savedPosition);
}
return msg;
}
private ControlMessage parseInjectKeycode() {
if (buffer.remaining() < INJECT_KEYCODE_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
int keycode = buffer.getInt();
int repeat = buffer.getInt();
int metaState = buffer.getInt();
return ControlMessage.createInjectKeycode(action, keycode, repeat, metaState);
}
private String parseString() {
if (buffer.remaining() < 4) {
return null;
}
int len = buffer.getInt();
if (buffer.remaining() < len) {
return null;
}
int position = buffer.position();
// Move the buffer position to consume the text
buffer.position(position + len);
return new String(rawBuffer, position, len, StandardCharsets.UTF_8);
}
private ControlMessage parseInjectText() {
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createInjectText(text);
}
private ControlMessage parseInjectTouchEvent() {
if (buffer.remaining() < INJECT_TOUCH_EVENT_PAYLOAD_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
long pointerId = buffer.getLong();
Position position = readPosition(buffer);
// 16 bits fixed-point
int pressureInt = toUnsigned(buffer.getShort());
// convert it to a float between 0 and 1 (0x1p16f is 2^16 as float)
float pressure = pressureInt == 0xffff ? 1f : (pressureInt / 0x1p16f);
int buttons = buffer.getInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
}
private ControlMessage parseInjectScrollEvent() {
if (buffer.remaining() < INJECT_SCROLL_EVENT_PAYLOAD_LENGTH) {
return null;
}
Position position = readPosition(buffer);
int hScroll = buffer.getInt();
int vScroll = buffer.getInt();
return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll);
}
private ControlMessage parseBackOrScreenOnEvent() {
if (buffer.remaining() < BACK_OR_SCREEN_ON_LENGTH) {
return null;
}
int action = toUnsigned(buffer.get());
return ControlMessage.createBackOrScreenOn(action);
}
private ControlMessage parseGetClipboard() {
if (buffer.remaining() < GET_CLIPBOARD_LENGTH) {
return null;
}
int copyKey = toUnsigned(buffer.get());
return ControlMessage.createGetClipboard(copyKey);
}
private ControlMessage parseSetClipboard() {
if (buffer.remaining() < SET_CLIPBOARD_FIXED_PAYLOAD_LENGTH) {
return null;
}
long sequence = buffer.getLong();
boolean paste = buffer.get() != 0;
String text = parseString();
if (text == null) {
return null;
}
return ControlMessage.createSetClipboard(sequence, text, paste);
}
private ControlMessage parseSetScreenPowerMode() {
if (buffer.remaining() < SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH) {
return null;
}
int mode = buffer.get();
return ControlMessage.createSetScreenPowerMode(mode);
}
private static Position readPosition(ByteBuffer buffer) {
int x = buffer.getInt();
int y = buffer.getInt();
int screenWidth = toUnsigned(buffer.getShort());
int screenHeight = toUnsigned(buffer.getShort());
return new Position(x, y, screenWidth, screenHeight);
}
private static int toUnsigned(short value) {
return value & 0xffff;
}
private static int toUnsigned(byte value) {
return value & 0xff;
}
}

View file

@ -1,315 +0,0 @@
package com.genymobile.scrcpy;
import android.os.Build;
import android.os.SystemClock;
import android.view.InputDevice;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import android.view.MotionEvent;
import java.io.IOException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Controller {
private static final int DEFAULT_DEVICE_ID = 0;
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private final Device device;
private final DesktopConnection connection;
private final DeviceMessageSender sender;
private final boolean clipboardAutosync;
private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD);
private long lastTouchDown;
private final PointersState pointersState = new PointersState();
private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS];
private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS];
private boolean keepPowerModeOff;
public Controller(Device device, DesktopConnection connection, boolean clipboardAutosync) {
this.device = device;
this.connection = connection;
this.clipboardAutosync = clipboardAutosync;
initPointers();
sender = new DeviceMessageSender(connection);
}
private void initPointers() {
for (int i = 0; i < PointersState.MAX_POINTERS; ++i) {
MotionEvent.PointerProperties props = new MotionEvent.PointerProperties();
props.toolType = MotionEvent.TOOL_TYPE_FINGER;
MotionEvent.PointerCoords coords = new MotionEvent.PointerCoords();
coords.orientation = 0;
coords.size = 0;
pointerProperties[i] = props;
pointerCoords[i] = coords;
}
}
public void control() throws IOException {
// on start, power on the device
if (!Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
// dirty hack
// After POWER is injected, the device is powered on asynchronously.
// To turn the device screen off while mirroring, the client will send a message that
// would be handled before the device is actually powered on, so its effect would
// be "canceled" once the device is turned back on.
// Adding this delay prevents to handle the message before the device is actually
// powered on.
SystemClock.sleep(500);
}
while (true) {
handleEvent();
}
}
public DeviceMessageSender getSender() {
return sender;
}
private void handleEvent() throws IOException {
ControlMessage msg = connection.receiveControlMessage();
switch (msg.getType()) {
case ControlMessage.TYPE_INJECT_KEYCODE:
if (device.supportsInputEvents()) {
injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState());
}
break;
case ControlMessage.TYPE_INJECT_TEXT:
if (device.supportsInputEvents()) {
injectText(msg.getText());
}
break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
}
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
if (device.supportsInputEvents()) {
injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll());
}
break;
case ControlMessage.TYPE_BACK_OR_SCREEN_ON:
if (device.supportsInputEvents()) {
pressBackOrTurnScreenOn(msg.getAction());
}
break;
case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL:
Device.expandNotificationPanel();
break;
case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL:
Device.expandSettingsPanel();
break;
case ControlMessage.TYPE_COLLAPSE_PANELS:
Device.collapsePanels();
break;
case ControlMessage.TYPE_GET_CLIPBOARD:
getClipboard(msg.getCopyKey());
break;
case ControlMessage.TYPE_SET_CLIPBOARD:
setClipboard(msg.getText(), msg.getPaste(), msg.getSequence());
break;
case ControlMessage.TYPE_SET_SCREEN_POWER_MODE:
if (device.supportsInputEvents()) {
int mode = msg.getAction();
boolean setPowerModeOk = Device.setScreenPowerMode(mode);
if (setPowerModeOk) {
keepPowerModeOff = mode == Device.POWER_MODE_OFF;
Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on"));
}
}
break;
case ControlMessage.TYPE_ROTATE_DEVICE:
Device.rotateDevice();
break;
default:
// do nothing
}
}
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)) {
schedulePowerModeOff();
}
return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC);
}
private boolean injectChar(char c) {
String decomposed = KeyComposition.decompose(c);
char[] chars = decomposed != null ? decomposed.toCharArray() : new char[]{c};
KeyEvent[] events = charMap.getEvents(chars);
if (events == null) {
return false;
}
for (KeyEvent event : events) {
if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
return true;
}
private int injectText(String text) {
int successCount = 0;
for (char c : text.toCharArray()) {
if (!injectChar(c)) {
Ln.w("Could not inject char u+" + String.format("%04x", (int) c));
continue;
}
successCount++;
}
return successCount;
}
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position);
if (point == null) {
Ln.w("Ignore touch event, it was generated for a different device size");
return false;
}
int pointerIndex = pointersState.getPointerIndex(pointerId);
if (pointerIndex == -1) {
Ln.w("Too many pointers for touch event");
return false;
}
Pointer pointer = pointersState.get(pointerIndex);
pointer.setPoint(point);
pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP);
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) {
lastTouchDown = now;
}
} else {
// secondary pointers must use ACTION_POINTER_* ORed with the pointerIndex
if (action == MotionEvent.ACTION_UP) {
action = MotionEvent.ACTION_POINTER_UP | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
} else if (action == MotionEvent.ACTION_DOWN) {
action = MotionEvent.ACTION_POINTER_DOWN | (pointerIndex << MotionEvent.ACTION_POINTER_INDEX_SHIFT);
}
}
// Right-click and middle-click only work if the source is a mouse
boolean nonPrimaryButtonPressed = (buttons & ~MotionEvent.BUTTON_PRIMARY) != 0;
int source = nonPrimaryButtonPressed ? InputDevice.SOURCE_MOUSE : InputDevice.SOURCE_TOUCHSCREEN;
if (source != InputDevice.SOURCE_MOUSE) {
// Buttons must not be set for touch events
buttons = 0;
}
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0);
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
}
private boolean injectScroll(Position position, int hScroll, int vScroll) {
long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position);
if (point == null) {
// ignore event
return false;
}
MotionEvent.PointerProperties props = pointerProperties[0];
props.id = 0;
MotionEvent.PointerCoords coords = pointerCoords[0];
coords.x = point.getX();
coords.y = point.getY();
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, 0, 1f, 1f, DEFAULT_DEVICE_ID, 0,
InputDevice.SOURCE_MOUSE, 0);
return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
}
/**
* Schedule a call to set power mode to off after a small delay.
*/
private static void schedulePowerModeOff() {
EXECUTOR.schedule(new Runnable() {
@Override
public void run() {
Ln.i("Forcing screen off");
Device.setScreenPowerMode(Device.POWER_MODE_OFF);
}
}, 200, TimeUnit.MILLISECONDS);
}
private boolean pressBackOrTurnScreenOn(int action) {
if (Device.isScreenOn()) {
return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC);
}
// Screen is off
// Only press POWER on ACTION_DOWN
if (action != KeyEvent.ACTION_DOWN) {
// do nothing,
return true;
}
if (keepPowerModeOff) {
schedulePowerModeOff();
}
return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
}
private void getClipboard(int copyKey) {
// On Android >= 7, press the COPY or CUT key if requested
if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT;
// Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one
device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH);
}
// If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in
// particular when COPY or CUT are injected, so it should not be synchronized twice. On Android < 7, do not synchronize at all rather than
// copying an old clipboard content.
if (!clipboardAutosync) {
String clipboardText = Device.getClipboardText();
if (clipboardText != null) {
sender.pushClipboardText(clipboardText);
}
}
}
private boolean setClipboard(String text, boolean paste, long sequence) {
boolean ok = device.setClipboardText(text);
if (ok) {
Ln.i("Device clipboard set");
}
// On Android >= 7, also press the PASTE key if requested
if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC);
}
if (sequence != ControlMessage.SEQUENCE_INVALID) {
// Acknowledgement requested
sender.pushAckClipboard(sequence);
}
return ok;
}
}

View file

@ -1,118 +0,0 @@
package com.genymobile.scrcpy;
import android.net.LocalServerSocket;
import android.net.LocalSocket;
import android.net.LocalSocketAddress;
import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
public final class DesktopConnection implements Closeable {
private static final int DEVICE_NAME_FIELD_LENGTH = 64;
private static final String SOCKET_NAME = "scrcpy";
private final LocalSocket videoSocket;
private final FileDescriptor videoFd;
private final LocalSocket controlSocket;
private final InputStream controlInputStream;
private final OutputStream controlOutputStream;
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket;
this.controlSocket = controlSocket;
controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream();
videoFd = videoSocket.getFileDescriptor();
}
private static LocalSocket connect(String abstractName) throws IOException {
LocalSocket localSocket = new LocalSocket();
localSocket.connect(new LocalSocketAddress(abstractName));
return localSocket;
}
public static DesktopConnection open(Device device, boolean tunnelForward) throws IOException {
LocalSocket videoSocket;
LocalSocket controlSocket;
if (tunnelForward) {
LocalServerSocket localServerSocket = new LocalServerSocket(SOCKET_NAME);
try {
videoSocket = localServerSocket.accept();
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
try {
controlSocket = localServerSocket.accept();
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
} finally {
localServerSocket.close();
}
} else {
videoSocket = connect(SOCKET_NAME);
try {
controlSocket = connect(SOCKET_NAME);
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
}
DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
Size videoSize = device.getScreenInfo().getVideoSize();
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
return connection;
}
public void close() throws IOException {
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
videoSocket.close();
controlSocket.shutdownInput();
controlSocket.shutdownOutput();
controlSocket.close();
}
private void send(String deviceName, int width, int height) throws IOException {
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
IO.writeFully(videoFd, buffer, 0, buffer.length);
}
public FileDescriptor getVideoFd() {
return videoFd;
}
public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {
reader.readFrom(controlInputStream);
msg = reader.next();
}
return msg;
}
public void sendDeviceMessage(DeviceMessage msg) throws IOException {
writer.writeTo(msg, controlOutputStream);
}
}

View file

@ -1,307 +0,0 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import com.genymobile.scrcpy.wrappers.WindowManager;
import android.content.IOnPrimaryClipChangedListener;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.IRotationWatcher;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
import android.view.KeyEvent;
import java.util.concurrent.atomic.AtomicBoolean;
public final class Device {
public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF;
public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL;
public static final int INJECT_MODE_ASYNC = InputManager.INJECT_INPUT_EVENT_MODE_ASYNC;
public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT;
public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH;
public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1;
public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2;
private static final ServiceManager SERVICE_MANAGER = new ServiceManager();
private static final Settings SETTINGS = new Settings(SERVICE_MANAGER);
public interface RotationListener {
void onRotationChanged(int rotation);
}
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
/**
* Logical display identifier
*/
private final int displayId;
/**
* The surface flinger layer stack associated with this logical display
*/
private final int layerStack;
private final boolean supportsInputEvents;
public Device(Options options) {
displayId = options.getDisplayId();
DisplayInfo displayInfo = SERVICE_MANAGER.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
int[] displayIds = SERVICE_MANAGER.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds);
}
int displayInfoFlags = displayInfo.getFlags();
screenInfo = ScreenInfo.computeScreenInfo(displayInfo, options.getCrop(), options.getMaxSize(), options.getLockVideoOrientation());
layerStack = displayInfo.getLayerStack();
SERVICE_MANAGER.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() {
@Override
public void onRotationChanged(int rotation) {
synchronized (Device.this) {
screenInfo = screenInfo.withDeviceRotation(rotation);
// notify
if (rotationListener != null) {
rotationListener.onRotationChanged(rotation);
}
}
}
}, displayId);
if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
if (clipboardManager != null) {
clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() {
@Override
public void dispatchPrimaryClipChanged() {
if (isSettingClipboard.get()) {
// This is a notification for the change we are currently applying, ignore it
return;
}
synchronized (Device.this) {
if (clipboardListener != null) {
String text = getClipboardText();
if (text != null) {
clipboardListener.onClipboardTextChanged(text);
}
}
}
}
});
} else {
Ln.w("No clipboard manager, copy-paste between device and computer will not work");
}
}
if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) {
Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted");
}
// main display or any display on Android >= Q
supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
if (!supportsInputEvents) {
Ln.w("Input events are not supported for secondary displays before Android 10");
}
}
public synchronized ScreenInfo getScreenInfo() {
return screenInfo;
}
public int getLayerStack() {
return layerStack;
}
public Point getPhysicalPoint(Position position) {
// it hides the field on purpose, to read it with a lock
@SuppressWarnings("checkstyle:HiddenField")
ScreenInfo screenInfo = getScreenInfo(); // read with synchronization
// ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation
Size unlockedVideoSize = screenInfo.getUnlockedVideoSize();
int reverseVideoRotation = screenInfo.getReverseVideoRotation();
// reverse the video rotation to apply the events
Position devicePosition = position.rotate(reverseVideoRotation);
Size clientVideoSize = devicePosition.getScreenSize();
if (!unlockedVideoSize.equals(clientVideoSize)) {
// The client sends a click relative to a video with wrong dimensions,
// the device may have been rotated since the event was generated, so ignore the event
return null;
}
Rect contentRect = screenInfo.getContentRect();
Point point = devicePosition.getPoint();
int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth();
int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight();
return new Point(convertedX, convertedY);
}
public static String getDeviceName() {
return Build.MODEL;
}
public static boolean supportsInputEvents(int displayId) {
return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
}
public boolean supportsInputEvents() {
return supportsInputEvents;
}
public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) {
if (!supportsInputEvents(displayId)) {
throw new AssertionError("Could not inject input event if !supportsInputEvents()");
}
if (displayId != 0 && !InputManager.setDisplayId(inputEvent, displayId)) {
return false;
}
return SERVICE_MANAGER.getInputManager().injectInputEvent(inputEvent, injectMode);
}
public boolean injectEvent(InputEvent event, int injectMode) {
return injectEvent(event, displayId, injectMode);
}
public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) {
long now = SystemClock.uptimeMillis();
KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0,
InputDevice.SOURCE_KEYBOARD);
return injectEvent(event, displayId, injectMode);
}
public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) {
return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode);
}
public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) {
return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode)
&& injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode);
}
public boolean pressReleaseKeycode(int keyCode, int injectMode) {
return pressReleaseKeycode(keyCode, displayId, injectMode);
}
public static boolean isScreenOn() {
return SERVICE_MANAGER.getPowerManager().isScreenOn();
}
public synchronized void setRotationListener(RotationListener rotationListener) {
this.rotationListener = rotationListener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}
public static void expandNotificationPanel() {
SERVICE_MANAGER.getStatusBarManager().expandNotificationsPanel();
}
public static void expandSettingsPanel() {
SERVICE_MANAGER.getStatusBarManager().expandSettingsPanel();
}
public static void collapsePanels() {
SERVICE_MANAGER.getStatusBarManager().collapsePanels();
}
public static String getClipboardText() {
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
if (clipboardManager == null) {
return null;
}
CharSequence s = clipboardManager.getText();
if (s == null) {
return null;
}
return s.toString();
}
public boolean setClipboardText(String text) {
ClipboardManager clipboardManager = SERVICE_MANAGER.getClipboardManager();
if (clipboardManager == null) {
return false;
}
String currentClipboard = getClipboardText();
if (currentClipboard != null && currentClipboard.equals(text)) {
// The clipboard already contains the requested text.
// Since pasting text from the computer involves setting the device clipboard, it could be set twice on a copy-paste. This would cause
// the clipboard listeners to be notified twice, and that would flood the Android keyboard clipboard history. To workaround this
// problem, do not explicitly set the clipboard text if it already contains the expected content.
return false;
}
isSettingClipboard.set(true);
boolean ok = clipboardManager.setText(text);
isSettingClipboard.set(false);
return ok;
}
/**
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");
return false;
}
return SurfaceControl.setDisplayPowerMode(d, mode);
}
public static boolean powerOffScreen(int displayId) {
if (!isScreenOn()) {
return true;
}
return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC);
}
/**
* Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled).
*/
public static void rotateDevice() {
WindowManager wm = SERVICE_MANAGER.getWindowManager();
boolean accelerometerRotation = !wm.isRotationFrozen();
int currentRotation = wm.getRotation();
int newRotation = (currentRotation & 1) ^ 1; // 0->1, 1->0, 2->1, 3->0
String newRotationString = newRotation == 0 ? "portrait" : "landscape";
Ln.i("Device rotation requested: " + newRotationString);
wm.freezeRotation(newRotation);
// restore auto-rotate if necessary
if (accelerometerRotation) {
wm.thawRotation();
}
}
public static Settings getSettings() {
return SETTINGS;
}
}

View file

@ -1,42 +0,0 @@
package com.genymobile.scrcpy;
public final class DeviceMessage {
public static final int TYPE_CLIPBOARD = 0;
public static final int TYPE_ACK_CLIPBOARD = 1;
public static final long SEQUENCE_INVALID = ControlMessage.SEQUENCE_INVALID;
private int type;
private String text;
private long sequence;
private DeviceMessage() {
}
public static DeviceMessage createClipboard(String text) {
DeviceMessage event = new DeviceMessage();
event.type = TYPE_CLIPBOARD;
event.text = text;
return event;
}
public static DeviceMessage createAckClipboard(long sequence) {
DeviceMessage event = new DeviceMessage();
event.type = TYPE_ACK_CLIPBOARD;
event.sequence = sequence;
return event;
}
public int getType() {
return type;
}
public String getText() {
return text;
}
public long getSequence() {
return sequence;
}
}

View file

@ -1,52 +0,0 @@
package com.genymobile.scrcpy;
import java.io.IOException;
public final class DeviceMessageSender {
private final DesktopConnection connection;
private String clipboardText;
private long ack;
public DeviceMessageSender(DesktopConnection connection) {
this.connection = connection;
}
public synchronized void pushClipboardText(String text) {
clipboardText = text;
notify();
}
public synchronized void pushAckClipboard(long sequence) {
ack = sequence;
notify();
}
public void loop() throws IOException, InterruptedException {
while (true) {
String text;
long sequence;
synchronized (this) {
while (ack == DeviceMessage.SEQUENCE_INVALID && clipboardText == null) {
wait();
}
text = clipboardText;
clipboardText = null;
sequence = ack;
ack = DeviceMessage.SEQUENCE_INVALID;
}
if (sequence != DeviceMessage.SEQUENCE_INVALID) {
DeviceMessage event = DeviceMessage.createAckClipboard(sequence);
connection.sendDeviceMessage(event);
}
if (text != null) {
DeviceMessage event = DeviceMessage.createClipboard(text);
connection.sendDeviceMessage(event);
}
}
}
}

View file

@ -1,37 +0,0 @@
package com.genymobile.scrcpy;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.charset.StandardCharsets;
public class DeviceMessageWriter {
private static final int MESSAGE_MAX_SIZE = 1 << 18; // 256k
public static final int CLIPBOARD_TEXT_MAX_LENGTH = MESSAGE_MAX_SIZE - 5; // type: 1 byte; length: 4 bytes
private final byte[] rawBuffer = new byte[MESSAGE_MAX_SIZE];
private final ByteBuffer buffer = ByteBuffer.wrap(rawBuffer);
public void writeTo(DeviceMessage msg, OutputStream output) throws IOException {
buffer.clear();
buffer.put((byte) msg.getType());
switch (msg.getType()) {
case DeviceMessage.TYPE_CLIPBOARD:
String text = msg.getText();
byte[] raw = text.getBytes(StandardCharsets.UTF_8);
int len = StringUtils.getUtf8TruncationIndex(raw, CLIPBOARD_TEXT_MAX_LENGTH);
buffer.putInt(len);
buffer.put(raw, 0, len);
output.write(rawBuffer, 0, buffer.position());
break;
case DeviceMessage.TYPE_ACK_CLIPBOARD:
buffer.putLong(msg.getSequence());
output.write(rawBuffer, 0, buffer.position());
break;
default:
Ln.w("Unknown device message: " + msg.getType());
break;
}
}
}

View file

@ -1,40 +0,0 @@
package com.genymobile.scrcpy;
public final class DisplayInfo {
private final int displayId;
private final Size size;
private final int rotation;
private final int layerStack;
private final int flags;
public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001;
public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) {
this.displayId = displayId;
this.size = size;
this.rotation = rotation;
this.layerStack = layerStack;
this.flags = flags;
}
public int getDisplayId() {
return displayId;
}
public Size getSize() {
return size;
}
public int getRotation() {
return rotation;
}
public int getLayerStack() {
return layerStack;
}
public int getFlags() {
return flags;
}
}

View file

@ -1,40 +0,0 @@
package com.genymobile.scrcpy;
import android.system.ErrnoException;
import android.system.Os;
import android.system.OsConstants;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
public final class IO {
private IO() {
// not instantiable
}
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException {
// ByteBuffer position is not updated as expected by Os.write() on old Android versions, so
// count the remaining bytes manually.
// See <https://github.com/Genymobile/scrcpy/issues/291>.
int remaining = from.remaining();
while (remaining > 0) {
try {
int w = Os.write(fd, from);
if (BuildConfig.DEBUG && w < 0) {
// w should not be negative, since an exception is thrown on error
throw new AssertionError("Os.write() returned a negative value (" + w + ")");
}
remaining -= w;
} catch (ErrnoException e) {
if (e.errno != OsConstants.EINTR) {
throw new IOException(e);
}
}
}
}
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException {
writeFully(fd, ByteBuffer.wrap(buffer, offset, len));
}
}

View file

@ -1,21 +0,0 @@
package com.genymobile.scrcpy;
public class InvalidDisplayIdException extends RuntimeException {
private final int displayId;
private final int[] availableDisplayIds;
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
super("There is no display having id " + displayId);
this.displayId = displayId;
this.availableDisplayIds = availableDisplayIds;
}
public int getDisplayId() {
return displayId;
}
public int[] getAvailableDisplayIds() {
return availableDisplayIds;
}
}

View file

@ -1,23 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaCodecInfo;
public class InvalidEncoderException extends RuntimeException {
private final String name;
private final MediaCodecInfo[] availableEncoders;
public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) {
super("There is no encoder having name '" + name + '"');
this.name = name;
this.availableEncoders = availableEncoders;
}
public String getName() {
return name;
}
public MediaCodecInfo[] getAvailableEncoders() {
return availableEncoders;
}
}

View file

@ -1,174 +0,0 @@
package com.genymobile.scrcpy;
import java.util.HashMap;
import java.util.Map;
/**
* Decompose accented characters.
* <p>
* For example, {@link #decompose(char) decompose('é')} returns {@code "\u0301e"}.
* <p>
* This is useful for injecting key events to generate the expected character ({@link android.view.KeyCharacterMap#getEvents(char[])}
* KeyCharacterMap.getEvents()} returns {@code null} with input {@code "é"} but works with input {@code "\u0301e"}).
* <p>
* See <a href="https://source.android.com/devices/input/key-character-map-files#behaviors">diacritical dead key characters</a>.
*/
public final class KeyComposition {
private static final String KEY_DEAD_GRAVE = "\u0300";
private static final String KEY_DEAD_ACUTE = "\u0301";
private static final String KEY_DEAD_CIRCUMFLEX = "\u0302";
private static final String KEY_DEAD_TILDE = "\u0303";
private static final String KEY_DEAD_UMLAUT = "\u0308";
private static final Map<Character, String> COMPOSITION_MAP = createDecompositionMap();
private KeyComposition() {
// not instantiable
}
public static String decompose(char c) {
return COMPOSITION_MAP.get(c);
}
private static String grave(char c) {
return KEY_DEAD_GRAVE + c;
}
private static String acute(char c) {
return KEY_DEAD_ACUTE + c;
}
private static String circumflex(char c) {
return KEY_DEAD_CIRCUMFLEX + c;
}
private static String tilde(char c) {
return KEY_DEAD_TILDE + c;
}
private static String umlaut(char c) {
return KEY_DEAD_UMLAUT + c;
}
private static Map<Character, String> createDecompositionMap() {
Map<Character, String> map = new HashMap<>();
map.put('À', grave('A'));
map.put('È', grave('E'));
map.put('Ì', grave('I'));
map.put('Ò', grave('O'));
map.put('Ù', grave('U'));
map.put('à', grave('a'));
map.put('è', grave('e'));
map.put('ì', grave('i'));
map.put('ò', grave('o'));
map.put('ù', grave('u'));
map.put('Ǹ', grave('N'));
map.put('ǹ', grave('n'));
map.put('Ẁ', grave('W'));
map.put('ẁ', grave('w'));
map.put('Ỳ', grave('Y'));
map.put('ỳ', grave('y'));
map.put('Á', acute('A'));
map.put('É', acute('E'));
map.put('Í', acute('I'));
map.put('Ó', acute('O'));
map.put('Ú', acute('U'));
map.put('Ý', acute('Y'));
map.put('á', acute('a'));
map.put('é', acute('e'));
map.put('í', acute('i'));
map.put('ó', acute('o'));
map.put('ú', acute('u'));
map.put('ý', acute('y'));
map.put('Ć', acute('C'));
map.put('ć', acute('c'));
map.put('Ĺ', acute('L'));
map.put('ĺ', acute('l'));
map.put('Ń', acute('N'));
map.put('ń', acute('n'));
map.put('Ŕ', acute('R'));
map.put('ŕ', acute('r'));
map.put('Ś', acute('S'));
map.put('ś', acute('s'));
map.put('Ź', acute('Z'));
map.put('ź', acute('z'));
map.put('Ǵ', acute('G'));
map.put('ǵ', acute('g'));
map.put('Ḉ', acute('Ç'));
map.put('ḉ', acute('ç'));
map.put('Ḱ', acute('K'));
map.put('ḱ', acute('k'));
map.put('Ḿ', acute('M'));
map.put('ḿ', acute('m'));
map.put('Ṕ', acute('P'));
map.put('ṕ', acute('p'));
map.put('Ẃ', acute('W'));
map.put('ẃ', acute('w'));
map.put('Â', circumflex('A'));
map.put('Ê', circumflex('E'));
map.put('Î', circumflex('I'));
map.put('Ô', circumflex('O'));
map.put('Û', circumflex('U'));
map.put('â', circumflex('a'));
map.put('ê', circumflex('e'));
map.put('î', circumflex('i'));
map.put('ô', circumflex('o'));
map.put('û', circumflex('u'));
map.put('Ĉ', circumflex('C'));
map.put('ĉ', circumflex('c'));
map.put('Ĝ', circumflex('G'));
map.put('ĝ', circumflex('g'));
map.put('Ĥ', circumflex('H'));
map.put('ĥ', circumflex('h'));
map.put('Ĵ', circumflex('J'));
map.put('ĵ', circumflex('j'));
map.put('Ŝ', circumflex('S'));
map.put('ŝ', circumflex('s'));
map.put('Ŵ', circumflex('W'));
map.put('ŵ', circumflex('w'));
map.put('Ŷ', circumflex('Y'));
map.put('ŷ', circumflex('y'));
map.put('Ẑ', circumflex('Z'));
map.put('ẑ', circumflex('z'));
map.put('Ã', tilde('A'));
map.put('Ñ', tilde('N'));
map.put('Õ', tilde('O'));
map.put('ã', tilde('a'));
map.put('ñ', tilde('n'));
map.put('õ', tilde('o'));
map.put('Ĩ', tilde('I'));
map.put('ĩ', tilde('i'));
map.put('Ũ', tilde('U'));
map.put('ũ', tilde('u'));
map.put('Ẽ', tilde('E'));
map.put('ẽ', tilde('e'));
map.put('Ỹ', tilde('Y'));
map.put('ỹ', tilde('y'));
map.put('Ä', umlaut('A'));
map.put('Ë', umlaut('E'));
map.put('Ï', umlaut('I'));
map.put('Ö', umlaut('O'));
map.put('Ü', umlaut('U'));
map.put('ä', umlaut('a'));
map.put('ë', umlaut('e'));
map.put('ï', umlaut('i'));
map.put('ö', umlaut('o'));
map.put('ü', umlaut('u'));
map.put('ÿ', umlaut('y'));
map.put('Ÿ', umlaut('Y'));
map.put('Ḧ', umlaut('H'));
map.put('ḧ', umlaut('h'));
map.put('Ẅ', umlaut('W'));
map.put('ẅ', umlaut('w'));
map.put('Ẍ', umlaut('X'));
map.put('ẍ', umlaut('x'));
map.put('ẗ', umlaut('t'));
return map;
}
}

View file

@ -1,87 +0,0 @@
package com.genymobile.scrcpy;
import android.util.Log;
/**
* Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal
* directly).
*/
public final class Ln {
private static final String TAG = "scrcpy";
private static final String PREFIX = "[server] ";
enum Level {
VERBOSE, DEBUG, INFO, WARN, ERROR
}
private static Level threshold = Level.INFO;
private Ln() {
// not instantiable
}
/**
* Initialize the log level.
* <p>
* Must be called before starting any new thread.
*
* @param level the log level
*/
public static void initLogLevel(Level level) {
threshold = level;
}
public static boolean isEnabled(Level level) {
return level.ordinal() >= threshold.ordinal();
}
public static void v(String message) {
if (isEnabled(Level.VERBOSE)) {
Log.v(TAG, message);
System.out.println(PREFIX + "VERBOSE: " + message);
}
}
public static void d(String message) {
if (isEnabled(Level.DEBUG)) {
Log.d(TAG, message);
System.out.println(PREFIX + "DEBUG: " + message);
}
}
public static void i(String message) {
if (isEnabled(Level.INFO)) {
Log.i(TAG, message);
System.out.println(PREFIX + "INFO: " + message);
}
}
public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) {
Log.w(TAG, message, throwable);
System.out.println(PREFIX + "WARN: " + message);
if (throwable != null) {
throwable.printStackTrace();
}
}
}
public static void w(String message) {
w(message, null);
}
public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable);
System.out.println(PREFIX + "ERROR: " + message);
if (throwable != null) {
throwable.printStackTrace();
}
}
}
public static void e(String message) {
e(message, null);
}
}

View file

@ -1,152 +0,0 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.util.List;
public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG;
private int maxSize;
private int bitRate = 8000000;
private int maxFps;
private int lockVideoOrientation = -1;
private boolean tunnelForward;
private Rect crop;
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
private boolean control = true;
private int displayId;
private boolean showTouches;
private boolean stayAwake;
private List<CodecOption> codecOptions;
private String encoderName;
private boolean powerOffScreenOnClose;
private boolean clipboardAutosync = true;
public Ln.Level getLogLevel() {
return logLevel;
}
public void setLogLevel(Ln.Level logLevel) {
this.logLevel = logLevel;
}
public int getMaxSize() {
return maxSize;
}
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public int getBitRate() {
return bitRate;
}
public void setBitRate(int bitRate) {
this.bitRate = bitRate;
}
public int getMaxFps() {
return maxFps;
}
public void setMaxFps(int maxFps) {
this.maxFps = maxFps;
}
public int getLockVideoOrientation() {
return lockVideoOrientation;
}
public void setLockVideoOrientation(int lockVideoOrientation) {
this.lockVideoOrientation = lockVideoOrientation;
}
public boolean isTunnelForward() {
return tunnelForward;
}
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
public Rect getCrop() {
return crop;
}
public void setCrop(Rect crop) {
this.crop = crop;
}
public boolean getSendFrameMeta() {
return sendFrameMeta;
}
public void setSendFrameMeta(boolean sendFrameMeta) {
this.sendFrameMeta = sendFrameMeta;
}
public boolean getControl() {
return control;
}
public void setControl(boolean control) {
this.control = control;
}
public int getDisplayId() {
return displayId;
}
public void setDisplayId(int displayId) {
this.displayId = displayId;
}
public boolean getShowTouches() {
return showTouches;
}
public void setShowTouches(boolean showTouches) {
this.showTouches = showTouches;
}
public boolean getStayAwake() {
return stayAwake;
}
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
}
public List<CodecOption> getCodecOptions() {
return codecOptions;
}
public void setCodecOptions(List<CodecOption> codecOptions) {
this.codecOptions = codecOptions;
}
public String getEncoderName() {
return encoderName;
}
public void setEncoderName(String encoderName) {
this.encoderName = encoderName;
}
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
this.powerOffScreenOnClose = powerOffScreenOnClose;
}
public boolean getPowerOffScreenOnClose() {
return this.powerOffScreenOnClose;
}
public boolean getClipboardAutosync() {
return clipboardAutosync;
}
public void setClipboardAutosync(boolean clipboardAutosync) {
this.clipboardAutosync = clipboardAutosync;
}
}

View file

@ -1,43 +0,0 @@
package com.genymobile.scrcpy;
import java.util.Objects;
public class Point {
private final int x;
private final int y;
public Point(int x, int y) {
this.x = x;
this.y = y;
}
public int getX() {
return x;
}
public int getY() {
return y;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Point point = (Point) o;
return x == point.x && y == point.y;
}
@Override
public int hashCode() {
return Objects.hash(x, y);
}
@Override
public String toString() {
return "Point{" + "x=" + x + ", y=" + y + '}';
}
}

View file

@ -1,55 +0,0 @@
package com.genymobile.scrcpy;
public class Pointer {
/**
* Pointer id as received from the client.
*/
private final long id;
/**
* Local pointer id, using the lowest possible values to fill the {@link android.view.MotionEvent.PointerProperties PointerProperties}.
*/
private final int localId;
private Point point;
private float pressure;
private boolean up;
public Pointer(long id, int localId) {
this.id = id;
this.localId = localId;
}
public long getId() {
return id;
}
public int getLocalId() {
return localId;
}
public Point getPoint() {
return point;
}
public void setPoint(Point point) {
this.point = point;
}
public float getPressure() {
return pressure;
}
public void setPressure(float pressure) {
this.pressure = pressure;
}
public boolean isUp() {
return up;
}
public void setUp(boolean up) {
this.up = up;
}
}

View file

@ -1,103 +0,0 @@
package com.genymobile.scrcpy;
import android.view.MotionEvent;
import java.util.ArrayList;
import java.util.List;
public class PointersState {
public static final int MAX_POINTERS = 10;
private final List<Pointer> pointers = new ArrayList<>();
private int indexOf(long id) {
for (int i = 0; i < pointers.size(); ++i) {
Pointer pointer = pointers.get(i);
if (pointer.getId() == id) {
return i;
}
}
return -1;
}
private boolean isLocalIdAvailable(int localId) {
for (int i = 0; i < pointers.size(); ++i) {
Pointer pointer = pointers.get(i);
if (pointer.getLocalId() == localId) {
return false;
}
}
return true;
}
private int nextUnusedLocalId() {
for (int localId = 0; localId < MAX_POINTERS; ++localId) {
if (isLocalIdAvailable(localId)) {
return localId;
}
}
return -1;
}
public Pointer get(int index) {
return pointers.get(index);
}
public int getPointerIndex(long id) {
int index = indexOf(id);
if (index != -1) {
// already exists, return it
return index;
}
if (pointers.size() >= MAX_POINTERS) {
// it's full
return -1;
}
// id 0 is reserved for mouse events
int localId = nextUnusedLocalId();
if (localId == -1) {
throw new AssertionError("pointers.size() < maxFingers implies that a local id is available");
}
Pointer pointer = new Pointer(id, localId);
pointers.add(pointer);
// return the index of the pointer
return pointers.size() - 1;
}
/**
* Initialize the motion event parameters.
*
* @param props the pointer properties
* @param coords the pointer coordinates
* @return The number of items initialized (the number of pointers).
*/
public int update(MotionEvent.PointerProperties[] props, MotionEvent.PointerCoords[] coords) {
int count = pointers.size();
for (int i = 0; i < count; ++i) {
Pointer pointer = pointers.get(i);
// id 0 is reserved for mouse events
props[i].id = pointer.getLocalId();
Point point = pointer.getPoint();
coords[i].x = point.getX();
coords[i].y = point.getY();
coords[i].pressure = pointer.getPressure();
}
cleanUp();
return count;
}
/**
* Remove all pointers which are UP.
*/
private void cleanUp() {
for (int i = pointers.size() - 1; i >= 0; --i) {
Pointer pointer = pointers.get(i);
if (pointer.isUp()) {
pointers.remove(i);
}
}
}
}

View file

@ -1,61 +0,0 @@
package com.genymobile.scrcpy;
import java.util.Objects;
public class Position {
private Point point;
private Size screenSize;
public Position(Point point, Size screenSize) {
this.point = point;
this.screenSize = screenSize;
}
public Position(int x, int y, int screenWidth, int screenHeight) {
this(new Point(x, y), new Size(screenWidth, screenHeight));
}
public Point getPoint() {
return point;
}
public Size getScreenSize() {
return screenSize;
}
public Position rotate(int rotation) {
switch (rotation) {
case 1:
return new Position(new Point(screenSize.getHeight() - point.getY(), point.getX()), screenSize.rotate());
case 2:
return new Position(new Point(screenSize.getWidth() - point.getX(), screenSize.getHeight() - point.getY()), screenSize);
case 3:
return new Position(new Point(point.getY(), screenSize.getWidth() - point.getX()), screenSize.rotate());
default:
return this;
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Position position = (Position) o;
return Objects.equals(point, position.point) && Objects.equals(screenSize, position.screenSize);
}
@Override
public int hashCode() {
return Objects.hash(point, screenSize);
}
@Override
public String toString() {
return "Position{" + "point=" + point + ", screenSize=" + screenSize + '}';
}
}

View file

@ -1,255 +0,0 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Build;
import android.os.IBinder;
import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
private static final String KEY_MAX_FPS_TO_ENCODER = "max-fps-to-encoder";
private static final int NO_PTS = -1;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private String encoderName;
private List<CodecOption> codecOptions;
private int bitRate;
private int maxFps;
private boolean sendFrameMeta;
private long ptsOrigin;
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName) {
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate;
this.maxFps = maxFps;
this.codecOptions = codecOptions;
this.encoderName = encoderName;
}
@Override
public void onRotationChanged(int rotation) {
rotationChanged.set(true);
}
public boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
}
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper();
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// <https://github.com/Genymobile/scrcpy/issues/240>
// <https://github.com/Genymobile/scrcpy/issues/2656>
Workarounds.fillAppInfo();
}
internalStreamScreen(device, fd);
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
device.setRotationListener(this);
boolean alive;
try {
do {
MediaCodec codec = createCodec(encoderName);
IBinder display = createDisplay();
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// include the locked video orientation
Rect videoRect = screenInfo.getVideoSize().toRect();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setSize(format, videoRect.width(), videoRect.height());
configure(codec, format);
Surface surface = codec.createInputSurface();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
codec.start();
try {
alive = encode(codec, fd);
// do not call stop() on exception, it would trigger an IllegalStateException
codec.stop();
} finally {
destroyDisplay(display);
codec.release();
surface.release();
}
} while (alive);
} finally {
device.setRotationListener(null);
}
}
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try {
if (consumeRotationChange()) {
// must restart encoding with new size
break;
}
if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
IO.writeFully(fd, codecBuffer);
}
} finally {
if (outputBufferId >= 0) {
codec.releaseOutputBuffer(outputBufferId, false);
}
}
}
return !eof;
}
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long pts;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
pts = NO_PTS; // non-media data packet
} else {
if (ptsOrigin == 0) {
ptsOrigin = bufferInfo.presentationTimeUs;
}
pts = bufferInfo.presentationTimeUs - ptsOrigin;
}
headerBuffer.putLong(pts);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static MediaCodecInfo[] listEncoders() {
List<MediaCodecInfo> result = new ArrayList<>();
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
result.add(codecInfo);
}
}
return result.toArray(new MediaCodecInfo[result.size()]);
}
private static MediaCodec createCodec(String encoderName) throws IOException {
if (encoderName != null) {
Ln.d("Creating encoder by name: '" + encoderName + "'");
try {
return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) {
MediaCodecInfo[] encoders = listEncoders();
throw new InvalidEncoderException(encoderName, encoders);
}
}
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
Ln.d("Using encoder: '" + codec.getName() + "'");
return codec;
}
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
String key = codecOption.getKey();
Object value = codecOption.getValue();
if (value instanceof Integer) {
format.setInteger(key, (Integer) value);
} else if (value instanceof Long) {
format.setLong(key, (Long) value);
} else if (value instanceof Float) {
format.setFloat(key, (Float) value);
} else if (value instanceof String) {
format.setString(key, (String) value);
}
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface);
format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL);
// display the very first frame, and recover from bad quality when no new frames
format.setLong(MediaFormat.KEY_REPEAT_PREVIOUS_FRAME_AFTER, REPEAT_FRAME_DELAY_US); // µs
if (maxFps > 0) {
// The key existed privately before Android 10:
// <https://android.googlesource.com/platform/frameworks/base/+/625f0aad9f7a259b6881006ad8710adce57d1384%5E%21/>
// <https://github.com/Genymobile/scrcpy/issues/488#issuecomment-567321437>
format.setFloat(KEY_MAX_FPS_TO_ENCODER, maxFps);
}
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
setCodecOption(format, option);
}
}
return format;
}
private static IBinder createDisplay() {
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S"
.equals(Build.VERSION.CODENAME));
return SurfaceControl.createDisplay("scrcpy", secure);
}
private static void configure(MediaCodec codec, MediaFormat format) {
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
}
private static void setSize(MediaFormat format, int width, int height) {
format.setInteger(MediaFormat.KEY_WIDTH, width);
format.setInteger(MediaFormat.KEY_HEIGHT, height);
}
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
SurfaceControl.openTransaction();
try {
SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, layerStack);
} finally {
SurfaceControl.closeTransaction();
}
}
private static void destroyDisplay(IBinder display) {
SurfaceControl.destroyDisplay(display);
}
}

View file

@ -1,169 +0,0 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
public final class ScreenInfo {
/**
* Device (physical) size, possibly cropped
*/
private final Rect contentRect; // device size, possibly cropped
/**
* Video size, possibly smaller than the device size, already taking the device rotation and crop into account.
* <p>
* However, it does not include the locked video orientation.
*/
private final Size unlockedVideoSize;
/**
* Device rotation, related to the natural device orientation (0, 1, 2 or 3)
*/
private final int deviceRotation;
/**
* The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW)
*/
private final int lockedVideoOrientation;
public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) {
this.contentRect = contentRect;
this.unlockedVideoSize = unlockedVideoSize;
this.deviceRotation = deviceRotation;
this.lockedVideoOrientation = lockedVideoOrientation;
}
public Rect getContentRect() {
return contentRect;
}
/**
* Return the video size as if locked video orientation was not set.
*
* @return the unlocked video size
*/
public Size getUnlockedVideoSize() {
return unlockedVideoSize;
}
/**
* Return the actual video size if locked video orientation is set.
*
* @return the actual video size
*/
public Size getVideoSize() {
if (getVideoRotation() % 2 == 0) {
return unlockedVideoSize;
}
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(DisplayInfo displayInfo, Rect crop, int maxSize, int lockedVideoOrientation) {
int rotation = displayInfo.getRotation();
if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) {
// The user requested to lock the video orientation to the current orientation
lockedVideoOrientation = rotation;
}
Size deviceSize = displayInfo.getSize();
Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight());
if (crop != null) {
if (rotation % 2 != 0) { // 180s preserve dimensions
// the crop (provided by the user) is expressed in the natural orientation
crop = flipRect(crop);
}
if (!contentRect.intersect(crop)) {
// intersect() changes contentRect so that it is intersected with crop
Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")");
contentRect = new Rect(); // empty
}
}
Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize);
return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation);
}
private static String formatCrop(Rect rect) {
return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top;
}
private static Size computeVideoSize(int w, int h, int maxSize) {
// Compute the video size and the padding of the content inside this video.
// Principle:
// - scale down the great side of the screen to maxSize (if necessary);
// - scale down the other side so that the aspect ratio is preserved;
// - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8)
w &= ~7; // in case it's not a multiple of 8
h &= ~7;
if (maxSize > 0) {
if (BuildConfig.DEBUG && maxSize % 8 != 0) {
throw new AssertionError("Max size must be a multiple of 8");
}
boolean portrait = h > w;
int major = portrait ? h : w;
int minor = portrait ? w : h;
if (major > maxSize) {
int minorExact = minor * maxSize / major;
// +4 to round the value to the nearest multiple of 8
minor = (minorExact + 4) & ~7;
major = maxSize;
}
w = portrait ? minor : major;
h = portrait ? major : minor;
}
return new Size(w, h);
}
private static Rect flipRect(Rect crop) {
return new Rect(crop.top, crop.left, crop.bottom, crop.right);
}
/**
* Return the rotation to apply to the device rotation to get the requested locked video orientation
*
* @return the rotation offset
*/
public int getVideoRotation() {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (deviceRotation + 4 - lockedVideoOrientation) % 4;
}
/**
* Return the rotation to apply to the requested locked video orientation to get the device rotation
*
* @return the (reverse) rotation offset
*/
public int getReverseVideoRotation() {
if (lockedVideoOrientation == -1) {
// no offset
return 0;
}
return (lockedVideoOrientation + 4 - deviceRotation) % 4;
}
}

View file

@ -1,311 +0,0 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.os.BatteryManager;
import android.os.Build;
import java.io.IOException;
import java.util.List;
import java.util.Locale;
public final class Server {
private Server() {
// not instantiable
}
private static void initAndCleanUp(Options options) {
boolean mustDisableShowTouchesOnCleanUp = false;
int restoreStayOn = -1;
if (options.getShowTouches() || options.getStayAwake()) {
Settings settings = Device.getSettings();
if (options.getShowTouches()) {
try {
String oldValue = settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1");
// If "show touches" was disabled, it must be disabled back on clean up
mustDisableShowTouchesOnCleanUp = !"1".equals(oldValue);
} catch (SettingsException e) {
Ln.e("Could not change \"show_touches\"", e);
}
}
if (options.getStayAwake()) {
int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS;
try {
String oldValue = settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn));
try {
restoreStayOn = Integer.parseInt(oldValue);
if (restoreStayOn == stayOn) {
// No need to restore
restoreStayOn = -1;
}
} catch (NumberFormatException e) {
restoreStayOn = 0;
}
} catch (SettingsException e) {
Ln.e("Could not change \"stay_on_while_plugged_in\"", e);
}
}
}
try {
CleanUp.configure(options.getDisplayId(), restoreStayOn, mustDisableShowTouchesOnCleanUp, true, options.getPowerOffScreenOnClose());
} catch (IOException e) {
Ln.e("Could not configure cleanup", e);
}
}
private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
List<CodecOption> codecOptions = options.getCodecOptions();
Thread initThread = startInitThread(options);
boolean tunnelForward = options.isTunnelForward();
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
options.getEncoderName());
Thread controllerThread = null;
Thread deviceMessageSenderThread = null;
if (options.getControl()) {
final Controller controller = new Controller(device, connection, options.getClipboardAutosync());
// asynchronous
controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(new Device.ClipboardListener() {
@Override
public void onClipboardTextChanged(String text) {
controller.getSender().pushClipboardText(text);
}
});
}
try {
// synchronous
screenEncoder.streamScreen(device, connection.getVideoFd());
} catch (IOException e) {
// this is expected on close
Ln.d("Screen streaming stopped");
} finally {
initThread.interrupt();
if (controllerThread != null) {
controllerThread.interrupt();
}
if (deviceMessageSenderThread != null) {
deviceMessageSenderThread.interrupt();
}
}
}
}
private static Thread startInitThread(final Options options) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
initAndCleanUp(options);
}
});
thread.start();
return thread;
}
private static Thread startController(final Controller controller) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
controller.control();
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
}
}
});
thread.start();
return thread;
}
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
try {
sender.loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
}
}
});
thread.start();
return thread;
}
private static Options createOptions(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
}
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
Options options = new Options();
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "log_level":
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
break;
case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize);
break;
case "bit_rate":
int bitRate = Integer.parseInt(value);
options.setBitRate(bitRate);
break;
case "max_fps":
int maxFps = Integer.parseInt(value);
options.setMaxFps(maxFps);
break;
case "lock_video_orientation":
int lockVideoOrientation = Integer.parseInt(value);
options.setLockVideoOrientation(lockVideoOrientation);
break;
case "tunnel_forward":
boolean tunnelForward = Boolean.parseBoolean(value);
options.setTunnelForward(tunnelForward);
break;
case "crop":
Rect crop = parseCrop(value);
options.setCrop(crop);
break;
case "send_frame_meta":
boolean sendFrameMeta = Boolean.parseBoolean(value);
options.setSendFrameMeta(sendFrameMeta);
break;
case "control":
boolean control = Boolean.parseBoolean(value);
options.setControl(control);
break;
case "display_id":
int displayId = Integer.parseInt(value);
options.setDisplayId(displayId);
break;
case "show_touches":
boolean showTouches = Boolean.parseBoolean(value);
options.setShowTouches(showTouches);
break;
case "stay_awake":
boolean stayAwake = Boolean.parseBoolean(value);
options.setStayAwake(stayAwake);
break;
case "codec_options":
List<CodecOption> codecOptions = CodecOption.parse(value);
options.setCodecOptions(codecOptions);
break;
case "encoder_name":
if (!value.isEmpty()) {
options.setEncoderName(value);
}
break;
case "power_off_on_close":
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
break;
case "clipboard_autosync":
boolean clipboardAutosync = Boolean.parseBoolean(value);
options.setClipboardAutosync(clipboardAutosync);
break;
default:
Ln.w("Unknown server option: " + key);
break;
}
}
return options;
}
private static Rect parseCrop(String crop) {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
}
private static void suggestFix(Throwable e) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (e instanceof MediaCodec.CodecException) {
MediaCodec.CodecException mce = (MediaCodec.CodecException) e;
if (mce.getErrorCode() == 0xfffffc0e) {
Ln.e("The hardware encoder is not able to encode at the given definition.");
Ln.e("Try with a lower definition:");
Ln.e(" scrcpy -m 1024");
}
}
}
if (e instanceof InvalidDisplayIdException) {
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
int[] displayIds = idie.getAvailableDisplayIds();
if (displayIds != null && displayIds.length > 0) {
Ln.e("Try to use one of the available display ids:");
for (int id : displayIds) {
Ln.e(" scrcpy --display " + id);
}
}
} else if (e instanceof InvalidEncoderException) {
InvalidEncoderException iee = (InvalidEncoderException) e;
MediaCodecInfo[] encoders = iee.getAvailableEncoders();
if (encoders != null && encoders.length > 0) {
Ln.e("Try to use one of the available encoders:");
for (MediaCodecInfo encoder : encoders) {
Ln.e(" scrcpy --encoder '" + encoder.getName() + "'");
}
}
}
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
@Override
public void uncaughtException(Thread t, Throwable e) {
Ln.e("Exception on thread " + t, e);
suggestFix(e);
}
});
Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());
scrcpy(options);
}
}

View file

@ -1,84 +0,0 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.os.Build;
import java.io.IOException;
public class Settings {
public static final String TABLE_SYSTEM = ContentProvider.TABLE_SYSTEM;
public static final String TABLE_SECURE = ContentProvider.TABLE_SECURE;
public static final String TABLE_GLOBAL = ContentProvider.TABLE_GLOBAL;
private final ServiceManager serviceManager;
public Settings(ServiceManager serviceManager) {
this.serviceManager = serviceManager;
}
private static void execSettingsPut(String table, String key, String value) throws SettingsException {
try {
Command.exec("settings", "put", table, key, value);
} catch (IOException | InterruptedException e) {
throw new SettingsException("put", table, key, value, e);
}
}
private static String execSettingsGet(String table, String key) throws SettingsException {
try {
return Command.execReadLine("settings", "get", table, key);
} catch (IOException | InterruptedException e) {
throw new SettingsException("get", table, key, null, e);
}
}
public String getValue(String table, String key) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
return provider.getValue(table, key);
} catch (SettingsException e) {
Ln.w("Could not get settings value via ContentProvider, fallback to settings process", e);
}
}
return execSettingsGet(table, key);
}
public void putValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
provider.putValue(table, key, value);
} catch (SettingsException e) {
Ln.w("Could not put settings value via ContentProvider, fallback to settings process", e);
}
}
execSettingsPut(table, key, value);
}
public String getAndPutValue(String table, String key, String value) throws SettingsException {
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
// on Android >= 12, it always fails: <https://github.com/Genymobile/scrcpy/issues/2788>
try (ContentProvider provider = serviceManager.getActivityManager().createSettingsProvider()) {
String oldValue = provider.getValue(table, key);
if (!value.equals(oldValue)) {
provider.putValue(table, key, value);
}
return oldValue;
} catch (SettingsException e) {
Ln.w("Could not get and put settings value via ContentProvider, fallback to settings process", e);
}
}
String oldValue = getValue(table, key);
if (!value.equals(oldValue)) {
putValue(table, key, value);
}
return oldValue;
}
}

View file

@ -1,11 +0,0 @@
package com.genymobile.scrcpy;
public class SettingsException extends Exception {
private static String createMessage(String method, String table, String key, String value) {
return "Could not access settings: " + method + " " + table + " " + key + (value != null ? " " + value : "");
}
public SettingsException(String method, String table, String key, String value, Throwable cause) {
super(createMessage(method, table, key, value), cause);
}
}

View file

@ -1,53 +0,0 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import java.util.Objects;
public final class Size {
private final int width;
private final int height;
public Size(int width, int height) {
this.width = width;
this.height = height;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public Size rotate() {
return new Size(height, width);
}
public Rect toRect() {
return new Rect(0, 0, width, height);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Size size = (Size) o;
return width == size.width && height == size.height;
}
@Override
public int hashCode() {
return Objects.hash(width, height);
}
@Override
public String toString() {
return "Size{" + "width=" + width + ", height=" + height + '}';
}
}

View file

@ -1,22 +0,0 @@
package com.genymobile.scrcpy;
public final class StringUtils {
private StringUtils() {
// not instantiable
}
public static int getUtf8TruncationIndex(byte[] utf8, int maxLength) {
int len = utf8.length;
if (len <= maxLength) {
return len;
}
len = maxLength;
// see UTF-8 encoding <https://en.wikipedia.org/wiki/UTF-8#Description>
while ((utf8[len] & 0x80) != 0 && (utf8[len] & 0xc0) != 0xc0) {
// the next byte is not the start of a new UTF-8 codepoint
// so if we would cut there, the character would be truncated
len--;
}
return len;
}
}

View file

@ -1,80 +0,0 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.app.Application;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Looper;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public final class Workarounds {
private Workarounds() {
// not instantiable
}
@SuppressWarnings("deprecation")
public static void prepareMainLooper() {
// Some devices internally create a Handler when creating an input Surface, causing an exception:
// "Can't create handler inside thread that has not called Looper.prepare()"
// <https://github.com/Genymobile/scrcpy/issues/240>
//
// Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException:
// "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue'
// on a null object reference"
// <https://github.com/Genymobile/scrcpy/issues/921>
Looper.prepareMainLooper();
}
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public static void fillAppInfo() {
try {
// ActivityThread activityThread = new ActivityThread();
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
Object activityThread = activityThreadConstructor.newInstance();
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, activityThread);
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
appBindDataConstructor.setAccessible(true);
Object appBindData = appBindDataConstructor.newInstance();
ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.packageName = "com.genymobile.scrcpy";
// appBindData.appInfo = applicationInfo;
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
appInfoField.setAccessible(true);
appInfoField.set(appBindData, applicationInfo);
// activityThread.mBoundApplication = appBindData;
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData);
// Context ctx = activityThread.getSystemContext();
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
Application app = Instrumentation.newApplication(Application.class, ctx);
// activityThread.mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
mInitialApplicationField.set(activityThread, app);
} catch (Throwable throwable) {
// this is a workaround, so failing is not an error
Ln.d("Could not fill app info: " + throwable.getMessage());
}
}
}

View file

@ -1,87 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.Binder;
import android.os.IBinder;
import android.os.IInterface;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ActivityManager {
private final IInterface manager;
private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodNewVersion = true;
private Method removeContentProviderExternalMethod;
public ActivityManager(IInterface manager) {
this.manager = manager;
}
private Method getGetContentProviderExternalMethod() throws NoSuchMethodException {
if (getContentProviderExternalMethod == null) {
try {
getContentProviderExternalMethod = manager.getClass()
.getMethod("getContentProviderExternal", String.class, int.class, IBinder.class, String.class);
} catch (NoSuchMethodException e) {
// old version
getContentProviderExternalMethod = manager.getClass().getMethod("getContentProviderExternal", String.class, int.class, IBinder.class);
getContentProviderExternalMethodNewVersion = false;
}
}
return getContentProviderExternalMethod;
}
private Method getRemoveContentProviderExternalMethod() throws NoSuchMethodException {
if (removeContentProviderExternalMethod == null) {
removeContentProviderExternalMethod = manager.getClass().getMethod("removeContentProviderExternal", String.class, IBinder.class);
}
return removeContentProviderExternalMethod;
}
private ContentProvider getContentProviderExternal(String name, IBinder token) {
try {
Method method = getGetContentProviderExternalMethod();
Object[] args;
if (getContentProviderExternalMethodNewVersion) {
// new version
args = new Object[]{name, ServiceManager.USER_ID, token, null};
} else {
// old version
args = new Object[]{name, ServiceManager.USER_ID, token};
}
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
Object providerHolder = method.invoke(manager, args);
if (providerHolder == null) {
return null;
}
// IContentProvider provider = providerHolder.provider;
Field providerField = providerHolder.getClass().getDeclaredField("provider");
providerField.setAccessible(true);
Object provider = providerField.get(providerHolder);
if (provider == null) {
return null;
}
return new ContentProvider(this, provider, name, token);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | NoSuchFieldException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
void removeContentProviderExternal(String name, IBinder token) {
try {
Method method = getRemoveContentProviderExternalMethod();
method.invoke(manager, name, token);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public ContentProvider createSettingsProvider() {
return getContentProviderExternal("settings", new Binder());
}
}

View file

@ -1,119 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.content.ClipData;
import android.content.IOnPrimaryClipChangedListener;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ClipboardManager {
private final IInterface manager;
private Method getPrimaryClipMethod;
private Method setPrimaryClipMethod;
private Method addPrimaryClipChangedListener;
public ClipboardManager(IInterface manager) {
this.manager = manager;
}
private Method getGetPrimaryClipMethod() throws NoSuchMethodException {
if (getPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
} else {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
}
}
return getPrimaryClipMethod;
}
private Method getSetPrimaryClipMethod() throws NoSuchMethodException {
if (setPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} else {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
}
}
return setPrimaryClipMethod;
}
private static ClipData getPrimaryClip(Method method, IInterface manager) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
}
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
private static void setPrimaryClip(Method method, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
} else {
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
}
public CharSequence getText() {
try {
Method method = getGetPrimaryClipMethod();
ClipData clipData = getPrimaryClip(method, manager);
if (clipData == null || clipData.getItemCount() == 0) {
return null;
}
return clipData.getItemAt(0).getText();
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
public boolean setText(CharSequence text) {
try {
Method method = getSetPrimaryClipMethod();
ClipData clipData = ClipData.newPlainText(null, text);
setPrimaryClip(method, manager, clipData);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
private static void addPrimaryClipChangedListener(Method method, IInterface manager, IOnPrimaryClipChangedListener listener)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
} else {
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
}
private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException {
if (addPrimaryClipChangedListener == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class);
} else {
addPrimaryClipChangedListener = manager.getClass()
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
}
}
return addPrimaryClipChangedListener;
}
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
try {
Method method = getAddPrimaryClipChangedListener();
addPrimaryClipChangedListener(method, manager, listener);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
}

View file

@ -1,174 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.SettingsException;
import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.IBinder;
import java.io.Closeable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class ContentProvider implements Closeable {
public static final String TABLE_SYSTEM = "system";
public static final String TABLE_SECURE = "secure";
public static final String TABLE_GLOBAL = "global";
// See android/providerHolder/Settings.java
private static final String CALL_METHOD_GET_SYSTEM = "GET_system";
private static final String CALL_METHOD_GET_SECURE = "GET_secure";
private static final String CALL_METHOD_GET_GLOBAL = "GET_global";
private static final String CALL_METHOD_PUT_SYSTEM = "PUT_system";
private static final String CALL_METHOD_PUT_SECURE = "PUT_secure";
private static final String CALL_METHOD_PUT_GLOBAL = "PUT_global";
private static final String CALL_METHOD_USER_KEY = "_user";
private static final String NAME_VALUE_TABLE_VALUE = "value";
private final ActivityManager manager;
// android.content.IContentProvider
private final Object provider;
private final String name;
private final IBinder token;
private Method callMethod;
private int callMethodVersion;
private Object attributionSource;
ContentProvider(ActivityManager manager, Object provider, String name, IBinder token) {
this.manager = manager;
this.provider = provider;
this.name = name;
this.token = token;
}
@SuppressLint("PrivateApi")
private Method getCallMethod() throws NoSuchMethodException {
if (callMethod == null) {
try {
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 0;
} catch (NoSuchMethodException | ClassNotFoundException e0) {
// old versions
try {
callMethod = provider.getClass()
.getMethod("call", String.class, String.class, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 1;
} catch (NoSuchMethodException e1) {
try {
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 2;
} catch (NoSuchMethodException e2) {
callMethod = provider.getClass().getMethod("call", String.class, String.class, String.class, Bundle.class);
callMethodVersion = 3;
}
}
}
}
return callMethod;
}
@SuppressLint("PrivateApi")
private Object getAttributionSource()
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (attributionSource == null) {
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
}
return attributionSource;
}
private Bundle call(String callMethod, String arg, Bundle extras)
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
try {
Method method = getCallMethod();
Object[] args;
switch (callMethodVersion) {
case 0:
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
break;
case 1:
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
break;
case 2:
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
break;
default:
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
break;
}
return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
Ln.e("Could not invoke method", e);
throw e;
}
}
public void close() {
manager.removeContentProviderExternal(name, token);
}
private static String getGetMethod(String table) {
switch (table) {
case TABLE_SECURE:
return CALL_METHOD_GET_SECURE;
case TABLE_SYSTEM:
return CALL_METHOD_GET_SYSTEM;
case TABLE_GLOBAL:
return CALL_METHOD_GET_GLOBAL;
default:
throw new IllegalArgumentException("Invalid table: " + table);
}
}
private static String getPutMethod(String table) {
switch (table) {
case TABLE_SECURE:
return CALL_METHOD_PUT_SECURE;
case TABLE_SYSTEM:
return CALL_METHOD_PUT_SYSTEM;
case TABLE_GLOBAL:
return CALL_METHOD_PUT_GLOBAL;
default:
throw new IllegalArgumentException("Invalid table: " + table);
}
}
public String getValue(String table, String key) throws SettingsException {
String method = getGetMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
try {
Bundle bundle = call(method, key, arg);
if (bundle == null) {
return null;
}
return bundle.getString("value");
} catch (Exception e) {
throw new SettingsException(table, "get", key, null, e);
}
}
public void putValue(String table, String key, String value) throws SettingsException {
String method = getPutMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
arg.putString(NAME_VALUE_TABLE_VALUE, value);
try {
call(method, key, arg);
} catch (Exception e) {
throw new SettingsException(table, "put", key, value, e);
}
}
}

View file

@ -1,41 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Size;
import android.os.IInterface;
public final class DisplayManager {
private final IInterface manager;
public DisplayManager(IInterface manager) {
this.manager = manager;
}
public DisplayInfo getDisplayInfo(int displayId) {
try {
Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId);
if (displayInfo == null) {
return null;
}
Class<?> cls = displayInfo.getClass();
// width and height already take the rotation into account
int width = cls.getDeclaredField("logicalWidth").getInt(displayInfo);
int height = cls.getDeclaredField("logicalHeight").getInt(displayInfo);
int rotation = cls.getDeclaredField("rotation").getInt(displayInfo);
int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo);
int flags = cls.getDeclaredField("flags").getInt(displayInfo);
return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public int[] getDisplayIds() {
try {
return (int[]) manager.getClass().getMethod("getDisplayIds").invoke(manager);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}

View file

@ -1,60 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import android.view.InputEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public final class InputManager {
public static final int INJECT_INPUT_EVENT_MODE_ASYNC = 0;
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
private final IInterface manager;
private Method injectInputEventMethod;
private static Method setDisplayIdMethod;
public InputManager(IInterface manager) {
this.manager = manager;
}
private Method getInjectInputEventMethod() throws NoSuchMethodException {
if (injectInputEventMethod == null) {
injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class);
}
return injectInputEventMethod;
}
public boolean injectInputEvent(InputEvent inputEvent, int mode) {
try {
Method method = getInjectInputEventMethod();
return (boolean) method.invoke(manager, inputEvent, mode);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
private static Method getSetDisplayIdMethod() throws NoSuchMethodException {
if (setDisplayIdMethod == null) {
setDisplayIdMethod = InputEvent.class.getMethod("setDisplayId", int.class);
}
return setDisplayIdMethod;
}
public static boolean setDisplayId(InputEvent inputEvent, int displayId) {
try {
Method method = getSetDisplayIdMethod();
method.invoke(inputEvent, displayId);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Cannot associate a display id to the input event", e);
return false;
}
}
}

View file

@ -1,38 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.os.Build;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public final class PowerManager {
private final IInterface manager;
private Method isScreenOnMethod;
public PowerManager(IInterface manager) {
this.manager = manager;
}
private Method getIsScreenOnMethod() throws NoSuchMethodException {
if (isScreenOnMethod == null) {
@SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future
String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn";
isScreenOnMethod = manager.getClass().getMethod(methodName);
}
return isScreenOnMethod;
}
public boolean isScreenOn() {
try {
Method method = getIsScreenOnMethod();
return (boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
}

View file

@ -1,108 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import android.annotation.SuppressLint;
import android.os.IBinder;
import android.os.IInterface;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class ServiceManager {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int USER_ID = 0;
private final Method getServiceMethod;
private WindowManager windowManager;
private DisplayManager displayManager;
private InputManager inputManager;
private PowerManager powerManager;
private StatusBarManager statusBarManager;
private ClipboardManager clipboardManager;
private ActivityManager activityManager;
public ServiceManager() {
try {
getServiceMethod = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class);
} catch (Exception e) {
throw new AssertionError(e);
}
}
private IInterface getService(String service, String type) {
try {
IBinder binder = (IBinder) getServiceMethod.invoke(null, service);
Method asInterfaceMethod = Class.forName(type + "$Stub").getMethod("asInterface", IBinder.class);
return (IInterface) asInterfaceMethod.invoke(null, binder);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public WindowManager getWindowManager() {
if (windowManager == null) {
windowManager = new WindowManager(getService("window", "android.view.IWindowManager"));
}
return windowManager;
}
public DisplayManager getDisplayManager() {
if (displayManager == null) {
displayManager = new DisplayManager(getService("display", "android.hardware.display.IDisplayManager"));
}
return displayManager;
}
public InputManager getInputManager() {
if (inputManager == null) {
inputManager = new InputManager(getService("input", "android.hardware.input.IInputManager"));
}
return inputManager;
}
public PowerManager getPowerManager() {
if (powerManager == null) {
powerManager = new PowerManager(getService("power", "android.os.IPowerManager"));
}
return powerManager;
}
public StatusBarManager getStatusBarManager() {
if (statusBarManager == null) {
statusBarManager = new StatusBarManager(getService("statusbar", "com.android.internal.statusbar.IStatusBarService"));
}
return statusBarManager;
}
public ClipboardManager getClipboardManager() {
if (clipboardManager == null) {
IInterface clipboard = getService("clipboard", "android.content.IClipboard");
if (clipboard == null) {
// Some devices have no clipboard manager
// <https://github.com/Genymobile/scrcpy/issues/1440>
// <https://github.com/Genymobile/scrcpy/issues/1556>
return null;
}
clipboardManager = new ClipboardManager(clipboard);
}
return clipboardManager;
}
public ActivityManager getActivityManager() {
if (activityManager == null) {
try {
// On old Android versions, the ActivityManager is not exposed via AIDL,
// so use ActivityManagerNative.getDefault()
Class<?> cls = Class.forName("android.app.ActivityManagerNative");
Method getDefaultMethod = cls.getDeclaredMethod("getDefault");
IInterface am = (IInterface) getDefaultMethod.invoke(null);
activityManager = new ActivityManager(am);
} catch (Exception e) {
throw new AssertionError(e);
}
}
return activityManager;
}
}

View file

@ -1,93 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class StatusBarManager {
private final IInterface manager;
private Method expandNotificationsPanelMethod;
private boolean expandNotificationPanelMethodCustomVersion;
private Method expandSettingsPanelMethod;
private boolean expandSettingsPanelMethodNewVersion = true;
private Method collapsePanelsMethod;
public StatusBarManager(IInterface manager) {
this.manager = manager;
}
private Method getExpandNotificationsPanelMethod() throws NoSuchMethodException {
if (expandNotificationsPanelMethod == null) {
try {
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel");
} catch (NoSuchMethodException e) {
// Custom version for custom vendor ROM: <https://github.com/Genymobile/scrcpy/issues/2551>
expandNotificationsPanelMethod = manager.getClass().getMethod("expandNotificationsPanel", int.class);
expandNotificationPanelMethodCustomVersion = true;
}
}
return expandNotificationsPanelMethod;
}
private Method getExpandSettingsPanel() throws NoSuchMethodException {
if (expandSettingsPanelMethod == null) {
try {
// Since Android 7: https://android.googlesource.com/platform/frameworks/base.git/+/a9927325eda025504d59bb6594fee8e240d95b01%5E%21/
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel", String.class);
} catch (NoSuchMethodException e) {
// old version
expandSettingsPanelMethod = manager.getClass().getMethod("expandSettingsPanel");
expandSettingsPanelMethodNewVersion = false;
}
}
return expandSettingsPanelMethod;
}
private Method getCollapsePanelsMethod() throws NoSuchMethodException {
if (collapsePanelsMethod == null) {
collapsePanelsMethod = manager.getClass().getMethod("collapsePanels");
}
return collapsePanelsMethod;
}
public void expandNotificationsPanel() {
try {
Method method = getExpandNotificationsPanelMethod();
if (expandNotificationPanelMethodCustomVersion) {
method.invoke(manager, 0);
} else {
method.invoke(manager);
}
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public void expandSettingsPanel() {
try {
Method method = getExpandSettingsPanel();
if (expandSettingsPanelMethodNewVersion) {
// new version
method.invoke(manager, (Object) null);
} else {
// old version
method.invoke(manager);
}
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public void collapsePanels() {
try {
Method method = getCollapsePanelsMethod();
method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
}

View file

@ -1,142 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.view.Surface;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi")
public final class SurfaceControl {
private static final Class<?> CLASS;
// see <https://android.googlesource.com/platform/frameworks/base.git/+/pie-release-2/core/java/android/view/SurfaceControl.java#305>
public static final int POWER_MODE_OFF = 0;
public static final int POWER_MODE_NORMAL = 2;
static {
try {
CLASS = Class.forName("android.view.SurfaceControl");
} catch (ClassNotFoundException e) {
throw new AssertionError(e);
}
}
private static Method getBuiltInDisplayMethod;
private static Method setDisplayPowerModeMethod;
private SurfaceControl() {
// only static methods
}
public static void openTransaction() {
try {
CLASS.getMethod("openTransaction").invoke(null);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void closeTransaction() {
try {
CLASS.getMethod("closeTransaction").invoke(null);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void setDisplayProjection(IBinder displayToken, int orientation, Rect layerStackRect, Rect displayRect) {
try {
CLASS.getMethod("setDisplayProjection", IBinder.class, int.class, Rect.class, Rect.class)
.invoke(null, displayToken, orientation, layerStackRect, displayRect);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void setDisplayLayerStack(IBinder displayToken, int layerStack) {
try {
CLASS.getMethod("setDisplayLayerStack", IBinder.class, int.class).invoke(null, displayToken, layerStack);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static void setDisplaySurface(IBinder displayToken, Surface surface) {
try {
CLASS.getMethod("setDisplaySurface", IBinder.class, Surface.class).invoke(null, displayToken, surface);
} catch (Exception e) {
throw new AssertionError(e);
}
}
public static IBinder createDisplay(String name, boolean secure) {
try {
return (IBinder) CLASS.getMethod("createDisplay", String.class, boolean.class).invoke(null, name, secure);
} catch (Exception e) {
throw new AssertionError(e);
}
}
private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException {
if (getBuiltInDisplayMethod == null) {
// the method signature has changed in Android Q
// <https://github.com/Genymobile/scrcpy/issues/586>
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class);
} else {
getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken");
}
}
return getBuiltInDisplayMethod;
}
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
// call getBuiltInDisplay(0)
return (IBinder) method.invoke(null, 0);
}
// call getInternalDisplayToken()
return (IBinder) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
if (setDisplayPowerModeMethod == null) {
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);
}
return setDisplayPowerModeMethod;
}
public static boolean setDisplayPowerMode(IBinder displayToken, int mode) {
try {
Method method = getSetDisplayPowerModeMethod();
method.invoke(null, displayToken, mode);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
public static void destroyDisplay(IBinder displayToken) {
try {
CLASS.getMethod("destroyDisplay", IBinder.class).invoke(null, displayToken);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}

View file

@ -1,111 +0,0 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import android.view.IRotationWatcher;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public final class WindowManager {
private final IInterface manager;
private Method getRotationMethod;
private Method freezeRotationMethod;
private Method isRotationFrozenMethod;
private Method thawRotationMethod;
public WindowManager(IInterface manager) {
this.manager = manager;
}
private Method getGetRotationMethod() throws NoSuchMethodException {
if (getRotationMethod == null) {
Class<?> cls = manager.getClass();
try {
// method changed since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/8ee7285128c3843401d4c4d0412cd66e86ba49e3%5E%21/#F2
getRotationMethod = cls.getMethod("getDefaultDisplayRotation");
} catch (NoSuchMethodException e) {
// old version
getRotationMethod = cls.getMethod("getRotation");
}
}
return getRotationMethod;
}
private Method getFreezeRotationMethod() throws NoSuchMethodException {
if (freezeRotationMethod == null) {
freezeRotationMethod = manager.getClass().getMethod("freezeRotation", int.class);
}
return freezeRotationMethod;
}
private Method getIsRotationFrozenMethod() throws NoSuchMethodException {
if (isRotationFrozenMethod == null) {
isRotationFrozenMethod = manager.getClass().getMethod("isRotationFrozen");
}
return isRotationFrozenMethod;
}
private Method getThawRotationMethod() throws NoSuchMethodException {
if (thawRotationMethod == null) {
thawRotationMethod = manager.getClass().getMethod("thawRotation");
}
return thawRotationMethod;
}
public int getRotation() {
try {
Method method = getGetRotationMethod();
return (int) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return 0;
}
}
public void freezeRotation(int rotation) {
try {
Method method = getFreezeRotationMethod();
method.invoke(manager, rotation);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public boolean isRotationFrozen() {
try {
Method method = getIsRotationFrozenMethod();
return (boolean) method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return false;
}
}
public void thawRotation() {
try {
Method method = getThawRotationMethod();
method.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
}
}
public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) {
try {
Class<?> cls = manager.getClass();
try {
// display parameter added since this commit:
// https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1
cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId);
} catch (NoSuchMethodException e) {
// old version
cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher);
}
} catch (Exception e) {
throw new AssertionError(e);
}
}
}

View file

@ -1,114 +0,0 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import org.junit.Test;
import java.util.List;
public class CodecOptionsTest {
@Test
public void testIntegerImplicit() {
List<CodecOption> codecOptions = CodecOption.parse("some_key=5");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertEquals(5, option.getValue());
}
@Test
public void testInteger() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:int=5");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof Integer);
Assert.assertEquals(5, option.getValue());
}
@Test
public void testLong() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:long=5");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof Long);
Assert.assertEquals(5L, option.getValue());
}
@Test
public void testFloat() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:float=4.5");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof Float);
Assert.assertEquals(4.5f, option.getValue());
}
@Test
public void testString() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=some_value");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof String);
Assert.assertEquals("some_value", option.getValue());
}
@Test
public void testStringEscaped() {
List<CodecOption> codecOptions = CodecOption.parse("some_key:string=warning\\,this_is_not=a_new_key");
Assert.assertEquals(1, codecOptions.size());
CodecOption option = codecOptions.get(0);
Assert.assertEquals("some_key", option.getKey());
Assert.assertTrue(option.getValue() instanceof String);
Assert.assertEquals("warning,this_is_not=a_new_key", option.getValue());
}
@Test
public void testList() {
List<CodecOption> codecOptions = CodecOption.parse("a=1,b:int=2,c:long=3,d:float=4.5,e:string=a\\,b=c");
Assert.assertEquals(5, codecOptions.size());
CodecOption option;
option = codecOptions.get(0);
Assert.assertEquals("a", option.getKey());
Assert.assertTrue(option.getValue() instanceof Integer);
Assert.assertEquals(1, option.getValue());
option = codecOptions.get(1);
Assert.assertEquals("b", option.getKey());
Assert.assertTrue(option.getValue() instanceof Integer);
Assert.assertEquals(2, option.getValue());
option = codecOptions.get(2);
Assert.assertEquals("c", option.getKey());
Assert.assertTrue(option.getValue() instanceof Long);
Assert.assertEquals(3L, option.getValue());
option = codecOptions.get(3);
Assert.assertEquals("d", option.getKey());
Assert.assertTrue(option.getValue() instanceof Float);
Assert.assertEquals(4.5f, option.getValue());
option = codecOptions.get(4);
Assert.assertEquals("e", option.getKey());
Assert.assertTrue(option.getValue() instanceof String);
Assert.assertEquals("a,b=c", option.getValue());
}
}

View file

@ -1,403 +0,0 @@
package com.genymobile.scrcpy;
import android.view.KeyEvent;
import android.view.MotionEvent;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class ControlMessageReaderTest {
@Test
public void testParseKeycodeEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(5); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
// The message type (1 byte) does not count
Assert.assertEquals(ControlMessageReader.INJECT_KEYCODE_PAYLOAD_LENGTH, packet.length - 1);
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(5, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
@Test
public void testParseTextEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeInt(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType());
Assert.assertEquals("testé", event.getText());
}
@Test
public void testParseLongTextEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TEXT);
byte[] text = new byte[ControlMessageReader.INJECT_TEXT_MAX_LENGTH];
Arrays.fill(text, (byte) 'a');
dos.writeInt(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_TEXT, event.getType());
Assert.assertEquals(new String(text, StandardCharsets.US_ASCII), event.getText());
}
@Test
public void testParseTouchEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_TOUCH_EVENT);
dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeLong(-42); // pointerId
dos.writeInt(100);
dos.writeInt(200);
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeShort(0xffff); // pressure
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
byte[] packet = bos.toByteArray();
// The message type (1 byte) does not count
Assert.assertEquals(ControlMessageReader.INJECT_TOUCH_EVENT_PAYLOAD_LENGTH, packet.length - 1);
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_TOUCH_EVENT, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(-42, event.getPointerId());
Assert.assertEquals(100, event.getPosition().getPoint().getX());
Assert.assertEquals(200, event.getPosition().getPoint().getY());
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
}
@Test
public void testParseScrollEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_SCROLL_EVENT);
dos.writeInt(260);
dos.writeInt(1026);
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeInt(1);
dos.writeInt(-1);
byte[] packet = bos.toByteArray();
// The message type (1 byte) does not count
Assert.assertEquals(ControlMessageReader.INJECT_SCROLL_EVENT_PAYLOAD_LENGTH, packet.length - 1);
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_SCROLL_EVENT, event.getType());
Assert.assertEquals(260, event.getPosition().getPoint().getX());
Assert.assertEquals(1026, event.getPosition().getPoint().getY());
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1, event.getHScroll());
Assert.assertEquals(-1, event.getVScroll());
}
@Test
public void testParseBackOrScreenOnEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_BACK_OR_SCREEN_ON);
dos.writeByte(KeyEvent.ACTION_UP);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_BACK_OR_SCREEN_ON, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
}
@Test
public void testParseExpandNotificationPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL, event.getType());
}
@Test
public void testParseExpandSettingsPanelEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_EXPAND_SETTINGS_PANEL, event.getType());
}
@Test
public void testParseCollapsePanelsEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_COLLAPSE_PANELS);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_COLLAPSE_PANELS, event.getType());
}
@Test
public void testParseGetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_GET_CLIPBOARD);
dos.writeByte(ControlMessage.COPY_KEY_COPY);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_GET_CLIPBOARD, event.getType());
Assert.assertEquals(ControlMessage.COPY_KEY_COPY, event.getCopyKey());
}
@Test
public void testParseSetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
dos.writeLong(0x0102030405060708L); // sequence
dos.writeByte(1); // paste
byte[] text = "testé".getBytes(StandardCharsets.UTF_8);
dos.writeInt(text.length);
dos.write(text);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(0x0102030405060708L, event.getSequence());
Assert.assertEquals("testé", event.getText());
Assert.assertTrue(event.getPaste());
}
@Test
public void testParseBigSetClipboardEvent() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_CLIPBOARD);
byte[] rawText = new byte[ControlMessageReader.CLIPBOARD_TEXT_MAX_LENGTH];
dos.writeLong(0x0807060504030201L); // sequence
dos.writeByte(1); // paste
Arrays.fill(rawText, (byte) 'a');
String text = new String(rawText, 0, rawText.length);
dos.writeInt(rawText.length);
dos.write(rawText);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_CLIPBOARD, event.getType());
Assert.assertEquals(0x0807060504030201L, event.getSequence());
Assert.assertEquals(text, event.getText());
Assert.assertTrue(event.getPaste());
}
@Test
public void testParseSetScreenPowerMode() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE);
dos.writeByte(Device.POWER_MODE_NORMAL);
byte[] packet = bos.toByteArray();
// The message type (1 byte) does not count
Assert.assertEquals(ControlMessageReader.SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH, packet.length - 1);
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType());
Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction());
}
@Test
public void testParseRotateDevice() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_ROTATE_DEVICE);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_ROTATE_DEVICE, event.getType());
}
@Test
public void testMultiEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(0); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN);
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(1); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(0, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(1, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
@Test
public void testPartialEvents() throws IOException {
ControlMessageReader reader = new ControlMessageReader();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(KeyEvent.ACTION_UP);
dos.writeInt(KeyEvent.KEYCODE_ENTER);
dos.writeInt(4); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON);
dos.writeByte(ControlMessage.TYPE_INJECT_KEYCODE);
dos.writeByte(MotionEvent.ACTION_DOWN);
byte[] packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
ControlMessage event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(KeyEvent.ACTION_UP, event.getAction());
Assert.assertEquals(KeyEvent.KEYCODE_ENTER, event.getKeycode());
Assert.assertEquals(4, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
event = reader.next();
Assert.assertNull(event); // the event is not complete
bos.reset();
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
dos.writeInt(5); // repeat
dos.writeInt(KeyEvent.META_CTRL_ON);
packet = bos.toByteArray();
reader.readFrom(new ByteArrayInputStream(packet));
// the event is now complete
event = reader.next();
Assert.assertEquals(ControlMessage.TYPE_INJECT_KEYCODE, event.getType());
Assert.assertEquals(MotionEvent.ACTION_DOWN, event.getAction());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getKeycode());
Assert.assertEquals(5, event.getRepeat());
Assert.assertEquals(KeyEvent.META_CTRL_ON, event.getMetaState());
}
}

View file

@ -1,55 +0,0 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import org.junit.Test;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
public class DeviceMessageWriterTest {
@Test
public void testSerializeClipboard() throws IOException {
DeviceMessageWriter writer = new DeviceMessageWriter();
String text = "aéûoç";
byte[] data = text.getBytes(StandardCharsets.UTF_8);
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_CLIPBOARD);
dos.writeInt(data.length);
dos.write(data);
byte[] expected = bos.toByteArray();
DeviceMessage msg = DeviceMessage.createClipboard(text);
bos = new ByteArrayOutputStream();
writer.writeTo(msg, bos);
byte[] actual = bos.toByteArray();
Assert.assertArrayEquals(expected, actual);
}
@Test
public void testSerializeAckSetClipboard() throws IOException {
DeviceMessageWriter writer = new DeviceMessageWriter();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(DeviceMessage.TYPE_ACK_CLIPBOARD);
dos.writeLong(0x0102030405060708L);
byte[] expected = bos.toByteArray();
DeviceMessage msg = DeviceMessage.createAckClipboard(0x0102030405060708L);
bos = new ByteArrayOutputStream();
writer.writeTo(msg, bos);
byte[] actual = bos.toByteArray();
Assert.assertArrayEquals(expected, actual);
}
}

View file

@ -1,42 +0,0 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
public class StringUtilsTest {
@Test
public void testUtf8Truncate() {
String s = "aÉbÔc";
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
Assert.assertEquals(7, utf8.length);
int count;
count = StringUtils.getUtf8TruncationIndex(utf8, 1);
Assert.assertEquals(1, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 2);
Assert.assertEquals(1, count); // É is 2 bytes-wide
count = StringUtils.getUtf8TruncationIndex(utf8, 3);
Assert.assertEquals(3, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 4);
Assert.assertEquals(4, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 5);
Assert.assertEquals(4, count); // Ô is 2 bytes-wide
count = StringUtils.getUtf8TruncationIndex(utf8, 6);
Assert.assertEquals(6, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 7);
Assert.assertEquals(7, count);
count = StringUtils.getUtf8TruncationIndex(utf8, 8);
Assert.assertEquals(7, count); // no more chars
}
}