Compare commits
63 Commits
audio
...
macos_open
Author | SHA1 | Date | |
---|---|---|---|
3615b3ff15 | |||
051b74c883 | |||
2e532afd2b | |||
fdf465851c | |||
669e9a8d1e | |||
f77e1c474e | |||
2f9396e24a | |||
0ebb3df69c | |||
2fff9b9edf | |||
57f879d68a | |||
3626d90004 | |||
02f4ff7534 | |||
a3871130cc | |||
53cb5635cf | |||
d7841664f4 | |||
39544f34b4 | |||
4755b97908 | |||
cba2501254 | |||
6ba99a62ff | |||
d2b7315ba6 | |||
337d6c2fd3 | |||
2eced46a37 | |||
1a80333747 | |||
fb61b779a6 | |||
5899af6a2f | |||
cbca79b95b | |||
02586cf21f | |||
80a6fa7a01 | |||
6b769675fa | |||
e5aa2ce01f | |||
cbc638c6ba | |||
abc1be4872 | |||
f1b2d6bbbb | |||
90926d40ad | |||
f12590ed08 | |||
05a55e3687 | |||
affda26bfa | |||
0bf866fa8d | |||
73727e7fdf | |||
c22c87eded | |||
426dfbf21d | |||
5512777404 | |||
cc07f8dac4 | |||
f5bb9e576d | |||
2380879376 | |||
eca8766545 | |||
0b8a5ca923 | |||
e06acc1ba2 | |||
14f9d82fda | |||
bb509d9317 | |||
238ab872ba | |||
3a72f3fb4d | |||
aa1efbc35c | |||
be985b8242 | |||
a9f6001f51 | |||
5052e15f7f | |||
4bdf632dfa | |||
4db50ddbb7 | |||
46f6918179 | |||
d93582724d | |||
b4caa483dd | |||
87da137238 | |||
b3f626feee |
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` issues
|
||||
## `adb` and USB issues
|
||||
|
||||
`scrcpy` execute `adb` commands to initialize the connection with the device. If
|
||||
`adb` fails, then scrcpy will not work.
|
||||
@ -133,6 +133,21 @@ 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
|
||||
|
||||
@ -153,8 +168,7 @@ 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 on Linux, it is possible to simulate a [physical
|
||||
keyboard][hid] (HID).
|
||||
Since scrcpy v1.20, 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
|
||||
@ -164,32 +178,6 @@ 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
|
||||
@ -224,102 +212,15 @@ As a workaround, [disable "Block compositing"][kwin].
|
||||
|
||||
### Exception
|
||||
|
||||
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:
|
||||
If you get any exception related to `MediaCodec`:
|
||||
|
||||
```
|
||||
scrcpy -m 1920
|
||||
scrcpy -m 1024
|
||||
scrcpy -m 800
|
||||
ERROR: Exception on thread Thread[main,5,main]
|
||||
java.lang.IllegalStateException
|
||||
at android.media.MediaCodec.native_dequeueOutputBuffer(Native Method)
|
||||
```
|
||||
|
||||
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/
|
||||
then try with another [encoder](doc/video.md#codec).
|
||||
|
||||
|
||||
## Translations
|
||||
|
2
LICENSE
2
LICENSE
@ -188,7 +188,7 @@
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright (C) 2018 Genymobile
|
||||
Copyright (C) 2018-2022 Romain Vimont
|
||||
Copyright (C) 2018-2023 Romain Vimont
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
|
@ -3,9 +3,11 @@ _scrcpy() {
|
||||
local opts="
|
||||
--always-on-top
|
||||
--audio-bit-rate=
|
||||
--audio-buffer=
|
||||
--audio-codec=
|
||||
--audio-codec-options=
|
||||
--audio-encoder=
|
||||
--audio-output-buffer=
|
||||
-b --video-bit-rate=
|
||||
--crop=
|
||||
-d --select-usb
|
||||
@ -115,20 +117,26 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
|
||||
return
|
||||
;;
|
||||
-b|--video-bit-rate \
|
||||
|--codec-options \
|
||||
--audio-bit-rate \
|
||||
|--audio-buffer \
|
||||
|-b|--video-bit-rate \
|
||||
|--audio-codec-options \
|
||||
|--audio-encoder \
|
||||
|--audio-output-buffer \
|
||||
|--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 any key to quit..."'
|
||||
Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter 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
|
||||
|
@ -10,9 +10,11 @@ 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]'
|
||||
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
|
||||
{-d,--select-usb}'[Use USB device]'
|
||||
|
@ -14,6 +14,7 @@ src = [
|
||||
'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',
|
||||
@ -277,10 +278,6 @@ 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',
|
||||
@ -310,7 +307,8 @@ if get_option('buildtype') == 'debug'
|
||||
]
|
||||
|
||||
foreach t : tests
|
||||
exe = executable(t[0], t[1],
|
||||
sources = t[1] + ['src/compat.c']
|
||||
exe = executable(t[0], sources,
|
||||
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-33.0.3
|
||||
DEP_DIR=platform-tools-34.0.1
|
||||
|
||||
FILENAME=platform-tools_r33.0.3-windows.zip
|
||||
SHA256SUM=1e59afd40a74c5c0eab0a9fad3f0faf8a674267106e0b19921be9f67081808c2
|
||||
FILENAME=platform-tools_r34.0.1-windows.zip
|
||||
SHA256SUM=5dd9c2be744c224fa3a7cbe30ba02d2cb378c763bd0f797a7e47e9f3156a5daa
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
@ -6,10 +6,10 @@ cd "$DIR"
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
DEP_DIR=SDL2-2.26.1
|
||||
DEP_DIR=SDL2-2.26.4
|
||||
|
||||
FILENAME=SDL2-devel-2.26.1-mingw.tar.gz
|
||||
SHA256SUM=aa43e1531a89551f9f9e14b27953a81d4ac946a9e574b5813cd0f2b36e83cc1c
|
||||
FILENAME=SDL2-devel-2.26.4-mingw.tar.gz
|
||||
SHA256SUM=fe899c8642caac2f180b1ee6f786857ddcaa0adc1fa82474312b09dd47d74712
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
@ -13,7 +13,7 @@ BEGIN
|
||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||
VALUE "OriginalFilename", "scrcpy.exe"
|
||||
VALUE "ProductName", "scrcpy"
|
||||
VALUE "ProductVersion", "1.25"
|
||||
VALUE "ProductVersion", "2.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
|
10
app/scrcpy.1
10
app/scrcpy.1
@ -33,6 +33,14 @@ Lower values decrease the latency, but increase the likelyhood of buffer underru
|
||||
|
||||
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).
|
||||
@ -571,7 +579,7 @@ Copyright \(co 2018 Genymobile
|
||||
Genymobile
|
||||
.UE
|
||||
|
||||
Copyright \(co 2018\-2022
|
||||
Copyright \(co 2018\-2023
|
||||
.MT rom@rom1v.com
|
||||
Romain Vimont
|
||||
.ME
|
||||
|
@ -204,6 +204,7 @@ 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);
|
||||
@ -215,12 +216,12 @@ sc_adb_parse_device_ip(char *str) {
|
||||
return ip;
|
||||
}
|
||||
|
||||
idx_line += len;
|
||||
|
||||
if (str[idx_line] != '\0') {
|
||||
// The next line starts after the '\n'
|
||||
++idx_line;
|
||||
if (is_last_line) {
|
||||
break;
|
||||
}
|
||||
|
||||
// The next line starts after the '\n'
|
||||
idx_line += len + 1;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
|
@ -1,55 +1,90 @@
|
||||
#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 SC_AUDIO_OUTPUT_BUFFER_SAMPLES 240 // 5ms at 48000Hz
|
||||
|
||||
static inline uint32_t
|
||||
bytes_to_samples(struct sc_audio_player *ap, size_t bytes) {
|
||||
assert(bytes % (ap->nb_channels * ap->out_bytes_per_sample) == 0);
|
||||
return bytes / (ap->nb_channels * ap->out_bytes_per_sample);
|
||||
}
|
||||
|
||||
static inline size_t
|
||||
samples_to_bytes(struct sc_audio_player *ap, uint32_t samples) {
|
||||
return samples * ap->nb_channels * ap->out_bytes_per_sample;
|
||||
}
|
||||
#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 bytebuf is protected
|
||||
// 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",
|
||||
bytes_to_samples(ap, len));
|
||||
LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count);
|
||||
#endif
|
||||
|
||||
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
|
||||
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
|
||||
if (!ap->played) {
|
||||
uint32_t buffered_samples = bytes_to_samples(ap, read_avail);
|
||||
|
||||
// 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", bytes_to_samples(ap, len));
|
||||
" 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).
|
||||
@ -58,26 +93,25 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
||||
}
|
||||
}
|
||||
|
||||
size_t read = MIN(read_avail, len);
|
||||
uint32_t read = MIN(buffered_samples, count);
|
||||
if (read) {
|
||||
sc_bytebuf_read(&ap->buf, stream, read);
|
||||
sc_audiobuf_read(&ap->buf, stream, read);
|
||||
}
|
||||
|
||||
if (read < len) {
|
||||
size_t silence_bytes = len - read;
|
||||
uint32_t silence_samples = bytes_to_samples(ap, silence_bytes);
|
||||
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_samples);
|
||||
memset(stream + read, 0, silence_bytes);
|
||||
silence);
|
||||
memset(stream + read, 0, TO_BYTES(silence));
|
||||
|
||||
if (ap->received) {
|
||||
// Inserting additional samples immediately increases buffering
|
||||
ap->avg_buffering.avg += silence_samples;
|
||||
ap->underflow += silence;
|
||||
}
|
||||
}
|
||||
|
||||
@ -86,7 +120,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
|
||||
|
||||
static uint8_t *
|
||||
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) {
|
||||
size_t min_buf_size = samples_to_bytes(ap, 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);
|
||||
@ -129,7 +163,6 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
// 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);
|
||||
size_t swr_buf_size = samples_to_bytes(ap, samples_written);
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
LOGD("[Audio] %" PRIu32 " samples written to buffer", samples_written);
|
||||
#endif
|
||||
@ -137,46 +170,40 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
// 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 = swr_buf_size <= ap->previous_write_avail;
|
||||
bool lockless_write = samples_written <= ap->previous_can_write;
|
||||
if (lockless_write) {
|
||||
sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size);
|
||||
sc_audiobuf_prepare_write(&ap->buf, swr_buf, samples_written);
|
||||
}
|
||||
|
||||
SDL_LockAudioDevice(ap->device);
|
||||
|
||||
size_t read_avail = sc_bytebuf_read_available(&ap->buf);
|
||||
uint32_t buffered_samples = bytes_to_samples(ap, read_avail);
|
||||
uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf);
|
||||
|
||||
if (lockless_write) {
|
||||
sc_bytebuf_commit_write(&ap->buf, swr_buf_size);
|
||||
sc_audiobuf_commit_write(&ap->buf, samples_written);
|
||||
} else {
|
||||
// Take care to keep full samples
|
||||
size_t align = ap->nb_channels * ap->out_bytes_per_sample;
|
||||
size_t write_avail =
|
||||
sc_bytebuf_write_available(&ap->buf) / align * align;
|
||||
if (swr_buf_size > write_avail) {
|
||||
// Entering this branch is very unlikely, the ring-buffer (bytebuf)
|
||||
// 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.
|
||||
size_t cap = sc_bytebuf_capacity(&ap->buf) / align * align;
|
||||
if (swr_buf_size > cap) {
|
||||
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 ring-buffer size (or something is very wrong).
|
||||
// exceed the audio buffer size (or something is very wrong).
|
||||
// Ignore the first bytes in swr_buf
|
||||
swr_buf += swr_buf_size - cap;
|
||||
swr_buf_size = cap;
|
||||
swr_buf += TO_BYTES(samples_written - cap);
|
||||
// This change in samples_written will impact the
|
||||
// instant_compensation below
|
||||
samples_written -= bytes_to_samples(ap, swr_buf_size - cap);
|
||||
samples_written = cap;
|
||||
}
|
||||
|
||||
assert(swr_buf_size >= write_avail);
|
||||
if (swr_buf_size > write_avail) {
|
||||
sc_bytebuf_skip(&ap->buf, swr_buf_size - write_avail);
|
||||
uint32_t skip_samples =
|
||||
bytes_to_samples(ap, swr_buf_size - write_avail);
|
||||
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
|
||||
@ -186,39 +213,62 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
|
||||
// It should remain exactly the expected size to write the new
|
||||
// samples.
|
||||
assert((sc_bytebuf_write_available(&ap->buf) / align * align)
|
||||
== swr_buf_size);
|
||||
assert(sc_audiobuf_can_write(&ap->buf) == samples_written);
|
||||
}
|
||||
|
||||
sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size);
|
||||
sc_audiobuf_write(&ap->buf, swr_buf, samples_written);
|
||||
}
|
||||
|
||||
buffered_samples += samples_written;
|
||||
assert(samples_to_bytes(ap, buffered_samples)
|
||||
== sc_bytebuf_read_available(&ap->buf));
|
||||
assert(buffered_samples == sc_audiobuf_can_read(&ap->buf));
|
||||
|
||||
// Read with lock held, to be used after unlocking
|
||||
bool played = ap->played;
|
||||
uint32_t underflow = ap->underflow;
|
||||
|
||||
if (played) {
|
||||
uint32_t max_buffered_samples = ap->target_buffering
|
||||
+ 12 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES
|
||||
+ 12 * ap->output_buffer
|
||||
+ ap->target_buffering / 10;
|
||||
if (buffered_samples > max_buffered_samples) {
|
||||
uint32_t skip_samples = buffered_samples - max_buffered_samples;
|
||||
size_t skip_bytes = samples_to_bytes(ap, skip_samples);
|
||||
sc_bytebuf_skip(&ap->buf, skip_bytes);
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
sc_audiobuf_skip(&ap->buf, skip_samples);
|
||||
LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32
|
||||
" samples", skip_samples);
|
||||
#endif
|
||||
}
|
||||
|
||||
// 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;
|
||||
ap->avg_buffering.avg += instant_compensation + inserted_silence;
|
||||
|
||||
|
||||
// However, the buffering level must be smoothed
|
||||
sc_average_push(&ap->avg_buffering, buffered_samples);
|
||||
@ -227,29 +277,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
LOGD("[Audio] buffered_samples=%" PRIu32 " avg_buffering=%f",
|
||||
buffered_samples, sc_average_get(&ap->avg_buffering));
|
||||
#endif
|
||||
} else {
|
||||
// SDL playback not started yet, do not accumulate more than
|
||||
// max_initial_buffering samples, this would cause unnecessary delay
|
||||
// (and glitches to compensate) on start.
|
||||
uint32_t max_initial_buffering = ap->target_buffering
|
||||
+ 2 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES;
|
||||
if (buffered_samples > max_initial_buffering) {
|
||||
uint32_t skip_samples = buffered_samples - max_initial_buffering;
|
||||
size_t skip_bytes = samples_to_bytes(ap, skip_samples);
|
||||
sc_bytebuf_skip(&ap->buf, skip_bytes);
|
||||
#ifndef SC_AUDIO_PLAYER_NDEBUG
|
||||
LOGD("[Audio] Playback not started, skipping %" PRIu32 " samples",
|
||||
skip_samples);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
|
||||
ap->received = true;
|
||||
|
||||
SDL_UnlockAudioDevice(ap->device);
|
||||
|
||||
if (played) {
|
||||
ap->samples_since_resync += samples_written;
|
||||
if (ap->samples_since_resync >= ap->sample_rate) {
|
||||
// Recompute compensation every second
|
||||
@ -257,7 +285,10 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
|
||||
float avg = sc_average_get(&ap->avg_buffering);
|
||||
int diff = ap->target_buffering - avg;
|
||||
if (diff < 0 && buffered_samples < ap->target_buffering) {
|
||||
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;
|
||||
@ -271,10 +302,15 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink,
|
||||
LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32
|
||||
" compensation=%d", ap->target_buffering, avg,
|
||||
buffered_samples, diff);
|
||||
int ret = swr_set_compensation(swr_ctx, diff, distance);
|
||||
if (ret < 0) {
|
||||
LOGW("Resampling compensation failed: %d", ret);
|
||||
// not fatal
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -295,11 +331,28 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
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 = SC_AUDIO_OUTPUT_BUFFER_SAMPLES,
|
||||
.samples = aout_samples,
|
||||
.callback = sc_audio_player_sdl_callback,
|
||||
.userdata = ap,
|
||||
};
|
||||
@ -318,11 +371,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
}
|
||||
ap->swr_ctx = swr_ctx;
|
||||
|
||||
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);
|
||||
|
||||
#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);
|
||||
@ -345,34 +393,27 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
goto error_free_swr_ctx;
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// 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 bytebuf_samples = ap->target_buffering + ap->sample_rate;
|
||||
size_t bytebuf_size = samples_to_bytes(ap, bytebuf_samples);
|
||||
size_t audiobuf_samples = ap->target_buffering + ap->sample_rate;
|
||||
|
||||
bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size);
|
||||
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 = samples_to_bytes(ap, 4096);
|
||||
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_bytebuf;
|
||||
goto error_destroy_audiobuf;
|
||||
}
|
||||
ap->swr_buf_alloc_size = initial_swr_buf_size;
|
||||
|
||||
ap->previous_write_avail = sc_bytebuf_write_available(&ap->buf);
|
||||
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.
|
||||
@ -381,6 +422,8 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
|
||||
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.
|
||||
@ -394,8 +437,8 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_bytebuf:
|
||||
sc_bytebuf_destroy(&ap->buf);
|
||||
error_destroy_audiobuf:
|
||||
sc_audiobuf_destroy(&ap->buf);
|
||||
error_free_swr_ctx:
|
||||
swr_free(&ap->swr_ctx);
|
||||
error_close_audio_device:
|
||||
@ -413,13 +456,15 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
|
||||
SDL_CloseAudioDevice(ap->device);
|
||||
|
||||
free(ap->swr_buf);
|
||||
sc_bytebuf_destroy(&ap->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_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,
|
||||
|
@ -5,8 +5,8 @@
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "trait/frame_sink.h"
|
||||
#include <util/audiobuf.h>
|
||||
#include <util/average.h>
|
||||
#include <util/bytebuf.h>
|
||||
#include <util/thread.h>
|
||||
#include <util/tick.h>
|
||||
|
||||
@ -27,13 +27,17 @@ struct sc_audio_player {
|
||||
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_bytebuf buf;
|
||||
struct sc_audiobuf buf;
|
||||
|
||||
// The previous number of bytes available in the buffer (only used by the
|
||||
// receiver thread)
|
||||
size_t previous_write_avail;
|
||||
// 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;
|
||||
@ -56,6 +60,13 @@ struct sc_audio_player {
|
||||
// (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;
|
||||
@ -73,6 +84,7 @@ struct sc_audio_player_callbacks {
|
||||
};
|
||||
|
||||
void
|
||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering);
|
||||
sc_audio_player_init(struct sc_audio_player *ap, sc_tick target_buffering,
|
||||
sc_tick audio_output_buffer);
|
||||
|
||||
#endif
|
||||
|
@ -18,8 +18,7 @@
|
||||
#define STR(x) STR_IMPL_(x)
|
||||
|
||||
enum {
|
||||
OPT_RENDER_EXPIRED_FRAMES = 1000,
|
||||
OPT_BIT_RATE,
|
||||
OPT_BIT_RATE = 1000,
|
||||
OPT_WINDOW_TITLE,
|
||||
OPT_PUSH_TARGET,
|
||||
OPT_ALWAYS_ON_TOP,
|
||||
@ -72,6 +71,7 @@ enum {
|
||||
OPT_LIST_DISPLAYS,
|
||||
OPT_REQUIRE_AUDIO,
|
||||
OPT_AUDIO_BUFFER,
|
||||
OPT_AUDIO_OUTPUT_BUFFER,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
@ -130,6 +130,16 @@ static const struct sc_option options[] = {
|
||||
"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",
|
||||
@ -471,11 +481,6 @@ static const struct sc_option options[] = {
|
||||
"\"opengles2\", \"opengles\", \"metal\" and \"software\".\n"
|
||||
"<https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>",
|
||||
},
|
||||
{
|
||||
// deprecated
|
||||
.longopt_id = OPT_RENDER_EXPIRED_FRAMES,
|
||||
.longopt = "render-expired-frames",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_REQUIRE_AUDIO,
|
||||
.longopt = "require-audio",
|
||||
@ -1210,6 +1215,19 @@ 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) {
|
||||
@ -1541,8 +1559,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
while ((c = getopt_long(argc, argv, optstring, longopts, NULL)) != -1) {
|
||||
switch (c) {
|
||||
case OPT_BIT_RATE:
|
||||
LOGW("--bit-rate is deprecated, use --video-bit-rate instead.");
|
||||
// fall through
|
||||
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;
|
||||
@ -1570,9 +1589,6 @@ 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;
|
||||
@ -1660,10 +1676,6 @@ 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;
|
||||
@ -1722,9 +1734,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->forward_key_repeat = false;
|
||||
break;
|
||||
case OPT_CODEC_OPTIONS:
|
||||
LOGW("--codec-options is deprecated, use --video-codec-options "
|
||||
"instead.");
|
||||
// fall through
|
||||
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;
|
||||
break;
|
||||
@ -1732,8 +1744,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->audio_codec_options = optarg;
|
||||
break;
|
||||
case OPT_ENCODER:
|
||||
LOGW("--encoder is deprecated, use --video-encoder instead.");
|
||||
// fall through
|
||||
LOGE("--encoder has been removed, "
|
||||
"use --video-encoder or --audio-encoder.");
|
||||
return false;
|
||||
case OPT_VIDEO_ENCODER:
|
||||
opts->video_encoder = optarg;
|
||||
break;
|
||||
@ -1788,8 +1801,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->start_fps_counter = true;
|
||||
break;
|
||||
case OPT_CODEC:
|
||||
LOGW("--codec is deprecated, use --video-codec instead.");
|
||||
// fall through
|
||||
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;
|
||||
@ -1841,6 +1855,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
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;
|
||||
|
108
app/src/clock.c
108
app/src/clock.c
@ -1,116 +1,36 @@
|
||||
#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->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);
|
||||
|
||||
if (clock->count == 1) {
|
||||
// If there is only 1 point, we can't compute a slope. Assume it is 1.
|
||||
struct sc_clock_point *single_point = &clock->right_sum;
|
||||
*out_slope = 1;
|
||||
*out_offset = single_point->system - single_point->stream;
|
||||
return;
|
||||
}
|
||||
|
||||
struct sc_clock_point left_avg = {
|
||||
.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;
|
||||
clock->range = 0;
|
||||
clock->offset = 0;
|
||||
}
|
||||
|
||||
void
|
||||
sc_clock_update(struct sc_clock *clock, sc_tick system, sc_tick stream) {
|
||||
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;
|
||||
if (clock->range < SC_CLOCK_RANGE) {
|
||||
++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;
|
||||
|
||||
// Update estimation
|
||||
sc_clock_estimate(clock, &clock->slope, &clock->offset);
|
||||
sc_tick offset = system - stream;
|
||||
clock->offset = ((clock->range - 1) * clock->offset + offset)
|
||||
/ clock->range;
|
||||
|
||||
#ifndef SC_CLOCK_NDEBUG
|
||||
LOGD("Clock estimation: %f * pts + %" PRItick, clock->slope, clock->offset);
|
||||
LOGD("Clock estimation: pts + %" PRItick, clock->offset);
|
||||
#endif
|
||||
}
|
||||
|
||||
sc_tick
|
||||
sc_clock_to_system_time(struct sc_clock *clock, sc_tick stream) {
|
||||
assert(clock->count); // sc_clock_update() must have been called
|
||||
return (sc_tick) (stream * clock->slope) + clock->offset;
|
||||
assert(clock->range); // sc_clock_update() must have been called
|
||||
return stream + clock->offset;
|
||||
}
|
||||
|
@ -3,13 +3,8 @@
|
||||
|
||||
#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;
|
||||
@ -21,40 +16,18 @@ struct sc_clock_point {
|
||||
*
|
||||
* f(stream) = slope * stream + offset
|
||||
*
|
||||
* 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.
|
||||
* Theoretically, the slope encodes the drift between the device clock and the
|
||||
* computer clock. It is expected to be very close to 1.
|
||||
*
|
||||
* 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.
|
||||
* 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 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.
|
||||
* Therefore, only the offset is estimated.
|
||||
*/
|
||||
struct sc_clock {
|
||||
// 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;
|
||||
unsigned range;
|
||||
sc_tick offset;
|
||||
};
|
||||
|
||||
|
@ -12,52 +12,20 @@
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_decoder, packet_sink)
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
decoder->codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||
|
||||
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||
// Hardcoded video properties
|
||||
decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
} else {
|
||||
// Hardcoded audio properties
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
decoder->codec_ctx->ch_layout =
|
||||
(AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
|
||||
#else
|
||||
decoder->codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||
decoder->codec_ctx->channels = 2;
|
||||
#endif
|
||||
decoder->codec_ctx->sample_rate = 48000;
|
||||
}
|
||||
|
||||
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
|
||||
LOGE("Decoder '%s': could not open codec", decoder->name);
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_decoder_open(struct sc_decoder *decoder, AVCodecContext *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_frame_source_sinks_open(&decoder->frame_source,
|
||||
decoder->codec_ctx)) {
|
||||
if (!sc_frame_source_sinks_open(&decoder->frame_source, ctx)) {
|
||||
av_frame_free(&decoder->frame);
|
||||
avcodec_close(decoder->codec_ctx);
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
return false;
|
||||
}
|
||||
|
||||
decoder->ctx = ctx;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -65,8 +33,6 @@ static void
|
||||
sc_decoder_close(struct sc_decoder *decoder) {
|
||||
sc_frame_source_sinks_close(&decoder->frame_source);
|
||||
av_frame_free(&decoder->frame);
|
||||
avcodec_close(decoder->codec_ctx);
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
}
|
||||
|
||||
static bool
|
||||
@ -77,7 +43,7 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
||||
return true;
|
||||
}
|
||||
|
||||
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
|
||||
int ret = avcodec_send_packet(decoder->ctx, packet);
|
||||
if (ret < 0 && ret != AVERROR(EAGAIN)) {
|
||||
LOGE("Decoder '%s': could not send video packet: %d",
|
||||
decoder->name, ret);
|
||||
@ -85,7 +51,7 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
|
||||
ret = avcodec_receive_frame(decoder->ctx, decoder->frame);
|
||||
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
|
||||
break;
|
||||
}
|
||||
@ -110,9 +76,9 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, const AVCodec *codec) {
|
||||
sc_decoder_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) {
|
||||
struct sc_decoder *decoder = DOWNCAST(sink);
|
||||
return sc_decoder_open(decoder, codec);
|
||||
return sc_decoder_open(decoder, ctx);
|
||||
}
|
||||
|
||||
static void
|
||||
|
@ -16,7 +16,7 @@ struct sc_decoder {
|
||||
|
||||
const char *name; // must be statically allocated (e.g. a string literal)
|
||||
|
||||
AVCodecContext *codec_ctx;
|
||||
AVCodecContext *ctx;
|
||||
AVFrame *frame;
|
||||
};
|
||||
|
||||
|
@ -194,7 +194,7 @@ sc_delay_buffer_frame_sink_push(struct sc_frame_sink *sink,
|
||||
sc_clock_update(&db->clock, sc_tick_now(), pts);
|
||||
sc_cond_signal(&db->wait_cond);
|
||||
|
||||
if (db->first_frame_asap && db->clock.count == 1) {
|
||||
if (db->first_frame_asap && db->clock.range == 1) {
|
||||
sc_mutex_unlock(&db->mutex);
|
||||
return sc_frame_source_sinks_push(&db->frame_source, frame);
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
#include "demuxer.h"
|
||||
|
||||
#include <assert.h>
|
||||
#include <libavutil/channel_layout.h>
|
||||
#include <libavutil/time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
@ -57,6 +58,20 @@ sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
|
||||
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
|
||||
@ -160,10 +175,45 @@ run_demuxer(void *data) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec)) {
|
||||
AVCodecContext *codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!codec_ctx) {
|
||||
LOG_OOM();
|
||||
goto end;
|
||||
}
|
||||
|
||||
codec_ctx->flags |= AV_CODEC_FLAG_LOW_DELAY;
|
||||
|
||||
if (codec->type == AVMEDIA_TYPE_VIDEO) {
|
||||
uint32_t width;
|
||||
uint32_t height;
|
||||
ok = sc_demuxer_recv_video_size(demuxer, &width, &height);
|
||||
if (!ok) {
|
||||
goto finally_free_context;
|
||||
}
|
||||
|
||||
codec_ctx->width = width;
|
||||
codec_ctx->height = height;
|
||||
codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
} else {
|
||||
// Hardcoded audio properties
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
codec_ctx->ch_layout = (AVChannelLayout) AV_CHANNEL_LAYOUT_STEREO;
|
||||
#else
|
||||
codec_ctx->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||
codec_ctx->channels = 2;
|
||||
#endif
|
||||
codec_ctx->sample_rate = 48000;
|
||||
}
|
||||
|
||||
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
||||
LOGE("Demuxer '%s': could not open codec", demuxer->name);
|
||||
goto finally_free_context;
|
||||
}
|
||||
|
||||
if (!sc_packet_source_sinks_open(&demuxer->packet_source, codec_ctx)) {
|
||||
goto finally_free_context;
|
||||
}
|
||||
|
||||
// Config packets must be merged with the next non-config packet only for
|
||||
// video streams
|
||||
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
|
||||
@ -214,6 +264,9 @@ run_demuxer(void *data) {
|
||||
av_packet_free(&packet);
|
||||
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);
|
||||
end:
|
||||
demuxer->cbs->on_ended(demuxer, status, demuxer->cbs_userdata);
|
||||
|
||||
|
185
app/src/display.c
Normal file
185
app/src/display.c
Normal file
@ -0,0 +1,185 @@
|
||||
#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;
|
||||
}
|
45
app/src/display.h
Normal file
45
app/src/display.h
Normal file
@ -0,0 +1,45 @@
|
||||
#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
|
@ -5,3 +5,4 @@
|
||||
#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)
|
||||
|
@ -172,14 +172,18 @@ sc_file_pusher_start(struct sc_file_pusher *fp) {
|
||||
|
||||
void
|
||||
sc_file_pusher_stop(struct sc_file_pusher *fp) {
|
||||
sc_mutex_lock(&fp->mutex);
|
||||
fp->stopped = true;
|
||||
sc_cond_signal(&fp->event_cond);
|
||||
sc_intr_interrupt(&fp->intr);
|
||||
sc_mutex_unlock(&fp->mutex);
|
||||
if (fp->initialized) {
|
||||
sc_mutex_lock(&fp->mutex);
|
||||
fp->stopped = true;
|
||||
sc_cond_signal(&fp->event_cond);
|
||||
sc_intr_interrupt(&fp->intr);
|
||||
sc_mutex_unlock(&fp->mutex);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
sc_file_pusher_join(struct sc_file_pusher *fp) {
|
||||
sc_thread_join(&fp->thread, NULL);
|
||||
if (fp->initialized) {
|
||||
sc_thread_join(&fp->thread, NULL);
|
||||
}
|
||||
}
|
||||
|
@ -96,6 +96,7 @@ 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;
|
||||
|
@ -797,7 +797,8 @@ sc_input_manager_process_file(struct sc_input_manager *im,
|
||||
}
|
||||
|
||||
void
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im, SDL_Event *event) {
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
const SDL_Event *event) {
|
||||
bool control = im->controller;
|
||||
switch (event->type) {
|
||||
case SDL_TEXTINPUT:
|
||||
|
@ -61,6 +61,7 @@ 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, SDL_Event *event);
|
||||
sc_input_manager_handle_event(struct sc_input_manager *im,
|
||||
const SDL_Event *event);
|
||||
|
||||
#endif
|
||||
|
@ -44,6 +44,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.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
|
||||
|
@ -127,6 +127,7 @@ struct scrcpy_options {
|
||||
sc_tick display_buffer;
|
||||
sc_tick v4l2_buffer;
|
||||
sc_tick audio_buffer;
|
||||
sc_tick audio_output_buffer;
|
||||
#ifdef HAVE_USB
|
||||
bool otg;
|
||||
#endif
|
||||
|
@ -150,72 +150,6 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) {
|
||||
avformat_free_context(recorder->ctx);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_wait_video_stream(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
while (!recorder->video_codec && !recorder->stopped) {
|
||||
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||
}
|
||||
const AVCodec *codec = recorder->video_codec;
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (codec) {
|
||||
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
stream->codecpar->codec_id = codec->id;
|
||||
stream->codecpar->format = AV_PIX_FMT_YUV420P;
|
||||
stream->codecpar->width = recorder->declared_frame_size.width;
|
||||
stream->codecpar->height = recorder->declared_frame_size.height;
|
||||
|
||||
recorder->video_stream_index = stream->index;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_wait_audio_stream(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
while (!recorder->audio_codec && !recorder->audio_disabled
|
||||
&& !recorder->stopped) {
|
||||
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
|
||||
}
|
||||
|
||||
if (recorder->audio_disabled) {
|
||||
// Reset audio flag. From there, the recorder thread may access this
|
||||
// flag without any mutex.
|
||||
recorder->audio = false;
|
||||
}
|
||||
|
||||
const AVCodec *codec = recorder->audio_codec;
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
if (codec) {
|
||||
AVStream *stream = avformat_new_stream(recorder->ctx, codec);
|
||||
if (!stream) {
|
||||
return false;
|
||||
}
|
||||
|
||||
stream->codecpar->codec_type = AVMEDIA_TYPE_AUDIO;
|
||||
stream->codecpar->codec_id = codec->id;
|
||||
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
|
||||
stream->codecpar->ch_layout.nb_channels = 2;
|
||||
#else
|
||||
stream->codecpar->channel_layout = AV_CH_LAYOUT_STEREO;
|
||||
stream->codecpar->channels = 2;
|
||||
#endif
|
||||
stream->codecpar->sample_rate = 48000;
|
||||
|
||||
recorder->audio_stream_index = stream->index;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static inline bool
|
||||
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
|
||||
if (sc_vecdeque_is_empty(&recorder->video_queue)) {
|
||||
@ -236,8 +170,10 @@ static bool
|
||||
sc_recorder_process_header(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) {
|
||||
sc_cond_wait(&recorder->queue_cond, &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)) {
|
||||
@ -480,22 +416,6 @@ sc_recorder_record(struct sc_recorder *recorder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_recorder_wait_video_stream(recorder);
|
||||
if (!ok) {
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (recorder->audio) {
|
||||
ok = sc_recorder_wait_audio_stream(recorder);
|
||||
if (!ok) {
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// If recorder->stopped, process any queued packet anyway
|
||||
|
||||
ok = sc_recorder_process_packets(recorder);
|
||||
sc_recorder_close_output_file(recorder);
|
||||
return ok;
|
||||
@ -536,9 +456,10 @@ run_recorder(void *data) {
|
||||
|
||||
static bool
|
||||
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
AVCodecContext *ctx) {
|
||||
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
|
||||
assert(codec);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->video_init);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
if (recorder->stopped) {
|
||||
@ -546,7 +467,21 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
return false;
|
||||
}
|
||||
|
||||
recorder->video_codec = codec;
|
||||
AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec);
|
||||
if (!stream) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
int r = avcodec_parameters_from_context(stream->codecpar, ctx);
|
||||
if (r < 0) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
recorder->video_stream_index = stream->index;
|
||||
|
||||
recorder->video_init = true;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
@ -556,6 +491,8 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
|
||||
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_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
@ -568,6 +505,8 @@ 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_mutex_lock(&recorder->mutex);
|
||||
|
||||
@ -601,15 +540,29 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
|
||||
|
||||
static bool
|
||||
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
AVCodecContext *ctx) {
|
||||
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
|
||||
assert(recorder->audio);
|
||||
// only written from this thread, no need to lock
|
||||
assert(!recorder->audio_disabled);
|
||||
assert(codec);
|
||||
assert(!recorder->audio_init);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->audio_codec = codec;
|
||||
|
||||
AVStream *stream = avformat_new_stream(recorder->ctx, ctx->codec);
|
||||
if (!stream) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
int r = avcodec_parameters_from_context(stream->codecpar, ctx);
|
||||
if (r < 0) {
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
|
||||
recorder->audio_stream_index = stream->index;
|
||||
|
||||
recorder->audio_init = true;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
@ -621,7 +574,7 @@ 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_disabled);
|
||||
assert(recorder->audio_init);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
@ -636,7 +589,7 @@ sc_recorder_audio_packet_sink_push(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_disabled);
|
||||
assert(recorder->audio_init);
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
|
||||
@ -673,13 +626,13 @@ 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_disabled);
|
||||
assert(!recorder->audio_codec);
|
||||
assert(!recorder->audio_init);
|
||||
|
||||
LOGW("Audio stream recording disabled");
|
||||
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->audio_disabled = true;
|
||||
recorder->audio = false;
|
||||
recorder->audio_init = true;
|
||||
sc_cond_signal(&recorder->stream_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
}
|
||||
@ -687,7 +640,6 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool audio,
|
||||
struct sc_size declared_frame_size,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
|
||||
recorder->filename = strdup(filename);
|
||||
if (!recorder->filename) {
|
||||
@ -716,15 +668,13 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
sc_vecdeque_init(&recorder->audio_queue);
|
||||
recorder->stopped = false;
|
||||
|
||||
recorder->video_codec = NULL;
|
||||
recorder->audio_codec = NULL;
|
||||
recorder->audio_disabled = 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;
|
||||
|
@ -31,7 +31,6 @@ struct sc_recorder {
|
||||
char *filename;
|
||||
enum sc_record_format format;
|
||||
AVFormatContext *ctx;
|
||||
struct sc_size declared_frame_size;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
@ -43,11 +42,8 @@ struct sc_recorder {
|
||||
|
||||
// wake up the recorder thread once the video or audio codec is known
|
||||
sc_cond stream_cond;
|
||||
const AVCodec *video_codec;
|
||||
const AVCodec *audio_codec;
|
||||
// Instead of providing an audio_codec, the demuxer may notify that the
|
||||
// stream is disabled if the device could not capture audio
|
||||
bool audio_disabled;
|
||||
bool video_init;
|
||||
bool audio_init;
|
||||
|
||||
int video_stream_index;
|
||||
int audio_stream_index;
|
||||
@ -64,7 +60,6 @@ struct sc_recorder_callbacks {
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool audio,
|
||||
struct sc_size declared_frame_size,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
bool
|
||||
|
@ -175,7 +175,9 @@ event_loop(struct scrcpy *s) {
|
||||
LOGD("User requested to quit");
|
||||
return SCRCPY_EXIT_SUCCESS;
|
||||
default:
|
||||
sc_screen_handle_event(&s->screen, &event);
|
||||
if (!sc_screen_handle_event(&s->screen, &event)) {
|
||||
return SCRCPY_EXIT_FAILURE;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -471,7 +473,7 @@ scrcpy(struct scrcpy_options *options) {
|
||||
};
|
||||
if (!sc_recorder_init(&s->recorder, options->record_filename,
|
||||
options->record_format, options->audio,
|
||||
info->frame_size, &recorder_cbs, NULL)) {
|
||||
&recorder_cbs, NULL)) {
|
||||
goto end;
|
||||
}
|
||||
recorder_initialized = true;
|
||||
@ -658,7 +660,6 @@ 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,
|
||||
@ -687,7 +688,8 @@ aoa_hid_end:
|
||||
sc_frame_source_add_sink(src, &s->screen.frame_sink);
|
||||
|
||||
if (options->audio) {
|
||||
sc_audio_player_init(&s->audio_player, options->audio_buffer);
|
||||
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);
|
||||
}
|
||||
@ -695,8 +697,7 @@ aoa_hid_end:
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
if (options->v4l2_device) {
|
||||
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device,
|
||||
info->frame_size)) {
|
||||
if (!sc_v4l2_sink_init(&s->v4l2_sink, options->v4l2_device)) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
|
236
app/src/screen.c
236
app/src/screen.c
@ -239,33 +239,6 @@ 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
|
||||
@ -276,35 +249,11 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
|
||||
sc_screen_update_content_rect(screen);
|
||||
}
|
||||
|
||||
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);
|
||||
bool ok = sc_display_render(&screen->display, &screen->rect,
|
||||
screen->rotation);
|
||||
(void) ok; // error already logged
|
||||
}
|
||||
|
||||
|
||||
#if defined(__APPLE__) || defined(__WINDOWS__)
|
||||
# define CONTINUOUS_RESIZING_WORKAROUND
|
||||
#endif
|
||||
@ -335,7 +284,25 @@ sc_screen_frame_sink_open(struct sc_frame_sink *sink,
|
||||
(void) ctx;
|
||||
|
||||
struct sc_screen *screen = DOWNCAST(sink);
|
||||
(void) screen;
|
||||
|
||||
assert(ctx->width > 0 && ctx->width <= 0xFFFF);
|
||||
assert(ctx->height > 0 && ctx->height <= 0xFFFF);
|
||||
// screen->frame_size is never used before the event is pushed, and the
|
||||
// event acts as a memory barrier so it is safe without mutex
|
||||
screen->frame_size.width = ctx->width;
|
||||
screen->frame_size.height = ctx->height;
|
||||
|
||||
static SDL_Event event = {
|
||||
.type = SC_EVENT_SCREEN_INIT_SIZE,
|
||||
};
|
||||
|
||||
// Post the event on the UI thread (the texture must be created from there)
|
||||
int ret = SDL_PushEvent(&event);
|
||||
if (ret < 0) {
|
||||
LOGW("Could not post init size event: %s", SDL_GetError());
|
||||
return false;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
screen->open = true;
|
||||
#endif
|
||||
@ -410,14 +377,10 @@ sc_screen_init(struct sc_screen *screen,
|
||||
goto error_destroy_frame_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
|
||||
@ -437,46 +400,11 @@ sc_screen_init(struct sc_screen *screen,
|
||||
goto error_destroy_fps_counter;
|
||||
}
|
||||
|
||||
screen->renderer = SDL_CreateRenderer(screen->window, -1,
|
||||
SDL_RENDERER_ACCELERATED);
|
||||
if (!screen->renderer) {
|
||||
LOGE("Could not create renderer: %s", SDL_GetError());
|
||||
ok = sc_display_init(&screen->display, screen->window, params->mipmaps);
|
||||
if (!ok) {
|
||||
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);
|
||||
@ -485,18 +413,10 @@ 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_texture;
|
||||
goto error_destroy_display;
|
||||
}
|
||||
|
||||
struct sc_input_manager_params im_params = {
|
||||
@ -531,10 +451,8 @@ sc_screen_init(struct sc_screen *screen,
|
||||
|
||||
return true;
|
||||
|
||||
error_destroy_texture:
|
||||
SDL_DestroyTexture(screen->texture);
|
||||
error_destroy_renderer:
|
||||
SDL_DestroyRenderer(screen->renderer);
|
||||
error_destroy_display:
|
||||
sc_display_destroy(&screen->display);
|
||||
error_destroy_window:
|
||||
SDL_DestroyWindow(screen->window);
|
||||
error_destroy_fps_counter:
|
||||
@ -590,9 +508,8 @@ 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);
|
||||
@ -655,47 +572,38 @@ 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) {
|
||||
// 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;
|
||||
}
|
||||
if (screen->frame_size.width == new_frame_size.width
|
||||
&& screen->frame_size.height == new_frame_size.height) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
// frame dimension changed
|
||||
screen->frame_size = new_frame_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]);
|
||||
struct sc_size new_content_size =
|
||||
get_rotated_size(new_frame_size, screen->rotation);
|
||||
set_content_size(screen, new_content_size);
|
||||
|
||||
if (screen->mipmaps) {
|
||||
SDL_GL_BindTexture(screen->texture, NULL, NULL);
|
||||
screen->gl.GenerateMipmap(GL_TEXTURE_2D);
|
||||
SDL_GL_UnbindTexture(screen->texture);
|
||||
}
|
||||
sc_screen_update_content_rect(screen);
|
||||
|
||||
return sc_display_set_texture_size(&screen->display, screen->frame_size);
|
||||
}
|
||||
|
||||
static bool
|
||||
@ -710,7 +618,10 @@ sc_screen_update_frame(struct sc_screen *screen) {
|
||||
if (!prepare_for_frame(screen, new_frame_size)) {
|
||||
return false;
|
||||
}
|
||||
update_texture(screen, frame);
|
||||
|
||||
if (!sc_display_update_texture(&screen->display, frame)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!screen->has_frame) {
|
||||
screen->has_frame = true;
|
||||
@ -790,22 +701,32 @@ sc_screen_is_mouse_capture_key(SDL_Keycode key) {
|
||||
return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI;
|
||||
}
|
||||
|
||||
void
|
||||
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||
bool
|
||||
sc_screen_handle_event(struct sc_screen *screen, const 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: {
|
||||
bool ok = sc_screen_update_frame(screen);
|
||||
if (!ok) {
|
||||
LOGW("Frame update failed\n");
|
||||
LOGE("Frame update failed\n");
|
||||
return false;
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
case SDL_WINDOWEVENT:
|
||||
if (!screen->has_frame) {
|
||||
// Do nothing
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
switch (event->window.event) {
|
||||
case SDL_WINDOWEVENT_EXPOSED:
|
||||
@ -836,7 +757,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||
}
|
||||
break;
|
||||
}
|
||||
return;
|
||||
return true;
|
||||
case SDL_KEYDOWN:
|
||||
if (relative_mode) {
|
||||
SDL_Keycode key = event->key.keysym.sym;
|
||||
@ -849,7 +770,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||
screen->mouse_capture_key_pressed = 0;
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -865,7 +786,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||
sc_screen_toggle_mouse_capture(screen);
|
||||
}
|
||||
// Mouse capture keys are never forwarded to the device
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
break;
|
||||
@ -875,7 +796,7 @@ sc_screen_handle_event(struct sc_screen *screen, 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;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case SDL_FINGERMOTION:
|
||||
@ -884,18 +805,19 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
|
||||
if (relative_mode) {
|
||||
// Touch events are not compatible with relative mode
|
||||
// (coordinates are not relative)
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
case SDL_MOUSEBUTTONUP:
|
||||
if (relative_mode && !sc_screen_get_mouse_capture(screen)) {
|
||||
sc_screen_set_mouse_capture(screen, true);
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
sc_input_manager_handle_event(&screen->im, event);
|
||||
return true;
|
||||
}
|
||||
|
||||
struct sc_point
|
||||
|
@ -9,6 +9,7 @@
|
||||
|
||||
#include "controller.h"
|
||||
#include "coords.h"
|
||||
#include "display.h"
|
||||
#include "fps_counter.h"
|
||||
#include "frame_buffer.h"
|
||||
#include "input_manager.h"
|
||||
@ -24,6 +25,7 @@ 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_fps_counter fps_counter;
|
||||
@ -39,9 +41,6 @@ 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
|
||||
|
||||
@ -57,7 +56,6 @@ struct sc_screen {
|
||||
bool has_frame;
|
||||
bool fullscreen;
|
||||
bool maximized;
|
||||
bool mipmaps;
|
||||
|
||||
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
|
||||
// RGUI) must be pressed. This variable tracks the pressed capture key.
|
||||
@ -78,7 +76,6 @@ 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
|
||||
@ -136,8 +133,9 @@ void
|
||||
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation);
|
||||
|
||||
// react to SDL events
|
||||
void
|
||||
sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event);
|
||||
// If this function returns false, scrcpy must exit with an error.
|
||||
bool
|
||||
sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event);
|
||||
|
||||
// convert point from window coordinates to frame coordinates
|
||||
// x and y are expressed in pixels
|
||||
|
@ -441,9 +441,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 + 4];
|
||||
unsigned char buf[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||
ssize_t r = net_recv_all_intr(intr, device_socket, buf, sizeof(buf));
|
||||
if (r < SC_DEVICE_NAME_FIELD_LENGTH + 4) {
|
||||
if (r < SC_DEVICE_NAME_FIELD_LENGTH) {
|
||||
LOGE("Could not retrieve device information");
|
||||
return false;
|
||||
}
|
||||
@ -451,9 +451,6 @@ 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));
|
||||
|
||||
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||
info->frame_size.width = sc_read16be(fields);
|
||||
info->frame_size.height = sc_read16be(&fields[2]);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -18,7 +18,6 @@
|
||||
#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 {
|
||||
|
@ -7,8 +7,6 @@
|
||||
#include <stdbool.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
typedef struct AVFrame AVFrame;
|
||||
|
||||
/**
|
||||
* Frame sink trait.
|
||||
*
|
||||
@ -19,6 +17,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);
|
||||
void (*close)(struct sc_frame_sink *sink);
|
||||
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
|
||||
|
@ -5,9 +5,7 @@
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
typedef struct AVCodec AVCodec;
|
||||
typedef struct AVPacket AVPacket;
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
/**
|
||||
* Packet sink trait.
|
||||
@ -19,8 +17,8 @@ struct sc_packet_sink {
|
||||
};
|
||||
|
||||
struct sc_packet_sink_ops {
|
||||
/* The codec instance is static, it is valid until the end of the program */
|
||||
bool (*open)(struct sc_packet_sink *sink, const AVCodec *codec);
|
||||
/* The codec context is valid until the sink is closed */
|
||||
bool (*open)(struct sc_packet_sink *sink, AVCodecContext *ctx);
|
||||
void (*close)(struct sc_packet_sink *sink);
|
||||
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
|
||||
|
||||
|
@ -25,11 +25,11 @@ sc_packet_source_sinks_close_firsts(struct sc_packet_source *source,
|
||||
|
||||
bool
|
||||
sc_packet_source_sinks_open(struct sc_packet_source *source,
|
||||
const AVCodec *codec) {
|
||||
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, codec)) {
|
||||
if (!sink->ops->open(sink, ctx)) {
|
||||
sc_packet_source_sinks_close_firsts(source, i);
|
||||
return false;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ sc_packet_source_add_sink(struct sc_packet_source *source,
|
||||
|
||||
bool
|
||||
sc_packet_source_sinks_open(struct sc_packet_source *source,
|
||||
const AVCodec *codec);
|
||||
AVCodecContext *ctx);
|
||||
|
||||
void
|
||||
sc_packet_source_sinks_close(struct sc_packet_source *source);
|
||||
|
94
app/src/util/audiobuf.h
Normal file
94
app/src/util/audiobuf.h
Normal file
@ -0,0 +1,94 @@
|
||||
#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
|
@ -30,7 +30,7 @@ sc_bytebuf_destroy(struct sc_bytebuf *buf) {
|
||||
void
|
||||
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
|
||||
assert(len);
|
||||
assert(len <= sc_bytebuf_read_available(buf));
|
||||
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;
|
||||
@ -50,7 +50,7 @@ sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
|
||||
void
|
||||
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
|
||||
assert(len);
|
||||
assert(len <= sc_bytebuf_read_available(buf));
|
||||
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;
|
||||
@ -78,7 +78,7 @@ sc_bytebuf_write_step1(struct sc_bytebuf *buf, size_t len) {
|
||||
void
|
||||
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
|
||||
assert(len);
|
||||
assert(len <= sc_bytebuf_write_available(buf));
|
||||
assert(len <= sc_bytebuf_can_write(buf));
|
||||
|
||||
sc_bytebuf_write_step0(buf, from, len);
|
||||
sc_bytebuf_write_step1(buf, len);
|
||||
@ -99,6 +99,6 @@ sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
|
||||
|
||||
void
|
||||
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
|
||||
assert(len <= sc_bytebuf_write_available(buf));
|
||||
assert(len <= sc_bytebuf_can_write(buf));
|
||||
sc_bytebuf_write_step1(buf, len);
|
||||
}
|
||||
|
@ -86,7 +86,7 @@ sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
|
||||
* It is an error to read more bytes than available.
|
||||
*/
|
||||
static inline size_t
|
||||
sc_bytebuf_read_available(struct sc_bytebuf *buf) {
|
||||
sc_bytebuf_can_read(struct sc_bytebuf *buf) {
|
||||
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
|
||||
}
|
||||
|
||||
@ -96,12 +96,12 @@ sc_bytebuf_read_available(struct sc_bytebuf *buf) {
|
||||
* It is an error to write more bytes than available.
|
||||
*/
|
||||
static inline size_t
|
||||
sc_bytebuf_write_available(struct sc_bytebuf *buf) {
|
||||
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 (read available + write available)
|
||||
* Return the actual capacity of the buffer (can_read() + can_write())
|
||||
*/
|
||||
static inline size_t
|
||||
sc_bytebuf_capacity(struct sc_bytebuf *buf) {
|
||||
|
@ -205,11 +205,13 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||
goto error_avformat_free_context;
|
||||
}
|
||||
|
||||
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
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_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) {
|
||||
@ -224,8 +226,8 @@ sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||
goto error_avio_close;
|
||||
}
|
||||
|
||||
vs->encoder_ctx->width = vs->frame_size.width;
|
||||
vs->encoder_ctx->height = vs->frame_size.height;
|
||||
vs->encoder_ctx->width = ctx->width;
|
||||
vs->encoder_ctx->height = ctx->height;
|
||||
vs->encoder_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
vs->encoder_ctx->time_base.num = 1;
|
||||
vs->encoder_ctx->time_base.den = 1;
|
||||
@ -341,16 +343,13 @@ 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,
|
||||
struct sc_size frame_size) {
|
||||
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name) {
|
||||
vs->device_name = strdup(device_name);
|
||||
if (!vs->device_name) {
|
||||
LOGE("Could not strdup v4l2 device name");
|
||||
return false;
|
||||
}
|
||||
|
||||
vs->frame_size = frame_size;
|
||||
|
||||
static const struct sc_frame_sink_ops ops = {
|
||||
.open = sc_v4l2_frame_sink_open,
|
||||
.close = sc_v4l2_frame_sink_close,
|
||||
|
@ -19,7 +19,6 @@ struct sc_v4l2_sink {
|
||||
AVCodecContext *encoder_ctx;
|
||||
|
||||
char *device_name;
|
||||
struct sc_size frame_size;
|
||||
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
@ -33,8 +32,7 @@ struct sc_v4l2_sink {
|
||||
};
|
||||
|
||||
bool
|
||||
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name,
|
||||
struct sc_size frame_size);
|
||||
sc_v4l2_sink_init(struct sc_v4l2_sink *vs, const char *device_name);
|
||||
|
||||
void
|
||||
sc_v4l2_sink_destroy(struct sc_v4l2_sink *vs);
|
||||
|
@ -217,6 +217,18 @@ 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";
|
||||
@ -259,6 +271,7 @@ 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();
|
||||
|
@ -13,23 +13,23 @@ void test_bytebuf_simple(void) {
|
||||
assert(ok);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 5);
|
||||
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_read_available(&buf) == 7);
|
||||
assert(sc_bytebuf_can_read(&buf) == 7);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 8);
|
||||
assert(sc_bytebuf_can_read(&buf) == 8);
|
||||
|
||||
sc_bytebuf_read(&buf, &data[4], 8);
|
||||
assert(sc_bytebuf_read_available(&buf) == 0);
|
||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
||||
|
||||
data[12] = '\0';
|
||||
assert(!strcmp((char *) data, "hello world!"));
|
||||
assert(sc_bytebuf_read_available(&buf) == 0);
|
||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
||||
|
||||
sc_bytebuf_destroy(&buf);
|
||||
}
|
||||
@ -42,31 +42,31 @@ void test_bytebuf_boundaries(void) {
|
||||
assert(ok);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 6);
|
||||
assert(sc_bytebuf_can_read(&buf) == 6);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 12);
|
||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 18);
|
||||
assert(sc_bytebuf_can_read(&buf) == 18);
|
||||
|
||||
sc_bytebuf_read(&buf, data, 9);
|
||||
assert(!strncmp((char *) data, "hello hel", 9));
|
||||
assert(sc_bytebuf_read_available(&buf) == 9);
|
||||
assert(sc_bytebuf_can_read(&buf) == 9);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 14);
|
||||
assert(sc_bytebuf_can_read(&buf) == 14);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 15);
|
||||
assert(sc_bytebuf_can_read(&buf) == 15);
|
||||
|
||||
sc_bytebuf_skip(&buf, 3);
|
||||
assert(sc_bytebuf_read_available(&buf) == 12);
|
||||
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_read_available(&buf) == 0);
|
||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
||||
|
||||
sc_bytebuf_destroy(&buf);
|
||||
}
|
||||
@ -79,37 +79,37 @@ void test_bytebuf_two_steps_write(void) {
|
||||
assert(ok);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 6);
|
||||
assert(sc_bytebuf_can_read(&buf) == 6);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 12);
|
||||
assert(sc_bytebuf_can_read(&buf) == 12);
|
||||
|
||||
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 12); // write not committed yet
|
||||
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_read_available(&buf) == 3);
|
||||
assert(sc_bytebuf_can_read(&buf) == 3);
|
||||
|
||||
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 9);
|
||||
assert(sc_bytebuf_can_read(&buf) == 9);
|
||||
|
||||
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 9); // write not committed yet
|
||||
assert(sc_bytebuf_can_read(&buf) == 9); // write not committed yet
|
||||
|
||||
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 14);
|
||||
assert(sc_bytebuf_can_read(&buf) == 14);
|
||||
|
||||
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
|
||||
assert(sc_bytebuf_read_available(&buf) == 15);
|
||||
assert(sc_bytebuf_can_read(&buf) == 15);
|
||||
|
||||
sc_bytebuf_skip(&buf, 3);
|
||||
assert(sc_bytebuf_read_available(&buf) == 12);
|
||||
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_read_available(&buf) == 0);
|
||||
assert(sc_bytebuf_can_read(&buf) == 0);
|
||||
|
||||
sc_bytebuf_destroy(&buf);
|
||||
}
|
||||
|
@ -1,79 +0,0 @@
|
||||
#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;
|
||||
};
|
@ -17,5 +17,5 @@ endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32'
|
||||
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
|
||||
prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32'
|
||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
||||
|
@ -17,5 +17,5 @@ endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64'
|
||||
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
|
||||
prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32'
|
||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
|
||||
|
101
doc/audio.md
Normal file
101
doc/audio.md
Normal file
@ -0,0 +1,101 @@
|
||||
# Audio
|
||||
|
||||
Audio forwarding is supported for devices with Android 11 or higher, and it is
|
||||
enabled by default:
|
||||
|
||||
- For **Android 12 or newer**, it works out-of-the-box.
|
||||
- For **Android 11**, you'll need to ensure that the device screen is unlocked
|
||||
when starting scrcpy. A fake popup will briefly appear to make the system
|
||||
think that the shell app is in the foreground. Without this, audio capture
|
||||
will fail.
|
||||
- For **Android 10 or earlier**, audio cannot be captured and is automatically
|
||||
disabled.
|
||||
|
||||
If audio capture fails, then mirroring continues with video only (since audio is
|
||||
enabled by default, it is not acceptable to make scrcpy fail if it is not
|
||||
available), unless `--require-audio` is set.
|
||||
|
||||
|
||||
## No audio
|
||||
|
||||
To disable audio:
|
||||
|
||||
```
|
||||
scrcpy --no-audio
|
||||
```
|
||||
|
||||
## Codec
|
||||
|
||||
The audio codec can be selected. The possible values are `opus` (default), `aac`
|
||||
and `raw` (uncompressed PCM 16-bit LE):
|
||||
|
||||
```bash
|
||||
scrcpy --audio-codec=opus # default
|
||||
scrcpy --audio-codec=aac
|
||||
scrcpy --audio-codec=raw
|
||||
```
|
||||
|
||||
Several encoders may be available on the device. They can be listed by:
|
||||
|
||||
```bash
|
||||
scrcpy --list-encoders
|
||||
```
|
||||
|
||||
To select a specific encoder:
|
||||
|
||||
```
|
||||
scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder'
|
||||
```
|
||||
|
||||
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
|
||||
check `--audio-codec-options` in the manpage or in `scrcpy --help`.
|
||||
|
||||
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
|
||||
|
||||
|
||||
## Bit rate
|
||||
|
||||
The default video bit-rate is 128Kbps. To change it:
|
||||
|
||||
```bash
|
||||
scrcpy --audio-bit-rate=64K
|
||||
scrcpy --audio-bit-rate=64000 # equivalent
|
||||
```
|
||||
|
||||
_This parameter does not apply to RAW audio codec (`--audio-codec=raw`)._
|
||||
|
||||
|
||||
## Buffering
|
||||
|
||||
Audio buffering is unavoidable. It must be kept small enough so that the latency
|
||||
is acceptable, but large enough to minimize buffer underrun (causing audio
|
||||
glitches).
|
||||
|
||||
The default buffer size is set to 50ms. It can be adjusted:
|
||||
|
||||
```bash
|
||||
scrcpy --audio-buffer=40 # smaller than default
|
||||
scrcpy --audio-buffer=100 # higher than default
|
||||
```
|
||||
|
||||
Note that this option changes the _target_ buffering. It is possible that this
|
||||
target buffering might not be reached (on frequent buffer underflow typically).
|
||||
|
||||
If you don't interact with the device (to watch a video for example), a higher
|
||||
latency (for both [video](video.md#buffering) and audio) might be preferable to
|
||||
avoid glitches and smooth the playback:
|
||||
|
||||
```
|
||||
scrcpy --display-buffer=200 --audio-buffer=200
|
||||
```
|
||||
|
||||
It is also possible to configure another audio buffer (the audio output buffer),
|
||||
by default set to 5ms. Don't change it, unless you get some [robotic and glitchy
|
||||
sound][#3793]:
|
||||
|
||||
```bash
|
||||
# Only if absolutely necessary
|
||||
scrcpy --audio-output-buffer=10
|
||||
```
|
||||
|
||||
[#3793]: https://github.com/Genymobile/scrcpy/issues/3793
|
@ -2,57 +2,16 @@
|
||||
|
||||
Here are the instructions to build _scrcpy_ (client and server).
|
||||
|
||||
|
||||
## 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 \
|
||||
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
|
||||
```
|
||||
|
||||
Then clone the repo and execute the installation script
|
||||
([source](install_release.sh)):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Genymobile/scrcpy
|
||||
cd scrcpy
|
||||
./install_release.sh
|
||||
```
|
||||
|
||||
When a new release is out, update the repo and reinstall:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
./install_release.sh
|
||||
```
|
||||
|
||||
To uninstall:
|
||||
|
||||
```bash
|
||||
sudo ninja -Cbuild-auto uninstall
|
||||
```
|
||||
|
||||
If you just want to build and install the latest release, follow the simplified
|
||||
process described in [doc/linux.md](linux.md).
|
||||
|
||||
## Branches
|
||||
|
||||
### `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.
|
||||
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.
|
||||
|
||||
If you want to contribute code, please base your commits on the latest `dev`
|
||||
branch.
|
||||
@ -69,6 +28,8 @@ 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
|
||||
@ -272,10 +233,10 @@ install` must be run as root)._
|
||||
|
||||
#### Option 2: Use prebuilt server
|
||||
|
||||
- [`scrcpy-server-v1.25`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb`</sub>
|
||||
- [`scrcpy-server-v2.0`][direct-scrcpy-server]
|
||||
<sub>SHA-256: `9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2`</sub>
|
||||
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
|
||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0
|
||||
|
||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||
configuration:
|
||||
@ -314,7 +275,8 @@ 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](README.md#run) `scrcpy`.
|
||||
You can then run `scrcpy`.
|
||||
|
||||
|
||||
### Uninstall
|
||||
|
149
doc/control.md
Normal file
149
doc/control.md
Normal file
@ -0,0 +1,149 @@
|
||||
# Control
|
||||
|
||||
## Read-only
|
||||
|
||||
To disable controls (everything which can interact with the device: input keys,
|
||||
mouse events, drag&drop files):
|
||||
|
||||
```bash
|
||||
scrcpy --no-control
|
||||
scrcpy -n # short version
|
||||
```
|
||||
|
||||
|
||||
## Text injection preference
|
||||
|
||||
Two kinds of [events][textevents] are generated when typing text:
|
||||
- _key events_, signaling that a key is pressed or released;
|
||||
- _text events_, signaling that a text has been entered.
|
||||
|
||||
By default, letters are injected using key events, so that the keyboard behaves
|
||||
as expected in games (typically for WASD keys).
|
||||
|
||||
But this may [cause issues][prefertext]. If you encounter such a problem, you
|
||||
can avoid it by:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text
|
||||
```
|
||||
|
||||
(but this will break keyboard behavior in games)
|
||||
|
||||
On the contrary, you could force to always inject raw key events:
|
||||
|
||||
```bash
|
||||
scrcpy --raw-key-events
|
||||
```
|
||||
|
||||
These options have no effect on HID keyboard (all key events are sent as
|
||||
scancodes in this mode).
|
||||
|
||||
[textevents]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-text-input
|
||||
[prefertext]: https://github.com/Genymobile/scrcpy/issues/650#issuecomment-512945343
|
||||
|
||||
|
||||
## Copy-paste
|
||||
|
||||
Any time the Android clipboard changes, it is automatically synchronized to the
|
||||
computer clipboard.
|
||||
|
||||
Any <kbd>Ctrl</kbd> shortcut is forwarded to the device. In particular:
|
||||
- <kbd>Ctrl</kbd>+<kbd>c</kbd> typically copies
|
||||
- <kbd>Ctrl</kbd>+<kbd>x</kbd> typically cuts
|
||||
- <kbd>Ctrl</kbd>+<kbd>v</kbd> typically pastes (after computer-to-device
|
||||
clipboard synchronization)
|
||||
|
||||
This typically works as you expect.
|
||||
|
||||
The actual behavior depends on the active application though. For example,
|
||||
_Termux_ sends SIGINT on <kbd>Ctrl</kbd>+<kbd>c</kbd> instead, and _K-9 Mail_
|
||||
composes a new message.
|
||||
|
||||
To copy, cut and paste in such cases (but only supported on Android >= 7):
|
||||
- <kbd>MOD</kbd>+<kbd>c</kbd> injects `COPY`
|
||||
- <kbd>MOD</kbd>+<kbd>x</kbd> injects `CUT`
|
||||
- <kbd>MOD</kbd>+<kbd>v</kbd> injects `PASTE` (after computer-to-device
|
||||
clipboard synchronization)
|
||||
|
||||
In addition, <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd> injects the computer
|
||||
clipboard text as a sequence of key events. This is useful when the component
|
||||
does not accept text pasting (for example in _Termux_), but it can break
|
||||
non-ASCII content.
|
||||
|
||||
**WARNING:** Pasting the computer clipboard to the device (either via
|
||||
<kbd>Ctrl</kbd>+<kbd>v</kbd> or <kbd>MOD</kbd>+<kbd>v</kbd>) copies the content
|
||||
into the Android clipboard. As a consequence, any Android application could read
|
||||
its content. You should avoid pasting sensitive content (like passwords) that
|
||||
way.
|
||||
|
||||
Some Android devices do not behave as expected when setting the device clipboard
|
||||
programmatically. An option `--legacy-paste` is provided to change the behavior
|
||||
of <kbd>Ctrl</kbd>+<kbd>v</kbd> and <kbd>MOD</kbd>+<kbd>v</kbd> so that they
|
||||
also inject the computer clipboard text as a sequence of key events (the same
|
||||
way as <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>).
|
||||
|
||||
To disable automatic clipboard synchronization, use
|
||||
`--no-clipboard-autosync`.
|
||||
|
||||
## Pinch-to-zoom
|
||||
|
||||
To simulate "pinch-to-zoom": <kbd>Ctrl</kbd>+_click-and-move_.
|
||||
|
||||
More precisely, hold down <kbd>Ctrl</kbd> while pressing the left-click button.
|
||||
Until the left-click button is released, all mouse movements scale and rotate
|
||||
the content (if supported by the app) relative to the center of the screen.
|
||||
|
||||
Technically, _scrcpy_ generates additional touch events from a "virtual finger"
|
||||
at a location inverted through the center of the screen.
|
||||
|
||||
|
||||
## Key repeat
|
||||
|
||||
By default, holding a key down generates repeated key events. This can cause
|
||||
performance problems in some games, where these events are useless anyway.
|
||||
|
||||
To avoid forwarding repeated key events:
|
||||
|
||||
```bash
|
||||
scrcpy --no-key-repeat
|
||||
```
|
||||
|
||||
This option has no effect on HID keyboard (key repeat is handled by Android
|
||||
directly in this mode).
|
||||
|
||||
|
||||
## Right-click and middle-click
|
||||
|
||||
By default, right-click triggers BACK (or POWER on) and middle-click triggers
|
||||
HOME. To disable these shortcuts and forward the clicks to the device instead:
|
||||
|
||||
```bash
|
||||
scrcpy --forward-all-clicks
|
||||
```
|
||||
|
||||
## File drop
|
||||
|
||||
### Install APK
|
||||
|
||||
To install an APK, drag & drop an APK file (ending with `.apk`) to the _scrcpy_
|
||||
window.
|
||||
|
||||
There is no visual feedback, a log is printed to the console.
|
||||
|
||||
|
||||
### Push file to device
|
||||
|
||||
To push a file to `/sdcard/Download/` on the device, drag & drop a (non-APK)
|
||||
file to the _scrcpy_ window.
|
||||
|
||||
There is no visual feedback, a log is printed to the console.
|
||||
|
||||
The target directory can be changed on start:
|
||||
|
||||
```bash
|
||||
scrcpy --push-target=/sdcard/Movies/
|
||||
```
|
||||
|
||||
## Physical keyboard and mouse simulation
|
||||
|
||||
See the dedicated [HID/OTG](hid-otg.md) page.
|
228
doc/device.md
Normal file
228
doc/device.md
Normal file
@ -0,0 +1,228 @@
|
||||
# Device
|
||||
|
||||
## Selection
|
||||
|
||||
If exactly one device is connected (i.e. listed by `adb devices`), then it is
|
||||
automatically selected.
|
||||
|
||||
However, if there are multiple devices connected, you must specify the one to
|
||||
use in one of 4 ways:
|
||||
- by its serial:
|
||||
```bash
|
||||
scrcpy --serial=0123456789abcdef
|
||||
scrcpy -s 0123456789abcdef # short version
|
||||
|
||||
# the serial is the ip:port if connected over TCP/IP (same behavior as adb)
|
||||
scrcpy --serial=192.168.1.1:5555
|
||||
```
|
||||
- the one connected over USB (if there is exactly one):
|
||||
```bash
|
||||
scrcpy --select-usb
|
||||
scrcpy -d # short version
|
||||
```
|
||||
- the one connected over TCP/IP (if there is exactly one):
|
||||
```bash
|
||||
scrcpy --select-tcpip
|
||||
scrcpy -e # short version
|
||||
```
|
||||
- a device already listening on TCP/IP (see [below](#tcpip-wireless)):
|
||||
```bash
|
||||
scrcpy --tcpip=192.168.1.1:5555
|
||||
scrcpy --tcpip=192.168.1.1 # default port is 5555
|
||||
```
|
||||
|
||||
The serial may also be provided via the environment variable `ANDROID_SERIAL`
|
||||
(also used by `adb`):
|
||||
|
||||
```bash
|
||||
# in bash
|
||||
export ANDROID_SERIAL=0123456789abcdef
|
||||
scrcpy
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: in cmd
|
||||
set ANDROID_SERIAL=0123456789abcdef
|
||||
scrcpy
|
||||
```
|
||||
|
||||
```powershell
|
||||
# in PowerShell
|
||||
$env:ANDROID_SERIAL = '0123456789abcdef'
|
||||
scrcpy
|
||||
```
|
||||
|
||||
|
||||
## TCP/IP (wireless)
|
||||
|
||||
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
|
||||
device over TCP/IP. The device must be connected on the same network as the
|
||||
computer.
|
||||
|
||||
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
|
||||
|
||||
|
||||
### Automatic
|
||||
|
||||
An option `--tcpip` allows to configure the connection automatically. There are
|
||||
two variants.
|
||||
|
||||
If the device (accessible at 192.168.1.1 in this example) already listens on a
|
||||
port (typically 5555) for incoming _adb_ connections, then run:
|
||||
|
||||
```bash
|
||||
scrcpy --tcpip=192.168.1.1 # default port is 5555
|
||||
scrcpy --tcpip=192.168.1.1:5555
|
||||
```
|
||||
|
||||
If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP
|
||||
address), connect the device over USB, then run:
|
||||
|
||||
```bash
|
||||
scrcpy --tcpip # without arguments
|
||||
```
|
||||
|
||||
It will automatically find the device IP address and adb port, enable TCP/IP
|
||||
mode if necessary, then connect to the device before starting.
|
||||
|
||||
|
||||
### Manual
|
||||
|
||||
Alternatively, it is possible to enable the TCP/IP connection manually using
|
||||
`adb`:
|
||||
|
||||
1. Plug the device into a USB port on your computer.
|
||||
2. Connect the device to the same Wi-Fi network as your computer.
|
||||
3. Get your device IP address, in Settings → About phone → Status, or by
|
||||
executing this command:
|
||||
|
||||
```bash
|
||||
adb shell ip route | awk '{print $9}'
|
||||
```
|
||||
|
||||
4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`.
|
||||
5. Unplug your device.
|
||||
6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`
|
||||
with the device IP address you found)_.
|
||||
7. Run `scrcpy` as usual.
|
||||
8. Run `adb disconnect` once you're done.
|
||||
|
||||
Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass
|
||||
having to physically connect your device directly to your computer.
|
||||
|
||||
[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line
|
||||
|
||||
|
||||
## Autostart
|
||||
|
||||
A small tool (by the scrcpy author) allows to run arbitrary commands whenever a
|
||||
new Android device is connected: [AutoAdb]. It can be used to start scrcpy:
|
||||
|
||||
```bash
|
||||
autoadb scrcpy -s '{}'
|
||||
```
|
||||
|
||||
[AutoAdb]: https://github.com/rom1v/autoadb
|
||||
|
||||
|
||||
## Display
|
||||
|
||||
If several displays are available on the Android device, it is possible to
|
||||
select the display to mirror:
|
||||
|
||||
```bash
|
||||
scrcpy --display=1
|
||||
```
|
||||
|
||||
The list of display ids can be retrieved by:
|
||||
|
||||
```bash
|
||||
scrcpy --list-displays
|
||||
```
|
||||
|
||||
A secondary display may only be controlled if the device runs at least Android
|
||||
10 (otherwise it is mirrored as read-only).
|
||||
|
||||
|
||||
## Actions
|
||||
|
||||
Some command line arguments perform actions on the device itself while scrcpy is
|
||||
running.
|
||||
|
||||
|
||||
### Stay awake
|
||||
|
||||
To prevent the device from sleeping after a delay **when the device is plugged
|
||||
in**:
|
||||
|
||||
```bash
|
||||
scrcpy --stay-awake
|
||||
scrcpy -w
|
||||
```
|
||||
|
||||
The initial state is restored when _scrcpy_ is closed.
|
||||
|
||||
If the device is not plugged in (i.e. only connected over TCP/IP),
|
||||
`--stay-awake` has no effect (this is the Android behavior).
|
||||
|
||||
|
||||
### Turn screen off
|
||||
|
||||
It is possible to turn the device screen off while mirroring on start with a
|
||||
command-line option:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off
|
||||
scrcpy -S # short version
|
||||
```
|
||||
|
||||
Or by pressing <kbd>MOD</kbd>+<kbd>o</kbd> at any time (see
|
||||
[shortcuts](shortcuts.md)).
|
||||
|
||||
To turn it back on, press <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>.
|
||||
|
||||
On Android, the `POWER` button always turns the screen on. For convenience, if
|
||||
`POWER` is sent via _scrcpy_ (via right-click or <kbd>MOD</kbd>+<kbd>p</kbd>),
|
||||
it will force to turn the screen off after a small delay (on a best effort
|
||||
basis). The physical `POWER` button will still cause the screen to be turned on.
|
||||
|
||||
It can also be useful to prevent the device from sleeping:
|
||||
|
||||
```bash
|
||||
scrcpy --turn-screen-off --stay-awake
|
||||
scrcpy -Sw # short version
|
||||
```
|
||||
|
||||
|
||||
### Show touches
|
||||
|
||||
For presentations, it may be useful to show physical touches (on the physical
|
||||
device). Android exposes this feature in _Developers options_.
|
||||
|
||||
_Scrcpy_ provides an option to enable this feature on start and restore the
|
||||
initial value on exit:
|
||||
|
||||
```bash
|
||||
scrcpy --show-touches
|
||||
scrcpy -t # short version
|
||||
```
|
||||
|
||||
Note that it only shows _physical_ touches (by a finger on the device).
|
||||
|
||||
|
||||
### Power off on close
|
||||
|
||||
To turn the device screen off when closing _scrcpy_:
|
||||
|
||||
```bash
|
||||
scrcpy --power-off-on-close
|
||||
```
|
||||
|
||||
### Power on on start
|
||||
|
||||
By default, on start, the device is powered on. To prevent this behavior:
|
||||
|
||||
```bash
|
||||
scrcpy --no-power-on
|
||||
```
|
||||
|
108
doc/hid-otg.md
Normal file
108
doc/hid-otg.md
Normal file
@ -0,0 +1,108 @@
|
||||
# HID/OTG
|
||||
|
||||
By default, _scrcpy_ injects input events at the Android API level. As an
|
||||
alternative, when connected over USB, it is possible to send HID events, so that
|
||||
scrcpy behaves as if it was a physical keyboard and/or mouse connected to the
|
||||
Android device.
|
||||
|
||||
A special [OTG](#otg) mode allows to control the device without mirroring (and
|
||||
without USB debugging).
|
||||
|
||||
|
||||
## Physical keyboard simulation
|
||||
|
||||
By default, _scrcpy_ uses Android key or text injection. It works everywhere,
|
||||
but is limited to ASCII.
|
||||
|
||||
Instead, it can simulate a physical USB keyboard on Android to provide a better
|
||||
input experience (using [USB HID over AOAv2][hid-aoav2]): the virtual keyboard
|
||||
is disabled and it works for all characters and IME.
|
||||
|
||||
[hid-aoav2]: https://source.android.com/devices/accessories/aoa2#hid-support
|
||||
|
||||
However, it only works if the device is connected via USB.
|
||||
|
||||
Note: On Windows, it may only work in [OTG mode](#otg), not while mirroring (it
|
||||
is not possible to open a USB device if it is already open by another process
|
||||
like the _adb daemon_).
|
||||
|
||||
To enable this mode:
|
||||
|
||||
```bash
|
||||
scrcpy --hid-keyboard
|
||||
scrcpy -K # short version
|
||||
```
|
||||
|
||||
If it fails for some reason (for example because the device is not connected via
|
||||
USB), it automatically fallbacks to the default mode (with a log in the
|
||||
console). This allows using the same command line options when connected over
|
||||
USB and TCP/IP.
|
||||
|
||||
In this mode, raw key events (scancodes) are sent to the device, independently
|
||||
of the host key mapping. Therefore, if your keyboard layout does not match, it
|
||||
must be configured on the Android device, in Settings → System → Languages and
|
||||
input → [Physical keyboard].
|
||||
|
||||
This settings page can be started directly:
|
||||
|
||||
```bash
|
||||
adb shell am start -a android.settings.HARD_KEYBOARD_SETTINGS
|
||||
```
|
||||
|
||||
However, the option is only available when the HID keyboard is enabled (or when
|
||||
a physical keyboard is connected).
|
||||
|
||||
[Physical keyboard]: https://github.com/Genymobile/scrcpy/pull/2632#issuecomment-923756915
|
||||
|
||||
|
||||
## Physical mouse simulation
|
||||
|
||||
By default, _scrcpy_ uses Android mouse events injection with absolute
|
||||
coordinates. By simulating a physical mouse, a mouse pointer appears on the
|
||||
Android device, and relative mouse motion, clicks and scrolls are injected.
|
||||
|
||||
To enable this mode:
|
||||
|
||||
```bash
|
||||
scrcpy --hid-mouse
|
||||
scrcpy -M # short version
|
||||
```
|
||||
|
||||
When this mode is enabled, the computer mouse is "captured" (the mouse pointer
|
||||
disappears from the computer and appears on the Android device instead).
|
||||
|
||||
Special capture keys, either <kbd>Alt</kbd> or <kbd>Super</kbd>, toggle
|
||||
(disable or enable) the mouse capture. Use one of them to give the control of
|
||||
the mouse back to the computer.
|
||||
|
||||
|
||||
## OTG
|
||||
|
||||
It is possible to run _scrcpy_ with only physical keyboard and mouse simulation
|
||||
(HID), as if the computer keyboard and mouse were plugged directly to the device
|
||||
via an OTG cable.
|
||||
|
||||
In this mode, `adb` (USB debugging) is not necessary, and mirroring is disabled.
|
||||
|
||||
This is similar to `--hid-keyboard --hid-mouse`, but without mirroring.
|
||||
|
||||
To enable OTG mode:
|
||||
|
||||
```bash
|
||||
scrcpy --otg
|
||||
# Pass the serial if several USB devices are available
|
||||
scrcpy --otg -s 0123456789abcdef
|
||||
```
|
||||
|
||||
It is possible to enable only HID keyboard or HID mouse:
|
||||
|
||||
```bash
|
||||
scrcpy --otg --hid-keyboard # keyboard only
|
||||
scrcpy --otg --hid-mouse # mouse only
|
||||
scrcpy --otg --hid-keyboard --hid-mouse # keyboard and mouse
|
||||
# for convenience, enable both by default
|
||||
scrcpy --otg # keyboard and mouse
|
||||
```
|
||||
|
||||
Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is
|
||||
connected over USB.
|
81
doc/linux.md
Normal file
81
doc/linux.md
Normal file
@ -0,0 +1,81 @@
|
||||
# On Linux
|
||||
|
||||
## Install
|
||||
|
||||
<a href="https://repology.org/project/scrcpy/versions"><img src="https://repology.org/badge/vertical-allrepos/scrcpy.svg" alt="Packaging status" align="right"></a>
|
||||
|
||||
Scrcpy is packaged in several distributions and package managers:
|
||||
|
||||
- Debian/Ubuntu: `apt install scrcpy`
|
||||
- Arch Linux: `pacman -S scrcpy`
|
||||
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
|
||||
- Gentoo: [ebuild][ebuild-link] file
|
||||
- Snap: `snap install scrcpy`
|
||||
- … (see [repology](https://repology.org/project/scrcpy/versions))
|
||||
|
||||
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
|
||||
|
||||
### Latest version
|
||||
|
||||
However, the packaged version is not always the latest release. To install the
|
||||
latest release from `master`, follow this simplified process.
|
||||
|
||||
First, you need to install the required packages:
|
||||
|
||||
```bash
|
||||
# for Debian/Ubuntu
|
||||
sudo apt install ffmpeg libsdl2-2.0-0 adb wget \
|
||||
gcc git pkg-config meson ninja-build libsdl2-dev \
|
||||
libavcodec-dev libavdevice-dev libavformat-dev libavutil-dev \
|
||||
libswresample-dev libusb-1.0-0 libusb-1.0-0-dev
|
||||
```
|
||||
|
||||
Then clone the repo and execute the installation script
|
||||
([source](/install_release.sh)):
|
||||
|
||||
```bash
|
||||
git clone https://github.com/Genymobile/scrcpy
|
||||
cd scrcpy
|
||||
./install_release.sh
|
||||
```
|
||||
|
||||
When a new release is out, update the repo and reinstall:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
./install_release.sh
|
||||
```
|
||||
|
||||
To uninstall:
|
||||
|
||||
```bash
|
||||
sudo ninja -Cbuild-auto uninstall
|
||||
```
|
||||
|
||||
_Note that this simplified process only works for released versions (it
|
||||
downloads a prebuilt server binary), so for example you can't use it for testing
|
||||
the development branch (`dev`)._
|
||||
|
||||
_See [build.md](build.md) to build and install the app manually._
|
||||
|
||||
|
||||
## Run
|
||||
|
||||
_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._
|
||||
|
||||
Once installed, run from a terminal:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
or with arguments (here to disable audio and record to `file.mkv`):
|
||||
|
||||
```bash
|
||||
scrcpy --no-audio --record=file.mkv
|
||||
```
|
||||
|
||||
Documentation for command line arguments is available:
|
||||
- `man scrcpy`
|
||||
- `scrcpy --help`
|
||||
- on [github](/README.md)
|
49
doc/macos.md
Normal file
49
doc/macos.md
Normal file
@ -0,0 +1,49 @@
|
||||
# On macOS
|
||||
|
||||
## Install
|
||||
|
||||
Scrcpy is available in [Homebrew]:
|
||||
|
||||
```bash
|
||||
brew install scrcpy
|
||||
```
|
||||
|
||||
[Homebrew]: https://brew.sh/
|
||||
|
||||
You need `adb`, accessible from your `PATH`. If you don't have it yet:
|
||||
|
||||
```bash
|
||||
brew install android-platform-tools
|
||||
```
|
||||
|
||||
Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you:
|
||||
|
||||
```bash
|
||||
sudo port install scrcpy
|
||||
```
|
||||
|
||||
[MacPorts]: https://www.macports.org/
|
||||
|
||||
_See [build.md](build.md) to build and install the app manually._
|
||||
|
||||
|
||||
## Run
|
||||
|
||||
_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._
|
||||
|
||||
Once installed, run from a terminal:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
or with arguments (here to disable audio and record to `file.mkv`):
|
||||
|
||||
```bash
|
||||
scrcpy --no-audio --record=file.mkv
|
||||
```
|
||||
|
||||
Documentation for command line arguments is available:
|
||||
- `man scrcpy`
|
||||
- `scrcpy --help`
|
||||
- on [github](/README.md)
|
44
doc/recording.md
Normal file
44
doc/recording.md
Normal file
@ -0,0 +1,44 @@
|
||||
# Recording
|
||||
|
||||
To record video and audio streams while mirroring:
|
||||
|
||||
```bash
|
||||
scrcpy --record=file.mp4
|
||||
scrcpy -r file.mkv
|
||||
```
|
||||
|
||||
To record only the video:
|
||||
|
||||
```bash
|
||||
scrcpy --no-audio --record=file.mp4
|
||||
```
|
||||
|
||||
_It is currently not possible to record only the audio._
|
||||
|
||||
To disable mirroring while recording:
|
||||
|
||||
```bash
|
||||
scrcpy --no-display --record=file.mp4
|
||||
scrcpy -Nr file.mkv
|
||||
# interrupt recording with Ctrl+C
|
||||
```
|
||||
|
||||
Timestamps are captured on the device, so [packet delay variation] does not
|
||||
impact the recorded file, which is always clean (only if you use `--record` of
|
||||
course, not if you capture your scrcpy window and audio output on the computer).
|
||||
|
||||
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
|
||||
|
||||
The video and audio streams are encoded on the device, but are muxed on the
|
||||
client side. Two formats (containers) are supported:
|
||||
- Matroska (`.mkv`)
|
||||
- MP4 (`.mp4`)
|
||||
|
||||
The container is automatically selected based on the filename.
|
||||
|
||||
It is also possible to explicitly select a container (in that case the filename
|
||||
needs not end with `.mkv` or `.mp4`):
|
||||
|
||||
```
|
||||
scrcpy --record=file --record-format=mkv
|
||||
```
|
68
doc/shortcuts.md
Normal file
68
doc/shortcuts.md
Normal file
@ -0,0 +1,68 @@
|
||||
# Shortcuts
|
||||
|
||||
Actions can be performed on the scrcpy window using keyboard and mouse
|
||||
shortcuts.
|
||||
|
||||
In the following list, <kbd>MOD</kbd> is the shortcut modifier. By default, it's
|
||||
(left) <kbd>Alt</kbd> or (left) <kbd>Super</kbd>.
|
||||
|
||||
It can be changed using `--shortcut-mod`. Possible keys are `lctrl`, `rctrl`,
|
||||
`lalt`, `ralt`, `lsuper` and `rsuper`. For example:
|
||||
|
||||
```bash
|
||||
# use RCtrl for shortcuts
|
||||
scrcpy --shortcut-mod=rctrl
|
||||
|
||||
# use either LCtrl+LAlt or LSuper for shortcuts
|
||||
scrcpy --shortcut-mod=lctrl+lalt,lsuper
|
||||
```
|
||||
|
||||
_<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
|
||||
|
||||
[Super]: https://en.wikipedia.org/wiki/Super_key_(keyboard_button)
|
||||
|
||||
| Action | Shortcut
|
||||
| ------------------------------------------- |:-----------------------------
|
||||
| Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
| Rotate display left | <kbd>MOD</kbd>+<kbd>←</kbd> _(left)_
|
||||
| Rotate display right | <kbd>MOD</kbd>+<kbd>→</kbd> _(right)_
|
||||
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
|
||||
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
|
||||
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
|
||||
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_
|
||||
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4th-click³_
|
||||
| Click on `MENU` (unlock screen)⁴ | <kbd>MOD</kbd>+<kbd>m</kbd>
|
||||
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd>↑</kbd> _(up)_
|
||||
| Click on `VOLUME_DOWN` | <kbd>MOD</kbd>+<kbd>↓</kbd> _(down)_
|
||||
| Click on `POWER` | <kbd>MOD</kbd>+<kbd>p</kbd>
|
||||
| Power on | _Right-click²_
|
||||
| Turn device screen off (keep mirroring) | <kbd>MOD</kbd>+<kbd>o</kbd>
|
||||
| Turn device screen on | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>o</kbd>
|
||||
| Rotate device screen | <kbd>MOD</kbd>+<kbd>r</kbd>
|
||||
| Expand notification panel | <kbd>MOD</kbd>+<kbd>n</kbd> \| _5th-click³_
|
||||
| Expand settings panel | <kbd>MOD</kbd>+<kbd>n</kbd>+<kbd>n</kbd> \| _Double-5th-click³_
|
||||
| Collapse panels | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>n</kbd>
|
||||
| Copy to clipboard⁵ | <kbd>MOD</kbd>+<kbd>c</kbd>
|
||||
| Cut to clipboard⁵ | <kbd>MOD</kbd>+<kbd>x</kbd>
|
||||
| Synchronize clipboards and paste⁵ | <kbd>MOD</kbd>+<kbd>v</kbd>
|
||||
| Inject computer clipboard text | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd>v</kbd>
|
||||
| Enable/disable FPS counter (on stdout) | <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
| Pinch-to-zoom | <kbd>Ctrl</kbd>+_click-and-move_
|
||||
| Drag & drop APK file | Install APK from computer
|
||||
| Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device)
|
||||
|
||||
_¹Double-click on black borders to remove them._
|
||||
_²Right-click turns the screen on if it was off, presses BACK otherwise._
|
||||
_³4th and 5th mouse buttons, if your mouse has them._
|
||||
_⁴For react-native apps in development, `MENU` triggers development menu._
|
||||
_⁵Only on Android >= 7._
|
||||
|
||||
Shortcuts with repeated keys are executed by releasing and pressing the key a
|
||||
second time. For example, to execute "Expand settings panel":
|
||||
|
||||
1. Press and keep pressing <kbd>MOD</kbd>.
|
||||
2. Then double-press <kbd>n</kbd>.
|
||||
3. Finally, release <kbd>MOD</kbd>.
|
||||
|
||||
All <kbd>Ctrl</kbd>+_key_ shortcuts are forwarded to the device, so they are
|
||||
handled by the active application.
|
123
doc/tunnels.md
Normal file
123
doc/tunnels.md
Normal file
@ -0,0 +1,123 @@
|
||||
# Tunnels
|
||||
|
||||
Scrcpy is designed to mirror local Android devices. Tunnels allow to connect to
|
||||
a remote device (e.g. over the Internet).
|
||||
|
||||
To connect to a remote device, it is possible to connect a local `adb` client to
|
||||
a remote `adb` server (provided they use the same version of the _adb_
|
||||
protocol).
|
||||
|
||||
|
||||
## Remote ADB server
|
||||
|
||||
To connect to a remote _adb server_, make the server listen on all interfaces:
|
||||
|
||||
```bash
|
||||
adb kill-server
|
||||
adb -a nodaemon server start
|
||||
# keep this open
|
||||
```
|
||||
|
||||
**Warning: all communications between clients and the _adb server_ are
|
||||
unencrypted.**
|
||||
|
||||
Suppose that this server is accessible at 192.168.1.2. Then, from another
|
||||
terminal, run `scrcpy`:
|
||||
|
||||
```bash
|
||||
# in bash
|
||||
export ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
|
||||
scrcpy --tunnel-host=192.168.1.2
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: in cmd
|
||||
set ADB_SERVER_SOCKET=tcp:192.168.1.2:5037
|
||||
scrcpy --tunnel-host=192.168.1.2
|
||||
```
|
||||
|
||||
```powershell
|
||||
# in PowerShell
|
||||
$env:ADB_SERVER_SOCKET = 'tcp:192.168.1.2:5037'
|
||||
scrcpy --tunnel-host=192.168.1.2
|
||||
```
|
||||
|
||||
By default, `scrcpy` uses the local port used for `adb forward` tunnel
|
||||
establishment (typically `27183`, see `--port`). It is also possible to force a
|
||||
different tunnel port (it may be useful in more complex situations, when more
|
||||
redirections are involved):
|
||||
|
||||
```
|
||||
scrcpy --tunnel-port=1234
|
||||
```
|
||||
|
||||
|
||||
## SSH tunnel
|
||||
|
||||
To communicate with a remote _adb server_ securely, it is preferable to use an
|
||||
SSH tunnel.
|
||||
|
||||
First, make sure the _adb server_ is running on the remote computer:
|
||||
|
||||
```bash
|
||||
adb start-server
|
||||
```
|
||||
|
||||
Then, establish an SSH tunnel:
|
||||
|
||||
```bash
|
||||
# local 5038 --> remote 5037
|
||||
# local 27183 <-- remote 27183
|
||||
ssh -CN -L5038:localhost:5037 -R27183:localhost:27183 your_remote_computer
|
||||
# keep this open
|
||||
```
|
||||
|
||||
From another terminal, run `scrcpy`:
|
||||
|
||||
```bash
|
||||
# in bash
|
||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: in cmd
|
||||
set ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy
|
||||
```
|
||||
|
||||
```powershell
|
||||
# in PowerShell
|
||||
$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038'
|
||||
scrcpy
|
||||
```
|
||||
|
||||
To avoid enabling remote port forwarding, you could force a forward connection
|
||||
instead (notice the `-L` instead of `-R`):
|
||||
|
||||
```bash
|
||||
# local 5038 --> remote 5037
|
||||
# local 27183 --> remote 27183
|
||||
ssh -CN -L5038:localhost:5037 -L27183:localhost:27183 your_remote_computer
|
||||
# keep this open
|
||||
```
|
||||
|
||||
From another terminal, run `scrcpy`:
|
||||
|
||||
```bash
|
||||
# in bash
|
||||
export ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
```cmd
|
||||
:: in cmd
|
||||
set ADB_SERVER_SOCKET=tcp:localhost:5038
|
||||
scrcpy --force-adb-forward
|
||||
```
|
||||
|
||||
```powershell
|
||||
# in PowerShell
|
||||
$env:ADB_SERVER_SOCKET = 'tcp:localhost:5038'
|
||||
scrcpy --force-adb-forward
|
||||
```
|
65
doc/v4l2.md
Normal file
65
doc/v4l2.md
Normal file
@ -0,0 +1,65 @@
|
||||
# Video4Linux
|
||||
|
||||
On Linux, it is possible to send the video stream to a [v4l2] loopback device,
|
||||
so that the Android device can be opened like a webcam by any v4l2-capable tool.
|
||||
|
||||
[v4l2]: https://en.wikipedia.org/wiki/Video4Linux
|
||||
|
||||
The module `v4l2loopback` must be installed:
|
||||
|
||||
```bash
|
||||
sudo apt install v4l2loopback-dkms
|
||||
```
|
||||
|
||||
To create a v4l2 device:
|
||||
|
||||
```bash
|
||||
sudo modprobe v4l2loopback
|
||||
```
|
||||
|
||||
This will create a new video device in `/dev/videoN`, where `N` is an integer
|
||||
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
|
||||
to create several devices or devices with specific IDs).
|
||||
|
||||
To list the enabled devices:
|
||||
|
||||
```bash
|
||||
# requires v4l-utils package
|
||||
v4l2-ctl --list-devices
|
||||
|
||||
# simple but might be sufficient
|
||||
ls /dev/video*
|
||||
```
|
||||
|
||||
To start `scrcpy` using a v4l2 sink:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/videoN
|
||||
scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window
|
||||
```
|
||||
|
||||
(replace `N` with the device ID, check with `ls /dev/video*`)
|
||||
|
||||
Once enabled, you can open your video stream with a v4l2-capable tool:
|
||||
|
||||
```bash
|
||||
ffplay -i /dev/videoN
|
||||
vlc v4l2:///dev/videoN # VLC might add some buffering delay
|
||||
```
|
||||
|
||||
For example, you could capture the video within [OBS] or within your video
|
||||
conference tool.
|
||||
|
||||
[OBS]: https://obsproject.com/
|
||||
|
||||
|
||||
## Buffering
|
||||
|
||||
By default, there is no video buffering, to get the lowest possible latency.
|
||||
|
||||
As for the [video display](video.md#buffering), it is possible to add
|
||||
buffering to delay the v4l2 stream:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
|
||||
```
|
175
doc/video.md
Normal file
175
doc/video.md
Normal file
@ -0,0 +1,175 @@
|
||||
# Video
|
||||
|
||||
## Size
|
||||
|
||||
By default, scrcpy attempts to mirror at the Android device resolution.
|
||||
|
||||
It might be useful to mirror at a lower definition to increase performance. To
|
||||
limit both width and height to some maximum value (here 1024):
|
||||
|
||||
```bash
|
||||
scrcpy --max-size=1024
|
||||
scrcpy -m 1024 # short version
|
||||
```
|
||||
|
||||
The other dimension is computed so that the Android device aspect ratio is
|
||||
preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
|
||||
|
||||
If encoding fails, scrcpy automatically tries again with a lower definition
|
||||
(unless `--no-downsize-on-error` is enabled).
|
||||
|
||||
|
||||
## Bit rate
|
||||
|
||||
The default video bit-rate is 8 Mbps. To change it:
|
||||
|
||||
```bash
|
||||
scrcpy --video-bit-rate=2M
|
||||
scrcpy --video-bit-rate=2000000 # equivalent
|
||||
scrcpy -b 2M # short version
|
||||
```
|
||||
|
||||
|
||||
## Frame rate
|
||||
|
||||
The capture frame rate can be limited:
|
||||
|
||||
```bash
|
||||
scrcpy --max-fps=15
|
||||
```
|
||||
|
||||
The actual capture frame rate may be printed to the console:
|
||||
|
||||
```
|
||||
scrcpy --print-fps
|
||||
```
|
||||
|
||||
It may also be enabled or disabled at anytime with <kbd>MOD</kbd>+<kbd>i</kbd>
|
||||
(see [shortcuts](shortcuts.md)).
|
||||
|
||||
The frame rate is intrinsically variable: a new frame is produced only when the
|
||||
screen content changes. For example, if you play a fullscreen video at 24fps on
|
||||
your device, you should not get more than 24 frames per second in scrcpy.
|
||||
|
||||
|
||||
## Codec
|
||||
|
||||
The video codec can be selected. The possible values are `h264` (default),
|
||||
`h265` and `av1`:
|
||||
|
||||
```bash
|
||||
scrcpy --video-codec=h264 # default
|
||||
scrcpy --video-codec=h265
|
||||
scrcpy --video-codec=av1
|
||||
```
|
||||
|
||||
H265 may provide better quality, but H264 should provide lower latency.
|
||||
AV1 encoders are not common on current Android devices.
|
||||
|
||||
Several encoders may be available on the device. They can be listed by:
|
||||
|
||||
```bash
|
||||
scrcpy --list-encoders
|
||||
```
|
||||
|
||||
Sometimes, the default encoder may have issues or even crash, so it is useful to
|
||||
try another one:
|
||||
|
||||
```bash
|
||||
scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
|
||||
```
|
||||
|
||||
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
|
||||
check `--video-codec-options` in the manpage or in `scrcpy --help`.
|
||||
|
||||
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
|
||||
|
||||
|
||||
## Rotation
|
||||
|
||||
The rotation may be applied at 3 different levels:
|
||||
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
|
||||
device to switch between portrait and landscape (the current running app may
|
||||
refuse, if it does not support the requested orientation).
|
||||
- `--lock-video-orientation` changes the mirroring orientation (the orientation
|
||||
of the video sent from the device to the computer). This affects the
|
||||
recording.
|
||||
- `--rotation` rotates only the window content. This only affects the display,
|
||||
not the recording. It may be changed dynamically at any time using the
|
||||
[shortcuts](shortcuts.md) <kbd>MOD</kbd>+<kbd>←</kbd> and
|
||||
<kbd>MOD</kbd>+<kbd>→</kbd>.
|
||||
|
||||
To lock the mirroring orientation:
|
||||
|
||||
```bash
|
||||
scrcpy --lock-video-orientation # initial (current) orientation
|
||||
scrcpy --lock-video-orientation=0 # natural orientation
|
||||
scrcpy --lock-video-orientation=1 # 90° counterclockwise
|
||||
scrcpy --lock-video-orientation=2 # 180°
|
||||
scrcpy --lock-video-orientation=3 # 90° clockwise
|
||||
```
|
||||
|
||||
To set an initial window rotation:
|
||||
|
||||
```bash
|
||||
scrcpy --rotation=0 # no rotation
|
||||
scrcpy --rotation=1 # 90 degrees counterclockwise
|
||||
scrcpy --rotation=2 # 180 degrees
|
||||
scrcpy --rotation=3 # 90 degrees clockwise
|
||||
```
|
||||
|
||||
## Crop
|
||||
|
||||
The device screen may be cropped to mirror only part of the screen.
|
||||
|
||||
This is useful, for example, to mirror only one eye of the Oculus Go:
|
||||
|
||||
```bash
|
||||
scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0)
|
||||
```
|
||||
|
||||
The values are expressed in the device natural orientation (portrait for a
|
||||
phone, landscape for a tablet).
|
||||
|
||||
If `--max-size` is also specified, resizing is applied after cropping.
|
||||
|
||||
|
||||
## Buffering
|
||||
|
||||
By default, there is no video buffering, to get the lowest possible latency.
|
||||
|
||||
Buffering can be added to delay the video stream and compensate for jitter to
|
||||
get a smoother playback (see [#2464]).
|
||||
|
||||
[#2464]: https://github.com/Genymobile/scrcpy/issues/2464
|
||||
|
||||
The configuration is available independently for the display,
|
||||
[v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback.
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 # add 50ms buffering for display
|
||||
scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink
|
||||
scrcpy --audio-buffer=200 # set 200ms buffering for audio playback
|
||||
```
|
||||
|
||||
They can be applied simultaneously:
|
||||
|
||||
```bash
|
||||
scrcpy --display-buffer=50 --v4l2-buffer=300
|
||||
```
|
||||
|
||||
|
||||
## No display
|
||||
|
||||
It is possible to capture an Android device without displaying a mirroring
|
||||
window. This option is available if either [recording](recording.md) or
|
||||
[v4l2](#video4linux) is enabled:
|
||||
|
||||
```bash
|
||||
scrcpy --v4l2-sink=/dev/video2 --no-display
|
||||
scrcpy --record=file.mkv --no-display
|
||||
```
|
||||
|
||||
## Video4Linux
|
||||
|
||||
See the dedicated [Video4Linux](v4l2.md) page.
|
55
doc/window.md
Normal file
55
doc/window.md
Normal file
@ -0,0 +1,55 @@
|
||||
# Window
|
||||
|
||||
## Title
|
||||
|
||||
By default, the window title is the device model. It can be changed:
|
||||
|
||||
```bash
|
||||
scrcpy --window-title='My device'
|
||||
```
|
||||
|
||||
## Position and size
|
||||
|
||||
The initial window position and size may be specified:
|
||||
|
||||
```bash
|
||||
scrcpy --window-x=100 --window-y=100 --window-width=800 --window-height=600
|
||||
```
|
||||
|
||||
## Borderless
|
||||
|
||||
To disable window decorations:
|
||||
|
||||
```bash
|
||||
scrcpy --window-borderless
|
||||
```
|
||||
|
||||
## Always on top
|
||||
|
||||
To keep the window always on top:
|
||||
|
||||
```bash
|
||||
scrcpy --always-on-top
|
||||
```
|
||||
|
||||
## Fullscreen
|
||||
|
||||
The app may be started directly in fullscreen:
|
||||
|
||||
```bash
|
||||
scrcpy --fullscreen
|
||||
scrcpy -f # short version
|
||||
```
|
||||
|
||||
Fullscreen mode can then be toggled dynamically with <kbd>MOD</kbd>+<kbd>f</kbd>
|
||||
(see [shortcuts](shortcuts.md)).
|
||||
|
||||
|
||||
## Disable screensaver
|
||||
|
||||
By default, _scrcpy_ does not prevent the screensaver from running on the
|
||||
computer. To disable it:
|
||||
|
||||
```bash
|
||||
scrcpy --disable-screensaver
|
||||
```
|
89
doc/windows.md
Normal file
89
doc/windows.md
Normal file
@ -0,0 +1,89 @@
|
||||
# On Windows
|
||||
|
||||
## Install
|
||||
|
||||
Download the [latest release]:
|
||||
|
||||
- [`scrcpy-win64-v2.0.zip`][direct-win64] (64-bit)
|
||||
<sub>SHA-256: `ae4c8d37a496b43f8974ba8f07f708e22a9570ba0cddc3dc3a36edbccd4d2a20`</sub>
|
||||
- [`scrcpy-win32-v2.0.zip`][direct-win32] (32-bit)
|
||||
<sub>SHA-256: `15d98c02cb0e0bbd84f8b5d54991e0f6925569b1286a86a40743944fcb1c2d8c`</sub>
|
||||
|
||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win64-v2.0.zip
|
||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win32-v2.0.zip
|
||||
|
||||
and extract it.
|
||||
|
||||
Alternatively, you could install it from packages manager, like [Chocolatey]:
|
||||
|
||||
```bash
|
||||
choco install scrcpy
|
||||
choco install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
or [Scoop]:
|
||||
|
||||
|
||||
```bash
|
||||
scoop install scrcpy
|
||||
scoop install adb # if you don't have it yet
|
||||
```
|
||||
|
||||
[Chocolatey]: https://chocolatey.org/
|
||||
[Scoop]: https://scoop.sh
|
||||
|
||||
_See [build.md](build.md) to build and install the app manually._
|
||||
|
||||
|
||||
## Run
|
||||
|
||||
_Make sure that your device meets the [prerequisites](/README.md#prerequisites)._
|
||||
|
||||
Scrcpy is a command line application: it is mainly intended to be executed from
|
||||
a terminal with command line arguments.
|
||||
|
||||
To open a terminal at the expected location, double-click on
|
||||
`open_a_terminal_here.bat` in your scrcpy directory, then type your command. For
|
||||
example, without arguments:
|
||||
|
||||
```bash
|
||||
scrcpy
|
||||
```
|
||||
|
||||
or with arguments (here to disable audio and record to `file.mkv`):
|
||||
|
||||
```
|
||||
scrcpy --no-audio --record=file.mkv
|
||||
```
|
||||
|
||||
Documentation for command line arguments is available:
|
||||
- `scrcpy --help`
|
||||
- on [github](/README.md)
|
||||
|
||||
To start scrcpy directly without opening a terminal, double-click on one of
|
||||
these files:
|
||||
- `scrcpy-console.bat`: start with a terminal open (it will close when scrcpy
|
||||
terminates, unless an error occurs);
|
||||
- `scrcpy-noconsole.vbs`: start without a terminal (but you won't see any error
|
||||
message).
|
||||
|
||||
_Avoid double-clicking on `scrcpy.exe` directly: on error, the terminal would
|
||||
close immediately and you won't have time to read any error message (this
|
||||
executable is intended to be run from the terminal). Use `scrcpy-console.bat`
|
||||
instead._
|
||||
|
||||
If you plan to always use the same arguments, create a file `myscrcpy.bat`
|
||||
(enable [show file extensions] to avoid confusion) containing your command, For
|
||||
example:
|
||||
|
||||
```bash
|
||||
scrcpy --prefer-text --turn-screen-off --stay-awake
|
||||
```
|
||||
|
||||
[show file extensions]: https://www.howtogeek.com/205086/beginner-how-to-make-windows-show-file-extensions/
|
||||
|
||||
Then just double-click on that file.
|
||||
|
||||
You could also edit (a copy of) `scrcpy-console.bat` or `scrcpy-noconsole.vbs`
|
||||
to add some arguments.
|
@ -2,8 +2,8 @@
|
||||
set -e
|
||||
|
||||
BUILDDIR=build-auto
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v1.25/scrcpy-server-v1.25
|
||||
PREBUILT_SERVER_SHA256=ce0306c7bbd06ae72f6d06f7ec0ee33774995a65de71e0a83813ecb67aec9bdb
|
||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0
|
||||
PREBUILT_SERVER_SHA256=9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2
|
||||
|
||||
echo "[scrcpy] Downloading prebuilt server..."
|
||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||
|
@ -1,5 +1,5 @@
|
||||
project('scrcpy', 'c',
|
||||
version: '1.25',
|
||||
version: '2.0',
|
||||
meson_version: '>= 0.48',
|
||||
default_options: [
|
||||
'c_std=c11',
|
||||
|
16
release.mk
16
release.mk
@ -99,10 +99,10 @@ dist-win32: build-server build-win32
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.26.1/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.26.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
|
||||
dist-win64: build-server build-win64
|
||||
@ -118,10 +118,10 @@ dist-win64: build-server build-win64
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-33.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.26.1/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/SDL2-2.26.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
|
||||
zip-win32: dist-win32
|
||||
|
@ -7,8 +7,8 @@ android {
|
||||
applicationId "com.genymobile.scrcpy"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
versionCode 12500
|
||||
versionName "1.25"
|
||||
versionCode 20000
|
||||
versionName "2.0"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
buildTypes {
|
||||
|
@ -12,7 +12,7 @@
|
||||
set -e
|
||||
|
||||
SCRCPY_DEBUG=false
|
||||
SCRCPY_VERSION_NAME=1.25
|
||||
SCRCPY_VERSION_NAME=2.0
|
||||
|
||||
PLATFORM=${ANDROID_PLATFORM:-33}
|
||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
|
||||
|
@ -14,7 +14,6 @@ import android.media.MediaRecorder;
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public final class AudioCapture {
|
||||
@ -60,45 +59,58 @@ public final class AudioCapture {
|
||||
}
|
||||
|
||||
private static void startWorkaroundAndroid11() {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
// Android 11 requires Apps to be at foreground to record audio.
|
||||
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
||||
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
||||
// shell ("com.android.shell").
|
||||
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
||||
// foreground.
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
||||
// Wait for activity to start
|
||||
SystemClock.sleep(150);
|
||||
}
|
||||
}
|
||||
// Android 11 requires Apps to be at foreground to record audio.
|
||||
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
|
||||
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
|
||||
// shell ("com.android.shell").
|
||||
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
|
||||
// foreground.
|
||||
Intent intent = new Intent(Intent.ACTION_MAIN);
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
|
||||
intent.addCategory(Intent.CATEGORY_LAUNCHER);
|
||||
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
|
||||
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
|
||||
}
|
||||
|
||||
private static void stopWorkaroundAndroid11() {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
|
||||
}
|
||||
|
||||
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
|
||||
while (attempts-- > 0) {
|
||||
// Wait for activity to start
|
||||
SystemClock.sleep(delayMs);
|
||||
try {
|
||||
startRecording();
|
||||
return; // it worked
|
||||
} catch (UnsupportedOperationException e) {
|
||||
if (attempts == 0) {
|
||||
Ln.e("Failed to start audio capture");
|
||||
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " +
|
||||
"scrcpy.");
|
||||
throw new AudioCaptureForegroundException();
|
||||
} else {
|
||||
Ln.d("Failed to start audio capture, retrying...");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void startRecording() {
|
||||
recorder = createAudioRecord();
|
||||
recorder.startRecording();
|
||||
}
|
||||
|
||||
public void start() throws AudioCaptureForegroundException {
|
||||
startWorkaroundAndroid11();
|
||||
try {
|
||||
recorder = createAudioRecord();
|
||||
recorder.startRecording();
|
||||
} catch (UnsupportedOperationException e) {
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
Ln.e("Failed to start audio capture");
|
||||
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
|
||||
throw new AudioCaptureForegroundException();
|
||||
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
startWorkaroundAndroid11();
|
||||
try {
|
||||
tryStartRecording(3, 100);
|
||||
} finally {
|
||||
stopWorkaroundAndroid11();
|
||||
}
|
||||
throw e;
|
||||
} finally {
|
||||
stopWorkaroundAndroid11();
|
||||
} else {
|
||||
startRecording();
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,9 +122,9 @@ public final class AudioCapture {
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.N)
|
||||
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) throws IOException {
|
||||
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
|
||||
int r = recorder.read(directBuffer, size);
|
||||
if (r < 0) {
|
||||
if (r <= 0) {
|
||||
return r;
|
||||
}
|
||||
|
||||
|
@ -92,7 +92,7 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
InputTask task = inputTasks.take();
|
||||
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
|
||||
int r = capture.read(buffer, READ_SIZE, bufferInfo);
|
||||
if (r < 0) {
|
||||
if (r <= 0) {
|
||||
throw new IOException("Could not read audio: " + r);
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
}
|
||||
|
||||
private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException {
|
||||
streamer.writeHeader();
|
||||
streamer.writeAudioHeader();
|
||||
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
OutputTask task = outputTasks.take();
|
||||
@ -271,13 +271,22 @@ public final class AudioEncoder implements AsyncProcessor {
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
} catch (IOException e) {
|
||||
Ln.e("Could not create audio encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
|
||||
try {
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Ln.e("Could not create default audio encoder for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private class EncoderCallback extends MediaCodec.Callback {
|
||||
|
@ -1,6 +1,7 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.media.MediaCodec;
|
||||
import android.os.Build;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
@ -19,6 +20,12 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
||||
}
|
||||
|
||||
private void record() throws IOException, AudioCaptureForegroundException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
Ln.w("Audio disabled: it is not supported before Android 11");
|
||||
streamer.writeDisableStream(false);
|
||||
return;
|
||||
}
|
||||
|
||||
final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
|
||||
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
@ -26,7 +33,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
||||
try {
|
||||
capture.start();
|
||||
|
||||
streamer.writeHeader();
|
||||
streamer.writeAudioHeader();
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
buffer.position(0);
|
||||
int r = capture.read(buffer, READ_SIZE, bufferInfo);
|
||||
|
@ -122,18 +122,14 @@ public final class DesktopConnection implements Closeable {
|
||||
}
|
||||
}
|
||||
|
||||
public void sendDeviceMeta(String deviceName, int width, int height) throws IOException {
|
||||
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH + 4];
|
||||
public void sendDeviceMeta(String deviceName) throws IOException {
|
||||
byte[] buffer = new byte[DEVICE_NAME_FIELD_LENGTH];
|
||||
|
||||
byte[] deviceNameBytes = deviceName.getBytes(StandardCharsets.UTF_8);
|
||||
int len = StringUtils.getUtf8TruncationIndex(deviceNameBytes, DEVICE_NAME_FIELD_LENGTH - 1);
|
||||
System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
|
||||
// byte[] are always 0-initialized in java, no need to set '\0' explicitly
|
||||
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH] = (byte) (width >> 8);
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 1] = (byte) width;
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 2] = (byte) (height >> 8);
|
||||
buffer[DEVICE_NAME_FIELD_LENGTH + 3] = (byte) height;
|
||||
IO.writeFully(videoFd, buffer, 0, buffer.length);
|
||||
}
|
||||
|
||||
|
@ -288,10 +288,7 @@ public final class Device {
|
||||
boolean allOk = true;
|
||||
for (long physicalDisplayId : physicalDisplayIds) {
|
||||
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
|
||||
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||
if (!ok) {
|
||||
allOk = false;
|
||||
}
|
||||
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode);
|
||||
}
|
||||
return allOk;
|
||||
}
|
||||
|
@ -38,4 +38,10 @@ public final class FakeContext extends ContextWrapper {
|
||||
builder.setPackageName(PACKAGE_NAME);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
// @Override to be added on SDK upgrade for Android 14
|
||||
@SuppressWarnings("unused")
|
||||
public int getDeviceId() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
@ -40,7 +40,7 @@ public class Options {
|
||||
private boolean sendDeviceMeta = true; // send device name and size
|
||||
private boolean sendFrameMeta = true; // send PTS so that the client may record properly
|
||||
private boolean sendDummyByte = true; // write a byte on start to detect connection issues
|
||||
private boolean sendCodecId = true; // write the codec ID (4 bytes) before the stream
|
||||
private boolean sendCodecMeta = true; // write the codec metadata before the stream
|
||||
|
||||
public Ln.Level getLogLevel() {
|
||||
return logLevel;
|
||||
@ -282,11 +282,11 @@ public class Options {
|
||||
this.sendDummyByte = sendDummyByte;
|
||||
}
|
||||
|
||||
public boolean getSendCodecId() {
|
||||
return sendCodecId;
|
||||
public boolean getSendCodecMeta() {
|
||||
return sendCodecMeta;
|
||||
}
|
||||
|
||||
public void setSendCodecId(boolean sendCodecId) {
|
||||
this.sendCodecId = sendCodecId;
|
||||
public void setSendCodecMeta(boolean sendCodecMeta) {
|
||||
this.sendCodecMeta = sendCodecMeta;
|
||||
}
|
||||
}
|
||||
|
@ -66,7 +66,7 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
IBinder display = createDisplay();
|
||||
device.setRotationListener(this);
|
||||
|
||||
streamer.writeHeader();
|
||||
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize());
|
||||
|
||||
boolean alive;
|
||||
try {
|
||||
@ -202,13 +202,22 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
} catch (IOException e) {
|
||||
Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
|
||||
try {
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using video encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
} catch (IOException | IllegalArgumentException e) {
|
||||
Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
|
@ -81,23 +81,22 @@ public final class Server {
|
||||
// But only apply when strictly necessary, since workarounds can cause other issues:
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/940>
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/994>
|
||||
boolean mustFillAppInfo = Build.BRAND.equalsIgnoreCase("meizu");
|
||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||
Workarounds.fillAppInfo();
|
||||
}
|
||||
|
||||
// Before Android 11, audio is not supported.
|
||||
// Since Android 12, we can properly set a context on the AudioRecord.
|
||||
// Only on Android 11 we must fill app info for the AudioRecord to work.
|
||||
mustFillAppInfo |= audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R;
|
||||
|
||||
if (mustFillAppInfo) {
|
||||
Workarounds.fillAppInfo();
|
||||
// Only on Android 11 we must fill the application context for the AudioRecord to work.
|
||||
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||
Workarounds.fillAppContext();
|
||||
}
|
||||
|
||||
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
|
||||
if (options.getSendDeviceMeta()) {
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
connection.sendDeviceMeta(Device.getDeviceName());
|
||||
}
|
||||
|
||||
if (control) {
|
||||
@ -108,7 +107,7 @@ public final class Server {
|
||||
|
||||
if (audio) {
|
||||
AudioCodec audioCodec = options.getAudioCodec();
|
||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecId(),
|
||||
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(),
|
||||
options.getSendFrameMeta());
|
||||
AsyncProcessor audioRecorder;
|
||||
if (audioCodec == AudioCodec.RAW) {
|
||||
@ -120,7 +119,7 @@ public final class Server {
|
||||
asyncProcessors.add(audioRecorder);
|
||||
}
|
||||
|
||||
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
|
||||
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
|
||||
options.getSendFrameMeta());
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
|
||||
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
|
||||
@ -315,9 +314,9 @@ public final class Server {
|
||||
boolean sendDummyByte = Boolean.parseBoolean(value);
|
||||
options.setSendDummyByte(sendDummyByte);
|
||||
break;
|
||||
case "send_codec_id":
|
||||
boolean sendCodecId = Boolean.parseBoolean(value);
|
||||
options.setSendCodecId(sendCodecId);
|
||||
case "send_codec_meta":
|
||||
boolean sendCodecMeta = Boolean.parseBoolean(value);
|
||||
options.setSendCodecMeta(sendCodecMeta);
|
||||
break;
|
||||
case "raw_video_stream":
|
||||
boolean rawVideoStream = Boolean.parseBoolean(value);
|
||||
@ -325,7 +324,7 @@ public final class Server {
|
||||
options.setSendDeviceMeta(false);
|
||||
options.setSendFrameMeta(false);
|
||||
options.setSendDummyByte(false);
|
||||
options.setSendCodecId(false);
|
||||
options.setSendCodecMeta(false);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -15,24 +15,23 @@ public final class Streamer {
|
||||
|
||||
private final FileDescriptor fd;
|
||||
private final Codec codec;
|
||||
private final boolean sendCodecId;
|
||||
private final boolean sendCodecMeta;
|
||||
private final boolean sendFrameMeta;
|
||||
|
||||
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
|
||||
|
||||
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
|
||||
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecMeta, boolean sendFrameMeta) {
|
||||
this.fd = fd;
|
||||
this.codec = codec;
|
||||
this.sendCodecId = sendCodecId;
|
||||
this.sendCodecMeta = sendCodecMeta;
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
}
|
||||
|
||||
public Codec getCodec() {
|
||||
return codec;
|
||||
}
|
||||
|
||||
public void writeHeader() throws IOException {
|
||||
if (sendCodecId) {
|
||||
public void writeAudioHeader() throws IOException {
|
||||
if (sendCodecMeta) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
buffer.putInt(codec.getId());
|
||||
buffer.flip();
|
||||
@ -40,6 +39,17 @@ public final class Streamer {
|
||||
}
|
||||
}
|
||||
|
||||
public void writeVideoHeader(Size videoSize) throws IOException {
|
||||
if (sendCodecMeta) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(12);
|
||||
buffer.putInt(codec.getId());
|
||||
buffer.putInt(videoSize.getWidth());
|
||||
buffer.putInt(videoSize.getHeight());
|
||||
buffer.flip();
|
||||
IO.writeFully(fd, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
public void writeDisableStream(boolean error) throws IOException {
|
||||
// Writing a specific code as codec-id means that the device disables the stream
|
||||
// code 0: it explicitly disables the stream (because it could not capture audio), scrcpy should continue mirroring video only
|
||||
|
@ -10,6 +10,10 @@ import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
|
||||
public final class Workarounds {
|
||||
|
||||
private static Class<?> activityThreadClass;
|
||||
private static Object activityThread;
|
||||
|
||||
private Workarounds() {
|
||||
// not instantiable
|
||||
}
|
||||
@ -28,18 +32,25 @@ public final class Workarounds {
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public static void fillAppInfo() {
|
||||
try {
|
||||
private static void fillActivityThread() throws Exception {
|
||||
if (activityThread == null) {
|
||||
// ActivityThread activityThread = new ActivityThread();
|
||||
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
activityThreadClass = Class.forName("android.app.ActivityThread");
|
||||
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
|
||||
activityThreadConstructor.setAccessible(true);
|
||||
Object activityThread = activityThreadConstructor.newInstance();
|
||||
activityThread = activityThreadConstructor.newInstance();
|
||||
|
||||
// ActivityThread.sCurrentActivityThread = activityThread;
|
||||
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
|
||||
sCurrentActivityThreadField.setAccessible(true);
|
||||
sCurrentActivityThreadField.set(null, activityThread);
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public static void fillAppInfo() {
|
||||
try {
|
||||
fillActivityThread();
|
||||
|
||||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
|
||||
@ -59,6 +70,16 @@ public final class Workarounds {
|
||||
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
||||
mBoundApplicationField.setAccessible(true);
|
||||
mBoundApplicationField.set(activityThread, appBindData);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.d("Could not fill app info: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
public static void fillAppContext() {
|
||||
try {
|
||||
fillActivityThread();
|
||||
|
||||
Application app = Application.class.newInstance();
|
||||
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||
@ -71,7 +92,7 @@ public final class Workarounds {
|
||||
mInitialApplicationField.set(activityThread, app);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.d("Could not fill app info: " + throwable.getMessage());
|
||||
Ln.d("Could not fill app context: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -16,9 +16,9 @@ public class ClipboardManager {
|
||||
private Method getPrimaryClipMethod;
|
||||
private Method setPrimaryClipMethod;
|
||||
private Method addPrimaryClipChangedListener;
|
||||
private boolean alternativeGetMethod;
|
||||
private boolean alternativeSetMethod;
|
||||
private boolean alternativeAddListenerMethod;
|
||||
private int getMethodVersion;
|
||||
private int setMethodVersion;
|
||||
private int addListenerMethodVersion;
|
||||
|
||||
public ClipboardManager(IInterface manager) {
|
||||
this.manager = manager;
|
||||
@ -31,9 +31,20 @@ public class ClipboardManager {
|
||||
} else {
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
|
||||
alternativeGetMethod = true;
|
||||
getMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
|
||||
getMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
try {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
|
||||
getMethodVersion = 2;
|
||||
} catch (NoSuchMethodException e3) {
|
||||
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
|
||||
getMethodVersion = 3;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -47,41 +58,64 @@ public class ClipboardManager {
|
||||
} else {
|
||||
try {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
|
||||
alternativeSetMethod = true;
|
||||
setMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
|
||||
setMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
setPrimaryClipMethod = manager.getClass()
|
||||
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
|
||||
setMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return setPrimaryClipMethod;
|
||||
}
|
||||
|
||||
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
|
||||
private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||
}
|
||||
if (alternativeMethod) {
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
case 1:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
case 2:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
default:
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null);
|
||||
}
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
}
|
||||
|
||||
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
|
||||
private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData)
|
||||
throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||
} else if (alternativeMethod) {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
} else {
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
break;
|
||||
default:
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public CharSequence getText() {
|
||||
try {
|
||||
Method method = getGetPrimaryClipMethod();
|
||||
ClipData clipData = getPrimaryClip(method, alternativeGetMethod, manager);
|
||||
ClipData clipData = getPrimaryClip(method, getMethodVersion, manager);
|
||||
if (clipData == null || clipData.getItemCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
@ -96,7 +130,7 @@ public class ClipboardManager {
|
||||
try {
|
||||
Method method = getSetPrimaryClipMethod();
|
||||
ClipData clipData = ClipData.newPlainText(null, text);
|
||||
setPrimaryClip(method, alternativeSetMethod, manager, clipData);
|
||||
setPrimaryClip(method, setMethodVersion, manager, clipData);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
@ -104,14 +138,23 @@ public class ClipboardManager {
|
||||
}
|
||||
}
|
||||
|
||||
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
|
||||
private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager,
|
||||
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||
} else if (alternativeMethod) {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
} else {
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (methodVersion) {
|
||||
case 0:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
|
||||
break;
|
||||
case 1:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
|
||||
break;
|
||||
default:
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -124,10 +167,19 @@ public class ClipboardManager {
|
||||
try {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class);
|
||||
} catch (NoSuchMethodException e) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, int.class);
|
||||
alternativeAddListenerMethod = true;
|
||||
addListenerMethodVersion = 0;
|
||||
} catch (NoSuchMethodException e1) {
|
||||
try {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||
int.class);
|
||||
addListenerMethodVersion = 1;
|
||||
} catch (NoSuchMethodException e2) {
|
||||
addPrimaryClipChangedListener = manager.getClass()
|
||||
.getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class,
|
||||
int.class, int.class);
|
||||
addListenerMethodVersion = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -137,7 +189,7 @@ public class ClipboardManager {
|
||||
public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) {
|
||||
try {
|
||||
Method method = getAddPrimaryClipChangedListener();
|
||||
addPrimaryClipChangedListener(method, alternativeAddListenerMethod, manager, listener);
|
||||
addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener);
|
||||
return true;
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
|
Reference in New Issue
Block a user