Compare commits

..

5 Commits

Author SHA1 Message Date
Romain Vimont
6eabe5a8bc Improve portable builds
In portable builds, scrcpy-server.jar was supposed to be present in the
current directory, so in practice it worked only if scrcpy was launched
from its own directory.

Instead, find the absolute path of the executable and build a suitable
path to use scrcpy-server.jar from the same directory.
2019-06-10 16:08:00 +02:00
Romain Vimont
9988d6186e Add functions to convert wide char to UTF-8
There was already utf8_to_wide_char(), used to correctly execute
commands on Windows.

Add the reverse converter: utf8_from_wide_char(). We will need it to
build the scrcpy-server path based on the executable directory.
2019-06-10 16:06:32 +02:00
Romain Vimont
451f3ed309 Extract "scrcpy-server.jar" string
The filename is used at several places.
2019-06-10 16:06:32 +02:00
Romain Vimont
5142e0e621 Simplify portable build configuration
To create a portable build (with scrcpy-server.jar accessible from the
scrcpy directory), replace OVERRIDE_SERVER_PATH by a simple compilation
flag: PORTABLE.

This paves the way to use more complex rules to determine the path of
scrcpy-server.jar in portable builds.
2019-06-10 16:02:57 +02:00
Romain Vimont
e9bacc196a Simplify scrcpy-server path configuration
The full path of scrcpy-server.jar was partially configured from
meson.build then concatenated by C code.

Instead, directly write the path in C.
2019-06-10 15:52:54 +02:00
38 changed files with 402 additions and 860 deletions

View File

@@ -12,7 +12,7 @@ case, use the [prebuilt server] (so you will not need Java or the Android SDK).
## Requirements
You need [adb]. It is available in the [Android SDK platform
tools][platform-tools], or packaged in your distribution (`adb`).
tools][platform-tools], or packaged in your distribution (`android-adb-tools`).
On Windows, download the [platform-tools][platform-tools-windows] and extract
the following files to a directory accessible from your `PATH`:
@@ -40,10 +40,10 @@ Install the required packages from your package manager.
```bash
# runtime dependencies
sudo apt install ffmpeg libsdl2-2.0-0
sudo apt install ffmpeg libsdl2-2.0.0
# client build dependencies
sudo apt install make gcc git pkg-config meson ninja-build \
sudo apt install make gcc pkg-config meson ninja-build \
libavcodec-dev libavformat-dev libavutil-dev \
libsdl2-dev
@@ -70,7 +70,7 @@ sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-rele
sudo dnf install SDL2-devel ffms2-devel meson gcc make
# server build dependencies
sudo dnf install java-devel
sudo dnf install java
```
@@ -234,10 +234,10 @@ You can then [run](README.md#run) _scrcpy_.
## Prebuilt server
- [`scrcpy-server-v1.9.jar`][direct-scrcpy-server]
_(SHA-256: ad7e539f100e48259b646f26982bc63e0a60a81ac87ae135e242855bef69bd1a)_
- [`scrcpy-server-v1.8.jar`][direct-scrcpy-server]
_(SHA-256: 839055ef905903bf98ead1b9b8a127fe402b39ad657a81f9a914b2dbcb2ce5c0)_
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-server-v1.9.jar
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-server-v1.8.jar
Download the prebuilt server somewhere, and specify its path during the Meson
configuration:

View File

@@ -32,7 +32,7 @@ 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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123
[main]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/Server.java#L100
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
@@ -65,18 +65,18 @@ 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/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view
[wrappers]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/aidl/android/view
### Threading
The server uses 3 threads:
The server uses 2 threads:
- the **main** thread, encoding and streaming the video to the client;
- the **controller** thread, listening for _control messages_ (typically,
keyboard and mouse events) from the client;
- the **receiver** thread (managed by the controller), sending _device messges_
- the **controller** thread, listening for _control events_ (typically,
keyboard and mouse events) from the client.
- the **receiver** thread (managed by the controller), sending _device events_
to the clients (currently, it is only used to send the device clipboard
content).
@@ -92,9 +92,9 @@ 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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/v1.8/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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69
[surface]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L69-L70
On device [rotation], the codec, surface and display are reinitialized, and a
new video stream is produced.
@@ -108,30 +108,31 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90
[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[rotation]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90
[repeat]:
https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
### Input events injection
_Control messages_ are received from the client by the [`Controller`] (run in a
separate thread). There are several types of input events:
_Control events_ are received from the client by the [`EventController`] (run in
a separate thread). There are several types of input events:
- keycode (cf [`KeyEvent`]),
- text (special characters may not be handled by keycodes directly),
- mouse motion/click,
- mouse scroll,
- other commands (e.g. to switch the screen on or to copy the clipboard).
Some of them need to inject input events to the system. To do so, they use the
_hidden_ method [`InputManager.injectInputEvent`] (exposed by our
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]).
[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81
[`EventController`]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/EventController.java#L66
[`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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/v1.8/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@@ -187,13 +188,12 @@ The client uses 4 threads:
- the **main** thread, executing the SDL event loop,
- the **stream** thread, receiving the video and used for decoding and
recording,
- the **controller** thread, sending _control messages_ to the server,
- the **controller** thread, sending _control events_ to the server.
- the **receiver** thread (managed by the controller), receiving _device
messages_ from the client.
events_ from the client.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window) or to
print the framerate regularly in the console.
installation or file push requests (via drag&drop on the main window).
@@ -217,10 +217,10 @@ to decode a new frame while the main thread renders the last one.
If a [recorder] is present (i.e. `--record` is enabled), then its muxes the raw
H.264 packet to the output video file.
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h
[stream]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/recorder.h
```
+----------+ +----------+
@@ -234,19 +234,19 @@ H.264 packet to the output video file.
### Controller
The [controller] is responsible to send _control messages_ to the device. It
runs in a separate thread, to avoid I/O on the main thread.
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 messages_][controlmsg]. It is responsible to
creates appropriate [_control events_][controlevent]. It is responsible to
convert SDL events to Android events (using [convert]). It pushes the _control
messages_ to a queue hold by the controller. On its own thread, the controller
takes messages from the queue, that it serializes and sends to the client.
events_ to a 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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h
[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h
[controller]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/controller.h
[controlevent]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/control_event.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/convert.h
### UI and event loop
@@ -257,9 +257,10 @@ 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/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c
[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201
[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c
[event loop]:
https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/scrcpy.c#L187
[screen]: https://github.com/Genymobile/scrcpy/blob/v1.8/app/src/screen.h
## Hack

4
FAQ.md
View File

@@ -19,6 +19,10 @@ Windows may need some [drivers] to detect your device.
[drivers]: https://developer.android.com/studio/run/oem-usb.html
If you still encounter problems, please see [issue 9].
[issue 9]: https://github.com/Genymobile/scrcpy/issues/9
### Mouse clicks do not work

View File

@@ -188,7 +188,6 @@
identification within third-party archives.
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -44,7 +44,7 @@ clean:
build-server:
[ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \
meson "$(SERVER_BUILD_DIR)" \
--buildtype release -Dcompile_app=false )
--buildtype release -Dbuild_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps-win32:
@@ -56,7 +56,7 @@ build-win32: prepare-deps-win32
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dbuild_server=false \
-Dportable=true )
ninja -C "$(WIN32_BUILD_DIR)"
@@ -66,7 +66,7 @@ build-win32-noconsole: prepare-deps-win32
--cross-file cross_win32.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
ninja -C "$(WIN32_NOCONSOLE_BUILD_DIR)"
@@ -80,7 +80,7 @@ build-win64: prepare-deps-win64
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dbuild_server=false \
-Dportable=true )
ninja -C "$(WIN64_BUILD_DIR)"
@@ -90,7 +90,7 @@ build-win64-noconsole: prepare-deps-win64
--cross-file cross_win64.txt \
--buildtype release --strip -Db_lto=true \
-Dcrossbuild_windows=true \
-Dcompile_server=false \
-Dbuild_server=false \
-Dwindows_noconsole=true \
-Dportable=true )
ninja -C "$(WIN64_NOCONSOLE_BUILD_DIR)"
@@ -100,29 +100,28 @@ dist-win32: build-server build-win32 build-win32-noconsole
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win32-shared/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.9/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 build-win64-noconsole
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server.jar "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_NOCONSOLE_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/scrcpy-noconsole.exe"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1.3-win64-shared/bin/swscale-5.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avutil-56.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avcodec-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/avformat-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/ffmpeg-4.1-win64-shared/bin/swresample-3.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/platform-tools/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.8/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp prebuilt-deps/SDL2-2.0.9/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \

101
README.md
View File

@@ -1,15 +1,15 @@
# scrcpy (v1.9)
# scrcpy (v1.8)
This application provides display and control of Android devices connected on
USB (or [over TCP/IP][article-tcpip]). It does not require any _root_ access.
It works on _GNU/Linux_, _Windows_ and _macOS_.
It works on _GNU/Linux_, _Windows_ and _MacOS_.
![screenshot](assets/screenshot-debian-600.jpg)
## Requirements
The Android device requires at least API 21 (Android 5.0).
The Android part requires at least API 21 (Android 5.0).
Make sure you [enabled adb debugging][enable-adb] on your device(s).
@@ -29,12 +29,6 @@ control it using keyboard and mouse.
On Linux, you typically need to [build the app manually][BUILD]. Don't worry,
it's not that hard.
A [Snap] package is available: [`scrcpy`][snap-link].
[snap-link]: https://snapstats.org/snaps/scrcpy
[snap]: https://en.wikipedia.org/wiki/Snappy_(package_manager)
For Arch Linux, an [AUR] package is available: [`scrcpy`][aur-link].
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
@@ -51,18 +45,18 @@ For Gentoo, an [Ebuild] is available: [`scrcpy/`][ebuild-link].
For Windows, for simplicity, prebuilt archives with all the dependencies
(including `adb`) are available:
- [`scrcpy-win32-v1.9.zip`][direct-win32]
_(SHA-256: 3234f7fbcc26b9e399f50b5ca9ed085708954c87fda1b0dd32719d6e7dd861ef)_
- [`scrcpy-win64-v1.9.zip`][direct-win64]
_(SHA-256: 0088eca1811ea7c7ac350d636c8465b266e6c830bb268770ff88fddbb493077e)_
- [`scrcpy-win32-v1.8.zip`][direct-win32]
_(SHA-256: c0c29ed1c66deaa73bdadacd09e598aafb3a117929cf7a314cce1cc45e34de53)_
- [`scrcpy-win64-v1.8.zip`][direct-win64]
_(SHA-256: 9cc980d07bd8f036ae4e91d0bc6fc3281d7fa8f9752d4913b643c0fb72a19fb7)_
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win32-v1.9.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.9/scrcpy-win64-v1.9.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win32-v1.8.zip
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.8/scrcpy-win64-v1.8.zip
You can also [build the app manually][BUILD].
### macOS
### Mac OS
The application is available in [Homebrew]. Just install it:
@@ -101,22 +95,22 @@ scrcpy --help
### Reduce size
Sometimes, it is useful to mirror an Android device at a lower definition to
increase performance.
increase performances.
To limit both the width and height to some value (e.g. 1024):
To limit both width and height to some value (e.g. 1024):
```bash
scrcpy --max-size 1024
scrcpy -m 1024 # short version
```
The other dimension is computed to that the device aspect ratio is preserved.
The other dimension is computed to that the device aspect-ratio is preserved.
That way, a device in 1920×1080 will be mirrored at 1024×576.
### Change bit-rate
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
The default bit-rate is 8Mbps. To change the video bitrate (e.g. to 2Mbps):
```bash
scrcpy --bit-rate 2M
@@ -128,7 +122,7 @@ scrcpy -b 2M # short version
The device screen may be cropped to mirror only part of the screen.
This is useful for example to mirror only one eye of the Oculus Go:
This is useful for example to mirror only 1 eye of the Oculus Go:
```bash
scrcpy --crop 1224:1440:0:0 # 1224x1440 at offset (0,0)
@@ -251,11 +245,6 @@ _scrcpy_ window.
There is no visual feedback, a log is printed to the console.
The target directory can be changed on start:
```bash
scrcpy --push-target /sdcard/foo/bar/
```
### Read-only
@@ -294,47 +283,42 @@ latency), use:
scrcpy --render-expired-frames
```
### Custom window title
By default, the window title is the device model. It can be changed:
```bash
scrcpy --window-title 'My device'
```
### Forward audio
Audio is not forwarded by _scrcpy_. Use [USBaudio] (Linux-only).
Audio is not forwarded by _scrcpy_.
Also see [issue #14].
There is a limited solution using [AOA], implemented in the [`audio`] branch. If
you are interested, see [issue 14].
[USBaudio]: https://github.com/rom1v/usbaudio
[issue #14]: https://github.com/Genymobile/scrcpy/issues/14
[AOA]: https://source.android.com/devices/accessories/aoa2
[`audio`]: https://github.com/Genymobile/scrcpy/commits/audio
[issue 14]: https://github.com/Genymobile/scrcpy/issues/14
## Shortcuts
| Action | Shortcut |
| -------------------------------------- |:---------------------------- |
| Switch fullscreen mode | `Ctrl`+`f` |
| 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)_ (`Cmd`+`↑` on macOS) |
| Click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on macOS) |
| Click on `POWER` | `Ctrl`+`p` |
| Power on | _Right-click²_ |
| Turn device screen off (keep mirroring)| `Ctrl`+`o` |
| Expand notification panel | `Ctrl`+`n` |
| Collapse notification panel | `Ctrl`+`Shift`+`n` |
| Copy device clipboard to computer | `Ctrl`+`c` |
| Paste computer clipboard to device | `Ctrl`+`v` |
| Copy computer clipboard to device | `Ctrl`+`Shift`+`v` |
| Enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
| switch fullscreen mode | `Ctrl`+`f` |
| 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)_ (`Cmd`+`↑` on MacOS) |
| click on `VOLUME_DOWN` | `Ctrl`+`↓` _(down)_ (`Cmd`+`↓` on MacOS) |
| click on `POWER` | `Ctrl`+`p` |
| power on | _Right-click²_ |
| turn device screen off (keep mirroring)| `Ctrl`+`o` |
| expand notification panel | `Ctrl`+`n` |
| collapse notification panel | `Ctrl`+`Shift`+`n` |
| copy device clipboard to computer | `Ctrl`+`c` |
| paste computer clipboard to device | `Ctrl`+`v` |
| copy computer clipboard to device | `Ctrl`+`Shift+`v` |
| enable/disable FPS counter (on stdout) | `Ctrl`+`i` |
_¹Double-click on black borders to remove them._
_²Right-click turns the screen on if it was off, presses BACK otherwise._
@@ -347,8 +331,8 @@ To use a specific _adb_ binary, configure its path in the environment variable
ADB=/path/to/adb scrcpy
To override the path of the `scrcpy-server.jar` file, configure its path in
`SCRCPY_SERVER_PATH`.
To override the path of the `scrcpy-server.jar` file (it can be [useful] on
Windows), configure its path in `SCRCPY_SERVER_PATH`.
[useful]: https://github.com/Genymobile/scrcpy/issues/278#issuecomment-429330345
@@ -385,7 +369,6 @@ Read the [developers page].
## Licence
Copyright (C) 2018 Genymobile
Copyright (C) 2018-2019 Romain Vimont
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.

View File

@@ -1,6 +1,5 @@
src = [
'src/main.c',
'src/buffered_reader.c',
'src/command.c',
'src/control_msg.c',
'src/controller.c',

View File

@@ -1,72 +0,0 @@
#include "buffered_reader.h"
#include <SDL2/SDL_assert.h>
#include "log.h"
bool
buffered_reader_init(struct buffered_reader *reader, socket_t socket,
size_t bufsize) {
reader->buf = SDL_malloc(bufsize);
if (!reader->buf) {
LOGC("Could not allocate buffer");
return false;
}
reader->socket = socket;
reader->bufsize = bufsize;
reader->offset = 0;
reader->len = 0;
return true;
}
void
buffered_reader_destroy(struct buffered_reader *reader) {
SDL_free(reader->buf);
}
static ssize_t
buffered_reader_fill(struct buffered_reader *reader) {
SDL_assert(!reader->len);
ssize_t r = net_recv(reader->socket, reader->buf, reader->bufsize);
if (r > 0) {
reader->offset = 0;
reader->len = r;
}
return r;
}
ssize_t
buffered_reader_recv(struct buffered_reader *reader, void *buf, size_t count) {
if (!reader->len) {
// read from the socket
ssize_t r = buffered_reader_fill(reader);
if (r <= 0) {
return r;
}
}
size_t r = count < reader->len ? count : reader->len;
memcpy(buf, reader->buf + reader->offset, r);
reader->offset += r;
reader->len -= r;
return r;
}
ssize_t
buffered_reader_recv_all(struct buffered_reader *reader, void *buf,
size_t count) {
size_t done = 0;
while (done < count) {
ssize_t r = buffered_reader_recv(reader, buf, count - done);
if (r <= 0) {
// if there was some data, return them immediately
return done ? done : r;
}
done += r;
buf += r;
}
return done;
}

View File

@@ -1,29 +0,0 @@
#ifndef BUFFERED_READER_H
#define BUFFERED_READER_H
#include "common.h"
#include "net.h"
struct buffered_reader {
socket_t socket;
void *buf;
size_t bufsize;
size_t offset;
size_t len;
};
bool
buffered_reader_init(struct buffered_reader *reader, socket_t socket,
size_t bufsize);
void
buffered_reader_destroy(struct buffered_reader *reader);
ssize_t
buffered_reader_recv(struct buffered_reader *reader, void *buf, size_t count);
ssize_t
buffered_reader_recv_all(struct buffered_reader *reader, void *buf,
size_t count);
#endif

View File

@@ -91,7 +91,7 @@ run_controller(void *data) {
bool ok = process_msg(controller, &msg);
control_msg_destroy(&msg);
if (!ok) {
LOGD("Could not write msg to socket");
LOGD("Cannot write msg to socket");
break;
}
}

View File

@@ -7,6 +7,7 @@
#include "net.h"
#define DEVICE_NAME_FIELD_LENGTH 64
#define DEVICE_SDCARD_PATH "/sdcard/"
// name must be at least DEVICE_NAME_FIELD_LENGTH bytes
bool

View File

@@ -5,19 +5,17 @@
#include "config.h"
#include "command.h"
#include "device.h"
#include "lock_util.h"
#include "log.h"
#define DEFAULT_PUSH_TARGET "/sdcard/"
static void
file_handler_request_destroy(struct file_handler_request *req) {
SDL_free(req->file);
}
bool
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target) {
file_handler_init(struct file_handler *file_handler, const char *serial) {
cbuf_init(&file_handler->queue);
@@ -33,7 +31,7 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
if (serial) {
file_handler->serial = SDL_strdup(serial);
if (!file_handler->serial) {
LOGW("Could not strdup serial");
LOGW("Cannot strdup serial");
SDL_DestroyCond(file_handler->event_cond);
SDL_DestroyMutex(file_handler->mutex);
return false;
@@ -48,8 +46,6 @@ file_handler_init(struct file_handler *file_handler, const char *serial,
file_handler->stopped = false;
file_handler->current_process = PROCESS_NONE;
file_handler->push_target = push_target ? push_target : DEFAULT_PUSH_TARGET;
return true;
}
@@ -71,8 +67,8 @@ install_apk(const char *serial, const char *file) {
}
static process_t
push_file(const char *serial, const char *file, const char *push_target) {
return adb_push(serial, file, push_target);
push_file(const char *serial, const char *file) {
return adb_push(serial, file, DEVICE_SDCARD_PATH);
}
bool
@@ -128,8 +124,7 @@ run_file_handler(void *data) {
process = install_apk(file_handler->serial, req.file);
} else {
LOGI("Pushing %s...", req.file);
process = push_file(file_handler->serial, req.file,
file_handler->push_target);
process = push_file(file_handler->serial, req.file);
}
file_handler->current_process = process;
mutex_unlock(file_handler->mutex);
@@ -142,11 +137,9 @@ run_file_handler(void *data) {
}
} else {
if (process_check_success(process, "adb push")) {
LOGI("%s successfully pushed to %s", req.file,
file_handler->push_target);
LOGI("%s successfully pushed to /sdcard/", req.file);
} else {
LOGE("Failed to push %s to %s", req.file,
file_handler->push_target);
LOGE("Failed to push %s to /sdcard/", req.file);
}
}
@@ -176,7 +169,7 @@ file_handler_stop(struct file_handler *file_handler) {
cond_signal(file_handler->event_cond);
if (file_handler->current_process != PROCESS_NONE) {
if (!cmd_terminate(file_handler->current_process)) {
LOGW("Could not terminate install process");
LOGW("Cannot terminate install process");
}
cmd_simple_wait(file_handler->current_process, NULL);
file_handler->current_process = PROCESS_NONE;

View File

@@ -22,7 +22,6 @@ struct file_handler_request_queue CBUF(struct file_handler_request, 16);
struct file_handler {
char *serial;
const char *push_target;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *event_cond;
@@ -33,8 +32,7 @@ struct file_handler {
};
bool
file_handler_init(struct file_handler *file_handler, const char *serial,
const char *push_target);
file_handler_init(struct file_handler *file_handler, const char *serial);
void
file_handler_destroy(struct file_handler *file_handler);

View File

@@ -47,7 +47,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
if (actions & ACTION_DOWN) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_DOWN;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject %s (DOWN)'", name);
LOGW("Cannot request 'inject %s (DOWN)'", name);
return;
}
}
@@ -55,7 +55,7 @@ send_keycode(struct controller *controller, enum android_keycode keycode,
if (actions & ACTION_UP) {
msg.inject_keycode.action = AKEY_EVENT_ACTION_UP;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject %s (UP)'", name);
LOGW("Cannot request 'inject %s (UP)'", name);
}
}
}
@@ -102,7 +102,7 @@ press_back_or_turn_screen_on(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_BACK_OR_SCREEN_ON;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'turn screen on'");
LOGW("Cannot request 'turn screen on'");
}
}
@@ -112,7 +112,7 @@ expand_notification_panel(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'expand notification panel'");
LOGW("Cannot request 'expand notification panel'");
}
}
@@ -122,7 +122,7 @@ collapse_notification_panel(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_COLLAPSE_NOTIFICATION_PANEL;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'collapse notification panel'");
LOGW("Cannot request 'collapse notification panel'");
}
}
@@ -132,7 +132,7 @@ request_device_clipboard(struct controller *controller) {
msg.type = CONTROL_MSG_TYPE_GET_CLIPBOARD;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request device clipboard");
LOGW("Cannot request device clipboard");
}
}
@@ -140,7 +140,7 @@ static void
set_device_clipboard(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError());
LOGW("Cannot get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
@@ -155,7 +155,7 @@ set_device_clipboard(struct controller *controller) {
if (!controller_push_msg(controller, &msg)) {
SDL_free(text);
LOGW("Could not request 'set device clipboard'");
LOGW("Cannot request 'set device clipboard'");
}
}
@@ -167,7 +167,7 @@ set_screen_power_mode(struct controller *controller,
msg.set_screen_power_mode.mode = mode;
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
LOGW("Cannot request 'set screen power mode'");
}
}
@@ -191,7 +191,7 @@ static void
clipboard_paste(struct controller *controller) {
char *text = SDL_GetClipboardText();
if (!text) {
LOGW("Could not get clipboard text: %s", SDL_GetError());
LOGW("Cannot get clipboard text: %s", SDL_GetError());
return;
}
if (!*text) {
@@ -205,7 +205,7 @@ clipboard_paste(struct controller *controller) {
msg.inject_text.text = text;
if (!controller_push_msg(controller, &msg)) {
SDL_free(text);
LOGW("Could not request 'paste clipboard'");
LOGW("Cannot request 'paste clipboard'");
}
}
@@ -222,12 +222,12 @@ input_manager_process_text_input(struct input_manager *input_manager,
msg.type = CONTROL_MSG_TYPE_INJECT_TEXT;
msg.inject_text.text = SDL_strdup(event->text);
if (!msg.inject_text.text) {
LOGW("Could not strdup input text");
LOGW("Cannot strdup input text");
return;
}
if (!controller_push_msg(input_manager->controller, &msg)) {
SDL_free(msg.inject_text.text);
LOGW("Could not request 'inject text'");
LOGW("Cannot request 'inject text'");
}
}
@@ -368,7 +368,7 @@ input_manager_process_key(struct input_manager *input_manager,
struct control_msg msg;
if (input_key_from_sdl_to_android(event, &msg)) {
if (!controller_push_msg(controller, &msg)) {
LOGW("Could not request 'inject keycode'");
LOGW("Cannot request 'inject keycode'");
}
}
}
@@ -385,7 +385,7 @@ input_manager_process_mouse_motion(struct input_manager *input_manager,
input_manager->screen->frame_size,
&msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse motion event'");
LOGW("Cannot request 'inject mouse motion event'");
}
}
}
@@ -431,7 +431,7 @@ input_manager_process_mouse_button(struct input_manager *input_manager,
input_manager->screen->frame_size,
&msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse button event'");
LOGW("Cannot request 'inject mouse button event'");
}
}
}
@@ -446,7 +446,7 @@ input_manager_process_mouse_wheel(struct input_manager *input_manager,
struct control_msg msg;
if (mouse_wheel_from_sdl_to_android(event, position, &msg)) {
if (!controller_push_msg(input_manager->controller, &msg)) {
LOGW("Could not request 'inject mouse wheel event'");
LOGW("Cannot request 'inject mouse wheel event'");
}
}
}

View File

@@ -17,8 +17,6 @@ struct args {
const char *serial;
const char *crop;
const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format;
bool fullscreen;
bool no_control;
@@ -77,11 +75,6 @@ static void usage(const char *arg0) {
" Set the TCP port the client listens on.\n"
" Default is %d.\n"
"\n"
" --push-target path\n"
" Set the target directory for pushing files to the device by\n"
" drag & drop. It is passed as-is to \"adb push\".\n"
" Default is \"/sdcard/\".\n"
"\n"
" -r, --record file.mp4\n"
" Record screen to file.\n"
" The format is determined by the -F/--record-format option if\n"
@@ -93,7 +86,7 @@ static void usage(const char *arg0) {
" This flag forces to render all frames, at a cost of a\n"
" possible increased latency.\n"
"\n"
" -s, --serial serial\n"
" -s, --serial\n"
" The device serial number. Mandatory only if several devices\n"
" are connected to adb.\n"
"\n"
@@ -110,9 +103,6 @@ static void usage(const char *arg0) {
" -v, --version\n"
" Print the version of scrcpy.\n"
"\n"
" --window-title text\n"
" Set a custom window title.\n"
"\n"
"Shortcuts:\n"
"\n"
" Ctrl+f\n"
@@ -305,8 +295,6 @@ guess_record_format(const char *filename) {
}
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002
static bool
parse_args(struct args *args, int argc, char *argv[]) {
@@ -320,8 +308,6 @@ parse_args(struct args *args, int argc, char *argv[]) {
{"no-control", no_argument, NULL, 'n'},
{"no-display", no_argument, NULL, 'N'},
{"port", required_argument, NULL, 'p'},
{"push-target", required_argument, NULL,
OPT_PUSH_TARGET},
{"record", required_argument, NULL, 'r'},
{"record-format", required_argument, NULL, 'f'},
{"render-expired-frames", no_argument, NULL,
@@ -330,8 +316,6 @@ parse_args(struct args *args, int argc, char *argv[]) {
{"show-touches", no_argument, NULL, 't'},
{"turn-screen-off", no_argument, NULL, 'S'},
{"version", no_argument, NULL, 'v'},
{"window-title", required_argument, NULL,
OPT_WINDOW_TITLE},
{NULL, 0, NULL, 0 },
};
int c;
@@ -394,12 +378,6 @@ parse_args(struct args *args, int argc, char *argv[]) {
case OPT_RENDER_EXPIRED_FRAMES:
args->render_expired_frames = true;
break;
case OPT_WINDOW_TITLE:
args->window_title = optarg;
break;
case OPT_PUSH_TARGET:
args->push_target = optarg;
break;
default:
// getopt prints the error message on stderr
return false;
@@ -436,11 +414,6 @@ parse_args(struct args *args, int argc, char *argv[]) {
}
}
if (args->no_control && args->turn_screen_off) {
LOGE("Could not request to turn screen off if control is disabled");
return false;
}
return true;
}
@@ -456,8 +429,6 @@ main(int argc, char *argv[]) {
.serial = NULL,
.crop = NULL,
.record_filename = NULL,
.window_title = NULL,
.push_target = NULL,
.record_format = 0,
.help = false,
.version = false,
@@ -485,8 +456,6 @@ main(int argc, char *argv[]) {
return 0;
}
LOGI("scrcpy " SCRCPY_VERSION " <https://github.com/Genymobile/scrcpy>");
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
av_register_all();
#endif
@@ -504,8 +473,6 @@ main(int argc, char *argv[]) {
.crop = args.crop,
.port = args.port,
.record_filename = args.record_filename,
.window_title = args.window_title,
.push_target = args.push_target,
.record_format = args.record_format,
.max_size = args.max_size,
.bit_rate = args.bit_rate,

View File

@@ -5,7 +5,6 @@
#include "compat.h"
#include "config.h"
#include "lock_util.h"
#include "log.h"
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@@ -27,82 +26,6 @@ find_muxer(const char *name) {
return oformat;
}
static struct record_packet *
record_packet_new(const AVPacket *packet) {
struct record_packet *rec = SDL_malloc(sizeof(*rec));
if (!rec) {
return NULL;
}
if (av_packet_ref(&rec->packet, packet)) {
SDL_free(rec);
return NULL;
}
rec->next = NULL;
return rec;
}
static void
record_packet_delete(struct record_packet *rec) {
av_packet_unref(&rec->packet);
SDL_free(rec);
}
static void
recorder_queue_init(struct recorder_queue *queue) {
queue->first = NULL;
// queue->last is undefined if queue->first == NULL
}
static inline bool
recorder_queue_is_empty(struct recorder_queue *queue) {
return !queue->first;
}
static bool
recorder_queue_push(struct recorder_queue *queue, const AVPacket *packet) {
struct record_packet *rec = record_packet_new(packet);
if (!rec) {
LOGC("Could not allocate record packet");
return false;
}
rec->next = NULL;
if (recorder_queue_is_empty(queue)) {
queue->first = queue->last = rec;
} else {
// chain rec after the (current) last packet
queue->last->next = rec;
// the last packet is now rec
queue->last = rec;
}
return true;
}
static inline struct record_packet *
recorder_queue_take(struct recorder_queue *queue) {
SDL_assert(!recorder_queue_is_empty(queue));
struct record_packet *rec = queue->first;
SDL_assert(rec);
queue->first = rec->next;
// no need to update queue->last if the queue is left empty:
// queue->last is undefined if queue->first == NULL
return rec;
}
static void
recorder_queue_clear(struct recorder_queue *queue) {
struct record_packet *rec = queue->first;
while (rec) {
struct record_packet *current = rec;
rec = rec->next;
record_packet_delete(current);
}
queue->first = NULL;
}
bool
recorder_init(struct recorder *recorder,
const char *filename,
@@ -110,28 +33,10 @@ recorder_init(struct recorder *recorder,
struct size declared_frame_size) {
recorder->filename = SDL_strdup(filename);
if (!recorder->filename) {
LOGE("Could not strdup filename");
LOGE("Cannot strdup filename");
return false;
}
recorder->mutex = SDL_CreateMutex();
if (!recorder->mutex) {
LOGC("Could not create mutex");
SDL_free(recorder->filename);
return false;
}
recorder->queue_cond = SDL_CreateCond();
if (!recorder->queue_cond) {
LOGC("Could not create cond");
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
return false;
}
recorder_queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
recorder->header_written = false;
@@ -141,8 +46,6 @@ recorder_init(struct recorder *recorder,
void
recorder_destroy(struct recorder *recorder) {
SDL_DestroyCond(recorder->queue_cond);
SDL_DestroyMutex(recorder->mutex);
SDL_free(recorder->filename);
}
@@ -216,17 +119,12 @@ recorder_close(struct recorder *recorder) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
const char *format_name = recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name, recorder->filename);
}
static bool
@@ -235,7 +133,7 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOGC("Could not allocate extradata");
LOGC("Cannot allocate extradata");
return false;
}
@@ -253,6 +151,9 @@ recorder_write_header(struct recorder *recorder, const AVPacket *packet) {
int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename);
SDL_free(extradata);
avio_closep(&recorder->ctx->pb);
avformat_free_context(recorder->ctx);
return false;
}
@@ -268,110 +169,13 @@ recorder_rescale_packet(struct recorder *recorder, AVPacket *packet) {
bool
recorder_write(struct recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) {
LOGE("The first packet is not a config packet");
return false;
}
bool ok = recorder_write_header(recorder, packet);
if (!ok) {
return false;
}
recorder->header_written = true;
return true;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
}
recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0;
}
static int
run_recorder(void *data) {
struct recorder *recorder = data;
for (;;) {
mutex_lock(recorder->mutex);
while (!recorder->stopped &&
recorder_queue_is_empty(&recorder->queue)) {
cond_wait(recorder->queue_cond, recorder->mutex);
}
// if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping
if (recorder->stopped && recorder_queue_is_empty(&recorder->queue)) {
mutex_unlock(recorder->mutex);
break;
}
struct record_packet *rec = recorder_queue_take(&recorder->queue);
mutex_unlock(recorder->mutex);
bool ok = recorder_write(recorder, &rec->packet);
record_packet_delete(rec);
if (!ok) {
LOGE("Could not record packet");
mutex_lock(recorder->mutex);
recorder->failed = true;
// discard pending packets
recorder_queue_clear(&recorder->queue);
mutex_unlock(recorder->mutex);
break;
}
}
LOGD("Recorder thread ended");
return 0;
}
bool
recorder_start(struct recorder *recorder) {
LOGD("Starting recorder thread");
recorder->thread = SDL_CreateThread(run_recorder, "recorder", recorder);
if (!recorder->thread) {
LOGC("Could not start recorder thread");
return false;
}
return true;
}
void
recorder_stop(struct recorder *recorder) {
mutex_lock(recorder->mutex);
recorder->stopped = true;
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
}
void
recorder_join(struct recorder *recorder) {
SDL_WaitThread(recorder->thread, NULL);
}
bool
recorder_push(struct recorder *recorder, const AVPacket *packet) {
mutex_lock(recorder->mutex);
SDL_assert(!recorder->stopped);
if (recorder->failed) {
// reject any new packet (this will stop the stream)
return false;
}
bool ok = recorder_queue_push(&recorder->queue, packet);
cond_signal(recorder->queue_cond);
mutex_unlock(recorder->mutex);
return ok;
}

View File

@@ -3,8 +3,6 @@
#include <stdbool.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_mutex.h>
#include <SDL2/SDL_thread.h>
#include "common.h"
@@ -13,29 +11,12 @@ enum recorder_format {
RECORDER_FORMAT_MKV,
};
struct record_packet {
AVPacket packet;
struct record_packet *next;
};
struct recorder_queue {
struct record_packet *first;
struct record_packet *last; // undefined if first is NULL
};
struct recorder {
char *filename;
enum recorder_format format;
AVFormatContext *ctx;
struct size declared_frame_size;
bool header_written;
SDL_Thread *thread;
SDL_mutex *mutex;
SDL_cond *queue_cond;
bool stopped; // set on recorder_stop() by the stream reader
bool failed; // set on packet write failure
struct recorder_queue queue;
};
bool
@@ -52,15 +33,6 @@ void
recorder_close(struct recorder *recorder);
bool
recorder_start(struct recorder *recorder);
void
recorder_stop(struct recorder *recorder);
void
recorder_join(struct recorder *recorder);
bool
recorder_push(struct recorder *recorder, const AVPacket *packet);
recorder_write(struct recorder *recorder, AVPacket *packet);
#endif

View File

@@ -259,7 +259,7 @@ av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
}
char *local_fmt = SDL_malloc(strlen(fmt) + 10);
if (!local_fmt) {
LOGC("Could not allocate string");
LOGC("Cannot allocate string");
return;
}
// strcpy is safe here, the destination is large enough
@@ -277,6 +277,7 @@ scrcpy(const struct scrcpy_options *options) {
.local_port = options->port,
.max_size = options->max_size,
.bit_rate = options->bit_rate,
.send_frame_meta = record,
.control = options->control,
};
if (!server_start(&server, options->serial, &params)) {
@@ -297,7 +298,6 @@ scrcpy(const struct scrcpy_options *options) {
bool video_buffer_initialized = false;
bool file_handler_initialized = false;
bool recorder_initialized = false;
bool stream_initialized = false;
bool stream_started = false;
bool controller_initialized = false;
bool controller_started = false;
@@ -334,8 +334,7 @@ scrcpy(const struct scrcpy_options *options) {
video_buffer_initialized = true;
if (options->control) {
if (!file_handler_init(&file_handler, server.serial,
options->push_target)) {
if (!file_handler_init(&file_handler, server.serial)) {
goto end;
}
file_handler_initialized = true;
@@ -359,10 +358,7 @@ scrcpy(const struct scrcpy_options *options) {
av_log_set_callback(av_log_callback);
if (!stream_init(&stream, server.video_socket, dec, rec)) {
goto end;
}
stream_initialized = true;
stream_init(&stream, server.video_socket, dec, rec);
// now we consumed the header values, the socket receives the video stream
// start the stream
@@ -384,10 +380,7 @@ scrcpy(const struct scrcpy_options *options) {
controller_started = true;
}
const char *window_title =
options->window_title ? options->window_title : device_name;
if (!screen_init_rendering(&screen, window_title, frame_size,
if (!screen_init_rendering(&screen, device_name, frame_size,
options->always_on_top)) {
goto end;
}
@@ -398,7 +391,7 @@ scrcpy(const struct scrcpy_options *options) {
msg.set_screen_power_mode.mode = SCREEN_POWER_MODE_OFF;
if (!controller_push_msg(&controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
LOGW("Cannot request 'set screen power mode'");
}
}
@@ -441,9 +434,6 @@ end:
if (stream_started) {
stream_join(&stream);
}
if (stream_initialized) {
stream_destroy(&stream);
}
if (controller_started) {
controller_join(&controller);
}

View File

@@ -9,8 +9,6 @@ struct scrcpy_options {
const char *serial;
const char *crop;
const char *record_filename;
const char *window_title;
const char *push_target;
enum recorder_format record_format;
uint16_t port;
uint16_t max_size;

View File

@@ -85,7 +85,7 @@ get_optimal_size(struct size current_size, struct size frame_size) {
uint32_t h;
if (!get_preferred_display_bounds(&display_size)) {
// could not get display bounds, do not constraint the size
// cannot get display bounds, do not constraint the size
w = current_size.width;
h = current_size.height;
} else {
@@ -134,7 +134,7 @@ create_texture(SDL_Renderer *renderer, struct size frame_size) {
}
bool
screen_init_rendering(struct screen *screen, const char *window_title,
screen_init_rendering(struct screen *screen, const char *device_name,
struct size frame_size, bool always_on_top) {
screen->frame_size = frame_size;
@@ -152,7 +152,7 @@ screen_init_rendering(struct screen *screen, const char *window_title,
#endif
}
screen->window = SDL_CreateWindow(window_title, SDL_WINDOWPOS_UNDEFINED,
screen->window = SDL_CreateWindow(device_name, SDL_WINDOWPOS_UNDEFINED,
SDL_WINDOWPOS_UNDEFINED,
window_size.width, window_size.height,
window_flags);

View File

@@ -44,7 +44,7 @@ screen_init(struct screen *screen);
// initialize screen, create window, renderer and texture (window is hidden)
bool
screen_init_rendering(struct screen *screen, const char *window_title,
screen_init_rendering(struct screen *screen, const char *device_name,
struct size frame_size, bool always_on_top);
// show the window

View File

@@ -15,7 +15,7 @@
#define SOCKET_NAME "scrcpy"
#define SERVER_FILENAME "scrcpy-server.jar"
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FILENAME
#define DEFAULT_SERVER_PATH PREFIX "/share/scrcpy/" SERVER_FLENAME
#define DEVICE_SERVER_PATH "/data/local/tmp/" SERVER_FILENAME
static const char *
@@ -35,7 +35,7 @@ get_server_path(void) {
// use scrcpy-server.jar in the same directory as the executable
char *executable_path = get_executable_path();
if (!executable_path) {
LOGE("Could not get executable path, "
LOGE("Cannot get executable path, "
"using " SERVER_FILENAME " from current directory");
// not found, use current directory
return SERVER_FILENAME;
@@ -47,7 +47,7 @@ get_server_path(void) {
size_t len = dirlen + 1 + sizeof(SERVER_FILENAME);
char *server_path = SDL_malloc(len);
if (!server_path) {
LOGE("Could not alloc server path string, "
LOGE("Cannot alloc server path string, "
"using " SERVER_FILENAME " from current directory");
SDL_free(executable_path);
return SERVER_FILENAME;
@@ -130,7 +130,7 @@ execute_server(struct server *server, const struct server_params *params) {
bit_rate_string,
server->tunnel_forward ? "true" : "false",
params->crop ? params->crop : "-",
"true", // always send frame meta (packet boundaries + timestamp)
params->send_frame_meta ? "true" : "false",
params->control ? "true" : "false",
};
return adb_execute(server->serial, cmd, sizeof(cmd) / sizeof(cmd[0]));
@@ -155,7 +155,6 @@ connect_and_read_byte(uint16_t port) {
// is not listening, so read one byte to detect a working connection
if (net_recv(socket, &byte, 1) != 1) {
// the server is not listening yet behind the adb tunnel
net_close(socket);
return INVALID_SOCKET;
}
return socket;
@@ -182,7 +181,7 @@ close_socket(socket_t *socket) {
SDL_assert(*socket != INVALID_SOCKET);
net_shutdown(*socket, SHUT_RDWR);
if (!net_close(*socket)) {
LOGW("Could not close socket");
LOGW("Cannot close socket");
return;
}
*socket = INVALID_SOCKET;
@@ -306,7 +305,7 @@ server_stop(struct server *server) {
SDL_assert(server->process != PROCESS_NONE);
if (!cmd_terminate(server->process)) {
LOGW("Could not terminate server");
LOGW("Cannot terminate server");
}
cmd_simple_wait(server->process, NULL); // ignore exit code

View File

@@ -34,6 +34,7 @@ struct server_params {
uint16_t local_port;
uint16_t max_size;
uint32_t bit_rate;
bool send_frame_meta;
bool control;
};

View File

@@ -17,13 +17,59 @@
#include "log.h"
#include "recorder.h"
#define STREAM_BUFSIZE 0x10000
#define BUFSIZE 0x10000
#define HEADER_SIZE 12
#define NO_PTS UINT64_C(-1)
static struct frame_meta *
frame_meta_new(uint64_t pts) {
struct frame_meta *meta = SDL_malloc(sizeof(*meta));
if (!meta) {
return meta;
}
meta->pts = pts;
meta->next = NULL;
return meta;
}
static void
frame_meta_delete(struct frame_meta *frame_meta) {
SDL_free(frame_meta);
}
static bool
stream_recv_packet(struct stream *stream, AVPacket *packet) {
receiver_state_push_meta(struct receiver_state *state, uint64_t pts) {
struct frame_meta *frame_meta = frame_meta_new(pts);
if (!frame_meta) {
return false;
}
// append to the list
// (iterate to find the last item, in practice the list should be tiny)
struct frame_meta **p = &state->frame_meta_queue;
while (*p) {
p = &(*p)->next;
}
*p = frame_meta;
return true;
}
static uint64_t
receiver_state_take_meta(struct receiver_state *state) {
struct frame_meta *frame_meta = state->frame_meta_queue; // first item
SDL_assert(frame_meta); // must not be empty
uint64_t pts = frame_meta->pts;
state->frame_meta_queue = frame_meta->next; // remove the item
frame_meta_delete(frame_meta);
return pts;
}
static int
read_packet_with_meta(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
struct receiver_state *state = &stream->receiver_state;
// The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet.
@@ -36,31 +82,60 @@ stream_recv_packet(struct stream *stream, AVPacket *packet) {
//
// It is followed by <packet_size> bytes containing the packet/frame.
uint8_t header[HEADER_SIZE];
ssize_t r =
buffered_reader_recv_all(&stream->buffered_reader, header, HEADER_SIZE);
if (r < HEADER_SIZE) {
return false;
if (!state->remaining) {
#define HEADER_SIZE 12
uint8_t header[HEADER_SIZE];
ssize_t r = net_recv_all(stream->socket, header, HEADER_SIZE);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
// no partial read (net_recv_all())
SDL_assert_release(r == HEADER_SIZE);
uint64_t pts = buffer_read64be(header);
state->remaining = buffer_read32be(&header[8]);
if (pts != NO_PTS && !receiver_state_push_meta(state, pts)) {
LOGE("Could not store PTS for recording");
// we cannot save the PTS, the recording would be broken
return AVERROR(ENOMEM);
}
}
uint64_t pts = buffer_read64be(header);
uint32_t len = buffer_read32be(&header[8]);
SDL_assert(len);
SDL_assert(state->remaining);
if (av_new_packet(packet, len)) {
LOGE("Could not allocate packet");
return false;
if (buf_size > state->remaining) {
buf_size = state->remaining;
}
r = buffered_reader_recv_all(&stream->buffered_reader, packet->data, len);
if (r < len) {
av_packet_unref(packet);
return false;
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
packet->pts = pts != NO_PTS ? pts : AV_NOPTS_VALUE;
SDL_assert(state->remaining >= r);
state->remaining -= r;
return true;
return r;
}
static int
read_raw_packet(void *opaque, uint8_t *buf, int buf_size) {
struct stream *stream = opaque;
ssize_t r = net_recv(stream->socket, buf, buf_size);
if (r == -1) {
return AVERROR(errno);
}
if (r == 0) {
return AVERROR_EOF;
}
return r;
}
static void
@@ -70,221 +145,128 @@ notify_stopped(void) {
SDL_PushEvent(&stop_event);
}
static bool
process_config_packet(struct stream *stream, AVPacket *packet) {
if (stream->recorder && !recorder_push(stream->recorder, packet)) {
LOGE("Could not send config packet to recorder");
return false;
}
return true;
}
static bool
process_frame(struct stream *stream, AVPacket *packet) {
if (stream->decoder && !decoder_push(stream->decoder, packet)) {
return false;
}
if (stream->recorder) {
packet->dts = packet->pts;
if (!recorder_push(stream->recorder, packet)) {
LOGE("Could not send packet to recorder");
return false;
}
}
return true;
}
static bool
stream_parse(struct stream *stream, AVPacket *packet) {
uint8_t *in_data = packet->data;
int in_len = packet->size;
uint8_t *out_data = NULL;
int out_len = 0;
int r = av_parser_parse2(stream->parser, stream->codec_ctx,
&out_data, &out_len, in_data, in_len,
AV_NOPTS_VALUE, AV_NOPTS_VALUE, -1);
// PARSER_FLAG_COMPLETE_FRAMES is set
SDL_assert(r == in_len);
SDL_assert(out_len == in_len);
if (stream->parser->key_frame == 1) {
packet->flags |= AV_PKT_FLAG_KEY;
}
bool ok = process_frame(stream, packet);
if (!ok) {
LOGE("Could not process frame");
return false;
}
return true;
}
static bool
stream_push_packet(struct stream *stream, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immetiately (it contains no
// frame); instead, it must be concatenated with the future data packet.
if (stream->has_pending || is_config) {
size_t offset;
if (stream->has_pending) {
offset = stream->pending.size;
if (av_grow_packet(&stream->pending, packet->size)) {
LOGE("Could not grow packet");
return false;
}
} else {
offset = 0;
if (av_new_packet(&stream->pending, packet->size)) {
LOGE("Could not create packet");
return false;
}
stream->has_pending = true;
}
memcpy(stream->pending.data + offset, packet->data, packet->size);
if (!is_config) {
// prepare the concat packet to send to the decoder
stream->pending.pts = packet->pts;
stream->pending.dts = packet->dts;
stream->pending.flags = packet->flags;
packet = &stream->pending;
}
}
if (is_config) {
// config packet
bool ok = process_config_packet(stream, packet);
if (!ok) {
return false;
}
} else {
// data packet
bool ok = stream_parse(stream, packet);
if (stream->has_pending) {
// the pending packet must be discarded (consumed or error)
stream->has_pending = false;
av_packet_unref(&stream->pending);
}
if (!ok) {
return false;
}
}
return true;
}
static int
run_stream(void *data) {
struct stream *stream = data;
AVFormatContext *format_ctx = avformat_alloc_context();
if (!format_ctx) {
LOGC("Could not allocate format context");
goto end;
}
unsigned char *buffer = av_malloc(BUFSIZE);
if (!buffer) {
LOGC("Could not allocate buffer");
goto finally_free_format_ctx;
}
// initialize the receiver state
stream->receiver_state.frame_meta_queue = NULL;
stream->receiver_state.remaining = 0;
// if recording is enabled, a "header" is sent between raw packets
int (*read_packet)(void *, uint8_t *, int) =
stream->recorder ? read_packet_with_meta : read_raw_packet;
AVIOContext *avio_ctx = avio_alloc_context(buffer, BUFSIZE, 0, stream,
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 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 finally_free_avio_ctx;
}
AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("H.264 decoder not found");
goto end;
}
stream->codec_ctx = avcodec_alloc_context3(codec);
if (!stream->codec_ctx) {
LOGC("Could not allocate codec context");
goto end;
}
if (stream->decoder && !decoder_open(stream->decoder, codec)) {
LOGE("Could not open decoder");
goto finally_free_codec_ctx;
goto finally_close_input;
}
if (stream->recorder) {
if (!recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_decoder;
}
if (!recorder_start(stream->recorder)) {
LOGE("Could not start recorder");
goto finally_close_recorder;
}
if (stream->recorder && !recorder_open(stream->recorder, codec)) {
LOGE("Could not open recorder");
goto finally_close_input;
}
stream->parser = av_parser_init(AV_CODEC_ID_H264);
if (!stream->parser) {
LOGE("Could not initialize parser");
goto finally_stop_and_join_recorder;
}
AVPacket packet;
av_init_packet(&packet);
packet.data = NULL;
packet.size = 0;
// We must only pass complete frames to av_parser_parse2()!
// It's more complicated, but this allows to reduce the latency by 1 frame!
stream->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
for (;;) {
AVPacket packet;
bool ok = stream_recv_packet(stream, &packet);
if (!ok) {
// end of stream
break;
while (!av_read_frame(format_ctx, &packet)) {
if (SDL_AtomicGet(&stream->stopped)) {
// if the stream is stopped, the socket had been shutdown, so the
// last packet is probably corrupted (but not detected as such by
// FFmpeg) and will not be decoded correctly
av_packet_unref(&packet);
goto quit;
}
if (stream->decoder && !decoder_push(stream->decoder, &packet)) {
av_packet_unref(&packet);
goto quit;
}
if (stream->recorder) {
// we retrieve the PTS in order they were received, so they will
// be assigned to the correct frame
uint64_t pts = receiver_state_take_meta(&stream->receiver_state);
packet.pts = pts;
packet.dts = pts;
// no need to rescale with av_packet_rescale_ts(), the timestamps
// are in microseconds both in input and output
if (!recorder_write(stream->recorder, &packet)) {
LOGE("Could not write frame to output file");
av_packet_unref(&packet);
goto quit;
}
}
ok = stream_push_packet(stream, &packet);
av_packet_unref(&packet);
if (!ok) {
// cannot process packet (error already logged)
if (avio_ctx->eof_reached) {
break;
}
}
LOGD("End of frames");
if (stream->has_pending) {
av_packet_unref(&stream->pending);
}
av_parser_close(stream->parser);
finally_stop_and_join_recorder:
if (stream->recorder) {
recorder_stop(stream->recorder);
LOGI("Finishing recording...");
recorder_join(stream->recorder);
}
finally_close_recorder:
quit:
if (stream->recorder) {
recorder_close(stream->recorder);
}
finally_close_decoder:
if (stream->decoder) {
decoder_close(stream->decoder);
}
finally_free_codec_ctx:
avcodec_free_context(&stream->codec_ctx);
finally_close_input:
avformat_close_input(&format_ctx);
finally_free_avio_ctx:
av_free(avio_ctx->buffer);
av_free(avio_ctx);
finally_free_format_ctx:
avformat_free_context(format_ctx);
end:
notify_stopped();
return 0;
}
bool
void
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder) {
if (!buffered_reader_init(&stream->buffered_reader, socket,
STREAM_BUFSIZE)) {
return false;
}
stream->socket = socket;
stream->decoder = decoder,
stream->recorder = recorder;
stream->has_pending = false;
return true;
}
void
stream_destroy(struct stream *stream) {
buffered_reader_destroy(&stream->buffered_reader);
SDL_AtomicSet(&stream->stopped, 0);
}
bool
@@ -301,6 +283,7 @@ stream_start(struct stream *stream) {
void
stream_stop(struct stream *stream) {
SDL_AtomicSet(&stream->stopped, 1);
if (stream->decoder) {
decoder_interrupt(stream->decoder);
}

View File

@@ -3,36 +3,35 @@
#include <stdbool.h>
#include <stdint.h>
#include <libavformat/avformat.h>
#include <SDL2/SDL_atomic.h>
#include <SDL2/SDL_thread.h>
#include "buffered_reader.h"
#include "net.h"
struct video_buffer;
struct stream {
socket_t socket;
struct buffered_reader buffered_reader;
struct video_buffer *video_buffer;
SDL_Thread *thread;
struct decoder *decoder;
struct recorder *recorder;
AVCodecContext *codec_ctx;
AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config
// packet is available
bool has_pending;
AVPacket pending;
struct frame_meta {
uint64_t pts;
struct frame_meta *next;
};
bool
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder);
struct stream {
socket_t socket;
struct video_buffer *video_buffer;
SDL_Thread *thread;
SDL_atomic_t stopped;
struct decoder *decoder;
struct recorder *recorder;
struct receiver_state {
// meta (in order) for frames not consumed yet
struct frame_meta *frame_meta_queue;
size_t remaining; // remaining bytes to receive for the current frame
} receiver_state;
};
void
stream_destroy(struct stream *stream);
stream_init(struct stream *stream, socket_t socket,
struct decoder *decoder, struct recorder *recorder);
bool
stream_start(struct stream *stream);

View File

@@ -94,7 +94,7 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
int status;
int code;
if (waitpid(pid, &status, 0) == -1 || !WIFEXITED(status)) {
// could not wait, or exited unexpectedly, probably by a signal
// cannot wait, or exited unexpectedly, probably by a signal
code = -1;
} else {
code = WEXITSTATUS(status);
@@ -107,8 +107,6 @@ cmd_simple_wait(pid_t pid, int *exit_code) {
char *
get_executable_path(void) {
// <https://stackoverflow.com/a/1024937/1987178>
#ifdef __linux__
char buf[PATH_MAX + 1]; // +1 for the null byte
ssize_t len = readlink("/proc/self/exe", buf, PATH_MAX);
if (len == -1) {
@@ -117,10 +115,4 @@ get_executable_path(void) {
}
buf[len] = '\0';
return SDL_strdup(buf);
#else
// in practice, we only need this feature for portable builds, only used on
// Windows, so we don't care implementing it for every platform
// (it's useful to have a working version on Linux for debugging though)
return NULL;
#endif
}

View File

@@ -33,7 +33,7 @@ cmd_execute(const char *path, const char *const argv[], HANDLE *handle) {
wchar_t *wide = utf8_to_wide_char(cmd);
if (!wide) {
LOGC("Could not allocate wide char string");
LOGC("Cannot allocate wide char string");
return PROCESS_ERROR_GENERIC;
}
@@ -67,7 +67,7 @@ cmd_simple_wait(HANDLE handle, DWORD *exit_code) {
DWORD code;
if (WaitForSingleObject(handle, INFINITE) != WAIT_OBJECT_0
|| !GetExitCodeProcess(handle, &code)) {
// could not wait or retrieve the exit code
// cannot wait or retrieve the exit code
code = -1; // max value, it's unsigned
}
if (exit_code) {

View File

@@ -15,6 +15,6 @@ cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.8/i686-w64-mingw32'
prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win32-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win32-dev'
prebuilt_sdl2 = 'SDL2-2.0.9/i686-w64-mingw32'

View File

@@ -15,6 +15,6 @@ cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg_shared = 'ffmpeg-4.1.3-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1.3-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.8/x86_64-w64-mingw32'
prebuilt_ffmpeg_shared = 'ffmpeg-4.1-win64-shared'
prebuilt_ffmpeg_dev = 'ffmpeg-4.1-win64-dev'
prebuilt_sdl2 = 'SDL2-2.0.9/x86_64-w64-mingw32'

View File

@@ -1,13 +1,13 @@
project('scrcpy', 'c',
version: '1.9',
version: '1.8',
meson_version: '>= 0.37',
default_options: 'c_std=c11')
if get_option('compile_app')
if get_option('build_app')
subdir('app')
endif
if get_option('compile_server')
if get_option('build_server')
subdir('server')
endif

View File

@@ -1,7 +1,8 @@
option('compile_app', type: 'boolean', value: true, description: 'Build the client')
option('compile_server', type: 'boolean', value: true, description: 'Build the server')
option('build_app', type: 'boolean', value: true, description: 'Build the client')
option('build_server', type: 'boolean', value: true, description: 'Build the server')
option('crossbuild_windows', type: 'boolean', value: false, description: 'Build for Windows from Linux')
option('windows_noconsole', type: 'boolean', value: false, description: 'Disable console on Windows (pass -mwindows flag)')
option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server')
option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
option('portable', type: 'boolean', description: 'Use scrcpy-server.jar from the same directory as the scrcpy executable')
option('skip_frames', type: 'boolean', value: true, description: 'Always display the most recent frame')
option('hidpi_support', type: 'boolean', value: true, description: 'Enable High DPI support')

View File

@@ -10,31 +10,31 @@ prepare-win32: prepare-sdl2 prepare-ffmpeg-shared-win32 prepare-ffmpeg-dev-win32
prepare-win64: prepare-sdl2 prepare-ffmpeg-shared-win64 prepare-ffmpeg-dev-win64 prepare-adb
prepare-ffmpeg-shared-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1.3-win32-shared.zip \
8ea472d673370d5e87517a75587abfa6f189ee4f82e8da21fdbc49d0db0c1a89 \
ffmpeg-4.1.3-win32-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/shared/ffmpeg-4.1-win32-shared.zip \
e692b18c01745d262c03294b382fd64df68fabe3c66aa4546a3ad3935175cde3 \
ffmpeg-4.1-win32-shared
prepare-ffmpeg-dev-win32:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1.3-win32-dev.zip \
e16d3150b6ccf0b71908f5b964cb8c051d79053c8f5cd6d777d617ab4f03613a \
ffmpeg-4.1.3-win32-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win32/dev/ffmpeg-4.1-win32-dev.zip \
34bc5e471fb9160609abd6bc271e361050f3ff7376b1b8a0873cca02b38277c8 \
ffmpeg-4.1-win32-dev
prepare-ffmpeg-shared-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1.3-win64-shared.zip \
0b974578e07d974c4bafb36c7ed0b46e46b001d38b149455089c13b57ddefe5d \
ffmpeg-4.1.3-win64-shared
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-4.1-win64-shared.zip \
c4908c97436c946509dc365e421159274fa4b1e66dce6fb5b63d82a6294d5357 \
ffmpeg-4.1-win64-shared
prepare-ffmpeg-dev-win64:
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1.3-win64-dev.zip \
334b473467db096a5b74242743592a73e120a137232794508e4fc55593696a5b \
ffmpeg-4.1.3-win64-dev
@./prepare-dep https://ffmpeg.zeranoe.com/builds/win64/dev/ffmpeg-4.1-win64-dev.zip \
761ec79aa3dae66698c9791a2f0bb9da8794246f8356cadc741ddc0eabab0471 \
ffmpeg-4.1-win64-dev
prepare-sdl2:
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.8-mingw.tar.gz \
ffff7305d634aff5e1df5b7bb935435c3a02c8b03ad94a1a2be9169a558a7961 \
SDL2-2.0.8
@./prepare-dep https://libsdl.org/release/SDL2-devel-2.0.9-mingw.tar.gz \
0f9f00d0f2a9a95dfb5cce929718210c3f85432cc2e9d4abade4adcb7f6bb39d \
SDL2-2.0.9
prepare-adb:
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r29.0.1-windows.zip \
2334f92cf571fd2d9bf6ff7c637765bee5d8323e0bd8e051e15927d87b54b4e8 \
@./prepare-dep https://dl.google.com/android/repository/platform-tools_r28.0.1-windows.zip \
db78f726d5dc653706dcd15a462ab1b946c643f598df76906c4c1858411c54df \
platform-tools

View File

@@ -1,13 +1,13 @@
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
compileSdkVersion 27
defaultConfig {
applicationId "com.genymobile.scrcpy"
minSdkVersion 21
targetSdkVersion 29
versionCode 10
versionName "1.9"
targetSdkVersion 27
versionCode 9
versionName "1.8"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {

View File

@@ -68,7 +68,7 @@ public final class Server {
@SuppressWarnings("checkstyle:MagicNumber")
private static Options createOptions(String... args) {
if (args.length != 6) {
throw new IllegalArgumentException("Expecting 6 parameters");
throw new IllegalArgumentException("Expecting 5 parameters");
}
Options options = new Options();
@@ -116,7 +116,7 @@ public final class Server {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {
Ln.e("Could not unlink server", e);
Ln.e("Cannot unlink server", e);
}
}

View File

@@ -29,7 +29,7 @@ public class StatusBarManager {
try {
expandNotificationsPanelMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke ServiceBarManager.expandNotificationsPanel()", e);
Ln.e("Cannot invoke ServiceBarManager.expandNotificationsPanel()", e);
}
}
@@ -45,7 +45,7 @@ public class StatusBarManager {
try {
collapsePanelsMethod.invoke(manager);
} catch (InvocationTargetException | IllegalAccessException e) {
Ln.e("Could not invoke ServiceBarManager.collapsePanels()", e);
Ln.e("Cannot invoke ServiceBarManager.collapsePanels()", e);
}
}
}

View File

@@ -2,7 +2,6 @@ package com.genymobile.scrcpy.wrappers;
import android.annotation.SuppressLint;
import android.graphics.Rect;
import android.os.Build;
import android.os.IBinder;
import android.view.Surface;
@@ -78,12 +77,7 @@ public final class SurfaceControl {
public static IBinder getBuiltInDisplay(int builtInDisplayId) {
try {
// the method signature has changed in Android Q
// <https://github.com/Genymobile/scrcpy/issues/586>
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
}
return (IBinder) CLASS.getMethod("getPhysicalDisplayToken", long.class).invoke(null, builtInDisplayId);
return (IBinder) CLASS.getMethod("getBuiltInDisplay", int.class).invoke(null, builtInDisplayId);
} catch (Exception e) {
throw new AssertionError(e);
}

View File

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

View File

@@ -1,6 +1,7 @@
package com.genymobile.scrcpy;
import org.junit.Assert;
import junit.framework.Assert;
import org.junit.Test;
import java.nio.charset.StandardCharsets;
@@ -9,7 +10,7 @@ public class StringUtilsTest {
@Test
@SuppressWarnings("checkstyle:MagicNumber")
public void testUtf8Truncate() {
public void testUtf8Trucate() {
String s = "aÉbÔc";
byte[] utf8 = s.getBytes(StandardCharsets.UTF_8);
Assert.assertEquals(7, utf8.length);