Compare commits
3 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
2bbf650758 | ||
|
36a94137b5 | ||
|
14112d8b11 |
3
.github/FUNDING.yml
vendored
3
.github/FUNDING.yml
vendored
@ -1,3 +0,0 @@
|
|||||||
github: [rom1v]
|
|
||||||
liberapay: rom1v
|
|
||||||
custom: ["https://paypal.me/rom2v"]
|
|
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
37
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -1,37 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Create a report to help us improve
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
_Please read the [prerequisites] to run scrcpy._
|
|
||||||
|
|
||||||
[prerequisites]: https://github.com/Genymobile/scrcpy#prerequisites
|
|
||||||
|
|
||||||
_Also read the [FAQ] and check if your [issue][issues] already exists._
|
|
||||||
|
|
||||||
[FAQ]: https://github.com/Genymobile/scrcpy/blob/master/FAQ.md
|
|
||||||
[issues]: https://github.com/Genymobile/scrcpy/issues
|
|
||||||
|
|
||||||
## Environment
|
|
||||||
|
|
||||||
- **OS:** [e.g. Debian, Windows, macOS...]
|
|
||||||
- **Scrcpy version:** [e.g. 2.5]
|
|
||||||
- **Installation method:** [e.g. manual build, apt, snap, brew, Windows release...]
|
|
||||||
- **Device model:**
|
|
||||||
- **Android version:** [e.g. 14]
|
|
||||||
|
|
||||||
## Describe the bug
|
|
||||||
|
|
||||||
A clear and concise description of what the bug is.
|
|
||||||
|
|
||||||
On errors, please provide the output of the console (and `adb logcat` if relevant).
|
|
||||||
|
|
||||||
```
|
|
||||||
Please paste terminal output in a code block.
|
|
||||||
```
|
|
||||||
|
|
||||||
Please do not post screenshots of your terminal, just post the content as text instead.
|
|
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
22
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@ -1,22 +0,0 @@
|
|||||||
---
|
|
||||||
name: Feature request
|
|
||||||
about: Suggest an idea for this project
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
- [ ] I have checked that a similar [feature request](https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3A%22feature+request%22) does not already exist.
|
|
||||||
|
|
||||||
**Is your feature request related to a problem? Please describe.**
|
|
||||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
|
||||||
|
|
||||||
**Describe the solution you'd like**
|
|
||||||
A clear and concise description of what you want to happen.
|
|
||||||
|
|
||||||
**Describe alternatives you've considered**
|
|
||||||
A clear and concise description of any alternative solutions or features you've considered.
|
|
||||||
|
|
||||||
**Additional context**
|
|
||||||
Add any other context or screenshots about the feature request here.
|
|
8
.github/ISSUE_TEMPLATE/question.md
vendored
8
.github/ISSUE_TEMPLATE/question.md
vendored
@ -1,8 +0,0 @@
|
|||||||
---
|
|
||||||
name: Question
|
|
||||||
about: Ask a question about scrcpy
|
|
||||||
title: ''
|
|
||||||
labels: ''
|
|
||||||
assignees: ''
|
|
||||||
|
|
||||||
---
|
|
502
.github/workflows/release.yml
vendored
502
.github/workflows/release.yml
vendored
@ -1,502 +0,0 @@
|
|||||||
name: Build
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
name:
|
|
||||||
description: 'Version name (default is ref name)'
|
|
||||||
|
|
||||||
env:
|
|
||||||
# $VERSION is used by release scripts
|
|
||||||
VERSION: ${{ github.event.inputs.name || github.ref_name }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
test-scrcpy-server:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
GRADLE: gradle # use native gradle instead of ./gradlew in scripts
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup JDK
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
|
|
||||||
- name: Test scrcpy-server
|
|
||||||
run: release/test_server.sh
|
|
||||||
|
|
||||||
build-scrcpy-server:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
GRADLE: gradle # use native gradle instead of ./gradlew in scripts
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup JDK
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: release/build_server.sh
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: scrcpy-server
|
|
||||||
path: release/work/build-server/server/scrcpy-server
|
|
||||||
|
|
||||||
test-build-scrcpy-server-without-gradle:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup JDK
|
|
||||||
uses: actions/setup-java@v4
|
|
||||||
with:
|
|
||||||
distribution: 'zulu'
|
|
||||||
java-version: '17'
|
|
||||||
|
|
||||||
- name: Build without gradle
|
|
||||||
run: server/build_without_gradle.sh
|
|
||||||
|
|
||||||
test-client:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
|
||||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
|
||||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
|
||||||
libv4l-dev
|
|
||||||
|
|
||||||
- name: Test
|
|
||||||
run: release/test_client.sh
|
|
||||||
|
|
||||||
build-linux-x86_64:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Check architecture
|
|
||||||
run: |
|
|
||||||
arch=$(uname -m)
|
|
||||||
if [[ "$arch" != x86_64 ]]
|
|
||||||
then
|
|
||||||
echo "Unexpected architecture: $arch" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
|
||||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
|
||||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
|
||||||
libv4l-dev
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: release/build_linux.sh x86_64
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Tar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-linux-x86_64
|
|
||||||
mkdir dist-tar
|
|
||||||
cd dist-tar
|
|
||||||
tar -C .. -cvf dist.tar.gz dist/
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-linux-x86_64-intermediate
|
|
||||||
path: release/work/build-linux-x86_64/dist-tar/
|
|
||||||
|
|
||||||
build-win32:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
|
||||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
|
||||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
|
||||||
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
|
||||||
|
|
||||||
- name: Workaround for old meson version run by Github Actions
|
|
||||||
run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: release/build_windows.sh 32
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Tar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-win32
|
|
||||||
mkdir dist-tar
|
|
||||||
cd dist-tar
|
|
||||||
tar -C .. -cvf dist.tar.gz dist/
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-win32-intermediate
|
|
||||||
path: release/work/build-win32/dist-tar/
|
|
||||||
|
|
||||||
build-win64:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \
|
|
||||||
libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \
|
|
||||||
libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \
|
|
||||||
mingw-w64 mingw-w64-tools libz-mingw-w64-dev
|
|
||||||
|
|
||||||
- name: Workaround for old meson version run by Github Actions
|
|
||||||
run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: release/build_windows.sh 64
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Tar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-win64
|
|
||||||
mkdir dist-tar
|
|
||||||
cd dist-tar
|
|
||||||
tar -C .. -cvf dist.tar.gz dist/
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-win64-intermediate
|
|
||||||
path: release/work/build-win64/dist-tar/
|
|
||||||
|
|
||||||
build-macos-aarch64:
|
|
||||||
runs-on: macos-latest
|
|
||||||
steps:
|
|
||||||
- name: Check architecture
|
|
||||||
run: |
|
|
||||||
arch=$(uname -m)
|
|
||||||
if [[ "$arch" != arm64 ]]
|
|
||||||
then
|
|
||||||
echo "Unexpected architecture: $arch" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: |
|
|
||||||
brew install meson ninja nasm libiconv zlib automake autoconf \
|
|
||||||
libtool
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: release/build_macos.sh aarch64
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Tar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-macos-aarch64
|
|
||||||
mkdir dist-tar
|
|
||||||
cd dist-tar
|
|
||||||
tar -C .. -cvf dist.tar.gz dist/
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-macos-aarch64-intermediate
|
|
||||||
path: release/work/build-macos-aarch64/dist-tar/
|
|
||||||
|
|
||||||
build-macos-x86_64:
|
|
||||||
runs-on: macos-13
|
|
||||||
steps:
|
|
||||||
- name: Check architecture
|
|
||||||
run: |
|
|
||||||
arch=$(uname -m)
|
|
||||||
if [[ "$arch" != x86_64 ]]
|
|
||||||
then
|
|
||||||
echo "Unexpected architecture: $arch" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: brew install meson ninja nasm libiconv zlib automake
|
|
||||||
# autoconf and libtool are already installed on macos-13
|
|
||||||
|
|
||||||
- name: Build
|
|
||||||
run: release/build_macos.sh x86_64
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Tar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-macos-x86_64
|
|
||||||
mkdir dist-tar
|
|
||||||
cd dist-tar
|
|
||||||
tar -C .. -cvf dist.tar.gz dist/
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-macos-x86_64-intermediate
|
|
||||||
path: release/work/build-macos-x86_64/dist-tar/
|
|
||||||
|
|
||||||
package-linux-x86_64:
|
|
||||||
needs:
|
|
||||||
- build-scrcpy-server
|
|
||||||
- build-linux-x86_64
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download scrcpy-server
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: scrcpy-server
|
|
||||||
path: release/work/build-server/server/
|
|
||||||
|
|
||||||
- name: Download build-linux-x86_64
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-linux-x86_64-intermediate
|
|
||||||
path: release/work/build-linux-x86_64/dist-tar/
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Detar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-linux-x86_64
|
|
||||||
tar xf dist-tar/dist.tar.gz
|
|
||||||
|
|
||||||
- name: Package
|
|
||||||
run: release/package_client.sh linux-x86_64 tar.gz
|
|
||||||
|
|
||||||
- name: Upload release
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-linux-x86_64
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
package-win32:
|
|
||||||
needs:
|
|
||||||
- build-scrcpy-server
|
|
||||||
- build-win32
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download scrcpy-server
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: scrcpy-server
|
|
||||||
path: release/work/build-server/server/
|
|
||||||
|
|
||||||
- name: Download build-win32
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-win32-intermediate
|
|
||||||
path: release/work/build-win32/dist-tar/
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Detar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-win32
|
|
||||||
tar xf dist-tar/dist.tar.gz
|
|
||||||
|
|
||||||
- name: Package
|
|
||||||
run: release/package_client.sh win32 zip
|
|
||||||
|
|
||||||
- name: Upload release
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-win32
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
package-win64:
|
|
||||||
needs:
|
|
||||||
- build-scrcpy-server
|
|
||||||
- build-win64
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download scrcpy-server
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: scrcpy-server
|
|
||||||
path: release/work/build-server/server/
|
|
||||||
|
|
||||||
- name: Download build-win64
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-win64-intermediate
|
|
||||||
path: release/work/build-win64/dist-tar/
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Detar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-win64
|
|
||||||
tar xf dist-tar/dist.tar.gz
|
|
||||||
|
|
||||||
- name: Package
|
|
||||||
run: release/package_client.sh win64 zip
|
|
||||||
|
|
||||||
- name: Upload release
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-win64
|
|
||||||
path: release/output
|
|
||||||
|
|
||||||
package-macos-aarch64:
|
|
||||||
needs:
|
|
||||||
- build-scrcpy-server
|
|
||||||
- build-macos-aarch64
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download scrcpy-server
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: scrcpy-server
|
|
||||||
path: release/work/build-server/server/
|
|
||||||
|
|
||||||
- name: Download build-macos-aarch64
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-macos-aarch64-intermediate
|
|
||||||
path: release/work/build-macos-aarch64/dist-tar/
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Detar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-macos-aarch64
|
|
||||||
tar xf dist-tar/dist.tar.gz
|
|
||||||
|
|
||||||
- name: Package
|
|
||||||
run: release/package_client.sh macos-aarch64 tar.gz
|
|
||||||
|
|
||||||
- name: Upload release
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-macos-aarch64
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
package-macos-x86_64:
|
|
||||||
needs:
|
|
||||||
- build-scrcpy-server
|
|
||||||
- build-macos-x86_64
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download scrcpy-server
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: scrcpy-server
|
|
||||||
path: release/work/build-server/server/
|
|
||||||
|
|
||||||
- name: Download build-macos
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: build-macos-x86_64-intermediate
|
|
||||||
path: release/work/build-macos-x86_64/dist-tar/
|
|
||||||
|
|
||||||
# upload-artifact does not preserve permissions
|
|
||||||
- name: Detar
|
|
||||||
run: |
|
|
||||||
cd release/work/build-macos-x86_64
|
|
||||||
tar xf dist-tar/dist.tar.gz
|
|
||||||
|
|
||||||
- name: Package
|
|
||||||
run: release/package_client.sh macos-x86_64 tar.gz
|
|
||||||
|
|
||||||
- name: Upload release
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-macos-x86_64
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
release:
|
|
||||||
needs:
|
|
||||||
- build-scrcpy-server
|
|
||||||
- package-linux-x86_64
|
|
||||||
- package-win32
|
|
||||||
- package-win64
|
|
||||||
- package-macos-aarch64
|
|
||||||
- package-macos-x86_64
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Download scrcpy-server
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: scrcpy-server
|
|
||||||
path: release/work/build-server/server/
|
|
||||||
|
|
||||||
- name: Download release-linux-x86_64
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-linux-x86_64
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
- name: Download release-win32
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-win32
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
- name: Download release-win64
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-win64
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
- name: Download release-macos-aarch64
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-macos-aarch64
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
- name: Download release-macos-x86_64
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
name: release-macos-x86_64
|
|
||||||
path: release/output/
|
|
||||||
|
|
||||||
- name: Package server
|
|
||||||
run: release/package_server.sh
|
|
||||||
|
|
||||||
- name: Generate checksums
|
|
||||||
run: release/generate_checksums.sh
|
|
||||||
|
|
||||||
- name: Upload release artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: scrcpy-release-${{ env.VERSION }}
|
|
||||||
path: release/output
|
|
6
.gitignore
vendored
6
.gitignore
vendored
@ -1,10 +1,4 @@
|
|||||||
build/
|
build/
|
||||||
/dist/
|
/dist/
|
||||||
/build-*/
|
|
||||||
/build_*/
|
|
||||||
/release-*/
|
|
||||||
.idea/
|
.idea/
|
||||||
.gradle/
|
.gradle/
|
||||||
/x/
|
|
||||||
local.properties
|
|
||||||
/scrcpy-server
|
|
||||||
|
238
DEVELOP.md
Normal file
238
DEVELOP.md
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
# scrcpy for developers
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This application is composed of two parts:
|
||||||
|
- the server (`scrcpy-server.jar`), to be executed on the device,
|
||||||
|
- the client (the `scrcpy` binary), executed on the host computer.
|
||||||
|
|
||||||
|
The client is responsible to push the server to the device and start its
|
||||||
|
execution.
|
||||||
|
|
||||||
|
Once the client and the server are connected to each other, the server initially
|
||||||
|
sends device information (name and initial screen dimensions), then starts to
|
||||||
|
send a raw H.264 video stream of the device screen. The client decodes the video
|
||||||
|
frames, and display them as soon as possible, without buffering, to minimize
|
||||||
|
latency. The client is not aware of the device rotation (which is handled by the
|
||||||
|
server), it just knows the dimensions of the video frames.
|
||||||
|
|
||||||
|
The client captures relevant keyboard and mouse events, that it transmits to the
|
||||||
|
server, which injects them to the device.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Server
|
||||||
|
|
||||||
|
|
||||||
|
### Privileges
|
||||||
|
|
||||||
|
Capturing the screen requires some privileges, which are granted to `shell`.
|
||||||
|
|
||||||
|
The server is a Java application (with a [`public static void main(String...
|
||||||
|
args)`][main] method), compiled against the Android framework, and executed as
|
||||||
|
`shell` on the Android device.
|
||||||
|
|
||||||
|
[main]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/Server.java#L61
|
||||||
|
|
||||||
|
To run such a Java application, the classes must be [_dexed_][dex] (typically,
|
||||||
|
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
|
||||||
|
`classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run
|
||||||
|
with:
|
||||||
|
|
||||||
|
adb shell CLASSPATH=/data/local/tmp/classes.dex \
|
||||||
|
app_process / my.package.MainClass
|
||||||
|
|
||||||
|
_The path `/data/local/tmp` is a good candidate to push the server, since it's
|
||||||
|
readable and writable by `shell`, but not world-writable, so a malicious
|
||||||
|
application may not replace the server just before the client executes it._
|
||||||
|
|
||||||
|
Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing
|
||||||
|
`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle
|
||||||
|
build system, the server is built to an (unsigned) APK (renamed to
|
||||||
|
`scrcpy-server.jar`).
|
||||||
|
|
||||||
|
[dex]: https://en.wikipedia.org/wiki/Dalvik_(software)
|
||||||
|
[apk]: https://en.wikipedia.org/wiki/Android_application_package
|
||||||
|
|
||||||
|
|
||||||
|
### Hidden methods
|
||||||
|
|
||||||
|
Although compiled against the Android framework, [hidden] methods and classes are
|
||||||
|
not directly accessible (and they may differ from one Android version to
|
||||||
|
another).
|
||||||
|
|
||||||
|
They can be called using reflection though. The communication with hidden
|
||||||
|
components is provided by [_wrappers_ classes][wrappers] and [aidl].
|
||||||
|
|
||||||
|
[hidden]: https://stackoverflow.com/a/31908373/1987178
|
||||||
|
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers
|
||||||
|
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/aidl/android/view
|
||||||
|
|
||||||
|
|
||||||
|
### Threading
|
||||||
|
|
||||||
|
The server uses 2 threads:
|
||||||
|
|
||||||
|
- the **main** thread, encoding and streaming the video to the client;
|
||||||
|
- the **controller** thread, listening for _control events_ (typically,
|
||||||
|
keyboard and mouse events) from the client.
|
||||||
|
|
||||||
|
Since the video encoding is typically hardware, there would be no benefit in
|
||||||
|
encoding and streaming in two different threads.
|
||||||
|
|
||||||
|
|
||||||
|
### Screen video encoding
|
||||||
|
|
||||||
|
The encoding is managed by [`ScreenEncoder`].
|
||||||
|
|
||||||
|
The video is encoded using the [`MediaCodec`] API. The codec takes its input
|
||||||
|
from a [surface] associated to the display, and writes the resulting H.264
|
||||||
|
stream to the provided output stream (the socket connected to the client).
|
||||||
|
|
||||||
|
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
|
||||||
|
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
|
||||||
|
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L63-L64
|
||||||
|
|
||||||
|
On device [rotation], the codec, surface and display are reinitialized, and a
|
||||||
|
new video stream is produced.
|
||||||
|
|
||||||
|
New frames are produced only when changes occur on the surface. This is good
|
||||||
|
because it avoids to send unnecessary frames, but there are drawbacks:
|
||||||
|
|
||||||
|
- it does not send any frame on start if the device screen does not change,
|
||||||
|
- after fast motion changes, the last frame may have poor quality.
|
||||||
|
|
||||||
|
Both problems are [solved][repeat] by the flag
|
||||||
|
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
|
||||||
|
|
||||||
|
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L89-L92
|
||||||
|
[repeat]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L125-L126
|
||||||
|
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
|
||||||
|
|
||||||
|
|
||||||
|
### Input events injection
|
||||||
|
|
||||||
|
_Control events_ are received from the client by the [`EventController`] (run in
|
||||||
|
a separate thread). There are 5 types of input events:
|
||||||
|
- keycode (cf [`KeyEvent`]),
|
||||||
|
- text (special characters may not be handled by keycodes directly),
|
||||||
|
- mouse motion/click,
|
||||||
|
- mouse scroll,
|
||||||
|
- custom command (e.g. to switch the screen on).
|
||||||
|
|
||||||
|
All of them may need to inject input events to the system. To do so, they use
|
||||||
|
the _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
|
||||||
|
[`InputManager` wrapper][inject-wrapper]).
|
||||||
|
|
||||||
|
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/EventController.java#L70
|
||||||
|
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
|
||||||
|
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
|
||||||
|
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857
|
||||||
|
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.0/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## Client
|
||||||
|
|
||||||
|
The client relies on [SDL], which provides cross-platform API for UI, input
|
||||||
|
events, threading, etc.
|
||||||
|
|
||||||
|
The video stream is decoded by [libav] (FFmpeg).
|
||||||
|
|
||||||
|
[SDL]: https://www.libsdl.org
|
||||||
|
[libav]: https://www.libav.org/
|
||||||
|
|
||||||
|
### Initialization
|
||||||
|
|
||||||
|
On startup, in addition to _libav_ and _SDL_ initialization, the client must
|
||||||
|
push and start the server on the device, and open a socket so that they may
|
||||||
|
communicate.
|
||||||
|
|
||||||
|
Note that the client-server roles are expressed at the application level:
|
||||||
|
|
||||||
|
- the server _serves_ video stream and handle requests from the client,
|
||||||
|
- the client _controls_ the device through the server.
|
||||||
|
|
||||||
|
However, the roles are inverted at the network level:
|
||||||
|
|
||||||
|
- the client opens a server socket and listen on a port before starting the
|
||||||
|
server,
|
||||||
|
- the server connects to the client.
|
||||||
|
|
||||||
|
This role inversion guarantees that the connection will not fail due to race
|
||||||
|
conditions, and avoids polling.
|
||||||
|
|
||||||
|
Once the server is connected, it sends the device information (name and initial
|
||||||
|
screen dimensions). Thus, the client may init the window and renderer, before
|
||||||
|
the first frame is available.
|
||||||
|
|
||||||
|
To minimize startup time, SDL initialization is performed while listening for
|
||||||
|
the connection from the server (see commit [90a46b4]).
|
||||||
|
|
||||||
|
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
|
||||||
|
|
||||||
|
|
||||||
|
### Threading
|
||||||
|
|
||||||
|
The client uses 3 threads:
|
||||||
|
|
||||||
|
- the **main** thread, executing the SDL event loop,
|
||||||
|
- the **decoder** thread, decoding video frames,
|
||||||
|
- the **controller** thread, sending _control events_ to the server.
|
||||||
|
|
||||||
|
|
||||||
|
### Decoder
|
||||||
|
|
||||||
|
The [decoder] runs in a separate thread. It uses _libav_ to decode the H.264
|
||||||
|
stream from the socket, and notifies the main thread when a new frame is
|
||||||
|
available.
|
||||||
|
|
||||||
|
There are two [frames] simultaneously in memory:
|
||||||
|
- the **decoding** frame, written by the decoder from the decoder thread,
|
||||||
|
- the **rendering** frame, rendered in a texture from the main thread.
|
||||||
|
|
||||||
|
When a new decoded frame is available, the decoder _swaps_ the decoding and
|
||||||
|
rendering frame (with proper synchronization). Thus, it immediatly starts
|
||||||
|
to decode a new frame while the main thread renders the last one.
|
||||||
|
|
||||||
|
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/decoder.c
|
||||||
|
[frames]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/frames.h
|
||||||
|
|
||||||
|
|
||||||
|
### Controller
|
||||||
|
|
||||||
|
The [controller] is responsible to send _control events_ to the device. It runs
|
||||||
|
in a separate thread, to avoid I/O on the main thread.
|
||||||
|
|
||||||
|
On SDL event, received on the main thread, the [input manager][inputmanager]
|
||||||
|
creates appropriate [_control events_][controlevent]. It is responsible to
|
||||||
|
convert SDL events to Android events (using [convert]). It pushes the _control
|
||||||
|
events_ to a blocking queue hold by the controller. On its own thread, the
|
||||||
|
controller takes events from the queue, that it serializes and sends to the
|
||||||
|
client.
|
||||||
|
|
||||||
|
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controller.h
|
||||||
|
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/controlevent.h
|
||||||
|
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/inputmanager.h
|
||||||
|
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/convert.h
|
||||||
|
|
||||||
|
|
||||||
|
### UI and event loop
|
||||||
|
|
||||||
|
Initialization, input events and rendering are all [managed][scrcpy] in the main
|
||||||
|
thread.
|
||||||
|
|
||||||
|
Events are handled in the [event loop], which either updates the [screen] or
|
||||||
|
delegates to the [input manager][inputmanager].
|
||||||
|
|
||||||
|
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c
|
||||||
|
[event loop]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/scrcpy.c#L38
|
||||||
|
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.0/app/src/screen.h
|
||||||
|
|
||||||
|
|
||||||
|
## Hack
|
||||||
|
|
||||||
|
For more details, go read the code!
|
||||||
|
|
||||||
|
If you find a bug, or have an awesome idea to implement, please discuss and
|
||||||
|
contribute ;-)
|
239
FAQ.md
239
FAQ.md
@ -1,205 +1,84 @@
|
|||||||
# Frequently Asked Questions
|
# Frequently Asked Questions
|
||||||
|
|
||||||
[Read in another language](#translations)
|
## Common issues
|
||||||
|
|
||||||
|
The application is very young, it is not unlikely that you encounter problems
|
||||||
|
with it.
|
||||||
|
|
||||||
Here are the common reported problems and their status.
|
Here are the common reported problems and their status.
|
||||||
|
|
||||||
If you encounter any error, the first step is to upgrade to the latest version.
|
### On Windows, I have no output in the console
|
||||||
|
|
||||||
|
When run in `cmd.exe`, the application does not print anything. Even `scrcpy
|
||||||
|
--help` have no output. We don't know why yet.
|
||||||
|
|
||||||
## `adb` and USB issues
|
However, if you run the very same `scrcpy.exe` from
|
||||||
|
[MSYS2](https://www.msys2.org/) (`mingw64`), then it correctly prints output.
|
||||||
|
|
||||||
`scrcpy` execute `adb` commands to initialize the connection with the device. If
|
As a workaround, redirect outputs to files, so that you can read the files
|
||||||
`adb` fails, then scrcpy will not work.
|
afterwards:
|
||||||
|
|
||||||
This is typically not a bug in _scrcpy_, but a problem in your environment.
|
```bash
|
||||||
|
scrcpy >stdout 2>stderr
|
||||||
|
type stdout
|
||||||
### `adb` not found
|
type stderr
|
||||||
|
|
||||||
You need `adb` accessible from your `PATH`.
|
|
||||||
|
|
||||||
On Windows, the current directory is in your `PATH`, and `adb.exe` is included
|
|
||||||
in the release, so it should work out-of-the-box.
|
|
||||||
|
|
||||||
|
|
||||||
### Device not detected
|
|
||||||
|
|
||||||
> ERROR: Could not find any ADB device
|
|
||||||
|
|
||||||
Check that you correctly enabled [adb debugging][enable-adb].
|
|
||||||
|
|
||||||
Your device must be detected by `adb`:
|
|
||||||
|
|
||||||
```
|
|
||||||
adb devices
|
|
||||||
```
|
```
|
||||||
|
|
||||||
If your device is not detected, you may need some [drivers] (on Windows). There is a separate [USB driver for Google devices][google-usb-driver].
|
_Note that all SDL logs are printed to stderr._
|
||||||
|
|
||||||
|
|
||||||
|
### On Windows, when I start the application, nothing happens
|
||||||
|
|
||||||
|
The previous problem does not help to get a clue about the cause.
|
||||||
|
|
||||||
|
The most common is your device not being detected by `adb`, or is unauthorized.
|
||||||
|
Check everything is ok by calling:
|
||||||
|
|
||||||
|
adb devices
|
||||||
|
|
||||||
|
Windows may need some [drivers] to detect your device.
|
||||||
|
|
||||||
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
|
||||||
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
[drivers]: https://developer.android.com/studio/run/oem-usb.html
|
||||||
[google-usb-driver]: https://developer.android.com/studio/run/win-usb
|
|
||||||
|
If you still encounter problems, please see [issue 9].
|
||||||
|
|
||||||
|
[issue 9]: https://github.com/Genymobile/scrcpy/issues/9
|
||||||
|
|
||||||
|
|
||||||
### Device unauthorized
|
### I get a black screen for some applications like Silence
|
||||||
|
|
||||||
> ERROR: Device is unauthorized:
|
This is expected, they requested to protect the screen.
|
||||||
> ERROR: --> (usb) 0123456789abcdef unauthorized
|
|
||||||
> ERROR: A popup should open on the device to request authorization.
|
|
||||||
|
|
||||||
When connecting, a popup should open on the device. You must authorize USB
|
In [Silence], you can disable it in settings → Privacy → Screen security.
|
||||||
debugging.
|
|
||||||
|
|
||||||
If it does not open, check [stackoverflow][device-unauthorized].
|
[silence]: https://f-droid.org/en/packages/org.smssecure.smssecure/
|
||||||
|
|
||||||
[device-unauthorized]: https://stackoverflow.com/questions/23081263/adb-android-device-unauthorized
|
See [issue 36].
|
||||||
|
|
||||||
|
[issue 36]: https://github.com/Genymobile/scrcpy/issues/36
|
||||||
|
|
||||||
|
|
||||||
### Several devices connected
|
### Mouse clicks do not work
|
||||||
|
|
||||||
If several devices are connected, you will encounter this error:
|
|
||||||
|
|
||||||
> ERROR: Multiple (2) ADB devices:
|
|
||||||
> ERROR: --> (usb) 0123456789abcdef device Nexus_5
|
|
||||||
> ERROR: --> (tcpip) 192.168.1.5:5555 device GM1913
|
|
||||||
> ERROR: Select a device via -s (--serial), -d (--select-usb) or -e (--select-tcpip)
|
|
||||||
|
|
||||||
In that case, you can either provide the identifier of the device you want to
|
|
||||||
mirror:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy -s 0123456789abcdef
|
|
||||||
```
|
|
||||||
|
|
||||||
Or request the single USB (or TCP/IP) device:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy -d # USB device
|
|
||||||
scrcpy -e # TCP/IP device
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that if your device is connected over TCP/IP, you might get this message:
|
|
||||||
|
|
||||||
> adb: error: more than one device/emulator
|
|
||||||
> ERROR: "adb reverse" returned with value 1
|
|
||||||
> WARN: 'adb reverse' failed, fallback to 'adb forward'
|
|
||||||
|
|
||||||
This is expected (due to a bug on old Android versions, see [#5]), but in that
|
|
||||||
case, scrcpy fallbacks to a different method, which should work.
|
|
||||||
|
|
||||||
[#5]: https://github.com/Genymobile/scrcpy/issues/5
|
|
||||||
|
|
||||||
|
|
||||||
### Conflicts between adb versions
|
|
||||||
|
|
||||||
> adb server version (41) doesn't match this client (39); killing...
|
|
||||||
|
|
||||||
This error occurs when you use several `adb` versions simultaneously. You must
|
|
||||||
find the program using a different `adb` version, and use the same `adb` version
|
|
||||||
everywhere.
|
|
||||||
|
|
||||||
You could overwrite the `adb` binary in the other program, or ask _scrcpy_ to
|
|
||||||
use a specific `adb` binary, by setting the `ADB` environment variable:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in bash
|
|
||||||
export ADB=/path/to/your/adb
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
:: in cmd
|
|
||||||
set ADB=C:\path\to\your\adb.exe
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# in PowerShell
|
|
||||||
$env:ADB = 'C:\path\to\your\adb.exe'
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Device disconnected
|
|
||||||
|
|
||||||
If _scrcpy_ stops itself with the warning "Device disconnected", then the
|
|
||||||
`adb` connection has been closed.
|
|
||||||
|
|
||||||
Try with another USB cable or plug it into another USB port. See [#281] and
|
|
||||||
[#283].
|
|
||||||
|
|
||||||
[#281]: https://github.com/Genymobile/scrcpy/issues/281
|
|
||||||
[#283]: https://github.com/Genymobile/scrcpy/issues/283
|
|
||||||
|
|
||||||
|
|
||||||
## OTG issues on Windows
|
|
||||||
|
|
||||||
On Windows, if `scrcpy --otg` (or `--keyboard=aoa`/`--mouse=aoa`) results in:
|
|
||||||
|
|
||||||
> ERROR: Could not find any USB device
|
|
||||||
|
|
||||||
(or if only unrelated USB devices are detected), there might be drivers issues.
|
|
||||||
|
|
||||||
Please read [#3654], in particular [this comment][#3654-comment1] and [the next
|
|
||||||
one][#3654-comment2].
|
|
||||||
|
|
||||||
[#3654]: https://github.com/Genymobile/scrcpy/issues/3654
|
|
||||||
[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232
|
|
||||||
[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011
|
|
||||||
|
|
||||||
|
|
||||||
## Control issues
|
|
||||||
|
|
||||||
### Mouse and keyboard do not work
|
|
||||||
|
|
||||||
On some devices, you may need to enable an option to allow [simulating input].
|
On some devices, you may need to enable an option to allow [simulating input].
|
||||||
In developer options, enable:
|
|
||||||
|
|
||||||
> **USB debugging (Security settings)**
|
|
||||||
> _Allow granting permissions and simulating input via USB debugging_
|
|
||||||
|
|
||||||
Rebooting the device is necessary once this option is set.
|
|
||||||
|
|
||||||
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
||||||
|
|
||||||
|
|
||||||
### Special characters do not work
|
### Mouse clicks at wrong location
|
||||||
|
|
||||||
The default text injection method is [limited to ASCII characters][text-input].
|
On MacOS, with HiDPI support and multiple screens, input location are wrongly
|
||||||
A trick allows to also inject some [accented characters][accented-characters],
|
scaled. See [issue 15].
|
||||||
but that's all. See [#37].
|
|
||||||
|
|
||||||
To avoid the problem, [change the keyboard mode to simulate a physical
|
[issue 15]: https://github.com/Genymobile/scrcpy/issues/15
|
||||||
keyboard][hid].
|
|
||||||
|
|
||||||
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
|
A workaround is to build with HiDPI support disabled:
|
||||||
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
|
|
||||||
[#37]: https://github.com/Genymobile/scrcpy/issues/37
|
|
||||||
[hid]: doc/keyboard.md#physical-keyboard-simulation
|
|
||||||
|
|
||||||
|
|
||||||
## Client issues
|
|
||||||
|
|
||||||
### Issue with Wayland
|
|
||||||
|
|
||||||
By default, SDL uses x11 on Linux. The [video driver] can be changed via the
|
|
||||||
`SDL_VIDEODRIVER` environment variable:
|
|
||||||
|
|
||||||
[video driver]: https://wiki.libsdl.org/FAQUsingSDL#how_do_i_choose_a_specific_video_driver
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
export SDL_VIDEODRIVER=wayland
|
meson x --buildtype release -Dhidpi_support=false
|
||||||
scrcpy
|
|
||||||
```
|
```
|
||||||
|
|
||||||
On some distributions (at least Fedora), the package `libdecor` must be
|
However, the video will be displayed at lower resolution.
|
||||||
installed manually.
|
|
||||||
|
|
||||||
See issues [#2554] and [#2559].
|
|
||||||
|
|
||||||
[#2554]: https://github.com/Genymobile/scrcpy/issues/2554
|
|
||||||
[#2559]: https://github.com/Genymobile/scrcpy/issues/2559
|
|
||||||
|
|
||||||
|
|
||||||
### KWin compositor crashes
|
### KWin compositor crashes
|
||||||
@ -209,27 +88,3 @@ On Plasma Desktop, compositor is disabled while _scrcpy_ is running.
|
|||||||
As a workaround, [disable "Block compositing"][kwin].
|
As a workaround, [disable "Block compositing"][kwin].
|
||||||
|
|
||||||
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
[kwin]: https://github.com/Genymobile/scrcpy/issues/114#issuecomment-378778613
|
||||||
|
|
||||||
|
|
||||||
## Crashes
|
|
||||||
|
|
||||||
### Exception
|
|
||||||
|
|
||||||
If you get any exception related to `MediaCodec`:
|
|
||||||
|
|
||||||
```
|
|
||||||
ERROR: Exception on thread Thread[main,5,main]
|
|
||||||
java.lang.IllegalStateException
|
|
||||||
at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
|
||||||
```
|
|
||||||
|
|
||||||
then try with another [encoder](doc/video.md#encoder).
|
|
||||||
|
|
||||||
|
|
||||||
## Translations
|
|
||||||
|
|
||||||
Translations of this FAQ in other languages are available in the [wiki].
|
|
||||||
|
|
||||||
[wiki]: https://github.com/Genymobile/scrcpy/wiki
|
|
||||||
|
|
||||||
Only this FAQ file is guaranteed to be up-to-date.
|
|
||||||
|
1
LICENSE
1
LICENSE
@ -188,7 +188,6 @@
|
|||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
Copyright (C) 2018 Genymobile
|
||||||
Copyright (C) 2018-2025 Romain Vimont
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
65
Makefile
Normal file
65
Makefile
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# This makefile provides recipes to build a "portable" version of scrcpy.
|
||||||
|
#
|
||||||
|
# Here, "portable" means that the client and server binaries are expected to be
|
||||||
|
# anywhere, but in the same directory, instead of well-defined separate
|
||||||
|
# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server.jar).
|
||||||
|
#
|
||||||
|
# In particular, this implies to change the location from where the client push
|
||||||
|
# the server to the device.
|
||||||
|
#
|
||||||
|
# "make release-portable" builds a zip containing the client and the server.
|
||||||
|
#
|
||||||
|
# On Windows with MSYS2/mingw64, execute:
|
||||||
|
# GRADLE="$PWD/gradlew" mingw32-make release-portable
|
||||||
|
#
|
||||||
|
# This is a simple Makefile because Meson is not flexible enough to execute some
|
||||||
|
# arbitrary commands.
|
||||||
|
|
||||||
|
.PHONY: default clean build-portable release-portable dist-portable dist-portable-zip sums test check
|
||||||
|
|
||||||
|
GRADLE ?= ./gradlew
|
||||||
|
|
||||||
|
PORTABLE_BUILD_DIR := build-portable
|
||||||
|
DIST := dist
|
||||||
|
TARGET_DIR := scrcpy
|
||||||
|
|
||||||
|
VERSION := $(shell git describe --tags --always)
|
||||||
|
TARGET := $(TARGET_DIR)-$(VERSION).zip
|
||||||
|
|
||||||
|
default:
|
||||||
|
@echo 'You must specify a target. Try: make release-portable'
|
||||||
|
|
||||||
|
clean:
|
||||||
|
$(GRADLE) clean
|
||||||
|
rm -rf "$(PORTABLE_BUILD_DIR)" "$(DIST)"
|
||||||
|
|
||||||
|
build-portable:
|
||||||
|
[ -d "$(PORTABLE_BUILD_DIR)" ] || ( mkdir "$(PORTABLE_BUILD_DIR)" && \
|
||||||
|
meson "$(PORTABLE_BUILD_DIR)" \
|
||||||
|
--buildtype release --strip -Db_lto=true \
|
||||||
|
-Doverride_server_path=scrcpy-server.jar )
|
||||||
|
ninja -C "$(PORTABLE_BUILD_DIR)"
|
||||||
|
|
||||||
|
release-portable: clean dist-portable-zip sums
|
||||||
|
@echo "Release created in $(DIST)/."
|
||||||
|
|
||||||
|
dist-portable: build-portable
|
||||||
|
mkdir -p "$(DIST)/$(TARGET_DIR)"
|
||||||
|
cp "$(PORTABLE_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(TARGET_DIR)/"
|
||||||
|
cp "$(PORTABLE_BUILD_DIR)"/app/scrcpy "$(DIST)/$(TARGET_DIR)/"
|
||||||
|
|
||||||
|
dist-portable-zip: dist-portable
|
||||||
|
cd "$(DIST)"; \
|
||||||
|
zip -r "$(TARGET)" "$(TARGET_DIR)"
|
||||||
|
|
||||||
|
sums:
|
||||||
|
cd "$(DIST)"; \
|
||||||
|
sha256sum *.zip > SHA256SUM.txt
|
||||||
|
|
||||||
|
test: build-portable
|
||||||
|
$(GRADLE) test
|
||||||
|
ninja -C "$(PORTABLE_BUILD_DIR)" test
|
||||||
|
|
||||||
|
check: build-portable
|
||||||
|
$(GRADLE) check
|
||||||
|
ninja -C "$(PORTABLE_BUILD_DIR)" test
|
436
README.md
436
README.md
@ -1,216 +1,343 @@
|
|||||||
**This GitHub repo (<https://github.com/Genymobile/scrcpy>) is the only official
|
# scrcpy
|
||||||
source for the project. Do not download releases from random websites, even if
|
|
||||||
their name contains `scrcpy`.**
|
|
||||||
|
|
||||||
# scrcpy (v3.1)
|
This application provides display and control of Android devices connected on
|
||||||
|
USB. It does not require any _root_ access. It works on _GNU/Linux_, _Windows_
|
||||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
and _MacOS_.
|
||||||
|
|
||||||
_pronounced "**scr**een **c**o**py**"_
|
|
||||||
|
|
||||||
This application mirrors Android devices (video and audio) connected via
|
|
||||||
USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the
|
|
||||||
device with the keyboard and the mouse of the computer. It does not require any
|
|
||||||
_root_ access. It works on _Linux_, _Windows_ and _macOS_.
|
|
||||||
|
|
||||||
![screenshot](assets/screenshot-debian-600.jpg)
|
![screenshot](assets/screenshot-debian-600.jpg)
|
||||||
|
|
||||||
It focuses on:
|
|
||||||
|
|
||||||
- **lightness**: native, displays only the device screen
|
## Requirements
|
||||||
- **performance**: 30~120fps, depending on the device
|
|
||||||
- **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 Android device
|
|
||||||
- **user benefits**: no account, no ads, no internet required
|
|
||||||
- **freedom**: free and open source software
|
|
||||||
|
|
||||||
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
|
The Android part requires at least API 21 (Android 5.0).
|
||||||
|
|
||||||
Its features include:
|
You need [adb]. It is available in the [Android SDK platform
|
||||||
- [audio forwarding](doc/audio.md) (Android 11+)
|
tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
|
||||||
- [recording](doc/recording.md)
|
|
||||||
- [virtual display](doc/virtual_display.md)
|
|
||||||
- mirroring with [Android device screen off](doc/device.md#turn-screen-off)
|
|
||||||
- [copy-paste](doc/control.md#copy-paste) in both directions
|
|
||||||
- [configurable quality](doc/video.md)
|
|
||||||
- [camera mirroring](doc/camera.md) (Android 12+)
|
|
||||||
- [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only)
|
|
||||||
- physical [keyboard][hid-keyboard] and [mouse][hid-mouse] simulation (HID)
|
|
||||||
- [gamepad](doc/gamepad.md) support
|
|
||||||
- [OTG mode](doc/otg.md)
|
|
||||||
- and more…
|
|
||||||
|
|
||||||
[hid-keyboard]: doc/keyboard.md#physical-keyboard-simulation
|
On Windows, if you use the [prebuilt application](#windows), it is already
|
||||||
[hid-mouse]: doc/mouse.md#physical-mouse-simulation
|
included. Otherwise, just download the [platform-tools][platform-tools-windows]
|
||||||
|
and extract the following files to a directory accessible from your `PATH`:
|
||||||
|
- `adb.exe`
|
||||||
|
- `AdbWinApi.dll`
|
||||||
|
- `AdbWinUsbApi.dll`
|
||||||
|
|
||||||
## Prerequisites
|
Make sure you [enabled adb debugging][enable-adb] on your device(s).
|
||||||
|
|
||||||
The Android device requires at least API 21 (Android 5.0).
|
The client requires [FFmpeg], [LibSDL2] and [LibUSB].
|
||||||
|
|
||||||
[Audio forwarding](doc/audio.md) is supported for API >= 30 (Android 11+).
|
[adb]: https://developer.android.com/studio/command-line/adb.html
|
||||||
|
[enable-adb]: https://developer.android.com/studio/command-line/adb.html#Enabling
|
||||||
|
[platform-tools]: https://developer.android.com/studio/releases/platform-tools.html
|
||||||
|
[platform-tools-windows]: https://dl.google.com/android/repository/platform-tools-latest-windows.zip
|
||||||
|
[ffmpeg]: https://en.wikipedia.org/wiki/FFmpeg
|
||||||
|
[libsdl2]: https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer
|
||||||
|
[libusb]: https://en.wikipedia.org/wiki/Libusb
|
||||||
|
|
||||||
Make sure you [enabled USB debugging][enable-adb] on your device(s).
|
## Build and install
|
||||||
|
|
||||||
[enable-adb]: https://developer.android.com/studio/debug/dev-options#enable
|
### System-specific steps
|
||||||
|
|
||||||
On some devices (especially Xiaomi), you might get the following error:
|
#### Linux
|
||||||
|
|
||||||
```
|
Install the required packages from your package manager.
|
||||||
java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission.
|
|
||||||
|
##### Debian/Ubuntu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# runtime dependencies
|
||||||
|
sudo apt install ffmpeg libsdl2-2.0.0 libusb-1.0-0
|
||||||
|
|
||||||
|
# client build dependencies
|
||||||
|
sudo apt install make gcc pkg-config meson \
|
||||||
|
libavcodec-dev libavformat-dev libavutil-dev \
|
||||||
|
libsdl2-dev libusb-1.0-0-dev
|
||||||
|
|
||||||
|
# server build dependencies
|
||||||
|
sudo apt install openjdk-8-jdk
|
||||||
```
|
```
|
||||||
|
|
||||||
In that case, you need to enable [an additional option][control] `USB debugging
|
##### Fedora
|
||||||
(Security Settings)` (this is an item different from `USB debugging`) to control
|
|
||||||
it using a keyboard and mouse. Rebooting the device is necessary once this
|
|
||||||
option is set.
|
|
||||||
|
|
||||||
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
|
```bash
|
||||||
|
# enable RPM fusion free
|
||||||
|
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
|
||||||
|
|
||||||
Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md).
|
# client build dependencies
|
||||||
|
sudo dnf install SDL2-devel ffms2-devel meson gcc make
|
||||||
|
|
||||||
|
# server build dependencies
|
||||||
|
sudo dnf install java
|
||||||
|
```
|
||||||
|
|
||||||
|
##### Arch Linux
|
||||||
|
|
||||||
|
Two [AUR] packages have been created by users:
|
||||||
|
|
||||||
|
- [`scrcpy`](https://aur.archlinux.org/packages/scrcpy/)
|
||||||
|
- [`scrcpy-prebuiltserver`](https://aur.archlinux.org/packages/scrcpy-prebuiltserver/)
|
||||||
|
|
||||||
|
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||||
|
|
||||||
|
|
||||||
## Get the app
|
#### Windows
|
||||||
|
|
||||||
- [Linux](doc/linux.md)
|
For Windows, for simplicity, a prebuilt archive with all the dependencies
|
||||||
- [Windows](doc/windows.md) (read [how to run](doc/windows.md#run))
|
(including `adb`) is available:
|
||||||
- [macOS](doc/macos.md)
|
|
||||||
|
- [`scrcpy-windows-with-deps-v1.1.zip`][direct-windows-with-deps].
|
||||||
|
_(SHA-256: 27eb36c15937601d1062c1dc0b45faae0e06fefea2019aadeb4fa7f76a07bb4c)_
|
||||||
|
|
||||||
|
[direct-windows-with-deps]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-windows-with-deps-v1.1.zip
|
||||||
|
|
||||||
|
_(It's just a portable version including _dll_ copied from MSYS2.)_
|
||||||
|
|
||||||
|
Instead, you may want to build it manually. You need [MSYS2] to build the
|
||||||
|
project. From an MSYS2 terminal, install the required packages:
|
||||||
|
|
||||||
|
[MSYS2]: http://www.msys2.org/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# runtime dependencies
|
||||||
|
pacman -S mingw-w64-x86_64-SDL2 \
|
||||||
|
mingw-w64-x86_64-ffmpeg
|
||||||
|
|
||||||
|
# client build dependencies
|
||||||
|
pacman -S mingw-w64-x86_64-make \
|
||||||
|
mingw-w64-x86_64-gcc \
|
||||||
|
mingw-w64-x86_64-pkg-config \
|
||||||
|
mingw-w64-x86_64-meson \
|
||||||
|
mingw-w64-x86_64-libusb
|
||||||
|
```
|
||||||
|
|
||||||
|
For a 32 bits version, replace `x86_64` by `i686`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# runtime dependencies
|
||||||
|
pacman -S mingw-w64-i686-SDL2 \
|
||||||
|
mingw-w64-i686-ffmpeg
|
||||||
|
|
||||||
|
# client build dependencies
|
||||||
|
pacman -S mingw-w64-i686-make \
|
||||||
|
mingw-w64-i686-gcc \
|
||||||
|
mingw-w64-i686-pkg-config \
|
||||||
|
mingw-w64-i686-meson
|
||||||
|
```
|
||||||
|
|
||||||
|
Java (>= 7) is not available in MSYS2, so if you plan to build the server,
|
||||||
|
install it manually and make it available from the `PATH`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Mac OS
|
||||||
|
|
||||||
|
The application is available in [Homebrew]. Just install it:
|
||||||
|
|
||||||
|
[Homebrew]: https://brew.sh/
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead, you may want to build it manually. Install the packages:
|
||||||
|
|
||||||
|
|
||||||
## Must-know tips
|
```bash
|
||||||
|
# runtime dependencies
|
||||||
|
brew install sdl2 ffmpeg
|
||||||
|
|
||||||
- [Reducing resolution](doc/video.md#size) may greatly improve performance
|
# client build dependencies
|
||||||
(`scrcpy -m1024`)
|
brew install pkg-config meson libusb
|
||||||
- [_Right-click_](doc/mouse.md#mouse-bindings) triggers `BACK`
|
```
|
||||||
- [_Middle-click_](doc/mouse.md#mouse-bindings) triggers `HOME`
|
|
||||||
- <kbd>Alt</kbd>+<kbd>f</kbd> toggles [fullscreen](doc/window.md#fullscreen)
|
Additionally, if you want to build the server, install Java 8 from Caskroom, and
|
||||||
- There are many other [shortcuts](doc/shortcuts.md)
|
make it avaliable from the `PATH`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew tap caskroom/versions
|
||||||
|
brew cask install java8
|
||||||
|
export JAVA_HOME="$(/usr/libexec/java_home --version 1.8)"
|
||||||
|
export PATH="$JAVA_HOME/bin:$PATH"
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker
|
||||||
|
|
||||||
|
See [pierlon/scrcpy-docker](https://github.com/pierlon/scrcpy-docker).
|
||||||
|
|
||||||
|
### Common steps
|
||||||
|
|
||||||
|
Install the [Android SDK] (_Android Studio_), and set `ANDROID_HOME` to
|
||||||
|
its directory. For example:
|
||||||
|
|
||||||
|
[Android SDK]: https://developer.android.com/studio/index.html
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export ANDROID_HOME=~/android/sdk
|
||||||
|
```
|
||||||
|
|
||||||
|
Clone the project:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Genymobile/scrcpy
|
||||||
|
cd scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
|
Then, build:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
meson x --buildtype release --strip -Db_lto=true
|
||||||
|
cd x
|
||||||
|
ninja
|
||||||
|
```
|
||||||
|
|
||||||
|
You can test it from here:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ninja run
|
||||||
|
```
|
||||||
|
|
||||||
|
Or you can install it on the system:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ninja install # without sudo on Windows
|
||||||
|
```
|
||||||
|
|
||||||
|
This installs two files:
|
||||||
|
|
||||||
|
- `/usr/local/bin/scrcpy`
|
||||||
|
- `/usr/local/share/scrcpy/scrcpy-server.jar`
|
||||||
|
|
||||||
|
Just remove them to "uninstall" the application.
|
||||||
|
|
||||||
|
|
||||||
## Usage examples
|
#### Prebuilt server
|
||||||
|
|
||||||
There are a lot of options, [documented](#user-documentation) in separate pages.
|
Since the server binary, that will be pushed to the Android device, does not
|
||||||
Here are just some common examples.
|
depend on your system and architecture, you may want to use the prebuilt binary
|
||||||
|
instead:
|
||||||
|
|
||||||
- Capture the screen in H.265 (better quality), limit the size to 1920, limit
|
- [`scrcpy-server-v1.1.jar`][direct-scrcpy-server].
|
||||||
the frame rate to 60fps, disable audio, and control the device by simulating
|
_(SHA-256: 14826512bf38447ec94adf3b531676ce038d19e7e06757fb4e537882b17e77b3)_
|
||||||
a physical keyboard:
|
|
||||||
|
|
||||||
```bash
|
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.1/scrcpy-server-v1.1.jar
|
||||||
scrcpy --video-codec=h265 --max-size=1920 --max-fps=60 --no-audio --keyboard=uhid
|
|
||||||
scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
- Start VLC in a new virtual display (separate from the device display):
|
In that case, the build does not require Java or the Android SDK.
|
||||||
|
|
||||||
```bash
|
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||||
scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc
|
configuration:
|
||||||
```
|
|
||||||
|
|
||||||
- Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4
|
```bash
|
||||||
file:
|
meson x --buildtype release --strip -Db_lto=true \
|
||||||
|
-Dprebuilt_server=/path/to/scrcpy-server.jar
|
||||||
|
cd x
|
||||||
|
ninja
|
||||||
|
sudo ninja install
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
## Run
|
||||||
scrcpy --video-source=camera --video-codec=h265 --camera-size=1920x1080 --record=file.mp4
|
|
||||||
```
|
|
||||||
|
|
||||||
- Capture the device front camera and expose it as a webcam on the computer (on
|
_At runtime, `adb` must be accessible from your `PATH`._
|
||||||
Linux):
|
|
||||||
|
|
||||||
```bash
|
If everything is ok, just plug an Android device, and execute:
|
||||||
scrcpy --video-source=camera --camera-size=1920x1080 --camera-facing=front --v4l2-sink=/dev/video2 --no-playback
|
|
||||||
```
|
|
||||||
|
|
||||||
- Control the device without mirroring by simulating a physical keyboard and
|
```bash
|
||||||
mouse (USB debugging not required):
|
scrcpy
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
It accepts command-line arguments, listed by:
|
||||||
scrcpy --otg
|
|
||||||
```
|
|
||||||
|
|
||||||
- Control the device using gamepad controllers plugged into the computer:
|
```bash
|
||||||
|
scrcpy --help
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
For example, to decrease video bitrate to 2Mbps (default is 8Mbps):
|
||||||
scrcpy --gamepad=uhid
|
|
||||||
scrcpy -G # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
## User documentation
|
```bash
|
||||||
|
scrcpy -b 2M
|
||||||
|
```
|
||||||
|
|
||||||
The application provides a lot of features and configuration options. They are
|
To limit the video dimensions (e.g. if the device is 2540×1440, but the host
|
||||||
documented in the following pages:
|
screen is smaller, or cannot decode such a high definition):
|
||||||
|
|
||||||
- [Connection](doc/connection.md)
|
```bash
|
||||||
- [Video](doc/video.md)
|
scrcpy -m 1024
|
||||||
- [Audio](doc/audio.md)
|
```
|
||||||
- [Control](doc/control.md)
|
|
||||||
- [Keyboard](doc/keyboard.md)
|
If several devices are listed in `adb devices`, you must specify the _serial_:
|
||||||
- [Mouse](doc/mouse.md)
|
|
||||||
- [Gamepad](doc/gamepad.md)
|
```bash
|
||||||
- [Device](doc/device.md)
|
scrcpy -s 0123456789abcdef
|
||||||
- [Window](doc/window.md)
|
```
|
||||||
- [Recording](doc/recording.md)
|
|
||||||
- [Virtual display](doc/virtual_display.md)
|
To show physical touches while scrcpy is running:
|
||||||
- [Tunnels](doc/tunnels.md)
|
|
||||||
- [OTG](doc/otg.md)
|
```bash
|
||||||
- [Camera](doc/camera.md)
|
scrcpy -t
|
||||||
- [Video4Linux](doc/v4l2.md)
|
```
|
||||||
- [Shortcuts](doc/shortcuts.md)
|
|
||||||
|
To enable audio forwarding:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy -a
|
||||||
|
```
|
||||||
|
|
||||||
|
To run without installing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./run x [options]
|
||||||
|
```
|
||||||
|
|
||||||
|
(where `x` is your build directory).
|
||||||
|
|
||||||
|
|
||||||
## Resources
|
## Shortcuts
|
||||||
|
|
||||||
- [FAQ](FAQ.md)
|
| Action | Shortcut |
|
||||||
- [Translations][wiki] (not necessarily up to date)
|
| -------------------------------------- |:---------------------------- |
|
||||||
- [Build instructions](doc/build.md)
|
| switch fullscreen mode | `Ctrl`+`f` |
|
||||||
- [Developers](doc/develop.md)
|
| resize window to 1:1 (pixel-perfect) | `Ctrl`+`g` |
|
||||||
|
| resize window to remove black borders | `Ctrl`+`x` \| _Double-click¹_ |
|
||||||
|
| click on `HOME` | `Ctrl`+`h` \| _Middle-click_ |
|
||||||
|
| click on `BACK` | `Ctrl`+`b` \| _Right-click²_ |
|
||||||
|
| click on `APP_SWITCH` | `Ctrl`+`s` |
|
||||||
|
| click on `MENU` | `Ctrl`+`m` |
|
||||||
|
| click on `VOLUME_UP` | `Ctrl`+`↑` _(up)_ |
|
||||||
|
| click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ |
|
||||||
|
| click on `POWER` | `Ctrl`+`p` |
|
||||||
|
| turn screen on | _Right-click²_ |
|
||||||
|
| paste computer clipboard to device | `Ctrl`+`v` |
|
||||||
|
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
|
||||||
|
|
||||||
[wiki]: https://github.com/Genymobile/scrcpy/wiki
|
_¹Double-click on black borders to remove them._
|
||||||
|
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||||
|
|
||||||
|
|
||||||
## Articles
|
## Why _scrcpy_?
|
||||||
|
|
||||||
- [Introducing scrcpy][article-intro]
|
A colleague challenged me to find a name as unpronounceable as [gnirehtet].
|
||||||
- [Scrcpy now works wirelessly][article-tcpip]
|
|
||||||
- [Scrcpy 2.0, with audio][article-scrcpy2]
|
|
||||||
|
|
||||||
[article-intro]: https://blog.rom1v.com/2018/03/introducing-scrcpy/
|
[`strcpy`] copies a **str**ing; `scrcpy` copies a **scr**een.
|
||||||
[article-tcpip]: https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/
|
|
||||||
[article-scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/
|
|
||||||
|
|
||||||
## Contact
|
[gnirehtet]: https://github.com/Genymobile/gnirehtet
|
||||||
|
[`strcpy`]: http://man7.org/linux/man-pages/man3/strcpy.3.html
|
||||||
You can open an [issue] for bug reports, feature requests or general questions.
|
|
||||||
|
|
||||||
For bug reports, please read the [FAQ](FAQ.md) first, you might find a solution
|
|
||||||
to your problem immediately.
|
|
||||||
|
|
||||||
[issue]: https://github.com/Genymobile/scrcpy/issues
|
|
||||||
|
|
||||||
You can also use:
|
|
||||||
|
|
||||||
- Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy)
|
|
||||||
- BlueSky: [`@scrcpy.bsky.social`](https://bsky.app/profile/scrcpy.bsky.social)
|
|
||||||
- Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app)
|
|
||||||
|
|
||||||
|
|
||||||
## Donate
|
## Common issues
|
||||||
|
|
||||||
I'm [@rom1v](https://github.com/rom1v), the author and maintainer of _scrcpy_.
|
See the [FAQ](FAQ.md).
|
||||||
|
|
||||||
If you appreciate this application, you can [support my open source
|
|
||||||
work][donate]:
|
|
||||||
- [GitHub Sponsors](https://github.com/sponsors/rom1v)
|
|
||||||
- [Liberapay](https://liberapay.com/rom1v/)
|
|
||||||
- [PayPal](https://paypal.me/rom2v)
|
|
||||||
|
|
||||||
[donate]: https://blog.rom1v.com/about/#support-my-open-source-work
|
## Developers
|
||||||
|
|
||||||
|
Read the [developers page].
|
||||||
|
|
||||||
|
[developers page]: DEVELOP.md
|
||||||
|
|
||||||
|
|
||||||
## Licence
|
## Licence
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
Copyright (C) 2018 Genymobile
|
||||||
Copyright (C) 2018-2025 Romain Vimont
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -223,3 +350,8 @@ work][donate]:
|
|||||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
See the License for the specific language governing permissions and
|
See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
|
|
||||||
|
## Articles
|
||||||
|
|
||||||
|
- [Introducing scrcpy](https://blog.rom1v.com/2018/03/introducing-scrcpy/)
|
||||||
|
- [Scrcpy now works wirelessly](https://www.genymotion.com/blog/open-source-project-scrcpy-now-works-wirelessly/)
|
||||||
|
@ -1,221 +0,0 @@
|
|||||||
_scrcpy() {
|
|
||||||
local cur prev words cword
|
|
||||||
local opts="
|
|
||||||
--always-on-top
|
|
||||||
--angle
|
|
||||||
--audio-bit-rate=
|
|
||||||
--audio-buffer=
|
|
||||||
--audio-codec=
|
|
||||||
--audio-codec-options=
|
|
||||||
--audio-dup
|
|
||||||
--audio-encoder=
|
|
||||||
--audio-source=
|
|
||||||
--audio-output-buffer=
|
|
||||||
-b --video-bit-rate=
|
|
||||||
--camera-ar=
|
|
||||||
--camera-id=
|
|
||||||
--camera-facing=
|
|
||||||
--camera-fps=
|
|
||||||
--camera-high-speed
|
|
||||||
--camera-size=
|
|
||||||
--capture-orientation=
|
|
||||||
--crop=
|
|
||||||
-d --select-usb
|
|
||||||
--disable-screensaver
|
|
||||||
--display-id=
|
|
||||||
--display-orientation=
|
|
||||||
-e --select-tcpip
|
|
||||||
-f --fullscreen
|
|
||||||
--force-adb-forward
|
|
||||||
-G
|
|
||||||
--gamepad=
|
|
||||||
-h --help
|
|
||||||
-K
|
|
||||||
--keyboard=
|
|
||||||
--kill-adb-on-close
|
|
||||||
--legacy-paste
|
|
||||||
--list-apps
|
|
||||||
--list-camera-sizes
|
|
||||||
--list-cameras
|
|
||||||
--list-displays
|
|
||||||
--list-encoders
|
|
||||||
-m --max-size=
|
|
||||||
-M
|
|
||||||
--max-fps=
|
|
||||||
--mouse=
|
|
||||||
--mouse-bind=
|
|
||||||
-n --no-control
|
|
||||||
-N --no-playback
|
|
||||||
--new-display
|
|
||||||
--new-display=
|
|
||||||
--no-audio
|
|
||||||
--no-audio-playback
|
|
||||||
--no-cleanup
|
|
||||||
--no-clipboard-autosync
|
|
||||||
--no-downsize-on-error
|
|
||||||
--no-key-repeat
|
|
||||||
--no-mipmaps
|
|
||||||
--no-mouse-hover
|
|
||||||
--no-power-on
|
|
||||||
--no-vd-destroy-content
|
|
||||||
--no-vd-system-decorations
|
|
||||||
--no-video
|
|
||||||
--no-video-playback
|
|
||||||
--orientation=
|
|
||||||
--otg
|
|
||||||
-p --port=
|
|
||||||
--pause-on-exit
|
|
||||||
--pause-on-exit=
|
|
||||||
--power-off-on-close
|
|
||||||
--prefer-text
|
|
||||||
--print-fps
|
|
||||||
--push-target=
|
|
||||||
-r --record=
|
|
||||||
--raw-key-events
|
|
||||||
--record-format=
|
|
||||||
--record-orientation=
|
|
||||||
--render-driver=
|
|
||||||
--require-audio
|
|
||||||
--rotation=
|
|
||||||
-s --serial=
|
|
||||||
-S --turn-screen-off
|
|
||||||
--screen-off-timeout=
|
|
||||||
--shortcut-mod=
|
|
||||||
--start-app=
|
|
||||||
-t --show-touches
|
|
||||||
--tcpip
|
|
||||||
--tcpip=
|
|
||||||
--time-limit=
|
|
||||||
--tunnel-host=
|
|
||||||
--tunnel-port=
|
|
||||||
--v4l2-buffer=
|
|
||||||
--v4l2-sink=
|
|
||||||
-v --version
|
|
||||||
-V --verbosity=
|
|
||||||
--video-buffer=
|
|
||||||
--video-codec=
|
|
||||||
--video-codec-options=
|
|
||||||
--video-encoder=
|
|
||||||
--video-source=
|
|
||||||
-w --stay-awake
|
|
||||||
--window-borderless
|
|
||||||
--window-title=
|
|
||||||
--window-x=
|
|
||||||
--window-y=
|
|
||||||
--window-width=
|
|
||||||
--window-height="
|
|
||||||
|
|
||||||
_init_completion -s || return
|
|
||||||
|
|
||||||
case "$prev" in
|
|
||||||
--video-codec)
|
|
||||||
COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--audio-codec)
|
|
||||||
COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--video-source)
|
|
||||||
COMPREPLY=($(compgen -W 'display camera' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--audio-source)
|
|
||||||
COMPREPLY=($(compgen -W 'output mic playback' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--camera-facing)
|
|
||||||
COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--keyboard)
|
|
||||||
COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--mouse)
|
|
||||||
COMPREPLY=($(compgen -W 'disabled sdk uhid aoa' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--gamepad)
|
|
||||||
COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--capture-orientation)
|
|
||||||
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--orientation|--display-orientation)
|
|
||||||
COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--record-orientation)
|
|
||||||
COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--pause-on-exit)
|
|
||||||
COMPREPLY=($(compgen -W 'true false if-error' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
-r|--record)
|
|
||||||
COMPREPLY=($(compgen -f -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--record-format)
|
|
||||||
COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac wav' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--render-driver)
|
|
||||||
COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--shortcut-mod)
|
|
||||||
# Only auto-complete a single key
|
|
||||||
COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
-V|--verbosity)
|
|
||||||
COMPREPLY=($(compgen -W 'verbose debug info warn error' -- "$cur"))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
-s|--serial)
|
|
||||||
# Use 'adb devices' to list serial numbers
|
|
||||||
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
--audio-bit-rate \
|
|
||||||
|--audio-buffer \
|
|
||||||
|-b|--video-bit-rate \
|
|
||||||
|--audio-codec-options \
|
|
||||||
|--audio-encoder \
|
|
||||||
|--audio-output-buffer \
|
|
||||||
|--camera-ar \
|
|
||||||
|--camera-id \
|
|
||||||
|--camera-fps \
|
|
||||||
|--camera-size \
|
|
||||||
|--crop \
|
|
||||||
|--display-id \
|
|
||||||
|--max-fps \
|
|
||||||
|-m|--max-size \
|
|
||||||
|--new-display \
|
|
||||||
|-p|--port \
|
|
||||||
|--push-target \
|
|
||||||
|--rotation \
|
|
||||||
|--tunnel-host \
|
|
||||||
|--tunnel-port \
|
|
||||||
|--v4l2-buffer \
|
|
||||||
|--v4l2-sink \
|
|
||||||
|--video-buffer \
|
|
||||||
|--video-codec-options \
|
|
||||||
|--video-encoder \
|
|
||||||
|--tcpip \
|
|
||||||
|--window-*)
|
|
||||||
# Option accepting an argument, but nothing to auto-complete
|
|
||||||
return
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
COMPREPLY=($(compgen -W "$opts" -- "$cur"))
|
|
||||||
[[ $COMPREPLY == *= ]] && compopt -o nospace
|
|
||||||
}
|
|
||||||
|
|
||||||
complete -F _scrcpy scrcpy
|
|
Binary file not shown.
Before Width: | Height: | Size: 8.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.4 KiB |
@ -1,16 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" version="1.1">
|
|
||||||
<path style="opacity:0.2" d="m 16.846877,12 c -1.116351,0 -2.227419,0.912229 -2.015075,2 l 3.122973,16 -5.596557,11.109375 C 11.959876,41.871734 11.885244,42.336988 12.177176,43 c 0.278672,0.632897 0.998812,1 1.747448,1 H 24 34.075375 c 0.748637,0 1.468777,-0.367103 1.747448,-1 0.291932,-0.663012 0.217302,-1.128266 -0.181041,-1.890625 L 30.045225,30 33.168198,14 c 0.212344,-1.087771 -0.898724,-2 -2.015075,-2 H 24 Z"/>
|
|
||||||
<path style="fill:#cccccc" d="m 16.846877,11 c -1.116351,0 -2.227419,0.912229 -2.015075,2 l 3.122973,16 -5.596557,11.109375 C 11.959876,40.871734 11.885244,41.336988 12.177176,42 c 0.278672,0.632897 0.998812,1 1.747448,1 H 24 34.075375 c 0.748637,0 1.468777,-0.367103 1.747448,-1 0.291932,-0.663012 0.217302,-1.128266 -0.181041,-1.890625 L 30.045225,29 33.168198,13 c 0.212344,-1.087771 -0.898724,-2 -2.015075,-2 H 24 Z"/>
|
|
||||||
<rect style="opacity:0.2" width="40" height="32" x="4" y="6" rx="2" ry="2"/>
|
|
||||||
<path style="fill:#e4e4e4" d="m 4,33 v 2 c 0,1.108 0.892,2 2,2 h 36 c 1.108,0 2,-0.892 2,-2 v -2 z"/>
|
|
||||||
<path style="opacity:0.1" d="m 11.494141,15 a 1.5,1.5 0 0 0 -0.832032,0.255859 1.5,1.5 0 0 0 -0.40625,2.082032 l 3.13086,4.654297 C 10.404945,24.606192 8.4012371,28.299159 8.0019531,32.460938 7.9284599,34.000879 9.5546875,34 9.5546875,34 H 38.40625 c 0,0 1.672856,-3.38e-4 1.591797,-1.617188 -0.416529,-4.131451 -2.415618,-7.796833 -5.380859,-10.394531 l 3.126953,-4.65039 a 1.5,1.5 0 0 0 -0.40625,-2.082032 1.5,1.5 0 0 0 -1.125,-0.228515 1.5,1.5 0 0 0 -0.957032,0.634765 l -3.072265,4.566407 C 29.78649,18.814887 26.990024,18 24.001953,18 c -2.989385,0 -5.786177,0.815488 -8.183594,2.230469 l -3.074218,-4.56836 A 1.5,1.5 0 0 0 11.787109,15.027344 1.5,1.5 0 0 0 11.494141,15 Z"/>
|
|
||||||
<path style="fill:#077063" d="M 6,5 C 4.892,5 4,5.892 4,7 V 33 H 44 V 7 C 44,5.892 43.108,5 42,5 Z"/>
|
|
||||||
<path style="opacity:0.1;fill:#ffffff" d="M 6,5 C 4.892,5 4,5.892 4,7 V 8 C 4,6.892 4.892,6 6,6 h 36 c 1.108,0 2,0.892 2,2 V 7 C 44,5.892 43.108,5 42,5 Z"/>
|
|
||||||
<path style="fill:none;stroke:#30dd81;stroke-width:3;stroke-linecap:round" d="M 15.1998,21.000026 11.5,15.5"/>
|
|
||||||
<path style="fill:none;stroke:#30dd81;stroke-width:3;stroke-linecap:round" d="M 32.799764,21.000026 36.5,15.5"/>
|
|
||||||
<path style="fill:#30dd81" d="m 24.002386,17.000034 c -8.355868,0 -15.2214979,6.346843 -15.9999669,14.460906 C 7.9289259,33.000882 9.5544999,33 9.5544999,33 H 38.406003 c 0,0 1.672201,-3.35e-4 1.591142,-1.617185 C 39.182807,23.305596 32.331836,17.000034 24.002386,17.000034 Z"/>
|
|
||||||
<path style="opacity:0.2" d="m 16,25 a 1.9999959,1.9999959 0 0 0 -2,2 1.9999959,1.9999959 0 0 0 2,2 1.9999959,1.9999959 0 0 0 2,-2 1.9999959,1.9999959 0 0 0 -2,-2 z m 16,0 a 1.9999959,1.9999959 0 0 0 -2,2 1.9999959,1.9999959 0 0 0 2,2 1.9999959,1.9999959 0 0 0 2,-2 1.9999959,1.9999959 0 0 0 -2,-2 z"/>
|
|
||||||
<path style="fill:#ffffff" d="M 15.999996,24.000008 A 1.9999959,1.9999959 0 0 1 17.999992,26.000004 1.9999959,1.9999959 0 0 1 15.999996,28 1.9999959,1.9999959 0 0 1 14,26.000004 1.9999959,1.9999959 0 0 1 15.999996,24.000008 Z"/>
|
|
||||||
<path style="fill:#ffffff" d="M 31.999996,24.000008 A 1.9999959,1.9999959 0 0 1 33.999991,26.000004 1.9999959,1.9999959 0 0 1 31.999996,28 1.9999959,1.9999959 0 0 1 30,26.000004 1.9999959,1.9999959 0 0 1 31.999996,24.000008 Z"/>
|
|
||||||
<path style="fill:#ffffff;opacity:0.2" d="M 11.494141 14 A 1.5 1.5 0 0 0 10.662109 14.255859 A 1.5 1.5 0 0 0 10.115234 16.001953 A 1.5 1.5 0 0 1 10.662109 15.255859 A 1.5 1.5 0 0 1 11.494141 15 A 1.5 1.5 0 0 1 11.787109 15.027344 A 1.5 1.5 0 0 1 12.744141 15.662109 L 15.818359 20.230469 C 18.215776 18.815488 21.012568 18 24.001953 18 C 26.990024 18 29.78649 18.814887 32.183594 20.228516 L 35.255859 15.662109 A 1.5 1.5 0 0 1 36.212891 15.027344 A 1.5 1.5 0 0 1 37.337891 15.255859 A 1.5 1.5 0 0 1 37.910156 16.001953 A 1.5 1.5 0 0 0 37.337891 14.255859 A 1.5 1.5 0 0 0 36.212891 14.027344 A 1.5 1.5 0 0 0 35.255859 14.662109 L 32.183594 19.228516 C 29.78649 17.814887 26.990024 17 24.001953 17 C 21.012568 17 18.215776 17.815488 15.818359 19.230469 L 12.744141 14.662109 A 1.5 1.5 0 0 0 11.787109 14.027344 A 1.5 1.5 0 0 0 11.494141 14 z M 35.033203 21.369141 L 34.617188 21.988281 C 37.477056 24.493668 39.433905 27.993356 39.943359 31.945312 C 39.986866 31.783283 40.008864 31.598575 39.998047 31.382812 C 39.601372 27.448291 37.768055 23.938648 35.033203 21.369141 z M 12.970703 21.373047 C 10.220358 23.959215 8.3822757 27.496796 8.0019531 31.460938 C 7.9920657 31.668114 8.0150508 31.844846 8.0585938 32 C 8.5570234 28.027243 10.515755 24.509049 13.386719 21.992188 L 12.970703 21.373047 z"/>
|
|
||||||
</svg>
|
|
Before Width: | Height: | Size: 4.5 KiB |
@ -1 +0,0 @@
|
|||||||
@cmd
|
|
@ -1,2 +0,0 @@
|
|||||||
@echo off
|
|
||||||
scrcpy.exe --pause-on-exit=if-error %*
|
|
@ -1,13 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Name=scrcpy (console)
|
|
||||||
GenericName=Android Remote Control
|
|
||||||
Comment=Display and control your Android device
|
|
||||||
# For some users, the PATH or ADB environment variables are set from the shell
|
|
||||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
|
||||||
# environment correctly initialized.
|
|
||||||
Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'"
|
|
||||||
Icon=scrcpy
|
|
||||||
Terminal=true
|
|
||||||
Type=Application
|
|
||||||
Categories=Utility;RemoteAccess;
|
|
||||||
StartupNotify=false
|
|
@ -1,7 +0,0 @@
|
|||||||
strCommand = "cmd /c scrcpy.exe"
|
|
||||||
|
|
||||||
For Each Arg In WScript.Arguments
|
|
||||||
strCommand = strCommand & " """ & replace(Arg, """", """""""""") & """"
|
|
||||||
Next
|
|
||||||
|
|
||||||
CreateObject("Wscript.Shell").Run strCommand, 0, false
|
|
@ -1,13 +0,0 @@
|
|||||||
[Desktop Entry]
|
|
||||||
Name=scrcpy
|
|
||||||
GenericName=Android Remote Control
|
|
||||||
Comment=Display and control your Android device
|
|
||||||
# For some users, the PATH or ADB environment variables are set from the shell
|
|
||||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
|
||||||
# environment correctly initialized.
|
|
||||||
Exec=/bin/sh -c "\\$SHELL -i -c scrcpy"
|
|
||||||
Icon=scrcpy
|
|
||||||
Terminal=false
|
|
||||||
Type=Application
|
|
||||||
Categories=Utility;RemoteAccess;
|
|
||||||
StartupNotify=false
|
|
@ -1,112 +0,0 @@
|
|||||||
#compdef -N scrcpy -N scrcpy.exe
|
|
||||||
#
|
|
||||||
# name: scrcpy
|
|
||||||
# auth: hltdev [hltdev8642@gmail.com]
|
|
||||||
# desc: completion file for scrcpy (all OSes)
|
|
||||||
#
|
|
||||||
|
|
||||||
local arguments
|
|
||||||
|
|
||||||
arguments=(
|
|
||||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
|
||||||
'--angle=[Rotate the video content by a custom angle, in degrees]'
|
|
||||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
|
||||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
|
||||||
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
|
|
||||||
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
|
||||||
'--audio-dup=[Duplicate audio]'
|
|
||||||
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
|
||||||
'--audio-source=[Select the audio source]:source:(output mic playback)'
|
|
||||||
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
|
|
||||||
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
|
|
||||||
'--camera-ar=[Select the camera size by its aspect ratio]'
|
|
||||||
'--camera-high-speed=[Enable high-speed camera capture mode]'
|
|
||||||
'--camera-id=[Specify the camera id to mirror]'
|
|
||||||
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
|
|
||||||
'--camera-fps=[Specify the camera capture frame rate]'
|
|
||||||
'--camera-size=[Specify an explicit camera capture size]'
|
|
||||||
'--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)'
|
|
||||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
|
||||||
{-d,--select-usb}'[Use USB device]'
|
|
||||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
|
||||||
'--display-id=[Specify the display id to mirror]'
|
|
||||||
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
|
||||||
{-e,--select-tcpip}'[Use TCP/IP device]'
|
|
||||||
{-f,--fullscreen}'[Start in fullscreen]'
|
|
||||||
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
|
|
||||||
'-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]'
|
|
||||||
'--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)'
|
|
||||||
{-h,--help}'[Print the help]'
|
|
||||||
'-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]'
|
|
||||||
'--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)'
|
|
||||||
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
|
|
||||||
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
|
|
||||||
'--list-apps[List Android apps installed on the device]'
|
|
||||||
'--list-camera-sizes[List the valid camera capture sizes]'
|
|
||||||
'--list-cameras[List cameras available on the device]'
|
|
||||||
'--list-displays[List displays available on the device]'
|
|
||||||
'--list-encoders[List video and audio encoders available on the device]'
|
|
||||||
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
|
||||||
'-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]'
|
|
||||||
'--max-fps=[Limit the frame rate of screen capture]'
|
|
||||||
'--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)'
|
|
||||||
'--mouse-bind=[Configure bindings of secondary clicks]'
|
|
||||||
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
|
|
||||||
{-N,--no-playback}'[Disable video and audio playback]'
|
|
||||||
'--new-display=[Create a new display]'
|
|
||||||
'--no-audio[Disable audio forwarding]'
|
|
||||||
'--no-audio-playback[Disable audio playback]'
|
|
||||||
'--no-cleanup[Disable device cleanup actions on exit]'
|
|
||||||
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
|
|
||||||
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
|
|
||||||
'--no-key-repeat[Do not forward repeated key events when a key is held down]'
|
|
||||||
'--no-mipmaps[Disable the generation of mipmaps]'
|
|
||||||
'--no-mouse-hover[Do not forward mouse hover events]'
|
|
||||||
'--no-power-on[Do not power on the device on start]'
|
|
||||||
'--no-vd-destroy-content[Disable virtual display "destroy content on removal" flag]'
|
|
||||||
'--no-vd-system-decorations[Disable virtual display system decorations flag]'
|
|
||||||
'--no-video[Disable video forwarding]'
|
|
||||||
'--no-video-playback[Disable video playback]'
|
|
||||||
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
|
|
||||||
'--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]'
|
|
||||||
{-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]'
|
|
||||||
'--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)'
|
|
||||||
'--power-off-on-close[Turn the device screen off when closing scrcpy]'
|
|
||||||
'--prefer-text[Inject alpha characters and space as text events instead of key events]'
|
|
||||||
'--print-fps[Start FPS counter, to print frame logs to the console]'
|
|
||||||
'--push-target=[Set the target directory for pushing files to the device by drag and drop]'
|
|
||||||
{-r,--record=}'[Record screen to file]:record file:_files'
|
|
||||||
'--raw-key-events[Inject key events for all input keys, and ignore text events]'
|
|
||||||
'--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)'
|
|
||||||
'--record-orientation=[Set the record orientation]:orientation values:(0 90 180 270)'
|
|
||||||
'--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)'
|
|
||||||
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
|
|
||||||
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
|
|
||||||
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
|
|
||||||
'--screen-off-timeout=[Set the screen off timeout in seconds]'
|
|
||||||
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
|
||||||
'--start-app=[Start an Android app]'
|
|
||||||
{-t,--show-touches}'[Show physical touches]'
|
|
||||||
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
|
|
||||||
'--time-limit=[Set the maximum mirroring time, in seconds]'
|
|
||||||
'--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]'
|
|
||||||
'--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]'
|
|
||||||
'--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]'
|
|
||||||
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
|
|
||||||
{-v,--version}'[Print the version of scrcpy]'
|
|
||||||
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
|
|
||||||
'--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]'
|
|
||||||
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
|
|
||||||
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
|
|
||||||
'--video-encoder=[Use a specific MediaCodec video encoder]'
|
|
||||||
'--video-source=[Select the video source]:source:(display camera)'
|
|
||||||
{-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]'
|
|
||||||
'--window-borderless[Disable window decorations \(display borderless window\)]'
|
|
||||||
'--window-title=[Set a custom window title]'
|
|
||||||
'--window-x=[Set the initial window horizontal position]'
|
|
||||||
'--window-y=[Set the initial window vertical position]'
|
|
||||||
'--window-width=[Set the initial window width]'
|
|
||||||
'--window-height=[Set the initial window height]'
|
|
||||||
)
|
|
||||||
|
|
||||||
_arguments -s $arguments
|
|
1
app/deps/.gitignore
vendored
1
app/deps/.gitignore
vendored
@ -1 +0,0 @@
|
|||||||
/work
|
|
@ -1,27 +0,0 @@
|
|||||||
This directory (app/deps/) contains:
|
|
||||||
|
|
||||||
*.sh : shell scripts to download and build dependencies
|
|
||||||
|
|
||||||
patches/ : patches to fix dependencies (used by scripts)
|
|
||||||
|
|
||||||
work/sources/ : downloaded tarballs and extracted folders
|
|
||||||
ffmpeg-6.1.1.tar.xz
|
|
||||||
ffmpeg-6.1.1/
|
|
||||||
libusb-1.0.27.tar.gz
|
|
||||||
libusb-1.0.27/
|
|
||||||
...
|
|
||||||
work/build/ : build dirs for each dependency/version/architecture
|
|
||||||
ffmpeg-6.1.1/win32/
|
|
||||||
ffmpeg-6.1.1/win64/
|
|
||||||
libusb-1.0.27/win32/
|
|
||||||
libusb-1.0.27/win64/
|
|
||||||
...
|
|
||||||
work/install/ : install dirs for each architexture
|
|
||||||
win32/bin/
|
|
||||||
win32/include/
|
|
||||||
win32/lib/
|
|
||||||
win32/share/
|
|
||||||
win64/bin/
|
|
||||||
win64/include/
|
|
||||||
win64/lib/
|
|
||||||
win64/share/
|
|
@ -1,29 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -ex
|
|
||||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
|
||||||
cd "$DEPS_DIR"
|
|
||||||
. common
|
|
||||||
|
|
||||||
VERSION=35.0.2
|
|
||||||
FILENAME=platform-tools_r$VERSION-linux.zip
|
|
||||||
PROJECT_DIR=platform-tools-$VERSION-linux
|
|
||||||
SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a
|
|
||||||
|
|
||||||
cd "$SOURCES_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$PROJECT_DIR" ]]
|
|
||||||
then
|
|
||||||
echo "$PWD/$PROJECT_DIR" found
|
|
||||||
else
|
|
||||||
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
|
|
||||||
mkdir -p "$PROJECT_DIR"
|
|
||||||
cd "$PROJECT_DIR"
|
|
||||||
ZIP_PREFIX=platform-tools
|
|
||||||
unzip "../$FILENAME" "$ZIP_PREFIX"/adb
|
|
||||||
mv "$ZIP_PREFIX"/* .
|
|
||||||
rmdir "$ZIP_PREFIX"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR/adb-linux"
|
|
||||||
cd "$INSTALL_DIR/adb-linux"
|
|
||||||
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-linux/"
|
|
@ -1,29 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -ex
|
|
||||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
|
||||||
cd "$DEPS_DIR"
|
|
||||||
. common
|
|
||||||
|
|
||||||
VERSION=35.0.2
|
|
||||||
FILENAME=platform-tools_r$VERSION-darwin.zip
|
|
||||||
PROJECT_DIR=platform-tools-$VERSION-darwin
|
|
||||||
SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78
|
|
||||||
|
|
||||||
cd "$SOURCES_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$PROJECT_DIR" ]]
|
|
||||||
then
|
|
||||||
echo "$PWD/$PROJECT_DIR" found
|
|
||||||
else
|
|
||||||
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
|
|
||||||
mkdir -p "$PROJECT_DIR"
|
|
||||||
cd "$PROJECT_DIR"
|
|
||||||
ZIP_PREFIX=platform-tools
|
|
||||||
unzip "../$FILENAME" "$ZIP_PREFIX"/adb
|
|
||||||
mv "$ZIP_PREFIX"/* .
|
|
||||||
rmdir "$ZIP_PREFIX"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR/adb-macos"
|
|
||||||
cd "$INSTALL_DIR/adb-macos"
|
|
||||||
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-macos/"
|
|
@ -1,32 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -ex
|
|
||||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
|
||||||
cd "$DEPS_DIR"
|
|
||||||
. common
|
|
||||||
|
|
||||||
VERSION=35.0.2
|
|
||||||
FILENAME=platform-tools_r$VERSION-win.zip
|
|
||||||
PROJECT_DIR=platform-tools-$VERSION-windows
|
|
||||||
SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9
|
|
||||||
|
|
||||||
cd "$SOURCES_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$PROJECT_DIR" ]]
|
|
||||||
then
|
|
||||||
echo "$PWD/$PROJECT_DIR" found
|
|
||||||
else
|
|
||||||
get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM"
|
|
||||||
mkdir -p "$PROJECT_DIR"
|
|
||||||
cd "$PROJECT_DIR"
|
|
||||||
ZIP_PREFIX=platform-tools
|
|
||||||
unzip "../$FILENAME" \
|
|
||||||
"$ZIP_PREFIX"/AdbWinApi.dll \
|
|
||||||
"$ZIP_PREFIX"/AdbWinUsbApi.dll \
|
|
||||||
"$ZIP_PREFIX"/adb.exe
|
|
||||||
mv "$ZIP_PREFIX"/* .
|
|
||||||
rmdir "$ZIP_PREFIX"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR/adb-windows"
|
|
||||||
cd "$INSTALL_DIR/adb-windows"
|
|
||||||
cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/"
|
|
@ -1,77 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
# This file is intended to be sourced by other scripts, not executed
|
|
||||||
|
|
||||||
process_args() {
|
|
||||||
if [[ $# != 3 ]]
|
|
||||||
then
|
|
||||||
# <host>: win32 or win64
|
|
||||||
# <build_type>: native or cross
|
|
||||||
# <link_type>: static or shared
|
|
||||||
echo "Syntax: $0 <host> <build_type> <link_type>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
HOST="$1"
|
|
||||||
BUILD_TYPE="$2" # native or cross
|
|
||||||
LINK_TYPE="$3" # static or shared
|
|
||||||
DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE"
|
|
||||||
|
|
||||||
if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]]
|
|
||||||
then
|
|
||||||
echo "Unsupported build type (expected native or cross): $BUILD_TYPE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$LINK_TYPE" != static && "$LINK_TYPE" != shared ]]
|
|
||||||
then
|
|
||||||
echo "Unsupported link type (expected static or shared): $LINK_TYPE" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$BUILD_TYPE" == cross ]]
|
|
||||||
then
|
|
||||||
if [[ "$HOST" = win32 ]]
|
|
||||||
then
|
|
||||||
HOST_TRIPLET=i686-w64-mingw32
|
|
||||||
elif [[ "$HOST" = win64 ]]
|
|
||||||
then
|
|
||||||
HOST_TRIPLET=x86_64-w64-mingw32
|
|
||||||
else
|
|
||||||
echo "Unsupported cross-build to host: $HOST" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
|
||||||
cd "$DEPS_DIR"
|
|
||||||
|
|
||||||
PATCHES_DIR="$PWD/patches"
|
|
||||||
|
|
||||||
WORK_DIR="$PWD/work"
|
|
||||||
SOURCES_DIR="$WORK_DIR/sources"
|
|
||||||
BUILD_DIR="$WORK_DIR/build"
|
|
||||||
INSTALL_DIR="$WORK_DIR/install"
|
|
||||||
|
|
||||||
mkdir -p "$INSTALL_DIR" "$SOURCES_DIR" "$WORK_DIR"
|
|
||||||
|
|
||||||
checksum() {
|
|
||||||
local file="$1"
|
|
||||||
local sum="$2"
|
|
||||||
echo "$file: verifying checksum..."
|
|
||||||
echo "$sum $file" | shasum -a256 -c
|
|
||||||
}
|
|
||||||
|
|
||||||
get_file() {
|
|
||||||
local url="$1"
|
|
||||||
local file="$2"
|
|
||||||
local sum="$3"
|
|
||||||
if [[ -f "$file" ]]
|
|
||||||
then
|
|
||||||
echo "$file: found"
|
|
||||||
else
|
|
||||||
echo "$file: not found, downloading..."
|
|
||||||
wget "$url" -O "$file"
|
|
||||||
fi
|
|
||||||
checksum "$file" "$sum"
|
|
||||||
}
|
|
@ -1,68 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -ex
|
|
||||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
|
||||||
cd "$DEPS_DIR"
|
|
||||||
. common
|
|
||||||
process_args "$@"
|
|
||||||
|
|
||||||
VERSION=1.5.0
|
|
||||||
FILENAME=dav1d-$VERSION.tar.gz
|
|
||||||
PROJECT_DIR=dav1d-$VERSION
|
|
||||||
SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b
|
|
||||||
|
|
||||||
cd "$SOURCES_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$PROJECT_DIR" ]]
|
|
||||||
then
|
|
||||||
echo "$PWD/$PROJECT_DIR" found
|
|
||||||
else
|
|
||||||
get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM"
|
|
||||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
|
|
||||||
cd "$BUILD_DIR/$PROJECT_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$DIRNAME" ]]
|
|
||||||
then
|
|
||||||
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
|
|
||||||
cd "$DIRNAME"
|
|
||||||
else
|
|
||||||
mkdir "$DIRNAME"
|
|
||||||
cd "$DIRNAME"
|
|
||||||
|
|
||||||
conf=(
|
|
||||||
--prefix="$INSTALL_DIR/$DIRNAME"
|
|
||||||
--libdir=lib
|
|
||||||
-Denable_tests=false
|
|
||||||
-Denable_tools=false
|
|
||||||
# Always build dav1d statically
|
|
||||||
--default-library=static
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ "$BUILD_TYPE" == cross ]]
|
|
||||||
then
|
|
||||||
case "$HOST" in
|
|
||||||
win32)
|
|
||||||
conf+=(
|
|
||||||
--cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/i686-w64-mingw32.meson"
|
|
||||||
)
|
|
||||||
;;
|
|
||||||
|
|
||||||
win64)
|
|
||||||
conf+=(
|
|
||||||
--cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/x86_64-w64-mingw32.meson"
|
|
||||||
)
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "Unsupported host: $HOST" >&2
|
|
||||||
exit 1
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
meson setup . "$SOURCES_DIR/$PROJECT_DIR" "${conf[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ninja
|
|
||||||
ninja install
|
|
@ -1,144 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -ex
|
|
||||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
|
||||||
cd "$DEPS_DIR"
|
|
||||||
. common
|
|
||||||
process_args "$@"
|
|
||||||
|
|
||||||
VERSION=7.1
|
|
||||||
FILENAME=ffmpeg-$VERSION.tar.xz
|
|
||||||
PROJECT_DIR=ffmpeg-$VERSION
|
|
||||||
SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6
|
|
||||||
|
|
||||||
cd "$SOURCES_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$PROJECT_DIR" ]]
|
|
||||||
then
|
|
||||||
echo "$PWD/$PROJECT_DIR" found
|
|
||||||
else
|
|
||||||
get_file "https://ffmpeg.org/releases/$FILENAME" "$FILENAME" "$SHA256SUM"
|
|
||||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
|
|
||||||
cd "$BUILD_DIR/$PROJECT_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$DIRNAME" ]]
|
|
||||||
then
|
|
||||||
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
|
|
||||||
cd "$DIRNAME"
|
|
||||||
else
|
|
||||||
mkdir "$DIRNAME"
|
|
||||||
cd "$DIRNAME"
|
|
||||||
|
|
||||||
if [[ "$HOST" == win* ]]
|
|
||||||
then
|
|
||||||
# -static-libgcc to avoid missing libgcc_s_dw2-1.dll
|
|
||||||
# -static to avoid dynamic dependency to zlib
|
|
||||||
export CFLAGS='-static-libgcc -static'
|
|
||||||
export CXXFLAGS="$CFLAGS"
|
|
||||||
export LDFLAGS='-static-libgcc -static'
|
|
||||||
elif [[ "$HOST" == "macos" ]]
|
|
||||||
then
|
|
||||||
export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig"
|
|
||||||
fi
|
|
||||||
|
|
||||||
export PKG_CONFIG_PATH="$INSTALL_DIR/$DIRNAME/lib/pkgconfig:$PKG_CONFIG_PATH"
|
|
||||||
|
|
||||||
conf=(
|
|
||||||
--prefix="$INSTALL_DIR/$DIRNAME"
|
|
||||||
--pkg-config-flags="--static"
|
|
||||||
--extra-cflags="-O2 -fPIC"
|
|
||||||
--disable-programs
|
|
||||||
--disable-doc
|
|
||||||
--disable-swscale
|
|
||||||
--disable-postproc
|
|
||||||
--disable-avfilter
|
|
||||||
--disable-network
|
|
||||||
--disable-everything
|
|
||||||
--disable-vulkan
|
|
||||||
--disable-vaapi
|
|
||||||
--disable-vdpau
|
|
||||||
--enable-swresample
|
|
||||||
--enable-libdav1d
|
|
||||||
--enable-decoder=h264
|
|
||||||
--enable-decoder=hevc
|
|
||||||
--enable-decoder=av1
|
|
||||||
--enable-decoder=libdav1d
|
|
||||||
--enable-decoder=pcm_s16le
|
|
||||||
--enable-decoder=opus
|
|
||||||
--enable-decoder=aac
|
|
||||||
--enable-decoder=flac
|
|
||||||
--enable-decoder=png
|
|
||||||
--enable-protocol=file
|
|
||||||
--enable-demuxer=image2
|
|
||||||
--enable-parser=png
|
|
||||||
--enable-zlib
|
|
||||||
--enable-muxer=matroska
|
|
||||||
--enable-muxer=mp4
|
|
||||||
--enable-muxer=opus
|
|
||||||
--enable-muxer=flac
|
|
||||||
--enable-muxer=wav
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ "$HOST" == linux ]]
|
|
||||||
then
|
|
||||||
conf+=(
|
|
||||||
--enable-libv4l2
|
|
||||||
--enable-outdev=v4l2
|
|
||||||
--enable-encoder=rawvideo
|
|
||||||
)
|
|
||||||
else
|
|
||||||
# libavdevice is only used for V4L2 on Linux
|
|
||||||
conf+=(
|
|
||||||
--disable-avdevice
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$LINK_TYPE" == static ]]
|
|
||||||
then
|
|
||||||
conf+=(
|
|
||||||
--enable-static
|
|
||||||
--disable-shared
|
|
||||||
)
|
|
||||||
else
|
|
||||||
conf+=(
|
|
||||||
--disable-static
|
|
||||||
--enable-shared
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$BUILD_TYPE" == cross ]]
|
|
||||||
then
|
|
||||||
conf+=(
|
|
||||||
--enable-cross-compile
|
|
||||||
--cross-prefix="${HOST_TRIPLET}-"
|
|
||||||
--cc="${HOST_TRIPLET}-gcc"
|
|
||||||
)
|
|
||||||
|
|
||||||
case "$HOST" in
|
|
||||||
win32)
|
|
||||||
conf+=(
|
|
||||||
--target-os=mingw32
|
|
||||||
--arch=x86
|
|
||||||
)
|
|
||||||
;;
|
|
||||||
|
|
||||||
win64)
|
|
||||||
conf+=(
|
|
||||||
--target-os=mingw32
|
|
||||||
--arch=x86_64
|
|
||||||
)
|
|
||||||
;;
|
|
||||||
|
|
||||||
*)
|
|
||||||
echo "Unsupported host: $HOST" >&2
|
|
||||||
exit 1
|
|
||||||
esac
|
|
||||||
fi
|
|
||||||
|
|
||||||
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
make -j
|
|
||||||
make install
|
|
@ -1,66 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -ex
|
|
||||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
|
||||||
cd "$DEPS_DIR"
|
|
||||||
. common
|
|
||||||
process_args "$@"
|
|
||||||
|
|
||||||
VERSION=1.0.27
|
|
||||||
FILENAME=libusb-$VERSION.tar.gz
|
|
||||||
PROJECT_DIR=libusb-$VERSION
|
|
||||||
SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de
|
|
||||||
|
|
||||||
cd "$SOURCES_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$PROJECT_DIR" ]]
|
|
||||||
then
|
|
||||||
echo "$PWD/$PROJECT_DIR" found
|
|
||||||
else
|
|
||||||
get_file "https://github.com/libusb/libusb/archive/refs/tags/v$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
|
|
||||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
|
|
||||||
cd "$BUILD_DIR/$PROJECT_DIR"
|
|
||||||
|
|
||||||
export CFLAGS='-O2'
|
|
||||||
export CXXFLAGS="$CFLAGS"
|
|
||||||
|
|
||||||
if [[ -d "$DIRNAME" ]]
|
|
||||||
then
|
|
||||||
echo "'$PWD/$DIRNAME' already exists, not reconfigured"
|
|
||||||
cd "$DIRNAME"
|
|
||||||
else
|
|
||||||
mkdir "$DIRNAME"
|
|
||||||
cd "$DIRNAME"
|
|
||||||
|
|
||||||
conf=(
|
|
||||||
--prefix="$INSTALL_DIR/$DIRNAME"
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ "$LINK_TYPE" == static ]]
|
|
||||||
then
|
|
||||||
conf+=(
|
|
||||||
--enable-static
|
|
||||||
--disable-shared
|
|
||||||
)
|
|
||||||
else
|
|
||||||
conf+=(
|
|
||||||
--disable-static
|
|
||||||
--enable-shared
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$BUILD_TYPE" == cross ]]
|
|
||||||
then
|
|
||||||
conf+=(
|
|
||||||
--host="$HOST_TRIPLET"
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
"$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh
|
|
||||||
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
make -j
|
|
||||||
make install-strip
|
|
@ -1,79 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -ex
|
|
||||||
DEPS_DIR=$(dirname ${BASH_SOURCE[0]})
|
|
||||||
cd "$DEPS_DIR"
|
|
||||||
. common
|
|
||||||
process_args "$@"
|
|
||||||
|
|
||||||
VERSION=2.30.10
|
|
||||||
FILENAME=SDL-$VERSION.tar.gz
|
|
||||||
PROJECT_DIR=SDL-release-$VERSION
|
|
||||||
SHA256SUM=35a8b9c4f3635d85762b904ac60ca4e0806bff89faeb269caafbe80860d67168
|
|
||||||
|
|
||||||
cd "$SOURCES_DIR"
|
|
||||||
|
|
||||||
if [[ -d "$PROJECT_DIR" ]]
|
|
||||||
then
|
|
||||||
echo "$PWD/$PROJECT_DIR" found
|
|
||||||
else
|
|
||||||
get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM"
|
|
||||||
tar xf "$FILENAME" # First level directory is "$PROJECT_DIR"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$BUILD_DIR/$PROJECT_DIR"
|
|
||||||
cd "$BUILD_DIR/$PROJECT_DIR"
|
|
||||||
|
|
||||||
export CFLAGS='-O2'
|
|
||||||
export CXXFLAGS="$CFLAGS"
|
|
||||||
|
|
||||||
if [[ -d "$DIRNAME" ]]
|
|
||||||
then
|
|
||||||
echo "'$PWD/$HDIRNAME' already exists, not reconfigured"
|
|
||||||
cd "$DIRNAME"
|
|
||||||
else
|
|
||||||
mkdir "$DIRNAME"
|
|
||||||
cd "$DIRNAME"
|
|
||||||
|
|
||||||
conf=(
|
|
||||||
--prefix="$INSTALL_DIR/$DIRNAME"
|
|
||||||
)
|
|
||||||
|
|
||||||
if [[ "$HOST" == linux ]]
|
|
||||||
then
|
|
||||||
conf+=(
|
|
||||||
--enable-video-wayland
|
|
||||||
--enable-video-x11
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$LINK_TYPE" == static ]]
|
|
||||||
then
|
|
||||||
conf+=(
|
|
||||||
--enable-static
|
|
||||||
--disable-shared
|
|
||||||
)
|
|
||||||
else
|
|
||||||
conf+=(
|
|
||||||
--disable-static
|
|
||||||
--enable-shared
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$BUILD_TYPE" == cross ]]
|
|
||||||
then
|
|
||||||
conf+=(
|
|
||||||
--host="$HOST_TRIPLET"
|
|
||||||
)
|
|
||||||
fi
|
|
||||||
|
|
||||||
"$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
make -j
|
|
||||||
# There is no "make install-strip"
|
|
||||||
make install
|
|
||||||
# Strip manually
|
|
||||||
if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]]
|
|
||||||
then
|
|
||||||
${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll"
|
|
||||||
fi
|
|
312
app/meson.build
312
app/meson.build
@ -1,281 +1,117 @@
|
|||||||
src = [
|
src = [
|
||||||
'src/main.c',
|
'src/main.c',
|
||||||
'src/adb/adb.c',
|
'src/command.c',
|
||||||
'src/adb/adb_device.c',
|
'src/controlevent.c',
|
||||||
'src/adb/adb_parser.c',
|
|
||||||
'src/adb/adb_tunnel.c',
|
|
||||||
'src/audio_player.c',
|
|
||||||
'src/audio_regulator.c',
|
|
||||||
'src/cli.c',
|
|
||||||
'src/clock.c',
|
|
||||||
'src/compat.c',
|
|
||||||
'src/control_msg.c',
|
|
||||||
'src/controller.c',
|
'src/controller.c',
|
||||||
|
'src/convert.c',
|
||||||
'src/decoder.c',
|
'src/decoder.c',
|
||||||
'src/delay_buffer.c',
|
'src/device.c',
|
||||||
'src/demuxer.c',
|
'src/fpscounter.c',
|
||||||
'src/device_msg.c',
|
'src/frames.c',
|
||||||
'src/display.c',
|
'src/inputmanager.c',
|
||||||
'src/events.c',
|
'src/lockutil.c',
|
||||||
'src/icon.c',
|
'src/net.c',
|
||||||
'src/file_pusher.c',
|
|
||||||
'src/fps_counter.c',
|
|
||||||
'src/frame_buffer.c',
|
|
||||||
'src/input_manager.c',
|
|
||||||
'src/keyboard_sdk.c',
|
|
||||||
'src/mouse_capture.c',
|
|
||||||
'src/mouse_sdk.c',
|
|
||||||
'src/opengl.c',
|
|
||||||
'src/options.c',
|
|
||||||
'src/packet_merger.c',
|
|
||||||
'src/receiver.c',
|
|
||||||
'src/recorder.c',
|
|
||||||
'src/scrcpy.c',
|
'src/scrcpy.c',
|
||||||
'src/screen.c',
|
'src/screen.c',
|
||||||
'src/server.c',
|
'src/server.c',
|
||||||
'src/version.c',
|
'src/strutil.c',
|
||||||
'src/hid/hid_gamepad.c',
|
'src/tinyxpm.c',
|
||||||
'src/hid/hid_keyboard.c',
|
|
||||||
'src/hid/hid_mouse.c',
|
|
||||||
'src/trait/frame_source.c',
|
|
||||||
'src/trait/packet_source.c',
|
|
||||||
'src/uhid/gamepad_uhid.c',
|
|
||||||
'src/uhid/keyboard_uhid.c',
|
|
||||||
'src/uhid/mouse_uhid.c',
|
|
||||||
'src/uhid/uhid_output.c',
|
|
||||||
'src/util/acksync.c',
|
|
||||||
'src/util/audiobuf.c',
|
|
||||||
'src/util/average.c',
|
|
||||||
'src/util/env.c',
|
|
||||||
'src/util/file.c',
|
|
||||||
'src/util/intmap.c',
|
|
||||||
'src/util/intr.c',
|
|
||||||
'src/util/log.c',
|
|
||||||
'src/util/memory.c',
|
|
||||||
'src/util/net.c',
|
|
||||||
'src/util/net_intr.c',
|
|
||||||
'src/util/process.c',
|
|
||||||
'src/util/process_intr.c',
|
|
||||||
'src/util/rand.c',
|
|
||||||
'src/util/strbuf.c',
|
|
||||||
'src/util/str.c',
|
|
||||||
'src/util/term.c',
|
|
||||||
'src/util/thread.c',
|
|
||||||
'src/util/tick.c',
|
|
||||||
'src/util/timeout.c',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
conf = configuration_data()
|
if get_option('audio_support')
|
||||||
|
|
||||||
conf.set('_POSIX_C_SOURCE', '200809L')
|
|
||||||
conf.set('_XOPEN_SOURCE', '700')
|
|
||||||
conf.set('_GNU_SOURCE', true)
|
|
||||||
|
|
||||||
if host_machine.system() == 'windows'
|
|
||||||
windows = import('windows')
|
|
||||||
src += [
|
src += [
|
||||||
'src/sys/win/file.c',
|
'src/aoa.c',
|
||||||
'src/sys/win/process.c',
|
'src/audio.c'
|
||||||
windows.compile_resources('scrcpy-windows.rc'),
|
|
||||||
]
|
]
|
||||||
conf.set('_WIN32_WINNT', '0x0600')
|
|
||||||
conf.set('WINVER', '0x0600')
|
|
||||||
else
|
|
||||||
src += [
|
|
||||||
'src/sys/unix/file.c',
|
|
||||||
'src/sys/unix/process.c',
|
|
||||||
]
|
|
||||||
if host_machine.system() == 'darwin'
|
|
||||||
conf.set('_DARWIN_C_SOURCE', true)
|
|
||||||
endif
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
v4l2_support = get_option('v4l2') and host_machine.system() == 'linux'
|
dependencies = [
|
||||||
if v4l2_support
|
dependency('libavformat'),
|
||||||
src += [ 'src/v4l2_sink.c' ]
|
dependency('libavcodec'),
|
||||||
endif
|
dependency('libavutil'),
|
||||||
|
dependency('sdl2'),
|
||||||
|
]
|
||||||
|
|
||||||
usb_support = get_option('usb')
|
if get_option('audio_support')
|
||||||
if usb_support
|
dependencies += dependency('libusb-1.0')
|
||||||
src += [
|
|
||||||
'src/usb/aoa_hid.c',
|
|
||||||
'src/usb/gamepad_aoa.c',
|
|
||||||
'src/usb/keyboard_aoa.c',
|
|
||||||
'src/usb/mouse_aoa.c',
|
|
||||||
'src/usb/scrcpy_otg.c',
|
|
||||||
'src/usb/screen_otg.c',
|
|
||||||
'src/usb/usb.c',
|
|
||||||
]
|
|
||||||
endif
|
endif
|
||||||
|
|
||||||
cc = meson.get_compiler('c')
|
cc = meson.get_compiler('c')
|
||||||
|
|
||||||
static = get_option('static')
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
dependency('libavformat', version: '>= 57.33', static: static),
|
|
||||||
dependency('libavcodec', version: '>= 57.37', static: static),
|
|
||||||
dependency('libavutil', static: static),
|
|
||||||
dependency('libswresample', static: static),
|
|
||||||
dependency('sdl2', version: '>= 2.0.5', static: static),
|
|
||||||
]
|
|
||||||
|
|
||||||
if v4l2_support
|
|
||||||
dependencies += dependency('libavdevice', static: static)
|
|
||||||
endif
|
|
||||||
|
|
||||||
if usb_support
|
|
||||||
dependencies += dependency('libusb-1.0', static: static)
|
|
||||||
endif
|
|
||||||
|
|
||||||
if host_machine.system() == 'windows'
|
if host_machine.system() == 'windows'
|
||||||
dependencies += cc.find_library('mingw32')
|
src += [ 'src/sys/win/command.c' ]
|
||||||
|
src += [ 'src/sys/win/net.c' ]
|
||||||
dependencies += cc.find_library('ws2_32')
|
dependencies += cc.find_library('ws2_32')
|
||||||
|
else
|
||||||
|
src += [ 'src/sys/unix/command.c' ]
|
||||||
|
src += [ 'src/sys/unix/net.c' ]
|
||||||
endif
|
endif
|
||||||
|
|
||||||
check_functions = [
|
conf = configuration_data()
|
||||||
'strdup',
|
|
||||||
'asprintf',
|
|
||||||
'vasprintf',
|
|
||||||
'nrand48',
|
|
||||||
'jrand48',
|
|
||||||
'reallocarray',
|
|
||||||
]
|
|
||||||
|
|
||||||
foreach f : check_functions
|
# expose the build type
|
||||||
if cc.has_function(f)
|
conf.set('BUILD_DEBUG', get_option('buildtype') == 'debug')
|
||||||
define = 'HAVE_' + f.underscorify().to_upper()
|
|
||||||
conf.set(define, true)
|
|
||||||
endif
|
|
||||||
endforeach
|
|
||||||
|
|
||||||
conf.set('HAVE_SOCK_CLOEXEC', host_machine.system() != 'windows' and
|
|
||||||
cc.has_header_symbol('sys/socket.h', 'SOCK_CLOEXEC'))
|
|
||||||
|
|
||||||
# the version, updated on release
|
# the version, updated on release
|
||||||
conf.set_quoted('SCRCPY_VERSION', meson.project_version())
|
conf.set_quoted('SCRCPY_VERSION', '1.1')
|
||||||
|
|
||||||
# the prefix used during configuration (meson --prefix=PREFIX)
|
# the prefix used during configuration (meson --prefix=PREFIX)
|
||||||
conf.set_quoted('PREFIX', get_option('prefix'))
|
conf.set_quoted('PREFIX', get_option('prefix'))
|
||||||
|
|
||||||
# build a "portable" version (with scrcpy-server accessible from the same
|
# the path of the server, which will be appended to the prefix
|
||||||
# directory as the executable)
|
# ignored if OVERRIDE_SERVER_PATH if defined
|
||||||
conf.set('PORTABLE', get_option('portable'))
|
# must be consistent with the install_dir in server/meson.build
|
||||||
|
conf.set_quoted('PREFIXED_SERVER_PATH', '/share/scrcpy/scrcpy-server.jar')
|
||||||
|
|
||||||
# the default client TCP port range for the "adb reverse" tunnel
|
# the path of the server to be used "as is"
|
||||||
|
# this is useful for building a "portable" version (with the server in the same
|
||||||
|
# directory as the client)
|
||||||
|
override_server_path = get_option('override_server_path')
|
||||||
|
if override_server_path != ''
|
||||||
|
conf.set_quoted('OVERRIDE_SERVER_PATH', override_server_path)
|
||||||
|
else
|
||||||
|
# undefine it
|
||||||
|
conf.set('OVERRIDE_SERVER_PATH', false)
|
||||||
|
endif
|
||||||
|
|
||||||
|
# the default client TCP port for the "adb reverse" tunnel
|
||||||
# overridden by option --port
|
# overridden by option --port
|
||||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
|
conf.set('DEFAULT_LOCAL_PORT', '27183')
|
||||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
|
|
||||||
|
|
||||||
# run a server debugger and wait for a client to be attached
|
# the default max video size for both dimensions, in pixels
|
||||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
# overridden by option --max-size
|
||||||
|
conf.set('DEFAULT_MAX_SIZE', '0') # 0: unlimited
|
||||||
|
|
||||||
# enable V4L2 support (linux only)
|
# the default video bitrate, in bits/second
|
||||||
conf.set('HAVE_V4L2', v4l2_support)
|
# overridden by option --bit-rate
|
||||||
|
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
||||||
|
|
||||||
# enable HID over AOA support (linux only)
|
# whether the app should always display the most recent available frame, even
|
||||||
conf.set('HAVE_USB', usb_support)
|
# if the previous one has not been displayed
|
||||||
|
# SKIP_FRAMES improves latency at the cost of framerate
|
||||||
|
conf.set('SKIP_FRAMES', get_option('skip_frames'))
|
||||||
|
|
||||||
|
# enable High DPI support
|
||||||
|
conf.set('HIDPI_SUPPORT', get_option('hidpi_support'))
|
||||||
|
|
||||||
|
# enable audio support (enable audio forwarding with --forward-audio)
|
||||||
|
conf.set('AUDIO_SUPPORT', get_option('audio_support'))
|
||||||
|
|
||||||
configure_file(configuration: conf, output: 'config.h')
|
configure_file(configuration: conf, output: 'config.h')
|
||||||
|
|
||||||
src_dir = include_directories('src')
|
src_dir = include_directories('src')
|
||||||
|
executable('scrcpy', src, dependencies: dependencies, include_directories: src_dir, install: true)
|
||||||
executable('scrcpy', src,
|
|
||||||
dependencies: dependencies,
|
|
||||||
include_directories: src_dir,
|
|
||||||
install: true,
|
|
||||||
c_args: [])
|
|
||||||
|
|
||||||
# <https://mesonbuild.com/Builtin-options.html#directories>
|
|
||||||
datadir = get_option('datadir') # by default 'share'
|
|
||||||
|
|
||||||
install_man('scrcpy.1')
|
|
||||||
install_data('data/icon.png',
|
|
||||||
rename: 'scrcpy.png',
|
|
||||||
install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps'))
|
|
||||||
install_data('data/zsh-completion/_scrcpy',
|
|
||||||
install_dir: join_paths(datadir, 'zsh/site-functions'))
|
|
||||||
install_data('data/bash-completion/scrcpy',
|
|
||||||
install_dir: join_paths(datadir, 'bash-completion/completions'))
|
|
||||||
|
|
||||||
# Desktop entry file for application launchers
|
|
||||||
if host_machine.system() == 'linux'
|
|
||||||
# Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop)
|
|
||||||
install_data('data/scrcpy.desktop',
|
|
||||||
install_dir: join_paths(datadir, 'applications'))
|
|
||||||
install_data('data/scrcpy-console.desktop',
|
|
||||||
install_dir: join_paths(datadir, 'applications'))
|
|
||||||
endif
|
|
||||||
|
|
||||||
|
|
||||||
### TESTS
|
### TESTS
|
||||||
|
|
||||||
# do not build tests in release (assertions would not be executed at all)
|
tests = [
|
||||||
if get_option('buildtype') == 'debug'
|
['test_control_event_queue', ['tests/test_control_event_queue.c', 'src/controlevent.c']],
|
||||||
tests = [
|
['test_control_event_serialize', ['tests/test_control_event_serialize.c', 'src/controlevent.c']],
|
||||||
['test_adb_parser', [
|
['test_strutil', ['tests/test_strutil.c', 'src/strutil.c']],
|
||||||
'tests/test_adb_parser.c',
|
]
|
||||||
'src/adb/adb_device.c',
|
|
||||||
'src/adb/adb_parser.c',
|
|
||||||
'src/util/str.c',
|
|
||||||
'src/util/strbuf.c',
|
|
||||||
]],
|
|
||||||
['test_binary', [
|
|
||||||
'tests/test_binary.c',
|
|
||||||
]],
|
|
||||||
['test_audiobuf', [
|
|
||||||
'tests/test_audiobuf.c',
|
|
||||||
'src/util/audiobuf.c',
|
|
||||||
'src/util/memory.c',
|
|
||||||
]],
|
|
||||||
['test_cli', [
|
|
||||||
'tests/test_cli.c',
|
|
||||||
'src/cli.c',
|
|
||||||
'src/options.c',
|
|
||||||
'src/util/log.c',
|
|
||||||
'src/util/net.c',
|
|
||||||
'src/util/str.c',
|
|
||||||
'src/util/strbuf.c',
|
|
||||||
'src/util/term.c',
|
|
||||||
]],
|
|
||||||
['test_control_msg_serialize', [
|
|
||||||
'tests/test_control_msg_serialize.c',
|
|
||||||
'src/control_msg.c',
|
|
||||||
'src/util/str.c',
|
|
||||||
'src/util/strbuf.c',
|
|
||||||
]],
|
|
||||||
['test_device_msg_deserialize', [
|
|
||||||
'tests/test_device_msg_deserialize.c',
|
|
||||||
'src/device_msg.c',
|
|
||||||
]],
|
|
||||||
['test_orientation', [
|
|
||||||
'tests/test_orientation.c',
|
|
||||||
'src/options.c',
|
|
||||||
]],
|
|
||||||
['test_strbuf', [
|
|
||||||
'tests/test_strbuf.c',
|
|
||||||
'src/util/strbuf.c',
|
|
||||||
]],
|
|
||||||
['test_str', [
|
|
||||||
'tests/test_str.c',
|
|
||||||
'src/util/str.c',
|
|
||||||
'src/util/strbuf.c',
|
|
||||||
]],
|
|
||||||
['test_vecdeque', [
|
|
||||||
'tests/test_vecdeque.c',
|
|
||||||
'src/util/memory.c',
|
|
||||||
]],
|
|
||||||
['test_vector', [
|
|
||||||
'tests/test_vector.c',
|
|
||||||
]],
|
|
||||||
]
|
|
||||||
|
|
||||||
foreach t : tests
|
foreach t : tests
|
||||||
sources = t[1] + ['src/compat.c']
|
exe = executable(t[0], t[1], include_directories: src_dir, dependencies: dependencies)
|
||||||
exe = executable(t[0], sources,
|
test(t[0], exe)
|
||||||
include_directories: src_dir,
|
endforeach
|
||||||
dependencies: dependencies,
|
|
||||||
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
|
|
||||||
test(t[0], exe)
|
|
||||||
endforeach
|
|
||||||
endif
|
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
|
||||||
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
|
||||||
<asmv3:application>
|
|
||||||
<asmv3:windowsSettings>
|
|
||||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
|
|
||||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
|
|
||||||
</asmv3:windowsSettings>
|
|
||||||
</asmv3:application>
|
|
||||||
</assembly>
|
|
@ -1,23 +0,0 @@
|
|||||||
#include <winuser.h>
|
|
||||||
|
|
||||||
0 ICON "data/icon.ico"
|
|
||||||
1 RT_MANIFEST "scrcpy-windows.manifest"
|
|
||||||
2 VERSIONINFO
|
|
||||||
BEGIN
|
|
||||||
BLOCK "StringFileInfo"
|
|
||||||
BEGIN
|
|
||||||
BLOCK "040904E4"
|
|
||||||
BEGIN
|
|
||||||
VALUE "FileDescription", "Display and control your Android device"
|
|
||||||
VALUE "InternalName", "scrcpy"
|
|
||||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
|
||||||
VALUE "OriginalFilename", "scrcpy.exe"
|
|
||||||
VALUE "ProductName", "scrcpy"
|
|
||||||
VALUE "ProductVersion", "3.1"
|
|
||||||
END
|
|
||||||
END
|
|
||||||
BLOCK "VarFileInfo"
|
|
||||||
BEGIN
|
|
||||||
VALUE "Translation", 0x409, 1252
|
|
||||||
END
|
|
||||||
END
|
|
837
app/scrcpy.1
837
app/scrcpy.1
@ -1,837 +0,0 @@
|
|||||||
.TH "scrcpy" "1"
|
|
||||||
.SH NAME
|
|
||||||
scrcpy \- Display and control your Android device
|
|
||||||
|
|
||||||
|
|
||||||
.SH SYNOPSIS
|
|
||||||
.B scrcpy
|
|
||||||
.RI [ options ]
|
|
||||||
|
|
||||||
|
|
||||||
.SH DESCRIPTION
|
|
||||||
.B scrcpy
|
|
||||||
provides display and control of Android devices connected on USB (or over TCP/IP). It does not require any root access.
|
|
||||||
|
|
||||||
|
|
||||||
.SH OPTIONS
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-always\-on\-top
|
|
||||||
Make scrcpy window always on top (above other windows).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-angle " degrees
|
|
||||||
Rotate the video content by a custom angle, in degrees (clockwise).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-audio\-bit\-rate " value
|
|
||||||
Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
|
||||||
|
|
||||||
Default is 128K (128000).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-audio\-buffer " ms
|
|
||||||
Configure the audio buffering delay (in milliseconds).
|
|
||||||
|
|
||||||
Lower values decrease the latency, but increase the likelihood of buffer underrun (causing audio glitches).
|
|
||||||
|
|
||||||
Default is 50.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-audio\-codec " name
|
|
||||||
Select an audio codec (opus, aac, flac or raw).
|
|
||||||
|
|
||||||
Default is opus.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-audio\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
|
||||||
Set a list of comma-separated key:type=value options for the device audio encoder.
|
|
||||||
|
|
||||||
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
|
||||||
|
|
||||||
The list of possible codec options is available in the Android documentation:
|
|
||||||
|
|
||||||
<https://d.android.com/reference/android/media/MediaFormat>
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-audio\-dup
|
|
||||||
Duplicate audio (capture and keep playing on the device).
|
|
||||||
|
|
||||||
This feature is only available with --audio-source=playback.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-audio\-encoder " name
|
|
||||||
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
|
|
||||||
|
|
||||||
The available encoders can be listed by \fB\-\-list\-encoders\fR.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-audio\-source " source
|
|
||||||
Select the audio source (output, mic or playback).
|
|
||||||
|
|
||||||
The "output" source forwards the whole audio output, and disables playback on the device.
|
|
||||||
|
|
||||||
The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
|
|
||||||
|
|
||||||
The "mic" source captures the microphone.
|
|
||||||
|
|
||||||
Default is output.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-audio\-output\-buffer " ms
|
|
||||||
Configure the size of the SDL audio output buffer (in milliseconds).
|
|
||||||
|
|
||||||
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
|
|
||||||
|
|
||||||
Default is 5.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-b, \-\-video\-bit\-rate " value
|
|
||||||
Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
|
||||||
|
|
||||||
Default is 8M (8000000).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-camera\-ar " ar
|
|
||||||
Select the camera size by its aspect ratio (+/- 10%).
|
|
||||||
|
|
||||||
Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6").
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-camera\-facing " facing
|
|
||||||
Select the device camera by its facing direction.
|
|
||||||
|
|
||||||
Possible values are "front", "back" and "external".
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-camera\-fps " fps
|
|
||||||
Specify the camera capture frame rate.
|
|
||||||
|
|
||||||
If not specified, Android's default frame rate (30 fps) is used.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-camera\-high\-speed
|
|
||||||
Enable high-speed camera capture mode.
|
|
||||||
|
|
||||||
This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-camera\-id " id
|
|
||||||
Specify the device camera id to mirror.
|
|
||||||
|
|
||||||
The available camera ids can be listed by \fB\-\-list\-cameras\fR.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-camera\-size " width\fRx\fIheight
|
|
||||||
Specify an explicit camera capture size.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-capture\-orientation " value
|
|
||||||
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'.
|
|
||||||
|
|
||||||
The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation.
|
|
||||||
|
|
||||||
If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation.
|
|
||||||
|
|
||||||
If '@' is passed alone, then the rotation is locked to the initial device orientation.
|
|
||||||
|
|
||||||
Default is 0.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
|
||||||
Crop the device screen on the server.
|
|
||||||
|
|
||||||
The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-d, \-\-select\-usb
|
|
||||||
Use USB device (if there is exactly one, like adb -d).
|
|
||||||
|
|
||||||
Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-disable\-screensaver"
|
|
||||||
Disable screensaver while scrcpy is running.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-display\-id " id
|
|
||||||
Specify the device display id to mirror.
|
|
||||||
|
|
||||||
The available display ids can be listed by \fB\-\-list\-displays\fR.
|
|
||||||
|
|
||||||
Default is 0.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-display\-orientation " value
|
|
||||||
Set the initial display orientation.
|
|
||||||
|
|
||||||
Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation.
|
|
||||||
|
|
||||||
Default is 0.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-e, \-\-select\-tcpip
|
|
||||||
Use TCP/IP device (if there is exactly one, like adb -e).
|
|
||||||
|
|
||||||
Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-f, \-\-fullscreen
|
|
||||||
Start in fullscreen.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-force\-adb\-forward
|
|
||||||
Do not attempt to use "adb reverse" to connect to the device.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-G
|
|
||||||
Same as \fB\-\-gamepad=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-gamepad " mode
|
|
||||||
Select how to send gamepad inputs to the device.
|
|
||||||
|
|
||||||
Possible values are "disabled", "uhid" and "aoa":
|
|
||||||
|
|
||||||
- "disabled" does not send gamepad inputs to the device.
|
|
||||||
- "uhid" simulates physical HID gamepads using the Linux HID kernel module on the device.
|
|
||||||
- "aoa" simulates physical HID gamepads using the AOAv2 protocol. It may only work over USB.
|
|
||||||
|
|
||||||
Also see \fB\-\-keyboard\f and R\fB\-\-mouse\fR.
|
|
||||||
.TP
|
|
||||||
.B \-h, \-\-help
|
|
||||||
Print this help.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-K
|
|
||||||
Same as \fB\-\-keyboard=uhid\fR, or \fB\-\-keyboard=aoa\fR if \fB\-\-otg\fR is set.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-keyboard " mode
|
|
||||||
Select how to send keyboard inputs to the device.
|
|
||||||
|
|
||||||
Possible values are "disabled", "sdk", "uhid" and "aoa":
|
|
||||||
|
|
||||||
- "disabled" does not send keyboard inputs to the device.
|
|
||||||
- "sdk" uses the Android system API to deliver keyboard events to applications.
|
|
||||||
- "uhid" simulates a physical HID keyboard using the Linux HID kernel module on the device.
|
|
||||||
- "aoa" simulates a physical HID keyboard using the AOAv2 protocol. It may only work over USB.
|
|
||||||
|
|
||||||
For "uhid" and "aoa", the keyboard layout must be configured (once and for all) on the device, via Settings -> System -> Languages and input -> Physical keyboard. This settings page can be started directly using the shortcut MOD+k (except in OTG mode), or by executing:
|
|
||||||
|
|
||||||
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS
|
|
||||||
|
|
||||||
This option is only available when the HID keyboard is enabled (or a physical keyboard is connected).
|
|
||||||
|
|
||||||
Also see \fB\-\-mouse\fR and \fB\-\-gamepad\fR.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-kill\-adb\-on\-close
|
|
||||||
Kill adb when scrcpy terminates.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-legacy\-paste
|
|
||||||
Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+Shift+v).
|
|
||||||
|
|
||||||
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-list\-apps
|
|
||||||
List Android apps installed on the device.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-list\-camera\-sizes
|
|
||||||
List the valid camera capture sizes.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-list\-cameras
|
|
||||||
List cameras available on the device.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-list\-encoders
|
|
||||||
List video and audio encoders available on the device.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-list\-displays
|
|
||||||
List displays available on the device.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-m, \-\-max\-size " value
|
|
||||||
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
|
|
||||||
|
|
||||||
Default is 0 (unlimited).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-M
|
|
||||||
Same as \fB\-\-mouse=uhid\fR, or \fB\-\-mouse=aoa\fR if \fB\-\-otg\fR is set.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-max\-fps " value
|
|
||||||
Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-mouse " mode
|
|
||||||
Select how to send mouse inputs to the device.
|
|
||||||
|
|
||||||
Possible values are "disabled", "sdk", "uhid" and "aoa":
|
|
||||||
|
|
||||||
- "disabled" does not send mouse inputs to the device.
|
|
||||||
- "sdk" uses the Android system API to deliver mouse events to applications.
|
|
||||||
- "uhid" simulates a physical HID mouse using the Linux HID kernel module on the device.
|
|
||||||
- "aoa" simulates a physical mouse using the AOAv2 protocol. It may only work over USB.
|
|
||||||
|
|
||||||
In "uhid" and "aoa" modes, the computer mouse is captured to control the device directly (relative mouse mode).
|
|
||||||
|
|
||||||
LAlt, LSuper or RSuper toggle the capture mode, to give control of the mouse back to the computer.
|
|
||||||
|
|
||||||
Also see \fB\-\-keyboard\fR and \fB\-\-gamepad\fR.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-mouse\-bind " xxxx[:xxxx]
|
|
||||||
Configure bindings of secondary clicks.
|
|
||||||
|
|
||||||
The argument must be one or two sequences (separated by ':') of exactly 4 characters, one for each secondary click (in order: right click, middle click, 4th click, 5th click).
|
|
||||||
|
|
||||||
The first sequence defines the primary bindings, used when a mouse button is pressed alone. The second sequence defines the secondary bindings, used when a mouse button is pressed while the Shift key is held.
|
|
||||||
|
|
||||||
If the second sequence of bindings is omitted, then it is the same as the first one.
|
|
||||||
|
|
||||||
Each character must be one of the following:
|
|
||||||
|
|
||||||
- '+': forward the click to the device
|
|
||||||
- '-': ignore the click
|
|
||||||
- 'b': trigger shortcut BACK (or turn screen on if off)
|
|
||||||
- 'h': trigger shortcut HOME
|
|
||||||
- 's': trigger shortcut APP_SWITCH
|
|
||||||
- 'n': trigger shortcut "expand notification panel"
|
|
||||||
|
|
||||||
Default is 'bhsn:++++' for SDK mouse, and '++++:bhsn' for AOA and UHID.
|
|
||||||
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-n, \-\-no\-control
|
|
||||||
Disable device control (mirror the device in read\-only).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-N, \-\-no\-playback
|
|
||||||
Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]]
|
|
||||||
Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
\-\-new\-display=1920x1080
|
|
||||||
\-\-new\-display=1920x1080/420
|
|
||||||
\-\-new\-display # main display size and density
|
|
||||||
\-\-new\-display=/240 # main display size and 240 dpi
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-audio
|
|
||||||
Disable audio forwarding.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-audio\-playback
|
|
||||||
Disable audio playback on the computer.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-cleanup
|
|
||||||
By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit.
|
|
||||||
|
|
||||||
This option disables this cleanup.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-clipboard\-autosync
|
|
||||||
By default, scrcpy automatically synchronizes the computer clipboard to the device clipboard before injecting Ctrl+v, and the device clipboard to the computer clipboard whenever it changes.
|
|
||||||
|
|
||||||
This option disables this automatic synchronization.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-downsize\-on\-error
|
|
||||||
By default, on MediaCodec error, scrcpy automatically tries again with a lower definition.
|
|
||||||
|
|
||||||
This option disables this behavior.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-key\-repeat
|
|
||||||
Do not forward repeated key events when a key is held down.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-mipmaps
|
|
||||||
If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically generated to improve downscaling quality. This option disables the generation of mipmaps.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-mouse\-hover
|
|
||||||
Do not forward mouse hover (mouse motion without any clicks) events.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-power\-on
|
|
||||||
Do not power on the device on start.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-vd\-destroy\-content
|
|
||||||
Disable virtual display "destroy content on removal" flag.
|
|
||||||
|
|
||||||
With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-vd\-system\-decorations
|
|
||||||
Disable virtual display system decorations flag.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-video
|
|
||||||
Disable video forwarding.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-video\-playback
|
|
||||||
Disable video playback on the computer.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-no\-window
|
|
||||||
Disable scrcpy window. Implies --no-video-playback and --no-control.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-orientation " value
|
|
||||||
Same as --display-orientation=value --record-orientation=value.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-otg
|
|
||||||
Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable.
|
|
||||||
|
|
||||||
In this mode, adb (USB debugging) is not necessary, and mirroring is disabled.
|
|
||||||
|
|
||||||
LAlt, LSuper or RSuper toggle the mouse capture mode, to give control of the mouse back to the computer.
|
|
||||||
|
|
||||||
If any of \fB\-\-hid\-keyboard\fR or \fB\-\-hid\-mouse\fR is set, only enable keyboard or mouse respectively, otherwise enable both.
|
|
||||||
|
|
||||||
It may only work over USB.
|
|
||||||
|
|
||||||
See \fB\-\-keyboard\fR, \fB\-\-mouse\fR and \fB\-\-gamepad\fR.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
|
|
||||||
Set the TCP port (range) used by the client to listen.
|
|
||||||
|
|
||||||
Default is 27183:27199.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
\fB\-\-pause\-on\-exit\fR[=\fImode\fR]
|
|
||||||
Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occurred).
|
|
||||||
|
|
||||||
This is useful to prevent the terminal window from automatically closing, so that error messages can be read.
|
|
||||||
|
|
||||||
Default is "false".
|
|
||||||
|
|
||||||
Passing the option without argument is equivalent to passing "true".
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-power\-off\-on\-close
|
|
||||||
Turn the device screen off when closing scrcpy.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-prefer\-text
|
|
||||||
Inject alpha characters and space as text events instead of key events.
|
|
||||||
|
|
||||||
This avoids issues when combining multiple keys to enter special characters,
|
|
||||||
but breaks the expected behavior of alpha keys in games (typically WASD).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B "\-\-print\-fps
|
|
||||||
Start FPS counter, to print framerate logs to the console. It can be started or stopped at any time with MOD+i.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-push\-target " path
|
|
||||||
Set the target directory for pushing files to the device by drag & drop. It is passed as\-is to "adb push".
|
|
||||||
|
|
||||||
Default is "/sdcard/Download/".
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-r, \-\-record " file
|
|
||||||
Record screen to
|
|
||||||
.IR file .
|
|
||||||
|
|
||||||
The format is determined by the
|
|
||||||
.B \-\-record\-format
|
|
||||||
option if set, or by the file extension.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-raw\-key\-events
|
|
||||||
Inject key events for all input keys, and ignore text events.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-record\-format " format
|
|
||||||
Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-record\-orientation " value
|
|
||||||
Set the record orientation.
|
|
||||||
|
|
||||||
Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees.
|
|
||||||
|
|
||||||
Default is 0.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-render\-driver " name
|
|
||||||
Request SDL to use the given render driver (this is just a hint).
|
|
||||||
|
|
||||||
Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software".
|
|
||||||
|
|
||||||
<https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-require\-audio
|
|
||||||
By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-s, \-\-serial " number
|
|
||||||
The device serial number. Mandatory only if several devices are connected to adb.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-S, \-\-turn\-screen\-off
|
|
||||||
Turn the device screen off immediately.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
|
|
||||||
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
|
|
||||||
|
|
||||||
Several shortcut modifiers can be specified, separated by ','.
|
|
||||||
|
|
||||||
For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsuper".
|
|
||||||
|
|
||||||
Default is "lalt,lsuper" (left-Alt or left-Super).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-start\-app " name
|
|
||||||
Start an Android app, by its exact package name.
|
|
||||||
|
|
||||||
Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time):
|
|
||||||
|
|
||||||
scrcpy --start-app=?firefox
|
|
||||||
|
|
||||||
Add a '+' prefix to force-stop before starting the app:
|
|
||||||
|
|
||||||
scrcpy --new-display --start-app=+org.mozilla.firefox
|
|
||||||
|
|
||||||
Both prefixes can be used, in that order:
|
|
||||||
|
|
||||||
scrcpy --start-app=+?firefox
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-t, \-\-show\-touches
|
|
||||||
Enable "show touches" on start, restore the initial value on exit.
|
|
||||||
|
|
||||||
It only shows physical touches (not clicks from scrcpy).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-tcpip\fR[=[+]\fIip\fR[:\fIport\fR]]
|
|
||||||
Configure and connect the device over TCP/IP.
|
|
||||||
|
|
||||||
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).
|
|
||||||
|
|
||||||
If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting.
|
|
||||||
|
|
||||||
Prefix the address with a '+' to force a reconnection.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-time\-limit " seconds
|
|
||||||
Set the maximum mirroring time, in seconds.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-tunnel\-host " ip
|
|
||||||
Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR.
|
|
||||||
|
|
||||||
Default is localhost.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-tunnel\-port " port
|
|
||||||
Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR.
|
|
||||||
|
|
||||||
Default is 0 (not forced): the local port used for establishing the tunnel will be used.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-v, \-\-version
|
|
||||||
Print the version of scrcpy.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-V, \-\-verbosity " value
|
|
||||||
Set the log level ("verbose", "debug", "info", "warn" or "error").
|
|
||||||
|
|
||||||
Default is "info" for release builds, "debug" for debug builds.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-v4l2-sink " /dev/videoN
|
|
||||||
Output to v4l2loopback device.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-v4l2-buffer " ms
|
|
||||||
Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter.
|
|
||||||
|
|
||||||
This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink.
|
|
||||||
|
|
||||||
Default is 0 (no buffering).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-video\-buffer " ms
|
|
||||||
Add a buffering delay (in milliseconds) before displaying video frames.
|
|
||||||
|
|
||||||
This increases latency to compensate for jitter.
|
|
||||||
|
|
||||||
Default is 0 (no buffering).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-video\-codec " name
|
|
||||||
Select a video codec (h264, h265 or av1).
|
|
||||||
|
|
||||||
Default is h264.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
|
||||||
Set a list of comma-separated key:type=value options for the device video encoder.
|
|
||||||
|
|
||||||
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
|
||||||
|
|
||||||
The list of possible codec options is available in the Android documentation:
|
|
||||||
|
|
||||||
<https://d.android.com/reference/android/media/MediaFormat>
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-video\-encoder " name
|
|
||||||
Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR).
|
|
||||||
|
|
||||||
The available encoders can be listed by \fB\-\-list\-encoders\fR.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-video\-source " source
|
|
||||||
Select the video source (display or camera).
|
|
||||||
|
|
||||||
Camera mirroring requires Android 12+.
|
|
||||||
|
|
||||||
Default is display.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-w, \-\-stay-awake
|
|
||||||
Keep the device on while scrcpy is running, when the device is plugged in.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B \-\-window\-borderless
|
|
||||||
Disable window decorations (display borderless window).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-window\-title " text
|
|
||||||
Set a custom window title.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-window\-x " value
|
|
||||||
Set the initial window horizontal position.
|
|
||||||
|
|
||||||
Default is "auto".
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-window\-y " value
|
|
||||||
Set the initial window vertical position.
|
|
||||||
|
|
||||||
Default is "auto".
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-window\-width " value
|
|
||||||
Set the initial window width.
|
|
||||||
|
|
||||||
Default is 0 (automatic).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-window\-height " value
|
|
||||||
Set the initial window height.
|
|
||||||
|
|
||||||
Default is 0 (automatic).
|
|
||||||
|
|
||||||
.SH EXIT STATUS
|
|
||||||
.B scrcpy
|
|
||||||
will exit with code 0 on normal program termination. If an initial
|
|
||||||
connection cannot be established, the exit code 1 will be returned. If the
|
|
||||||
device disconnects while a session is active, exit code 2 will be returned.
|
|
||||||
|
|
||||||
.SH SHORTCUTS
|
|
||||||
|
|
||||||
In the following list, MOD is the shortcut modifier. By default, it's (left)
|
|
||||||
Alt or (left) Super, but it can be configured by \fB\-\-shortcut\-mod\fR (see above).
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+f
|
|
||||||
Switch fullscreen mode
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Left
|
|
||||||
Rotate display left
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Right
|
|
||||||
Rotate display right
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Shift+Left, MOD+Shift+Right
|
|
||||||
Flip display horizontally
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Shift+Up, MOD+Shift+Down
|
|
||||||
Flip display vertically
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+z
|
|
||||||
Pause or re-pause display
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Shift+z
|
|
||||||
Unpause display
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Shift+r
|
|
||||||
Reset video capture/encoding
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+g
|
|
||||||
Resize window to 1:1 (pixel\-perfect)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+w, Double\-click on black borders
|
|
||||||
Resize window to remove black borders
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+h, Home, Middle\-click
|
|
||||||
Click on HOME
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+b, MOD+Backspace, Right\-click (when screen is on)
|
|
||||||
Click on BACK
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+s
|
|
||||||
Click on APP_SWITCH
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+m
|
|
||||||
Click on MENU
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Up
|
|
||||||
Click on VOLUME_UP
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Down
|
|
||||||
Click on VOLUME_DOWN
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+p
|
|
||||||
Click on POWER (turn screen on/off)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Right\-click (when screen is off)
|
|
||||||
Turn screen on
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+o
|
|
||||||
Turn device screen off (keep mirroring)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Shift+o
|
|
||||||
Turn device screen on
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+r
|
|
||||||
Rotate device screen
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+n
|
|
||||||
Expand notification panel
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Shift+n
|
|
||||||
Collapse notification panel
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Mod+c
|
|
||||||
Copy to clipboard (inject COPY keycode, Android >= 7 only)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Mod+x
|
|
||||||
Cut to clipboard (inject CUT keycode, Android >= 7 only)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+v
|
|
||||||
Copy computer clipboard to device, then paste (inject PASTE keycode, Android >= 7 only)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+Shift+v
|
|
||||||
Inject computer clipboard text as a sequence of key events
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+k
|
|
||||||
Open keyboard settings on the device (for HID keyboard only)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B MOD+i
|
|
||||||
Enable/disable FPS counter (print frames/second in logs)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Ctrl+click-and-move
|
|
||||||
Pinch-to-zoom and rotate from the center of the screen
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Shift+click-and-move
|
|
||||||
Tilt vertically (slide with 2 fingers)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Ctrl+Shift+click-and-move
|
|
||||||
Tilt horizontally (slide with 2 fingers)
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Drag & drop APK file
|
|
||||||
Install APK from computer
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B Drag & drop non-APK file
|
|
||||||
Push file to device (see \fB\-\-push\-target\fR)
|
|
||||||
|
|
||||||
|
|
||||||
.SH Environment variables
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B ADB
|
|
||||||
Path to adb.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B ANDROID_SERIAL
|
|
||||||
Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B SCRCPY_ICON_PATH
|
|
||||||
Path to the program icon.
|
|
||||||
|
|
||||||
.TP
|
|
||||||
.B SCRCPY_SERVER_PATH
|
|
||||||
Path to the server binary.
|
|
||||||
|
|
||||||
|
|
||||||
.SH AUTHORS
|
|
||||||
.B scrcpy
|
|
||||||
is written by Romain Vimont.
|
|
||||||
|
|
||||||
This manual page was written by
|
|
||||||
.MT mmyangfl@gmail.com
|
|
||||||
Yangfl
|
|
||||||
.ME
|
|
||||||
for the Debian Project (and may be used by others).
|
|
||||||
|
|
||||||
|
|
||||||
.SH "REPORTING BUGS"
|
|
||||||
Report bugs to <https://github.com/Genymobile/scrcpy/issues>.
|
|
||||||
|
|
||||||
.SH COPYRIGHT
|
|
||||||
Copyright \(co 2018 Genymobile <https://www.genymobile.com>
|
|
||||||
|
|
||||||
Copyright \(co 2018\-2025 Romain Vimont <rom@rom1v.com>
|
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0.
|
|
||||||
|
|
||||||
.SH WWW
|
|
||||||
<https://github.com/Genymobile/scrcpy>
|
|
@ -1,791 +0,0 @@
|
|||||||
#include "adb.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "adb_device.h"
|
|
||||||
#include "adb_parser.h"
|
|
||||||
#include "util/env.h"
|
|
||||||
#include "util/file.h"
|
|
||||||
#include "util/log.h"
|
|
||||||
#include "util/process_intr.h"
|
|
||||||
#include "util/str.h"
|
|
||||||
|
|
||||||
/* Convenience macro to expand:
|
|
||||||
*
|
|
||||||
* const char *const argv[] =
|
|
||||||
* SC_ADB_COMMAND("shell", "echo", "hello");
|
|
||||||
*
|
|
||||||
* to:
|
|
||||||
*
|
|
||||||
* const char *const argv[] =
|
|
||||||
* { sc_adb_get_executable(), "shell", "echo", "hello", NULL };
|
|
||||||
*/
|
|
||||||
#define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL }
|
|
||||||
|
|
||||||
static char *adb_executable;
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_init(void) {
|
|
||||||
adb_executable = sc_get_env("ADB");
|
|
||||||
if (adb_executable) {
|
|
||||||
LOGD("Using adb: %s", adb_executable);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
#if !defined(PORTABLE) || defined(_WIN32)
|
|
||||||
adb_executable = strdup("adb");
|
|
||||||
if (!adb_executable) {
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
// For portable builds, use the absolute path to the adb executable
|
|
||||||
// in the same directory as scrcpy (except on Windows, where "adb"
|
|
||||||
// is sufficient)
|
|
||||||
adb_executable = sc_file_get_local_path("adb");
|
|
||||||
if (!adb_executable) {
|
|
||||||
// Error already logged
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGD("Using adb (portable): %s", adb_executable);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_adb_destroy(void) {
|
|
||||||
free(adb_executable);
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *
|
|
||||||
sc_adb_get_executable(void) {
|
|
||||||
return adb_executable;
|
|
||||||
}
|
|
||||||
|
|
||||||
// serialize argv to string "[arg1], [arg2], [arg3]"
|
|
||||||
static size_t
|
|
||||||
argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
|
|
||||||
size_t idx = 0;
|
|
||||||
bool first = true;
|
|
||||||
while (*argv) {
|
|
||||||
const char *arg = *argv;
|
|
||||||
size_t len = strlen(arg);
|
|
||||||
// count space for "[], ...\0"
|
|
||||||
if (idx + len + 8 >= bufsize) {
|
|
||||||
// not enough space, truncate
|
|
||||||
assert(idx < bufsize - 4);
|
|
||||||
memcpy(&buf[idx], "...", 3);
|
|
||||||
idx += 3;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (first) {
|
|
||||||
first = false;
|
|
||||||
} else {
|
|
||||||
buf[idx++] = ',';
|
|
||||||
buf[idx++] = ' ';
|
|
||||||
}
|
|
||||||
buf[idx++] = '[';
|
|
||||||
memcpy(&buf[idx], arg, len);
|
|
||||||
idx += len;
|
|
||||||
buf[idx++] = ']';
|
|
||||||
argv++;
|
|
||||||
}
|
|
||||||
assert(idx < bufsize);
|
|
||||||
buf[idx] = '\0';
|
|
||||||
return idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
show_adb_installation_msg(void) {
|
|
||||||
#ifndef __WINDOWS__
|
|
||||||
static const struct {
|
|
||||||
const char *binary;
|
|
||||||
const char *command;
|
|
||||||
} pkg_managers[] = {
|
|
||||||
{"apt", "apt install adb"},
|
|
||||||
{"apt-get", "apt-get install adb"},
|
|
||||||
{"brew", "brew cask install android-platform-tools"},
|
|
||||||
{"dnf", "dnf install android-tools"},
|
|
||||||
{"emerge", "emerge dev-util/android-tools"},
|
|
||||||
{"pacman", "pacman -S android-tools"},
|
|
||||||
};
|
|
||||||
for (size_t i = 0; i < ARRAY_LEN(pkg_managers); ++i) {
|
|
||||||
if (sc_file_executable_exists(pkg_managers[i].binary)) {
|
|
||||||
LOGI("You may install 'adb' by \"%s\"", pkg_managers[i].command);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
show_adb_err_msg(enum sc_process_result err, const char *const argv[]) {
|
|
||||||
#define MAX_COMMAND_STRING_LEN 1024
|
|
||||||
char *buf = malloc(MAX_COMMAND_STRING_LEN);
|
|
||||||
if (!buf) {
|
|
||||||
LOG_OOM();
|
|
||||||
LOGE("Failed to execute");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (err) {
|
|
||||||
case SC_PROCESS_ERROR_GENERIC:
|
|
||||||
argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN);
|
|
||||||
LOGE("Failed to execute: %s", buf);
|
|
||||||
break;
|
|
||||||
case SC_PROCESS_ERROR_MISSING_BINARY:
|
|
||||||
argv_to_string(argv, buf, MAX_COMMAND_STRING_LEN);
|
|
||||||
LOGE("Command not found: %s", buf);
|
|
||||||
LOGE("(make 'adb' accessible from your PATH or define its full"
|
|
||||||
"path in the ADB environment variable)");
|
|
||||||
show_adb_installation_msg();
|
|
||||||
break;
|
|
||||||
case SC_PROCESS_SUCCESS:
|
|
||||||
// do nothing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
free(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
process_check_success_internal(sc_pid pid, const char *name, bool close,
|
|
||||||
unsigned flags) {
|
|
||||||
bool log_errors = !(flags & SC_ADB_NO_LOGERR);
|
|
||||||
|
|
||||||
if (pid == SC_PROCESS_NONE) {
|
|
||||||
if (log_errors) {
|
|
||||||
LOGE("Could not execute \"%s\"", name);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
sc_exit_code exit_code = sc_process_wait(pid, close);
|
|
||||||
if (exit_code) {
|
|
||||||
if (log_errors) {
|
|
||||||
if (exit_code != SC_EXIT_CODE_NONE) {
|
|
||||||
LOGE("\"%s\" returned with value %" SC_PRIexitcode, name,
|
|
||||||
exit_code);
|
|
||||||
} else {
|
|
||||||
LOGE("\"%s\" exited unexpectedly", name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
process_check_success_intr(struct sc_intr *intr, sc_pid pid, const char *name,
|
|
||||||
unsigned flags) {
|
|
||||||
if (intr && !sc_intr_set_process(intr, pid)) {
|
|
||||||
// Already interrupted
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Always pass close=false, interrupting would be racy otherwise
|
|
||||||
bool ret = process_check_success_internal(pid, name, false, flags);
|
|
||||||
|
|
||||||
if (intr) {
|
|
||||||
sc_intr_set_process(intr, SC_PROCESS_NONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close separately
|
|
||||||
sc_process_close(pid);
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
static sc_pid
|
|
||||||
sc_adb_execute_p(const char *const argv[], unsigned flags, sc_pipe *pout) {
|
|
||||||
unsigned process_flags = 0;
|
|
||||||
if (flags & SC_ADB_NO_STDOUT) {
|
|
||||||
process_flags |= SC_PROCESS_NO_STDOUT;
|
|
||||||
}
|
|
||||||
if (flags & SC_ADB_NO_STDERR) {
|
|
||||||
process_flags |= SC_PROCESS_NO_STDERR;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_pid pid;
|
|
||||||
enum sc_process_result r =
|
|
||||||
sc_process_execute_p(argv, &pid, process_flags, NULL, pout, NULL);
|
|
||||||
if (r != SC_PROCESS_SUCCESS) {
|
|
||||||
// If the execution itself failed (not the command exit code), log the
|
|
||||||
// error in all cases
|
|
||||||
show_adb_err_msg(r, argv);
|
|
||||||
pid = SC_PROCESS_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return pid;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_pid
|
|
||||||
sc_adb_execute(const char *const argv[], unsigned flags) {
|
|
||||||
return sc_adb_execute_p(argv, flags, NULL);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_start_server(struct sc_intr *intr, unsigned flags) {
|
|
||||||
const char *const argv[] = SC_ADB_COMMAND("start-server");
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
return process_check_success_intr(intr, pid, "adb start-server", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_kill_server(struct sc_intr *intr, unsigned flags) {
|
|
||||||
const char *const argv[] = SC_ADB_COMMAND("kill-server");
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
return process_check_success_intr(intr, pid, "adb kill-server", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port,
|
|
||||||
const char *device_socket_name, unsigned flags) {
|
|
||||||
char local[4 + 5 + 1]; // tcp:PORT
|
|
||||||
char remote[108 + 14 + 1]; // localabstract:NAME
|
|
||||||
|
|
||||||
int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port);
|
|
||||||
assert(r >= 0 && (size_t) r < sizeof(local));
|
|
||||||
|
|
||||||
r = snprintf(remote, sizeof(remote), "localabstract:%s",
|
|
||||||
device_socket_name);
|
|
||||||
if (r < 0 || (size_t) r >= sizeof(remote)) {
|
|
||||||
LOGE("Could not write socket name");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "forward", local, remote);
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
return process_check_success_intr(intr, pid, "adb forward", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_forward_remove(struct sc_intr *intr, const char *serial,
|
|
||||||
uint16_t local_port, unsigned flags) {
|
|
||||||
char local[4 + 5 + 1]; // tcp:PORT
|
|
||||||
int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port);
|
|
||||||
assert(r >= 0 && (size_t) r < sizeof(local));
|
|
||||||
(void) r;
|
|
||||||
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "forward", "--remove", local);
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
return process_check_success_intr(intr, pid, "adb forward --remove", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_reverse(struct sc_intr *intr, const char *serial,
|
|
||||||
const char *device_socket_name, uint16_t local_port,
|
|
||||||
unsigned flags) {
|
|
||||||
char local[4 + 5 + 1]; // tcp:PORT
|
|
||||||
char remote[108 + 14 + 1]; // localabstract:NAME
|
|
||||||
int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port);
|
|
||||||
assert(r >= 0 && (size_t) r < sizeof(local));
|
|
||||||
|
|
||||||
r = snprintf(remote, sizeof(remote), "localabstract:%s",
|
|
||||||
device_socket_name);
|
|
||||||
if (r < 0 || (size_t) r >= sizeof(remote)) {
|
|
||||||
LOGE("Could not write socket name");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "reverse", remote, local);
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
return process_check_success_intr(intr, pid, "adb reverse", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
|
|
||||||
const char *device_socket_name, unsigned flags) {
|
|
||||||
char remote[108 + 14 + 1]; // localabstract:NAME
|
|
||||||
int r = snprintf(remote, sizeof(remote), "localabstract:%s",
|
|
||||||
device_socket_name);
|
|
||||||
if (r < 0 || (size_t) r >= sizeof(remote)) {
|
|
||||||
LOGE("Device socket name too long");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "reverse", "--remove", remote);
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
return process_check_success_intr(intr, pid, "adb reverse --remove", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
|
|
||||||
const char *remote, unsigned flags) {
|
|
||||||
#ifdef __WINDOWS__
|
|
||||||
// Windows will parse the string, so the paths must be quoted
|
|
||||||
// (see sys/win/command.c)
|
|
||||||
local = sc_str_quote(local);
|
|
||||||
if (!local) {
|
|
||||||
return SC_PROCESS_NONE;
|
|
||||||
}
|
|
||||||
remote = sc_str_quote(remote);
|
|
||||||
if (!remote) {
|
|
||||||
free((void *) local);
|
|
||||||
return SC_PROCESS_NONE;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "push", local, remote);
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
|
|
||||||
#ifdef __WINDOWS__
|
|
||||||
free((void *) remote);
|
|
||||||
free((void *) local);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return process_check_success_intr(intr, pid, "adb push", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
|
|
||||||
unsigned flags) {
|
|
||||||
#ifdef __WINDOWS__
|
|
||||||
// Windows will parse the string, so the local name must be quoted
|
|
||||||
// (see sys/win/command.c)
|
|
||||||
local = sc_str_quote(local);
|
|
||||||
if (!local) {
|
|
||||||
return SC_PROCESS_NONE;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "install", "-r", local);
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
|
|
||||||
#ifdef __WINDOWS__
|
|
||||||
free((void *) local);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
return process_check_success_intr(intr, pid, "adb install", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port,
|
|
||||||
unsigned flags) {
|
|
||||||
char port_string[5 + 1];
|
|
||||||
int r = snprintf(port_string, sizeof(port_string), "%" PRIu16, port);
|
|
||||||
assert(r >= 0 && (size_t) r < sizeof(port_string));
|
|
||||||
(void) r;
|
|
||||||
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "tcpip", port_string);
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
return process_check_success_intr(intr, pid, "adb tcpip", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) {
|
|
||||||
const char *const argv[] = SC_ADB_COMMAND("connect", ip_port);
|
|
||||||
|
|
||||||
sc_pipe pout;
|
|
||||||
sc_pid pid = sc_adb_execute_p(argv, flags, &pout);
|
|
||||||
if (pid == SC_PROCESS_NONE) {
|
|
||||||
LOGE("Could not execute \"adb connect\"");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// "adb connect" always returns successfully (with exit code 0), even in
|
|
||||||
// case of failure. As a workaround, check if its output starts with
|
|
||||||
// "connected" or "already connected".
|
|
||||||
char buf[128];
|
|
||||||
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1);
|
|
||||||
sc_pipe_close(pout);
|
|
||||||
|
|
||||||
bool ok = process_check_success_intr(intr, pid, "adb connect", flags);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r == -1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert((size_t) r < sizeof(buf));
|
|
||||||
buf[r] = '\0';
|
|
||||||
|
|
||||||
ok = !strncmp("connected", buf, sizeof("connected") - 1)
|
|
||||||
|| !strncmp("already connected", buf, sizeof("already connected") - 1);
|
|
||||||
if (!ok && !(flags & SC_ADB_NO_STDERR)) {
|
|
||||||
// "adb connect" also prints errors to stdout. Since we capture it,
|
|
||||||
// re-print the error to stderr.
|
|
||||||
size_t len = strcspn(buf, "\r\n");
|
|
||||||
buf[len] = '\0';
|
|
||||||
fprintf(stderr, "%s\n", buf);
|
|
||||||
}
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags) {
|
|
||||||
assert(ip_port);
|
|
||||||
const char *const argv[] = SC_ADB_COMMAND("disconnect", ip_port);
|
|
||||||
|
|
||||||
sc_pid pid = sc_adb_execute(argv, flags);
|
|
||||||
return process_check_success_intr(intr, pid, "adb disconnect", flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_adb_list_devices(struct sc_intr *intr, unsigned flags,
|
|
||||||
struct sc_vec_adb_devices *out_vec) {
|
|
||||||
const char *const argv[] = SC_ADB_COMMAND("devices", "-l");
|
|
||||||
|
|
||||||
#define BUFSIZE 65536
|
|
||||||
char *buf = malloc(BUFSIZE);
|
|
||||||
if (!buf) {
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_pipe pout;
|
|
||||||
sc_pid pid = sc_adb_execute_p(argv, flags, &pout);
|
|
||||||
if (pid == SC_PROCESS_NONE) {
|
|
||||||
LOGE("Could not execute \"adb devices -l\"");
|
|
||||||
free(buf);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, BUFSIZE - 1);
|
|
||||||
sc_pipe_close(pout);
|
|
||||||
|
|
||||||
bool ok = process_check_success_intr(intr, pid, "adb devices -l", flags);
|
|
||||||
if (!ok) {
|
|
||||||
free(buf);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r == -1) {
|
|
||||||
free(buf);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert((size_t) r < BUFSIZE);
|
|
||||||
if (r == BUFSIZE - 1) {
|
|
||||||
// The implementation assumes that the output of "adb devices -l" fits
|
|
||||||
// in the buffer in a single pass
|
|
||||||
LOGW("Result of \"adb devices -l\" does not fit in 64Kb. "
|
|
||||||
"Please report an issue.");
|
|
||||||
free(buf);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is parsed as a NUL-terminated string
|
|
||||||
buf[r] = '\0';
|
|
||||||
|
|
||||||
// List all devices to the output list directly
|
|
||||||
ok = sc_adb_parse_devices(buf, out_vec);
|
|
||||||
free(buf);
|
|
||||||
return ok;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_adb_accept_device(const struct sc_adb_device *device,
|
|
||||||
const struct sc_adb_device_selector *selector) {
|
|
||||||
switch (selector->type) {
|
|
||||||
case SC_ADB_DEVICE_SELECT_ALL:
|
|
||||||
return true;
|
|
||||||
case SC_ADB_DEVICE_SELECT_SERIAL:
|
|
||||||
assert(selector->serial);
|
|
||||||
char *device_serial_colon = strchr(device->serial, ':');
|
|
||||||
if (device_serial_colon) {
|
|
||||||
// The device serial is an IP:port...
|
|
||||||
char *serial_colon = strchr(selector->serial, ':');
|
|
||||||
if (!serial_colon) {
|
|
||||||
// But the requested serial has no ':', so only consider
|
|
||||||
// the IP part of the device serial. This allows to use
|
|
||||||
// "192.168.1.1" to match any "192.168.1.1:port".
|
|
||||||
size_t serial_len = strlen(selector->serial);
|
|
||||||
size_t device_ip_len = device_serial_colon - device->serial;
|
|
||||||
if (serial_len != device_ip_len) {
|
|
||||||
// They are not equal, they don't even have the same
|
|
||||||
// length
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return !strncmp(selector->serial, device->serial,
|
|
||||||
device_ip_len);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return !strcmp(selector->serial, device->serial);
|
|
||||||
case SC_ADB_DEVICE_SELECT_USB:
|
|
||||||
return sc_adb_device_get_type(device->serial) ==
|
|
||||||
SC_ADB_DEVICE_TYPE_USB;
|
|
||||||
case SC_ADB_DEVICE_SELECT_TCPIP:
|
|
||||||
// Both emulators and TCP/IP devices are selected via -e
|
|
||||||
return sc_adb_device_get_type(device->serial) !=
|
|
||||||
SC_ADB_DEVICE_TYPE_USB;
|
|
||||||
default:
|
|
||||||
assert(!"Missing SC_ADB_DEVICE_SELECT_* handling");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static size_t
|
|
||||||
sc_adb_devices_select(struct sc_adb_device *devices, size_t len,
|
|
||||||
const struct sc_adb_device_selector *selector,
|
|
||||||
size_t *idx_out) {
|
|
||||||
size_t count = 0;
|
|
||||||
for (size_t i = 0; i < len; ++i) {
|
|
||||||
struct sc_adb_device *device = &devices[i];
|
|
||||||
device->selected = sc_adb_accept_device(device, selector);
|
|
||||||
if (device->selected) {
|
|
||||||
if (idx_out && !count) {
|
|
||||||
*idx_out = i;
|
|
||||||
}
|
|
||||||
++count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return count;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_adb_devices_log(enum sc_log_level level, struct sc_adb_device *devices,
|
|
||||||
size_t count) {
|
|
||||||
for (size_t i = 0; i < count; ++i) {
|
|
||||||
struct sc_adb_device *d = &devices[i];
|
|
||||||
const char *selection = d->selected ? "-->" : " ";
|
|
||||||
bool is_usb =
|
|
||||||
sc_adb_device_get_type(d->serial) == SC_ADB_DEVICE_TYPE_USB;
|
|
||||||
const char *type = is_usb ? " (usb)"
|
|
||||||
: "(tcpip)";
|
|
||||||
LOG(level, " %s %s %-20s %16s %s",
|
|
||||||
selection, type, d->serial, d->state, d->model ? d->model : "");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_adb_device_check_state(struct sc_adb_device *device,
|
|
||||||
struct sc_adb_device *devices, size_t count) {
|
|
||||||
const char *state = device->state;
|
|
||||||
|
|
||||||
if (!strcmp("device", state)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!strcmp("unauthorized", state)) {
|
|
||||||
LOGE("Device is unauthorized:");
|
|
||||||
sc_adb_devices_log(SC_LOG_LEVEL_ERROR, devices, count);
|
|
||||||
LOGE("A popup should open on the device to request authorization.");
|
|
||||||
LOGE("Check the FAQ: "
|
|
||||||
"<https://github.com/Genymobile/scrcpy/blob/master/FAQ.md>");
|
|
||||||
} else {
|
|
||||||
LOGE("Device could not be connected (state=%s)", state);
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_select_device(struct sc_intr *intr,
|
|
||||||
const struct sc_adb_device_selector *selector,
|
|
||||||
unsigned flags, struct sc_adb_device *out_device) {
|
|
||||||
struct sc_vec_adb_devices vec = SC_VECTOR_INITIALIZER;
|
|
||||||
bool ok = sc_adb_list_devices(intr, flags, &vec);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Could not list ADB devices");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (vec.size == 0) {
|
|
||||||
LOGE("Could not find any ADB device");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t sel_idx; // index of the single matching device if sel_count == 1
|
|
||||||
size_t sel_count =
|
|
||||||
sc_adb_devices_select(vec.data, vec.size, selector, &sel_idx);
|
|
||||||
|
|
||||||
if (sel_count == 0) {
|
|
||||||
// if count > 0 && sel_count == 0, then necessarily a selection is
|
|
||||||
// requested
|
|
||||||
assert(selector->type != SC_ADB_DEVICE_SELECT_ALL);
|
|
||||||
|
|
||||||
switch (selector->type) {
|
|
||||||
case SC_ADB_DEVICE_SELECT_SERIAL:
|
|
||||||
assert(selector->serial);
|
|
||||||
LOGE("Could not find ADB device %s:", selector->serial);
|
|
||||||
break;
|
|
||||||
case SC_ADB_DEVICE_SELECT_USB:
|
|
||||||
LOGE("Could not find any ADB device over USB:");
|
|
||||||
break;
|
|
||||||
case SC_ADB_DEVICE_SELECT_TCPIP:
|
|
||||||
LOGE("Could not find any ADB device over TCP/IP:");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
assert(!"Unexpected selector type");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size);
|
|
||||||
sc_adb_devices_destroy(&vec);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sel_count > 1) {
|
|
||||||
switch (selector->type) {
|
|
||||||
case SC_ADB_DEVICE_SELECT_ALL:
|
|
||||||
LOGE("Multiple (%" SC_PRIsizet ") ADB devices:", sel_count);
|
|
||||||
break;
|
|
||||||
case SC_ADB_DEVICE_SELECT_SERIAL:
|
|
||||||
assert(selector->serial);
|
|
||||||
LOGE("Multiple (%" SC_PRIsizet ") ADB devices with serial %s:",
|
|
||||||
sel_count, selector->serial);
|
|
||||||
break;
|
|
||||||
case SC_ADB_DEVICE_SELECT_USB:
|
|
||||||
LOGE("Multiple (%" SC_PRIsizet ") ADB devices over USB:",
|
|
||||||
sel_count);
|
|
||||||
break;
|
|
||||||
case SC_ADB_DEVICE_SELECT_TCPIP:
|
|
||||||
LOGE("Multiple (%" SC_PRIsizet ") ADB devices over TCP/IP:",
|
|
||||||
sel_count);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
assert(!"Unexpected selector type");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
sc_adb_devices_log(SC_LOG_LEVEL_ERROR, vec.data, vec.size);
|
|
||||||
LOGE("Select a device via -s (--serial), -d (--select-usb) or -e "
|
|
||||||
"(--select-tcpip)");
|
|
||||||
sc_adb_devices_destroy(&vec);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sel_count == 1); // sel_idx is valid only if sel_count == 1
|
|
||||||
struct sc_adb_device *device = &vec.data[sel_idx];
|
|
||||||
|
|
||||||
ok = sc_adb_device_check_state(device, vec.data, vec.size);
|
|
||||||
if (!ok) {
|
|
||||||
sc_adb_devices_destroy(&vec);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGI("ADB device found:");
|
|
||||||
sc_adb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size);
|
|
||||||
|
|
||||||
// Move devics into out_device (do not destroy device)
|
|
||||||
sc_adb_device_move(out_device, device);
|
|
||||||
sc_adb_devices_destroy(&vec);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *
|
|
||||||
sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
|
|
||||||
unsigned flags) {
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "shell", "getprop", prop);
|
|
||||||
|
|
||||||
sc_pipe pout;
|
|
||||||
sc_pid pid = sc_adb_execute_p(argv, flags, &pout);
|
|
||||||
if (pid == SC_PROCESS_NONE) {
|
|
||||||
LOGE("Could not execute \"adb getprop\"");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
char buf[128];
|
|
||||||
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1);
|
|
||||||
sc_pipe_close(pout);
|
|
||||||
|
|
||||||
bool ok = process_check_success_intr(intr, pid, "adb getprop", flags);
|
|
||||||
if (!ok) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert((size_t) r < sizeof(buf));
|
|
||||||
buf[r] = '\0';
|
|
||||||
size_t len = strcspn(buf, " \r\n");
|
|
||||||
buf[len] = '\0';
|
|
||||||
|
|
||||||
return strdup(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
char *
|
|
||||||
sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) {
|
|
||||||
assert(serial);
|
|
||||||
const char *const argv[] =
|
|
||||||
SC_ADB_COMMAND("-s", serial, "shell", "ip", "route");
|
|
||||||
|
|
||||||
sc_pipe pout;
|
|
||||||
sc_pid pid = sc_adb_execute_p(argv, flags, &pout);
|
|
||||||
if (pid == SC_PROCESS_NONE) {
|
|
||||||
LOGD("Could not execute \"ip route\"");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// "adb shell ip route" output should contain only a few lines
|
|
||||||
char buf[1024];
|
|
||||||
ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1);
|
|
||||||
sc_pipe_close(pout);
|
|
||||||
|
|
||||||
bool ok = process_check_success_intr(intr, pid, "ip route", flags);
|
|
||||||
if (!ok) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (r == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert((size_t) r < sizeof(buf));
|
|
||||||
if (r == sizeof(buf) - 1) {
|
|
||||||
// The implementation assumes that the output of "ip route" fits in the
|
|
||||||
// buffer in a single pass
|
|
||||||
LOGW("Result of \"ip route\" does not fit in 1Kb. "
|
|
||||||
"Please report an issue.");
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// It is parsed as a NUL-terminated string
|
|
||||||
buf[r] = '\0';
|
|
||||||
|
|
||||||
return sc_adb_parse_device_ip(buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t
|
|
||||||
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) {
|
|
||||||
char *sdk_version =
|
|
||||||
sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT);
|
|
||||||
if (!sdk_version) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
long value;
|
|
||||||
bool ok = sc_str_parse_integer(sdk_version, &value);
|
|
||||||
free(sdk_version);
|
|
||||||
if (!ok || value < 0 || value > 0xFFFF) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return value;
|
|
||||||
}
|
|
@ -1,129 +0,0 @@
|
|||||||
#ifndef SC_ADB_H
|
|
||||||
#define SC_ADB_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <inttypes.h>
|
|
||||||
|
|
||||||
#include "adb_device.h"
|
|
||||||
#include "util/intr.h"
|
|
||||||
|
|
||||||
#define SC_ADB_NO_STDOUT (1 << 0)
|
|
||||||
#define SC_ADB_NO_STDERR (1 << 1)
|
|
||||||
#define SC_ADB_NO_LOGERR (1 << 2)
|
|
||||||
|
|
||||||
#define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR)
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_init(void);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_adb_destroy(void);
|
|
||||||
|
|
||||||
const char *
|
|
||||||
sc_adb_get_executable(void);
|
|
||||||
|
|
||||||
enum sc_adb_device_selector_type {
|
|
||||||
SC_ADB_DEVICE_SELECT_ALL,
|
|
||||||
SC_ADB_DEVICE_SELECT_SERIAL,
|
|
||||||
SC_ADB_DEVICE_SELECT_USB,
|
|
||||||
SC_ADB_DEVICE_SELECT_TCPIP,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_adb_device_selector {
|
|
||||||
enum sc_adb_device_selector_type type;
|
|
||||||
const char *serial;
|
|
||||||
};
|
|
||||||
|
|
||||||
sc_pid
|
|
||||||
sc_adb_execute(const char *const argv[], unsigned flags);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_start_server(struct sc_intr *intr, unsigned flags);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_kill_server(struct sc_intr *intr, unsigned flags);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port,
|
|
||||||
const char *device_socket_name, unsigned flags);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_forward_remove(struct sc_intr *intr, const char *serial,
|
|
||||||
uint16_t local_port, unsigned flags);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_reverse(struct sc_intr *intr, const char *serial,
|
|
||||||
const char *device_socket_name, uint16_t local_port,
|
|
||||||
unsigned flags);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
|
|
||||||
const char *device_socket_name, unsigned flags);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_push(struct sc_intr *intr, const char *serial, const char *local,
|
|
||||||
const char *remote, unsigned flags);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_install(struct sc_intr *intr, const char *serial, const char *local,
|
|
||||||
unsigned flags);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute `adb tcpip <port>`
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port,
|
|
||||||
unsigned flags);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute `adb connect <ip_port>`
|
|
||||||
*
|
|
||||||
* `ip_port` may not be NULL.
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute `adb disconnect [<ip_port>]`
|
|
||||||
*
|
|
||||||
* If `ip_port` is NULL, execute `adb disconnect`.
|
|
||||||
* Otherwise, execute `adb disconnect <ip_port>`.
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
sc_adb_disconnect(struct sc_intr *intr, const char *ip_port, unsigned flags);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute `adb devices` and parse the result to select a device
|
|
||||||
*
|
|
||||||
* Return true if a single matching device is found, and write it to out_device.
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
sc_adb_select_device(struct sc_intr *intr,
|
|
||||||
const struct sc_adb_device_selector *selector,
|
|
||||||
unsigned flags, struct sc_adb_device *out_device);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute `adb getprop <prop>`
|
|
||||||
*/
|
|
||||||
char *
|
|
||||||
sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop,
|
|
||||||
unsigned flags);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to retrieve the device IP
|
|
||||||
*
|
|
||||||
* Return the IP as a string of the form "xxx.xxx.xxx.xxx", to be freed by the
|
|
||||||
* caller, or NULL on error.
|
|
||||||
*/
|
|
||||||
char *
|
|
||||||
sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the device SDK version.
|
|
||||||
*/
|
|
||||||
uint16_t
|
|
||||||
sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,43 +0,0 @@
|
|||||||
#include "adb_device.h"
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_adb_device_destroy(struct sc_adb_device *device) {
|
|
||||||
free(device->serial);
|
|
||||||
free(device->state);
|
|
||||||
free(device->model);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src) {
|
|
||||||
*dst = *src;
|
|
||||||
src->serial = NULL;
|
|
||||||
src->state = NULL;
|
|
||||||
src->model = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_adb_devices_destroy(struct sc_vec_adb_devices *devices) {
|
|
||||||
for (size_t i = 0; i < devices->size; ++i) {
|
|
||||||
sc_adb_device_destroy(&devices->data[i]);
|
|
||||||
}
|
|
||||||
sc_vector_destroy(devices);
|
|
||||||
}
|
|
||||||
|
|
||||||
enum sc_adb_device_type
|
|
||||||
sc_adb_device_get_type(const char *serial) {
|
|
||||||
// Starts with "emulator-"
|
|
||||||
if (!strncmp(serial, "emulator-", sizeof("emulator-") - 1)) {
|
|
||||||
return SC_ADB_DEVICE_TYPE_EMULATOR;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the serial contains a ':', then it is a TCP/IP device (it is
|
|
||||||
// sufficient to distinguish an ip:port from a real USB serial)
|
|
||||||
if (strchr(serial, ':')) {
|
|
||||||
return SC_ADB_DEVICE_TYPE_TCPIP;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SC_ADB_DEVICE_TYPE_USB;
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
#ifndef SC_ADB_DEVICE_H
|
|
||||||
#define SC_ADB_DEVICE_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
#include "util/vector.h"
|
|
||||||
|
|
||||||
struct sc_adb_device {
|
|
||||||
char *serial;
|
|
||||||
char *state;
|
|
||||||
char *model;
|
|
||||||
bool selected;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum sc_adb_device_type {
|
|
||||||
SC_ADB_DEVICE_TYPE_USB,
|
|
||||||
SC_ADB_DEVICE_TYPE_TCPIP,
|
|
||||||
SC_ADB_DEVICE_TYPE_EMULATOR,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_vec_adb_devices SC_VECTOR(struct sc_adb_device);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_adb_device_destroy(struct sc_adb_device *device);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Move src to dst
|
|
||||||
*
|
|
||||||
* After this call, the content of src is undefined, except that
|
|
||||||
* sc_adb_device_destroy() can be called.
|
|
||||||
*
|
|
||||||
* This is useful to take a device from a list that will be destroyed, without
|
|
||||||
* making unnecessary copies.
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
sc_adb_device_move(struct sc_adb_device *dst, struct sc_adb_device *src);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_adb_devices_destroy(struct sc_vec_adb_devices *devices);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deduce the device type from the serial
|
|
||||||
*/
|
|
||||||
enum sc_adb_device_type
|
|
||||||
sc_adb_device_get_type(const char *serial);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,228 +0,0 @@
|
|||||||
#include "adb_parser.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
#include "util/str.h"
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_adb_parse_device(char *line, struct sc_adb_device *device) {
|
|
||||||
// One device line looks like:
|
|
||||||
// "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "
|
|
||||||
// "device:MyDevice transport_id:1"
|
|
||||||
|
|
||||||
if (line[0] == '*') {
|
|
||||||
// Garbage lines printed by adb daemon while starting start with a '*'
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!strncmp("adb server", line, sizeof("adb server") - 1)) {
|
|
||||||
// Ignore lines starting with "adb server":
|
|
||||||
// adb server version (41) doesn't match this client (39); killing...
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
char *s = line; // cursor in the line
|
|
||||||
|
|
||||||
// After the serial:
|
|
||||||
// - "adb devices" writes a single '\t'
|
|
||||||
// - "adb devices -l" writes multiple spaces
|
|
||||||
// For flexibility, accept both.
|
|
||||||
size_t serial_len = strcspn(s, " \t");
|
|
||||||
if (!serial_len) {
|
|
||||||
// empty serial
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
bool eol = s[serial_len] == '\0';
|
|
||||||
if (eol) {
|
|
||||||
// serial alone is unexpected
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
s[serial_len] = '\0';
|
|
||||||
char *serial = s;
|
|
||||||
s += serial_len + 1;
|
|
||||||
// After the serial, there might be several spaces
|
|
||||||
s += strspn(s, " \t"); // consume all separators
|
|
||||||
|
|
||||||
size_t state_len = strcspn(s, " ");
|
|
||||||
if (!state_len) {
|
|
||||||
// empty state
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
eol = s[state_len] == '\0';
|
|
||||||
s[state_len] = '\0';
|
|
||||||
char *state = s;
|
|
||||||
|
|
||||||
char *model = NULL;
|
|
||||||
if (!eol) {
|
|
||||||
s += state_len + 1;
|
|
||||||
|
|
||||||
// Iterate over all properties "key:value key:value ..."
|
|
||||||
for (;;) {
|
|
||||||
size_t token_len = strcspn(s, " ");
|
|
||||||
if (!token_len) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
eol = s[token_len] == '\0';
|
|
||||||
s[token_len] = '\0';
|
|
||||||
char *token = s;
|
|
||||||
|
|
||||||
if (!strncmp("model:", token, sizeof("model:") - 1)) {
|
|
||||||
model = &token[sizeof("model:") - 1];
|
|
||||||
// We only need the model
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eol) {
|
|
||||||
break;
|
|
||||||
} else {
|
|
||||||
s+= token_len + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
device->serial = strdup(serial);
|
|
||||||
if (!device->serial) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
device->state = strdup(state);
|
|
||||||
if (!device->state) {
|
|
||||||
free(device->serial);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (model) {
|
|
||||||
device->model = strdup(model);
|
|
||||||
if (!device->model) {
|
|
||||||
LOG_OOM();
|
|
||||||
// model is optional, do not fail
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
device->model = NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
device->selected = false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec) {
|
|
||||||
#define HEADER "List of devices attached"
|
|
||||||
#define HEADER_LEN (sizeof(HEADER) - 1)
|
|
||||||
bool header_found = false;
|
|
||||||
|
|
||||||
size_t idx_line = 0;
|
|
||||||
while (str[idx_line] != '\0') {
|
|
||||||
char *line = &str[idx_line];
|
|
||||||
size_t len = strcspn(line, "\n");
|
|
||||||
|
|
||||||
// The next line starts after the '\n' (replaced by `\0`)
|
|
||||||
idx_line += len;
|
|
||||||
|
|
||||||
if (str[idx_line] != '\0') {
|
|
||||||
// The next line starts after the '\n'
|
|
||||||
++idx_line;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!header_found) {
|
|
||||||
if (!strncmp(line, HEADER, HEADER_LEN)) {
|
|
||||||
header_found = true;
|
|
||||||
}
|
|
||||||
// Skip everything until the header, there might be garbage lines
|
|
||||||
// related to daemon starting before
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The line, but without any trailing '\r'
|
|
||||||
size_t line_len = sc_str_remove_trailing_cr(line, len);
|
|
||||||
line[line_len] = '\0';
|
|
||||||
|
|
||||||
struct sc_adb_device device;
|
|
||||||
bool ok = sc_adb_parse_device(line, &device);
|
|
||||||
if (!ok) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_vector_push(out_vec, device);
|
|
||||||
if (!ok) {
|
|
||||||
LOG_OOM();
|
|
||||||
LOGE("Could not push adb_device to vector");
|
|
||||||
sc_adb_device_destroy(&device);
|
|
||||||
// continue anyway
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(header_found || out_vec->size == 0);
|
|
||||||
return header_found;
|
|
||||||
}
|
|
||||||
|
|
||||||
static char *
|
|
||||||
sc_adb_parse_device_ip_from_line(char *line) {
|
|
||||||
// One line from "ip route" looks like:
|
|
||||||
// "192.168.1.0/24 dev wlan0 proto kernel scope link src 192.168.1.x"
|
|
||||||
|
|
||||||
// Get the location of the device name (index of "wlan0" in the example)
|
|
||||||
ssize_t idx_dev_name = sc_str_index_of_column(line, 2, " ");
|
|
||||||
if (idx_dev_name == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the location of the ip address (column 8, but column 6 if we start
|
|
||||||
// from column 2). Must be computed before truncating individual columns.
|
|
||||||
ssize_t idx_ip = sc_str_index_of_column(&line[idx_dev_name], 6, " ");
|
|
||||||
if (idx_ip == -1) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
// idx_ip is searched from &line[idx_dev_name]
|
|
||||||
idx_ip += idx_dev_name;
|
|
||||||
|
|
||||||
char *dev_name = &line[idx_dev_name];
|
|
||||||
size_t dev_name_len = strcspn(dev_name, " \t");
|
|
||||||
dev_name[dev_name_len] = '\0';
|
|
||||||
|
|
||||||
char *ip = &line[idx_ip];
|
|
||||||
size_t ip_len = strcspn(ip, " \t");
|
|
||||||
ip[ip_len] = '\0';
|
|
||||||
|
|
||||||
// Only consider lines where the device name starts with "wlan"
|
|
||||||
if (strncmp(dev_name, "wlan", sizeof("wlan") - 1)) {
|
|
||||||
LOGD("Device ip lookup: ignoring %s (%s)", ip, dev_name);
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
return strdup(ip);
|
|
||||||
}
|
|
||||||
|
|
||||||
char *
|
|
||||||
sc_adb_parse_device_ip(char *str) {
|
|
||||||
size_t idx_line = 0;
|
|
||||||
while (str[idx_line] != '\0') {
|
|
||||||
char *line = &str[idx_line];
|
|
||||||
size_t len = strcspn(line, "\n");
|
|
||||||
bool is_last_line = line[len] == '\0';
|
|
||||||
|
|
||||||
// The same, but without any trailing '\r'
|
|
||||||
size_t line_len = sc_str_remove_trailing_cr(line, len);
|
|
||||||
line[line_len] = '\0';
|
|
||||||
|
|
||||||
char *ip = sc_adb_parse_device_ip_from_line(line);
|
|
||||||
if (ip) {
|
|
||||||
// Found
|
|
||||||
return ip;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (is_last_line) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The next line starts after the '\n'
|
|
||||||
idx_line += len + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return NULL;
|
|
||||||
}
|
|
@ -1,30 +0,0 @@
|
|||||||
#ifndef SC_ADB_PARSER_H
|
|
||||||
#define SC_ADB_PARSER_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
#include "adb_device.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the available devices from the output of `adb devices`
|
|
||||||
*
|
|
||||||
* The parameter must be a NUL-terminated string.
|
|
||||||
*
|
|
||||||
* Warning: this function modifies the buffer for optimization purposes.
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
sc_adb_parse_devices(char *str, struct sc_vec_adb_devices *out_vec);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Parse the ip from the output of `adb shell ip route`
|
|
||||||
*
|
|
||||||
* The parameter must be a NUL-terminated string.
|
|
||||||
*
|
|
||||||
* Warning: this function modifies the buffer for optimization purposes.
|
|
||||||
*/
|
|
||||||
char *
|
|
||||||
sc_adb_parse_device_ip(char *str);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,172 +0,0 @@
|
|||||||
#include "adb_tunnel.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include "adb.h"
|
|
||||||
#include "util/log.h"
|
|
||||||
#include "util/net_intr.h"
|
|
||||||
#include "util/process_intr.h"
|
|
||||||
|
|
||||||
static bool
|
|
||||||
listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) {
|
|
||||||
return net_listen_intr(intr, socket, IPV4_LOCALHOST, port, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
enable_tunnel_reverse_any_port(struct sc_adb_tunnel *tunnel,
|
|
||||||
struct sc_intr *intr, const char *serial,
|
|
||||||
const char *device_socket_name,
|
|
||||||
struct sc_port_range port_range) {
|
|
||||||
uint16_t port = port_range.first;
|
|
||||||
for (;;) {
|
|
||||||
if (!sc_adb_reverse(intr, serial, device_socket_name, port,
|
|
||||||
SC_ADB_NO_STDOUT)) {
|
|
||||||
// the command itself failed, it will fail on any port
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// At the application level, the device part is "the server" because it
|
|
||||||
// serves video stream and control. However, at the network level, the
|
|
||||||
// client listens and the server connects to the client. That way, the
|
|
||||||
// client can listen before starting the server app, so there is no
|
|
||||||
// need to try to connect until the server socket is listening on the
|
|
||||||
// device.
|
|
||||||
sc_socket server_socket = net_socket();
|
|
||||||
if (server_socket != SC_SOCKET_NONE) {
|
|
||||||
bool ok = listen_on_port(intr, server_socket, port);
|
|
||||||
if (ok) {
|
|
||||||
// success
|
|
||||||
tunnel->server_socket = server_socket;
|
|
||||||
tunnel->local_port = port;
|
|
||||||
tunnel->enabled = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
net_close(server_socket);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sc_intr_is_interrupted(intr)) {
|
|
||||||
// Stop immediately
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// failure, disable tunnel and try another port
|
|
||||||
if (!sc_adb_reverse_remove(intr, serial, device_socket_name,
|
|
||||||
SC_ADB_NO_STDOUT)) {
|
|
||||||
LOGW("Could not remove reverse tunnel on port %" PRIu16, port);
|
|
||||||
}
|
|
||||||
|
|
||||||
// check before incrementing to avoid overflow on port 65535
|
|
||||||
if (port < port_range.last) {
|
|
||||||
LOGW("Could not listen on port %" PRIu16", retrying on %" PRIu16,
|
|
||||||
port, (uint16_t) (port + 1));
|
|
||||||
port++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port_range.first == port_range.last) {
|
|
||||||
LOGE("Could not listen on port %" PRIu16, port_range.first);
|
|
||||||
} else {
|
|
||||||
LOGE("Could not listen on any port in range %" PRIu16 ":%" PRIu16,
|
|
||||||
port_range.first, port_range.last);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
enable_tunnel_forward_any_port(struct sc_adb_tunnel *tunnel,
|
|
||||||
struct sc_intr *intr, const char *serial,
|
|
||||||
const char *device_socket_name,
|
|
||||||
struct sc_port_range port_range) {
|
|
||||||
tunnel->forward = true;
|
|
||||||
|
|
||||||
uint16_t port = port_range.first;
|
|
||||||
for (;;) {
|
|
||||||
if (sc_adb_forward(intr, serial, port, device_socket_name,
|
|
||||||
SC_ADB_NO_STDOUT)) {
|
|
||||||
// success
|
|
||||||
tunnel->local_port = port;
|
|
||||||
tunnel->enabled = true;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sc_intr_is_interrupted(intr)) {
|
|
||||||
// Stop immediately
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port < port_range.last) {
|
|
||||||
LOGW("Could not forward port %" PRIu16", retrying on %" PRIu16,
|
|
||||||
port, (uint16_t) (port + 1));
|
|
||||||
port++;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port_range.first == port_range.last) {
|
|
||||||
LOGE("Could not forward port %" PRIu16, port_range.first);
|
|
||||||
} else {
|
|
||||||
LOGE("Could not forward any port in range %" PRIu16 ":%" PRIu16,
|
|
||||||
port_range.first, port_range.last);
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel) {
|
|
||||||
tunnel->enabled = false;
|
|
||||||
tunnel->forward = false;
|
|
||||||
tunnel->server_socket = SC_SOCKET_NONE;
|
|
||||||
tunnel->local_port = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
|
||||||
const char *serial, const char *device_socket_name,
|
|
||||||
struct sc_port_range port_range, bool force_adb_forward) {
|
|
||||||
assert(!tunnel->enabled);
|
|
||||||
|
|
||||||
if (!force_adb_forward) {
|
|
||||||
// Attempt to use "adb reverse"
|
|
||||||
if (enable_tunnel_reverse_any_port(tunnel, intr, serial,
|
|
||||||
device_socket_name, port_range)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if "adb reverse" does not work (e.g. over "adb connect"), it
|
|
||||||
// fallbacks to "adb forward", so the app socket is the client
|
|
||||||
|
|
||||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
|
||||||
}
|
|
||||||
|
|
||||||
return enable_tunnel_forward_any_port(tunnel, intr, serial,
|
|
||||||
device_socket_name, port_range);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
|
||||||
const char *serial, const char *device_socket_name) {
|
|
||||||
assert(tunnel->enabled);
|
|
||||||
|
|
||||||
bool ret;
|
|
||||||
if (tunnel->forward) {
|
|
||||||
ret = sc_adb_forward_remove(intr, serial, tunnel->local_port,
|
|
||||||
SC_ADB_NO_STDOUT);
|
|
||||||
} else {
|
|
||||||
ret = sc_adb_reverse_remove(intr, serial, device_socket_name,
|
|
||||||
SC_ADB_NO_STDOUT);
|
|
||||||
|
|
||||||
assert(tunnel->server_socket != SC_SOCKET_NONE);
|
|
||||||
if (!net_close(tunnel->server_socket)) {
|
|
||||||
LOGW("Could not close server socket");
|
|
||||||
}
|
|
||||||
|
|
||||||
// server_socket is never used anymore
|
|
||||||
}
|
|
||||||
|
|
||||||
// Consider tunnel disabled even if the command failed
|
|
||||||
tunnel->enabled = false;
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
@ -1,47 +0,0 @@
|
|||||||
#ifndef SC_ADB_TUNNEL_H
|
|
||||||
#define SC_ADB_TUNNEL_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#include "options.h"
|
|
||||||
#include "util/intr.h"
|
|
||||||
#include "util/net.h"
|
|
||||||
|
|
||||||
struct sc_adb_tunnel {
|
|
||||||
bool enabled;
|
|
||||||
bool forward; // use "adb forward" instead of "adb reverse"
|
|
||||||
sc_socket server_socket; // only used if !forward
|
|
||||||
uint16_t local_port;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the adb tunnel struct to default values
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
sc_adb_tunnel_init(struct sc_adb_tunnel *tunnel);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Open a tunnel
|
|
||||||
*
|
|
||||||
* Blocking calls may be interrupted asynchronously via `intr`.
|
|
||||||
*
|
|
||||||
* If `force_adb_forward` is not set, then attempts to set up an "adb reverse"
|
|
||||||
* tunnel first. Only if it fails (typical on old Android version connected via
|
|
||||||
* TCP/IP), use "adb forward".
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
sc_adb_tunnel_open(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
|
||||||
const char *serial, const char *device_socket_name,
|
|
||||||
struct sc_port_range port_range, bool force_adb_forward);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Close the tunnel
|
|
||||||
*/
|
|
||||||
bool
|
|
||||||
sc_adb_tunnel_close(struct sc_adb_tunnel *tunnel, struct sc_intr *intr,
|
|
||||||
const char *serial, const char *device_socket_name);
|
|
||||||
|
|
||||||
#endif
|
|
@ -21,7 +21,7 @@
|
|||||||
#define _ANDROID_INPUT_H
|
#define _ANDROID_INPUT_H
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Meta key / modifier state.
|
* Meta key / modifer state.
|
||||||
*/
|
*/
|
||||||
enum android_metastate {
|
enum android_metastate {
|
||||||
/** No meta keys are pressed. */
|
/** No meta keys are pressed. */
|
||||||
@ -777,6 +777,9 @@ enum android_input_source {
|
|||||||
AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK,
|
AINPUT_SOURCE_JOYSTICK = 0x01000000 | AINPUT_SOURCE_CLASS_JOYSTICK,
|
||||||
/** rotary encoder */
|
/** rotary encoder */
|
||||||
AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE,
|
AINPUT_SOURCE_ROTARY_ENCODER = 0x00400000 | AINPUT_SOURCE_CLASS_NONE,
|
||||||
|
|
||||||
|
/** any */
|
||||||
|
AINPUT_SOURCE_ANY = 0xffffff00,
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -633,7 +633,7 @@ enum android_keycode {
|
|||||||
* Toggles between BS and CS digital satellite services. */
|
* Toggles between BS and CS digital satellite services. */
|
||||||
AKEYCODE_TV_SATELLITE_SERVICE = 240,
|
AKEYCODE_TV_SATELLITE_SERVICE = 240,
|
||||||
/** Toggle Network key.
|
/** Toggle Network key.
|
||||||
* Toggles selecting broadcast services. */
|
* Toggles selecting broacast services. */
|
||||||
AKEYCODE_TV_NETWORK = 241,
|
AKEYCODE_TV_NETWORK = 241,
|
||||||
/** Antenna/Cable key.
|
/** Antenna/Cable key.
|
||||||
* Toggles broadcast input source between antenna and cable. */
|
* Toggles broadcast input source between antenna and cable. */
|
||||||
|
206
app/src/aoa.c
Normal file
206
app/src/aoa.c
Normal file
@ -0,0 +1,206 @@
|
|||||||
|
#include "aoa.h"
|
||||||
|
|
||||||
|
#include "command.h" // must be first to include "winsock2.h" before "windows.h"
|
||||||
|
#include <libusb-1.0/libusb.h>
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
// <https://source.android.com/devices/accessories/aoa2>
|
||||||
|
#define AOA_GET_PROTOCOL 51
|
||||||
|
#define AOA_START_ACCESSORY 53
|
||||||
|
#define AOA_SET_AUDIO_MODE 58
|
||||||
|
|
||||||
|
#define AUDIO_MODE_NO_AUDIO 0
|
||||||
|
#define AUDIO_MODE_S16LSB_STEREO_44100HZ 1
|
||||||
|
|
||||||
|
#define DEFAULT_TIMEOUT 1000
|
||||||
|
|
||||||
|
typedef struct control_params {
|
||||||
|
uint8_t request_type;
|
||||||
|
uint8_t request;
|
||||||
|
uint16_t value;
|
||||||
|
uint16_t index;
|
||||||
|
unsigned char *data;
|
||||||
|
uint16_t length;
|
||||||
|
unsigned int timeout;
|
||||||
|
} control_params;
|
||||||
|
|
||||||
|
static void log_libusb_error(enum libusb_error errcode) {
|
||||||
|
LOGE("%s", libusb_strerror(errcode));
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_bool control_transfer(libusb_device_handle *handle, control_params *params) {
|
||||||
|
int r = libusb_control_transfer(handle,
|
||||||
|
params->request_type,
|
||||||
|
params->request,
|
||||||
|
params->value,
|
||||||
|
params->index,
|
||||||
|
params->data,
|
||||||
|
params->length,
|
||||||
|
params->timeout);
|
||||||
|
if (r < 0) {
|
||||||
|
log_libusb_error(r);
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_bool get_serial(libusb_device *device, struct libusb_device_descriptor *desc, unsigned char *data, int length) {
|
||||||
|
|
||||||
|
libusb_device_handle *handle;
|
||||||
|
int r;
|
||||||
|
if ((r = libusb_open(device, &handle))) {
|
||||||
|
// silently ignore
|
||||||
|
LOGD("USB: cannot open device %04x:%04x (%s)", desc->idVendor, desc->idProduct, libusb_strerror(r));
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!desc->iSerialNumber) {
|
||||||
|
LOGD("USB: device %04x:%04x has no serial number available", desc->idVendor, desc->idProduct);
|
||||||
|
libusb_close(handle);
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((r = libusb_get_string_descriptor_ascii(handle, desc->iSerialNumber, data, length)) <= 0) {
|
||||||
|
// silently ignore
|
||||||
|
LOGD("USB: cannot read serial of device %04x:%04x (%s)", desc->idVendor, desc->idProduct, libusb_strerror(r));
|
||||||
|
libusb_close(handle);
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
data[length - 1] = '\0'; // just in case
|
||||||
|
|
||||||
|
libusb_close(handle);
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static libusb_device *find_device(const char *serial) {
|
||||||
|
libusb_device **list;
|
||||||
|
libusb_device *found = NULL;
|
||||||
|
ssize_t cnt = libusb_get_device_list(NULL, &list);
|
||||||
|
ssize_t i = 0;
|
||||||
|
if (cnt < 0) {
|
||||||
|
log_libusb_error(cnt);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
for (i = 0; i < cnt; ++i) {
|
||||||
|
libusb_device *device = list[i];
|
||||||
|
|
||||||
|
struct libusb_device_descriptor desc;
|
||||||
|
libusb_get_device_descriptor(device, &desc);
|
||||||
|
|
||||||
|
char usb_serial[128];
|
||||||
|
if (get_serial(device, &desc, (unsigned char *) usb_serial, sizeof(usb_serial))) {
|
||||||
|
if (!strncmp(serial, usb_serial, sizeof(usb_serial))) {
|
||||||
|
libusb_ref_device(device);
|
||||||
|
found = device;
|
||||||
|
LOGD("USB device with serial %s found: %04x:%04x", serial, desc.idVendor, desc.idProduct);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
libusb_free_device_list(list, 1);
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_bool aoa_get_protocol(libusb_device_handle *handle, uint16_t *version) {
|
||||||
|
unsigned char data[2];
|
||||||
|
control_params params = {
|
||||||
|
.request_type = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR,
|
||||||
|
.request = AOA_GET_PROTOCOL,
|
||||||
|
.value = 0,
|
||||||
|
.index = 0,
|
||||||
|
.data = data,
|
||||||
|
.length = sizeof(data),
|
||||||
|
.timeout = DEFAULT_TIMEOUT
|
||||||
|
};
|
||||||
|
if (control_transfer(handle, ¶ms)) {
|
||||||
|
// little endian
|
||||||
|
*version = (data[1] << 8) | data[0];
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_bool set_audio_mode(libusb_device_handle *handle, uint16_t mode) {
|
||||||
|
control_params params = {
|
||||||
|
.request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR,
|
||||||
|
.request = AOA_SET_AUDIO_MODE,
|
||||||
|
// <https://source.android.com/devices/accessories/aoa2.html#audio-support>
|
||||||
|
.value = mode,
|
||||||
|
.index = 0, // unused
|
||||||
|
.data = NULL,
|
||||||
|
.length = 0,
|
||||||
|
.timeout = DEFAULT_TIMEOUT
|
||||||
|
};
|
||||||
|
return control_transfer(handle, ¶ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_bool start_accessory(libusb_device_handle *handle) {
|
||||||
|
control_params params = {
|
||||||
|
.request_type = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR,
|
||||||
|
.request = AOA_START_ACCESSORY,
|
||||||
|
.value = 0, // unused
|
||||||
|
.index = 0, // unused
|
||||||
|
.data = NULL,
|
||||||
|
.length = 0,
|
||||||
|
.timeout = DEFAULT_TIMEOUT
|
||||||
|
};
|
||||||
|
return control_transfer(handle, ¶ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool aoa_init(void) {
|
||||||
|
return !libusb_init(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
void aoa_exit(void) {
|
||||||
|
libusb_exit(NULL);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool aoa_forward_audio(const char *serial, SDL_bool forward) {
|
||||||
|
LOGD("%s audio accessory...", forward ? "Enabling" : "Disabling");
|
||||||
|
libusb_device *device = find_device(serial);
|
||||||
|
if (!device) {
|
||||||
|
LOGE("Cannot find USB device having serial %s", serial);
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool ret = SDL_FALSE;
|
||||||
|
|
||||||
|
libusb_device_handle *handle;
|
||||||
|
int r = libusb_open(device, &handle);
|
||||||
|
if (r) {
|
||||||
|
log_libusb_error(r);
|
||||||
|
goto finally_unref_device;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t version;
|
||||||
|
if (!aoa_get_protocol(handle, &version)) {
|
||||||
|
LOGE("Cannot get AOA protocol version");
|
||||||
|
goto finally_close_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Device AOA version: %" PRIu16 "\n", version);
|
||||||
|
if (version < 2) {
|
||||||
|
LOGE("Device does not support AOA 2: %" PRIu16, version);
|
||||||
|
goto finally_close_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint16_t mode = forward ? AUDIO_MODE_S16LSB_STEREO_44100HZ : AUDIO_MODE_NO_AUDIO;
|
||||||
|
if (!set_audio_mode(handle, mode)) {
|
||||||
|
LOGE("Cannot set audio mode: %" PRIu16, mode);
|
||||||
|
goto finally_close_handle;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!start_accessory(handle)) {
|
||||||
|
LOGE("Cannot start accessory");
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = SDL_TRUE;
|
||||||
|
|
||||||
|
finally_close_handle:
|
||||||
|
libusb_close(handle);
|
||||||
|
finally_unref_device:
|
||||||
|
libusb_unref_device(device);
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
15
app/src/aoa.h
Normal file
15
app/src/aoa.h
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#ifndef AOA_H
|
||||||
|
#define AOA_H
|
||||||
|
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
|
||||||
|
#define AUDIO_MODE_NO_AUDIO 0
|
||||||
|
#define AUDIO_MODE_S16LSB_STEREO_44100HZ 1
|
||||||
|
|
||||||
|
SDL_bool aoa_init(void);
|
||||||
|
void aoa_exit(void);
|
||||||
|
|
||||||
|
// serial must not be NULL
|
||||||
|
SDL_bool aoa_forward_audio(const char *serial, SDL_bool forward);
|
||||||
|
|
||||||
|
#endif
|
205
app/src/audio.c
Normal file
205
app/src/audio.c
Normal file
@ -0,0 +1,205 @@
|
|||||||
|
#include "audio.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL.h>
|
||||||
|
#include "aoa.h"
|
||||||
|
#include "command.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
SDL_bool sdl_audio_init(void) {
|
||||||
|
if (SDL_InitSubSystem(SDL_INIT_AUDIO)) {
|
||||||
|
LOGC("Could not initialize SDL audio: %s", SDL_GetError());
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void init_audio_spec(SDL_AudioSpec *spec) {
|
||||||
|
SDL_zero(*spec);
|
||||||
|
spec->freq = 44100;
|
||||||
|
spec->format = AUDIO_S16LSB;
|
||||||
|
spec->channels = 2;
|
||||||
|
spec->samples = 1024;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool audio_player_init(struct audio_player *player, const char *serial) {
|
||||||
|
player->serial = SDL_strdup(serial);
|
||||||
|
return !!player->serial;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_player_destroy(struct audio_player *player) {
|
||||||
|
SDL_free((void *) player->serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void audio_input_callback(void *userdata, Uint8 *stream, int len) {
|
||||||
|
struct audio_player *player = userdata;
|
||||||
|
if (SDL_QueueAudio(player->output_device, stream, len)) {
|
||||||
|
LOGE("Cannot queue audio: %s", SDL_GetError());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int get_matching_audio_device(const char *serial, int count) {
|
||||||
|
for (int i = 0; i < count; ++i) {
|
||||||
|
LOGD("Audio input #%d: %s", i, SDL_GetAudioDeviceName(i, 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
char model[128];
|
||||||
|
int r = adb_read_model(serial, model, sizeof(model));
|
||||||
|
if (r <= 0) {
|
||||||
|
LOGE("Cannot read Android device model");
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Device model is: %s", model);
|
||||||
|
|
||||||
|
// iterate backwards since the matching device is probably the last one
|
||||||
|
for (int i = count - 1; i >= 0; i--) {
|
||||||
|
// model is a NUL-terminated string
|
||||||
|
const char *name = SDL_GetAudioDeviceName(i, 1);
|
||||||
|
if (strstr(name, model)) {
|
||||||
|
// the device name contains the device model, we found it!
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_AudioDeviceID open_accessory_audio_input(struct audio_player *player) {
|
||||||
|
int count = SDL_GetNumAudioDevices(1);
|
||||||
|
if (!count) {
|
||||||
|
LOGE("No audio input source found");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int selected = get_matching_audio_device(player->serial, count);
|
||||||
|
if (selected == -1) {
|
||||||
|
LOGE("Cannot find the Android accessory audio input source");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *selected_name = SDL_GetAudioDeviceName(selected, 1);
|
||||||
|
LOGI("Selecting audio input source: %s", selected_name);
|
||||||
|
|
||||||
|
SDL_AudioSpec spec;
|
||||||
|
init_audio_spec(&spec);
|
||||||
|
spec.callback = audio_input_callback;
|
||||||
|
spec.userdata = player;
|
||||||
|
|
||||||
|
int id = SDL_OpenAudioDevice(selected_name, 1, &spec, NULL, 0);
|
||||||
|
if (!id) {
|
||||||
|
LOGE("Cannot open audio input: %s", SDL_GetError());
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_AudioDeviceID open_default_audio_output() {
|
||||||
|
SDL_AudioSpec spec;
|
||||||
|
init_audio_spec(&spec);
|
||||||
|
int id = SDL_OpenAudioDevice(NULL, 0, &spec, NULL, 0);
|
||||||
|
if (!id) {
|
||||||
|
LOGE("Cannot open audio output: %s", SDL_GetError());
|
||||||
|
}
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool audio_player_open(struct audio_player *player) {
|
||||||
|
player->output_device = open_default_audio_output();
|
||||||
|
if (!player->output_device) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
player->input_device = open_accessory_audio_input(player);
|
||||||
|
if (!player->input_device) {
|
||||||
|
SDL_CloseAudioDevice(player->output_device);
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void audio_player_set_paused(struct audio_player *player, SDL_bool paused) {
|
||||||
|
SDL_PauseAudioDevice(player->input_device, paused);
|
||||||
|
SDL_PauseAudioDevice(player->output_device, paused);
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_player_play(struct audio_player *player) {
|
||||||
|
audio_player_set_paused(player, SDL_FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_player_pause(struct audio_player *player) {
|
||||||
|
audio_player_set_paused(player, SDL_TRUE);
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_player_close(struct audio_player *player) {
|
||||||
|
SDL_CloseAudioDevice(player->input_device);
|
||||||
|
SDL_CloseAudioDevice(player->output_device);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool audio_forwarding_start(struct audio_player *player, const char *serial) {
|
||||||
|
if (!aoa_init()) {
|
||||||
|
LOGE("Cannot initialize AOA");
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
char serialno[128];
|
||||||
|
if (!serial) {
|
||||||
|
LOGD("No serial provided, request it to the device");
|
||||||
|
int r = adb_read_serialno(NULL, serialno, sizeof(serialno));
|
||||||
|
if (r <= 0) {
|
||||||
|
LOGE("Cannot read serial from the device");
|
||||||
|
goto error_aoa_exit;
|
||||||
|
}
|
||||||
|
LOGD("Device serial is %s", serialno);
|
||||||
|
serial = serialno;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!audio_player_init(player, serial)) {
|
||||||
|
LOGE("Cannot initialize audio player");
|
||||||
|
goto error_aoa_exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// adb connection will be reset!
|
||||||
|
if (!aoa_forward_audio(player->serial, SDL_TRUE)) {
|
||||||
|
LOGE("AOA audio forwarding failed");
|
||||||
|
goto error_destroy_player;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGI("Audio accessory enabled");
|
||||||
|
|
||||||
|
if (!sdl_audio_init()) {
|
||||||
|
goto error_disable_audio_forwarding;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGI("Waiting 2s for USB reconfiguration...");
|
||||||
|
SDL_Delay(2000);
|
||||||
|
|
||||||
|
if (!audio_player_open(player)) {
|
||||||
|
goto error_disable_audio_forwarding;
|
||||||
|
}
|
||||||
|
|
||||||
|
audio_player_play(player);
|
||||||
|
return SDL_TRUE;
|
||||||
|
|
||||||
|
error_disable_audio_forwarding:
|
||||||
|
if (!aoa_forward_audio(serial, SDL_FALSE)) {
|
||||||
|
LOGW("Cannot disable audio forwarding");
|
||||||
|
}
|
||||||
|
error_destroy_player:
|
||||||
|
audio_player_destroy(player);
|
||||||
|
error_aoa_exit:
|
||||||
|
aoa_exit();
|
||||||
|
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void audio_forwarding_stop(struct audio_player *player) {
|
||||||
|
audio_player_close(player);
|
||||||
|
|
||||||
|
if (aoa_forward_audio(player->serial, SDL_FALSE)) {
|
||||||
|
LOGI("Audio forwarding disabled");
|
||||||
|
} else {
|
||||||
|
LOGW("Cannot disable audio forwarding");
|
||||||
|
}
|
||||||
|
aoa_exit();
|
||||||
|
|
||||||
|
audio_player_destroy(player);
|
||||||
|
}
|
29
app/src/audio.h
Normal file
29
app/src/audio.h
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
#ifndef AUDIO_H
|
||||||
|
#define AUDIO_H
|
||||||
|
|
||||||
|
#include <SDL2/SDL_audio.h>
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
|
||||||
|
struct audio_player {
|
||||||
|
const char *serial;
|
||||||
|
SDL_AudioDeviceID input_device;
|
||||||
|
SDL_AudioDeviceID output_device;
|
||||||
|
};
|
||||||
|
|
||||||
|
SDL_bool sdl_audio_init(void);
|
||||||
|
|
||||||
|
// serial must not be NULL
|
||||||
|
SDL_bool audio_player_init(struct audio_player *player, const char *serial);
|
||||||
|
void audio_player_destroy(struct audio_player *player);
|
||||||
|
|
||||||
|
SDL_bool audio_player_open(struct audio_player *player);
|
||||||
|
void audio_player_close(struct audio_player *player);
|
||||||
|
|
||||||
|
void audio_player_play(struct audio_player *player);
|
||||||
|
void audio_player_pause(struct audio_player *player);
|
||||||
|
|
||||||
|
// for convenience, these functions handle everything
|
||||||
|
SDL_bool audio_forwarding_start(struct audio_player *player, const char *serial);
|
||||||
|
void audio_forwarding_stop(struct audio_player *player);
|
||||||
|
|
||||||
|
#endif
|
@ -1,118 +0,0 @@
|
|||||||
#include "audio_player.h"
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
/** Downcast frame_sink to sc_audio_player */
|
|
||||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
|
||||||
|
|
||||||
#define SC_SDL_SAMPLE_FMT AUDIO_F32
|
|
||||||
|
|
||||||
static void SDLCALL
|
|
||||||
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
|
||||||
struct sc_audio_player *ap = userdata;
|
|
||||||
|
|
||||||
assert(len_int > 0);
|
|
||||||
size_t len = len_int;
|
|
||||||
|
|
||||||
assert(len % ap->audioreg.sample_size == 0);
|
|
||||||
uint32_t out_samples = len / ap->audioreg.sample_size;
|
|
||||||
|
|
||||||
sc_audio_regulator_pull(&ap->audioreg, stream, out_samples);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
|
||||||
const AVFrame *frame) {
|
|
||||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
|
||||||
|
|
||||||
return sc_audio_regulator_push(&ap->audioreg, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
|
||||||
const AVCodecContext *ctx) {
|
|
||||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
|
||||||
|
|
||||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
|
||||||
assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256);
|
|
||||||
uint8_t nb_channels = ctx->ch_layout.nb_channels;
|
|
||||||
#else
|
|
||||||
int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout);
|
|
||||||
assert(tmp > 0 && tmp < 256);
|
|
||||||
uint8_t nb_channels = tmp;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
assert(ctx->sample_rate > 0);
|
|
||||||
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
|
||||||
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
|
||||||
assert(out_bytes_per_sample > 0);
|
|
||||||
|
|
||||||
uint32_t target_buffering_samples =
|
|
||||||
ap->target_buffering_delay * ctx->sample_rate / SC_TICK_FREQ;
|
|
||||||
|
|
||||||
size_t sample_size = nb_channels * out_bytes_per_sample;
|
|
||||||
bool ok = sc_audio_regulator_init(&ap->audioreg, sample_size, ctx,
|
|
||||||
target_buffering_samples);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t aout_samples = ap->output_buffer_duration * ctx->sample_rate
|
|
||||||
/ SC_TICK_FREQ;
|
|
||||||
assert(aout_samples <= 0xFFFF);
|
|
||||||
|
|
||||||
SDL_AudioSpec desired = {
|
|
||||||
.freq = ctx->sample_rate,
|
|
||||||
.format = SC_SDL_SAMPLE_FMT,
|
|
||||||
.channels = nb_channels,
|
|
||||||
.samples = aout_samples,
|
|
||||||
.callback = sc_audio_player_sdl_callback,
|
|
||||||
.userdata = ap,
|
|
||||||
};
|
|
||||||
SDL_AudioSpec obtained;
|
|
||||||
|
|
||||||
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
|
|
||||||
if (!ap->device) {
|
|
||||||
LOGE("Could not open audio device: %s", SDL_GetError());
|
|
||||||
sc_audio_regulator_destroy(&ap->audioreg);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The thread calling open() is the thread calling push(), which fills the
|
|
||||||
// audio buffer consumed by the SDL audio thread.
|
|
||||||
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL);
|
|
||||||
if (!ok) {
|
|
||||||
ok = sc_thread_set_priority(SC_THREAD_PRIORITY_HIGH);
|
|
||||||
(void) ok; // We don't care if it worked, at least we tried
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_PauseAudioDevice(ap->device, 0);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
|
|
||||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
|
||||||
|
|
||||||
assert(ap->device);
|
|
||||||
SDL_PauseAudioDevice(ap->device, 1);
|
|
||||||
SDL_CloseAudioDevice(ap->device);
|
|
||||||
|
|
||||||
sc_audio_regulator_destroy(&ap->audioreg);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
|
|
||||||
sc_tick output_buffer_duration) {
|
|
||||||
ap->target_buffering_delay = target_buffering;
|
|
||||||
ap->output_buffer_duration = output_buffer_duration;
|
|
||||||
|
|
||||||
static const struct sc_frame_sink_ops ops = {
|
|
||||||
.open = sc_audio_player_frame_sink_open,
|
|
||||||
.close = sc_audio_player_frame_sink_close,
|
|
||||||
.push = sc_audio_player_frame_sink_push,
|
|
||||||
};
|
|
||||||
|
|
||||||
ap->frame_sink.ops = &ops;
|
|
||||||
}
|
|
@ -1,35 +0,0 @@
|
|||||||
#ifndef SC_AUDIO_PLAYER_H
|
|
||||||
#define SC_AUDIO_PLAYER_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdatomic.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <SDL2/SDL.h>
|
|
||||||
|
|
||||||
#include "audio_regulator.h"
|
|
||||||
#include "trait/frame_sink.h"
|
|
||||||
#include "util/tick.h"
|
|
||||||
|
|
||||||
struct sc_audio_player {
|
|
||||||
struct sc_frame_sink frame_sink;
|
|
||||||
|
|
||||||
// The target buffering between the producer and the consumer. This value
|
|
||||||
// is directly use for compensation.
|
|
||||||
// Since audio capture and/or encoding on the device typically produce
|
|
||||||
// blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target
|
|
||||||
// value should be higher.
|
|
||||||
sc_tick target_buffering_delay;
|
|
||||||
|
|
||||||
// SDL audio output buffer size
|
|
||||||
sc_tick output_buffer_duration;
|
|
||||||
|
|
||||||
SDL_AudioDeviceID device;
|
|
||||||
struct sc_audio_regulator audioreg;
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
|
|
||||||
sc_tick audio_output_buffer);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,413 +0,0 @@
|
|||||||
#include "audio_regulator.h"
|
|
||||||
|
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
#include <libavutil/opt.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
//#define SC_AUDIO_REGULATOR_DEBUG // uncomment to debug
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Real-time audio regulator with configurable latency
|
|
||||||
*
|
|
||||||
* As input, the regulator regularly receives AVFrames of decoded audio samples.
|
|
||||||
* As output, the audio player regularly requests audio samples to be played.
|
|
||||||
* In the middle, an audio buffer stores the samples produced but not consumed
|
|
||||||
* yet.
|
|
||||||
*
|
|
||||||
* The goal of the regulator is to feed the audio player with a latency as low
|
|
||||||
* as possible while avoiding buffer underrun (i.e. not being able to provide
|
|
||||||
* samples when requested).
|
|
||||||
*
|
|
||||||
* To achieve this, it attempts to maintain the average buffering (the number
|
|
||||||
* of samples present in the buffer) around a target value. If this target
|
|
||||||
* buffering is too low, then buffer underrun will occur frequently. If it is
|
|
||||||
* too high, then latency will become unacceptable. This target value is
|
|
||||||
* configured using the scrcpy option --audio-buffer.
|
|
||||||
*
|
|
||||||
* The regulator cannot adjust the sample input rate (it receives samples
|
|
||||||
* produced in real-time) or the sample output rate (it must provide samples as
|
|
||||||
* requested by the audio player). Therefore, it may only apply compensation by
|
|
||||||
* resampling (converting _m_ input samples to _n_ output samples).
|
|
||||||
*
|
|
||||||
* The compensation itself is applied by libswresample (FFmpeg). It is
|
|
||||||
* configured using swr_set_compensation(). An important work for the regulator
|
|
||||||
* is to estimate the compensation value regularly and apply it.
|
|
||||||
*
|
|
||||||
* The estimated buffering level is the result of averaging the "natural"
|
|
||||||
* buffering (samples are produced and consumed by blocks, so it must be
|
|
||||||
* smoothed), and making instant adjustments resulting of its own actions
|
|
||||||
* (explicit compensation and silence insertion on underflow), which are not
|
|
||||||
* smoothed.
|
|
||||||
*
|
|
||||||
* Buffer underflow events can occur when packets arrive too late. In that case,
|
|
||||||
* the regulator inserts silence. Once the packets finally arrive (late), one
|
|
||||||
* strategy could be to drop the samples that were replaced by silence, in
|
|
||||||
* order to keep a minimal latency. However, dropping samples in case of buffer
|
|
||||||
* underflow is inadvisable, as it would temporarily increase the underflow
|
|
||||||
* even more and cause very noticeable audio glitches.
|
|
||||||
*
|
|
||||||
* Therefore, the regulator doesn't drop any sample on underflow. The
|
|
||||||
* compensation mechanism will absorb the delay introduced by the inserted
|
|
||||||
* silence.
|
|
||||||
*/
|
|
||||||
|
|
||||||
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ar->buf, (SAMPLES))
|
|
||||||
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ar->buf, (BYTES))
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
|
|
||||||
uint32_t out_samples) {
|
|
||||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
|
||||||
LOGD("[Audio] Audio regulator pulls %" PRIu32 " samples", out_samples);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// A lock is necessary in the rare case where the producer needs to drop
|
|
||||||
// samples already pushed (when the buffer is full)
|
|
||||||
sc_mutex_lock(&ar->mutex);
|
|
||||||
|
|
||||||
bool played = atomic_load_explicit(&ar->played, memory_order_relaxed);
|
|
||||||
if (!played) {
|
|
||||||
uint32_t buffered_samples = sc_audiobuf_can_read(&ar->buf);
|
|
||||||
// Wait until the buffer is filled up to at least target_buffering
|
|
||||||
// before playing
|
|
||||||
if (buffered_samples < ar->target_buffering) {
|
|
||||||
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
|
|
||||||
" samples", out_samples);
|
|
||||||
// Delay playback starting to reach the target buffering. Fill the
|
|
||||||
// whole buffer with silence (len is small compared to the
|
|
||||||
// arbitrary margin value).
|
|
||||||
memset(out, 0, out_samples * ar->sample_size);
|
|
||||||
sc_mutex_unlock(&ar->mutex);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t read = sc_audiobuf_read(&ar->buf, out, out_samples);
|
|
||||||
|
|
||||||
sc_mutex_unlock(&ar->mutex);
|
|
||||||
|
|
||||||
if (read < out_samples) {
|
|
||||||
uint32_t silence = out_samples - read;
|
|
||||||
// Insert silence. In theory, the inserted silent samples replace the
|
|
||||||
// missing real samples, which will arrive later, so they should be
|
|
||||||
// dropped to keep the latency minimal. However, this would cause very
|
|
||||||
// audible glitches, so let the clock compensation restore the target
|
|
||||||
// latency.
|
|
||||||
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
|
|
||||||
silence);
|
|
||||||
memset(out + TO_BYTES(read), 0, TO_BYTES(silence));
|
|
||||||
|
|
||||||
bool received = atomic_load_explicit(&ar->received,
|
|
||||||
memory_order_relaxed);
|
|
||||||
if (received) {
|
|
||||||
// Inserting additional samples immediately increases buffering
|
|
||||||
atomic_fetch_add_explicit(&ar->underflow, silence,
|
|
||||||
memory_order_relaxed);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic_store_explicit(&ar->played, true, memory_order_relaxed);
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t *
|
|
||||||
sc_audio_regulator_get_swr_buf(struct sc_audio_regulator *ar,
|
|
||||||
uint32_t min_samples) {
|
|
||||||
size_t min_buf_size = TO_BYTES(min_samples);
|
|
||||||
if (min_buf_size > ar->swr_buf_alloc_size) {
|
|
||||||
size_t new_size = min_buf_size + 4096;
|
|
||||||
uint8_t *buf = realloc(ar->swr_buf, new_size);
|
|
||||||
if (!buf) {
|
|
||||||
LOG_OOM();
|
|
||||||
// Could not realloc to the requested size
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
ar->swr_buf = buf;
|
|
||||||
ar->swr_buf_alloc_size = new_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
return ar->swr_buf;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) {
|
|
||||||
SwrContext *swr_ctx = ar->swr_ctx;
|
|
||||||
|
|
||||||
int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate);
|
|
||||||
// No need to av_rescale_rnd(), input and output sample rates are the same.
|
|
||||||
// Add more space (256) for clock compensation.
|
|
||||||
int dst_nb_samples = swr_delay + frame->nb_samples + 256;
|
|
||||||
|
|
||||||
uint8_t *swr_buf = sc_audio_regulator_get_swr_buf(ar, dst_nb_samples);
|
|
||||||
if (!swr_buf) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
|
|
||||||
(const uint8_t **) frame->data, frame->nb_samples);
|
|
||||||
if (ret < 0) {
|
|
||||||
LOGE("Resampling failed: %d", ret);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// swr_convert() returns the number of samples which would have been
|
|
||||||
// written if the buffer was big enough.
|
|
||||||
uint32_t samples = MIN(ret, dst_nb_samples);
|
|
||||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
|
||||||
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
uint32_t cap = sc_audiobuf_capacity(&ar->buf);
|
|
||||||
if (samples > cap) {
|
|
||||||
// Very very unlikely: a single resampled frame should never
|
|
||||||
// exceed the audio buffer size (or something is very wrong).
|
|
||||||
// Ignore the first bytes in swr_buf to avoid memory corruption anyway.
|
|
||||||
swr_buf += TO_BYTES(samples - cap);
|
|
||||||
samples = cap;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t skipped_samples = 0;
|
|
||||||
|
|
||||||
uint32_t written = sc_audiobuf_write(&ar->buf, swr_buf, samples);
|
|
||||||
if (written < samples) {
|
|
||||||
uint32_t remaining = samples - written;
|
|
||||||
|
|
||||||
// All samples that could be written without locking have been written,
|
|
||||||
// now we need to lock to drop/consume old samples
|
|
||||||
sc_mutex_lock(&ar->mutex);
|
|
||||||
|
|
||||||
// Retry with the lock
|
|
||||||
written += sc_audiobuf_write(&ar->buf,
|
|
||||||
swr_buf + TO_BYTES(written),
|
|
||||||
remaining);
|
|
||||||
if (written < samples) {
|
|
||||||
remaining = samples - written;
|
|
||||||
// Still insufficient, drop old samples to make space
|
|
||||||
skipped_samples = sc_audiobuf_read(&ar->buf, NULL, remaining);
|
|
||||||
assert(skipped_samples == remaining);
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_mutex_unlock(&ar->mutex);
|
|
||||||
|
|
||||||
if (written < samples) {
|
|
||||||
// Now there is enough space
|
|
||||||
uint32_t w = sc_audiobuf_write(&ar->buf,
|
|
||||||
swr_buf + TO_BYTES(written),
|
|
||||||
remaining);
|
|
||||||
assert(w == remaining);
|
|
||||||
(void) w;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t underflow = 0;
|
|
||||||
uint32_t max_buffered_samples;
|
|
||||||
bool played = atomic_load_explicit(&ar->played, memory_order_relaxed);
|
|
||||||
if (played) {
|
|
||||||
underflow = atomic_exchange_explicit(&ar->underflow, 0,
|
|
||||||
memory_order_relaxed);
|
|
||||||
|
|
||||||
max_buffered_samples = ar->target_buffering * 11 / 10
|
|
||||||
+ 60 * ar->sample_rate / 1000 /* 60 ms */;
|
|
||||||
} else {
|
|
||||||
// Playback not started yet, do not accumulate more than
|
|
||||||
// max_initial_buffering samples, this would cause unnecessary delay
|
|
||||||
// (and glitches to compensate) on start.
|
|
||||||
max_buffered_samples = ar->target_buffering
|
|
||||||
+ 10 * ar->sample_rate / 1000 /* 10 ms */;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t can_read = sc_audiobuf_can_read(&ar->buf);
|
|
||||||
if (can_read > max_buffered_samples) {
|
|
||||||
uint32_t skip_samples = 0;
|
|
||||||
|
|
||||||
sc_mutex_lock(&ar->mutex);
|
|
||||||
can_read = sc_audiobuf_can_read(&ar->buf);
|
|
||||||
if (can_read > max_buffered_samples) {
|
|
||||||
skip_samples = can_read - max_buffered_samples;
|
|
||||||
uint32_t r = sc_audiobuf_read(&ar->buf, NULL, skip_samples);
|
|
||||||
assert(r == skip_samples);
|
|
||||||
(void) r;
|
|
||||||
skipped_samples += skip_samples;
|
|
||||||
}
|
|
||||||
sc_mutex_unlock(&ar->mutex);
|
|
||||||
|
|
||||||
if (skip_samples) {
|
|
||||||
if (played) {
|
|
||||||
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
|
|
||||||
" samples", skip_samples);
|
|
||||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
|
||||||
} else {
|
|
||||||
LOGD("[Audio] Playback not started, skipping %" PRIu32
|
|
||||||
" samples", skip_samples);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
atomic_store_explicit(&ar->received, true, memory_order_relaxed);
|
|
||||||
if (!played) {
|
|
||||||
// Nothing more to do
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Number of samples added (or removed, if negative) for compensation
|
|
||||||
int32_t instant_compensation = (int32_t) written - frame->nb_samples;
|
|
||||||
// Inserting silence instantly increases buffering
|
|
||||||
int32_t inserted_silence = (int32_t) underflow;
|
|
||||||
// Dropping input samples instantly decreases buffering
|
|
||||||
int32_t dropped = (int32_t) skipped_samples;
|
|
||||||
|
|
||||||
// The compensation must apply instantly, it must not be smoothed
|
|
||||||
ar->avg_buffering.avg += instant_compensation + inserted_silence - dropped;
|
|
||||||
if (ar->avg_buffering.avg < 0) {
|
|
||||||
// Since dropping samples instantly reduces buffering, the difference
|
|
||||||
// is applied immediately to the average value, assuming that the delay
|
|
||||||
// between the producer and the consumer will be caught up.
|
|
||||||
//
|
|
||||||
// However, when this assumption is not valid, the average buffering
|
|
||||||
// may decrease indefinitely. Prevent it to become negative to limit
|
|
||||||
// the consequences.
|
|
||||||
ar->avg_buffering.avg = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// However, the buffering level must be smoothed
|
|
||||||
sc_average_push(&ar->avg_buffering, can_read);
|
|
||||||
|
|
||||||
#ifdef SC_AUDIO_REGULATOR_DEBUG
|
|
||||||
LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f",
|
|
||||||
can_read, sc_average_get(&ar->avg_buffering));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ar->samples_since_resync += written;
|
|
||||||
if (ar->samples_since_resync >= ar->sample_rate) {
|
|
||||||
// Recompute compensation every second
|
|
||||||
ar->samples_since_resync = 0;
|
|
||||||
|
|
||||||
float avg = sc_average_get(&ar->avg_buffering);
|
|
||||||
int diff = ar->target_buffering - avg;
|
|
||||||
|
|
||||||
// Enable compensation when the difference exceeds +/- 4ms.
|
|
||||||
// Disable compensation when the difference is lower than +/- 1ms.
|
|
||||||
int threshold = ar->compensation_active
|
|
||||||
? ar->sample_rate / 1000 /* 1ms */
|
|
||||||
: ar->sample_rate * 4 / 1000; /* 4ms */
|
|
||||||
|
|
||||||
if (abs(diff) < threshold) {
|
|
||||||
// Do not compensate for small values, the error is just noise
|
|
||||||
diff = 0;
|
|
||||||
} else if (diff < 0 && can_read < ar->target_buffering) {
|
|
||||||
// Do not accelerate if the instant buffering level is below the
|
|
||||||
// target, this would increase underflow
|
|
||||||
diff = 0;
|
|
||||||
}
|
|
||||||
// Compensate the diff over 4 seconds (but will be recomputed after 1
|
|
||||||
// second)
|
|
||||||
int distance = 4 * ar->sample_rate;
|
|
||||||
// Limit compensation rate to 2%
|
|
||||||
int abs_max_diff = distance / 50;
|
|
||||||
diff = CLAMP(diff, -abs_max_diff, abs_max_diff);
|
|
||||||
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
|
|
||||||
" compensation=%d", ar->target_buffering, avg, can_read, diff);
|
|
||||||
|
|
||||||
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
|
||||||
if (ret < 0) {
|
|
||||||
LOGW("Resampling compensation failed: %d", ret);
|
|
||||||
// not fatal
|
|
||||||
} else {
|
|
||||||
ar->compensation_active = diff != 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
|
|
||||||
const AVCodecContext *ctx, uint32_t target_buffering) {
|
|
||||||
SwrContext *swr_ctx = swr_alloc();
|
|
||||||
if (!swr_ctx) {
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
ar->swr_ctx = swr_ctx;
|
|
||||||
|
|
||||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
|
||||||
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
|
|
||||||
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
|
|
||||||
#else
|
|
||||||
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
|
|
||||||
ctx->channel_layout, 0);
|
|
||||||
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
|
|
||||||
ctx->channel_layout, 0);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
|
|
||||||
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
|
|
||||||
|
|
||||||
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
|
|
||||||
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
|
|
||||||
|
|
||||||
int ret = swr_init(swr_ctx);
|
|
||||||
if (ret) {
|
|
||||||
LOGE("Failed to initialize the resampling context");
|
|
||||||
goto error_free_swr_ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ok = sc_mutex_init(&ar->mutex);
|
|
||||||
if (!ok) {
|
|
||||||
goto error_free_swr_ctx;
|
|
||||||
}
|
|
||||||
|
|
||||||
ar->target_buffering = target_buffering;
|
|
||||||
ar->sample_size = sample_size;
|
|
||||||
ar->sample_rate = ctx->sample_rate;
|
|
||||||
|
|
||||||
// Use a ring-buffer of the target buffering size plus 1 second between the
|
|
||||||
// producer and the consumer. It's too big on purpose, to guarantee that
|
|
||||||
// the producer and the consumer will be able to access it in parallel
|
|
||||||
// without locking.
|
|
||||||
uint32_t audiobuf_samples = target_buffering + ar->sample_rate;
|
|
||||||
|
|
||||||
ok = sc_audiobuf_init(&ar->buf, sample_size, audiobuf_samples);
|
|
||||||
if (!ok) {
|
|
||||||
goto error_destroy_mutex;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t initial_swr_buf_size = TO_BYTES(4096);
|
|
||||||
ar->swr_buf = malloc(initial_swr_buf_size);
|
|
||||||
if (!ar->swr_buf) {
|
|
||||||
LOG_OOM();
|
|
||||||
goto error_destroy_audiobuf;
|
|
||||||
}
|
|
||||||
ar->swr_buf_alloc_size = initial_swr_buf_size;
|
|
||||||
|
|
||||||
// Samples are produced and consumed by blocks, so the buffering must be
|
|
||||||
// smoothed to get a relatively stable value.
|
|
||||||
sc_average_init(&ar->avg_buffering, 128);
|
|
||||||
ar->samples_since_resync = 0;
|
|
||||||
|
|
||||||
ar->received = false;
|
|
||||||
atomic_init(&ar->played, false);
|
|
||||||
atomic_init(&ar->received, false);
|
|
||||||
atomic_init(&ar->underflow, 0);
|
|
||||||
ar->compensation_active = false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
error_destroy_audiobuf:
|
|
||||||
sc_audiobuf_destroy(&ar->buf);
|
|
||||||
error_destroy_mutex:
|
|
||||||
sc_mutex_destroy(&ar->mutex);
|
|
||||||
error_free_swr_ctx:
|
|
||||||
swr_free(&ar->swr_ctx);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_audio_regulator_destroy(struct sc_audio_regulator *ar) {
|
|
||||||
free(ar->swr_buf);
|
|
||||||
sc_audiobuf_destroy(&ar->buf);
|
|
||||||
sc_mutex_destroy(&ar->mutex);
|
|
||||||
swr_free(&ar->swr_ctx);
|
|
||||||
}
|
|
@ -1,71 +0,0 @@
|
|||||||
#ifndef SC_AUDIO_REGULATOR_H
|
|
||||||
#define SC_AUDIO_REGULATOR_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdatomic.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
#include <libswresample/swresample.h>
|
|
||||||
#include "util/audiobuf.h"
|
|
||||||
#include "util/average.h"
|
|
||||||
#include "util/thread.h"
|
|
||||||
|
|
||||||
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
|
|
||||||
|
|
||||||
struct sc_audio_regulator {
|
|
||||||
sc_mutex mutex;
|
|
||||||
|
|
||||||
// Target buffering between the producer and the consumer (in samples)
|
|
||||||
uint32_t target_buffering;
|
|
||||||
|
|
||||||
// Audio buffer to communicate between the receiver and the player
|
|
||||||
struct sc_audiobuf buf;
|
|
||||||
|
|
||||||
// Resampler (only used from the receiver thread)
|
|
||||||
struct SwrContext *swr_ctx;
|
|
||||||
|
|
||||||
// The sample rate is the same for input and output
|
|
||||||
uint32_t sample_rate;
|
|
||||||
// The number of bytes per sample (for all channels)
|
|
||||||
size_t sample_size;
|
|
||||||
|
|
||||||
// Target buffer for resampling (only used by the receiver thread)
|
|
||||||
uint8_t *swr_buf;
|
|
||||||
size_t swr_buf_alloc_size;
|
|
||||||
|
|
||||||
// Number of buffered samples (may be negative on underflow) (only used by
|
|
||||||
// the receiver thread)
|
|
||||||
struct sc_average avg_buffering;
|
|
||||||
// Count the number of samples to trigger a compensation update regularly
|
|
||||||
// (only used by the receiver thread)
|
|
||||||
uint32_t samples_since_resync;
|
|
||||||
|
|
||||||
// Number of silence samples inserted since the last received packet
|
|
||||||
atomic_uint_least32_t underflow;
|
|
||||||
|
|
||||||
// Non-zero compensation applied (only used by the receiver thread)
|
|
||||||
bool compensation_active;
|
|
||||||
|
|
||||||
// Set to true the first time a sample is received
|
|
||||||
atomic_bool received;
|
|
||||||
|
|
||||||
// Set to true the first time samples are pulled by the player
|
|
||||||
atomic_bool played;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size,
|
|
||||||
const AVCodecContext *ctx, uint32_t target_buffering);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_audio_regulator_destroy(struct sc_audio_regulator *ar);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out,
|
|
||||||
uint32_t samples);
|
|
||||||
|
|
||||||
#endif
|
|
3261
app/src/cli.c
3261
app/src/cli.c
File diff suppressed because it is too large
Load Diff
@ -1,34 +0,0 @@
|
|||||||
#ifndef SCRCPY_CLI_H
|
|
||||||
#define SCRCPY_CLI_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#include "options.h"
|
|
||||||
|
|
||||||
enum sc_pause_on_exit {
|
|
||||||
SC_PAUSE_ON_EXIT_TRUE,
|
|
||||||
SC_PAUSE_ON_EXIT_FALSE,
|
|
||||||
SC_PAUSE_ON_EXIT_IF_ERROR,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct scrcpy_cli_args {
|
|
||||||
struct scrcpy_options opts;
|
|
||||||
bool help;
|
|
||||||
bool version;
|
|
||||||
enum sc_pause_on_exit pause_on_exit;
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
scrcpy_print_usage(const char *arg0);
|
|
||||||
|
|
||||||
bool
|
|
||||||
scrcpy_parse_args(struct scrcpy_cli_args *args, int argc, char *argv[]);
|
|
||||||
|
|
||||||
#ifdef SC_TEST
|
|
||||||
bool
|
|
||||||
sc_parse_shortcut_mods(const char *s, uint8_t *shortcut_mods);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,38 +0,0 @@
|
|||||||
#include "clock.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
//#define SC_CLOCK_DEBUG // uncomment to debug
|
|
||||||
|
|
||||||
#define SC_CLOCK_RANGE 32
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_clock_init(struct sc_clock *clock) {
|
|
||||||
clock->range = 0;
|
|
||||||
clock->offset = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
|
||||||
if (clock->range < SC_CLOCK_RANGE) {
|
|
||||||
++clock->range;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_tick offset = system - stream;
|
|
||||||
unsigned clock_weight = clock->range - 1;
|
|
||||||
unsigned value_weight = SC_CLOCK_RANGE - clock->range + 1;
|
|
||||||
clock->offset = (clock->offset * clock_weight + offset * value_weight)
|
|
||||||
/ SC_CLOCK_RANGE;
|
|
||||||
|
|
||||||
#ifdef SC_CLOCK_DEBUG
|
|
||||||
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_tick
|
|
||||||
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
|
||||||
assert(clock->range); // sc_clock_update() must have been called
|
|
||||||
return stream + clock->offset;
|
|
||||||
}
|
|
@ -1,43 +0,0 @@
|
|||||||
#ifndef SC_CLOCK_H
|
|
||||||
#define SC_CLOCK_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include "util/tick.h"
|
|
||||||
|
|
||||||
struct sc_clock_point {
|
|
||||||
sc_tick system;
|
|
||||||
sc_tick stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The clock aims to estimate the affine relation between the stream (device)
|
|
||||||
* time and the system time:
|
|
||||||
*
|
|
||||||
* f(stream) = slope * stream + offset
|
|
||||||
*
|
|
||||||
* Theoretically, the slope encodes the drift between the device clock and the
|
|
||||||
* computer clock. It is expected to be very close to 1.
|
|
||||||
*
|
|
||||||
* Since the clock is used to estimate very close points in the future (which
|
|
||||||
* are reestimated on every clock update, see delay_buffer), the error caused
|
|
||||||
* by clock drift is totally negligible, so it is better to assume that the
|
|
||||||
* slope is 1 than to estimate it (the estimation error would be larger).
|
|
||||||
*
|
|
||||||
* Therefore, only the offset is estimated.
|
|
||||||
*/
|
|
||||||
struct sc_clock {
|
|
||||||
unsigned range;
|
|
||||||
sc_tick offset;
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_clock_init(struct sc_clock *clock);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream);
|
|
||||||
|
|
||||||
sc_tick
|
|
||||||
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream);
|
|
||||||
|
|
||||||
#endif
|
|
140
app/src/command.c
Normal file
140
app/src/command.c
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
#include "command.h"
|
||||||
|
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
static const char *adb_command;
|
||||||
|
|
||||||
|
static inline const char *get_adb_command() {
|
||||||
|
if (!adb_command) {
|
||||||
|
adb_command = getenv("ADB");
|
||||||
|
if (!adb_command)
|
||||||
|
adb_command = "adb";
|
||||||
|
}
|
||||||
|
return adb_command;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void fill_cmd(const char *cmd[], const char *serial, const char *const adb_cmd[], int len) {
|
||||||
|
int i;
|
||||||
|
cmd[0] = get_adb_command();
|
||||||
|
if (serial) {
|
||||||
|
cmd[1] = "-s";
|
||||||
|
cmd[2] = serial;
|
||||||
|
i = 3;
|
||||||
|
} else {
|
||||||
|
i = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(&cmd[i], adb_cmd, len * sizeof(const char *));
|
||||||
|
cmd[len + i] = NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len) {
|
||||||
|
const char *cmd[len + 4];
|
||||||
|
fill_cmd(cmd, serial, adb_cmd, len);
|
||||||
|
return cmd_execute(cmd[0], cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t adb_execute_redirect(const char *serial, const char *const adb_cmd[], int len,
|
||||||
|
pipe_t *pipe_stdin, pipe_t *pipe_stdout, pipe_t *pipe_stderr) {
|
||||||
|
const char *cmd[len + 4];
|
||||||
|
fill_cmd(cmd, serial, adb_cmd, len);
|
||||||
|
return cmd_execute_redirect(cmd[0], cmd, pipe_stdin, pipe_stdout, pipe_stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name) {
|
||||||
|
char local[4 + 5 + 1]; // tcp:PORT
|
||||||
|
char remote[108 + 14 + 1]; // localabstract:NAME
|
||||||
|
sprintf(local, "tcp:%" PRIu16, local_port);
|
||||||
|
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
|
||||||
|
const char *const adb_cmd[] = {"forward", local, remote};
|
||||||
|
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t adb_forward_remove(const char *serial, uint16_t local_port) {
|
||||||
|
char local[4 + 5 + 1]; // tcp:PORT
|
||||||
|
sprintf(local, "tcp:%" PRIu16, local_port);
|
||||||
|
const char *const adb_cmd[] = {"forward", "--remove", local};
|
||||||
|
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port) {
|
||||||
|
char local[4 + 5 + 1]; // tcp:PORT
|
||||||
|
char remote[108 + 14 + 1]; // localabstract:NAME
|
||||||
|
sprintf(local, "tcp:%" PRIu16, local_port);
|
||||||
|
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
|
||||||
|
const char *const adb_cmd[] = {"reverse", remote, local};
|
||||||
|
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t adb_reverse_remove(const char *serial, const char *device_socket_name) {
|
||||||
|
char remote[108 + 14 + 1]; // localabstract:NAME
|
||||||
|
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name);
|
||||||
|
const char *const adb_cmd[] = {"reverse", "--remove", remote};
|
||||||
|
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t adb_push(const char *serial, const char *local, const char *remote) {
|
||||||
|
const char *const adb_cmd[] = {"push", local, remote};
|
||||||
|
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
process_t adb_remove_path(const char *serial, const char *path) {
|
||||||
|
const char *const adb_cmd[] = {"shell", "rm", path};
|
||||||
|
return adb_execute(serial, adb_cmd, ARRAY_LEN(adb_cmd));
|
||||||
|
}
|
||||||
|
|
||||||
|
static int adb_execute_get_output(const char *serial, const char *const adb_cmd[], int adb_cmd_len,
|
||||||
|
char *data, size_t data_len, const char *name) {
|
||||||
|
pipe_t pipe_stdout;
|
||||||
|
process_t proc = adb_execute_redirect(serial, adb_cmd, adb_cmd_len, NULL, &pipe_stdout, NULL);
|
||||||
|
if (!process_check_success(proc, name)) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int r = read_pipe(pipe_stdout, data, data_len);
|
||||||
|
close_pipe(pipe_stdout);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int truncate_first_line(char *data, int len) {
|
||||||
|
data[len - 1] = '\0';
|
||||||
|
char *eol = strpbrk(data, "\r\n");
|
||||||
|
if (eol) {
|
||||||
|
*eol = '\0';
|
||||||
|
len = eol - data;
|
||||||
|
}
|
||||||
|
return len;
|
||||||
|
}
|
||||||
|
|
||||||
|
int adb_read_serialno(const char *serial, char *data, size_t len) {
|
||||||
|
const char *const adb_cmd[] = {"get-serialno"};
|
||||||
|
int r = adb_execute_get_output(serial, adb_cmd, ARRAY_LEN(adb_cmd), data, len, "get-serialno");
|
||||||
|
return r <= 0 ? r : truncate_first_line(data, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
int adb_read_model(const char *serial, char *data, size_t len) {
|
||||||
|
const char *const adb_cmd[] = {"shell", "getprop", "ro.product.model"};
|
||||||
|
int r = adb_execute_get_output(serial, adb_cmd, ARRAY_LEN(adb_cmd), data, len, "getprop model");
|
||||||
|
return r <= 0 ? r : truncate_first_line(data, r);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool process_check_success(process_t proc, const char *name) {
|
||||||
|
if (proc == PROCESS_NONE) {
|
||||||
|
LOGE("Could not execute \"%s\"", name);
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
exit_code_t exit_code;
|
||||||
|
if (!cmd_simple_wait(proc, &exit_code)) {
|
||||||
|
if (exit_code != NO_EXIT_CODE) {
|
||||||
|
LOGE("\"%s\" returned with value %" PRIexitcode, name, exit_code);
|
||||||
|
} else {
|
||||||
|
LOGE("\"%s\" exited unexpectedly", name);
|
||||||
|
}
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
65
app/src/command.h
Normal file
65
app/src/command.h
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
#ifndef COMMAND_H
|
||||||
|
#define COMMAND_H
|
||||||
|
|
||||||
|
#include <inttypes.h>
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
#include <SDL2/SDL_platform.h>
|
||||||
|
|
||||||
|
// <https://stackoverflow.com/a/44383330/1987178>
|
||||||
|
#ifdef _WIN32
|
||||||
|
# define PRIexitcode "lu"
|
||||||
|
# ifdef _WIN64
|
||||||
|
# define PRIsizet PRIu64
|
||||||
|
# else
|
||||||
|
# define PRIsizet PRIu32
|
||||||
|
# endif
|
||||||
|
#else
|
||||||
|
# define PRIsizet "zu"
|
||||||
|
# define PRIexitcode "d"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#ifdef __WINDOWS__
|
||||||
|
# include <winsock2.h> // not needed here, but must never be included AFTER windows.h
|
||||||
|
# include <windows.h>
|
||||||
|
# define PROCESS_NONE NULL
|
||||||
|
typedef HANDLE process_t;
|
||||||
|
typedef DWORD exit_code_t;
|
||||||
|
typedef HANDLE pipe_t;
|
||||||
|
#else
|
||||||
|
# include <sys/types.h>
|
||||||
|
# define PROCESS_NONE -1
|
||||||
|
typedef pid_t process_t;
|
||||||
|
typedef int exit_code_t;
|
||||||
|
typedef int pipe_t;
|
||||||
|
#endif
|
||||||
|
# define NO_EXIT_CODE -1
|
||||||
|
|
||||||
|
process_t cmd_execute(const char *path, const char *const argv[]);
|
||||||
|
process_t cmd_execute_redirect(const char *path, const char *const argv[],
|
||||||
|
pipe_t *pipe_stdin, pipe_t *pipe_stdout, pipe_t *pipe_stderr);
|
||||||
|
SDL_bool cmd_terminate(process_t pid);
|
||||||
|
SDL_bool cmd_simple_wait(process_t pid, exit_code_t *exit_code);
|
||||||
|
|
||||||
|
process_t adb_execute(const char *serial, const char *const adb_cmd[], int len);
|
||||||
|
process_t adb_execute_redirect(const char *serial, const char *const adb_cmd[], int len,
|
||||||
|
pipe_t *pipe_stdin, pipe_t *pipe_stdout, pipe_t *pipe_stderr);
|
||||||
|
|
||||||
|
process_t adb_forward(const char *serial, uint16_t local_port, const char *device_socket_name);
|
||||||
|
process_t adb_forward_remove(const char *serial, uint16_t local_port);
|
||||||
|
process_t adb_reverse(const char *serial, const char *device_socket_name, uint16_t local_port);
|
||||||
|
process_t adb_reverse_remove(const char *serial, const char *device_socket_name);
|
||||||
|
process_t adb_push(const char *serial, const char *local, const char *remote);
|
||||||
|
process_t adb_remove_path(const char *serial, const char *path);
|
||||||
|
|
||||||
|
// return number of bytes read (-1 on error)
|
||||||
|
int adb_read_serialno(const char *serial, char *data, size_t len);
|
||||||
|
int adb_read_model(const char *serial, char *data, size_t len);
|
||||||
|
|
||||||
|
// convenience function to wait for a successful process execution
|
||||||
|
// automatically log process errors with the provided process name
|
||||||
|
SDL_bool process_check_success(process_t process, const char *name);
|
||||||
|
|
||||||
|
int read_pipe(pipe_t pipe, char *data, size_t len);
|
||||||
|
void close_pipe(pipe_t pipe);
|
||||||
|
|
||||||
|
#endif
|
@ -1,15 +1,27 @@
|
|||||||
#ifndef SC_COMMON_H
|
#ifndef COMMON_H
|
||||||
#define SC_COMMON_H
|
#define COMMON_H
|
||||||
|
|
||||||
#include "config.h"
|
#include <SDL2/SDL_stdinc.h>
|
||||||
#include "compat.h"
|
|
||||||
|
|
||||||
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
|
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
|
||||||
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
|
#define MIN(X,Y) (X) < (Y) ? (X) : (Y)
|
||||||
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
|
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)
|
||||||
#define CLAMP(V,X,Y) MIN( MAX((V),(X)), (Y) )
|
|
||||||
|
|
||||||
#define container_of(ptr, type, member) \
|
struct size {
|
||||||
((type *) (((char *) (ptr)) - offsetof(type, member)))
|
Uint16 width;
|
||||||
|
Uint16 height;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct point {
|
||||||
|
Uint16 x;
|
||||||
|
Uint16 y;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct position {
|
||||||
|
// The video screen size may be different from the real device screen size,
|
||||||
|
// so store to which size the absolute position apply, to scale it accordingly.
|
||||||
|
struct size screen_size;
|
||||||
|
struct point point;
|
||||||
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
110
app/src/compat.c
110
app/src/compat.c
@ -1,110 +0,0 @@
|
|||||||
#include "compat.h"
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#ifndef HAVE_REALLOCARRAY
|
|
||||||
# include <errno.h>
|
|
||||||
#endif
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <stdarg.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#ifndef HAVE_STRDUP
|
|
||||||
char *strdup(const char *s) {
|
|
||||||
size_t size = strlen(s) + 1;
|
|
||||||
char *dup = malloc(size);
|
|
||||||
if (dup) {
|
|
||||||
memcpy(dup, s, size);
|
|
||||||
}
|
|
||||||
return dup;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_ASPRINTF
|
|
||||||
int asprintf(char **strp, const char *fmt, ...) {
|
|
||||||
va_list va;
|
|
||||||
va_start(va, fmt);
|
|
||||||
int ret = vasprintf(strp, fmt, va);
|
|
||||||
va_end(va);
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_VASPRINTF
|
|
||||||
int vasprintf(char **strp, const char *fmt, va_list ap) {
|
|
||||||
va_list va;
|
|
||||||
va_copy(va, ap);
|
|
||||||
int len = vsnprintf(NULL, 0, fmt, va);
|
|
||||||
va_end(va);
|
|
||||||
|
|
||||||
char *str = malloc(len + 1);
|
|
||||||
if (!str) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
va_copy(va, ap);
|
|
||||||
int len2 = vsnprintf(str, len + 1, fmt, va);
|
|
||||||
(void) len2;
|
|
||||||
assert(len == len2);
|
|
||||||
va_end(va);
|
|
||||||
|
|
||||||
*strp = str;
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if !defined(HAVE_NRAND48) || !defined(HAVE_JRAND48)
|
|
||||||
#define SC_RAND48_MASK UINT64_C(0xFFFFFFFFFFFF) // 48 bits
|
|
||||||
#define SC_RAND48_A UINT64_C(0x5DEECE66D)
|
|
||||||
#define SC_RAND48_C 0xB
|
|
||||||
static inline uint64_t rand_iter48(uint64_t x) {
|
|
||||||
assert((x & ~SC_RAND48_MASK) == 0);
|
|
||||||
return (x * SC_RAND48_A + SC_RAND48_C) & SC_RAND48_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint64_t rand_iter48_xsubi(unsigned short xsubi[3]) {
|
|
||||||
uint64_t x = ((uint64_t) xsubi[0] << 32)
|
|
||||||
| ((uint64_t) xsubi[1] << 16)
|
|
||||||
| xsubi[2];
|
|
||||||
|
|
||||||
x = rand_iter48(x);
|
|
||||||
|
|
||||||
xsubi[0] = (x >> 32) & 0XFFFF;
|
|
||||||
xsubi[1] = (x >> 16) & 0XFFFF;
|
|
||||||
xsubi[2] = x & 0XFFFF;
|
|
||||||
|
|
||||||
return x;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef HAVE_NRAND48
|
|
||||||
long nrand48(unsigned short xsubi[3]) {
|
|
||||||
// range [0, 2^31)
|
|
||||||
return rand_iter48_xsubi(xsubi) >> 17;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_JRAND48
|
|
||||||
long jrand48(unsigned short xsubi[3]) {
|
|
||||||
// range [-2^31, 2^31)
|
|
||||||
union {
|
|
||||||
uint32_t u;
|
|
||||||
int32_t i;
|
|
||||||
} v;
|
|
||||||
v.u = rand_iter48_xsubi(xsubi) >> 16;
|
|
||||||
return v.i;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_REALLOCARRAY
|
|
||||||
void *reallocarray(void *ptr, size_t nmemb, size_t size) {
|
|
||||||
size_t bytes;
|
|
||||||
if (__builtin_mul_overflow(nmemb, size, &bytes)) {
|
|
||||||
errno = ENOMEM;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return realloc(ptr, bytes);
|
|
||||||
}
|
|
||||||
#endif
|
|
102
app/src/compat.h
102
app/src/compat.h
@ -1,102 +0,0 @@
|
|||||||
#ifndef SC_COMPAT_H
|
|
||||||
#define SC_COMPAT_H
|
|
||||||
|
|
||||||
#include "config.h"
|
|
||||||
|
|
||||||
#include <libavcodec/version.h>
|
|
||||||
#include <libavformat/version.h>
|
|
||||||
#include <libavutil/version.h>
|
|
||||||
#include <SDL2/SDL_version.h>
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
# define PRIu64_ PRIu64
|
|
||||||
# define SC_PRIsizet "zu"
|
|
||||||
#else
|
|
||||||
# define PRIu64_ "I64u" // Windows...
|
|
||||||
# define SC_PRIsizet "Iu"
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// In ffmpeg/doc/APIchanges:
|
|
||||||
// 2018-02-06 - 0694d87024 - lavf 58.9.100 - avformat.h
|
|
||||||
// Deprecate use of av_register_input_format(), av_register_output_format(),
|
|
||||||
// av_register_all(), av_iformat_next(), av_oformat_next().
|
|
||||||
// Add av_demuxer_iterate(), and av_muxer_iterate().
|
|
||||||
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 9, 100)
|
|
||||||
# define SCRCPY_LAVF_HAS_NEW_MUXER_ITERATOR_API
|
|
||||||
#else
|
|
||||||
# define SCRCPY_LAVF_REQUIRES_REGISTER_ALL
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Not documented in ffmpeg/doc/APIchanges, but AV_CODEC_ID_AV1 has been added
|
|
||||||
// by FFmpeg commit d42809f9835a4e9e5c7c63210abb09ad0ef19cfb (included in tag
|
|
||||||
// n3.3).
|
|
||||||
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 89, 100)
|
|
||||||
# define SCRCPY_LAVC_HAS_AV1
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// In ffmpeg/doc/APIchanges:
|
|
||||||
// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h
|
|
||||||
// Deprecate AVFormatContext filename field which had limited length, use the
|
|
||||||
// new dynamically allocated url field instead.
|
|
||||||
//
|
|
||||||
// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h
|
|
||||||
// Add url field to AVFormatContext and add ff_format_set_url helper function.
|
|
||||||
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(58, 7, 100)
|
|
||||||
# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Not documented in ffmpeg/doc/APIchanges, but the channel_layout API
|
|
||||||
// has been replaced by chlayout in FFmpeg commit
|
|
||||||
// f423497b455da06c1337846902c770028760e094.
|
|
||||||
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100)
|
|
||||||
# define SCRCPY_LAVU_HAS_CHLAYOUT
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// In ffmpeg/doc/APIchanges:
|
|
||||||
// 2023-10-06 - 5432d2aacad - lavc 60.15.100 - avformat.h
|
|
||||||
// Deprecate AVFormatContext.{nb_,}side_data, av_stream_add_side_data(),
|
|
||||||
// av_stream_new_side_data(), and av_stream_get_side_data(). Side data fields
|
|
||||||
// from AVFormatContext.codecpar should be used from now on.
|
|
||||||
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 15, 100)
|
|
||||||
# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 6)
|
|
||||||
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
|
|
||||||
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 8)
|
|
||||||
// <https://hg.libsdl.org/SDL/rev/dfde5d3f9781>
|
|
||||||
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 16)
|
|
||||||
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_STRDUP
|
|
||||||
char *strdup(const char *s);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_ASPRINTF
|
|
||||||
int asprintf(char **strp, const char *fmt, ...);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_VASPRINTF
|
|
||||||
int vasprintf(char **strp, const char *fmt, va_list ap);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_NRAND48
|
|
||||||
long nrand48(unsigned short xsubi[3]);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_JRAND48
|
|
||||||
long jrand48(unsigned short xsubi[3]);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_REALLOCARRAY
|
|
||||||
void *reallocarray(void *ptr, size_t nmemb, size_t size);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,349 +0,0 @@
|
|||||||
#include "control_msg.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <inttypes.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "util/binary.h"
|
|
||||||
#include "util/log.h"
|
|
||||||
#include "util/str.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Map an enum value to a string based on an array, without crashing on an
|
|
||||||
* out-of-bounds index.
|
|
||||||
*/
|
|
||||||
#define ENUM_TO_LABEL(labels, value) \
|
|
||||||
((size_t) (value) < ARRAY_LEN(labels) ? labels[value] : "???")
|
|
||||||
|
|
||||||
#define KEYEVENT_ACTION_LABEL(value) \
|
|
||||||
ENUM_TO_LABEL(android_keyevent_action_labels, value)
|
|
||||||
|
|
||||||
#define MOTIONEVENT_ACTION_LABEL(value) \
|
|
||||||
ENUM_TO_LABEL(android_motionevent_action_labels, value)
|
|
||||||
|
|
||||||
static const char *const android_keyevent_action_labels[] = {
|
|
||||||
"down",
|
|
||||||
"up",
|
|
||||||
"multi",
|
|
||||||
};
|
|
||||||
|
|
||||||
static const char *const android_motionevent_action_labels[] = {
|
|
||||||
"down",
|
|
||||||
"up",
|
|
||||||
"move",
|
|
||||||
"cancel",
|
|
||||||
"outside",
|
|
||||||
"pointer-down",
|
|
||||||
"pointer-up",
|
|
||||||
"hover-move",
|
|
||||||
"scroll",
|
|
||||||
"hover-enter",
|
|
||||||
"hover-exit",
|
|
||||||
"btn-press",
|
|
||||||
"btn-release",
|
|
||||||
};
|
|
||||||
|
|
||||||
static const char *const copy_key_labels[] = {
|
|
||||||
"none",
|
|
||||||
"copy",
|
|
||||||
"cut",
|
|
||||||
};
|
|
||||||
|
|
||||||
static inline const char *
|
|
||||||
get_well_known_pointer_id_name(uint64_t pointer_id) {
|
|
||||||
switch (pointer_id) {
|
|
||||||
case SC_POINTER_ID_MOUSE:
|
|
||||||
return "mouse";
|
|
||||||
case SC_POINTER_ID_GENERIC_FINGER:
|
|
||||||
return "finger";
|
|
||||||
case SC_POINTER_ID_VIRTUAL_FINGER:
|
|
||||||
return "vfinger";
|
|
||||||
default:
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
write_position(uint8_t *buf, const struct sc_position *position) {
|
|
||||||
sc_write32be(&buf[0], position->point.x);
|
|
||||||
sc_write32be(&buf[4], position->point.y);
|
|
||||||
sc_write16be(&buf[8], position->screen_size.width);
|
|
||||||
sc_write16be(&buf[10], position->screen_size.height);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write truncated string, and return the size
|
|
||||||
static size_t
|
|
||||||
write_string_payload(uint8_t *payload, const char *utf8, size_t max_len) {
|
|
||||||
if (!utf8) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
size_t len = sc_str_utf8_truncation_index(utf8, max_len);
|
|
||||||
memcpy(payload, utf8, len);
|
|
||||||
return len;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write length (4 bytes) + string (non null-terminated)
|
|
||||||
static size_t
|
|
||||||
write_string(uint8_t *buf, const char *utf8, size_t max_len) {
|
|
||||||
size_t len = write_string_payload(buf + 4, utf8, max_len);
|
|
||||||
sc_write32be(buf, len);
|
|
||||||
return 4 + len;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write length (1 byte) + string (non null-terminated)
|
|
||||||
static size_t
|
|
||||||
write_string_tiny(uint8_t *buf, const char *utf8, size_t max_len) {
|
|
||||||
assert(max_len <= 0xFF);
|
|
||||||
size_t len = write_string_payload(buf + 1, utf8, max_len);
|
|
||||||
buf[0] = len;
|
|
||||||
return 1 + len;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t
|
|
||||||
sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) {
|
|
||||||
buf[0] = msg->type;
|
|
||||||
switch (msg->type) {
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE:
|
|
||||||
buf[1] = msg->inject_keycode.action;
|
|
||||||
sc_write32be(&buf[2], msg->inject_keycode.keycode);
|
|
||||||
sc_write32be(&buf[6], msg->inject_keycode.repeat);
|
|
||||||
sc_write32be(&buf[10], msg->inject_keycode.metastate);
|
|
||||||
return 14;
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_TEXT: {
|
|
||||||
size_t len = write_string(&buf[1], msg->inject_text.text,
|
|
||||||
SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH);
|
|
||||||
return 1 + len;
|
|
||||||
}
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT:
|
|
||||||
buf[1] = msg->inject_touch_event.action;
|
|
||||||
sc_write64be(&buf[2], msg->inject_touch_event.pointer_id);
|
|
||||||
write_position(&buf[10], &msg->inject_touch_event.position);
|
|
||||||
uint16_t pressure =
|
|
||||||
sc_float_to_u16fp(msg->inject_touch_event.pressure);
|
|
||||||
sc_write16be(&buf[22], pressure);
|
|
||||||
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
|
|
||||||
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
|
|
||||||
return 32;
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
|
||||||
write_position(&buf[1], &msg->inject_scroll_event.position);
|
|
||||||
int16_t hscroll =
|
|
||||||
sc_float_to_i16fp(msg->inject_scroll_event.hscroll);
|
|
||||||
int16_t vscroll =
|
|
||||||
sc_float_to_i16fp(msg->inject_scroll_event.vscroll);
|
|
||||||
sc_write16be(&buf[13], (uint16_t) hscroll);
|
|
||||||
sc_write16be(&buf[15], (uint16_t) vscroll);
|
|
||||||
sc_write32be(&buf[17], msg->inject_scroll_event.buttons);
|
|
||||||
return 21;
|
|
||||||
case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
|
|
||||||
buf[1] = msg->inject_keycode.action;
|
|
||||||
return 2;
|
|
||||||
case SC_CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
|
||||||
buf[1] = msg->get_clipboard.copy_key;
|
|
||||||
return 2;
|
|
||||||
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
|
|
||||||
sc_write64be(&buf[1], msg->set_clipboard.sequence);
|
|
||||||
buf[9] = !!msg->set_clipboard.paste;
|
|
||||||
size_t len = write_string(&buf[10], msg->set_clipboard.text,
|
|
||||||
SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH);
|
|
||||||
return 10 + len;
|
|
||||||
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
|
|
||||||
buf[1] = msg->set_display_power.on;
|
|
||||||
return 2;
|
|
||||||
case SC_CONTROL_MSG_TYPE_UHID_CREATE:
|
|
||||||
sc_write16be(&buf[1], msg->uhid_create.id);
|
|
||||||
sc_write16be(&buf[3], msg->uhid_create.vendor_id);
|
|
||||||
sc_write16be(&buf[5], msg->uhid_create.product_id);
|
|
||||||
|
|
||||||
size_t index = 7;
|
|
||||||
index += write_string_tiny(&buf[index], msg->uhid_create.name, 127);
|
|
||||||
|
|
||||||
sc_write16be(&buf[index], msg->uhid_create.report_desc_size);
|
|
||||||
index += 2;
|
|
||||||
|
|
||||||
memcpy(&buf[index], msg->uhid_create.report_desc,
|
|
||||||
msg->uhid_create.report_desc_size);
|
|
||||||
index += msg->uhid_create.report_desc_size;
|
|
||||||
|
|
||||||
return index;
|
|
||||||
case SC_CONTROL_MSG_TYPE_UHID_INPUT:
|
|
||||||
sc_write16be(&buf[1], msg->uhid_input.id);
|
|
||||||
sc_write16be(&buf[3], msg->uhid_input.size);
|
|
||||||
memcpy(&buf[5], msg->uhid_input.data, msg->uhid_input.size);
|
|
||||||
return 5 + msg->uhid_input.size;
|
|
||||||
case SC_CONTROL_MSG_TYPE_UHID_DESTROY:
|
|
||||||
sc_write16be(&buf[1], msg->uhid_destroy.id);
|
|
||||||
return 3;
|
|
||||||
case SC_CONTROL_MSG_TYPE_START_APP: {
|
|
||||||
size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255);
|
|
||||||
return 1 + len;
|
|
||||||
}
|
|
||||||
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
|
||||||
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
|
||||||
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
|
||||||
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
|
||||||
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
|
||||||
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
|
|
||||||
// no additional data
|
|
||||||
return 1;
|
|
||||||
default:
|
|
||||||
LOGW("Unknown message type: %u", (unsigned) msg->type);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_control_msg_log(const struct sc_control_msg *msg) {
|
|
||||||
#define LOG_CMSG(fmt, ...) LOGV("input: " fmt, ## __VA_ARGS__)
|
|
||||||
switch (msg->type) {
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_KEYCODE:
|
|
||||||
LOG_CMSG("key %-4s code=%d repeat=%" PRIu32 " meta=%06lx",
|
|
||||||
KEYEVENT_ACTION_LABEL(msg->inject_keycode.action),
|
|
||||||
(int) msg->inject_keycode.keycode,
|
|
||||||
msg->inject_keycode.repeat,
|
|
||||||
(long) msg->inject_keycode.metastate);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_TEXT:
|
|
||||||
LOG_CMSG("text \"%s\"", msg->inject_text.text);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT: {
|
|
||||||
int action = msg->inject_touch_event.action
|
|
||||||
& AMOTION_EVENT_ACTION_MASK;
|
|
||||||
uint64_t id = msg->inject_touch_event.pointer_id;
|
|
||||||
const char *pointer_name = get_well_known_pointer_id_name(id);
|
|
||||||
if (pointer_name) {
|
|
||||||
// string pointer id
|
|
||||||
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
|
|
||||||
" pressure=%f action_button=%06lx buttons=%06lx",
|
|
||||||
pointer_name,
|
|
||||||
MOTIONEVENT_ACTION_LABEL(action),
|
|
||||||
msg->inject_touch_event.position.point.x,
|
|
||||||
msg->inject_touch_event.position.point.y,
|
|
||||||
msg->inject_touch_event.pressure,
|
|
||||||
(long) msg->inject_touch_event.action_button,
|
|
||||||
(long) msg->inject_touch_event.buttons);
|
|
||||||
} else {
|
|
||||||
// numeric pointer id
|
|
||||||
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
|
|
||||||
PRIi32 " pressure=%f action_button=%06lx"
|
|
||||||
" buttons=%06lx",
|
|
||||||
id,
|
|
||||||
MOTIONEVENT_ACTION_LABEL(action),
|
|
||||||
msg->inject_touch_event.position.point.x,
|
|
||||||
msg->inject_touch_event.position.point.y,
|
|
||||||
msg->inject_touch_event.pressure,
|
|
||||||
(long) msg->inject_touch_event.action_button,
|
|
||||||
(long) msg->inject_touch_event.buttons);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
|
||||||
LOG_CMSG("scroll position=%" PRIi32 ",%" PRIi32 " hscroll=%f"
|
|
||||||
" vscroll=%f buttons=%06lx",
|
|
||||||
msg->inject_scroll_event.position.point.x,
|
|
||||||
msg->inject_scroll_event.position.point.y,
|
|
||||||
msg->inject_scroll_event.hscroll,
|
|
||||||
msg->inject_scroll_event.vscroll,
|
|
||||||
(long) msg->inject_scroll_event.buttons);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON:
|
|
||||||
LOG_CMSG("back-or-screen-on %s",
|
|
||||||
KEYEVENT_ACTION_LABEL(msg->inject_keycode.action));
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_GET_CLIPBOARD:
|
|
||||||
LOG_CMSG("get clipboard copy_key=%s",
|
|
||||||
copy_key_labels[msg->get_clipboard.copy_key]);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
|
|
||||||
LOG_CMSG("clipboard %" PRIu64_ " %s \"%s\"",
|
|
||||||
msg->set_clipboard.sequence,
|
|
||||||
msg->set_clipboard.paste ? "paste" : "nopaste",
|
|
||||||
msg->set_clipboard.text);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER:
|
|
||||||
LOG_CMSG("display power %s",
|
|
||||||
msg->set_display_power.on ? "on" : "off");
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL:
|
|
||||||
LOG_CMSG("expand notification panel");
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL:
|
|
||||||
LOG_CMSG("expand settings panel");
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS:
|
|
||||||
LOG_CMSG("collapse panels");
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE:
|
|
||||||
LOG_CMSG("rotate device");
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_UHID_CREATE: {
|
|
||||||
// Quote only if name is not null
|
|
||||||
const char *name = msg->uhid_create.name;
|
|
||||||
const char *quote = name ? "\"" : "";
|
|
||||||
LOG_CMSG("UHID create [%" PRIu16 "] %04" PRIx16 ":%04" PRIx16
|
|
||||||
" name=%s%s%s report_desc_size=%" PRIu16,
|
|
||||||
msg->uhid_create.id,
|
|
||||||
msg->uhid_create.vendor_id,
|
|
||||||
msg->uhid_create.product_id,
|
|
||||||
quote, name, quote,
|
|
||||||
msg->uhid_create.report_desc_size);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SC_CONTROL_MSG_TYPE_UHID_INPUT: {
|
|
||||||
char *hex = sc_str_to_hex_string(msg->uhid_input.data,
|
|
||||||
msg->uhid_input.size);
|
|
||||||
if (hex) {
|
|
||||||
LOG_CMSG("UHID input [%" PRIu16 "] %s",
|
|
||||||
msg->uhid_input.id, hex);
|
|
||||||
free(hex);
|
|
||||||
} else {
|
|
||||||
LOG_CMSG("UHID input [%" PRIu16 "] size=%" PRIu16,
|
|
||||||
msg->uhid_input.id, msg->uhid_input.size);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case SC_CONTROL_MSG_TYPE_UHID_DESTROY:
|
|
||||||
LOG_CMSG("UHID destroy [%" PRIu16 "]", msg->uhid_destroy.id);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS:
|
|
||||||
LOG_CMSG("open hard keyboard settings");
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_START_APP:
|
|
||||||
LOG_CMSG("start app \"%s\"", msg->start_app.name);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_RESET_VIDEO:
|
|
||||||
LOG_CMSG("reset video");
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
LOG_CMSG("unknown type: %u", (unsigned) msg->type);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_control_msg_is_droppable(const struct sc_control_msg *msg) {
|
|
||||||
// Cannot drop UHID_CREATE messages, because it would cause all further
|
|
||||||
// UHID_INPUT messages for this device to be invalid.
|
|
||||||
// Cannot drop UHID_DESTROY messages either, because a further UHID_CREATE
|
|
||||||
// with the same id may fail.
|
|
||||||
return msg->type != SC_CONTROL_MSG_TYPE_UHID_CREATE
|
|
||||||
&& msg->type != SC_CONTROL_MSG_TYPE_UHID_DESTROY;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_control_msg_destroy(struct sc_control_msg *msg) {
|
|
||||||
switch (msg->type) {
|
|
||||||
case SC_CONTROL_MSG_TYPE_INJECT_TEXT:
|
|
||||||
free(msg->inject_text.text);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD:
|
|
||||||
free(msg->set_clipboard.text);
|
|
||||||
break;
|
|
||||||
case SC_CONTROL_MSG_TYPE_START_APP:
|
|
||||||
free(msg->start_app.name);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// do nothing
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,133 +0,0 @@
|
|||||||
#ifndef SC_CONTROLMSG_H
|
|
||||||
#define SC_CONTROLMSG_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#include "android/input.h"
|
|
||||||
#include "android/keycodes.h"
|
|
||||||
#include "coords.h"
|
|
||||||
#include "hid/hid_event.h"
|
|
||||||
|
|
||||||
#define SC_CONTROL_MSG_MAX_SIZE (1 << 18) // 256k
|
|
||||||
|
|
||||||
#define SC_CONTROL_MSG_INJECT_TEXT_MAX_LENGTH 300
|
|
||||||
// type: 1 byte; sequence: 8 bytes; paste flag: 1 byte; length: 4 bytes
|
|
||||||
#define SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH (SC_CONTROL_MSG_MAX_SIZE - 14)
|
|
||||||
|
|
||||||
#define SC_POINTER_ID_MOUSE UINT64_C(-1)
|
|
||||||
#define SC_POINTER_ID_GENERIC_FINGER UINT64_C(-2)
|
|
||||||
|
|
||||||
// Used for injecting an additional virtual pointer for pinch-to-zoom
|
|
||||||
#define SC_POINTER_ID_VIRTUAL_FINGER UINT64_C(-3)
|
|
||||||
|
|
||||||
enum sc_control_msg_type {
|
|
||||||
SC_CONTROL_MSG_TYPE_INJECT_KEYCODE,
|
|
||||||
SC_CONTROL_MSG_TYPE_INJECT_TEXT,
|
|
||||||
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
|
||||||
SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT,
|
|
||||||
SC_CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON,
|
|
||||||
SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL,
|
|
||||||
SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL,
|
|
||||||
SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS,
|
|
||||||
SC_CONTROL_MSG_TYPE_GET_CLIPBOARD,
|
|
||||||
SC_CONTROL_MSG_TYPE_SET_CLIPBOARD,
|
|
||||||
SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER,
|
|
||||||
SC_CONTROL_MSG_TYPE_ROTATE_DEVICE,
|
|
||||||
SC_CONTROL_MSG_TYPE_UHID_CREATE,
|
|
||||||
SC_CONTROL_MSG_TYPE_UHID_INPUT,
|
|
||||||
SC_CONTROL_MSG_TYPE_UHID_DESTROY,
|
|
||||||
SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS,
|
|
||||||
SC_CONTROL_MSG_TYPE_START_APP,
|
|
||||||
SC_CONTROL_MSG_TYPE_RESET_VIDEO,
|
|
||||||
};
|
|
||||||
|
|
||||||
enum sc_copy_key {
|
|
||||||
SC_COPY_KEY_NONE,
|
|
||||||
SC_COPY_KEY_COPY,
|
|
||||||
SC_COPY_KEY_CUT,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_control_msg {
|
|
||||||
enum sc_control_msg_type type;
|
|
||||||
union {
|
|
||||||
struct {
|
|
||||||
enum android_keyevent_action action;
|
|
||||||
enum android_keycode keycode;
|
|
||||||
uint32_t repeat;
|
|
||||||
enum android_metastate metastate;
|
|
||||||
} inject_keycode;
|
|
||||||
struct {
|
|
||||||
char *text; // owned, to be freed by free()
|
|
||||||
} inject_text;
|
|
||||||
struct {
|
|
||||||
enum android_motionevent_action action;
|
|
||||||
enum android_motionevent_buttons action_button;
|
|
||||||
enum android_motionevent_buttons buttons;
|
|
||||||
uint64_t pointer_id;
|
|
||||||
struct sc_position position;
|
|
||||||
float pressure;
|
|
||||||
} inject_touch_event;
|
|
||||||
struct {
|
|
||||||
struct sc_position position;
|
|
||||||
float hscroll;
|
|
||||||
float vscroll;
|
|
||||||
enum android_motionevent_buttons buttons;
|
|
||||||
} inject_scroll_event;
|
|
||||||
struct {
|
|
||||||
enum android_keyevent_action action; // action for the BACK key
|
|
||||||
// screen may only be turned on on ACTION_DOWN
|
|
||||||
} back_or_screen_on;
|
|
||||||
struct {
|
|
||||||
enum sc_copy_key copy_key;
|
|
||||||
} get_clipboard;
|
|
||||||
struct {
|
|
||||||
uint64_t sequence;
|
|
||||||
char *text; // owned, to be freed by free()
|
|
||||||
bool paste;
|
|
||||||
} set_clipboard;
|
|
||||||
struct {
|
|
||||||
bool on;
|
|
||||||
} set_display_power;
|
|
||||||
struct {
|
|
||||||
uint16_t id;
|
|
||||||
uint16_t vendor_id;
|
|
||||||
uint16_t product_id;
|
|
||||||
const char *name; // pointer to static data
|
|
||||||
uint16_t report_desc_size;
|
|
||||||
const uint8_t *report_desc; // pointer to static data
|
|
||||||
} uhid_create;
|
|
||||||
struct {
|
|
||||||
uint16_t id;
|
|
||||||
uint16_t size;
|
|
||||||
uint8_t data[SC_HID_MAX_SIZE];
|
|
||||||
} uhid_input;
|
|
||||||
struct {
|
|
||||||
uint16_t id;
|
|
||||||
} uhid_destroy;
|
|
||||||
struct {
|
|
||||||
char *name;
|
|
||||||
} start_app;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// buf size must be at least CONTROL_MSG_MAX_SIZE
|
|
||||||
// return the number of bytes written
|
|
||||||
size_t
|
|
||||||
sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_control_msg_log(const struct sc_control_msg *msg);
|
|
||||||
|
|
||||||
// Even when the buffer is "full", some messages must absolutely not be dropped
|
|
||||||
// to avoid inconsistencies.
|
|
||||||
bool
|
|
||||||
sc_control_msg_is_droppable(const struct sc_control_msg *msg);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_control_msg_destroy(struct sc_control_msg *msg);
|
|
||||||
|
|
||||||
#endif
|
|
110
app/src/controlevent.c
Normal file
110
app/src/controlevent.c
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#include "controlevent.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
|
||||||
|
#include "lockutil.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
static inline void write16(Uint8 *buf, Uint16 value) {
|
||||||
|
buf[0] = value >> 8;
|
||||||
|
buf[1] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void write32(Uint8 *buf, Uint32 value) {
|
||||||
|
buf[0] = value >> 24;
|
||||||
|
buf[1] = value >> 16;
|
||||||
|
buf[2] = value >> 8;
|
||||||
|
buf[3] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void write_position(Uint8 *buf, const struct position *position) {
|
||||||
|
write16(&buf[0], position->point.x);
|
||||||
|
write16(&buf[2], position->point.y);
|
||||||
|
write16(&buf[4], position->screen_size.width);
|
||||||
|
write16(&buf[6], position->screen_size.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
int control_event_serialize(const struct control_event *event, unsigned char *buf) {
|
||||||
|
buf[0] = event->type;
|
||||||
|
switch (event->type) {
|
||||||
|
case CONTROL_EVENT_TYPE_KEYCODE:
|
||||||
|
buf[1] = event->keycode_event.action;
|
||||||
|
write32(&buf[2], event->keycode_event.keycode);
|
||||||
|
write32(&buf[6], event->keycode_event.metastate);
|
||||||
|
return 10;
|
||||||
|
case CONTROL_EVENT_TYPE_TEXT: {
|
||||||
|
// write length (1 byte) + date (non nul-terminated)
|
||||||
|
size_t len = strlen(event->text_event.text);
|
||||||
|
if (len > TEXT_MAX_LENGTH) {
|
||||||
|
// injecting a text takes time, so limit the text length
|
||||||
|
len = TEXT_MAX_LENGTH;
|
||||||
|
}
|
||||||
|
write16(&buf[1], (Uint16) len);
|
||||||
|
memcpy(&buf[3], event->text_event.text, len);
|
||||||
|
return 3 + len;
|
||||||
|
}
|
||||||
|
case CONTROL_EVENT_TYPE_MOUSE:
|
||||||
|
buf[1] = event->mouse_event.action;
|
||||||
|
write32(&buf[2], event->mouse_event.buttons);
|
||||||
|
write_position(&buf[6], &event->mouse_event.position);
|
||||||
|
return 14;
|
||||||
|
case CONTROL_EVENT_TYPE_SCROLL:
|
||||||
|
write_position(&buf[1], &event->scroll_event.position);
|
||||||
|
write32(&buf[9], (Uint32) event->scroll_event.hscroll);
|
||||||
|
write32(&buf[13], (Uint32) event->scroll_event.vscroll);
|
||||||
|
return 17;
|
||||||
|
case CONTROL_EVENT_TYPE_COMMAND:
|
||||||
|
buf[1] = event->command_event.action;
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
LOGW("Unknown event type: %u", (unsigned) event->type);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void control_event_destroy(struct control_event *event) {
|
||||||
|
if (event->type == CONTROL_EVENT_TYPE_TEXT) {
|
||||||
|
SDL_free(event->text_event.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue) {
|
||||||
|
return queue->head == queue->tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool control_event_queue_is_full(const struct control_event_queue *queue) {
|
||||||
|
return (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE == queue->tail;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool control_event_queue_init(struct control_event_queue *queue) {
|
||||||
|
queue->head = 0;
|
||||||
|
queue->tail = 0;
|
||||||
|
// the current implementation may not fail
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void control_event_queue_destroy(struct control_event_queue *queue) {
|
||||||
|
int i = queue->tail;
|
||||||
|
while (i != queue->head) {
|
||||||
|
control_event_destroy(&queue->data[i]);
|
||||||
|
i = (i + 1) % CONTROL_EVENT_QUEUE_SIZE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event) {
|
||||||
|
if (control_event_queue_is_full(queue)) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
queue->data[queue->head] = *event;
|
||||||
|
queue->head = (queue->head + 1) % CONTROL_EVENT_QUEUE_SIZE;
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event) {
|
||||||
|
if (control_event_queue_is_empty(queue)) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
*event = queue->data[queue->tail];
|
||||||
|
queue->tail = (queue->tail + 1) % CONTROL_EVENT_QUEUE_SIZE;
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
73
app/src/controlevent.h
Normal file
73
app/src/controlevent.h
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
#ifndef CONTROLEVENT_H
|
||||||
|
#define CONTROLEVENT_H
|
||||||
|
|
||||||
|
#include <SDL2/SDL_mutex.h>
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
|
||||||
|
#include "android/input.h"
|
||||||
|
#include "android/keycodes.h"
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#define CONTROL_EVENT_QUEUE_SIZE 64
|
||||||
|
#define TEXT_MAX_LENGTH 300
|
||||||
|
#define SERIALIZED_EVENT_MAX_SIZE 3 + TEXT_MAX_LENGTH
|
||||||
|
|
||||||
|
enum control_event_type {
|
||||||
|
CONTROL_EVENT_TYPE_KEYCODE,
|
||||||
|
CONTROL_EVENT_TYPE_TEXT,
|
||||||
|
CONTROL_EVENT_TYPE_MOUSE,
|
||||||
|
CONTROL_EVENT_TYPE_SCROLL,
|
||||||
|
CONTROL_EVENT_TYPE_COMMAND,
|
||||||
|
};
|
||||||
|
|
||||||
|
#define CONTROL_EVENT_COMMAND_BACK_OR_SCREEN_ON 0
|
||||||
|
|
||||||
|
struct control_event {
|
||||||
|
enum control_event_type type;
|
||||||
|
union {
|
||||||
|
struct {
|
||||||
|
enum android_keyevent_action action;
|
||||||
|
enum android_keycode keycode;
|
||||||
|
enum android_metastate metastate;
|
||||||
|
} keycode_event;
|
||||||
|
struct {
|
||||||
|
char *text; // owned, to be freed by SDL_free()
|
||||||
|
} text_event;
|
||||||
|
struct {
|
||||||
|
enum android_motionevent_action action;
|
||||||
|
enum android_motionevent_buttons buttons;
|
||||||
|
struct position position;
|
||||||
|
} mouse_event;
|
||||||
|
struct {
|
||||||
|
struct position position;
|
||||||
|
Sint32 hscroll;
|
||||||
|
Sint32 vscroll;
|
||||||
|
} scroll_event;
|
||||||
|
struct {
|
||||||
|
int action;
|
||||||
|
} command_event;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct control_event_queue {
|
||||||
|
struct control_event data[CONTROL_EVENT_QUEUE_SIZE];
|
||||||
|
int head;
|
||||||
|
int tail;
|
||||||
|
};
|
||||||
|
|
||||||
|
// buf size must be at least SERIALIZED_EVENT_MAX_SIZE
|
||||||
|
int control_event_serialize(const struct control_event *event, unsigned char *buf);
|
||||||
|
|
||||||
|
SDL_bool control_event_queue_init(struct control_event_queue *queue);
|
||||||
|
void control_event_queue_destroy(struct control_event_queue *queue);
|
||||||
|
|
||||||
|
SDL_bool control_event_queue_is_empty(const struct control_event_queue *queue);
|
||||||
|
SDL_bool control_event_queue_is_full(const struct control_event_queue *queue);
|
||||||
|
|
||||||
|
// event is copied, the queue does not use the event after the function returns
|
||||||
|
SDL_bool control_event_queue_push(struct control_event_queue *queue, const struct control_event *event);
|
||||||
|
SDL_bool control_event_queue_take(struct control_event_queue *queue, struct control_event *event);
|
||||||
|
|
||||||
|
void control_event_destroy(struct control_event *event);
|
||||||
|
|
||||||
|
#endif
|
@ -1,218 +1,102 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
|
|
||||||
#include <assert.h>
|
#include "lockutil.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
#include "util/log.h"
|
SDL_bool controller_init(struct controller *controller, socket_t video_socket) {
|
||||||
|
if (!control_event_queue_init(&controller->queue)) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
// Drop droppable events above this limit
|
if (!(controller->mutex = SDL_CreateMutex())) {
|
||||||
#define SC_CONTROL_MSG_QUEUE_LIMIT 60
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
if (!(controller->event_cond = SDL_CreateCond())) {
|
||||||
sc_controller_receiver_on_ended(struct sc_receiver *receiver, bool error,
|
SDL_DestroyMutex(controller->mutex);
|
||||||
void *userdata) {
|
return SDL_FALSE;
|
||||||
(void) receiver;
|
}
|
||||||
|
|
||||||
struct sc_controller *controller = userdata;
|
controller->video_socket = video_socket;
|
||||||
// Forward the event to the controller listener
|
controller->stopped = SDL_FALSE;
|
||||||
controller->cbs->on_ended(controller, error, controller->cbs_userdata);
|
|
||||||
|
return SDL_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
void controller_destroy(struct controller *controller) {
|
||||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
SDL_DestroyCond(controller->event_cond);
|
||||||
const struct sc_controller_callbacks *cbs,
|
SDL_DestroyMutex(controller->mutex);
|
||||||
void *cbs_userdata) {
|
control_event_queue_destroy(&controller->queue);
|
||||||
sc_vecdeque_init(&controller->queue);
|
|
||||||
|
|
||||||
// Add 4 to support 4 non-droppable events without re-allocation
|
|
||||||
bool ok = sc_vecdeque_reserve(&controller->queue,
|
|
||||||
SC_CONTROL_MSG_QUEUE_LIMIT + 4);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static const struct sc_receiver_callbacks receiver_cbs = {
|
|
||||||
.on_ended = sc_controller_receiver_on_ended,
|
|
||||||
};
|
|
||||||
|
|
||||||
ok = sc_receiver_init(&controller->receiver, control_socket, &receiver_cbs,
|
|
||||||
controller);
|
|
||||||
if (!ok) {
|
|
||||||
sc_vecdeque_destroy(&controller->queue);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_mutex_init(&controller->mutex);
|
|
||||||
if (!ok) {
|
|
||||||
sc_receiver_destroy(&controller->receiver);
|
|
||||||
sc_vecdeque_destroy(&controller->queue);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_cond_init(&controller->msg_cond);
|
|
||||||
if (!ok) {
|
|
||||||
sc_receiver_destroy(&controller->receiver);
|
|
||||||
sc_mutex_destroy(&controller->mutex);
|
|
||||||
sc_vecdeque_destroy(&controller->queue);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
controller->control_socket = control_socket;
|
|
||||||
controller->stopped = false;
|
|
||||||
|
|
||||||
assert(cbs && cbs->on_ended);
|
|
||||||
controller->cbs = cbs;
|
|
||||||
controller->cbs_userdata = cbs_userdata;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
SDL_bool controller_push_event(struct controller *controller, const struct control_event *event) {
|
||||||
sc_controller_configure(struct sc_controller *controller,
|
SDL_bool res;
|
||||||
struct sc_acksync *acksync,
|
mutex_lock(controller->mutex);
|
||||||
struct sc_uhid_devices *uhid_devices) {
|
SDL_bool was_empty = control_event_queue_is_empty(&controller->queue);
|
||||||
controller->receiver.acksync = acksync;
|
res = control_event_queue_push(&controller->queue, event);
|
||||||
controller->receiver.uhid_devices = uhid_devices;
|
if (was_empty) {
|
||||||
|
cond_signal(controller->event_cond);
|
||||||
|
}
|
||||||
|
mutex_unlock(controller->mutex);
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
static SDL_bool process_event(struct controller *controller, const struct control_event *event) {
|
||||||
sc_controller_destroy(struct sc_controller *controller) {
|
unsigned char serialized_event[SERIALIZED_EVENT_MAX_SIZE];
|
||||||
sc_cond_destroy(&controller->msg_cond);
|
int length = control_event_serialize(event, serialized_event);
|
||||||
sc_mutex_destroy(&controller->mutex);
|
|
||||||
|
|
||||||
while (!sc_vecdeque_is_empty(&controller->queue)) {
|
|
||||||
struct sc_control_msg *msg = sc_vecdeque_popref(&controller->queue);
|
|
||||||
assert(msg);
|
|
||||||
sc_control_msg_destroy(msg);
|
|
||||||
}
|
|
||||||
sc_vecdeque_destroy(&controller->queue);
|
|
||||||
|
|
||||||
sc_receiver_destroy(&controller->receiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_controller_push_msg(struct sc_controller *controller,
|
|
||||||
const struct sc_control_msg *msg) {
|
|
||||||
if (sc_get_log_level() <= SC_LOG_LEVEL_VERBOSE) {
|
|
||||||
sc_control_msg_log(msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool pushed = false;
|
|
||||||
|
|
||||||
sc_mutex_lock(&controller->mutex);
|
|
||||||
size_t size = sc_vecdeque_size(&controller->queue);
|
|
||||||
if (size < SC_CONTROL_MSG_QUEUE_LIMIT) {
|
|
||||||
bool was_empty = sc_vecdeque_is_empty(&controller->queue);
|
|
||||||
sc_vecdeque_push_noresize(&controller->queue, *msg);
|
|
||||||
pushed = true;
|
|
||||||
if (was_empty) {
|
|
||||||
sc_cond_signal(&controller->msg_cond);
|
|
||||||
}
|
|
||||||
} else if (!sc_control_msg_is_droppable(msg)) {
|
|
||||||
bool ok = sc_vecdeque_push(&controller->queue, *msg);
|
|
||||||
if (ok) {
|
|
||||||
pushed = true;
|
|
||||||
} else {
|
|
||||||
// A non-droppable event must be dropped anyway
|
|
||||||
LOG_OOM();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Otherwise, the msg is discarded
|
|
||||||
|
|
||||||
sc_mutex_unlock(&controller->mutex);
|
|
||||||
|
|
||||||
return pushed;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
process_msg(struct sc_controller *controller,
|
|
||||||
const struct sc_control_msg *msg, bool *eos) {
|
|
||||||
static uint8_t serialized_msg[SC_CONTROL_MSG_MAX_SIZE];
|
|
||||||
size_t length = sc_control_msg_serialize(msg, serialized_msg);
|
|
||||||
if (!length) {
|
if (!length) {
|
||||||
*eos = false;
|
return SDL_FALSE;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
int w = net_send_all(controller->video_socket, serialized_event, length);
|
||||||
ssize_t w =
|
return w == length;
|
||||||
net_send_all(controller->control_socket, serialized_msg, length);
|
|
||||||
if ((size_t) w != length) {
|
|
||||||
*eos = true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int run_controller(void *data) {
|
||||||
run_controller(void *data) {
|
struct controller *controller = data;
|
||||||
struct sc_controller *controller = data;
|
|
||||||
|
|
||||||
bool error = false;
|
|
||||||
|
|
||||||
|
mutex_lock(controller->mutex);
|
||||||
for (;;) {
|
for (;;) {
|
||||||
sc_mutex_lock(&controller->mutex);
|
while (!controller->stopped && control_event_queue_is_empty(&controller->queue)) {
|
||||||
while (!controller->stopped
|
cond_wait(controller->event_cond, controller->mutex);
|
||||||
&& sc_vecdeque_is_empty(&controller->queue)) {
|
|
||||||
sc_cond_wait(&controller->msg_cond, &controller->mutex);
|
|
||||||
}
|
}
|
||||||
if (controller->stopped) {
|
if (controller->stopped) {
|
||||||
// stop immediately, do not process further msgs
|
// stop immediately, do not process further events
|
||||||
sc_mutex_unlock(&controller->mutex);
|
|
||||||
LOGD("Controller stopped");
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
struct control_event event;
|
||||||
assert(!sc_vecdeque_is_empty(&controller->queue));
|
while (control_event_queue_take(&controller->queue, &event)) {
|
||||||
struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue);
|
SDL_bool ok = process_event(controller, &event);
|
||||||
sc_mutex_unlock(&controller->mutex);
|
control_event_destroy(&event);
|
||||||
|
if (!ok) {
|
||||||
bool eos;
|
LOGD("Cannot write event to socket");
|
||||||
bool ok = process_msg(controller, &msg, &eos);
|
goto end;
|
||||||
sc_control_msg_destroy(&msg);
|
}
|
||||||
if (!ok) {
|
|
||||||
if (eos) {
|
|
||||||
LOGD("Controller stopped (socket closed)");
|
|
||||||
} // else error already logged
|
|
||||||
error = !eos;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
end:
|
||||||
controller->cbs->on_ended(controller, error, controller->cbs_userdata);
|
mutex_unlock(controller->mutex);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
SDL_bool controller_start(struct controller *controller) {
|
||||||
sc_controller_start(struct sc_controller *controller) {
|
|
||||||
LOGD("Starting controller thread");
|
LOGD("Starting controller thread");
|
||||||
|
|
||||||
bool ok = sc_thread_create(&controller->thread, run_controller,
|
controller->thread = SDL_CreateThread(run_controller, "controller", controller);
|
||||||
"scrcpy-ctl", controller);
|
if (!controller->thread) {
|
||||||
if (!ok) {
|
LOGC("Could not start controller thread");
|
||||||
LOGE("Could not start controller thread");
|
return SDL_FALSE;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sc_receiver_start(&controller->receiver)) {
|
return SDL_TRUE;
|
||||||
sc_controller_stop(controller);
|
|
||||||
sc_thread_join(&controller->thread, NULL);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void controller_stop(struct controller *controller) {
|
||||||
sc_controller_stop(struct sc_controller *controller) {
|
mutex_lock(controller->mutex);
|
||||||
sc_mutex_lock(&controller->mutex);
|
controller->stopped = SDL_TRUE;
|
||||||
controller->stopped = true;
|
cond_signal(controller->event_cond);
|
||||||
sc_cond_signal(&controller->msg_cond);
|
mutex_unlock(controller->mutex);
|
||||||
sc_mutex_unlock(&controller->mutex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void controller_join(struct controller *controller) {
|
||||||
sc_controller_join(struct sc_controller *controller) {
|
SDL_WaitThread(controller->thread, NULL);
|
||||||
sc_thread_join(&controller->thread, NULL);
|
|
||||||
sc_receiver_join(&controller->receiver);
|
|
||||||
}
|
}
|
||||||
|
@ -1,61 +1,31 @@
|
|||||||
#ifndef SC_CONTROLLER_H
|
#ifndef CONTROL_H
|
||||||
#define SC_CONTROLLER_H
|
#define CONTROL_H
|
||||||
|
|
||||||
#include "common.h"
|
#include "controlevent.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <SDL2/SDL_mutex.h>
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
#include "control_msg.h"
|
#include "net.h"
|
||||||
#include "receiver.h"
|
|
||||||
#include "util/acksync.h"
|
|
||||||
#include "util/net.h"
|
|
||||||
#include "util/thread.h"
|
|
||||||
#include "util/vecdeque.h"
|
|
||||||
|
|
||||||
struct sc_control_msg_queue SC_VECDEQUE(struct sc_control_msg);
|
struct controller {
|
||||||
|
socket_t video_socket;
|
||||||
struct sc_controller {
|
SDL_Thread *thread;
|
||||||
sc_socket control_socket;
|
SDL_mutex *mutex;
|
||||||
sc_thread thread;
|
SDL_cond *event_cond;
|
||||||
sc_mutex mutex;
|
SDL_bool stopped;
|
||||||
sc_cond msg_cond;
|
struct control_event_queue queue;
|
||||||
bool stopped;
|
|
||||||
struct sc_control_msg_queue queue;
|
|
||||||
struct sc_receiver receiver;
|
|
||||||
|
|
||||||
const struct sc_controller_callbacks *cbs;
|
|
||||||
void *cbs_userdata;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_controller_callbacks {
|
SDL_bool controller_init(struct controller *controller, socket_t video_socket);
|
||||||
void (*on_ended)(struct sc_controller *controller, bool error,
|
void controller_destroy(struct controller *controller);
|
||||||
void *userdata);
|
|
||||||
};
|
|
||||||
|
|
||||||
bool
|
SDL_bool controller_start(struct controller *controller);
|
||||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
void controller_stop(struct controller *controller);
|
||||||
const struct sc_controller_callbacks *cbs,
|
void controller_join(struct controller *controller);
|
||||||
void *cbs_userdata);
|
|
||||||
|
|
||||||
void
|
// expose simple API to hide control_event_queue
|
||||||
sc_controller_configure(struct sc_controller *controller,
|
SDL_bool controller_push_event(struct controller *controller, const struct control_event *event);
|
||||||
struct sc_acksync *acksync,
|
|
||||||
struct sc_uhid_devices *uhid_devices);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_controller_destroy(struct sc_controller *controller);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_controller_start(struct sc_controller *controller);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_controller_stop(struct sc_controller *controller);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_controller_join(struct sc_controller *controller);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_controller_push_msg(struct sc_controller *controller,
|
|
||||||
const struct sc_control_msg *msg);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
183
app/src/convert.c
Normal file
183
app/src/convert.c
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
#include "convert.h"
|
||||||
|
|
||||||
|
#define MAP(FROM, TO) case FROM: *to = TO; return SDL_TRUE
|
||||||
|
#define FAIL default: return SDL_FALSE
|
||||||
|
|
||||||
|
static SDL_bool convert_keycode_action(SDL_EventType from, enum android_keyevent_action *to) {
|
||||||
|
switch (from) {
|
||||||
|
MAP(SDL_KEYDOWN, AKEY_EVENT_ACTION_DOWN);
|
||||||
|
MAP(SDL_KEYUP, AKEY_EVENT_ACTION_UP);
|
||||||
|
FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static enum android_metastate autocomplete_metastate(enum android_metastate metastate) {
|
||||||
|
// fill dependant flags
|
||||||
|
if (metastate & (AMETA_SHIFT_LEFT_ON | AMETA_SHIFT_RIGHT_ON)) {
|
||||||
|
metastate |= AMETA_SHIFT_ON;
|
||||||
|
}
|
||||||
|
if (metastate & (AMETA_CTRL_LEFT_ON | AMETA_CTRL_RIGHT_ON)) {
|
||||||
|
metastate |= AMETA_CTRL_ON;
|
||||||
|
}
|
||||||
|
if (metastate & (AMETA_ALT_LEFT_ON | AMETA_ALT_RIGHT_ON)) {
|
||||||
|
metastate |= AMETA_ALT_ON;
|
||||||
|
}
|
||||||
|
if (metastate & (AMETA_META_LEFT_ON | AMETA_META_RIGHT_ON)) {
|
||||||
|
metastate |= AMETA_META_ON;
|
||||||
|
}
|
||||||
|
|
||||||
|
return metastate;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static enum android_metastate convert_meta_state(SDL_Keymod mod) {
|
||||||
|
enum android_metastate metastate = 0;
|
||||||
|
if (mod & KMOD_LSHIFT) {
|
||||||
|
metastate |= AMETA_SHIFT_LEFT_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_RSHIFT) {
|
||||||
|
metastate |= AMETA_SHIFT_RIGHT_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_LCTRL) {
|
||||||
|
metastate |= AMETA_CTRL_LEFT_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_RCTRL) {
|
||||||
|
metastate |= AMETA_CTRL_RIGHT_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_LALT) {
|
||||||
|
metastate |= AMETA_ALT_LEFT_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_RALT) {
|
||||||
|
metastate |= AMETA_ALT_RIGHT_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_LGUI) { // Windows key
|
||||||
|
metastate |= AMETA_META_LEFT_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_RGUI) { // Windows key
|
||||||
|
metastate |= AMETA_META_RIGHT_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_NUM) {
|
||||||
|
metastate |= AMETA_NUM_LOCK_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_CAPS) {
|
||||||
|
metastate |= AMETA_CAPS_LOCK_ON;
|
||||||
|
}
|
||||||
|
if (mod & KMOD_MODE) { // Alt Gr
|
||||||
|
// no mapping?
|
||||||
|
}
|
||||||
|
|
||||||
|
// fill the dependent fields
|
||||||
|
return autocomplete_metastate(metastate);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_bool convert_keycode(SDL_Keycode from, enum android_keycode *to) {
|
||||||
|
switch (from) {
|
||||||
|
MAP(SDLK_RETURN, AKEYCODE_ENTER);
|
||||||
|
MAP(SDLK_KP_ENTER, AKEYCODE_NUMPAD_ENTER);
|
||||||
|
MAP(SDLK_ESCAPE, AKEYCODE_ESCAPE);
|
||||||
|
MAP(SDLK_BACKSPACE, AKEYCODE_DEL);
|
||||||
|
MAP(SDLK_TAB, AKEYCODE_TAB);
|
||||||
|
MAP(SDLK_HOME, AKEYCODE_HOME);
|
||||||
|
MAP(SDLK_PAGEUP, AKEYCODE_PAGE_UP);
|
||||||
|
MAP(SDLK_DELETE, AKEYCODE_FORWARD_DEL);
|
||||||
|
MAP(SDLK_END, AKEYCODE_MOVE_END);
|
||||||
|
MAP(SDLK_PAGEDOWN, AKEYCODE_PAGE_DOWN);
|
||||||
|
MAP(SDLK_RIGHT, AKEYCODE_DPAD_RIGHT);
|
||||||
|
MAP(SDLK_LEFT, AKEYCODE_DPAD_LEFT);
|
||||||
|
MAP(SDLK_DOWN, AKEYCODE_DPAD_DOWN);
|
||||||
|
MAP(SDLK_UP, AKEYCODE_DPAD_UP);
|
||||||
|
FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static SDL_bool convert_mouse_action(SDL_EventType from, enum android_motionevent_action *to) {
|
||||||
|
switch (from) {
|
||||||
|
MAP(SDL_MOUSEBUTTONDOWN, AMOTION_EVENT_ACTION_DOWN);
|
||||||
|
MAP(SDL_MOUSEBUTTONUP, AMOTION_EVENT_ACTION_UP);
|
||||||
|
FAIL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static enum android_motionevent_buttons convert_mouse_buttons(Uint32 state) {
|
||||||
|
enum android_motionevent_buttons buttons = 0;
|
||||||
|
if (state & SDL_BUTTON_LMASK) {
|
||||||
|
buttons |= AMOTION_EVENT_BUTTON_PRIMARY;
|
||||||
|
}
|
||||||
|
if (state & SDL_BUTTON_RMASK) {
|
||||||
|
buttons |= AMOTION_EVENT_BUTTON_SECONDARY;
|
||||||
|
}
|
||||||
|
if (state & SDL_BUTTON_MMASK) {
|
||||||
|
buttons |= AMOTION_EVENT_BUTTON_TERTIARY;
|
||||||
|
}
|
||||||
|
if (state & SDL_BUTTON_X1) {
|
||||||
|
buttons |= AMOTION_EVENT_BUTTON_BACK;
|
||||||
|
}
|
||||||
|
if (state & SDL_BUTTON_X2) {
|
||||||
|
buttons |= AMOTION_EVENT_BUTTON_FORWARD;
|
||||||
|
}
|
||||||
|
return buttons;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
|
||||||
|
struct control_event *to) {
|
||||||
|
to->type = CONTROL_EVENT_TYPE_KEYCODE;
|
||||||
|
|
||||||
|
if (!convert_keycode_action(from->type, &to->keycode_event.action)) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!convert_keycode(from->keysym.sym, &to->keycode_event.keycode)) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
to->keycode_event.metastate = convert_meta_state(from->keysym.mod);
|
||||||
|
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
|
||||||
|
struct size screen_size,
|
||||||
|
struct control_event *to) {
|
||||||
|
to->type = CONTROL_EVENT_TYPE_MOUSE;
|
||||||
|
|
||||||
|
if (!convert_mouse_action(from->type, &to->mouse_event.action)) {
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
to->mouse_event.buttons = convert_mouse_buttons(SDL_BUTTON(from->button));
|
||||||
|
to->mouse_event.position.screen_size = screen_size;
|
||||||
|
to->mouse_event.position.point.x = from->x;
|
||||||
|
to->mouse_event.position.point.y = from->y;
|
||||||
|
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
|
||||||
|
struct size screen_size,
|
||||||
|
struct control_event *to) {
|
||||||
|
to->type = CONTROL_EVENT_TYPE_MOUSE;
|
||||||
|
to->mouse_event.action = AMOTION_EVENT_ACTION_MOVE;
|
||||||
|
to->mouse_event.buttons = convert_mouse_buttons(from->state);
|
||||||
|
to->mouse_event.position.screen_size = screen_size;
|
||||||
|
to->mouse_event.position.point.x = from->x;
|
||||||
|
to->mouse_event.position.point.y = from->y;
|
||||||
|
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
|
||||||
|
struct position position,
|
||||||
|
struct control_event *to) {
|
||||||
|
to->type = CONTROL_EVENT_TYPE_SCROLL;
|
||||||
|
|
||||||
|
to->scroll_event.position = position;
|
||||||
|
|
||||||
|
int mul = from->direction == SDL_MOUSEWHEEL_NORMAL ? 1 : -1;
|
||||||
|
// SDL behavior seems inconsistent between horizontal and vertical scrolling
|
||||||
|
// so reverse the horizontal
|
||||||
|
// <https://wiki.libsdl.org/SDL_MouseWheelEvent#Remarks>
|
||||||
|
to->scroll_event.hscroll = -mul * from->x;
|
||||||
|
to->scroll_event.vscroll = mul * from->y;
|
||||||
|
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
35
app/src/convert.h
Normal file
35
app/src/convert.h
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#ifndef CONVERT_H
|
||||||
|
#define CONVERT_H
|
||||||
|
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
#include <SDL2/SDL_events.h>
|
||||||
|
#include "controlevent.h"
|
||||||
|
|
||||||
|
struct complete_mouse_motion_event {
|
||||||
|
SDL_MouseMotionEvent *mouse_motion_event;
|
||||||
|
struct size screen_size;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct complete_mouse_wheel_event {
|
||||||
|
SDL_MouseWheelEvent *mouse_wheel_event;
|
||||||
|
struct point position;
|
||||||
|
};
|
||||||
|
|
||||||
|
SDL_bool input_key_from_sdl_to_android(const SDL_KeyboardEvent *from,
|
||||||
|
struct control_event *to);
|
||||||
|
SDL_bool mouse_button_from_sdl_to_android(const SDL_MouseButtonEvent *from,
|
||||||
|
struct size screen_size,
|
||||||
|
struct control_event *to);
|
||||||
|
|
||||||
|
// the video size may be different from the real device size, so we need the size
|
||||||
|
// to which the absolute position apply, to scale it accordingly
|
||||||
|
SDL_bool mouse_motion_from_sdl_to_android(const SDL_MouseMotionEvent *from,
|
||||||
|
struct size screen_size,
|
||||||
|
struct control_event *to);
|
||||||
|
|
||||||
|
// on Android, a scroll event requires the current mouse position
|
||||||
|
SDL_bool mouse_wheel_from_sdl_to_android(const SDL_MouseWheelEvent *from,
|
||||||
|
struct position position,
|
||||||
|
struct control_event *to);
|
||||||
|
|
||||||
|
#endif
|
@ -1,24 +0,0 @@
|
|||||||
#ifndef SC_COORDS
|
|
||||||
#define SC_COORDS
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
struct sc_size {
|
|
||||||
uint16_t width;
|
|
||||||
uint16_t height;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_point {
|
|
||||||
int32_t x;
|
|
||||||
int32_t y;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_position {
|
|
||||||
// The video screen size may be different from the real device screen size,
|
|
||||||
// so store to which size the absolute position apply, to scale it
|
|
||||||
// accordingly.
|
|
||||||
struct sc_size screen_size;
|
|
||||||
struct sc_point point;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,109 +1,174 @@
|
|||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
|
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
#include <libavutil/channel_layout.h>
|
#include <SDL2/SDL_events.h>
|
||||||
|
#include <SDL2/SDL_mutex.h>
|
||||||
|
#include <SDL2/SDL_thread.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "trait/frame_sink.h"
|
#include "frames.h"
|
||||||
#include "util/log.h"
|
#include "lockutil.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
/** Downcast packet_sink to decoder */
|
#define BUFSIZE 0x10000
|
||||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
|
|
||||||
|
|
||||||
static bool
|
static int read_packet(void *opaque, uint8_t *buf, int buf_size) {
|
||||||
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) {
|
struct decoder *decoder = opaque;
|
||||||
decoder->frame = av_frame_alloc();
|
return net_recv(decoder->video_socket, buf, buf_size);
|
||||||
if (!decoder->frame) {
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) {
|
|
||||||
av_frame_free(&decoder->frame);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
decoder->ctx = ctx;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
// set the decoded frame as ready for rendering, and notify
|
||||||
sc_decoder_close(struct sc_decoder *decoder) {
|
static void push_frame(struct decoder *decoder) {
|
||||||
sc_frame_source_sinks_close(&decoder->frame_source);
|
SDL_bool previous_frame_consumed = frames_offer_decoded_frame(decoder->frames);
|
||||||
av_frame_free(&decoder->frame);
|
if (!previous_frame_consumed) {
|
||||||
|
// the previous EVENT_NEW_FRAME will consume this frame
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
static SDL_Event new_frame_event = {
|
||||||
|
.type = EVENT_NEW_FRAME,
|
||||||
|
};
|
||||||
|
SDL_PushEvent(&new_frame_event);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static void notify_stopped(void) {
|
||||||
sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
SDL_Event stop_event;
|
||||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
stop_event.type = EVENT_DECODER_STOPPED;
|
||||||
if (is_config) {
|
SDL_PushEvent(&stop_event);
|
||||||
// nothing to do
|
}
|
||||||
return true;
|
|
||||||
|
static int run_decoder(void *data) {
|
||||||
|
struct decoder *decoder = data;
|
||||||
|
|
||||||
|
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||||
|
if (!codec) {
|
||||||
|
LOGE("H.264 decoder not found");
|
||||||
|
goto run_end;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ret = avcodec_send_packet(decoder->ctx, packet);
|
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
|
||||||
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
if (!codec_ctx) {
|
||||||
LOGE("Decoder '%s': could not send video packet: %d",
|
LOGC("Could not allocate decoder context");
|
||||||
decoder->name, ret);
|
goto run_end;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for (;;) {
|
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
||||||
ret = avcodec_receive_frame(decoder->ctx, decoder->frame);
|
LOGE("Could not open H.264 codec");
|
||||||
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
goto run_finally_free_codec_ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVFormatContext *format_ctx = avformat_alloc_context();
|
||||||
|
if (!format_ctx) {
|
||||||
|
LOGC("Could not allocate format context");
|
||||||
|
goto run_finally_close_codec;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsigned char *buffer = av_malloc(BUFSIZE);
|
||||||
|
if (!buffer) {
|
||||||
|
LOGC("Could not allocate buffer");
|
||||||
|
goto run_finally_free_format_ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, decoder, read_packet, NULL, NULL);
|
||||||
|
if (!avio_ctx) {
|
||||||
|
LOGC("Could not allocate avio context");
|
||||||
|
// avformat_open_input takes ownership of 'buffer'
|
||||||
|
// so only free the buffer before avformat_open_input()
|
||||||
|
av_free(buffer);
|
||||||
|
goto run_finally_free_format_ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
format_ctx->pb = avio_ctx;
|
||||||
|
|
||||||
|
if (avformat_open_input(&format_ctx, NULL, NULL, NULL) < 0) {
|
||||||
|
LOGE("Could not open video stream");
|
||||||
|
goto run_finally_free_avio_ctx;
|
||||||
|
}
|
||||||
|
|
||||||
|
AVPacket packet;
|
||||||
|
av_init_packet(&packet);
|
||||||
|
packet.data = NULL;
|
||||||
|
packet.size = 0;
|
||||||
|
|
||||||
|
while (!av_read_frame(format_ctx, &packet)) {
|
||||||
|
// the new decoding/encoding API has been introduced by:
|
||||||
|
// <http://git.videolan.org/?p=ffmpeg.git;a=commitdiff;h=7fc329e2dd6226dfecaa4a1d7adf353bf2773726>
|
||||||
|
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(57, 37, 0)
|
||||||
|
int ret;
|
||||||
|
if ((ret = avcodec_send_packet(codec_ctx, &packet)) < 0) {
|
||||||
|
LOGE("Could not send video packet: %d", ret);
|
||||||
|
goto run_quit;
|
||||||
|
}
|
||||||
|
ret = avcodec_receive_frame(codec_ctx, decoder->frames->decoding_frame);
|
||||||
|
if (!ret) {
|
||||||
|
// a frame was received
|
||||||
|
push_frame(decoder);
|
||||||
|
} else if (ret != AVERROR(EAGAIN)) {
|
||||||
|
LOGE("Could not receive video frame: %d", ret);
|
||||||
|
av_packet_unref(&packet);
|
||||||
|
goto run_quit;
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
while (packet.size > 0) {
|
||||||
|
int got_picture;
|
||||||
|
int len = avcodec_decode_video2(codec_ctx, decoder->frames->decoding_frame, &got_picture, &packet);
|
||||||
|
if (len < 0) {
|
||||||
|
LOGE("Could not decode video packet: %d", len);
|
||||||
|
goto run_quit;
|
||||||
|
}
|
||||||
|
if (got_picture) {
|
||||||
|
push_frame(decoder);
|
||||||
|
}
|
||||||
|
packet.size -= len;
|
||||||
|
packet.data += len;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
av_packet_unref(&packet);
|
||||||
|
|
||||||
|
if (avio_ctx->eof_reached) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ret) {
|
|
||||||
LOGE("Decoder '%s', could not receive video frame: %d",
|
|
||||||
decoder->name, ret);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// a frame was received
|
|
||||||
bool ok = sc_frame_source_sinks_push(&decoder->frame_source,
|
|
||||||
decoder->frame);
|
|
||||||
av_frame_unref(decoder->frame);
|
|
||||||
if (!ok) {
|
|
||||||
// Error already logged
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
LOGD("End of frames");
|
||||||
|
|
||||||
|
run_quit:
|
||||||
|
avformat_close_input(&format_ctx);
|
||||||
|
run_finally_free_avio_ctx:
|
||||||
|
av_freep(&avio_ctx);
|
||||||
|
run_finally_free_format_ctx:
|
||||||
|
avformat_free_context(format_ctx);
|
||||||
|
run_finally_close_codec:
|
||||||
|
avcodec_close(codec_ctx);
|
||||||
|
run_finally_free_codec_ctx:
|
||||||
|
avcodec_free_context(&codec_ctx);
|
||||||
|
notify_stopped();
|
||||||
|
run_end:
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket) {
|
||||||
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) {
|
decoder->frames = frames;
|
||||||
struct sc_decoder *decoder = DOWNCAST(sink);
|
decoder->video_socket = video_socket;
|
||||||
return sc_decoder_open(decoder, ctx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
SDL_bool decoder_start(struct decoder *decoder) {
|
||||||
sc_decoder_packet_sink_close(struct sc_packet_sink *sink) {
|
LOGD("Starting decoder thread");
|
||||||
struct sc_decoder *decoder = DOWNCAST(sink);
|
|
||||||
sc_decoder_close(decoder);
|
decoder->thread = SDL_CreateThread(run_decoder, "video_decoder", decoder);
|
||||||
|
if (!decoder->thread) {
|
||||||
|
LOGC("Could not start decoder thread");
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SDL_TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
void decoder_stop(struct decoder *decoder) {
|
||||||
sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
|
frames_stop(decoder->frames);
|
||||||
const AVPacket *packet) {
|
|
||||||
struct sc_decoder *decoder = DOWNCAST(sink);
|
|
||||||
return sc_decoder_push(decoder, packet);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void decoder_join(struct decoder *decoder) {
|
||||||
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
SDL_WaitThread(decoder->thread, NULL);
|
||||||
decoder->name = name; // statically allocated
|
|
||||||
sc_frame_source_init(&decoder->frame_source);
|
|
||||||
|
|
||||||
static const struct sc_packet_sink_ops ops = {
|
|
||||||
.open = sc_decoder_packet_sink_open,
|
|
||||||
.close = sc_decoder_packet_sink_close,
|
|
||||||
.push = sc_decoder_packet_sink_push,
|
|
||||||
};
|
|
||||||
|
|
||||||
decoder->packet_sink.ops = &ops;
|
|
||||||
}
|
}
|
||||||
|
@ -1,27 +1,23 @@
|
|||||||
#ifndef SC_DECODER_H
|
#ifndef DECODER_H
|
||||||
#define SC_DECODER_H
|
#define DECODER_H
|
||||||
|
|
||||||
#include "common.h"
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
#include <SDL2/SDL_thread.h>
|
||||||
|
|
||||||
#include "trait/frame_source.h"
|
#include "net.h"
|
||||||
#include "trait/packet_sink.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
struct frames;
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
|
|
||||||
struct sc_decoder {
|
struct decoder {
|
||||||
struct sc_packet_sink packet_sink; // packet sink trait
|
struct frames *frames;
|
||||||
struct sc_frame_source frame_source; // frame source trait
|
socket_t video_socket;
|
||||||
|
SDL_Thread *thread;
|
||||||
const char *name; // must be statically allocated (e.g. a string literal)
|
SDL_mutex *mutex;
|
||||||
|
|
||||||
AVCodecContext *ctx;
|
|
||||||
AVFrame *frame;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The name must be statically allocated (e.g. a string literal)
|
void decoder_init(struct decoder *decoder, struct frames *frames, socket_t video_socket);
|
||||||
void
|
SDL_bool decoder_start(struct decoder *decoder);
|
||||||
sc_decoder_init(struct sc_decoder *decoder, const char *name);
|
void decoder_stop(struct decoder *decoder);
|
||||||
|
void decoder_join(struct decoder *decoder);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -1,243 +0,0 @@
|
|||||||
#include "delay_buffer.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#include <libavutil/avutil.h>
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
/** Downcast frame_sink to sc_delay_buffer */
|
|
||||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_delay_buffer, frame_sink)
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_delayed_frame_init(struct sc_delayed_frame *dframe, const AVFrame *frame) {
|
|
||||||
dframe->frame = av_frame_alloc();
|
|
||||||
if (!dframe->frame) {
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (av_frame_ref(dframe->frame, frame)) {
|
|
||||||
LOG_OOM();
|
|
||||||
av_frame_free(&dframe->frame);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_delayed_frame_destroy(struct sc_delayed_frame *dframe) {
|
|
||||||
av_frame_unref(dframe->frame);
|
|
||||||
av_frame_free(&dframe->frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
run_buffering(void *data) {
|
|
||||||
struct sc_delay_buffer *db = data;
|
|
||||||
|
|
||||||
assert(db->delay > 0);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
sc_mutex_lock(&db->mutex);
|
|
||||||
|
|
||||||
while (!db->stopped && sc_vecdeque_is_empty(&db->queue)) {
|
|
||||||
sc_cond_wait(&db->queue_cond, &db->mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (db->stopped) {
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
goto stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sc_delayed_frame dframe = sc_vecdeque_pop(&db->queue);
|
|
||||||
|
|
||||||
sc_tick max_deadline = sc_tick_now() + db->delay;
|
|
||||||
// PTS (written by the server) are expressed in microseconds
|
|
||||||
sc_tick pts = SC_TICK_FROM_US(dframe.frame->pts);
|
|
||||||
|
|
||||||
bool timed_out = false;
|
|
||||||
while (!db->stopped && !timed_out) {
|
|
||||||
sc_tick deadline = sc_clock_to_system_time(&db->clock, pts)
|
|
||||||
+ db->delay;
|
|
||||||
if (deadline > max_deadline) {
|
|
||||||
deadline = max_deadline;
|
|
||||||
}
|
|
||||||
|
|
||||||
timed_out =
|
|
||||||
!sc_cond_timedwait(&db->wait_cond, &db->mutex, deadline);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool stopped = db->stopped;
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
|
|
||||||
if (stopped) {
|
|
||||||
sc_delayed_frame_destroy(&dframe);
|
|
||||||
goto stopped;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef SC_BUFFERING_DEBUG
|
|
||||||
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
|
|
||||||
pts, dframe.push_date, sc_tick_now());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool ok = sc_frame_source_sinks_push(&db->frame_source, dframe.frame);
|
|
||||||
sc_delayed_frame_destroy(&dframe);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Delayed frame could not be pushed, stopping");
|
|
||||||
sc_mutex_lock(&db->mutex);
|
|
||||||
// Prevent to push any new frame
|
|
||||||
db->stopped = true;
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
goto stopped;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stopped:
|
|
||||||
assert(db->stopped);
|
|
||||||
|
|
||||||
// Flush queue
|
|
||||||
while (!sc_vecdeque_is_empty(&db->queue)) {
|
|
||||||
struct sc_delayed_frame *dframe = sc_vecdeque_popref(&db->queue);
|
|
||||||
sc_delayed_frame_destroy(dframe);
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGD("Buffering thread ended");
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_delay_buffer_frame_sink_open(struct sc_frame_sink *sink,
|
|
||||||
const AVCodecContext *ctx) {
|
|
||||||
struct sc_delay_buffer *db = DOWNCAST(sink);
|
|
||||||
(void) ctx;
|
|
||||||
|
|
||||||
bool ok = sc_mutex_init(&db->mutex);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_cond_init(&db->queue_cond);
|
|
||||||
if (!ok) {
|
|
||||||
goto error_destroy_mutex;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_cond_init(&db->wait_cond);
|
|
||||||
if (!ok) {
|
|
||||||
goto error_destroy_queue_cond;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_clock_init(&db->clock);
|
|
||||||
sc_vecdeque_init(&db->queue);
|
|
||||||
db->stopped = false;
|
|
||||||
|
|
||||||
if (!sc_frame_source_sinks_open(&db->frame_source, ctx)) {
|
|
||||||
goto error_destroy_wait_cond;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_thread_create(&db->thread, run_buffering, "scrcpy-dbuf", db);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Could not start buffering thread");
|
|
||||||
goto error_close_sinks;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
error_close_sinks:
|
|
||||||
sc_frame_source_sinks_close(&db->frame_source);
|
|
||||||
error_destroy_wait_cond:
|
|
||||||
sc_cond_destroy(&db->wait_cond);
|
|
||||||
error_destroy_queue_cond:
|
|
||||||
sc_cond_destroy(&db->queue_cond);
|
|
||||||
error_destroy_mutex:
|
|
||||||
sc_mutex_destroy(&db->mutex);
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_delay_buffer_frame_sink_close(struct sc_frame_sink *sink) {
|
|
||||||
struct sc_delay_buffer *db = DOWNCAST(sink);
|
|
||||||
|
|
||||||
sc_mutex_lock(&db->mutex);
|
|
||||||
db->stopped = true;
|
|
||||||
sc_cond_signal(&db->queue_cond);
|
|
||||||
sc_cond_signal(&db->wait_cond);
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
|
|
||||||
sc_thread_join(&db->thread, NULL);
|
|
||||||
|
|
||||||
sc_frame_source_sinks_close(&db->frame_source);
|
|
||||||
|
|
||||||
sc_cond_destroy(&db->wait_cond);
|
|
||||||
sc_cond_destroy(&db->queue_cond);
|
|
||||||
sc_mutex_destroy(&db->mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
|
|
||||||
const AVFrame *frame) {
|
|
||||||
struct sc_delay_buffer *db = DOWNCAST(sink);
|
|
||||||
|
|
||||||
sc_mutex_lock(&db->mutex);
|
|
||||||
|
|
||||||
if (db->stopped) {
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_tick pts = SC_TICK_FROM_US(frame->pts);
|
|
||||||
sc_clock_update(&db->clock, sc_tick_now(), pts);
|
|
||||||
sc_cond_signal(&db->wait_cond);
|
|
||||||
|
|
||||||
if (db->first_frame_asap && db->clock.range == 1) {
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
return sc_frame_source_sinks_push(&db->frame_source, frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sc_delayed_frame dframe;
|
|
||||||
bool ok = sc_delayed_frame_init(&dframe, frame);
|
|
||||||
if (!ok) {
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifdef SC_BUFFERING_DEBUG
|
|
||||||
dframe.push_date = sc_tick_now();
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ok = sc_vecdeque_push(&db->queue, dframe);
|
|
||||||
if (!ok) {
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_cond_signal(&db->queue_cond);
|
|
||||||
|
|
||||||
sc_mutex_unlock(&db->mutex);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay,
|
|
||||||
bool first_frame_asap) {
|
|
||||||
assert(delay > 0);
|
|
||||||
|
|
||||||
db->delay = delay;
|
|
||||||
db->first_frame_asap = first_frame_asap;
|
|
||||||
|
|
||||||
sc_frame_source_init(&db->frame_source);
|
|
||||||
|
|
||||||
static const struct sc_frame_sink_ops ops = {
|
|
||||||
.open = sc_delay_buffer_frame_sink_open,
|
|
||||||
.close = sc_delay_buffer_frame_sink_close,
|
|
||||||
.push = sc_delay_buffer_frame_sink_push,
|
|
||||||
};
|
|
||||||
|
|
||||||
db->frame_sink.ops = &ops;
|
|
||||||
}
|
|
@ -1,62 +0,0 @@
|
|||||||
#ifndef SC_DELAY_BUFFER_H
|
|
||||||
#define SC_DELAY_BUFFER_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#include "clock.h"
|
|
||||||
#include "trait/frame_source.h"
|
|
||||||
#include "trait/frame_sink.h"
|
|
||||||
#include "util/thread.h"
|
|
||||||
#include "util/tick.h"
|
|
||||||
#include "util/vecdeque.h"
|
|
||||||
|
|
||||||
//#define SC_BUFFERING_DEBUG // uncomment to debug
|
|
||||||
|
|
||||||
// forward declarations
|
|
||||||
typedef struct AVFrame AVFrame;
|
|
||||||
|
|
||||||
struct sc_delayed_frame {
|
|
||||||
AVFrame *frame;
|
|
||||||
#ifdef SC_BUFFERING_DEBUG
|
|
||||||
sc_tick push_date;
|
|
||||||
#endif
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_delayed_frame_queue SC_VECDEQUE(struct sc_delayed_frame);
|
|
||||||
|
|
||||||
struct sc_delay_buffer {
|
|
||||||
struct sc_frame_source frame_source; // frame source trait
|
|
||||||
struct sc_frame_sink frame_sink; // frame sink trait
|
|
||||||
|
|
||||||
sc_tick delay;
|
|
||||||
bool first_frame_asap;
|
|
||||||
|
|
||||||
sc_thread thread;
|
|
||||||
sc_mutex mutex;
|
|
||||||
sc_cond queue_cond;
|
|
||||||
sc_cond wait_cond;
|
|
||||||
|
|
||||||
struct sc_clock clock;
|
|
||||||
struct sc_delayed_frame_queue queue;
|
|
||||||
bool stopped;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_delay_buffer_callbacks {
|
|
||||||
bool (*on_new_frame)(struct sc_delay_buffer *db, const AVFrame *frame,
|
|
||||||
void *userdata);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize a delay buffer.
|
|
||||||
*
|
|
||||||
* \param delay a (strictly) positive delay
|
|
||||||
* \param first_frame_asap if true, do not delay the first frame (useful for
|
|
||||||
a video stream).
|
|
||||||
*/
|
|
||||||
void
|
|
||||||
sc_delay_buffer_init(struct sc_delay_buffer *db, sc_tick delay,
|
|
||||||
bool first_frame_asap);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,319 +0,0 @@
|
|||||||
#include "demuxer.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <libavutil/channel_layout.h>
|
|
||||||
#include <libavutil/time.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#include "decoder.h"
|
|
||||||
#include "events.h"
|
|
||||||
#include "packet_merger.h"
|
|
||||||
#include "recorder.h"
|
|
||||||
#include "util/binary.h"
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
#define SC_PACKET_HEADER_SIZE 12
|
|
||||||
|
|
||||||
#define SC_PACKET_FLAG_CONFIG (UINT64_C(1) << 63)
|
|
||||||
#define SC_PACKET_FLAG_KEY_FRAME (UINT64_C(1) << 62)
|
|
||||||
|
|
||||||
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
|
|
||||||
|
|
||||||
static enum AVCodecID
|
|
||||||
sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|
||||||
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
|
|
||||||
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
|
||||||
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
|
||||||
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
|
||||||
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII
|
|
||||||
#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII
|
|
||||||
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
|
|
||||||
switch (codec_id) {
|
|
||||||
case SC_CODEC_ID_H264:
|
|
||||||
return AV_CODEC_ID_H264;
|
|
||||||
case SC_CODEC_ID_H265:
|
|
||||||
return AV_CODEC_ID_HEVC;
|
|
||||||
case SC_CODEC_ID_AV1:
|
|
||||||
#ifdef SCRCPY_LAVC_HAS_AV1
|
|
||||||
return AV_CODEC_ID_AV1;
|
|
||||||
#else
|
|
||||||
LOGE("AV1 not supported by this FFmpeg version");
|
|
||||||
return AV_CODEC_ID_NONE;
|
|
||||||
#endif
|
|
||||||
case SC_CODEC_ID_OPUS:
|
|
||||||
return AV_CODEC_ID_OPUS;
|
|
||||||
case SC_CODEC_ID_AAC:
|
|
||||||
return AV_CODEC_ID_AAC;
|
|
||||||
case SC_CODEC_ID_FLAC:
|
|
||||||
return AV_CODEC_ID_FLAC;
|
|
||||||
case SC_CODEC_ID_RAW:
|
|
||||||
return AV_CODEC_ID_PCM_S16LE;
|
|
||||||
default:
|
|
||||||
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
|
||||||
return AV_CODEC_ID_NONE;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
|
|
||||||
uint8_t data[4];
|
|
||||||
ssize_t r = net_recv_all(demuxer->socket, data, 4);
|
|
||||||
if (r < 4) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*codec_id = sc_read32be(data);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width,
|
|
||||||
uint32_t *height) {
|
|
||||||
uint8_t data[8];
|
|
||||||
ssize_t r = net_recv_all(demuxer->socket, data, 8);
|
|
||||||
if (r < 8) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
*width = sc_read32be(data);
|
|
||||||
*height = sc_read32be(data + 4);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
|
||||||
// The video and audio streams contain a sequence of raw packets (as
|
|
||||||
// provided by MediaCodec), each prefixed with a "meta" header.
|
|
||||||
//
|
|
||||||
// The "meta" header length is 12 bytes:
|
|
||||||
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
|
|
||||||
// <-------------> <-----> <-----------------------------...
|
|
||||||
// PTS packet raw packet
|
|
||||||
// size
|
|
||||||
//
|
|
||||||
// It is followed by <packet_size> bytes containing the packet/frame.
|
|
||||||
//
|
|
||||||
// The most significant bits of the PTS are used for packet flags:
|
|
||||||
//
|
|
||||||
// byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0
|
|
||||||
// CK...... ........ ........ ........ ........ ........ ........ ........
|
|
||||||
// ^^<------------------------------------------------------------------->
|
|
||||||
// || PTS
|
|
||||||
// | `- key frame
|
|
||||||
// `-- config packet
|
|
||||||
|
|
||||||
uint8_t header[SC_PACKET_HEADER_SIZE];
|
|
||||||
ssize_t r = net_recv_all(demuxer->socket, header, SC_PACKET_HEADER_SIZE);
|
|
||||||
if (r < SC_PACKET_HEADER_SIZE) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint64_t pts_flags = sc_read64be(header);
|
|
||||||
uint32_t len = sc_read32be(&header[8]);
|
|
||||||
assert(len);
|
|
||||||
|
|
||||||
if (av_new_packet(packet, len)) {
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
r = net_recv_all(demuxer->socket, packet->data, len);
|
|
||||||
if (r < 0 || ((uint32_t) r) < len) {
|
|
||||||
av_packet_unref(packet);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pts_flags & SC_PACKET_FLAG_CONFIG) {
|
|
||||||
packet->pts = AV_NOPTS_VALUE;
|
|
||||||
} else {
|
|
||||||
packet->pts = pts_flags & SC_PACKET_PTS_MASK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (pts_flags & SC_PACKET_FLAG_KEY_FRAME) {
|
|
||||||
packet->flags |= AV_PKT_FLAG_KEY;
|
|
||||||
}
|
|
||||||
|
|
||||||
packet->dts = packet->pts;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
run_demuxer(void *data) {
|
|
||||||
struct sc_demuxer *demuxer = data;
|
|
||||||
|
|
||||||
// Flag to report end-of-stream (i.e. device disconnected)
|
|
||||||
enum sc_demuxer_status status = SC_DEMUXER_STATUS_ERROR;
|
|
||||||
|
|
||||||
uint32_t raw_codec_id;
|
|
||||||
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Demuxer '%s': stream disabled due to connection error",
|
|
||||||
demuxer->name);
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raw_codec_id == 0) {
|
|
||||||
LOGW("Demuxer '%s': stream explicitly disabled by the device",
|
|
||||||
demuxer->name);
|
|
||||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
|
||||||
status = SC_DEMUXER_STATUS_DISABLED;
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (raw_codec_id == 1) {
|
|
||||||
LOGE("Demuxer '%s': stream configuration error on the device",
|
|
||||||
demuxer->name);
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
|
|
||||||
if (codec_id == AV_CODEC_ID_NONE) {
|
|
||||||
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
|
|
||||||
demuxer->name);
|
|
||||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
|
|
||||||
const AVCodec *codec = avcodec_find_decoder(codec_id);
|
|
||||||
if (!codec) {
|
|
||||||
LOGE("Demuxer '%s': stream disabled due to missing decoder",
|
|
||||||
demuxer->name);
|
|
||||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
|
|
||||||
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
|
|
||||||
if (!codec_ctx) {
|
|
||||||
LOG_OOM();
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
|
|
||||||
codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
|
||||||
|
|
||||||
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
|
||||||
uint32_t width;
|
|
||||||
uint32_t height;
|
|
||||||
ok = sc_demuxer_recv_video_size(demuxer, &width, &height);
|
|
||||||
if (!ok) {
|
|
||||||
goto finally_free_context;
|
|
||||||
}
|
|
||||||
|
|
||||||
codec_ctx->width = width;
|
|
||||||
codec_ctx->height = height;
|
|
||||||
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
|
||||||
} else {
|
|
||||||
// Hardcoded audio properties
|
|
||||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
|
||||||
codec_ctx->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
|
|
||||||
#else
|
|
||||||
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
|
|
||||||
codec_ctx->channels = 2;
|
|
||||||
#endif
|
|
||||||
codec_ctx->sample_rate = 48000;
|
|
||||||
|
|
||||||
if (raw_codec_id == SC_CODEC_ID_FLAC) {
|
|
||||||
// The sample_fmt is not set by the FLAC decoder
|
|
||||||
codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
|
||||||
LOGE("Demuxer '%s': could not open codec", demuxer->name);
|
|
||||||
goto finally_free_context;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) {
|
|
||||||
goto finally_free_context;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Config packets must be merged with the next non-config packet only for
|
|
||||||
// H.26x
|
|
||||||
bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264
|
|
||||||
|| raw_codec_id == SC_CODEC_ID_H265;
|
|
||||||
|
|
||||||
struct sc_packet_merger merger;
|
|
||||||
|
|
||||||
if (must_merge_config_packet) {
|
|
||||||
sc_packet_merger_init(&merger);
|
|
||||||
}
|
|
||||||
|
|
||||||
AVPacket *packet = av_packet_alloc();
|
|
||||||
if (!packet) {
|
|
||||||
LOG_OOM();
|
|
||||||
goto finally_close_sinks;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
|
||||||
if (!ok) {
|
|
||||||
// end of stream
|
|
||||||
status = SC_DEMUXER_STATUS_EOS;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (must_merge_config_packet) {
|
|
||||||
// Prepend any config packet to the next media packet
|
|
||||||
ok = sc_packet_merger_merge(&merger, packet);
|
|
||||||
if (!ok) {
|
|
||||||
av_packet_unref(packet);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
|
|
||||||
av_packet_unref(packet);
|
|
||||||
if (!ok) {
|
|
||||||
// The sink already logged its concrete error
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGD("Demuxer '%s': end of frames", demuxer->name);
|
|
||||||
|
|
||||||
if (must_merge_config_packet) {
|
|
||||||
sc_packet_merger_destroy(&merger);
|
|
||||||
}
|
|
||||||
|
|
||||||
av_packet_free(&packet);
|
|
||||||
finally_close_sinks:
|
|
||||||
sc_packet_source_sinks_close(&demuxer->packet_source);
|
|
||||||
finally_free_context:
|
|
||||||
avcodec_free_context(&codec_ctx);
|
|
||||||
end:
|
|
||||||
demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
|
||||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
|
||||||
assert(socket != SC_SOCKET_NONE);
|
|
||||||
|
|
||||||
demuxer->name = name; // statically allocated
|
|
||||||
demuxer->socket = socket;
|
|
||||||
sc_packet_source_init(&demuxer->packet_source);
|
|
||||||
|
|
||||||
assert(cbs && cbs->on_ended);
|
|
||||||
|
|
||||||
demuxer->cbs = cbs;
|
|
||||||
demuxer->cbs_userdata = cbs_userdata;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
|
||||||
LOGD("Demuxer '%s': starting thread", demuxer->name);
|
|
||||||
|
|
||||||
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
|
|
||||||
demuxer);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Demuxer '%s': could not start thread", demuxer->name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_demuxer_join(struct sc_demuxer *demuxer) {
|
|
||||||
sc_thread_join(&demuxer->thread, NULL);
|
|
||||||
}
|
|
@ -1,50 +0,0 @@
|
|||||||
#ifndef SC_DEMUXER_H
|
|
||||||
#define SC_DEMUXER_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
|
|
||||||
#include "trait/packet_source.h"
|
|
||||||
#include "trait/packet_sink.h"
|
|
||||||
#include "util/net.h"
|
|
||||||
#include "util/thread.h"
|
|
||||||
|
|
||||||
struct sc_demuxer {
|
|
||||||
struct sc_packet_source packet_source; // packet source trait
|
|
||||||
|
|
||||||
const char *name; // must be statically allocated (e.g. a string literal)
|
|
||||||
|
|
||||||
sc_socket socket;
|
|
||||||
sc_thread thread;
|
|
||||||
|
|
||||||
const struct sc_demuxer_callbacks *cbs;
|
|
||||||
void *cbs_userdata;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum sc_demuxer_status {
|
|
||||||
SC_DEMUXER_STATUS_EOS,
|
|
||||||
SC_DEMUXER_STATUS_DISABLED,
|
|
||||||
SC_DEMUXER_STATUS_ERROR,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_demuxer_callbacks {
|
|
||||||
void (*on_ended)(struct sc_demuxer *demuxer, enum sc_demuxer_status,
|
|
||||||
void *userdata);
|
|
||||||
};
|
|
||||||
|
|
||||||
// The name must be statically allocated (e.g. a string literal)
|
|
||||||
void
|
|
||||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
|
||||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_demuxer_start(struct sc_demuxer *demuxer);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_demuxer_join(struct sc_demuxer *demuxer);
|
|
||||||
|
|
||||||
#endif
|
|
18
app/src/device.c
Normal file
18
app/src/device.c
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
#include "device.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
SDL_bool device_read_info(socket_t device_socket, char *device_name, struct size *size) {
|
||||||
|
unsigned char buf[DEVICE_NAME_FIELD_LENGTH + 4];
|
||||||
|
int r = net_recv_all(device_socket, buf, sizeof(buf));
|
||||||
|
if (r < DEVICE_NAME_FIELD_LENGTH + 4) {
|
||||||
|
LOGE("Could not retrieve device information");
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
buf[DEVICE_NAME_FIELD_LENGTH - 1] = '\0'; // in case the client sends garbage
|
||||||
|
// strcpy is safe here, since name contains at least DEVICE_NAME_FIELD_LENGTH bytes
|
||||||
|
// and strlen(buf) < DEVICE_NAME_FIELD_LENGTH
|
||||||
|
strcpy(device_name, (char *) buf);
|
||||||
|
size->width = (buf[DEVICE_NAME_FIELD_LENGTH] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 1];
|
||||||
|
size->height = (buf[DEVICE_NAME_FIELD_LENGTH + 2] << 8) | buf[DEVICE_NAME_FIELD_LENGTH + 3];
|
||||||
|
return SDL_TRUE;
|
||||||
|
}
|
14
app/src/device.h
Normal file
14
app/src/device.h
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
#ifndef DEVICE_H
|
||||||
|
#define DEVICE_H
|
||||||
|
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "net.h"
|
||||||
|
|
||||||
|
#define DEVICE_NAME_FIELD_LENGTH 64
|
||||||
|
|
||||||
|
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
|
||||||
|
SDL_bool device_read_info(socket_t device_socket, char *name, struct size *frame_size);
|
||||||
|
|
||||||
|
#endif
|
@ -1,93 +0,0 @@
|
|||||||
#include "device_msg.h"
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "util/binary.h"
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
ssize_t
|
|
||||||
sc_device_msg_deserialize(const uint8_t *buf, size_t len,
|
|
||||||
struct sc_device_msg *msg) {
|
|
||||||
if (!len) {
|
|
||||||
return 0; // no message
|
|
||||||
}
|
|
||||||
|
|
||||||
msg->type = buf[0];
|
|
||||||
switch (msg->type) {
|
|
||||||
case DEVICE_MSG_TYPE_CLIPBOARD: {
|
|
||||||
if (len < 5) {
|
|
||||||
// at least type + empty string length
|
|
||||||
return 0; // no complete message
|
|
||||||
}
|
|
||||||
size_t clipboard_len = sc_read32be(&buf[1]);
|
|
||||||
if (clipboard_len > len - 5) {
|
|
||||||
return 0; // no complete message
|
|
||||||
}
|
|
||||||
char *text = malloc(clipboard_len + 1);
|
|
||||||
if (!text) {
|
|
||||||
LOG_OOM();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (clipboard_len) {
|
|
||||||
memcpy(text, &buf[5], clipboard_len);
|
|
||||||
}
|
|
||||||
text[clipboard_len] = '\0';
|
|
||||||
|
|
||||||
msg->clipboard.text = text;
|
|
||||||
return 5 + clipboard_len;
|
|
||||||
}
|
|
||||||
case DEVICE_MSG_TYPE_ACK_CLIPBOARD: {
|
|
||||||
if (len < 9) {
|
|
||||||
return 0; // no complete message
|
|
||||||
}
|
|
||||||
uint64_t sequence = sc_read64be(&buf[1]);
|
|
||||||
msg->ack_clipboard.sequence = sequence;
|
|
||||||
return 9;
|
|
||||||
}
|
|
||||||
case DEVICE_MSG_TYPE_UHID_OUTPUT: {
|
|
||||||
if (len < 5) {
|
|
||||||
// at least id + size
|
|
||||||
return 0; // not available
|
|
||||||
}
|
|
||||||
uint16_t id = sc_read16be(&buf[1]);
|
|
||||||
size_t size = sc_read16be(&buf[3]);
|
|
||||||
if (size < len - 5) {
|
|
||||||
return 0; // not available
|
|
||||||
}
|
|
||||||
uint8_t *data = malloc(size);
|
|
||||||
if (!data) {
|
|
||||||
LOG_OOM();
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
if (size) {
|
|
||||||
memcpy(data, &buf[5], size);
|
|
||||||
}
|
|
||||||
|
|
||||||
msg->uhid_output.id = id;
|
|
||||||
msg->uhid_output.size = size;
|
|
||||||
msg->uhid_output.data = data;
|
|
||||||
|
|
||||||
return 5 + size;
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
LOGW("Unknown device message type: %d", (int) msg->type);
|
|
||||||
return -1; // error, we cannot recover
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_device_msg_destroy(struct sc_device_msg *msg) {
|
|
||||||
switch (msg->type) {
|
|
||||||
case DEVICE_MSG_TYPE_CLIPBOARD:
|
|
||||||
free(msg->clipboard.text);
|
|
||||||
break;
|
|
||||||
case DEVICE_MSG_TYPE_UHID_OUTPUT:
|
|
||||||
free(msg->uhid_output.data);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
// nothing to do
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,45 +0,0 @@
|
|||||||
#ifndef SC_DEVICEMSG_H
|
|
||||||
#define SC_DEVICEMSG_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <unistd.h>
|
|
||||||
|
|
||||||
#define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k
|
|
||||||
// type: 1 byte; length: 4 bytes
|
|
||||||
#define DEVICE_MSG_TEXT_MAX_LENGTH (DEVICE_MSG_MAX_SIZE - 5)
|
|
||||||
|
|
||||||
enum sc_device_msg_type {
|
|
||||||
DEVICE_MSG_TYPE_CLIPBOARD,
|
|
||||||
DEVICE_MSG_TYPE_ACK_CLIPBOARD,
|
|
||||||
DEVICE_MSG_TYPE_UHID_OUTPUT,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_device_msg {
|
|
||||||
enum sc_device_msg_type type;
|
|
||||||
union {
|
|
||||||
struct {
|
|
||||||
char *text; // owned, to be freed by free()
|
|
||||||
} clipboard;
|
|
||||||
struct {
|
|
||||||
uint64_t sequence;
|
|
||||||
} ack_clipboard;
|
|
||||||
struct {
|
|
||||||
uint16_t id;
|
|
||||||
uint16_t size;
|
|
||||||
uint8_t *data; // owned, to be freed by free()
|
|
||||||
} uhid_output;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// return the number of bytes consumed (0 for no msg available, -1 on error)
|
|
||||||
ssize_t
|
|
||||||
sc_device_msg_deserialize(const uint8_t *buf, size_t len,
|
|
||||||
struct sc_device_msg *msg);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_device_msg_destroy(struct sc_device_msg *msg);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,342 +0,0 @@
|
|||||||
#include "display.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <libavutil/pixfmt.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_display_init_novideo_icon(struct sc_display *display,
|
|
||||||
SDL_Surface *icon_novideo) {
|
|
||||||
assert(icon_novideo);
|
|
||||||
|
|
||||||
if (SDL_RenderSetLogicalSize(display->renderer,
|
|
||||||
icon_novideo->w, icon_novideo->h)) {
|
|
||||||
LOGW("Could not set renderer logical size: %s", SDL_GetError());
|
|
||||||
// don't fail
|
|
||||||
}
|
|
||||||
|
|
||||||
display->texture = SDL_CreateTextureFromSurface(display->renderer,
|
|
||||||
icon_novideo);
|
|
||||||
if (!display->texture) {
|
|
||||||
LOGE("Could not create texture: %s", SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_display_init(struct sc_display *display, SDL_Window *window,
|
|
||||||
SDL_Surface *icon_novideo, bool mipmaps) {
|
|
||||||
display->renderer =
|
|
||||||
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
|
||||||
if (!display->renderer) {
|
|
||||||
LOGE("Could not create renderer: %s", SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_RendererInfo renderer_info;
|
|
||||||
int r = SDL_GetRendererInfo(display->renderer, &renderer_info);
|
|
||||||
const char *renderer_name = r ? NULL : renderer_info.name;
|
|
||||||
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
|
||||||
|
|
||||||
display->mipmaps = false;
|
|
||||||
|
|
||||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
|
||||||
display->gl_context = NULL;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// starts with "opengl"
|
|
||||||
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
|
||||||
if (use_opengl) {
|
|
||||||
|
|
||||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
|
||||||
// Persuade macOS to give us something better than OpenGL 2.1.
|
|
||||||
// If we create a Core Profile context, we get the best OpenGL version.
|
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
|
|
||||||
SDL_GL_CONTEXT_PROFILE_CORE);
|
|
||||||
|
|
||||||
LOGD("Creating OpenGL Core Profile context");
|
|
||||||
display->gl_context = SDL_GL_CreateContext(window);
|
|
||||||
if (!display->gl_context) {
|
|
||||||
LOGE("Could not create OpenGL context: %s", SDL_GetError());
|
|
||||||
SDL_DestroyRenderer(display->renderer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct sc_opengl *gl = &display->gl;
|
|
||||||
sc_opengl_init(gl);
|
|
||||||
|
|
||||||
LOGI("OpenGL version: %s", gl->version);
|
|
||||||
|
|
||||||
if (mipmaps) {
|
|
||||||
bool supports_mipmaps =
|
|
||||||
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
|
|
||||||
2, 0 /* OpenGL ES 2.0+ */);
|
|
||||||
if (supports_mipmaps) {
|
|
||||||
LOGI("Trilinear filtering enabled");
|
|
||||||
display->mipmaps = true;
|
|
||||||
} else {
|
|
||||||
LOGW("Trilinear filtering disabled "
|
|
||||||
"(OpenGL 3.0+ or ES 2.0+ required)");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGI("Trilinear filtering disabled");
|
|
||||||
}
|
|
||||||
} else if (mipmaps) {
|
|
||||||
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
|
|
||||||
}
|
|
||||||
|
|
||||||
display->texture = NULL;
|
|
||||||
display->pending.flags = 0;
|
|
||||||
display->pending.frame = NULL;
|
|
||||||
display->has_frame = false;
|
|
||||||
|
|
||||||
if (icon_novideo) {
|
|
||||||
// Without video, set a static scrcpy icon as window content
|
|
||||||
bool ok = sc_display_init_novideo_icon(display, icon_novideo);
|
|
||||||
if (!ok) {
|
|
||||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
|
||||||
SDL_GL_DeleteContext(display->gl_context);
|
|
||||||
#endif
|
|
||||||
SDL_DestroyRenderer(display->renderer);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_display_destroy(struct sc_display *display) {
|
|
||||||
if (display->pending.frame) {
|
|
||||||
av_frame_free(&display->pending.frame);
|
|
||||||
}
|
|
||||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
|
||||||
SDL_GL_DeleteContext(display->gl_context);
|
|
||||||
#endif
|
|
||||||
if (display->texture) {
|
|
||||||
SDL_DestroyTexture(display->texture);
|
|
||||||
}
|
|
||||||
SDL_DestroyRenderer(display->renderer);
|
|
||||||
}
|
|
||||||
|
|
||||||
static SDL_Texture *
|
|
||||||
sc_display_create_texture(struct sc_display *display,
|
|
||||||
struct sc_size size) {
|
|
||||||
SDL_Renderer *renderer = display->renderer;
|
|
||||||
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
|
|
||||||
SDL_TEXTUREACCESS_STREAMING,
|
|
||||||
size.width, size.height);
|
|
||||||
if (!texture) {
|
|
||||||
LOGD("Could not create texture: %s", SDL_GetError());
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (display->mipmaps) {
|
|
||||||
struct sc_opengl *gl = &display->gl;
|
|
||||||
|
|
||||||
SDL_GL_BindTexture(texture, NULL, NULL);
|
|
||||||
|
|
||||||
// Enable trilinear filtering for downscaling
|
|
||||||
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
|
|
||||||
GL_LINEAR_MIPMAP_LINEAR);
|
|
||||||
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
|
|
||||||
|
|
||||||
SDL_GL_UnbindTexture(texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return texture;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_display_set_pending_size(struct sc_display *display, struct sc_size size) {
|
|
||||||
assert(!display->texture);
|
|
||||||
display->pending.size = size;
|
|
||||||
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
|
|
||||||
if (!display->pending.frame) {
|
|
||||||
display->pending.frame = av_frame_alloc();
|
|
||||||
if (!display->pending.frame) {
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int r = av_frame_ref(display->pending.frame, frame);
|
|
||||||
if (r) {
|
|
||||||
LOGE("Could not ref frame: %d", r);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_display_apply_pending(struct sc_display *display) {
|
|
||||||
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) {
|
|
||||||
assert(!display->texture);
|
|
||||||
display->texture =
|
|
||||||
sc_display_create_texture(display, display->pending.size);
|
|
||||||
if (!display->texture) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_SIZE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
|
|
||||||
assert(display->pending.frame);
|
|
||||||
bool ok = sc_display_update_texture(display, display->pending.frame);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
av_frame_unref(display->pending.frame);
|
|
||||||
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_display_set_texture_size_internal(struct sc_display *display,
|
|
||||||
struct sc_size size) {
|
|
||||||
assert(size.width && size.height);
|
|
||||||
|
|
||||||
if (display->texture) {
|
|
||||||
SDL_DestroyTexture(display->texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
display->texture = sc_display_create_texture(display, size);
|
|
||||||
if (!display->texture) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum sc_display_result
|
|
||||||
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
|
|
||||||
bool ok = sc_display_set_texture_size_internal(display, size);
|
|
||||||
if (!ok) {
|
|
||||||
sc_display_set_pending_size(display, size);
|
|
||||||
return SC_DISPLAY_RESULT_PENDING;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return SC_DISPLAY_RESULT_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
static SDL_YUV_CONVERSION_MODE
|
|
||||||
sc_display_to_sdl_color_range(enum AVColorRange color_range) {
|
|
||||||
return color_range == AVCOL_RANGE_JPEG ? SDL_YUV_CONVERSION_JPEG
|
|
||||||
: SDL_YUV_CONVERSION_AUTOMATIC;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_display_update_texture_internal(struct sc_display *display,
|
|
||||||
const AVFrame *frame) {
|
|
||||||
if (!display->has_frame) {
|
|
||||||
// First frame
|
|
||||||
display->has_frame = true;
|
|
||||||
|
|
||||||
// Configure YUV color range conversion
|
|
||||||
SDL_YUV_CONVERSION_MODE sdl_color_range =
|
|
||||||
sc_display_to_sdl_color_range(frame->color_range);
|
|
||||||
SDL_SetYUVConversionMode(sdl_color_range);
|
|
||||||
}
|
|
||||||
|
|
||||||
int ret = SDL_UpdateYUVTexture(display->texture, NULL,
|
|
||||||
frame->data[0], frame->linesize[0],
|
|
||||||
frame->data[1], frame->linesize[1],
|
|
||||||
frame->data[2], frame->linesize[2]);
|
|
||||||
if (ret) {
|
|
||||||
LOGD("Could not update texture: %s", SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (display->mipmaps) {
|
|
||||||
SDL_GL_BindTexture(display->texture, NULL, NULL);
|
|
||||||
display->gl.GenerateMipmap(GL_TEXTURE_2D);
|
|
||||||
SDL_GL_UnbindTexture(display->texture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum sc_display_result
|
|
||||||
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
|
|
||||||
bool ok = sc_display_update_texture_internal(display, frame);
|
|
||||||
if (!ok) {
|
|
||||||
ok = sc_display_set_pending_frame(display, frame);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Could not set pending frame");
|
|
||||||
return SC_DISPLAY_RESULT_ERROR;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SC_DISPLAY_RESULT_PENDING;
|
|
||||||
}
|
|
||||||
|
|
||||||
return SC_DISPLAY_RESULT_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
enum sc_display_result
|
|
||||||
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
|
||||||
enum sc_orientation orientation) {
|
|
||||||
SDL_RenderClear(display->renderer);
|
|
||||||
|
|
||||||
if (display->pending.flags) {
|
|
||||||
bool ok = sc_display_apply_pending(display);
|
|
||||||
if (!ok) {
|
|
||||||
return SC_DISPLAY_RESULT_PENDING;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_Renderer *renderer = display->renderer;
|
|
||||||
SDL_Texture *texture = display->texture;
|
|
||||||
|
|
||||||
if (orientation == SC_ORIENTATION_0) {
|
|
||||||
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
|
|
||||||
if (ret) {
|
|
||||||
LOGE("Could not render texture: %s", SDL_GetError());
|
|
||||||
return SC_DISPLAY_RESULT_ERROR;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
unsigned cw_rotation = sc_orientation_get_rotation(orientation);
|
|
||||||
double angle = 90 * cw_rotation;
|
|
||||||
|
|
||||||
const SDL_Rect *dstrect = NULL;
|
|
||||||
SDL_Rect rect;
|
|
||||||
if (sc_orientation_is_swap(orientation)) {
|
|
||||||
rect.x = geometry->x + (geometry->w - geometry->h) / 2;
|
|
||||||
rect.y = geometry->y + (geometry->h - geometry->w) / 2;
|
|
||||||
rect.w = geometry->h;
|
|
||||||
rect.h = geometry->w;
|
|
||||||
dstrect = ▭
|
|
||||||
} else {
|
|
||||||
dstrect = geometry;
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_RendererFlip flip = sc_orientation_is_mirror(orientation)
|
|
||||||
? SDL_FLIP_HORIZONTAL : 0;
|
|
||||||
|
|
||||||
int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle,
|
|
||||||
NULL, flip);
|
|
||||||
if (ret) {
|
|
||||||
LOGE("Could not render texture: %s", SDL_GetError());
|
|
||||||
return SC_DISPLAY_RESULT_ERROR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_RenderPresent(display->renderer);
|
|
||||||
return SC_DISPLAY_RESULT_OK;
|
|
||||||
}
|
|
@ -1,63 +0,0 @@
|
|||||||
#ifndef SC_DISPLAY_H
|
|
||||||
#define SC_DISPLAY_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
#include <SDL2/SDL.h>
|
|
||||||
|
|
||||||
#include "coords.h"
|
|
||||||
#include "opengl.h"
|
|
||||||
#include "options.h"
|
|
||||||
|
|
||||||
#ifdef __APPLE__
|
|
||||||
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
|
||||||
#endif
|
|
||||||
|
|
||||||
struct sc_display {
|
|
||||||
SDL_Renderer *renderer;
|
|
||||||
SDL_Texture *texture;
|
|
||||||
|
|
||||||
struct sc_opengl gl;
|
|
||||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
|
||||||
SDL_GLContext *gl_context;
|
|
||||||
#endif
|
|
||||||
|
|
||||||
bool mipmaps;
|
|
||||||
|
|
||||||
struct {
|
|
||||||
#define SC_DISPLAY_PENDING_FLAG_SIZE 1
|
|
||||||
#define SC_DISPLAY_PENDING_FLAG_FRAME 2
|
|
||||||
int8_t flags;
|
|
||||||
struct sc_size size;
|
|
||||||
AVFrame *frame;
|
|
||||||
} pending;
|
|
||||||
|
|
||||||
bool has_frame;
|
|
||||||
};
|
|
||||||
|
|
||||||
enum sc_display_result {
|
|
||||||
SC_DISPLAY_RESULT_OK,
|
|
||||||
SC_DISPLAY_RESULT_PENDING,
|
|
||||||
SC_DISPLAY_RESULT_ERROR,
|
|
||||||
};
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_display_init(struct sc_display *display, SDL_Window *window,
|
|
||||||
SDL_Surface *icon_novideo, bool mipmaps);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_display_destroy(struct sc_display *display);
|
|
||||||
|
|
||||||
enum sc_display_result
|
|
||||||
sc_display_set_texture_size(struct sc_display *display, struct sc_size size);
|
|
||||||
|
|
||||||
enum sc_display_result
|
|
||||||
sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
|
|
||||||
|
|
||||||
enum sc_display_result
|
|
||||||
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
|
||||||
enum sc_orientation orientation);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,66 +0,0 @@
|
|||||||
#include "events.h"
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
#include "util/thread.h"
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_push_event_impl(uint32_t type, const char *name) {
|
|
||||||
SDL_Event event;
|
|
||||||
event.type = type;
|
|
||||||
int ret = SDL_PushEvent(&event);
|
|
||||||
// ret < 0: error (queue full)
|
|
||||||
// ret == 0: event was filtered
|
|
||||||
// ret == 1: success
|
|
||||||
if (ret != 1) {
|
|
||||||
LOGE("Could not post %s event: %s", name, SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_post_to_main_thread(sc_runnable_fn run, void *userdata) {
|
|
||||||
SDL_Event event = {
|
|
||||||
.user = {
|
|
||||||
.type = SC_EVENT_RUN_ON_MAIN_THREAD,
|
|
||||||
.data1 = run,
|
|
||||||
.data2 = userdata,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
int ret = SDL_PushEvent(&event);
|
|
||||||
// ret < 0: error (queue full)
|
|
||||||
// ret == 0: event was filtered
|
|
||||||
// ret == 1: success
|
|
||||||
if (ret != 1) {
|
|
||||||
if (ret == 0) {
|
|
||||||
// if ret == 0, this is expected on exit, log in debug mode
|
|
||||||
LOGD("Could not post runnable to main thread (filtered)");
|
|
||||||
} else {
|
|
||||||
assert(ret < 0);
|
|
||||||
LOGW("Could not post runnable to main thread: %s", SDL_GetError());
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int SDLCALL
|
|
||||||
task_event_filter(void *userdata, SDL_Event *event) {
|
|
||||||
(void) userdata;
|
|
||||||
|
|
||||||
if (event->type == SC_EVENT_RUN_ON_MAIN_THREAD) {
|
|
||||||
// Reject this event type from now on
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_reject_new_runnables(void) {
|
|
||||||
assert(sc_thread_get_id() == SC_MAIN_THREAD_ID);
|
|
||||||
|
|
||||||
SDL_SetEventFilter(task_event_filter, NULL);
|
|
||||||
}
|
|
@ -1,38 +1,3 @@
|
|||||||
#ifndef SC_EVENTS_H
|
#define EVENT_NEW_SESSION SDL_USEREVENT
|
||||||
#define SC_EVENTS_H
|
#define EVENT_NEW_FRAME (SDL_USEREVENT + 1)
|
||||||
|
#define EVENT_DECODER_STOPPED (SDL_USEREVENT + 2)
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
#include <SDL2/SDL_events.h>
|
|
||||||
|
|
||||||
enum {
|
|
||||||
SC_EVENT_NEW_FRAME = SDL_USEREVENT,
|
|
||||||
SC_EVENT_RUN_ON_MAIN_THREAD,
|
|
||||||
SC_EVENT_DEVICE_DISCONNECTED,
|
|
||||||
SC_EVENT_SERVER_CONNECTION_FAILED,
|
|
||||||
SC_EVENT_SERVER_CONNECTED,
|
|
||||||
SC_EVENT_USB_DEVICE_DISCONNECTED,
|
|
||||||
SC_EVENT_DEMUXER_ERROR,
|
|
||||||
SC_EVENT_RECORDER_ERROR,
|
|
||||||
SC_EVENT_SCREEN_INIT_SIZE,
|
|
||||||
SC_EVENT_TIME_LIMIT_REACHED,
|
|
||||||
SC_EVENT_CONTROLLER_ERROR,
|
|
||||||
SC_EVENT_AOA_OPEN_ERROR,
|
|
||||||
};
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_push_event_impl(uint32_t type, const char *name);
|
|
||||||
|
|
||||||
#define sc_push_event(TYPE) sc_push_event_impl(TYPE, # TYPE)
|
|
||||||
|
|
||||||
typedef void (*sc_runnable_fn)(void *userdata);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_post_to_main_thread(sc_runnable_fn run, void *userdata);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_reject_new_runnables(void);
|
|
||||||
|
|
||||||
#endif
|
|
||||||
|
@ -1,189 +0,0 @@
|
|||||||
#include "file_pusher.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "adb/adb.h"
|
|
||||||
#include "util/log.h"
|
|
||||||
#include "util/process_intr.h"
|
|
||||||
|
|
||||||
#define DEFAULT_PUSH_TARGET "/sdcard/Download/"
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_file_pusher_request_destroy(struct sc_file_pusher_request *req) {
|
|
||||||
free(req->file);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial,
|
|
||||||
const char *push_target) {
|
|
||||||
assert(serial);
|
|
||||||
|
|
||||||
sc_vecdeque_init(&fp->queue);
|
|
||||||
|
|
||||||
bool ok = sc_mutex_init(&fp->mutex);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_cond_init(&fp->event_cond);
|
|
||||||
if (!ok) {
|
|
||||||
sc_mutex_destroy(&fp->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_intr_init(&fp->intr);
|
|
||||||
if (!ok) {
|
|
||||||
sc_cond_destroy(&fp->event_cond);
|
|
||||||
sc_mutex_destroy(&fp->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fp->serial = strdup(serial);
|
|
||||||
if (!fp->serial) {
|
|
||||||
LOG_OOM();
|
|
||||||
sc_intr_destroy(&fp->intr);
|
|
||||||
sc_cond_destroy(&fp->event_cond);
|
|
||||||
sc_mutex_destroy(&fp->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// lazy initialization
|
|
||||||
fp->initialized = false;
|
|
||||||
|
|
||||||
fp->stopped = false;
|
|
||||||
|
|
||||||
fp->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_file_pusher_destroy(struct sc_file_pusher *fp) {
|
|
||||||
sc_cond_destroy(&fp->event_cond);
|
|
||||||
sc_mutex_destroy(&fp->mutex);
|
|
||||||
sc_intr_destroy(&fp->intr);
|
|
||||||
free(fp->serial);
|
|
||||||
|
|
||||||
while (!sc_vecdeque_is_empty(&fp->queue)) {
|
|
||||||
struct sc_file_pusher_request *req = sc_vecdeque_popref(&fp->queue);
|
|
||||||
assert(req);
|
|
||||||
sc_file_pusher_request_destroy(req);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_file_pusher_request(struct sc_file_pusher *fp,
|
|
||||||
enum sc_file_pusher_action action, char *file) {
|
|
||||||
// start file_pusher if it's used for the first time
|
|
||||||
if (!fp->initialized) {
|
|
||||||
if (!sc_file_pusher_start(fp)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
fp->initialized = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGI("Request to %s %s", action == SC_FILE_PUSHER_ACTION_INSTALL_APK
|
|
||||||
? "install" : "push",
|
|
||||||
file);
|
|
||||||
struct sc_file_pusher_request req = {
|
|
||||||
.action = action,
|
|
||||||
.file = file,
|
|
||||||
};
|
|
||||||
|
|
||||||
sc_mutex_lock(&fp->mutex);
|
|
||||||
bool was_empty = sc_vecdeque_is_empty(&fp->queue);
|
|
||||||
bool res = sc_vecdeque_push(&fp->queue, req);
|
|
||||||
if (!res) {
|
|
||||||
LOG_OOM();
|
|
||||||
sc_mutex_unlock(&fp->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (was_empty) {
|
|
||||||
sc_cond_signal(&fp->event_cond);
|
|
||||||
}
|
|
||||||
sc_mutex_unlock(&fp->mutex);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
run_file_pusher(void *data) {
|
|
||||||
struct sc_file_pusher *fp = data;
|
|
||||||
struct sc_intr *intr = &fp->intr;
|
|
||||||
|
|
||||||
const char *serial = fp->serial;
|
|
||||||
assert(serial);
|
|
||||||
|
|
||||||
const char *push_target = fp->push_target;
|
|
||||||
assert(push_target);
|
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
sc_mutex_lock(&fp->mutex);
|
|
||||||
while (!fp->stopped && sc_vecdeque_is_empty(&fp->queue)) {
|
|
||||||
sc_cond_wait(&fp->event_cond, &fp->mutex);
|
|
||||||
}
|
|
||||||
if (fp->stopped) {
|
|
||||||
// stop immediately, do not process further events
|
|
||||||
sc_mutex_unlock(&fp->mutex);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(!sc_vecdeque_is_empty(&fp->queue));
|
|
||||||
struct sc_file_pusher_request req = sc_vecdeque_pop(&fp->queue);
|
|
||||||
sc_mutex_unlock(&fp->mutex);
|
|
||||||
|
|
||||||
if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) {
|
|
||||||
LOGI("Installing %s...", req.file);
|
|
||||||
bool ok = sc_adb_install(intr, serial, req.file, 0);
|
|
||||||
if (ok) {
|
|
||||||
LOGI("%s successfully installed", req.file);
|
|
||||||
} else {
|
|
||||||
LOGE("Failed to install %s", req.file);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
LOGI("Pushing %s...", req.file);
|
|
||||||
bool ok = sc_adb_push(intr, serial, req.file, push_target, 0);
|
|
||||||
if (ok) {
|
|
||||||
LOGI("%s successfully pushed to %s", req.file, push_target);
|
|
||||||
} else {
|
|
||||||
LOGE("Failed to push %s to %s", req.file, push_target);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_file_pusher_request_destroy(&req);
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_file_pusher_start(struct sc_file_pusher *fp) {
|
|
||||||
LOGD("Starting file_pusher thread");
|
|
||||||
|
|
||||||
bool ok = sc_thread_create(&fp->thread, run_file_pusher, "scrcpy-file", fp);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Could not start file_pusher thread");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_file_pusher_stop(struct sc_file_pusher *fp) {
|
|
||||||
if (fp->initialized) {
|
|
||||||
sc_mutex_lock(&fp->mutex);
|
|
||||||
fp->stopped = true;
|
|
||||||
sc_cond_signal(&fp->event_cond);
|
|
||||||
sc_intr_interrupt(&fp->intr);
|
|
||||||
sc_mutex_unlock(&fp->mutex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_file_pusher_join(struct sc_file_pusher *fp) {
|
|
||||||
if (fp->initialized) {
|
|
||||||
sc_thread_join(&fp->thread, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,58 +0,0 @@
|
|||||||
#ifndef SC_FILE_PUSHER_H
|
|
||||||
#define SC_FILE_PUSHER_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#include "util/intr.h"
|
|
||||||
#include "util/thread.h"
|
|
||||||
#include "util/vecdeque.h"
|
|
||||||
|
|
||||||
enum sc_file_pusher_action {
|
|
||||||
SC_FILE_PUSHER_ACTION_INSTALL_APK,
|
|
||||||
SC_FILE_PUSHER_ACTION_PUSH_FILE,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_file_pusher_request {
|
|
||||||
enum sc_file_pusher_action action;
|
|
||||||
char *file;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_file_pusher_request_queue SC_VECDEQUE(struct sc_file_pusher_request);
|
|
||||||
|
|
||||||
struct sc_file_pusher {
|
|
||||||
char *serial;
|
|
||||||
const char *push_target;
|
|
||||||
sc_thread thread;
|
|
||||||
sc_mutex mutex;
|
|
||||||
sc_cond event_cond;
|
|
||||||
bool stopped;
|
|
||||||
bool initialized;
|
|
||||||
struct sc_file_pusher_request_queue queue;
|
|
||||||
|
|
||||||
struct sc_intr intr;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial,
|
|
||||||
const char *push_target);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_file_pusher_destroy(struct sc_file_pusher *fp);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_file_pusher_start(struct sc_file_pusher *fp);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_file_pusher_stop(struct sc_file_pusher *fp);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_file_pusher_join(struct sc_file_pusher *fp);
|
|
||||||
|
|
||||||
// take ownership of file, and will free() it
|
|
||||||
bool
|
|
||||||
sc_file_pusher_request(struct sc_file_pusher *fp,
|
|
||||||
enum sc_file_pusher_action action, char *file);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,185 +0,0 @@
|
|||||||
#include "fps_counter.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
#define SC_FPS_COUNTER_INTERVAL SC_TICK_FROM_SEC(1)
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_fps_counter_init(struct sc_fps_counter *counter) {
|
|
||||||
bool ok = sc_mutex_init(&counter->mutex);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_cond_init(&counter->state_cond);
|
|
||||||
if (!ok) {
|
|
||||||
sc_mutex_destroy(&counter->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
counter->thread_started = false;
|
|
||||||
atomic_init(&counter->started, 0);
|
|
||||||
// no need to initialize the other fields, they are unused until started
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_destroy(struct sc_fps_counter *counter) {
|
|
||||||
sc_cond_destroy(&counter->state_cond);
|
|
||||||
sc_mutex_destroy(&counter->mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool
|
|
||||||
is_started(struct sc_fps_counter *counter) {
|
|
||||||
return atomic_load_explicit(&counter->started, memory_order_acquire);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
set_started(struct sc_fps_counter *counter, bool started) {
|
|
||||||
atomic_store_explicit(&counter->started, started, memory_order_release);
|
|
||||||
}
|
|
||||||
|
|
||||||
// must be called with mutex locked
|
|
||||||
static void
|
|
||||||
display_fps(struct sc_fps_counter *counter) {
|
|
||||||
unsigned rendered_per_second =
|
|
||||||
counter->nr_rendered * SC_TICK_FREQ / SC_FPS_COUNTER_INTERVAL;
|
|
||||||
if (counter->nr_skipped) {
|
|
||||||
LOGI("%u fps (+%u frames skipped)", rendered_per_second,
|
|
||||||
counter->nr_skipped);
|
|
||||||
} else {
|
|
||||||
LOGI("%u fps", rendered_per_second);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// must be called with mutex locked
|
|
||||||
static void
|
|
||||||
check_interval_expired(struct sc_fps_counter *counter, sc_tick now) {
|
|
||||||
if (now < counter->next_timestamp) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
display_fps(counter);
|
|
||||||
counter->nr_rendered = 0;
|
|
||||||
counter->nr_skipped = 0;
|
|
||||||
// add a multiple of the interval
|
|
||||||
uint32_t elapsed_slices =
|
|
||||||
(now - counter->next_timestamp) / SC_FPS_COUNTER_INTERVAL + 1;
|
|
||||||
counter->next_timestamp += SC_FPS_COUNTER_INTERVAL * elapsed_slices;
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
run_fps_counter(void *data) {
|
|
||||||
struct sc_fps_counter *counter = data;
|
|
||||||
|
|
||||||
sc_mutex_lock(&counter->mutex);
|
|
||||||
while (!counter->interrupted) {
|
|
||||||
while (!counter->interrupted && !is_started(counter)) {
|
|
||||||
sc_cond_wait(&counter->state_cond, &counter->mutex);
|
|
||||||
}
|
|
||||||
while (!counter->interrupted && is_started(counter)) {
|
|
||||||
sc_tick now = sc_tick_now();
|
|
||||||
check_interval_expired(counter, now);
|
|
||||||
|
|
||||||
// ignore the reason (timeout or signaled), we just loop anyway
|
|
||||||
sc_cond_timedwait(&counter->state_cond, &counter->mutex,
|
|
||||||
counter->next_timestamp);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sc_mutex_unlock(&counter->mutex);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_fps_counter_start(struct sc_fps_counter *counter) {
|
|
||||||
sc_mutex_lock(&counter->mutex);
|
|
||||||
counter->interrupted = false;
|
|
||||||
counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL;
|
|
||||||
counter->nr_rendered = 0;
|
|
||||||
counter->nr_skipped = 0;
|
|
||||||
sc_mutex_unlock(&counter->mutex);
|
|
||||||
|
|
||||||
set_started(counter, true);
|
|
||||||
sc_cond_signal(&counter->state_cond);
|
|
||||||
|
|
||||||
// counter->thread_started and counter->thread are always accessed from the
|
|
||||||
// same thread, no need to lock
|
|
||||||
if (!counter->thread_started) {
|
|
||||||
bool ok = sc_thread_create(&counter->thread, run_fps_counter,
|
|
||||||
"scrcpy-fps", counter);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Could not start FPS counter thread");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
counter->thread_started = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
LOGI("FPS counter started");
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_stop(struct sc_fps_counter *counter) {
|
|
||||||
set_started(counter, false);
|
|
||||||
sc_cond_signal(&counter->state_cond);
|
|
||||||
LOGI("FPS counter stopped");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_fps_counter_is_started(struct sc_fps_counter *counter) {
|
|
||||||
return is_started(counter);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_interrupt(struct sc_fps_counter *counter) {
|
|
||||||
if (!counter->thread_started) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_mutex_lock(&counter->mutex);
|
|
||||||
counter->interrupted = true;
|
|
||||||
sc_mutex_unlock(&counter->mutex);
|
|
||||||
// wake up blocking wait
|
|
||||||
sc_cond_signal(&counter->state_cond);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_join(struct sc_fps_counter *counter) {
|
|
||||||
if (counter->thread_started) {
|
|
||||||
// interrupted must be set by the thread calling join(), so no need to
|
|
||||||
// lock for the assertion
|
|
||||||
assert(counter->interrupted);
|
|
||||||
|
|
||||||
sc_thread_join(&counter->thread, NULL);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter) {
|
|
||||||
if (!is_started(counter)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_mutex_lock(&counter->mutex);
|
|
||||||
sc_tick now = sc_tick_now();
|
|
||||||
check_interval_expired(counter, now);
|
|
||||||
++counter->nr_rendered;
|
|
||||||
sc_mutex_unlock(&counter->mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter) {
|
|
||||||
if (!is_started(counter)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_mutex_lock(&counter->mutex);
|
|
||||||
sc_tick now = sc_tick_now();
|
|
||||||
check_interval_expired(counter, now);
|
|
||||||
++counter->nr_skipped;
|
|
||||||
sc_mutex_unlock(&counter->mutex);
|
|
||||||
}
|
|
@ -1,59 +0,0 @@
|
|||||||
#ifndef SC_FPSCOUNTER_H
|
|
||||||
#define SC_FPSCOUNTER_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdatomic.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#include "util/thread.h"
|
|
||||||
|
|
||||||
struct sc_fps_counter {
|
|
||||||
sc_thread thread;
|
|
||||||
sc_mutex mutex;
|
|
||||||
sc_cond state_cond;
|
|
||||||
|
|
||||||
bool thread_started;
|
|
||||||
|
|
||||||
// atomic so that we can check without locking the mutex
|
|
||||||
// if the FPS counter is disabled, we don't want to lock unnecessarily
|
|
||||||
atomic_bool started;
|
|
||||||
|
|
||||||
// the following fields are protected by the mutex
|
|
||||||
bool interrupted;
|
|
||||||
unsigned nr_rendered;
|
|
||||||
unsigned nr_skipped;
|
|
||||||
sc_tick next_timestamp;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_fps_counter_init(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_destroy(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_fps_counter_start(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_stop(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_fps_counter_is_started(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
// request to stop the thread (on quit)
|
|
||||||
// must be called before sc_fps_counter_join()
|
|
||||||
void
|
|
||||||
sc_fps_counter_interrupt(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_join(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_add_rendered_frame(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_fps_counter_add_skipped_frame(struct sc_fps_counter *counter);
|
|
||||||
|
|
||||||
#endif
|
|
62
app/src/fpscounter.c
Normal file
62
app/src/fpscounter.c
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
#include "fpscounter.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL_timer.h>
|
||||||
|
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
void fps_counter_init(struct fps_counter *counter) {
|
||||||
|
counter->started = SDL_FALSE;
|
||||||
|
// no need to initialize the other fields, they are meaningful only when
|
||||||
|
// started is true
|
||||||
|
}
|
||||||
|
|
||||||
|
void fps_counter_start(struct fps_counter *counter) {
|
||||||
|
counter->started = SDL_TRUE;
|
||||||
|
counter->slice_start = SDL_GetTicks();
|
||||||
|
counter->nr_rendered = 0;
|
||||||
|
#ifdef SKIP_FRAMES
|
||||||
|
counter->nr_skipped = 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void fps_counter_stop(struct fps_counter *counter) {
|
||||||
|
counter->started = SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void display_fps(struct fps_counter *counter) {
|
||||||
|
#ifdef SKIP_FRAMES
|
||||||
|
if (counter->nr_skipped) {
|
||||||
|
LOGI("%d fps (+%d frames skipped)", counter->nr_rendered, counter->nr_skipped);
|
||||||
|
} else {
|
||||||
|
#endif
|
||||||
|
LOGI("%d fps", counter->nr_rendered);
|
||||||
|
#ifdef SKIP_FRAMES
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
static void check_expired(struct fps_counter *counter) {
|
||||||
|
Uint32 now = SDL_GetTicks();
|
||||||
|
if (now - counter->slice_start >= 1000) {
|
||||||
|
display_fps(counter);
|
||||||
|
// add a multiple of one second
|
||||||
|
Uint32 elapsed_slices = (now - counter->slice_start) / 1000;
|
||||||
|
counter->slice_start += 1000 * elapsed_slices;
|
||||||
|
counter->nr_rendered = 0;
|
||||||
|
#ifdef SKIP_FRAMES
|
||||||
|
counter->nr_skipped = 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void fps_counter_add_rendered_frame(struct fps_counter *counter) {
|
||||||
|
check_expired(counter);
|
||||||
|
++counter->nr_rendered;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef SKIP_FRAMES
|
||||||
|
void fps_counter_add_skipped_frame(struct fps_counter *counter) {
|
||||||
|
check_expired(counter);
|
||||||
|
++counter->nr_skipped;
|
||||||
|
}
|
||||||
|
#endif
|
26
app/src/fpscounter.h
Normal file
26
app/src/fpscounter.h
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#ifndef FPSCOUNTER_H
|
||||||
|
#define FPSCOUNTER_H
|
||||||
|
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
|
||||||
|
struct fps_counter {
|
||||||
|
SDL_bool started;
|
||||||
|
Uint32 slice_start; // initialized by SDL_GetTicks()
|
||||||
|
int nr_rendered;
|
||||||
|
#ifdef SKIP_FRAMES
|
||||||
|
int nr_skipped;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
void fps_counter_init(struct fps_counter *counter);
|
||||||
|
void fps_counter_start(struct fps_counter *counter);
|
||||||
|
void fps_counter_stop(struct fps_counter *counter);
|
||||||
|
|
||||||
|
void fps_counter_add_rendered_frame(struct fps_counter *counter);
|
||||||
|
#ifdef SKIP_FRAMES
|
||||||
|
void fps_counter_add_skipped_frame(struct fps_counter *counter);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif
|
@ -1,90 +0,0 @@
|
|||||||
#include "frame_buffer.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <libavutil/avutil.h>
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_frame_buffer_init(struct sc_frame_buffer *fb) {
|
|
||||||
fb->pending_frame = av_frame_alloc();
|
|
||||||
if (!fb->pending_frame) {
|
|
||||||
LOG_OOM();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
fb->tmp_frame = av_frame_alloc();
|
|
||||||
if (!fb->tmp_frame) {
|
|
||||||
LOG_OOM();
|
|
||||||
av_frame_free(&fb->pending_frame);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool ok = sc_mutex_init(&fb->mutex);
|
|
||||||
if (!ok) {
|
|
||||||
av_frame_free(&fb->pending_frame);
|
|
||||||
av_frame_free(&fb->tmp_frame);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// there is initially no frame, so consider it has already been consumed
|
|
||||||
fb->pending_frame_consumed = true;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_buffer_destroy(struct sc_frame_buffer *fb) {
|
|
||||||
sc_mutex_destroy(&fb->mutex);
|
|
||||||
av_frame_free(&fb->pending_frame);
|
|
||||||
av_frame_free(&fb->tmp_frame);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
swap_frames(AVFrame **lhs, AVFrame **rhs) {
|
|
||||||
AVFrame *tmp = *lhs;
|
|
||||||
*lhs = *rhs;
|
|
||||||
*rhs = tmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame,
|
|
||||||
bool *previous_frame_skipped) {
|
|
||||||
// Use a temporary frame to preserve pending_frame in case of error.
|
|
||||||
// tmp_frame is an empty frame, no need to call av_frame_unref() beforehand.
|
|
||||||
int r = av_frame_ref(fb->tmp_frame, frame);
|
|
||||||
if (r) {
|
|
||||||
LOGE("Could not ref frame: %d", r);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_mutex_lock(&fb->mutex);
|
|
||||||
|
|
||||||
// Now that av_frame_ref() succeeded, we can replace the previous
|
|
||||||
// pending_frame
|
|
||||||
swap_frames(&fb->pending_frame, &fb->tmp_frame);
|
|
||||||
av_frame_unref(fb->tmp_frame);
|
|
||||||
|
|
||||||
if (previous_frame_skipped) {
|
|
||||||
*previous_frame_skipped = !fb->pending_frame_consumed;
|
|
||||||
}
|
|
||||||
fb->pending_frame_consumed = false;
|
|
||||||
|
|
||||||
sc_mutex_unlock(&fb->mutex);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst) {
|
|
||||||
sc_mutex_lock(&fb->mutex);
|
|
||||||
assert(!fb->pending_frame_consumed);
|
|
||||||
fb->pending_frame_consumed = true;
|
|
||||||
|
|
||||||
av_frame_move_ref(dst, fb->pending_frame);
|
|
||||||
// av_frame_move_ref() resets its source frame, so no need to call
|
|
||||||
// av_frame_unref()
|
|
||||||
|
|
||||||
sc_mutex_unlock(&fb->mutex);
|
|
||||||
}
|
|
@ -1,44 +0,0 @@
|
|||||||
#ifndef SC_FRAME_BUFFER_H
|
|
||||||
#define SC_FRAME_BUFFER_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#include "util/thread.h"
|
|
||||||
|
|
||||||
// forward declarations
|
|
||||||
typedef struct AVFrame AVFrame;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A frame buffer holds 1 pending frame, which is the last frame received from
|
|
||||||
* the producer (typically, the decoder).
|
|
||||||
*
|
|
||||||
* If a pending frame has not been consumed when the producer pushes a new
|
|
||||||
* frame, then it is lost. The intent is to always provide access to the very
|
|
||||||
* last frame to minimize latency.
|
|
||||||
*/
|
|
||||||
|
|
||||||
struct sc_frame_buffer {
|
|
||||||
AVFrame *pending_frame;
|
|
||||||
AVFrame *tmp_frame; // To preserve the pending frame on error
|
|
||||||
|
|
||||||
sc_mutex mutex;
|
|
||||||
|
|
||||||
bool pending_frame_consumed;
|
|
||||||
};
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_frame_buffer_init(struct sc_frame_buffer *fb);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_buffer_destroy(struct sc_frame_buffer *fb);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_frame_buffer_push(struct sc_frame_buffer *fb, const AVFrame *frame,
|
|
||||||
bool *skipped);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_buffer_consume(struct sc_frame_buffer *fb, AVFrame *dst);
|
|
||||||
|
|
||||||
#endif
|
|
110
app/src/frames.c
Normal file
110
app/src/frames.c
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
#include "frames.h"
|
||||||
|
|
||||||
|
#include <SDL2/SDL_assert.h>
|
||||||
|
#include <SDL2/SDL_mutex.h>
|
||||||
|
#include <libavutil/avutil.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "lockutil.h"
|
||||||
|
#include "log.h"
|
||||||
|
|
||||||
|
SDL_bool frames_init(struct frames *frames) {
|
||||||
|
if (!(frames->decoding_frame = av_frame_alloc())) {
|
||||||
|
goto error_0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(frames->rendering_frame = av_frame_alloc())) {
|
||||||
|
goto error_1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(frames->mutex = SDL_CreateMutex())) {
|
||||||
|
goto error_2;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef SKIP_FRAMES
|
||||||
|
if (!(frames->rendering_frame_consumed_cond = SDL_CreateCond())) {
|
||||||
|
SDL_DestroyMutex(frames->mutex);
|
||||||
|
goto error_2;
|
||||||
|
}
|
||||||
|
frames->stopped = SDL_FALSE;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// there is initially no rendering frame, so consider it has already been
|
||||||
|
// consumed
|
||||||
|
frames->rendering_frame_consumed = SDL_TRUE;
|
||||||
|
fps_counter_init(&frames->fps_counter);
|
||||||
|
|
||||||
|
return SDL_TRUE;
|
||||||
|
|
||||||
|
error_2:
|
||||||
|
av_frame_free(&frames->rendering_frame);
|
||||||
|
error_1:
|
||||||
|
av_frame_free(&frames->decoding_frame);
|
||||||
|
error_0:
|
||||||
|
return SDL_FALSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
void frames_destroy(struct frames *frames) {
|
||||||
|
#ifndef SKIP_FRAMES
|
||||||
|
SDL_DestroyCond(frames->rendering_frame_consumed_cond);
|
||||||
|
#endif
|
||||||
|
SDL_DestroyMutex(frames->mutex);
|
||||||
|
av_frame_free(&frames->rendering_frame);
|
||||||
|
av_frame_free(&frames->decoding_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void frames_swap(struct frames *frames) {
|
||||||
|
AVFrame *tmp = frames->decoding_frame;
|
||||||
|
frames->decoding_frame = frames->rendering_frame;
|
||||||
|
frames->rendering_frame = tmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_bool frames_offer_decoded_frame(struct frames *frames) {
|
||||||
|
mutex_lock(frames->mutex);
|
||||||
|
#ifndef SKIP_FRAMES
|
||||||
|
// if SKIP_FRAMES is disabled, then the decoder must wait for the current
|
||||||
|
// frame to be consumed
|
||||||
|
while (!frames->rendering_frame_consumed && !frames->stopped) {
|
||||||
|
cond_wait(frames->rendering_frame_consumed_cond, frames->mutex);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (frames->fps_counter.started && !frames->rendering_frame_consumed) {
|
||||||
|
fps_counter_add_skipped_frame(&frames->fps_counter);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
frames_swap(frames);
|
||||||
|
|
||||||
|
SDL_bool previous_frame_consumed = frames->rendering_frame_consumed;
|
||||||
|
frames->rendering_frame_consumed = SDL_FALSE;
|
||||||
|
|
||||||
|
mutex_unlock(frames->mutex);
|
||||||
|
return previous_frame_consumed;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVFrame *frames_consume_rendered_frame(struct frames *frames) {
|
||||||
|
SDL_assert(!frames->rendering_frame_consumed);
|
||||||
|
frames->rendering_frame_consumed = SDL_TRUE;
|
||||||
|
if (frames->fps_counter.started) {
|
||||||
|
fps_counter_add_rendered_frame(&frames->fps_counter);
|
||||||
|
}
|
||||||
|
#ifndef SKIP_FRAMES
|
||||||
|
// if SKIP_FRAMES is disabled, then notify the decoder the current frame is
|
||||||
|
// consumed, so that it may push a new one
|
||||||
|
cond_signal(frames->rendering_frame_consumed_cond);
|
||||||
|
#endif
|
||||||
|
return frames->rendering_frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
void frames_stop(struct frames *frames) {
|
||||||
|
#ifdef SKIP_FRAMES
|
||||||
|
(void) frames; // unused
|
||||||
|
#else
|
||||||
|
mutex_lock(frames->mutex);
|
||||||
|
frames->stopped = SDL_TRUE;
|
||||||
|
mutex_unlock(frames->mutex);
|
||||||
|
// wake up blocking wait
|
||||||
|
cond_signal(frames->rendering_frame_consumed_cond);
|
||||||
|
#endif
|
||||||
|
}
|
42
app/src/frames.h
Normal file
42
app/src/frames.h
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
#ifndef FRAMES_H
|
||||||
|
#define FRAMES_H
|
||||||
|
|
||||||
|
#include <SDL2/SDL_mutex.h>
|
||||||
|
#include <SDL2/SDL_stdinc.h>
|
||||||
|
|
||||||
|
#include "config.h"
|
||||||
|
#include "fpscounter.h"
|
||||||
|
|
||||||
|
// forward declarations
|
||||||
|
typedef struct AVFrame AVFrame;
|
||||||
|
|
||||||
|
struct frames {
|
||||||
|
AVFrame *decoding_frame;
|
||||||
|
AVFrame *rendering_frame;
|
||||||
|
SDL_mutex *mutex;
|
||||||
|
#ifndef SKIP_FRAMES
|
||||||
|
SDL_bool stopped;
|
||||||
|
SDL_cond *rendering_frame_consumed_cond;
|
||||||
|
#endif
|
||||||
|
SDL_bool rendering_frame_consumed;
|
||||||
|
struct fps_counter fps_counter;
|
||||||
|
};
|
||||||
|
|
||||||
|
SDL_bool frames_init(struct frames *frames);
|
||||||
|
void frames_destroy(struct frames *frames);
|
||||||
|
|
||||||
|
// set the decoder frame as ready for rendering
|
||||||
|
// this function locks frames->mutex during its execution
|
||||||
|
// returns true if the previous frame had been consumed
|
||||||
|
SDL_bool frames_offer_decoded_frame(struct frames *frames);
|
||||||
|
|
||||||
|
// mark the rendering frame as consumed and return it
|
||||||
|
// MUST be called with frames->mutex locked!!!
|
||||||
|
// the caller is expected to render the returned frame to some texture before
|
||||||
|
// unlocking frames->mutex
|
||||||
|
const AVFrame *frames_consume_rendered_frame(struct frames *frames);
|
||||||
|
|
||||||
|
// wake up and avoid any blocking call
|
||||||
|
void frames_stop(struct frames *frames);
|
||||||
|
|
||||||
|
#endif
|
@ -1,26 +0,0 @@
|
|||||||
#ifndef SC_HID_EVENT_H
|
|
||||||
#define SC_HID_EVENT_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#define SC_HID_MAX_SIZE 15
|
|
||||||
|
|
||||||
struct sc_hid_input {
|
|
||||||
uint16_t hid_id;
|
|
||||||
uint8_t data[SC_HID_MAX_SIZE];
|
|
||||||
uint8_t size;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_hid_open {
|
|
||||||
uint16_t hid_id;
|
|
||||||
const uint8_t *report_desc; // pointer to static memory
|
|
||||||
size_t report_desc_size;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_hid_close {
|
|
||||||
uint16_t hid_id;
|
|
||||||
};
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,452 +0,0 @@
|
|||||||
#include "hid_gamepad.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <inttypes.h>
|
|
||||||
|
|
||||||
#include "util/binary.h"
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
// 2x2 bytes for left stick (X, Y)
|
|
||||||
// 2x2 bytes for right stick (Z, Rz)
|
|
||||||
// 2x2 bytes for L2/R2 triggers
|
|
||||||
// 2 bytes for buttons + padding,
|
|
||||||
// 1 byte for hat switch (dpad) + padding
|
|
||||||
#define SC_HID_GAMEPAD_EVENT_SIZE 15
|
|
||||||
|
|
||||||
// The ->buttons field stores the state for all buttons, but only some of them
|
|
||||||
// (the 16 LSB) must be transmitted "as is". The DPAD (hat switch) buttons are
|
|
||||||
// stored locally in the MSB of this field, but not transmitted as is: they are
|
|
||||||
// transformed to generate another specific byte.
|
|
||||||
#define SC_HID_BUTTONS_MASK 0xFFFF
|
|
||||||
|
|
||||||
// outside SC_HID_BUTTONS_MASK
|
|
||||||
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_UP UINT32_C(0x10000)
|
|
||||||
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN UINT32_C(0x20000)
|
|
||||||
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT UINT32_C(0x40000)
|
|
||||||
#define SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT UINT32_C(0x80000)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gamepad descriptor manually crafted to transmit the input reports.
|
|
||||||
*
|
|
||||||
* The HID specification is available here:
|
|
||||||
* <https://www.usb.org/document-library/device-class-definition-hid-111>
|
|
||||||
*
|
|
||||||
* The HID Usage Tables is also useful:
|
|
||||||
* <https://www.usb.org/document-library/hid-usage-tables-15>
|
|
||||||
*/
|
|
||||||
static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = {
|
|
||||||
// Usage Page (Generic Desktop)
|
|
||||||
0x05, 0x01,
|
|
||||||
// Usage (Gamepad)
|
|
||||||
0x09, 0x05,
|
|
||||||
|
|
||||||
// Collection (Application)
|
|
||||||
0xA1, 0x01,
|
|
||||||
|
|
||||||
// Collection (Physical)
|
|
||||||
0xA1, 0x00,
|
|
||||||
|
|
||||||
// Usage Page (Generic Desktop)
|
|
||||||
0x05, 0x01,
|
|
||||||
// Usage (X) Left stick x
|
|
||||||
0x09, 0x30,
|
|
||||||
// Usage (Y) Left stick y
|
|
||||||
0x09, 0x31,
|
|
||||||
// Usage (Rx) Right stick x
|
|
||||||
0x09, 0x33,
|
|
||||||
// Usage (Ry) Right stick y
|
|
||||||
0x09, 0x34,
|
|
||||||
// Logical Minimum (0)
|
|
||||||
0x15, 0x00,
|
|
||||||
// Logical Maximum (65535)
|
|
||||||
// Cannot use 26 FF FF because 0xFFFF is interpreted as signed 16-bit
|
|
||||||
0x27, 0xFF, 0xFF, 0x00, 0x00, // little-endian
|
|
||||||
// Report Size (16)
|
|
||||||
0x75, 0x10,
|
|
||||||
// Report Count (4)
|
|
||||||
0x95, 0x04,
|
|
||||||
// Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz)
|
|
||||||
0x81, 0x02,
|
|
||||||
|
|
||||||
// Usage Page (Generic Desktop)
|
|
||||||
0x05, 0x01,
|
|
||||||
// Usage (Z)
|
|
||||||
0x09, 0x32,
|
|
||||||
// Usage (Rz)
|
|
||||||
0x09, 0x35,
|
|
||||||
// Logical Minimum (0)
|
|
||||||
0x15, 0x00,
|
|
||||||
// Logical Maximum (32767)
|
|
||||||
0x26, 0xFF, 0x7F,
|
|
||||||
// Report Size (16)
|
|
||||||
0x75, 0x10,
|
|
||||||
// Report Count (2)
|
|
||||||
0x95, 0x02,
|
|
||||||
// Input (Data, Variable, Absolute): 2x2 bytes (L2, R2)
|
|
||||||
0x81, 0x02,
|
|
||||||
|
|
||||||
// Usage Page (Buttons)
|
|
||||||
0x05, 0x09,
|
|
||||||
// Usage Minimum (1)
|
|
||||||
0x19, 0x01,
|
|
||||||
// Usage Maximum (16)
|
|
||||||
0x29, 0x10,
|
|
||||||
// Logical Minimum (0)
|
|
||||||
0x15, 0x00,
|
|
||||||
// Logical Maximum (1)
|
|
||||||
0x25, 0x01,
|
|
||||||
// Report Count (16)
|
|
||||||
0x95, 0x10,
|
|
||||||
// Report Size (1)
|
|
||||||
0x75, 0x01,
|
|
||||||
// Input (Data, Variable, Absolute): 16 buttons bits
|
|
||||||
0x81, 0x02,
|
|
||||||
|
|
||||||
// Usage Page (Generic Desktop)
|
|
||||||
0x05, 0x01,
|
|
||||||
// Usage (Hat switch)
|
|
||||||
0x09, 0x39,
|
|
||||||
// Logical Minimum (1)
|
|
||||||
0x15, 0x01,
|
|
||||||
// Logical Maximum (8)
|
|
||||||
0x25, 0x08,
|
|
||||||
// Report Size (4)
|
|
||||||
0x75, 0x04,
|
|
||||||
// Report Count (1)
|
|
||||||
0x95, 0x01,
|
|
||||||
// Input (Data, Variable, Null State): 4-bit value
|
|
||||||
0x81, 0x42,
|
|
||||||
|
|
||||||
// End Collection
|
|
||||||
0xC0,
|
|
||||||
|
|
||||||
// End Collection
|
|
||||||
0xC0,
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A gamepad HID input report is 15 bytes long:
|
|
||||||
* - bytes 0-3: left stick state
|
|
||||||
* - bytes 4-7: right stick state
|
|
||||||
* - bytes 8-11: L2/R2 triggers state
|
|
||||||
* - bytes 12-13: buttons state
|
|
||||||
* - bytes 14: hat switch position (dpad)
|
|
||||||
*
|
|
||||||
* +---------------+
|
|
||||||
* byte 0: |. . . . . . . .|
|
|
||||||
* | | left stick x (0-65535, little-endian)
|
|
||||||
* byte 1: |. . . . . . . .|
|
|
||||||
* +---------------+
|
|
||||||
* byte 2: |. . . . . . . .|
|
|
||||||
* | | left stick y (0-65535, little-endian)
|
|
||||||
* byte 3: |. . . . . . . .|
|
|
||||||
* +---------------+
|
|
||||||
* byte 4: |. . . . . . . .|
|
|
||||||
* | | right stick x (0-65535, little-endian)
|
|
||||||
* byte 5: |. . . . . . . .|
|
|
||||||
* +---------------+
|
|
||||||
* byte 6: |. . . . . . . .|
|
|
||||||
* | | right stick y (0-65535, little-endian)
|
|
||||||
* byte 7: |. . . . . . . .|
|
|
||||||
* +---------------+
|
|
||||||
* byte 8: |. . . . . . . .|
|
|
||||||
* | | L2 trigger (0-32767, little-endian)
|
|
||||||
* byte 9: |0 . . . . . . .|
|
|
||||||
* +---------------+
|
|
||||||
* byte 10: |. . . . . . . .|
|
|
||||||
* | | R2 trigger (0-32767, little-endian)
|
|
||||||
* byte 11: |0 . . . . . . .|
|
|
||||||
* +---------------+
|
|
||||||
*
|
|
||||||
* ,--------------- SC_GAMEPAD_BUTTON_RIGHT_SHOULDER
|
|
||||||
* | ,------------- SC_GAMEPAD_BUTTON_LEFT_SHOULDER
|
|
||||||
* | |
|
|
||||||
* | | ,--------- SC_GAMEPAD_BUTTON_NORTH
|
|
||||||
* | | | ,------- SC_GAMEPAD_BUTTON_WEST
|
|
||||||
* | | | |
|
|
||||||
* | | | | ,--- SC_GAMEPAD_BUTTON_EAST
|
|
||||||
* | | | | | ,- SC_GAMEPAD_BUTTON_SOUTH
|
|
||||||
* v v v v v v
|
|
||||||
* +---------------+
|
|
||||||
* byte 12: |. . 0 . . 0 . .|
|
|
||||||
* | | Buttons (16-bit little-endian)
|
|
||||||
* byte 13: |0 . . . . . 0 0|
|
|
||||||
* +---------------+
|
|
||||||
* ^ ^ ^ ^ ^
|
|
||||||
* | | | | |
|
|
||||||
* | | | | |
|
|
||||||
* | | | | `----- SC_GAMEPAD_BUTTON_BACK
|
|
||||||
* | | | `------- SC_GAMEPAD_BUTTON_START
|
|
||||||
* | | `--------- SC_GAMEPAD_BUTTON_GUIDE
|
|
||||||
* | `----------- SC_GAMEPAD_BUTTON_LEFT_STICK
|
|
||||||
* `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK
|
|
||||||
*
|
|
||||||
* +---------------+
|
|
||||||
* byte 14: |0 0 0 0 . . . .| hat switch (dpad) position (0-8)
|
|
||||||
* +---------------+
|
|
||||||
* 9 possible positions and their values:
|
|
||||||
* 8 1 2
|
|
||||||
* 7 0 3
|
|
||||||
* 6 5 4
|
|
||||||
* (8 is top-left, 1 is top, 2 is top-right, etc.)
|
|
||||||
*/
|
|
||||||
|
|
||||||
// [-32768 to 32767] -> [0 to 65535]
|
|
||||||
#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000)
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot,
|
|
||||||
uint32_t gamepad_id) {
|
|
||||||
assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
|
|
||||||
slot->gamepad_id = gamepad_id;
|
|
||||||
slot->buttons = 0;
|
|
||||||
slot->axis_left_x = AXIS_RESCALE(0);
|
|
||||||
slot->axis_left_y = AXIS_RESCALE(0);
|
|
||||||
slot->axis_right_x = AXIS_RESCALE(0);
|
|
||||||
slot->axis_right_y = AXIS_RESCALE(0);
|
|
||||||
slot->axis_left_trigger = 0;
|
|
||||||
slot->axis_right_trigger = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static ssize_t
|
|
||||||
sc_hid_gamepad_slot_find(struct sc_hid_gamepad *hid, uint32_t gamepad_id) {
|
|
||||||
for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) {
|
|
||||||
if (gamepad_id == hid->slots[i].gamepad_id) {
|
|
||||||
// found
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_hid_gamepad_init(struct sc_hid_gamepad *hid) {
|
|
||||||
for (size_t i = 0; i < SC_MAX_GAMEPADS; ++i) {
|
|
||||||
hid->slots[i].gamepad_id = SC_GAMEPAD_ID_INVALID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline uint16_t
|
|
||||||
sc_hid_gamepad_slot_get_id(size_t slot_idx) {
|
|
||||||
assert(slot_idx < SC_MAX_GAMEPADS);
|
|
||||||
return SC_HID_ID_GAMEPAD_FIRST + slot_idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
|
|
||||||
struct sc_hid_open *hid_open,
|
|
||||||
uint32_t gamepad_id) {
|
|
||||||
assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
|
|
||||||
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, SC_GAMEPAD_ID_INVALID);
|
|
||||||
if (slot_idx == -1) {
|
|
||||||
LOGW("No gamepad slot available for new gamepad %" PRIu32, gamepad_id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id);
|
|
||||||
|
|
||||||
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
|
|
||||||
hid_open->hid_id = hid_id;
|
|
||||||
hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC;
|
|
||||||
hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid,
|
|
||||||
struct sc_hid_close *hid_close,
|
|
||||||
uint32_t gamepad_id) {
|
|
||||||
assert(gamepad_id != SC_GAMEPAD_ID_INVALID);
|
|
||||||
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id);
|
|
||||||
if (slot_idx == -1) {
|
|
||||||
LOGW("Unknown gamepad removed %" PRIu32, gamepad_id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
hid->slots[slot_idx].gamepad_id = SC_GAMEPAD_ID_INVALID;
|
|
||||||
|
|
||||||
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
|
|
||||||
hid_close->hid_id = hid_id;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint8_t
|
|
||||||
sc_hid_gamepad_get_dpad_value(uint32_t buttons) {
|
|
||||||
// Value depending on direction:
|
|
||||||
// 8 1 2
|
|
||||||
// 7 0 3
|
|
||||||
// 6 5 4
|
|
||||||
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_UP) {
|
|
||||||
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) {
|
|
||||||
return 8;
|
|
||||||
}
|
|
||||||
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) {
|
|
||||||
return 2;
|
|
||||||
}
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN) {
|
|
||||||
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) {
|
|
||||||
return 6;
|
|
||||||
}
|
|
||||||
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) {
|
|
||||||
return 4;
|
|
||||||
}
|
|
||||||
return 5;
|
|
||||||
}
|
|
||||||
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT) {
|
|
||||||
return 7;
|
|
||||||
}
|
|
||||||
if (buttons & SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT) {
|
|
||||||
return 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_hid_gamepad_event_from_slot(uint16_t hid_id,
|
|
||||||
const struct sc_hid_gamepad_slot *slot,
|
|
||||||
struct sc_hid_input *hid_input) {
|
|
||||||
hid_input->hid_id = hid_id;
|
|
||||||
hid_input->size = SC_HID_GAMEPAD_EVENT_SIZE;
|
|
||||||
|
|
||||||
uint8_t *data = hid_input->data;
|
|
||||||
// Values must be written in little-endian
|
|
||||||
sc_write16le(data, slot->axis_left_x);
|
|
||||||
sc_write16le(data + 2, slot->axis_left_y);
|
|
||||||
sc_write16le(data + 4, slot->axis_right_x);
|
|
||||||
sc_write16le(data + 6, slot->axis_right_y);
|
|
||||||
sc_write16le(data + 8, slot->axis_left_trigger);
|
|
||||||
sc_write16le(data + 10, slot->axis_right_trigger);
|
|
||||||
sc_write16le(data + 12, slot->buttons & SC_HID_BUTTONS_MASK);
|
|
||||||
data[14] = sc_hid_gamepad_get_dpad_value(slot->buttons);
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint32_t
|
|
||||||
sc_hid_gamepad_get_button_id(enum sc_gamepad_button button) {
|
|
||||||
switch (button) {
|
|
||||||
case SC_GAMEPAD_BUTTON_SOUTH:
|
|
||||||
return 0x0001;
|
|
||||||
case SC_GAMEPAD_BUTTON_EAST:
|
|
||||||
return 0x0002;
|
|
||||||
case SC_GAMEPAD_BUTTON_WEST:
|
|
||||||
return 0x0008;
|
|
||||||
case SC_GAMEPAD_BUTTON_NORTH:
|
|
||||||
return 0x0010;
|
|
||||||
case SC_GAMEPAD_BUTTON_BACK:
|
|
||||||
return 0x0400;
|
|
||||||
case SC_GAMEPAD_BUTTON_GUIDE:
|
|
||||||
return 0x1000;
|
|
||||||
case SC_GAMEPAD_BUTTON_START:
|
|
||||||
return 0x0800;
|
|
||||||
case SC_GAMEPAD_BUTTON_LEFT_STICK:
|
|
||||||
return 0x2000;
|
|
||||||
case SC_GAMEPAD_BUTTON_RIGHT_STICK:
|
|
||||||
return 0x4000;
|
|
||||||
case SC_GAMEPAD_BUTTON_LEFT_SHOULDER:
|
|
||||||
return 0x0040;
|
|
||||||
case SC_GAMEPAD_BUTTON_RIGHT_SHOULDER:
|
|
||||||
return 0x0080;
|
|
||||||
case SC_GAMEPAD_BUTTON_DPAD_UP:
|
|
||||||
return SC_GAMEPAD_BUTTONS_BIT_DPAD_UP;
|
|
||||||
case SC_GAMEPAD_BUTTON_DPAD_DOWN:
|
|
||||||
return SC_GAMEPAD_BUTTONS_BIT_DPAD_DOWN;
|
|
||||||
case SC_GAMEPAD_BUTTON_DPAD_LEFT:
|
|
||||||
return SC_GAMEPAD_BUTTONS_BIT_DPAD_LEFT;
|
|
||||||
case SC_GAMEPAD_BUTTON_DPAD_RIGHT:
|
|
||||||
return SC_GAMEPAD_BUTTONS_BIT_DPAD_RIGHT;
|
|
||||||
default:
|
|
||||||
// unknown button, ignore
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid,
|
|
||||||
struct sc_hid_input *hid_input,
|
|
||||||
const struct sc_gamepad_button_event *event) {
|
|
||||||
if ((event->button < 0) || (event->button > 15)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint32_t gamepad_id = event->gamepad_id;
|
|
||||||
|
|
||||||
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id);
|
|
||||||
if (slot_idx == -1) {
|
|
||||||
LOGW("Axis event for unknown gamepad %" PRIu32, gamepad_id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(slot_idx < SC_MAX_GAMEPADS);
|
|
||||||
|
|
||||||
struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx];
|
|
||||||
|
|
||||||
uint32_t button = sc_hid_gamepad_get_button_id(event->button);
|
|
||||||
if (!button) {
|
|
||||||
// unknown button, ignore
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (event->action == SC_ACTION_DOWN) {
|
|
||||||
slot->buttons |= button;
|
|
||||||
} else {
|
|
||||||
assert(event->action == SC_ACTION_UP);
|
|
||||||
slot->buttons &= ~button;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
|
|
||||||
sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid,
|
|
||||||
struct sc_hid_input *hid_input,
|
|
||||||
const struct sc_gamepad_axis_event *event) {
|
|
||||||
uint32_t gamepad_id = event->gamepad_id;
|
|
||||||
|
|
||||||
ssize_t slot_idx = sc_hid_gamepad_slot_find(hid, gamepad_id);
|
|
||||||
if (slot_idx == -1) {
|
|
||||||
LOGW("Button event for unknown gamepad %" PRIu32, gamepad_id);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(slot_idx < SC_MAX_GAMEPADS);
|
|
||||||
|
|
||||||
struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx];
|
|
||||||
|
|
||||||
switch (event->axis) {
|
|
||||||
case SC_GAMEPAD_AXIS_LEFTX:
|
|
||||||
slot->axis_left_x = AXIS_RESCALE(event->value);
|
|
||||||
break;
|
|
||||||
case SC_GAMEPAD_AXIS_LEFTY:
|
|
||||||
slot->axis_left_y = AXIS_RESCALE(event->value);
|
|
||||||
break;
|
|
||||||
case SC_GAMEPAD_AXIS_RIGHTX:
|
|
||||||
slot->axis_right_x = AXIS_RESCALE(event->value);
|
|
||||||
break;
|
|
||||||
case SC_GAMEPAD_AXIS_RIGHTY:
|
|
||||||
slot->axis_right_y = AXIS_RESCALE(event->value);
|
|
||||||
break;
|
|
||||||
case SC_GAMEPAD_AXIS_LEFT_TRIGGER:
|
|
||||||
// Trigger is always positive between 0 and 32767
|
|
||||||
slot->axis_left_trigger = MAX(0, event->value);
|
|
||||||
break;
|
|
||||||
case SC_GAMEPAD_AXIS_RIGHT_TRIGGER:
|
|
||||||
// Trigger is always positive between 0 and 32767
|
|
||||||
slot->axis_right_trigger = MAX(0, event->value);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx);
|
|
||||||
sc_hid_gamepad_event_from_slot(hid_id, slot, hid_input);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
#ifndef SC_HID_GAMEPAD_H
|
|
||||||
#define SC_HID_GAMEPAD_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#include "hid/hid_event.h"
|
|
||||||
#include "input_events.h"
|
|
||||||
|
|
||||||
#define SC_MAX_GAMEPADS 8
|
|
||||||
#define SC_HID_ID_GAMEPAD_FIRST 3
|
|
||||||
#define SC_HID_ID_GAMEPAD_LAST (SC_HID_ID_GAMEPAD_FIRST + SC_MAX_GAMEPADS - 1)
|
|
||||||
|
|
||||||
struct sc_hid_gamepad_slot {
|
|
||||||
uint32_t gamepad_id;
|
|
||||||
uint32_t buttons;
|
|
||||||
uint16_t axis_left_x;
|
|
||||||
uint16_t axis_left_y;
|
|
||||||
uint16_t axis_right_x;
|
|
||||||
uint16_t axis_right_y;
|
|
||||||
uint16_t axis_left_trigger;
|
|
||||||
uint16_t axis_right_trigger;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_hid_gamepad {
|
|
||||||
struct sc_hid_gamepad_slot slots[SC_MAX_GAMEPADS];
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_hid_gamepad_init(struct sc_hid_gamepad *hid);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid,
|
|
||||||
struct sc_hid_open *hid_open,
|
|
||||||
uint32_t gamepad_id);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_gamepad_generate_close(struct sc_hid_gamepad *hid,
|
|
||||||
struct sc_hid_close *hid_close,
|
|
||||||
uint32_t gamepad_id);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_gamepad_generate_input_from_button(struct sc_hid_gamepad *hid,
|
|
||||||
struct sc_hid_input *hid_input,
|
|
||||||
const struct sc_gamepad_button_event *event);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid,
|
|
||||||
struct sc_hid_input *hid_input,
|
|
||||||
const struct sc_gamepad_axis_event *event);
|
|
||||||
|
|
||||||
#endif
|
|
@ -1,344 +0,0 @@
|
|||||||
#include "hid_keyboard.h"
|
|
||||||
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
#define SC_HID_MOD_NONE 0x00
|
|
||||||
#define SC_HID_MOD_LEFT_CONTROL (1 << 0)
|
|
||||||
#define SC_HID_MOD_LEFT_SHIFT (1 << 1)
|
|
||||||
#define SC_HID_MOD_LEFT_ALT (1 << 2)
|
|
||||||
#define SC_HID_MOD_LEFT_GUI (1 << 3)
|
|
||||||
#define SC_HID_MOD_RIGHT_CONTROL (1 << 4)
|
|
||||||
#define SC_HID_MOD_RIGHT_SHIFT (1 << 5)
|
|
||||||
#define SC_HID_MOD_RIGHT_ALT (1 << 6)
|
|
||||||
#define SC_HID_MOD_RIGHT_GUI (1 << 7)
|
|
||||||
|
|
||||||
#define SC_HID_KEYBOARD_INDEX_MODS 0
|
|
||||||
#define SC_HID_KEYBOARD_INDEX_KEYS 2
|
|
||||||
|
|
||||||
// USB HID protocol says 6 keys in an event is the requirement for BIOS
|
|
||||||
// keyboard support, though OS could support more keys via modifying the report
|
|
||||||
// desc. 6 should be enough for scrcpy.
|
|
||||||
#define SC_HID_KEYBOARD_MAX_KEYS 6
|
|
||||||
#define SC_HID_KEYBOARD_INPUT_SIZE \
|
|
||||||
(SC_HID_KEYBOARD_INDEX_KEYS + SC_HID_KEYBOARD_MAX_KEYS)
|
|
||||||
|
|
||||||
#define SC_HID_RESERVED 0x00
|
|
||||||
#define SC_HID_ERROR_ROLL_OVER 0x01
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For HID, only report descriptor is needed.
|
|
||||||
*
|
|
||||||
* The specification is available here:
|
|
||||||
* <https://www.usb.org/document-library/device-class-definition-hid-111>
|
|
||||||
*
|
|
||||||
* In particular, read:
|
|
||||||
* - §6.2.2 Report Descriptor
|
|
||||||
* - Appendix B.1 Protocol 1 (Keyboard)
|
|
||||||
* - Appendix C: Keyboard Implementation
|
|
||||||
*
|
|
||||||
* The HID Usage Tables is also useful:
|
|
||||||
* <https://www.usb.org/document-library/hid-usage-tables-15>
|
|
||||||
*
|
|
||||||
* Normally a basic HID keyboard uses 8 bytes:
|
|
||||||
* Modifier Reserved Key Key Key Key Key Key
|
|
||||||
*
|
|
||||||
* You can dump your device's report descriptor with:
|
|
||||||
*
|
|
||||||
* sudo usbhid-dump -m vid:pid -e descriptor
|
|
||||||
*
|
|
||||||
* (change vid:pid' to your device's vendor ID and product ID).
|
|
||||||
*/
|
|
||||||
static const uint8_t SC_HID_KEYBOARD_REPORT_DESC[] = {
|
|
||||||
// Usage Page (Generic Desktop)
|
|
||||||
0x05, 0x01,
|
|
||||||
// Usage (Keyboard)
|
|
||||||
0x09, 0x06,
|
|
||||||
|
|
||||||
// Collection (Application)
|
|
||||||
0xA1, 0x01,
|
|
||||||
|
|
||||||
// Usage Page (Key Codes)
|
|
||||||
0x05, 0x07,
|
|
||||||
// Usage Minimum (224)
|
|
||||||
0x19, 0xE0,
|
|
||||||
// Usage Maximum (231)
|
|
||||||
0x29, 0xE7,
|
|
||||||
// Logical Minimum (0)
|
|
||||||
0x15, 0x00,
|
|
||||||
// Logical Maximum (1)
|
|
||||||
0x25, 0x01,
|
|
||||||
// Report Size (1)
|
|
||||||
0x75, 0x01,
|
|
||||||
// Report Count (8)
|
|
||||||
0x95, 0x08,
|
|
||||||
// Input (Data, Variable, Absolute): Modifier byte
|
|
||||||
0x81, 0x02,
|
|
||||||
|
|
||||||
// Report Size (8)
|
|
||||||
0x75, 0x08,
|
|
||||||
// Report Count (1)
|
|
||||||
0x95, 0x01,
|
|
||||||
// Input (Constant): Reserved byte
|
|
||||||
0x81, 0x01,
|
|
||||||
|
|
||||||
// Usage Page (LEDs)
|
|
||||||
0x05, 0x08,
|
|
||||||
// Usage Minimum (1)
|
|
||||||
0x19, 0x01,
|
|
||||||
// Usage Maximum (5)
|
|
||||||
0x29, 0x05,
|
|
||||||
// Report Size (1)
|
|
||||||
0x75, 0x01,
|
|
||||||
// Report Count (5)
|
|
||||||
0x95, 0x05,
|
|
||||||
// Output (Data, Variable, Absolute): LED report
|
|
||||||
0x91, 0x02,
|
|
||||||
|
|
||||||
// Report Size (3)
|
|
||||||
0x75, 0x03,
|
|
||||||
// Report Count (1)
|
|
||||||
0x95, 0x01,
|
|
||||||
// Output (Constant): LED report padding
|
|
||||||
0x91, 0x01,
|
|
||||||
|
|
||||||
// Usage Page (Key Codes)
|
|
||||||
0x05, 0x07,
|
|
||||||
// Usage Minimum (0)
|
|
||||||
0x19, 0x00,
|
|
||||||
// Usage Maximum (101)
|
|
||||||
0x29, SC_HID_KEYBOARD_KEYS - 1,
|
|
||||||
// Logical Minimum (0)
|
|
||||||
0x15, 0x00,
|
|
||||||
// Logical Maximum(101)
|
|
||||||
0x25, SC_HID_KEYBOARD_KEYS - 1,
|
|
||||||
// Report Size (8)
|
|
||||||
0x75, 0x08,
|
|
||||||
// Report Count (6)
|
|
||||||
0x95, SC_HID_KEYBOARD_MAX_KEYS,
|
|
||||||
// Input (Data, Array): Keys
|
|
||||||
0x81, 0x00,
|
|
||||||
|
|
||||||
// End Collection
|
|
||||||
0xC0
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A keyboard HID input report is 8 bytes long:
|
|
||||||
*
|
|
||||||
* - byte 0: modifiers (1 flag per modifier key, 8 possible modifier keys)
|
|
||||||
* - byte 1: reserved (always 0)
|
|
||||||
* - bytes 2 to 7: pressed keys (6 at most)
|
|
||||||
*
|
|
||||||
* 7 6 5 4 3 2 1 0
|
|
||||||
* +---------------+
|
|
||||||
* byte 0: |. . . . . . . .| modifiers
|
|
||||||
* +---------------+
|
|
||||||
* ^ ^ ^ ^ ^ ^ ^ ^
|
|
||||||
* | | | | | | | `- left Ctrl
|
|
||||||
* | | | | | | `--- left Shift
|
|
||||||
* | | | | | `----- left Alt
|
|
||||||
* | | | | `------- left Gui
|
|
||||||
* | | | `--------- right Ctrl
|
|
||||||
* | | `----------- right Shift
|
|
||||||
* | `------------- right Alt
|
|
||||||
* `--------------- right Gui
|
|
||||||
*
|
|
||||||
* +---------------+
|
|
||||||
* byte 1: |0 0 0 0 0 0 0 0| reserved
|
|
||||||
* +---------------+
|
|
||||||
*
|
|
||||||
* +---------------+
|
|
||||||
* bytes 2 to 7: |. . . . . . . .| scancode of 1st key pressed
|
|
||||||
* +---------------+
|
|
||||||
* |. . . . . . . .| scancode of 2nd key pressed
|
|
||||||
* +---------------+
|
|
||||||
* |. . . . . . . .| scancode of 3rd key pressed
|
|
||||||
* +---------------+
|
|
||||||
* |. . . . . . . .| scancode of 4th key pressed
|
|
||||||
* +---------------+
|
|
||||||
* |. . . . . . . .| scancode of 5th key pressed
|
|
||||||
* +---------------+
|
|
||||||
* |. . . . . . . .| scancode of 6th key pressed
|
|
||||||
* +---------------+
|
|
||||||
*
|
|
||||||
* If there are less than 6 keys pressed, the last items are set to 0.
|
|
||||||
* For example, if A and W are pressed:
|
|
||||||
*
|
|
||||||
* +---------------+
|
|
||||||
* bytes 2 to 7: |0 0 0 0 0 1 0 0| A is pressed (scancode = 4)
|
|
||||||
* +---------------+
|
|
||||||
* |0 0 0 1 1 0 1 0| W is pressed (scancode = 26)
|
|
||||||
* +---------------+
|
|
||||||
* |0 0 0 0 0 0 0 0| ^
|
|
||||||
* +---------------+ | only 2 keys are pressed, the
|
|
||||||
* |0 0 0 0 0 0 0 0| | remaining items are set to 0
|
|
||||||
* +---------------+ |
|
|
||||||
* |0 0 0 0 0 0 0 0| |
|
|
||||||
* +---------------+ |
|
|
||||||
* |0 0 0 0 0 0 0 0| v
|
|
||||||
* +---------------+
|
|
||||||
*
|
|
||||||
* Pressing more than 6 keys is not supported. If this happens (typically,
|
|
||||||
* never in practice), report a "phantom state":
|
|
||||||
*
|
|
||||||
* +---------------+
|
|
||||||
* bytes 2 to 7: |0 0 0 0 0 0 0 1| ^
|
|
||||||
* +---------------+ |
|
|
||||||
* |0 0 0 0 0 0 0 1| | more than 6 keys pressed:
|
|
||||||
* +---------------+ | the list is filled with a special
|
|
||||||
* |0 0 0 0 0 0 0 1| | rollover error code (0x01)
|
|
||||||
* +---------------+ |
|
|
||||||
* |0 0 0 0 0 0 0 1| |
|
|
||||||
* +---------------+ |
|
|
||||||
* |0 0 0 0 0 0 0 1| |
|
|
||||||
* +---------------+ |
|
|
||||||
* |0 0 0 0 0 0 0 1| v
|
|
||||||
* +---------------+
|
|
||||||
*/
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_hid_keyboard_input_init(struct sc_hid_input *hid_input) {
|
|
||||||
hid_input->hid_id = SC_HID_ID_KEYBOARD;
|
|
||||||
hid_input->size = SC_HID_KEYBOARD_INPUT_SIZE;
|
|
||||||
|
|
||||||
uint8_t *data = hid_input->data;
|
|
||||||
|
|
||||||
data[SC_HID_KEYBOARD_INDEX_MODS] = SC_HID_MOD_NONE;
|
|
||||||
data[1] = SC_HID_RESERVED;
|
|
||||||
memset(&data[SC_HID_KEYBOARD_INDEX_KEYS], 0, SC_HID_KEYBOARD_MAX_KEYS);
|
|
||||||
}
|
|
||||||
|
|
||||||
static uint16_t
|
|
||||||
sc_hid_mod_from_sdl_keymod(uint16_t mod) {
|
|
||||||
uint16_t mods = SC_HID_MOD_NONE;
|
|
||||||
if (mod & SC_MOD_LCTRL) {
|
|
||||||
mods |= SC_HID_MOD_LEFT_CONTROL;
|
|
||||||
}
|
|
||||||
if (mod & SC_MOD_LSHIFT) {
|
|
||||||
mods |= SC_HID_MOD_LEFT_SHIFT;
|
|
||||||
}
|
|
||||||
if (mod & SC_MOD_LALT) {
|
|
||||||
mods |= SC_HID_MOD_LEFT_ALT;
|
|
||||||
}
|
|
||||||
if (mod & SC_MOD_LGUI) {
|
|
||||||
mods |= SC_HID_MOD_LEFT_GUI;
|
|
||||||
}
|
|
||||||
if (mod & SC_MOD_RCTRL) {
|
|
||||||
mods |= SC_HID_MOD_RIGHT_CONTROL;
|
|
||||||
}
|
|
||||||
if (mod & SC_MOD_RSHIFT) {
|
|
||||||
mods |= SC_HID_MOD_RIGHT_SHIFT;
|
|
||||||
}
|
|
||||||
if (mod & SC_MOD_RALT) {
|
|
||||||
mods |= SC_HID_MOD_RIGHT_ALT;
|
|
||||||
}
|
|
||||||
if (mod & SC_MOD_RGUI) {
|
|
||||||
mods |= SC_HID_MOD_RIGHT_GUI;
|
|
||||||
}
|
|
||||||
return mods;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_hid_keyboard_init(struct sc_hid_keyboard *hid) {
|
|
||||||
memset(hid->keys, false, SC_HID_KEYBOARD_KEYS);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool
|
|
||||||
scancode_is_modifier(enum sc_scancode scancode) {
|
|
||||||
return scancode >= SC_SCANCODE_LCTRL && scancode <= SC_SCANCODE_RGUI;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid,
|
|
||||||
struct sc_hid_input *hid_input,
|
|
||||||
const struct sc_key_event *event) {
|
|
||||||
enum sc_scancode scancode = event->scancode;
|
|
||||||
assert(scancode >= 0);
|
|
||||||
|
|
||||||
// SDL also generates events when only modifiers are pressed, we cannot
|
|
||||||
// ignore them totally, for example press 'a' first then press 'Control',
|
|
||||||
// if we ignore 'Control' event, only 'a' is sent.
|
|
||||||
if (scancode >= SC_HID_KEYBOARD_KEYS && !scancode_is_modifier(scancode)) {
|
|
||||||
// Scancode to ignore
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_hid_keyboard_input_init(hid_input);
|
|
||||||
|
|
||||||
uint16_t mods = sc_hid_mod_from_sdl_keymod(event->mods_state);
|
|
||||||
|
|
||||||
if (scancode < SC_HID_KEYBOARD_KEYS) {
|
|
||||||
// Pressed is true and released is false
|
|
||||||
hid->keys[scancode] = (event->action == SC_ACTION_DOWN);
|
|
||||||
LOGV("keys[%02x] = %s", scancode,
|
|
||||||
hid->keys[scancode] ? "true" : "false");
|
|
||||||
}
|
|
||||||
|
|
||||||
hid_input->data[SC_HID_KEYBOARD_INDEX_MODS] = mods;
|
|
||||||
|
|
||||||
uint8_t *keys_data = &hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS];
|
|
||||||
// Re-calculate pressed keys every time
|
|
||||||
int keys_pressed_count = 0;
|
|
||||||
for (int i = 0; i < SC_HID_KEYBOARD_KEYS; ++i) {
|
|
||||||
if (hid->keys[i]) {
|
|
||||||
// USB HID protocol says that if keys exceeds report count, a
|
|
||||||
// phantom state should be reported
|
|
||||||
if (keys_pressed_count >= SC_HID_KEYBOARD_MAX_KEYS) {
|
|
||||||
// Phantom state:
|
|
||||||
// - Modifiers
|
|
||||||
// - Reserved
|
|
||||||
// - ErrorRollOver * HID_MAX_KEYS
|
|
||||||
memset(keys_data, SC_HID_ERROR_ROLL_OVER,
|
|
||||||
SC_HID_KEYBOARD_MAX_KEYS);
|
|
||||||
goto end;
|
|
||||||
}
|
|
||||||
|
|
||||||
keys_data[keys_pressed_count] = i;
|
|
||||||
++keys_pressed_count;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
end:
|
|
||||||
LOGV("hid keyboard: key %-4s scancode=%02x (%u) mod=%02x",
|
|
||||||
event->action == SC_ACTION_DOWN ? "down" : "up", event->scancode,
|
|
||||||
event->scancode, mods);
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
|
|
||||||
uint16_t mods_state) {
|
|
||||||
bool capslock = mods_state & SC_MOD_CAPS;
|
|
||||||
bool numlock = mods_state & SC_MOD_NUM;
|
|
||||||
if (!capslock && !numlock) {
|
|
||||||
// Nothing to do
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_hid_keyboard_input_init(hid_input);
|
|
||||||
|
|
||||||
unsigned i = 0;
|
|
||||||
if (capslock) {
|
|
||||||
hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_CAPSLOCK;
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
if (numlock) {
|
|
||||||
hid_input->data[SC_HID_KEYBOARD_INDEX_KEYS + i] = SC_SCANCODE_NUMLOCK;
|
|
||||||
++i;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) {
|
|
||||||
hid_open->hid_id = SC_HID_ID_KEYBOARD;
|
|
||||||
hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC;
|
|
||||||
hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC);
|
|
||||||
}
|
|
||||||
|
|
||||||
void sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close) {
|
|
||||||
hid_close->hid_id = SC_HID_ID_KEYBOARD;
|
|
||||||
}
|
|
@ -1,53 +0,0 @@
|
|||||||
#ifndef SC_HID_KEYBOARD_H
|
|
||||||
#define SC_HID_KEYBOARD_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
|
|
||||||
#include "hid/hid_event.h"
|
|
||||||
#include "input_events.h"
|
|
||||||
|
|
||||||
// See "SDL2/SDL_scancode.h".
|
|
||||||
// Maybe SDL_Keycode is used by most people, but SDL_Scancode is taken from USB
|
|
||||||
// HID protocol.
|
|
||||||
// 0x65 is Application, typically AT-101 Keyboard ends here.
|
|
||||||
#define SC_HID_KEYBOARD_KEYS 0x66
|
|
||||||
|
|
||||||
#define SC_HID_ID_KEYBOARD 1
|
|
||||||
|
|
||||||
/**
|
|
||||||
* HID keyboard events are sequence-based, every time keyboard state changes
|
|
||||||
* it sends an array of currently pressed keys, the host is responsible for
|
|
||||||
* compare events and determine which key becomes pressed and which key becomes
|
|
||||||
* released. In order to convert SDL_KeyboardEvent to HID events, we first use
|
|
||||||
* an array of keys to save each keys' state. And when a SDL_KeyboardEvent was
|
|
||||||
* emitted, we updated our state, and then we use a loop to generate HID
|
|
||||||
* events. The sequence of array elements is unimportant and when too much keys
|
|
||||||
* pressed at the same time (more than report count), we should generate
|
|
||||||
* phantom state. Don't forget that modifiers should be updated too, even for
|
|
||||||
* phantom state.
|
|
||||||
*/
|
|
||||||
struct sc_hid_keyboard {
|
|
||||||
bool keys[SC_HID_KEYBOARD_KEYS];
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_hid_keyboard_init(struct sc_hid_keyboard *hid);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_hid_keyboard_generate_close(struct sc_hid_close *hid_close);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_keyboard_generate_input_from_key(struct sc_hid_keyboard *hid,
|
|
||||||
struct sc_hid_input *hid_input,
|
|
||||||
const struct sc_key_event *event);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input,
|
|
||||||
uint16_t mods_state);
|
|
||||||
|
|
||||||
#endif
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user