Compare commits
80 Commits
basedoc
...
custom-ffm
Author | SHA1 | Date | |
---|---|---|---|
fa6c5b5149 | |||
884997e854 | |||
a0fa9967b8 | |||
260edc318f | |||
e48098ec8d | |||
8fad02aafa | |||
bb935764ae | |||
fe127af72e | |||
8e32d15e6c | |||
7f2989e1d5 | |||
da57902dd5 | |||
28701090f6 | |||
c50dc53bc2 | |||
30b8429752 | |||
38e5dafba6 | |||
d60a502485 | |||
55f4c42f19 | |||
7a9eefb04a | |||
e39ad0f695 | |||
377b6f57c5 | |||
aa8ed923f0 | |||
13211f82a1 | |||
7f50ed2458 | |||
9187472014 | |||
a331c2c653 | |||
f75dc2e477 | |||
8a5be9e2a6 | |||
705d69aaea | |||
ad51a2b411 | |||
7581dc10d3 | |||
68cd396e1f | |||
8dc1fd172a | |||
9c34c34e5d | |||
0abb268432 | |||
f2c65808fa | |||
93e86a5661 | |||
e614e19df4 | |||
e7c931139b | |||
98ece15421 | |||
8050de011c | |||
714b01204a | |||
9e831005c4 | |||
55eb874ed6 | |||
667882e9c4 | |||
0e62580570 | |||
ded19ca0f0 | |||
ad683461d6 | |||
504793b5c9 | |||
00e88acfaa | |||
7c0ee70261 | |||
2a4eec702d | |||
1e113feb59 | |||
08da34835c | |||
7b3a39bdc7 | |||
5586335276 | |||
8bb63cf14c | |||
5b01457364 | |||
010da4df59 | |||
de332e3e96 | |||
e679e3a966 | |||
97ae0a2d13 | |||
f1a4349834 | |||
a95bfe4f01 | |||
84f1792c6f | |||
317a5e93bb | |||
6b669d2dba | |||
21dd946edc | |||
669cbc7457 | |||
d3adda176b | |||
e7fa099be4 | |||
7ec2c7e232 | |||
932e698cdd | |||
ca5b962377 | |||
9233f1990e | |||
17a486d763 | |||
65e9206b3f | |||
bf7c0f2c33 | |||
dc585f033e | |||
b67ea5173c | |||
cf9718bee4 |
@ -2,16 +2,57 @@
|
|||||||
|
|
||||||
Here are the instructions to build _scrcpy_ (client and server).
|
Here are the instructions to build _scrcpy_ (client and server).
|
||||||
|
|
||||||
If you just want to build and install the latest release, follow the simplified
|
|
||||||
process described in [doc/linux.md](linux.md).
|
## Simple
|
||||||
|
|
||||||
|
If you just want to install the latest release from `master`, follow this
|
||||||
|
simplified process.
|
||||||
|
|
||||||
|
First, you need to install the required packages:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# for Debian/Ubuntu
|
||||||
|
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
||||||
|
gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||||
|
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||||
|
libusb-1.0-0 libusb-1.0-0-dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Then clone the repo and execute the installation script
|
||||||
|
([source](install_release.sh)):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/Genymobile/scrcpy
|
||||||
|
cd scrcpy
|
||||||
|
./install_release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
When a new release is out, update the repo and reinstall:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git pull
|
||||||
|
./install_release.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
To uninstall:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo ninja -Cbuild-auto uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
## Branches
|
## Branches
|
||||||
|
|
||||||
There are two main branches:
|
### `master`
|
||||||
- `master`: contains the latest release. It is the home page of the project on
|
|
||||||
GitHub.
|
The `master` branch concerns the latest release, and is the home page of the
|
||||||
- `dev`: the current development branch. Every commit present in `dev` will be
|
project on GitHub.
|
||||||
in the next release.
|
|
||||||
|
|
||||||
|
### `dev`
|
||||||
|
|
||||||
|
`dev` is the current development branch. Every commit present in `dev` will be
|
||||||
|
in the next release.
|
||||||
|
|
||||||
If you want to contribute code, please base your commits on the latest `dev`
|
If you want to contribute code, please base your commits on the latest `dev`
|
||||||
branch.
|
branch.
|
||||||
@ -28,8 +69,6 @@ the following files to a directory accessible from your `PATH`:
|
|||||||
- `AdbWinApi.dll`
|
- `AdbWinApi.dll`
|
||||||
- `AdbWinUsbApi.dll`
|
- `AdbWinUsbApi.dll`
|
||||||
|
|
||||||
It is also available in scrcpy releases.
|
|
||||||
|
|
||||||
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
|
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
|
||||||
|
|
||||||
[adb]: https://developer.android.com/studio/command-line/adb.html
|
[adb]: https://developer.android.com/studio/command-line/adb.html
|
||||||
@ -55,7 +94,7 @@ sudo apt install ffmpeg libsdl2-2.0-0 adb libusb-1.0-0
|
|||||||
# client build dependencies
|
# client build dependencies
|
||||||
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
|
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||||
libswresample-dev libusb-1.0-0-dev
|
libusb-1.0-0-dev
|
||||||
|
|
||||||
# server build dependencies
|
# server build dependencies
|
||||||
sudo apt install openjdk-11-jdk
|
sudo apt install openjdk-11-jdk
|
||||||
@ -275,8 +314,7 @@ This installs several files:
|
|||||||
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
|
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
|
||||||
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)
|
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)
|
||||||
|
|
||||||
You can then run `scrcpy`.
|
You can then [run](README.md#run) `scrcpy`.
|
||||||
|
|
||||||
|
|
||||||
### Uninstall
|
### Uninstall
|
||||||
|
|
123
FAQ.md
123
FAQ.md
@ -164,6 +164,32 @@ keyboard][hid] (HID).
|
|||||||
|
|
||||||
## Client issues
|
## Client issues
|
||||||
|
|
||||||
|
### The quality is low
|
||||||
|
|
||||||
|
If the definition of your client window is smaller than that of your device
|
||||||
|
screen, then you might get poor quality, especially visible on text (see [#40]).
|
||||||
|
|
||||||
|
[#40]: https://github.com/Genymobile/scrcpy/issues/40
|
||||||
|
|
||||||
|
This problem should be fixed in scrcpy v1.22: **update to the latest version**.
|
||||||
|
|
||||||
|
On older versions, you must configure the [scaling behavior]:
|
||||||
|
|
||||||
|
> `scrcpy.exe` > Properties > Compatibility > Change high DPI settings >
|
||||||
|
> Override high DPI scaling behavior > Scaling performed by: _Application_.
|
||||||
|
|
||||||
|
[scaling behavior]: https://github.com/Genymobile/scrcpy/issues/40#issuecomment-424466723
|
||||||
|
|
||||||
|
Also, to improve downscaling quality, trilinear filtering is enabled
|
||||||
|
automatically if the renderer is OpenGL and if it supports mipmapping.
|
||||||
|
|
||||||
|
On Windows, you might want to force OpenGL to enable mipmapping:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --render-driver=opengl
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Issue with Wayland
|
### Issue with Wayland
|
||||||
|
|
||||||
By default, SDL uses x11 on Linux. The [video driver] can be changed via the
|
By default, SDL uses x11 on Linux. The [video driver] can be changed via the
|
||||||
@ -198,15 +224,102 @@ As a workaround, [disable "Block compositing"][kwin].
|
|||||||
|
|
||||||
### Exception
|
### Exception
|
||||||
|
|
||||||
If you get any exception related to `MediaCodec`:
|
There may be many reasons. One common cause is that the hardware encoder of your
|
||||||
|
device is not able to encode at the given definition:
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> ERROR: Exception on thread Thread[main,5,main]
|
||||||
|
> android.media.MediaCodec$CodecException: Error 0xfffffc0e
|
||||||
|
> ...
|
||||||
|
> Exit due to uncaughtException in main thread:
|
||||||
|
> ERROR: Could not open video stream
|
||||||
|
> INFO: Initial texture: 1080x2336
|
||||||
|
> ```
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
> ```
|
||||||
|
> ERROR: Exception on thread Thread[main,5,main]
|
||||||
|
> java.lang.IllegalStateException
|
||||||
|
> at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
||||||
|
> ```
|
||||||
|
|
||||||
|
Just try with a lower definition:
|
||||||
|
|
||||||
```
|
```
|
||||||
ERROR: Exception on thread Thread[main,5,main]
|
scrcpy -m 1920
|
||||||
java.lang.IllegalStateException
|
scrcpy -m 1024
|
||||||
at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
scrcpy -m 800
|
||||||
```
|
```
|
||||||
|
|
||||||
then try with another [encoder](doc/video.md#codec).
|
Since scrcpy v1.22, scrcpy automatically tries again with a lower definition
|
||||||
|
before failing. This behavior can be disabled with `--no-downsize-on-error`.
|
||||||
|
|
||||||
|
You could also try another [encoder](README.md#encoder).
|
||||||
|
|
||||||
|
|
||||||
|
If you encounter this exception on Android 12, then just upgrade to scrcpy >=
|
||||||
|
1.18 (see [#2129]):
|
||||||
|
|
||||||
|
```
|
||||||
|
> ERROR: Exception on thread Thread[main,5,main]
|
||||||
|
java.lang.AssertionError: java.lang.reflect.InvocationTargetException
|
||||||
|
at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:75)
|
||||||
|
...
|
||||||
|
Caused by: java.lang.reflect.InvocationTargetException
|
||||||
|
at java.lang.reflect.Method.invoke(Native Method)
|
||||||
|
at com.genymobile.scrcpy.wrappers.SurfaceControl.setDisplaySurface(SurfaceControl.java:73)
|
||||||
|
... 7 more
|
||||||
|
Caused by: java.lang.IllegalArgumentException: displayToken must not be null
|
||||||
|
at android.view.SurfaceControl$Transaction.setDisplaySurface(SurfaceControl.java:3067)
|
||||||
|
at android.view.SurfaceControl.setDisplaySurface(SurfaceControl.java:2147)
|
||||||
|
... 9 more
|
||||||
|
```
|
||||||
|
|
||||||
|
[#2129]: https://github.com/Genymobile/scrcpy/issues/2129
|
||||||
|
|
||||||
|
|
||||||
|
## Command line on Windows
|
||||||
|
|
||||||
|
Since v1.22, a "shortcut" has been added to directly open a terminal in the
|
||||||
|
scrcpy directory. Double-click on `open_a_terminal_here.bat`, then type your
|
||||||
|
command. For example:
|
||||||
|
|
||||||
|
```
|
||||||
|
scrcpy --record file.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
You could also open a terminal and go to the scrcpy folder manually:
|
||||||
|
|
||||||
|
1. Press <kbd>Windows</kbd>+<kbd>r</kbd>, this opens a dialog box.
|
||||||
|
2. Type `cmd` and press <kbd>Enter</kbd>, this opens a terminal.
|
||||||
|
3. Go to your _scrcpy_ directory, by typing (adapt the path):
|
||||||
|
|
||||||
|
```bat
|
||||||
|
cd C:\Users\user\Downloads\scrcpy-win64-xxx
|
||||||
|
```
|
||||||
|
|
||||||
|
and press <kbd>Enter</kbd>
|
||||||
|
4. Type your command. For example:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
scrcpy --record file.mkv
|
||||||
|
```
|
||||||
|
|
||||||
|
If you plan to always use the same arguments, create a file `myscrcpy.bat`
|
||||||
|
(enable [show file extensions] to avoid confusion) in the `scrcpy` directory,
|
||||||
|
containing your command. For example:
|
||||||
|
|
||||||
|
```bat
|
||||||
|
scrcpy --prefer-text --turn-screen-off --stay-awake
|
||||||
|
```
|
||||||
|
|
||||||
|
Then just double-click on that file.
|
||||||
|
|
||||||
|
You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs`
|
||||||
|
to add some arguments.
|
||||||
|
|
||||||
|
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
|
||||||
|
|
||||||
|
|
||||||
## Translations
|
## Translations
|
||||||
|
2
LICENSE
2
LICENSE
@ -188,7 +188,7 @@
|
|||||||
identification within third-party archives.
|
identification within third-party archives.
|
||||||
|
|
||||||
Copyright (C) 2018 Genymobile
|
Copyright (C) 2018 Genymobile
|
||||||
Copyright (C) 2018-2023 Romain Vimont
|
Copyright (C) 2018-2022 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.
|
||||||
|
@ -45,7 +45,6 @@ _scrcpy() {
|
|||||||
-r --record=
|
-r --record=
|
||||||
--record-format=
|
--record-format=
|
||||||
--render-driver=
|
--render-driver=
|
||||||
--require-audio
|
|
||||||
--rotation=
|
--rotation=
|
||||||
-s --serial=
|
-s --serial=
|
||||||
--shortcut-mod=
|
--shortcut-mod=
|
||||||
@ -78,7 +77,7 @@ _scrcpy() {
|
|||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--audio-codec)
|
--audio-codec)
|
||||||
COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur"))
|
COMPREPLY=($(compgen -W 'opus aac' -- "$cur"))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--lock-video-orientation)
|
--lock-video-orientation)
|
||||||
|
@ -10,7 +10,7 @@ local arguments
|
|||||||
arguments=(
|
arguments=(
|
||||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||||
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
|
'--audio-codec=[Select the audio codec]:codec:(opus aac)'
|
||||||
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
||||||
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
||||||
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
|
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
|
||||||
@ -51,7 +51,6 @@ arguments=(
|
|||||||
{-r,--record=}'[Record screen to file]:record file:_files'
|
{-r,--record=}'[Record screen to file]:record file:_files'
|
||||||
'--record-format=[Force recording format]:format:(mp4 mkv)'
|
'--record-format=[Force recording format]:format:(mp4 mkv)'
|
||||||
'--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)'
|
'--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]'
|
|
||||||
'--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)'
|
'--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)'
|
||||||
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
|
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
|
||||||
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
|
||||||
|
@ -11,7 +11,6 @@ src = [
|
|||||||
'src/control_msg.c',
|
'src/control_msg.c',
|
||||||
'src/controller.c',
|
'src/controller.c',
|
||||||
'src/decoder.c',
|
'src/decoder.c',
|
||||||
'src/delay_buffer.c',
|
|
||||||
'src/demuxer.c',
|
'src/demuxer.c',
|
||||||
'src/device_msg.c',
|
'src/device_msg.c',
|
||||||
'src/icon.c',
|
'src/icon.c',
|
||||||
@ -30,8 +29,7 @@ src = [
|
|||||||
'src/screen.c',
|
'src/screen.c',
|
||||||
'src/server.c',
|
'src/server.c',
|
||||||
'src/version.c',
|
'src/version.c',
|
||||||
'src/trait/frame_source.c',
|
'src/video_buffer.c',
|
||||||
'src/trait/packet_source.c',
|
|
||||||
'src/util/acksync.c',
|
'src/util/acksync.c',
|
||||||
'src/util/average.c',
|
'src/util/average.c',
|
||||||
'src/util/bytebuf.c',
|
'src/util/bytebuf.c',
|
||||||
@ -39,7 +37,6 @@ src = [
|
|||||||
'src/util/intmap.c',
|
'src/util/intmap.c',
|
||||||
'src/util/intr.c',
|
'src/util/intr.c',
|
||||||
'src/util/log.c',
|
'src/util/log.c',
|
||||||
'src/util/memory.c',
|
|
||||||
'src/util/net.c',
|
'src/util/net.c',
|
||||||
'src/util/net_intr.c',
|
'src/util/net_intr.c',
|
||||||
'src/util/process.c',
|
'src/util/process.c',
|
||||||
@ -176,7 +173,6 @@ check_functions = [
|
|||||||
'vasprintf',
|
'vasprintf',
|
||||||
'nrand48',
|
'nrand48',
|
||||||
'jrand48',
|
'jrand48',
|
||||||
'reallocarray',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
foreach f : check_functions
|
foreach f : check_functions
|
||||||
@ -267,6 +263,9 @@ if get_option('buildtype') == 'debug'
|
|||||||
'tests/test_bytebuf.c',
|
'tests/test_bytebuf.c',
|
||||||
'src/util/bytebuf.c',
|
'src/util/bytebuf.c',
|
||||||
]],
|
]],
|
||||||
|
['test_cbuf', [
|
||||||
|
'tests/test_cbuf.c',
|
||||||
|
]],
|
||||||
['test_cli', [
|
['test_cli', [
|
||||||
'tests/test_cli.c',
|
'tests/test_cli.c',
|
||||||
'src/cli.c',
|
'src/cli.c',
|
||||||
@ -291,6 +290,9 @@ if get_option('buildtype') == 'debug'
|
|||||||
'tests/test_device_msg_deserialize.c',
|
'tests/test_device_msg_deserialize.c',
|
||||||
'src/device_msg.c',
|
'src/device_msg.c',
|
||||||
]],
|
]],
|
||||||
|
['test_queue', [
|
||||||
|
'tests/test_queue.c',
|
||||||
|
]],
|
||||||
['test_strbuf', [
|
['test_strbuf', [
|
||||||
'tests/test_strbuf.c',
|
'tests/test_strbuf.c',
|
||||||
'src/util/strbuf.c',
|
'src/util/strbuf.c',
|
||||||
@ -300,10 +302,6 @@ if get_option('buildtype') == 'debug'
|
|||||||
'src/util/str.c',
|
'src/util/str.c',
|
||||||
'src/util/strbuf.c',
|
'src/util/strbuf.c',
|
||||||
]],
|
]],
|
||||||
['test_vecdeque', [
|
|
||||||
'tests/test_vecdeque.c',
|
|
||||||
'src/util/memory.c',
|
|
||||||
]],
|
|
||||||
['test_vector', [
|
['test_vector', [
|
||||||
'tests/test_vector.c',
|
'tests/test_vector.c',
|
||||||
]],
|
]],
|
||||||
|
@ -6,10 +6,10 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
DEP_DIR=platform-tools-34.0.1
|
DEP_DIR=platform-tools-33.0.3
|
||||||
|
|
||||||
FILENAME=platform-tools_r34.0.1-windows.zip
|
FILENAME=platform-tools_r33.0.3-windows.zip
|
||||||
SHA256SUM=5dd9c2be744c224fa3a7cbe30ba02d2cb378c763bd0f797a7e47e9f3156a5daa
|
SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
then
|
then
|
||||||
|
@ -6,11 +6,11 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
VERSION=6.0-scrcpy-2
|
VERSION=6.0-scrcpy
|
||||||
DEP_DIR="ffmpeg-$VERSION"
|
DEP_DIR="ffmpeg-$VERSION"
|
||||||
|
|
||||||
FILENAME="$DEP_DIR".7z
|
FILENAME="$DEP_DIR".7z
|
||||||
SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14
|
SHA256SUM=f3956295b4325a84aada05447ba3f314fbed96697811666d495de4de40d59f98
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
then
|
then
|
||||||
|
@ -6,10 +6,10 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
DEP_DIR=SDL2-2.26.4
|
DEP_DIR=SDL2-2.26.1
|
||||||
|
|
||||||
FILENAME=SDL2-devel-2.26.4-mingw.tar.gz
|
FILENAME=SDL2-devel-2.26.1-mingw.tar.gz
|
||||||
SHA256SUM=fe899c8642caac2f180b1ee6f786857ddcaa0adc1fa82474312b09dd47d74712
|
SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
then
|
then
|
||||||
|
28
app/scrcpy.1
28
app/scrcpy.1
@ -23,32 +23,14 @@ Make scrcpy window always on top (above other windows).
|
|||||||
.BI "\-\-audio\-bit\-rate " value
|
.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).
|
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).
|
Default is 196K (196000).
|
||||||
|
|
||||||
.TP
|
|
||||||
.BI "\-\-audio\-buffer ms
|
|
||||||
Configure the audio buffering delay (in milliseconds).
|
|
||||||
|
|
||||||
Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches).
|
|
||||||
|
|
||||||
Default is 50.
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-audio\-codec " name
|
.BI "\-\-audio\-codec " name
|
||||||
Select an audio codec (opus, aac or raw).
|
Select an audio codec (opus or aac).
|
||||||
|
|
||||||
Default is opus.
|
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
|
|
||||||
.UR https://d.android.com/reference/android/media/MediaFormat
|
|
||||||
.UE .
|
|
||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-audio\-encoder " name
|
.BI "\-\-audio\-encoder " name
|
||||||
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
|
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
|
||||||
@ -280,10 +262,6 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me
|
|||||||
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
|
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
|
||||||
.UE
|
.UE
|
||||||
|
|
||||||
.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
|
.TP
|
||||||
.BI "\-\-rotation " value
|
.BI "\-\-rotation " value
|
||||||
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
|
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
|
||||||
@ -571,7 +549,7 @@ Copyright \(co 2018 Genymobile
|
|||||||
Genymobile
|
Genymobile
|
||||||
.UE
|
.UE
|
||||||
|
|
||||||
Copyright \(co 2018\-2023
|
Copyright \(co 2018\-2022
|
||||||
.MT rom@rom1v.com
|
.MT rom@rom1v.com
|
||||||
Romain Vimont
|
Romain Vimont
|
||||||
.ME
|
.ME
|
||||||
|
@ -1,129 +1,93 @@
|
|||||||
#include "audio_player.h"
|
#include "audio_player.h"
|
||||||
|
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
#include <libavutil/opt.h>
|
#include <libavutil/opt.h>
|
||||||
|
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
|
#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
|
||||||
|
|
||||||
/**
|
|
||||||
* Real-time audio player with configurable latency
|
|
||||||
*
|
|
||||||
* As input, the player regularly receives AVFrames of decoded audio samples.
|
|
||||||
* As output, an SDL callback 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 player is to feed the audio output with a latency as low as
|
|
||||||
* possible while avoiding buffer underrun (i.e. not being able to provide
|
|
||||||
* samples when requested).
|
|
||||||
*
|
|
||||||
* The player aims to feed the audio output with as little latency as possible
|
|
||||||
* while avoiding buffer underrun. 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 player 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 output callback). 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 player
|
|
||||||
* 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 player 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 player doesn't drop any sample on underflow. The compensation
|
|
||||||
* mechanism will absorb the delay introduced by the inserted silence.
|
|
||||||
*/
|
|
||||||
|
|
||||||
/** Downcast frame_sink to sc_audio_player */
|
/** Downcast frame_sink to sc_audio_player */
|
||||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
||||||
|
|
||||||
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
|
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
|
||||||
#define SC_SDL_SAMPLE_FMT AUDIO_F32
|
#define SC_SDL_SAMPLE_FMT AUDIO_F32
|
||||||
|
|
||||||
#define SC_AUDIO_OUTPUT_BUFFER_MS 5
|
#define SC_AUDIO_OUTPUT_BUFFER_SAMPLES 480 // 10ms at 48000Hz
|
||||||
|
|
||||||
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES))
|
// The target number of buffered samples between the producer and the consumer.
|
||||||
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES))
|
// This value is directly use for compensation.
|
||||||
|
#define SC_TARGET_BUFFERED_SAMPLES (3 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES)
|
||||||
|
|
||||||
static void SDLCALL
|
// If the consumer is too late, skip samples to keep at most this value
|
||||||
|
#define SC_BUFFERED_SAMPLES_THRESHOLD 2400 // 50ms at 48000Hz
|
||||||
|
|
||||||
|
// Use a ring-buffer of 1 second (at 48000Hz) between the producer and the
|
||||||
|
// consumer. It too big, but it guarantees that the producer and the consumer
|
||||||
|
// will be able to access it in parallel without locking.
|
||||||
|
#define SC_BYTEBUF_SIZE_IN_SAMPLES 48000
|
||||||
|
|
||||||
|
void
|
||||||
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
||||||
struct sc_audio_player *ap = userdata;
|
struct sc_audio_player *ap = userdata;
|
||||||
|
|
||||||
// This callback is called with the lock used by SDL_AudioDeviceLock(), so
|
// This callback is called with the lock used by SDL_AudioDeviceLock(), so
|
||||||
// the audiobuf is protected
|
// the bytebuf is protected
|
||||||
|
|
||||||
assert(len_int > 0);
|
assert(len_int > 0);
|
||||||
size_t len = len_int;
|
size_t len = len_int;
|
||||||
uint32_t count = TO_SAMPLES(len);
|
|
||||||
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
|
LOGD("[Audio] SDL callback requests %" SC_PRIsizet " samples",
|
||||||
|
len / (ap->nb_channels * ap->out_bytes_per_sample));
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
|
size_t read = sc_bytebuf_read_remaining(&ap->buf);
|
||||||
if (!ap->played) {
|
size_t max_buffered_bytes = SC_BUFFERED_SAMPLES_THRESHOLD
|
||||||
// Part of the buffering is handled by inserting initial silence. The
|
* ap->nb_channels * ap->out_bytes_per_sample;
|
||||||
// remaining (margin) last samples will be handled by compensation.
|
if (read > max_buffered_bytes + len) {
|
||||||
uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms
|
size_t skip = read - (max_buffered_bytes + len);
|
||||||
if (buffered_samples + margin < ap->target_buffering) {
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
|
LOGD("[Audio] Buffered samples threshold exceeded: %" SC_PRIsizet
|
||||||
" samples", count);
|
" bytes, skipping %" SC_PRIsizet " bytes", read, skip);
|
||||||
// Delay playback starting to reach the target buffering. Fill the
|
#endif
|
||||||
// whole buffer with silence (len is small compared to the
|
// After this callback, exactly max_buffered_bytes will remain
|
||||||
// arbitrary margin value).
|
sc_bytebuf_skip(&ap->buf, skip);
|
||||||
memset(stream, 0, len);
|
read = max_buffered_bytes + len;
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
uint32_t read = MIN(buffered_samples, count);
|
// Number of buffered samples (may be negative on underflow)
|
||||||
|
float buffered_samples = ((float) read - len_int)
|
||||||
|
/ (ap->nb_channels * ap->out_bytes_per_sample);
|
||||||
|
sc_average_push(&ap->avg_buffered_samples, buffered_samples);
|
||||||
|
|
||||||
if (read) {
|
if (read) {
|
||||||
sc_audiobuf_read(&ap->buf, stream, read);
|
if (read > len) {
|
||||||
}
|
read = len;
|
||||||
|
|
||||||
if (read < count) {
|
|
||||||
uint32_t silence = count - 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(stream + read, 0, TO_BYTES(silence));
|
|
||||||
|
|
||||||
if (ap->received) {
|
|
||||||
// Inserting additional samples immediately increases buffering
|
|
||||||
ap->underflow += silence;
|
|
||||||
}
|
}
|
||||||
|
sc_bytebuf_read(&ap->buf, stream, read);
|
||||||
}
|
}
|
||||||
|
|
||||||
ap->played = true;
|
if (read < len) {
|
||||||
|
// Insert silence
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGD("[Audio] Buffer underflow, inserting silence: %" SC_PRIsizet
|
||||||
|
" bytes", len - read);
|
||||||
|
#endif
|
||||||
|
memset(stream + read, 0, len - read);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static size_t
|
||||||
|
sc_audio_player_get_buf_size(struct sc_audio_player *ap, size_t samples) {
|
||||||
|
assert(ap->nb_channels);
|
||||||
|
assert(ap->out_bytes_per_sample);
|
||||||
|
return samples * ap->nb_channels * ap->out_bytes_per_sample;
|
||||||
}
|
}
|
||||||
|
|
||||||
static uint8_t *
|
static uint8_t *
|
||||||
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) {
|
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, size_t min_samples) {
|
||||||
size_t min_buf_size = TO_BYTES(min_samples);
|
size_t min_buf_size = sc_audio_player_get_buf_size(ap, min_samples);
|
||||||
if (min_buf_size > ap->swr_buf_alloc_size) {
|
if (min_buf_size < ap->swr_buf_alloc_size) {
|
||||||
size_t new_size = min_buf_size + 4096;
|
size_t new_size = min_buf_size + 4096;
|
||||||
uint8_t *buf = realloc(ap->swr_buf, new_size);
|
uint8_t *buf = realloc(ap->swr_buf, new_size);
|
||||||
if (!buf) {
|
if (!buf) {
|
||||||
@ -138,188 +102,6 @@ sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) {
|
|||||||
return ap->swr_buf;
|
return ap->swr_buf;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
|
||||||
const AVFrame *frame) {
|
|
||||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
|
||||||
|
|
||||||
SwrContext *swr_ctx = ap->swr_ctx;
|
|
||||||
|
|
||||||
int64_t swr_delay = swr_get_delay(swr_ctx, ap->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_player_get_swr_buf(ap, 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_written = MIN(ret, dst_nb_samples);
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
|
||||||
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Since this function is the only writer, the current available space is
|
|
||||||
// at least the previous available space. In practice, it should almost
|
|
||||||
// always be possible to write without lock.
|
|
||||||
bool lockless_write = samples_written <= ap->previous_can_write;
|
|
||||||
if (lockless_write) {
|
|
||||||
sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written);
|
|
||||||
}
|
|
||||||
|
|
||||||
SDL_LockAudioDevice(ap->device);
|
|
||||||
|
|
||||||
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
|
|
||||||
|
|
||||||
if (lockless_write) {
|
|
||||||
sc_audiobuf_commit_write(&ap->buf, samples_written);
|
|
||||||
} else {
|
|
||||||
uint32_t can_write = sc_audiobuf_can_write(&ap->buf);
|
|
||||||
if (samples_written > can_write) {
|
|
||||||
// Entering this branch is very unlikely, the audio buffer is
|
|
||||||
// allocated with a size sufficient to store 1 second more than the
|
|
||||||
// target buffering. If this happens, though, we have to skip old
|
|
||||||
// samples.
|
|
||||||
uint32_t cap = sc_audiobuf_capacity(&ap->buf);
|
|
||||||
if (samples_written > 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
|
|
||||||
swr_buf += TO_BYTES(samples_written - cap);
|
|
||||||
// This change in samples_written will impact the
|
|
||||||
// instant_compensation below
|
|
||||||
samples_written = cap;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(samples_written >= can_write);
|
|
||||||
if (samples_written > can_write) {
|
|
||||||
uint32_t skip_samples = samples_written - can_write;
|
|
||||||
assert(buffered_samples >= skip_samples);
|
|
||||||
sc_audiobuf_skip(&ap->buf, skip_samples);
|
|
||||||
buffered_samples -= skip_samples;
|
|
||||||
if (ap->played) {
|
|
||||||
// Dropping input samples instantly decreases buffering
|
|
||||||
ap->avg_buffering.avg -= skip_samples;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// It should remain exactly the expected size to write the new
|
|
||||||
// samples.
|
|
||||||
assert(sc_audiobuf_can_write(&ap->buf) == samples_written);
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_audiobuf_write(&ap->buf, swr_buf, samples_written);
|
|
||||||
}
|
|
||||||
|
|
||||||
buffered_samples += samples_written;
|
|
||||||
assert(buffered_samples == sc_audiobuf_can_read(&ap->buf));
|
|
||||||
|
|
||||||
// Read with lock held, to be used after unlocking
|
|
||||||
bool played = ap->played;
|
|
||||||
uint32_t underflow = ap->underflow;
|
|
||||||
|
|
||||||
if (played) {
|
|
||||||
uint32_t max_buffered_samples = ap->target_buffering
|
|
||||||
+ 12 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000
|
|
||||||
+ ap->target_buffering / 10;
|
|
||||||
if (buffered_samples > max_buffered_samples) {
|
|
||||||
uint32_t skip_samples = buffered_samples - max_buffered_samples;
|
|
||||||
sc_audiobuf_skip(&ap->buf, skip_samples);
|
|
||||||
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
|
|
||||||
" samples", skip_samples);
|
|
||||||
}
|
|
||||||
|
|
||||||
// reset (the current value was copied to a local variable)
|
|
||||||
ap->underflow = 0;
|
|
||||||
} else {
|
|
||||||
// SDL playback not started yet, do not accumulate more than
|
|
||||||
// max_initial_buffering samples, this would cause unnecessary delay
|
|
||||||
// (and glitches to compensate) on start.
|
|
||||||
uint32_t max_initial_buffering = ap->target_buffering
|
|
||||||
+ 2 * SC_AUDIO_OUTPUT_BUFFER_MS * ap->sample_rate / 1000;
|
|
||||||
if (buffered_samples > max_initial_buffering) {
|
|
||||||
uint32_t skip_samples = buffered_samples - max_initial_buffering;
|
|
||||||
sc_audiobuf_skip(&ap->buf, skip_samples);
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
|
||||||
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples",
|
|
||||||
skip_samples);
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf);
|
|
||||||
ap->received = true;
|
|
||||||
|
|
||||||
SDL_UnlockAudioDevice(ap->device);
|
|
||||||
|
|
||||||
if (played) {
|
|
||||||
// Number of samples added (or removed, if negative) for compensation
|
|
||||||
int32_t instant_compensation =
|
|
||||||
(int32_t) samples_written - frame->nb_samples;
|
|
||||||
int32_t inserted_silence = (int32_t) underflow;
|
|
||||||
|
|
||||||
// The compensation must apply instantly, it must not be smoothed
|
|
||||||
ap->avg_buffering.avg += instant_compensation + inserted_silence;
|
|
||||||
|
|
||||||
|
|
||||||
// However, the buffering level must be smoothed
|
|
||||||
sc_average_push(&ap->avg_buffering, buffered_samples);
|
|
||||||
|
|
||||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
|
||||||
LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f",
|
|
||||||
buffered_samples, sc_average_get(&ap->avg_buffering));
|
|
||||||
#endif
|
|
||||||
|
|
||||||
ap->samples_since_resync += samples_written;
|
|
||||||
if (ap->samples_since_resync >= ap->sample_rate) {
|
|
||||||
// Recompute compensation every second
|
|
||||||
ap->samples_since_resync = 0;
|
|
||||||
|
|
||||||
float avg = sc_average_get(&ap->avg_buffering);
|
|
||||||
int diff = ap->target_buffering - avg;
|
|
||||||
if (abs(diff) < ap->sample_rate / 1000) {
|
|
||||||
// Do not compensate for less than 1ms, the error is just noise
|
|
||||||
diff = 0;
|
|
||||||
} else if (diff < 0 && buffered_samples < ap->target_buffering) {
|
|
||||||
// Do not accelerate if the instant buffering level is below
|
|
||||||
// the average, this would increase underflow
|
|
||||||
diff = 0;
|
|
||||||
}
|
|
||||||
// Compensate the diff over 4 seconds (but will be recomputed after
|
|
||||||
// 1 second)
|
|
||||||
int distance = 4 * ap->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", ap->target_buffering, avg,
|
|
||||||
buffered_samples, diff);
|
|
||||||
|
|
||||||
if (diff != ap->compensation) {
|
|
||||||
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
|
||||||
if (ret < 0) {
|
|
||||||
LOGW("Resampling compensation failed: %d", ret);
|
|
||||||
// not fatal
|
|
||||||
} else {
|
|
||||||
ap->compensation = diff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||||
const AVCodecContext *ctx) {
|
const AVCodecContext *ctx) {
|
||||||
@ -337,7 +119,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
|||||||
.freq = ctx->sample_rate,
|
.freq = ctx->sample_rate,
|
||||||
.format = SC_SDL_SAMPLE_FMT,
|
.format = SC_SDL_SAMPLE_FMT,
|
||||||
.channels = nb_channels,
|
.channels = nb_channels,
|
||||||
.samples = SC_AUDIO_OUTPUT_BUFFER_MS * ctx->sample_rate / 1000,
|
.samples = SC_AUDIO_OUTPUT_BUFFER_SAMPLES,
|
||||||
.callback = sc_audio_player_sdl_callback,
|
.callback = sc_audio_player_sdl_callback,
|
||||||
.userdata = ap,
|
.userdata = ap,
|
||||||
};
|
};
|
||||||
@ -358,6 +140,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
|||||||
|
|
||||||
assert(ctx->sample_rate > 0);
|
assert(ctx->sample_rate > 0);
|
||||||
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
||||||
|
|
||||||
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
||||||
assert(out_bytes_per_sample > 0);
|
assert(out_bytes_per_sample > 0);
|
||||||
|
|
||||||
@ -387,55 +170,33 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
|||||||
ap->nb_channels = nb_channels;
|
ap->nb_channels = nb_channels;
|
||||||
ap->out_bytes_per_sample = out_bytes_per_sample;
|
ap->out_bytes_per_sample = out_bytes_per_sample;
|
||||||
|
|
||||||
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
|
size_t bytebuf_size =
|
||||||
/ SC_TICK_FREQ;
|
sc_audio_player_get_buf_size(ap, SC_BYTEBUF_SIZE_IN_SAMPLES);
|
||||||
|
|
||||||
// Use a ring-buffer of the target buffering size plus 1 second between the
|
bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size);
|
||||||
// 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.
|
|
||||||
size_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
|
|
||||||
|
|
||||||
size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample;
|
|
||||||
bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples);
|
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
goto error_free_swr_ctx;
|
goto error_free_swr_ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
size_t initial_swr_buf_size = TO_BYTES(4096);
|
ap->safe_empty_buffer = sc_bytebuf_write_remaining(&ap->buf);
|
||||||
|
|
||||||
|
size_t initial_swr_buf_size = sc_audio_player_get_buf_size(ap, 4096);
|
||||||
ap->swr_buf = malloc(initial_swr_buf_size);
|
ap->swr_buf = malloc(initial_swr_buf_size);
|
||||||
if (!ap->swr_buf) {
|
if (!ap->swr_buf) {
|
||||||
LOG_OOM();
|
LOG_OOM();
|
||||||
goto error_destroy_audiobuf;
|
goto error_destroy_bytebuf;
|
||||||
}
|
}
|
||||||
ap->swr_buf_alloc_size = initial_swr_buf_size;
|
ap->swr_buf_alloc_size = initial_swr_buf_size;
|
||||||
|
|
||||||
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf);
|
sc_average_init(&ap->avg_buffered_samples, 32);
|
||||||
|
|
||||||
// Samples are produced and consumed by blocks, so the buffering must be
|
|
||||||
// smoothed to get a relatively stable value.
|
|
||||||
sc_average_init(&ap->avg_buffering, 32);
|
|
||||||
ap->samples_since_resync = 0;
|
ap->samples_since_resync = 0;
|
||||||
|
|
||||||
ap->received = false;
|
|
||||||
ap->played = false;
|
|
||||||
ap->underflow = 0;
|
|
||||||
ap->compensation = 0;
|
|
||||||
|
|
||||||
// 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);
|
SDL_PauseAudioDevice(ap->device, 0);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
error_destroy_audiobuf:
|
error_destroy_bytebuf:
|
||||||
sc_audiobuf_destroy(&ap->buf);
|
sc_bytebuf_destroy(&ap->buf);
|
||||||
error_free_swr_ctx:
|
error_free_swr_ctx:
|
||||||
swr_free(&ap->swr_ctx);
|
swr_free(&ap->swr_ctx);
|
||||||
error_close_audio_device:
|
error_close_audio_device:
|
||||||
@ -453,14 +214,86 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
|
|||||||
SDL_CloseAudioDevice(ap->device);
|
SDL_CloseAudioDevice(ap->device);
|
||||||
|
|
||||||
free(ap->swr_buf);
|
free(ap->swr_buf);
|
||||||
sc_audiobuf_destroy(&ap->buf);
|
sc_bytebuf_destroy(&ap->buf);
|
||||||
swr_free(&ap->swr_ctx);
|
swr_free(&ap->swr_ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
static bool
|
||||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering) {
|
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||||
ap->target_buffering_delay = target_buffering;
|
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||||
|
|
||||||
|
SwrContext *swr_ctx = ap->swr_ctx;
|
||||||
|
|
||||||
|
int64_t delay = swr_get_delay(swr_ctx, ap->sample_rate);
|
||||||
|
// No need to av_rescale_rnd(), input and output sample rates are the same
|
||||||
|
int dst_nb_samples = delay + frame->nb_samples;
|
||||||
|
|
||||||
|
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, frame->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;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t samples_written = ret;
|
||||||
|
size_t swr_buf_size = sc_audio_player_get_buf_size(ap, samples_written);
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGI("[Audio] %" SC_PRIsizet " samples written to buffer", samples_written);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// It should almost always be possible to write without lock
|
||||||
|
bool can_write_without_lock = swr_buf_size <= ap->safe_empty_buffer;
|
||||||
|
if (can_write_without_lock) {
|
||||||
|
sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
SDL_LockAudioDevice(ap->device);
|
||||||
|
if (can_write_without_lock) {
|
||||||
|
sc_bytebuf_commit_write(&ap->buf, swr_buf_size);
|
||||||
|
} else {
|
||||||
|
sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The next time, it will remain at least the current empty space
|
||||||
|
ap->safe_empty_buffer = sc_bytebuf_write_remaining(&ap->buf);
|
||||||
|
|
||||||
|
// Read the value written by the SDL thread under lock
|
||||||
|
float avg;
|
||||||
|
bool has_avg = sc_average_get(&ap->avg_buffered_samples, &avg);
|
||||||
|
|
||||||
|
SDL_UnlockAudioDevice(ap->device);
|
||||||
|
|
||||||
|
if (has_avg) {
|
||||||
|
ap->samples_since_resync += samples_written;
|
||||||
|
if (ap->samples_since_resync >= ap->sample_rate) {
|
||||||
|
// Resync every second
|
||||||
|
ap->samples_since_resync = 0;
|
||||||
|
|
||||||
|
int diff = SC_TARGET_BUFFERED_SAMPLES - avg;
|
||||||
|
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||||
|
LOGI("[Audio] Average buffered samples = %f, compensation %d",
|
||||||
|
avg, diff);
|
||||||
|
#endif
|
||||||
|
// Compensate the diff over 3 seconds (but will be recomputed after
|
||||||
|
// 1 second)
|
||||||
|
int ret = swr_set_compensation(swr_ctx, diff, 3 * ap->sample_rate);
|
||||||
|
if (ret < 0) {
|
||||||
|
LOGW("Resampling compensation failed: %d", ret);
|
||||||
|
// not fatal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_audio_player_init(struct sc_audio_player *ap) {
|
||||||
static const struct sc_frame_sink_ops ops = {
|
static const struct sc_frame_sink_ops ops = {
|
||||||
.open = sc_audio_player_frame_sink_open,
|
.open = sc_audio_player_frame_sink_open,
|
||||||
.close = sc_audio_player_frame_sink_close,
|
.close = sc_audio_player_frame_sink_close,
|
||||||
|
@ -5,10 +5,9 @@
|
|||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include "trait/frame_sink.h"
|
#include "trait/frame_sink.h"
|
||||||
#include <util/audiobuf.h>
|
|
||||||
#include <util/average.h>
|
#include <util/average.h>
|
||||||
|
#include <util/bytebuf.h>
|
||||||
#include <util/thread.h>
|
#include <util/thread.h>
|
||||||
#include <util/tick.h>
|
|
||||||
|
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
#include <libswresample/swresample.h>
|
#include <libswresample/swresample.h>
|
||||||
@ -19,57 +18,27 @@ struct sc_audio_player {
|
|||||||
|
|
||||||
SDL_AudioDeviceID device;
|
SDL_AudioDeviceID device;
|
||||||
|
|
||||||
// The target buffering between the producer and the consumer. This value
|
// protected by SDL_AudioDeviceLock()
|
||||||
// is directly use for compensation.
|
struct sc_bytebuf buf;
|
||||||
// Since audio capture and/or encoding on the device typically produce
|
// Number of bytes which could be written without locking
|
||||||
// blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target
|
size_t safe_empty_buffer;
|
||||||
// value should be higher.
|
|
||||||
sc_tick target_buffering_delay;
|
|
||||||
uint32_t target_buffering; // in samples
|
|
||||||
|
|
||||||
// Audio buffer to communicate between the receiver and the SDL audio
|
|
||||||
// callback (protected by SDL_AudioDeviceLock())
|
|
||||||
struct sc_audiobuf buf;
|
|
||||||
|
|
||||||
// The previous empty space in the buffer (only used by the receiver
|
|
||||||
// thread)
|
|
||||||
uint32_t previous_can_write;
|
|
||||||
|
|
||||||
// Resampler (only used from the receiver thread)
|
|
||||||
struct SwrContext *swr_ctx;
|
struct SwrContext *swr_ctx;
|
||||||
|
|
||||||
// The sample rate is the same for input and output
|
// The sample rate is the same for input and output
|
||||||
unsigned sample_rate;
|
unsigned sample_rate;
|
||||||
// The number of channels is the same for input and output
|
// The number of channels is the same for input and output
|
||||||
unsigned nb_channels;
|
unsigned nb_channels;
|
||||||
// The number of bytes per sample for a single channel
|
|
||||||
unsigned out_bytes_per_sample;
|
unsigned out_bytes_per_sample;
|
||||||
|
|
||||||
// Target buffer for resampling (only used by the receiver thread)
|
// Target buffer for resampling
|
||||||
uint8_t *swr_buf;
|
uint8_t *swr_buf;
|
||||||
size_t swr_buf_alloc_size;
|
size_t swr_buf_alloc_size;
|
||||||
|
|
||||||
// Number of buffered samples (may be negative on underflow) (only used by
|
// Number of buffered samples (may be negative on underflow)
|
||||||
// the receiver thread)
|
struct sc_average avg_buffered_samples;
|
||||||
struct sc_average avg_buffering;
|
unsigned samples_since_resync;
|
||||||
// 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
|
|
||||||
// (protected by SDL_AudioDeviceLock())
|
|
||||||
uint32_t underflow;
|
|
||||||
|
|
||||||
// Current applied compensation value (only used by the receiver thread)
|
|
||||||
int compensation;
|
|
||||||
|
|
||||||
// Set to true the first time a sample is received (protected by
|
|
||||||
// SDL_AudioDeviceLock())
|
|
||||||
bool received;
|
|
||||||
|
|
||||||
// Set to true the first time the SDL callback is called (protected by
|
|
||||||
// SDL_AudioDeviceLock())
|
|
||||||
bool played;
|
|
||||||
|
|
||||||
const struct sc_audio_player_callbacks *cbs;
|
const struct sc_audio_player_callbacks *cbs;
|
||||||
void *cbs_userdata;
|
void *cbs_userdata;
|
||||||
@ -80,6 +49,6 @@ struct sc_audio_player_callbacks {
|
|||||||
};
|
};
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering);
|
sc_audio_player_init(struct sc_audio_player *ap);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -18,7 +18,7 @@
|
|||||||
#define STR(x) STR_IMPL_(x)
|
#define STR(x) STR_IMPL_(x)
|
||||||
|
|
||||||
enum {
|
enum {
|
||||||
OPT_BIT_RATE = 1000,
|
OPT_RENDER_EXPIRED_FRAMES = 1000,
|
||||||
OPT_WINDOW_TITLE,
|
OPT_WINDOW_TITLE,
|
||||||
OPT_PUSH_TARGET,
|
OPT_PUSH_TARGET,
|
||||||
OPT_ALWAYS_ON_TOP,
|
OPT_ALWAYS_ON_TOP,
|
||||||
@ -69,8 +69,6 @@ enum {
|
|||||||
OPT_AUDIO_ENCODER,
|
OPT_AUDIO_ENCODER,
|
||||||
OPT_LIST_ENCODERS,
|
OPT_LIST_ENCODERS,
|
||||||
OPT_LIST_DISPLAYS,
|
OPT_LIST_DISPLAYS,
|
||||||
OPT_REQUIRE_AUDIO,
|
|
||||||
OPT_AUDIO_BUFFER,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_option {
|
struct sc_option {
|
||||||
@ -118,22 +116,13 @@ static const struct sc_option options[] = {
|
|||||||
.argdesc = "value",
|
.argdesc = "value",
|
||||||
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
|
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
|
||||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
"Default is 128K (128000).",
|
"Default is 196K (196000).",
|
||||||
},
|
|
||||||
{
|
|
||||||
.longopt_id = OPT_AUDIO_BUFFER,
|
|
||||||
.longopt = "audio-buffer",
|
|
||||||
.argdesc = "ms",
|
|
||||||
.text = "Configure the audio buffering delay (in milliseconds).\n"
|
|
||||||
"Lower values decrease the latency, but increase the "
|
|
||||||
"likelyhood of buffer underrun (causing audio glitches).\n"
|
|
||||||
"Default is 50.",
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_AUDIO_CODEC,
|
.longopt_id = OPT_AUDIO_CODEC,
|
||||||
.longopt = "audio-codec",
|
.longopt = "audio-codec",
|
||||||
.argdesc = "name",
|
.argdesc = "name",
|
||||||
.text = "Select an audio codec (opus, aac or raw).\n"
|
.text = "Select an audio codec (opus or aac).\n"
|
||||||
"Default is opus.",
|
"Default is opus.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -164,12 +153,6 @@ static const struct sc_option options[] = {
|
|||||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||||
"Default is 8M (8000000).",
|
"Default is 8M (8000000).",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
// deprecated
|
|
||||||
.longopt_id = OPT_BIT_RATE,
|
|
||||||
.longopt = "bit-rate",
|
|
||||||
.argdesc = "value",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// Not really deprecated (--codec has never been released), but without
|
// Not really deprecated (--codec has never been released), but without
|
||||||
// declaring an explicit --codec option, getopt_long() partial matching
|
// declaring an explicit --codec option, getopt_long() partial matching
|
||||||
@ -471,11 +454,9 @@ static const struct sc_option options[] = {
|
|||||||
"<https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>",
|
"<https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_REQUIRE_AUDIO,
|
// deprecated
|
||||||
.longopt = "require-audio",
|
.longopt_id = OPT_RENDER_EXPIRED_FRAMES,
|
||||||
.text = "By default, scrcpy mirrors only the video when audio capture "
|
.longopt = "render-expired-frames",
|
||||||
"fails on the device. This option makes scrcpy fail if audio "
|
|
||||||
"is enabled but does not work."
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
.longopt_id = OPT_ROTATION,
|
.longopt_id = OPT_ROTATION,
|
||||||
@ -1516,11 +1497,7 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
|||||||
*codec = SC_CODEC_AAC;
|
*codec = SC_CODEC_AAC;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (!strcmp(optarg, "raw")) {
|
LOGE("Unsupported audio codec: %s (expected opus)", optarg);
|
||||||
*codec = SC_CODEC_RAW;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1534,10 +1511,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
int c;
|
int c;
|
||||||
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
|
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case OPT_BIT_RATE:
|
|
||||||
LOGE("--bit-rate has been removed, "
|
|
||||||
"use --video-bit-rate or --audio-bit-rate.");
|
|
||||||
return false;
|
|
||||||
case 'b':
|
case 'b':
|
||||||
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
|
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
|
||||||
return false;
|
return false;
|
||||||
@ -1565,6 +1538,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
case 'f':
|
case 'f':
|
||||||
opts->fullscreen = true;
|
opts->fullscreen = true;
|
||||||
break;
|
break;
|
||||||
|
case 'F':
|
||||||
|
LOGW("Deprecated option -F. Use --record-format instead.");
|
||||||
|
// fall through
|
||||||
case OPT_RECORD_FORMAT:
|
case OPT_RECORD_FORMAT:
|
||||||
if (!parse_record_format(optarg, &opts->record_format)) {
|
if (!parse_record_format(optarg, &opts->record_format)) {
|
||||||
return false;
|
return false;
|
||||||
@ -1652,6 +1628,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
case 'w':
|
case 'w':
|
||||||
opts->stay_awake = true;
|
opts->stay_awake = true;
|
||||||
break;
|
break;
|
||||||
|
case OPT_RENDER_EXPIRED_FRAMES:
|
||||||
|
LOGW("Option --render-expired-frames has been removed. This "
|
||||||
|
"flag has been ignored.");
|
||||||
|
break;
|
||||||
case OPT_WINDOW_TITLE:
|
case OPT_WINDOW_TITLE:
|
||||||
opts->window_title = optarg;
|
opts->window_title = optarg;
|
||||||
break;
|
break;
|
||||||
@ -1710,9 +1690,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->forward_key_repeat = false;
|
opts->forward_key_repeat = false;
|
||||||
break;
|
break;
|
||||||
case OPT_CODEC_OPTIONS:
|
case OPT_CODEC_OPTIONS:
|
||||||
LOGE("--codec-options has been removed, "
|
LOGW("--codec-options is deprecated, use --video-codec-options "
|
||||||
"use --video-codec-options or --audio-codec-options.");
|
"instead.");
|
||||||
return false;
|
// fall through
|
||||||
case OPT_VIDEO_CODEC_OPTIONS:
|
case OPT_VIDEO_CODEC_OPTIONS:
|
||||||
opts->video_codec_options = optarg;
|
opts->video_codec_options = optarg;
|
||||||
break;
|
break;
|
||||||
@ -1720,9 +1700,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->audio_codec_options = optarg;
|
opts->audio_codec_options = optarg;
|
||||||
break;
|
break;
|
||||||
case OPT_ENCODER:
|
case OPT_ENCODER:
|
||||||
LOGE("--encoder has been removed, "
|
LOGW("--encoder is deprecated, use --video-encoder instead.");
|
||||||
"use --video-encoder or --audio-encoder.");
|
// fall through
|
||||||
return false;
|
|
||||||
case OPT_VIDEO_ENCODER:
|
case OPT_VIDEO_ENCODER:
|
||||||
opts->video_encoder = optarg;
|
opts->video_encoder = optarg;
|
||||||
break;
|
break;
|
||||||
@ -1777,9 +1756,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->start_fps_counter = true;
|
opts->start_fps_counter = true;
|
||||||
break;
|
break;
|
||||||
case OPT_CODEC:
|
case OPT_CODEC:
|
||||||
LOGE("--codec has been removed, "
|
LOGW("--codec is deprecated, use --video-codec instead.");
|
||||||
"use --video-codec or --audio-codec.");
|
// fall through
|
||||||
return false;
|
|
||||||
case OPT_VIDEO_CODEC:
|
case OPT_VIDEO_CODEC:
|
||||||
if (!parse_video_codec(optarg, &opts->video_codec)) {
|
if (!parse_video_codec(optarg, &opts->video_codec)) {
|
||||||
return false;
|
return false;
|
||||||
@ -1823,14 +1801,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
case OPT_LIST_DISPLAYS:
|
case OPT_LIST_DISPLAYS:
|
||||||
opts->list_displays = true;
|
opts->list_displays = true;
|
||||||
break;
|
break;
|
||||||
case OPT_REQUIRE_AUDIO:
|
|
||||||
opts->require_audio = true;
|
|
||||||
break;
|
|
||||||
case OPT_AUDIO_BUFFER:
|
|
||||||
if (!parse_buffering_time(optarg, &opts->audio_buffer)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
default:
|
||||||
// getopt prints the error message on stderr
|
// getopt prints the error message on stderr
|
||||||
return false;
|
return false;
|
||||||
@ -1891,11 +1861,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (opts->audio && !opts->display && !opts->record_filename) {
|
|
||||||
LOGI("No display and no recording: audio disabled");
|
|
||||||
opts->audio = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
|
if ((opts->tunnel_host || opts->tunnel_port) && !opts->force_adb_forward) {
|
||||||
LOGI("Tunnel host/port is set, "
|
LOGI("Tunnel host/port is set, "
|
||||||
"--force-adb-forward automatically enabled.");
|
"--force-adb-forward automatically enabled.");
|
||||||
@ -1917,23 +1882,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts->record_filename && opts->audio_codec == SC_CODEC_RAW) {
|
|
||||||
LOGW("Recording does not support RAW audio codec");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (opts->audio_codec == SC_CODEC_RAW) {
|
|
||||||
if (opts->audio_bit_rate) {
|
|
||||||
LOGW("--audio-bit-rate is ignored for raw audio codec");
|
|
||||||
}
|
|
||||||
if (opts->audio_codec_options) {
|
|
||||||
LOGW("--audio-codec-options is ignored for raw audio codec");
|
|
||||||
}
|
|
||||||
if (opts->audio_encoder) {
|
|
||||||
LOGW("--audio-encoder is ignored for raw audio codec");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!opts->control) {
|
if (!opts->control) {
|
||||||
if (opts->turn_screen_off) {
|
if (opts->turn_screen_off) {
|
||||||
LOGE("Could not request to turn screen off if control is disabled");
|
LOGE("Could not request to turn screen off if control is disabled");
|
||||||
|
@ -18,15 +18,7 @@ sc_clock_init(struct sc_clock *clock) {
|
|||||||
static void
|
static void
|
||||||
sc_clock_estimate(struct sc_clock *clock,
|
sc_clock_estimate(struct sc_clock *clock,
|
||||||
double *out_slope, sc_tick *out_offset) {
|
double *out_slope, sc_tick *out_offset) {
|
||||||
assert(clock->count);
|
assert(clock->count > 1); // two points are necessary
|
||||||
|
|
||||||
if (clock->count == 1) {
|
|
||||||
// If there is only 1 point, we can't compute a slope. Assume it is 1.
|
|
||||||
struct sc_clock_point *single_point = &clock->right_sum;
|
|
||||||
*out_slope = 1;
|
|
||||||
*out_offset = single_point->system - single_point->stream;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct sc_clock_point left_avg = {
|
struct sc_clock_point left_avg = {
|
||||||
.system = clock->left_sum.system / (clock->count / 2),
|
.system = clock->left_sum.system / (clock->count / 2),
|
||||||
@ -101,16 +93,19 @@ sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
|||||||
|
|
||||||
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
|
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
|
||||||
|
|
||||||
// Update estimation
|
if (clock->count > 1) {
|
||||||
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
// Update estimation
|
||||||
|
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
||||||
|
|
||||||
#ifndef SC_CLOCK_NDEBUG
|
#ifndef SC_CLOCK_NDEBUG
|
||||||
LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset);
|
LOGD("Clock estimation: %f * pts + %" PRItick,
|
||||||
|
clock->slope, clock->offset);
|
||||||
#endif
|
#endif
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_tick
|
sc_tick
|
||||||
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
||||||
assert(clock->count); // sc_clock_update() must have been called
|
assert(clock->count > 1); // sc_clock_update() must have been called
|
||||||
return (sc_tick) (stream * clock->slope) + clock->offset;
|
return (sc_tick) (stream * clock->slope) + clock->offset;
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@
|
|||||||
#include "compat.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 CLAMP(V,X,Y) MIN( MAX((V),(X)), (Y) )
|
||||||
|
|
||||||
#define container_of(ptr, type, member) \
|
#define container_of(ptr, type, member) \
|
||||||
|
@ -3,9 +3,6 @@
|
|||||||
#include "config.h"
|
#include "config.h"
|
||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#ifndef HAVE_REALLOCARRAY
|
|
||||||
# include <errno.h>
|
|
||||||
#endif
|
|
||||||
#include <stdlib.h>
|
#include <stdlib.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
@ -96,15 +93,5 @@ long jrand48(unsigned short xsubi[3]) {
|
|||||||
return v.i;
|
return v.i;
|
||||||
}
|
}
|
||||||
#endif
|
#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
|
#endif
|
||||||
|
@ -54,10 +54,6 @@
|
|||||||
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
|
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#if SDL_VERSION_ATLEAST(2, 0, 16)
|
|
||||||
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#ifndef HAVE_STRDUP
|
#ifndef HAVE_STRDUP
|
||||||
char *strdup(const char *s);
|
char *strdup(const char *s);
|
||||||
#endif
|
#endif
|
||||||
@ -78,8 +74,4 @@ long nrand48(unsigned short xsubi[3]);
|
|||||||
long jrand48(unsigned short xsubi[3]);
|
long jrand48(unsigned short xsubi[3]);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifndef HAVE_REALLOCARRAY
|
|
||||||
void *reallocarray(void *ptr, size_t nmemb, size_t size);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -4,28 +4,19 @@
|
|||||||
|
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
#define SC_CONTROL_MSG_QUEUE_MAX 64
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
||||||
struct sc_acksync *acksync) {
|
struct sc_acksync *acksync) {
|
||||||
sc_vecdeque_init(&controller->queue);
|
cbuf_init(&controller->queue);
|
||||||
|
|
||||||
bool ok = sc_vecdeque_reserve(&controller->queue, SC_CONTROL_MSG_QUEUE_MAX);
|
bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
|
|
||||||
if (!ok) {
|
|
||||||
sc_vecdeque_destroy(&controller->queue);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ok = sc_mutex_init(&controller->mutex);
|
ok = sc_mutex_init(&controller->mutex);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
sc_receiver_destroy(&controller->receiver);
|
sc_receiver_destroy(&controller->receiver);
|
||||||
sc_vecdeque_destroy(&controller->queue);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -33,7 +24,6 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
|||||||
if (!ok) {
|
if (!ok) {
|
||||||
sc_receiver_destroy(&controller->receiver);
|
sc_receiver_destroy(&controller->receiver);
|
||||||
sc_mutex_destroy(&controller->mutex);
|
sc_mutex_destroy(&controller->mutex);
|
||||||
sc_vecdeque_destroy(&controller->queue);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,12 +38,10 @@ sc_controller_destroy(struct sc_controller *controller) {
|
|||||||
sc_cond_destroy(&controller->msg_cond);
|
sc_cond_destroy(&controller->msg_cond);
|
||||||
sc_mutex_destroy(&controller->mutex);
|
sc_mutex_destroy(&controller->mutex);
|
||||||
|
|
||||||
while (!sc_vecdeque_is_empty(&controller->queue)) {
|
struct sc_control_msg msg;
|
||||||
struct sc_control_msg *msg = sc_vecdeque_popref(&controller->queue);
|
while (cbuf_take(&controller->queue, &msg)) {
|
||||||
assert(msg);
|
sc_control_msg_destroy(&msg);
|
||||||
sc_control_msg_destroy(msg);
|
|
||||||
}
|
}
|
||||||
sc_vecdeque_destroy(&controller->queue);
|
|
||||||
|
|
||||||
sc_receiver_destroy(&controller->receiver);
|
sc_receiver_destroy(&controller->receiver);
|
||||||
}
|
}
|
||||||
@ -66,19 +54,13 @@ sc_controller_push_msg(struct sc_controller *controller,
|
|||||||
}
|
}
|
||||||
|
|
||||||
sc_mutex_lock(&controller->mutex);
|
sc_mutex_lock(&controller->mutex);
|
||||||
bool full = sc_vecdeque_is_full(&controller->queue);
|
bool was_empty = cbuf_is_empty(&controller->queue);
|
||||||
if (!full) {
|
bool res = cbuf_push(&controller->queue, *msg);
|
||||||
bool was_empty = sc_vecdeque_is_empty(&controller->queue);
|
if (was_empty) {
|
||||||
sc_vecdeque_push_noresize(&controller->queue, *msg);
|
sc_cond_signal(&controller->msg_cond);
|
||||||
if (was_empty) {
|
|
||||||
sc_cond_signal(&controller->msg_cond);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Otherwise (if the queue is full), the msg is discarded
|
|
||||||
|
|
||||||
sc_mutex_unlock(&controller->mutex);
|
sc_mutex_unlock(&controller->mutex);
|
||||||
|
return res;
|
||||||
return !full;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@ -100,8 +82,7 @@ run_controller(void *data) {
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
sc_mutex_lock(&controller->mutex);
|
sc_mutex_lock(&controller->mutex);
|
||||||
while (!controller->stopped
|
while (!controller->stopped && cbuf_is_empty(&controller->queue)) {
|
||||||
&& sc_vecdeque_is_empty(&controller->queue)) {
|
|
||||||
sc_cond_wait(&controller->msg_cond, &controller->mutex);
|
sc_cond_wait(&controller->msg_cond, &controller->mutex);
|
||||||
}
|
}
|
||||||
if (controller->stopped) {
|
if (controller->stopped) {
|
||||||
@ -109,9 +90,10 @@ run_controller(void *data) {
|
|||||||
sc_mutex_unlock(&controller->mutex);
|
sc_mutex_unlock(&controller->mutex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
struct sc_control_msg msg;
|
||||||
assert(!sc_vecdeque_is_empty(&controller->queue));
|
bool non_empty = cbuf_take(&controller->queue, &msg);
|
||||||
struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue);
|
assert(non_empty);
|
||||||
|
(void) non_empty;
|
||||||
sc_mutex_unlock(&controller->mutex);
|
sc_mutex_unlock(&controller->mutex);
|
||||||
|
|
||||||
bool ok = process_msg(controller, &msg);
|
bool ok = process_msg(controller, &msg);
|
||||||
|
@ -8,11 +8,11 @@
|
|||||||
#include "control_msg.h"
|
#include "control_msg.h"
|
||||||
#include "receiver.h"
|
#include "receiver.h"
|
||||||
#include "util/acksync.h"
|
#include "util/acksync.h"
|
||||||
|
#include "util/cbuf.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
#include "util/vecdeque.h"
|
|
||||||
|
|
||||||
struct sc_control_msg_queue SC_VECDEQUE(struct sc_control_msg);
|
struct sc_control_msg_queue CBUF(struct sc_control_msg, 64);
|
||||||
|
|
||||||
struct sc_controller {
|
struct sc_controller {
|
||||||
sc_socket control_socket;
|
sc_socket control_socket;
|
||||||
|
@ -5,34 +5,106 @@
|
|||||||
#include <libavutil/channel_layout.h>
|
#include <libavutil/channel_layout.h>
|
||||||
|
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
|
#include "video_buffer.h"
|
||||||
#include "trait/frame_sink.h"
|
#include "trait/frame_sink.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
/** Downcast packet_sink to decoder */
|
/** Downcast packet_sink to decoder */
|
||||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
|
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_decoder_close_first_sinks(struct sc_decoder *decoder, unsigned count) {
|
||||||
|
while (count) {
|
||||||
|
struct sc_frame_sink *sink = decoder->sinks[--count];
|
||||||
|
sink->ops->close(sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
sc_decoder_close_sinks(struct sc_decoder *decoder) {
|
||||||
|
sc_decoder_close_first_sinks(decoder, decoder->sink_count);
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) {
|
sc_decoder_open_sinks(struct sc_decoder *decoder, const AVCodecContext *ctx) {
|
||||||
decoder->frame = av_frame_alloc();
|
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
||||||
if (!decoder->frame) {
|
struct sc_frame_sink *sink = decoder->sinks[i];
|
||||||
|
if (!sink->ops->open(sink, ctx)) {
|
||||||
|
sc_decoder_close_first_sinks(decoder, i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
||||||
|
decoder->codec_ctx = avcodec_alloc_context3(codec);
|
||||||
|
if (!decoder->codec_ctx) {
|
||||||
LOG_OOM();
|
LOG_OOM();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) {
|
decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||||
av_frame_free(&decoder->frame);
|
|
||||||
|
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||||
|
// Hardcoded video properties
|
||||||
|
decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||||
|
} else {
|
||||||
|
// Hardcoded audio properties
|
||||||
|
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||||
|
decoder->codec_ctx->ch_layout =
|
||||||
|
(AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
|
||||||
|
#else
|
||||||
|
decoder->codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||||
|
decoder->codec_ctx->channels = 2;
|
||||||
|
#endif
|
||||||
|
decoder->codec_ctx->sample_rate = 48000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
|
||||||
|
LOGE("Decoder '%s': could not open codec", decoder->name);
|
||||||
|
avcodec_free_context(&decoder->codec_ctx);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
decoder->ctx = ctx;
|
decoder->frame = av_frame_alloc();
|
||||||
|
if (!decoder->frame) {
|
||||||
|
LOG_OOM();
|
||||||
|
avcodec_close(decoder->codec_ctx);
|
||||||
|
avcodec_free_context(&decoder->codec_ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sc_decoder_open_sinks(decoder, decoder->codec_ctx)) {
|
||||||
|
av_frame_free(&decoder->frame);
|
||||||
|
avcodec_close(decoder->codec_ctx);
|
||||||
|
avcodec_free_context(&decoder->codec_ctx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_decoder_close(struct sc_decoder *decoder) {
|
sc_decoder_close(struct sc_decoder *decoder) {
|
||||||
sc_frame_source_sinks_close(&decoder->frame_source);
|
sc_decoder_close_sinks(decoder);
|
||||||
av_frame_free(&decoder->frame);
|
av_frame_free(&decoder->frame);
|
||||||
|
avcodec_close(decoder->codec_ctx);
|
||||||
|
avcodec_free_context(&decoder->codec_ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) {
|
||||||
|
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
||||||
|
struct sc_frame_sink *sink = decoder->sinks[i];
|
||||||
|
if (!sink->ops->push(sink, frame)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@ -43,42 +115,33 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ret = avcodec_send_packet(decoder->ctx, packet);
|
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
|
||||||
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
||||||
LOGE("Decoder '%s': could not send video packet: %d",
|
LOGE("Decoder '%s': could not send video packet: %d",
|
||||||
decoder->name, ret);
|
decoder->name, ret);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
|
||||||
for (;;) {
|
if (!ret) {
|
||||||
ret = avcodec_receive_frame(decoder->ctx, decoder->frame);
|
|
||||||
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ret) {
|
|
||||||
LOGE("Decoder '%s', could not receive video frame: %d",
|
|
||||||
decoder->name, ret);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// a frame was received
|
// a frame was received
|
||||||
bool ok = sc_frame_source_sinks_push(&decoder->frame_source,
|
bool ok = push_frame_to_sinks(decoder, decoder->frame);
|
||||||
decoder->frame);
|
// A frame lost should not make the whole pipeline fail. The error, if
|
||||||
av_frame_unref(decoder->frame);
|
// any, is already logged.
|
||||||
if (!ok) {
|
(void) ok;
|
||||||
// Error already logged
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
av_frame_unref(decoder->frame);
|
||||||
|
} else if (ret != AVERROR(EAGAIN)) {
|
||||||
|
LOGE("Decoder '%s', could not receive video frame: %d",
|
||||||
|
decoder->name, ret);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) {
|
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) {
|
||||||
struct sc_decoder *decoder = DOWNCAST(sink);
|
struct sc_decoder *decoder = DOWNCAST(sink);
|
||||||
return sc_decoder_open(decoder, ctx);
|
return sc_decoder_open(decoder, codec);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@ -97,7 +160,7 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
|
|||||||
void
|
void
|
||||||
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
||||||
decoder->name = name; // statically allocated
|
decoder->name = name; // statically allocated
|
||||||
sc_frame_source_init(&decoder->frame_source);
|
decoder->sink_count = 0;
|
||||||
|
|
||||||
static const struct sc_packet_sink_ops ops = {
|
static const struct sc_packet_sink_ops ops = {
|
||||||
.open = sc_decoder_packet_sink_open,
|
.open = sc_decoder_packet_sink_open,
|
||||||
@ -107,3 +170,11 @@ sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
|||||||
|
|
||||||
decoder->packet_sink.ops = &ops;
|
decoder->packet_sink.ops = &ops;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink) {
|
||||||
|
assert(decoder->sink_count < SC_DECODER_MAX_SINKS);
|
||||||
|
assert(sink);
|
||||||
|
assert(sink->ops);
|
||||||
|
decoder->sinks[decoder->sink_count++] = sink;
|
||||||
|
}
|
||||||
|
@ -3,20 +3,23 @@
|
|||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
|
||||||
#include "trait/frame_source.h"
|
|
||||||
#include "trait/packet_sink.h"
|
#include "trait/packet_sink.h"
|
||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
#define SC_DECODER_MAX_SINKS 2
|
||||||
|
|
||||||
struct sc_decoder {
|
struct sc_decoder {
|
||||||
struct sc_packet_sink packet_sink; // packet sink trait
|
struct sc_packet_sink packet_sink; // packet sink trait
|
||||||
struct sc_frame_source frame_source; // frame source trait
|
|
||||||
|
|
||||||
const char *name; // must be statically allocated (e.g. a string literal)
|
const char *name; // must be statically allocated (e.g. a string literal)
|
||||||
|
|
||||||
AVCodecContext *ctx;
|
struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS];
|
||||||
|
unsigned sink_count;
|
||||||
|
|
||||||
|
AVCodecContext *codec_ctx;
|
||||||
AVFrame *frame;
|
AVFrame *frame;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -24,4 +27,7 @@ struct sc_decoder {
|
|||||||
void
|
void
|
||||||
sc_decoder_init(struct sc_decoder *decoder, const char *name);
|
sc_decoder_init(struct sc_decoder *decoder, const char *name);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -1,244 +0,0 @@
|
|||||||
#include "delay_buffer.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
|
|
||||||
#include <libavutil/avutil.h>
|
|
||||||
#include <libavformat/avformat.h>
|
|
||||||
|
|
||||||
#include "util/log.h"
|
|
||||||
|
|
||||||
#define SC_BUFFERING_NDEBUG // comment to debug
|
|
||||||
|
|
||||||
/** 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef SC_BUFFERING_NDEBUG
|
|
||||||
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);
|
|
||||||
|
|
||||||
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.count == 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef SC_BUFFERING_NDEBUG
|
|
||||||
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,60 +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"
|
|
||||||
|
|
||||||
// forward declarations
|
|
||||||
typedef struct AVFrame AVFrame;
|
|
||||||
|
|
||||||
struct sc_delayed_frame {
|
|
||||||
AVFrame *frame;
|
|
||||||
#ifndef NDEBUG
|
|
||||||
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
|
|
@ -25,7 +25,6 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|||||||
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" 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_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
||||||
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
||||||
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
|
|
||||||
switch (codec_id) {
|
switch (codec_id) {
|
||||||
case SC_CODEC_ID_H264:
|
case SC_CODEC_ID_H264:
|
||||||
return AV_CODEC_ID_H264;
|
return AV_CODEC_ID_H264;
|
||||||
@ -37,8 +36,6 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|||||||
return AV_CODEC_ID_OPUS;
|
return AV_CODEC_ID_OPUS;
|
||||||
case SC_CODEC_ID_AAC:
|
case SC_CODEC_ID_AAC:
|
||||||
return AV_CODEC_ID_AAC;
|
return AV_CODEC_ID_AAC;
|
||||||
case SC_CODEC_ID_RAW:
|
|
||||||
return AV_CODEC_ID_PCM_S16LE;
|
|
||||||
default:
|
default:
|
||||||
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
||||||
return AV_CODEC_ID_NONE;
|
return AV_CODEC_ID_NONE;
|
||||||
@ -57,20 +54,6 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
|
|||||||
return true;
|
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
|
static bool
|
||||||
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||||
// The video stream contains raw packets, without time information. When we
|
// The video stream contains raw packets, without time information. When we
|
||||||
@ -129,26 +112,86 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
|
||||||
|
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||||
|
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||||
|
if (!sink->ops->push(sink, packet)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||||
|
bool ok = push_packet_to_sinks(demuxer, packet);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Demuxer '%s': could not process packet", demuxer->name);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_demuxer_close_first_sinks(struct sc_demuxer *demuxer, unsigned count) {
|
||||||
|
while (count) {
|
||||||
|
struct sc_packet_sink *sink = demuxer->sinks[--count];
|
||||||
|
sink->ops->close(sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
sc_demuxer_close_sinks(struct sc_demuxer *demuxer) {
|
||||||
|
sc_demuxer_close_first_sinks(demuxer, demuxer->sink_count);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
|
||||||
|
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||||
|
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||||
|
if (!sink->ops->open(sink, codec)) {
|
||||||
|
sc_demuxer_close_first_sinks(demuxer, i);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_demuxer_disable_sinks(struct sc_demuxer *demuxer) {
|
||||||
|
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
|
||||||
|
struct sc_packet_sink *sink = demuxer->sinks[i];
|
||||||
|
if (sink->ops->disable) {
|
||||||
|
sink->ops->disable(sink);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
run_demuxer(void *data) {
|
run_demuxer(void *data) {
|
||||||
struct sc_demuxer *demuxer = data;
|
struct sc_demuxer *demuxer = data;
|
||||||
|
|
||||||
// Flag to report end-of-stream (i.e. device disconnected)
|
// Flag to report end-of-stream (i.e. device disconnected)
|
||||||
enum sc_demuxer_status status = SC_DEMUXER_STATUS_ERROR;
|
bool eos = false;
|
||||||
|
|
||||||
uint32_t raw_codec_id;
|
uint32_t raw_codec_id;
|
||||||
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
|
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
LOGE("Demuxer '%s': stream disabled due to connection error",
|
LOGE("Demuxer '%s': stream disabled due to connection error",
|
||||||
demuxer->name);
|
demuxer->name);
|
||||||
|
eos = true;
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (raw_codec_id == 0) {
|
if (raw_codec_id == 0) {
|
||||||
LOGW("Demuxer '%s': stream explicitly disabled by the device",
|
LOGW("Demuxer '%s': stream explicitly disabled by the device",
|
||||||
demuxer->name);
|
demuxer->name);
|
||||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
sc_demuxer_disable_sinks(demuxer);
|
||||||
status = SC_DEMUXER_STATUS_DISABLED;
|
eos = true;
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -162,7 +205,7 @@ run_demuxer(void *data) {
|
|||||||
if (codec_id == AV_CODEC_ID_NONE) {
|
if (codec_id == AV_CODEC_ID_NONE) {
|
||||||
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
|
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
|
||||||
demuxer->name);
|
demuxer->name);
|
||||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
sc_demuxer_disable_sinks(demuxer);
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -170,49 +213,14 @@ run_demuxer(void *data) {
|
|||||||
if (!codec) {
|
if (!codec) {
|
||||||
LOGE("Demuxer '%s': stream disabled due to missing decoder",
|
LOGE("Demuxer '%s': stream disabled due to missing decoder",
|
||||||
demuxer->name);
|
demuxer->name);
|
||||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
sc_demuxer_disable_sinks(demuxer);
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
|
if (!sc_demuxer_open_sinks(demuxer, codec)) {
|
||||||
if (!codec_ctx) {
|
|
||||||
LOG_OOM();
|
|
||||||
goto end;
|
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 (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
|
// Config packets must be merged with the next non-config packet only for
|
||||||
// video streams
|
// video streams
|
||||||
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
||||||
@ -233,7 +241,7 @@ run_demuxer(void *data) {
|
|||||||
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
// end of stream
|
// end of stream
|
||||||
status = SC_DEMUXER_STATUS_EOS;
|
eos = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -246,10 +254,10 @@ run_demuxer(void *data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
|
ok = sc_demuxer_push_packet(demuxer, packet);
|
||||||
av_packet_unref(packet);
|
av_packet_unref(packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
// The sink already logged its concrete error
|
// cannot process packet (error already logged)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -262,12 +270,9 @@ run_demuxer(void *data) {
|
|||||||
|
|
||||||
av_packet_free(&packet);
|
av_packet_free(&packet);
|
||||||
finally_close_sinks:
|
finally_close_sinks:
|
||||||
sc_packet_source_sinks_close(&demuxer->packet_source);
|
sc_demuxer_close_sinks(demuxer);
|
||||||
finally_free_context:
|
|
||||||
// This also calls avcodec_close() internally
|
|
||||||
avcodec_free_context(&codec_ctx);
|
|
||||||
end:
|
end:
|
||||||
demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata);
|
demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@ -279,7 +284,7 @@ sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
|||||||
|
|
||||||
demuxer->name = name; // statically allocated
|
demuxer->name = name; // statically allocated
|
||||||
demuxer->socket = socket;
|
demuxer->socket = socket;
|
||||||
sc_packet_source_init(&demuxer->packet_source);
|
demuxer->sink_count = 0;
|
||||||
|
|
||||||
assert(cbs && cbs->on_ended);
|
assert(cbs && cbs->on_ended);
|
||||||
|
|
||||||
@ -287,6 +292,14 @@ sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
|||||||
demuxer->cbs_userdata = cbs_userdata;
|
demuxer->cbs_userdata = cbs_userdata;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) {
|
||||||
|
assert(demuxer->sink_count < SC_DEMUXER_MAX_SINKS);
|
||||||
|
assert(sink);
|
||||||
|
assert(sink->ops);
|
||||||
|
demuxer->sinks[demuxer->sink_count++] = sink;
|
||||||
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
||||||
LOGD("Demuxer '%s': starting thread", demuxer->name);
|
LOGD("Demuxer '%s': starting thread", demuxer->name);
|
||||||
|
@ -8,32 +8,27 @@
|
|||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
#include <libavformat/avformat.h>
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
#include "trait/packet_source.h"
|
|
||||||
#include "trait/packet_sink.h"
|
#include "trait/packet_sink.h"
|
||||||
#include "util/net.h"
|
#include "util/net.h"
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
|
|
||||||
struct sc_demuxer {
|
#define SC_DEMUXER_MAX_SINKS 2
|
||||||
struct sc_packet_source packet_source; // packet source trait
|
|
||||||
|
|
||||||
|
struct sc_demuxer {
|
||||||
const char *name; // must be statically allocated (e.g. a string literal)
|
const char *name; // must be statically allocated (e.g. a string literal)
|
||||||
|
|
||||||
sc_socket socket;
|
sc_socket socket;
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
|
|
||||||
|
struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS];
|
||||||
|
unsigned sink_count;
|
||||||
|
|
||||||
const struct sc_demuxer_callbacks *cbs;
|
const struct sc_demuxer_callbacks *cbs;
|
||||||
void *cbs_userdata;
|
void *cbs_userdata;
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_demuxer_status {
|
|
||||||
SC_DEMUXER_STATUS_EOS,
|
|
||||||
SC_DEMUXER_STATUS_DISABLED,
|
|
||||||
SC_DEMUXER_STATUS_ERROR,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct sc_demuxer_callbacks {
|
struct sc_demuxer_callbacks {
|
||||||
void (*on_ended)(struct sc_demuxer *demuxer, enum sc_demuxer_status,
|
void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata);
|
||||||
void *userdata);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The name must be statically allocated (e.g. a string literal)
|
// The name must be statically allocated (e.g. a string literal)
|
||||||
@ -41,6 +36,9 @@ void
|
|||||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_demuxer_start(struct sc_demuxer *demuxer);
|
sc_demuxer_start(struct sc_demuxer *demuxer);
|
||||||
|
|
||||||
|
@ -5,4 +5,3 @@
|
|||||||
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||||
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
||||||
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
|
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
|
||||||
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
|
|
||||||
|
@ -19,7 +19,7 @@ sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial,
|
|||||||
const char *push_target) {
|
const char *push_target) {
|
||||||
assert(serial);
|
assert(serial);
|
||||||
|
|
||||||
sc_vecdeque_init(&fp->queue);
|
cbuf_init(&fp->queue);
|
||||||
|
|
||||||
bool ok = sc_mutex_init(&fp->mutex);
|
bool ok = sc_mutex_init(&fp->mutex);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
@ -65,10 +65,9 @@ sc_file_pusher_destroy(struct sc_file_pusher *fp) {
|
|||||||
sc_intr_destroy(&fp->intr);
|
sc_intr_destroy(&fp->intr);
|
||||||
free(fp->serial);
|
free(fp->serial);
|
||||||
|
|
||||||
while (!sc_vecdeque_is_empty(&fp->queue)) {
|
struct sc_file_pusher_request req;
|
||||||
struct sc_file_pusher_request *req = sc_vecdeque_popref(&fp->queue);
|
while (cbuf_take(&fp->queue, &req)) {
|
||||||
assert(req);
|
sc_file_pusher_request_destroy(&req);
|
||||||
sc_file_pusher_request_destroy(req);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,20 +91,13 @@ sc_file_pusher_request(struct sc_file_pusher *fp,
|
|||||||
};
|
};
|
||||||
|
|
||||||
sc_mutex_lock(&fp->mutex);
|
sc_mutex_lock(&fp->mutex);
|
||||||
bool was_empty = sc_vecdeque_is_empty(&fp->queue);
|
bool was_empty = cbuf_is_empty(&fp->queue);
|
||||||
bool res = sc_vecdeque_push(&fp->queue, req);
|
bool res = cbuf_push(&fp->queue, req);
|
||||||
if (!res) {
|
|
||||||
LOG_OOM();
|
|
||||||
sc_mutex_unlock(&fp->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (was_empty) {
|
if (was_empty) {
|
||||||
sc_cond_signal(&fp->event_cond);
|
sc_cond_signal(&fp->event_cond);
|
||||||
}
|
}
|
||||||
sc_mutex_unlock(&fp->mutex);
|
sc_mutex_unlock(&fp->mutex);
|
||||||
|
return res;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@ -121,7 +113,7 @@ run_file_pusher(void *data) {
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
sc_mutex_lock(&fp->mutex);
|
sc_mutex_lock(&fp->mutex);
|
||||||
while (!fp->stopped && sc_vecdeque_is_empty(&fp->queue)) {
|
while (!fp->stopped && cbuf_is_empty(&fp->queue)) {
|
||||||
sc_cond_wait(&fp->event_cond, &fp->mutex);
|
sc_cond_wait(&fp->event_cond, &fp->mutex);
|
||||||
}
|
}
|
||||||
if (fp->stopped) {
|
if (fp->stopped) {
|
||||||
@ -129,9 +121,10 @@ run_file_pusher(void *data) {
|
|||||||
sc_mutex_unlock(&fp->mutex);
|
sc_mutex_unlock(&fp->mutex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
struct sc_file_pusher_request req;
|
||||||
assert(!sc_vecdeque_is_empty(&fp->queue));
|
bool non_empty = cbuf_take(&fp->queue, &req);
|
||||||
struct sc_file_pusher_request req = sc_vecdeque_pop(&fp->queue);
|
assert(non_empty);
|
||||||
|
(void) non_empty;
|
||||||
sc_mutex_unlock(&fp->mutex);
|
sc_mutex_unlock(&fp->mutex);
|
||||||
|
|
||||||
if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) {
|
if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) {
|
||||||
@ -172,18 +165,14 @@ sc_file_pusher_start(struct sc_file_pusher *fp) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
sc_file_pusher_stop(struct sc_file_pusher *fp) {
|
sc_file_pusher_stop(struct sc_file_pusher *fp) {
|
||||||
if (fp->initialized) {
|
sc_mutex_lock(&fp->mutex);
|
||||||
sc_mutex_lock(&fp->mutex);
|
fp->stopped = true;
|
||||||
fp->stopped = true;
|
sc_cond_signal(&fp->event_cond);
|
||||||
sc_cond_signal(&fp->event_cond);
|
sc_intr_interrupt(&fp->intr);
|
||||||
sc_intr_interrupt(&fp->intr);
|
sc_mutex_unlock(&fp->mutex);
|
||||||
sc_mutex_unlock(&fp->mutex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_file_pusher_join(struct sc_file_pusher *fp) {
|
sc_file_pusher_join(struct sc_file_pusher *fp) {
|
||||||
if (fp->initialized) {
|
sc_thread_join(&fp->thread, NULL);
|
||||||
sc_thread_join(&fp->thread, NULL);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -5,9 +5,9 @@
|
|||||||
|
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
|
||||||
#include "util/intr.h"
|
#include "util/cbuf.h"
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
#include "util/vecdeque.h"
|
#include "util/intr.h"
|
||||||
|
|
||||||
enum sc_file_pusher_action {
|
enum sc_file_pusher_action {
|
||||||
SC_FILE_PUSHER_ACTION_INSTALL_APK,
|
SC_FILE_PUSHER_ACTION_INSTALL_APK,
|
||||||
@ -19,7 +19,7 @@ struct sc_file_pusher_request {
|
|||||||
char *file;
|
char *file;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_file_pusher_request_queue SC_VECDEQUE(struct sc_file_pusher_request);
|
struct sc_file_pusher_request_queue CBUF(struct sc_file_pusher_request, 16);
|
||||||
|
|
||||||
struct sc_file_pusher {
|
struct sc_file_pusher {
|
||||||
char *serial;
|
char *serial;
|
||||||
|
@ -96,7 +96,6 @@ run_fps_counter(void *data) {
|
|||||||
bool
|
bool
|
||||||
sc_fps_counter_start(struct sc_fps_counter *counter) {
|
sc_fps_counter_start(struct sc_fps_counter *counter) {
|
||||||
sc_mutex_lock(&counter->mutex);
|
sc_mutex_lock(&counter->mutex);
|
||||||
counter->interrupted = false;
|
|
||||||
counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL;
|
counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL;
|
||||||
counter->nr_rendered = 0;
|
counter->nr_rendered = 0;
|
||||||
counter->nr_skipped = 0;
|
counter->nr_skipped = 0;
|
||||||
|
@ -43,7 +43,6 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.display_id = 0,
|
.display_id = 0,
|
||||||
.display_buffer = 0,
|
.display_buffer = 0,
|
||||||
.v4l2_buffer = 0,
|
.v4l2_buffer = 0,
|
||||||
.audio_buffer = SC_TICK_FROM_MS(50),
|
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
.otg = false,
|
.otg = false,
|
||||||
#endif
|
#endif
|
||||||
@ -73,7 +72,6 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.start_fps_counter = false,
|
.start_fps_counter = false,
|
||||||
.power_on = true,
|
.power_on = true,
|
||||||
.audio = true,
|
.audio = true,
|
||||||
.require_audio = false,
|
|
||||||
.list_encoders = false,
|
.list_encoders = false,
|
||||||
.list_displays = false,
|
.list_displays = false,
|
||||||
};
|
};
|
||||||
|
@ -29,7 +29,6 @@ enum sc_codec {
|
|||||||
SC_CODEC_AV1,
|
SC_CODEC_AV1,
|
||||||
SC_CODEC_OPUS,
|
SC_CODEC_OPUS,
|
||||||
SC_CODEC_AAC,
|
SC_CODEC_AAC,
|
||||||
SC_CODEC_RAW,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
enum sc_lock_video_orientation {
|
enum sc_lock_video_orientation {
|
||||||
@ -126,7 +125,6 @@ struct scrcpy_options {
|
|||||||
uint32_t display_id;
|
uint32_t display_id;
|
||||||
sc_tick display_buffer;
|
sc_tick display_buffer;
|
||||||
sc_tick v4l2_buffer;
|
sc_tick v4l2_buffer;
|
||||||
sc_tick audio_buffer;
|
|
||||||
#ifdef HAVE_USB
|
#ifdef HAVE_USB
|
||||||
bool otg;
|
bool otg;
|
||||||
#endif
|
#endif
|
||||||
@ -156,7 +154,6 @@ struct scrcpy_options {
|
|||||||
bool start_fps_counter;
|
bool start_fps_counter;
|
||||||
bool power_on;
|
bool power_on;
|
||||||
bool audio;
|
bool audio;
|
||||||
bool require_audio;
|
|
||||||
bool list_encoders;
|
bool list_encoders;
|
||||||
bool list_displays;
|
bool list_displays;
|
||||||
};
|
};
|
||||||
|
@ -33,27 +33,41 @@ find_muxer(const char *name) {
|
|||||||
return oformat;
|
return oformat;
|
||||||
}
|
}
|
||||||
|
|
||||||
static AVPacket *
|
static struct sc_record_packet *
|
||||||
sc_recorder_packet_ref(const AVPacket *packet) {
|
sc_record_packet_new(const AVPacket *packet) {
|
||||||
AVPacket *p = av_packet_alloc();
|
struct sc_record_packet *rec = malloc(sizeof(*rec));
|
||||||
if (!p) {
|
if (!rec) {
|
||||||
LOG_OOM();
|
LOG_OOM();
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (av_packet_ref(p, packet)) {
|
rec->packet = av_packet_alloc();
|
||||||
av_packet_free(&p);
|
if (!rec->packet) {
|
||||||
|
LOG_OOM();
|
||||||
|
free(rec);
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return p;
|
if (av_packet_ref(rec->packet, packet)) {
|
||||||
|
av_packet_free(&rec->packet);
|
||||||
|
free(rec);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
return rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_record_packet_delete(struct sc_record_packet *rec) {
|
||||||
|
av_packet_free(&rec->packet);
|
||||||
|
free(rec);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_recorder_queue_clear(struct sc_recorder_queue *queue) {
|
sc_recorder_queue_clear(struct sc_recorder_queue *queue) {
|
||||||
while (!sc_vecdeque_is_empty(queue)) {
|
while (!sc_queue_is_empty(queue)) {
|
||||||
AVPacket *p = sc_vecdeque_pop(queue);
|
struct sc_record_packet *rec;
|
||||||
av_packet_free(&p);
|
sc_queue_take(queue, next, &rec);
|
||||||
|
sc_record_packet_delete(rec);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,14 +164,80 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) {
|
|||||||
avformat_free_context(recorder->ctx);
|
avformat_free_context(recorder->ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_recorder_wait_video_stream(struct sc_recorder *recorder) {
|
||||||
|
sc_mutex_lock(&recorder->mutex);
|
||||||
|
while (!recorder->video_codec && !recorder->stopped) {
|
||||||
|
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||||
|
}
|
||||||
|
const AVCodec *codec = recorder->video_codec;
|
||||||
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
|
|
||||||
|
if (codec) {
|
||||||
|
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
|
||||||
|
if (!stream) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||||
|
stream->codecpar->codec_id = codec->id;
|
||||||
|
stream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||||
|
stream->codecpar->width = recorder->declared_frame_size.width;
|
||||||
|
stream->codecpar->height = recorder->declared_frame_size.height;
|
||||||
|
|
||||||
|
recorder->video_stream_index = stream->index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_recorder_wait_audio_stream(struct sc_recorder *recorder) {
|
||||||
|
sc_mutex_lock(&recorder->mutex);
|
||||||
|
while (!recorder->audio_codec && !recorder->audio_disabled
|
||||||
|
&& !recorder->stopped) {
|
||||||
|
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recorder->audio_disabled) {
|
||||||
|
// Reset audio flag. From there, the recorder thread may access this
|
||||||
|
// flag without any mutex.
|
||||||
|
recorder->audio = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const AVCodec *codec = recorder->audio_codec;
|
||||||
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
|
|
||||||
|
if (codec) {
|
||||||
|
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
|
||||||
|
if (!stream) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
|
||||||
|
stream->codecpar->codec_id = codec->id;
|
||||||
|
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||||
|
stream->codecpar->ch_layout.nb_channels = 2;
|
||||||
|
#else
|
||||||
|
stream->codecpar->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||||
|
stream->codecpar->channels = 2;
|
||||||
|
#endif
|
||||||
|
stream->codecpar->sample_rate = 48000;
|
||||||
|
|
||||||
|
recorder->audio_stream_index = stream->index;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
|
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
|
||||||
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
|
if (sc_queue_is_empty(&recorder->video_queue)) {
|
||||||
// The video queue is empty
|
// The video queue is empty
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recorder->audio && sc_vecdeque_is_empty(&recorder->audio_queue)) {
|
if (recorder->audio && sc_queue_is_empty(&recorder->audio_queue)) {
|
||||||
// The audio queue is empty (when audio is enabled)
|
// The audio queue is empty (when audio is enabled)
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -170,33 +250,31 @@ static bool
|
|||||||
sc_recorder_process_header(struct sc_recorder *recorder) {
|
sc_recorder_process_header(struct sc_recorder *recorder) {
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
|
|
||||||
while (!recorder->stopped && (!recorder->video_init
|
while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) {
|
||||||
|| !recorder->audio_init
|
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||||
|| sc_recorder_has_empty_queues(recorder))) {
|
|
||||||
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
|
if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) {
|
||||||
assert(recorder->stopped);
|
|
||||||
// If the recorder is stopped, don't process anything if there are not
|
// If the recorder is stopped, don't process anything if there are not
|
||||||
// at least video packets
|
// at least video packets
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue);
|
struct sc_record_packet *video_pkt;
|
||||||
|
sc_queue_take(&recorder->video_queue, next, &video_pkt);
|
||||||
|
|
||||||
AVPacket *audio_pkt = NULL;
|
struct sc_record_packet *audio_pkt = NULL;
|
||||||
if (!sc_vecdeque_is_empty(&recorder->audio_queue)) {
|
if (!sc_queue_is_empty(&recorder->audio_queue)) {
|
||||||
assert(recorder->audio);
|
assert(recorder->audio);
|
||||||
audio_pkt = sc_vecdeque_pop(&recorder->audio_queue);
|
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
|
|
||||||
int ret = false;
|
int ret = false;
|
||||||
|
|
||||||
if (video_pkt->pts != AV_NOPTS_VALUE) {
|
if (video_pkt->packet->pts != AV_NOPTS_VALUE) {
|
||||||
LOGE("The first video packet is not a config packet");
|
LOGE("The first video packet is not a config packet");
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
@ -204,13 +282,13 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
|
|||||||
assert(recorder->video_stream_index >= 0);
|
assert(recorder->video_stream_index >= 0);
|
||||||
AVStream *video_stream =
|
AVStream *video_stream =
|
||||||
recorder->ctx->streams[recorder->video_stream_index];
|
recorder->ctx->streams[recorder->video_stream_index];
|
||||||
bool ok = sc_recorder_set_extradata(video_stream, video_pkt);
|
bool ok = sc_recorder_set_extradata(video_stream, video_pkt->packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio_pkt) {
|
if (audio_pkt) {
|
||||||
if (audio_pkt->pts != AV_NOPTS_VALUE) {
|
if (audio_pkt->packet->pts != AV_NOPTS_VALUE) {
|
||||||
LOGE("The first audio packet is not a config packet");
|
LOGE("The first audio packet is not a config packet");
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
@ -218,7 +296,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
|
|||||||
assert(recorder->audio_stream_index >= 0);
|
assert(recorder->audio_stream_index >= 0);
|
||||||
AVStream *audio_stream =
|
AVStream *audio_stream =
|
||||||
recorder->ctx->streams[recorder->audio_stream_index];
|
recorder->ctx->streams[recorder->audio_stream_index];
|
||||||
ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
|
ok = sc_recorder_set_extradata(audio_stream, audio_pkt->packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
@ -233,9 +311,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
|
|||||||
ret = true;
|
ret = true;
|
||||||
|
|
||||||
end:
|
end:
|
||||||
av_packet_free(&video_pkt);
|
sc_record_packet_delete(video_pkt);
|
||||||
if (audio_pkt) {
|
if (audio_pkt) {
|
||||||
av_packet_free(&audio_pkt);
|
sc_record_packet_delete(audio_pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
@ -250,12 +328,12 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AVPacket *video_pkt = NULL;
|
struct sc_record_packet *video_pkt = NULL;
|
||||||
AVPacket *audio_pkt = NULL;
|
struct sc_record_packet *audio_pkt = NULL;
|
||||||
|
|
||||||
// We can write a video packet only once we received the next one so that
|
// We can write a video packet only once we received the next one so that
|
||||||
// we can set its duration (next_pts - current_pts)
|
// we can set its duration (next_pts - current_pts)
|
||||||
AVPacket *video_pkt_previous = NULL;
|
struct sc_record_packet *video_pkt_previous = NULL;
|
||||||
|
|
||||||
bool error = false;
|
bool error = false;
|
||||||
|
|
||||||
@ -263,12 +341,12 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
|
|||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
|
|
||||||
while (!recorder->stopped) {
|
while (!recorder->stopped) {
|
||||||
if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) {
|
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
|
||||||
// A new packet may be assigned to video_pkt and be processed
|
// A new packet may be assigned to video_pkt and be processed
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (recorder->audio && !audio_pkt
|
if (recorder->audio && !audio_pkt
|
||||||
&& !sc_vecdeque_is_empty(&recorder->audio_queue)) {
|
&& !sc_queue_is_empty(&recorder->audio_queue)) {
|
||||||
// A new packet may be assigned to audio_pkt and be processed
|
// A new packet may be assigned to audio_pkt and be processed
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -280,20 +358,20 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
|
|||||||
|
|
||||||
// If there is no audio, then the audio_queue will remain empty forever
|
// If there is no audio, then the audio_queue will remain empty forever
|
||||||
// and audio_pkt will always be NULL.
|
// and audio_pkt will always be NULL.
|
||||||
assert(recorder->audio || (!audio_pkt
|
assert(recorder->audio
|
||||||
&& sc_vecdeque_is_empty(&recorder->audio_queue)));
|
|| (!audio_pkt && sc_queue_is_empty(&recorder->audio_queue)));
|
||||||
|
|
||||||
if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) {
|
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
|
||||||
video_pkt = sc_vecdeque_pop(&recorder->video_queue);
|
sc_queue_take(&recorder->video_queue, next, &video_pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!audio_pkt && !sc_vecdeque_is_empty(&recorder->audio_queue)) {
|
if (!audio_pkt && !sc_queue_is_empty(&recorder->audio_queue)) {
|
||||||
audio_pkt = sc_vecdeque_pop(&recorder->audio_queue);
|
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (recorder->stopped && !video_pkt && !audio_pkt) {
|
if (recorder->stopped && !video_pkt && !audio_pkt) {
|
||||||
assert(sc_vecdeque_is_empty(&recorder->video_queue));
|
assert(sc_queue_is_empty(&recorder->video_queue));
|
||||||
assert(sc_vecdeque_is_empty(&recorder->audio_queue));
|
assert(sc_queue_is_empty(&recorder->audio_queue));
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -305,32 +383,38 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
|
|||||||
// Ignore further config packets (e.g. on device orientation
|
// Ignore further config packets (e.g. on device orientation
|
||||||
// change). The next non-config packet will have the config packet
|
// change). The next non-config packet will have the config packet
|
||||||
// data prepended.
|
// data prepended.
|
||||||
if (video_pkt && video_pkt->pts == AV_NOPTS_VALUE) {
|
if (video_pkt && video_pkt->packet->pts == AV_NOPTS_VALUE) {
|
||||||
av_packet_free(&video_pkt);
|
sc_record_packet_delete(video_pkt);
|
||||||
video_pkt = NULL;
|
video_pkt = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio_pkt && audio_pkt->pts == AV_NOPTS_VALUE) {
|
if (audio_pkt && audio_pkt->packet->pts == AV_NOPTS_VALUE) {
|
||||||
av_packet_free(&audio_pkt);
|
sc_record_packet_delete(audio_pkt);
|
||||||
audio_pkt = NULL;
|
audio_pkt= NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pts_origin == AV_NOPTS_VALUE) {
|
if (pts_origin == AV_NOPTS_VALUE) {
|
||||||
if (!recorder->audio) {
|
if (!recorder->audio) {
|
||||||
assert(video_pkt);
|
assert(video_pkt);
|
||||||
pts_origin = video_pkt->pts;
|
pts_origin = video_pkt->packet->pts;
|
||||||
} else if (video_pkt && audio_pkt) {
|
} else if (video_pkt && audio_pkt) {
|
||||||
pts_origin = MIN(video_pkt->pts, audio_pkt->pts);
|
pts_origin =
|
||||||
|
MIN(video_pkt->packet->pts, audio_pkt->packet->pts);
|
||||||
} else if (recorder->stopped) {
|
} else if (recorder->stopped) {
|
||||||
if (video_pkt) {
|
if (video_pkt) {
|
||||||
// The recorder is stopped without audio, record the video
|
// The recorder is stopped without audio, record the video
|
||||||
// packets
|
// packets
|
||||||
pts_origin = video_pkt->pts;
|
pts_origin = video_pkt->packet->pts;
|
||||||
} else {
|
} else {
|
||||||
// Fail if there is no video
|
// Fail if there is no video
|
||||||
error = true;
|
error = true;
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
// If the recorder is stopped while one of the streams has no
|
||||||
|
// packets, then we must avoid a live-loop and correctly record
|
||||||
|
// the stream having packets.
|
||||||
|
pts_origin = video_pkt ? video_pkt->packet->pts
|
||||||
|
: audio_pkt->packet->pts;
|
||||||
} else {
|
} else {
|
||||||
// We need both video and audio packets to initialize pts_origin
|
// We need both video and audio packets to initialize pts_origin
|
||||||
continue;
|
continue;
|
||||||
@ -340,16 +424,17 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
|
|||||||
assert(pts_origin != AV_NOPTS_VALUE);
|
assert(pts_origin != AV_NOPTS_VALUE);
|
||||||
|
|
||||||
if (video_pkt) {
|
if (video_pkt) {
|
||||||
video_pkt->pts -= pts_origin;
|
video_pkt->packet->pts -= pts_origin;
|
||||||
video_pkt->dts = video_pkt->pts;
|
video_pkt->packet->dts = video_pkt->packet->pts;
|
||||||
|
|
||||||
if (video_pkt_previous) {
|
if (video_pkt_previous) {
|
||||||
// we now know the duration of the previous packet
|
// we now know the duration of the previous packet
|
||||||
video_pkt_previous->duration = video_pkt->pts
|
video_pkt_previous->packet->duration =
|
||||||
- video_pkt_previous->pts;
|
video_pkt->packet->pts - video_pkt_previous->packet->pts;
|
||||||
|
|
||||||
bool ok = sc_recorder_write_video(recorder, video_pkt_previous);
|
bool ok = sc_recorder_write_video(recorder,
|
||||||
av_packet_free(&video_pkt_previous);
|
video_pkt_previous->packet);
|
||||||
|
sc_record_packet_delete(video_pkt_previous);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
LOGE("Could not record video packet");
|
LOGE("Could not record video packet");
|
||||||
error = true;
|
error = true;
|
||||||
@ -362,34 +447,34 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (audio_pkt) {
|
if (audio_pkt) {
|
||||||
audio_pkt->pts -= pts_origin;
|
audio_pkt->packet->pts -= pts_origin;
|
||||||
audio_pkt->dts = audio_pkt->pts;
|
audio_pkt->packet->dts = audio_pkt->packet->pts;
|
||||||
|
|
||||||
bool ok = sc_recorder_write_audio(recorder, audio_pkt);
|
bool ok = sc_recorder_write_audio(recorder, audio_pkt->packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
LOGE("Could not record audio packet");
|
LOGE("Could not record audio packet");
|
||||||
error = true;
|
error = true;
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
av_packet_free(&audio_pkt);
|
sc_record_packet_delete(audio_pkt);
|
||||||
audio_pkt = NULL;
|
audio_pkt = NULL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the last video packet
|
// Write the last video packet
|
||||||
AVPacket *last = video_pkt_previous;
|
struct sc_record_packet *last = video_pkt_previous;
|
||||||
if (last) {
|
if (last) {
|
||||||
// assign an arbitrary duration to the last packet
|
// assign an arbitrary duration to the last packet
|
||||||
last->duration = 100000;
|
last->packet->duration = 100000;
|
||||||
bool ok = sc_recorder_write_video(recorder, last);
|
bool ok = sc_recorder_write_video(recorder, last->packet);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
// failing to write the last frame is not very serious, no
|
// failing to write the last frame is not very serious, no
|
||||||
// future frame may depend on it, so the resulting file
|
// future frame may depend on it, so the resulting file
|
||||||
// will still be valid
|
// will still be valid
|
||||||
LOGW("Could not record last packet");
|
LOGW("Could not record last packet");
|
||||||
}
|
}
|
||||||
av_packet_free(&last);
|
sc_record_packet_delete(last);
|
||||||
}
|
}
|
||||||
|
|
||||||
int ret = av_write_trailer(recorder->ctx);
|
int ret = av_write_trailer(recorder->ctx);
|
||||||
@ -400,10 +485,10 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
|
|||||||
|
|
||||||
end:
|
end:
|
||||||
if (video_pkt) {
|
if (video_pkt) {
|
||||||
av_packet_free(&video_pkt);
|
sc_record_packet_delete(video_pkt);
|
||||||
}
|
}
|
||||||
if (audio_pkt) {
|
if (audio_pkt) {
|
||||||
av_packet_free(&audio_pkt);
|
sc_record_packet_delete(audio_pkt);
|
||||||
}
|
}
|
||||||
|
|
||||||
return !error;
|
return !error;
|
||||||
@ -416,6 +501,22 @@ sc_recorder_record(struct sc_recorder *recorder) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ok = sc_recorder_wait_video_stream(recorder);
|
||||||
|
if (!ok) {
|
||||||
|
sc_recorder_close_output_file(recorder);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recorder->audio) {
|
||||||
|
ok = sc_recorder_wait_audio_stream(recorder);
|
||||||
|
if (!ok) {
|
||||||
|
sc_recorder_close_output_file(recorder);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If recorder->stopped, process any queued packet anyway
|
||||||
|
|
||||||
ok = sc_recorder_process_packets(recorder);
|
ok = sc_recorder_process_packets(recorder);
|
||||||
sc_recorder_close_output_file(recorder);
|
sc_recorder_close_output_file(recorder);
|
||||||
return ok;
|
return ok;
|
||||||
@ -425,10 +526,6 @@ static int
|
|||||||
run_recorder(void *data) {
|
run_recorder(void *data) {
|
||||||
struct sc_recorder *recorder = data;
|
struct sc_recorder *recorder = data;
|
||||||
|
|
||||||
// Recording is a background task
|
|
||||||
bool ok = sc_thread_set_priority(SC_THREAD_PRIORITY_LOW);
|
|
||||||
(void) ok; // We don't care if it worked
|
|
||||||
|
|
||||||
bool success = sc_recorder_record(recorder);
|
bool success = sc_recorder_record(recorder);
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
@ -436,7 +533,6 @@ run_recorder(void *data) {
|
|||||||
recorder->stopped = true;
|
recorder->stopped = true;
|
||||||
// Discard pending packets
|
// Discard pending packets
|
||||||
sc_recorder_queue_clear(&recorder->video_queue);
|
sc_recorder_queue_clear(&recorder->video_queue);
|
||||||
sc_recorder_queue_clear(&recorder->audio_queue);
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
@ -456,10 +552,9 @@ run_recorder(void *data) {
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||||
AVCodecContext *ctx) {
|
const AVCodec *codec) {
|
||||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||||
// only written from this thread, no need to lock
|
assert(codec);
|
||||||
assert(!recorder->video_init);
|
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
if (recorder->stopped) {
|
if (recorder->stopped) {
|
||||||
@ -467,21 +562,7 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec);
|
recorder->video_codec = codec;
|
||||||
if (!stream) {
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int r = avcodec_parameters_from_context(stream->codecpar, ctx);
|
|
||||||
if (r < 0) {
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
recorder->video_stream_index = stream->index;
|
|
||||||
|
|
||||||
recorder->video_init = true;
|
|
||||||
sc_cond_signal(&recorder->stream_cond);
|
sc_cond_signal(&recorder->stream_cond);
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
|
|
||||||
@ -491,8 +572,6 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
|||||||
static void
|
static void
|
||||||
sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
|
sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
|
||||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||||
// only written from this thread, no need to lock
|
|
||||||
assert(recorder->video_init);
|
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
// EOS also stops the recorder
|
// EOS also stops the recorder
|
||||||
@ -505,8 +584,6 @@ static bool
|
|||||||
sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||||
const AVPacket *packet) {
|
const AVPacket *packet) {
|
||||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||||
// only written from this thread, no need to lock
|
|
||||||
assert(recorder->video_init);
|
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
|
|
||||||
@ -516,22 +593,16 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AVPacket *rec = sc_recorder_packet_ref(packet);
|
struct sc_record_packet *rec = sc_record_packet_new(packet);
|
||||||
if (!rec) {
|
if (!rec) {
|
||||||
LOG_OOM();
|
LOG_OOM();
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rec->stream_index = recorder->video_stream_index;
|
rec->packet->stream_index = 0;
|
||||||
|
|
||||||
bool ok = sc_vecdeque_push(&recorder->video_queue, rec);
|
|
||||||
if (!ok) {
|
|
||||||
LOG_OOM();
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
sc_queue_push(&recorder->video_queue, next, rec);
|
||||||
sc_cond_signal(&recorder->queue_cond);
|
sc_cond_signal(&recorder->queue_cond);
|
||||||
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
@ -540,29 +611,15 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
|||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
|
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
|
||||||
AVCodecContext *ctx) {
|
const AVCodec *codec) {
|
||||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||||
assert(recorder->audio);
|
assert(recorder->audio);
|
||||||
// only written from this thread, no need to lock
|
// only written from this thread, no need to lock
|
||||||
assert(!recorder->audio_init);
|
assert(!recorder->audio_disabled);
|
||||||
|
assert(codec);
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
|
recorder->audio_codec = codec;
|
||||||
AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec);
|
|
||||||
if (!stream) {
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
int r = avcodec_parameters_from_context(stream->codecpar, ctx);
|
|
||||||
if (r < 0) {
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
recorder->audio_stream_index = stream->index;
|
|
||||||
|
|
||||||
recorder->audio_init = true;
|
|
||||||
sc_cond_signal(&recorder->stream_cond);
|
sc_cond_signal(&recorder->stream_cond);
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
|
|
||||||
@ -574,7 +631,7 @@ sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) {
|
|||||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||||
assert(recorder->audio);
|
assert(recorder->audio);
|
||||||
// only written from this thread, no need to lock
|
// only written from this thread, no need to lock
|
||||||
assert(recorder->audio_init);
|
assert(!recorder->audio_disabled);
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
// EOS also stops the recorder
|
// EOS also stops the recorder
|
||||||
@ -589,7 +646,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
|
|||||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||||
assert(recorder->audio);
|
assert(recorder->audio);
|
||||||
// only written from this thread, no need to lock
|
// only written from this thread, no need to lock
|
||||||
assert(recorder->audio_init);
|
assert(!recorder->audio_disabled);
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
|
|
||||||
@ -599,22 +656,16 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
AVPacket *rec = sc_recorder_packet_ref(packet);
|
struct sc_record_packet *rec = sc_record_packet_new(packet);
|
||||||
if (!rec) {
|
if (!rec) {
|
||||||
LOG_OOM();
|
LOG_OOM();
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rec->stream_index = recorder->audio_stream_index;
|
rec->packet->stream_index = 1;
|
||||||
|
|
||||||
bool ok = sc_vecdeque_push(&recorder->audio_queue, rec);
|
|
||||||
if (!ok) {
|
|
||||||
LOG_OOM();
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
sc_queue_push(&recorder->audio_queue, next, rec);
|
||||||
sc_cond_signal(&recorder->queue_cond);
|
sc_cond_signal(&recorder->queue_cond);
|
||||||
|
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
@ -626,13 +677,13 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
|
|||||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||||
assert(recorder->audio);
|
assert(recorder->audio);
|
||||||
// only written from this thread, no need to lock
|
// only written from this thread, no need to lock
|
||||||
assert(!recorder->audio_init);
|
assert(!recorder->audio_disabled);
|
||||||
|
assert(!recorder->audio_codec);
|
||||||
|
|
||||||
LOGW("Audio stream recording disabled");
|
LOGW("Audio stream recording disabled");
|
||||||
|
|
||||||
sc_mutex_lock(&recorder->mutex);
|
sc_mutex_lock(&recorder->mutex);
|
||||||
recorder->audio = false;
|
recorder->audio_disabled = true;
|
||||||
recorder->audio_init = true;
|
|
||||||
sc_cond_signal(&recorder->stream_cond);
|
sc_cond_signal(&recorder->stream_cond);
|
||||||
sc_mutex_unlock(&recorder->mutex);
|
sc_mutex_unlock(&recorder->mutex);
|
||||||
}
|
}
|
||||||
@ -640,6 +691,7 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
|
|||||||
bool
|
bool
|
||||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||||
enum sc_record_format format, bool audio,
|
enum sc_record_format format, bool audio,
|
||||||
|
struct sc_size declared_frame_size,
|
||||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
|
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
|
||||||
recorder->filename = strdup(filename);
|
recorder->filename = strdup(filename);
|
||||||
if (!recorder->filename) {
|
if (!recorder->filename) {
|
||||||
@ -664,17 +716,19 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
|||||||
|
|
||||||
recorder->audio = audio;
|
recorder->audio = audio;
|
||||||
|
|
||||||
sc_vecdeque_init(&recorder->video_queue);
|
sc_queue_init(&recorder->video_queue);
|
||||||
sc_vecdeque_init(&recorder->audio_queue);
|
sc_queue_init(&recorder->audio_queue);
|
||||||
recorder->stopped = false;
|
recorder->stopped = false;
|
||||||
|
|
||||||
recorder->video_init = false;
|
recorder->video_codec = NULL;
|
||||||
recorder->audio_init = false;
|
recorder->audio_codec = NULL;
|
||||||
|
recorder->audio_disabled = false;
|
||||||
|
|
||||||
recorder->video_stream_index = -1;
|
recorder->video_stream_index = -1;
|
||||||
recorder->audio_stream_index = -1;
|
recorder->audio_stream_index = -1;
|
||||||
|
|
||||||
recorder->format = format;
|
recorder->format = format;
|
||||||
|
recorder->declared_frame_size = declared_frame_size;
|
||||||
|
|
||||||
assert(cbs && cbs->on_ended);
|
assert(cbs && cbs->on_ended);
|
||||||
recorder->cbs = cbs;
|
recorder->cbs = cbs;
|
||||||
|
@ -9,10 +9,15 @@
|
|||||||
#include "coords.h"
|
#include "coords.h"
|
||||||
#include "options.h"
|
#include "options.h"
|
||||||
#include "trait/packet_sink.h"
|
#include "trait/packet_sink.h"
|
||||||
|
#include "util/queue.h"
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
#include "util/vecdeque.h"
|
|
||||||
|
|
||||||
struct sc_recorder_queue SC_VECDEQUE(AVPacket *);
|
struct sc_record_packet {
|
||||||
|
AVPacket *packet;
|
||||||
|
struct sc_record_packet *next;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sc_recorder_queue SC_QUEUE(struct sc_record_packet);
|
||||||
|
|
||||||
struct sc_recorder {
|
struct sc_recorder {
|
||||||
struct sc_packet_sink video_packet_sink;
|
struct sc_packet_sink video_packet_sink;
|
||||||
@ -31,6 +36,7 @@ struct sc_recorder {
|
|||||||
char *filename;
|
char *filename;
|
||||||
enum sc_record_format format;
|
enum sc_record_format format;
|
||||||
AVFormatContext *ctx;
|
AVFormatContext *ctx;
|
||||||
|
struct sc_size declared_frame_size;
|
||||||
|
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
sc_mutex mutex;
|
sc_mutex mutex;
|
||||||
@ -42,8 +48,11 @@ struct sc_recorder {
|
|||||||
|
|
||||||
// wake up the recorder thread once the video or audio codec is known
|
// wake up the recorder thread once the video or audio codec is known
|
||||||
sc_cond stream_cond;
|
sc_cond stream_cond;
|
||||||
bool video_init;
|
const AVCodec *video_codec;
|
||||||
bool audio_init;
|
const AVCodec *audio_codec;
|
||||||
|
// Instead of providing an audio_codec, the demuxer may notify that the
|
||||||
|
// stream is disabled if the device could not capture audio
|
||||||
|
bool audio_disabled;
|
||||||
|
|
||||||
int video_stream_index;
|
int video_stream_index;
|
||||||
int audio_stream_index;
|
int audio_stream_index;
|
||||||
@ -60,6 +69,7 @@ struct sc_recorder_callbacks {
|
|||||||
bool
|
bool
|
||||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||||
enum sc_record_format format, bool audio,
|
enum sc_record_format format, bool audio,
|
||||||
|
struct sc_size declared_frame_size,
|
||||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
|
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
|
||||||
|
|
||||||
bool
|
bool
|
||||||
|
@ -16,7 +16,6 @@
|
|||||||
#include "audio_player.h"
|
#include "audio_player.h"
|
||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "decoder.h"
|
#include "decoder.h"
|
||||||
#include "delay_buffer.h"
|
|
||||||
#include "demuxer.h"
|
#include "demuxer.h"
|
||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "file_pusher.h"
|
#include "file_pusher.h"
|
||||||
@ -48,10 +47,8 @@ struct scrcpy {
|
|||||||
struct sc_decoder video_decoder;
|
struct sc_decoder video_decoder;
|
||||||
struct sc_decoder audio_decoder;
|
struct sc_decoder audio_decoder;
|
||||||
struct sc_recorder recorder;
|
struct sc_recorder recorder;
|
||||||
struct sc_delay_buffer display_buffer;
|
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
struct sc_v4l2_sink v4l2_sink;
|
struct sc_v4l2_sink v4l2_sink;
|
||||||
struct sc_delay_buffer v4l2_buffer;
|
|
||||||
#endif
|
#endif
|
||||||
struct sc_controller controller;
|
struct sc_controller controller;
|
||||||
struct sc_file_pusher file_pusher;
|
struct sc_file_pusher file_pusher;
|
||||||
@ -175,9 +172,7 @@ event_loop(struct scrcpy *s) {
|
|||||||
LOGD("User requested to quit");
|
LOGD("User requested to quit");
|
||||||
return SCRCPY_EXIT_SUCCESS;
|
return SCRCPY_EXIT_SUCCESS;
|
||||||
default:
|
default:
|
||||||
if (!sc_screen_handle_event(&s->screen, &event)) {
|
sc_screen_handle_event(&s->screen, &event);
|
||||||
return SCRCPY_EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -223,15 +218,12 @@ sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer,
|
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
|
||||||
enum sc_demuxer_status status, void *userdata) {
|
void *userdata) {
|
||||||
(void) demuxer;
|
(void) demuxer;
|
||||||
(void) userdata;
|
(void) userdata;
|
||||||
|
|
||||||
// The device may not decide to disable the video
|
if (eos) {
|
||||||
assert(status != SC_DEMUXER_STATUS_DISABLED);
|
|
||||||
|
|
||||||
if (status == SC_DEMUXER_STATUS_EOS) {
|
|
||||||
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
||||||
} else {
|
} else {
|
||||||
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||||
@ -239,17 +231,20 @@ sc_video_demuxer_on_ended(struct sc_demuxer *demuxer,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
|
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
|
||||||
enum sc_demuxer_status status, void *userdata) {
|
void *userdata) {
|
||||||
(void) demuxer;
|
(void) demuxer;
|
||||||
|
(void) userdata;
|
||||||
|
|
||||||
const struct scrcpy_options *options = userdata;
|
// Contrary to the video demuxer, keep mirroring if only the audio fails.
|
||||||
|
// 'eos' is true on end-of-stream, including when audio capture is not
|
||||||
|
// possible on the device (so that scrcpy continue to mirror video without
|
||||||
|
// failing).
|
||||||
|
// However, if an audio configuration failure occurs (for example the user
|
||||||
|
// explicitly selected an unknown audio encoder), 'eos' is false and scrcpy
|
||||||
|
// must exit.
|
||||||
|
|
||||||
// Contrary to the video demuxer, keep mirroring if only the audio fails
|
if (!eos) {
|
||||||
// (unless --require-audio is set).
|
|
||||||
if (status == SC_DEMUXER_STATUS_ERROR
|
|
||||||
|| (status == SC_DEMUXER_STATUS_DISABLED
|
|
||||||
&& options->require_audio)) {
|
|
||||||
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -448,7 +443,7 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
.on_ended = sc_audio_demuxer_on_ended,
|
.on_ended = sc_audio_demuxer_on_ended,
|
||||||
};
|
};
|
||||||
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
|
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
|
||||||
&audio_demuxer_cbs, options);
|
&audio_demuxer_cbs, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool needs_video_decoder = options->display;
|
bool needs_video_decoder = options->display;
|
||||||
@ -458,13 +453,11 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
#endif
|
#endif
|
||||||
if (needs_video_decoder) {
|
if (needs_video_decoder) {
|
||||||
sc_decoder_init(&s->video_decoder, "video");
|
sc_decoder_init(&s->video_decoder, "video");
|
||||||
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
|
sc_demuxer_add_sink(&s->video_demuxer, &s->video_decoder.packet_sink);
|
||||||
&s->video_decoder.packet_sink);
|
|
||||||
}
|
}
|
||||||
if (needs_audio_decoder) {
|
if (needs_audio_decoder) {
|
||||||
sc_decoder_init(&s->audio_decoder, "audio");
|
sc_decoder_init(&s->audio_decoder, "audio");
|
||||||
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
|
sc_demuxer_add_sink(&s->audio_demuxer, &s->audio_decoder.packet_sink);
|
||||||
&s->audio_decoder.packet_sink);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->record_filename) {
|
if (options->record_filename) {
|
||||||
@ -473,7 +466,7 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
};
|
};
|
||||||
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
||||||
options->record_format, options->audio,
|
options->record_format, options->audio,
|
||||||
&recorder_cbs, NULL)) {
|
info->frame_size, &recorder_cbs, NULL)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
recorder_initialized = true;
|
recorder_initialized = true;
|
||||||
@ -483,11 +476,10 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
}
|
}
|
||||||
recorder_started = true;
|
recorder_started = true;
|
||||||
|
|
||||||
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
|
sc_demuxer_add_sink(&s->video_demuxer, &s->recorder.video_packet_sink);
|
||||||
&s->recorder.video_packet_sink);
|
|
||||||
if (options->audio) {
|
if (options->audio) {
|
||||||
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
|
sc_demuxer_add_sink(&s->audio_demuxer,
|
||||||
&s->recorder.audio_packet_sink);
|
&s->recorder.audio_packet_sink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -660,6 +652,7 @@ aoa_hid_end:
|
|||||||
.clipboard_autosync = options->clipboard_autosync,
|
.clipboard_autosync = options->clipboard_autosync,
|
||||||
.shortcut_mods = &options->shortcut_mods,
|
.shortcut_mods = &options->shortcut_mods,
|
||||||
.window_title = window_title,
|
.window_title = window_title,
|
||||||
|
.frame_size = info->frame_size,
|
||||||
.always_on_top = options->always_on_top,
|
.always_on_top = options->always_on_top,
|
||||||
.window_x = options->window_x,
|
.window_x = options->window_x,
|
||||||
.window_y = options->window_y,
|
.window_y = options->window_y,
|
||||||
@ -670,6 +663,7 @@ aoa_hid_end:
|
|||||||
.mipmaps = options->mipmaps,
|
.mipmaps = options->mipmaps,
|
||||||
.fullscreen = options->fullscreen,
|
.fullscreen = options->fullscreen,
|
||||||
.start_fps_counter = options->start_fps_counter,
|
.start_fps_counter = options->start_fps_counter,
|
||||||
|
.buffering_time = options->display_buffer,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!sc_screen_init(&s->screen, &screen_params)) {
|
if (!sc_screen_init(&s->screen, &screen_params)) {
|
||||||
@ -677,37 +671,22 @@ aoa_hid_end:
|
|||||||
}
|
}
|
||||||
screen_initialized = true;
|
screen_initialized = true;
|
||||||
|
|
||||||
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
sc_decoder_add_sink(&s->video_decoder, &s->screen.frame_sink);
|
||||||
if (options->display_buffer) {
|
|
||||||
sc_delay_buffer_init(&s->display_buffer, options->display_buffer,
|
|
||||||
true);
|
|
||||||
sc_frame_source_add_sink(src, &s->display_buffer.frame_sink);
|
|
||||||
src = &s->display_buffer.frame_source;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
|
||||||
|
|
||||||
if (options->audio) {
|
if (options->audio) {
|
||||||
sc_audio_player_init(&s->audio_player, options->audio_buffer);
|
sc_audio_player_init(&s->audio_player);
|
||||||
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
|
sc_decoder_add_sink(&s->audio_decoder, &s->audio_player.frame_sink);
|
||||||
&s->audio_player.frame_sink);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
if (options->v4l2_device) {
|
if (options->v4l2_device) {
|
||||||
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device)) {
|
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device,
|
||||||
|
info->frame_size, options->v4l2_buffer)) {
|
||||||
goto end;
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
sc_decoder_add_sink(&s->video_decoder, &s->v4l2_sink.frame_sink);
|
||||||
if (options->v4l2_buffer) {
|
|
||||||
sc_delay_buffer_init(&s->v4l2_buffer, options->v4l2_buffer, true);
|
|
||||||
sc_frame_source_add_sink(src, &s->v4l2_buffer.frame_sink);
|
|
||||||
src = &s->v4l2_buffer.frame_source;
|
|
||||||
}
|
|
||||||
|
|
||||||
sc_frame_source_add_sink(src, &s->v4l2_sink.frame_sink);
|
|
||||||
|
|
||||||
v4l2_sink_initialized = true;
|
v4l2_sink_initialized = true;
|
||||||
}
|
}
|
||||||
|
162
app/src/screen.c
162
app/src/screen.c
@ -7,6 +7,7 @@
|
|||||||
#include "events.h"
|
#include "events.h"
|
||||||
#include "icon.h"
|
#include "icon.h"
|
||||||
#include "options.h"
|
#include "options.h"
|
||||||
|
#include "video_buffer.h"
|
||||||
#include "util/log.h"
|
#include "util/log.h"
|
||||||
|
|
||||||
#define DISPLAY_MARGINS 96
|
#define DISPLAY_MARGINS 96
|
||||||
@ -239,7 +240,7 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static inline SDL_Texture *
|
||||||
create_texture(struct sc_screen *screen) {
|
create_texture(struct sc_screen *screen) {
|
||||||
SDL_Renderer *renderer = screen->renderer;
|
SDL_Renderer *renderer = screen->renderer;
|
||||||
struct sc_size size = screen->frame_size;
|
struct sc_size size = screen->frame_size;
|
||||||
@ -247,8 +248,7 @@ create_texture(struct sc_screen *screen) {
|
|||||||
SDL_TEXTUREACCESS_STREAMING,
|
SDL_TEXTUREACCESS_STREAMING,
|
||||||
size.width, size.height);
|
size.width, size.height);
|
||||||
if (!texture) {
|
if (!texture) {
|
||||||
LOGE("Could not create texture: %s", SDL_GetError());
|
return NULL;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screen->mipmaps) {
|
if (screen->mipmaps) {
|
||||||
@ -264,8 +264,7 @@ create_texture(struct sc_screen *screen) {
|
|||||||
SDL_GL_UnbindTexture(texture);
|
SDL_GL_UnbindTexture(texture);
|
||||||
}
|
}
|
||||||
|
|
||||||
screen->texture = texture;
|
return texture;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// render the texture to the renderer
|
// render the texture to the renderer
|
||||||
@ -337,25 +336,7 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
|
|||||||
(void) ctx;
|
(void) ctx;
|
||||||
|
|
||||||
struct sc_screen *screen = DOWNCAST(sink);
|
struct sc_screen *screen = DOWNCAST(sink);
|
||||||
|
(void) screen;
|
||||||
assert(ctx->width > 0 && ctx->width <= 0xFFFF);
|
|
||||||
assert(ctx->height > 0 && ctx->height <= 0xFFFF);
|
|
||||||
// screen->frame_size is never used before the event is pushed, and the
|
|
||||||
// event acts as a memory barrier so it is safe without mutex
|
|
||||||
screen->frame_size.width = ctx->width;
|
|
||||||
screen->frame_size.height = ctx->height;
|
|
||||||
|
|
||||||
static SDL_Event event = {
|
|
||||||
.type = SC_EVENT_SCREEN_INIT_SIZE,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Post the event on the UI thread (the texture must be created from there)
|
|
||||||
int ret = SDL_PushEvent(&event);
|
|
||||||
if (ret < 0) {
|
|
||||||
LOGW("Could not post init size event: %s", SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
screen->open = true;
|
screen->open = true;
|
||||||
#endif
|
#endif
|
||||||
@ -378,18 +359,30 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) {
|
|||||||
static bool
|
static bool
|
||||||
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||||
struct sc_screen *screen = DOWNCAST(sink);
|
struct sc_screen *screen = DOWNCAST(sink);
|
||||||
|
return sc_video_buffer_push(&screen->vb, frame);
|
||||||
|
}
|
||||||
|
|
||||||
bool previous_skipped;
|
static void
|
||||||
bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped);
|
sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
||||||
if (!ok) {
|
void *userdata) {
|
||||||
return false;
|
(void) vb;
|
||||||
}
|
struct sc_screen *screen = userdata;
|
||||||
|
|
||||||
|
// event_failed implies previous_skipped (the previous frame may not have
|
||||||
|
// been consumed if the event was not sent)
|
||||||
|
assert(!screen->event_failed || previous_skipped);
|
||||||
|
|
||||||
|
bool need_new_event;
|
||||||
if (previous_skipped) {
|
if (previous_skipped) {
|
||||||
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
|
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
|
||||||
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
|
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||||
// this new frame instead
|
// this new frame instead, unless the previous event failed
|
||||||
|
need_new_event = screen->event_failed;
|
||||||
} else {
|
} else {
|
||||||
|
need_new_event = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (need_new_event) {
|
||||||
static SDL_Event new_frame_event = {
|
static SDL_Event new_frame_event = {
|
||||||
.type = SC_EVENT_NEW_FRAME,
|
.type = SC_EVENT_NEW_FRAME,
|
||||||
};
|
};
|
||||||
@ -398,11 +391,11 @@ sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
|||||||
int ret = SDL_PushEvent(&new_frame_event);
|
int ret = SDL_PushEvent(&new_frame_event);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
LOGW("Could not post new frame event: %s", SDL_GetError());
|
LOGW("Could not post new frame event: %s", SDL_GetError());
|
||||||
return false;
|
screen->event_failed = true;
|
||||||
|
} else {
|
||||||
|
screen->event_failed = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@ -412,6 +405,7 @@ sc_screen_init(struct sc_screen *screen,
|
|||||||
screen->has_frame = false;
|
screen->has_frame = false;
|
||||||
screen->fullscreen = false;
|
screen->fullscreen = false;
|
||||||
screen->maximized = false;
|
screen->maximized = false;
|
||||||
|
screen->event_failed = false;
|
||||||
screen->mouse_capture_key_pressed = 0;
|
screen->mouse_capture_key_pressed = 0;
|
||||||
|
|
||||||
screen->req.x = params->window_x;
|
screen->req.x = params->window_x;
|
||||||
@ -421,19 +415,33 @@ sc_screen_init(struct sc_screen *screen,
|
|||||||
screen->req.fullscreen = params->fullscreen;
|
screen->req.fullscreen = params->fullscreen;
|
||||||
screen->req.start_fps_counter = params->start_fps_counter;
|
screen->req.start_fps_counter = params->start_fps_counter;
|
||||||
|
|
||||||
bool ok = sc_frame_buffer_init(&screen->fb);
|
static const struct sc_video_buffer_callbacks cbs = {
|
||||||
|
.on_new_frame = sc_video_buffer_on_new_frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool ok = sc_video_buffer_init(&screen->vb, params->buffering_time, &cbs,
|
||||||
|
screen);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sc_fps_counter_init(&screen->fps_counter)) {
|
ok = sc_video_buffer_start(&screen->vb);
|
||||||
goto error_destroy_frame_buffer;
|
if (!ok) {
|
||||||
|
goto error_destroy_video_buffer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!sc_fps_counter_init(&screen->fps_counter)) {
|
||||||
|
goto error_stop_and_join_video_buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
screen->frame_size = params->frame_size;
|
||||||
screen->rotation = params->rotation;
|
screen->rotation = params->rotation;
|
||||||
if (screen->rotation) {
|
if (screen->rotation) {
|
||||||
LOGI("Initial display rotation set to %u", screen->rotation);
|
LOGI("Initial display rotation set to %u", screen->rotation);
|
||||||
}
|
}
|
||||||
|
struct sc_size content_size =
|
||||||
|
get_rotated_size(screen->frame_size, screen->rotation);
|
||||||
|
screen->content_size = content_size;
|
||||||
|
|
||||||
uint32_t window_flags = SDL_WINDOW_HIDDEN
|
uint32_t window_flags = SDL_WINDOW_HIDDEN
|
||||||
| SDL_WINDOW_RESIZABLE
|
| SDL_WINDOW_RESIZABLE
|
||||||
@ -501,10 +509,18 @@ sc_screen_init(struct sc_screen *screen,
|
|||||||
LOGW("Could not load icon");
|
LOGW("Could not load icon");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LOGI("Initial texture: %" PRIu16 "x%" PRIu16, params->frame_size.width,
|
||||||
|
params->frame_size.height);
|
||||||
|
screen->texture = create_texture(screen);
|
||||||
|
if (!screen->texture) {
|
||||||
|
LOGE("Could not create texture: %s", SDL_GetError());
|
||||||
|
goto error_destroy_renderer;
|
||||||
|
}
|
||||||
|
|
||||||
screen->frame = av_frame_alloc();
|
screen->frame = av_frame_alloc();
|
||||||
if (!screen->frame) {
|
if (!screen->frame) {
|
||||||
LOG_OOM();
|
LOG_OOM();
|
||||||
goto error_destroy_renderer;
|
goto error_destroy_texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sc_input_manager_params im_params = {
|
struct sc_input_manager_params im_params = {
|
||||||
@ -539,14 +555,19 @@ sc_screen_init(struct sc_screen *screen,
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
error_destroy_texture:
|
||||||
|
SDL_DestroyTexture(screen->texture);
|
||||||
error_destroy_renderer:
|
error_destroy_renderer:
|
||||||
SDL_DestroyRenderer(screen->renderer);
|
SDL_DestroyRenderer(screen->renderer);
|
||||||
error_destroy_window:
|
error_destroy_window:
|
||||||
SDL_DestroyWindow(screen->window);
|
SDL_DestroyWindow(screen->window);
|
||||||
error_destroy_fps_counter:
|
error_destroy_fps_counter:
|
||||||
sc_fps_counter_destroy(&screen->fps_counter);
|
sc_fps_counter_destroy(&screen->fps_counter);
|
||||||
error_destroy_frame_buffer:
|
error_stop_and_join_video_buffer:
|
||||||
sc_frame_buffer_destroy(&screen->fb);
|
sc_video_buffer_stop(&screen->vb);
|
||||||
|
sc_video_buffer_join(&screen->vb);
|
||||||
|
error_destroy_video_buffer:
|
||||||
|
sc_video_buffer_destroy(&screen->vb);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -583,11 +604,13 @@ sc_screen_hide_window(struct sc_screen *screen) {
|
|||||||
|
|
||||||
void
|
void
|
||||||
sc_screen_interrupt(struct sc_screen *screen) {
|
sc_screen_interrupt(struct sc_screen *screen) {
|
||||||
|
sc_video_buffer_stop(&screen->vb);
|
||||||
sc_fps_counter_interrupt(&screen->fps_counter);
|
sc_fps_counter_interrupt(&screen->fps_counter);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_screen_join(struct sc_screen *screen) {
|
sc_screen_join(struct sc_screen *screen) {
|
||||||
|
sc_video_buffer_join(&screen->vb);
|
||||||
sc_fps_counter_join(&screen->fps_counter);
|
sc_fps_counter_join(&screen->fps_counter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -597,13 +620,11 @@ sc_screen_destroy(struct sc_screen *screen) {
|
|||||||
assert(!screen->open);
|
assert(!screen->open);
|
||||||
#endif
|
#endif
|
||||||
av_frame_free(&screen->frame);
|
av_frame_free(&screen->frame);
|
||||||
if (screen->texture) {
|
SDL_DestroyTexture(screen->texture);
|
||||||
SDL_DestroyTexture(screen->texture);
|
|
||||||
}
|
|
||||||
SDL_DestroyRenderer(screen->renderer);
|
SDL_DestroyRenderer(screen->renderer);
|
||||||
SDL_DestroyWindow(screen->window);
|
SDL_DestroyWindow(screen->window);
|
||||||
sc_fps_counter_destroy(&screen->fps_counter);
|
sc_fps_counter_destroy(&screen->fps_counter);
|
||||||
sc_frame_buffer_destroy(&screen->fb);
|
sc_video_buffer_destroy(&screen->vb);
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
@ -663,23 +684,6 @@ sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) {
|
|||||||
sc_screen_render(screen, true);
|
sc_screen_render(screen, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
|
||||||
sc_screen_init_size(struct sc_screen *screen) {
|
|
||||||
// Before first frame
|
|
||||||
assert(!screen->has_frame);
|
|
||||||
assert(!screen->texture);
|
|
||||||
|
|
||||||
// The requested size is passed via screen->frame_size
|
|
||||||
|
|
||||||
struct sc_size content_size =
|
|
||||||
get_rotated_size(screen->frame_size, screen->rotation);
|
|
||||||
screen->content_size = content_size;
|
|
||||||
|
|
||||||
LOGI("Initial texture: %" PRIu16 "x%" PRIu16,
|
|
||||||
screen->frame_size.width, screen->frame_size.height);
|
|
||||||
return create_texture(screen);
|
|
||||||
}
|
|
||||||
|
|
||||||
// recreate the texture and resize the window if the frame size has changed
|
// recreate the texture and resize the window if the frame size has changed
|
||||||
static bool
|
static bool
|
||||||
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
|
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
|
||||||
@ -698,7 +702,11 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
|
|||||||
|
|
||||||
LOGI("New texture: %" PRIu16 "x%" PRIu16,
|
LOGI("New texture: %" PRIu16 "x%" PRIu16,
|
||||||
screen->frame_size.width, screen->frame_size.height);
|
screen->frame_size.width, screen->frame_size.height);
|
||||||
return create_texture(screen);
|
screen->texture = create_texture(screen);
|
||||||
|
if (!screen->texture) {
|
||||||
|
LOGE("Could not create texture: %s", SDL_GetError());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@ -722,7 +730,7 @@ update_texture(struct sc_screen *screen, const AVFrame *frame) {
|
|||||||
static bool
|
static bool
|
||||||
sc_screen_update_frame(struct sc_screen *screen) {
|
sc_screen_update_frame(struct sc_screen *screen) {
|
||||||
av_frame_unref(screen->frame);
|
av_frame_unref(screen->frame);
|
||||||
sc_frame_buffer_consume(&screen->fb, screen->frame);
|
sc_video_buffer_consume(&screen->vb, screen->frame);
|
||||||
AVFrame *frame = screen->frame;
|
AVFrame *frame = screen->frame;
|
||||||
|
|
||||||
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
|
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
|
||||||
@ -811,31 +819,22 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) {
|
|||||||
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
|
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool
|
void
|
||||||
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||||
bool relative_mode = sc_screen_is_relative_mode(screen);
|
bool relative_mode = sc_screen_is_relative_mode(screen);
|
||||||
|
|
||||||
switch (event->type) {
|
switch (event->type) {
|
||||||
case SC_EVENT_SCREEN_INIT_SIZE:
|
|
||||||
// The initial size is passed via screen->frame_size
|
|
||||||
bool ok = sc_screen_init_size(screen);
|
|
||||||
if (!ok) {
|
|
||||||
LOGE("Could not initialize screen size");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
case SC_EVENT_NEW_FRAME: {
|
case SC_EVENT_NEW_FRAME: {
|
||||||
bool ok = sc_screen_update_frame(screen);
|
bool ok = sc_screen_update_frame(screen);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
LOGE("Frame update failed\n");
|
LOGW("Frame update failed\n");
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
if (!screen->has_frame) {
|
if (!screen->has_frame) {
|
||||||
// Do nothing
|
// Do nothing
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
switch (event->window.event) {
|
switch (event->window.event) {
|
||||||
case SDL_WINDOWEVENT_EXPOSED:
|
case SDL_WINDOWEVENT_EXPOSED:
|
||||||
@ -866,7 +865,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return true;
|
return;
|
||||||
case SDL_KEYDOWN:
|
case SDL_KEYDOWN:
|
||||||
if (relative_mode) {
|
if (relative_mode) {
|
||||||
SDL_Keycode key = event->key.keysym.sym;
|
SDL_Keycode key = event->key.keysym.sym;
|
||||||
@ -879,7 +878,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
|||||||
screen->mouse_capture_key_pressed = 0;
|
screen->mouse_capture_key_pressed = 0;
|
||||||
}
|
}
|
||||||
// Mouse capture keys are never forwarded to the device
|
// Mouse capture keys are never forwarded to the device
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -895,7 +894,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
|||||||
sc_screen_toggle_mouse_capture(screen);
|
sc_screen_toggle_mouse_capture(screen);
|
||||||
}
|
}
|
||||||
// Mouse capture keys are never forwarded to the device
|
// Mouse capture keys are never forwarded to the device
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
@ -905,7 +904,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
|||||||
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
||||||
// Do not forward to input manager, the mouse will be captured
|
// Do not forward to input manager, the mouse will be captured
|
||||||
// on SDL_MOUSEBUTTONUP
|
// on SDL_MOUSEBUTTONUP
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SDL_FINGERMOTION:
|
case SDL_FINGERMOTION:
|
||||||
@ -914,19 +913,18 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
|||||||
if (relative_mode) {
|
if (relative_mode) {
|
||||||
// Touch events are not compatible with relative mode
|
// Touch events are not compatible with relative mode
|
||||||
// (coordinates are not relative)
|
// (coordinates are not relative)
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case SDL_MOUSEBUTTONUP:
|
case SDL_MOUSEBUTTONUP:
|
||||||
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
||||||
sc_screen_set_mouse_capture(screen, true);
|
sc_screen_set_mouse_capture(screen, true);
|
||||||
return true;
|
return;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_input_manager_handle_event(&screen->im, event);
|
sc_input_manager_handle_event(&screen->im, event);
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct sc_point
|
struct sc_point
|
||||||
|
@ -10,12 +10,12 @@
|
|||||||
#include "controller.h"
|
#include "controller.h"
|
||||||
#include "coords.h"
|
#include "coords.h"
|
||||||
#include "fps_counter.h"
|
#include "fps_counter.h"
|
||||||
#include "frame_buffer.h"
|
|
||||||
#include "input_manager.h"
|
#include "input_manager.h"
|
||||||
#include "opengl.h"
|
#include "opengl.h"
|
||||||
#include "trait/key_processor.h"
|
#include "trait/key_processor.h"
|
||||||
#include "trait/frame_sink.h"
|
#include "trait/frame_sink.h"
|
||||||
#include "trait/mouse_processor.h"
|
#include "trait/mouse_processor.h"
|
||||||
|
#include "video_buffer.h"
|
||||||
|
|
||||||
struct sc_screen {
|
struct sc_screen {
|
||||||
struct sc_frame_sink frame_sink; // frame sink trait
|
struct sc_frame_sink frame_sink; // frame sink trait
|
||||||
@ -25,7 +25,7 @@ struct sc_screen {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
struct sc_input_manager im;
|
struct sc_input_manager im;
|
||||||
struct sc_frame_buffer fb;
|
struct sc_video_buffer vb;
|
||||||
struct sc_fps_counter fps_counter;
|
struct sc_fps_counter fps_counter;
|
||||||
|
|
||||||
// The initial requested window properties
|
// The initial requested window properties
|
||||||
@ -59,6 +59,8 @@ struct sc_screen {
|
|||||||
bool maximized;
|
bool maximized;
|
||||||
bool mipmaps;
|
bool mipmaps;
|
||||||
|
|
||||||
|
bool event_failed; // in case SDL_PushEvent() returned an error
|
||||||
|
|
||||||
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
|
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
|
||||||
// RGUI) must be pressed. This variable tracks the pressed capture key.
|
// RGUI) must be pressed. This variable tracks the pressed capture key.
|
||||||
SDL_Keycode mouse_capture_key_pressed;
|
SDL_Keycode mouse_capture_key_pressed;
|
||||||
@ -78,6 +80,7 @@ struct sc_screen_params {
|
|||||||
const struct sc_shortcut_mods *shortcut_mods;
|
const struct sc_shortcut_mods *shortcut_mods;
|
||||||
|
|
||||||
const char *window_title;
|
const char *window_title;
|
||||||
|
struct sc_size frame_size;
|
||||||
bool always_on_top;
|
bool always_on_top;
|
||||||
|
|
||||||
int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED
|
int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED
|
||||||
@ -92,6 +95,8 @@ struct sc_screen_params {
|
|||||||
|
|
||||||
bool fullscreen;
|
bool fullscreen;
|
||||||
bool start_fps_counter;
|
bool start_fps_counter;
|
||||||
|
|
||||||
|
sc_tick buffering_time;
|
||||||
};
|
};
|
||||||
|
|
||||||
// initialize screen, create window, renderer and texture (window is hidden)
|
// initialize screen, create window, renderer and texture (window is hidden)
|
||||||
@ -135,8 +140,7 @@ void
|
|||||||
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation);
|
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation);
|
||||||
|
|
||||||
// react to SDL events
|
// react to SDL events
|
||||||
// If this function returns false, scrcpy must exit with an error.
|
void
|
||||||
bool
|
|
||||||
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event);
|
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event);
|
||||||
|
|
||||||
// convert point from window coordinates to frame coordinates
|
// convert point from window coordinates to frame coordinates
|
||||||
|
@ -173,8 +173,6 @@ sc_server_get_codec_name(enum sc_codec codec) {
|
|||||||
return "opus";
|
return "opus";
|
||||||
case SC_CODEC_AAC:
|
case SC_CODEC_AAC:
|
||||||
return "aac";
|
return "aac";
|
||||||
case SC_CODEC_RAW:
|
|
||||||
return "raw";
|
|
||||||
default:
|
default:
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
@ -216,7 +214,7 @@ execute_server(struct sc_server *server,
|
|||||||
|
|
||||||
unsigned dyn_idx = count; // from there, the strings are allocated
|
unsigned dyn_idx = count; // from there, the strings are allocated
|
||||||
#define ADD_PARAM(fmt, ...) { \
|
#define ADD_PARAM(fmt, ...) { \
|
||||||
char *p; \
|
char *p = (char *) &cmd[count]; \
|
||||||
if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \
|
if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \
|
||||||
goto end; \
|
goto end; \
|
||||||
} \
|
} \
|
||||||
@ -441,9 +439,9 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
|
|||||||
static bool
|
static bool
|
||||||
device_read_info(struct sc_intr *intr, sc_socket device_socket,
|
device_read_info(struct sc_intr *intr, sc_socket device_socket,
|
||||||
struct sc_server_info *info) {
|
struct sc_server_info *info) {
|
||||||
unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH];
|
unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH + 4];
|
||||||
ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf));
|
ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf));
|
||||||
if (r < SC_DEVICE_NAME_FIELD_LENGTH) {
|
if (r < SC_DEVICE_NAME_FIELD_LENGTH + 4) {
|
||||||
LOGE("Could not retrieve device information");
|
LOGE("Could not retrieve device information");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -451,6 +449,9 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
|
|||||||
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
|
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
|
||||||
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
|
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
|
||||||
|
|
||||||
|
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||||
|
info->frame_size.width = sc_read16be(fields);
|
||||||
|
info->frame_size.height = sc_read16be(&fields[2]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@
|
|||||||
#define SC_DEVICE_NAME_FIELD_LENGTH 64
|
#define SC_DEVICE_NAME_FIELD_LENGTH 64
|
||||||
struct sc_server_info {
|
struct sc_server_info {
|
||||||
char device_name[SC_DEVICE_NAME_FIELD_LENGTH];
|
char device_name[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||||
|
struct sc_size frame_size;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct sc_server_params {
|
struct sc_server_params {
|
||||||
|
@ -7,6 +7,8 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <libavcodec/avcodec.h>
|
#include <libavcodec/avcodec.h>
|
||||||
|
|
||||||
|
typedef struct AVFrame AVFrame;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Frame sink trait.
|
* Frame sink trait.
|
||||||
*
|
*
|
||||||
@ -17,7 +19,6 @@ struct sc_frame_sink {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct sc_frame_sink_ops {
|
struct sc_frame_sink_ops {
|
||||||
/* The codec context is valid until the sink is closed */
|
|
||||||
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
|
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
|
||||||
void (*close)(struct sc_frame_sink *sink);
|
void (*close)(struct sc_frame_sink *sink);
|
||||||
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
|
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
#include "frame_source.h"
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_source_init(struct sc_frame_source *source) {
|
|
||||||
source->sink_count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_source_add_sink(struct sc_frame_source *source,
|
|
||||||
struct sc_frame_sink *sink) {
|
|
||||||
assert(source->sink_count < SC_FRAME_SOURCE_MAX_SINKS);
|
|
||||||
assert(sink);
|
|
||||||
assert(sink->ops);
|
|
||||||
source->sinks[source->sink_count++] = sink;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_frame_source_sinks_close_firsts(struct sc_frame_source *source,
|
|
||||||
unsigned count) {
|
|
||||||
while (count) {
|
|
||||||
struct sc_frame_sink *sink = source->sinks[--count];
|
|
||||||
sink->ops->close(sink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_frame_source_sinks_open(struct sc_frame_source *source,
|
|
||||||
const AVCodecContext *ctx) {
|
|
||||||
assert(source->sink_count);
|
|
||||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
|
||||||
struct sc_frame_sink *sink = source->sinks[i];
|
|
||||||
if (!sink->ops->open(sink, ctx)) {
|
|
||||||
sc_frame_source_sinks_close_firsts(source, i);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_source_sinks_close(struct sc_frame_source *source) {
|
|
||||||
assert(source->sink_count);
|
|
||||||
sc_frame_source_sinks_close_firsts(source, source->sink_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_frame_source_sinks_push(struct sc_frame_source *source,
|
|
||||||
const AVFrame *frame) {
|
|
||||||
assert(source->sink_count);
|
|
||||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
|
||||||
struct sc_frame_sink *sink = source->sinks[i];
|
|
||||||
if (!sink->ops->push(sink, frame)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
@ -1,38 +0,0 @@
|
|||||||
#ifndef SC_FRAME_SOURCE_H
|
|
||||||
#define SC_FRAME_SOURCE_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include "frame_sink.h"
|
|
||||||
|
|
||||||
#define SC_FRAME_SOURCE_MAX_SINKS 2
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Frame source trait
|
|
||||||
*
|
|
||||||
* Component able to send AVFrames should implement this trait.
|
|
||||||
*/
|
|
||||||
struct sc_frame_source {
|
|
||||||
struct sc_frame_sink *sinks[SC_FRAME_SOURCE_MAX_SINKS];
|
|
||||||
unsigned sink_count;
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_source_init(struct sc_frame_source *source);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_source_add_sink(struct sc_frame_source *source,
|
|
||||||
struct sc_frame_sink *sink);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_frame_source_sinks_open(struct sc_frame_source *source,
|
|
||||||
const AVCodecContext *ctx);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_frame_source_sinks_close(struct sc_frame_source *source);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_frame_source_sinks_push(struct sc_frame_source *source,
|
|
||||||
const AVFrame *frame);
|
|
||||||
|
|
||||||
#endif
|
|
@ -5,7 +5,9 @@
|
|||||||
|
|
||||||
#include <assert.h>
|
#include <assert.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <libavcodec/avcodec.h>
|
|
||||||
|
typedef struct AVCodec AVCodec;
|
||||||
|
typedef struct AVPacket AVPacket;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Packet sink trait.
|
* Packet sink trait.
|
||||||
@ -17,8 +19,8 @@ struct sc_packet_sink {
|
|||||||
};
|
};
|
||||||
|
|
||||||
struct sc_packet_sink_ops {
|
struct sc_packet_sink_ops {
|
||||||
/* The codec context is valid until the sink is closed */
|
/* The codec instance is static, it is valid until the end of the program */
|
||||||
bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx);
|
bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec);
|
||||||
void (*close)(struct sc_packet_sink *sink);
|
void (*close)(struct sc_packet_sink *sink);
|
||||||
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
||||||
|
|
||||||
|
@ -1,70 +0,0 @@
|
|||||||
#include "packet_source.h"
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_packet_source_init(struct sc_packet_source *source) {
|
|
||||||
source->sink_count = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_packet_source_add_sink(struct sc_packet_source *source,
|
|
||||||
struct sc_packet_sink *sink) {
|
|
||||||
assert(source->sink_count < SC_PACKET_SOURCE_MAX_SINKS);
|
|
||||||
assert(sink);
|
|
||||||
assert(sink->ops);
|
|
||||||
source->sinks[source->sink_count++] = sink;
|
|
||||||
}
|
|
||||||
|
|
||||||
static void
|
|
||||||
sc_packet_source_sinks_close_firsts(struct sc_packet_source *source,
|
|
||||||
unsigned count) {
|
|
||||||
while (count) {
|
|
||||||
struct sc_packet_sink *sink = source->sinks[--count];
|
|
||||||
sink->ops->close(sink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_packet_source_sinks_open(struct sc_packet_source *source,
|
|
||||||
AVCodecContext *ctx) {
|
|
||||||
assert(source->sink_count);
|
|
||||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
|
||||||
struct sc_packet_sink *sink = source->sinks[i];
|
|
||||||
if (!sink->ops->open(sink, ctx)) {
|
|
||||||
sc_packet_source_sinks_close_firsts(source, i);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_packet_source_sinks_close(struct sc_packet_source *source) {
|
|
||||||
assert(source->sink_count);
|
|
||||||
sc_packet_source_sinks_close_firsts(source, source->sink_count);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_packet_source_sinks_push(struct sc_packet_source *source,
|
|
||||||
const AVPacket *packet) {
|
|
||||||
assert(source->sink_count);
|
|
||||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
|
||||||
struct sc_packet_sink *sink = source->sinks[i];
|
|
||||||
if (!sink->ops->push(sink, packet)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_packet_source_sinks_disable(struct sc_packet_source *source) {
|
|
||||||
assert(source->sink_count);
|
|
||||||
for (unsigned i = 0; i < source->sink_count; ++i) {
|
|
||||||
struct sc_packet_sink *sink = source->sinks[i];
|
|
||||||
if (sink->ops->disable) {
|
|
||||||
sink->ops->disable(sink);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,41 +0,0 @@
|
|||||||
#ifndef SC_PACKET_SOURCE_H
|
|
||||||
#define SC_PACKET_SOURCE_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include "packet_sink.h"
|
|
||||||
|
|
||||||
#define SC_PACKET_SOURCE_MAX_SINKS 2
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Packet source trait
|
|
||||||
*
|
|
||||||
* Component able to send AVPackets should implement this trait.
|
|
||||||
*/
|
|
||||||
struct sc_packet_source {
|
|
||||||
struct sc_packet_sink *sinks[SC_PACKET_SOURCE_MAX_SINKS];
|
|
||||||
unsigned sink_count;
|
|
||||||
};
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_packet_source_init(struct sc_packet_source *source);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_packet_source_add_sink(struct sc_packet_source *source,
|
|
||||||
struct sc_packet_sink *sink);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_packet_source_sinks_open(struct sc_packet_source *source,
|
|
||||||
AVCodecContext *ctx);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_packet_source_sinks_close(struct sc_packet_source *source);
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_packet_source_sinks_push(struct sc_packet_source *source,
|
|
||||||
const AVPacket *packet);
|
|
||||||
|
|
||||||
void
|
|
||||||
sc_packet_source_sinks_disable(struct sc_packet_source *source);
|
|
||||||
|
|
||||||
#endif
|
|
@ -14,8 +14,6 @@
|
|||||||
|
|
||||||
#define DEFAULT_TIMEOUT 1000
|
#define DEFAULT_TIMEOUT 1000
|
||||||
|
|
||||||
#define SC_HID_EVENT_QUEUE_MAX 64
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
sc_hid_event_log(const struct sc_hid_event *event) {
|
sc_hid_event_log(const struct sc_hid_event *event) {
|
||||||
// HID Event: [00] FF FF FF FF...
|
// HID Event: [00] FF FF FF FF...
|
||||||
@ -50,20 +48,14 @@ sc_hid_event_destroy(struct sc_hid_event *hid_event) {
|
|||||||
bool
|
bool
|
||||||
sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
|
sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
|
||||||
struct sc_acksync *acksync) {
|
struct sc_acksync *acksync) {
|
||||||
sc_vecdeque_init(&aoa->queue);
|
cbuf_init(&aoa->queue);
|
||||||
|
|
||||||
if (!sc_vecdeque_reserve(&aoa->queue, SC_HID_EVENT_QUEUE_MAX)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!sc_mutex_init(&aoa->mutex)) {
|
if (!sc_mutex_init(&aoa->mutex)) {
|
||||||
sc_vecdeque_destroy(&aoa->queue);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sc_cond_init(&aoa->event_cond)) {
|
if (!sc_cond_init(&aoa->event_cond)) {
|
||||||
sc_mutex_destroy(&aoa->mutex);
|
sc_mutex_destroy(&aoa->mutex);
|
||||||
sc_vecdeque_destroy(&aoa->queue);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,10 +69,9 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
|
|||||||
void
|
void
|
||||||
sc_aoa_destroy(struct sc_aoa *aoa) {
|
sc_aoa_destroy(struct sc_aoa *aoa) {
|
||||||
// Destroy remaining events
|
// Destroy remaining events
|
||||||
while (!sc_vecdeque_is_empty(&aoa->queue)) {
|
struct sc_hid_event event;
|
||||||
struct sc_hid_event *event = sc_vecdeque_popref(&aoa->queue);
|
while (cbuf_take(&aoa->queue, &event)) {
|
||||||
assert(event);
|
sc_hid_event_destroy(&event);
|
||||||
sc_hid_event_destroy(event);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sc_cond_destroy(&aoa->event_cond);
|
sc_cond_destroy(&aoa->event_cond);
|
||||||
@ -221,19 +212,13 @@ sc_aoa_push_hid_event(struct sc_aoa *aoa, const struct sc_hid_event *event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
sc_mutex_lock(&aoa->mutex);
|
sc_mutex_lock(&aoa->mutex);
|
||||||
bool full = sc_vecdeque_is_full(&aoa->queue);
|
bool was_empty = cbuf_is_empty(&aoa->queue);
|
||||||
if (!full) {
|
bool res = cbuf_push(&aoa->queue, *event);
|
||||||
bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
|
if (was_empty) {
|
||||||
sc_vecdeque_push_noresize(&aoa->queue, *event);
|
sc_cond_signal(&aoa->event_cond);
|
||||||
if (was_empty) {
|
|
||||||
sc_cond_signal(&aoa->event_cond);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Otherwise (if the queue is full), the event is discarded
|
|
||||||
|
|
||||||
sc_mutex_unlock(&aoa->mutex);
|
sc_mutex_unlock(&aoa->mutex);
|
||||||
|
return res;
|
||||||
return !full;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@ -242,7 +227,7 @@ run_aoa_thread(void *data) {
|
|||||||
|
|
||||||
for (;;) {
|
for (;;) {
|
||||||
sc_mutex_lock(&aoa->mutex);
|
sc_mutex_lock(&aoa->mutex);
|
||||||
while (!aoa->stopped && sc_vecdeque_is_empty(&aoa->queue)) {
|
while (!aoa->stopped && cbuf_is_empty(&aoa->queue)) {
|
||||||
sc_cond_wait(&aoa->event_cond, &aoa->mutex);
|
sc_cond_wait(&aoa->event_cond, &aoa->mutex);
|
||||||
}
|
}
|
||||||
if (aoa->stopped) {
|
if (aoa->stopped) {
|
||||||
@ -250,9 +235,11 @@ run_aoa_thread(void *data) {
|
|||||||
sc_mutex_unlock(&aoa->mutex);
|
sc_mutex_unlock(&aoa->mutex);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
struct sc_hid_event event;
|
||||||
|
bool non_empty = cbuf_take(&aoa->queue, &event);
|
||||||
|
assert(non_empty);
|
||||||
|
(void) non_empty;
|
||||||
|
|
||||||
assert(!sc_vecdeque_is_empty(&aoa->queue));
|
|
||||||
struct sc_hid_event event = sc_vecdeque_pop(&aoa->queue);
|
|
||||||
uint64_t ack_to_wait = event.ack_to_wait;
|
uint64_t ack_to_wait = event.ack_to_wait;
|
||||||
sc_mutex_unlock(&aoa->mutex);
|
sc_mutex_unlock(&aoa->mutex);
|
||||||
|
|
||||||
|
@ -8,9 +8,9 @@
|
|||||||
|
|
||||||
#include "usb.h"
|
#include "usb.h"
|
||||||
#include "util/acksync.h"
|
#include "util/acksync.h"
|
||||||
|
#include "util/cbuf.h"
|
||||||
#include "util/thread.h"
|
#include "util/thread.h"
|
||||||
#include "util/tick.h"
|
#include "util/tick.h"
|
||||||
#include "util/vecdeque.h"
|
|
||||||
|
|
||||||
struct sc_hid_event {
|
struct sc_hid_event {
|
||||||
uint16_t accessory_id;
|
uint16_t accessory_id;
|
||||||
@ -27,7 +27,7 @@ sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id,
|
|||||||
void
|
void
|
||||||
sc_hid_event_destroy(struct sc_hid_event *hid_event);
|
sc_hid_event_destroy(struct sc_hid_event *hid_event);
|
||||||
|
|
||||||
struct sc_hid_event_queue SC_VECDEQUE(struct sc_hid_event);
|
struct sc_hid_event_queue CBUF(struct sc_hid_event, 64);
|
||||||
|
|
||||||
struct sc_aoa {
|
struct sc_aoa {
|
||||||
struct sc_usb *usb;
|
struct sc_usb *usb;
|
||||||
|
@ -1,94 +0,0 @@
|
|||||||
#ifndef SC_AUDIOBUF_H
|
|
||||||
#define SC_AUDIOBUF_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stdint.h>
|
|
||||||
|
|
||||||
#include "util/bytebuf.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wrapper around bytebuf to read and write samples
|
|
||||||
*
|
|
||||||
* Each sample takes sample_size bytes.
|
|
||||||
*/
|
|
||||||
struct sc_audiobuf {
|
|
||||||
struct sc_bytebuf buf;
|
|
||||||
size_t sample_size;
|
|
||||||
};
|
|
||||||
|
|
||||||
static inline uint32_t
|
|
||||||
sc_audiobuf_to_samples(struct sc_audiobuf *buf, size_t bytes) {
|
|
||||||
assert(bytes % buf->sample_size == 0);
|
|
||||||
return bytes / buf->sample_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline size_t
|
|
||||||
sc_audiobuf_to_bytes(struct sc_audiobuf *buf, uint32_t samples) {
|
|
||||||
return samples * buf->sample_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline bool
|
|
||||||
sc_audiobuf_init(struct sc_audiobuf *buf, size_t sample_size,
|
|
||||||
uint32_t capacity) {
|
|
||||||
buf->sample_size = sample_size;
|
|
||||||
return sc_bytebuf_init(&buf->buf, capacity * sample_size + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_audiobuf_read(struct sc_audiobuf *buf, uint8_t *to, uint32_t samples) {
|
|
||||||
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
|
|
||||||
sc_bytebuf_read(&buf->buf, to, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_audiobuf_skip(struct sc_audiobuf *buf, uint32_t samples) {
|
|
||||||
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
|
|
||||||
sc_bytebuf_skip(&buf->buf, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_audiobuf_write(struct sc_audiobuf *buf, const uint8_t *from,
|
|
||||||
uint32_t samples) {
|
|
||||||
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
|
|
||||||
sc_bytebuf_write(&buf->buf, from, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_audiobuf_prepare_write(struct sc_audiobuf *buf, const uint8_t *from,
|
|
||||||
uint32_t samples) {
|
|
||||||
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
|
|
||||||
sc_bytebuf_prepare_write(&buf->buf, from, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_audiobuf_commit_write(struct sc_audiobuf *buf, uint32_t samples) {
|
|
||||||
size_t bytes = sc_audiobuf_to_bytes(buf, samples);
|
|
||||||
sc_bytebuf_commit_write(&buf->buf, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline uint32_t
|
|
||||||
sc_audiobuf_can_read(struct sc_audiobuf *buf) {
|
|
||||||
size_t bytes = sc_bytebuf_can_read(&buf->buf);
|
|
||||||
return sc_audiobuf_to_samples(buf, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline uint32_t
|
|
||||||
sc_audiobuf_can_write(struct sc_audiobuf *buf) {
|
|
||||||
size_t bytes = sc_bytebuf_can_write(&buf->buf);
|
|
||||||
return sc_audiobuf_to_samples(buf, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline uint32_t
|
|
||||||
sc_audiobuf_capacity(struct sc_audiobuf *buf) {
|
|
||||||
size_t bytes = sc_bytebuf_capacity(&buf->buf);
|
|
||||||
return sc_audiobuf_to_samples(buf, bytes);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
|
||||||
sc_audiobuf_destroy(struct sc_audiobuf *buf) {
|
|
||||||
sc_bytebuf_destroy(&buf->buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif
|
|
@ -19,8 +19,8 @@ sc_average_push(struct sc_average *avg, float value) {
|
|||||||
avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count;
|
avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count;
|
||||||
}
|
}
|
||||||
|
|
||||||
float
|
bool
|
||||||
sc_average_get(struct sc_average *avg) {
|
sc_average_get(struct sc_average *avg, float *value) {
|
||||||
assert(avg->count);
|
*value = avg->avg;
|
||||||
return avg->avg;
|
return avg->count;
|
||||||
}
|
}
|
||||||
|
@ -22,19 +22,15 @@ struct sc_average {
|
|||||||
void
|
void
|
||||||
sc_average_init(struct sc_average *avg, unsigned range);
|
sc_average_init(struct sc_average *avg, unsigned range);
|
||||||
|
|
||||||
/**
|
/* Push a new value to update the "rolling" average */
|
||||||
* Push a new value to update the "rolling" average
|
|
||||||
*/
|
|
||||||
void
|
void
|
||||||
sc_average_push(struct sc_average *avg, float value);
|
sc_average_push(struct sc_average *avg, float value);
|
||||||
|
|
||||||
/**
|
/* Get the current average value (if available)
|
||||||
* Get the current average value
|
|
||||||
*
|
*
|
||||||
* It is an error to call this function if sc_average_push() has not been
|
* An average is available if sc_average_push() has been called at least once.
|
||||||
* called at least once.
|
|
||||||
*/
|
*/
|
||||||
float
|
bool
|
||||||
sc_average_get(struct sc_average *avg);
|
sc_average_get(struct sc_average *avg, float *value);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
bool
|
bool
|
||||||
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
|
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
|
||||||
assert(alloc_size);
|
assert(alloc_size);
|
||||||
|
// sufficient, but use more for alignment.
|
||||||
buf->data = malloc(alloc_size);
|
buf->data = malloc(alloc_size);
|
||||||
if (!buf->data) {
|
if (!buf->data) {
|
||||||
LOG_OOM();
|
LOG_OOM();
|
||||||
@ -30,7 +31,7 @@ sc_bytebuf_destroy(struct sc_bytebuf *buf) {
|
|||||||
void
|
void
|
||||||
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
|
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
|
||||||
assert(len);
|
assert(len);
|
||||||
assert(len <= sc_bytebuf_can_read(buf));
|
assert(sc_bytebuf_read_remaining(buf) >= len);
|
||||||
assert(buf->tail != buf->head); // the buffer could not be empty
|
assert(buf->tail != buf->head); // the buffer could not be empty
|
||||||
|
|
||||||
size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size;
|
size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size;
|
||||||
@ -50,40 +51,42 @@ sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
|
|||||||
void
|
void
|
||||||
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
|
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
|
||||||
assert(len);
|
assert(len);
|
||||||
assert(len <= sc_bytebuf_can_read(buf));
|
assert(sc_bytebuf_read_remaining(buf) >= len);
|
||||||
assert(buf->tail != buf->head); // the buffer could not be empty
|
assert(buf->tail != buf->head); // the buffer could not be empty
|
||||||
|
|
||||||
buf->tail = (buf->tail + len) % buf->alloc_size;
|
buf->tail = (buf->tail + len) % buf->alloc_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline void
|
void
|
||||||
sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from,
|
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
|
||||||
size_t len) {
|
assert(len);
|
||||||
size_t right_len = buf->alloc_size - buf->head;
|
|
||||||
|
size_t max_len = buf->alloc_size - 1;
|
||||||
|
if (len >= max_len) {
|
||||||
|
// Copy only the right-most bytes
|
||||||
|
memcpy(buf->data, from + len - max_len, max_len);
|
||||||
|
buf->tail = 0;
|
||||||
|
buf->head = max_len;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t right_limit = buf->head < buf->tail ? buf->tail : buf->alloc_size;
|
||||||
|
size_t right_len = right_limit - buf->head;
|
||||||
if (len < right_len) {
|
if (len < right_len) {
|
||||||
right_len = len;
|
right_len = len;
|
||||||
}
|
}
|
||||||
memcpy(buf->data + buf->head, from, right_len);
|
memcpy(buf->data + buf->head, from, right_len);
|
||||||
|
|
||||||
if (len > right_len) {
|
if (len > right_len) {
|
||||||
memcpy(buf->data, from + right_len, len - right_len);
|
memcpy(buf->data, from + right_len, len - right_len);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static inline void
|
size_t empty_space = sc_bytebuf_write_remaining(buf);
|
||||||
sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) {
|
if (len > empty_space) {
|
||||||
|
buf->tail = (buf->tail + len - empty_space) % buf->alloc_size;
|
||||||
|
}
|
||||||
buf->head = (buf->head + len) % buf->alloc_size;
|
buf->head = (buf->head + len) % buf->alloc_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
|
||||||
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
|
|
||||||
assert(len);
|
|
||||||
assert(len <= sc_bytebuf_can_write(buf));
|
|
||||||
|
|
||||||
sc_bytebuf_write_step0(buf, from, len);
|
|
||||||
sc_bytebuf_write_step1(buf, len);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
||||||
size_t len) {
|
size_t len) {
|
||||||
@ -94,11 +97,20 @@ sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
|||||||
// be called with lock held).
|
// be called with lock held).
|
||||||
|
|
||||||
assert(len < buf->alloc_size - 1);
|
assert(len < buf->alloc_size - 1);
|
||||||
sc_bytebuf_write_step0(buf, from, len);
|
|
||||||
|
size_t right_len = buf->alloc_size - buf->head;
|
||||||
|
if (len < right_len) {
|
||||||
|
right_len = len;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(buf->data + buf->head, from, right_len);
|
||||||
|
if (len > right_len) {
|
||||||
|
memcpy(buf->data, from + right_len, len - right_len);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
|
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
|
||||||
assert(len <= sc_bytebuf_can_write(buf));
|
assert(len <= sc_bytebuf_write_remaining(buf));
|
||||||
sc_bytebuf_write_step1(buf, len);
|
buf->head = (buf->head + len) % buf->alloc_size;
|
||||||
}
|
}
|
||||||
|
@ -11,10 +11,10 @@ struct sc_bytebuf {
|
|||||||
// The actual capacity is (allocated - 1) so that head == tail is
|
// The actual capacity is (allocated - 1) so that head == tail is
|
||||||
// non-ambiguous
|
// non-ambiguous
|
||||||
size_t alloc_size;
|
size_t alloc_size;
|
||||||
size_t head; // writter cursor
|
size_t head;
|
||||||
size_t tail; // reader cursor
|
size_t tail;
|
||||||
// empty: tail == head
|
// empty: tail == head
|
||||||
// full: ((tail + 1) % alloc_size) == head
|
// full: (tail + 1) % allocated == head
|
||||||
};
|
};
|
||||||
|
|
||||||
bool
|
bool
|
||||||
@ -23,10 +23,10 @@ sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size);
|
|||||||
/**
|
/**
|
||||||
* Copy from the bytebuf to a user-provided array
|
* Copy from the bytebuf to a user-provided array
|
||||||
*
|
*
|
||||||
* The caller must check that len <= sc_bytebuf_read_available() (it is an
|
* The caller must check that len <= buf->len (it is an error to attempt to read
|
||||||
* error to attempt to read more bytes than available).
|
* more bytes than available).
|
||||||
*
|
*
|
||||||
* This function is guaranteed not to write to buf->head.
|
* This function is guaranteed to not change the head.
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
|
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
|
||||||
@ -34,13 +34,13 @@ sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
|
|||||||
/**
|
/**
|
||||||
* Drop len bytes from the buffer
|
* Drop len bytes from the buffer
|
||||||
*
|
*
|
||||||
* The caller must check that len <= sc_bytebuf_read_available() (it is an
|
* The caller must check that len <= buf->len (it is an error to attempt to skip
|
||||||
* error to attempt to skip more bytes than available).
|
* more bytes than available).
|
||||||
*
|
*
|
||||||
* This function is guaranteed not to write to buf->head.
|
* This function is guaranteed to not change the head.
|
||||||
*
|
*
|
||||||
* It is equivalent to call sc_bytebuf_read() to some array and discard the
|
* It is equivalent to call sc_bytebuf_read() to some array and discard the
|
||||||
* array (but this function is more efficient since there is no copy).
|
* array (but more efficient since there is no copy).
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
|
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
|
||||||
@ -48,10 +48,9 @@ sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
|
|||||||
/**
|
/**
|
||||||
* Copy the user-provided array to the bytebuf
|
* Copy the user-provided array to the bytebuf
|
||||||
*
|
*
|
||||||
* The caller must check that len <= sc_bytebuf_write_available() (it is an
|
* The length of the input array is not restricted:
|
||||||
* error to write more bytes than the remaining available space).
|
* if len >= sc_bytebuf_write_remaining(buf), then the excessive input bytes
|
||||||
*
|
* will overwrite the oldest bytes in the buffer.
|
||||||
* This function is guaranteed not to write to buf->tail.
|
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len);
|
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len);
|
||||||
@ -59,16 +58,14 @@ sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len);
|
|||||||
/**
|
/**
|
||||||
* Copy the user-provided array to the bytebuf, but do not advance the cursor
|
* Copy the user-provided array to the bytebuf, but do not advance the cursor
|
||||||
*
|
*
|
||||||
* The caller must check that len <= sc_bytebuf_write_available() (it is an
|
* The caller must check that len <= buf->len (it is an error to attempt to
|
||||||
* error to write more bytes than the remaining available space).
|
* write more bytes than available).
|
||||||
*
|
*
|
||||||
* After this function is called, the write must be committed with
|
* After this function is called, the write must be committed with
|
||||||
* sc_bytebuf_commit_write().
|
* sc_bytebuf_commit_write().
|
||||||
*
|
*
|
||||||
* The purpose of this mechanism is to acquire a lock only to commit the write,
|
* The purpose of this mechanism is to acquire a lock only to commit the write,
|
||||||
* but not to perform the actual copy.
|
* but not to perform the actual copy.
|
||||||
*
|
|
||||||
* This function is guaranteed not to access buf->tail.
|
|
||||||
*/
|
*/
|
||||||
void
|
void
|
||||||
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
||||||
@ -86,28 +83,21 @@ sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
|
|||||||
* It is an error to read more bytes than available.
|
* It is an error to read more bytes than available.
|
||||||
*/
|
*/
|
||||||
static inline size_t
|
static inline size_t
|
||||||
sc_bytebuf_can_read(struct sc_bytebuf *buf) {
|
sc_bytebuf_read_remaining(struct sc_bytebuf *buf) {
|
||||||
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
|
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return the number of bytes which can be written
|
* Return the number of bytes which can be written without overwriting
|
||||||
*
|
*
|
||||||
* It is an error to write more bytes than available.
|
* It is not an error to write more bytes than the available space, but this
|
||||||
|
* would overwrite the oldest bytes in the buffer.
|
||||||
*/
|
*/
|
||||||
static inline size_t
|
static inline size_t
|
||||||
sc_bytebuf_can_write(struct sc_bytebuf *buf) {
|
sc_bytebuf_write_remaining(struct sc_bytebuf *buf) {
|
||||||
return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size;
|
return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the actual capacity of the buffer (can_read() + can_write())
|
|
||||||
*/
|
|
||||||
static inline size_t
|
|
||||||
sc_bytebuf_capacity(struct sc_bytebuf *buf) {
|
|
||||||
return buf->alloc_size - 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_bytebuf_destroy(struct sc_bytebuf *buf);
|
sc_bytebuf_destroy(struct sc_bytebuf *buf);
|
||||||
|
|
||||||
|
52
app/src/util/cbuf.h
Normal file
52
app/src/util/cbuf.h
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
// generic circular buffer (bounded queue) implementation
|
||||||
|
#ifndef SC_CBUF_H
|
||||||
|
#define SC_CBUF_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
// To define a circular buffer type of 20 ints:
|
||||||
|
// struct cbuf_int CBUF(int, 20);
|
||||||
|
//
|
||||||
|
// data has length CAP + 1 to distinguish empty vs full.
|
||||||
|
#define CBUF(TYPE, CAP) { \
|
||||||
|
TYPE data[(CAP) + 1]; \
|
||||||
|
size_t head; \
|
||||||
|
size_t tail; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define cbuf_size_(PCBUF) \
|
||||||
|
(sizeof((PCBUF)->data) / sizeof(*(PCBUF)->data))
|
||||||
|
|
||||||
|
#define cbuf_is_empty(PCBUF) \
|
||||||
|
((PCBUF)->head == (PCBUF)->tail)
|
||||||
|
|
||||||
|
#define cbuf_is_full(PCBUF) \
|
||||||
|
(((PCBUF)->head + 1) % cbuf_size_(PCBUF) == (PCBUF)->tail)
|
||||||
|
|
||||||
|
#define cbuf_init(PCBUF) \
|
||||||
|
(void) ((PCBUF)->head = (PCBUF)->tail = 0)
|
||||||
|
|
||||||
|
#define cbuf_push(PCBUF, ITEM) \
|
||||||
|
({ \
|
||||||
|
bool ok = !cbuf_is_full(PCBUF); \
|
||||||
|
if (ok) { \
|
||||||
|
(PCBUF)->data[(PCBUF)->head] = (ITEM); \
|
||||||
|
(PCBUF)->head = ((PCBUF)->head + 1) % cbuf_size_(PCBUF); \
|
||||||
|
} \
|
||||||
|
ok; \
|
||||||
|
})
|
||||||
|
|
||||||
|
#define cbuf_take(PCBUF, PITEM) \
|
||||||
|
({ \
|
||||||
|
bool ok = !cbuf_is_empty(PCBUF); \
|
||||||
|
if (ok) { \
|
||||||
|
*(PITEM) = (PCBUF)->data[(PCBUF)->tail]; \
|
||||||
|
(PCBUF)->tail = ((PCBUF)->tail + 1) % cbuf_size_(PCBUF); \
|
||||||
|
} \
|
||||||
|
ok; \
|
||||||
|
})
|
||||||
|
|
||||||
|
#endif
|
@ -125,30 +125,8 @@ sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
|||||||
free(local_fmt);
|
free(local_fmt);
|
||||||
}
|
}
|
||||||
|
|
||||||
static const char *const sc_sdl_log_priority_names[SDL_NUM_LOG_PRIORITIES] = {
|
|
||||||
[SDL_LOG_PRIORITY_VERBOSE] = "VERBOSE",
|
|
||||||
[SDL_LOG_PRIORITY_DEBUG] = "DEBUG",
|
|
||||||
[SDL_LOG_PRIORITY_INFO] = "INFO",
|
|
||||||
[SDL_LOG_PRIORITY_WARN] = "WARN",
|
|
||||||
[SDL_LOG_PRIORITY_ERROR] = "ERROR",
|
|
||||||
[SDL_LOG_PRIORITY_CRITICAL] = "CRITICAL",
|
|
||||||
};
|
|
||||||
|
|
||||||
static void SDLCALL
|
|
||||||
sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority,
|
|
||||||
const char *message) {
|
|
||||||
(void) userdata;
|
|
||||||
(void) category;
|
|
||||||
|
|
||||||
FILE *out = priority < SDL_LOG_PRIORITY_WARN ? stdout : stderr;
|
|
||||||
assert(priority < SDL_NUM_LOG_PRIORITIES);
|
|
||||||
const char *prio_name = sc_sdl_log_priority_names[priority];
|
|
||||||
fprintf(out, "%s: %s\n", prio_name, message);
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_log_configure() {
|
sc_log_configure() {
|
||||||
SDL_LogSetOutputFunction(sc_sdl_log_print, NULL);
|
|
||||||
// Redirect FFmpeg logs to SDL logs
|
// Redirect FFmpeg logs to SDL logs
|
||||||
av_log_set_callback(sc_av_log_callback);
|
av_log_set_callback(sc_av_log_callback);
|
||||||
}
|
}
|
||||||
|
@ -1,14 +0,0 @@
|
|||||||
#include "memory.h"
|
|
||||||
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <errno.h>
|
|
||||||
|
|
||||||
void *
|
|
||||||
sc_allocarray(size_t nmemb, size_t size) {
|
|
||||||
size_t bytes;
|
|
||||||
if (__builtin_mul_overflow(nmemb, size, &bytes)) {
|
|
||||||
errno = ENOMEM;
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
return malloc(bytes);
|
|
||||||
}
|
|
@ -1,15 +0,0 @@
|
|||||||
#ifndef SC_MEMORY_H
|
|
||||||
#define SC_MEMORY_H
|
|
||||||
|
|
||||||
#include <stddef.h>
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Allocate an array of `nmemb` items of `size` bytes each
|
|
||||||
*
|
|
||||||
* Like calloc(), but without initialization.
|
|
||||||
* Like reallocarray(), but without reallocation.
|
|
||||||
*/
|
|
||||||
void *
|
|
||||||
sc_allocarray(size_t nmemb, size_t size);
|
|
||||||
|
|
||||||
#endif
|
|
77
app/src/util/queue.h
Normal file
77
app/src/util/queue.h
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
// generic intrusive FIFO queue
|
||||||
|
#ifndef SC_QUEUE_H
|
||||||
|
#define SC_QUEUE_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stddef.h>
|
||||||
|
|
||||||
|
// To define a queue type of "struct foo":
|
||||||
|
// struct queue_foo QUEUE(struct foo);
|
||||||
|
#define SC_QUEUE(TYPE) { \
|
||||||
|
TYPE *first; \
|
||||||
|
TYPE *last; \
|
||||||
|
}
|
||||||
|
|
||||||
|
#define sc_queue_init(PQ) \
|
||||||
|
(void) ((PQ)->first = (PQ)->last = NULL)
|
||||||
|
|
||||||
|
#define sc_queue_is_empty(PQ) \
|
||||||
|
!(PQ)->first
|
||||||
|
|
||||||
|
// NEXTFIELD is the field in the ITEM type used for intrusive linked-list
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
// struct foo {
|
||||||
|
// int value;
|
||||||
|
// struct foo *next;
|
||||||
|
// };
|
||||||
|
//
|
||||||
|
// // define the type "struct my_queue"
|
||||||
|
// struct my_queue SC_QUEUE(struct foo);
|
||||||
|
//
|
||||||
|
// struct my_queue queue;
|
||||||
|
// sc_queue_init(&queue);
|
||||||
|
//
|
||||||
|
// struct foo v1 = { .value = 42 };
|
||||||
|
// struct foo v2 = { .value = 27 };
|
||||||
|
//
|
||||||
|
// sc_queue_push(&queue, next, v1);
|
||||||
|
// sc_queue_push(&queue, next, v2);
|
||||||
|
//
|
||||||
|
// struct foo *foo;
|
||||||
|
// sc_queue_take(&queue, next, &foo);
|
||||||
|
// assert(foo->value == 42);
|
||||||
|
// sc_queue_take(&queue, next, &foo);
|
||||||
|
// assert(foo->value == 27);
|
||||||
|
// assert(sc_queue_is_empty(&queue));
|
||||||
|
//
|
||||||
|
|
||||||
|
// push a new item into the queue
|
||||||
|
#define sc_queue_push(PQ, NEXTFIELD, ITEM) \
|
||||||
|
(void) ({ \
|
||||||
|
(ITEM)->NEXTFIELD = NULL; \
|
||||||
|
if (sc_queue_is_empty(PQ)) { \
|
||||||
|
(PQ)->first = (PQ)->last = (ITEM); \
|
||||||
|
} else { \
|
||||||
|
(PQ)->last->NEXTFIELD = (ITEM); \
|
||||||
|
(PQ)->last = (ITEM); \
|
||||||
|
} \
|
||||||
|
})
|
||||||
|
|
||||||
|
// take the next item and remove it from the queue (the queue must not be empty)
|
||||||
|
// the result is stored in *(PITEM)
|
||||||
|
// (without typeof(), we could not store a local variable having the correct
|
||||||
|
// type so that we can "return" it)
|
||||||
|
#define sc_queue_take(PQ, NEXTFIELD, PITEM) \
|
||||||
|
(void) ({ \
|
||||||
|
assert(!sc_queue_is_empty(PQ)); \
|
||||||
|
*(PITEM) = (PQ)->first; \
|
||||||
|
(PQ)->first = (PQ)->first->NEXTFIELD; \
|
||||||
|
})
|
||||||
|
// no need to update (PQ)->last if the queue is left empty:
|
||||||
|
// (PQ)->last is undefined if !(PQ)->first anyway
|
||||||
|
|
||||||
|
#endif
|
@ -23,39 +23,6 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
static SDL_ThreadPriority
|
|
||||||
to_sdl_thread_priority(enum sc_thread_priority priority) {
|
|
||||||
switch (priority) {
|
|
||||||
case SC_THREAD_PRIORITY_TIME_CRITICAL:
|
|
||||||
#ifdef SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
|
|
||||||
return SDL_THREAD_PRIORITY_TIME_CRITICAL;
|
|
||||||
#else
|
|
||||||
// fall through
|
|
||||||
#endif
|
|
||||||
case SC_THREAD_PRIORITY_HIGH:
|
|
||||||
return SDL_THREAD_PRIORITY_HIGH;
|
|
||||||
case SC_THREAD_PRIORITY_NORMAL:
|
|
||||||
return SDL_THREAD_PRIORITY_NORMAL;
|
|
||||||
case SC_THREAD_PRIORITY_LOW:
|
|
||||||
return SDL_THREAD_PRIORITY_LOW;
|
|
||||||
default:
|
|
||||||
assert(!"Unknown thread priority");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
bool
|
|
||||||
sc_thread_set_priority(enum sc_thread_priority priority) {
|
|
||||||
SDL_ThreadPriority sdl_priority = to_sdl_thread_priority(priority);
|
|
||||||
int r = SDL_SetThreadPriority(sdl_priority);
|
|
||||||
if (r) {
|
|
||||||
LOGD("Could not set thread priority: %s", SDL_GetError());
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_thread_join(sc_thread *thread, int *status) {
|
sc_thread_join(sc_thread *thread, int *status) {
|
||||||
SDL_WaitThread(thread->thread, status);
|
SDL_WaitThread(thread->thread, status);
|
||||||
|
@ -21,13 +21,6 @@ typedef struct sc_thread {
|
|||||||
SDL_Thread *thread;
|
SDL_Thread *thread;
|
||||||
} sc_thread;
|
} sc_thread;
|
||||||
|
|
||||||
enum sc_thread_priority {
|
|
||||||
SC_THREAD_PRIORITY_LOW,
|
|
||||||
SC_THREAD_PRIORITY_NORMAL,
|
|
||||||
SC_THREAD_PRIORITY_HIGH,
|
|
||||||
SC_THREAD_PRIORITY_TIME_CRITICAL,
|
|
||||||
};
|
|
||||||
|
|
||||||
typedef struct sc_mutex {
|
typedef struct sc_mutex {
|
||||||
SDL_mutex *mutex;
|
SDL_mutex *mutex;
|
||||||
#ifndef NDEBUG
|
#ifndef NDEBUG
|
||||||
@ -46,9 +39,6 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
|
|||||||
void
|
void
|
||||||
sc_thread_join(sc_thread *thread, int *status);
|
sc_thread_join(sc_thread *thread, int *status);
|
||||||
|
|
||||||
bool
|
|
||||||
sc_thread_set_priority(enum sc_thread_priority priority);
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_mutex_init(sc_mutex *mutex);
|
sc_mutex_init(sc_mutex *mutex);
|
||||||
|
|
||||||
|
@ -1,379 +0,0 @@
|
|||||||
#ifndef SC_VECDEQUE_H
|
|
||||||
#define SC_VECDEQUE_H
|
|
||||||
|
|
||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
#include <stdbool.h>
|
|
||||||
#include <stddef.h>
|
|
||||||
#include <stdlib.h>
|
|
||||||
#include <string.h>
|
|
||||||
|
|
||||||
#include "util/memory.h"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A double-ended queue implemented with a growable ring buffer.
|
|
||||||
*
|
|
||||||
* Inspired from the Rust VecDeque type:
|
|
||||||
* <https://doc.rust-lang.org/std/collections/struct.VecDeque.html>
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* VecDeque struct body
|
|
||||||
*
|
|
||||||
* A VecDeque is a dynamic ring-buffer, managed by the sc_vecdeque_* helpers.
|
|
||||||
*
|
|
||||||
* It is generic over the type of its items, so it is implemented via macros.
|
|
||||||
*
|
|
||||||
* To use a VecDeque, a new type must be defined:
|
|
||||||
*
|
|
||||||
* struct vecdeque_int SC_VECDEQUE(int);
|
|
||||||
*
|
|
||||||
* The struct may be anonymous:
|
|
||||||
*
|
|
||||||
* struct SC_VECDEQUE(const char *) names;
|
|
||||||
*
|
|
||||||
* Functions and macros having name ending with '_' are private.
|
|
||||||
*/
|
|
||||||
#define SC_VECDEQUE(type) { \
|
|
||||||
size_t cap; \
|
|
||||||
size_t origin; \
|
|
||||||
size_t size; \
|
|
||||||
type *data; \
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Static initializer for a VecDeque
|
|
||||||
*/
|
|
||||||
#define SC_VECDEQUE_INITIALIZER { 0, 0, 0, NULL }
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize an empty VecDeque
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_init(pv) \
|
|
||||||
({ \
|
|
||||||
(pv)->cap = 0; \
|
|
||||||
(pv)->origin = 0; \
|
|
||||||
(pv)->size = 0; \
|
|
||||||
(pv)->data = NULL; \
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Destroy a VecDeque
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_destroy(pv) \
|
|
||||||
free((pv)->data)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear a VecDeque
|
|
||||||
*
|
|
||||||
* Remove all items.
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_clear(pv) \
|
|
||||||
(void) ({ \
|
|
||||||
sc_vecdeque_destroy(pv); \
|
|
||||||
sc_vecdeque_init(pv); \
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the content size
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_size(pv) \
|
|
||||||
(pv)->size
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether the VecDeque is empty (i.e. its size is 0)
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_is_empty(pv) \
|
|
||||||
((pv)->size == 0)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether the VecDeque is full
|
|
||||||
*
|
|
||||||
* A VecDeque is full when its size equals its current capacity. However, it
|
|
||||||
* does not prevent to push a new item (with sc_vecdeque_push()), since this
|
|
||||||
* will increase its capacity.
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_is_full(pv) \
|
|
||||||
((pv)->size == (pv)->cap)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The minimal allocation size, in number of items
|
|
||||||
*
|
|
||||||
* Private.
|
|
||||||
*/
|
|
||||||
#define SC_VECDEQUE_MINCAP_ ((size_t) 10)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The maximal allocation size, in number of items
|
|
||||||
*
|
|
||||||
* Use SIZE_MAX/2 to fit in ssize_t, and so that cap*1.5 does not overflow.
|
|
||||||
*
|
|
||||||
* Private.
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_max_cap_(pv) (SIZE_MAX / 2 / sizeof(*(pv)->data))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Realloc the internal array to a specific capacity
|
|
||||||
*
|
|
||||||
* On reallocation success, update the VecDeque capacity (`*pcap`) and origin
|
|
||||||
* (`*porigin`), and return the reallocated data.
|
|
||||||
*
|
|
||||||
* On reallocation failure, return NULL without any change.
|
|
||||||
*
|
|
||||||
* Private.
|
|
||||||
*
|
|
||||||
* \param ptr the current `data` field of the SC_VECDEQUE to realloc
|
|
||||||
* \param newcap the requested capacity, in number of items
|
|
||||||
* \param item_size the size of one item (the generic type is unknown from this
|
|
||||||
* function)
|
|
||||||
* \param pcap a pointer to the `cap` field of the SC_VECDEQUE [IN/OUT]
|
|
||||||
* \param porigin a pointer to pv->origin [IN/OUT]
|
|
||||||
* \param size the `size` field of the SC_VECDEQUE
|
|
||||||
* \return the new array to assign to the `data` field of the SC_VECDEQUE (if
|
|
||||||
* not NULL)
|
|
||||||
*/
|
|
||||||
static inline void *
|
|
||||||
sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size,
|
|
||||||
size_t *pcap, size_t *porigin, size_t size) {
|
|
||||||
|
|
||||||
size_t oldcap = *pcap;
|
|
||||||
size_t oldorigin = *porigin;
|
|
||||||
|
|
||||||
assert(newcap > oldcap); // Could only grow
|
|
||||||
|
|
||||||
if (oldorigin + size <= oldcap) {
|
|
||||||
// The current content will stay in place, just realloc
|
|
||||||
//
|
|
||||||
// As an example, here is the content of a ring-buffer (oldcap=10)
|
|
||||||
// before the realloc:
|
|
||||||
//
|
|
||||||
// _ _ 2 3 4 5 6 7 _ _
|
|
||||||
// ^
|
|
||||||
// origin
|
|
||||||
//
|
|
||||||
// It is resized (newcap=15), e.g. with sc_vecdeque_reserve():
|
|
||||||
//
|
|
||||||
// _ _ 2 3 4 5 6 7 _ _ _ _ _ _ _
|
|
||||||
// ^
|
|
||||||
// origin
|
|
||||||
|
|
||||||
void *newptr = reallocarray(ptr, newcap, item_size);
|
|
||||||
if (!newptr) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
*pcap = newcap;
|
|
||||||
return newptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy the current content to the new array
|
|
||||||
//
|
|
||||||
// As an example, here is the content of a ring-buffer (oldcap=10) before
|
|
||||||
// the realloc:
|
|
||||||
//
|
|
||||||
// 5 6 7 _ _ 0 1 2 3 4
|
|
||||||
// ^
|
|
||||||
// origin
|
|
||||||
//
|
|
||||||
// It is resized (newcap=15), e.g. with sc_vecdeque_reserve():
|
|
||||||
//
|
|
||||||
// 0 1 2 3 4 5 6 7 _ _ _ _ _ _ _
|
|
||||||
// ^
|
|
||||||
// origin
|
|
||||||
|
|
||||||
assert(size);
|
|
||||||
void *newptr = sc_allocarray(newcap, item_size);
|
|
||||||
if (!newptr) {
|
|
||||||
return NULL;
|
|
||||||
}
|
|
||||||
|
|
||||||
size_t right_len = MIN(size, oldcap - oldorigin);
|
|
||||||
assert(right_len);
|
|
||||||
memcpy(newptr, ptr + (oldorigin * item_size), right_len * item_size);
|
|
||||||
|
|
||||||
if (size > right_len) {
|
|
||||||
memcpy(newptr + (right_len * item_size), ptr,
|
|
||||||
(size - right_len) * item_size);
|
|
||||||
}
|
|
||||||
|
|
||||||
free(ptr);
|
|
||||||
|
|
||||||
*pcap = newcap;
|
|
||||||
*porigin = 0;
|
|
||||||
return newptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Macro to realloc the internal data to a new capacity
|
|
||||||
*
|
|
||||||
* Private.
|
|
||||||
*
|
|
||||||
* \retval true on success
|
|
||||||
* \retval false on allocation failure (the VecDeque is left untouched)
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_realloc_(pv, newcap) \
|
|
||||||
({ \
|
|
||||||
void *p = sc_vecdeque_reallocdata_((pv)->data, newcap, \
|
|
||||||
sizeof(*(pv)->data), &(pv)->cap, \
|
|
||||||
&(pv)->origin, (pv)->size); \
|
|
||||||
if (p) { \
|
|
||||||
(pv)->data = p; \
|
|
||||||
} \
|
|
||||||
(bool) p; \
|
|
||||||
});
|
|
||||||
|
|
||||||
static inline size_t
|
|
||||||
sc_vecdeque_growsize_(size_t value)
|
|
||||||
{
|
|
||||||
/* integer multiplication by 1.5 */
|
|
||||||
return value + (value >> 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Increase the capacity of the VecDeque to at least `mincap`
|
|
||||||
*
|
|
||||||
* \param pv a pointer to the VecDeque
|
|
||||||
* \param mincap (`size_t`) the requested capacity
|
|
||||||
* \retval true on success
|
|
||||||
* \retval false on allocation failure (the VecDeque is left untouched)
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_reserve(pv, mincap) \
|
|
||||||
({ \
|
|
||||||
assert(mincap <= sc_vecdeque_max_cap_(pv)); \
|
|
||||||
bool ok; \
|
|
||||||
/* avoid to allocate tiny arrays (< SC_VECDEQUE_MINCAP_) */ \
|
|
||||||
size_t mincap_ = MAX(mincap, SC_VECDEQUE_MINCAP_); \
|
|
||||||
if (mincap_ <= (pv)->cap) { \
|
|
||||||
/* nothing to do */ \
|
|
||||||
ok = true; \
|
|
||||||
} else if (mincap_ <= sc_vecdeque_max_cap_(pv)) { \
|
|
||||||
/* not too big */ \
|
|
||||||
size_t newsize = sc_vecdeque_growsize_((pv)->cap); \
|
|
||||||
newsize = CLAMP(newsize, mincap_, sc_vecdeque_max_cap_(pv)); \
|
|
||||||
ok = sc_vecdeque_realloc_(pv, newsize); \
|
|
||||||
} else { \
|
|
||||||
ok = false; \
|
|
||||||
} \
|
|
||||||
ok; \
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Automatically grow the VecDeque capacity
|
|
||||||
*
|
|
||||||
* Private.
|
|
||||||
*
|
|
||||||
* \retval true on success
|
|
||||||
* \retval false on allocation failure (the VecDeque is left untouched)
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_grow_(pv) \
|
|
||||||
({ \
|
|
||||||
bool ok; \
|
|
||||||
if ((pv)->cap < sc_vecdeque_max_cap_(pv)) { \
|
|
||||||
size_t newsize = sc_vecdeque_growsize_((pv)->cap); \
|
|
||||||
newsize = CLAMP(newsize, SC_VECDEQUE_MINCAP_, \
|
|
||||||
sc_vecdeque_max_cap_(pv)); \
|
|
||||||
ok = sc_vecdeque_realloc_(pv, newsize); \
|
|
||||||
} else { \
|
|
||||||
ok = false; \
|
|
||||||
} \
|
|
||||||
ok; \
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Grow the VecDeque capacity if it is full
|
|
||||||
*
|
|
||||||
* Private.
|
|
||||||
*
|
|
||||||
* \retval true on success
|
|
||||||
* \retval false on allocation failure (the VecDeque is left untouched)
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_grow_if_needed_(pv) \
|
|
||||||
(!sc_vecdeque_is_full(pv) || sc_vecdeque_grow_(pv))
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push an uninitialized item, and return a pointer to it
|
|
||||||
*
|
|
||||||
* It does not attempt to resize the VecDeque. It is an error to this function
|
|
||||||
* if the VecDeque is full.
|
|
||||||
*
|
|
||||||
* This function may not fail. It returns a valid non-NULL pointer to the
|
|
||||||
* uninitialized item just pushed.
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_push_hole_noresize(pv) \
|
|
||||||
({ \
|
|
||||||
assert(!sc_vecdeque_is_full(pv)); \
|
|
||||||
++(pv)->size; \
|
|
||||||
&(pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap]; \
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push an uninitialized item, and return a pointer to it
|
|
||||||
*
|
|
||||||
* If the VecDeque is full, it is resized.
|
|
||||||
*
|
|
||||||
* This function returns either a valid non-NULL pointer to the uninitialized
|
|
||||||
* item just pushed, or NULL on reallocation failure.
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_push_hole(pv) \
|
|
||||||
(sc_vecdeque_grow_if_needed_(pv) ? \
|
|
||||||
sc_vecdeque_push_hole_noresize(pv) : NULL)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push an item
|
|
||||||
*
|
|
||||||
* It does not attempt to resize the VecDeque. It is an error to this function
|
|
||||||
* if the VecDeque is full.
|
|
||||||
*
|
|
||||||
* This function may not fail.
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_push_noresize(pv, item) \
|
|
||||||
(void) ({ \
|
|
||||||
assert(!sc_vecdeque_is_full(pv)); \
|
|
||||||
++(pv)->size; \
|
|
||||||
(pv)->data[((pv)->origin + (pv)->size - 1) % (pv)->cap] = item; \
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push an item
|
|
||||||
*
|
|
||||||
* If the VecDeque is full, it is resized.
|
|
||||||
*
|
|
||||||
* \retval true on success
|
|
||||||
* \retval false on allocation failure (the VecDeque is left untouched)
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_push(pv, item) \
|
|
||||||
({ \
|
|
||||||
bool ok = sc_vecdeque_grow_if_needed_(pv); \
|
|
||||||
if (ok) { \
|
|
||||||
sc_vecdeque_push_noresize(pv, item); \
|
|
||||||
} \
|
|
||||||
ok; \
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pop an item and return a pointer to it (still in the VecDeque)
|
|
||||||
*
|
|
||||||
* Returning a pointer allows the caller to destroy it in place without copy
|
|
||||||
* (especially if the item type is big).
|
|
||||||
*
|
|
||||||
* It is an error to call this function if the VecDeque is empty.
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_popref(pv) \
|
|
||||||
({ \
|
|
||||||
assert(!sc_vecdeque_is_empty(pv)); \
|
|
||||||
size_t pos = (pv)->origin; \
|
|
||||||
(pv)->origin = ((pv)->origin + 1) % (pv)->cap; \
|
|
||||||
--(pv)->size; \
|
|
||||||
&(pv)->data[pos]; \
|
|
||||||
})
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Pop an item and return it
|
|
||||||
*
|
|
||||||
* It is an error to call this function if the VecDeque is empty.
|
|
||||||
*/
|
|
||||||
#define sc_vecdeque_pop(pv) \
|
|
||||||
(*sc_vecdeque_popref(pv))
|
|
||||||
|
|
||||||
#endif
|
|
@ -118,7 +118,7 @@ static inline void *
|
|||||||
sc_vector_reallocdata_(void *ptr, size_t count, size_t size,
|
sc_vector_reallocdata_(void *ptr, size_t count, size_t size,
|
||||||
size_t *restrict pcap, size_t *restrict psize)
|
size_t *restrict pcap, size_t *restrict psize)
|
||||||
{
|
{
|
||||||
void *p = reallocarray(ptr, count, size);
|
void *p = realloc(ptr, count * size);
|
||||||
if (!p) {
|
if (!p) {
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
@ -126,7 +126,7 @@ run_v4l2_sink(void *data) {
|
|||||||
vs->has_frame = false;
|
vs->has_frame = false;
|
||||||
sc_mutex_unlock(&vs->mutex);
|
sc_mutex_unlock(&vs->mutex);
|
||||||
|
|
||||||
sc_frame_buffer_consume(&vs->fb, vs->frame);
|
sc_video_buffer_consume(&vs->vb, vs->frame);
|
||||||
|
|
||||||
bool ok = encode_and_write_frame(vs, vs->frame);
|
bool ok = encode_and_write_frame(vs, vs->frame);
|
||||||
av_frame_unref(vs->frame);
|
av_frame_unref(vs->frame);
|
||||||
@ -141,19 +141,42 @@ run_v4l2_sink(void *data) {
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
||||||
|
void *userdata) {
|
||||||
|
(void) vb;
|
||||||
|
struct sc_v4l2_sink *vs = userdata;
|
||||||
|
|
||||||
|
if (!previous_skipped) {
|
||||||
|
sc_mutex_lock(&vs->mutex);
|
||||||
|
vs->has_frame = true;
|
||||||
|
sc_cond_signal(&vs->cond);
|
||||||
|
sc_mutex_unlock(&vs->mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||||
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||||
(void) ctx;
|
(void) ctx;
|
||||||
|
|
||||||
bool ok = sc_frame_buffer_init(&vs->fb);
|
static const struct sc_video_buffer_callbacks cbs = {
|
||||||
|
.on_new_frame = sc_video_buffer_on_new_frame,
|
||||||
|
};
|
||||||
|
|
||||||
|
bool ok = sc_video_buffer_init(&vs->vb, vs->buffering_time, &cbs, vs);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ok = sc_video_buffer_start(&vs->vb);
|
||||||
|
if (!ok) {
|
||||||
|
goto error_video_buffer_destroy;
|
||||||
|
}
|
||||||
|
|
||||||
ok = sc_mutex_init(&vs->mutex);
|
ok = sc_mutex_init(&vs->mutex);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
goto error_frame_buffer_destroy;
|
goto error_video_buffer_stop_and_join;
|
||||||
}
|
}
|
||||||
|
|
||||||
ok = sc_cond_init(&vs->cond);
|
ok = sc_cond_init(&vs->cond);
|
||||||
@ -205,10 +228,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
|||||||
goto error_avformat_free_context;
|
goto error_avformat_free_context;
|
||||||
}
|
}
|
||||||
|
|
||||||
int r = avcodec_parameters_from_context(ostream->codecpar, ctx);
|
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||||
if (r < 0) {
|
ostream->codecpar->codec_id = encoder->id;
|
||||||
goto error_avformat_free_context;
|
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||||
}
|
ostream->codecpar->width = vs->frame_size.width;
|
||||||
|
ostream->codecpar->height = vs->frame_size.height;
|
||||||
|
|
||||||
int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE);
|
int ret = avio_open(&vs->format_ctx->pb, vs->device_name, AVIO_FLAG_WRITE);
|
||||||
if (ret < 0) {
|
if (ret < 0) {
|
||||||
@ -223,8 +247,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
|||||||
goto error_avio_close;
|
goto error_avio_close;
|
||||||
}
|
}
|
||||||
|
|
||||||
vs->encoder_ctx->width = ctx->width;
|
vs->encoder_ctx->width = vs->frame_size.width;
|
||||||
vs->encoder_ctx->height = ctx->height;
|
vs->encoder_ctx->height = vs->frame_size.height;
|
||||||
vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||||
vs->encoder_ctx->time_base.num = 1;
|
vs->encoder_ctx->time_base.num = 1;
|
||||||
vs->encoder_ctx->time_base.den = 1;
|
vs->encoder_ctx->time_base.den = 1;
|
||||||
@ -277,8 +301,11 @@ error_cond_destroy:
|
|||||||
sc_cond_destroy(&vs->cond);
|
sc_cond_destroy(&vs->cond);
|
||||||
error_mutex_destroy:
|
error_mutex_destroy:
|
||||||
sc_mutex_destroy(&vs->mutex);
|
sc_mutex_destroy(&vs->mutex);
|
||||||
error_frame_buffer_destroy:
|
error_video_buffer_stop_and_join:
|
||||||
sc_frame_buffer_destroy(&vs->fb);
|
sc_video_buffer_stop(&vs->vb);
|
||||||
|
sc_video_buffer_join(&vs->vb);
|
||||||
|
error_video_buffer_destroy:
|
||||||
|
sc_video_buffer_destroy(&vs->vb);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -290,7 +317,10 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
|
|||||||
sc_cond_signal(&vs->cond);
|
sc_cond_signal(&vs->cond);
|
||||||
sc_mutex_unlock(&vs->mutex);
|
sc_mutex_unlock(&vs->mutex);
|
||||||
|
|
||||||
|
sc_video_buffer_stop(&vs->vb);
|
||||||
|
|
||||||
sc_thread_join(&vs->thread, NULL);
|
sc_thread_join(&vs->thread, NULL);
|
||||||
|
sc_video_buffer_join(&vs->vb);
|
||||||
|
|
||||||
av_packet_free(&vs->packet);
|
av_packet_free(&vs->packet);
|
||||||
av_frame_free(&vs->frame);
|
av_frame_free(&vs->frame);
|
||||||
@ -300,25 +330,12 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
|
|||||||
avformat_free_context(vs->format_ctx);
|
avformat_free_context(vs->format_ctx);
|
||||||
sc_cond_destroy(&vs->cond);
|
sc_cond_destroy(&vs->cond);
|
||||||
sc_mutex_destroy(&vs->mutex);
|
sc_mutex_destroy(&vs->mutex);
|
||||||
sc_frame_buffer_destroy(&vs->fb);
|
sc_video_buffer_destroy(&vs->vb);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
||||||
bool previous_skipped;
|
return sc_video_buffer_push(&vs->vb, frame);
|
||||||
bool ok = sc_frame_buffer_push(&vs->fb, frame, &previous_skipped);
|
|
||||||
if (!ok) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!previous_skipped) {
|
|
||||||
sc_mutex_lock(&vs->mutex);
|
|
||||||
vs->has_frame = true;
|
|
||||||
sc_cond_signal(&vs->cond);
|
|
||||||
sc_mutex_unlock(&vs->mutex);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@ -340,13 +357,17 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name) {
|
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
||||||
|
struct sc_size frame_size, sc_tick buffering_time) {
|
||||||
vs->device_name = strdup(device_name);
|
vs->device_name = strdup(device_name);
|
||||||
if (!vs->device_name) {
|
if (!vs->device_name) {
|
||||||
LOGE("Could not strdup v4l2 device name");
|
LOGE("Could not strdup v4l2 device name");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
vs->frame_size = frame_size;
|
||||||
|
vs->buffering_time = buffering_time;
|
||||||
|
|
||||||
static const struct sc_frame_sink_ops ops = {
|
static const struct sc_frame_sink_ops ops = {
|
||||||
.open = sc_v4l2_frame_sink_open,
|
.open = sc_v4l2_frame_sink_open,
|
||||||
.close = sc_v4l2_frame_sink_close,
|
.close = sc_v4l2_frame_sink_close,
|
||||||
|
@ -8,17 +8,19 @@
|
|||||||
|
|
||||||
#include "coords.h"
|
#include "coords.h"
|
||||||
#include "trait/frame_sink.h"
|
#include "trait/frame_sink.h"
|
||||||
#include "frame_buffer.h"
|
#include "video_buffer.h"
|
||||||
#include "util/tick.h"
|
#include "util/tick.h"
|
||||||
|
|
||||||
struct sc_v4l2_sink {
|
struct sc_v4l2_sink {
|
||||||
struct sc_frame_sink frame_sink; // frame sink trait
|
struct sc_frame_sink frame_sink; // frame sink trait
|
||||||
|
|
||||||
struct sc_frame_buffer fb;
|
struct sc_video_buffer vb;
|
||||||
AVFormatContext *format_ctx;
|
AVFormatContext *format_ctx;
|
||||||
AVCodecContext *encoder_ctx;
|
AVCodecContext *encoder_ctx;
|
||||||
|
|
||||||
char *device_name;
|
char *device_name;
|
||||||
|
struct sc_size frame_size;
|
||||||
|
sc_tick buffering_time;
|
||||||
|
|
||||||
sc_thread thread;
|
sc_thread thread;
|
||||||
sc_mutex mutex;
|
sc_mutex mutex;
|
||||||
@ -32,7 +34,8 @@ struct sc_v4l2_sink {
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool
|
bool
|
||||||
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name);
|
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
||||||
|
struct sc_size frame_size, sc_tick buffering_time);
|
||||||
|
|
||||||
void
|
void
|
||||||
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
|
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
|
||||||
|
254
app/src/video_buffer.c
Normal file
254
app/src/video_buffer.c
Normal file
@ -0,0 +1,254 @@
|
|||||||
|
#include "video_buffer.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
#include <libavutil/avutil.h>
|
||||||
|
#include <libavformat/avformat.h>
|
||||||
|
|
||||||
|
#include "util/log.h"
|
||||||
|
|
||||||
|
#define SC_BUFFERING_NDEBUG // comment to debug
|
||||||
|
|
||||||
|
static struct sc_video_buffer_frame *
|
||||||
|
sc_video_buffer_frame_new(const AVFrame *frame) {
|
||||||
|
struct sc_video_buffer_frame *vb_frame = malloc(sizeof(*vb_frame));
|
||||||
|
if (!vb_frame) {
|
||||||
|
LOG_OOM();
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
vb_frame->frame = av_frame_alloc();
|
||||||
|
if (!vb_frame->frame) {
|
||||||
|
LOG_OOM();
|
||||||
|
free(vb_frame);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (av_frame_ref(vb_frame->frame, frame)) {
|
||||||
|
av_frame_free(&vb_frame->frame);
|
||||||
|
free(vb_frame);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return vb_frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
sc_video_buffer_frame_delete(struct sc_video_buffer_frame *vb_frame) {
|
||||||
|
av_frame_unref(vb_frame->frame);
|
||||||
|
av_frame_free(&vb_frame->frame);
|
||||||
|
free(vb_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
sc_video_buffer_offer(struct sc_video_buffer *vb, const AVFrame *frame) {
|
||||||
|
bool previous_skipped;
|
||||||
|
bool ok = sc_frame_buffer_push(&vb->fb, frame, &previous_skipped);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
vb->cbs->on_new_frame(vb, previous_skipped, vb->cbs_userdata);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
run_buffering(void *data) {
|
||||||
|
struct sc_video_buffer *vb = data;
|
||||||
|
|
||||||
|
assert(vb->buffering_time > 0);
|
||||||
|
|
||||||
|
for (;;) {
|
||||||
|
sc_mutex_lock(&vb->b.mutex);
|
||||||
|
|
||||||
|
while (!vb->b.stopped && sc_queue_is_empty(&vb->b.queue)) {
|
||||||
|
sc_cond_wait(&vb->b.queue_cond, &vb->b.mutex);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vb->b.stopped) {
|
||||||
|
sc_mutex_unlock(&vb->b.mutex);
|
||||||
|
goto stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sc_video_buffer_frame *vb_frame;
|
||||||
|
sc_queue_take(&vb->b.queue, next, &vb_frame);
|
||||||
|
|
||||||
|
sc_tick max_deadline = sc_tick_now() + vb->buffering_time;
|
||||||
|
// PTS (written by the server) are expressed in microseconds
|
||||||
|
sc_tick pts = SC_TICK_TO_US(vb_frame->frame->pts);
|
||||||
|
|
||||||
|
bool timed_out = false;
|
||||||
|
while (!vb->b.stopped && !timed_out) {
|
||||||
|
sc_tick deadline = sc_clock_to_system_time(&vb->b.clock, pts)
|
||||||
|
+ vb->buffering_time;
|
||||||
|
if (deadline > max_deadline) {
|
||||||
|
deadline = max_deadline;
|
||||||
|
}
|
||||||
|
|
||||||
|
timed_out =
|
||||||
|
!sc_cond_timedwait(&vb->b.wait_cond, &vb->b.mutex, deadline);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vb->b.stopped) {
|
||||||
|
sc_video_buffer_frame_delete(vb_frame);
|
||||||
|
sc_mutex_unlock(&vb->b.mutex);
|
||||||
|
goto stopped;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_mutex_unlock(&vb->b.mutex);
|
||||||
|
|
||||||
|
#ifndef SC_BUFFERING_NDEBUG
|
||||||
|
LOGD("Buffering: %" PRItick ";%" PRItick ";%" PRItick,
|
||||||
|
pts, vb_frame->push_date, sc_tick_now());
|
||||||
|
#endif
|
||||||
|
|
||||||
|
sc_video_buffer_offer(vb, vb_frame->frame);
|
||||||
|
|
||||||
|
sc_video_buffer_frame_delete(vb_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopped:
|
||||||
|
// Flush queue
|
||||||
|
while (!sc_queue_is_empty(&vb->b.queue)) {
|
||||||
|
struct sc_video_buffer_frame *vb_frame;
|
||||||
|
sc_queue_take(&vb->b.queue, next, &vb_frame);
|
||||||
|
sc_video_buffer_frame_delete(vb_frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGD("Buffering thread ended");
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time,
|
||||||
|
const struct sc_video_buffer_callbacks *cbs,
|
||||||
|
void *cbs_userdata) {
|
||||||
|
bool ok = sc_frame_buffer_init(&vb->fb);
|
||||||
|
if (!ok) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(buffering_time >= 0);
|
||||||
|
if (buffering_time) {
|
||||||
|
ok = sc_mutex_init(&vb->b.mutex);
|
||||||
|
if (!ok) {
|
||||||
|
sc_frame_buffer_destroy(&vb->fb);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_cond_init(&vb->b.queue_cond);
|
||||||
|
if (!ok) {
|
||||||
|
sc_mutex_destroy(&vb->b.mutex);
|
||||||
|
sc_frame_buffer_destroy(&vb->fb);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ok = sc_cond_init(&vb->b.wait_cond);
|
||||||
|
if (!ok) {
|
||||||
|
sc_cond_destroy(&vb->b.queue_cond);
|
||||||
|
sc_mutex_destroy(&vb->b.mutex);
|
||||||
|
sc_frame_buffer_destroy(&vb->fb);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_clock_init(&vb->b.clock);
|
||||||
|
sc_queue_init(&vb->b.queue);
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(cbs);
|
||||||
|
assert(cbs->on_new_frame);
|
||||||
|
|
||||||
|
vb->buffering_time = buffering_time;
|
||||||
|
vb->cbs = cbs;
|
||||||
|
vb->cbs_userdata = cbs_userdata;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_video_buffer_start(struct sc_video_buffer *vb) {
|
||||||
|
if (vb->buffering_time) {
|
||||||
|
bool ok =
|
||||||
|
sc_thread_create(&vb->b.thread, run_buffering, "scrcpy-vbuf", vb);
|
||||||
|
if (!ok) {
|
||||||
|
LOGE("Could not start buffering thread");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_video_buffer_stop(struct sc_video_buffer *vb) {
|
||||||
|
if (vb->buffering_time) {
|
||||||
|
sc_mutex_lock(&vb->b.mutex);
|
||||||
|
vb->b.stopped = true;
|
||||||
|
sc_cond_signal(&vb->b.queue_cond);
|
||||||
|
sc_cond_signal(&vb->b.wait_cond);
|
||||||
|
sc_mutex_unlock(&vb->b.mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_video_buffer_join(struct sc_video_buffer *vb) {
|
||||||
|
if (vb->buffering_time) {
|
||||||
|
sc_thread_join(&vb->b.thread, NULL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_video_buffer_destroy(struct sc_video_buffer *vb) {
|
||||||
|
sc_frame_buffer_destroy(&vb->fb);
|
||||||
|
if (vb->buffering_time) {
|
||||||
|
sc_cond_destroy(&vb->b.wait_cond);
|
||||||
|
sc_cond_destroy(&vb->b.queue_cond);
|
||||||
|
sc_mutex_destroy(&vb->b.mutex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame) {
|
||||||
|
if (!vb->buffering_time) {
|
||||||
|
// No buffering
|
||||||
|
return sc_video_buffer_offer(vb, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
sc_mutex_lock(&vb->b.mutex);
|
||||||
|
|
||||||
|
sc_tick pts = SC_TICK_FROM_US(frame->pts);
|
||||||
|
sc_clock_update(&vb->b.clock, sc_tick_now(), pts);
|
||||||
|
sc_cond_signal(&vb->b.wait_cond);
|
||||||
|
|
||||||
|
if (vb->b.clock.count == 1) {
|
||||||
|
sc_mutex_unlock(&vb->b.mutex);
|
||||||
|
// First frame, offer it immediately, for two reasons:
|
||||||
|
// - not to delay the opening of the scrcpy window
|
||||||
|
// - the buffering estimation needs at least two clock points, so it
|
||||||
|
// could not handle the first frame
|
||||||
|
return sc_video_buffer_offer(vb, frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct sc_video_buffer_frame *vb_frame = sc_video_buffer_frame_new(frame);
|
||||||
|
if (!vb_frame) {
|
||||||
|
sc_mutex_unlock(&vb->b.mutex);
|
||||||
|
LOG_OOM();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifndef SC_BUFFERING_NDEBUG
|
||||||
|
vb_frame->push_date = sc_tick_now();
|
||||||
|
#endif
|
||||||
|
sc_queue_push(&vb->b.queue, next, vb_frame);
|
||||||
|
sc_cond_signal(&vb->b.queue_cond);
|
||||||
|
|
||||||
|
sc_mutex_unlock(&vb->b.mutex);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst) {
|
||||||
|
sc_frame_buffer_consume(&vb->fb, dst);
|
||||||
|
}
|
76
app/src/video_buffer.h
Normal file
76
app/src/video_buffer.h
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
#ifndef SC_VIDEO_BUFFER_H
|
||||||
|
#define SC_VIDEO_BUFFER_H
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
|
||||||
|
#include "clock.h"
|
||||||
|
#include "frame_buffer.h"
|
||||||
|
#include "util/queue.h"
|
||||||
|
#include "util/thread.h"
|
||||||
|
#include "util/tick.h"
|
||||||
|
|
||||||
|
// forward declarations
|
||||||
|
typedef struct AVFrame AVFrame;
|
||||||
|
|
||||||
|
struct sc_video_buffer_frame {
|
||||||
|
AVFrame *frame;
|
||||||
|
struct sc_video_buffer_frame *next;
|
||||||
|
#ifndef NDEBUG
|
||||||
|
sc_tick push_date;
|
||||||
|
#endif
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sc_video_buffer_frame_queue SC_QUEUE(struct sc_video_buffer_frame);
|
||||||
|
|
||||||
|
struct sc_video_buffer {
|
||||||
|
struct sc_frame_buffer fb;
|
||||||
|
|
||||||
|
sc_tick buffering_time;
|
||||||
|
|
||||||
|
// only if buffering_time > 0
|
||||||
|
struct {
|
||||||
|
sc_thread thread;
|
||||||
|
sc_mutex mutex;
|
||||||
|
sc_cond queue_cond;
|
||||||
|
sc_cond wait_cond;
|
||||||
|
|
||||||
|
struct sc_clock clock;
|
||||||
|
struct sc_video_buffer_frame_queue queue;
|
||||||
|
bool stopped;
|
||||||
|
} b; // buffering
|
||||||
|
|
||||||
|
const struct sc_video_buffer_callbacks *cbs;
|
||||||
|
void *cbs_userdata;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct sc_video_buffer_callbacks {
|
||||||
|
void (*on_new_frame)(struct sc_video_buffer *vb, bool previous_skipped,
|
||||||
|
void *userdata);
|
||||||
|
};
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_video_buffer_init(struct sc_video_buffer *vb, sc_tick buffering_time,
|
||||||
|
const struct sc_video_buffer_callbacks *cbs,
|
||||||
|
void *cbs_userdata);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_video_buffer_start(struct sc_video_buffer *vb);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_video_buffer_stop(struct sc_video_buffer *vb);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_video_buffer_join(struct sc_video_buffer *vb);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_video_buffer_destroy(struct sc_video_buffer *vb);
|
||||||
|
|
||||||
|
bool
|
||||||
|
sc_video_buffer_push(struct sc_video_buffer *vb, const AVFrame *frame);
|
||||||
|
|
||||||
|
void
|
||||||
|
sc_video_buffer_consume(struct sc_video_buffer *vb, AVFrame *dst);
|
||||||
|
|
||||||
|
#endif
|
@ -13,23 +13,23 @@ void test_bytebuf_simple(void) {
|
|||||||
assert(ok);
|
assert(ok);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 5);
|
assert(sc_bytebuf_read_remaining(&buf) == 5);
|
||||||
|
|
||||||
sc_bytebuf_read(&buf, data, 4);
|
sc_bytebuf_read(&buf, data, 4);
|
||||||
assert(!strncmp((char *) data, "hell", 4));
|
assert(!strncmp((char *) data, "hell", 4));
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1);
|
sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 7);
|
assert(sc_bytebuf_read_remaining(&buf) == 7);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 8);
|
assert(sc_bytebuf_read_remaining(&buf) == 8);
|
||||||
|
|
||||||
sc_bytebuf_read(&buf, &data[4], 8);
|
sc_bytebuf_read(&buf, &data[4], 8);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
assert(sc_bytebuf_read_remaining(&buf) == 0);
|
||||||
|
|
||||||
data[12] = '\0';
|
data[12] = '\0';
|
||||||
assert(!strcmp((char *) data, "hello world!"));
|
assert(!strcmp((char *) data, "hello world!"));
|
||||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
assert(sc_bytebuf_read_remaining(&buf) == 0);
|
||||||
|
|
||||||
sc_bytebuf_destroy(&buf);
|
sc_bytebuf_destroy(&buf);
|
||||||
}
|
}
|
||||||
@ -42,31 +42,58 @@ void test_bytebuf_boundaries(void) {
|
|||||||
assert(ok);
|
assert(ok);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 6);
|
assert(sc_bytebuf_read_remaining(&buf) == 6);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
assert(sc_bytebuf_read_remaining(&buf) == 12);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 18);
|
assert(sc_bytebuf_read_remaining(&buf) == 18);
|
||||||
|
|
||||||
sc_bytebuf_read(&buf, data, 9);
|
sc_bytebuf_read(&buf, data, 9);
|
||||||
assert(!strncmp((char *) data, "hello hel", 9));
|
assert(!strncmp((char *) data, "hello hel", 9));
|
||||||
assert(sc_bytebuf_can_read(&buf) == 9);
|
assert(sc_bytebuf_read_remaining(&buf) == 9);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 14);
|
assert(sc_bytebuf_read_remaining(&buf) == 14);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 15);
|
assert(sc_bytebuf_read_remaining(&buf) == 15);
|
||||||
|
|
||||||
sc_bytebuf_skip(&buf, 3);
|
sc_bytebuf_skip(&buf, 3);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
assert(sc_bytebuf_read_remaining(&buf) == 12);
|
||||||
|
|
||||||
sc_bytebuf_read(&buf, data, 12);
|
sc_bytebuf_read(&buf, data, 12);
|
||||||
data[12] = '\0';
|
data[12] = '\0';
|
||||||
assert(!strcmp((char *) data, "hello world!"));
|
assert(!strcmp((char *) data, "hello world!"));
|
||||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
assert(sc_bytebuf_read_remaining(&buf) == 0);
|
||||||
|
|
||||||
|
sc_bytebuf_destroy(&buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
void test_bytebuf_overwrite(void) {
|
||||||
|
struct sc_bytebuf buf;
|
||||||
|
uint8_t data[10];
|
||||||
|
|
||||||
|
bool ok = sc_bytebuf_init(&buf, 10); // so actual capacity is 9
|
||||||
|
assert(ok);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
|
assert(sc_bytebuf_read_remaining(&buf) == 6);
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "abcdef", sizeof("abcdef") - 1);
|
||||||
|
assert(sc_bytebuf_read_remaining(&buf) == 9);
|
||||||
|
|
||||||
|
sc_bytebuf_read(&buf, data, 9);
|
||||||
|
assert(!strncmp((char *) data, "lo abcdef", 9));
|
||||||
|
|
||||||
|
sc_bytebuf_write(&buf, (uint8_t *) "a very big buffer",
|
||||||
|
sizeof("a very big buffer") - 1);
|
||||||
|
assert(sc_bytebuf_read_remaining(&buf) == 9);
|
||||||
|
|
||||||
|
sc_bytebuf_read(&buf, data, 9);
|
||||||
|
assert(!strncmp((char *) data, "ig buffer", 9));
|
||||||
|
assert(sc_bytebuf_read_remaining(&buf) == 0);
|
||||||
|
|
||||||
sc_bytebuf_destroy(&buf);
|
sc_bytebuf_destroy(&buf);
|
||||||
}
|
}
|
||||||
@ -79,37 +106,37 @@ void test_bytebuf_two_steps_write(void) {
|
|||||||
assert(ok);
|
assert(ok);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 6);
|
assert(sc_bytebuf_read_remaining(&buf) == 6);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
assert(sc_bytebuf_read_remaining(&buf) == 12);
|
||||||
|
|
||||||
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet
|
assert(sc_bytebuf_read_remaining(&buf) == 12); // write not committed yet
|
||||||
|
|
||||||
sc_bytebuf_read(&buf, data, 9);
|
sc_bytebuf_read(&buf, data, 9);
|
||||||
assert(!strncmp((char *) data, "hello hel", 3));
|
assert(!strncmp((char *) data, "hello hel", 3));
|
||||||
assert(sc_bytebuf_can_read(&buf) == 3);
|
assert(sc_bytebuf_read_remaining(&buf) == 3);
|
||||||
|
|
||||||
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
|
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 9);
|
assert(sc_bytebuf_read_remaining(&buf) == 9);
|
||||||
|
|
||||||
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet
|
assert(sc_bytebuf_read_remaining(&buf) == 9); // write not committed yet
|
||||||
|
|
||||||
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
|
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 14);
|
assert(sc_bytebuf_read_remaining(&buf) == 14);
|
||||||
|
|
||||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 15);
|
assert(sc_bytebuf_read_remaining(&buf) == 15);
|
||||||
|
|
||||||
sc_bytebuf_skip(&buf, 3);
|
sc_bytebuf_skip(&buf, 3);
|
||||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
assert(sc_bytebuf_read_remaining(&buf) == 12);
|
||||||
|
|
||||||
sc_bytebuf_read(&buf, data, 12);
|
sc_bytebuf_read(&buf, data, 12);
|
||||||
data[12] = '\0';
|
data[12] = '\0';
|
||||||
assert(!strcmp((char *) data, "hello world!"));
|
assert(!strcmp((char *) data, "hello world!"));
|
||||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
assert(sc_bytebuf_read_remaining(&buf) == 0);
|
||||||
|
|
||||||
sc_bytebuf_destroy(&buf);
|
sc_bytebuf_destroy(&buf);
|
||||||
}
|
}
|
||||||
@ -120,6 +147,7 @@ int main(int argc, char *argv[]) {
|
|||||||
|
|
||||||
test_bytebuf_simple();
|
test_bytebuf_simple();
|
||||||
test_bytebuf_boundaries();
|
test_bytebuf_boundaries();
|
||||||
|
test_bytebuf_overwrite();
|
||||||
test_bytebuf_two_steps_write();
|
test_bytebuf_two_steps_write();
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
|
78
app/tests/test_cbuf.c
Normal file
78
app/tests/test_cbuf.c
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "util/cbuf.h"
|
||||||
|
|
||||||
|
struct int_queue CBUF(int, 32);
|
||||||
|
|
||||||
|
static void test_cbuf_empty(void) {
|
||||||
|
struct int_queue queue;
|
||||||
|
cbuf_init(&queue);
|
||||||
|
|
||||||
|
assert(cbuf_is_empty(&queue));
|
||||||
|
|
||||||
|
bool push_ok = cbuf_push(&queue, 42);
|
||||||
|
assert(push_ok);
|
||||||
|
assert(!cbuf_is_empty(&queue));
|
||||||
|
|
||||||
|
int item;
|
||||||
|
bool take_ok = cbuf_take(&queue, &item);
|
||||||
|
assert(take_ok);
|
||||||
|
assert(cbuf_is_empty(&queue));
|
||||||
|
|
||||||
|
bool take_empty_ok = cbuf_take(&queue, &item);
|
||||||
|
assert(!take_empty_ok); // the queue is empty
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_cbuf_full(void) {
|
||||||
|
struct int_queue queue;
|
||||||
|
cbuf_init(&queue);
|
||||||
|
|
||||||
|
assert(!cbuf_is_full(&queue));
|
||||||
|
|
||||||
|
// fill the queue
|
||||||
|
for (int i = 0; i < 32; ++i) {
|
||||||
|
bool ok = cbuf_push(&queue, i);
|
||||||
|
assert(ok);
|
||||||
|
}
|
||||||
|
bool ok = cbuf_push(&queue, 42);
|
||||||
|
assert(!ok); // the queue if full
|
||||||
|
|
||||||
|
int item;
|
||||||
|
bool take_ok = cbuf_take(&queue, &item);
|
||||||
|
assert(take_ok);
|
||||||
|
assert(!cbuf_is_full(&queue));
|
||||||
|
}
|
||||||
|
|
||||||
|
static void test_cbuf_push_take(void) {
|
||||||
|
struct int_queue queue;
|
||||||
|
cbuf_init(&queue);
|
||||||
|
|
||||||
|
bool push1_ok = cbuf_push(&queue, 42);
|
||||||
|
assert(push1_ok);
|
||||||
|
|
||||||
|
bool push2_ok = cbuf_push(&queue, 35);
|
||||||
|
assert(push2_ok);
|
||||||
|
|
||||||
|
int item;
|
||||||
|
|
||||||
|
bool take1_ok = cbuf_take(&queue, &item);
|
||||||
|
assert(take1_ok);
|
||||||
|
assert(item == 42);
|
||||||
|
|
||||||
|
bool take2_ok = cbuf_take(&queue, &item);
|
||||||
|
assert(take2_ok);
|
||||||
|
assert(item == 35);
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
(void) argc;
|
||||||
|
(void) argv;
|
||||||
|
|
||||||
|
test_cbuf_empty();
|
||||||
|
test_cbuf_full();
|
||||||
|
test_cbuf_push_take();
|
||||||
|
return 0;
|
||||||
|
}
|
43
app/tests/test_queue.c
Normal file
43
app/tests/test_queue.c
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
#include "common.h"
|
||||||
|
|
||||||
|
#include <assert.h>
|
||||||
|
|
||||||
|
#include "util/queue.h"
|
||||||
|
|
||||||
|
struct foo {
|
||||||
|
int value;
|
||||||
|
struct foo *next;
|
||||||
|
};
|
||||||
|
|
||||||
|
static void test_queue(void) {
|
||||||
|
struct my_queue SC_QUEUE(struct foo) queue;
|
||||||
|
sc_queue_init(&queue);
|
||||||
|
|
||||||
|
assert(sc_queue_is_empty(&queue));
|
||||||
|
|
||||||
|
struct foo v1 = { .value = 42 };
|
||||||
|
struct foo v2 = { .value = 27 };
|
||||||
|
|
||||||
|
sc_queue_push(&queue, next, &v1);
|
||||||
|
sc_queue_push(&queue, next, &v2);
|
||||||
|
|
||||||
|
struct foo *foo;
|
||||||
|
|
||||||
|
assert(!sc_queue_is_empty(&queue));
|
||||||
|
sc_queue_take(&queue, next, &foo);
|
||||||
|
assert(foo->value == 42);
|
||||||
|
|
||||||
|
assert(!sc_queue_is_empty(&queue));
|
||||||
|
sc_queue_take(&queue, next, &foo);
|
||||||
|
assert(foo->value == 27);
|
||||||
|
|
||||||
|
assert(sc_queue_is_empty(&queue));
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
(void) argc;
|
||||||
|
(void) argv;
|
||||||
|
|
||||||
|
test_queue();
|
||||||
|
return 0;
|
||||||
|
}
|
@ -1,197 +0,0 @@
|
|||||||
#include "common.h"
|
|
||||||
|
|
||||||
#include <assert.h>
|
|
||||||
|
|
||||||
#include "util/vecdeque.h"
|
|
||||||
|
|
||||||
#define pr(pv) \
|
|
||||||
({ \
|
|
||||||
fprintf(stderr, "cap=%lu origin=%lu size=%lu\n", (pv)->cap, (pv)->origin, (pv)->size); \
|
|
||||||
for (size_t i = 0; i < (pv)->cap; ++i) \
|
|
||||||
fprintf(stderr, "%d ", (pv)->data[i]); \
|
|
||||||
fprintf(stderr, "\n"); \
|
|
||||||
})
|
|
||||||
|
|
||||||
static void test_vecdeque_push_pop(void) {
|
|
||||||
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
|
|
||||||
|
|
||||||
assert(sc_vecdeque_is_empty(&vdq));
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 0);
|
|
||||||
|
|
||||||
bool ok = sc_vecdeque_push(&vdq, 5);
|
|
||||||
assert(ok);
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 1);
|
|
||||||
|
|
||||||
ok = sc_vecdeque_push(&vdq, 12);
|
|
||||||
assert(ok);
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 2);
|
|
||||||
|
|
||||||
int v = sc_vecdeque_pop(&vdq);
|
|
||||||
assert(v == 5);
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 1);
|
|
||||||
|
|
||||||
ok = sc_vecdeque_push(&vdq, 7);
|
|
||||||
assert(ok);
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 2);
|
|
||||||
|
|
||||||
int *p = sc_vecdeque_popref(&vdq);
|
|
||||||
assert(p);
|
|
||||||
assert(*p == 12);
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 1);
|
|
||||||
|
|
||||||
v = sc_vecdeque_pop(&vdq);
|
|
||||||
assert(v == 7);
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 0);
|
|
||||||
assert(sc_vecdeque_is_empty(&vdq));
|
|
||||||
|
|
||||||
sc_vecdeque_destroy(&vdq);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_vecdeque_reserve(void) {
|
|
||||||
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
|
|
||||||
|
|
||||||
bool ok = sc_vecdeque_reserve(&vdq, 20);
|
|
||||||
assert(ok);
|
|
||||||
assert(vdq.cap == 20);
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 0);
|
|
||||||
|
|
||||||
for (size_t i = 0; i < 20; ++i) {
|
|
||||||
ok = sc_vecdeque_push(&vdq, i);
|
|
||||||
assert(ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 20);
|
|
||||||
|
|
||||||
// It is now full
|
|
||||||
|
|
||||||
for (int i = 0; i < 5; ++i) {
|
|
||||||
int v = sc_vecdeque_pop(&vdq);
|
|
||||||
assert(v == i);
|
|
||||||
}
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 15);
|
|
||||||
|
|
||||||
for (int i = 20; i < 25; ++i) {
|
|
||||||
ok = sc_vecdeque_push(&vdq, i);
|
|
||||||
assert(ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 20);
|
|
||||||
assert(vdq.cap == 20);
|
|
||||||
|
|
||||||
// Now, the content wraps around the ring buffer:
|
|
||||||
// 20 21 22 23 24 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
|
|
||||||
// ^
|
|
||||||
// origin
|
|
||||||
|
|
||||||
// It is now full, let's reserve some space
|
|
||||||
ok = sc_vecdeque_reserve(&vdq, 30);
|
|
||||||
assert(ok);
|
|
||||||
assert(vdq.cap == 30);
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 20);
|
|
||||||
|
|
||||||
for (int i = 0; i < 20; ++i) {
|
|
||||||
// We should retrieve the items we inserted in order
|
|
||||||
int v = sc_vecdeque_pop(&vdq);
|
|
||||||
assert(v == i + 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 0);
|
|
||||||
|
|
||||||
sc_vecdeque_destroy(&vdq);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_vecdeque_grow() {
|
|
||||||
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
|
|
||||||
|
|
||||||
bool ok = sc_vecdeque_reserve(&vdq, 20);
|
|
||||||
assert(ok);
|
|
||||||
assert(vdq.cap == 20);
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 0);
|
|
||||||
|
|
||||||
for (int i = 0; i < 500; ++i) {
|
|
||||||
ok = sc_vecdeque_push(&vdq, i);
|
|
||||||
assert(ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 500);
|
|
||||||
|
|
||||||
for (int i = 0; i < 100; ++i) {
|
|
||||||
int v = sc_vecdeque_pop(&vdq);
|
|
||||||
assert(v == i);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 400);
|
|
||||||
|
|
||||||
for (int i = 500; i < 1000; ++i) {
|
|
||||||
ok = sc_vecdeque_push(&vdq, i);
|
|
||||||
assert(ok);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 900);
|
|
||||||
|
|
||||||
for (int i = 100; i < 1000; ++i) {
|
|
||||||
int v = sc_vecdeque_pop(&vdq);
|
|
||||||
assert(v == i);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 0);
|
|
||||||
|
|
||||||
sc_vecdeque_destroy(&vdq);
|
|
||||||
}
|
|
||||||
|
|
||||||
static void test_vecdeque_push_hole() {
|
|
||||||
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
|
|
||||||
|
|
||||||
bool ok = sc_vecdeque_reserve(&vdq, 20);
|
|
||||||
assert(ok);
|
|
||||||
assert(vdq.cap == 20);
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 0);
|
|
||||||
|
|
||||||
for (int i = 0; i < 20; ++i) {
|
|
||||||
int *p = sc_vecdeque_push_hole(&vdq);
|
|
||||||
assert(p);
|
|
||||||
*p = i * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 20);
|
|
||||||
|
|
||||||
for (int i = 0; i < 10; ++i) {
|
|
||||||
int v = sc_vecdeque_pop(&vdq);
|
|
||||||
assert(v == i * 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 10);
|
|
||||||
|
|
||||||
for (int i = 20; i < 30; ++i) {
|
|
||||||
int *p = sc_vecdeque_push_hole(&vdq);
|
|
||||||
assert(p);
|
|
||||||
*p = i * 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 20);
|
|
||||||
|
|
||||||
for (int i = 10; i < 30; ++i) {
|
|
||||||
int v = sc_vecdeque_pop(&vdq);
|
|
||||||
assert(v == i * 10);
|
|
||||||
}
|
|
||||||
|
|
||||||
assert(sc_vecdeque_size(&vdq) == 0);
|
|
||||||
|
|
||||||
sc_vecdeque_destroy(&vdq);
|
|
||||||
}
|
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
|
||||||
(void) argc;
|
|
||||||
(void) argv;
|
|
||||||
|
|
||||||
test_vecdeque_push_pop();
|
|
||||||
test_vecdeque_reserve();
|
|
||||||
test_vecdeque_grow();
|
|
||||||
test_vecdeque_push_hole();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
@ -16,6 +16,6 @@ cpu = 'i686'
|
|||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
[properties]
|
[properties]
|
||||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32'
|
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win32'
|
||||||
prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
|
||||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
||||||
|
@ -16,6 +16,6 @@ cpu = 'x86_64'
|
|||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
[properties]
|
[properties]
|
||||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64'
|
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win64'
|
||||||
prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
|
||||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
|
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
|
||||||
|
90
doc/audio.md
90
doc/audio.md
@ -1,90 +0,0 @@
|
|||||||
# Audio
|
|
||||||
|
|
||||||
Audio forwarding is supported for devices with Android 11 or higher, and it is
|
|
||||||
enabled by default:
|
|
||||||
|
|
||||||
- For **Android 12 or newer**, it works out-of-the-box.
|
|
||||||
- For **Android 11**, you'll need to ensure that the device screen is unlocked
|
|
||||||
when starting scrcpy. A fake popup will briefly appear to make the system
|
|
||||||
think that the shell app is in the foreground. Without this, audio capture
|
|
||||||
will fail.
|
|
||||||
- For **Android 10 or earlier**, audio cannot be captured and is automatically
|
|
||||||
disabled.
|
|
||||||
|
|
||||||
If audio capture fails, then mirroring continues with video only (since audio is
|
|
||||||
enabled by default, it is not acceptable to make scrcpy fail if it is not
|
|
||||||
available), unless `--require-audio` is set.
|
|
||||||
|
|
||||||
|
|
||||||
## No audio
|
|
||||||
|
|
||||||
To disable audio:
|
|
||||||
|
|
||||||
```
|
|
||||||
scrcpy --no-audio
|
|
||||||
```
|
|
||||||
|
|
||||||
## Codec
|
|
||||||
|
|
||||||
The audio codec can be selected. The possible values are `opus` (default), `aac`
|
|
||||||
and `raw` (uncompressed PCM 16-bit LE):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --audio-codec=opus # default
|
|
||||||
scrcpy --audio-codec=aac
|
|
||||||
scrcpy --audio-codec=raw
|
|
||||||
```
|
|
||||||
|
|
||||||
Several encoders may be available on the device. They can be listed by:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --list-encoders
|
|
||||||
```
|
|
||||||
|
|
||||||
To select a specific encoder:
|
|
||||||
|
|
||||||
```
|
|
||||||
scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder'
|
|
||||||
```
|
|
||||||
|
|
||||||
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
|
|
||||||
check `--audio-codec-options` in the manpage or in `scrcpy --help`.
|
|
||||||
|
|
||||||
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
|
|
||||||
|
|
||||||
|
|
||||||
## Bit rate
|
|
||||||
|
|
||||||
The default video bit-rate is 128Kbps. To change it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --audio-bit-rate=64K
|
|
||||||
scrcpy --audio-bit-rate=64000 # equivalent
|
|
||||||
```
|
|
||||||
|
|
||||||
_This parameter does not apply to RAW audio codec (`--audio-codec=raw`)._
|
|
||||||
|
|
||||||
|
|
||||||
## Buffering
|
|
||||||
|
|
||||||
Audio buffering is unavoidable. It must be kept small enough so that the latency
|
|
||||||
is acceptable, but large enough to minimize buffer underrun (causing audio
|
|
||||||
glitches).
|
|
||||||
|
|
||||||
The default buffer size is set to 50ms. It can be adjusted:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --audio-buffer=40 # smaller than default
|
|
||||||
scrcpy --audio-buffer=100 # higher than default
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that this option changes the _target_ buffering. It is possible that this
|
|
||||||
target buffering might not be reached (on frequent buffer underflow typically).
|
|
||||||
|
|
||||||
If you don't interact with the device (to watch a video for example), a higher
|
|
||||||
latency (for both [video](video.md#buffering) and audio) might be preferable to
|
|
||||||
avoid glitches and smooth the playback:
|
|
||||||
|
|
||||||
```
|
|
||||||
scrcpy --display-buffer=200 --audio-buffer=200
|
|
||||||
```
|
|
149
doc/control.md
149
doc/control.md
@ -1,149 +0,0 @@
|
|||||||
# Control
|
|
||||||
|
|
||||||
## Read-only
|
|
||||||
|
|
||||||
To disable controls (everything which can interact with the device: input keys,
|
|
||||||
mouse events, drag&drop files):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-control
|
|
||||||
scrcpy -n # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Text injection preference
|
|
||||||
|
|
||||||
Two kinds of [events][textevents] are generated when typing text:
|
|
||||||
- _key events_, signaling that a key is pressed or released;
|
|
||||||
- _text events_, signaling that a text has been entered.
|
|
||||||
|
|
||||||
By default, letters are injected using key events, so that the keyboard behaves
|
|
||||||
as expected in games (typically for WASD keys).
|
|
||||||
|
|
||||||
But this may [cause issues][prefertext]. If you encounter such a problem, you
|
|
||||||
can avoid it by:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --prefer-text
|
|
||||||
```
|
|
||||||
|
|
||||||
(but this will break keyboard behavior in games)
|
|
||||||
|
|
||||||
On the contrary, you could force to always inject raw key events:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --raw-key-events
|
|
||||||
```
|
|
||||||
|
|
||||||
These options have no effect on HID keyboard (all key events are sent as
|
|
||||||
scancodes in this mode).
|
|
||||||
|
|
||||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
|
||||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
|
||||||
|
|
||||||
|
|
||||||
## Copy-paste
|
|
||||||
|
|
||||||
Any time the Android clipboard changes, it is automatically synchronized to the
|
|
||||||
computer clipboard.
|
|
||||||
|
|
||||||
Any <kbd>Ctrl</kbd> shortcut is forwarded to the device. In particular:
|
|
||||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> typically copies
|
|
||||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> typically cuts
|
|
||||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> typically pastes (after computer-to-device
|
|
||||||
clipboard synchronization)
|
|
||||||
|
|
||||||
This typically works as you expect.
|
|
||||||
|
|
||||||
The actual behavior depends on the active application though. For example,
|
|
||||||
_Termux_ sends SIGINT on <kbd>Ctrl</kbd>+<kbd>c</kbd> instead, and _K-9 Mail_
|
|
||||||
composes a new message.
|
|
||||||
|
|
||||||
To copy, cut and paste in such cases (but only supported on Android >= 7):
|
|
||||||
- <kbd>MOD</kbd>+<kbd>c</kbd> injects `COPY`
|
|
||||||
- <kbd>MOD</kbd>+<kbd>x</kbd> injects `CUT`
|
|
||||||
- <kbd>MOD</kbd>+<kbd>v</kbd> injects `PASTE` (after computer-to-device
|
|
||||||
clipboard synchronization)
|
|
||||||
|
|
||||||
In addition, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> injects the computer
|
|
||||||
clipboard text as a sequence of key events. This is useful when the component
|
|
||||||
does not accept text pasting (for example in _Termux_), but it can break
|
|
||||||
non-ASCII content.
|
|
||||||
|
|
||||||
**WARNING:** Pasting the computer clipboard to the device (either via
|
|
||||||
<kbd>Ctrl</kbd>+<kbd>v</kbd> or <kbd>MOD</kbd>+<kbd>v</kbd>) copies the content
|
|
||||||
into the Android clipboard. As a consequence, any Android application could read
|
|
||||||
its content. You should avoid pasting sensitive content (like passwords) that
|
|
||||||
way.
|
|
||||||
|
|
||||||
Some Android devices do not behave as expected when setting the device clipboard
|
|
||||||
programmatically. An option `--legacy-paste` is provided to change the behavior
|
|
||||||
of <kbd>Ctrl</kbd>+<kbd>v</kbd> and <kbd>MOD</kbd>+<kbd>v</kbd> so that they
|
|
||||||
also inject the computer clipboard text as a sequence of key events (the same
|
|
||||||
way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
|
|
||||||
|
|
||||||
To disable automatic clipboard synchronization, use
|
|
||||||
`--no-clipboard-autosync`.
|
|
||||||
|
|
||||||
## Pinch-to-zoom
|
|
||||||
|
|
||||||
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
|
|
||||||
|
|
||||||
More precisely, hold down <kbd>Ctrl</kbd> while pressing the left-click button.
|
|
||||||
Until the left-click button is released, all mouse movements scale and rotate
|
|
||||||
the content (if supported by the app) relative to the center of the screen.
|
|
||||||
|
|
||||||
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
|
|
||||||
at a location inverted through the center of the screen.
|
|
||||||
|
|
||||||
|
|
||||||
## Key repeat
|
|
||||||
|
|
||||||
By default, holding a key down generates repeated key events. This can cause
|
|
||||||
performance problems in some games, where these events are useless anyway.
|
|
||||||
|
|
||||||
To avoid forwarding repeated key events:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-key-repeat
|
|
||||||
```
|
|
||||||
|
|
||||||
This option has no effect on HID keyboard (key repeat is handled by Android
|
|
||||||
directly in this mode).
|
|
||||||
|
|
||||||
|
|
||||||
## Right-click and middle-click
|
|
||||||
|
|
||||||
By default, right-click triggers BACK (or POWER on) and middle-click triggers
|
|
||||||
HOME. To disable these shortcuts and forward the clicks to the device instead:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --forward-all-clicks
|
|
||||||
```
|
|
||||||
|
|
||||||
## File drop
|
|
||||||
|
|
||||||
### Install APK
|
|
||||||
|
|
||||||
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
|
|
||||||
window.
|
|
||||||
|
|
||||||
There is no visual feedback, a log is printed to the console.
|
|
||||||
|
|
||||||
|
|
||||||
### Push file to device
|
|
||||||
|
|
||||||
To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK)
|
|
||||||
file to the _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/Movies/
|
|
||||||
```
|
|
||||||
|
|
||||||
## Physical keyboard and mouse simulation
|
|
||||||
|
|
||||||
See the dedicated [HID/OTG](hid-otg.md) page.
|
|
228
doc/device.md
228
doc/device.md
@ -1,228 +0,0 @@
|
|||||||
# Device
|
|
||||||
|
|
||||||
## Selection
|
|
||||||
|
|
||||||
If exactly one device is connected (i.e. listed by `adb devices`), then it is
|
|
||||||
automatically selected.
|
|
||||||
|
|
||||||
However, if there are multiple devices connected, you must specify the one to
|
|
||||||
use in one of 4 ways:
|
|
||||||
- by its serial:
|
|
||||||
```bash
|
|
||||||
scrcpy --serial=0123456789abcdef
|
|
||||||
scrcpy -s 0123456789abcdef # short version
|
|
||||||
|
|
||||||
# the serial is the ip:port if connected over TCP/IP (same behavior as adb)
|
|
||||||
scrcpy --serial=192.168.1.1:5555
|
|
||||||
```
|
|
||||||
- the one connected over USB (if there is exactly one):
|
|
||||||
```bash
|
|
||||||
scrcpy --select-usb
|
|
||||||
scrcpy -d # short version
|
|
||||||
```
|
|
||||||
- the one connected over TCP/IP (if there is exactly one):
|
|
||||||
```bash
|
|
||||||
scrcpy --select-tcpip
|
|
||||||
scrcpy -e # short version
|
|
||||||
```
|
|
||||||
- a device already listening on TCP/IP (see [below](#tcpip-wireless)):
|
|
||||||
```bash
|
|
||||||
scrcpy --tcpip=192.168.1.1:5555
|
|
||||||
scrcpy --tcpip=192.168.1.1 # default port is 5555
|
|
||||||
```
|
|
||||||
|
|
||||||
The serial may also be provided via the environment variable `ANDROID_SERIAL`
|
|
||||||
(also used by `adb`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in bash
|
|
||||||
export ANDROID_SERIAL=0123456789abcdef
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
:: in cmd
|
|
||||||
set ANDROID_SERIAL=0123456789abcdef
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# in PowerShell
|
|
||||||
$env:ANDROID_SERIAL = '0123456789abcdef'
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## TCP/IP (wireless)
|
|
||||||
|
|
||||||
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
|
|
||||||
device over TCP/IP. The device must be connected on the same network as the
|
|
||||||
computer.
|
|
||||||
|
|
||||||
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
|
||||||
|
|
||||||
|
|
||||||
### Automatic
|
|
||||||
|
|
||||||
An option `--tcpip` allows to configure the connection automatically. There are
|
|
||||||
two variants.
|
|
||||||
|
|
||||||
If the device (accessible at 192.168.1.1 in this example) already listens on a
|
|
||||||
port (typically 5555) for incoming _adb_ connections, then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --tcpip=192.168.1.1 # default port is 5555
|
|
||||||
scrcpy --tcpip=192.168.1.1:5555
|
|
||||||
```
|
|
||||||
|
|
||||||
If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP
|
|
||||||
address), connect the device over USB, then run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --tcpip # without arguments
|
|
||||||
```
|
|
||||||
|
|
||||||
It will automatically find the device IP address and adb port, enable TCP/IP
|
|
||||||
mode if necessary, then connect to the device before starting.
|
|
||||||
|
|
||||||
|
|
||||||
### Manual
|
|
||||||
|
|
||||||
Alternatively, it is possible to enable the TCP/IP connection manually using
|
|
||||||
`adb`:
|
|
||||||
|
|
||||||
1. Plug the device into a USB port on your computer.
|
|
||||||
2. Connect the device to the same Wi-Fi network as your computer.
|
|
||||||
3. Get your device IP address, in Settings → About phone → Status, or by
|
|
||||||
executing this command:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
adb shell ip route | awk '{print $9}'
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`.
|
|
||||||
5. Unplug your device.
|
|
||||||
6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`
|
|
||||||
with the device IP address you found)_.
|
|
||||||
7. Run `scrcpy` as usual.
|
|
||||||
8. Run `adb disconnect` once you're done.
|
|
||||||
|
|
||||||
Since Android 11, a [Wireless debugging option][adb-wireless] allows to bypass
|
|
||||||
having to physically connect your device directly to your computer.
|
|
||||||
|
|
||||||
[adb-wireless]: https://developer.android.com/studio/command-line/adb#connect-to-a-device-over-wi-fi-android-11+
|
|
||||||
|
|
||||||
|
|
||||||
## Autostart
|
|
||||||
|
|
||||||
A small tool (by the scrcpy author) allows to run arbitrary commands whenever a
|
|
||||||
new Android device is connected: [AutoAdb]. It can be used to start scrcpy:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
autoadb scrcpy -s '{}'
|
|
||||||
```
|
|
||||||
|
|
||||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
|
||||||
|
|
||||||
|
|
||||||
## Display
|
|
||||||
|
|
||||||
If several displays are available on the Android device, it is possible to
|
|
||||||
select the display to mirror:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --display=1
|
|
||||||
```
|
|
||||||
|
|
||||||
The list of display ids can be retrieved by:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --list-displays
|
|
||||||
```
|
|
||||||
|
|
||||||
A secondary display may only be controlled if the device runs at least Android
|
|
||||||
10 (otherwise it is mirrored as read-only).
|
|
||||||
|
|
||||||
|
|
||||||
## Actions
|
|
||||||
|
|
||||||
Some command line arguments perform actions on the device itself while scrcpy is
|
|
||||||
running.
|
|
||||||
|
|
||||||
|
|
||||||
### Stay awake
|
|
||||||
|
|
||||||
To prevent the device from sleeping after a delay **when the device is plugged
|
|
||||||
in**:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --stay-awake
|
|
||||||
scrcpy -w
|
|
||||||
```
|
|
||||||
|
|
||||||
The initial state is restored when _scrcpy_ is closed.
|
|
||||||
|
|
||||||
If the device is not plugged in (i.e. only connected over TCP/IP),
|
|
||||||
`--stay-awake` has no effect (this is the Android behavior).
|
|
||||||
|
|
||||||
|
|
||||||
### Turn screen off
|
|
||||||
|
|
||||||
It is possible to turn the device screen off while mirroring on start with a
|
|
||||||
command-line option:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --turn-screen-off
|
|
||||||
scrcpy -S # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
Or by pressing <kbd>MOD</kbd>+<kbd>o</kbd> at any time (see
|
|
||||||
[shortcuts](shortcuts.md)).
|
|
||||||
|
|
||||||
To turn it back on, press <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
|
|
||||||
|
|
||||||
On Android, the `POWER` button always turns the screen on. For convenience, if
|
|
||||||
`POWER` is sent via _scrcpy_ (via right-click or <kbd>MOD</kbd>+<kbd>p</kbd>),
|
|
||||||
it will force to turn the screen off after a small delay (on a best effort
|
|
||||||
basis). The physical `POWER` button will still cause the screen to be turned on.
|
|
||||||
|
|
||||||
It can also be useful to prevent the device from sleeping:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --turn-screen-off --stay-awake
|
|
||||||
scrcpy -Sw # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### Show touches
|
|
||||||
|
|
||||||
For presentations, it may be useful to show physical touches (on the physical
|
|
||||||
device). Android exposes this feature in _Developers options_.
|
|
||||||
|
|
||||||
_Scrcpy_ provides an option to enable this feature on start and restore the
|
|
||||||
initial value on exit:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --show-touches
|
|
||||||
scrcpy -t # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
Note that it only shows _physical_ touches (by a finger on the device).
|
|
||||||
|
|
||||||
|
|
||||||
### Power off on close
|
|
||||||
|
|
||||||
To turn the device screen off when closing _scrcpy_:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --power-off-on-close
|
|
||||||
```
|
|
||||||
|
|
||||||
### Power on on start
|
|
||||||
|
|
||||||
By default, on start, the device is powered on. To prevent this behavior:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-power-on
|
|
||||||
```
|
|
||||||
|
|
108
doc/hid-otg.md
108
doc/hid-otg.md
@ -1,108 +0,0 @@
|
|||||||
# HID/OTG
|
|
||||||
|
|
||||||
By default, _scrcpy_ injects input events at the Android API level. As an
|
|
||||||
alternative, when connected over USB, it is possible to send HID events, so that
|
|
||||||
scrcpy behaves as if it was a physical keyboard and/or mouse connected to the
|
|
||||||
Android device.
|
|
||||||
|
|
||||||
A special [OTG](#otg) mode allows to control the device without mirroring (and
|
|
||||||
without USB debugging).
|
|
||||||
|
|
||||||
|
|
||||||
## Physical keyboard simulation
|
|
||||||
|
|
||||||
By default, _scrcpy_ uses Android key or text injection. It works everywhere,
|
|
||||||
but is limited to ASCII.
|
|
||||||
|
|
||||||
Instead, it can simulate a physical USB keyboard on Android to provide a better
|
|
||||||
input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual keyboard
|
|
||||||
is disabled and it works for all characters and IME.
|
|
||||||
|
|
||||||
[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support
|
|
||||||
|
|
||||||
However, it only works if the device is connected via USB.
|
|
||||||
|
|
||||||
Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it
|
|
||||||
is not possible to open a USB device if it is already open by another process
|
|
||||||
like the _adb daemon_).
|
|
||||||
|
|
||||||
To enable this mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --hid-keyboard
|
|
||||||
scrcpy -K # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
If it fails for some reason (for example because the device is not connected via
|
|
||||||
USB), it automatically fallbacks to the default mode (with a log in the
|
|
||||||
console). This allows using the same command line options when connected over
|
|
||||||
USB and TCP/IP.
|
|
||||||
|
|
||||||
In this mode, raw key events (scancodes) are sent to the device, independently
|
|
||||||
of the host key mapping. Therefore, if your keyboard layout does not match, it
|
|
||||||
must be configured on the Android device, in Settings → System → Languages and
|
|
||||||
input → [Physical keyboard].
|
|
||||||
|
|
||||||
This settings page can be started directly:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS
|
|
||||||
```
|
|
||||||
|
|
||||||
However, the option is only available when the HID keyboard is enabled (or when
|
|
||||||
a physical keyboard is connected).
|
|
||||||
|
|
||||||
[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
|
|
||||||
|
|
||||||
|
|
||||||
## Physical mouse simulation
|
|
||||||
|
|
||||||
By default, _scrcpy_ uses Android mouse events injection with absolute
|
|
||||||
coordinates. By simulating a physical mouse, a mouse pointer appears on the
|
|
||||||
Android device, and relative mouse motion, clicks and scrolls are injected.
|
|
||||||
|
|
||||||
To enable this mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --hid-mouse
|
|
||||||
scrcpy -M # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
When this mode is enabled, the computer mouse is "captured" (the mouse pointer
|
|
||||||
disappears from the computer and appears on the Android device instead).
|
|
||||||
|
|
||||||
Special capture keys, either <kbd>Alt</kbd> or <kbd>Super</kbd>, toggle
|
|
||||||
(disable or enable) the mouse capture. Use one of them to give the control of
|
|
||||||
the mouse back to the computer.
|
|
||||||
|
|
||||||
|
|
||||||
## OTG
|
|
||||||
|
|
||||||
It is possible to run _scrcpy_ with only physical keyboard and mouse simulation
|
|
||||||
(HID), 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.
|
|
||||||
|
|
||||||
This is similar to `--hid-keyboard --hid-mouse`, but without mirroring.
|
|
||||||
|
|
||||||
To enable OTG mode:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --otg
|
|
||||||
# Pass the serial if several USB devices are available
|
|
||||||
scrcpy --otg -s 0123456789abcdef
|
|
||||||
```
|
|
||||||
|
|
||||||
It is possible to enable only HID keyboard or HID mouse:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --otg --hid-keyboard # keyboard only
|
|
||||||
scrcpy --otg --hid-mouse # mouse only
|
|
||||||
scrcpy --otg --hid-keyboard --hid-mouse # keyboard and mouse
|
|
||||||
# for convenience, enable both by default
|
|
||||||
scrcpy --otg # keyboard and mouse
|
|
||||||
```
|
|
||||||
|
|
||||||
Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is
|
|
||||||
connected over USB.
|
|
79
doc/linux.md
79
doc/linux.md
@ -1,79 +0,0 @@
|
|||||||
# On Linux
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
|
|
||||||
|
|
||||||
Scrcpy is packaged in several distributions and package managers:
|
|
||||||
|
|
||||||
- Debian/Ubuntu: `apt install scrcpy`
|
|
||||||
- Arch Linux: `pacman -S scrcpy`
|
|
||||||
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
|
|
||||||
- Gentoo: [ebuild][ebuild-link] file
|
|
||||||
- Snap: `snap install scrcpy`
|
|
||||||
- … (see [repology](https://repology.org/project/scrcpy/versions))
|
|
||||||
|
|
||||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
|
||||||
|
|
||||||
### Latest version
|
|
||||||
|
|
||||||
However, the packaged version is not always the latest release. To install the
|
|
||||||
latest release from `master`, follow this simplified process.
|
|
||||||
|
|
||||||
First, you need to install the required packages:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# for Debian/Ubuntu
|
|
||||||
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
|
||||||
gcc git pkg-config meson ninja-build libsdl2-dev \
|
|
||||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
|
||||||
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Then clone the repo and execute the installation script
|
|
||||||
([source](/install_release.sh)):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git clone https://github.com/Genymobile/scrcpy
|
|
||||||
cd scrcpy
|
|
||||||
./install_release.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
When a new release is out, update the repo and reinstall:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git pull
|
|
||||||
./install_release.sh
|
|
||||||
```
|
|
||||||
|
|
||||||
To uninstall:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo ninja -Cbuild-auto uninstall
|
|
||||||
```
|
|
||||||
|
|
||||||
_Note that this simplified process only works for released versions (it
|
|
||||||
downloads a prebuilt server binary), so for example you can't use it for testing
|
|
||||||
the development branch (`dev`)._
|
|
||||||
|
|
||||||
_See [build.md](build.md) to build and install the app manually._
|
|
||||||
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
Once installed, run from a terminal:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
or with arguments (here to disable audio and record to `file.mkv`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-audio --record=file.mkv
|
|
||||||
```
|
|
||||||
|
|
||||||
Documentation for command line arguments is available:
|
|
||||||
- `man scrcpy`
|
|
||||||
- `scrcpy --help`
|
|
||||||
- on [github](/README.md)
|
|
47
doc/macos.md
47
doc/macos.md
@ -1,47 +0,0 @@
|
|||||||
# On macOS
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Scrcpy is available in [Homebrew]:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
[Homebrew]: https://brew.sh/
|
|
||||||
|
|
||||||
You need `adb`, accessible from your `PATH`. If you don't have it yet:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
brew install android-platform-tools
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo port install scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
[MacPorts]: https://www.macports.org/
|
|
||||||
|
|
||||||
_See [build.md](build.md) to build and install the app manually._
|
|
||||||
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
Once installed, run from a terminal:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
or with arguments (here to disable audio and record to `file.mkv`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-audio --record=file.mkv
|
|
||||||
```
|
|
||||||
|
|
||||||
Documentation for command line arguments is available:
|
|
||||||
- `man scrcpy`
|
|
||||||
- `scrcpy --help`
|
|
||||||
- on [github](/README.md)
|
|
@ -1,44 +0,0 @@
|
|||||||
# Recording
|
|
||||||
|
|
||||||
To record video and audio streams while mirroring:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --record=file.mp4
|
|
||||||
scrcpy -r file.mkv
|
|
||||||
```
|
|
||||||
|
|
||||||
To record only the video:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-audio --record=file.mp4
|
|
||||||
```
|
|
||||||
|
|
||||||
_It is currently not possible to record only the audio._
|
|
||||||
|
|
||||||
To disable mirroring while recording:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --no-display --record=file.mp4
|
|
||||||
scrcpy -Nr file.mkv
|
|
||||||
# interrupt recording with Ctrl+C
|
|
||||||
```
|
|
||||||
|
|
||||||
Timestamps are captured on the device, so [packet delay variation] does not
|
|
||||||
impact the recorded file, which is always clean (only if you use `--record` of
|
|
||||||
course, not if you capture your scrcpy window and audio output on the computer).
|
|
||||||
|
|
||||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
|
||||||
|
|
||||||
The video and audio streams are encoded on the device, but are muxed on the
|
|
||||||
client side. Two formats (containers) are supported:
|
|
||||||
- Matroska (`.mkv`)
|
|
||||||
- MP4 (`.mp4`)
|
|
||||||
|
|
||||||
The container is automatically selected based on the filename.
|
|
||||||
|
|
||||||
It is also possible to explicitly select a container (in that case the filename
|
|
||||||
needs not end with `.mkv` or `.mp4`):
|
|
||||||
|
|
||||||
```
|
|
||||||
scrcpy --record=file --record-format=mkv
|
|
||||||
```
|
|
@ -1,68 +0,0 @@
|
|||||||
# Shortcuts
|
|
||||||
|
|
||||||
Actions can be performed on the scrcpy window using keyboard and mouse
|
|
||||||
shortcuts.
|
|
||||||
|
|
||||||
In the following list, <kbd>MOD</kbd> is the shortcut modifier. By default, it's
|
|
||||||
(left) <kbd>Alt</kbd> or (left) <kbd>Super</kbd>.
|
|
||||||
|
|
||||||
It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`,
|
|
||||||
`lalt`, `ralt`, `lsuper` and `rsuper`. For example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# use RCtrl for shortcuts
|
|
||||||
scrcpy --shortcut-mod=rctrl
|
|
||||||
|
|
||||||
# use either LCtrl+LAlt or LSuper for shortcuts
|
|
||||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
|
||||||
```
|
|
||||||
|
|
||||||
_<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
|
||||||
|
|
||||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
|
||||||
|
|
||||||
| Action | Shortcut
|
|
||||||
| ------------------------------------------- |:-----------------------------
|
|
||||||
| Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd>
|
|
||||||
| Rotate display left | <kbd>MOD</kbd>+<kbd>←</kbd> _(left)_
|
|
||||||
| Rotate display right | <kbd>MOD</kbd>+<kbd>→</kbd> _(right)_
|
|
||||||
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
|
||||||
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
|
|
||||||
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
|
|
||||||
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
|
|
||||||
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4th-click³_
|
|
||||||
| Click on `MENU` (unlock screen)⁴ | <kbd>MOD</kbd>+<kbd>m</kbd>
|
|
||||||
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(up)_
|
|
||||||
| Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_
|
|
||||||
| Click on `POWER` | <kbd>MOD</kbd>+<kbd>p</kbd>
|
|
||||||
| Power on | _Right-click²_
|
|
||||||
| Turn device screen off (keep mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
|
||||||
| Turn device screen on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
|
||||||
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
|
|
||||||
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5th-click³_
|
|
||||||
| Expand settings panel | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Double-5th-click³_
|
|
||||||
| Collapse panels | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
|
||||||
| Copy to clipboard⁵ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
|
||||||
| Cut to clipboard⁵ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
|
||||||
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
|
||||||
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
|
||||||
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
|
||||||
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
|
|
||||||
| Drag & drop APK file | Install APK from computer
|
|
||||||
| Drag & drop non-APK file | [Push file to device](#push-file-to-device)
|
|
||||||
|
|
||||||
_¹Double-click on black borders to remove them._
|
|
||||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
|
||||||
_³4th and 5th mouse buttons, if your mouse has them._
|
|
||||||
_⁴For react-native apps in development, `MENU` triggers development menu._
|
|
||||||
_⁵Only on Android >= 7._
|
|
||||||
|
|
||||||
Shortcuts with repeated keys are executed by releasing and pressing the key a
|
|
||||||
second time. For example, to execute "Expand settings panel":
|
|
||||||
|
|
||||||
1. Press and keep pressing <kbd>MOD</kbd>.
|
|
||||||
2. Then double-press <kbd>n</kbd>.
|
|
||||||
3. Finally, release <kbd>MOD</kbd>.
|
|
||||||
|
|
||||||
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
|
|
||||||
handled by the active application.
|
|
123
doc/tunnels.md
123
doc/tunnels.md
@ -1,123 +0,0 @@
|
|||||||
# Tunnels
|
|
||||||
|
|
||||||
Scrcpy is designed to mirror local Android devices. Tunnels allow to connect to
|
|
||||||
a remote device (e.g. over the Internet).
|
|
||||||
|
|
||||||
To connect to a remote device, it is possible to connect a local `adb` client to
|
|
||||||
a remote `adb` server (provided they use the same version of the _adb_
|
|
||||||
protocol).
|
|
||||||
|
|
||||||
|
|
||||||
## Remote ADB server
|
|
||||||
|
|
||||||
To connect to a remote _adb server_, make the server listen on all interfaces:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
adb kill-server
|
|
||||||
adb -a nodaemon server start
|
|
||||||
# keep this open
|
|
||||||
```
|
|
||||||
|
|
||||||
**Warning: all communications between clients and the _adb server_ are
|
|
||||||
unencrypted.**
|
|
||||||
|
|
||||||
Suppose that this server is accessible at 192.168.1.2. Then, from another
|
|
||||||
terminal, run `scrcpy`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in bash
|
|
||||||
export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
|
|
||||||
scrcpy --tunnel-host=192.168.1.2
|
|
||||||
```
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
:: in cmd
|
|
||||||
set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
|
|
||||||
scrcpy --tunnel-host=192.168.1.2
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# in PowerShell
|
|
||||||
$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037'
|
|
||||||
scrcpy --tunnel-host=192.168.1.2
|
|
||||||
```
|
|
||||||
|
|
||||||
By default, `scrcpy` uses the local port used for `adb forward` tunnel
|
|
||||||
establishment (typically `27183`, see `--port`). It is also possible to force a
|
|
||||||
different tunnel port (it may be useful in more complex situations, when more
|
|
||||||
redirections are involved):
|
|
||||||
|
|
||||||
```
|
|
||||||
scrcpy --tunnel-port=1234
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## SSH tunnel
|
|
||||||
|
|
||||||
To communicate with a remote _adb server_ securely, it is preferable to use an
|
|
||||||
SSH tunnel.
|
|
||||||
|
|
||||||
First, make sure the _adb server_ is running on the remote computer:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
adb start-server
|
|
||||||
```
|
|
||||||
|
|
||||||
Then, establish an SSH tunnel:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# local 5038 --> remote 5037
|
|
||||||
# local 27183 <-- remote 27183
|
|
||||||
ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
|
||||||
# keep this open
|
|
||||||
```
|
|
||||||
|
|
||||||
From another terminal, run `scrcpy`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in bash
|
|
||||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
:: in cmd
|
|
||||||
set ADB_SERVER_SOCKET=tcp:localhost:5038
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# in PowerShell
|
|
||||||
$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038'
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
To avoid enabling remote port forwarding, you could force a forward connection
|
|
||||||
instead (notice the `-L` instead of `-R`):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# local 5038 --> remote 5037
|
|
||||||
# local 27183 --> remote 27183
|
|
||||||
ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
|
||||||
# keep this open
|
|
||||||
```
|
|
||||||
|
|
||||||
From another terminal, run `scrcpy`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# in bash
|
|
||||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
|
||||||
scrcpy --force-adb-forward
|
|
||||||
```
|
|
||||||
|
|
||||||
```cmd
|
|
||||||
:: in cmd
|
|
||||||
set ADB_SERVER_SOCKET=tcp:localhost:5038
|
|
||||||
scrcpy --force-adb-forward
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
# in PowerShell
|
|
||||||
$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038'
|
|
||||||
scrcpy --force-adb-forward
|
|
||||||
```
|
|
65
doc/v4l2.md
65
doc/v4l2.md
@ -1,65 +0,0 @@
|
|||||||
# Video4Linux
|
|
||||||
|
|
||||||
On Linux, it is possible to send the video stream to a [v4l2] loopback device,
|
|
||||||
so that the Android device can be opened like a webcam by any v4l2-capable tool.
|
|
||||||
|
|
||||||
[v4l2]: https://en.wikipedia.org/wiki/Video4Linux
|
|
||||||
|
|
||||||
The module `v4l2loopback` must be installed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo apt install v4l2loopback-dkms
|
|
||||||
```
|
|
||||||
|
|
||||||
To create a v4l2 device:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo modprobe v4l2loopback
|
|
||||||
```
|
|
||||||
|
|
||||||
This will create a new video device in `/dev/videoN`, where `N` is an integer
|
|
||||||
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
|
|
||||||
to create several devices or devices with specific IDs).
|
|
||||||
|
|
||||||
To list the enabled devices:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# requires v4l-utils package
|
|
||||||
v4l2-ctl --list-devices
|
|
||||||
|
|
||||||
# simple but might be sufficient
|
|
||||||
ls /dev/video*
|
|
||||||
```
|
|
||||||
|
|
||||||
To start `scrcpy` using a v4l2 sink:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --v4l2-sink=/dev/videoN
|
|
||||||
scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window
|
|
||||||
```
|
|
||||||
|
|
||||||
(replace `N` with the device ID, check with `ls /dev/video*`)
|
|
||||||
|
|
||||||
Once enabled, you can open your video stream with a v4l2-capable tool:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ffplay -i /dev/videoN
|
|
||||||
vlc v4l2:///dev/videoN # VLC might add some buffering delay
|
|
||||||
```
|
|
||||||
|
|
||||||
For example, you could capture the video within [OBS] or within your video
|
|
||||||
conference tool.
|
|
||||||
|
|
||||||
[OBS]: https://obsproject.com/
|
|
||||||
|
|
||||||
|
|
||||||
## Buffering
|
|
||||||
|
|
||||||
By default, there is no video buffering, to get the lowest possible latency.
|
|
||||||
|
|
||||||
As for the [video display](video.md#buffering), it is possible to add
|
|
||||||
buffering to delay the v4l2 stream:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
|
|
||||||
```
|
|
175
doc/video.md
175
doc/video.md
@ -1,175 +0,0 @@
|
|||||||
# Video
|
|
||||||
|
|
||||||
## Size
|
|
||||||
|
|
||||||
By default, scrcpy attempts to mirror at the Android device resolution.
|
|
||||||
|
|
||||||
It might be useful to mirror at a lower definition to increase performance. To
|
|
||||||
limit both width and height to some maximum value (here 1024):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --max-size=1024
|
|
||||||
scrcpy -m 1024 # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
The other dimension is computed so that the Android device aspect ratio is
|
|
||||||
preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
|
|
||||||
|
|
||||||
If encoding fails, scrcpy automatically tries again with a lower definition
|
|
||||||
(unless `--no-downsize-on-error` is enabled).
|
|
||||||
|
|
||||||
|
|
||||||
## Bit rate
|
|
||||||
|
|
||||||
The default video bit-rate is 8 Mbps. To change it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --video-bit-rate=2M
|
|
||||||
scrcpy --video-bit-rate=2000000 # equivalent
|
|
||||||
scrcpy -b 2M # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Frame rate
|
|
||||||
|
|
||||||
The capture frame rate can be limited:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --max-fps=15
|
|
||||||
```
|
|
||||||
|
|
||||||
The actual capture frame rate may be printed to the console:
|
|
||||||
|
|
||||||
```
|
|
||||||
scrcpy --print-fps
|
|
||||||
```
|
|
||||||
|
|
||||||
It may also be enabled or disabled at anytime with <kbd>MOD</kbd>+<kbd>i</kbd>
|
|
||||||
(see [shortcuts](shortcuts.md)).
|
|
||||||
|
|
||||||
The frame rate is intrinsically variable: a new frame is produced only when the
|
|
||||||
screen content changes. For example, if you play a fullscreen video at 24fps on
|
|
||||||
your device, you should not get more than 24 frames per second in scrcpy.
|
|
||||||
|
|
||||||
|
|
||||||
## Codec
|
|
||||||
|
|
||||||
The video codec can be selected. The possible values are `h264` (default),
|
|
||||||
`h265` and `av1`:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --video-codec=h264 # default
|
|
||||||
scrcpy --video-codec=h265
|
|
||||||
scrcpy --video-codec=av1
|
|
||||||
```
|
|
||||||
|
|
||||||
H265 may provide better quality, but H264 should provide lower latency.
|
|
||||||
AV1 encoders are not common on current Android devices.
|
|
||||||
|
|
||||||
Several encoders may be available on the device. They can be listed by:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --list-encoders
|
|
||||||
```
|
|
||||||
|
|
||||||
Sometimes, the default encoder may have issues or even crash, so it is useful to
|
|
||||||
try another one:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
|
|
||||||
```
|
|
||||||
|
|
||||||
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
|
|
||||||
check `--video-codec-options` in the manpage or in `scrcpy --help`.
|
|
||||||
|
|
||||||
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
|
|
||||||
|
|
||||||
|
|
||||||
## Rotation
|
|
||||||
|
|
||||||
The rotation may be applied at 3 different levels:
|
|
||||||
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
|
|
||||||
device to switch between portrait and landscape (the current running app may
|
|
||||||
refuse, if it does not support the requested orientation).
|
|
||||||
- `--lock-video-orientation` changes the mirroring orientation (the orientation
|
|
||||||
of the video sent from the device to the computer). This affects the
|
|
||||||
recording.
|
|
||||||
- `--rotation` rotates only the window content. This only affects the display,
|
|
||||||
not the recording. It may be changed dynamically at any time using the
|
|
||||||
[shortcuts](shortcuts.md) <kbd>MOD</kbd>+<kbd>←</kbd> and
|
|
||||||
<kbd>MOD</kbd>+<kbd>→</kbd>.
|
|
||||||
|
|
||||||
To lock the mirroring orientation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --lock-video-orientation # initial (current) orientation
|
|
||||||
scrcpy --lock-video-orientation=0 # natural orientation
|
|
||||||
scrcpy --lock-video-orientation=1 # 90° counterclockwise
|
|
||||||
scrcpy --lock-video-orientation=2 # 180°
|
|
||||||
scrcpy --lock-video-orientation=3 # 90° clockwise
|
|
||||||
```
|
|
||||||
|
|
||||||
To set an initial window rotation:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --rotation=0 # no rotation
|
|
||||||
scrcpy --rotation=1 # 90 degrees counterclockwise
|
|
||||||
scrcpy --rotation=2 # 180 degrees
|
|
||||||
scrcpy --rotation=3 # 90 degrees clockwise
|
|
||||||
```
|
|
||||||
|
|
||||||
## Crop
|
|
||||||
|
|
||||||
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:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0)
|
|
||||||
```
|
|
||||||
|
|
||||||
The values are expressed in the device natural orientation (portrait for a
|
|
||||||
phone, landscape for a tablet).
|
|
||||||
|
|
||||||
If `--max-size` is also specified, resizing is applied after cropping.
|
|
||||||
|
|
||||||
|
|
||||||
## Buffering
|
|
||||||
|
|
||||||
By default, there is no video buffering, to get the lowest possible latency.
|
|
||||||
|
|
||||||
Buffering can be added to delay the video stream and compensate for jitter to
|
|
||||||
get a smoother playback (see [#2464]).
|
|
||||||
|
|
||||||
[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
|
|
||||||
|
|
||||||
The configuration is available independently for the display,
|
|
||||||
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --display-buffer=50 # add 50ms buffering for display
|
|
||||||
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
|
|
||||||
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
|
|
||||||
```
|
|
||||||
|
|
||||||
They can be applied simultaneously:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --display-buffer=50 --v4l2-buffer=300
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## No display
|
|
||||||
|
|
||||||
It is possible to capture an Android device without displaying a mirroring
|
|
||||||
window. This option is available if either [recording](recording.md) or
|
|
||||||
[v4l2](#video4linux) is enabled:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --v4l2-sink=/dev/video2 --no-display
|
|
||||||
scrcpy --record=file.mkv --no-display
|
|
||||||
```
|
|
||||||
|
|
||||||
## Video4Linux
|
|
||||||
|
|
||||||
See the dedicated [Video4Linux](v4l2.md) page.
|
|
@ -1,55 +0,0 @@
|
|||||||
# Window
|
|
||||||
|
|
||||||
## Title
|
|
||||||
|
|
||||||
By default, the window title is the device model. It can be changed:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --window-title='My device'
|
|
||||||
```
|
|
||||||
|
|
||||||
## Position and size
|
|
||||||
|
|
||||||
The initial window position and size may be specified:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600
|
|
||||||
```
|
|
||||||
|
|
||||||
## Borderless
|
|
||||||
|
|
||||||
To disable window decorations:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --window-borderless
|
|
||||||
```
|
|
||||||
|
|
||||||
## Always on top
|
|
||||||
|
|
||||||
To keep the window always on top:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --always-on-top
|
|
||||||
```
|
|
||||||
|
|
||||||
## Fullscreen
|
|
||||||
|
|
||||||
The app may be started directly in fullscreen:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --fullscreen
|
|
||||||
scrcpy -f # short version
|
|
||||||
```
|
|
||||||
|
|
||||||
Fullscreen mode can then be toggled dynamically with <kbd>MOD</kbd>+<kbd>f</kbd>
|
|
||||||
(see [shortcuts](shortcuts.md)).
|
|
||||||
|
|
||||||
|
|
||||||
## Disable screensaver
|
|
||||||
|
|
||||||
By default, _scrcpy_ does not prevent the screensaver from running on the
|
|
||||||
computer. To disable it:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --disable-screensaver
|
|
||||||
```
|
|
@ -1,84 +0,0 @@
|
|||||||
# On Windows
|
|
||||||
|
|
||||||
## Install
|
|
||||||
|
|
||||||
Download the [latest release]:
|
|
||||||
|
|
||||||
- [`scrcpy-win64-v1.25.zip`][direct-win64]
|
|
||||||
<sub>SHA-256: `db65125e9c65acd00359efb7cea9c05f63cc7ccd5833000cd243cc92f5053028`</sub>
|
|
||||||
|
|
||||||
[release]: https://github.com/Genymobile/scrcpy/releases/latest
|
|
||||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-win64-v1.25.zip
|
|
||||||
|
|
||||||
and extract it.
|
|
||||||
|
|
||||||
Alternatively, you could install it from packages manager, like [Chocolatey]:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
choco install scrcpy
|
|
||||||
choco install adb # if you don't have it yet
|
|
||||||
```
|
|
||||||
|
|
||||||
or [Scoop]:
|
|
||||||
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scoop install scrcpy
|
|
||||||
scoop install adb # if you don't have it yet
|
|
||||||
```
|
|
||||||
|
|
||||||
[Chocolatey]: https://chocolatey.org/
|
|
||||||
[Scoop]: https://scoop.sh
|
|
||||||
|
|
||||||
_See [build.md](build.md) to build and install the app manually._
|
|
||||||
|
|
||||||
|
|
||||||
## Run
|
|
||||||
|
|
||||||
Scrcpy is a command line application: it is mainly intended to be executed from
|
|
||||||
a terminal with command line arguments.
|
|
||||||
|
|
||||||
To open a terminal at the expected location, double-click on
|
|
||||||
`open_a_terminal_here.bat` in your scrcpy directory, then type your command. For
|
|
||||||
example, without arguments:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy
|
|
||||||
```
|
|
||||||
|
|
||||||
or with arguments (here to disable audio and record to `file.mkv`):
|
|
||||||
|
|
||||||
```
|
|
||||||
scrcpy --no-audio --record=file.mkv
|
|
||||||
```
|
|
||||||
|
|
||||||
Documentation for command line arguments is available:
|
|
||||||
- `scrcpy --help`
|
|
||||||
- on [github](/README.md)
|
|
||||||
|
|
||||||
To start scrcpy directly without opening a terminal, double-click on one of
|
|
||||||
these files:
|
|
||||||
- `scrcpy-console.bat`: start with a terminal open (it will close when scrcpy
|
|
||||||
terminates, unless an error occurs);
|
|
||||||
- `scrcpy-noconsole.vbs`: start without a terminal (but you won't see any error
|
|
||||||
message).
|
|
||||||
|
|
||||||
_Avoid double-clicking on `scrcpy.exe` directly: on error, the terminal would
|
|
||||||
close immediately and you won't have time to read any error message (this
|
|
||||||
executable is intended to be run from the terminal). Use `scrcpy-console.bat`
|
|
||||||
instead._
|
|
||||||
|
|
||||||
If you plan to always use the same arguments, create a file `myscrcpy.bat`
|
|
||||||
(enable [show file extensions] to avoid confusion) containing your command, For
|
|
||||||
example:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
scrcpy --prefer-text --turn-screen-off --stay-awake
|
|
||||||
```
|
|
||||||
|
|
||||||
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
|
|
||||||
|
|
||||||
Then just double-click on that file.
|
|
||||||
|
|
||||||
You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs`
|
|
||||||
to add some arguments.
|
|
36
release.mk
36
release.mk
@ -94,15 +94,15 @@ dist-win32: build-server build-win32
|
|||||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
|
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
|
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
|
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/SDL2-2.26.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
|
|
||||||
dist-win64: build-server build-win64
|
dist-win64: build-server build-win64
|
||||||
@ -113,15 +113,15 @@ dist-win64: build-server build-win64
|
|||||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
|
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
|
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/SDL2-2.26.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
|
||||||
zip-win32: dist-win32
|
zip-win32: dist-win32
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
public interface AsyncProcessor {
|
|
||||||
void start();
|
|
||||||
void stop();
|
|
||||||
void join() throws InterruptedException;
|
|
||||||
}
|
|
@ -1,147 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint;
|
|
||||||
import android.annotation.TargetApi;
|
|
||||||
import android.content.ComponentName;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.media.AudioFormat;
|
|
||||||
import android.media.AudioRecord;
|
|
||||||
import android.media.AudioTimestamp;
|
|
||||||
import android.media.MediaCodec;
|
|
||||||
import android.media.MediaRecorder;
|
|
||||||
import android.os.Build;
|
|
||||||
import android.os.SystemClock;
|
|
||||||
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
public final class AudioCapture {
|
|
||||||
|
|
||||||
public static final int SAMPLE_RATE = 48000;
|
|
||||||
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
|
|
||||||
public static final int CHANNELS = 2;
|
|
||||||
public static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
|
|
||||||
public static final int BYTES_PER_SAMPLE = 2;
|
|
||||||
|
|
||||||
private AudioRecord recorder;
|
|
||||||
|
|
||||||
private final AudioTimestamp timestamp = new AudioTimestamp();
|
|
||||||
private long previousPts = 0;
|
|
||||||
private long nextPts = 0;
|
|
||||||
|
|
||||||
public static int millisToBytes(int millis) {
|
|
||||||
return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static AudioFormat createAudioFormat() {
|
|
||||||
AudioFormat.Builder builder = new AudioFormat.Builder();
|
|
||||||
builder.setEncoding(FORMAT);
|
|
||||||
builder.setSampleRate(SAMPLE_RATE);
|
|
||||||
builder.setChannelMask(CHANNEL_CONFIG);
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
|
||||||
@SuppressLint({"WrongConstant", "MissingPermission"})
|
|
||||||
private static AudioRecord createAudioRecord() {
|
|
||||||
AudioRecord.Builder builder = new AudioRecord.Builder();
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
||||||
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
|
|
||||||
builder.setContext(FakeContext.get());
|
|
||||||
}
|
|
||||||
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
|
|
||||||
builder.setAudioFormat(createAudioFormat());
|
|
||||||
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
|
|
||||||
// This buffer size does not impact latency
|
|
||||||
builder.setBufferSizeInBytes(8 * minBufferSize);
|
|
||||||
return builder.build();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void startWorkaroundAndroid11() {
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
|
||||||
// Android 11 requires Apps to be at foreground to record audio.
|
|
||||||
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
|
||||||
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
|
||||||
// shell ("com.android.shell").
|
|
||||||
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
|
||||||
// foreground.
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
|
||||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
|
||||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
|
||||||
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
|
||||||
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
|
||||||
// Wait for activity to start
|
|
||||||
SystemClock.sleep(150);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void stopWorkaroundAndroid11() {
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
|
||||||
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() throws AudioCaptureForegroundException {
|
|
||||||
startWorkaroundAndroid11();
|
|
||||||
try {
|
|
||||||
recorder = createAudioRecord();
|
|
||||||
recorder.startRecording();
|
|
||||||
} catch (UnsupportedOperationException e) {
|
|
||||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
|
||||||
Ln.e("Failed to start audio capture");
|
|
||||||
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
|
|
||||||
throw new AudioCaptureForegroundException();
|
|
||||||
}
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
stopWorkaroundAndroid11();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (recorder != null) {
|
|
||||||
// Will call .stop() if necessary, without throwing an IllegalStateException
|
|
||||||
recorder.release();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
|
||||||
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
|
|
||||||
int r = recorder.read(directBuffer, size);
|
|
||||||
if (r < 0) {
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
|
|
||||||
long pts;
|
|
||||||
|
|
||||||
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
|
|
||||||
if (ret == AudioRecord.SUCCESS) {
|
|
||||||
pts = timestamp.nanoTime / 1000;
|
|
||||||
} else {
|
|
||||||
if (nextPts == 0) {
|
|
||||||
Ln.w("Could not get any audio timestamp");
|
|
||||||
}
|
|
||||||
// compute from previous timestamp and packet size
|
|
||||||
pts = nextPts;
|
|
||||||
}
|
|
||||||
|
|
||||||
long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
|
|
||||||
nextPts = pts + durationUs;
|
|
||||||
|
|
||||||
if (previousPts != 0 && pts < previousPts) {
|
|
||||||
// Audio PTS may come from two sources:
|
|
||||||
// - recorder.getTimestamp() if the call works;
|
|
||||||
// - an estimation from the previous PTS and the packet size as a fallback.
|
|
||||||
//
|
|
||||||
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
|
|
||||||
pts = previousPts + 1;
|
|
||||||
}
|
|
||||||
previousPts = pts;
|
|
||||||
|
|
||||||
outBufferInfo.set(0, r, pts, 0);
|
|
||||||
return r;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground.
|
|
||||||
*/
|
|
||||||
public class AudioCaptureForegroundException extends Exception {
|
|
||||||
}
|
|
@ -4,8 +4,7 @@ import android.media.MediaFormat;
|
|||||||
|
|
||||||
public enum AudioCodec implements Codec {
|
public enum AudioCodec implements Codec {
|
||||||
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
||||||
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC),
|
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC);
|
||||||
RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW);
|
|
||||||
|
|
||||||
private final int id; // 4-byte ASCII representation of the name
|
private final int id; // 4-byte ASCII representation of the name
|
||||||
private final String name;
|
private final String name;
|
||||||
|
@ -1,12 +1,22 @@
|
|||||||
package com.genymobile.scrcpy;
|
package com.genymobile.scrcpy;
|
||||||
|
|
||||||
|
import com.genymobile.scrcpy.wrappers.ServiceManager;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
|
import android.content.ComponentName;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.media.AudioFormat;
|
||||||
|
import android.media.AudioRecord;
|
||||||
|
import android.media.AudioTimestamp;
|
||||||
import android.media.MediaCodec;
|
import android.media.MediaCodec;
|
||||||
import android.media.MediaFormat;
|
import android.media.MediaFormat;
|
||||||
|
import android.media.MediaRecorder;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.HandlerThread;
|
import android.os.HandlerThread;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
|
import android.os.SystemClock;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
@ -14,7 +24,7 @@ import java.util.List;
|
|||||||
import java.util.concurrent.ArrayBlockingQueue;
|
import java.util.concurrent.ArrayBlockingQueue;
|
||||||
import java.util.concurrent.BlockingQueue;
|
import java.util.concurrent.BlockingQueue;
|
||||||
|
|
||||||
public final class AudioEncoder implements AsyncProcessor {
|
public final class AudioEncoder {
|
||||||
|
|
||||||
private static class InputTask {
|
private static class InputTask {
|
||||||
private final int index;
|
private final int index;
|
||||||
@ -34,11 +44,14 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE;
|
private static final int SAMPLE_RATE = 48000;
|
||||||
private static final int CHANNELS = AudioCapture.CHANNELS;
|
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
|
||||||
|
private static final int CHANNELS = 2;
|
||||||
|
private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
|
||||||
|
private static final int BYTES_PER_SAMPLE = 2;
|
||||||
|
|
||||||
private static final int READ_MS = 5; // milliseconds
|
private static final int BUFFER_MS = 5; // milliseconds
|
||||||
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
|
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * BUFFER_MS / 1000;
|
||||||
|
|
||||||
private final Streamer streamer;
|
private final Streamer streamer;
|
||||||
private final int bitRate;
|
private final int bitRate;
|
||||||
@ -65,6 +78,29 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
this.encoderName = encoderName;
|
this.encoderName = encoderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static AudioFormat createAudioFormat() {
|
||||||
|
AudioFormat.Builder builder = new AudioFormat.Builder();
|
||||||
|
builder.setEncoding(FORMAT);
|
||||||
|
builder.setSampleRate(SAMPLE_RATE);
|
||||||
|
builder.setChannelMask(CHANNEL_CONFIG);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
|
@SuppressLint({"WrongConstant", "MissingPermission"})
|
||||||
|
private static AudioRecord createAudioRecord() {
|
||||||
|
AudioRecord.Builder builder = new AudioRecord.Builder();
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
|
||||||
|
builder.setContext(FakeContext.get());
|
||||||
|
}
|
||||||
|
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
|
||||||
|
builder.setAudioFormat(createAudioFormat());
|
||||||
|
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
|
||||||
|
builder.setBufferSizeInBytes(minBufferSize);
|
||||||
|
return builder.build();
|
||||||
|
}
|
||||||
|
|
||||||
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {
|
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {
|
||||||
MediaFormat format = new MediaFormat();
|
MediaFormat format = new MediaFormat();
|
||||||
format.setString(MediaFormat.KEY_MIME, mimeType);
|
format.setString(MediaFormat.KEY_MIME, mimeType);
|
||||||
@ -85,23 +121,52 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.N)
|
@TargetApi(Build.VERSION_CODES.N)
|
||||||
private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException {
|
private void inputThread(MediaCodec mediaCodec, AudioRecord recorder) throws IOException, InterruptedException {
|
||||||
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
final AudioTimestamp timestamp = new AudioTimestamp();
|
||||||
|
long previousPts = 0;
|
||||||
|
long nextPts = 0;
|
||||||
|
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
InputTask task = inputTasks.take();
|
InputTask task = inputTasks.take();
|
||||||
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
|
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
|
||||||
int r = capture.read(buffer, READ_SIZE, bufferInfo);
|
int r = recorder.read(buffer, BUFFER_SIZE);
|
||||||
if (r < 0) {
|
if (r < 0) {
|
||||||
throw new IOException("Could not read audio: " + r);
|
throw new IOException("Could not read audio: " + r);
|
||||||
}
|
}
|
||||||
|
|
||||||
mediaCodec.queueInputBuffer(task.index, bufferInfo.offset, bufferInfo.size, bufferInfo.presentationTimeUs, bufferInfo.flags);
|
long pts;
|
||||||
|
|
||||||
|
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
|
||||||
|
if (ret == AudioRecord.SUCCESS) {
|
||||||
|
pts = timestamp.nanoTime / 1000;
|
||||||
|
} else {
|
||||||
|
if (nextPts == 0) {
|
||||||
|
Ln.w("Could not get any audio timestamp");
|
||||||
|
}
|
||||||
|
// compute from previous timestamp and packet size
|
||||||
|
pts = nextPts;
|
||||||
|
}
|
||||||
|
|
||||||
|
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
|
||||||
|
nextPts = pts + durationMs;
|
||||||
|
|
||||||
|
if (previousPts != 0 && pts < previousPts) {
|
||||||
|
// Audio PTS may come from two sources:
|
||||||
|
// - recorder.getTimestamp() if the call works;
|
||||||
|
// - an estimation from the previous PTS and the packet size as a fallback.
|
||||||
|
//
|
||||||
|
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
|
||||||
|
pts = previousPts + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0);
|
||||||
|
|
||||||
|
previousPts = pts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException {
|
private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException {
|
||||||
streamer.writeAudioHeader();
|
streamer.writeHeader();
|
||||||
|
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
OutputTask task = outputTasks.take();
|
OutputTask task = outputTasks.take();
|
||||||
@ -118,8 +183,6 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
thread = new Thread(() -> {
|
thread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
encode();
|
encode();
|
||||||
} catch (ConfigurationException | AudioCaptureForegroundException e) {
|
|
||||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Ln.e("Audio encoding error", e);
|
Ln.e("Audio encoding error", e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -157,8 +220,34 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void startWorkaroundAndroid11() {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
// Android 11 requires Apps to be at foreground to record audio.
|
||||||
|
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
||||||
|
// But Scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
||||||
|
// shell ("com.android.shell").
|
||||||
|
// If there is an Activity from Android shell running at foreground, then the permission system will believe Scrcpy is also in the
|
||||||
|
// foreground.
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||||
|
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||||
|
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||||
|
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
||||||
|
// Wait for activity to start
|
||||||
|
SystemClock.sleep(150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void stopWorkaroundAndroid11() {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.M)
|
@TargetApi(Build.VERSION_CODES.M)
|
||||||
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
|
public void encode() throws IOException {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||||
Ln.w("Audio disabled: it is not supported before Android 11");
|
Ln.w("Audio disabled: it is not supported before Android 11");
|
||||||
streamer.writeDisableStream(false);
|
streamer.writeDisableStream(false);
|
||||||
@ -166,9 +255,11 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
MediaCodec mediaCodec = null;
|
MediaCodec mediaCodec = null;
|
||||||
AudioCapture capture = new AudioCapture();
|
AudioRecord recorder = null;
|
||||||
|
|
||||||
boolean mediaCodecStarted = false;
|
boolean mediaCodecStarted = false;
|
||||||
|
boolean recorderStarted = false;
|
||||||
|
boolean configurationError = false;
|
||||||
try {
|
try {
|
||||||
Codec codec = streamer.getCodec();
|
Codec codec = streamer.getCodec();
|
||||||
mediaCodec = createMediaCodec(codec, encoderName);
|
mediaCodec = createMediaCodec(codec, encoderName);
|
||||||
@ -180,13 +271,26 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
|
||||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||||
|
|
||||||
capture.start();
|
startWorkaroundAndroid11();
|
||||||
|
try {
|
||||||
|
recorder = createAudioRecord();
|
||||||
|
recorder.startRecording();
|
||||||
|
} catch (UnsupportedOperationException e) {
|
||||||
|
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
|
Ln.e("Failed to start audio capture");
|
||||||
|
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
|
||||||
|
throw new ConfigurationException("Unsupported audio capture");
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
stopWorkaroundAndroid11();
|
||||||
|
}
|
||||||
|
recorderStarted = true;
|
||||||
|
|
||||||
final MediaCodec mediaCodecRef = mediaCodec;
|
final MediaCodec mediaCodecRef = mediaCodec;
|
||||||
final AudioCapture captureRef = capture;
|
final AudioRecord recorderRef = recorder;
|
||||||
inputThread = new Thread(() -> {
|
inputThread = new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
inputThread(mediaCodecRef, captureRef);
|
inputThread(mediaCodecRef, recorderRef);
|
||||||
} catch (IOException | InterruptedException e) {
|
} catch (IOException | InterruptedException e) {
|
||||||
Ln.e("Audio capture error", e);
|
Ln.e("Audio capture error", e);
|
||||||
} finally {
|
} finally {
|
||||||
@ -216,14 +320,15 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
|
|
||||||
waitEnded();
|
waitEnded();
|
||||||
} catch (ConfigurationException e) {
|
} catch (ConfigurationException e) {
|
||||||
// Notify the error to make scrcpy exit
|
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||||
streamer.writeDisableStream(true);
|
// Notify the error to scrcpy to make it exit
|
||||||
throw e;
|
configurationError = true;
|
||||||
} catch (Throwable e) {
|
|
||||||
// Notify the client that the audio could not be captured
|
|
||||||
streamer.writeDisableStream(false);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
} finally {
|
||||||
|
if (!recorderStarted) {
|
||||||
|
// Notify the client that the audio could not be captured
|
||||||
|
streamer.writeDisableStream(configurationError);
|
||||||
|
}
|
||||||
|
|
||||||
// Cleanup everything (either at the end or on error at any step of the initialization)
|
// Cleanup everything (either at the end or on error at any step of the initialization)
|
||||||
if (mediaCodecThread != null) {
|
if (mediaCodecThread != null) {
|
||||||
Looper looper = mediaCodecThread.getLooper();
|
Looper looper = mediaCodecThread.getLooper();
|
||||||
@ -259,8 +364,11 @@ public final class AudioEncoder implements AsyncProcessor {
|
|||||||
}
|
}
|
||||||
mediaCodec.release();
|
mediaCodec.release();
|
||||||
}
|
}
|
||||||
if (capture != null) {
|
if (recorder != null) {
|
||||||
capture.stop();
|
if (recorderStarted) {
|
||||||
|
recorder.stop();
|
||||||
|
}
|
||||||
|
recorder.release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
package com.genymobile.scrcpy;
|
|
||||||
|
|
||||||
import android.media.MediaCodec;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.ByteBuffer;
|
|
||||||
|
|
||||||
public final class AudioRawRecorder implements AsyncProcessor {
|
|
||||||
|
|
||||||
private final Streamer streamer;
|
|
||||||
|
|
||||||
private Thread thread;
|
|
||||||
|
|
||||||
private static final int READ_MS = 5; // milliseconds
|
|
||||||
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
|
|
||||||
|
|
||||||
public AudioRawRecorder(Streamer streamer) {
|
|
||||||
this.streamer = streamer;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void record() throws IOException, AudioCaptureForegroundException {
|
|
||||||
final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
|
|
||||||
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
|
||||||
|
|
||||||
AudioCapture capture = new AudioCapture();
|
|
||||||
try {
|
|
||||||
capture.start();
|
|
||||||
|
|
||||||
streamer.writeAudioHeader();
|
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
|
||||||
buffer.position(0);
|
|
||||||
int r = capture.read(buffer, READ_SIZE, bufferInfo);
|
|
||||||
if (r < 0) {
|
|
||||||
throw new IOException("Could not read audio: " + r);
|
|
||||||
}
|
|
||||||
buffer.limit(r);
|
|
||||||
|
|
||||||
streamer.writePacket(buffer, bufferInfo);
|
|
||||||
}
|
|
||||||
} catch (Throwable e) {
|
|
||||||
// Notify the client that the audio could not be captured
|
|
||||||
streamer.writeDisableStream(false);
|
|
||||||
throw e;
|
|
||||||
} finally {
|
|
||||||
capture.stop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void start() {
|
|
||||||
thread = new Thread(() -> {
|
|
||||||
try {
|
|
||||||
record();
|
|
||||||
} catch (AudioCaptureForegroundException e) {
|
|
||||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
|
||||||
} catch (IOException e) {
|
|
||||||
Ln.e("Audio recording error", e);
|
|
||||||
} finally {
|
|
||||||
Ln.d("Audio recorder stopped");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
thread.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void stop() {
|
|
||||||
if (thread != null) {
|
|
||||||
thread.interrupt();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void join() throws InterruptedException {
|
|
||||||
if (thread != null) {
|
|
||||||
thread.join();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,7 +14,7 @@ import java.util.concurrent.Executors;
|
|||||||
import java.util.concurrent.ScheduledExecutorService;
|
import java.util.concurrent.ScheduledExecutorService;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
public class Controller implements AsyncProcessor {
|
public class Controller {
|
||||||
|
|
||||||
private static final int DEFAULT_DEVICE_ID = 0;
|
private static final int DEFAULT_DEVICE_ID = 0;
|
||||||
|
|
||||||
|
@ -110,11 +110,6 @@ public final class DesktopConnection implements Closeable {
|
|||||||
videoSocket.shutdownInput();
|
videoSocket.shutdownInput();
|
||||||
videoSocket.shutdownOutput();
|
videoSocket.shutdownOutput();
|
||||||
videoSocket.close();
|
videoSocket.close();
|
||||||
if (audioSocket != null) {
|
|
||||||
audioSocket.shutdownInput();
|
|
||||||
audioSocket.shutdownOutput();
|
|
||||||
audioSocket.close();
|
|
||||||
}
|
|
||||||
if (controlSocket != null) {
|
if (controlSocket != null) {
|
||||||
controlSocket.shutdownInput();
|
controlSocket.shutdownInput();
|
||||||
controlSocket.shutdownOutput();
|
controlSocket.shutdownOutput();
|
||||||
@ -122,14 +117,18 @@ public final class DesktopConnection implements Closeable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendDeviceMeta(String deviceName) throws IOException {
|
public void sendDeviceMeta(String deviceName, int width, int height) throws IOException {
|
||||||
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH];
|
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
|
||||||
|
|
||||||
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
|
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
|
||||||
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
|
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
|
||||||
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
|
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
|
||||||
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
|
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
|
||||||
|
|
||||||
|
buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8);
|
||||||
|
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
|
||||||
|
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
|
||||||
|
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
|
||||||
IO.writeFully(videoFd, buffer, 0, buffer.length);
|
IO.writeFully(videoFd, buffer, 0, buffer.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,28 +39,28 @@ public final class Ln {
|
|||||||
public static void v(String message) {
|
public static void v(String message) {
|
||||||
if (isEnabled(Level.VERBOSE)) {
|
if (isEnabled(Level.VERBOSE)) {
|
||||||
Log.v(TAG, message);
|
Log.v(TAG, message);
|
||||||
System.out.print(PREFIX + "VERBOSE: " + message + '\n');
|
System.out.println(PREFIX + "VERBOSE: " + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void d(String message) {
|
public static void d(String message) {
|
||||||
if (isEnabled(Level.DEBUG)) {
|
if (isEnabled(Level.DEBUG)) {
|
||||||
Log.d(TAG, message);
|
Log.d(TAG, message);
|
||||||
System.out.print(PREFIX + "DEBUG: " + message + '\n');
|
System.out.println(PREFIX + "DEBUG: " + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void i(String message) {
|
public static void i(String message) {
|
||||||
if (isEnabled(Level.INFO)) {
|
if (isEnabled(Level.INFO)) {
|
||||||
Log.i(TAG, message);
|
Log.i(TAG, message);
|
||||||
System.out.print(PREFIX + "INFO: " + message + '\n');
|
System.out.println(PREFIX + "INFO: " + message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void w(String message, Throwable throwable) {
|
public static void w(String message, Throwable throwable) {
|
||||||
if (isEnabled(Level.WARN)) {
|
if (isEnabled(Level.WARN)) {
|
||||||
Log.w(TAG, message, throwable);
|
Log.w(TAG, message, throwable);
|
||||||
System.err.print(PREFIX + "WARN: " + message + '\n');
|
System.out.println(PREFIX + "WARN: " + message);
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
}
|
}
|
||||||
@ -74,7 +74,7 @@ public final class Ln {
|
|||||||
public static void e(String message, Throwable throwable) {
|
public static void e(String message, Throwable throwable) {
|
||||||
if (isEnabled(Level.ERROR)) {
|
if (isEnabled(Level.ERROR)) {
|
||||||
Log.e(TAG, message, throwable);
|
Log.e(TAG, message, throwable);
|
||||||
System.err.print(PREFIX + "ERROR: " + message + "\n");
|
System.out.println(PREFIX + "ERROR: " + message);
|
||||||
if (throwable != null) {
|
if (throwable != null) {
|
||||||
throwable.printStackTrace();
|
throwable.printStackTrace();
|
||||||
}
|
}
|
||||||
|
@ -13,7 +13,7 @@ public class Options {
|
|||||||
private VideoCodec videoCodec = VideoCodec.H264;
|
private VideoCodec videoCodec = VideoCodec.H264;
|
||||||
private AudioCodec audioCodec = AudioCodec.OPUS;
|
private AudioCodec audioCodec = AudioCodec.OPUS;
|
||||||
private int videoBitRate = 8000000;
|
private int videoBitRate = 8000000;
|
||||||
private int audioBitRate = 128000;
|
private int audioBitRate = 196000;
|
||||||
private int maxFps;
|
private int maxFps;
|
||||||
private int lockVideoOrientation = -1;
|
private int lockVideoOrientation = -1;
|
||||||
private boolean tunnelForward;
|
private boolean tunnelForward;
|
||||||
@ -40,7 +40,7 @@ public class Options {
|
|||||||
private boolean sendDeviceMeta = true; // send device name and size
|
private boolean sendDeviceMeta = true; // send device name and size
|
||||||
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||||
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
|
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
|
||||||
private boolean sendCodecMeta = true; // write the codec metadata before the stream
|
private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream
|
||||||
|
|
||||||
public Ln.Level getLogLevel() {
|
public Ln.Level getLogLevel() {
|
||||||
return logLevel;
|
return logLevel;
|
||||||
@ -282,11 +282,11 @@ public class Options {
|
|||||||
this.sendDummyByte = sendDummyByte;
|
this.sendDummyByte = sendDummyByte;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean getSendCodecMeta() {
|
public boolean getSendCodecId() {
|
||||||
return sendCodecMeta;
|
return sendCodecId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSendCodecMeta(boolean sendCodecMeta) {
|
public void setSendCodecId(boolean sendCodecId) {
|
||||||
this.sendCodecMeta = sendCodecMeta;
|
this.sendCodecId = sendCodecId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -66,7 +66,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||||||
IBinder display = createDisplay();
|
IBinder display = createDisplay();
|
||||||
device.setRotationListener(this);
|
device.setRotationListener(this);
|
||||||
|
|
||||||
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize());
|
streamer.writeHeader();
|
||||||
|
|
||||||
boolean alive;
|
boolean alive;
|
||||||
try {
|
try {
|
||||||
|
@ -5,7 +5,6 @@ import android.os.BatteryManager;
|
|||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
@ -92,42 +91,34 @@ public final class Server {
|
|||||||
Workarounds.fillAppInfo();
|
Workarounds.fillAppInfo();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
|
Controller controller = null;
|
||||||
|
AudioEncoder audioEncoder = null;
|
||||||
|
|
||||||
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
|
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
|
||||||
if (options.getSendDeviceMeta()) {
|
if (options.getSendDeviceMeta()) {
|
||||||
connection.sendDeviceMeta(Device.getDeviceName());
|
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||||
|
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (control) {
|
if (control) {
|
||||||
Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||||
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
|
controller.start();
|
||||||
asyncProcessors.add(controller);
|
|
||||||
|
final Controller controllerRef = controller;
|
||||||
|
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio) {
|
if (audio) {
|
||||||
AudioCodec audioCodec = options.getAudioCodec();
|
Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(),
|
||||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(),
|
|
||||||
options.getSendFrameMeta());
|
options.getSendFrameMeta());
|
||||||
AsyncProcessor audioRecorder;
|
audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder());
|
||||||
if (audioCodec == AudioCodec.RAW) {
|
audioEncoder.start();
|
||||||
audioRecorder = new AudioRawRecorder(audioStreamer);
|
|
||||||
} else {
|
|
||||||
audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
|
|
||||||
options.getAudioEncoder());
|
|
||||||
}
|
|
||||||
asyncProcessors.add(audioRecorder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
|
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
|
||||||
options.getSendFrameMeta());
|
options.getSendFrameMeta());
|
||||||
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
|
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
|
||||||
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
|
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
|
||||||
|
|
||||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
|
||||||
asyncProcessor.start();
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// synchronous
|
// synchronous
|
||||||
screenEncoder.streamScreen();
|
screenEncoder.streamScreen();
|
||||||
@ -140,14 +131,20 @@ public final class Server {
|
|||||||
} finally {
|
} finally {
|
||||||
Ln.d("Screen streaming stopped");
|
Ln.d("Screen streaming stopped");
|
||||||
initThread.interrupt();
|
initThread.interrupt();
|
||||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
if (audioEncoder != null) {
|
||||||
asyncProcessor.stop();
|
audioEncoder.stop();
|
||||||
|
}
|
||||||
|
if (controller != null) {
|
||||||
|
controller.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
initThread.join();
|
initThread.join();
|
||||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
if (audioEncoder != null) {
|
||||||
asyncProcessor.join();
|
audioEncoder.join();
|
||||||
|
}
|
||||||
|
if (controller != null) {
|
||||||
|
controller.join();
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
// ignore
|
// ignore
|
||||||
@ -314,9 +311,9 @@ public final class Server {
|
|||||||
boolean sendDummyByte = Boolean.parseBoolean(value);
|
boolean sendDummyByte = Boolean.parseBoolean(value);
|
||||||
options.setSendDummyByte(sendDummyByte);
|
options.setSendDummyByte(sendDummyByte);
|
||||||
break;
|
break;
|
||||||
case "send_codec_meta":
|
case "send_codec_id":
|
||||||
boolean sendCodecMeta = Boolean.parseBoolean(value);
|
boolean sendCodecId = Boolean.parseBoolean(value);
|
||||||
options.setSendCodecMeta(sendCodecMeta);
|
options.setSendCodecId(sendCodecId);
|
||||||
break;
|
break;
|
||||||
case "raw_video_stream":
|
case "raw_video_stream":
|
||||||
boolean rawVideoStream = Boolean.parseBoolean(value);
|
boolean rawVideoStream = Boolean.parseBoolean(value);
|
||||||
@ -324,7 +321,7 @@ public final class Server {
|
|||||||
options.setSendDeviceMeta(false);
|
options.setSendDeviceMeta(false);
|
||||||
options.setSendFrameMeta(false);
|
options.setSendFrameMeta(false);
|
||||||
options.setSendDummyByte(false);
|
options.setSendDummyByte(false);
|
||||||
options.setSendCodecMeta(false);
|
options.setSendCodecId(false);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
@ -15,23 +15,24 @@ public final class Streamer {
|
|||||||
|
|
||||||
private final FileDescriptor fd;
|
private final FileDescriptor fd;
|
||||||
private final Codec codec;
|
private final Codec codec;
|
||||||
private final boolean sendCodecMeta;
|
private final boolean sendCodecId;
|
||||||
private final boolean sendFrameMeta;
|
private final boolean sendFrameMeta;
|
||||||
|
|
||||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||||
|
|
||||||
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) {
|
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
|
||||||
this.fd = fd;
|
this.fd = fd;
|
||||||
this.codec = codec;
|
this.codec = codec;
|
||||||
this.sendCodecMeta = sendCodecMeta;
|
this.sendCodecId = sendCodecId;
|
||||||
this.sendFrameMeta = sendFrameMeta;
|
this.sendFrameMeta = sendFrameMeta;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Codec getCodec() {
|
public Codec getCodec() {
|
||||||
return codec;
|
return codec;
|
||||||
}
|
}
|
||||||
public void writeAudioHeader() throws IOException {
|
|
||||||
if (sendCodecMeta) {
|
public void writeHeader() throws IOException {
|
||||||
|
if (sendCodecId) {
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||||
buffer.putInt(codec.getId());
|
buffer.putInt(codec.getId());
|
||||||
buffer.flip();
|
buffer.flip();
|
||||||
@ -39,17 +40,6 @@ public final class Streamer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeVideoHeader(Size videoSize) throws IOException {
|
|
||||||
if (sendCodecMeta) {
|
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(12);
|
|
||||||
buffer.putInt(codec.getId());
|
|
||||||
buffer.putInt(videoSize.getWidth());
|
|
||||||
buffer.putInt(videoSize.getHeight());
|
|
||||||
buffer.flip();
|
|
||||||
IO.writeFully(fd, buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void writeDisableStream(boolean error) throws IOException {
|
public void writeDisableStream(boolean error) throws IOException {
|
||||||
// Writing a specific code as codec-id means that the device disables the stream
|
// Writing a specific code as codec-id means that the device disables the stream
|
||||||
// code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only
|
// code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only
|
||||||
@ -61,34 +51,27 @@ public final class Streamer {
|
|||||||
IO.writeFully(fd, code, 0, code.length);
|
IO.writeFully(fd, code, 0, code.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException {
|
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||||
if (config && codec == AudioCodec.OPUS) {
|
if (codec == AudioCodec.OPUS) {
|
||||||
fixOpusConfigPacket(buffer);
|
fixOpusConfigPacket(codecBuffer, bufferInfo);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendFrameMeta) {
|
if (sendFrameMeta) {
|
||||||
writeFrameMeta(fd, buffer.remaining(), pts, config, keyFrame);
|
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||||
}
|
}
|
||||||
|
|
||||||
IO.writeFully(fd, buffer);
|
IO.writeFully(fd, codecBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
||||||
long pts = bufferInfo.presentationTimeUs;
|
|
||||||
boolean config = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
|
||||||
boolean keyFrame = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0;
|
|
||||||
writePacket(codecBuffer, pts, config, keyFrame);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void writeFrameMeta(FileDescriptor fd, int packetSize, long pts, boolean config, boolean keyFrame) throws IOException {
|
|
||||||
headerBuffer.clear();
|
headerBuffer.clear();
|
||||||
|
|
||||||
long ptsAndFlags;
|
long ptsAndFlags;
|
||||||
if (config) {
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
|
||||||
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
|
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
|
||||||
} else {
|
} else {
|
||||||
ptsAndFlags = pts;
|
ptsAndFlags = bufferInfo.presentationTimeUs;
|
||||||
if (keyFrame) {
|
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
|
||||||
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
|
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -99,7 +82,7 @@ public final class Streamer {
|
|||||||
IO.writeFully(fd, headerBuffer);
|
IO.writeFully(fd, headerBuffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void fixOpusConfigPacket(ByteBuffer buffer) throws IOException {
|
private static void fixOpusConfigPacket(ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
|
||||||
// Here is an example of the config packet received for an OPUS stream:
|
// Here is an example of the config packet received for an OPUS stream:
|
||||||
//
|
//
|
||||||
// 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........|
|
// 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........|
|
||||||
@ -116,6 +99,11 @@ public final class Streamer {
|
|||||||
//
|
//
|
||||||
// <https://developer.android.com/reference/android/media/MediaCodec#CSD>
|
// <https://developer.android.com/reference/android/media/MediaCodec#CSD>
|
||||||
|
|
||||||
|
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
|
||||||
|
if (!isConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (buffer.remaining() < 16) {
|
if (buffer.remaining() < 16) {
|
||||||
throw new IOException("Not enough data in OPUS config packet");
|
throw new IOException("Not enough data in OPUS config packet");
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user