Compare commits
2 Commits
macos_open
...
refactor-e
Author | SHA1 | Date | |
---|---|---|---|
b0a4e6df25 | |||
545a8a8f32 |
@ -2,16 +2,57 @@
|
||||
|
||||
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
|
||||
|
||||
There are two main branches:
|
||||
- `master`: contains the latest release. It is the home page of the project on
|
||||
GitHub.
|
||||
- `dev`: the current development branch. Every commit present in `dev` will be
|
||||
in the next release.
|
||||
### `master`
|
||||
|
||||
The `master` branch concerns the latest release, and is the home page of the
|
||||
project on GitHub.
|
||||
|
||||
|
||||
### `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`
|
||||
branch.
|
||||
@ -28,8 +69,6 @@ the following files to a directory accessible from your `PATH`:
|
||||
- `AdbWinApi.dll`
|
||||
- `AdbWinUsbApi.dll`
|
||||
|
||||
It is also available in scrcpy releases.
|
||||
|
||||
The client requires [FFmpeg] and [LibSDL2]. Just follow the instructions.
|
||||
|
||||
[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
|
||||
sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libswresample-dev libusb-1.0-0-dev
|
||||
libusb-1.0-0-dev
|
||||
|
||||
# server build dependencies
|
||||
sudo apt install openjdk-11-jdk
|
||||
@ -233,10 +272,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v2.0`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2`</sub>
|
||||
- [`scrcpy-server-v1.25`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
@ -275,8 +314,7 @@ This installs several files:
|
||||
- `/usr/local/share/zsh/site-functions/_scrcpy` (zsh completion)
|
||||
- `/usr/local/share/bash-completion/completions/scrcpy` (bash completion)
|
||||
|
||||
You can then run `scrcpy`.
|
||||
|
||||
You can then [run](README.md#run) `scrcpy`.
|
||||
|
||||
### Uninstall
|
||||
|
143
FAQ.md
143
FAQ.md
@ -7,7 +7,7 @@ Here are the common reported problems and their status.
|
||||
If you encounter any error, the first step is to upgrade to the latest version.
|
||||
|
||||
|
||||
## `adb` and USB issues
|
||||
## `adb` issues
|
||||
|
||||
`scrcpy` execute `adb` commands to initialize the connection with the device. If
|
||||
`adb` fails, then scrcpy will not work.
|
||||
@ -133,21 +133,6 @@ Try with another USB cable or plug it into another USB port. See [#281] and
|
||||
[#283]: https://github.com/Genymobile/scrcpy/issues/283
|
||||
|
||||
|
||||
## HID/OTG issues on Windows
|
||||
|
||||
On Windows, if `scrcpy --otg` (or `--hid-keyboard`/`--hid-mouse`) results in:
|
||||
|
||||
> ERROR: Could not find any USB device
|
||||
|
||||
(or if only unrelated USB devices are detected), there might be drivers issues.
|
||||
|
||||
Please read [#3654], in particular [this comment][#3654-comment1] and [the next
|
||||
one][#3654-comment2].
|
||||
|
||||
[#3654]: https://github.com/Genymobile/scrcpy/issues/3654
|
||||
[#3654-comment1]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369278232
|
||||
[#3654-comment2]: https://github.com/Genymobile/scrcpy/issues/3654#issuecomment-1369295011
|
||||
|
||||
|
||||
## Control issues
|
||||
|
||||
@ -168,7 +153,8 @@ The default text injection method is [limited to ASCII characters][text-input].
|
||||
A trick allows to also inject some [accented characters][accented-characters],
|
||||
but that's all. See [#37].
|
||||
|
||||
Since scrcpy v1.20, it is possible to simulate a [physical keyboard][hid] (HID).
|
||||
Since scrcpy v1.20 on Linux, it is possible to simulate a [physical
|
||||
keyboard][hid] (HID).
|
||||
|
||||
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
|
||||
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
|
||||
@ -178,6 +164,32 @@ Since scrcpy v1.20, it is possible to simulate a [physical keyboard][hid] (HID).
|
||||
|
||||
## 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
|
||||
|
||||
By default, SDL uses x11 on Linux. The [video driver] can be changed via the
|
||||
@ -212,15 +224,102 @@ As a workaround, [disable "Block compositing"][kwin].
|
||||
|
||||
### 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]
|
||||
java.lang.IllegalStateException
|
||||
at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
||||
scrcpy -m 1920
|
||||
scrcpy -m 1024
|
||||
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
|
||||
|
2
LICENSE
2
LICENSE
@ -188,7 +188,7 @@
|
||||
identification within third-party archives.
|
||||
|
||||
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");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -2,35 +2,28 @@ _scrcpy() {
|
||||
local cur prev words cword
|
||||
local opts="
|
||||
--always-on-top
|
||||
--audio-bit-rate=
|
||||
--audio-buffer=
|
||||
--audio-codec=
|
||||
--audio-codec-options=
|
||||
--audio-encoder=
|
||||
--audio-output-buffer=
|
||||
-b --video-bit-rate=
|
||||
-b --bit-rate=
|
||||
--codec-options=
|
||||
--crop=
|
||||
-d --select-usb
|
||||
--disable-screensaver
|
||||
--display=
|
||||
--display-buffer=
|
||||
-e --select-tcpip
|
||||
--encoder=
|
||||
--force-adb-forward
|
||||
--forward-all-clicks
|
||||
-f --fullscreen
|
||||
-K --hid-keyboard
|
||||
-h --help
|
||||
--legacy-paste
|
||||
--list-displays
|
||||
--list-encoders
|
||||
--lock-video-orientation
|
||||
--lock-video-orientation=
|
||||
--max-fps=
|
||||
-M --hid-mouse
|
||||
-m --max-size=
|
||||
--no-audio
|
||||
--no-cleanup
|
||||
--no-clipboard-autosync
|
||||
--no-clipboard-on-error
|
||||
--no-downsize-on-error
|
||||
-n --no-control
|
||||
-N --no-display
|
||||
@ -47,7 +40,6 @@ _scrcpy() {
|
||||
-r --record=
|
||||
--record-format=
|
||||
--render-driver=
|
||||
--require-audio
|
||||
--rotation=
|
||||
-s --serial=
|
||||
--shortcut-mod=
|
||||
@ -61,9 +53,6 @@ _scrcpy() {
|
||||
--v4l2-sink=
|
||||
-V --verbosity=
|
||||
-v --version
|
||||
--video-codec=
|
||||
--video-codec-options=
|
||||
--video-encoder=
|
||||
-w --stay-awake
|
||||
--window-borderless
|
||||
--window-title=
|
||||
@ -75,14 +64,6 @@ _scrcpy() {
|
||||
_init_completion -s || return
|
||||
|
||||
case "$prev" in
|
||||
--video-codec)
|
||||
COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--audio-codec)
|
||||
COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--lock-video-orientation)
|
||||
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
|
||||
return
|
||||
@ -117,26 +98,20 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
|
||||
return
|
||||
;;
|
||||
--audio-bit-rate \
|
||||
|--audio-buffer \
|
||||
|-b|--video-bit-rate \
|
||||
|--audio-codec-options \
|
||||
|--audio-encoder \
|
||||
|--audio-output-buffer \
|
||||
-b|--bitrate \
|
||||
|--codec-options \
|
||||
|--crop \
|
||||
|--display \
|
||||
|--display-buffer \
|
||||
|--encoder \
|
||||
|--max-fps \
|
||||
|-m|--max-size \
|
||||
|-p|--port \
|
||||
|--push-target \
|
||||
|--rotation \
|
||||
|--tunnel-host \
|
||||
|--tunnel-port \
|
||||
|--v4l2-buffer \
|
||||
|--v4l2-sink \
|
||||
|--video-codec-options \
|
||||
|--video-encoder \
|
||||
|--tcpip \
|
||||
|--window-*)
|
||||
# Option accepting an argument, but nothing to auto-complete
|
||||
|
@ -5,7 +5,7 @@ Comment=Display and control your Android device
|
||||
# For some users, the PATH or ADB environment variables are set from the shell
|
||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||
# environment correctly initialized.
|
||||
Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter to quit...'"
|
||||
Exec=/bin/bash --norc --noprofile -i -c '"$SHELL" -i -c scrcpy || read -p "Press any key to quit..."'
|
||||
Icon=scrcpy
|
||||
Terminal=true
|
||||
Type=Application
|
||||
|
@ -5,7 +5,7 @@ Comment=Display and control your Android device
|
||||
# For some users, the PATH or ADB environment variables are set from the shell
|
||||
# startup file, like .bashrc or .zshrc… Run an interactive shell to get
|
||||
# environment correctly initialized.
|
||||
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy"
|
||||
Exec=/bin/sh -c '"$SHELL" -i -c scrcpy'
|
||||
Icon=scrcpy
|
||||
Terminal=false
|
||||
Type=Application
|
||||
|
@ -9,32 +9,25 @@ local arguments
|
||||
|
||||
arguments=(
|
||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
||||
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
|
||||
'--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-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
|
||||
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
|
||||
{-b,--bit-rate=}'[Encode the video at the given bit-rate]'
|
||||
'--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]'
|
||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
||||
{-d,--select-usb}'[Use USB device]'
|
||||
'--disable-screensaver[Disable screensaver while scrcpy is running]'
|
||||
'--display=[Specify the display id to mirror]'
|
||||
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
|
||||
{-e,--select-tcpip}'[Use TCP/IP device]'
|
||||
'--encoder=[Use a specific MediaCodec encoder \(must be a H.264 encoder\)]'
|
||||
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
|
||||
'--forward-all-clicks[Forward clicks to device]'
|
||||
{-f,--fullscreen}'[Start in fullscreen]'
|
||||
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
|
||||
{-h,--help}'[Print the help]'
|
||||
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
|
||||
'--list-displays[List displays available on the device]'
|
||||
'--list-encoders[List video and audio encoders available on the device]'
|
||||
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)'
|
||||
'--max-fps=[Limit the frame rate of screen capture]'
|
||||
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
|
||||
{-m,--max-size=}'[Limit both the width and height of the video to value]'
|
||||
'--no-audio[Disable audio forwarding]'
|
||||
'--no-cleanup[Disable device cleanup actions on exit]'
|
||||
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
|
||||
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
|
||||
@ -53,7 +46,6 @@ arguments=(
|
||||
{-r,--record=}'[Record screen to file]:record file:_files'
|
||||
'--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)'
|
||||
'--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)'
|
||||
{-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)'
|
||||
@ -66,9 +58,6 @@ arguments=(
|
||||
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
|
||||
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
|
||||
{-v,--version}'[Print the version of scrcpy]'
|
||||
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
|
||||
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
|
||||
'--video-encoder=[Use a specific MediaCodec video encoder]'
|
||||
{-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]'
|
||||
'--window-borderless[Disable window decorations \(display borderless window\)]'
|
||||
'--window-title=[Set a custom window title]'
|
||||
|
@ -4,17 +4,14 @@ src = [
|
||||
'src/adb/adb_device.c',
|
||||
'src/adb/adb_parser.c',
|
||||
'src/adb/adb_tunnel.c',
|
||||
'src/audio_player.c',
|
||||
'src/cli.c',
|
||||
'src/clock.c',
|
||||
'src/compat.c',
|
||||
'src/control_msg.c',
|
||||
'src/controller.c',
|
||||
'src/decoder.c',
|
||||
'src/delay_buffer.c',
|
||||
'src/demuxer.c',
|
||||
'src/device_msg.c',
|
||||
'src/display.c',
|
||||
'src/icon.c',
|
||||
'src/file_pusher.c',
|
||||
'src/fps_counter.c',
|
||||
@ -24,23 +21,18 @@ src = [
|
||||
'src/mouse_inject.c',
|
||||
'src/opengl.c',
|
||||
'src/options.c',
|
||||
'src/packet_merger.c',
|
||||
'src/receiver.c',
|
||||
'src/recorder.c',
|
||||
'src/scrcpy.c',
|
||||
'src/screen.c',
|
||||
'src/server.c',
|
||||
'src/version.c',
|
||||
'src/trait/frame_source.c',
|
||||
'src/trait/packet_source.c',
|
||||
'src/video_buffer.c',
|
||||
'src/util/acksync.c',
|
||||
'src/util/average.c',
|
||||
'src/util/bytebuf.c',
|
||||
'src/util/file.c',
|
||||
'src/util/intmap.c',
|
||||
'src/util/intr.c',
|
||||
'src/util/log.c',
|
||||
'src/util/memory.c',
|
||||
'src/util/net.c',
|
||||
'src/util/net_intr.c',
|
||||
'src/util/process.c',
|
||||
@ -106,7 +98,6 @@ if not crossbuild_windows
|
||||
dependency('libavformat', version: '>= 57.33'),
|
||||
dependency('libavcodec', version: '>= 57.37'),
|
||||
dependency('libavutil'),
|
||||
dependency('libswresample'),
|
||||
dependency('sdl2', version: '>= 2.0.5'),
|
||||
]
|
||||
|
||||
@ -137,19 +128,24 @@ else
|
||||
ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin'
|
||||
ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include'
|
||||
|
||||
# ffmpeg versions are different for win32 and win64 builds
|
||||
ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec')
|
||||
ffmpeg_avformat = meson.get_cross_property('ffmpeg_avformat')
|
||||
ffmpeg_avutil = meson.get_cross_property('ffmpeg_avutil')
|
||||
|
||||
ffmpeg = declare_dependency(
|
||||
dependencies: [
|
||||
cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir),
|
||||
cc.find_library('avformat-60', dirs: ffmpeg_bin_dir),
|
||||
cc.find_library('avutil-58', dirs: ffmpeg_bin_dir),
|
||||
cc.find_library('swresample-4', dirs: ffmpeg_bin_dir),
|
||||
cc.find_library(ffmpeg_avcodec, dirs: ffmpeg_bin_dir),
|
||||
cc.find_library(ffmpeg_avformat, dirs: ffmpeg_bin_dir),
|
||||
cc.find_library(ffmpeg_avutil, dirs: ffmpeg_bin_dir),
|
||||
],
|
||||
include_directories: include_directories(ffmpeg_include_dir)
|
||||
)
|
||||
|
||||
prebuilt_libusb = meson.get_cross_property('prebuilt_libusb')
|
||||
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin'
|
||||
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include'
|
||||
prebuilt_libusb_root = meson.get_cross_property('prebuilt_libusb_root')
|
||||
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb
|
||||
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb_root + '/include'
|
||||
|
||||
libusb = declare_dependency(
|
||||
dependencies: [
|
||||
@ -177,7 +173,6 @@ check_functions = [
|
||||
'vasprintf',
|
||||
'nrand48',
|
||||
'jrand48',
|
||||
'reallocarray',
|
||||
]
|
||||
|
||||
foreach f : check_functions
|
||||
@ -205,6 +200,10 @@ conf.set('PORTABLE', get_option('portable'))
|
||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
|
||||
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
|
||||
|
||||
# the default video bitrate, in bits/second
|
||||
# overridden by option --bit-rate
|
||||
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
|
||||
|
||||
# run a server debugger and wait for a client to be attached
|
||||
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
|
||||
|
||||
@ -264,9 +263,8 @@ if get_option('buildtype') == 'debug'
|
||||
['test_binary', [
|
||||
'tests/test_binary.c',
|
||||
]],
|
||||
['test_bytebuf', [
|
||||
'tests/test_bytebuf.c',
|
||||
'src/util/bytebuf.c',
|
||||
['test_cbuf', [
|
||||
'tests/test_cbuf.c',
|
||||
]],
|
||||
['test_cli', [
|
||||
'tests/test_cli.c',
|
||||
@ -278,6 +276,10 @@ if get_option('buildtype') == 'debug'
|
||||
'src/util/strbuf.c',
|
||||
'src/util/term.c',
|
||||
]],
|
||||
['test_clock', [
|
||||
'tests/test_clock.c',
|
||||
'src/clock.c',
|
||||
]],
|
||||
['test_control_msg_serialize', [
|
||||
'tests/test_control_msg_serialize.c',
|
||||
'src/control_msg.c',
|
||||
@ -288,6 +290,9 @@ if get_option('buildtype') == 'debug'
|
||||
'tests/test_device_msg_deserialize.c',
|
||||
'src/device_msg.c',
|
||||
]],
|
||||
['test_queue', [
|
||||
'tests/test_queue.c',
|
||||
]],
|
||||
['test_strbuf', [
|
||||
'tests/test_strbuf.c',
|
||||
'src/util/strbuf.c',
|
||||
@ -297,18 +302,13 @@ if get_option('buildtype') == 'debug'
|
||||
'src/util/str.c',
|
||||
'src/util/strbuf.c',
|
||||
]],
|
||||
['test_vecdeque', [
|
||||
'tests/test_vecdeque.c',
|
||||
'src/util/memory.c',
|
||||
]],
|
||||
['test_vector', [
|
||||
'tests/test_vector.c',
|
||||
]],
|
||||
]
|
||||
|
||||
foreach t : tests
|
||||
sources = t[1] + ['src/compat.c']
|
||||
exe = executable(t[0], sources,
|
||||
exe = executable(t[0], t[1],
|
||||
include_directories: src_dir,
|
||||
dependencies: dependencies,
|
||||
c_args: ['-DSDL_MAIN_HANDLED', '-DSC_TEST'])
|
||||
|
@ -6,10 +6,10 @@ cd "$DIR"
|
||||
mkdir -p "$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
|
||||
SHA256SUM=5dd9c2be744c224fa3a7cbe30ba02d2cb378c763bd0f797a7e47e9f3156a5daa
|
||||
FILENAME=platform-tools_r33.0.3-windows.zip
|
||||
SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
45
app/prebuilt-deps/prepare-ffmpeg-win32.sh
Executable file
45
app/prebuilt-deps/prepare-ffmpeg-win32.sh
Executable file
@ -0,0 +1,45 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DIR"
|
||||
. common
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
DEP_DIR=ffmpeg-win32-4.3.1
|
||||
|
||||
FILENAME_SHARED=ffmpeg-4.3.1-win32-shared.zip
|
||||
SHA256SUM_SHARED=357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2
|
||||
|
||||
FILENAME_DEV=ffmpeg-4.3.1-win32-dev.zip
|
||||
SHA256SUM_DEV=230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
echo "$DEP_DIR" found
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_SHARED" \
|
||||
"$FILENAME_SHARED" "$SHA256SUM_SHARED"
|
||||
get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_DEV" \
|
||||
"$FILENAME_DEV" "$SHA256SUM_DEV"
|
||||
|
||||
mkdir "$DEP_DIR"
|
||||
cd "$DEP_DIR"
|
||||
|
||||
ZIP_PREFIX_SHARED=ffmpeg-4.3.1-win32-shared
|
||||
unzip "../$FILENAME_SHARED" \
|
||||
"$ZIP_PREFIX_SHARED"/bin/avutil-56.dll \
|
||||
"$ZIP_PREFIX_SHARED"/bin/avcodec-58.dll \
|
||||
"$ZIP_PREFIX_SHARED"/bin/avformat-58.dll \
|
||||
"$ZIP_PREFIX_SHARED"/bin/swresample-3.dll \
|
||||
"$ZIP_PREFIX_SHARED"/bin/swscale-5.dll
|
||||
|
||||
ZIP_PREFIX_DEV=ffmpeg-4.3.1-win32-dev
|
||||
unzip "../$FILENAME_DEV" \
|
||||
"$ZIP_PREFIX_DEV/include/*"
|
||||
|
||||
mv "$ZIP_PREFIX_SHARED"/* .
|
||||
mv "$ZIP_PREFIX_DEV"/* .
|
||||
rmdir "$ZIP_PREFIX_SHARED" "$ZIP_PREFIX_DEV"
|
36
app/prebuilt-deps/prepare-ffmpeg-win64.sh
Executable file
36
app/prebuilt-deps/prepare-ffmpeg-win64.sh
Executable file
@ -0,0 +1,36 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DIR"
|
||||
. common
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
VERSION=5.1.2
|
||||
DEP_DIR=ffmpeg-win64-$VERSION
|
||||
|
||||
FILENAME=ffmpeg-$VERSION-full_build-shared.7z
|
||||
SHA256SUM=d9eb97b72d7cfdae4d0f7eaea59ccffb8c364d67d88018ea715d5e2e193f00e9
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
echo "$DEP_DIR" found
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_file "https://github.com/GyanD/codexffmpeg/releases/download/$VERSION/$FILENAME" \
|
||||
"$FILENAME" "$SHA256SUM"
|
||||
|
||||
mkdir "$DEP_DIR"
|
||||
cd "$DEP_DIR"
|
||||
|
||||
ZIP_PREFIX=ffmpeg-$VERSION-full_build-shared
|
||||
7z x "../$FILENAME" \
|
||||
"$ZIP_PREFIX"/bin/avutil-57.dll \
|
||||
"$ZIP_PREFIX"/bin/avcodec-59.dll \
|
||||
"$ZIP_PREFIX"/bin/avformat-59.dll \
|
||||
"$ZIP_PREFIX"/bin/swresample-4.dll \
|
||||
"$ZIP_PREFIX"/bin/swscale-6.dll \
|
||||
"$ZIP_PREFIX"/include
|
||||
mv "$ZIP_PREFIX"/* .
|
||||
rmdir "$ZIP_PREFIX"
|
@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
DIR=$(dirname ${BASH_SOURCE[0]})
|
||||
cd "$DIR"
|
||||
. common
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
VERSION=6.0-scrcpy-2
|
||||
DEP_DIR="ffmpeg-$VERSION"
|
||||
|
||||
FILENAME="$DEP_DIR".7z
|
||||
SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
echo "$DEP_DIR" found
|
||||
exit 0
|
||||
fi
|
||||
|
||||
get_file "https://github.com/rom1v/scrcpy-deps/releases/download/$VERSION/$FILENAME" \
|
||||
"$FILENAME" "$SHA256SUM"
|
||||
|
||||
mkdir "$DEP_DIR"
|
||||
cd "$DEP_DIR"
|
||||
|
||||
ZIP_PREFIX=ffmpeg
|
||||
7z x "../$FILENAME"
|
||||
mv "$ZIP_PREFIX"/* .
|
||||
rmdir "$ZIP_PREFIX"
|
@ -22,12 +22,13 @@ get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME"
|
||||
mkdir "$DEP_DIR"
|
||||
cd "$DEP_DIR"
|
||||
|
||||
# include/ is the same in all folders of the archive
|
||||
7z x "../$FILENAME" \
|
||||
libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \
|
||||
libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \
|
||||
libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \
|
||||
libusb-1.0.26-binaries/libusb-MinGW-x64/include/
|
||||
|
||||
mv libusb-1.0.26-binaries/libusb-MinGW-Win32 .
|
||||
mv libusb-1.0.26-binaries/libusb-MinGW-x64 .
|
||||
mv libusb-1.0.26-binaries/libusb-MinGW-Win32/bin MinGW-Win32
|
||||
mv libusb-1.0.26-binaries/libusb-MinGW-x64/bin MinGW-x64
|
||||
mv libusb-1.0.26-binaries/libusb-MinGW-x64/include .
|
||||
rm -rf libusb-1.0.26-binaries
|
||||
|
@ -6,10 +6,10 @@ cd "$DIR"
|
||||
mkdir -p "$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
|
||||
SHA256SUM=fe899c8642caac2f180b1ee6f786857ddcaa0adc1fa82474312b09dd47d74712
|
||||
FILENAME=SDL2-devel-2.26.1-mingw.tar.gz
|
||||
SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "2.0"
|
||||
VALUE "ProductVersion", "1.25"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
97
app/scrcpy.1
97
app/scrcpy.1
@ -20,36 +20,14 @@ provides display and control of Android devices connected on USB (or over TCP/IP
|
||||
Make scrcpy window always on top (above other windows).
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-bit\-rate " value
|
||||
Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
.BI "\-b, \-\-bit\-rate " value
|
||||
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
|
||||
Default is 128K (128000).
|
||||
Default is 8000000.
|
||||
|
||||
.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
|
||||
.BI "\-\-audio\-output\-buffer ms
|
||||
Configure the size of the SDL audio output buffer (in milliseconds).
|
||||
|
||||
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
|
||||
|
||||
Default is 5.
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-codec " name
|
||||
Select an audio codec (opus, aac or raw).
|
||||
|
||||
Default is opus.
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
||||
Set a list of comma-separated key:type=value options for the device audio encoder.
|
||||
.BI "\-\-codec\-options " key[:type]=value[,...]
|
||||
Set a list of comma-separated key:type=value options for the device encoder.
|
||||
|
||||
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
||||
|
||||
@ -57,18 +35,6 @@ The list of possible codec options is available in the Android documentation
|
||||
.UR https://d.android.com/reference/android/media/MediaFormat
|
||||
.UE .
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-encoder " name
|
||||
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
|
||||
|
||||
The available encoders can be listed by \-\-list\-encoders.
|
||||
|
||||
.TP
|
||||
.BI "\-b, \-\-video\-bit\-rate " value
|
||||
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
|
||||
Default is 8M (8000000).
|
||||
|
||||
.TP
|
||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||
Crop the device screen on the server.
|
||||
@ -89,9 +55,10 @@ Disable screensaver while scrcpy is running.
|
||||
|
||||
.TP
|
||||
.BI "\-\-display " id
|
||||
Specify the device display id to mirror.
|
||||
Specify the display id to mirror.
|
||||
|
||||
The available display ids can be listed by \-\-list\-displays.
|
||||
The list of possible display ids can be listed by "adb shell dumpsys display"
|
||||
(search "mDisplayId=" in the output).
|
||||
|
||||
Default is 0.
|
||||
|
||||
@ -107,6 +74,10 @@ Use TCP/IP device (if there is exactly one, like adb -e).
|
||||
|
||||
Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
|
||||
|
||||
.TP
|
||||
.BI "\-\-encoder " name
|
||||
Use a specific MediaCodec encoder (must be a H.264 encoder).
|
||||
|
||||
.TP
|
||||
.B \-\-force\-adb\-forward
|
||||
Do not attempt to use "adb reverse" to connect to the device.
|
||||
@ -146,15 +117,7 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
|
||||
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
|
||||
|
||||
.TP
|
||||
.B \-\-list\-encoders
|
||||
List video and audio encoders available on the device.
|
||||
|
||||
.TP
|
||||
.B \-\-list\-displays
|
||||
List displays available on the device.
|
||||
|
||||
.TP
|
||||
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
|
||||
.BI "\-\-lock\-video\-orientation[=value]
|
||||
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
|
||||
|
||||
Default is "unlocked".
|
||||
@ -236,7 +199,7 @@ It may only work over USB.
|
||||
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
|
||||
.BI "\-p, \-\-port " port[:port]
|
||||
Set the TCP port (range) used by the client to listen.
|
||||
|
||||
Default is 27183:27199.
|
||||
@ -288,10 +251,6 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me
|
||||
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER
|
||||
.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
|
||||
.BI "\-\-rotation " value
|
||||
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
|
||||
@ -301,7 +260,7 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
|
||||
The device serial number. Mandatory only if several devices are connected to adb.
|
||||
|
||||
.TP
|
||||
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
|
||||
.BI "\-\-shortcut\-mod " key[+...]][,...]
|
||||
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
|
||||
|
||||
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
|
||||
@ -311,7 +270,7 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
|
||||
Default is "lalt,lsuper" (left-Alt or left-Super).
|
||||
|
||||
.TP
|
||||
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
|
||||
.BI "\-\-tcpip[=ip[:port]]
|
||||
Configure and reconnect the device over TCP/IP.
|
||||
|
||||
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).
|
||||
@ -364,28 +323,6 @@ Default is "info" for release builds, "debug" for debug builds.
|
||||
.B \-v, \-\-version
|
||||
Print the version of scrcpy.
|
||||
|
||||
.TP
|
||||
.BI "\-\-video\-codec " name
|
||||
Select a video codec (h264, h265 or av1).
|
||||
|
||||
Default is h264.
|
||||
|
||||
.TP
|
||||
.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
|
||||
Set a list of comma-separated key:type=value options for the device video encoder.
|
||||
|
||||
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
||||
|
||||
The list of possible codec options is available in the Android documentation
|
||||
.UR https://d.android.com/reference/android/media/MediaFormat
|
||||
.UE .
|
||||
|
||||
.TP
|
||||
.BI "\-\-video\-encoder " name
|
||||
Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR).
|
||||
|
||||
The available encoders can be listed by \-\-list\-encoders.
|
||||
|
||||
.TP
|
||||
.B \-w, \-\-stay-awake
|
||||
Keep the device on while scrcpy is running, when the device is plugged in.
|
||||
@ -579,7 +516,7 @@ Copyright \(co 2018 Genymobile
|
||||
Genymobile
|
||||
.UE
|
||||
|
||||
Copyright \(co 2018\-2023
|
||||
Copyright \(co 2018\-2022
|
||||
.MT rom@rom1v.com
|
||||
Romain Vimont
|
||||
.ME
|
||||
|
@ -204,7 +204,6 @@ sc_adb_parse_device_ip(char *str) {
|
||||
while (str[idx_line] != '\0') {
|
||||
char *line = &str[idx_line];
|
||||
size_t len = strcspn(line, "\n");
|
||||
bool is_last_line = line[len] == '\0';
|
||||
|
||||
// The same, but without any trailing '\r'
|
||||
size_t line_len = sc_str_remove_trailing_cr(line, len);
|
||||
@ -216,12 +215,12 @@ sc_adb_parse_device_ip(char *str) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
if (is_last_line) {
|
||||
break;
|
||||
}
|
||||
idx_line += len;
|
||||
|
||||
// The next line starts after the '\n'
|
||||
idx_line += len + 1;
|
||||
if (str[idx_line] != '\0') {
|
||||
// The next line starts after the '\n'
|
||||
++idx_line;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
|
@ -1,476 +0,0 @@
|
||||
#include "audio_player.h"
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/opt.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
#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 */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
||||
|
||||
#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT
|
||||
#define SC_SDL_SAMPLE_FMT AUDIO_F32
|
||||
|
||||
#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES))
|
||||
#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES))
|
||||
|
||||
static void SDLCALL
|
||||
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
||||
struct sc_audio_player *ap = userdata;
|
||||
|
||||
// This callback is called with the lock used by SDL_AudioDeviceLock(), so
|
||||
// the audiobuf is protected
|
||||
|
||||
assert(len_int > 0);
|
||||
size_t len = len_int;
|
||||
uint32_t count = TO_SAMPLES(len);
|
||||
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
|
||||
#endif
|
||||
|
||||
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
|
||||
if (!ap->played) {
|
||||
// Part of the buffering is handled by inserting initial silence. The
|
||||
// remaining (margin) last samples will be handled by compensation.
|
||||
uint32_t margin = 30 * ap->sample_rate / 1000; // 30ms
|
||||
if (buffered_samples + margin < ap->target_buffering) {
|
||||
LOGV("[Audio] Inserting initial buffering silence: %" PRIu32
|
||||
" samples", count);
|
||||
// Delay playback starting to reach the target buffering. Fill the
|
||||
// whole buffer with silence (len is small compared to the
|
||||
// arbitrary margin value).
|
||||
memset(stream, 0, len);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t read = MIN(buffered_samples, count);
|
||||
if (read) {
|
||||
sc_audiobuf_read(&ap->buf, stream, read);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
ap->played = true;
|
||||
}
|
||||
|
||||
static uint8_t *
|
||||
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) {
|
||||
size_t min_buf_size = TO_BYTES(min_samples);
|
||||
if (min_buf_size > ap->swr_buf_alloc_size) {
|
||||
size_t new_size = min_buf_size + 4096;
|
||||
uint8_t *buf = realloc(ap->swr_buf, new_size);
|
||||
if (!buf) {
|
||||
LOG_OOM();
|
||||
// Could not realloc to the requested size
|
||||
return NULL;
|
||||
}
|
||||
ap->swr_buf = buf;
|
||||
ap->swr_buf_alloc_size = new_size;
|
||||
}
|
||||
|
||||
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 * ap->output_buffer
|
||||
+ 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 * ap->output_buffer;
|
||||
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) < (int) 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
|
||||
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
const AVCodecContext *ctx) {
|
||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
assert(ctx->ch_layout.nb_channels > 0);
|
||||
unsigned nb_channels = ctx->ch_layout.nb_channels;
|
||||
#else
|
||||
int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout);
|
||||
assert(tmp > 0);
|
||||
unsigned nb_channels = tmp;
|
||||
#endif
|
||||
|
||||
assert(ctx->sample_rate > 0);
|
||||
assert(!av_sample_fmt_is_planar(SC_AV_SAMPLE_FMT));
|
||||
int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT);
|
||||
assert(out_bytes_per_sample > 0);
|
||||
|
||||
ap->sample_rate = ctx->sample_rate;
|
||||
ap->nb_channels = nb_channels;
|
||||
ap->out_bytes_per_sample = out_bytes_per_sample;
|
||||
|
||||
ap->target_buffering = ap->target_buffering_delay * ap->sample_rate
|
||||
/ SC_TICK_FREQ;
|
||||
|
||||
uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate
|
||||
/ SC_TICK_FREQ;
|
||||
assert(aout_samples <= 0xFFFF);
|
||||
ap->output_buffer = (uint16_t) aout_samples;
|
||||
|
||||
SDL_AudioSpec desired = {
|
||||
.freq = ctx->sample_rate,
|
||||
.format = SC_SDL_SAMPLE_FMT,
|
||||
.channels = nb_channels,
|
||||
.samples = aout_samples,
|
||||
.callback = sc_audio_player_sdl_callback,
|
||||
.userdata = ap,
|
||||
};
|
||||
SDL_AudioSpec obtained;
|
||||
|
||||
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
|
||||
if (!ap->device) {
|
||||
LOGE("Could not open audio device: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SwrContext *swr_ctx = swr_alloc();
|
||||
if (!swr_ctx) {
|
||||
LOG_OOM();
|
||||
goto error_close_audio_device;
|
||||
}
|
||||
ap->swr_ctx = swr_ctx;
|
||||
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0);
|
||||
av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0);
|
||||
#else
|
||||
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
|
||||
ctx->channel_layout, 0);
|
||||
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
|
||||
ctx->channel_layout, 0);
|
||||
#endif
|
||||
|
||||
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
|
||||
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
|
||||
|
||||
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
|
||||
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
|
||||
|
||||
int ret = swr_init(swr_ctx);
|
||||
if (ret) {
|
||||
LOGE("Failed to initialize the resampling context");
|
||||
goto error_free_swr_ctx;
|
||||
}
|
||||
|
||||
// Use a ring-buffer of the target buffering size plus 1 second between the
|
||||
// producer and the consumer. It's too big on purpose, to guarantee that
|
||||
// the producer and the consumer will be able to access it in parallel
|
||||
// without locking.
|
||||
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) {
|
||||
goto error_free_swr_ctx;
|
||||
}
|
||||
|
||||
size_t initial_swr_buf_size = TO_BYTES(4096);
|
||||
ap->swr_buf = malloc(initial_swr_buf_size);
|
||||
if (!ap->swr_buf) {
|
||||
LOG_OOM();
|
||||
goto error_destroy_audiobuf;
|
||||
}
|
||||
ap->swr_buf_alloc_size = initial_swr_buf_size;
|
||||
|
||||
ap->previous_can_write = sc_audiobuf_can_write(&ap->buf);
|
||||
|
||||
// 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->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);
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_audiobuf:
|
||||
sc_audiobuf_destroy(&ap->buf);
|
||||
error_free_swr_ctx:
|
||||
swr_free(&ap->swr_ctx);
|
||||
error_close_audio_device:
|
||||
SDL_CloseAudioDevice(ap->device);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
|
||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||
|
||||
assert(ap->device);
|
||||
SDL_PauseAudioDevice(ap->device, 1);
|
||||
SDL_CloseAudioDevice(ap->device);
|
||||
|
||||
free(ap->swr_buf);
|
||||
sc_audiobuf_destroy(&ap->buf);
|
||||
swr_free(&ap->swr_ctx);
|
||||
}
|
||||
|
||||
void
|
||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
|
||||
sc_tick output_buffer_duration) {
|
||||
ap->target_buffering_delay = target_buffering;
|
||||
ap->output_buffer_duration = output_buffer_duration;
|
||||
|
||||
static const struct sc_frame_sink_ops ops = {
|
||||
.open = sc_audio_player_frame_sink_open,
|
||||
.close = sc_audio_player_frame_sink_close,
|
||||
.push = sc_audio_player_frame_sink_push,
|
||||
};
|
||||
|
||||
ap->frame_sink.ops = &ops;
|
||||
}
|
@ -1,90 +0,0 @@
|
||||
#ifndef SC_AUDIO_PLAYER_H
|
||||
#define SC_AUDIO_PLAYER_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "trait/frame_sink.h"
|
||||
#include <util/audiobuf.h>
|
||||
#include <util/average.h>
|
||||
#include <util/thread.h>
|
||||
#include <util/tick.h>
|
||||
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libswresample/swresample.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
struct sc_audio_player {
|
||||
struct sc_frame_sink frame_sink;
|
||||
|
||||
SDL_AudioDeviceID device;
|
||||
|
||||
// The target buffering between the producer and the consumer. This value
|
||||
// is directly use for compensation.
|
||||
// Since audio capture and/or encoding on the device typically produce
|
||||
// blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target
|
||||
// value should be higher.
|
||||
sc_tick target_buffering_delay;
|
||||
uint32_t target_buffering; // in samples
|
||||
|
||||
// SDL audio output buffer size.
|
||||
sc_tick output_buffer_duration;
|
||||
uint16_t output_buffer;
|
||||
|
||||
// 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;
|
||||
|
||||
// The sample rate is the same for input and output
|
||||
unsigned sample_rate;
|
||||
// The number of channels is the same for input and output
|
||||
unsigned nb_channels;
|
||||
// The number of bytes per sample for a single channel
|
||||
unsigned out_bytes_per_sample;
|
||||
|
||||
// Target buffer for resampling (only used by the receiver thread)
|
||||
uint8_t *swr_buf;
|
||||
size_t swr_buf_alloc_size;
|
||||
|
||||
// Number of buffered samples (may be negative on underflow) (only used by
|
||||
// the receiver thread)
|
||||
struct sc_average avg_buffering;
|
||||
// Count the number of samples to trigger a compensation update regularly
|
||||
// (only used by the receiver thread)
|
||||
uint32_t samples_since_resync;
|
||||
|
||||
// Number of silence samples inserted since the last received packet
|
||||
// (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;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_audio_player_callbacks {
|
||||
void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata);
|
||||
};
|
||||
|
||||
void
|
||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
|
||||
sc_tick audio_output_buffer);
|
||||
|
||||
#endif
|
380
app/src/cli.c
380
app/src/cli.c
@ -17,62 +17,46 @@
|
||||
#define STR_IMPL_(x) #x
|
||||
#define STR(x) STR_IMPL_(x)
|
||||
|
||||
enum {
|
||||
OPT_BIT_RATE = 1000,
|
||||
OPT_WINDOW_TITLE,
|
||||
OPT_PUSH_TARGET,
|
||||
OPT_ALWAYS_ON_TOP,
|
||||
OPT_CROP,
|
||||
OPT_RECORD_FORMAT,
|
||||
OPT_PREFER_TEXT,
|
||||
OPT_WINDOW_X,
|
||||
OPT_WINDOW_Y,
|
||||
OPT_WINDOW_WIDTH,
|
||||
OPT_WINDOW_HEIGHT,
|
||||
OPT_WINDOW_BORDERLESS,
|
||||
OPT_MAX_FPS,
|
||||
OPT_LOCK_VIDEO_ORIENTATION,
|
||||
OPT_DISPLAY_ID,
|
||||
OPT_ROTATION,
|
||||
OPT_RENDER_DRIVER,
|
||||
OPT_NO_MIPMAPS,
|
||||
OPT_CODEC_OPTIONS,
|
||||
OPT_VIDEO_CODEC_OPTIONS,
|
||||
OPT_FORCE_ADB_FORWARD,
|
||||
OPT_DISABLE_SCREENSAVER,
|
||||
OPT_SHORTCUT_MOD,
|
||||
OPT_NO_KEY_REPEAT,
|
||||
OPT_FORWARD_ALL_CLICKS,
|
||||
OPT_LEGACY_PASTE,
|
||||
OPT_ENCODER,
|
||||
OPT_VIDEO_ENCODER,
|
||||
OPT_POWER_OFF_ON_CLOSE,
|
||||
OPT_V4L2_SINK,
|
||||
OPT_DISPLAY_BUFFER,
|
||||
OPT_V4L2_BUFFER,
|
||||
OPT_TUNNEL_HOST,
|
||||
OPT_TUNNEL_PORT,
|
||||
OPT_NO_CLIPBOARD_AUTOSYNC,
|
||||
OPT_TCPIP,
|
||||
OPT_RAW_KEY_EVENTS,
|
||||
OPT_NO_DOWNSIZE_ON_ERROR,
|
||||
OPT_OTG,
|
||||
OPT_NO_CLEANUP,
|
||||
OPT_PRINT_FPS,
|
||||
OPT_NO_POWER_ON,
|
||||
OPT_CODEC,
|
||||
OPT_VIDEO_CODEC,
|
||||
OPT_NO_AUDIO,
|
||||
OPT_AUDIO_BIT_RATE,
|
||||
OPT_AUDIO_CODEC,
|
||||
OPT_AUDIO_CODEC_OPTIONS,
|
||||
OPT_AUDIO_ENCODER,
|
||||
OPT_LIST_ENCODERS,
|
||||
OPT_LIST_DISPLAYS,
|
||||
OPT_REQUIRE_AUDIO,
|
||||
OPT_AUDIO_BUFFER,
|
||||
OPT_AUDIO_OUTPUT_BUFFER,
|
||||
};
|
||||
#define OPT_RENDER_EXPIRED_FRAMES 1000
|
||||
#define OPT_WINDOW_TITLE 1001
|
||||
#define OPT_PUSH_TARGET 1002
|
||||
#define OPT_ALWAYS_ON_TOP 1003
|
||||
#define OPT_CROP 1004
|
||||
#define OPT_RECORD_FORMAT 1005
|
||||
#define OPT_PREFER_TEXT 1006
|
||||
#define OPT_WINDOW_X 1007
|
||||
#define OPT_WINDOW_Y 1008
|
||||
#define OPT_WINDOW_WIDTH 1009
|
||||
#define OPT_WINDOW_HEIGHT 1010
|
||||
#define OPT_WINDOW_BORDERLESS 1011
|
||||
#define OPT_MAX_FPS 1012
|
||||
#define OPT_LOCK_VIDEO_ORIENTATION 1013
|
||||
#define OPT_DISPLAY_ID 1014
|
||||
#define OPT_ROTATION 1015
|
||||
#define OPT_RENDER_DRIVER 1016
|
||||
#define OPT_NO_MIPMAPS 1017
|
||||
#define OPT_CODEC_OPTIONS 1018
|
||||
#define OPT_FORCE_ADB_FORWARD 1019
|
||||
#define OPT_DISABLE_SCREENSAVER 1020
|
||||
#define OPT_SHORTCUT_MOD 1021
|
||||
#define OPT_NO_KEY_REPEAT 1022
|
||||
#define OPT_FORWARD_ALL_CLICKS 1023
|
||||
#define OPT_LEGACY_PASTE 1024
|
||||
#define OPT_ENCODER_NAME 1025
|
||||
#define OPT_POWER_OFF_ON_CLOSE 1026
|
||||
#define OPT_V4L2_SINK 1027
|
||||
#define OPT_DISPLAY_BUFFER 1028
|
||||
#define OPT_V4L2_BUFFER 1029
|
||||
#define OPT_TUNNEL_HOST 1030
|
||||
#define OPT_TUNNEL_PORT 1031
|
||||
#define OPT_NO_CLIPBOARD_AUTOSYNC 1032
|
||||
#define OPT_TCPIP 1033
|
||||
#define OPT_RAW_KEY_EVENTS 1034
|
||||
#define OPT_NO_DOWNSIZE_ON_ERROR 1035
|
||||
#define OPT_OTG 1036
|
||||
#define OPT_NO_CLEANUP 1037
|
||||
#define OPT_PRINT_FPS 1038
|
||||
#define OPT_NO_POWER_ON 1039
|
||||
|
||||
struct sc_option {
|
||||
char shortopt;
|
||||
@ -114,88 +98,25 @@ static const struct sc_option options[] = {
|
||||
.text = "Make scrcpy window always on top (above other windows).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_BIT_RATE,
|
||||
.longopt = "audio-bit-rate",
|
||||
.shortopt = 'b',
|
||||
.longopt = "bit-rate",
|
||||
.argdesc = "value",
|
||||
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
|
||||
.text = "Encode the video at the gitven bit-rate, expressed in bits/s. "
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"Default is 128K (128000).",
|
||||
"Default is " STR(DEFAULT_BIT_RATE) ".",
|
||||
},
|
||||
{
|
||||
.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_OUTPUT_BUFFER,
|
||||
.longopt = "audio-output-buffer",
|
||||
.argdesc = "ms",
|
||||
.text = "Configure the size of the SDL audio output buffer (in "
|
||||
"milliseconds).\n"
|
||||
"If you get \"robotic\" audio playback, you should test with "
|
||||
"a higher value (10). Do not change this setting otherwise.\n"
|
||||
"Default is 5.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_CODEC,
|
||||
.longopt = "audio-codec",
|
||||
.argdesc = "name",
|
||||
.text = "Select an audio codec (opus, aac or raw).\n"
|
||||
"Default is opus.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_CODEC_OPTIONS,
|
||||
.longopt = "audio-codec-options",
|
||||
.longopt_id = OPT_CODEC_OPTIONS,
|
||||
.longopt = "codec-options",
|
||||
.argdesc = "key[:type]=value[,...]",
|
||||
.text = "Set a list of comma-separated key:type=value options for the "
|
||||
"device audio encoder.\n"
|
||||
"device encoder.\n"
|
||||
"The possible values for 'type' are 'int' (default), 'long', "
|
||||
"'float' and 'string'.\n"
|
||||
"The list of possible codec options is available in the "
|
||||
"Android documentation: "
|
||||
"<https://d.android.com/reference/android/media/MediaFormat>",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_AUDIO_ENCODER,
|
||||
.longopt = "audio-encoder",
|
||||
.argdesc = "name",
|
||||
.text = "Use a specific MediaCodec audio encoder (depending on the "
|
||||
"codec provided by --audio-codec).\n"
|
||||
"The available encoders can be listed by --list-encoders.",
|
||||
},
|
||||
{
|
||||
.shortopt = 'b',
|
||||
.longopt = "video-bit-rate",
|
||||
.argdesc = "value",
|
||||
.text = "Encode the video at the given bit-rate, expressed in bits/s. "
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"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
|
||||
// declaring an explicit --codec option, getopt_long() partial matching
|
||||
// behavior would consider --codec to be equivalent to --codec-options,
|
||||
// which would be confusing.
|
||||
.longopt_id = OPT_CODEC,
|
||||
.longopt = "codec",
|
||||
.argdesc = "value",
|
||||
},
|
||||
{
|
||||
// deprecated
|
||||
.longopt_id = OPT_CODEC_OPTIONS,
|
||||
.longopt = "codec-options",
|
||||
.argdesc = "key[:type]=value[,...]",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CROP,
|
||||
.longopt = "crop",
|
||||
@ -220,9 +141,10 @@ static const struct sc_option options[] = {
|
||||
.longopt_id = OPT_DISPLAY_ID,
|
||||
.longopt = "display",
|
||||
.argdesc = "id",
|
||||
.text = "Specify the device display id to mirror.\n"
|
||||
"The available display ids can be listed by:\n"
|
||||
" scrcpy --list-displays\n"
|
||||
.text = "Specify the display id to mirror.\n"
|
||||
"The list of possible display ids can be listed by:\n"
|
||||
" adb shell dumpsys display\n"
|
||||
"(search \"mDisplayId=\" in the output)\n"
|
||||
"Default is 0.",
|
||||
},
|
||||
{
|
||||
@ -240,10 +162,10 @@ static const struct sc_option options[] = {
|
||||
"Also see -d (--select-usb).",
|
||||
},
|
||||
{
|
||||
// deprecated
|
||||
.longopt_id = OPT_ENCODER,
|
||||
.longopt_id = OPT_ENCODER_NAME,
|
||||
.longopt = "encoder",
|
||||
.argdesc = "name",
|
||||
.text = "Use a specific MediaCodec encoder (must be a H.264 encoder).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_FORCE_ADB_FORWARD,
|
||||
@ -293,16 +215,6 @@ static const struct sc_option options[] = {
|
||||
"This is a workaround for some devices not behaving as "
|
||||
"expected when setting the device clipboard programmatically.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_LIST_DISPLAYS,
|
||||
.longopt = "list-displays",
|
||||
.text = "List device displays.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_LIST_ENCODERS,
|
||||
.longopt = "list-encoders",
|
||||
.text = "List video and audio encoders available on the device.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
|
||||
.longopt = "lock-video-orientation",
|
||||
@ -344,11 +256,6 @@ static const struct sc_option options[] = {
|
||||
"is preserved.\n"
|
||||
"Default is 0 (unlimited).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_AUDIO,
|
||||
.longopt = "no-audio",
|
||||
.text = "Disable audio forwarding.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_NO_CLEANUP,
|
||||
.longopt = "no-cleanup",
|
||||
@ -482,11 +389,9 @@ static const struct sc_option options[] = {
|
||||
"<https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_REQUIRE_AUDIO,
|
||||
.longopt = "require-audio",
|
||||
.text = "By default, scrcpy mirrors only the video when audio capture "
|
||||
"fails on the device. This option makes scrcpy fail if audio "
|
||||
"is enabled but does not work."
|
||||
// deprecated
|
||||
.longopt_id = OPT_RENDER_EXPIRED_FRAMES,
|
||||
.longopt = "render-expired-frames",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_ROTATION,
|
||||
@ -597,33 +502,6 @@ static const struct sc_option options[] = {
|
||||
.longopt = "version",
|
||||
.text = "Print the version of scrcpy.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_VIDEO_CODEC,
|
||||
.longopt = "video-codec",
|
||||
.argdesc = "name",
|
||||
.text = "Select a video codec (h264, h265 or av1).\n"
|
||||
"Default is h264.",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_VIDEO_CODEC_OPTIONS,
|
||||
.longopt = "video-codec-options",
|
||||
.argdesc = "key[:type]=value[,...]",
|
||||
.text = "Set a list of comma-separated key:type=value options for the "
|
||||
"device video encoder.\n"
|
||||
"The possible values for 'type' are 'int' (default), 'long', "
|
||||
"'float' and 'string'.\n"
|
||||
"The list of possible codec options is available in the "
|
||||
"Android documentation: "
|
||||
"<https://d.android.com/reference/android/media/MediaFormat>",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_VIDEO_ENCODER,
|
||||
.longopt = "video-encoder",
|
||||
.argdesc = "name",
|
||||
.text = "Use a specific MediaCodec video encoder (depending on the "
|
||||
"codec provided by --video-codec).\n"
|
||||
"The available encoders can be listed by --list-encoders.",
|
||||
},
|
||||
{
|
||||
.shortopt = 'w',
|
||||
.longopt = "stay-awake",
|
||||
@ -1215,19 +1093,6 @@ parse_buffering_time(const char *s, sc_tick *tick) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_audio_output_buffer(const char *s, sc_tick *tick) {
|
||||
long value;
|
||||
bool ok = parse_integer_arg(s, &value, false, 0, 1000,
|
||||
"audio output buffer");
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*tick = SC_TICK_FROM_MS(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_lock_video_orientation(const char *s,
|
||||
enum sc_lock_video_orientation *lock_mode) {
|
||||
@ -1512,42 +1377,6 @@ guess_record_format(const char *filename) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_video_codec(const char *optarg, enum sc_codec *codec) {
|
||||
if (!strcmp(optarg, "h264")) {
|
||||
*codec = SC_CODEC_H264;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "h265")) {
|
||||
*codec = SC_CODEC_H265;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "av1")) {
|
||||
*codec = SC_CODEC_AV1;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
||||
if (!strcmp(optarg, "opus")) {
|
||||
*codec = SC_CODEC_OPUS;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "aac")) {
|
||||
*codec = SC_CODEC_AAC;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "raw")) {
|
||||
*codec = SC_CODEC_RAW;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
const char *optstring, const struct option *longopts) {
|
||||
@ -1558,17 +1387,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
int c;
|
||||
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case OPT_BIT_RATE:
|
||||
LOGE("--bit-rate has been removed, "
|
||||
"use --video-bit-rate or --audio-bit-rate.");
|
||||
return false;
|
||||
case 'b':
|
||||
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_AUDIO_BIT_RATE:
|
||||
if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) {
|
||||
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@ -1589,6 +1409,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case 'f':
|
||||
opts->fullscreen = true;
|
||||
break;
|
||||
case 'F':
|
||||
LOGW("Deprecated option -F. Use --record-format instead.");
|
||||
// fall through
|
||||
case OPT_RECORD_FORMAT:
|
||||
if (!parse_record_format(optarg, &opts->record_format)) {
|
||||
return false;
|
||||
@ -1676,6 +1499,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case 'w':
|
||||
opts->stay_awake = true;
|
||||
break;
|
||||
case OPT_RENDER_EXPIRED_FRAMES:
|
||||
LOGW("Option --render-expired-frames has been removed. This "
|
||||
"flag has been ignored.");
|
||||
break;
|
||||
case OPT_WINDOW_TITLE:
|
||||
opts->window_title = optarg;
|
||||
break;
|
||||
@ -1734,24 +1561,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->forward_key_repeat = false;
|
||||
break;
|
||||
case OPT_CODEC_OPTIONS:
|
||||
LOGE("--codec-options has been removed, "
|
||||
"use --video-codec-options or --audio-codec-options.");
|
||||
return false;
|
||||
case OPT_VIDEO_CODEC_OPTIONS:
|
||||
opts->video_codec_options = optarg;
|
||||
opts->codec_options = optarg;
|
||||
break;
|
||||
case OPT_AUDIO_CODEC_OPTIONS:
|
||||
opts->audio_codec_options = optarg;
|
||||
break;
|
||||
case OPT_ENCODER:
|
||||
LOGE("--encoder has been removed, "
|
||||
"use --video-encoder or --audio-encoder.");
|
||||
return false;
|
||||
case OPT_VIDEO_ENCODER:
|
||||
opts->video_encoder = optarg;
|
||||
break;
|
||||
case OPT_AUDIO_ENCODER:
|
||||
opts->audio_encoder = optarg;
|
||||
case OPT_ENCODER_NAME:
|
||||
opts->encoder_name = optarg;
|
||||
break;
|
||||
case OPT_FORCE_ADB_FORWARD:
|
||||
opts->force_adb_forward = true;
|
||||
@ -1788,9 +1601,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_NO_DOWNSIZE_ON_ERROR:
|
||||
opts->downsize_on_error = false;
|
||||
break;
|
||||
case OPT_NO_AUDIO:
|
||||
opts->audio = false;
|
||||
break;
|
||||
case OPT_NO_CLEANUP:
|
||||
opts->cleanup = false;
|
||||
break;
|
||||
@ -1800,20 +1610,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
case OPT_PRINT_FPS:
|
||||
opts->start_fps_counter = true;
|
||||
break;
|
||||
case OPT_CODEC:
|
||||
LOGE("--codec has been removed, "
|
||||
"use --video-codec or --audio-codec.");
|
||||
return false;
|
||||
case OPT_VIDEO_CODEC:
|
||||
if (!parse_video_codec(optarg, &opts->video_codec)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_AUDIO_CODEC:
|
||||
if (!parse_audio_codec(optarg, &opts->audio_codec)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case OPT_OTG:
|
||||
#ifdef HAVE_USB
|
||||
opts->otg = true;
|
||||
@ -1841,26 +1637,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
LOGE("V4L2 (--v4l2-buffer) is only available on Linux.");
|
||||
return false;
|
||||
#endif
|
||||
case OPT_LIST_ENCODERS:
|
||||
opts->list_encoders = true;
|
||||
break;
|
||||
case OPT_LIST_DISPLAYS:
|
||||
opts->list_displays = true;
|
||||
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;
|
||||
case OPT_AUDIO_OUTPUT_BUFFER:
|
||||
if (!parse_audio_output_buffer(optarg,
|
||||
&opts->audio_output_buffer)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
// getopt prints the error message on stderr
|
||||
return false;
|
||||
@ -1921,11 +1697,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
#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) {
|
||||
LOGI("Tunnel host/port is set, "
|
||||
"--force-adb-forward automatically enabled.");
|
||||
@ -1947,23 +1718,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->turn_screen_off) {
|
||||
LOGE("Could not request to turn screen off if control is disabled");
|
||||
|
103
app/src/clock.c
103
app/src/clock.c
@ -1,36 +1,111 @@
|
||||
#include "clock.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
#define SC_CLOCK_NDEBUG // comment to debug
|
||||
|
||||
#define SC_CLOCK_RANGE 32
|
||||
|
||||
void
|
||||
sc_clock_init(struct sc_clock *clock) {
|
||||
clock->range = 0;
|
||||
clock->offset = 0;
|
||||
clock->count = 0;
|
||||
clock->head = 0;
|
||||
clock->left_sum.system = 0;
|
||||
clock->left_sum.stream = 0;
|
||||
clock->right_sum.system = 0;
|
||||
clock->right_sum.stream = 0;
|
||||
}
|
||||
|
||||
// Estimate the affine function f(stream) = slope * stream + offset
|
||||
static void
|
||||
sc_clock_estimate(struct sc_clock *clock,
|
||||
double *out_slope, sc_tick *out_offset) {
|
||||
assert(clock->count > 1); // two points are necessary
|
||||
|
||||
struct sc_clock_point left_avg = {
|
||||
.system = clock->left_sum.system / (clock->count / 2),
|
||||
.stream = clock->left_sum.stream / (clock->count / 2),
|
||||
};
|
||||
struct sc_clock_point right_avg = {
|
||||
.system = clock->right_sum.system / ((clock->count + 1) / 2),
|
||||
.stream = clock->right_sum.stream / ((clock->count + 1) / 2),
|
||||
};
|
||||
|
||||
double slope = (double) (right_avg.system - left_avg.system)
|
||||
/ (right_avg.stream - left_avg.stream);
|
||||
|
||||
if (clock->count < SC_CLOCK_RANGE) {
|
||||
/* The first frames are typically received and decoded with more delay
|
||||
* than the others, causing a wrong slope estimation on start. To
|
||||
* compensate, assume an initial slope of 1, then progressively use the
|
||||
* estimated slope. */
|
||||
slope = (clock->count * slope + (SC_CLOCK_RANGE - clock->count))
|
||||
/ SC_CLOCK_RANGE;
|
||||
}
|
||||
|
||||
struct sc_clock_point global_avg = {
|
||||
.system = (clock->left_sum.system + clock->right_sum.system)
|
||||
/ clock->count,
|
||||
.stream = (clock->left_sum.stream + clock->right_sum.stream)
|
||||
/ clock->count,
|
||||
};
|
||||
|
||||
sc_tick offset = global_avg.system - (sc_tick) (global_avg.stream * slope);
|
||||
|
||||
*out_slope = slope;
|
||||
*out_offset = offset;
|
||||
}
|
||||
|
||||
void
|
||||
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
||||
if (clock->range < SC_CLOCK_RANGE) {
|
||||
++clock->range;
|
||||
struct sc_clock_point *point = &clock->points[clock->head];
|
||||
|
||||
if (clock->count == SC_CLOCK_RANGE || clock->count & 1) {
|
||||
// One point passes from the right sum to the left sum
|
||||
|
||||
unsigned mid;
|
||||
if (clock->count == SC_CLOCK_RANGE) {
|
||||
mid = (clock->head + SC_CLOCK_RANGE / 2) % SC_CLOCK_RANGE;
|
||||
} else {
|
||||
// Only for the first frames
|
||||
mid = clock->count / 2;
|
||||
}
|
||||
|
||||
struct sc_clock_point *mid_point = &clock->points[mid];
|
||||
clock->left_sum.system += mid_point->system;
|
||||
clock->left_sum.stream += mid_point->stream;
|
||||
clock->right_sum.system -= mid_point->system;
|
||||
clock->right_sum.stream -= mid_point->stream;
|
||||
}
|
||||
|
||||
sc_tick offset = system - stream;
|
||||
clock->offset = ((clock->range - 1) * clock->offset + offset)
|
||||
/ clock->range;
|
||||
if (clock->count == SC_CLOCK_RANGE) {
|
||||
// The current point overwrites the previous value in the circular
|
||||
// array, update the left sum accordingly
|
||||
clock->left_sum.system -= point->system;
|
||||
clock->left_sum.stream -= point->stream;
|
||||
} else {
|
||||
++clock->count;
|
||||
}
|
||||
|
||||
point->system = system;
|
||||
point->stream = stream;
|
||||
|
||||
clock->right_sum.system += system;
|
||||
clock->right_sum.stream += stream;
|
||||
|
||||
clock->head = (clock->head + 1) % SC_CLOCK_RANGE;
|
||||
|
||||
if (clock->count > 1) {
|
||||
// Update estimation
|
||||
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
||||
|
||||
#ifndef SC_CLOCK_NDEBUG
|
||||
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
|
||||
LOGD("Clock estimation: %f * pts + %" PRItick,
|
||||
clock->slope, clock->offset);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
sc_tick
|
||||
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
||||
assert(clock->range); // sc_clock_update() must have been called
|
||||
return stream + clock->offset;
|
||||
assert(clock->count > 1); // sc_clock_update() must have been called
|
||||
return (sc_tick) (stream * clock->slope) + clock->offset;
|
||||
}
|
||||
|
@ -3,8 +3,13 @@
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "util/tick.h"
|
||||
|
||||
#define SC_CLOCK_RANGE 32
|
||||
static_assert(!(SC_CLOCK_RANGE & 1), "SC_CLOCK_RANGE must be even");
|
||||
|
||||
struct sc_clock_point {
|
||||
sc_tick system;
|
||||
sc_tick stream;
|
||||
@ -16,18 +21,40 @@ struct sc_clock_point {
|
||||
*
|
||||
* f(stream) = slope * stream + offset
|
||||
*
|
||||
* Theoretically, the slope encodes the drift between the device clock and the
|
||||
* computer clock. It is expected to be very close to 1.
|
||||
* To that end, it stores the SC_CLOCK_RANGE last clock points (the timestamps
|
||||
* of a frame expressed both in stream time and system time) in a circular
|
||||
* array.
|
||||
*
|
||||
* Since the clock is used to estimate very close points in the future (which
|
||||
* are reestimated on every clock update, see delay_buffer), the error caused
|
||||
* by clock drift is totally negligible, so it is better to assume that the
|
||||
* slope is 1 than to estimate it (the estimation error would be larger).
|
||||
* To estimate the slope, it splits the last SC_CLOCK_RANGE points into two
|
||||
* sets of SC_CLOCK_RANGE/2 points, and computes their centroid ("average
|
||||
* point"). The slope of the estimated affine function is that of the line
|
||||
* passing through these two points.
|
||||
*
|
||||
* Therefore, only the offset is estimated.
|
||||
* To estimate the offset, it computes the centroid of all the SC_CLOCK_RANGE
|
||||
* points. The resulting affine function passes by this centroid.
|
||||
*
|
||||
* With a circular array, the rolling sums (and average) are quick to compute.
|
||||
* In practice, the estimation is stable and the evolution is smooth.
|
||||
*/
|
||||
struct sc_clock {
|
||||
unsigned range;
|
||||
// Circular array
|
||||
struct sc_clock_point points[SC_CLOCK_RANGE];
|
||||
|
||||
// Number of points in the array (count <= SC_CLOCK_RANGE)
|
||||
unsigned count;
|
||||
|
||||
// Index of the next point to write
|
||||
unsigned head;
|
||||
|
||||
// Sum of the first count/2 points
|
||||
struct sc_clock_point left_sum;
|
||||
|
||||
// Sum of the last (count+1)/2 points
|
||||
struct sc_clock_point right_sum;
|
||||
|
||||
// Estimated slope and offset
|
||||
// (computed on sc_clock_update(), used by sc_clock_to_system_time())
|
||||
double slope;
|
||||
sc_tick offset;
|
||||
};
|
||||
|
||||
|
@ -5,8 +5,8 @@
|
||||
#include "compat.h"
|
||||
|
||||
#define ARRAY_LEN(a) (sizeof(a) / sizeof(a[0]))
|
||||
#define MIN(X,Y) ((X) < (Y) ? (X) : (Y))
|
||||
#define MAX(X,Y) ((X) > (Y) ? (X) : (Y))
|
||||
#define MIN(X,Y) (X) < (Y) ? (X) : (Y)
|
||||
#define MAX(X,Y) (X) > (Y) ? (X) : (Y)
|
||||
#define CLAMP(V,X,Y) MIN( MAX((V),(X)), (Y) )
|
||||
|
||||
#define container_of(ptr, type, member) \
|
||||
|
@ -3,9 +3,6 @@
|
||||
#include "config.h"
|
||||
|
||||
#include <assert.h>
|
||||
#ifndef HAVE_REALLOCARRAY
|
||||
# include <errno.h>
|
||||
#endif
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h>
|
||||
#include <stdarg.h>
|
||||
@ -96,15 +93,5 @@ long jrand48(unsigned short xsubi[3]) {
|
||||
return v.i;
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_REALLOCARRAY
|
||||
void *reallocarray(void *ptr, size_t nmemb, size_t size) {
|
||||
size_t bytes;
|
||||
if (__builtin_mul_overflow(nmemb, size, &bytes)) {
|
||||
errno = ENOMEM;
|
||||
return NULL;
|
||||
}
|
||||
return realloc(ptr, bytes);
|
||||
}
|
||||
#endif
|
||||
|
@ -37,13 +37,6 @@
|
||||
# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL
|
||||
#endif
|
||||
|
||||
// Not documented in ffmpeg/doc/APIchanges, but the channel_layout API
|
||||
// has been replaced by chlayout in FFmpeg commit
|
||||
// f423497b455da06c1337846902c770028760e094.
|
||||
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100)
|
||||
# define SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 6)
|
||||
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
|
||||
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS
|
||||
@ -54,10 +47,6 @@
|
||||
# define SCRCPY_SDL_HAS_HINT_VIDEO_X11_NET_WM_BYPASS_COMPOSITOR
|
||||
#endif
|
||||
|
||||
#if SDL_VERSION_ATLEAST(2, 0, 16)
|
||||
# define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_STRDUP
|
||||
char *strdup(const char *s);
|
||||
#endif
|
||||
@ -78,8 +67,4 @@ long nrand48(unsigned short xsubi[3]);
|
||||
long jrand48(unsigned short xsubi[3]);
|
||||
#endif
|
||||
|
||||
#ifndef HAVE_REALLOCARRAY
|
||||
void *reallocarray(void *ptr, size_t nmemb, size_t size);
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
@ -117,9 +117,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
|
||||
uint16_t pressure =
|
||||
sc_float_to_u16fp(msg->inject_touch_event.pressure);
|
||||
sc_write16be(&buf[22], pressure);
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
|
||||
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
|
||||
return 32;
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.buttons);
|
||||
return 28;
|
||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
||||
write_position(&buf[1], &msg->inject_scroll_event.position);
|
||||
int16_t hscroll =
|
||||
@ -180,25 +179,22 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
|
||||
if (pointer_name) {
|
||||
// string pointer id
|
||||
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
|
||||
" pressure=%f action_button=%06lx buttons=%06lx",
|
||||
" pressure=%f buttons=%06lx",
|
||||
pointer_name,
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
} else {
|
||||
// numeric pointer id
|
||||
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
|
||||
PRIi32 " pressure=%f action_button=%06lx"
|
||||
" buttons=%06lx",
|
||||
PRIi32 " pressure=%f buttons=%06lx",
|
||||
id,
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
}
|
||||
break;
|
||||
|
@ -65,7 +65,6 @@ struct sc_control_msg {
|
||||
} inject_text;
|
||||
struct {
|
||||
enum android_motionevent_action action;
|
||||
enum android_motionevent_buttons action_button;
|
||||
enum android_motionevent_buttons buttons;
|
||||
uint64_t pointer_id;
|
||||
struct sc_position position;
|
||||
|
@ -4,36 +4,26 @@
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
#define SC_CONTROL_MSG_QUEUE_MAX 64
|
||||
|
||||
bool
|
||||
sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
|
||||
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 = receiver_init(&controller->receiver, control_socket, acksync);
|
||||
if (!ok) {
|
||||
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);
|
||||
if (!ok) {
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
sc_vecdeque_destroy(&controller->queue);
|
||||
receiver_destroy(&controller->receiver);
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&controller->msg_cond);
|
||||
if (!ok) {
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
receiver_destroy(&controller->receiver);
|
||||
sc_mutex_destroy(&controller->mutex);
|
||||
sc_vecdeque_destroy(&controller->queue);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -48,14 +38,12 @@ sc_controller_destroy(struct sc_controller *controller) {
|
||||
sc_cond_destroy(&controller->msg_cond);
|
||||
sc_mutex_destroy(&controller->mutex);
|
||||
|
||||
while (!sc_vecdeque_is_empty(&controller->queue)) {
|
||||
struct sc_control_msg *msg = sc_vecdeque_popref(&controller->queue);
|
||||
assert(msg);
|
||||
sc_control_msg_destroy(msg);
|
||||
struct sc_control_msg msg;
|
||||
while (cbuf_take(&controller->queue, &msg)) {
|
||||
sc_control_msg_destroy(&msg);
|
||||
}
|
||||
sc_vecdeque_destroy(&controller->queue);
|
||||
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
receiver_destroy(&controller->receiver);
|
||||
}
|
||||
|
||||
bool
|
||||
@ -66,19 +54,13 @@ sc_controller_push_msg(struct sc_controller *controller,
|
||||
}
|
||||
|
||||
sc_mutex_lock(&controller->mutex);
|
||||
bool full = sc_vecdeque_is_full(&controller->queue);
|
||||
if (!full) {
|
||||
bool was_empty = sc_vecdeque_is_empty(&controller->queue);
|
||||
sc_vecdeque_push_noresize(&controller->queue, *msg);
|
||||
if (was_empty) {
|
||||
sc_cond_signal(&controller->msg_cond);
|
||||
}
|
||||
bool was_empty = cbuf_is_empty(&controller->queue);
|
||||
bool res = cbuf_push(&controller->queue, *msg);
|
||||
if (was_empty) {
|
||||
sc_cond_signal(&controller->msg_cond);
|
||||
}
|
||||
// Otherwise (if the queue is full), the msg is discarded
|
||||
|
||||
sc_mutex_unlock(&controller->mutex);
|
||||
|
||||
return !full;
|
||||
return res;
|
||||
}
|
||||
|
||||
static bool
|
||||
@ -100,8 +82,7 @@ run_controller(void *data) {
|
||||
|
||||
for (;;) {
|
||||
sc_mutex_lock(&controller->mutex);
|
||||
while (!controller->stopped
|
||||
&& sc_vecdeque_is_empty(&controller->queue)) {
|
||||
while (!controller->stopped && cbuf_is_empty(&controller->queue)) {
|
||||
sc_cond_wait(&controller->msg_cond, &controller->mutex);
|
||||
}
|
||||
if (controller->stopped) {
|
||||
@ -109,9 +90,10 @@ run_controller(void *data) {
|
||||
sc_mutex_unlock(&controller->mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(!sc_vecdeque_is_empty(&controller->queue));
|
||||
struct sc_control_msg msg = sc_vecdeque_pop(&controller->queue);
|
||||
struct sc_control_msg msg;
|
||||
bool non_empty = cbuf_take(&controller->queue, &msg);
|
||||
assert(non_empty);
|
||||
(void) non_empty;
|
||||
sc_mutex_unlock(&controller->mutex);
|
||||
|
||||
bool ok = process_msg(controller, &msg);
|
||||
@ -135,7 +117,7 @@ sc_controller_start(struct sc_controller *controller) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sc_receiver_start(&controller->receiver)) {
|
||||
if (!receiver_start(&controller->receiver)) {
|
||||
sc_controller_stop(controller);
|
||||
sc_thread_join(&controller->thread, NULL);
|
||||
return false;
|
||||
@ -155,5 +137,5 @@ sc_controller_stop(struct sc_controller *controller) {
|
||||
void
|
||||
sc_controller_join(struct sc_controller *controller) {
|
||||
sc_thread_join(&controller->thread, NULL);
|
||||
sc_receiver_join(&controller->receiver);
|
||||
receiver_join(&controller->receiver);
|
||||
}
|
||||
|
@ -8,11 +8,11 @@
|
||||
#include "control_msg.h"
|
||||
#include "receiver.h"
|
||||
#include "util/acksync.h"
|
||||
#include "util/cbuf.h"
|
||||
#include "util/net.h"
|
||||
#include "util/thread.h"
|
||||
#include "util/vecdeque.h"
|
||||
|
||||
struct sc_control_msg_queue SC_VECDEQUE(struct sc_control_msg);
|
||||
struct sc_control_msg_queue CBUF(struct sc_control_msg, 64);
|
||||
|
||||
struct sc_controller {
|
||||
sc_socket control_socket;
|
||||
@ -21,7 +21,7 @@ struct sc_controller {
|
||||
sc_cond msg_cond;
|
||||
bool stopped;
|
||||
struct sc_control_msg_queue queue;
|
||||
struct sc_receiver receiver;
|
||||
struct receiver receiver;
|
||||
};
|
||||
|
||||
bool
|
||||
|
@ -2,37 +2,96 @@
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/channel_layout.h>
|
||||
|
||||
#include "events.h"
|
||||
#include "video_buffer.h"
|
||||
#include "trait/frame_sink.h"
|
||||
#include "util/log.h"
|
||||
|
||||
/** Downcast packet_sink to decoder */
|
||||
#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
|
||||
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *ctx) {
|
||||
decoder->frame = av_frame_alloc();
|
||||
if (!decoder->frame) {
|
||||
sc_decoder_open_sinks(struct sc_decoder *decoder) {
|
||||
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
||||
struct sc_frame_sink *sink = decoder->sinks[i];
|
||||
if (!sink->ops->open(sink)) {
|
||||
LOGE("Could not open frame sink %d", i);
|
||||
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();
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) {
|
||||
av_frame_free(&decoder->frame);
|
||||
decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||
|
||||
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
|
||||
LOGE("Could not open codec");
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
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)) {
|
||||
LOGE("Could not open decoder sinks");
|
||||
av_frame_free(&decoder->frame);
|
||||
avcodec_close(decoder->codec_ctx);
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
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);
|
||||
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)) {
|
||||
LOGE("Could not send frame to sink %d", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
@ -43,42 +102,31 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
||||
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)) {
|
||||
LOGE("Decoder '%s': could not send video packet: %d",
|
||||
decoder->name, ret);
|
||||
LOGE("Could not send video packet: %d", ret);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
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;
|
||||
}
|
||||
|
||||
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
|
||||
if (!ret) {
|
||||
// a frame was received
|
||||
bool ok = sc_frame_source_sinks_push(&decoder->frame_source,
|
||||
decoder->frame);
|
||||
av_frame_unref(decoder->frame);
|
||||
if (!ok) {
|
||||
// Error already logged
|
||||
return false;
|
||||
}
|
||||
}
|
||||
bool ok = push_frame_to_sinks(decoder, decoder->frame);
|
||||
// A frame lost should not make the whole pipeline fail. The error, if
|
||||
// any, is already logged.
|
||||
(void) ok;
|
||||
|
||||
av_frame_unref(decoder->frame);
|
||||
} else if (ret != AVERROR(EAGAIN)) {
|
||||
LOGE("Could not receive video frame: %d", ret);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
return sc_decoder_open(decoder, ctx);
|
||||
return sc_decoder_open(decoder, codec);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -95,9 +143,8 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
|
||||
}
|
||||
|
||||
void
|
||||
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
||||
decoder->name = name; // statically allocated
|
||||
sc_frame_source_init(&decoder->frame_source);
|
||||
sc_decoder_init(struct sc_decoder *decoder) {
|
||||
decoder->sink_count = 0;
|
||||
|
||||
static const struct sc_packet_sink_ops ops = {
|
||||
.open = sc_decoder_packet_sink_open,
|
||||
@ -107,3 +154,11 @@ sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
||||
|
||||
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,25 +3,28 @@
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include "trait/frame_source.h"
|
||||
#include "trait/packet_sink.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
#define SC_DECODER_MAX_SINKS 2
|
||||
|
||||
struct sc_decoder {
|
||||
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)
|
||||
struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS];
|
||||
unsigned sink_count;
|
||||
|
||||
AVCodecContext *ctx;
|
||||
AVCodecContext *codec_ctx;
|
||||
AVFrame *frame;
|
||||
};
|
||||
|
||||
// The name must be statically allocated (e.g. a string literal)
|
||||
void
|
||||
sc_decoder_init(struct sc_decoder *decoder, const char *name);
|
||||
sc_decoder_init(struct sc_decoder *decoder);
|
||||
|
||||
void
|
||||
sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink);
|
||||
|
||||
#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.range == 1) {
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
return sc_frame_source_sinks_push(&db->frame_source, frame);
|
||||
}
|
||||
|
||||
struct sc_delayed_frame dframe;
|
||||
bool ok = sc_delayed_frame_init(&dframe, frame);
|
||||
if (!ok) {
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
#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
|
@ -1,13 +1,11 @@
|
||||
#include "demuxer.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <libavutil/channel_layout.h>
|
||||
#include <libavutil/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include "decoder.h"
|
||||
#include "events.h"
|
||||
#include "packet_merger.h"
|
||||
#include "recorder.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/log.h"
|
||||
@ -19,59 +17,6 @@
|
||||
|
||||
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
|
||||
|
||||
static enum AVCodecID
|
||||
sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
||||
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
|
||||
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
||||
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
||||
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
||||
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
||||
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
|
||||
switch (codec_id) {
|
||||
case SC_CODEC_ID_H264:
|
||||
return AV_CODEC_ID_H264;
|
||||
case SC_CODEC_ID_H265:
|
||||
return AV_CODEC_ID_HEVC;
|
||||
case SC_CODEC_ID_AV1:
|
||||
return AV_CODEC_ID_AV1;
|
||||
case SC_CODEC_ID_OPUS:
|
||||
return AV_CODEC_ID_OPUS;
|
||||
case SC_CODEC_ID_AAC:
|
||||
return AV_CODEC_ID_AAC;
|
||||
case SC_CODEC_ID_RAW:
|
||||
return AV_CODEC_ID_PCM_S16LE;
|
||||
default:
|
||||
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
|
||||
return AV_CODEC_ID_NONE;
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
|
||||
uint8_t data[4];
|
||||
ssize_t r = net_recv_all(demuxer->socket, data, 4);
|
||||
if (r < 4) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*codec_id = sc_read32be(data);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width,
|
||||
uint32_t *height) {
|
||||
uint8_t data[8];
|
||||
ssize_t r = net_recv_all(demuxer->socket, data, 8);
|
||||
if (r < 8) {
|
||||
return false;
|
||||
}
|
||||
|
||||
*width = sc_read32be(data);
|
||||
*height = sc_read32be(data + 4);
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
// The video stream contains raw packets, without time information. When we
|
||||
@ -130,172 +75,198 @@ sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
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)) {
|
||||
LOGE("Could not send config packet to sink %d", i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
|
||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||
|
||||
// A config packet must not be decoded immediately (it contains no
|
||||
// frame); instead, it must be concatenated with the future data packet.
|
||||
if (demuxer->pending || is_config) {
|
||||
if (demuxer->pending) {
|
||||
size_t offset = demuxer->pending->size;
|
||||
if (av_grow_packet(demuxer->pending, packet->size)) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
|
||||
} else {
|
||||
demuxer->pending = av_packet_alloc();
|
||||
if (!demuxer->pending) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
if (av_packet_ref(demuxer->pending, packet)) {
|
||||
LOG_OOM();
|
||||
av_packet_free(&demuxer->pending);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_config) {
|
||||
// prepare the concat packet to send to the decoder
|
||||
demuxer->pending->pts = packet->pts;
|
||||
demuxer->pending->dts = packet->dts;
|
||||
demuxer->pending->flags = packet->flags;
|
||||
packet = demuxer->pending;
|
||||
}
|
||||
}
|
||||
|
||||
bool ok = push_packet_to_sinks(demuxer, packet);
|
||||
|
||||
if (!is_config && demuxer->pending) {
|
||||
// the pending packet must be discarded (consumed or error)
|
||||
av_packet_free(&demuxer->pending);
|
||||
}
|
||||
|
||||
if (!ok) {
|
||||
LOGE("Could not process packet");
|
||||
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)) {
|
||||
LOGE("Could not open packet sink %d", i);
|
||||
sc_demuxer_close_first_sinks(demuxer, i);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static int
|
||||
run_demuxer(void *data) {
|
||||
struct sc_demuxer *demuxer = data;
|
||||
|
||||
// Flag to report end-of-stream (i.e. device disconnected)
|
||||
enum sc_demuxer_status status = SC_DEMUXER_STATUS_ERROR;
|
||||
|
||||
uint32_t raw_codec_id;
|
||||
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
|
||||
if (!ok) {
|
||||
LOGE("Demuxer '%s': stream disabled due to connection error",
|
||||
demuxer->name);
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (raw_codec_id == 0) {
|
||||
LOGW("Demuxer '%s': stream explicitly disabled by the device",
|
||||
demuxer->name);
|
||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
||||
status = SC_DEMUXER_STATUS_DISABLED;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (raw_codec_id == 1) {
|
||||
LOGE("Demuxer '%s': stream configuration error on the device",
|
||||
demuxer->name);
|
||||
goto end;
|
||||
}
|
||||
|
||||
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
|
||||
if (codec_id == AV_CODEC_ID_NONE) {
|
||||
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
|
||||
demuxer->name);
|
||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
||||
goto end;
|
||||
}
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(codec_id);
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||
if (!codec) {
|
||||
LOGE("Demuxer '%s': stream disabled due to missing decoder",
|
||||
demuxer->name);
|
||||
sc_packet_source_sinks_disable(&demuxer->packet_source);
|
||||
LOGE("H.264 decoder not found");
|
||||
goto end;
|
||||
}
|
||||
|
||||
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!codec_ctx) {
|
||||
demuxer->codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!demuxer->codec_ctx) {
|
||||
LOG_OOM();
|
||||
goto end;
|
||||
}
|
||||
|
||||
codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||
|
||||
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
ok = sc_demuxer_recv_video_size(demuxer, &width, &height);
|
||||
if (!ok) {
|
||||
goto finally_free_context;
|
||||
}
|
||||
|
||||
codec_ctx->width = width;
|
||||
codec_ctx->height = height;
|
||||
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
} else {
|
||||
// Hardcoded audio properties
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
codec_ctx->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
|
||||
#else
|
||||
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||
codec_ctx->channels = 2;
|
||||
#endif
|
||||
codec_ctx->sample_rate = 48000;
|
||||
if (!sc_demuxer_open_sinks(demuxer, codec)) {
|
||||
LOGE("Could not open demuxer sinks");
|
||||
goto finally_free_codec_ctx;
|
||||
}
|
||||
|
||||
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
||||
LOGE("Demuxer '%s': could not open codec", demuxer->name);
|
||||
goto finally_free_context;
|
||||
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||
if (!demuxer->parser) {
|
||||
LOGE("Could not initialize parser");
|
||||
goto finally_close_sinks;
|
||||
}
|
||||
|
||||
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
|
||||
// video streams
|
||||
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
||||
|
||||
struct sc_packet_merger merger;
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
sc_packet_merger_init(&merger);
|
||||
}
|
||||
// We must only pass complete frames to av_parser_parse2()!
|
||||
// It's more complicated, but this allows to reduce the latency by 1 frame!
|
||||
demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
|
||||
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
if (!packet) {
|
||||
LOG_OOM();
|
||||
goto finally_close_sinks;
|
||||
goto finally_close_parser;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
bool ok = sc_demuxer_recv_packet(demuxer, packet);
|
||||
if (!ok) {
|
||||
// end of stream
|
||||
status = SC_DEMUXER_STATUS_EOS;
|
||||
break;
|
||||
}
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
// Prepend any config packet to the next media packet
|
||||
ok = sc_packet_merger_merge(&merger, packet);
|
||||
if (!ok) {
|
||||
av_packet_unref(packet);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
ok = sc_packet_source_sinks_push(&demuxer->packet_source, packet);
|
||||
ok = sc_demuxer_push_packet(demuxer, packet);
|
||||
av_packet_unref(packet);
|
||||
if (!ok) {
|
||||
// The sink already logged its concrete error
|
||||
// cannot process packet (error already logged)
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
LOGD("Demuxer '%s': end of frames", demuxer->name);
|
||||
LOGD("End of frames");
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
sc_packet_merger_destroy(&merger);
|
||||
if (demuxer->pending) {
|
||||
av_packet_free(&demuxer->pending);
|
||||
}
|
||||
|
||||
av_packet_free(&packet);
|
||||
finally_close_parser:
|
||||
av_parser_close(demuxer->parser);
|
||||
finally_close_sinks:
|
||||
sc_packet_source_sinks_close(&demuxer->packet_source);
|
||||
finally_free_context:
|
||||
// This also calls avcodec_close() internally
|
||||
avcodec_free_context(&codec_ctx);
|
||||
sc_demuxer_close_sinks(demuxer);
|
||||
finally_free_codec_ctx:
|
||||
avcodec_free_context(&demuxer->codec_ctx);
|
||||
end:
|
||||
demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata);
|
||||
demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
||||
assert(socket != SC_SOCKET_NONE);
|
||||
|
||||
demuxer->name = name; // statically allocated
|
||||
demuxer->socket = socket;
|
||||
sc_packet_source_init(&demuxer->packet_source);
|
||||
demuxer->pending = NULL;
|
||||
demuxer->sink_count = 0;
|
||||
|
||||
assert(cbs && cbs->on_ended);
|
||||
assert(cbs && cbs->on_eos);
|
||||
|
||||
demuxer->cbs = cbs;
|
||||
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
|
||||
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
||||
LOGD("Demuxer '%s': starting thread", demuxer->name);
|
||||
LOGD("Starting demuxer thread");
|
||||
|
||||
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
|
||||
demuxer);
|
||||
if (!ok) {
|
||||
LOGE("Demuxer '%s': could not start thread", demuxer->name);
|
||||
LOGE("Could not start demuxer thread");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -8,39 +8,40 @@
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
#include "trait/packet_source.h"
|
||||
#include "trait/packet_sink.h"
|
||||
#include "util/net.h"
|
||||
#include "util/thread.h"
|
||||
|
||||
#define SC_DEMUXER_MAX_SINKS 2
|
||||
|
||||
struct sc_demuxer {
|
||||
struct sc_packet_source packet_source; // packet source trait
|
||||
|
||||
const char *name; // must be statically allocated (e.g. a string literal)
|
||||
|
||||
sc_socket socket;
|
||||
sc_thread thread;
|
||||
|
||||
struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS];
|
||||
unsigned sink_count;
|
||||
|
||||
AVCodecContext *codec_ctx;
|
||||
AVCodecParserContext *parser;
|
||||
// successive packets may need to be concatenated, until a non-config
|
||||
// packet is available
|
||||
AVPacket *pending;
|
||||
|
||||
const struct sc_demuxer_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
enum sc_demuxer_status {
|
||||
SC_DEMUXER_STATUS_EOS,
|
||||
SC_DEMUXER_STATUS_DISABLED,
|
||||
SC_DEMUXER_STATUS_ERROR,
|
||||
};
|
||||
|
||||
struct sc_demuxer_callbacks {
|
||||
void (*on_ended)(struct sc_demuxer *demuxer, enum sc_demuxer_status,
|
||||
void *userdata);
|
||||
void (*on_eos)(struct sc_demuxer *demuxer, void *userdata);
|
||||
};
|
||||
|
||||
// The name must be statically allocated (e.g. a string literal)
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
void
|
||||
sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink);
|
||||
|
||||
bool
|
||||
sc_demuxer_start(struct sc_demuxer *demuxer);
|
||||
|
||||
|
@ -1,185 +0,0 @@
|
||||
#include "display.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
bool
|
||||
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
|
||||
display->renderer =
|
||||
SDL_CreateRenderer(window, -1, SDL_RENDERER_ACCELERATED);
|
||||
if (!display->renderer) {
|
||||
LOGE("Could not create renderer: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
SDL_RendererInfo renderer_info;
|
||||
int r = SDL_GetRendererInfo(display->renderer, &renderer_info);
|
||||
const char *renderer_name = r ? NULL : renderer_info.name;
|
||||
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
||||
|
||||
display->mipmaps = false;
|
||||
|
||||
// starts with "opengl"
|
||||
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
||||
if (use_opengl) {
|
||||
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
// Persuade macOS to give us something better than OpenGL 2.1.
|
||||
// If we create a Core Profile context, we get the best OpenGL version.
|
||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
|
||||
SDL_GL_CONTEXT_PROFILE_CORE);
|
||||
|
||||
LOGD("Creating OpenGL Core Profile Context");
|
||||
display->gl_context = SDL_GL_CreateContext(window);
|
||||
if (!display->gl_context) {
|
||||
LOGE("Could not create OpenGL context: %s", SDL_GetError());
|
||||
SDL_DestroyRenderer(display->renderer);
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
struct sc_opengl *gl = &display->gl;
|
||||
sc_opengl_init(gl);
|
||||
|
||||
LOGI("OpenGL version: %s", gl->version);
|
||||
|
||||
if (mipmaps) {
|
||||
bool supports_mipmaps =
|
||||
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
|
||||
2, 0 /* OpenGL ES 2.0+ */);
|
||||
if (supports_mipmaps) {
|
||||
LOGI("Trilinear filtering enabled");
|
||||
display->mipmaps = true;
|
||||
} else {
|
||||
LOGW("Trilinear filtering disabled "
|
||||
"(OpenGL 3.0+ or ES 2.0+ required");
|
||||
}
|
||||
} else {
|
||||
LOGI("Trilinear filtering disabled");
|
||||
}
|
||||
} else if (mipmaps) {
|
||||
LOGD("Trilinear filtering disabled (not an OpenGL renderer");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_display_destroy(struct sc_display *display) {
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GL_DeleteContext(display->gl_context);
|
||||
#endif
|
||||
if (display->texture) {
|
||||
SDL_DestroyTexture(display->texture);
|
||||
}
|
||||
SDL_DestroyRenderer(display->renderer);
|
||||
}
|
||||
|
||||
static SDL_Texture *
|
||||
sc_display_create_texture(struct sc_display *display,
|
||||
struct sc_size size) {
|
||||
SDL_Renderer *renderer = display->renderer;
|
||||
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
|
||||
SDL_TEXTUREACCESS_STREAMING,
|
||||
size.width, size.height);
|
||||
if (!texture) {
|
||||
LOGE("Could not create texture: %s", SDL_GetError());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (display->mipmaps) {
|
||||
struct sc_opengl *gl = &display->gl;
|
||||
|
||||
SDL_GL_BindTexture(texture, NULL, NULL);
|
||||
|
||||
// Enable trilinear filtering for downscaling
|
||||
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
|
||||
GL_LINEAR_MIPMAP_LINEAR);
|
||||
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
|
||||
|
||||
SDL_GL_UnbindTexture(texture);
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
|
||||
if (display->texture) {
|
||||
SDL_DestroyTexture(display->texture);
|
||||
}
|
||||
|
||||
display->texture = sc_display_create_texture(display, size);
|
||||
if (!display->texture) {
|
||||
return false;
|
||||
}
|
||||
|
||||
LOGI("Texture: %" PRIu16 "x%" PRIu16, size.width, size.height);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
|
||||
int ret = SDL_UpdateYUVTexture(display->texture, NULL,
|
||||
frame->data[0], frame->linesize[0],
|
||||
frame->data[1], frame->linesize[1],
|
||||
frame->data[2], frame->linesize[2]);
|
||||
if (ret) {
|
||||
LOGE("Could not update texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
if (display->mipmaps) {
|
||||
SDL_GL_BindTexture(display->texture, NULL, NULL);
|
||||
display->gl.GenerateMipmap(GL_TEXTURE_2D);
|
||||
SDL_GL_UnbindTexture(display->texture);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
||||
unsigned rotation) {
|
||||
SDL_RenderClear(display->renderer);
|
||||
|
||||
SDL_Renderer *renderer = display->renderer;
|
||||
SDL_Texture *texture = display->texture;
|
||||
|
||||
if (rotation == 0) {
|
||||
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
|
||||
if (ret) {
|
||||
LOGE("Could not render texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
|
||||
// counterclockwise (to be consistent with --lock-video-orientation)
|
||||
int cw_rotation = (4 - rotation) % 4;
|
||||
double angle = 90 * cw_rotation;
|
||||
|
||||
const SDL_Rect *dstrect = NULL;
|
||||
SDL_Rect rect;
|
||||
if (rotation & 1) {
|
||||
rect.x = geometry->x + (geometry->w - geometry->h) / 2;
|
||||
rect.y = geometry->y + (geometry->h - geometry->w) / 2;
|
||||
rect.w = geometry->h;
|
||||
rect.h = geometry->w;
|
||||
dstrect = ▭
|
||||
} else {
|
||||
assert(rotation == 2);
|
||||
dstrect = geometry;
|
||||
}
|
||||
|
||||
int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle,
|
||||
NULL, 0);
|
||||
if (ret) {
|
||||
LOGE("Could not render texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
SDL_RenderPresent(display->renderer);
|
||||
return true;
|
||||
}
|
@ -1,45 +0,0 @@
|
||||
#ifndef SC_DISPLAY_H
|
||||
#define SC_DISPLAY_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <SDL2/SDL.h>
|
||||
|
||||
#include "coords.h"
|
||||
#include "opengl.h"
|
||||
|
||||
#ifdef __APPLE__
|
||||
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
#endif
|
||||
|
||||
struct sc_display {
|
||||
SDL_Renderer *renderer;
|
||||
SDL_Texture *texture;
|
||||
|
||||
struct sc_opengl gl;
|
||||
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
|
||||
SDL_GLContext *gl_context;
|
||||
#endif
|
||||
|
||||
bool mipmaps;
|
||||
};
|
||||
|
||||
bool
|
||||
sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps);
|
||||
|
||||
void
|
||||
sc_display_destroy(struct sc_display *display);
|
||||
|
||||
bool
|
||||
sc_display_set_texture_size(struct sc_display *display, struct sc_size size);
|
||||
|
||||
bool
|
||||
sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
|
||||
|
||||
bool
|
||||
sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
|
||||
unsigned rotation);
|
||||
|
||||
#endif
|
@ -1,8 +1,5 @@
|
||||
#define SC_EVENT_NEW_FRAME SDL_USEREVENT
|
||||
#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1)
|
||||
#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
|
||||
#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
||||
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
|
||||
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
|
||||
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
|
||||
#define EVENT_NEW_FRAME SDL_USEREVENT
|
||||
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
|
||||
#define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
|
||||
#define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
||||
#define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||
|
@ -19,7 +19,7 @@ sc_file_pusher_init(struct sc_file_pusher *fp, const char *serial,
|
||||
const char *push_target) {
|
||||
assert(serial);
|
||||
|
||||
sc_vecdeque_init(&fp->queue);
|
||||
cbuf_init(&fp->queue);
|
||||
|
||||
bool ok = sc_mutex_init(&fp->mutex);
|
||||
if (!ok) {
|
||||
@ -65,10 +65,9 @@ sc_file_pusher_destroy(struct sc_file_pusher *fp) {
|
||||
sc_intr_destroy(&fp->intr);
|
||||
free(fp->serial);
|
||||
|
||||
while (!sc_vecdeque_is_empty(&fp->queue)) {
|
||||
struct sc_file_pusher_request *req = sc_vecdeque_popref(&fp->queue);
|
||||
assert(req);
|
||||
sc_file_pusher_request_destroy(req);
|
||||
struct sc_file_pusher_request req;
|
||||
while (cbuf_take(&fp->queue, &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);
|
||||
bool was_empty = sc_vecdeque_is_empty(&fp->queue);
|
||||
bool res = sc_vecdeque_push(&fp->queue, req);
|
||||
if (!res) {
|
||||
LOG_OOM();
|
||||
sc_mutex_unlock(&fp->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool was_empty = cbuf_is_empty(&fp->queue);
|
||||
bool res = cbuf_push(&fp->queue, req);
|
||||
if (was_empty) {
|
||||
sc_cond_signal(&fp->event_cond);
|
||||
}
|
||||
sc_mutex_unlock(&fp->mutex);
|
||||
|
||||
return true;
|
||||
return res;
|
||||
}
|
||||
|
||||
static int
|
||||
@ -121,7 +113,7 @@ run_file_pusher(void *data) {
|
||||
|
||||
for (;;) {
|
||||
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);
|
||||
}
|
||||
if (fp->stopped) {
|
||||
@ -129,9 +121,10 @@ run_file_pusher(void *data) {
|
||||
sc_mutex_unlock(&fp->mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(!sc_vecdeque_is_empty(&fp->queue));
|
||||
struct sc_file_pusher_request req = sc_vecdeque_pop(&fp->queue);
|
||||
struct sc_file_pusher_request req;
|
||||
bool non_empty = cbuf_take(&fp->queue, &req);
|
||||
assert(non_empty);
|
||||
(void) non_empty;
|
||||
sc_mutex_unlock(&fp->mutex);
|
||||
|
||||
if (req.action == SC_FILE_PUSHER_ACTION_INSTALL_APK) {
|
||||
@ -172,18 +165,14 @@ sc_file_pusher_start(struct sc_file_pusher *fp) {
|
||||
|
||||
void
|
||||
sc_file_pusher_stop(struct sc_file_pusher *fp) {
|
||||
if (fp->initialized) {
|
||||
sc_mutex_lock(&fp->mutex);
|
||||
fp->stopped = true;
|
||||
sc_cond_signal(&fp->event_cond);
|
||||
sc_intr_interrupt(&fp->intr);
|
||||
sc_mutex_unlock(&fp->mutex);
|
||||
}
|
||||
sc_mutex_lock(&fp->mutex);
|
||||
fp->stopped = true;
|
||||
sc_cond_signal(&fp->event_cond);
|
||||
sc_intr_interrupt(&fp->intr);
|
||||
sc_mutex_unlock(&fp->mutex);
|
||||
}
|
||||
|
||||
void
|
||||
sc_file_pusher_join(struct sc_file_pusher *fp) {
|
||||
if (fp->initialized) {
|
||||
sc_thread_join(&fp->thread, NULL);
|
||||
}
|
||||
sc_thread_join(&fp->thread, NULL);
|
||||
}
|
||||
|
@ -5,9 +5,9 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
|
||||
#include "util/intr.h"
|
||||
#include "util/cbuf.h"
|
||||
#include "util/thread.h"
|
||||
#include "util/vecdeque.h"
|
||||
#include "util/intr.h"
|
||||
|
||||
enum sc_file_pusher_action {
|
||||
SC_FILE_PUSHER_ACTION_INSTALL_APK,
|
||||
@ -19,7 +19,7 @@ struct sc_file_pusher_request {
|
||||
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 {
|
||||
char *serial;
|
||||
|
@ -96,7 +96,6 @@ run_fps_counter(void *data) {
|
||||
bool
|
||||
sc_fps_counter_start(struct sc_fps_counter *counter) {
|
||||
sc_mutex_lock(&counter->mutex);
|
||||
counter->interrupted = false;
|
||||
counter->next_timestamp = sc_tick_now() + SC_FPS_COUNTER_INTERVAL;
|
||||
counter->nr_rendered = 0;
|
||||
counter->nr_skipped = 0;
|
||||
|
@ -69,7 +69,7 @@ decode_image(const char *path) {
|
||||
}
|
||||
|
||||
if (avformat_open_input(&ctx, path, NULL, NULL) < 0) {
|
||||
LOGE("Could not open icon image: %s", path);
|
||||
LOGE("Could not open image codec: %s", path);
|
||||
goto free_ctx;
|
||||
}
|
||||
|
||||
|
@ -339,7 +339,6 @@ simulate_virtual_finger(struct sc_input_manager *im,
|
||||
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
|
||||
: POINTER_ID_VIRTUAL_FINGER;
|
||||
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
|
||||
msg.inject_touch_event.action_button = 0;
|
||||
msg.inject_touch_event.buttons = 0;
|
||||
|
||||
if (!sc_controller_push_msg(im->controller, &msg)) {
|
||||
@ -797,8 +796,7 @@ sc_input_manager_process_file(struct sc_input_manager *im,
|
||||
}
|
||||
|
||||
void
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
const SDL_Event *event) {
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) {
|
||||
bool control = im->controller;
|
||||
switch (event->type) {
|
||||
case SDL_TEXTINPUT:
|
||||
|
@ -61,7 +61,6 @@ sc_input_manager_init(struct sc_input_manager *im,
|
||||
const struct sc_input_manager_params *params);
|
||||
|
||||
void
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
const SDL_Event *event);
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event);
|
||||
|
||||
#endif
|
||||
|
@ -4,6 +4,10 @@
|
||||
#include <stdbool.h>
|
||||
#include <unistd.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include "util/str.h"
|
||||
#endif
|
||||
#ifdef HAVE_V4L2
|
||||
# include <libavdevice/avdevice.h>
|
||||
#endif
|
||||
@ -15,14 +19,8 @@
|
||||
#include "scrcpy.h"
|
||||
#include "usb/scrcpy_otg.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net.h"
|
||||
#include "version.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#include "util/str.h"
|
||||
#endif
|
||||
|
||||
int
|
||||
main_scrcpy(int argc, char *argv[]) {
|
||||
#ifdef _WIN32
|
||||
@ -71,12 +69,10 @@ main_scrcpy(int argc, char *argv[]) {
|
||||
}
|
||||
#endif
|
||||
|
||||
if (!net_init()) {
|
||||
if (avformat_network_init()) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
}
|
||||
|
||||
sc_log_configure();
|
||||
|
||||
#ifdef HAVE_USB
|
||||
enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts)
|
||||
: scrcpy(&args.opts);
|
||||
@ -84,6 +80,8 @@ main_scrcpy(int argc, char *argv[]) {
|
||||
enum scrcpy_exit_code ret = scrcpy(&args.opts);
|
||||
#endif
|
||||
|
||||
avformat_network_deinit(); // ignore failure
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,6 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
|
||||
.pointer_id = event->pointer_id,
|
||||
.position = event->position,
|
||||
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
|
||||
.action_button = convert_mouse_buttons(event->button),
|
||||
.buttons = convert_mouse_buttons(event->buttons_state),
|
||||
},
|
||||
};
|
||||
|
@ -7,19 +7,14 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.window_title = NULL,
|
||||
.push_target = NULL,
|
||||
.render_driver = NULL,
|
||||
.video_codec_options = NULL,
|
||||
.audio_codec_options = NULL,
|
||||
.video_encoder = NULL,
|
||||
.audio_encoder = NULL,
|
||||
.codec_options = NULL,
|
||||
.encoder_name = NULL,
|
||||
#ifdef HAVE_V4L2
|
||||
.v4l2_device = NULL,
|
||||
#endif
|
||||
.log_level = SC_LOG_LEVEL_INFO,
|
||||
.video_codec = SC_CODEC_H264,
|
||||
.audio_codec = SC_CODEC_OPUS,
|
||||
.record_format = SC_RECORD_FORMAT_AUTO,
|
||||
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
|
||||
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT,
|
||||
.port_range = {
|
||||
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
|
||||
.last = DEFAULT_LOCAL_PORT_RANGE_LAST,
|
||||
@ -31,8 +26,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.count = 2,
|
||||
},
|
||||
.max_size = 0,
|
||||
.video_bit_rate = 0,
|
||||
.audio_bit_rate = 0,
|
||||
.bit_rate = DEFAULT_BIT_RATE,
|
||||
.max_fps = 0,
|
||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
||||
.rotation = 0,
|
||||
@ -43,8 +37,6 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.display_id = 0,
|
||||
.display_buffer = 0,
|
||||
.v4l2_buffer = 0,
|
||||
.audio_buffer = SC_TICK_FROM_MS(50),
|
||||
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
||||
#ifdef HAVE_USB
|
||||
.otg = false,
|
||||
#endif
|
||||
@ -73,8 +65,4 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.cleanup = true,
|
||||
.start_fps_counter = false,
|
||||
.power_on = true,
|
||||
.audio = true,
|
||||
.require_audio = false,
|
||||
.list_encoders = false,
|
||||
.list_displays = false,
|
||||
};
|
||||
|
@ -23,15 +23,6 @@ enum sc_record_format {
|
||||
SC_RECORD_FORMAT_MKV,
|
||||
};
|
||||
|
||||
enum sc_codec {
|
||||
SC_CODEC_H264,
|
||||
SC_CODEC_H265,
|
||||
SC_CODEC_AV1,
|
||||
SC_CODEC_OPUS,
|
||||
SC_CODEC_AAC,
|
||||
SC_CODEC_RAW,
|
||||
};
|
||||
|
||||
enum sc_lock_video_orientation {
|
||||
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
|
||||
// lock the current orientation when scrcpy starts
|
||||
@ -96,16 +87,12 @@ struct scrcpy_options {
|
||||
const char *window_title;
|
||||
const char *push_target;
|
||||
const char *render_driver;
|
||||
const char *video_codec_options;
|
||||
const char *audio_codec_options;
|
||||
const char *video_encoder;
|
||||
const char *audio_encoder;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
#ifdef HAVE_V4L2
|
||||
const char *v4l2_device;
|
||||
#endif
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec video_codec;
|
||||
enum sc_codec audio_codec;
|
||||
enum sc_record_format record_format;
|
||||
enum sc_keyboard_input_mode keyboard_input_mode;
|
||||
enum sc_mouse_input_mode mouse_input_mode;
|
||||
@ -114,8 +101,7 @@ struct scrcpy_options {
|
||||
uint16_t tunnel_port;
|
||||
struct sc_shortcut_mods shortcut_mods;
|
||||
uint16_t max_size;
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint32_t bit_rate;
|
||||
uint16_t max_fps;
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
uint8_t rotation;
|
||||
@ -126,8 +112,6 @@ struct scrcpy_options {
|
||||
uint32_t display_id;
|
||||
sc_tick display_buffer;
|
||||
sc_tick v4l2_buffer;
|
||||
sc_tick audio_buffer;
|
||||
sc_tick audio_output_buffer;
|
||||
#ifdef HAVE_USB
|
||||
bool otg;
|
||||
#endif
|
||||
@ -156,10 +140,6 @@ struct scrcpy_options {
|
||||
bool cleanup;
|
||||
bool start_fps_counter;
|
||||
bool power_on;
|
||||
bool audio;
|
||||
bool require_audio;
|
||||
bool list_encoders;
|
||||
bool list_displays;
|
||||
};
|
||||
|
||||
extern const struct scrcpy_options scrcpy_options_default;
|
||||
|
@ -1,48 +0,0 @@
|
||||
#include "packet_merger.h"
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
void
|
||||
sc_packet_merger_init(struct sc_packet_merger *merger) {
|
||||
merger->config = NULL;
|
||||
}
|
||||
|
||||
void
|
||||
sc_packet_merger_destroy(struct sc_packet_merger *merger) {
|
||||
free(merger->config);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) {
|
||||
bool is_config = packet->pts == AV_NOPTS_VALUE;
|
||||
|
||||
if (is_config) {
|
||||
free(merger->config);
|
||||
|
||||
merger->config = malloc(packet->size);
|
||||
if (!merger->config) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memcpy(merger->config, packet->data, packet->size);
|
||||
merger->config_size = packet->size;
|
||||
} else if (merger->config) {
|
||||
size_t config_size = merger->config_size;
|
||||
size_t media_size = packet->size;
|
||||
|
||||
if (av_grow_packet(packet, config_size)) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
memmove(packet->data + config_size, packet->data, media_size);
|
||||
memcpy(packet->data, merger->config, config_size);
|
||||
|
||||
free(merger->config);
|
||||
merger->config = NULL;
|
||||
// merger->size is meaningless when merger->config is NULL
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
@ -1,43 +0,0 @@
|
||||
#ifndef SC_PACKET_MERGER_H
|
||||
#define SC_PACKET_MERGER_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
/**
|
||||
* Config packets (containing the SPS/PPS) are sent in-band. A new config
|
||||
* packet is sent whenever a new encoding session is started (on start and on
|
||||
* device orientation change).
|
||||
*
|
||||
* Every time a config packet is received, it must be sent alone (for recorder
|
||||
* extradata), then concatenated to the next media packet (for correct decoding
|
||||
* and recording).
|
||||
*
|
||||
* This helper reads every input packet and modifies each media packet which
|
||||
* immediately follows a config packet to prepend the config packet payload.
|
||||
*/
|
||||
|
||||
struct sc_packet_merger {
|
||||
uint8_t *config;
|
||||
size_t config_size;
|
||||
};
|
||||
|
||||
void
|
||||
sc_packet_merger_init(struct sc_packet_merger *merger);
|
||||
|
||||
void
|
||||
sc_packet_merger_destroy(struct sc_packet_merger *merger);
|
||||
|
||||
/**
|
||||
* If the packet is a config packet, then keep its data for later.
|
||||
* Otherwise (if the packet is a media packet), then if a config packet is
|
||||
* pending, prepend the config packet to this packet (so the packet is
|
||||
* modified!).
|
||||
*/
|
||||
bool
|
||||
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet);
|
||||
|
||||
#endif
|
@ -7,7 +7,7 @@
|
||||
#include "util/log.h"
|
||||
|
||||
bool
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync) {
|
||||
bool ok = sc_mutex_init(&receiver->mutex);
|
||||
if (!ok) {
|
||||
@ -21,12 +21,12 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
}
|
||||
|
||||
void
|
||||
sc_receiver_destroy(struct sc_receiver *receiver) {
|
||||
receiver_destroy(struct receiver *receiver) {
|
||||
sc_mutex_destroy(&receiver->mutex);
|
||||
}
|
||||
|
||||
static void
|
||||
process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
|
||||
process_msg(struct receiver *receiver, struct device_msg *msg) {
|
||||
switch (msg->type) {
|
||||
case DEVICE_MSG_TYPE_CLIPBOARD: {
|
||||
char *current = SDL_GetClipboardText();
|
||||
@ -51,7 +51,7 @@ process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
size_t head = 0;
|
||||
for (;;) {
|
||||
struct device_msg msg;
|
||||
@ -76,7 +76,7 @@ process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len)
|
||||
|
||||
static int
|
||||
run_receiver(void *data) {
|
||||
struct sc_receiver *receiver = data;
|
||||
struct receiver *receiver = data;
|
||||
|
||||
static unsigned char buf[DEVICE_MSG_MAX_SIZE];
|
||||
size_t head = 0;
|
||||
@ -108,7 +108,7 @@ run_receiver(void *data) {
|
||||
}
|
||||
|
||||
bool
|
||||
sc_receiver_start(struct sc_receiver *receiver) {
|
||||
receiver_start(struct receiver *receiver) {
|
||||
LOGD("Starting receiver thread");
|
||||
|
||||
bool ok = sc_thread_create(&receiver->thread, run_receiver,
|
||||
@ -122,6 +122,6 @@ sc_receiver_start(struct sc_receiver *receiver) {
|
||||
}
|
||||
|
||||
void
|
||||
sc_receiver_join(struct sc_receiver *receiver) {
|
||||
receiver_join(struct receiver *receiver) {
|
||||
sc_thread_join(&receiver->thread, NULL);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
// receive events from the device
|
||||
// managed by the controller
|
||||
struct sc_receiver {
|
||||
struct receiver {
|
||||
sc_socket control_socket;
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
@ -20,18 +20,18 @@ struct sc_receiver {
|
||||
};
|
||||
|
||||
bool
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync);
|
||||
receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync);
|
||||
|
||||
void
|
||||
sc_receiver_destroy(struct sc_receiver *receiver);
|
||||
receiver_destroy(struct receiver *receiver);
|
||||
|
||||
bool
|
||||
sc_receiver_start(struct sc_receiver *receiver);
|
||||
receiver_start(struct receiver *receiver);
|
||||
|
||||
// no sc_receiver_stop(), it will automatically stop on control_socket shutdown
|
||||
// no receiver_stop(), it will automatically stop on control_socket shutdown
|
||||
|
||||
void
|
||||
sc_receiver_join(struct sc_receiver *receiver);
|
||||
receiver_join(struct receiver *receiver);
|
||||
|
||||
#endif
|
||||
|
@ -8,11 +8,8 @@
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
|
||||
/** Downcast packet sinks to recorder */
|
||||
#define DOWNCAST_VIDEO(SINK) \
|
||||
container_of(SINK, struct sc_recorder, video_packet_sink)
|
||||
#define DOWNCAST_AUDIO(SINK) \
|
||||
container_of(SINK, struct sc_recorder, audio_packet_sink)
|
||||
/** Downcast packet_sink to recorder */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
|
||||
@ -33,27 +30,41 @@ find_muxer(const char *name) {
|
||||
return oformat;
|
||||
}
|
||||
|
||||
static AVPacket *
|
||||
sc_recorder_packet_ref(const AVPacket *packet) {
|
||||
AVPacket *p = av_packet_alloc();
|
||||
if (!p) {
|
||||
static struct sc_record_packet *
|
||||
sc_record_packet_new(const AVPacket *packet) {
|
||||
struct sc_record_packet *rec = malloc(sizeof(*rec));
|
||||
if (!rec) {
|
||||
LOG_OOM();
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (av_packet_ref(p, packet)) {
|
||||
av_packet_free(&p);
|
||||
rec->packet = av_packet_alloc();
|
||||
if (!rec->packet) {
|
||||
LOG_OOM();
|
||||
free(rec);
|
||||
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
|
||||
sc_recorder_queue_clear(struct sc_recorder_queue *queue) {
|
||||
while (!sc_vecdeque_is_empty(queue)) {
|
||||
AVPacket *p = sc_vecdeque_pop(queue);
|
||||
av_packet_free(&p);
|
||||
while (!sc_queue_is_empty(queue)) {
|
||||
struct sc_record_packet *rec;
|
||||
sc_queue_take(queue, next, &rec);
|
||||
sc_record_packet_delete(rec);
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,7 +78,9 @@ sc_recorder_get_format_name(enum sc_record_format format) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
|
||||
sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
AVStream *ostream = recorder->ctx->streams[0];
|
||||
|
||||
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
||||
if (!extradata) {
|
||||
LOG_OOM();
|
||||
@ -79,56 +92,170 @@ sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
|
||||
|
||||
ostream->codecpar->extradata = extradata;
|
||||
ostream->codecpar->extradata_size = packet->size;
|
||||
|
||||
int ret = avformat_write_header(recorder->ctx, NULL);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write header to %s", recorder->filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline void
|
||||
sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) {
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base);
|
||||
static void
|
||||
sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
AVStream *ostream = recorder->ctx->streams[0];
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index,
|
||||
AVPacket *packet) {
|
||||
AVStream *stream = recorder->ctx->streams[stream_index];
|
||||
sc_recorder_rescale_packet(stream, packet);
|
||||
return av_interleaved_write_frame(recorder->ctx, packet) >= 0;
|
||||
sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
if (!recorder->header_written) {
|
||||
if (packet->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first packet is not a config packet");
|
||||
return false;
|
||||
}
|
||||
bool ok = sc_recorder_write_header(recorder, packet);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
recorder->header_written = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (packet->pts == AV_NOPTS_VALUE) {
|
||||
// ignore config packets
|
||||
return true;
|
||||
}
|
||||
|
||||
sc_recorder_rescale_packet(recorder, packet);
|
||||
return av_write_frame(recorder->ctx, packet) >= 0;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return sc_recorder_write_stream(recorder, recorder->video_stream_index,
|
||||
packet);
|
||||
}
|
||||
static int
|
||||
run_recorder(void *data) {
|
||||
struct sc_recorder *recorder = data;
|
||||
|
||||
static inline bool
|
||||
sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return sc_recorder_write_stream(recorder, recorder->audio_stream_index,
|
||||
packet);
|
||||
for (;;) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
|
||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
// if stopped is set, continue to process the remaining events (to
|
||||
// finish the recording) before actually stopping
|
||||
|
||||
if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
struct sc_record_packet *last = recorder->previous;
|
||||
if (last) {
|
||||
// assign an arbitrary duration to the last packet
|
||||
last->packet->duration = 100000;
|
||||
bool ok = sc_recorder_write(recorder, last->packet);
|
||||
if (!ok) {
|
||||
// failing to write the last frame is not very serious, no
|
||||
// future frame may depend on it, so the resulting file
|
||||
// will still be valid
|
||||
LOGW("Could not record last packet");
|
||||
}
|
||||
sc_record_packet_delete(last);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
struct sc_record_packet *rec;
|
||||
sc_queue_take(&recorder->queue, next, &rec);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
// recorder->previous is only written from this thread, no need to lock
|
||||
struct sc_record_packet *previous = recorder->previous;
|
||||
recorder->previous = rec;
|
||||
|
||||
if (!previous) {
|
||||
// we just received the first packet
|
||||
continue;
|
||||
}
|
||||
|
||||
// config packets have no PTS, we must ignore them
|
||||
if (rec->packet->pts != AV_NOPTS_VALUE
|
||||
&& previous->packet->pts != AV_NOPTS_VALUE) {
|
||||
// we now know the duration of the previous packet
|
||||
previous->packet->duration =
|
||||
rec->packet->pts - previous->packet->pts;
|
||||
}
|
||||
|
||||
bool ok = sc_recorder_write(recorder, previous->packet);
|
||||
sc_record_packet_delete(previous);
|
||||
if (!ok) {
|
||||
LOGE("Could not record packet");
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->failed = true;
|
||||
// discard pending packets
|
||||
sc_recorder_queue_clear(&recorder->queue);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!recorder->failed) {
|
||||
if (recorder->header_written) {
|
||||
int ret = av_write_trailer(recorder->ctx);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||
recorder->failed = true;
|
||||
}
|
||||
} else {
|
||||
// the recorded file is empty
|
||||
recorder->failed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (recorder->failed) {
|
||||
LOGE("Recording failed to %s", recorder->filename);
|
||||
} else {
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
LOGI("Recording complete to %s file: %s", format_name,
|
||||
recorder->filename);
|
||||
}
|
||||
|
||||
LOGD("Recorder thread ended");
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_open_output_file(struct sc_recorder *recorder) {
|
||||
sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
|
||||
bool ok = sc_mutex_init(&recorder->mutex);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->queue_cond);
|
||||
if (!ok) {
|
||||
goto error_mutex_destroy;
|
||||
}
|
||||
|
||||
sc_queue_init(&recorder->queue);
|
||||
recorder->stopped = false;
|
||||
recorder->failed = false;
|
||||
recorder->header_written = false;
|
||||
recorder->previous = NULL;
|
||||
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
assert(format_name);
|
||||
const AVOutputFormat *format = find_muxer(format_name);
|
||||
if (!format) {
|
||||
LOGE("Could not find muxer");
|
||||
return false;
|
||||
goto error_cond_destroy;
|
||||
}
|
||||
|
||||
recorder->ctx = avformat_alloc_context();
|
||||
if (!recorder->ctx) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
|
||||
AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to open output file: %s", recorder->filename);
|
||||
avformat_free_context(recorder->ctx);
|
||||
return false;
|
||||
goto error_cond_destroy;
|
||||
}
|
||||
|
||||
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
|
||||
@ -140,398 +267,83 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) {
|
||||
av_dict_set(&recorder->ctx->metadata, "comment",
|
||||
"Recorded by scrcpy " SCRCPY_VERSION, 0);
|
||||
|
||||
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
|
||||
if (!ostream) {
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
ostream->codecpar->codec_id = input_codec->id;
|
||||
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||
ostream->codecpar->width = recorder->declared_frame_size.width;
|
||||
ostream->codecpar->height = recorder->declared_frame_size.height;
|
||||
|
||||
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
|
||||
AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to open output file: %s", recorder->filename);
|
||||
// ostream will be cleaned up during context cleaning
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
LOGD("Starting recorder thread");
|
||||
ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder",
|
||||
recorder);
|
||||
if (!ok) {
|
||||
LOGE("Could not start recorder thread");
|
||||
goto error_avio_close;
|
||||
}
|
||||
|
||||
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_close_output_file(struct sc_recorder *recorder) {
|
||||
error_avio_close:
|
||||
avio_close(recorder->ctx->pb);
|
||||
error_avformat_free_context:
|
||||
avformat_free_context(recorder->ctx);
|
||||
}
|
||||
error_cond_destroy:
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
error_mutex_destroy:
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
|
||||
static inline bool
|
||||
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
|
||||
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
|
||||
// The video queue is empty
|
||||
return true;
|
||||
}
|
||||
|
||||
if (recorder->audio && sc_vecdeque_is_empty(&recorder->audio_queue)) {
|
||||
// The audio queue is empty (when audio is enabled)
|
||||
return true;
|
||||
}
|
||||
|
||||
// No queue is empty
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_process_header(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped && (!recorder->video_init
|
||||
|| !recorder->audio_init
|
||||
|| sc_recorder_has_empty_queues(recorder))) {
|
||||
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
|
||||
assert(recorder->stopped);
|
||||
// If the recorder is stopped, don't process anything if there are not
|
||||
// at least video packets
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue);
|
||||
|
||||
AVPacket *audio_pkt = NULL;
|
||||
if (!sc_vecdeque_is_empty(&recorder->audio_queue)) {
|
||||
assert(recorder->audio);
|
||||
audio_pkt = sc_vecdeque_pop(&recorder->audio_queue);
|
||||
}
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
int ret = false;
|
||||
|
||||
if (video_pkt->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first video packet is not a config packet");
|
||||
goto end;
|
||||
}
|
||||
|
||||
assert(recorder->video_stream_index >= 0);
|
||||
AVStream *video_stream =
|
||||
recorder->ctx->streams[recorder->video_stream_index];
|
||||
bool ok = sc_recorder_set_extradata(video_stream, video_pkt);
|
||||
if (!ok) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (audio_pkt) {
|
||||
if (audio_pkt->pts != AV_NOPTS_VALUE) {
|
||||
LOGE("The first audio packet is not a config packet");
|
||||
goto end;
|
||||
}
|
||||
|
||||
assert(recorder->audio_stream_index >= 0);
|
||||
AVStream *audio_stream =
|
||||
recorder->ctx->streams[recorder->audio_stream_index];
|
||||
ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
|
||||
if (!ok) {
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
ok = avformat_write_header(recorder->ctx, NULL) >= 0;
|
||||
if (!ok) {
|
||||
LOGE("Failed to write header to %s", recorder->filename);
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = true;
|
||||
|
||||
end:
|
||||
av_packet_free(&video_pkt);
|
||||
if (audio_pkt) {
|
||||
av_packet_free(&audio_pkt);
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_process_packets(struct sc_recorder *recorder) {
|
||||
int64_t pts_origin = AV_NOPTS_VALUE;
|
||||
|
||||
bool header_written = sc_recorder_process_header(recorder);
|
||||
if (!header_written) {
|
||||
return false;
|
||||
}
|
||||
|
||||
AVPacket *video_pkt = NULL;
|
||||
AVPacket *audio_pkt = NULL;
|
||||
|
||||
// We can write a video packet only once we received the next one so that
|
||||
// we can set its duration (next_pts - current_pts)
|
||||
AVPacket *video_pkt_previous = NULL;
|
||||
|
||||
bool error = false;
|
||||
|
||||
for (;;) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped) {
|
||||
if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) {
|
||||
// A new packet may be assigned to video_pkt and be processed
|
||||
break;
|
||||
}
|
||||
if (recorder->audio && !audio_pkt
|
||||
&& !sc_vecdeque_is_empty(&recorder->audio_queue)) {
|
||||
// A new packet may be assigned to audio_pkt and be processed
|
||||
break;
|
||||
}
|
||||
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
// If stopped is set, continue to process the remaining events (to
|
||||
// finish the recording) before actually stopping.
|
||||
|
||||
// If there is no audio, then the audio_queue will remain empty forever
|
||||
// and audio_pkt will always be NULL.
|
||||
assert(recorder->audio || (!audio_pkt
|
||||
&& sc_vecdeque_is_empty(&recorder->audio_queue)));
|
||||
|
||||
if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) {
|
||||
video_pkt = sc_vecdeque_pop(&recorder->video_queue);
|
||||
}
|
||||
|
||||
if (!audio_pkt && !sc_vecdeque_is_empty(&recorder->audio_queue)) {
|
||||
audio_pkt = sc_vecdeque_pop(&recorder->audio_queue);
|
||||
}
|
||||
|
||||
if (recorder->stopped && !video_pkt && !audio_pkt) {
|
||||
assert(sc_vecdeque_is_empty(&recorder->video_queue));
|
||||
assert(sc_vecdeque_is_empty(&recorder->audio_queue));
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
break;
|
||||
}
|
||||
|
||||
assert(video_pkt || audio_pkt); // at least one
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
// Ignore further config packets (e.g. on device orientation
|
||||
// change). The next non-config packet will have the config packet
|
||||
// data prepended.
|
||||
if (video_pkt && video_pkt->pts == AV_NOPTS_VALUE) {
|
||||
av_packet_free(&video_pkt);
|
||||
video_pkt = NULL;
|
||||
}
|
||||
|
||||
if (audio_pkt && audio_pkt->pts == AV_NOPTS_VALUE) {
|
||||
av_packet_free(&audio_pkt);
|
||||
audio_pkt = NULL;
|
||||
}
|
||||
|
||||
if (pts_origin == AV_NOPTS_VALUE) {
|
||||
if (!recorder->audio) {
|
||||
assert(video_pkt);
|
||||
pts_origin = video_pkt->pts;
|
||||
} else if (video_pkt && audio_pkt) {
|
||||
pts_origin = MIN(video_pkt->pts, audio_pkt->pts);
|
||||
} else if (recorder->stopped) {
|
||||
if (video_pkt) {
|
||||
// The recorder is stopped without audio, record the video
|
||||
// packets
|
||||
pts_origin = video_pkt->pts;
|
||||
} else {
|
||||
// Fail if there is no video
|
||||
error = true;
|
||||
goto end;
|
||||
}
|
||||
} else {
|
||||
// We need both video and audio packets to initialize pts_origin
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
assert(pts_origin != AV_NOPTS_VALUE);
|
||||
|
||||
if (video_pkt) {
|
||||
video_pkt->pts -= pts_origin;
|
||||
video_pkt->dts = video_pkt->pts;
|
||||
|
||||
if (video_pkt_previous) {
|
||||
// we now know the duration of the previous packet
|
||||
video_pkt_previous->duration = video_pkt->pts
|
||||
- video_pkt_previous->pts;
|
||||
|
||||
bool ok = sc_recorder_write_video(recorder, video_pkt_previous);
|
||||
av_packet_free(&video_pkt_previous);
|
||||
if (!ok) {
|
||||
LOGE("Could not record video packet");
|
||||
error = true;
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
video_pkt_previous = video_pkt;
|
||||
video_pkt = NULL;
|
||||
}
|
||||
|
||||
if (audio_pkt) {
|
||||
audio_pkt->pts -= pts_origin;
|
||||
audio_pkt->dts = audio_pkt->pts;
|
||||
|
||||
bool ok = sc_recorder_write_audio(recorder, audio_pkt);
|
||||
if (!ok) {
|
||||
LOGE("Could not record audio packet");
|
||||
error = true;
|
||||
goto end;
|
||||
}
|
||||
|
||||
av_packet_free(&audio_pkt);
|
||||
audio_pkt = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
// Write the last video packet
|
||||
AVPacket *last = video_pkt_previous;
|
||||
if (last) {
|
||||
// assign an arbitrary duration to the last packet
|
||||
last->duration = 100000;
|
||||
bool ok = sc_recorder_write_video(recorder, last);
|
||||
if (!ok) {
|
||||
// failing to write the last frame is not very serious, no
|
||||
// future frame may depend on it, so the resulting file
|
||||
// will still be valid
|
||||
LOGW("Could not record last packet");
|
||||
}
|
||||
av_packet_free(&last);
|
||||
}
|
||||
|
||||
int ret = av_write_trailer(recorder->ctx);
|
||||
if (ret < 0) {
|
||||
LOGE("Failed to write trailer to %s", recorder->filename);
|
||||
error = false;
|
||||
}
|
||||
|
||||
end:
|
||||
if (video_pkt) {
|
||||
av_packet_free(&video_pkt);
|
||||
}
|
||||
if (audio_pkt) {
|
||||
av_packet_free(&audio_pkt);
|
||||
}
|
||||
|
||||
return !error;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_record(struct sc_recorder *recorder) {
|
||||
bool ok = sc_recorder_open_output_file(recorder);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_recorder_process_packets(recorder);
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static int
|
||||
run_recorder(void *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);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// Prevent the producer to push any new packet
|
||||
recorder->stopped = true;
|
||||
// Discard pending packets
|
||||
sc_recorder_queue_clear(&recorder->video_queue);
|
||||
sc_recorder_queue_clear(&recorder->audio_queue);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (success) {
|
||||
const char *format_name = sc_recorder_get_format_name(recorder->format);
|
||||
LOGI("Recording complete to %s file: %s", format_name,
|
||||
recorder->filename);
|
||||
} else {
|
||||
LOGE("Recording failed to %s", recorder->filename);
|
||||
}
|
||||
|
||||
LOGD("Recorder thread ended");
|
||||
|
||||
recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
AVCodecContext *ctx) {
|
||||
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);
|
||||
if (recorder->stopped) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
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->video_stream_index = stream->index;
|
||||
|
||||
recorder->video_init = true;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
// only written from this thread, no need to lock
|
||||
assert(recorder->video_init);
|
||||
|
||||
sc_recorder_close(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
sc_thread_join(&recorder->thread, NULL);
|
||||
|
||||
avio_close(recorder->ctx->pb);
|
||||
avformat_free_context(recorder->ctx);
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
// only written from this thread, no need to lock
|
||||
assert(recorder->video_init);
|
||||
|
||||
sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
assert(!recorder->stopped);
|
||||
|
||||
if (recorder->stopped) {
|
||||
// reject any new packet
|
||||
if (recorder->failed) {
|
||||
// reject any new packet (this will stop the stream)
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
AVPacket *rec = sc_recorder_packet_ref(packet);
|
||||
struct sc_record_packet *rec = sc_record_packet_new(packet);
|
||||
if (!rec) {
|
||||
LOG_OOM();
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
rec->stream_index = recorder->video_stream_index;
|
||||
|
||||
bool ok = sc_vecdeque_push(&recorder->video_queue, rec);
|
||||
if (!ok) {
|
||||
LOG_OOM();
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_queue_push(&recorder->queue, next, rec);
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
@ -539,208 +351,51 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
|
||||
AVCodecContext *ctx) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_init);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
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_mutex_unlock(&recorder->mutex);
|
||||
|
||||
return true;
|
||||
sc_recorder_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
return sc_recorder_open(recorder, codec);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(recorder->audio_init);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
sc_recorder_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
sc_recorder_close(recorder);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(recorder->audio_init);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
if (recorder->stopped) {
|
||||
// reject any new packet
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
AVPacket *rec = sc_recorder_packet_ref(packet);
|
||||
if (!rec) {
|
||||
LOG_OOM();
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
rec->stream_index = recorder->audio_stream_index;
|
||||
|
||||
bool ok = sc_vecdeque_push(&recorder->audio_queue, rec);
|
||||
if (!ok) {
|
||||
LOG_OOM();
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return true;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_init);
|
||||
|
||||
LOGW("Audio stream recording disabled");
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->audio = false;
|
||||
recorder->audio_init = true;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
sc_recorder_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
return sc_recorder_push(recorder, packet);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool audio,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
|
||||
sc_recorder_init(struct sc_recorder *recorder,
|
||||
const char *filename,
|
||||
enum sc_record_format format,
|
||||
struct sc_size declared_frame_size) {
|
||||
recorder->filename = strdup(filename);
|
||||
if (!recorder->filename) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool ok = sc_mutex_init(&recorder->mutex);
|
||||
if (!ok) {
|
||||
goto error_free_filename;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->queue_cond);
|
||||
if (!ok) {
|
||||
goto error_mutex_destroy;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&recorder->stream_cond);
|
||||
if (!ok) {
|
||||
goto error_queue_cond_destroy;
|
||||
}
|
||||
|
||||
recorder->audio = audio;
|
||||
|
||||
sc_vecdeque_init(&recorder->video_queue);
|
||||
sc_vecdeque_init(&recorder->audio_queue);
|
||||
recorder->stopped = false;
|
||||
|
||||
recorder->video_init = false;
|
||||
recorder->audio_init = false;
|
||||
|
||||
recorder->video_stream_index = -1;
|
||||
recorder->audio_stream_index = -1;
|
||||
|
||||
recorder->format = format;
|
||||
recorder->declared_frame_size = declared_frame_size;
|
||||
|
||||
assert(cbs && cbs->on_ended);
|
||||
recorder->cbs = cbs;
|
||||
recorder->cbs_userdata = cbs_userdata;
|
||||
|
||||
static const struct sc_packet_sink_ops video_ops = {
|
||||
.open = sc_recorder_video_packet_sink_open,
|
||||
.close = sc_recorder_video_packet_sink_close,
|
||||
.push = sc_recorder_video_packet_sink_push,
|
||||
static const struct sc_packet_sink_ops ops = {
|
||||
.open = sc_recorder_packet_sink_open,
|
||||
.close = sc_recorder_packet_sink_close,
|
||||
.push = sc_recorder_packet_sink_push,
|
||||
};
|
||||
|
||||
recorder->video_packet_sink.ops = &video_ops;
|
||||
|
||||
if (audio) {
|
||||
static const struct sc_packet_sink_ops audio_ops = {
|
||||
.open = sc_recorder_audio_packet_sink_open,
|
||||
.close = sc_recorder_audio_packet_sink_close,
|
||||
.push = sc_recorder_audio_packet_sink_push,
|
||||
.disable = sc_recorder_audio_packet_sink_disable,
|
||||
};
|
||||
|
||||
recorder->audio_packet_sink.ops = &audio_ops;
|
||||
}
|
||||
recorder->packet_sink.ops = &ops;
|
||||
|
||||
return true;
|
||||
|
||||
error_queue_cond_destroy:
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
error_mutex_destroy:
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
error_free_filename:
|
||||
free(recorder->filename);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_recorder_start(struct sc_recorder *recorder) {
|
||||
bool ok = sc_thread_create(&recorder->thread, run_recorder,
|
||||
"scrcpy-recorder", recorder);
|
||||
if (!ok) {
|
||||
LOGE("Could not start recorder thread");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_stop(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_join(struct sc_recorder *recorder) {
|
||||
sc_thread_join(&recorder->thread, NULL);
|
||||
}
|
||||
|
||||
void
|
||||
sc_recorder_destroy(struct sc_recorder *recorder) {
|
||||
sc_cond_destroy(&recorder->stream_cond);
|
||||
sc_cond_destroy(&recorder->queue_cond);
|
||||
sc_mutex_destroy(&recorder->mutex);
|
||||
free(recorder->filename);
|
||||
}
|
||||
|
@ -9,67 +9,43 @@
|
||||
#include "coords.h"
|
||||
#include "options.h"
|
||||
#include "trait/packet_sink.h"
|
||||
#include "util/queue.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_packet_sink video_packet_sink;
|
||||
struct sc_packet_sink audio_packet_sink;
|
||||
|
||||
/* The audio flag is unprotected:
|
||||
* - it is initialized from sc_recorder_init() from the main thread;
|
||||
* - it may be reset once from the recorder thread if the audio is
|
||||
* disabled dynamically.
|
||||
*
|
||||
* Therefore, once the recorder thread is started, only the recorder thread
|
||||
* may access it without data races.
|
||||
*/
|
||||
bool audio;
|
||||
struct sc_packet_sink packet_sink; // packet sink trait
|
||||
|
||||
char *filename;
|
||||
enum sc_record_format format;
|
||||
AVFormatContext *ctx;
|
||||
struct sc_size declared_frame_size;
|
||||
bool header_written;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
sc_cond queue_cond;
|
||||
// set on sc_recorder_stop(), packet_sink close or recording failure
|
||||
bool stopped;
|
||||
struct sc_recorder_queue video_queue;
|
||||
struct sc_recorder_queue audio_queue;
|
||||
bool stopped; // set on recorder_close()
|
||||
bool failed; // set on packet write failure
|
||||
struct sc_recorder_queue queue;
|
||||
|
||||
// wake up the recorder thread once the video or audio codec is known
|
||||
sc_cond stream_cond;
|
||||
bool video_init;
|
||||
bool audio_init;
|
||||
|
||||
int video_stream_index;
|
||||
int audio_stream_index;
|
||||
|
||||
const struct sc_recorder_callbacks *cbs;
|
||||
void *cbs_userdata;
|
||||
};
|
||||
|
||||
struct sc_recorder_callbacks {
|
||||
void (*on_ended)(struct sc_recorder *recorder, bool success,
|
||||
void *userdata);
|
||||
// we can write a packet only once we received the next one so that we can
|
||||
// set its duration (next_pts - current_pts)
|
||||
// "previous" is only accessed from the recorder thread, so it does not
|
||||
// need to be protected by the mutex
|
||||
struct sc_record_packet *previous;
|
||||
};
|
||||
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool audio,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
bool
|
||||
sc_recorder_start(struct sc_recorder *recorder);
|
||||
|
||||
void
|
||||
sc_recorder_stop(struct sc_recorder *recorder);
|
||||
|
||||
void
|
||||
sc_recorder_join(struct sc_recorder *recorder);
|
||||
enum sc_record_format format,
|
||||
struct sc_size declared_frame_size);
|
||||
|
||||
void
|
||||
sc_recorder_destroy(struct sc_recorder *recorder);
|
||||
|
290
app/src/scrcpy.c
290
app/src/scrcpy.c
@ -13,10 +13,8 @@
|
||||
# include <windows.h>
|
||||
#endif
|
||||
|
||||
#include "audio_player.h"
|
||||
#include "controller.h"
|
||||
#include "decoder.h"
|
||||
#include "delay_buffer.h"
|
||||
#include "demuxer.h"
|
||||
#include "events.h"
|
||||
#include "file_pusher.h"
|
||||
@ -42,16 +40,11 @@
|
||||
struct scrcpy {
|
||||
struct sc_server server;
|
||||
struct sc_screen screen;
|
||||
struct sc_audio_player audio_player;
|
||||
struct sc_demuxer video_demuxer;
|
||||
struct sc_demuxer audio_demuxer;
|
||||
struct sc_decoder video_decoder;
|
||||
struct sc_decoder audio_decoder;
|
||||
struct sc_demuxer demuxer;
|
||||
struct sc_decoder decoder;
|
||||
struct sc_recorder recorder;
|
||||
struct sc_delay_buffer display_buffer;
|
||||
#ifdef HAVE_V4L2
|
||||
struct sc_v4l2_sink v4l2_sink;
|
||||
struct sc_delay_buffer v4l2_buffer;
|
||||
#endif
|
||||
struct sc_controller controller;
|
||||
struct sc_file_pusher file_pusher;
|
||||
@ -162,22 +155,14 @@ event_loop(struct scrcpy *s) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SC_EVENT_DEVICE_DISCONNECTED:
|
||||
case EVENT_STREAM_STOPPED:
|
||||
LOGW("Device disconnected");
|
||||
return SCRCPY_EXIT_DISCONNECTED;
|
||||
case SC_EVENT_DEMUXER_ERROR:
|
||||
LOGE("Demuxer error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
case SC_EVENT_RECORDER_ERROR:
|
||||
LOGE("Recorder error");
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
case SDL_QUIT:
|
||||
LOGD("User requested to quit");
|
||||
return SCRCPY_EXIT_SUCCESS;
|
||||
default:
|
||||
if (!sc_screen_handle_event(&s->screen, &event)) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
}
|
||||
sc_screen_handle_event(&s->screen, &event);
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -191,16 +176,15 @@ await_for_server(bool *connected) {
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
if (connected) {
|
||||
*connected = false;
|
||||
}
|
||||
LOGD("User requested to quit");
|
||||
*connected = false;
|
||||
return true;
|
||||
case SC_EVENT_SERVER_CONNECTION_FAILED:
|
||||
case EVENT_SERVER_CONNECTION_FAILED:
|
||||
LOGE("Server connection failed");
|
||||
return false;
|
||||
case SC_EVENT_SERVER_CONNECTED:
|
||||
if (connected) {
|
||||
*connected = true;
|
||||
}
|
||||
case EVENT_SERVER_CONNECTED:
|
||||
LOGD("Server connected");
|
||||
*connected = true;
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
@ -211,47 +195,49 @@ await_for_server(bool *connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
|
||||
void *userdata) {
|
||||
(void) recorder;
|
||||
(void) userdata;
|
||||
|
||||
if (!success) {
|
||||
PUSH_EVENT(SC_EVENT_RECORDER_ERROR);
|
||||
static SDL_LogPriority
|
||||
sdl_priority_from_av_level(int level) {
|
||||
switch (level) {
|
||||
case AV_LOG_PANIC:
|
||||
case AV_LOG_FATAL:
|
||||
return SDL_LOG_PRIORITY_CRITICAL;
|
||||
case AV_LOG_ERROR:
|
||||
return SDL_LOG_PRIORITY_ERROR;
|
||||
case AV_LOG_WARNING:
|
||||
return SDL_LOG_PRIORITY_WARN;
|
||||
case AV_LOG_INFO:
|
||||
return SDL_LOG_PRIORITY_INFO;
|
||||
}
|
||||
// do not forward others, which are too verbose
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer,
|
||||
enum sc_demuxer_status status, void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
// The device may not decide to disable the video
|
||||
assert(status != SC_DEMUXER_STATUS_DISABLED);
|
||||
|
||||
if (status == SC_DEMUXER_STATUS_EOS) {
|
||||
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
||||
} else {
|
||||
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||
(void) avcl;
|
||||
SDL_LogPriority priority = sdl_priority_from_av_level(level);
|
||||
if (priority == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t fmt_len = strlen(fmt);
|
||||
char *local_fmt = malloc(fmt_len + 10);
|
||||
if (!local_fmt) {
|
||||
LOG_OOM();
|
||||
return;
|
||||
}
|
||||
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
|
||||
memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0'
|
||||
SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl);
|
||||
free(local_fmt);
|
||||
}
|
||||
|
||||
static void
|
||||
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
|
||||
enum sc_demuxer_status status, void *userdata) {
|
||||
sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
const struct scrcpy_options *options = userdata;
|
||||
|
||||
// Contrary to the video demuxer, keep mirroring if only the audio fails
|
||||
// (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(EVENT_STREAM_STOPPED);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -259,7 +245,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED);
|
||||
PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -267,7 +253,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(SC_EVENT_SERVER_CONNECTED);
|
||||
PUSH_EVENT(EVENT_SERVER_CONNECTED);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -280,9 +266,8 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
|
||||
// event
|
||||
}
|
||||
|
||||
// Generate a scrcpy id to differentiate multiple running scrcpy instances
|
||||
static uint32_t
|
||||
scrcpy_generate_scid() {
|
||||
scrcpy_generate_uid() {
|
||||
struct sc_rand rand;
|
||||
sc_rand_init(&rand);
|
||||
// Only use 31 bits to avoid issues with signed values on the Java-side
|
||||
@ -307,12 +292,10 @@ scrcpy(struct scrcpy_options *options) {
|
||||
bool server_started = false;
|
||||
bool file_pusher_initialized = false;
|
||||
bool recorder_initialized = false;
|
||||
bool recorder_started = false;
|
||||
#ifdef HAVE_V4L2
|
||||
bool v4l2_sink_initialized = false;
|
||||
#endif
|
||||
bool video_demuxer_started = false;
|
||||
bool audio_demuxer_started = false;
|
||||
bool demuxer_started = false;
|
||||
#ifdef HAVE_USB
|
||||
bool aoa_hid_initialized = false;
|
||||
bool hid_keyboard_initialized = false;
|
||||
@ -324,34 +307,28 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
struct sc_acksync *acksync = NULL;
|
||||
|
||||
uint32_t scid = scrcpy_generate_scid();
|
||||
uint32_t uid = scrcpy_generate_uid();
|
||||
|
||||
struct sc_server_params params = {
|
||||
.scid = scid,
|
||||
.uid = uid,
|
||||
.req_serial = options->serial,
|
||||
.select_usb = options->select_usb,
|
||||
.select_tcpip = options->select_tcpip,
|
||||
.log_level = options->log_level,
|
||||
.video_codec = options->video_codec,
|
||||
.audio_codec = options->audio_codec,
|
||||
.crop = options->crop,
|
||||
.port_range = options->port_range,
|
||||
.tunnel_host = options->tunnel_host,
|
||||
.tunnel_port = options->tunnel_port,
|
||||
.max_size = options->max_size,
|
||||
.video_bit_rate = options->video_bit_rate,
|
||||
.audio_bit_rate = options->audio_bit_rate,
|
||||
.bit_rate = options->bit_rate,
|
||||
.max_fps = options->max_fps,
|
||||
.lock_video_orientation = options->lock_video_orientation,
|
||||
.control = options->control,
|
||||
.display_id = options->display_id,
|
||||
.audio = options->audio,
|
||||
.show_touches = options->show_touches,
|
||||
.stay_awake = options->stay_awake,
|
||||
.video_codec_options = options->video_codec_options,
|
||||
.audio_codec_options = options->audio_codec_options,
|
||||
.video_encoder = options->video_encoder,
|
||||
.audio_encoder = options->audio_encoder,
|
||||
.codec_options = options->codec_options,
|
||||
.encoder_name = options->encoder_name,
|
||||
.force_adb_forward = options->force_adb_forward,
|
||||
.power_off_on_close = options->power_off_on_close,
|
||||
.clipboard_autosync = options->clipboard_autosync,
|
||||
@ -360,8 +337,6 @@ scrcpy(struct scrcpy_options *options) {
|
||||
.tcpip_dst = options->tcpip_dst,
|
||||
.cleanup = options->cleanup,
|
||||
.power_on = options->power_on,
|
||||
.list_encoders = options->list_encoders,
|
||||
.list_displays = options->list_displays,
|
||||
};
|
||||
|
||||
static const struct sc_server_callbacks cbs = {
|
||||
@ -379,27 +354,14 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
server_started = true;
|
||||
|
||||
if (options->list_encoders || options->list_displays) {
|
||||
bool ok = await_for_server(NULL);
|
||||
ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE;
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (options->display) {
|
||||
sdl_set_hints(options->render_driver);
|
||||
}
|
||||
|
||||
// Initialize SDL video in addition if display is enabled
|
||||
if (options->display) {
|
||||
if (SDL_Init(SDL_INIT_VIDEO)) {
|
||||
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (options->audio && SDL_Init(SDL_INIT_AUDIO)) {
|
||||
LOGE("Could not initialize SDL audio: %s", SDL_GetError());
|
||||
goto end;
|
||||
}
|
||||
if (options->display && SDL_Init(SDL_INIT_VIDEO)) {
|
||||
LOGE("Could not initialize SDL: %s", SDL_GetError());
|
||||
goto end;
|
||||
}
|
||||
|
||||
sdl_configure(options->display, options->disable_screensaver);
|
||||
@ -407,19 +369,15 @@ scrcpy(struct scrcpy_options *options) {
|
||||
// Await for server without blocking Ctrl+C handling
|
||||
bool connected;
|
||||
if (!await_for_server(&connected)) {
|
||||
LOGE("Server connection failed");
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!connected) {
|
||||
// This is not an error, user requested to quit
|
||||
LOGD("User requested to quit");
|
||||
ret = SCRCPY_EXIT_SUCCESS;
|
||||
goto end;
|
||||
}
|
||||
|
||||
LOGD("Server connected");
|
||||
|
||||
// It is necessarily initialized here, since the device is connected
|
||||
struct sc_server_info *info = &s->server.info;
|
||||
|
||||
@ -437,58 +395,41 @@ scrcpy(struct scrcpy_options *options) {
|
||||
file_pusher_initialized = true;
|
||||
}
|
||||
|
||||
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
|
||||
.on_ended = sc_video_demuxer_on_ended,
|
||||
};
|
||||
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
|
||||
&video_demuxer_cbs, NULL);
|
||||
|
||||
if (options->audio) {
|
||||
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
|
||||
.on_ended = sc_audio_demuxer_on_ended,
|
||||
};
|
||||
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
|
||||
&audio_demuxer_cbs, options);
|
||||
}
|
||||
|
||||
bool needs_video_decoder = options->display;
|
||||
bool needs_audio_decoder = options->audio && options->display;
|
||||
struct sc_decoder *dec = NULL;
|
||||
bool needs_decoder = options->display;
|
||||
#ifdef HAVE_V4L2
|
||||
needs_video_decoder |= !!options->v4l2_device;
|
||||
needs_decoder |= !!options->v4l2_device;
|
||||
#endif
|
||||
if (needs_video_decoder) {
|
||||
sc_decoder_init(&s->video_decoder, "video");
|
||||
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
|
||||
&s->video_decoder.packet_sink);
|
||||
}
|
||||
if (needs_audio_decoder) {
|
||||
sc_decoder_init(&s->audio_decoder, "audio");
|
||||
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
|
||||
&s->audio_decoder.packet_sink);
|
||||
if (needs_decoder) {
|
||||
sc_decoder_init(&s->decoder);
|
||||
dec = &s->decoder;
|
||||
}
|
||||
|
||||
struct sc_recorder *rec = NULL;
|
||||
if (options->record_filename) {
|
||||
static const struct sc_recorder_callbacks recorder_cbs = {
|
||||
.on_ended = sc_recorder_on_ended,
|
||||
};
|
||||
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
||||
options->record_format, options->audio,
|
||||
&recorder_cbs, NULL)) {
|
||||
if (!sc_recorder_init(&s->recorder,
|
||||
options->record_filename,
|
||||
options->record_format,
|
||||
info->frame_size)) {
|
||||
goto end;
|
||||
}
|
||||
rec = &s->recorder;
|
||||
recorder_initialized = true;
|
||||
}
|
||||
|
||||
if (!sc_recorder_start(&s->recorder)) {
|
||||
goto end;
|
||||
}
|
||||
recorder_started = true;
|
||||
av_log_set_callback(av_log_callback);
|
||||
|
||||
sc_packet_source_add_sink(&s->video_demuxer.packet_source,
|
||||
&s->recorder.video_packet_sink);
|
||||
if (options->audio) {
|
||||
sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
|
||||
&s->recorder.audio_packet_sink);
|
||||
}
|
||||
static const struct sc_demuxer_callbacks demuxer_cbs = {
|
||||
.on_eos = sc_demuxer_on_eos,
|
||||
};
|
||||
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
|
||||
|
||||
if (dec) {
|
||||
sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
|
||||
}
|
||||
|
||||
if (rec) {
|
||||
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
|
||||
}
|
||||
|
||||
struct sc_controller *controller = NULL;
|
||||
@ -660,6 +601,7 @@ aoa_hid_end:
|
||||
.clipboard_autosync = options->clipboard_autosync,
|
||||
.shortcut_mods = &options->shortcut_mods,
|
||||
.window_title = window_title,
|
||||
.frame_size = info->frame_size,
|
||||
.always_on_top = options->always_on_top,
|
||||
.window_x = options->window_x,
|
||||
.window_y = options->window_y,
|
||||
@ -670,6 +612,7 @@ aoa_hid_end:
|
||||
.mipmaps = options->mipmaps,
|
||||
.fullscreen = options->fullscreen,
|
||||
.start_fps_counter = options->start_fps_counter,
|
||||
.buffering_time = options->display_buffer,
|
||||
};
|
||||
|
||||
if (!sc_screen_init(&s->screen, &screen_params)) {
|
||||
@ -677,62 +620,34 @@ aoa_hid_end:
|
||||
}
|
||||
screen_initialized = true;
|
||||
|
||||
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
||||
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) {
|
||||
sc_audio_player_init(&s->audio_player, options->audio_buffer,
|
||||
options->audio_output_buffer);
|
||||
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
|
||||
&s->audio_player.frame_sink);
|
||||
}
|
||||
sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
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;
|
||||
}
|
||||
|
||||
struct sc_frame_source *src = &s->video_decoder.frame_source;
|
||||
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);
|
||||
sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);
|
||||
|
||||
v4l2_sink_initialized = true;
|
||||
}
|
||||
#endif
|
||||
|
||||
// now we consumed the header values, the socket receives the video stream
|
||||
// start the video demuxer
|
||||
if (!sc_demuxer_start(&s->video_demuxer)) {
|
||||
// start the demuxer
|
||||
if (!sc_demuxer_start(&s->demuxer)) {
|
||||
goto end;
|
||||
}
|
||||
video_demuxer_started = true;
|
||||
|
||||
if (options->audio) {
|
||||
if (!sc_demuxer_start(&s->audio_demuxer)) {
|
||||
goto end;
|
||||
}
|
||||
audio_demuxer_started = true;
|
||||
}
|
||||
demuxer_started = true;
|
||||
|
||||
ret = event_loop(s);
|
||||
LOGD("quit...");
|
||||
|
||||
// Close the window immediately on closing, because screen_destroy() may
|
||||
// only be called once the video demuxer thread is joined (it may take time)
|
||||
// only be called once the demuxer thread is joined (it may take time)
|
||||
sc_screen_hide_window(&s->screen);
|
||||
|
||||
end:
|
||||
@ -759,9 +674,6 @@ end:
|
||||
if (file_pusher_initialized) {
|
||||
sc_file_pusher_stop(&s->file_pusher);
|
||||
}
|
||||
if (recorder_initialized) {
|
||||
sc_recorder_stop(&s->recorder);
|
||||
}
|
||||
if (screen_initialized) {
|
||||
sc_screen_interrupt(&s->screen);
|
||||
}
|
||||
@ -773,12 +685,8 @@ end:
|
||||
|
||||
// now that the sockets are shutdown, the demuxer and controller are
|
||||
// interrupted, we can join them
|
||||
if (video_demuxer_started) {
|
||||
sc_demuxer_join(&s->video_demuxer);
|
||||
}
|
||||
|
||||
if (audio_demuxer_started) {
|
||||
sc_demuxer_join(&s->audio_demuxer);
|
||||
if (demuxer_started) {
|
||||
sc_demuxer_join(&s->demuxer);
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
@ -797,9 +705,8 @@ end:
|
||||
}
|
||||
#endif
|
||||
|
||||
// Destroy the screen only after the video demuxer is guaranteed to be
|
||||
// finished, because otherwise the screen could receive new frames after
|
||||
// destruction
|
||||
// Destroy the screen only after the demuxer is guaranteed to be finished,
|
||||
// because otherwise the screen could receive new frames after destruction
|
||||
if (screen_initialized) {
|
||||
sc_screen_join(&s->screen);
|
||||
sc_screen_destroy(&s->screen);
|
||||
@ -812,9 +719,6 @@ end:
|
||||
sc_controller_destroy(&s->controller);
|
||||
}
|
||||
|
||||
if (recorder_started) {
|
||||
sc_recorder_join(&s->recorder);
|
||||
}
|
||||
if (recorder_initialized) {
|
||||
sc_recorder_destroy(&s->recorder);
|
||||
}
|
||||
@ -824,10 +728,6 @@ end:
|
||||
sc_file_pusher_destroy(&s->file_pusher);
|
||||
}
|
||||
|
||||
if (server_started) {
|
||||
sc_server_join(&s->server);
|
||||
}
|
||||
|
||||
sc_server_destroy(&s->server);
|
||||
|
||||
return ret;
|
||||
|
309
app/src/screen.c
309
app/src/screen.c
@ -7,6 +7,7 @@
|
||||
#include "events.h"
|
||||
#include "icon.h"
|
||||
#include "options.h"
|
||||
#include "video_buffer.h"
|
||||
#include "util/log.h"
|
||||
|
||||
#define DISPLAY_MARGINS 96
|
||||
@ -239,6 +240,33 @@ sc_screen_update_content_rect(struct sc_screen *screen) {
|
||||
}
|
||||
}
|
||||
|
||||
static inline SDL_Texture *
|
||||
create_texture(struct sc_screen *screen) {
|
||||
SDL_Renderer *renderer = screen->renderer;
|
||||
struct sc_size size = screen->frame_size;
|
||||
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12,
|
||||
SDL_TEXTUREACCESS_STREAMING,
|
||||
size.width, size.height);
|
||||
if (!texture) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
if (screen->mipmaps) {
|
||||
struct sc_opengl *gl = &screen->gl;
|
||||
|
||||
SDL_GL_BindTexture(texture, NULL, NULL);
|
||||
|
||||
// Enable trilinear filtering for downscaling
|
||||
gl->TexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER,
|
||||
GL_LINEAR_MIPMAP_LINEAR);
|
||||
gl->TexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS, -1.f);
|
||||
|
||||
SDL_GL_UnbindTexture(texture);
|
||||
}
|
||||
|
||||
return texture;
|
||||
}
|
||||
|
||||
// render the texture to the renderer
|
||||
//
|
||||
// Set the update_content_rect flag if the window or content size may have
|
||||
@ -249,11 +277,35 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
|
||||
sc_screen_update_content_rect(screen);
|
||||
}
|
||||
|
||||
bool ok = sc_display_render(&screen->display, &screen->rect,
|
||||
screen->rotation);
|
||||
(void) ok; // error already logged
|
||||
SDL_RenderClear(screen->renderer);
|
||||
if (screen->rotation == 0) {
|
||||
SDL_RenderCopy(screen->renderer, screen->texture, NULL, &screen->rect);
|
||||
} else {
|
||||
// rotation in RenderCopyEx() is clockwise, while screen->rotation is
|
||||
// counterclockwise (to be consistent with --lock-video-orientation)
|
||||
int cw_rotation = (4 - screen->rotation) % 4;
|
||||
double angle = 90 * cw_rotation;
|
||||
|
||||
SDL_Rect *dstrect = NULL;
|
||||
SDL_Rect rect;
|
||||
if (screen->rotation & 1) {
|
||||
rect.x = screen->rect.x + (screen->rect.w - screen->rect.h) / 2;
|
||||
rect.y = screen->rect.y + (screen->rect.h - screen->rect.w) / 2;
|
||||
rect.w = screen->rect.h;
|
||||
rect.h = screen->rect.w;
|
||||
dstrect = ▭
|
||||
} else {
|
||||
assert(screen->rotation == 2);
|
||||
dstrect = &screen->rect;
|
||||
}
|
||||
|
||||
SDL_RenderCopyEx(screen->renderer, screen->texture, NULL, dstrect,
|
||||
angle, NULL, 0);
|
||||
}
|
||||
SDL_RenderPresent(screen->renderer);
|
||||
}
|
||||
|
||||
|
||||
#if defined(__APPLE__) || defined(__WINDOWS__)
|
||||
# define CONTINUOUS_RESIZING_WORKAROUND
|
||||
#endif
|
||||
@ -278,31 +330,9 @@ event_watcher(void *data, SDL_Event *event) {
|
||||
#endif
|
||||
|
||||
static bool
|
||||
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
|
||||
const AVCodecContext *ctx) {
|
||||
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||
(void) ctx;
|
||||
|
||||
sc_screen_frame_sink_open(struct sc_frame_sink *sink) {
|
||||
struct sc_screen *screen = DOWNCAST(sink);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
(void) screen;
|
||||
#ifndef NDEBUG
|
||||
screen->open = true;
|
||||
#endif
|
||||
@ -325,31 +355,43 @@ sc_screen_frame_sink_close(struct sc_frame_sink *sink) {
|
||||
static bool
|
||||
sc_screen_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||
struct sc_screen *screen = DOWNCAST(sink);
|
||||
return sc_video_buffer_push(&screen->vb, frame);
|
||||
}
|
||||
|
||||
bool previous_skipped;
|
||||
bool ok = sc_frame_buffer_push(&screen->fb, frame, &previous_skipped);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
static void
|
||||
sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
|
||||
void *userdata) {
|
||||
(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) {
|
||||
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
|
||||
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||
// this new frame instead
|
||||
// The EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||
// this new frame instead, unless the previous event failed
|
||||
need_new_event = screen->event_failed;
|
||||
} else {
|
||||
need_new_event = true;
|
||||
}
|
||||
|
||||
if (need_new_event) {
|
||||
static SDL_Event new_frame_event = {
|
||||
.type = SC_EVENT_NEW_FRAME,
|
||||
.type = EVENT_NEW_FRAME,
|
||||
};
|
||||
|
||||
// Post the event on the UI thread
|
||||
int ret = SDL_PushEvent(&new_frame_event);
|
||||
if (ret < 0) {
|
||||
LOGW("Could not post new frame event: %s", SDL_GetError());
|
||||
return false;
|
||||
screen->event_failed = true;
|
||||
} else {
|
||||
screen->event_failed = false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
@ -359,6 +401,7 @@ sc_screen_init(struct sc_screen *screen,
|
||||
screen->has_frame = false;
|
||||
screen->fullscreen = false;
|
||||
screen->maximized = false;
|
||||
screen->event_failed = false;
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
|
||||
screen->req.x = params->window_x;
|
||||
@ -368,19 +411,33 @@ sc_screen_init(struct sc_screen *screen,
|
||||
screen->req.fullscreen = params->fullscreen;
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sc_fps_counter_init(&screen->fps_counter)) {
|
||||
goto error_destroy_frame_buffer;
|
||||
ok = sc_video_buffer_start(&screen->vb);
|
||||
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;
|
||||
if (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
|
||||
| SDL_WINDOW_RESIZABLE
|
||||
@ -400,11 +457,46 @@ sc_screen_init(struct sc_screen *screen,
|
||||
goto error_destroy_fps_counter;
|
||||
}
|
||||
|
||||
ok = sc_display_init(&screen->display, screen->window, params->mipmaps);
|
||||
if (!ok) {
|
||||
screen->renderer = SDL_CreateRenderer(screen->window, -1,
|
||||
SDL_RENDERER_ACCELERATED);
|
||||
if (!screen->renderer) {
|
||||
LOGE("Could not create renderer: %s", SDL_GetError());
|
||||
goto error_destroy_window;
|
||||
}
|
||||
|
||||
SDL_RendererInfo renderer_info;
|
||||
int r = SDL_GetRendererInfo(screen->renderer, &renderer_info);
|
||||
const char *renderer_name = r ? NULL : renderer_info.name;
|
||||
LOGI("Renderer: %s", renderer_name ? renderer_name : "(unknown)");
|
||||
|
||||
screen->mipmaps = false;
|
||||
|
||||
// starts with "opengl"
|
||||
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
|
||||
if (use_opengl) {
|
||||
struct sc_opengl *gl = &screen->gl;
|
||||
sc_opengl_init(gl);
|
||||
|
||||
LOGI("OpenGL version: %s", gl->version);
|
||||
|
||||
if (params->mipmaps) {
|
||||
bool supports_mipmaps =
|
||||
sc_opengl_version_at_least(gl, 3, 0, /* OpenGL 3.0+ */
|
||||
2, 0 /* OpenGL ES 2.0+ */);
|
||||
if (supports_mipmaps) {
|
||||
LOGI("Trilinear filtering enabled");
|
||||
screen->mipmaps = true;
|
||||
} else {
|
||||
LOGW("Trilinear filtering disabled "
|
||||
"(OpenGL 3.0+ or ES 2.0+ required)");
|
||||
}
|
||||
} else {
|
||||
LOGI("Trilinear filtering disabled");
|
||||
}
|
||||
} else if (params->mipmaps) {
|
||||
LOGD("Trilinear filtering disabled (not an OpenGL renderer)");
|
||||
}
|
||||
|
||||
SDL_Surface *icon = scrcpy_icon_load();
|
||||
if (icon) {
|
||||
SDL_SetWindowIcon(screen->window, icon);
|
||||
@ -413,10 +505,18 @@ sc_screen_init(struct sc_screen *screen,
|
||||
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();
|
||||
if (!screen->frame) {
|
||||
LOG_OOM();
|
||||
goto error_destroy_display;
|
||||
goto error_destroy_texture;
|
||||
}
|
||||
|
||||
struct sc_input_manager_params im_params = {
|
||||
@ -451,14 +551,19 @@ sc_screen_init(struct sc_screen *screen,
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_display:
|
||||
sc_display_destroy(&screen->display);
|
||||
error_destroy_texture:
|
||||
SDL_DestroyTexture(screen->texture);
|
||||
error_destroy_renderer:
|
||||
SDL_DestroyRenderer(screen->renderer);
|
||||
error_destroy_window:
|
||||
SDL_DestroyWindow(screen->window);
|
||||
error_destroy_fps_counter:
|
||||
sc_fps_counter_destroy(&screen->fps_counter);
|
||||
error_destroy_frame_buffer:
|
||||
sc_frame_buffer_destroy(&screen->fb);
|
||||
error_stop_and_join_video_buffer:
|
||||
sc_video_buffer_stop(&screen->vb);
|
||||
sc_video_buffer_join(&screen->vb);
|
||||
error_destroy_video_buffer:
|
||||
sc_video_buffer_destroy(&screen->vb);
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -495,11 +600,13 @@ sc_screen_hide_window(struct sc_screen *screen) {
|
||||
|
||||
void
|
||||
sc_screen_interrupt(struct sc_screen *screen) {
|
||||
sc_video_buffer_stop(&screen->vb);
|
||||
sc_fps_counter_interrupt(&screen->fps_counter);
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_join(struct sc_screen *screen) {
|
||||
sc_video_buffer_join(&screen->vb);
|
||||
sc_fps_counter_join(&screen->fps_counter);
|
||||
}
|
||||
|
||||
@ -508,11 +615,12 @@ sc_screen_destroy(struct sc_screen *screen) {
|
||||
#ifndef NDEBUG
|
||||
assert(!screen->open);
|
||||
#endif
|
||||
sc_display_destroy(&screen->display);
|
||||
av_frame_free(&screen->frame);
|
||||
SDL_DestroyTexture(screen->texture);
|
||||
SDL_DestroyRenderer(screen->renderer);
|
||||
SDL_DestroyWindow(screen->window);
|
||||
sc_fps_counter_destroy(&screen->fps_counter);
|
||||
sc_frame_buffer_destroy(&screen->fb);
|
||||
sc_video_buffer_destroy(&screen->vb);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -572,44 +680,53 @@ sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) {
|
||||
sc_screen_render(screen, true);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_screen_init_size(struct sc_screen *screen) {
|
||||
// Before first frame
|
||||
assert(!screen->has_frame);
|
||||
|
||||
// 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;
|
||||
|
||||
return sc_display_set_texture_size(&screen->display, screen->frame_size);
|
||||
}
|
||||
|
||||
// recreate the texture and resize the window if the frame size has changed
|
||||
static bool
|
||||
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
|
||||
if (screen->frame_size.width == new_frame_size.width
|
||||
&& screen->frame_size.height == new_frame_size.height) {
|
||||
return true;
|
||||
if (screen->frame_size.width != new_frame_size.width
|
||||
|| screen->frame_size.height != new_frame_size.height) {
|
||||
// frame dimension changed, destroy texture
|
||||
SDL_DestroyTexture(screen->texture);
|
||||
|
||||
screen->frame_size = new_frame_size;
|
||||
|
||||
struct sc_size new_content_size =
|
||||
get_rotated_size(new_frame_size, screen->rotation);
|
||||
set_content_size(screen, new_content_size);
|
||||
|
||||
sc_screen_update_content_rect(screen);
|
||||
|
||||
LOGI("New texture: %" PRIu16 "x%" PRIu16,
|
||||
screen->frame_size.width, screen->frame_size.height);
|
||||
screen->texture = create_texture(screen);
|
||||
if (!screen->texture) {
|
||||
LOGE("Could not create texture: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// frame dimension changed
|
||||
screen->frame_size = new_frame_size;
|
||||
return true;
|
||||
}
|
||||
|
||||
struct sc_size new_content_size =
|
||||
get_rotated_size(new_frame_size, screen->rotation);
|
||||
set_content_size(screen, new_content_size);
|
||||
// write the frame into the texture
|
||||
static void
|
||||
update_texture(struct sc_screen *screen, const AVFrame *frame) {
|
||||
SDL_UpdateYUVTexture(screen->texture, NULL,
|
||||
frame->data[0], frame->linesize[0],
|
||||
frame->data[1], frame->linesize[1],
|
||||
frame->data[2], frame->linesize[2]);
|
||||
|
||||
sc_screen_update_content_rect(screen);
|
||||
|
||||
return sc_display_set_texture_size(&screen->display, screen->frame_size);
|
||||
if (screen->mipmaps) {
|
||||
SDL_GL_BindTexture(screen->texture, NULL, NULL);
|
||||
screen->gl.GenerateMipmap(GL_TEXTURE_2D);
|
||||
SDL_GL_UnbindTexture(screen->texture);
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_screen_update_frame(struct sc_screen *screen) {
|
||||
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;
|
||||
|
||||
sc_fps_counter_add_rendered_frame(&screen->fps_counter);
|
||||
@ -618,10 +735,7 @@ sc_screen_update_frame(struct sc_screen *screen) {
|
||||
if (!prepare_for_frame(screen, new_frame_size)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sc_display_update_texture(&screen->display, frame)) {
|
||||
return false;
|
||||
}
|
||||
update_texture(screen, frame);
|
||||
|
||||
if (!screen->has_frame) {
|
||||
screen->has_frame = true;
|
||||
@ -701,32 +815,22 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) {
|
||||
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
void
|
||||
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||
bool relative_mode = sc_screen_is_relative_mode(screen);
|
||||
|
||||
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 EVENT_NEW_FRAME: {
|
||||
bool ok = sc_screen_update_frame(screen);
|
||||
if (!ok) {
|
||||
LOGE("Frame update failed\n");
|
||||
return false;
|
||||
LOGW("Frame update failed\n");
|
||||
}
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
case SDL_WINDOWEVENT:
|
||||
if (!screen->has_frame) {
|
||||
// Do nothing
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
switch (event->window.event) {
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
@ -757,7 +861,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
return true;
|
||||
return;
|
||||
case SDL_KEYDOWN:
|
||||
if (relative_mode) {
|
||||
SDL_Keycode key = event->key.keysym.sym;
|
||||
@ -770,7 +874,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -786,7 +890,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
sc_screen_toggle_mouse_capture(screen);
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -796,7 +900,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
||||
// Do not forward to input manager, the mouse will be captured
|
||||
// on SDL_MOUSEBUTTONUP
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SDL_FINGERMOTION:
|
||||
@ -805,19 +909,18 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
|
||||
if (relative_mode) {
|
||||
// Touch events are not compatible with relative mode
|
||||
// (coordinates are not relative)
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
||||
sc_screen_set_mouse_capture(screen, true);
|
||||
return true;
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
sc_input_manager_handle_event(&screen->im, event);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct sc_point
|
||||
|
@ -9,14 +9,13 @@
|
||||
|
||||
#include "controller.h"
|
||||
#include "coords.h"
|
||||
#include "display.h"
|
||||
#include "fps_counter.h"
|
||||
#include "frame_buffer.h"
|
||||
#include "input_manager.h"
|
||||
#include "opengl.h"
|
||||
#include "trait/key_processor.h"
|
||||
#include "trait/frame_sink.h"
|
||||
#include "trait/mouse_processor.h"
|
||||
#include "video_buffer.h"
|
||||
|
||||
struct sc_screen {
|
||||
struct sc_frame_sink frame_sink; // frame sink trait
|
||||
@ -25,9 +24,8 @@ struct sc_screen {
|
||||
bool open; // track the open/close state to assert correct behavior
|
||||
#endif
|
||||
|
||||
struct sc_display display;
|
||||
struct sc_input_manager im;
|
||||
struct sc_frame_buffer fb;
|
||||
struct sc_video_buffer vb;
|
||||
struct sc_fps_counter fps_counter;
|
||||
|
||||
// The initial requested window properties
|
||||
@ -41,6 +39,9 @@ struct sc_screen {
|
||||
} req;
|
||||
|
||||
SDL_Window *window;
|
||||
SDL_Renderer *renderer;
|
||||
SDL_Texture *texture;
|
||||
struct sc_opengl gl;
|
||||
struct sc_size frame_size;
|
||||
struct sc_size content_size; // rotated frame_size
|
||||
|
||||
@ -56,6 +57,9 @@ struct sc_screen {
|
||||
bool has_frame;
|
||||
bool fullscreen;
|
||||
bool maximized;
|
||||
bool mipmaps;
|
||||
|
||||
bool event_failed; // in case SDL_PushEvent() returned an error
|
||||
|
||||
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
|
||||
// RGUI) must be pressed. This variable tracks the pressed capture key.
|
||||
@ -76,6 +80,7 @@ struct sc_screen_params {
|
||||
const struct sc_shortcut_mods *shortcut_mods;
|
||||
|
||||
const char *window_title;
|
||||
struct sc_size frame_size;
|
||||
bool always_on_top;
|
||||
|
||||
int16_t window_x; // accepts SC_WINDOW_POSITION_UNDEFINED
|
||||
@ -90,6 +95,8 @@ struct sc_screen_params {
|
||||
|
||||
bool fullscreen;
|
||||
bool start_fps_counter;
|
||||
|
||||
sc_tick buffering_time;
|
||||
};
|
||||
|
||||
// initialize screen, create window, renderer and texture (window is hidden)
|
||||
@ -133,9 +140,8 @@ void
|
||||
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation);
|
||||
|
||||
// react to SDL events
|
||||
// If this function returns false, scrcpy must exit with an error.
|
||||
bool
|
||||
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event);
|
||||
void
|
||||
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event);
|
||||
|
||||
// convert point from window coordinates to frame coordinates
|
||||
// x and y are expressed in pixels
|
||||
|
155
app/src/server.c
155
app/src/server.c
@ -8,7 +8,6 @@
|
||||
#include <SDL2/SDL_platform.h>
|
||||
|
||||
#include "adb/adb.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/file.h"
|
||||
#include "util/log.h"
|
||||
#include "util/net_intr.h"
|
||||
@ -71,10 +70,8 @@ sc_server_params_destroy(struct sc_server_params *params) {
|
||||
// The server stores a copy of the params provided by the user
|
||||
free((char *) params->req_serial);
|
||||
free((char *) params->crop);
|
||||
free((char *) params->video_codec_options);
|
||||
free((char *) params->audio_codec_options);
|
||||
free((char *) params->video_encoder);
|
||||
free((char *) params->audio_encoder);
|
||||
free((char *) params->codec_options);
|
||||
free((char *) params->encoder_name);
|
||||
free((char *) params->tcpip_dst);
|
||||
}
|
||||
|
||||
@ -97,10 +94,8 @@ sc_server_params_copy(struct sc_server_params *dst,
|
||||
|
||||
COPY(req_serial);
|
||||
COPY(crop);
|
||||
COPY(video_codec_options);
|
||||
COPY(audio_codec_options);
|
||||
COPY(video_encoder);
|
||||
COPY(audio_encoder);
|
||||
COPY(codec_options);
|
||||
COPY(encoder_name);
|
||||
COPY(tcpip_dst);
|
||||
#undef COPY
|
||||
|
||||
@ -160,26 +155,6 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
|
||||
return !stopped;
|
||||
}
|
||||
|
||||
static const char *
|
||||
sc_server_get_codec_name(enum sc_codec codec) {
|
||||
switch (codec) {
|
||||
case SC_CODEC_H264:
|
||||
return "h264";
|
||||
case SC_CODEC_H265:
|
||||
return "h265";
|
||||
case SC_CODEC_AV1:
|
||||
return "av1";
|
||||
case SC_CODEC_OPUS:
|
||||
return "opus";
|
||||
case SC_CODEC_AAC:
|
||||
return "aac";
|
||||
case SC_CODEC_RAW:
|
||||
return "raw";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static sc_pid
|
||||
execute_server(struct sc_server *server,
|
||||
const struct sc_server_params *params) {
|
||||
@ -216,32 +191,17 @@ execute_server(struct sc_server *server,
|
||||
|
||||
unsigned dyn_idx = count; // from there, the strings are allocated
|
||||
#define ADD_PARAM(fmt, ...) { \
|
||||
char *p; \
|
||||
char *p = (char *) &cmd[count]; \
|
||||
if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \
|
||||
goto end; \
|
||||
} \
|
||||
cmd[count++] = p; \
|
||||
}
|
||||
|
||||
ADD_PARAM("scid=%08x", params->scid);
|
||||
ADD_PARAM("uid=%08x", params->uid);
|
||||
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
|
||||
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
|
||||
|
||||
if (params->video_bit_rate) {
|
||||
ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate);
|
||||
}
|
||||
if (!params->audio) {
|
||||
ADD_PARAM("audio=false");
|
||||
} else if (params->audio_bit_rate) {
|
||||
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
|
||||
}
|
||||
if (params->video_codec != SC_CODEC_H264) {
|
||||
ADD_PARAM("video_codec=%s",
|
||||
sc_server_get_codec_name(params->video_codec));
|
||||
}
|
||||
if (params->audio_codec != SC_CODEC_OPUS) {
|
||||
ADD_PARAM("audio_codec=%s",
|
||||
sc_server_get_codec_name(params->audio_codec));
|
||||
}
|
||||
if (params->max_size) {
|
||||
ADD_PARAM("max_size=%" PRIu16, params->max_size);
|
||||
}
|
||||
@ -271,17 +231,11 @@ execute_server(struct sc_server *server,
|
||||
if (params->stay_awake) {
|
||||
ADD_PARAM("stay_awake=true");
|
||||
}
|
||||
if (params->video_codec_options) {
|
||||
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
|
||||
if (params->codec_options) {
|
||||
ADD_PARAM("codec_options=%s", params->codec_options);
|
||||
}
|
||||
if (params->audio_codec_options) {
|
||||
ADD_PARAM("audio_codec_options=%s", params->audio_codec_options);
|
||||
}
|
||||
if (params->video_encoder) {
|
||||
ADD_PARAM("video_encoder=%s", params->video_encoder);
|
||||
}
|
||||
if (params->audio_encoder) {
|
||||
ADD_PARAM("audio_encoder=%s", params->audio_encoder);
|
||||
if (params->encoder_name) {
|
||||
ADD_PARAM("encoder_name=%s", params->encoder_name);
|
||||
}
|
||||
if (params->power_off_on_close) {
|
||||
ADD_PARAM("power_off_on_close=true");
|
||||
@ -302,12 +256,6 @@ execute_server(struct sc_server *server,
|
||||
// By default, power_on is true
|
||||
ADD_PARAM("power_on=false");
|
||||
}
|
||||
if (params->list_encoders) {
|
||||
ADD_PARAM("list_encoders=true");
|
||||
}
|
||||
if (params->list_displays) {
|
||||
ADD_PARAM("list_displays=true");
|
||||
}
|
||||
|
||||
#undef ADD_PARAM
|
||||
|
||||
@ -422,7 +370,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
|
||||
server->stopped = false;
|
||||
|
||||
server->video_socket = SC_SOCKET_NONE;
|
||||
server->audio_socket = SC_SOCKET_NONE;
|
||||
server->control_socket = SC_SOCKET_NONE;
|
||||
|
||||
sc_adb_tunnel_init(&server->tunnel);
|
||||
@ -441,9 +388,9 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
|
||||
static bool
|
||||
device_read_info(struct sc_intr *intr, sc_socket device_socket,
|
||||
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));
|
||||
if (r < SC_DEVICE_NAME_FIELD_LENGTH) {
|
||||
if (r < SC_DEVICE_NAME_FIELD_LENGTH + 4) {
|
||||
LOGE("Could not retrieve device information");
|
||||
return false;
|
||||
}
|
||||
@ -451,6 +398,10 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
|
||||
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
|
||||
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
|
||||
|
||||
info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
|
||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
|
||||
info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
|
||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -463,11 +414,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
const char *serial = server->serial;
|
||||
assert(serial);
|
||||
|
||||
bool audio = server->params.audio;
|
||||
bool control = server->params.control;
|
||||
|
||||
sc_socket video_socket = SC_SOCKET_NONE;
|
||||
sc_socket audio_socket = SC_SOCKET_NONE;
|
||||
sc_socket control_socket = SC_SOCKET_NONE;
|
||||
if (!tunnel->forward) {
|
||||
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
|
||||
@ -475,14 +424,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
audio_socket =
|
||||
net_accept_intr(&server->intr, tunnel->server_socket);
|
||||
if (audio_socket == SC_SOCKET_NONE) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
if (control) {
|
||||
control_socket =
|
||||
net_accept_intr(&server->intr, tunnel->server_socket);
|
||||
@ -509,18 +450,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
goto fail;
|
||||
}
|
||||
|
||||
if (audio) {
|
||||
audio_socket = net_socket();
|
||||
if (audio_socket == SC_SOCKET_NONE) {
|
||||
goto fail;
|
||||
}
|
||||
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host,
|
||||
tunnel_port);
|
||||
if (!ok) {
|
||||
goto fail;
|
||||
}
|
||||
}
|
||||
|
||||
if (control) {
|
||||
// we know that the device is listening, we don't need several
|
||||
// attempts
|
||||
@ -547,11 +476,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
|
||||
}
|
||||
|
||||
assert(video_socket != SC_SOCKET_NONE);
|
||||
assert(!audio || audio_socket != SC_SOCKET_NONE);
|
||||
assert(!control || control_socket != SC_SOCKET_NONE);
|
||||
|
||||
server->video_socket = video_socket;
|
||||
server->audio_socket = audio_socket;
|
||||
server->control_socket = control_socket;
|
||||
|
||||
return true;
|
||||
@ -563,12 +490,6 @@ fail:
|
||||
}
|
||||
}
|
||||
|
||||
if (audio_socket != SC_SOCKET_NONE) {
|
||||
if (!net_close(audio_socket)) {
|
||||
LOGW("Could not close audio socket");
|
||||
}
|
||||
}
|
||||
|
||||
if (control_socket != SC_SOCKET_NONE) {
|
||||
if (!net_close(control_socket)) {
|
||||
LOGW("Could not close control socket");
|
||||
@ -751,11 +672,6 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server,
|
||||
if (is_already_tcpip) {
|
||||
// Nothing to do
|
||||
LOGI("Device already connected via TCP/IP: %s", serial);
|
||||
server->serial = strdup(serial);
|
||||
if (!server->serial) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -853,27 +769,8 @@ run_server(void *data) {
|
||||
assert(serial);
|
||||
LOGD("Device serial: %s", serial);
|
||||
|
||||
ok = push_server(&server->intr, serial);
|
||||
if (!ok) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
|
||||
// If --list-* is passed, then the server just prints the requested data
|
||||
// then exits.
|
||||
if (params->list_encoders || params->list_displays) {
|
||||
sc_pid pid = execute_server(server, params);
|
||||
if (pid == SC_PROCESS_NONE) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
sc_process_wait(pid, NULL); // ignore exit code
|
||||
sc_process_close(pid);
|
||||
// Wake up await_for_server()
|
||||
server->cbs->on_connected(server, server->cbs_userdata);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
|
||||
params->scid);
|
||||
params->uid);
|
||||
if (r == -1) {
|
||||
LOG_OOM();
|
||||
goto error_connection_failed;
|
||||
@ -881,6 +778,11 @@ run_server(void *data) {
|
||||
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
|
||||
assert(server->device_socket_name);
|
||||
|
||||
ok = push_server(&server->intr, serial);
|
||||
if (!ok) {
|
||||
goto error_connection_failed;
|
||||
}
|
||||
|
||||
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
|
||||
server->device_socket_name, params->port_range,
|
||||
params->force_adb_forward);
|
||||
@ -933,11 +835,6 @@ run_server(void *data) {
|
||||
assert(server->video_socket != SC_SOCKET_NONE);
|
||||
net_interrupt(server->video_socket);
|
||||
|
||||
if (server->audio_socket != SC_SOCKET_NONE) {
|
||||
// There is no audio_socket if --no-audio is set
|
||||
net_interrupt(server->audio_socket);
|
||||
}
|
||||
|
||||
if (server->control_socket != SC_SOCKET_NONE) {
|
||||
// There is no control_socket if --no-control is set
|
||||
net_interrupt(server->control_socket);
|
||||
@ -990,10 +887,7 @@ sc_server_stop(struct sc_server *server) {
|
||||
sc_cond_signal(&server->cond_stopped);
|
||||
sc_intr_interrupt(&server->intr);
|
||||
sc_mutex_unlock(&server->mutex);
|
||||
}
|
||||
|
||||
void
|
||||
sc_server_join(struct sc_server *server) {
|
||||
sc_thread_join(&server->thread, NULL);
|
||||
}
|
||||
|
||||
@ -1002,9 +896,6 @@ sc_server_destroy(struct sc_server *server) {
|
||||
if (server->video_socket != SC_SOCKET_NONE) {
|
||||
net_close(server->video_socket);
|
||||
}
|
||||
if (server->audio_socket != SC_SOCKET_NONE) {
|
||||
net_close(server->audio_socket);
|
||||
}
|
||||
if (server->control_socket != SC_SOCKET_NONE) {
|
||||
net_close(server->control_socket);
|
||||
}
|
||||
|
@ -18,30 +18,25 @@
|
||||
#define SC_DEVICE_NAME_FIELD_LENGTH 64
|
||||
struct sc_server_info {
|
||||
char device_name[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||
struct sc_size frame_size;
|
||||
};
|
||||
|
||||
struct sc_server_params {
|
||||
uint32_t scid;
|
||||
uint32_t uid;
|
||||
const char *req_serial;
|
||||
enum sc_log_level log_level;
|
||||
enum sc_codec video_codec;
|
||||
enum sc_codec audio_codec;
|
||||
const char *crop;
|
||||
const char *video_codec_options;
|
||||
const char *audio_codec_options;
|
||||
const char *video_encoder;
|
||||
const char *audio_encoder;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
struct sc_port_range port_range;
|
||||
uint32_t tunnel_host;
|
||||
uint16_t tunnel_port;
|
||||
uint16_t max_size;
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint32_t bit_rate;
|
||||
uint16_t max_fps;
|
||||
int8_t lock_video_orientation;
|
||||
bool control;
|
||||
uint32_t display_id;
|
||||
bool audio;
|
||||
bool show_touches;
|
||||
bool stay_awake;
|
||||
bool force_adb_forward;
|
||||
@ -54,8 +49,6 @@ struct sc_server_params {
|
||||
bool select_tcpip;
|
||||
bool cleanup;
|
||||
bool power_on;
|
||||
bool list_encoders;
|
||||
bool list_displays;
|
||||
};
|
||||
|
||||
struct sc_server {
|
||||
@ -75,7 +68,6 @@ struct sc_server {
|
||||
struct sc_adb_tunnel tunnel;
|
||||
|
||||
sc_socket video_socket;
|
||||
sc_socket audio_socket;
|
||||
sc_socket control_socket;
|
||||
|
||||
const struct sc_server_callbacks *cbs;
|
||||
@ -115,10 +107,6 @@ sc_server_start(struct sc_server *server);
|
||||
void
|
||||
sc_server_stop(struct sc_server *server);
|
||||
|
||||
// join the server thread
|
||||
void
|
||||
sc_server_join(struct sc_server *server);
|
||||
|
||||
// close and release sockets
|
||||
void
|
||||
sc_server_destroy(struct sc_server *server);
|
||||
|
@ -5,7 +5,8 @@
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
typedef struct AVFrame AVFrame;
|
||||
|
||||
/**
|
||||
* Frame sink trait.
|
||||
@ -17,8 +18,7 @@ struct sc_frame_sink {
|
||||
};
|
||||
|
||||
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);
|
||||
void (*close)(struct sc_frame_sink *sink);
|
||||
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 <stdbool.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
typedef struct AVCodec AVCodec;
|
||||
typedef struct AVPacket AVPacket;
|
||||
|
||||
/**
|
||||
* Packet sink trait.
|
||||
@ -17,20 +19,9 @@ struct sc_packet_sink {
|
||||
};
|
||||
|
||||
struct sc_packet_sink_ops {
|
||||
/* The codec context is valid until the sink is closed */
|
||||
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);
|
||||
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
||||
|
||||
/*/
|
||||
* Called when the input stream has been disabled at runtime.
|
||||
*
|
||||
* If it is called, then open(), close() and push() will never be called.
|
||||
*
|
||||
* It is useful to notify the recorder that the requested audio stream has
|
||||
* finally been disabled because the device could not capture it.
|
||||
*/
|
||||
void (*disable)(struct sc_packet_sink *sink);
|
||||
};
|
||||
|
||||
#endif
|
||||
|
@ -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 SC_HID_EVENT_QUEUE_MAX 64
|
||||
|
||||
static void
|
||||
sc_hid_event_log(const struct sc_hid_event *event) {
|
||||
// HID Event: [00] FF FF FF FF...
|
||||
@ -50,20 +48,14 @@ sc_hid_event_destroy(struct sc_hid_event *hid_event) {
|
||||
bool
|
||||
sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
|
||||
struct sc_acksync *acksync) {
|
||||
sc_vecdeque_init(&aoa->queue);
|
||||
|
||||
if (!sc_vecdeque_reserve(&aoa->queue, SC_HID_EVENT_QUEUE_MAX)) {
|
||||
return false;
|
||||
}
|
||||
cbuf_init(&aoa->queue);
|
||||
|
||||
if (!sc_mutex_init(&aoa->mutex)) {
|
||||
sc_vecdeque_destroy(&aoa->queue);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sc_cond_init(&aoa->event_cond)) {
|
||||
sc_mutex_destroy(&aoa->mutex);
|
||||
sc_vecdeque_destroy(&aoa->queue);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -77,10 +69,9 @@ sc_aoa_init(struct sc_aoa *aoa, struct sc_usb *usb,
|
||||
void
|
||||
sc_aoa_destroy(struct sc_aoa *aoa) {
|
||||
// Destroy remaining events
|
||||
while (!sc_vecdeque_is_empty(&aoa->queue)) {
|
||||
struct sc_hid_event *event = sc_vecdeque_popref(&aoa->queue);
|
||||
assert(event);
|
||||
sc_hid_event_destroy(event);
|
||||
struct sc_hid_event event;
|
||||
while (cbuf_take(&aoa->queue, &event)) {
|
||||
sc_hid_event_destroy(&event);
|
||||
}
|
||||
|
||||
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);
|
||||
bool full = sc_vecdeque_is_full(&aoa->queue);
|
||||
if (!full) {
|
||||
bool was_empty = sc_vecdeque_is_empty(&aoa->queue);
|
||||
sc_vecdeque_push_noresize(&aoa->queue, *event);
|
||||
if (was_empty) {
|
||||
sc_cond_signal(&aoa->event_cond);
|
||||
}
|
||||
bool was_empty = cbuf_is_empty(&aoa->queue);
|
||||
bool res = cbuf_push(&aoa->queue, *event);
|
||||
if (was_empty) {
|
||||
sc_cond_signal(&aoa->event_cond);
|
||||
}
|
||||
// Otherwise (if the queue is full), the event is discarded
|
||||
|
||||
sc_mutex_unlock(&aoa->mutex);
|
||||
|
||||
return !full;
|
||||
return res;
|
||||
}
|
||||
|
||||
static int
|
||||
@ -242,7 +227,7 @@ run_aoa_thread(void *data) {
|
||||
|
||||
for (;;) {
|
||||
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);
|
||||
}
|
||||
if (aoa->stopped) {
|
||||
@ -250,9 +235,11 @@ run_aoa_thread(void *data) {
|
||||
sc_mutex_unlock(&aoa->mutex);
|
||||
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;
|
||||
sc_mutex_unlock(&aoa->mutex);
|
||||
|
||||
|
@ -8,9 +8,9 @@
|
||||
|
||||
#include "usb.h"
|
||||
#include "util/acksync.h"
|
||||
#include "util/cbuf.h"
|
||||
#include "util/thread.h"
|
||||
#include "util/tick.h"
|
||||
#include "util/vecdeque.h"
|
||||
|
||||
struct sc_hid_event {
|
||||
uint16_t accessory_id;
|
||||
@ -27,7 +27,7 @@ sc_hid_event_init(struct sc_hid_event *hid_event, uint16_t accessory_id,
|
||||
void
|
||||
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_usb *usb;
|
||||
|
@ -22,7 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
|
||||
(void) userdata;
|
||||
|
||||
SDL_Event event;
|
||||
event.type = SC_EVENT_USB_DEVICE_DISCONNECTED;
|
||||
event.type = EVENT_USB_DEVICE_DISCONNECTED;
|
||||
int ret = SDL_PushEvent(&event);
|
||||
if (ret < 0) {
|
||||
LOGE("Could not post USB disconnection event: %s", SDL_GetError());
|
||||
@ -34,7 +34,7 @@ event_loop(struct scrcpy_otg *s) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SC_EVENT_USB_DEVICE_DISCONNECTED:
|
||||
case EVENT_USB_DEVICE_DISCONNECTED:
|
||||
LOGW("Device disconnected");
|
||||
return SCRCPY_EXIT_DISCONNECTED;
|
||||
case SDL_QUIT:
|
||||
|
@ -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
|
@ -1,26 +0,0 @@
|
||||
#include "average.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
void
|
||||
sc_average_init(struct sc_average *avg, unsigned range) {
|
||||
avg->range = range;
|
||||
avg->avg = 0;
|
||||
avg->count = 0;
|
||||
}
|
||||
|
||||
void
|
||||
sc_average_push(struct sc_average *avg, float value) {
|
||||
if (avg->count < avg->range) {
|
||||
++avg->count;
|
||||
}
|
||||
|
||||
assert(avg->count);
|
||||
avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count;
|
||||
}
|
||||
|
||||
float
|
||||
sc_average_get(struct sc_average *avg) {
|
||||
assert(avg->count);
|
||||
return avg->avg;
|
||||
}
|
@ -1,40 +0,0 @@
|
||||
#ifndef SC_AVERAGE
|
||||
#define SC_AVERAGE
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct sc_average {
|
||||
// Current average value
|
||||
float avg;
|
||||
|
||||
// Target range, to update the average as follow:
|
||||
// avg = ((range - 1) * avg + new_value) / range
|
||||
unsigned range;
|
||||
|
||||
// Number of values pushed when less than range (count <= range).
|
||||
// The purpose is to handle the first (range - 1) values properly.
|
||||
unsigned count;
|
||||
};
|
||||
|
||||
void
|
||||
sc_average_init(struct sc_average *avg, unsigned range);
|
||||
|
||||
/**
|
||||
* Push a new value to update the "rolling" average
|
||||
*/
|
||||
void
|
||||
sc_average_push(struct sc_average *avg, float value);
|
||||
|
||||
/**
|
||||
* Get the current average value
|
||||
*
|
||||
* It is an error to call this function if sc_average_push() has not been
|
||||
* called at least once.
|
||||
*/
|
||||
float
|
||||
sc_average_get(struct sc_average *avg);
|
||||
|
||||
#endif
|
@ -1,104 +0,0 @@
|
||||
#include "bytebuf.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
bool
|
||||
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
|
||||
assert(alloc_size);
|
||||
buf->data = malloc(alloc_size);
|
||||
if (!buf->data) {
|
||||
LOG_OOM();
|
||||
return false;
|
||||
}
|
||||
|
||||
buf->alloc_size = alloc_size;
|
||||
buf->head = 0;
|
||||
buf->tail = 0;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_bytebuf_destroy(struct sc_bytebuf *buf) {
|
||||
free(buf->data);
|
||||
}
|
||||
|
||||
void
|
||||
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
|
||||
assert(len);
|
||||
assert(len <= sc_bytebuf_can_read(buf));
|
||||
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_len = right_limit - buf->tail;
|
||||
if (len < right_len) {
|
||||
right_len = len;
|
||||
}
|
||||
memcpy(to, buf->data + buf->tail, right_len);
|
||||
|
||||
if (len > right_len) {
|
||||
memcpy(to + right_len, buf->data, len - right_len);
|
||||
}
|
||||
|
||||
buf->tail = (buf->tail + len) % buf->alloc_size;
|
||||
}
|
||||
|
||||
void
|
||||
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
|
||||
assert(len);
|
||||
assert(len <= sc_bytebuf_can_read(buf));
|
||||
assert(buf->tail != buf->head); // the buffer could not be empty
|
||||
|
||||
buf->tail = (buf->tail + len) % buf->alloc_size;
|
||||
}
|
||||
|
||||
static inline void
|
||||
sc_bytebuf_write_step0(struct sc_bytebuf *buf, const uint8_t *from,
|
||||
size_t 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);
|
||||
}
|
||||
}
|
||||
|
||||
static inline void
|
||||
sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) {
|
||||
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
|
||||
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
||||
size_t len) {
|
||||
// *This function MUST NOT access buf->tail (even in assert()).*
|
||||
// The purpose of this function is to allow a reader and a writer to access
|
||||
// different parts of the buffer in parallel simultaneously. It is intended
|
||||
// to be called without lock (only sc_bytebuf_commit_write() is intended to
|
||||
// be called with lock held).
|
||||
|
||||
assert(len < buf->alloc_size - 1);
|
||||
sc_bytebuf_write_step0(buf, from, len);
|
||||
}
|
||||
|
||||
void
|
||||
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
|
||||
assert(len <= sc_bytebuf_can_write(buf));
|
||||
sc_bytebuf_write_step1(buf, len);
|
||||
}
|
@ -1,114 +0,0 @@
|
||||
#ifndef SC_BYTEBUF_H
|
||||
#define SC_BYTEBUF_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
struct sc_bytebuf {
|
||||
uint8_t *data;
|
||||
// The actual capacity is (allocated - 1) so that head == tail is
|
||||
// non-ambiguous
|
||||
size_t alloc_size;
|
||||
size_t head; // writter cursor
|
||||
size_t tail; // reader cursor
|
||||
// empty: tail == head
|
||||
// full: ((tail + 1) % alloc_size) == head
|
||||
};
|
||||
|
||||
bool
|
||||
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size);
|
||||
|
||||
/**
|
||||
* Copy from the bytebuf to a user-provided array
|
||||
*
|
||||
* The caller must check that len <= sc_bytebuf_read_available() (it is an
|
||||
* error to attempt to read more bytes than available).
|
||||
*
|
||||
* This function is guaranteed not to write to buf->head.
|
||||
*/
|
||||
void
|
||||
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
|
||||
|
||||
/**
|
||||
* Drop len bytes from the buffer
|
||||
*
|
||||
* The caller must check that len <= sc_bytebuf_read_available() (it is an
|
||||
* error to attempt to skip more bytes than available).
|
||||
*
|
||||
* This function is guaranteed not to write to buf->head.
|
||||
*
|
||||
* 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).
|
||||
*/
|
||||
void
|
||||
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
|
||||
|
||||
/**
|
||||
* Copy the user-provided array to the bytebuf
|
||||
*
|
||||
* The caller must check that len <= sc_bytebuf_write_available() (it is an
|
||||
* error to write more bytes than the remaining available space).
|
||||
*
|
||||
* This function is guaranteed not to write to buf->tail.
|
||||
*/
|
||||
void
|
||||
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
|
||||
*
|
||||
* The caller must check that len <= sc_bytebuf_write_available() (it is an
|
||||
* error to write more bytes than the remaining available space).
|
||||
*
|
||||
* After this function is called, the write must be committed with
|
||||
* sc_bytebuf_commit_write().
|
||||
*
|
||||
* The purpose of this mechanism is to acquire a lock only to commit the write,
|
||||
* but not to perform the actual copy.
|
||||
*
|
||||
* This function is guaranteed not to access buf->tail.
|
||||
*/
|
||||
void
|
||||
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
||||
size_t len);
|
||||
|
||||
/**
|
||||
* Commit a prepared write
|
||||
*/
|
||||
void
|
||||
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
|
||||
|
||||
/**
|
||||
* Return the number of bytes which can be read
|
||||
*
|
||||
* It is an error to read more bytes than available.
|
||||
*/
|
||||
static inline size_t
|
||||
sc_bytebuf_can_read(struct sc_bytebuf *buf) {
|
||||
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the number of bytes which can be written
|
||||
*
|
||||
* It is an error to write more bytes than available.
|
||||
*/
|
||||
static inline size_t
|
||||
sc_bytebuf_can_write(struct sc_bytebuf *buf) {
|
||||
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
|
||||
sc_bytebuf_destroy(struct sc_bytebuf *buf);
|
||||
|
||||
#endif
|
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
|
@ -4,7 +4,6 @@
|
||||
# include <windows.h>
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
static SDL_LogPriority
|
||||
log_level_sc_to_sdl(enum sc_log_level level) {
|
||||
@ -48,7 +47,6 @@ void
|
||||
sc_set_log_level(enum sc_log_level level) {
|
||||
SDL_LogPriority sdl_log = log_level_sc_to_sdl(level);
|
||||
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
|
||||
SDL_LogSetPriority(SDL_LOG_CATEGORY_CUSTOM, sdl_log);
|
||||
}
|
||||
|
||||
enum sc_log_level
|
||||
@ -87,68 +85,3 @@ sc_log_windows_error(const char *prefix, int error) {
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
|
||||
static SDL_LogPriority
|
||||
sdl_priority_from_av_level(int level) {
|
||||
switch (level) {
|
||||
case AV_LOG_PANIC:
|
||||
case AV_LOG_FATAL:
|
||||
return SDL_LOG_PRIORITY_CRITICAL;
|
||||
case AV_LOG_ERROR:
|
||||
return SDL_LOG_PRIORITY_ERROR;
|
||||
case AV_LOG_WARNING:
|
||||
return SDL_LOG_PRIORITY_WARN;
|
||||
case AV_LOG_INFO:
|
||||
return SDL_LOG_PRIORITY_INFO;
|
||||
}
|
||||
// do not forward others, which are too verbose
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
|
||||
(void) avcl;
|
||||
SDL_LogPriority priority = sdl_priority_from_av_level(level);
|
||||
if (priority == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
size_t fmt_len = strlen(fmt);
|
||||
char *local_fmt = malloc(fmt_len + 10);
|
||||
if (!local_fmt) {
|
||||
LOG_OOM();
|
||||
return;
|
||||
}
|
||||
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
|
||||
memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0'
|
||||
SDL_LogMessageV(SDL_LOG_CATEGORY_CUSTOM, priority, local_fmt, vl);
|
||||
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
|
||||
sc_log_configure() {
|
||||
SDL_LogSetOutputFunction(sc_sdl_log_print, NULL);
|
||||
// Redirect FFmpeg logs to SDL logs
|
||||
av_log_set_callback(sc_av_log_callback);
|
||||
}
|
||||
|
@ -35,7 +35,4 @@ bool
|
||||
sc_log_windows_error(const char *prefix, int error);
|
||||
#endif
|
||||
|
||||
void
|
||||
sc_log_configure();
|
||||
|
||||
#endif
|
||||
|
@ -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
|
@ -30,8 +30,8 @@ bool
|
||||
net_init(void) {
|
||||
#ifdef _WIN32
|
||||
WSADATA wsa;
|
||||
int res = WSAStartup(MAKEWORD(1, 1), &wsa);
|
||||
if (res) {
|
||||
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
|
||||
if (res < 0) {
|
||||
LOGE("WSAStartup failed with error %d", res);
|
||||
return false;
|
||||
}
|
||||
|
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;
|
||||
}
|
||||
|
||||
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
|
||||
sc_thread_join(sc_thread *thread, int *status) {
|
||||
SDL_WaitThread(thread->thread, status);
|
||||
|
@ -21,13 +21,6 @@ typedef struct sc_thread {
|
||||
SDL_Thread *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 {
|
||||
SDL_mutex *mutex;
|
||||
#ifndef NDEBUG
|
||||
@ -46,9 +39,6 @@ sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
|
||||
void
|
||||
sc_thread_join(sc_thread *thread, int *status);
|
||||
|
||||
bool
|
||||
sc_thread_set_priority(enum sc_thread_priority priority);
|
||||
|
||||
bool
|
||||
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,
|
||||
size_t *restrict pcap, size_t *restrict psize)
|
||||
{
|
||||
void *p = reallocarray(ptr, count, size);
|
||||
void *p = realloc(ptr, count * size);
|
||||
if (!p) {
|
||||
return NULL;
|
||||
}
|
||||
|
@ -126,7 +126,7 @@ run_v4l2_sink(void *data) {
|
||||
vs->has_frame = false;
|
||||
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);
|
||||
av_frame_unref(vs->frame);
|
||||
@ -141,19 +141,39 @@ run_v4l2_sink(void *data) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||
(void) ctx;
|
||||
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;
|
||||
|
||||
bool ok = sc_frame_buffer_init(&vs->fb);
|
||||
if (!previous_skipped) {
|
||||
sc_mutex_lock(&vs->mutex);
|
||||
vs->has_frame = true;
|
||||
sc_cond_signal(&vs->cond);
|
||||
sc_mutex_unlock(&vs->mutex);
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
|
||||
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) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_video_buffer_start(&vs->vb);
|
||||
if (!ok) {
|
||||
goto error_video_buffer_destroy;
|
||||
}
|
||||
|
||||
ok = sc_mutex_init(&vs->mutex);
|
||||
if (!ok) {
|
||||
goto error_frame_buffer_destroy;
|
||||
goto error_video_buffer_stop_and_join;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&vs->cond);
|
||||
@ -205,13 +225,11 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
int r = avcodec_parameters_from_context(ostream->codecpar, ctx);
|
||||
if (r < 0) {
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
// The codec is from the v4l2 encoder, not from the decoder
|
||||
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
ostream->codecpar->codec_id = encoder->id;
|
||||
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);
|
||||
if (ret < 0) {
|
||||
@ -226,8 +244,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||
goto error_avio_close;
|
||||
}
|
||||
|
||||
vs->encoder_ctx->width = ctx->width;
|
||||
vs->encoder_ctx->height = ctx->height;
|
||||
vs->encoder_ctx->width = vs->frame_size.width;
|
||||
vs->encoder_ctx->height = vs->frame_size.height;
|
||||
vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
vs->encoder_ctx->time_base.num = 1;
|
||||
vs->encoder_ctx->time_base.den = 1;
|
||||
@ -280,8 +298,11 @@ error_cond_destroy:
|
||||
sc_cond_destroy(&vs->cond);
|
||||
error_mutex_destroy:
|
||||
sc_mutex_destroy(&vs->mutex);
|
||||
error_frame_buffer_destroy:
|
||||
sc_frame_buffer_destroy(&vs->fb);
|
||||
error_video_buffer_stop_and_join:
|
||||
sc_video_buffer_stop(&vs->vb);
|
||||
sc_video_buffer_join(&vs->vb);
|
||||
error_video_buffer_destroy:
|
||||
sc_video_buffer_destroy(&vs->vb);
|
||||
|
||||
return false;
|
||||
}
|
||||
@ -293,7 +314,10 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
|
||||
sc_cond_signal(&vs->cond);
|
||||
sc_mutex_unlock(&vs->mutex);
|
||||
|
||||
sc_video_buffer_stop(&vs->vb);
|
||||
|
||||
sc_thread_join(&vs->thread, NULL);
|
||||
sc_video_buffer_join(&vs->vb);
|
||||
|
||||
av_packet_free(&vs->packet);
|
||||
av_frame_free(&vs->frame);
|
||||
@ -303,31 +327,18 @@ sc_v4l2_sink_close(struct sc_v4l2_sink *vs) {
|
||||
avformat_free_context(vs->format_ctx);
|
||||
sc_cond_destroy(&vs->cond);
|
||||
sc_mutex_destroy(&vs->mutex);
|
||||
sc_frame_buffer_destroy(&vs->fb);
|
||||
sc_video_buffer_destroy(&vs->vb);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
|
||||
bool previous_skipped;
|
||||
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;
|
||||
return sc_video_buffer_push(&vs->vb, frame);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) {
|
||||
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) {
|
||||
struct sc_v4l2_sink *vs = DOWNCAST(sink);
|
||||
return sc_v4l2_sink_open(vs, ctx);
|
||||
return sc_v4l2_sink_open(vs);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -343,13 +354,17 @@ sc_v4l2_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||
}
|
||||
|
||||
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);
|
||||
if (!vs->device_name) {
|
||||
LOGE("Could not strdup v4l2 device name");
|
||||
return false;
|
||||
}
|
||||
|
||||
vs->frame_size = frame_size;
|
||||
vs->buffering_time = buffering_time;
|
||||
|
||||
static const struct sc_frame_sink_ops ops = {
|
||||
.open = sc_v4l2_frame_sink_open,
|
||||
.close = sc_v4l2_frame_sink_close,
|
||||
|
@ -8,17 +8,19 @@
|
||||
|
||||
#include "coords.h"
|
||||
#include "trait/frame_sink.h"
|
||||
#include "frame_buffer.h"
|
||||
#include "video_buffer.h"
|
||||
#include "util/tick.h"
|
||||
|
||||
struct sc_v4l2_sink {
|
||||
struct sc_frame_sink frame_sink; // frame sink trait
|
||||
|
||||
struct sc_frame_buffer fb;
|
||||
struct sc_video_buffer vb;
|
||||
AVFormatContext *format_ctx;
|
||||
AVCodecContext *encoder_ctx;
|
||||
|
||||
char *device_name;
|
||||
struct sc_size frame_size;
|
||||
sc_tick buffering_time;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
@ -32,7 +34,8 @@ struct sc_v4l2_sink {
|
||||
};
|
||||
|
||||
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
|
||||
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
|
@ -217,18 +217,6 @@ static void test_get_ip_multiline_second_ok(void) {
|
||||
free(ip);
|
||||
}
|
||||
|
||||
static void test_get_ip_multiline_second_ok_without_cr(void) {
|
||||
char ip_route[] = "10.0.0.0/24 dev rmnet proto kernel scope link src "
|
||||
"10.0.0.3\n"
|
||||
"192.168.1.0/24 dev wlan0 proto kernel scope link src "
|
||||
"192.168.1.3\n";
|
||||
|
||||
char *ip = sc_adb_parse_device_ip(ip_route);
|
||||
assert(ip);
|
||||
assert(!strcmp(ip, "192.168.1.3"));
|
||||
free(ip);
|
||||
}
|
||||
|
||||
static void test_get_ip_no_wlan(void) {
|
||||
char ip_route[] = "192.168.1.0/24 dev rmnet proto kernel scope link src "
|
||||
"192.168.12.34\r\r\n";
|
||||
@ -271,7 +259,6 @@ int main(int argc, char *argv[]) {
|
||||
test_get_ip_single_line_with_trailing_space();
|
||||
test_get_ip_multiline_first_ok();
|
||||
test_get_ip_multiline_second_ok();
|
||||
test_get_ip_multiline_second_ok_without_cr();
|
||||
test_get_ip_no_wlan();
|
||||
test_get_ip_no_wlan_without_eol();
|
||||
test_get_ip_truncated();
|
||||
|
@ -1,126 +0,0 @@
|
||||
#include "common.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <string.h>
|
||||
|
||||
#include "util/bytebuf.h"
|
||||
|
||||
void test_bytebuf_simple(void) {
|
||||
struct sc_bytebuf buf;
|
||||
uint8_t data[20];
|
||||
|
||||
bool ok = sc_bytebuf_init(&buf, 20);
|
||||
assert(ok);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 5);
|
||||
|
||||
sc_bytebuf_read(&buf, data, 4);
|
||||
assert(!strncmp((char *) data, "hell", 4));
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) " world", sizeof(" world") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 7);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 8);
|
||||
|
||||
sc_bytebuf_read(&buf, &data[4], 8);
|
||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
||||
|
||||
data[12] = '\0';
|
||||
assert(!strcmp((char *) data, "hello world!"));
|
||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
||||
|
||||
sc_bytebuf_destroy(&buf);
|
||||
}
|
||||
|
||||
void test_bytebuf_boundaries(void) {
|
||||
struct sc_bytebuf buf;
|
||||
uint8_t data[20];
|
||||
|
||||
bool ok = sc_bytebuf_init(&buf, 20);
|
||||
assert(ok);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 6);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 18);
|
||||
|
||||
sc_bytebuf_read(&buf, data, 9);
|
||||
assert(!strncmp((char *) data, "hello hel", 9));
|
||||
assert(sc_bytebuf_can_read(&buf) == 9);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 14);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 15);
|
||||
|
||||
sc_bytebuf_skip(&buf, 3);
|
||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
||||
|
||||
sc_bytebuf_read(&buf, data, 12);
|
||||
data[12] = '\0';
|
||||
assert(!strcmp((char *) data, "hello world!"));
|
||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
||||
|
||||
sc_bytebuf_destroy(&buf);
|
||||
}
|
||||
|
||||
void test_bytebuf_two_steps_write(void) {
|
||||
struct sc_bytebuf buf;
|
||||
uint8_t data[20];
|
||||
|
||||
bool ok = sc_bytebuf_init(&buf, 20);
|
||||
assert(ok);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 6);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
||||
|
||||
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 12); // write not committed yet
|
||||
|
||||
sc_bytebuf_read(&buf, data, 9);
|
||||
assert(!strncmp((char *) data, "hello hel", 3));
|
||||
assert(sc_bytebuf_can_read(&buf) == 3);
|
||||
|
||||
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 9);
|
||||
|
||||
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet
|
||||
|
||||
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 14);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||
assert(sc_bytebuf_can_read(&buf) == 15);
|
||||
|
||||
sc_bytebuf_skip(&buf, 3);
|
||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
||||
|
||||
sc_bytebuf_read(&buf, data, 12);
|
||||
data[12] = '\0';
|
||||
assert(!strcmp((char *) data, "hello world!"));
|
||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
||||
|
||||
sc_bytebuf_destroy(&buf);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
test_bytebuf_simple();
|
||||
test_bytebuf_boundaries();
|
||||
test_bytebuf_two_steps_write();
|
||||
|
||||
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;
|
||||
}
|
@ -46,7 +46,7 @@ static void test_options(void) {
|
||||
char *argv[] = {
|
||||
"scrcpy",
|
||||
"--always-on-top",
|
||||
"--video-bit-rate", "5M",
|
||||
"--bit-rate", "5M",
|
||||
"--crop", "100:200:300:400",
|
||||
"--fullscreen",
|
||||
"--max-fps", "30",
|
||||
@ -75,7 +75,7 @@ static void test_options(void) {
|
||||
|
||||
const struct scrcpy_options *opts = &args.opts;
|
||||
assert(opts->always_on_top);
|
||||
assert(opts->video_bit_rate == 5000000);
|
||||
assert(opts->bit_rate == 5000000);
|
||||
assert(!strcmp(opts->crop, "100:200:300:400"));
|
||||
assert(opts->fullscreen);
|
||||
assert(opts->max_fps == 30);
|
||||
|
79
app/tests/test_clock.c
Normal file
79
app/tests/test_clock.c
Normal file
@ -0,0 +1,79 @@
|
||||
#include "common.h"
|
||||
|
||||
#include <assert.h>
|
||||
|
||||
#include "clock.h"
|
||||
|
||||
void test_small_rolling_sum(void) {
|
||||
struct sc_clock clock;
|
||||
sc_clock_init(&clock);
|
||||
|
||||
assert(clock.count == 0);
|
||||
assert(clock.left_sum.system == 0);
|
||||
assert(clock.left_sum.stream == 0);
|
||||
assert(clock.right_sum.system == 0);
|
||||
assert(clock.right_sum.stream == 0);
|
||||
|
||||
sc_clock_update(&clock, 2, 3);
|
||||
assert(clock.count == 1);
|
||||
assert(clock.left_sum.system == 0);
|
||||
assert(clock.left_sum.stream == 0);
|
||||
assert(clock.right_sum.system == 2);
|
||||
assert(clock.right_sum.stream == 3);
|
||||
|
||||
sc_clock_update(&clock, 10, 20);
|
||||
assert(clock.count == 2);
|
||||
assert(clock.left_sum.system == 2);
|
||||
assert(clock.left_sum.stream == 3);
|
||||
assert(clock.right_sum.system == 10);
|
||||
assert(clock.right_sum.stream == 20);
|
||||
|
||||
sc_clock_update(&clock, 40, 80);
|
||||
assert(clock.count == 3);
|
||||
assert(clock.left_sum.system == 2);
|
||||
assert(clock.left_sum.stream == 3);
|
||||
assert(clock.right_sum.system == 50);
|
||||
assert(clock.right_sum.stream == 100);
|
||||
|
||||
sc_clock_update(&clock, 400, 800);
|
||||
assert(clock.count == 4);
|
||||
assert(clock.left_sum.system == 12);
|
||||
assert(clock.left_sum.stream == 23);
|
||||
assert(clock.right_sum.system == 440);
|
||||
assert(clock.right_sum.stream == 880);
|
||||
}
|
||||
|
||||
void test_large_rolling_sum(void) {
|
||||
const unsigned half_range = SC_CLOCK_RANGE / 2;
|
||||
|
||||
struct sc_clock clock1;
|
||||
sc_clock_init(&clock1);
|
||||
for (unsigned i = 0; i < 5 * half_range; ++i) {
|
||||
sc_clock_update(&clock1, i, 2 * i + 1);
|
||||
}
|
||||
|
||||
struct sc_clock clock2;
|
||||
sc_clock_init(&clock2);
|
||||
for (unsigned i = 3 * half_range; i < 5 * half_range; ++i) {
|
||||
sc_clock_update(&clock2, i, 2 * i + 1);
|
||||
}
|
||||
|
||||
assert(clock1.count == SC_CLOCK_RANGE);
|
||||
assert(clock2.count == SC_CLOCK_RANGE);
|
||||
|
||||
// The values before the last SC_CLOCK_RANGE points in clock1 should have
|
||||
// no impact
|
||||
assert(clock1.left_sum.system == clock2.left_sum.system);
|
||||
assert(clock1.left_sum.stream == clock2.left_sum.stream);
|
||||
assert(clock1.right_sum.system == clock2.right_sum.system);
|
||||
assert(clock1.right_sum.stream == clock2.right_sum.stream);
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
(void) argc;
|
||||
(void) argv;
|
||||
|
||||
test_small_rolling_sum();
|
||||
test_large_rolling_sum();
|
||||
return 0;
|
||||
};
|
@ -90,14 +90,13 @@ static void test_serialize_inject_touch_event(void) {
|
||||
},
|
||||
},
|
||||
.pressure = 1.0f,
|
||||
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
|
||||
},
|
||||
};
|
||||
|
||||
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
|
||||
size_t size = sc_control_msg_serialize(&msg, buf);
|
||||
assert(size == 32);
|
||||
assert(size == 28);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
||||
@ -106,8 +105,7 @@ static void test_serialize_inject_touch_event(void) {
|
||||
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
|
||||
0x04, 0x38, 0x07, 0x80, // 1080 1920
|
||||
0xff, 0xff, // pressure
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
|
||||
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
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;
|
||||
}
|
@ -7,7 +7,7 @@ buildscript {
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.4.0'
|
||||
classpath 'com.android.tools.build:gradle:7.2.2'
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
// in the individual module build.gradle files
|
||||
|
@ -16,6 +16,10 @@ cpu = 'i686'
|
||||
endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32'
|
||||
prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32'
|
||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
||||
ffmpeg_avcodec = 'avcodec-58'
|
||||
ffmpeg_avformat = 'avformat-58'
|
||||
ffmpeg_avutil = 'avutil-56'
|
||||
prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1'
|
||||
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
|
||||
prebuilt_libusb_root = 'libusb-1.0.26'
|
||||
prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32'
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user