Compare commits

..

2 Commits

Author SHA1 Message Date
b0a4e6df25 Retry on recoverable MediaCodec errors
Refs <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
Fixes #3693 <https://github.com/Genymobile/scrcpy/issues/3693>
2023-01-27 23:15:44 +01:00
545a8a8f32 Extract downsize-retry handling
Move the code to downscale and retry on error out of the catch-block.

Refs 26b4104844
2023-01-27 23:15:03 +01:00
89 changed files with 1101 additions and 4123 deletions

View File

@ -199,7 +199,7 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576.
The default bit-rate is 8 Mbps. To change the video bitrate (e.g. to 2 Mbps):
```bash
scrcpy --video-bit-rate=2M
scrcpy --bit-rate=2M
scrcpy -b 2M # short version
```
@ -252,31 +252,20 @@ This affects recording orientation.
The [window may also be rotated](#rotation) independently.
#### Codec
#### Encoder
The video codec can be selected. The possible values are `h264` (default),
`h265` and `av1`:
Some devices have more than one encoder, and some of them may cause issues or
crash. It is possible to select a different encoder:
```bash
scrcpy --video-codec=h264 # default
scrcpy --video-codec=h265
scrcpy --video-codec=av1
scrcpy --encoder=OMX.qcom.video.encoder.avc
```
##### Encoder
Some devices have more than one encoder for a specific codec, and some of them
may cause issues or crash. It is possible to select a different encoder:
To list the available encoders, you can pass an invalid encoder name; the
error will give the available encoders:
```bash
scrcpy --video-encoder=OMX.qcom.video.encoder.avc
```
To list the available encoders:
```bash
scrcpy --list-encoders
scrcpy --encoder=_
```
### Capture
@ -442,7 +431,7 @@ none found, try running `adb disconnect`, and then run those two commands again.
It may be useful to decrease the bit-rate and the resolution:
```bash
scrcpy --video-bit-rate=2M --max-size=800
scrcpy --bit-rate=2M --max-size=800
scrcpy -b2M -m800 # short version
```

View File

@ -2,33 +2,28 @@ _scrcpy() {
local cur prev words cword
local opts="
--always-on-top
--audio-bit-rate=
--audio-codec=
--audio-codec-options=
--audio-encoder=
-b --video-bit-rate=
-b --bit-rate=
--codec-options=
--crop=
-d --select-usb
--disable-screensaver
--display=
--display-buffer=
-e --select-tcpip
--encoder=
--force-adb-forward
--forward-all-clicks
-f --fullscreen
-K --hid-keyboard
-h --help
--legacy-paste
--list-displays
--list-encoders
--lock-video-orientation
--lock-video-orientation=
--max-fps=
-M --hid-mouse
-m --max-size=
--no-audio
--no-cleanup
--no-clipboard-autosync
--no-clipboard-on-error
--no-downsize-on-error
-n --no-control
-N --no-display
@ -58,9 +53,6 @@ _scrcpy() {
--v4l2-sink=
-V --verbosity=
-v --version
--video-codec=
--video-codec-options=
--video-encoder=
-w --stay-awake
--window-borderless
--window-title=
@ -72,14 +64,6 @@ _scrcpy() {
_init_completion -s || return
case "$prev" in
--video-codec)
COMPREPLY=($(compgen -W 'h264 h265 av1' -- "$cur"))
return
;;
--audio-codec)
COMPREPLY=($(compgen -W 'opus aac' -- "$cur"))
return
;;
--lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
return
@ -114,7 +98,7 @@ _scrcpy() {
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
return
;;
-b|--video-bit-rate \
-b|--bitrate \
|--codec-options \
|--crop \
|--display \

View File

@ -9,30 +9,25 @@ local arguments
arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-codec=[Select the audio codec]:codec:(opus aac)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
{-b,--bit-rate=}'[Encode the video at the given bit-rate]'
'--codec-options=[Set a list of comma-separated key\:type=value options for the device encoder]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]'
'--display=[Specify the display id to mirror]'
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
{-e,--select-tcpip}'[Use TCP/IP device]'
'--encoder=[Use a specific MediaCodec encoder \(must be a H.264 encoder\)]'
'--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'--forward-all-clicks[Forward clicks to device]'
{-f,--fullscreen}'[Start in fullscreen]'
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
{-h,--help}'[Print the help]'
'--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders available on the device]'
'--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 1 2 3)'
'--max-fps=[Limit the frame rate of screen capture]'
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
{-m,--max-size=}'[Limit both the width and height of the video to value]'
'--no-audio[Disable audio forwarding]'
'--no-cleanup[Disable device cleanup actions on exit]'
'--no-clipboard-autosync[Disable automatic clipboard synchronization]'
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
@ -63,9 +58,6 @@ arguments=(
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
{-v,--version}'[Print the version of scrcpy]'
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
'--video-encoder=[Use a specific MediaCodec video encoder]'
{-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]'
'--window-borderless[Disable window decorations \(display borderless window\)]'
'--window-title=[Set a custom window title]'

View File

@ -4,7 +4,6 @@ src = [
'src/adb/adb_device.c',
'src/adb/adb_parser.c',
'src/adb/adb_tunnel.c',
'src/audio_player.c',
'src/cli.c',
'src/clock.c',
'src/compat.c',
@ -22,7 +21,6 @@ src = [
'src/mouse_inject.c',
'src/opengl.c',
'src/options.c',
'src/packet_merger.c',
'src/receiver.c',
'src/recorder.c',
'src/scrcpy.c',
@ -31,8 +29,6 @@ src = [
'src/version.c',
'src/video_buffer.c',
'src/util/acksync.c',
'src/util/average.c',
'src/util/bytebuf.c',
'src/util/file.c',
'src/util/intmap.c',
'src/util/intr.c',
@ -102,7 +98,6 @@ if not crossbuild_windows
dependency('libavformat', version: '>= 57.33'),
dependency('libavcodec', version: '>= 57.37'),
dependency('libavutil'),
dependency('libswresample'),
dependency('sdl2', version: '>= 2.0.5'),
]
@ -133,19 +128,24 @@ else
ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin'
ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include'
# ffmpeg versions are different for win32 and win64 builds
ffmpeg_avcodec = meson.get_cross_property('ffmpeg_avcodec')
ffmpeg_avformat = meson.get_cross_property('ffmpeg_avformat')
ffmpeg_avutil = meson.get_cross_property('ffmpeg_avutil')
ffmpeg = declare_dependency(
dependencies: [
cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir),
cc.find_library('avformat-60', dirs: ffmpeg_bin_dir),
cc.find_library('avutil-58', dirs: ffmpeg_bin_dir),
cc.find_library('swresample-4', dirs: ffmpeg_bin_dir),
cc.find_library(ffmpeg_avcodec, dirs: ffmpeg_bin_dir),
cc.find_library(ffmpeg_avformat, dirs: ffmpeg_bin_dir),
cc.find_library(ffmpeg_avutil, dirs: ffmpeg_bin_dir),
],
include_directories: include_directories(ffmpeg_include_dir)
)
prebuilt_libusb = meson.get_cross_property('prebuilt_libusb')
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin'
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include'
prebuilt_libusb_root = meson.get_cross_property('prebuilt_libusb_root')
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb_root + '/include'
libusb = declare_dependency(
dependencies: [
@ -200,6 +200,10 @@ conf.set('PORTABLE', get_option('portable'))
conf.set('DEFAULT_LOCAL_PORT_RANGE_FIRST', '27183')
conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199')
# the default video bitrate, in bits/second
# overridden by option --bit-rate
conf.set('DEFAULT_BIT_RATE', '8000000') # 8Mbps
# run a server debugger and wait for a client to be attached
conf.set('SERVER_DEBUGGER', get_option('server_debugger'))
@ -259,10 +263,6 @@ if get_option('buildtype') == 'debug'
['test_binary', [
'tests/test_binary.c',
]],
['test_bytebuf', [
'tests/test_bytebuf.c',
'src/util/bytebuf.c',
]],
['test_cbuf', [
'tests/test_cbuf.c',
]],

View File

@ -0,0 +1,45 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
DEP_DIR=ffmpeg-win32-4.3.1
FILENAME_SHARED=ffmpeg-4.3.1-win32-shared.zip
SHA256SUM_SHARED=357af9901a456f4dcbacd107e83a934d344c9cb07ddad8aaf80612eeab7d26d2
FILENAME_DEV=ffmpeg-4.3.1-win32-dev.zip
SHA256SUM_DEV=230efb08e9bcf225bd474da29676c70e591fc94d8790a740ca801408fddcb78b
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_SHARED" \
"$FILENAME_SHARED" "$SHA256SUM_SHARED"
get_file "https://github.com/Genymobile/scrcpy/releases/download/v1.16/$FILENAME_DEV" \
"$FILENAME_DEV" "$SHA256SUM_DEV"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
ZIP_PREFIX_SHARED=ffmpeg-4.3.1-win32-shared
unzip "../$FILENAME_SHARED" \
"$ZIP_PREFIX_SHARED"/bin/avutil-56.dll \
"$ZIP_PREFIX_SHARED"/bin/avcodec-58.dll \
"$ZIP_PREFIX_SHARED"/bin/avformat-58.dll \
"$ZIP_PREFIX_SHARED"/bin/swresample-3.dll \
"$ZIP_PREFIX_SHARED"/bin/swscale-5.dll
ZIP_PREFIX_DEV=ffmpeg-4.3.1-win32-dev
unzip "../$FILENAME_DEV" \
"$ZIP_PREFIX_DEV/include/*"
mv "$ZIP_PREFIX_SHARED"/* .
mv "$ZIP_PREFIX_DEV"/* .
rmdir "$ZIP_PREFIX_SHARED" "$ZIP_PREFIX_DEV"

View File

@ -0,0 +1,36 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=5.1.2
DEP_DIR=ffmpeg-win64-$VERSION
FILENAME=ffmpeg-$VERSION-full_build-shared.7z
SHA256SUM=d9eb97b72d7cfdae4d0f7eaea59ccffb8c364d67d88018ea715d5e2e193f00e9
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://github.com/GyanD/codexffmpeg/releases/download/$VERSION/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
ZIP_PREFIX=ffmpeg-$VERSION-full_build-shared
7z x "../$FILENAME" \
"$ZIP_PREFIX"/bin/avutil-57.dll \
"$ZIP_PREFIX"/bin/avcodec-59.dll \
"$ZIP_PREFIX"/bin/avformat-59.dll \
"$ZIP_PREFIX"/bin/swresample-4.dll \
"$ZIP_PREFIX"/bin/swscale-6.dll \
"$ZIP_PREFIX"/include
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"

View File

@ -1,30 +0,0 @@
#!/usr/bin/env bash
set -e
DIR=$(dirname ${BASH_SOURCE[0]})
cd "$DIR"
. common
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=6.0-scrcpy
DEP_DIR="ffmpeg-$VERSION"
FILENAME="$DEP_DIR".7z
SHA256SUM=f3956295b4325a84aada05447ba3f314fbed96697811666d495de4de40d59f98
if [[ -d "$DEP_DIR" ]]
then
echo "$DEP_DIR" found
exit 0
fi
get_file "https://github.com/rom1v/scrcpy-deps/releases/download/$VERSION/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
ZIP_PREFIX=ffmpeg
7z x "../$FILENAME"
mv "$ZIP_PREFIX"/* .
rmdir "$ZIP_PREFIX"

View File

@ -22,12 +22,13 @@ get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME"
mkdir "$DEP_DIR"
cd "$DEP_DIR"
# include/ is the same in all folders of the archive
7z x "../$FILENAME" \
libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \
libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \
libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \
libusb-1.0.26-binaries/libusb-MinGW-x64/include/
mv libusb-1.0.26-binaries/libusb-MinGW-Win32 .
mv libusb-1.0.26-binaries/libusb-MinGW-x64 .
mv libusb-1.0.26-binaries/libusb-MinGW-Win32/bin MinGW-Win32
mv libusb-1.0.26-binaries/libusb-MinGW-x64/bin MinGW-x64
mv libusb-1.0.26-binaries/libusb-MinGW-x64/include .
rm -rf libusb-1.0.26-binaries

View File

@ -20,28 +20,20 @@ provides display and control of Android devices connected on USB (or over TCP/IP
Make scrcpy window always on top (above other windows).
.TP
.BI "\-\-audio\-bit\-rate " value
Encode the audio at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
Default is 196K (196000).
.TP
.BI "\-\-audio\-codec " name
Select an audio codec (opus or aac).
Default is opus.
.TP
.BI "\-\-audio\-encoder " name
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
The available encoders can be listed by \-\-list\-encoders.
.TP
.BI "\-b, \-\-video\-bit\-rate " value
.BI "\-b, \-\-bit\-rate " value
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
Default is 8M (8000000).
Default is 8000000.
.TP
.BI "\-\-codec\-options " key[:type]=value[,...]
Set a list of comma-separated key:type=value options for the device encoder.
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
The list of possible codec options is available in the Android documentation
.UR https://d.android.com/reference/android/media/MediaFormat
.UE .
.TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
@ -63,9 +55,10 @@ Disable screensaver while scrcpy is running.
.TP
.BI "\-\-display " id
Specify the device display id to mirror.
Specify the display id to mirror.
The available display ids can be listed by \-\-list\-displays.
The list of possible display ids can be listed by "adb shell dumpsys display"
(search "mDisplayId=" in the output).
Default is 0.
@ -81,6 +74,10 @@ Use TCP/IP device (if there is exactly one, like adb -e).
Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
.TP
.BI "\-\-encoder " name
Use a specific MediaCodec encoder (must be a H.264 encoder).
.TP
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.
@ -120,15 +117,7 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S
This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
.B \-\-list\-encoders
List video and audio encoders available on the device.
.TP
.B \-\-list\-displays
List displays available on the device.
.TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR]
.BI "\-\-lock\-video\-orientation[=value]
Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise.
Default is "unlocked".
@ -210,7 +199,7 @@ It may only work over USB.
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
.TP
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
.BI "\-p, \-\-port " port[:port]
Set the TCP port (range) used by the client to listen.
Default is 27183:27199.
@ -271,7 +260,7 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
The device serial number. Mandatory only if several devices are connected to adb.
.TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
.BI "\-\-shortcut\-mod " key[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
A shortcut can consist in several keys, separated by '+'. Several shortcuts can be specified, separated by ','.
@ -281,7 +270,7 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
.BI "\-\-tcpip[=ip[:port]]
Configure and reconnect the device over TCP/IP.
If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555).
@ -334,28 +323,6 @@ Default is "info" for release builds, "debug" for debug builds.
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.BI "\-\-video\-codec " name
Select a video codec (h264, h265 or av1).
Default is h264.
.TP
.BI "\-\-video\-codec\-options " key\fR[:\fItype\fR]=\fIvalue\fR[,...]
Set a list of comma-separated key:type=value options for the device video encoder.
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
The list of possible codec options is available in the Android documentation
.UR https://d.android.com/reference/android/media/MediaFormat
.UE .
.TP
.BI "\-\-video\-encoder " name
Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR).
The available encoders can be listed by \-\-list\-encoders.
.TP
.B \-w, \-\-stay-awake
Keep the device on while scrcpy is running, when the device is plugged in.

View File

@ -1,304 +0,0 @@
#include "audio_player.h"
#include <libavutil/opt.h>
#include "util/log.h"
#define SC_AUDIO_PLAYER_NDEBUG // comment to debug
/** 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 480 // 10ms at 48000Hz
// The target number of buffered samples between the producer and the consumer.
// This value is directly use for compensation.
#define SC_TARGET_BUFFERED_SAMPLES (3 * SC_AUDIO_OUTPUT_BUFFER_SAMPLES)
// If the consumer is too late, skip samples to keep at most this value
#define SC_BUFFERED_SAMPLES_THRESHOLD 2400 // 50ms at 48000Hz
// Use a ring-buffer of 1 second (at 48000Hz) between the producer and the
// consumer. It too big, but it guarantees that the producer and the consumer
// will be able to access it in parallel without locking.
#define SC_BYTEBUF_SIZE_IN_SAMPLES 48000
void
sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
struct sc_audio_player *ap = userdata;
// This callback is called with the lock used by SDL_AudioDeviceLock(), so
// the bytebuf is protected
assert(len_int > 0);
size_t len = len_int;
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] SDL callback requests %" SC_PRIsizet " samples",
len / (ap->nb_channels * ap->out_bytes_per_sample));
#endif
size_t read = sc_bytebuf_read_remaining(&ap->buf);
size_t max_buffered_bytes = SC_BUFFERED_SAMPLES_THRESHOLD
* ap->nb_channels * ap->out_bytes_per_sample;
if (read > max_buffered_bytes + len) {
size_t skip = read - (max_buffered_bytes + len);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Buffered samples threshold exceeded: %" SC_PRIsizet
" bytes, skipping %" SC_PRIsizet " bytes", read, skip);
#endif
// After this callback, exactly max_buffered_bytes will remain
sc_bytebuf_skip(&ap->buf, skip);
read = max_buffered_bytes + len;
}
// Number of buffered samples (may be negative on underflow)
float buffered_samples = ((float) read - len_int)
/ (ap->nb_channels * ap->out_bytes_per_sample);
sc_average_push(&ap->avg_buffered_samples, buffered_samples);
if (read) {
if (read > len) {
read = len;
}
sc_bytebuf_read(&ap->buf, stream, read);
}
if (read < len) {
// Insert silence
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGD("[Audio] Buffer underflow, inserting silence: %" SC_PRIsizet
" bytes", len - read);
#endif
memset(stream + read, 0, len - read);
}
}
static size_t
sc_audio_player_get_buf_size(struct sc_audio_player *ap, size_t samples) {
assert(ap->nb_channels);
assert(ap->out_bytes_per_sample);
return samples * ap->nb_channels * ap->out_bytes_per_sample;
}
static uint8_t *
sc_audio_player_get_swr_buf(struct sc_audio_player *ap, size_t min_samples) {
size_t min_buf_size = sc_audio_player_get_buf_size(ap, min_samples);
if (min_buf_size < ap->swr_buf_alloc_size) {
size_t new_size = min_buf_size + 4096;
uint8_t *buf = realloc(ap->swr_buf, new_size);
if (!buf) {
LOG_OOM();
// Could not realloc to the requested size
return NULL;
}
ap->swr_buf = buf;
ap->swr_buf_alloc_size = new_size;
}
return ap->swr_buf;
}
static bool
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
struct sc_audio_player *ap = DOWNCAST(sink);
#ifdef SCRCPY_LAVU_HAS_CHLAYOUT
assert(ctx->ch_layout.nb_channels > 0);
unsigned nb_channels = ctx->ch_layout.nb_channels;
#else
int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout);
assert(tmp > 0);
unsigned nb_channels = tmp;
#endif
SDL_AudioSpec desired = {
.freq = ctx->sample_rate,
.format = SC_SDL_SAMPLE_FMT,
.channels = nb_channels,
.samples = SC_AUDIO_OUTPUT_BUFFER_SAMPLES,
.callback = sc_audio_player_sdl_callback,
.userdata = ap,
};
SDL_AudioSpec obtained;
ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0);
if (!ap->device) {
LOGE("Could not open audio device: %s", SDL_GetError());
return false;
}
SwrContext *swr_ctx = swr_alloc();
if (!swr_ctx) {
LOG_OOM();
goto error_close_audio_device;
}
ap->swr_ctx = swr_ctx;
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);
#else
av_opt_set_channel_layout(swr_ctx, "in_channel_layout",
ctx->channel_layout, 0);
av_opt_set_channel_layout(swr_ctx, "out_channel_layout",
ctx->channel_layout, 0);
#endif
av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0);
av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0);
av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0);
av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0);
int ret = swr_init(swr_ctx);
if (ret) {
LOGE("Failed to initialize the resampling context");
goto error_free_swr_ctx;
}
ap->sample_rate = ctx->sample_rate;
ap->nb_channels = nb_channels;
ap->out_bytes_per_sample = out_bytes_per_sample;
size_t bytebuf_size =
sc_audio_player_get_buf_size(ap, SC_BYTEBUF_SIZE_IN_SAMPLES);
bool ok = sc_bytebuf_init(&ap->buf, bytebuf_size);
if (!ok) {
goto error_free_swr_ctx;
}
ap->safe_empty_buffer = sc_bytebuf_write_remaining(&ap->buf);
size_t initial_swr_buf_size = sc_audio_player_get_buf_size(ap, 4096);
ap->swr_buf = malloc(initial_swr_buf_size);
if (!ap->swr_buf) {
LOG_OOM();
goto error_destroy_bytebuf;
}
ap->swr_buf_alloc_size = initial_swr_buf_size;
sc_average_init(&ap->avg_buffered_samples, 32);
ap->samples_since_resync = 0;
SDL_PauseAudioDevice(ap->device, 0);
return true;
error_destroy_bytebuf:
sc_bytebuf_destroy(&ap->buf);
error_free_swr_ctx:
swr_free(&ap->swr_ctx);
error_close_audio_device:
SDL_CloseAudioDevice(ap->device);
return false;
}
static void
sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) {
struct sc_audio_player *ap = DOWNCAST(sink);
assert(ap->device);
SDL_PauseAudioDevice(ap->device, 1);
SDL_CloseAudioDevice(ap->device);
free(ap->swr_buf);
sc_bytebuf_destroy(&ap->buf);
swr_free(&ap->swr_ctx);
}
static bool
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
struct sc_audio_player *ap = DOWNCAST(sink);
SwrContext *swr_ctx = ap->swr_ctx;
int64_t delay = swr_get_delay(swr_ctx, ap->sample_rate);
// No need to av_rescale_rnd(), input and output sample rates are the same
int dst_nb_samples = delay + frame->nb_samples;
uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, frame->nb_samples);
if (!swr_buf) {
return false;
}
int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples,
(const uint8_t **) frame->data, frame->nb_samples);
if (ret < 0) {
LOGE("Resampling failed: %d", ret);
return false;
}
size_t samples_written = ret;
size_t swr_buf_size = sc_audio_player_get_buf_size(ap, samples_written);
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGI("[Audio] %" SC_PRIsizet " samples written to buffer", samples_written);
#endif
// It should almost always be possible to write without lock
bool can_write_without_lock = swr_buf_size <= ap->safe_empty_buffer;
if (can_write_without_lock) {
sc_bytebuf_prepare_write(&ap->buf, swr_buf, swr_buf_size);
}
SDL_LockAudioDevice(ap->device);
if (can_write_without_lock) {
sc_bytebuf_commit_write(&ap->buf, swr_buf_size);
} else {
sc_bytebuf_write(&ap->buf, swr_buf, swr_buf_size);
}
// The next time, it will remain at least the current empty space
ap->safe_empty_buffer = sc_bytebuf_write_remaining(&ap->buf);
// Read the value written by the SDL thread under lock
float avg;
bool has_avg = sc_average_get(&ap->avg_buffered_samples, &avg);
SDL_UnlockAudioDevice(ap->device);
if (has_avg) {
ap->samples_since_resync += samples_written;
if (ap->samples_since_resync >= ap->sample_rate) {
// Resync every second
ap->samples_since_resync = 0;
int diff = SC_TARGET_BUFFERED_SAMPLES - avg;
#ifndef SC_AUDIO_PLAYER_NDEBUG
LOGI("[Audio] Average buffered samples = %f, compensation %d",
avg, diff);
#endif
// Compensate the diff over 3 seconds (but will be recomputed after
// 1 second)
int ret = swr_set_compensation(swr_ctx, diff, 3 * ap->sample_rate);
if (ret < 0) {
LOGW("Resampling compensation failed: %d", ret);
// not fatal
}
}
}
return true;
}
void
sc_audio_player_init(struct sc_audio_player *ap) {
static const struct sc_frame_sink_ops ops = {
.open = sc_audio_player_frame_sink_open,
.close = sc_audio_player_frame_sink_close,
.push = sc_audio_player_frame_sink_push,
};
ap->frame_sink.ops = &ops;
}

View File

@ -1,54 +0,0 @@
#ifndef SC_AUDIO_PLAYER_H
#define SC_AUDIO_PLAYER_H
#include "common.h"
#include <stdbool.h>
#include "trait/frame_sink.h"
#include <util/average.h>
#include <util/bytebuf.h>
#include <util/thread.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
#include <SDL2/SDL.h>
struct sc_audio_player {
struct sc_frame_sink frame_sink;
SDL_AudioDeviceID device;
// protected by SDL_AudioDeviceLock()
struct sc_bytebuf buf;
// Number of bytes which could be written without locking
size_t safe_empty_buffer;
struct SwrContext *swr_ctx;
// The sample rate is the same for input and output
unsigned sample_rate;
// The number of channels is the same for input and output
unsigned nb_channels;
unsigned out_bytes_per_sample;
// Target buffer for resampling
uint8_t *swr_buf;
size_t swr_buf_alloc_size;
// Number of buffered samples (may be negative on underflow)
struct sc_average avg_buffered_samples;
unsigned samples_since_resync;
const struct sc_audio_player_callbacks *cbs;
void *cbs_userdata;
};
struct sc_audio_player_callbacks {
void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata);
};
void
sc_audio_player_init(struct sc_audio_player *ap);
#endif

View File

@ -17,59 +17,46 @@
#define STR_IMPL_(x) #x
#define STR(x) STR_IMPL_(x)
enum {
OPT_RENDER_EXPIRED_FRAMES = 1000,
OPT_WINDOW_TITLE,
OPT_PUSH_TARGET,
OPT_ALWAYS_ON_TOP,
OPT_CROP,
OPT_RECORD_FORMAT,
OPT_PREFER_TEXT,
OPT_WINDOW_X,
OPT_WINDOW_Y,
OPT_WINDOW_WIDTH,
OPT_WINDOW_HEIGHT,
OPT_WINDOW_BORDERLESS,
OPT_MAX_FPS,
OPT_LOCK_VIDEO_ORIENTATION,
OPT_DISPLAY_ID,
OPT_ROTATION,
OPT_RENDER_DRIVER,
OPT_NO_MIPMAPS,
OPT_CODEC_OPTIONS,
OPT_VIDEO_CODEC_OPTIONS,
OPT_FORCE_ADB_FORWARD,
OPT_DISABLE_SCREENSAVER,
OPT_SHORTCUT_MOD,
OPT_NO_KEY_REPEAT,
OPT_FORWARD_ALL_CLICKS,
OPT_LEGACY_PASTE,
OPT_ENCODER,
OPT_VIDEO_ENCODER,
OPT_POWER_OFF_ON_CLOSE,
OPT_V4L2_SINK,
OPT_DISPLAY_BUFFER,
OPT_V4L2_BUFFER,
OPT_TUNNEL_HOST,
OPT_TUNNEL_PORT,
OPT_NO_CLIPBOARD_AUTOSYNC,
OPT_TCPIP,
OPT_RAW_KEY_EVENTS,
OPT_NO_DOWNSIZE_ON_ERROR,
OPT_OTG,
OPT_NO_CLEANUP,
OPT_PRINT_FPS,
OPT_NO_POWER_ON,
OPT_CODEC,
OPT_VIDEO_CODEC,
OPT_NO_AUDIO,
OPT_AUDIO_BIT_RATE,
OPT_AUDIO_CODEC,
OPT_AUDIO_CODEC_OPTIONS,
OPT_AUDIO_ENCODER,
OPT_LIST_ENCODERS,
OPT_LIST_DISPLAYS,
};
#define OPT_RENDER_EXPIRED_FRAMES 1000
#define OPT_WINDOW_TITLE 1001
#define OPT_PUSH_TARGET 1002
#define OPT_ALWAYS_ON_TOP 1003
#define OPT_CROP 1004
#define OPT_RECORD_FORMAT 1005
#define OPT_PREFER_TEXT 1006
#define OPT_WINDOW_X 1007
#define OPT_WINDOW_Y 1008
#define OPT_WINDOW_WIDTH 1009
#define OPT_WINDOW_HEIGHT 1010
#define OPT_WINDOW_BORDERLESS 1011
#define OPT_MAX_FPS 1012
#define OPT_LOCK_VIDEO_ORIENTATION 1013
#define OPT_DISPLAY_ID 1014
#define OPT_ROTATION 1015
#define OPT_RENDER_DRIVER 1016
#define OPT_NO_MIPMAPS 1017
#define OPT_CODEC_OPTIONS 1018
#define OPT_FORCE_ADB_FORWARD 1019
#define OPT_DISABLE_SCREENSAVER 1020
#define OPT_SHORTCUT_MOD 1021
#define OPT_NO_KEY_REPEAT 1022
#define OPT_FORWARD_ALL_CLICKS 1023
#define OPT_LEGACY_PASTE 1024
#define OPT_ENCODER_NAME 1025
#define OPT_POWER_OFF_ON_CLOSE 1026
#define OPT_V4L2_SINK 1027
#define OPT_DISPLAY_BUFFER 1028
#define OPT_V4L2_BUFFER 1029
#define OPT_TUNNEL_HOST 1030
#define OPT_TUNNEL_PORT 1031
#define OPT_NO_CLIPBOARD_AUTOSYNC 1032
#define OPT_TCPIP 1033
#define OPT_RAW_KEY_EVENTS 1034
#define OPT_NO_DOWNSIZE_ON_ERROR 1035
#define OPT_OTG 1036
#define OPT_NO_CLEANUP 1037
#define OPT_PRINT_FPS 1038
#define OPT_NO_POWER_ON 1039
struct sc_option {
char shortopt;
@ -111,63 +98,25 @@ static const struct sc_option options[] = {
.text = "Make scrcpy window always on top (above other windows).",
},
{
.longopt_id = OPT_AUDIO_BIT_RATE,
.longopt = "audio-bit-rate",
.shortopt = 'b',
.longopt = "bit-rate",
.argdesc = "value",
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
.text = "Encode the video at the gitven bit-rate, expressed in bits/s. "
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
"Default is 196K (196000).",
"Default is " STR(DEFAULT_BIT_RATE) ".",
},
{
.longopt_id = OPT_AUDIO_CODEC,
.longopt = "audio-codec",
.argdesc = "name",
.text = "Select an audio codec (opus or aac).\n"
"Default is opus.",
},
{
.longopt_id = OPT_AUDIO_CODEC_OPTIONS,
.longopt = "audio-codec-options",
.longopt_id = OPT_CODEC_OPTIONS,
.longopt = "codec-options",
.argdesc = "key[:type]=value[,...]",
.text = "Set a list of comma-separated key:type=value options for the "
"device audio encoder.\n"
"device encoder.\n"
"The possible values for 'type' are 'int' (default), 'long', "
"'float' and 'string'.\n"
"The list of possible codec options is available in the "
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_AUDIO_ENCODER,
.longopt = "audio-encoder",
.argdesc = "name",
.text = "Use a specific MediaCodec audio encoder (depending on the "
"codec provided by --audio-codec).\n"
"The available encoders can be listed by --list-encoders.",
},
{
.shortopt = 'b',
.longopt = "video-bit-rate",
.argdesc = "value",
.text = "Encode the video at the given bit-rate, expressed in bits/s. "
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
"Default is 8M (8000000).",
},
{
// Not really deprecated (--codec has never been released), but without
// declaring an explicit --codec option, getopt_long() partial matching
// behavior would consider --codec to be equivalent to --codec-options,
// which would be confusing.
.longopt_id = OPT_CODEC,
.longopt = "codec",
.argdesc = "value",
},
{
// deprecated
.longopt_id = OPT_CODEC_OPTIONS,
.longopt = "codec-options",
.argdesc = "key[:type]=value[,...]",
},
{
.longopt_id = OPT_CROP,
.longopt = "crop",
@ -192,9 +141,10 @@ static const struct sc_option options[] = {
.longopt_id = OPT_DISPLAY_ID,
.longopt = "display",
.argdesc = "id",
.text = "Specify the device display id to mirror.\n"
"The available display ids can be listed by:\n"
" scrcpy --list-displays\n"
.text = "Specify the display id to mirror.\n"
"The list of possible display ids can be listed by:\n"
" adb shell dumpsys display\n"
"(search \"mDisplayId=\" in the output)\n"
"Default is 0.",
},
{
@ -212,10 +162,10 @@ static const struct sc_option options[] = {
"Also see -d (--select-usb).",
},
{
// deprecated
.longopt_id = OPT_ENCODER,
.longopt_id = OPT_ENCODER_NAME,
.longopt = "encoder",
.argdesc = "name",
.text = "Use a specific MediaCodec encoder (must be a H.264 encoder).",
},
{
.longopt_id = OPT_FORCE_ADB_FORWARD,
@ -265,16 +215,6 @@ static const struct sc_option options[] = {
"This is a workaround for some devices not behaving as "
"expected when setting the device clipboard programmatically.",
},
{
.longopt_id = OPT_LIST_DISPLAYS,
.longopt = "list-displays",
.text = "List device displays.",
},
{
.longopt_id = OPT_LIST_ENCODERS,
.longopt = "list-encoders",
.text = "List video and audio encoders available on the device.",
},
{
.longopt_id = OPT_LOCK_VIDEO_ORIENTATION,
.longopt = "lock-video-orientation",
@ -316,11 +256,6 @@ static const struct sc_option options[] = {
"is preserved.\n"
"Default is 0 (unlimited).",
},
{
.longopt_id = OPT_NO_AUDIO,
.longopt = "no-audio",
.text = "Disable audio forwarding.",
},
{
.longopt_id = OPT_NO_CLEANUP,
.longopt = "no-cleanup",
@ -567,33 +502,6 @@ static const struct sc_option options[] = {
.longopt = "version",
.text = "Print the version of scrcpy.",
},
{
.longopt_id = OPT_VIDEO_CODEC,
.longopt = "video-codec",
.argdesc = "name",
.text = "Select a video codec (h264, h265 or av1).\n"
"Default is h264.",
},
{
.longopt_id = OPT_VIDEO_CODEC_OPTIONS,
.longopt = "video-codec-options",
.argdesc = "key[:type]=value[,...]",
.text = "Set a list of comma-separated key:type=value options for the "
"device video encoder.\n"
"The possible values for 'type' are 'int' (default), 'long', "
"'float' and 'string'.\n"
"The list of possible codec options is available in the "
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_VIDEO_ENCODER,
.longopt = "video-encoder",
.argdesc = "name",
.text = "Use a specific MediaCodec video encoder (depending on the "
"codec provided by --video-codec).\n"
"The available encoders can be listed by --list-encoders.",
},
{
.shortopt = 'w',
.longopt = "stay-awake",
@ -1469,38 +1377,6 @@ guess_record_format(const char *filename) {
return 0;
}
static bool
parse_video_codec(const char *optarg, enum sc_codec *codec) {
if (!strcmp(optarg, "h264")) {
*codec = SC_CODEC_H264;
return true;
}
if (!strcmp(optarg, "h265")) {
*codec = SC_CODEC_H265;
return true;
}
if (!strcmp(optarg, "av1")) {
*codec = SC_CODEC_AV1;
return true;
}
LOGE("Unsupported video codec: %s (expected h264, h265 or av1)", optarg);
return false;
}
static bool
parse_audio_codec(const char *optarg, enum sc_codec *codec) {
if (!strcmp(optarg, "opus")) {
*codec = SC_CODEC_OPUS;
return true;
}
if (!strcmp(optarg, "aac")) {
*codec = SC_CODEC_AAC;
return true;
}
LOGE("Unsupported audio codec: %s (expected opus)", optarg);
return false;
}
static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) {
@ -1512,12 +1388,7 @@ 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 'b':
if (!parse_bit_rate(optarg, &opts->video_bit_rate)) {
return false;
}
break;
case OPT_AUDIO_BIT_RATE:
if (!parse_bit_rate(optarg, &opts->audio_bit_rate)) {
if (!parse_bit_rate(optarg, &opts->bit_rate)) {
return false;
}
break;
@ -1690,23 +1561,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
opts->forward_key_repeat = false;
break;
case OPT_CODEC_OPTIONS:
LOGW("--codec-options is deprecated, use --video-codec-options "
"instead.");
// fall through
case OPT_VIDEO_CODEC_OPTIONS:
opts->video_codec_options = optarg;
opts->codec_options = optarg;
break;
case OPT_AUDIO_CODEC_OPTIONS:
opts->audio_codec_options = optarg;
break;
case OPT_ENCODER:
LOGW("--encoder is deprecated, use --video-encoder instead.");
// fall through
case OPT_VIDEO_ENCODER:
opts->video_encoder = optarg;
break;
case OPT_AUDIO_ENCODER:
opts->audio_encoder = optarg;
case OPT_ENCODER_NAME:
opts->encoder_name = optarg;
break;
case OPT_FORCE_ADB_FORWARD:
opts->force_adb_forward = true;
@ -1743,9 +1601,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NO_DOWNSIZE_ON_ERROR:
opts->downsize_on_error = false;
break;
case OPT_NO_AUDIO:
opts->audio = false;
break;
case OPT_NO_CLEANUP:
opts->cleanup = false;
break;
@ -1755,19 +1610,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_PRINT_FPS:
opts->start_fps_counter = true;
break;
case OPT_CODEC:
LOGW("--codec is deprecated, use --video-codec instead.");
// fall through
case OPT_VIDEO_CODEC:
if (!parse_video_codec(optarg, &opts->video_codec)) {
return false;
}
break;
case OPT_AUDIO_CODEC:
if (!parse_audio_codec(optarg, &opts->audio_codec)) {
return false;
}
break;
case OPT_OTG:
#ifdef HAVE_USB
opts->otg = true;
@ -1795,12 +1637,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
LOGE("V4L2 (--v4l2-buffer) is only available on Linux.");
return false;
#endif
case OPT_LIST_ENCODERS:
opts->list_encoders = true;
break;
case OPT_LIST_DISPLAYS:
opts->list_displays = true;
break;
default:
// getopt prints the error message on stderr
return false;

View File

@ -37,13 +37,6 @@
# define SCRCPY_LAVF_HAS_AVFORMATCONTEXT_URL
#endif
// Not documented in ffmpeg/doc/APIchanges, but the channel_layout API
// has been replaced by chlayout in FFmpeg commit
// f423497b455da06c1337846902c770028760e094.
#if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 23, 100)
# define SCRCPY_LAVU_HAS_CHLAYOUT
#endif
#if SDL_VERSION_ATLEAST(2, 0, 6)
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS

View File

@ -117,9 +117,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, unsigned char *buf) {
uint16_t pressure =
sc_float_to_u16fp(msg->inject_touch_event.pressure);
sc_write16be(&buf[22], pressure);
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
return 32;
sc_write32be(&buf[24], msg->inject_touch_event.buttons);
return 28;
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
write_position(&buf[1], &msg->inject_scroll_event.position);
int16_t hscroll =
@ -180,25 +179,22 @@ sc_control_msg_log(const struct sc_control_msg *msg) {
if (pointer_name) {
// string pointer id
LOG_CMSG("touch [id=%s] %-4s position=%" PRIi32 ",%" PRIi32
" pressure=%f action_button=%06lx buttons=%06lx",
" pressure=%f buttons=%06lx",
pointer_name,
MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y,
msg->inject_touch_event.pressure,
(long) msg->inject_touch_event.action_button,
(long) msg->inject_touch_event.buttons);
} else {
// numeric pointer id
LOG_CMSG("touch [id=%" PRIu64_ "] %-4s position=%" PRIi32 ",%"
PRIi32 " pressure=%f action_button=%06lx"
" buttons=%06lx",
PRIi32 " pressure=%f buttons=%06lx",
id,
MOTIONEVENT_ACTION_LABEL(action),
msg->inject_touch_event.position.point.x,
msg->inject_touch_event.position.point.y,
msg->inject_touch_event.pressure,
(long) msg->inject_touch_event.action_button,
(long) msg->inject_touch_event.buttons);
}
break;

View File

@ -65,7 +65,6 @@ struct sc_control_msg {
} inject_text;
struct {
enum android_motionevent_action action;
enum android_motionevent_buttons action_button;
enum android_motionevent_buttons buttons;
uint64_t pointer_id;
struct sc_position position;

View File

@ -9,20 +9,20 @@ sc_controller_init(struct sc_controller *controller, sc_socket control_socket,
struct sc_acksync *acksync) {
cbuf_init(&controller->queue);
bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
bool ok = receiver_init(&controller->receiver, control_socket, acksync);
if (!ok) {
return false;
}
ok = sc_mutex_init(&controller->mutex);
if (!ok) {
sc_receiver_destroy(&controller->receiver);
receiver_destroy(&controller->receiver);
return false;
}
ok = sc_cond_init(&controller->msg_cond);
if (!ok) {
sc_receiver_destroy(&controller->receiver);
receiver_destroy(&controller->receiver);
sc_mutex_destroy(&controller->mutex);
return false;
}
@ -43,7 +43,7 @@ sc_controller_destroy(struct sc_controller *controller) {
sc_control_msg_destroy(&msg);
}
sc_receiver_destroy(&controller->receiver);
receiver_destroy(&controller->receiver);
}
bool
@ -117,7 +117,7 @@ sc_controller_start(struct sc_controller *controller) {
return false;
}
if (!sc_receiver_start(&controller->receiver)) {
if (!receiver_start(&controller->receiver)) {
sc_controller_stop(controller);
sc_thread_join(&controller->thread, NULL);
return false;
@ -137,5 +137,5 @@ sc_controller_stop(struct sc_controller *controller) {
void
sc_controller_join(struct sc_controller *controller) {
sc_thread_join(&controller->thread, NULL);
sc_receiver_join(&controller->receiver);
receiver_join(&controller->receiver);
}

View File

@ -21,7 +21,7 @@ struct sc_controller {
sc_cond msg_cond;
bool stopped;
struct sc_control_msg_queue queue;
struct sc_receiver receiver;
struct receiver receiver;
};
bool

View File

@ -2,7 +2,6 @@
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/channel_layout.h>
#include "events.h"
#include "video_buffer.h"
@ -26,10 +25,11 @@ sc_decoder_close_sinks(struct sc_decoder *decoder) {
}
static bool
sc_decoder_open_sinks(struct sc_decoder *decoder, const AVCodecContext *ctx) {
sc_decoder_open_sinks(struct sc_decoder *decoder) {
for (unsigned i = 0; i < decoder->sink_count; ++i) {
struct sc_frame_sink *sink = decoder->sinks[i];
if (!sink->ops->open(sink, ctx)) {
if (!sink->ops->open(sink)) {
LOGE("Could not open frame sink %d", i);
sc_decoder_close_first_sinks(decoder, i);
return false;
}
@ -48,23 +48,8 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
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);
LOGE("Could not open codec");
avcodec_free_context(&decoder->codec_ctx);
return false;
}
@ -77,7 +62,8 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
return false;
}
if (!sc_decoder_open_sinks(decoder, decoder->codec_ctx)) {
if (!sc_decoder_open_sinks(decoder)) {
LOGE("Could not open decoder sinks");
av_frame_free(&decoder->frame);
avcodec_close(decoder->codec_ctx);
avcodec_free_context(&decoder->codec_ctx);
@ -100,6 +86,7 @@ push_frame_to_sinks(struct sc_decoder *decoder, const AVFrame *frame) {
for (unsigned i = 0; i < decoder->sink_count; ++i) {
struct sc_frame_sink *sink = decoder->sinks[i];
if (!sink->ops->push(sink, frame)) {
LOGE("Could not send frame to sink %d", i);
return false;
}
}
@ -117,8 +104,7 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
int ret = avcodec_send_packet(decoder->codec_ctx, packet);
if (ret < 0 && ret != AVERROR(EAGAIN)) {
LOGE("Decoder '%s': could not send video packet: %d",
decoder->name, ret);
LOGE("Could not send video packet: %d", ret);
return false;
}
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
@ -131,8 +117,7 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
av_frame_unref(decoder->frame);
} else if (ret != AVERROR(EAGAIN)) {
LOGE("Decoder '%s', could not receive video frame: %d",
decoder->name, ret);
LOGE("Could not receive video frame: %d", ret);
return false;
}
return true;
@ -158,8 +143,7 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
}
void
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
decoder->name = name; // statically allocated
sc_decoder_init(struct sc_decoder *decoder) {
decoder->sink_count = 0;
static const struct sc_packet_sink_ops ops = {

View File

@ -14,8 +14,6 @@
struct sc_decoder {
struct sc_packet_sink packet_sink; // packet sink trait
const char *name; // must be statically allocated (e.g. a string literal)
struct sc_frame_sink *sinks[SC_DECODER_MAX_SINKS];
unsigned sink_count;
@ -23,9 +21,8 @@ struct sc_decoder {
AVFrame *frame;
};
// The name must be statically allocated (e.g. a string literal)
void
sc_decoder_init(struct sc_decoder *decoder, const char *name);
sc_decoder_init(struct sc_decoder *decoder);
void
sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink);

View File

@ -6,7 +6,6 @@
#include "decoder.h"
#include "events.h"
#include "packet_merger.h"
#include "recorder.h"
#include "util/binary.h"
#include "util/log.h"
@ -18,42 +17,6 @@
#define SC_PACKET_PTS_MASK (SC_PACKET_FLAG_KEY_FRAME - 1)
static enum AVCodecID
sc_demuxer_to_avcodec_id(uint32_t codec_id) {
#define SC_CODEC_ID_H264 UINT32_C(0x68323634) // "h264" in ASCII
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
switch (codec_id) {
case SC_CODEC_ID_H264:
return AV_CODEC_ID_H264;
case SC_CODEC_ID_H265:
return AV_CODEC_ID_HEVC;
case SC_CODEC_ID_AV1:
return AV_CODEC_ID_AV1;
case SC_CODEC_ID_OPUS:
return AV_CODEC_ID_OPUS;
case SC_CODEC_ID_AAC:
return AV_CODEC_ID_AAC;
default:
LOGE("Unknown codec id 0x%08" PRIx32, codec_id);
return AV_CODEC_ID_NONE;
}
}
static bool
sc_demuxer_recv_codec_id(struct sc_demuxer *demuxer, uint32_t *codec_id) {
uint8_t data[4];
ssize_t r = net_recv_all(demuxer->socket, data, 4);
if (r < 4) {
return false;
}
*codec_id = sc_read32be(data);
return true;
}
static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we
@ -117,6 +80,7 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
struct sc_packet_sink *sink = demuxer->sinks[i];
if (!sink->ops->push(sink, packet)) {
LOGE("Could not send config packet to sink %d", i);
return false;
}
}
@ -126,9 +90,50 @@ push_packet_to_sinks(struct sc_demuxer *demuxer, const AVPacket *packet) {
static bool
sc_demuxer_push_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
// A config packet must not be decoded immediately (it contains no
// frame); instead, it must be concatenated with the future data packet.
if (demuxer->pending || is_config) {
if (demuxer->pending) {
size_t offset = demuxer->pending->size;
if (av_grow_packet(demuxer->pending, packet->size)) {
LOG_OOM();
return false;
}
memcpy(demuxer->pending->data + offset, packet->data, packet->size);
} else {
demuxer->pending = av_packet_alloc();
if (!demuxer->pending) {
LOG_OOM();
return false;
}
if (av_packet_ref(demuxer->pending, packet)) {
LOG_OOM();
av_packet_free(&demuxer->pending);
return false;
}
}
if (!is_config) {
// prepare the concat packet to send to the decoder
demuxer->pending->pts = packet->pts;
demuxer->pending->dts = packet->dts;
demuxer->pending->flags = packet->flags;
packet = demuxer->pending;
}
}
bool ok = push_packet_to_sinks(demuxer, packet);
if (!is_config && demuxer->pending) {
// the pending packet must be discarded (consumed or error)
av_packet_free(&demuxer->pending);
}
if (!ok) {
LOGE("Demuxer '%s': could not process packet", demuxer->name);
LOGE("Could not process packet");
return false;
}
@ -153,6 +158,7 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
struct sc_packet_sink *sink = demuxer->sinks[i];
if (!sink->ops->open(sink, codec)) {
LOGE("Could not open packet sink %d", i);
sc_demuxer_close_first_sinks(demuxer, i);
return false;
}
@ -161,99 +167,50 @@ sc_demuxer_open_sinks(struct sc_demuxer *demuxer, const AVCodec *codec) {
return true;
}
static void
sc_demuxer_disable_sinks(struct sc_demuxer *demuxer) {
for (unsigned i = 0; i < demuxer->sink_count; ++i) {
struct sc_packet_sink *sink = demuxer->sinks[i];
if (sink->ops->disable) {
sink->ops->disable(sink);
}
}
}
static int
run_demuxer(void *data) {
struct sc_demuxer *demuxer = data;
// Flag to report end-of-stream (i.e. device disconnected)
bool eos = false;
uint32_t raw_codec_id;
bool ok = sc_demuxer_recv_codec_id(demuxer, &raw_codec_id);
if (!ok) {
LOGE("Demuxer '%s': stream disabled due to connection error",
demuxer->name);
eos = true;
goto end;
}
if (raw_codec_id == 0) {
LOGW("Demuxer '%s': stream explicitly disabled by the device",
demuxer->name);
sc_demuxer_disable_sinks(demuxer);
eos = true;
goto end;
}
if (raw_codec_id == 1) {
LOGE("Demuxer '%s': stream configuration error on the device",
demuxer->name);
goto end;
}
enum AVCodecID codec_id = sc_demuxer_to_avcodec_id(raw_codec_id);
if (codec_id == AV_CODEC_ID_NONE) {
LOGE("Demuxer '%s': stream disabled due to unsupported codec",
demuxer->name);
sc_demuxer_disable_sinks(demuxer);
goto end;
}
const AVCodec *codec = avcodec_find_decoder(codec_id);
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if (!codec) {
LOGE("Demuxer '%s': stream disabled due to missing decoder",
demuxer->name);
sc_demuxer_disable_sinks(demuxer);
LOGE("H.264 decoder not found");
goto end;
}
demuxer->codec_ctx = avcodec_alloc_context3(codec);
if (!demuxer->codec_ctx) {
LOG_OOM();
goto end;
}
if (!sc_demuxer_open_sinks(demuxer, codec)) {
goto end;
LOGE("Could not open demuxer sinks");
goto finally_free_codec_ctx;
}
// Config packets must be merged with the next non-config packet only for
// video streams
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO;
struct sc_packet_merger merger;
if (must_merge_config_packet) {
sc_packet_merger_init(&merger);
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
if (!demuxer->parser) {
LOGE("Could not initialize parser");
goto finally_close_sinks;
}
// We must only pass complete frames to av_parser_parse2()!
// It's more complicated, but this allows to reduce the latency by 1 frame!
demuxer->parser->flags |= PARSER_FLAG_COMPLETE_FRAMES;
AVPacket *packet = av_packet_alloc();
if (!packet) {
LOG_OOM();
goto finally_close_sinks;
goto finally_close_parser;
}
for (;;) {
bool ok = sc_demuxer_recv_packet(demuxer, packet);
if (!ok) {
// end of stream
eos = true;
break;
}
if (must_merge_config_packet) {
// Prepend any config packet to the next media packet
ok = sc_packet_merger_merge(&merger, packet);
if (!ok) {
av_packet_unref(packet);
break;
}
}
ok = sc_demuxer_push_packet(demuxer, packet);
av_packet_unref(packet);
if (!ok) {
@ -262,31 +219,33 @@ run_demuxer(void *data) {
}
}
LOGD("Demuxer '%s': end of frames", demuxer->name);
LOGD("End of frames");
if (must_merge_config_packet) {
sc_packet_merger_destroy(&merger);
if (demuxer->pending) {
av_packet_free(&demuxer->pending);
}
av_packet_free(&packet);
finally_close_parser:
av_parser_close(demuxer->parser);
finally_close_sinks:
sc_demuxer_close_sinks(demuxer);
finally_free_codec_ctx:
avcodec_free_context(&demuxer->codec_ctx);
end:
demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata);
demuxer->cbs->on_eos(demuxer, demuxer->cbs_userdata);
return 0;
}
void
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
assert(socket != SC_SOCKET_NONE);
demuxer->name = name; // statically allocated
demuxer->socket = socket;
demuxer->pending = NULL;
demuxer->sink_count = 0;
assert(cbs && cbs->on_ended);
assert(cbs && cbs->on_eos);
demuxer->cbs = cbs;
demuxer->cbs_userdata = cbs_userdata;
@ -302,12 +261,12 @@ sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) {
bool
sc_demuxer_start(struct sc_demuxer *demuxer) {
LOGD("Demuxer '%s': starting thread", demuxer->name);
LOGD("Starting demuxer thread");
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
demuxer);
if (!ok) {
LOGE("Demuxer '%s': could not start thread", demuxer->name);
LOGE("Could not start demuxer thread");
return false;
}
return true;

View File

@ -15,25 +15,28 @@
#define SC_DEMUXER_MAX_SINKS 2
struct sc_demuxer {
const char *name; // must be statically allocated (e.g. a string literal)
sc_socket socket;
sc_thread thread;
struct sc_packet_sink *sinks[SC_DEMUXER_MAX_SINKS];
unsigned sink_count;
AVCodecContext *codec_ctx;
AVCodecParserContext *parser;
// successive packets may need to be concatenated, until a non-config
// packet is available
AVPacket *pending;
const struct sc_demuxer_callbacks *cbs;
void *cbs_userdata;
};
struct sc_demuxer_callbacks {
void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata);
void (*on_eos)(struct sc_demuxer *demuxer, void *userdata);
};
// The name must be statically allocated (e.g. a string literal)
void
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
void

View File

@ -1,7 +1,5 @@
#define SC_EVENT_NEW_FRAME SDL_USEREVENT
#define SC_EVENT_DEVICE_DISCONNECTED (SDL_USEREVENT + 1)
#define SC_EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
#define SC_EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
#define SC_EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
#define EVENT_NEW_FRAME SDL_USEREVENT
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
#define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
#define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
#define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)

View File

@ -69,7 +69,7 @@ decode_image(const char *path) {
}
if (avformat_open_input(&ctx, path, NULL, NULL) < 0) {
LOGE("Could not open icon image: %s", path);
LOGE("Could not open image codec: %s", path);
goto free_ctx;
}

View File

@ -339,7 +339,6 @@ simulate_virtual_finger(struct sc_input_manager *im,
im->forward_all_clicks ? POINTER_ID_VIRTUAL_MOUSE
: POINTER_ID_VIRTUAL_FINGER;
msg.inject_touch_event.pressure = up ? 0.0f : 1.0f;
msg.inject_touch_event.action_button = 0;
msg.inject_touch_event.buttons = 0;
if (!sc_controller_push_msg(im->controller, &msg)) {

View File

@ -4,6 +4,10 @@
#include <stdbool.h>
#include <unistd.h>
#include <libavformat/avformat.h>
#ifdef _WIN32
#include <windows.h>
#include "util/str.h"
#endif
#ifdef HAVE_V4L2
# include <libavdevice/avdevice.h>
#endif
@ -15,14 +19,8 @@
#include "scrcpy.h"
#include "usb/scrcpy_otg.h"
#include "util/log.h"
#include "util/net.h"
#include "version.h"
#ifdef _WIN32
#include <windows.h>
#include "util/str.h"
#endif
int
main_scrcpy(int argc, char *argv[]) {
#ifdef _WIN32
@ -71,12 +69,10 @@ main_scrcpy(int argc, char *argv[]) {
}
#endif
if (!net_init()) {
if (avformat_network_init()) {
return SCRCPY_EXIT_FAILURE;
}
sc_log_configure();
#ifdef HAVE_USB
enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts)
: scrcpy(&args.opts);
@ -84,6 +80,8 @@ main_scrcpy(int argc, char *argv[]) {
enum scrcpy_exit_code ret = scrcpy(&args.opts);
#endif
avformat_network_deinit(); // ignore failure
return ret;
}

View File

@ -93,7 +93,6 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
.pointer_id = event->pointer_id,
.position = event->position,
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
.action_button = convert_mouse_buttons(event->button),
.buttons = convert_mouse_buttons(event->buttons_state),
},
};

View File

@ -7,19 +7,14 @@ const struct scrcpy_options scrcpy_options_default = {
.window_title = NULL,
.push_target = NULL,
.render_driver = NULL,
.video_codec_options = NULL,
.audio_codec_options = NULL,
.video_encoder = NULL,
.audio_encoder = NULL,
.codec_options = NULL,
.encoder_name = NULL,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
#endif
.log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264,
.audio_codec = SC_CODEC_OPUS,
.record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT,
.port_range = {
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
.last = DEFAULT_LOCAL_PORT_RANGE_LAST,
@ -31,8 +26,7 @@ const struct scrcpy_options scrcpy_options_default = {
.count = 2,
},
.max_size = 0,
.video_bit_rate = 0,
.audio_bit_rate = 0,
.bit_rate = DEFAULT_BIT_RATE,
.max_fps = 0,
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
.rotation = 0,
@ -71,7 +65,4 @@ const struct scrcpy_options scrcpy_options_default = {
.cleanup = true,
.start_fps_counter = false,
.power_on = true,
.audio = true,
.list_encoders = false,
.list_displays = false,
};

View File

@ -23,14 +23,6 @@ enum sc_record_format {
SC_RECORD_FORMAT_MKV,
};
enum sc_codec {
SC_CODEC_H264,
SC_CODEC_H265,
SC_CODEC_AV1,
SC_CODEC_OPUS,
SC_CODEC_AAC,
};
enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts
@ -95,16 +87,12 @@ struct scrcpy_options {
const char *window_title;
const char *push_target;
const char *render_driver;
const char *video_codec_options;
const char *audio_codec_options;
const char *video_encoder;
const char *audio_encoder;
const char *codec_options;
const char *encoder_name;
#ifdef HAVE_V4L2
const char *v4l2_device;
#endif
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode;
@ -113,8 +101,7 @@ struct scrcpy_options {
uint16_t tunnel_port;
struct sc_shortcut_mods shortcut_mods;
uint16_t max_size;
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
uint32_t bit_rate;
uint16_t max_fps;
enum sc_lock_video_orientation lock_video_orientation;
uint8_t rotation;
@ -153,9 +140,6 @@ struct scrcpy_options {
bool cleanup;
bool start_fps_counter;
bool power_on;
bool audio;
bool list_encoders;
bool list_displays;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@ -1,48 +0,0 @@
#include "packet_merger.h"
#include "util/log.h"
void
sc_packet_merger_init(struct sc_packet_merger *merger) {
merger->config = NULL;
}
void
sc_packet_merger_destroy(struct sc_packet_merger *merger) {
free(merger->config);
}
bool
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet) {
bool is_config = packet->pts == AV_NOPTS_VALUE;
if (is_config) {
free(merger->config);
merger->config = malloc(packet->size);
if (!merger->config) {
LOG_OOM();
return false;
}
memcpy(merger->config, packet->data, packet->size);
merger->config_size = packet->size;
} else if (merger->config) {
size_t config_size = merger->config_size;
size_t media_size = packet->size;
if (av_grow_packet(packet, config_size)) {
LOG_OOM();
return false;
}
memmove(packet->data + config_size, packet->data, media_size);
memcpy(packet->data, merger->config, config_size);
free(merger->config);
merger->config = NULL;
// merger->size is meaningless when merger->config is NULL
}
return true;
}

View File

@ -1,43 +0,0 @@
#ifndef SC_PACKET_MERGER_H
#define SC_PACKET_MERGER_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
#include <libavcodec/avcodec.h>
/**
* Config packets (containing the SPS/PPS) are sent in-band. A new config
* packet is sent whenever a new encoding session is started (on start and on
* device orientation change).
*
* Every time a config packet is received, it must be sent alone (for recorder
* extradata), then concatenated to the next media packet (for correct decoding
* and recording).
*
* This helper reads every input packet and modifies each media packet which
* immediately follows a config packet to prepend the config packet payload.
*/
struct sc_packet_merger {
uint8_t *config;
size_t config_size;
};
void
sc_packet_merger_init(struct sc_packet_merger *merger);
void
sc_packet_merger_destroy(struct sc_packet_merger *merger);
/**
* If the packet is a config packet, then keep its data for later.
* Otherwise (if the packet is a media packet), then if a config packet is
* pending, prepend the config packet to this packet (so the packet is
* modified!).
*/
bool
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet);
#endif

View File

@ -7,7 +7,7 @@
#include "util/log.h"
bool
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
receiver_init(struct receiver *receiver, sc_socket control_socket,
struct sc_acksync *acksync) {
bool ok = sc_mutex_init(&receiver->mutex);
if (!ok) {
@ -21,12 +21,12 @@ sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
}
void
sc_receiver_destroy(struct sc_receiver *receiver) {
receiver_destroy(struct receiver *receiver) {
sc_mutex_destroy(&receiver->mutex);
}
static void
process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
process_msg(struct receiver *receiver, struct device_msg *msg) {
switch (msg->type) {
case DEVICE_MSG_TYPE_CLIPBOARD: {
char *current = SDL_GetClipboardText();
@ -51,7 +51,7 @@ process_msg(struct sc_receiver *receiver, struct device_msg *msg) {
}
static ssize_t
process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len) {
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
size_t head = 0;
for (;;) {
struct device_msg msg;
@ -76,7 +76,7 @@ process_msgs(struct sc_receiver *receiver, const unsigned char *buf, size_t len)
static int
run_receiver(void *data) {
struct sc_receiver *receiver = data;
struct receiver *receiver = data;
static unsigned char buf[DEVICE_MSG_MAX_SIZE];
size_t head = 0;
@ -108,7 +108,7 @@ run_receiver(void *data) {
}
bool
sc_receiver_start(struct sc_receiver *receiver) {
receiver_start(struct receiver *receiver) {
LOGD("Starting receiver thread");
bool ok = sc_thread_create(&receiver->thread, run_receiver,
@ -122,6 +122,6 @@ sc_receiver_start(struct sc_receiver *receiver) {
}
void
sc_receiver_join(struct sc_receiver *receiver) {
receiver_join(struct receiver *receiver) {
sc_thread_join(&receiver->thread, NULL);
}

View File

@ -11,7 +11,7 @@
// receive events from the device
// managed by the controller
struct sc_receiver {
struct receiver {
sc_socket control_socket;
sc_thread thread;
sc_mutex mutex;
@ -20,18 +20,18 @@ struct sc_receiver {
};
bool
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
struct sc_acksync *acksync);
receiver_init(struct receiver *receiver, sc_socket control_socket,
struct sc_acksync *acksync);
void
sc_receiver_destroy(struct sc_receiver *receiver);
receiver_destroy(struct receiver *receiver);
bool
sc_receiver_start(struct sc_receiver *receiver);
receiver_start(struct receiver *receiver);
// no sc_receiver_stop(), it will automatically stop on control_socket shutdown
// no receiver_stop(), it will automatically stop on control_socket shutdown
void
sc_receiver_join(struct sc_receiver *receiver);
receiver_join(struct receiver *receiver);
#endif

View File

@ -8,11 +8,8 @@
#include "util/log.h"
#include "util/str.h"
/** Downcast packet sinks to recorder */
#define DOWNCAST_VIDEO(SINK) \
container_of(SINK, struct sc_recorder, video_packet_sink)
#define DOWNCAST_AUDIO(SINK) \
container_of(SINK, struct sc_recorder, audio_packet_sink)
/** Downcast packet_sink to recorder */
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
@ -81,7 +78,9 @@ sc_recorder_get_format_name(enum sc_record_format format) {
}
static bool
sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
if (!extradata) {
LOG_OOM();
@ -93,56 +92,170 @@ sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
ostream->codecpar->extradata = extradata;
ostream->codecpar->extradata_size = packet->size;
int ret = avformat_write_header(recorder->ctx, NULL);
if (ret < 0) {
LOGE("Failed to write header to %s", recorder->filename);
return false;
}
return true;
}
static inline void
sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) {
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base);
static void
sc_recorder_rescale_packet(struct sc_recorder *recorder, AVPacket *packet) {
AVStream *ostream = recorder->ctx->streams[0];
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, ostream->time_base);
}
static bool
sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index,
AVPacket *packet) {
AVStream *stream = recorder->ctx->streams[stream_index];
sc_recorder_rescale_packet(stream, packet);
return av_interleaved_write_frame(recorder->ctx, packet) >= 0;
sc_recorder_write(struct sc_recorder *recorder, AVPacket *packet) {
if (!recorder->header_written) {
if (packet->pts != AV_NOPTS_VALUE) {
LOGE("The first packet is not a config packet");
return false;
}
bool ok = sc_recorder_write_header(recorder, packet);
if (!ok) {
return false;
}
recorder->header_written = true;
return true;
}
if (packet->pts == AV_NOPTS_VALUE) {
// ignore config packets
return true;
}
sc_recorder_rescale_packet(recorder, packet);
return av_write_frame(recorder->ctx, packet) >= 0;
}
static inline bool
sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) {
return sc_recorder_write_stream(recorder, recorder->video_stream_index,
packet);
}
static int
run_recorder(void *data) {
struct sc_recorder *recorder = data;
static inline bool
sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) {
return sc_recorder_write_stream(recorder, recorder->audio_stream_index,
packet);
for (;;) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
}
// if stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping
if (recorder->stopped && sc_queue_is_empty(&recorder->queue)) {
sc_mutex_unlock(&recorder->mutex);
struct sc_record_packet *last = recorder->previous;
if (last) {
// assign an arbitrary duration to the last packet
last->packet->duration = 100000;
bool ok = sc_recorder_write(recorder, last->packet);
if (!ok) {
// failing to write the last frame is not very serious, no
// future frame may depend on it, so the resulting file
// will still be valid
LOGW("Could not record last packet");
}
sc_record_packet_delete(last);
}
break;
}
struct sc_record_packet *rec;
sc_queue_take(&recorder->queue, next, &rec);
sc_mutex_unlock(&recorder->mutex);
// recorder->previous is only written from this thread, no need to lock
struct sc_record_packet *previous = recorder->previous;
recorder->previous = rec;
if (!previous) {
// we just received the first packet
continue;
}
// config packets have no PTS, we must ignore them
if (rec->packet->pts != AV_NOPTS_VALUE
&& previous->packet->pts != AV_NOPTS_VALUE) {
// we now know the duration of the previous packet
previous->packet->duration =
rec->packet->pts - previous->packet->pts;
}
bool ok = sc_recorder_write(recorder, previous->packet);
sc_record_packet_delete(previous);
if (!ok) {
LOGE("Could not record packet");
sc_mutex_lock(&recorder->mutex);
recorder->failed = true;
// discard pending packets
sc_recorder_queue_clear(&recorder->queue);
sc_mutex_unlock(&recorder->mutex);
break;
}
}
if (!recorder->failed) {
if (recorder->header_written) {
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
recorder->failed = true;
}
} else {
// the recorded file is empty
recorder->failed = true;
}
}
if (recorder->failed) {
LOGE("Recording failed to %s", recorder->filename);
} else {
const char *format_name = sc_recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name,
recorder->filename);
}
LOGD("Recorder thread ended");
return 0;
}
static bool
sc_recorder_open_output_file(struct sc_recorder *recorder) {
sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
bool ok = sc_mutex_init(&recorder->mutex);
if (!ok) {
return false;
}
ok = sc_cond_init(&recorder->queue_cond);
if (!ok) {
goto error_mutex_destroy;
}
sc_queue_init(&recorder->queue);
recorder->stopped = false;
recorder->failed = false;
recorder->header_written = false;
recorder->previous = NULL;
const char *format_name = sc_recorder_get_format_name(recorder->format);
assert(format_name);
const AVOutputFormat *format = find_muxer(format_name);
if (!format) {
LOGE("Could not find muxer");
return false;
goto error_cond_destroy;
}
recorder->ctx = avformat_alloc_context();
if (!recorder->ctx) {
LOG_OOM();
return false;
}
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
avformat_free_context(recorder->ctx);
return false;
goto error_cond_destroy;
}
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
@ -154,441 +267,71 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) {
av_dict_set(&recorder->ctx->metadata, "comment",
"Recorded by scrcpy " SCRCPY_VERSION, 0);
AVStream *ostream = avformat_new_stream(recorder->ctx, input_codec);
if (!ostream) {
goto error_avformat_free_context;
}
ostream->codecpar->codec_type = AVMEDIA_TYPE_VIDEO;
ostream->codecpar->codec_id = input_codec->id;
ostream->codecpar->format = AV_PIX_FMT_YUV420P;
ostream->codecpar->width = recorder->declared_frame_size.width;
ostream->codecpar->height = recorder->declared_frame_size.height;
int ret = avio_open(&recorder->ctx->pb, recorder->filename,
AVIO_FLAG_WRITE);
if (ret < 0) {
LOGE("Failed to open output file: %s", recorder->filename);
// ostream will be cleaned up during context cleaning
goto error_avformat_free_context;
}
LOGD("Starting recorder thread");
ok = sc_thread_create(&recorder->thread, run_recorder, "scrcpy-recorder",
recorder);
if (!ok) {
LOGE("Could not start recorder thread");
goto error_avio_close;
}
LOGI("Recording started to %s file: %s", format_name, recorder->filename);
return true;
}
static void
sc_recorder_close_output_file(struct sc_recorder *recorder) {
return true;
error_avio_close:
avio_close(recorder->ctx->pb);
error_avformat_free_context:
avformat_free_context(recorder->ctx);
}
error_cond_destroy:
sc_cond_destroy(&recorder->queue_cond);
error_mutex_destroy:
sc_mutex_destroy(&recorder->mutex);
static 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_queue_is_empty(&recorder->video_queue)) {
// The video queue is empty
return true;
}
if (recorder->audio && sc_queue_is_empty(&recorder->audio_queue)) {
// The audio queue is empty (when audio is enabled)
return true;
}
// No queue is empty
return false;
}
static bool
sc_recorder_process_header(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped && sc_recorder_has_empty_queues(recorder)) {
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
}
if (recorder->stopped && sc_queue_is_empty(&recorder->video_queue)) {
// If the recorder is stopped, don't process anything if there are not
// at least video packets
sc_mutex_unlock(&recorder->mutex);
return false;
}
struct sc_record_packet *video_pkt;
sc_queue_take(&recorder->video_queue, next, &video_pkt);
struct sc_record_packet *audio_pkt = NULL;
if (!sc_queue_is_empty(&recorder->audio_queue)) {
assert(recorder->audio);
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
}
sc_mutex_unlock(&recorder->mutex);
int ret = false;
if (video_pkt->packet->pts != AV_NOPTS_VALUE) {
LOGE("The first video packet is not a config packet");
goto end;
}
assert(recorder->video_stream_index >= 0);
AVStream *video_stream =
recorder->ctx->streams[recorder->video_stream_index];
bool ok = sc_recorder_set_extradata(video_stream, video_pkt->packet);
if (!ok) {
goto end;
}
if (audio_pkt) {
if (audio_pkt->packet->pts != AV_NOPTS_VALUE) {
LOGE("The first audio packet is not a config packet");
goto end;
}
assert(recorder->audio_stream_index >= 0);
AVStream *audio_stream =
recorder->ctx->streams[recorder->audio_stream_index];
ok = sc_recorder_set_extradata(audio_stream, audio_pkt->packet);
if (!ok) {
goto end;
}
}
ok = avformat_write_header(recorder->ctx, NULL) >= 0;
if (!ok) {
LOGE("Failed to write header to %s", recorder->filename);
goto end;
}
ret = true;
end:
sc_record_packet_delete(video_pkt);
if (audio_pkt) {
sc_record_packet_delete(audio_pkt);
}
return ret;
}
static bool
sc_recorder_process_packets(struct sc_recorder *recorder) {
int64_t pts_origin = AV_NOPTS_VALUE;
bool header_written = sc_recorder_process_header(recorder);
if (!header_written) {
return false;
}
struct sc_record_packet *video_pkt = NULL;
struct sc_record_packet *audio_pkt = NULL;
// We can write a video packet only once we received the next one so that
// we can set its duration (next_pts - current_pts)
struct sc_record_packet *video_pkt_previous = NULL;
bool error = false;
for (;;) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped) {
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
// A new packet may be assigned to video_pkt and be processed
break;
}
if (recorder->audio && !audio_pkt
&& !sc_queue_is_empty(&recorder->audio_queue)) {
// A new packet may be assigned to audio_pkt and be processed
break;
}
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
}
// If stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping.
// If there is no audio, then the audio_queue will remain empty forever
// and audio_pkt will always be NULL.
assert(recorder->audio
|| (!audio_pkt && sc_queue_is_empty(&recorder->audio_queue)));
if (!video_pkt && !sc_queue_is_empty(&recorder->video_queue)) {
sc_queue_take(&recorder->video_queue, next, &video_pkt);
}
if (!audio_pkt && !sc_queue_is_empty(&recorder->audio_queue)) {
sc_queue_take(&recorder->audio_queue, next, &audio_pkt);
}
if (recorder->stopped && !video_pkt && !audio_pkt) {
assert(sc_queue_is_empty(&recorder->video_queue));
assert(sc_queue_is_empty(&recorder->audio_queue));
sc_mutex_unlock(&recorder->mutex);
break;
}
assert(video_pkt || audio_pkt); // at least one
sc_mutex_unlock(&recorder->mutex);
// Ignore further config packets (e.g. on device orientation
// change). The next non-config packet will have the config packet
// data prepended.
if (video_pkt && video_pkt->packet->pts == AV_NOPTS_VALUE) {
sc_record_packet_delete(video_pkt);
video_pkt = NULL;
}
if (audio_pkt && audio_pkt->packet->pts == AV_NOPTS_VALUE) {
sc_record_packet_delete(audio_pkt);
audio_pkt= NULL;
}
if (pts_origin == AV_NOPTS_VALUE) {
if (!recorder->audio) {
assert(video_pkt);
pts_origin = video_pkt->packet->pts;
} else if (video_pkt && audio_pkt) {
pts_origin =
MIN(video_pkt->packet->pts, audio_pkt->packet->pts);
} else if (recorder->stopped) {
if (video_pkt) {
// The recorder is stopped without audio, record the video
// packets
pts_origin = video_pkt->packet->pts;
} else {
// Fail if there is no video
error = true;
goto end;
}
// If the recorder is stopped while one of the streams has no
// packets, then we must avoid a live-loop and correctly record
// the stream having packets.
pts_origin = video_pkt ? video_pkt->packet->pts
: audio_pkt->packet->pts;
} else {
// We need both video and audio packets to initialize pts_origin
continue;
}
}
assert(pts_origin != AV_NOPTS_VALUE);
if (video_pkt) {
video_pkt->packet->pts -= pts_origin;
video_pkt->packet->dts = video_pkt->packet->pts;
if (video_pkt_previous) {
// we now know the duration of the previous packet
video_pkt_previous->packet->duration =
video_pkt->packet->pts - video_pkt_previous->packet->pts;
bool ok = sc_recorder_write_video(recorder,
video_pkt_previous->packet);
sc_record_packet_delete(video_pkt_previous);
if (!ok) {
LOGE("Could not record video packet");
error = true;
goto end;
}
}
video_pkt_previous = video_pkt;
video_pkt = NULL;
}
if (audio_pkt) {
audio_pkt->packet->pts -= pts_origin;
audio_pkt->packet->dts = audio_pkt->packet->pts;
bool ok = sc_recorder_write_audio(recorder, audio_pkt->packet);
if (!ok) {
LOGE("Could not record audio packet");
error = true;
goto end;
}
sc_record_packet_delete(audio_pkt);
audio_pkt = NULL;
}
}
// Write the last video packet
struct sc_record_packet *last = video_pkt_previous;
if (last) {
// assign an arbitrary duration to the last packet
last->packet->duration = 100000;
bool ok = sc_recorder_write_video(recorder, last->packet);
if (!ok) {
// failing to write the last frame is not very serious, no
// future frame may depend on it, so the resulting file
// will still be valid
LOGW("Could not record last packet");
}
sc_record_packet_delete(last);
}
int ret = av_write_trailer(recorder->ctx);
if (ret < 0) {
LOGE("Failed to write trailer to %s", recorder->filename);
error = false;
}
end:
if (video_pkt) {
sc_record_packet_delete(video_pkt);
}
if (audio_pkt) {
sc_record_packet_delete(audio_pkt);
}
return !error;
}
static bool
sc_recorder_record(struct sc_recorder *recorder) {
bool ok = sc_recorder_open_output_file(recorder);
if (!ok) {
return false;
}
ok = sc_recorder_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;
}
static int
run_recorder(void *data) {
struct sc_recorder *recorder = data;
bool success = sc_recorder_record(recorder);
sc_mutex_lock(&recorder->mutex);
// Prevent the producer to push any new packet
recorder->stopped = true;
// Discard pending packets
sc_recorder_queue_clear(&recorder->video_queue);
sc_mutex_unlock(&recorder->mutex);
if (success) {
const char *format_name = sc_recorder_get_format_name(recorder->format);
LOGI("Recording complete to %s file: %s", format_name,
recorder->filename);
} else {
LOGE("Recording failed to %s", recorder->filename);
}
LOGD("Recorder thread ended");
recorder->cbs->on_ended(recorder, success, recorder->cbs_userdata);
return 0;
}
static bool
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
const AVCodec *codec) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
assert(codec);
sc_mutex_lock(&recorder->mutex);
if (recorder->stopped) {
sc_mutex_unlock(&recorder->mutex);
return false;
}
recorder->video_codec = codec;
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
return true;
}
static void
sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
sc_recorder_close(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
sc_thread_join(&recorder->thread, NULL);
avio_close(recorder->ctx->pb);
avformat_free_context(recorder->ctx);
sc_cond_destroy(&recorder->queue_cond);
sc_mutex_destroy(&recorder->mutex);
}
static bool
sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
const AVPacket *packet) {
struct sc_recorder *recorder = DOWNCAST_VIDEO(sink);
sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
sc_mutex_lock(&recorder->mutex);
assert(!recorder->stopped);
if (recorder->stopped) {
// reject any new packet
if (recorder->failed) {
// reject any new packet (this will stop the stream)
sc_mutex_unlock(&recorder->mutex);
return false;
}
@ -600,9 +343,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
return false;
}
rec->packet->stream_index = 0;
sc_queue_push(&recorder->video_queue, next, rec);
sc_queue_push(&recorder->queue, next, rec);
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
@ -610,191 +351,51 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
}
static bool
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
const AVCodec *codec) {
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);
sc_mutex_lock(&recorder->mutex);
recorder->audio_codec = codec;
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
return true;
sc_recorder_packet_sink_open(struct sc_packet_sink *sink,
const AVCodec *codec) {
struct sc_recorder *recorder = DOWNCAST(sink);
return sc_recorder_open(recorder, codec);
}
static void
sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock
assert(!recorder->audio_disabled);
sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
sc_recorder_packet_sink_close(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST(sink);
sc_recorder_close(recorder);
}
static bool
sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
const AVPacket *packet) {
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock
assert(!recorder->audio_disabled);
sc_mutex_lock(&recorder->mutex);
if (recorder->stopped) {
// reject any new packet
sc_mutex_unlock(&recorder->mutex);
return false;
}
struct sc_record_packet *rec = sc_record_packet_new(packet);
if (!rec) {
LOG_OOM();
sc_mutex_unlock(&recorder->mutex);
return false;
}
rec->packet->stream_index = 1;
sc_queue_push(&recorder->audio_queue, next, rec);
sc_cond_signal(&recorder->queue_cond);
sc_mutex_unlock(&recorder->mutex);
return true;
}
static void
sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
struct sc_recorder *recorder = DOWNCAST_AUDIO(sink);
assert(recorder->audio);
// only written from this thread, no need to lock
assert(!recorder->audio_disabled);
assert(!recorder->audio_codec);
LOGW("Audio stream recording disabled");
sc_mutex_lock(&recorder->mutex);
recorder->audio_disabled = true;
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
sc_recorder_packet_sink_push(struct sc_packet_sink *sink,
const AVPacket *packet) {
struct sc_recorder *recorder = DOWNCAST(sink);
return sc_recorder_push(recorder, packet);
}
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool audio,
struct sc_size declared_frame_size,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
sc_recorder_init(struct sc_recorder *recorder,
const char *filename,
enum sc_record_format format,
struct sc_size declared_frame_size) {
recorder->filename = strdup(filename);
if (!recorder->filename) {
LOG_OOM();
return false;
}
bool ok = sc_mutex_init(&recorder->mutex);
if (!ok) {
goto error_free_filename;
}
ok = sc_cond_init(&recorder->queue_cond);
if (!ok) {
goto error_mutex_destroy;
}
ok = sc_cond_init(&recorder->stream_cond);
if (!ok) {
goto error_queue_cond_destroy;
}
recorder->audio = audio;
sc_queue_init(&recorder->video_queue);
sc_queue_init(&recorder->audio_queue);
recorder->stopped = false;
recorder->video_codec = NULL;
recorder->audio_codec = NULL;
recorder->audio_disabled = false;
recorder->video_stream_index = -1;
recorder->audio_stream_index = -1;
recorder->format = format;
recorder->declared_frame_size = declared_frame_size;
assert(cbs && cbs->on_ended);
recorder->cbs = cbs;
recorder->cbs_userdata = cbs_userdata;
static const struct sc_packet_sink_ops video_ops = {
.open = sc_recorder_video_packet_sink_open,
.close = sc_recorder_video_packet_sink_close,
.push = sc_recorder_video_packet_sink_push,
static const struct sc_packet_sink_ops ops = {
.open = sc_recorder_packet_sink_open,
.close = sc_recorder_packet_sink_close,
.push = sc_recorder_packet_sink_push,
};
recorder->video_packet_sink.ops = &video_ops;
if (audio) {
static const struct sc_packet_sink_ops audio_ops = {
.open = sc_recorder_audio_packet_sink_open,
.close = sc_recorder_audio_packet_sink_close,
.push = sc_recorder_audio_packet_sink_push,
.disable = sc_recorder_audio_packet_sink_disable,
};
recorder->audio_packet_sink.ops = &audio_ops;
}
recorder->packet_sink.ops = &ops;
return true;
error_queue_cond_destroy:
sc_cond_destroy(&recorder->queue_cond);
error_mutex_destroy:
sc_mutex_destroy(&recorder->mutex);
error_free_filename:
free(recorder->filename);
return false;
}
bool
sc_recorder_start(struct sc_recorder *recorder) {
bool ok = sc_thread_create(&recorder->thread, run_recorder,
"scrcpy-recorder", recorder);
if (!ok) {
LOGE("Could not start recorder thread");
return false;
}
return true;
}
void
sc_recorder_stop(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex);
}
void
sc_recorder_join(struct sc_recorder *recorder) {
sc_thread_join(&recorder->thread, NULL);
}
void
sc_recorder_destroy(struct sc_recorder *recorder) {
sc_cond_destroy(&recorder->stream_cond);
sc_cond_destroy(&recorder->queue_cond);
sc_mutex_destroy(&recorder->mutex);
free(recorder->filename);
}

View File

@ -20,66 +20,32 @@ struct sc_record_packet {
struct sc_recorder_queue SC_QUEUE(struct sc_record_packet);
struct sc_recorder {
struct sc_packet_sink video_packet_sink;
struct sc_packet_sink audio_packet_sink;
/* The audio flag is unprotected:
* - it is initialized from sc_recorder_init() from the main thread;
* - it may be reset once from the recorder thread if the audio is
* disabled dynamically.
*
* Therefore, once the recorder thread is started, only the recorder thread
* may access it without data races.
*/
bool audio;
struct sc_packet_sink packet_sink; // packet sink trait
char *filename;
enum sc_record_format format;
AVFormatContext *ctx;
struct sc_size declared_frame_size;
bool header_written;
sc_thread thread;
sc_mutex mutex;
sc_cond queue_cond;
// set on sc_recorder_stop(), packet_sink close or recording failure
bool stopped;
struct sc_recorder_queue video_queue;
struct sc_recorder_queue audio_queue;
bool stopped; // set on recorder_close()
bool failed; // set on packet write failure
struct sc_recorder_queue queue;
// wake up the recorder thread once the video or audio codec is known
sc_cond stream_cond;
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;
int video_stream_index;
int audio_stream_index;
const struct sc_recorder_callbacks *cbs;
void *cbs_userdata;
};
struct sc_recorder_callbacks {
void (*on_ended)(struct sc_recorder *recorder, bool success,
void *userdata);
// we can write a packet only once we received the next one so that we can
// set its duration (next_pts - current_pts)
// "previous" is only accessed from the recorder thread, so it does not
// need to be protected by the mutex
struct sc_record_packet *previous;
};
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool audio,
struct sc_size declared_frame_size,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
bool
sc_recorder_start(struct sc_recorder *recorder);
void
sc_recorder_stop(struct sc_recorder *recorder);
void
sc_recorder_join(struct sc_recorder *recorder);
enum sc_record_format format,
struct sc_size declared_frame_size);
void
sc_recorder_destroy(struct sc_recorder *recorder);

View File

@ -13,7 +13,6 @@
# include <windows.h>
#endif
#include "audio_player.h"
#include "controller.h"
#include "decoder.h"
#include "demuxer.h"
@ -41,11 +40,8 @@
struct scrcpy {
struct sc_server server;
struct sc_screen screen;
struct sc_audio_player audio_player;
struct sc_demuxer video_demuxer;
struct sc_demuxer audio_demuxer;
struct sc_decoder video_decoder;
struct sc_decoder audio_decoder;
struct sc_demuxer demuxer;
struct sc_decoder decoder;
struct sc_recorder recorder;
#ifdef HAVE_V4L2
struct sc_v4l2_sink v4l2_sink;
@ -159,15 +155,9 @@ event_loop(struct scrcpy *s) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SC_EVENT_DEVICE_DISCONNECTED:
case EVENT_STREAM_STOPPED:
LOGW("Device disconnected");
return SCRCPY_EXIT_DISCONNECTED;
case SC_EVENT_DEMUXER_ERROR:
LOGE("Demuxer error");
return SCRCPY_EXIT_FAILURE;
case SC_EVENT_RECORDER_ERROR:
LOGE("Recorder error");
return SCRCPY_EXIT_FAILURE;
case SDL_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
@ -186,16 +176,15 @@ await_for_server(bool *connected) {
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SDL_QUIT:
if (connected) {
*connected = false;
}
LOGD("User requested to quit");
*connected = false;
return true;
case SC_EVENT_SERVER_CONNECTION_FAILED:
case EVENT_SERVER_CONNECTION_FAILED:
LOGE("Server connection failed");
return false;
case SC_EVENT_SERVER_CONNECTED:
if (connected) {
*connected = true;
}
case EVENT_SERVER_CONNECTED:
LOGD("Server connected");
*connected = true;
return true;
default:
break;
@ -206,47 +195,49 @@ await_for_server(bool *connected) {
return false;
}
static void
sc_recorder_on_ended(struct sc_recorder *recorder, bool success,
void *userdata) {
(void) recorder;
(void) userdata;
if (!success) {
PUSH_EVENT(SC_EVENT_RECORDER_ERROR);
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
return SDL_LOG_PRIORITY_CRITICAL;
case AV_LOG_ERROR:
return SDL_LOG_PRIORITY_ERROR;
case AV_LOG_WARNING:
return SDL_LOG_PRIORITY_WARN;
case AV_LOG_INFO:
return SDL_LOG_PRIORITY_INFO;
}
// do not forward others, which are too verbose
return 0;
}
static void
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
void *userdata) {
(void) demuxer;
(void) userdata;
if (eos) {
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
} else {
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
(void) avcl;
SDL_LogPriority priority = sdl_priority_from_av_level(level);
if (priority == 0) {
return;
}
size_t fmt_len = strlen(fmt);
char *local_fmt = malloc(fmt_len + 10);
if (!local_fmt) {
LOG_OOM();
return;
}
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0'
SDL_LogMessageV(SDL_LOG_CATEGORY_VIDEO, priority, local_fmt, vl);
free(local_fmt);
}
static void
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
void *userdata) {
sc_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
(void) demuxer;
(void) userdata;
// Contrary to the video demuxer, keep mirroring if only the audio fails.
// 'eos' is true on end-of-stream, including when audio capture is not
// possible on the device (so that scrcpy continue to mirror video without
// failing).
// However, if an audio configuration failure occurs (for example the user
// explicitly selected an unknown audio encoder), 'eos' is false and scrcpy
// must exit.
if (!eos) {
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
}
PUSH_EVENT(EVENT_STREAM_STOPPED);
}
static void
@ -254,7 +245,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
(void) server;
(void) userdata;
PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED);
PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED);
}
static void
@ -262,7 +253,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) {
(void) server;
(void) userdata;
PUSH_EVENT(SC_EVENT_SERVER_CONNECTED);
PUSH_EVENT(EVENT_SERVER_CONNECTED);
}
static void
@ -275,9 +266,8 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
// event
}
// Generate a scrcpy id to differentiate multiple running scrcpy instances
static uint32_t
scrcpy_generate_scid() {
scrcpy_generate_uid() {
struct sc_rand rand;
sc_rand_init(&rand);
// Only use 31 bits to avoid issues with signed values on the Java-side
@ -302,12 +292,10 @@ scrcpy(struct scrcpy_options *options) {
bool server_started = false;
bool file_pusher_initialized = false;
bool recorder_initialized = false;
bool recorder_started = false;
#ifdef HAVE_V4L2
bool v4l2_sink_initialized = false;
#endif
bool video_demuxer_started = false;
bool audio_demuxer_started = false;
bool demuxer_started = false;
#ifdef HAVE_USB
bool aoa_hid_initialized = false;
bool hid_keyboard_initialized = false;
@ -319,34 +307,28 @@ scrcpy(struct scrcpy_options *options) {
struct sc_acksync *acksync = NULL;
uint32_t scid = scrcpy_generate_scid();
uint32_t uid = scrcpy_generate_uid();
struct sc_server_params params = {
.scid = scid,
.uid = uid,
.req_serial = options->serial,
.select_usb = options->select_usb,
.select_tcpip = options->select_tcpip,
.log_level = options->log_level,
.video_codec = options->video_codec,
.audio_codec = options->audio_codec,
.crop = options->crop,
.port_range = options->port_range,
.tunnel_host = options->tunnel_host,
.tunnel_port = options->tunnel_port,
.max_size = options->max_size,
.video_bit_rate = options->video_bit_rate,
.audio_bit_rate = options->audio_bit_rate,
.bit_rate = options->bit_rate,
.max_fps = options->max_fps,
.lock_video_orientation = options->lock_video_orientation,
.control = options->control,
.display_id = options->display_id,
.audio = options->audio,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.video_codec_options = options->video_codec_options,
.audio_codec_options = options->audio_codec_options,
.video_encoder = options->video_encoder,
.audio_encoder = options->audio_encoder,
.codec_options = options->codec_options,
.encoder_name = options->encoder_name,
.force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close,
.clipboard_autosync = options->clipboard_autosync,
@ -355,8 +337,6 @@ scrcpy(struct scrcpy_options *options) {
.tcpip_dst = options->tcpip_dst,
.cleanup = options->cleanup,
.power_on = options->power_on,
.list_encoders = options->list_encoders,
.list_displays = options->list_displays,
};
static const struct sc_server_callbacks cbs = {
@ -374,27 +354,14 @@ scrcpy(struct scrcpy_options *options) {
server_started = true;
if (options->list_encoders || options->list_displays) {
bool ok = await_for_server(NULL);
ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE;
goto end;
}
if (options->display) {
sdl_set_hints(options->render_driver);
}
// Initialize SDL video in addition if display is enabled
if (options->display) {
if (SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
}
if (options->audio && SDL_Init(SDL_INIT_AUDIO)) {
LOGE("Could not initialize SDL audio: %s", SDL_GetError());
goto end;
}
if (options->display && SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL: %s", SDL_GetError());
goto end;
}
sdl_configure(options->display, options->disable_screensaver);
@ -402,19 +369,15 @@ scrcpy(struct scrcpy_options *options) {
// Await for server without blocking Ctrl+C handling
bool connected;
if (!await_for_server(&connected)) {
LOGE("Server connection failed");
goto end;
}
if (!connected) {
// This is not an error, user requested to quit
LOGD("User requested to quit");
ret = SCRCPY_EXIT_SUCCESS;
goto end;
}
LOGD("Server connected");
// It is necessarily initialized here, since the device is connected
struct sc_server_info *info = &s->server.info;
@ -432,55 +395,41 @@ scrcpy(struct scrcpy_options *options) {
file_pusher_initialized = true;
}
static const struct sc_demuxer_callbacks video_demuxer_cbs = {
.on_ended = sc_video_demuxer_on_ended,
};
sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
&video_demuxer_cbs, NULL);
if (options->audio) {
static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
.on_ended = sc_audio_demuxer_on_ended,
};
sc_demuxer_init(&s->audio_demuxer, "audio", s->server.audio_socket,
&audio_demuxer_cbs, NULL);
}
bool needs_video_decoder = options->display;
bool needs_audio_decoder = options->audio && options->display;
struct sc_decoder *dec = NULL;
bool needs_decoder = options->display;
#ifdef HAVE_V4L2
needs_video_decoder |= !!options->v4l2_device;
needs_decoder |= !!options->v4l2_device;
#endif
if (needs_video_decoder) {
sc_decoder_init(&s->video_decoder, "video");
sc_demuxer_add_sink(&s->video_demuxer, &s->video_decoder.packet_sink);
}
if (needs_audio_decoder) {
sc_decoder_init(&s->audio_decoder, "audio");
sc_demuxer_add_sink(&s->audio_demuxer, &s->audio_decoder.packet_sink);
if (needs_decoder) {
sc_decoder_init(&s->decoder);
dec = &s->decoder;
}
struct sc_recorder *rec = NULL;
if (options->record_filename) {
static const struct sc_recorder_callbacks recorder_cbs = {
.on_ended = sc_recorder_on_ended,
};
if (!sc_recorder_init(&s->recorder, options->record_filename,
options->record_format, options->audio,
info->frame_size, &recorder_cbs, NULL)) {
if (!sc_recorder_init(&s->recorder,
options->record_filename,
options->record_format,
info->frame_size)) {
goto end;
}
rec = &s->recorder;
recorder_initialized = true;
}
if (!sc_recorder_start(&s->recorder)) {
goto end;
}
recorder_started = true;
av_log_set_callback(av_log_callback);
sc_demuxer_add_sink(&s->video_demuxer, &s->recorder.video_packet_sink);
if (options->audio) {
sc_demuxer_add_sink(&s->audio_demuxer,
&s->recorder.audio_packet_sink);
}
static const struct sc_demuxer_callbacks demuxer_cbs = {
.on_eos = sc_demuxer_on_eos,
};
sc_demuxer_init(&s->demuxer, s->server.video_socket, &demuxer_cbs, NULL);
if (dec) {
sc_demuxer_add_sink(&s->demuxer, &dec->packet_sink);
}
if (rec) {
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
}
struct sc_controller *controller = NULL;
@ -671,12 +620,7 @@ aoa_hid_end:
}
screen_initialized = true;
sc_decoder_add_sink(&s->video_decoder, &s->screen.frame_sink);
if (options->audio) {
sc_audio_player_init(&s->audio_player);
sc_decoder_add_sink(&s->audio_decoder, &s->audio_player.frame_sink);
}
sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);
}
#ifdef HAVE_V4L2
@ -686,31 +630,24 @@ aoa_hid_end:
goto end;
}
sc_decoder_add_sink(&s->video_decoder, &s->v4l2_sink.frame_sink);
sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);
v4l2_sink_initialized = true;
}
#endif
// now we consumed the header values, the socket receives the video stream
// start the video demuxer
if (!sc_demuxer_start(&s->video_demuxer)) {
// start the demuxer
if (!sc_demuxer_start(&s->demuxer)) {
goto end;
}
video_demuxer_started = true;
if (options->audio) {
if (!sc_demuxer_start(&s->audio_demuxer)) {
goto end;
}
audio_demuxer_started = true;
}
demuxer_started = true;
ret = event_loop(s);
LOGD("quit...");
// Close the window immediately on closing, because screen_destroy() may
// only be called once the video demuxer thread is joined (it may take time)
// only be called once the demuxer thread is joined (it may take time)
sc_screen_hide_window(&s->screen);
end:
@ -737,9 +674,6 @@ end:
if (file_pusher_initialized) {
sc_file_pusher_stop(&s->file_pusher);
}
if (recorder_initialized) {
sc_recorder_stop(&s->recorder);
}
if (screen_initialized) {
sc_screen_interrupt(&s->screen);
}
@ -751,12 +685,8 @@ end:
// now that the sockets are shutdown, the demuxer and controller are
// interrupted, we can join them
if (video_demuxer_started) {
sc_demuxer_join(&s->video_demuxer);
}
if (audio_demuxer_started) {
sc_demuxer_join(&s->audio_demuxer);
if (demuxer_started) {
sc_demuxer_join(&s->demuxer);
}
#ifdef HAVE_V4L2
@ -775,9 +705,8 @@ end:
}
#endif
// Destroy the screen only after the video demuxer is guaranteed to be
// finished, because otherwise the screen could receive new frames after
// destruction
// Destroy the screen only after the demuxer is guaranteed to be finished,
// because otherwise the screen could receive new frames after destruction
if (screen_initialized) {
sc_screen_join(&s->screen);
sc_screen_destroy(&s->screen);
@ -790,9 +719,6 @@ end:
sc_controller_destroy(&s->controller);
}
if (recorder_started) {
sc_recorder_join(&s->recorder);
}
if (recorder_initialized) {
sc_recorder_destroy(&s->recorder);
}
@ -802,10 +728,6 @@ end:
sc_file_pusher_destroy(&s->file_pusher);
}
if (server_started) {
sc_server_join(&s->server);
}
sc_server_destroy(&s->server);
return ret;

View File

@ -330,11 +330,7 @@ event_watcher(void *data, SDL_Event *event) {
#endif
static bool
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
const AVCodecContext *ctx) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
sc_screen_frame_sink_open(struct sc_frame_sink *sink) {
struct sc_screen *screen = DOWNCAST(sink);
(void) screen;
#ifndef NDEBUG
@ -375,7 +371,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
bool need_new_event;
if (previous_skipped) {
sc_fps_counter_add_skipped_frame(&screen->fps_counter);
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
// The EVENT_NEW_FRAME triggered for the previous frame will consume
// this new frame instead, unless the previous event failed
need_new_event = screen->event_failed;
} else {
@ -384,7 +380,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
if (need_new_event) {
static SDL_Event new_frame_event = {
.type = SC_EVENT_NEW_FRAME,
.type = EVENT_NEW_FRAME,
};
// Post the event on the UI thread
@ -824,7 +820,7 @@ sc_screen_handle_event(struct sc_screen *screen, SDL_Event *event) {
bool relative_mode = sc_screen_is_relative_mode(screen);
switch (event->type) {
case SC_EVENT_NEW_FRAME: {
case EVENT_NEW_FRAME: {
bool ok = sc_screen_update_frame(screen);
if (!ok) {
LOGW("Frame update failed\n");

View File

@ -8,7 +8,6 @@
#include <SDL2/SDL_platform.h>
#include "adb/adb.h"
#include "util/binary.h"
#include "util/file.h"
#include "util/log.h"
#include "util/net_intr.h"
@ -71,10 +70,8 @@ sc_server_params_destroy(struct sc_server_params *params) {
// The server stores a copy of the params provided by the user
free((char *) params->req_serial);
free((char *) params->crop);
free((char *) params->video_codec_options);
free((char *) params->audio_codec_options);
free((char *) params->video_encoder);
free((char *) params->audio_encoder);
free((char *) params->codec_options);
free((char *) params->encoder_name);
free((char *) params->tcpip_dst);
}
@ -97,10 +94,8 @@ sc_server_params_copy(struct sc_server_params *dst,
COPY(req_serial);
COPY(crop);
COPY(video_codec_options);
COPY(audio_codec_options);
COPY(video_encoder);
COPY(audio_encoder);
COPY(codec_options);
COPY(encoder_name);
COPY(tcpip_dst);
#undef COPY
@ -160,24 +155,6 @@ sc_server_sleep(struct sc_server *server, sc_tick deadline) {
return !stopped;
}
static const char *
sc_server_get_codec_name(enum sc_codec codec) {
switch (codec) {
case SC_CODEC_H264:
return "h264";
case SC_CODEC_H265:
return "h265";
case SC_CODEC_AV1:
return "av1";
case SC_CODEC_OPUS:
return "opus";
case SC_CODEC_AAC:
return "aac";
default:
return NULL;
}
}
static sc_pid
execute_server(struct sc_server *server,
const struct sc_server_params *params) {
@ -221,25 +198,10 @@ execute_server(struct sc_server *server,
cmd[count++] = p; \
}
ADD_PARAM("scid=%08x", params->scid);
ADD_PARAM("uid=%08x", params->uid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
ADD_PARAM("bit_rate=%" PRIu32, params->bit_rate);
if (params->video_bit_rate) {
ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate);
}
if (!params->audio) {
ADD_PARAM("audio=false");
} else if (params->audio_bit_rate) {
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
}
if (params->video_codec != SC_CODEC_H264) {
ADD_PARAM("video_codec=%s",
sc_server_get_codec_name(params->video_codec));
}
if (params->audio_codec != SC_CODEC_OPUS) {
ADD_PARAM("audio_codec=%s",
sc_server_get_codec_name(params->audio_codec));
}
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);
}
@ -269,17 +231,11 @@ execute_server(struct sc_server *server,
if (params->stay_awake) {
ADD_PARAM("stay_awake=true");
}
if (params->video_codec_options) {
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
if (params->codec_options) {
ADD_PARAM("codec_options=%s", params->codec_options);
}
if (params->audio_codec_options) {
ADD_PARAM("audio_codec_options=%s", params->audio_codec_options);
}
if (params->video_encoder) {
ADD_PARAM("video_encoder=%s", params->video_encoder);
}
if (params->audio_encoder) {
ADD_PARAM("audio_encoder=%s", params->audio_encoder);
if (params->encoder_name) {
ADD_PARAM("encoder_name=%s", params->encoder_name);
}
if (params->power_off_on_close) {
ADD_PARAM("power_off_on_close=true");
@ -300,12 +256,6 @@ execute_server(struct sc_server *server,
// By default, power_on is true
ADD_PARAM("power_on=false");
}
if (params->list_encoders) {
ADD_PARAM("list_encoders=true");
}
if (params->list_displays) {
ADD_PARAM("list_displays=true");
}
#undef ADD_PARAM
@ -420,7 +370,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params,
server->stopped = false;
server->video_socket = SC_SOCKET_NONE;
server->audio_socket = SC_SOCKET_NONE;
server->control_socket = SC_SOCKET_NONE;
sc_adb_tunnel_init(&server->tunnel);
@ -449,9 +398,10 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
info->frame_size.width = sc_read16be(fields);
info->frame_size.height = sc_read16be(&fields[2]);
info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
return true;
}
@ -464,11 +414,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
const char *serial = server->serial;
assert(serial);
bool audio = server->params.audio;
bool control = server->params.control;
sc_socket video_socket = SC_SOCKET_NONE;
sc_socket audio_socket = SC_SOCKET_NONE;
sc_socket control_socket = SC_SOCKET_NONE;
if (!tunnel->forward) {
video_socket = net_accept_intr(&server->intr, tunnel->server_socket);
@ -476,14 +424,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail;
}
if (audio) {
audio_socket =
net_accept_intr(&server->intr, tunnel->server_socket);
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
}
}
if (control) {
control_socket =
net_accept_intr(&server->intr, tunnel->server_socket);
@ -510,18 +450,6 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
goto fail;
}
if (audio) {
audio_socket = net_socket();
if (audio_socket == SC_SOCKET_NONE) {
goto fail;
}
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host,
tunnel_port);
if (!ok) {
goto fail;
}
}
if (control) {
// we know that the device is listening, we don't need several
// attempts
@ -548,11 +476,9 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
}
assert(video_socket != SC_SOCKET_NONE);
assert(!audio || audio_socket != SC_SOCKET_NONE);
assert(!control || control_socket != SC_SOCKET_NONE);
server->video_socket = video_socket;
server->audio_socket = audio_socket;
server->control_socket = control_socket;
return true;
@ -564,12 +490,6 @@ fail:
}
}
if (audio_socket != SC_SOCKET_NONE) {
if (!net_close(audio_socket)) {
LOGW("Could not close audio socket");
}
}
if (control_socket != SC_SOCKET_NONE) {
if (!net_close(control_socket)) {
LOGW("Could not close control socket");
@ -752,11 +672,6 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server,
if (is_already_tcpip) {
// Nothing to do
LOGI("Device already connected via TCP/IP: %s", serial);
server->serial = strdup(serial);
if (!server->serial) {
LOG_OOM();
return false;
}
return true;
}
@ -854,27 +769,8 @@ run_server(void *data) {
assert(serial);
LOGD("Device serial: %s", serial);
ok = push_server(&server->intr, serial);
if (!ok) {
goto error_connection_failed;
}
// If --list-* is passed, then the server just prints the requested data
// then exits.
if (params->list_encoders || params->list_displays) {
sc_pid pid = execute_server(server, params);
if (pid == SC_PROCESS_NONE) {
goto error_connection_failed;
}
sc_process_wait(pid, NULL); // ignore exit code
sc_process_close(pid);
// Wake up await_for_server()
server->cbs->on_connected(server, server->cbs_userdata);
return 0;
}
int r = asprintf(&server->device_socket_name, SC_SOCKET_NAME_PREFIX "%08x",
params->scid);
params->uid);
if (r == -1) {
LOG_OOM();
goto error_connection_failed;
@ -882,6 +778,11 @@ run_server(void *data) {
assert(r == sizeof(SC_SOCKET_NAME_PREFIX) - 1 + 8);
assert(server->device_socket_name);
ok = push_server(&server->intr, serial);
if (!ok) {
goto error_connection_failed;
}
ok = sc_adb_tunnel_open(&server->tunnel, &server->intr, serial,
server->device_socket_name, params->port_range,
params->force_adb_forward);
@ -934,11 +835,6 @@ run_server(void *data) {
assert(server->video_socket != SC_SOCKET_NONE);
net_interrupt(server->video_socket);
if (server->audio_socket != SC_SOCKET_NONE) {
// There is no audio_socket if --no-audio is set
net_interrupt(server->audio_socket);
}
if (server->control_socket != SC_SOCKET_NONE) {
// There is no control_socket if --no-control is set
net_interrupt(server->control_socket);
@ -991,10 +887,7 @@ sc_server_stop(struct sc_server *server) {
sc_cond_signal(&server->cond_stopped);
sc_intr_interrupt(&server->intr);
sc_mutex_unlock(&server->mutex);
}
void
sc_server_join(struct sc_server *server) {
sc_thread_join(&server->thread, NULL);
}
@ -1003,9 +896,6 @@ sc_server_destroy(struct sc_server *server) {
if (server->video_socket != SC_SOCKET_NONE) {
net_close(server->video_socket);
}
if (server->audio_socket != SC_SOCKET_NONE) {
net_close(server->audio_socket);
}
if (server->control_socket != SC_SOCKET_NONE) {
net_close(server->control_socket);
}

View File

@ -22,27 +22,21 @@ struct sc_server_info {
};
struct sc_server_params {
uint32_t scid;
uint32_t uid;
const char *req_serial;
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
const char *crop;
const char *video_codec_options;
const char *audio_codec_options;
const char *video_encoder;
const char *audio_encoder;
const char *codec_options;
const char *encoder_name;
struct sc_port_range port_range;
uint32_t tunnel_host;
uint16_t tunnel_port;
uint16_t max_size;
uint32_t video_bit_rate;
uint32_t audio_bit_rate;
uint32_t bit_rate;
uint16_t max_fps;
int8_t lock_video_orientation;
bool control;
uint32_t display_id;
bool audio;
bool show_touches;
bool stay_awake;
bool force_adb_forward;
@ -55,8 +49,6 @@ struct sc_server_params {
bool select_tcpip;
bool cleanup;
bool power_on;
bool list_encoders;
bool list_displays;
};
struct sc_server {
@ -76,7 +68,6 @@ struct sc_server {
struct sc_adb_tunnel tunnel;
sc_socket video_socket;
sc_socket audio_socket;
sc_socket control_socket;
const struct sc_server_callbacks *cbs;
@ -116,10 +107,6 @@ sc_server_start(struct sc_server *server);
void
sc_server_stop(struct sc_server *server);
// join the server thread
void
sc_server_join(struct sc_server *server);
// close and release sockets
void
sc_server_destroy(struct sc_server *server);

View File

@ -5,7 +5,6 @@
#include <assert.h>
#include <stdbool.h>
#include <libavcodec/avcodec.h>
typedef struct AVFrame AVFrame;
@ -19,7 +18,7 @@ struct sc_frame_sink {
};
struct sc_frame_sink_ops {
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
bool (*open)(struct sc_frame_sink *sink);
void (*close)(struct sc_frame_sink *sink);
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
};

View File

@ -19,20 +19,9 @@ 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);
void (*close)(struct sc_packet_sink *sink);
bool (*push)(struct sc_packet_sink *sink, const AVPacket *packet);
/*/
* Called when the input stream has been disabled at runtime.
*
* If it is called, then open(), close() and push() will never be called.
*
* It is useful to notify the recorder that the requested audio stream has
* finally been disabled because the device could not capture it.
*/
void (*disable)(struct sc_packet_sink *sink);
};
#endif

View File

@ -22,7 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
(void) userdata;
SDL_Event event;
event.type = SC_EVENT_USB_DEVICE_DISCONNECTED;
event.type = EVENT_USB_DEVICE_DISCONNECTED;
int ret = SDL_PushEvent(&event);
if (ret < 0) {
LOGE("Could not post USB disconnection event: %s", SDL_GetError());
@ -34,7 +34,7 @@ event_loop(struct scrcpy_otg *s) {
SDL_Event event;
while (SDL_WaitEvent(&event)) {
switch (event.type) {
case SC_EVENT_USB_DEVICE_DISCONNECTED:
case EVENT_USB_DEVICE_DISCONNECTED:
LOGW("Device disconnected");
return SCRCPY_EXIT_DISCONNECTED;
case SDL_QUIT:

View File

@ -1,26 +0,0 @@
#include "average.h"
#include <assert.h>
void
sc_average_init(struct sc_average *avg, unsigned range) {
avg->range = range;
avg->avg = 0;
avg->count = 0;
}
void
sc_average_push(struct sc_average *avg, float value) {
if (avg->count < avg->range) {
++avg->count;
}
assert(avg->count);
avg->avg = ((avg->count - 1) * avg->avg + value) / avg->count;
}
bool
sc_average_get(struct sc_average *avg, float *value) {
*value = avg->avg;
return avg->count;
}

View File

@ -1,36 +0,0 @@
#ifndef SC_AVERAGE
#define SC_AVERAGE
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
struct sc_average {
// Current average value
float avg;
// Target range, to update the average as follow:
// avg = ((range - 1) * avg + new_value) / range
unsigned range;
// Number of values pushed when less than range (count <= range).
// The purpose is to handle the first (range - 1) values properly.
unsigned count;
};
void
sc_average_init(struct sc_average *avg, unsigned range);
/* Push a new value to update the "rolling" average */
void
sc_average_push(struct sc_average *avg, float value);
/* Get the current average value (if available)
*
* An average is available if sc_average_push() has been called at least once.
*/
bool
sc_average_get(struct sc_average *avg, float *value);
#endif

View File

@ -1,116 +0,0 @@
#include "bytebuf.h"
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include "util/log.h"
bool
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size) {
assert(alloc_size);
// sufficient, but use more for alignment.
buf->data = malloc(alloc_size);
if (!buf->data) {
LOG_OOM();
return false;
}
buf->alloc_size = alloc_size;
buf->head = 0;
buf->tail = 0;
return true;
}
void
sc_bytebuf_destroy(struct sc_bytebuf *buf) {
free(buf->data);
}
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len) {
assert(len);
assert(sc_bytebuf_read_remaining(buf) >= len);
assert(buf->tail != buf->head); // the buffer could not be empty
size_t right_limit = buf->tail < buf->head ? buf->head : buf->alloc_size;
size_t right_len = right_limit - buf->tail;
if (len < right_len) {
right_len = len;
}
memcpy(to, buf->data + buf->tail, right_len);
if (len > right_len) {
memcpy(to + right_len, buf->data, len - right_len);
}
buf->tail = (buf->tail + len) % buf->alloc_size;
}
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len) {
assert(len);
assert(sc_bytebuf_read_remaining(buf) >= len);
assert(buf->tail != buf->head); // the buffer could not be empty
buf->tail = (buf->tail + len) % buf->alloc_size;
}
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len) {
assert(len);
size_t max_len = buf->alloc_size - 1;
if (len >= max_len) {
// Copy only the right-most bytes
memcpy(buf->data, from + len - max_len, max_len);
buf->tail = 0;
buf->head = max_len;
return;
}
size_t right_limit = buf->head < buf->tail ? buf->tail : buf->alloc_size;
size_t right_len = right_limit - buf->head;
if (len < right_len) {
right_len = len;
}
memcpy(buf->data + buf->head, from, right_len);
if (len > right_len) {
memcpy(buf->data, from + right_len, len - right_len);
}
size_t empty_space = sc_bytebuf_write_remaining(buf);
if (len > empty_space) {
buf->tail = (buf->tail + len - empty_space) % buf->alloc_size;
}
buf->head = (buf->head + len) % buf->alloc_size;
}
void
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
size_t len) {
// *This function MUST NOT access buf->tail (even in assert()).*
// The purpose of this function is to allow a reader and a writer to access
// different parts of the buffer in parallel simultaneously. It is intended
// to be called without lock (only sc_bytebuf_commit_write() is intended to
// be called with lock held).
assert(len < buf->alloc_size - 1);
size_t right_len = buf->alloc_size - buf->head;
if (len < right_len) {
right_len = len;
}
memcpy(buf->data + buf->head, from, right_len);
if (len > right_len) {
memcpy(buf->data, from + right_len, len - right_len);
}
}
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len) {
assert(len <= sc_bytebuf_write_remaining(buf));
buf->head = (buf->head + len) % buf->alloc_size;
}

View File

@ -1,104 +0,0 @@
#ifndef SC_BYTEBUF_H
#define SC_BYTEBUF_H
#include "common.h"
#include <stdbool.h>
#include <stdint.h>
struct sc_bytebuf {
uint8_t *data;
// The actual capacity is (allocated - 1) so that head == tail is
// non-ambiguous
size_t alloc_size;
size_t head;
size_t tail;
// empty: tail == head
// full: (tail + 1) % allocated == head
};
bool
sc_bytebuf_init(struct sc_bytebuf *buf, size_t alloc_size);
/**
* Copy from the bytebuf to a user-provided array
*
* The caller must check that len <= buf->len (it is an error to attempt to read
* more bytes than available).
*
* This function is guaranteed to not change the head.
*/
void
sc_bytebuf_read(struct sc_bytebuf *buf, uint8_t *to, size_t len);
/**
* Drop len bytes from the buffer
*
* The caller must check that len <= buf->len (it is an error to attempt to skip
* more bytes than available).
*
* This function is guaranteed to not change the head.
*
* It is equivalent to call sc_bytebuf_read() to some array and discard the
* array (but more efficient since there is no copy).
*/
void
sc_bytebuf_skip(struct sc_bytebuf *buf, size_t len);
/**
* Copy the user-provided array to the bytebuf
*
* The length of the input array is not restricted:
* if len >= sc_bytebuf_write_remaining(buf), then the excessive input bytes
* will overwrite the oldest bytes in the buffer.
*/
void
sc_bytebuf_write(struct sc_bytebuf *buf, const uint8_t *from, size_t len);
/**
* Copy the user-provided array to the bytebuf, but do not advance the cursor
*
* The caller must check that len <= buf->len (it is an error to attempt to
* write more bytes than available).
*
* After this function is called, the write must be committed with
* sc_bytebuf_commit_write().
*
* The purpose of this mechanism is to acquire a lock only to commit the write,
* but not to perform the actual copy.
*/
void
sc_bytebuf_prepare_write(struct sc_bytebuf *buf, const uint8_t *from,
size_t len);
/**
* Commit a prepared write
*/
void
sc_bytebuf_commit_write(struct sc_bytebuf *buf, size_t len);
/**
* Return the number of bytes which can be read
*
* It is an error to read more bytes than available.
*/
static inline size_t
sc_bytebuf_read_remaining(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->head - buf->tail) % buf->alloc_size;
}
/**
* Return the number of bytes which can be written without overwriting
*
* It is not an error to write more bytes than the available space, but this
* would overwrite the oldest bytes in the buffer.
*/
static inline size_t
sc_bytebuf_write_remaining(struct sc_bytebuf *buf) {
return (buf->alloc_size + buf->tail - buf->head - 1) % buf->alloc_size;
}
void
sc_bytebuf_destroy(struct sc_bytebuf *buf);
#endif

View File

@ -4,7 +4,6 @@
# include <windows.h>
#endif
#include <assert.h>
#include <libavformat/avformat.h>
static SDL_LogPriority
log_level_sc_to_sdl(enum sc_log_level level) {
@ -48,7 +47,6 @@ void
sc_set_log_level(enum sc_log_level level) {
SDL_LogPriority sdl_log = log_level_sc_to_sdl(level);
SDL_LogSetPriority(SDL_LOG_CATEGORY_APPLICATION, sdl_log);
SDL_LogSetPriority(SDL_LOG_CATEGORY_CUSTOM, sdl_log);
}
enum sc_log_level
@ -87,46 +85,3 @@ sc_log_windows_error(const char *prefix, int error) {
return true;
}
#endif
static SDL_LogPriority
sdl_priority_from_av_level(int level) {
switch (level) {
case AV_LOG_PANIC:
case AV_LOG_FATAL:
return SDL_LOG_PRIORITY_CRITICAL;
case AV_LOG_ERROR:
return SDL_LOG_PRIORITY_ERROR;
case AV_LOG_WARNING:
return SDL_LOG_PRIORITY_WARN;
case AV_LOG_INFO:
return SDL_LOG_PRIORITY_INFO;
}
// do not forward others, which are too verbose
return 0;
}
static void
sc_av_log_callback(void *avcl, int level, const char *fmt, va_list vl) {
(void) avcl;
SDL_LogPriority priority = sdl_priority_from_av_level(level);
if (priority == 0) {
return;
}
size_t fmt_len = strlen(fmt);
char *local_fmt = malloc(fmt_len + 10);
if (!local_fmt) {
LOG_OOM();
return;
}
memcpy(local_fmt, "[FFmpeg] ", 9); // do not write the final '\0'
memcpy(local_fmt + 9, fmt, fmt_len + 1); // include '\0'
SDL_LogMessageV(SDL_LOG_CATEGORY_CUSTOM, priority, local_fmt, vl);
free(local_fmt);
}
void
sc_log_configure() {
// Redirect FFmpeg logs to SDL logs
av_log_set_callback(sc_av_log_callback);
}

View File

@ -35,7 +35,4 @@ bool
sc_log_windows_error(const char *prefix, int error);
#endif
void
sc_log_configure();
#endif

View File

@ -30,8 +30,8 @@ bool
net_init(void) {
#ifdef _WIN32
WSADATA wsa;
int res = WSAStartup(MAKEWORD(1, 1), &wsa);
if (res) {
int res = WSAStartup(MAKEWORD(2, 2), &wsa) < 0;
if (res < 0) {
LOGE("WSAStartup failed with error %d", res);
return false;
}

View File

@ -156,10 +156,7 @@ sc_video_buffer_on_new_frame(struct sc_video_buffer *vb, bool previous_skipped,
}
static bool
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
(void) ctx;
sc_v4l2_sink_open(struct sc_v4l2_sink *vs) {
static const struct sc_video_buffer_callbacks cbs = {
.on_new_frame = sc_video_buffer_on_new_frame,
};
@ -339,9 +336,9 @@ sc_v4l2_sink_push(struct sc_v4l2_sink *vs, const AVFrame *frame) {
}
static bool
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) {
sc_v4l2_frame_sink_open(struct sc_frame_sink *sink) {
struct sc_v4l2_sink *vs = DOWNCAST(sink);
return sc_v4l2_sink_open(vs, ctx);
return sc_v4l2_sink_open(vs);
}
static void

View File

@ -1,154 +0,0 @@
#include "common.h"
#include <assert.h>
#include <string.h>
#include "util/bytebuf.h"
void test_bytebuf_simple(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello", sizeof("hello") - 1);
assert(sc_bytebuf_read_remaining(&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_remaining(&buf) == 7);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_read_remaining(&buf) == 8);
sc_bytebuf_read(&buf, &data[4], 8);
assert(sc_bytebuf_read_remaining(&buf) == 0);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_read_remaining(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
void test_bytebuf_boundaries(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 12);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 18);
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 9));
assert(sc_bytebuf_read_remaining(&buf) == 9);
sc_bytebuf_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_read_remaining(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_read_remaining(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_read_remaining(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
void test_bytebuf_overwrite(void) {
struct sc_bytebuf buf;
uint8_t data[10];
bool ok = sc_bytebuf_init(&buf, 10); // so actual capacity is 9
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "abcdef", sizeof("abcdef") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 9);
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "lo abcdef", 9));
sc_bytebuf_write(&buf, (uint8_t *) "a very big buffer",
sizeof("a very big buffer") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 9);
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "ig buffer", 9));
assert(sc_bytebuf_read_remaining(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
void test_bytebuf_two_steps_write(void) {
struct sc_bytebuf buf;
uint8_t data[20];
bool ok = sc_bytebuf_init(&buf, 20);
assert(ok);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 6);
sc_bytebuf_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 12);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "hello ", sizeof("hello ") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 12); // write not committed yet
sc_bytebuf_read(&buf, data, 9);
assert(!strncmp((char *) data, "hello hel", 3));
assert(sc_bytebuf_read_remaining(&buf) == 3);
sc_bytebuf_commit_write(&buf, sizeof("hello ") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 9);
sc_bytebuf_prepare_write(&buf, (uint8_t *) "world", sizeof("world") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 9); // write not committed yet
sc_bytebuf_commit_write(&buf, sizeof("world") - 1);
assert(sc_bytebuf_read_remaining(&buf) == 14);
sc_bytebuf_write(&buf, (uint8_t *) "!", 1);
assert(sc_bytebuf_read_remaining(&buf) == 15);
sc_bytebuf_skip(&buf, 3);
assert(sc_bytebuf_read_remaining(&buf) == 12);
sc_bytebuf_read(&buf, data, 12);
data[12] = '\0';
assert(!strcmp((char *) data, "hello world!"));
assert(sc_bytebuf_read_remaining(&buf) == 0);
sc_bytebuf_destroy(&buf);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_bytebuf_simple();
test_bytebuf_boundaries();
test_bytebuf_overwrite();
test_bytebuf_two_steps_write();
return 0;
}

View File

@ -46,7 +46,7 @@ static void test_options(void) {
char *argv[] = {
"scrcpy",
"--always-on-top",
"--video-bit-rate", "5M",
"--bit-rate", "5M",
"--crop", "100:200:300:400",
"--fullscreen",
"--max-fps", "30",
@ -75,7 +75,7 @@ static void test_options(void) {
const struct scrcpy_options *opts = &args.opts;
assert(opts->always_on_top);
assert(opts->video_bit_rate == 5000000);
assert(opts->bit_rate == 5000000);
assert(!strcmp(opts->crop, "100:200:300:400"));
assert(opts->fullscreen);
assert(opts->max_fps == 30);

View File

@ -90,14 +90,13 @@ static void test_serialize_inject_touch_event(void) {
},
},
.pressure = 1.0f,
.action_button = AMOTION_EVENT_BUTTON_PRIMARY,
.buttons = AMOTION_EVENT_BUTTON_PRIMARY,
},
};
unsigned char buf[SC_CONTROL_MSG_MAX_SIZE];
size_t size = sc_control_msg_serialize(&msg, buf);
assert(size == 32);
assert(size == 28);
const unsigned char expected[] = {
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
@ -106,8 +105,7 @@ static void test_serialize_inject_touch_event(void) {
0x00, 0x00, 0x00, 0x64, 0x00, 0x00, 0x00, 0xc8, // 100 200
0x04, 0x38, 0x07, 0x80, // 1080 1920
0xff, 0xff, // pressure
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
0x00, 0x00, 0x00, 0x01 // AMOTION_EVENT_BUTTON_PRIMARY
};
assert(!memcmp(buf, expected, sizeof(expected)));
}

View File

@ -7,7 +7,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.4.0'
classpath 'com.android.tools.build:gradle:7.2.2'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -16,6 +16,10 @@ cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win32'
ffmpeg_avcodec = 'avcodec-58'
ffmpeg_avformat = 'avformat-58'
ffmpeg_avutil = 'avutil-56'
prebuilt_ffmpeg = 'ffmpeg-win32-4.3.1'
prebuilt_sdl2 = 'SDL2-2.26.1/i686-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
prebuilt_libusb_root = 'libusb-1.0.26'
prebuilt_libusb = 'libusb-1.0.26/MinGW-Win32'

View File

@ -16,6 +16,10 @@ cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy/win64'
ffmpeg_avcodec = 'avcodec-59'
ffmpeg_avformat = 'avformat-59'
ffmpeg_avutil = 'avutil-57'
prebuilt_ffmpeg = 'ffmpeg-win64-5.1.2'
prebuilt_sdl2 = 'SDL2-2.26.1/x86_64-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
prebuilt_libusb_root = 'libusb-1.0.26'
prebuilt_libusb = 'libusb-1.0.26/MinGW-x64'

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-all.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -11,7 +11,7 @@
.PHONY: default clean \
test \
build-server \
prepare-deps \
prepare-deps-win32 prepare-deps-win64 \
build-win32 build-win64 \
dist-win32 dist-win64 \
zip-win32 zip-win64 \
@ -62,13 +62,19 @@ build-server:
meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false )
ninja -C "$(SERVER_BUILD_DIR)"
prepare-deps:
prepare-deps-win32:
@app/prebuilt-deps/prepare-adb.sh
@app/prebuilt-deps/prepare-sdl.sh
@app/prebuilt-deps/prepare-ffmpeg.sh
@app/prebuilt-deps/prepare-ffmpeg-win32.sh
@app/prebuilt-deps/prepare-libusb.sh
build-win32: prepare-deps
prepare-deps-win64:
@app/prebuilt-deps/prepare-adb.sh
@app/prebuilt-deps/prepare-sdl.sh
@app/prebuilt-deps/prepare-ffmpeg-win64.sh
@app/prebuilt-deps/prepare-libusb.sh
build-win32: prepare-deps-win32
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \
meson setup "$(WIN32_BUILD_DIR)" \
--cross-file cross_win32.txt \
@ -77,7 +83,7 @@ build-win32: prepare-deps
-Dportable=true )
ninja -C "$(WIN32_BUILD_DIR)"
build-win64: prepare-deps
build-win64: prepare-deps-win64
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \
meson setup "$(WIN64_BUILD_DIR)" \
--cross-file cross_win64.txt \
@ -94,16 +100,16 @@ dist-win32: build-server build-win32
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avutil-56.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avcodec-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/avformat-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swresample-3.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win32-4.3.1/bin/swscale-5.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/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-Win32/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
@ -113,16 +119,16 @@ dist-win64: build-server build-win64
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avutil-57.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avcodec-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/avformat-59.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-win64-5.1.2/bin/swscale-6.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/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/MinGW-x64/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32
cd "$(DIST)"; \

View File

@ -1,7 +1,6 @@
apply plugin: 'com.android.application'
android {
namespace 'com.genymobile.scrcpy'
compileSdkVersion 33
defaultConfig {
applicationId "com.genymobile.scrcpy"

View File

@ -1,2 +1,2 @@
<!-- not a real Android application, it is run by app_process manually -->
<manifest />
<manifest package="com.genymobile.scrcpy"/>

View File

@ -1,47 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaFormat;
public enum AudioCodec implements Codec {
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC);
private final int id; // 4-byte ASCII representation of the name
private final String name;
private final String mimeType;
AudioCodec(int id, String name, String mimeType) {
this.id = id;
this.name = name;
this.mimeType = mimeType;
}
@Override
public Type getType() {
return Type.AUDIO;
}
@Override
public int getId() {
return id;
}
@Override
public String getName() {
return name;
}
@Override
public String getMimeType() {
return mimeType;
}
public static AudioCodec findByName(String name) {
for (AudioCodec codec : values()) {
if (codec.name.equals(name)) {
return codec;
}
}
return null;
}
}

View File

@ -1,422 +0,0 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Looper;
import android.os.SystemClock;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
public final class AudioEncoder {
private static class InputTask {
private final int index;
InputTask(int index) {
this.index = index;
}
}
private static class OutputTask {
private final int index;
private final MediaCodec.BufferInfo bufferInfo;
OutputTask(int index, MediaCodec.BufferInfo bufferInfo) {
this.index = index;
this.bufferInfo = bufferInfo;
}
}
private static final int SAMPLE_RATE = 48000;
private static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
private static final int CHANNELS = 2;
private static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT;
private static final int BYTES_PER_SAMPLE = 2;
private static final int BUFFER_MS = 5; // milliseconds
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * BUFFER_MS / 1000;
private final Streamer streamer;
private final int bitRate;
private final List<CodecOption> codecOptions;
private final String encoderName;
// Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4).
// So many pending tasks would lead to an unacceptable delay anyway.
private final BlockingQueue<InputTask> inputTasks = new ArrayBlockingQueue<>(64);
private final BlockingQueue<OutputTask> outputTasks = new ArrayBlockingQueue<>(64);
private Thread thread;
private HandlerThread mediaCodecThread;
private Thread inputThread;
private Thread outputThread;
private boolean ended;
public AudioEncoder(Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
this.streamer = streamer;
this.bitRate = bitRate;
this.codecOptions = codecOptions;
this.encoderName = encoderName;
}
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(FORMAT);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
}
@TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() {
AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get());
}
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX);
builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT);
builder.setBufferSizeInBytes(minBufferSize);
return builder.build();
}
private static MediaFormat createFormat(String mimeType, int bitRate, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, mimeType);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, CHANNELS);
format.setInteger(MediaFormat.KEY_SAMPLE_RATE, SAMPLE_RATE);
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
String key = option.getKey();
Object value = option.getValue();
CodecUtils.setCodecOption(format, key, value);
Ln.d("Audio codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
}
return format;
}
@TargetApi(Build.VERSION_CODES.N)
private void inputThread(MediaCodec mediaCodec, AudioRecord recorder) throws IOException, InterruptedException {
final AudioTimestamp timestamp = new AudioTimestamp();
long previousPts = 0;
long nextPts = 0;
while (!Thread.currentThread().isInterrupted()) {
InputTask task = inputTasks.take();
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
int r = recorder.read(buffer, BUFFER_SIZE);
if (r < 0) {
throw new IOException("Could not read audio: " + r);
}
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS) {
pts = timestamp.nanoTime / 1000;
} else {
if (nextPts == 0) {
Ln.w("Could not get any audio timestamp");
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationMs = r * 1000 / CHANNELS / SAMPLE_RATE;
nextPts = pts + durationMs;
if (previousPts != 0 && pts < previousPts) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + 1;
}
mediaCodec.queueInputBuffer(task.index, 0, r, pts, 0);
previousPts = pts;
}
}
private void outputThread(MediaCodec mediaCodec) throws IOException, InterruptedException {
streamer.writeHeader();
while (!Thread.currentThread().isInterrupted()) {
OutputTask task = outputTasks.take();
ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index);
try {
streamer.writePacket(buffer, task.bufferInfo);
} finally {
mediaCodec.releaseOutputBuffer(task.index, false);
}
}
}
public void start() {
thread = new Thread(() -> {
try {
encode();
} catch (IOException e) {
Ln.e("Audio encoding error", e);
} finally {
Ln.d("Audio encoder stopped");
}
});
thread.start();
}
public void stop() {
if (thread != null) {
// Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates
end();
}
}
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
}
private synchronized void end() {
ended = true;
notify();
}
private synchronized void waitEnded() {
try {
while (!ended) {
wait();
}
} catch (InterruptedException e) {
// ignore
}
}
private static void startWorkaroundAndroid11() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// Android 11 requires Apps to be at foreground to record audio.
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
// But Scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
// shell ("com.android.shell").
// If there is an Activity from Android shell running at foreground, then the permission system will believe Scrcpy is also in the
// foreground.
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
// Wait for activity to start
SystemClock.sleep(150);
}
}
}
private static void stopWorkaroundAndroid11() {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
}
@TargetApi(Build.VERSION_CODES.M)
public void encode() throws IOException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
}
MediaCodec mediaCodec = null;
AudioRecord recorder = null;
boolean mediaCodecStarted = false;
boolean recorderStarted = false;
boolean configurationError = false;
try {
Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName);
mediaCodecThread = new HandlerThread("AudioEncoder");
mediaCodecThread.start();
MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions);
mediaCodec.setCallback(new EncoderCallback(), new Handler(mediaCodecThread.getLooper()));
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
startWorkaroundAndroid11();
try {
recorder = createAudioRecord();
recorder.startRecording();
} catch (UnsupportedOperationException e) {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
throw new ConfigurationException("Unsupported audio capture");
}
} finally {
stopWorkaroundAndroid11();
}
recorderStarted = true;
final MediaCodec mediaCodecRef = mediaCodec;
final AudioRecord recorderRef = recorder;
inputThread = new Thread(() -> {
try {
inputThread(mediaCodecRef, recorderRef);
} catch (IOException | InterruptedException e) {
Ln.e("Audio capture error", e);
} finally {
end();
}
});
outputThread = new Thread(() -> {
try {
outputThread(mediaCodecRef);
} catch (InterruptedException e) {
// this is expected on close
} catch (IOException e) {
// Broken pipe is expected on close, because the socket is closed by the client
if (!IO.isBrokenPipe(e)) {
Ln.e("Audio encoding error", e);
}
} finally {
end();
}
});
mediaCodec.start();
mediaCodecStarted = true;
inputThread.start();
outputThread.start();
waitEnded();
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
// Notify the error to scrcpy to make it exit
configurationError = true;
} finally {
if (!recorderStarted) {
// Notify the client that the audio could not be captured
streamer.writeDisableStream(configurationError);
}
// Cleanup everything (either at the end or on error at any step of the initialization)
if (mediaCodecThread != null) {
Looper looper = mediaCodecThread.getLooper();
if (looper != null) {
looper.quitSafely();
}
}
if (inputThread != null) {
inputThread.interrupt();
}
if (outputThread != null) {
outputThread.interrupt();
}
try {
if (mediaCodecThread != null) {
mediaCodecThread.join();
}
if (inputThread != null) {
inputThread.join();
}
if (outputThread != null) {
outputThread.join();
}
} catch (InterruptedException e) {
// Should never happen
throw new AssertionError(e);
}
if (mediaCodec != null) {
if (mediaCodecStarted) {
mediaCodec.stop();
}
mediaCodec.release();
}
if (recorder != null) {
if (recorderStarted) {
recorder.stop();
}
recorder.release();
}
}
}
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
if (encoderName != null) {
Ln.d("Creating audio encoder by name: '" + encoderName + "'");
try {
return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) {
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName);
}
}
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
}
private class EncoderCallback extends MediaCodec.Callback {
@TargetApi(Build.VERSION_CODES.N)
@Override
public void onInputBufferAvailable(MediaCodec codec, int index) {
try {
inputTasks.put(new InputTask(index));
} catch (InterruptedException e) {
end();
}
}
@Override
public void onOutputBufferAvailable(MediaCodec codec, int index, MediaCodec.BufferInfo bufferInfo) {
try {
outputTasks.put(new OutputTask(index, bufferInfo));
} catch (InterruptedException e) {
end();
}
}
@Override
public void onError(MediaCodec codec, MediaCodec.CodecException e) {
Ln.e("MediaCodec error", e);
end();
}
@Override
public void onOutputFormatChanged(MediaCodec codec, MediaFormat format) {
// ignore
}
}
}

View File

@ -139,7 +139,7 @@ public final class CleanUp {
builder.start();
}
public static void unlinkSelf() {
private static void unlinkSelf() {
try {
new File(SERVER_PATH).delete();
} catch (Exception e) {

View File

@ -1,17 +0,0 @@
package com.genymobile.scrcpy;
public interface Codec {
enum Type {
VIDEO,
AUDIO,
}
Type getType();
int getId();
String getName();
String getMimeType();
}

View File

@ -1,78 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public final class CodecUtils {
public static final class DeviceEncoder {
private final Codec codec;
private final MediaCodecInfo info;
DeviceEncoder(Codec codec, MediaCodecInfo info) {
this.codec = codec;
this.info = info;
}
public Codec getCodec() {
return codec;
}
public MediaCodecInfo getInfo() {
return info;
}
}
private CodecUtils() {
// not instantiable
}
public static void setCodecOption(MediaFormat format, String key, Object value) {
if (value instanceof Integer) {
format.setInteger(key, (Integer) value);
} else if (value instanceof Long) {
format.setLong(key, (Long) value);
} else if (value instanceof Float) {
format.setFloat(key, (Float) value);
} else if (value instanceof String) {
format.setString(key, (String) value);
}
}
private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) {
List<MediaCodecInfo> result = new ArrayList<>();
for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) {
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) {
result.add(codecInfo);
}
}
return result.toArray(new MediaCodecInfo[result.size()]);
}
public static List<DeviceEncoder> listVideoEncoders() {
List<DeviceEncoder> encoders = new ArrayList<>();
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (VideoCodec codec : VideoCodec.values()) {
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) {
encoders.add(new DeviceEncoder(codec, info));
}
}
return encoders;
}
public static List<DeviceEncoder> listAudioEncoders() {
List<DeviceEncoder> encoders = new ArrayList<>();
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (AudioCodec codec : AudioCodec.values()) {
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) {
encoders.add(new DeviceEncoder(codec, info));
}
}
return encoders;
}
}

View File

@ -1,7 +0,0 @@
package com.genymobile.scrcpy;
public class ConfigurationException extends Exception {
public ConfigurationException(String message) {
super(message);
}
}

View File

@ -29,7 +29,6 @@ public final class ControlMessage {
private int metaState; // KeyEvent.META_*
private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_*
private int keycode; // KeyEvent.KEYCODE_*
private int actionButton; // MotionEvent.BUTTON_*
private int buttons; // MotionEvent.BUTTON_*
private long pointerId;
private float pressure;
@ -61,15 +60,13 @@ public final class ControlMessage {
return msg;
}
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton,
int buttons) {
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
ControlMessage msg = new ControlMessage();
msg.type = TYPE_INJECT_TOUCH_EVENT;
msg.action = action;
msg.pointerId = pointerId;
msg.pressure = pressure;
msg.position = position;
msg.actionButton = actionButton;
msg.buttons = buttons;
return msg;
}
@ -143,10 +140,6 @@ public final class ControlMessage {
return keycode;
}
public int getActionButton() {
return actionButton;
}
public int getButtons() {
return buttons;
}

View File

@ -9,7 +9,7 @@ import java.nio.charset.StandardCharsets;
public class ControlMessageReader {
static final int INJECT_KEYCODE_PAYLOAD_LENGTH = 13;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 27;
static final int INJECT_SCROLL_EVENT_PAYLOAD_LENGTH = 20;
static final int BACK_OR_SCREEN_ON_LENGTH = 1;
static final int SET_SCREEN_POWER_MODE_PAYLOAD_LENGTH = 1;
@ -140,9 +140,8 @@ public class ControlMessageReader {
long pointerId = buffer.getLong();
Position position = readPosition(buffer);
float pressure = Binary.u16FixedPointToFloat(buffer.getShort());
int actionButton = buffer.getInt();
int buttons = buffer.getInt();
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, buttons);
}
private ControlMessage parseInjectScrollEvent() {

View File

@ -1,7 +1,5 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.InputManager;
import android.os.Build;
import android.os.SystemClock;
import android.view.InputDevice;
@ -24,8 +22,6 @@ public class Controller {
private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor();
private Thread thread;
private final Device device;
private final DesktopConnection connection;
private final DeviceMessageSender sender;
@ -64,7 +60,7 @@ public class Controller {
}
}
private void control() throws IOException {
public void control() throws IOException {
// on start, power on the device
if (powerOn && !Device.isScreenOn()) {
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
@ -84,34 +80,6 @@ public class Controller {
}
}
public void start() {
thread = new Thread(() -> {
try {
control();
} catch (IOException e) {
// this is expected on close
} finally {
Ln.d("Controller stopped");
}
});
thread.start();
sender.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
}
sender.stop();
}
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
sender.join();
}
public DeviceMessageSender getSender() {
return sender;
}
@ -131,7 +99,7 @@ public class Controller {
break;
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
if (device.supportsInputEvents()) {
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
}
break;
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
@ -211,7 +179,7 @@ public class Controller {
return successCount;
}
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
long now = SystemClock.uptimeMillis();
Point point = device.getPhysicalPoint(position);
@ -228,23 +196,22 @@ public class Controller {
Pointer pointer = pointersState.get(pointerIndex);
pointer.setPoint(point);
pointer.setPressure(pressure);
pointer.setUp(action == MotionEvent.ACTION_UP);
int source;
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerId == POINTER_ID_MOUSE || pointerId == POINTER_ID_VIRTUAL_MOUSE) {
// real mouse event (forced by the client when --forward-on-click)
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_MOUSE;
source = InputDevice.SOURCE_MOUSE;
pointer.setUp(buttons == 0);
} else {
// POINTER_ID_GENERIC_FINGER, POINTER_ID_VIRTUAL_FINGER or real touch from device
pointerProperties[pointerIndex].toolType = MotionEvent.TOOL_TYPE_FINGER;
source = InputDevice.SOURCE_TOUCHSCREEN;
// Buttons must not be set for touch events
buttons = 0;
pointer.setUp(action == MotionEvent.ACTION_UP);
}
int pointerCount = pointersState.update(pointerProperties, pointerCoords);
if (pointerCount == 1) {
if (action == MotionEvent.ACTION_DOWN) {
lastTouchDown = now;
@ -258,62 +225,6 @@ public class Controller {
}
}
/* If the input device is a mouse (on API >= 23):
* - the first button pressed must first generate ACTION_DOWN;
* - all button pressed (including the first one) must generate ACTION_BUTTON_PRESS;
* - all button released (including the last one) must generate ACTION_BUTTON_RELEASE;
* - the last button released must in addition generate ACTION_UP.
*
* Otherwise, Chrome does not work properly: <https://github.com/Genymobile/scrcpy/issues/3635>
*/
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) {
if (action == MotionEvent.ACTION_DOWN) {
if (actionButton == buttons) {
// First button pressed: ACTION_DOWN
MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
// Any button pressed: ACTION_BUTTON_PRESS
MotionEvent pressEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_PRESS, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!InputManager.setActionButton(pressEvent, actionButton)) {
return false;
}
if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
return true;
}
if (action == MotionEvent.ACTION_UP) {
// Any button released: ACTION_BUTTON_RELEASE
MotionEvent releaseEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_BUTTON_RELEASE, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!InputManager.setActionButton(releaseEvent, actionButton)) {
return false;
}
if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
if (buttons == 0) {
// Last button released: ACTION_UP
MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties,
pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0);
if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) {
return false;
}
}
return true;
}
}
MotionEvent event = MotionEvent
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source,
0);

View File

@ -20,9 +20,6 @@ public final class DesktopConnection implements Closeable {
private final LocalSocket videoSocket;
private final FileDescriptor videoFd;
private final LocalSocket audioSocket;
private final FileDescriptor audioFd;
private final LocalSocket controlSocket;
private final InputStream controlInputStream;
private final OutputStream controlOutputStream;
@ -30,10 +27,9 @@ public final class DesktopConnection implements Closeable {
private final ControlMessageReader reader = new ControlMessageReader();
private final DeviceMessageWriter writer = new DeviceMessageWriter();
private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException {
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
this.videoSocket = videoSocket;
this.controlSocket = controlSocket;
this.audioSocket = audioSocket;
if (controlSocket != null) {
controlInputStream = controlSocket.getInputStream();
controlOutputStream = controlSocket.getOutputStream();
@ -42,7 +38,6 @@ public final class DesktopConnection implements Closeable {
controlOutputStream = null;
}
videoFd = videoSocket.getFileDescriptor();
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
}
private static LocalSocket connect(String abstractName) throws IOException {
@ -51,59 +46,49 @@ public final class DesktopConnection implements Closeable {
return localSocket;
}
private static String getSocketName(int scid) {
if (scid == -1) {
// If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone
private static String getSocketName(int uid) {
if (uid == -1) {
// If no UID is set, use "scrcpy" to simplify using scrcpy-server alone
return SOCKET_NAME_PREFIX;
}
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
return SOCKET_NAME_PREFIX + String.format("_%08x", uid);
}
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(scid);
public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
String socketName = getSocketName(uid);
LocalSocket videoSocket = null;
LocalSocket audioSocket = null;
LocalSocket videoSocket;
LocalSocket controlSocket = null;
try {
if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
videoSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
}
if (audio) {
audioSocket = localServerSocket.accept();
}
if (control) {
controlSocket = localServerSocket.accept();
}
}
} else {
videoSocket = connect(socketName);
if (audio) {
audioSocket = connect(socketName);
if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
videoSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
}
if (control) {
controlSocket = connect(socketName);
try {
controlSocket = localServerSocket.accept();
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
}
}
} catch (IOException | RuntimeException e) {
if (videoSocket != null) {
videoSocket.close();
} else {
videoSocket = connect(socketName);
if (control) {
try {
controlSocket = connect(socketName);
} catch (IOException | RuntimeException e) {
videoSocket.close();
throw e;
}
}
if (audioSocket != null) {
audioSocket.close();
}
if (controlSocket != null) {
controlSocket.close();
}
throw e;
}
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
return new DesktopConnection(videoSocket, controlSocket);
}
public void close() throws IOException {
@ -136,10 +121,6 @@ public final class DesktopConnection implements Closeable {
return videoFd;
}
public FileDescriptor getAudioFd() {
return audioFd;
}
public ControlMessage receiveControlMessage() throws IOException {
ControlMessage msg = reader.next();
while (msg == null) {

View File

@ -61,12 +61,12 @@ public final class Device {
private final boolean supportsInputEvents;
public Device(Options options) throws ConfigurationException {
public Device(Options options) {
displayId = options.getDisplayId();
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
throw new ConfigurationException("Unknown display id: " + displayId);
int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds();
throw new InvalidDisplayIdException(displayId, displayIds);
}
int displayInfoFlags = displayInfo.getFlags();
@ -277,26 +277,6 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Change the power mode for all physical displays
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) {
Ln.e("Could not get physical display ids");
return false;
}
boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
if (!ok) {
allOk = false;
}
}
return allOk;
}
// Older Android versions, only 1 display
IBinder d = SurfaceControl.getBuiltInDisplay();
if (d == null) {
Ln.e("Could not get built-in display");

View File

@ -6,8 +6,6 @@ public final class DeviceMessageSender {
private final DesktopConnection connection;
private Thread thread;
private String clipboardText;
private long ack;
@ -26,7 +24,7 @@ public final class DeviceMessageSender {
notify();
}
private void loop() throws IOException, InterruptedException {
public void loop() throws IOException, InterruptedException {
while (!Thread.currentThread().isInterrupted()) {
String text;
long sequence;
@ -51,28 +49,4 @@ public final class DeviceMessageSender {
}
}
}
public void start() {
thread = new Thread(() -> {
try {
loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
} finally {
Ln.d("Device message sender stopped");
}
});
thread.start();
}
public void stop() {
if (thread != null) {
thread.interrupt();
}
}
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
}
}
}

View File

@ -1,41 +0,0 @@
package com.genymobile.scrcpy;
import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.Process;
public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
private static final FakeContext INSTANCE = new FakeContext();
public static FakeContext get() {
return INSTANCE;
}
private FakeContext() {
super(null);
}
@Override
public String getPackageName() {
return PACKAGE_NAME;
}
@Override
public String getOpPackageName() {
return PACKAGE_NAME;
}
@TargetApi(Build.VERSION_CODES.S)
@Override
public AttributionSource getAttributionSource() {
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID);
builder.setPackageName(PACKAGE_NAME);
return builder.build();
}
}

View File

@ -48,9 +48,4 @@ public final class IO {
}
return builder.toString();
}
public static boolean isBrokenPipe(IOException e) {
Throwable cause = e.getCause();
return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE;
}
}

View File

@ -0,0 +1,21 @@
package com.genymobile.scrcpy;
public class InvalidDisplayIdException extends RuntimeException {
private final int displayId;
private final int[] availableDisplayIds;
public InvalidDisplayIdException(int displayId, int[] availableDisplayIds) {
super("There is no display having id " + displayId);
this.displayId = displayId;
this.availableDisplayIds = availableDisplayIds;
}
public int getDisplayId() {
return displayId;
}
public int[] getAvailableDisplayIds() {
return availableDisplayIds;
}
}

View File

@ -0,0 +1,23 @@
package com.genymobile.scrcpy;
import android.media.MediaCodecInfo;
public class InvalidEncoderException extends RuntimeException {
private final String name;
private final MediaCodecInfo[] availableEncoders;
public InvalidEncoderException(String name, MediaCodecInfo[] availableEncoders) {
super("There is no encoder having name '" + name + '"');
this.name = name;
this.availableEncoders = availableEncoders;
}
public String getName() {
return name;
}
public MediaCodecInfo[] getAvailableEncoders() {
return availableEncoders;
}
}

View File

@ -1,63 +0,0 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import java.util.List;
public final class LogUtils {
private LogUtils() {
// not instantiable
}
public static String buildVideoEncoderListMessage() {
StringBuilder builder = new StringBuilder("List of video encoders:");
List<CodecUtils.DeviceEncoder> videoEncoders = CodecUtils.listVideoEncoders();
if (videoEncoders.isEmpty()) {
builder.append("\n (none)");
} else {
for (CodecUtils.DeviceEncoder encoder : videoEncoders) {
builder.append("\n --video-codec=").append(encoder.getCodec().getName());
builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'");
}
}
return builder.toString();
}
public static String buildAudioEncoderListMessage() {
StringBuilder builder = new StringBuilder("List of audio encoders:");
List<CodecUtils.DeviceEncoder> audioEncoders = CodecUtils.listAudioEncoders();
if (audioEncoders.isEmpty()) {
builder.append("\n (none)");
} else {
for (CodecUtils.DeviceEncoder encoder : audioEncoders) {
builder.append("\n --audio-codec=").append(encoder.getCodec().getName());
builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'");
}
}
return builder.toString();
}
public static String buildDisplayListMessage() {
StringBuilder builder = new StringBuilder("List of displays:");
DisplayManager displayManager = ServiceManager.getDisplayManager();
int[] displayIds = displayManager.getDisplayIds();
if (displayIds == null || displayIds.length == 0) {
builder.append("\n (none)");
} else {
for (int id : displayIds) {
builder.append("\n --display=").append(id).append(" (");
DisplayInfo displayInfo = displayManager.getDisplayInfo(id);
if (displayInfo != null) {
Size size = displayInfo.getSize();
builder.append(size.getWidth()).append("x").append(size.getHeight());
} else {
builder.append("size unknown");
}
builder.append(")");
}
}
return builder.toString();
}
}

View File

@ -5,15 +5,10 @@ import android.graphics.Rect;
import java.util.List;
public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG;
private int scid = -1; // 31-bit non-negative value, or -1
private boolean audio = true;
private int uid = -1; // 31-bit non-negative value, or -1
private int maxSize;
private VideoCodec videoCodec = VideoCodec.H264;
private AudioCodec audioCodec = AudioCodec.OPUS;
private int videoBitRate = 8000000;
private int audioBitRate = 196000;
private int bitRate = 8000000;
private int maxFps;
private int lockVideoOrientation = -1;
private boolean tunnelForward;
@ -22,25 +17,18 @@ public class Options {
private int displayId;
private boolean showTouches;
private boolean stayAwake;
private List<CodecOption> videoCodecOptions;
private List<CodecOption> audioCodecOptions;
private String videoEncoder;
private String audioEncoder;
private List<CodecOption> codecOptions;
private String encoderName;
private boolean powerOffScreenOnClose;
private boolean clipboardAutosync = true;
private boolean downsizeOnError = true;
private boolean cleanup = true;
private boolean powerOn = true;
private boolean listEncoders;
private boolean listDisplays;
// Options not used by the scrcpy client, but useful to use scrcpy-server directly
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
public Ln.Level getLogLevel() {
return logLevel;
@ -50,20 +38,12 @@ public class Options {
this.logLevel = logLevel;
}
public int getScid() {
return scid;
public int getUid() {
return uid;
}
public void setScid(int scid) {
this.scid = scid;
}
public boolean getAudio() {
return audio;
}
public void setAudio(boolean audio) {
this.audio = audio;
public void setUid(int uid) {
this.uid = uid;
}
public int getMaxSize() {
@ -74,36 +54,12 @@ public class Options {
this.maxSize = maxSize;
}
public VideoCodec getVideoCodec() {
return videoCodec;
public int getBitRate() {
return bitRate;
}
public void setVideoCodec(VideoCodec videoCodec) {
this.videoCodec = videoCodec;
}
public AudioCodec getAudioCodec() {
return audioCodec;
}
public void setAudioCodec(AudioCodec audioCodec) {
this.audioCodec = audioCodec;
}
public int getVideoBitRate() {
return videoBitRate;
}
public void setVideoBitRate(int videoBitRate) {
this.videoBitRate = videoBitRate;
}
public int getAudioBitRate() {
return audioBitRate;
}
public void setAudioBitRate(int audioBitRate) {
this.audioBitRate = audioBitRate;
public void setBitRate(int bitRate) {
this.bitRate = bitRate;
}
public int getMaxFps() {
@ -170,36 +126,20 @@ public class Options {
this.stayAwake = stayAwake;
}
public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions;
public List<CodecOption> getCodecOptions() {
return codecOptions;
}
public void setVideoCodecOptions(List<CodecOption> videoCodecOptions) {
this.videoCodecOptions = videoCodecOptions;
public void setCodecOptions(List<CodecOption> codecOptions) {
this.codecOptions = codecOptions;
}
public List<CodecOption> getAudioCodecOptions() {
return audioCodecOptions;
public String getEncoderName() {
return encoderName;
}
public void setAudioCodecOptions(List<CodecOption> audioCodecOptions) {
this.audioCodecOptions = audioCodecOptions;
}
public String getVideoEncoder() {
return videoEncoder;
}
public void setVideoEncoder(String videoEncoder) {
this.videoEncoder = videoEncoder;
}
public String getAudioEncoder() {
return audioEncoder;
}
public void setAudioEncoder(String audioEncoder) {
this.audioEncoder = audioEncoder;
public void setEncoderName(String encoderName) {
this.encoderName = encoderName;
}
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
@ -242,22 +182,6 @@ public class Options {
this.powerOn = powerOn;
}
public boolean getListEncoders() {
return listEncoders;
}
public void setListEncoders(boolean listEncoders) {
this.listEncoders = listEncoders;
}
public boolean getListDisplays() {
return listDisplays;
}
public void setListDisplays(boolean listDisplays) {
this.listDisplays = listDisplays;
}
public boolean getSendDeviceMeta() {
return sendDeviceMeta;
}
@ -281,12 +205,4 @@ public class Options {
public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte;
}
public boolean getSendCodecId() {
return sendCodecId;
}
public void setSendCodecId(boolean sendCodecId) {
this.sendCodecId = sendCodecId;
}
}

View File

@ -5,14 +5,18 @@ import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;
import android.media.MediaFormat;
import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.Surface;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
@ -24,26 +28,27 @@ public class ScreenEncoder implements Device.RotationListener {
// Keep the values in descending order
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final int MAX_CONSECUTIVE_ERRORS = 3;
private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
private final Device device;
private final Streamer streamer;
private final String encoderName;
private final List<CodecOption> codecOptions;
private final int videoBitRate;
private final int bitRate;
private final int maxFps;
private final boolean sendFrameMeta;
private final boolean downsizeOnError;
private long ptsOrigin;
private boolean firstFrameSent;
private int consecutiveErrors;
public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
boolean downsizeOnError) {
this.device = device;
this.streamer = streamer;
this.videoBitRate = videoBitRate;
this.sendFrameMeta = sendFrameMeta;
this.bitRate = bitRate;
this.maxFps = maxFps;
this.codecOptions = codecOptions;
this.encoderName = encoderName;
@ -59,15 +64,22 @@ public class ScreenEncoder implements Device.RotationListener {
return rotationChanged.getAndSet(false);
}
public void streamScreen() throws IOException, ConfigurationException {
Codec codec = streamer.getCodec();
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
Workarounds.prepareMainLooper();
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// <https://github.com/Genymobile/scrcpy/issues/240>
// <https://github.com/Genymobile/scrcpy/issues/2656>
Workarounds.fillAppInfo();
}
internalStreamScreen(device, fd);
}
private void internalStreamScreen(Device device, FileDescriptor fd) throws IOException {
MediaCodec codec = createCodec(encoderName);
MediaFormat format = createFormat(bitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
streamer.writeHeader();
boolean alive;
try {
do {
@ -81,8 +93,8 @@ public class ScreenEncoder implements Device.RotationListener {
Surface surface = null;
try {
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = mediaCodec.createInputSurface();
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = codec.createInputSurface();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
@ -90,52 +102,49 @@ public class ScreenEncoder implements Device.RotationListener {
int layerStack = device.getLayerStack();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
mediaCodec.start();
codec.start();
alive = encode(mediaCodec, streamer);
alive = encode(codec, fd);
// do not call stop() on exception, it would trigger an IllegalStateException
mediaCodec.stop();
codec.stop();
} catch (MediaCodec.CodecException e) {
Ln.e("Codec error: " + e.getMessage());
// <https://developer.android.com/reference/android/media/MediaCodec#error-handling>
// For simplicity, handle isTransient() like isRecoverable()
if (e.isRecoverable() || e.isTransient()) {
// Avoid busy-loop if too many errors are generated
SystemClock.sleep(50);
} else if (!prepareDownsizeRetry(device, screenInfo)) {
throw e;
}
alive = true;
} catch (IllegalStateException | IllegalArgumentException e) {
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
if (!prepareRetry(device, screenInfo)) {
if (!prepareDownsizeRetry(device, screenInfo)) {
throw e;
}
Ln.i("Retrying...");
alive = true;
} finally {
mediaCodec.reset();
codec.reset();
if (surface != null) {
surface.release();
}
}
} while (alive);
} finally {
mediaCodec.release();
codec.release();
device.setRotationListener(null);
SurfaceControl.destroyDisplay(display);
}
}
private boolean prepareRetry(Device device, ScreenInfo screenInfo) {
if (firstFrameSent) {
++consecutiveErrors;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
// Definitively fail
return false;
}
// Wait a bit to increase the probability that retrying will fix the problem
SystemClock.sleep(50);
return true;
}
if (!downsizeOnError) {
private boolean prepareDownsizeRetry(Device device, ScreenInfo screenInfo) {
if (!downsizeOnError || firstFrameSent) {
Ln.i("#1 " + downsizeOnError + " " + firstFrameSent);
// Must fail immediately
return false;
}
// Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising)
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
Ln.i("newMaxSize = " + newMaxSize);
if (newMaxSize == 0) {
@ -161,30 +170,30 @@ public class ScreenEncoder implements Device.RotationListener {
return 0;
}
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
boolean eof = false;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
try {
if (consumeRotationChange()) {
// must restart encoding with new size
break;
}
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
if (outputBufferId >= 0) {
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
if (!isConfig) {
// If this is not a config packet, then it contains a frame
firstFrameSent = true;
consecutiveErrors = 0;
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
streamer.writePacket(codecBuffer, bufferInfo);
IO.writeFully(fd, codecBuffer);
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
// If this is not a config packet, then it contains a frame
firstFrameSent = true;
}
}
} finally {
if (outputBufferId >= 0) {
@ -196,24 +205,74 @@ public class ScreenEncoder implements Device.RotationListener {
return !eof;
}
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long pts;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
pts = PACKET_FLAG_CONFIG; // non-media data packet
} else {
if (ptsOrigin == 0) {
ptsOrigin = bufferInfo.presentationTimeUs;
}
pts = bufferInfo.presentationTimeUs - ptsOrigin;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
pts |= PACKET_FLAG_KEY_FRAME;
}
}
headerBuffer.putLong(pts);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static MediaCodecInfo[] listEncoders() {
List<MediaCodecInfo> result = new ArrayList<>();
MediaCodecList list = new MediaCodecList(MediaCodecList.REGULAR_CODECS);
for (MediaCodecInfo codecInfo : list.getCodecInfos()) {
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(MediaFormat.MIMETYPE_VIDEO_AVC)) {
result.add(codecInfo);
}
}
return result.toArray(new MediaCodecInfo[result.size()]);
}
private static MediaCodec createCodec(String encoderName) throws IOException {
if (encoderName != null) {
Ln.d("Creating encoder by name: '" + encoderName + "'");
try {
return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) {
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName);
MediaCodecInfo[] encoders = listEncoders();
throw new InvalidEncoderException(encoderName, encoders);
}
}
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
Ln.d("Using encoder: '" + codec.getName() + "'");
return codec;
}
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
private static void setCodecOption(MediaFormat format, CodecOption codecOption) {
String key = codecOption.getKey();
Object value = codecOption.getValue();
if (value instanceof Integer) {
format.setInteger(key, (Integer) value);
} else if (value instanceof Long) {
format.setLong(key, (Long) value);
} else if (value instanceof Float) {
format.setFloat(key, (Float) value);
} else if (value instanceof String) {
format.setString(key, (String) value);
}
Ln.d("Codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
}
private static MediaFormat createFormat(int bitRate, int maxFps, List<CodecOption> codecOptions) {
MediaFormat format = new MediaFormat();
format.setString(MediaFormat.KEY_MIME, videoMimeType);
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
format.setInteger(MediaFormat.KEY_BIT_RATE, bitRate);
// must be present to configure the encoder, but does not impact the actual frame rate, which is variable
format.setInteger(MediaFormat.KEY_FRAME_RATE, 60);
@ -230,10 +289,7 @@ public class ScreenEncoder implements Device.RotationListener {
if (codecOptions != null) {
for (CodecOption option : codecOptions) {
String key = option.getKey();
Object value = option.getValue();
CodecUtils.setCodecOption(format, key, value);
Ln.d("Video codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
setCodecOption(format, option);
}
}

View File

@ -1,6 +1,7 @@
package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.media.MediaCodecInfo;
import android.os.BatteryManager;
import android.os.Build;
@ -58,96 +59,52 @@ public final class Server {
}
}
private static void scrcpy(Options options) throws IOException, ConfigurationException {
private static void scrcpy(Options options) throws IOException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
List<CodecOption> codecOptions = options.getCodecOptions();
Thread initThread = startInitThread(options);
int scid = options.getScid();
int uid = options.getUid();
boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl();
boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte();
Workarounds.prepareMainLooper();
// Workarounds must be applied for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/240>
// - <https://github.com/Genymobile/scrcpy/issues/365>
// - <https://github.com/Genymobile/scrcpy/issues/2656>
//
// 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");
// 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();
}
Controller controller = null;
AudioEncoder audioEncoder = null;
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) {
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
if (options.getSendDeviceMeta()) {
Size videoSize = device.getScreenInfo().getVideoSize();
connection.sendDeviceMeta(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
}
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps(), codecOptions,
options.getEncoderName(), options.getDownsizeOnError());
Thread controllerThread = null;
Thread deviceMessageSenderThread = null;
if (control) {
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
controller.start();
final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
final Controller controllerRef = controller;
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
// asynchronous
controllerThread = startController(controller);
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
device.setClipboardListener(text -> controller.getSender().pushClipboardText(text));
}
if (audio) {
Streamer audioStreamer = new Streamer(connection.getAudioFd(), options.getAudioCodec(), options.getSendCodecId(),
options.getSendFrameMeta());
audioEncoder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), options.getAudioEncoder());
audioEncoder.start();
}
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecId(),
options.getSendFrameMeta());
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
try {
// synchronous
screenEncoder.streamScreen();
screenEncoder.streamScreen(device, connection.getVideoFd());
} catch (IOException e) {
// Broken pipe is expected on close, because the socket is closed by the client
if (!IO.isBrokenPipe(e)) {
Ln.e("Video encoding error", e);
// this is expected on close
Ln.d("Screen streaming stopped");
} finally {
initThread.interrupt();
if (controllerThread != null) {
controllerThread.interrupt();
}
}
} finally {
Ln.d("Screen streaming stopped");
initThread.interrupt();
if (audioEncoder != null) {
audioEncoder.stop();
}
if (controller != null) {
controller.stop();
}
try {
initThread.join();
if (audioEncoder != null) {
audioEncoder.join();
if (deviceMessageSenderThread != null) {
deviceMessageSenderThread.interrupt();
}
if (controller != null) {
controller.join();
}
} catch (InterruptedException e) {
// ignore
}
}
}
@ -158,7 +115,32 @@ public final class Server {
return thread;
}
@SuppressWarnings("MethodLength")
private static Thread startController(final Controller controller) {
Thread thread = new Thread(() -> {
try {
controller.control();
} catch (IOException e) {
// this is expected on close
Ln.d("Controller stopped");
}
});
thread.start();
return thread;
}
private static Thread startDeviceMessageSender(final DeviceMessageSender sender) {
Thread thread = new Thread(() -> {
try {
sender.loop();
} catch (IOException | InterruptedException e) {
// this is expected on close
Ln.d("Device message sender stopped");
}
});
thread.start();
return thread;
}
private static Options createOptions(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
@ -181,46 +163,24 @@ public final class Server {
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "scid":
int scid = Integer.parseInt(value, 0x10);
if (scid < -1) {
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
case "uid":
int uid = Integer.parseInt(value, 0x10);
if (uid < -1) {
throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid);
}
options.setScid(scid);
options.setUid(uid);
break;
case "log_level":
Ln.Level level = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
options.setLogLevel(level);
break;
case "audio":
boolean audio = Boolean.parseBoolean(value);
options.setAudio(audio);
break;
case "video_codec":
VideoCodec videoCodec = VideoCodec.findByName(value);
if (videoCodec == null) {
throw new IllegalArgumentException("Video codec " + value + " not supported");
}
options.setVideoCodec(videoCodec);
break;
case "audio_codec":
AudioCodec audioCodec = AudioCodec.findByName(value);
if (audioCodec == null) {
throw new IllegalArgumentException("Audio codec " + value + " not supported");
}
options.setAudioCodec(audioCodec);
break;
case "max_size":
int maxSize = Integer.parseInt(value) & ~7; // multiple of 8
options.setMaxSize(maxSize);
break;
case "video_bit_rate":
int videoBitRate = Integer.parseInt(value);
options.setVideoBitRate(videoBitRate);
break;
case "audio_bit_rate":
int audioBitRate = Integer.parseInt(value);
options.setAudioBitRate(audioBitRate);
case "bit_rate":
int bitRate = Integer.parseInt(value);
options.setBitRate(bitRate);
break;
case "max_fps":
int maxFps = Integer.parseInt(value);
@ -254,23 +214,15 @@ public final class Server {
boolean stayAwake = Boolean.parseBoolean(value);
options.setStayAwake(stayAwake);
break;
case "video_codec_options":
List<CodecOption> videoCodecOptions = CodecOption.parse(value);
options.setVideoCodecOptions(videoCodecOptions);
case "codec_options":
List<CodecOption> codecOptions = CodecOption.parse(value);
options.setCodecOptions(codecOptions);
break;
case "audio_codec_options":
List<CodecOption> audioCodecOptions = CodecOption.parse(value);
options.setAudioCodecOptions(audioCodecOptions);
break;
case "video_encoder":
case "encoder_name":
if (!value.isEmpty()) {
options.setVideoEncoder(value);
options.setEncoderName(value);
}
break;
case "audio_encoder":
if (!value.isEmpty()) {
options.setAudioEncoder(value);
}
case "power_off_on_close":
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
@ -291,14 +243,6 @@ public final class Server {
boolean powerOn = Boolean.parseBoolean(value);
options.setPowerOn(powerOn);
break;
case "list_encoders":
boolean listEncoders = Boolean.parseBoolean(value);
options.setListEncoders(listEncoders);
break;
case "list_displays":
boolean listDisplays = Boolean.parseBoolean(value);
options.setListDisplays(listDisplays);
break;
case "send_device_meta":
boolean sendDeviceMeta = Boolean.parseBoolean(value);
options.setSendDeviceMeta(sendDeviceMeta);
@ -311,17 +255,12 @@ 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);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
options.setSendDeviceMeta(false);
options.setSendFrameMeta(false);
options.setSendDummyByte(false);
options.setSendCodecId(false);
}
break;
default:
@ -349,35 +288,38 @@ public final class Server {
return new Rect(x, y, x + width, y + height);
}
private static void suggestFix(Throwable e) {
if (e instanceof InvalidDisplayIdException) {
InvalidDisplayIdException idie = (InvalidDisplayIdException) e;
int[] displayIds = idie.getAvailableDisplayIds();
if (displayIds != null && displayIds.length > 0) {
Ln.e("Try to use one of the available display ids:");
for (int id : displayIds) {
Ln.e(" scrcpy --display " + id);
}
}
} else if (e instanceof InvalidEncoderException) {
InvalidEncoderException iee = (InvalidEncoderException) e;
MediaCodecInfo[] encoders = iee.getAvailableEncoders();
if (encoders != null && encoders.length > 0) {
Ln.e("Try to use one of the available encoders:");
for (MediaCodecInfo encoder : encoders) {
Ln.e(" scrcpy --encoder '" + encoder.getName() + "'");
}
}
}
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e);
suggestFix(e);
});
Options options = createOptions(args);
Ln.initLogLevel(options.getLogLevel());
if (options.getListEncoders() || options.getListDisplays()) {
if (options.getCleanup()) {
CleanUp.unlinkSelf();
}
if (options.getListEncoders()) {
Ln.i(LogUtils.buildVideoEncoderListMessage());
Ln.i(LogUtils.buildAudioEncoderListMessage());
}
if (options.getListDisplays()) {
Ln.i(LogUtils.buildDisplayListMessage());
}
// Just print the requested data, do not mirror
return;
}
try {
scrcpy(options);
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
}
scrcpy(options);
}
}

View File

@ -1,129 +0,0 @@
package com.genymobile.scrcpy;
import android.media.MediaCodec;
import java.io.FileDescriptor;
import java.io.IOException;
import java.nio.ByteBuffer;
public final class Streamer {
private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian)
private final FileDescriptor fd;
private final Codec codec;
private final boolean sendCodecId;
private final boolean sendFrameMeta;
private final ByteBuffer headerBuffer = ByteBuffer.allocate(12);
public Streamer(FileDescriptor fd, Codec codec, boolean sendCodecId, boolean sendFrameMeta) {
this.fd = fd;
this.codec = codec;
this.sendCodecId = sendCodecId;
this.sendFrameMeta = sendFrameMeta;
}
public Codec getCodec() {
return codec;
}
public void writeHeader() throws IOException {
if (sendCodecId) {
ByteBuffer buffer = ByteBuffer.allocate(4);
buffer.putInt(codec.getId());
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
// code 1: a configuration error occurred, scrcpy must be stopped
byte[] code = new byte[4];
if (error) {
code[3] = 1;
}
IO.writeFully(fd, code, 0, code.length);
}
public void writePacket(ByteBuffer codecBuffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
if (codec == AudioCodec.OPUS) {
fixOpusConfigPacket(codecBuffer, bufferInfo);
}
if (sendFrameMeta) {
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
}
IO.writeFully(fd, codecBuffer);
}
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
headerBuffer.clear();
long ptsAndFlags;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) {
ptsAndFlags = PACKET_FLAG_CONFIG; // non-media data packet
} else {
ptsAndFlags = bufferInfo.presentationTimeUs;
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_KEY_FRAME) != 0) {
ptsAndFlags |= PACKET_FLAG_KEY_FRAME;
}
}
headerBuffer.putLong(ptsAndFlags);
headerBuffer.putInt(packetSize);
headerBuffer.flip();
IO.writeFully(fd, headerBuffer);
}
private static void fixOpusConfigPacket(ByteBuffer buffer, MediaCodec.BufferInfo bufferInfo) throws IOException {
// Here is an example of the config packet received for an OPUS stream:
//
// 00000000 41 4f 50 55 53 48 44 52 13 00 00 00 00 00 00 00 |AOPUSHDR........|
// -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA -------------------
// 00000010 4f 70 75 73 48 65 61 64 01 01 38 01 80 bb 00 00 |OpusHead..8.....|
// 00000020 00 00 00 |... |
// ------------------------------------------------------------------------------
// 00000020 41 4f 50 55 53 44 4c 59 08 00 00 00 00 | AOPUSDLY.....|
// 00000030 00 00 00 a0 2e 63 00 00 00 00 00 41 4f 50 55 53 |.....c.....AOPUS|
// 00000040 50 52 4c 08 00 00 00 00 00 00 00 00 b4 c4 04 00 |PRL.............|
// 00000050 00 00 00 |...|
//
// Each "section" is prefixed by a 64-bit ID and a 64-bit length.
//
// <https://developer.android.com/reference/android/media/MediaCodec#CSD>
boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0;
if (!isConfig) {
return;
}
if (buffer.remaining() < 16) {
throw new IOException("Not enough data in OPUS config packet");
}
long id = buffer.getLong();
if (id != AOPUSHDR) {
throw new IOException("OPUS header not found");
}
long sizeLong = buffer.getLong();
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
throw new IOException("Invalid block size in OPUS header: " + sizeLong);
}
int size = (int) sizeLong;
if (buffer.remaining() < size) {
throw new IOException("Not enough data in OPUS header (invalid size: " + size + ")");
}
// Set the buffer to point to the OPUS header slice
buffer.limit(buffer.position() + size);
}
}

View File

@ -1,50 +0,0 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.media.MediaFormat;
public enum VideoCodec implements Codec {
H264(0x68_32_36_34, "h264", MediaFormat.MIMETYPE_VIDEO_AVC),
H265(0x68_32_36_35, "h265", MediaFormat.MIMETYPE_VIDEO_HEVC),
@SuppressLint("InlinedApi") // introduced in API 21
AV1(0x00_61_76_31, "av1", MediaFormat.MIMETYPE_VIDEO_AV1);
private final int id; // 4-byte ASCII representation of the name
private final String name;
private final String mimeType;
VideoCodec(int id, String name, String mimeType) {
this.id = id;
this.name = name;
this.mimeType = mimeType;
}
@Override
public Type getType() {
return Type.VIDEO;
}
@Override
public int getId() {
return id;
}
@Override
public String getName() {
return name;
}
@Override
public String getMimeType() {
return mimeType;
}
public static VideoCodec findByName(String name) {
for (VideoCodec codec : values()) {
if (codec.name.equals(name)) {
return codec;
}
}
return null;
}
}

View File

@ -2,12 +2,14 @@ package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.app.Application;
import android.content.ContextWrapper;
import android.app.Instrumentation;
import android.content.Context;
import android.content.pm.ApplicationInfo;
import android.os.Looper;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
public final class Workarounds {
private Workarounds() {
@ -48,7 +50,7 @@ public final class Workarounds {
Object appBindData = appBindDataConstructor.newInstance();
ApplicationInfo applicationInfo = new ApplicationInfo();
applicationInfo.packageName = FakeContext.PACKAGE_NAME;
applicationInfo.packageName = "com.genymobile.scrcpy";
// appBindData.appInfo = applicationInfo;
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
@ -60,10 +62,11 @@ public final class Workarounds {
mBoundApplicationField.setAccessible(true);
mBoundApplicationField.set(activityThread, appBindData);
Application app = Application.class.newInstance();
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
baseField.setAccessible(true);
baseField.set(app, FakeContext.get());
// Context ctx = activityThread.getSystemContext();
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
Application app = Instrumentation.newApplication(Application.class, ctx);
// activityThread.mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");

View File

@ -1,14 +1,8 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Intent;
import android.os.Binder;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
import android.os.IInterface;
@ -16,15 +10,12 @@ import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public class ActivityManager {
private final IInterface manager;
private Method getContentProviderExternalMethod;
private boolean getContentProviderExternalMethodNewVersion = true;
private Method removeContentProviderExternalMethod;
private Method startActivityAsUserWithFeatureMethod;
private Method forceStopPackageMethod;
public ActivityManager(IInterface manager) {
this.manager = manager;
@ -51,17 +42,16 @@ public class ActivityManager {
return removeContentProviderExternalMethod;
}
@TargetApi(Build.VERSION_CODES.Q)
private ContentProvider getContentProviderExternal(String name, IBinder token) {
try {
Method method = getGetContentProviderExternalMethod();
Object[] args;
if (getContentProviderExternalMethodNewVersion) {
// new version
args = new Object[]{name, FakeContext.ROOT_UID, token, null};
args = new Object[]{name, ServiceManager.USER_ID, token, null};
} else {
// old version
args = new Object[]{name, FakeContext.ROOT_UID, token};
args = new Object[]{name, ServiceManager.USER_ID, token};
}
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
Object providerHolder = method.invoke(manager, args);
@ -94,55 +84,4 @@ public class ActivityManager {
public ContentProvider createSettingsProvider() {
return getContentProviderExternal("settings", new Binder());
}
private Method getStartActivityAsUserWithFeatureMethod() throws NoSuchMethodException, ClassNotFoundException {
if (startActivityAsUserWithFeatureMethod == null) {
Class<?> iApplicationThreadClass = Class.forName("android.app.IApplicationThread");
Class<?> profilerInfo = Class.forName("android.app.ProfilerInfo");
startActivityAsUserWithFeatureMethod = manager.getClass()
.getMethod("startActivityAsUserWithFeature", iApplicationThreadClass, String.class, String.class, Intent.class, String.class,
IBinder.class, String.class, int.class, int.class, profilerInfo, Bundle.class, int.class);
}
return startActivityAsUserWithFeatureMethod;
}
@SuppressWarnings("ConstantConditions")
public int startActivityAsUserWithFeature(Intent intent) {
try {
Method method = getStartActivityAsUserWithFeatureMethod();
return (int) method.invoke(
/* this */ manager,
/* caller */ null,
/* callingPackage */ FakeContext.PACKAGE_NAME,
/* callingFeatureId */ null,
/* intent */ intent,
/* resolvedType */ null,
/* resultTo */ null,
/* resultWho */ null,
/* requestCode */ 0,
/* startFlags */ 0,
/* profilerInfo */ null,
/* bOptions */ null,
/* userId */ /* UserHandle.USER_CURRENT */ -2);
} catch (Throwable e) {
Ln.e("Could not invoke method", e);
return 0;
}
}
private Method getForceStopPackageMethod() throws NoSuchMethodException {
if (forceStopPackageMethod == null) {
forceStopPackageMethod = manager.getClass().getMethod("forceStopPackage", String.class, int.class);
}
return forceStopPackageMethod;
}
public void forceStopPackage(String packageName) {
try {
Method method = getForceStopPackageMethod();
method.invoke(manager, packageName, /* userId */ /* UserHandle.USER_CURRENT */ -2);
} catch (Throwable e) {
Ln.e("Could not invoke method", e);
}
}
}

View File

@ -1,6 +1,5 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import android.content.ClipData;
@ -59,22 +58,22 @@ public class ClipboardManager {
private static ClipData getPrimaryClip(Method method, boolean alternativeMethod, IInterface manager)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME);
}
if (alternativeMethod) {
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
}
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
private static void setPrimaryClip(Method method, boolean alternativeMethod, IInterface manager, ClipData clipData)
throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME);
} else if (alternativeMethod) {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
} else {
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
}
@ -107,11 +106,11 @@ public class ClipboardManager {
private static void addPrimaryClipChangedListener(Method method, boolean alternativeMethod, IInterface manager,
IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME);
} else if (alternativeMethod) {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID);
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
} else {
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID);
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
}
}

View File

@ -1,12 +1,9 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.SettingsException;
import android.annotation.SuppressLint;
import android.content.AttributionSource;
import android.os.Build;
import android.os.Bundle;
import android.os.IBinder;
@ -54,10 +51,11 @@ public class ContentProvider implements Closeable {
@SuppressLint("PrivateApi")
private Method getCallMethod() throws NoSuchMethodException {
if (callMethod == null) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
try {
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
callMethodVersion = 0;
} else {
} catch (NoSuchMethodException | ClassNotFoundException e0) {
// old versions
try {
callMethod = provider.getClass()
@ -77,29 +75,40 @@ public class ContentProvider implements Closeable {
return callMethod;
}
@SuppressLint("PrivateApi")
private Object getAttributionSource()
throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
if (attributionSource == null) {
Class<?> cl = Class.forName("android.content.AttributionSource$Builder");
Object builder = cl.getConstructor(int.class).newInstance(ServiceManager.USER_ID);
cl.getDeclaredMethod("setPackageName", String.class).invoke(builder, ServiceManager.PACKAGE_NAME);
attributionSource = cl.getDeclaredMethod("build").invoke(builder);
}
return attributionSource;
}
private Bundle call(String callMethod, String arg, Bundle extras)
throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
try {
Method method = getCallMethod();
Object[] args;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) {
args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras};
} else {
switch (callMethodVersion) {
case 1:
args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
break;
case 2:
args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras};
break;
default:
args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras};
break;
}
switch (callMethodVersion) {
case 0:
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
break;
case 1:
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
break;
case 2:
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
break;
default:
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
break;
}
return (Bundle) method.invoke(provider, args);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
Ln.e("Could not invoke method", e);
throw e;
}
@ -138,7 +147,7 @@ public class ContentProvider implements Closeable {
public String getValue(String table, String key) throws SettingsException {
String method = getGetMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID);
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
try {
Bundle bundle = call(method, key, arg);
if (bundle == null) {
@ -154,7 +163,7 @@ public class ContentProvider implements Closeable {
public void putValue(String table, String key, String value) throws SettingsException {
String method = getPutMethod(table);
Bundle arg = new Bundle();
arg.putInt(CALL_METHOD_USER_KEY, FakeContext.ROOT_UID);
arg.putInt(CALL_METHOD_USER_KEY, ServiceManager.USER_ID);
arg.putString(NAME_VALUE_TABLE_VALUE, value);
try {
call(method, key, arg);

View File

@ -3,7 +3,6 @@ package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import android.view.InputEvent;
import android.view.MotionEvent;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -18,7 +17,6 @@ public final class InputManager {
private Method injectInputEventMethod;
private static Method setDisplayIdMethod;
private static Method setActionButtonMethod;
public InputManager(android.hardware.input.InputManager manager) {
this.manager = manager;
@ -58,22 +56,4 @@ public final class InputManager {
return false;
}
}
private static Method getSetActionButtonMethod() throws NoSuchMethodException {
if (setActionButtonMethod == null) {
setActionButtonMethod = MotionEvent.class.getMethod("setActionButton", int.class);
}
return setActionButtonMethod;
}
public static boolean setActionButton(MotionEvent motionEvent, int actionButton) {
try {
Method method = getSetActionButtonMethod();
method.invoke(motionEvent, actionButton);
return true;
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Cannot set action button on MotionEvent", e);
return false;
}
}
}

View File

@ -10,6 +10,9 @@ import java.lang.reflect.Method;
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
public final class ServiceManager {
public static final String PACKAGE_NAME = "com.android.shell";
public static final int USER_ID = 0;
private static final Method GET_SERVICE_METHOD;
static {
try {

View File

@ -30,8 +30,6 @@ public final class SurfaceControl {
private static Method getBuiltInDisplayMethod;
private static Method setDisplayPowerModeMethod;
private static Method getPhysicalDisplayTokenMethod;
private static Method getPhysicalDisplayIdsMethod;
private SurfaceControl() {
// only static methods
@ -100,6 +98,7 @@ public final class SurfaceControl {
}
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
@ -115,40 +114,6 @@ public final class SurfaceControl {
}
}
private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException {
if (getPhysicalDisplayTokenMethod == null) {
getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class);
}
return getPhysicalDisplayTokenMethod;
}
public static IBinder getPhysicalDisplayToken(long physicalDisplayId) {
try {
Method method = getGetPhysicalDisplayTokenMethod();
return (IBinder) method.invoke(null, physicalDisplayId);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException {
if (getPhysicalDisplayIdsMethod == null) {
getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds");
}
return getPhysicalDisplayIdsMethod;
}
public static long[] getPhysicalDisplayIds() {
try {
Method method = getGetPhysicalDisplayIdsMethod();
return (long[]) method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
Ln.e("Could not invoke method", e);
return null;
}
}
private static Method getSetDisplayPowerModeMethod() throws NoSuchMethodException {
if (setDisplayPowerModeMethod == null) {
setDisplayPowerModeMethod = CLASS.getMethod("setDisplayPowerMode", IBinder.class, int.class);

View File

@ -94,8 +94,7 @@ public class ControlMessageReaderTest {
dos.writeShort(1080);
dos.writeShort(1920);
dos.writeShort(0xffff); // pressure
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
byte[] packet = bos.toByteArray();
@ -113,7 +112,6 @@ public class ControlMessageReaderTest {
Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth());
Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight());
Assert.assertEquals(1f, event.getPressure(), 0f); // must be exact
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getActionButton());
Assert.assertEquals(MotionEvent.BUTTON_PRIMARY, event.getButtons());
}