Compare commits
115 Commits
refactor-e
...
audio.76
Author | SHA1 | Date | |
---|---|---|---|
a68500aa13 | |||
dd32e53ad6 | |||
96a05383b9 | |||
048ff837f7 | |||
84703fe6af | |||
4f0f0e9412 | |||
5648d9a7ee | |||
b0f4857ca1 | |||
e1146666dc | |||
2109d15e6c | |||
f91f5ad637 | |||
4b710b5307 | |||
09c3ef52b7 | |||
a388caafe9 | |||
82d92948d5 | |||
0d72399bc1 | |||
24ae14cf46 | |||
269bffdbf9 | |||
ed1de493f9 | |||
00c8d9e289 | |||
39dd5da9cd | |||
4294573225 | |||
ea32ed6444 | |||
802fdf3e0b | |||
e565d31c59 | |||
1de01c1c0e | |||
35c032267c | |||
c7324e16c7 | |||
0e84fe12d6 | |||
66fe1fa003 | |||
0058b9e191 | |||
a5384efd9e | |||
696d61e6d9 | |||
9a10a1dc06 | |||
12de66cf25 | |||
92b4ee21fe | |||
c8749db90b | |||
57b6110e72 | |||
f479beef83 | |||
114e226164 | |||
241d41b683 | |||
4997a2e6a3 | |||
453eca7c18 | |||
de31f4e9a5 | |||
6dacd1ad62 | |||
0e8705f611 | |||
e982679166 | |||
bcd6a998b8 | |||
23a6b3e6c2 | |||
91e2e8a5a5 | |||
f5d0e3211c | |||
260d411c34 | |||
92098d2a06 | |||
768f5714d2 | |||
7650f41aad | |||
0d5e3afdf5 | |||
947df80a01 | |||
9e47989d86 | |||
2b6642807e | |||
9c72cecfef | |||
4dc76e58bc | |||
9e455b803d | |||
4f992519a5 | |||
359a8cbed5 | |||
3910033e36 | |||
94be1730f2 | |||
d0cd5e536e | |||
bdfb8ae725 | |||
2b69eecc90 | |||
97bbbb81ad | |||
ca24b3a8c8 | |||
5563676946 | |||
3d10fbd9b4 | |||
3e3756a323 | |||
5d6bcc5966 | |||
5973d4cdd7 | |||
0a151b96fe | |||
ebecbe6bc6 | |||
d5dff239c8 | |||
5cf86ef7ff | |||
e02f30f895 | |||
25e2eb7d7c | |||
280a9afda8 | |||
e91618586c | |||
680ddf64be | |||
f4e7085c34 | |||
439a1fd4ed | |||
49eb326ce9 | |||
f03f32267e | |||
45b2e6db5c | |||
400a1c69b1 | |||
730eb1086a | |||
4f9e9c6619 | |||
953edfd1df | |||
230b8274b9 | |||
40866ddc10 | |||
bd56c0abf7 | |||
6524e90c68 | |||
f2dee20a20 | |||
d2dce51038 | |||
4342c5637d | |||
3e517cd40e | |||
f70f6cdd3e | |||
87972e2022 | |||
3aac74e9e9 | |||
1c82c3923d | |||
36d656e91f | |||
bdbf1f4eb7 | |||
4177de5880 | |||
6a07e3d470 | |||
9b286ec8a7 | |||
8c5c55f9e1 | |||
0afef0c634 | |||
07806ba915 | |||
a52053421a |
29
README.md
29
README.md
@ -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 --bit-rate=2M
|
||||
scrcpy --video-bit-rate=2M
|
||||
scrcpy -b 2M # short version
|
||||
```
|
||||
|
||||
@ -252,20 +252,31 @@ This affects recording orientation.
|
||||
The [window may also be rotated](#rotation) independently.
|
||||
|
||||
|
||||
#### Encoder
|
||||
#### Codec
|
||||
|
||||
Some devices have more than one encoder, and some of them may cause issues or
|
||||
crash. It is possible to select a different encoder:
|
||||
The video codec can be selected. The possible values are `h264` (default),
|
||||
`h265` and `av1`:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder=OMX.qcom.video.encoder.avc
|
||||
scrcpy --video-codec=h264 # default
|
||||
scrcpy --video-codec=h265
|
||||
scrcpy --video-codec=av1
|
||||
```
|
||||
|
||||
To list the available encoders, you can pass an invalid encoder name; the
|
||||
error will give the available encoders:
|
||||
|
||||
##### 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:
|
||||
|
||||
```bash
|
||||
scrcpy --encoder=_
|
||||
scrcpy --video-encoder=OMX.qcom.video.encoder.avc
|
||||
```
|
||||
|
||||
To list the available encoders:
|
||||
|
||||
```bash
|
||||
scrcpy --list-encoders
|
||||
```
|
||||
|
||||
### Capture
|
||||
@ -431,7 +442,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 --bit-rate=2M --max-size=800
|
||||
scrcpy --video-bit-rate=2M --max-size=800
|
||||
scrcpy -b2M -m800 # short version
|
||||
```
|
||||
|
||||
|
@ -2,28 +2,33 @@ _scrcpy() {
|
||||
local cur prev words cword
|
||||
local opts="
|
||||
--always-on-top
|
||||
-b --bit-rate=
|
||||
--codec-options=
|
||||
--audio-bit-rate=
|
||||
--audio-codec=
|
||||
--audio-codec-options=
|
||||
--audio-encoder=
|
||||
-b --video-bit-rate=
|
||||
--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-on-error
|
||||
--no-clipboard-autosync
|
||||
--no-downsize-on-error
|
||||
-n --no-control
|
||||
-N --no-display
|
||||
@ -53,6 +58,9 @@ _scrcpy() {
|
||||
--v4l2-sink=
|
||||
-V --verbosity=
|
||||
-v --version
|
||||
--video-codec=
|
||||
--video-codec-options=
|
||||
--video-encoder=
|
||||
-w --stay-awake
|
||||
--window-borderless
|
||||
--window-title=
|
||||
@ -64,6 +72,14 @@ _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
|
||||
@ -98,7 +114,7 @@ _scrcpy() {
|
||||
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
|
||||
return
|
||||
;;
|
||||
-b|--bitrate \
|
||||
-b|--video-bit-rate \
|
||||
|--codec-options \
|
||||
|--crop \
|
||||
|--display \
|
||||
|
@ -9,25 +9,30 @@ local arguments
|
||||
|
||||
arguments=(
|
||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||
{-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]'
|
||||
'--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]'
|
||||
'--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]'
|
||||
@ -58,6 +63,9 @@ arguments=(
|
||||
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]'
|
||||
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
|
||||
{-v,--version}'[Print the version of scrcpy]'
|
||||
'--video-codec=[Select the video codec]:codec:(h264 h265 av1)'
|
||||
'--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]'
|
||||
'--video-encoder=[Use a specific MediaCodec video encoder]'
|
||||
{-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]'
|
||||
'--window-borderless[Disable window decorations \(display borderless window\)]'
|
||||
'--window-title=[Set a custom window title]'
|
||||
|
@ -4,6 +4,7 @@ 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',
|
||||
@ -21,6 +22,7 @@ src = [
|
||||
'src/mouse_inject.c',
|
||||
'src/opengl.c',
|
||||
'src/options.c',
|
||||
'src/packet_merger.c',
|
||||
'src/receiver.c',
|
||||
'src/recorder.c',
|
||||
'src/scrcpy.c',
|
||||
@ -29,6 +31,7 @@ src = [
|
||||
'src/version.c',
|
||||
'src/video_buffer.c',
|
||||
'src/util/acksync.c',
|
||||
'src/util/bytebuf.c',
|
||||
'src/util/file.c',
|
||||
'src/util/intmap.c',
|
||||
'src/util/intr.c',
|
||||
@ -200,10 +203,6 @@ 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'))
|
||||
|
||||
@ -263,6 +262,10 @@ 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',
|
||||
]],
|
||||
|
73
app/scrcpy.1
73
app/scrcpy.1
@ -20,20 +20,28 @@ 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 "\-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).
|
||||
.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 8000000.
|
||||
Default is 196K (196000).
|
||||
|
||||
.TP
|
||||
.BI "\-\-codec\-options " key[:type]=value[,...]
|
||||
Set a list of comma-separated key:type=value options for the device encoder.
|
||||
.BI "\-\-audio\-codec " name
|
||||
Select an audio codec (opus or aac).
|
||||
|
||||
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
|
||||
Default is opus.
|
||||
|
||||
The list of possible codec options is available in the Android documentation
|
||||
.UR https://d.android.com/reference/android/media/MediaFormat
|
||||
.UE .
|
||||
.TP
|
||||
.BI "\-\-audio\-encoder " name
|
||||
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
|
||||
|
||||
The available encoders can be listed by \-\-list\-encoders.
|
||||
|
||||
.TP
|
||||
.BI "\-b, \-\-video\-bit\-rate " value
|
||||
Encode the video at the given bit\-rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
|
||||
|
||||
Default is 8M (8000000).
|
||||
|
||||
.TP
|
||||
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
|
||||
@ -55,10 +63,9 @@ Disable screensaver while scrcpy is running.
|
||||
|
||||
.TP
|
||||
.BI "\-\-display " id
|
||||
Specify the display id to mirror.
|
||||
Specify the device display id to mirror.
|
||||
|
||||
The list of possible display ids can be listed by "adb shell dumpsys display"
|
||||
(search "mDisplayId=" in the output).
|
||||
The available display ids can be listed by \-\-list\-displays.
|
||||
|
||||
Default is 0.
|
||||
|
||||
@ -74,10 +81,6 @@ 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.
|
||||
@ -117,7 +120,15 @@ 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
|
||||
.BI "\-\-lock\-video\-orientation[=value]
|
||||
.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]
|
||||
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".
|
||||
@ -199,7 +210,7 @@ It may only work over USB.
|
||||
See \fB\-\-hid\-keyboard\fR and \fB\-\-hid\-mouse\fR.
|
||||
|
||||
.TP
|
||||
.BI "\-p, \-\-port " port[:port]
|
||||
.BI "\-p, \-\-port " port\fR[:\fIport\fR]
|
||||
Set the TCP port (range) used by the client to listen.
|
||||
|
||||
Default is 27183:27199.
|
||||
@ -260,7 +271,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[+...]][,...]
|
||||
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
|
||||
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 ','.
|
||||
@ -270,7 +281,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[=ip[:port]]
|
||||
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
|
||||
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).
|
||||
@ -323,6 +334,28 @@ 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.
|
||||
|
150
app/src/audio_player.c
Normal file
150
app/src/audio_player.c
Normal file
@ -0,0 +1,150 @@
|
||||
#include "audio_player.h"
|
||||
|
||||
#include "util/log.h"
|
||||
|
||||
/** Downcast frame_sink to sc_v4l2_sink */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink)
|
||||
|
||||
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;
|
||||
|
||||
size_t read = sc_bytebuf_read_remaining(&ap->buf);
|
||||
if (read) {
|
||||
if (read > len) {
|
||||
read = len;
|
||||
}
|
||||
sc_bytebuf_read(&ap->buf, stream, read);
|
||||
}
|
||||
if (read < len) {
|
||||
// Insert silence
|
||||
memset(stream + read, 0, len - read);
|
||||
}
|
||||
}
|
||||
|
||||
static SDL_AudioFormat
|
||||
sc_audio_player_ffmpeg_to_sdl_format(enum AVSampleFormat format) {
|
||||
switch (format) {
|
||||
case AV_SAMPLE_FMT_S16:
|
||||
return AUDIO_S16;
|
||||
case AV_SAMPLE_FMT_S32:
|
||||
return AUDIO_S32;
|
||||
case AV_SAMPLE_FMT_FLT:
|
||||
return AUDIO_F32;
|
||||
default:
|
||||
LOGE("Unsupported FFmpeg sample format: %s",
|
||||
av_get_sample_fmt_name(format));
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_audio_player_frame_sink_open(struct sc_frame_sink *sink,
|
||||
const AVCodecContext *ctx) {
|
||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||
|
||||
SDL_AudioFormat format =
|
||||
sc_audio_player_ffmpeg_to_sdl_format(ctx->sample_fmt);
|
||||
if (!format) {
|
||||
// error already logged
|
||||
//return false;
|
||||
format = AUDIO_F32; // it's planar, but for now there is only 1 channel
|
||||
}
|
||||
LOGI("%d\n", ctx->sample_rate);
|
||||
|
||||
SDL_AudioSpec desired = {
|
||||
.freq = ctx->sample_rate,
|
||||
.format = format,
|
||||
.channels = 1,
|
||||
.samples = 2048,
|
||||
.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;
|
||||
}
|
||||
|
||||
SDL_PauseAudioDevice(ap->device, 0);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) {
|
||||
struct sc_audio_player *ap = DOWNCAST(sink);
|
||||
|
||||
const uint8_t *data = frame->data[0];
|
||||
size_t size = frame->linesize[0];
|
||||
|
||||
// TODO convert to non planar format
|
||||
// TODO then re-enable stereo
|
||||
// TODO clock drift compensation
|
||||
|
||||
// It should almost always be possible to write without lock
|
||||
bool can_write_without_lock = size <= ap->safe_empty_buffer;
|
||||
if (can_write_without_lock) {
|
||||
sc_bytebuf_prepare_write(&ap->buf, data, size);
|
||||
}
|
||||
|
||||
SDL_LockAudioDevice(ap->device);
|
||||
if (can_write_without_lock) {
|
||||
sc_bytebuf_commit_write(&ap->buf, size);
|
||||
} else {
|
||||
sc_bytebuf_write(&ap->buf, data, size);
|
||||
}
|
||||
|
||||
// The next time, it will remain at least the current empty space
|
||||
ap->safe_empty_buffer = sc_bytebuf_write_remaining(&ap->buf);
|
||||
SDL_UnlockAudioDevice(ap->device);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool
|
||||
sc_audio_player_init(struct sc_audio_player *ap,
|
||||
const struct sc_audio_player_callbacks *cbs,
|
||||
void *cbs_userdata) {
|
||||
bool ok = sc_bytebuf_init(&ap->buf, 128 * 1024);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ap->safe_empty_buffer = sc_bytebuf_write_remaining(&ap->buf);
|
||||
|
||||
assert(cbs && cbs->on_ended);
|
||||
ap->cbs = cbs;
|
||||
ap->cbs_userdata = cbs_userdata;
|
||||
|
||||
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;
|
||||
return true;
|
||||
}
|
||||
|
||||
void
|
||||
sc_audio_player_destroy(struct sc_audio_player *ap) {
|
||||
sc_bytebuf_destroy(&ap->buf);
|
||||
}
|
40
app/src/audio_player.h
Normal file
40
app/src/audio_player.h
Normal file
@ -0,0 +1,40 @@
|
||||
#ifndef SC_AUDIO_PLAYER_H
|
||||
#define SC_AUDIO_PLAYER_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include "trait/frame_sink.h"
|
||||
#include <util/bytebuf.h>
|
||||
#include <util/thread.h>
|
||||
|
||||
#include <libavformat/avformat.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;
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
bool
|
||||
sc_audio_player_init(struct sc_audio_player *ap,
|
||||
const struct sc_audio_player_callbacks *cbs,
|
||||
void *cbs_userdata);
|
||||
|
||||
void
|
||||
sc_audio_player_destroy(struct sc_audio_player *ap);
|
||||
|
||||
#endif
|
278
app/src/cli.c
278
app/src/cli.c
@ -17,46 +17,59 @@
|
||||
#define STR_IMPL_(x) #x
|
||||
#define STR(x) STR_IMPL_(x)
|
||||
|
||||
#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
|
||||
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,
|
||||
};
|
||||
|
||||
struct sc_option {
|
||||
char shortopt;
|
||||
@ -98,25 +111,63 @@ static const struct sc_option options[] = {
|
||||
.text = "Make scrcpy window always on top (above other windows).",
|
||||
},
|
||||
{
|
||||
.shortopt = 'b',
|
||||
.longopt = "bit-rate",
|
||||
.longopt_id = OPT_AUDIO_BIT_RATE,
|
||||
.longopt = "audio-bit-rate",
|
||||
.argdesc = "value",
|
||||
.text = "Encode the video at the gitven bit-rate, expressed in bits/s. "
|
||||
.text = "Encode the audio at the given bit-rate, expressed in bits/s. "
|
||||
"Unit suffixes are supported: 'K' (x1000) and 'M' (x1000000).\n"
|
||||
"Default is " STR(DEFAULT_BIT_RATE) ".",
|
||||
"Default is 196K (196000).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_CODEC_OPTIONS,
|
||||
.longopt = "codec-options",
|
||||
.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",
|
||||
.argdesc = "key[:type]=value[,...]",
|
||||
.text = "Set a list of comma-separated key:type=value options for the "
|
||||
"device encoder.\n"
|
||||
"device audio 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",
|
||||
@ -141,10 +192,9 @@ static const struct sc_option options[] = {
|
||||
.longopt_id = OPT_DISPLAY_ID,
|
||||
.longopt = "display",
|
||||
.argdesc = "id",
|
||||
.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"
|
||||
.text = "Specify the device display id to mirror.\n"
|
||||
"The available display ids can be listed by:\n"
|
||||
" scrcpy --list-displays\n"
|
||||
"Default is 0.",
|
||||
},
|
||||
{
|
||||
@ -162,10 +212,10 @@ static const struct sc_option options[] = {
|
||||
"Also see -d (--select-usb).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_ENCODER_NAME,
|
||||
// deprecated
|
||||
.longopt_id = OPT_ENCODER,
|
||||
.longopt = "encoder",
|
||||
.argdesc = "name",
|
||||
.text = "Use a specific MediaCodec encoder (must be a H.264 encoder).",
|
||||
},
|
||||
{
|
||||
.longopt_id = OPT_FORCE_ADB_FORWARD,
|
||||
@ -215,6 +265,16 @@ 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",
|
||||
@ -256,6 +316,11 @@ 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",
|
||||
@ -502,6 +567,33 @@ 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",
|
||||
@ -1377,6 +1469,38 @@ 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) {
|
||||
@ -1388,7 +1512,12 @@ 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->bit_rate)) {
|
||||
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)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@ -1561,10 +1690,23 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->forward_key_repeat = false;
|
||||
break;
|
||||
case OPT_CODEC_OPTIONS:
|
||||
opts->codec_options = optarg;
|
||||
LOGW("--codec-options is deprecated, use --video-codec-options "
|
||||
"instead.");
|
||||
// fall through
|
||||
case OPT_VIDEO_CODEC_OPTIONS:
|
||||
opts->video_codec_options = optarg;
|
||||
break;
|
||||
case OPT_ENCODER_NAME:
|
||||
opts->encoder_name = optarg;
|
||||
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;
|
||||
break;
|
||||
case OPT_FORCE_ADB_FORWARD:
|
||||
opts->force_adb_forward = true;
|
||||
@ -1601,6 +1743,9 @@ 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;
|
||||
@ -1610,6 +1755,19 @@ 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;
|
||||
@ -1637,6 +1795,12 @@ 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;
|
||||
|
@ -117,8 +117,9 @@ 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.buttons);
|
||||
return 28;
|
||||
sc_write32be(&buf[24], msg->inject_touch_event.action_button);
|
||||
sc_write32be(&buf[28], msg->inject_touch_event.buttons);
|
||||
return 32;
|
||||
case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT:
|
||||
write_position(&buf[1], &msg->inject_scroll_event.position);
|
||||
int16_t hscroll =
|
||||
@ -179,22 +180,25 @@ 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 buttons=%06lx",
|
||||
" pressure=%f action_button=%06lx 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 buttons=%06lx",
|
||||
PRIi32 " pressure=%f action_button=%06lx"
|
||||
" buttons=%06lx",
|
||||
id,
|
||||
MOTIONEVENT_ACTION_LABEL(action),
|
||||
msg->inject_touch_event.position.point.x,
|
||||
msg->inject_touch_event.position.point.y,
|
||||
msg->inject_touch_event.pressure,
|
||||
(long) msg->inject_touch_event.action_button,
|
||||
(long) msg->inject_touch_event.buttons);
|
||||
}
|
||||
break;
|
||||
|
@ -65,6 +65,7 @@ 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;
|
||||
|
@ -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 = receiver_init(&controller->receiver, control_socket, acksync);
|
||||
bool ok = sc_receiver_init(&controller->receiver, control_socket, acksync);
|
||||
if (!ok) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_mutex_init(&controller->mutex);
|
||||
if (!ok) {
|
||||
receiver_destroy(&controller->receiver);
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
return false;
|
||||
}
|
||||
|
||||
ok = sc_cond_init(&controller->msg_cond);
|
||||
if (!ok) {
|
||||
receiver_destroy(&controller->receiver);
|
||||
sc_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);
|
||||
}
|
||||
|
||||
receiver_destroy(&controller->receiver);
|
||||
sc_receiver_destroy(&controller->receiver);
|
||||
}
|
||||
|
||||
bool
|
||||
@ -117,7 +117,7 @@ sc_controller_start(struct sc_controller *controller) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!receiver_start(&controller->receiver)) {
|
||||
if (!sc_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);
|
||||
receiver_join(&controller->receiver);
|
||||
sc_receiver_join(&controller->receiver);
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ struct sc_controller {
|
||||
sc_cond msg_cond;
|
||||
bool stopped;
|
||||
struct sc_control_msg_queue queue;
|
||||
struct receiver receiver;
|
||||
struct sc_receiver receiver;
|
||||
};
|
||||
|
||||
bool
|
||||
|
@ -25,11 +25,10 @@ sc_decoder_close_sinks(struct sc_decoder *decoder) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_decoder_open_sinks(struct sc_decoder *decoder) {
|
||||
sc_decoder_open_sinks(struct sc_decoder *decoder, const AVCodecContext *ctx) {
|
||||
for (unsigned i = 0; i < decoder->sink_count; ++i) {
|
||||
struct sc_frame_sink *sink = decoder->sinks[i];
|
||||
if (!sink->ops->open(sink)) {
|
||||
LOGE("Could not open frame sink %d", i);
|
||||
if (!sink->ops->open(sink, ctx)) {
|
||||
sc_decoder_close_first_sinks(decoder, i);
|
||||
return false;
|
||||
}
|
||||
@ -48,8 +47,13 @@ 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) {
|
||||
// Only YUV 4:2:0 is supported, hardcode it
|
||||
decoder->codec_ctx->pix_fmt = AV_PIX_FMT_YUV420P;
|
||||
}
|
||||
|
||||
if (avcodec_open2(decoder->codec_ctx, codec, NULL) < 0) {
|
||||
LOGE("Could not open codec");
|
||||
LOGE("Decoder '%s': could not open codec", decoder->name);
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
return false;
|
||||
}
|
||||
@ -62,8 +66,7 @@ sc_decoder_open(struct sc_decoder *decoder, const AVCodec *codec) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!sc_decoder_open_sinks(decoder)) {
|
||||
LOGE("Could not open decoder sinks");
|
||||
if (!sc_decoder_open_sinks(decoder, decoder->codec_ctx)) {
|
||||
av_frame_free(&decoder->frame);
|
||||
avcodec_close(decoder->codec_ctx);
|
||||
avcodec_free_context(&decoder->codec_ctx);
|
||||
@ -86,7 +89,6 @@ 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;
|
||||
}
|
||||
}
|
||||
@ -104,7 +106,8 @@ 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("Could not send video packet: %d", ret);
|
||||
LOGE("Decoder '%s': could not send video packet: %d",
|
||||
decoder->name, ret);
|
||||
return false;
|
||||
}
|
||||
ret = avcodec_receive_frame(decoder->codec_ctx, decoder->frame);
|
||||
@ -117,7 +120,8 @@ sc_decoder_push(struct sc_decoder *decoder, const AVPacket *packet) {
|
||||
|
||||
av_frame_unref(decoder->frame);
|
||||
} else if (ret != AVERROR(EAGAIN)) {
|
||||
LOGE("Could not receive video frame: %d", ret);
|
||||
LOGE("Decoder '%s', could not receive video frame: %d",
|
||||
decoder->name, ret);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@ -143,7 +147,8 @@ sc_decoder_packet_sink_push(struct sc_packet_sink *sink,
|
||||
}
|
||||
|
||||
void
|
||||
sc_decoder_init(struct sc_decoder *decoder) {
|
||||
sc_decoder_init(struct sc_decoder *decoder, const char *name) {
|
||||
decoder->name = name; // statically allocated
|
||||
decoder->sink_count = 0;
|
||||
|
||||
static const struct sc_packet_sink_ops ops = {
|
||||
|
@ -14,6 +14,8 @@
|
||||
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;
|
||||
|
||||
@ -21,8 +23,9 @@ struct sc_decoder {
|
||||
AVFrame *frame;
|
||||
};
|
||||
|
||||
// The name must be statically allocated (e.g. a string literal)
|
||||
void
|
||||
sc_decoder_init(struct sc_decoder *decoder);
|
||||
sc_decoder_init(struct sc_decoder *decoder, const char *name);
|
||||
|
||||
void
|
||||
sc_decoder_add_sink(struct sc_decoder *decoder, struct sc_frame_sink *sink);
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
#include "decoder.h"
|
||||
#include "events.h"
|
||||
#include "packet_merger.h"
|
||||
#include "recorder.h"
|
||||
#include "util/binary.h"
|
||||
#include "util/log.h"
|
||||
@ -17,6 +18,42 @@
|
||||
|
||||
#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
|
||||
@ -80,7 +117,6 @@ 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;
|
||||
}
|
||||
}
|
||||
@ -90,50 +126,9 @@ 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("Could not process packet");
|
||||
LOGE("Demuxer '%s': could not process packet", demuxer->name);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -158,7 +153,6 @@ 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;
|
||||
}
|
||||
@ -167,50 +161,99 @@ 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;
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_H264);
|
||||
if (!codec) {
|
||||
LOGE("H.264 decoder not found");
|
||||
// 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;
|
||||
}
|
||||
|
||||
demuxer->codec_ctx = avcodec_alloc_context3(codec);
|
||||
if (!demuxer->codec_ctx) {
|
||||
LOG_OOM();
|
||||
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);
|
||||
if (!codec) {
|
||||
LOGE("Demuxer '%s': stream disabled due to missing decoder",
|
||||
demuxer->name);
|
||||
sc_demuxer_disable_sinks(demuxer);
|
||||
goto end;
|
||||
}
|
||||
|
||||
if (!sc_demuxer_open_sinks(demuxer, codec)) {
|
||||
LOGE("Could not open demuxer sinks");
|
||||
goto finally_free_codec_ctx;
|
||||
goto end;
|
||||
}
|
||||
|
||||
demuxer->parser = av_parser_init(AV_CODEC_ID_H264);
|
||||
if (!demuxer->parser) {
|
||||
LOGE("Could not initialize parser");
|
||||
goto finally_close_sinks;
|
||||
}
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
struct sc_packet_merger merger;
|
||||
|
||||
if (must_merge_config_packet) {
|
||||
sc_packet_merger_init(&merger);
|
||||
}
|
||||
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
if (!packet) {
|
||||
LOG_OOM();
|
||||
goto finally_close_parser;
|
||||
goto finally_close_sinks;
|
||||
}
|
||||
|
||||
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) {
|
||||
@ -219,33 +262,31 @@ run_demuxer(void *data) {
|
||||
}
|
||||
}
|
||||
|
||||
LOGD("End of frames");
|
||||
LOGD("Demuxer '%s': end of frames", demuxer->name);
|
||||
|
||||
if (demuxer->pending) {
|
||||
av_packet_free(&demuxer->pending);
|
||||
if (must_merge_config_packet) {
|
||||
sc_packet_merger_destroy(&merger);
|
||||
}
|
||||
|
||||
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_eos(demuxer, demuxer->cbs_userdata);
|
||||
demuxer->cbs->on_ended(demuxer, eos, demuxer->cbs_userdata);
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata) {
|
||||
assert(socket != SC_SOCKET_NONE);
|
||||
|
||||
demuxer->name = name; // statically allocated
|
||||
demuxer->socket = socket;
|
||||
demuxer->pending = NULL;
|
||||
demuxer->sink_count = 0;
|
||||
|
||||
assert(cbs && cbs->on_eos);
|
||||
assert(cbs && cbs->on_ended);
|
||||
|
||||
demuxer->cbs = cbs;
|
||||
demuxer->cbs_userdata = cbs_userdata;
|
||||
@ -261,12 +302,12 @@ sc_demuxer_add_sink(struct sc_demuxer *demuxer, struct sc_packet_sink *sink) {
|
||||
|
||||
bool
|
||||
sc_demuxer_start(struct sc_demuxer *demuxer) {
|
||||
LOGD("Starting demuxer thread");
|
||||
LOGD("Demuxer '%s': starting thread", demuxer->name);
|
||||
|
||||
bool ok = sc_thread_create(&demuxer->thread, run_demuxer, "scrcpy-demuxer",
|
||||
demuxer);
|
||||
if (!ok) {
|
||||
LOGE("Could not start demuxer thread");
|
||||
LOGE("Demuxer '%s': could not start thread", demuxer->name);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
@ -15,28 +15,25 @@
|
||||
#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_eos)(struct sc_demuxer *demuxer, void *userdata);
|
||||
void (*on_ended)(struct sc_demuxer *demuxer, bool eos, void *userdata);
|
||||
};
|
||||
|
||||
// The name must be statically allocated (e.g. a string literal)
|
||||
void
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, sc_socket socket,
|
||||
sc_demuxer_init(struct sc_demuxer *demuxer, const char *name, sc_socket socket,
|
||||
const struct sc_demuxer_callbacks *cbs, void *cbs_userdata);
|
||||
|
||||
void
|
||||
|
@ -1,5 +1,7 @@
|
||||
#define EVENT_NEW_FRAME SDL_USEREVENT
|
||||
#define EVENT_STREAM_STOPPED (SDL_USEREVENT + 1)
|
||||
#define EVENT_SERVER_CONNECTION_FAILED (SDL_USEREVENT + 2)
|
||||
#define EVENT_SERVER_CONNECTED (SDL_USEREVENT + 3)
|
||||
#define EVENT_USB_DEVICE_DISCONNECTED (SDL_USEREVENT + 4)
|
||||
#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)
|
||||
|
@ -339,6 +339,7 @@ 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)) {
|
||||
|
@ -73,6 +73,8 @@ main_scrcpy(int argc, char *argv[]) {
|
||||
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);
|
||||
|
@ -93,6 +93,7 @@ sc_mouse_processor_process_mouse_click(struct sc_mouse_processor *mp,
|
||||
.pointer_id = event->pointer_id,
|
||||
.position = event->position,
|
||||
.pressure = event->action == SC_ACTION_DOWN ? 1.f : 0.f,
|
||||
.action_button = convert_mouse_buttons(event->button),
|
||||
.buttons = convert_mouse_buttons(event->buttons_state),
|
||||
},
|
||||
};
|
||||
|
@ -7,14 +7,19 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.window_title = NULL,
|
||||
.push_target = NULL,
|
||||
.render_driver = NULL,
|
||||
.codec_options = NULL,
|
||||
.encoder_name = NULL,
|
||||
.video_codec_options = NULL,
|
||||
.audio_codec_options = NULL,
|
||||
.video_encoder = NULL,
|
||||
.audio_encoder = 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,
|
||||
@ -26,7 +31,8 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.count = 2,
|
||||
},
|
||||
.max_size = 0,
|
||||
.bit_rate = DEFAULT_BIT_RATE,
|
||||
.video_bit_rate = 0,
|
||||
.audio_bit_rate = 0,
|
||||
.max_fps = 0,
|
||||
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
|
||||
.rotation = 0,
|
||||
@ -65,4 +71,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.cleanup = true,
|
||||
.start_fps_counter = false,
|
||||
.power_on = true,
|
||||
.audio = true,
|
||||
.list_encoders = false,
|
||||
.list_displays = false,
|
||||
};
|
||||
|
@ -23,6 +23,14 @@ 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
|
||||
@ -87,12 +95,16 @@ struct scrcpy_options {
|
||||
const char *window_title;
|
||||
const char *push_target;
|
||||
const char *render_driver;
|
||||
const char *codec_options;
|
||||
const char *encoder_name;
|
||||
const char *video_codec_options;
|
||||
const char *audio_codec_options;
|
||||
const char *video_encoder;
|
||||
const char *audio_encoder;
|
||||
#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;
|
||||
@ -101,7 +113,8 @@ struct scrcpy_options {
|
||||
uint16_t tunnel_port;
|
||||
struct sc_shortcut_mods shortcut_mods;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_bit_rate;
|
||||
uint16_t max_fps;
|
||||
enum sc_lock_video_orientation lock_video_orientation;
|
||||
uint8_t rotation;
|
||||
@ -140,6 +153,9 @@ 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;
|
||||
|
48
app/src/packet_merger.c
Normal file
48
app/src/packet_merger.c
Normal file
@ -0,0 +1,48 @@
|
||||
#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;
|
||||
}
|
43
app/src/packet_merger.h
Normal file
43
app/src/packet_merger.h
Normal file
@ -0,0 +1,43 @@
|
||||
#ifndef SC_PACKET_MERGER_H
|
||||
#define SC_PACKET_MERGER_H
|
||||
|
||||
#include "common.h"
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
/**
|
||||
* Config packets (containing the SPS/PPS) are sent in-band. A new config
|
||||
* packet is sent whenever a new encoding session is started (on start and on
|
||||
* device orientation change).
|
||||
*
|
||||
* Every time a config packet is received, it must be sent alone (for recorder
|
||||
* extradata), then concatenated to the next media packet (for correct decoding
|
||||
* and recording).
|
||||
*
|
||||
* This helper reads every input packet and modifies each media packet which
|
||||
* immediately follows a config packet to prepend the config packet payload.
|
||||
*/
|
||||
|
||||
struct sc_packet_merger {
|
||||
uint8_t *config;
|
||||
size_t config_size;
|
||||
};
|
||||
|
||||
void
|
||||
sc_packet_merger_init(struct sc_packet_merger *merger);
|
||||
|
||||
void
|
||||
sc_packet_merger_destroy(struct sc_packet_merger *merger);
|
||||
|
||||
/**
|
||||
* If the packet is a config packet, then keep its data for later.
|
||||
* Otherwise (if the packet is a media packet), then if a config packet is
|
||||
* pending, prepend the config packet to this packet (so the packet is
|
||||
* modified!).
|
||||
*/
|
||||
bool
|
||||
sc_packet_merger_merge(struct sc_packet_merger *merger, AVPacket *packet);
|
||||
|
||||
#endif
|
@ -7,7 +7,7 @@
|
||||
#include "util/log.h"
|
||||
|
||||
bool
|
||||
receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync) {
|
||||
bool ok = sc_mutex_init(&receiver->mutex);
|
||||
if (!ok) {
|
||||
@ -21,12 +21,12 @@ receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
}
|
||||
|
||||
void
|
||||
receiver_destroy(struct receiver *receiver) {
|
||||
sc_receiver_destroy(struct sc_receiver *receiver) {
|
||||
sc_mutex_destroy(&receiver->mutex);
|
||||
}
|
||||
|
||||
static void
|
||||
process_msg(struct receiver *receiver, struct device_msg *msg) {
|
||||
process_msg(struct sc_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 receiver *receiver, struct device_msg *msg) {
|
||||
}
|
||||
|
||||
static ssize_t
|
||||
process_msgs(struct receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
process_msgs(struct sc_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 receiver *receiver, const unsigned char *buf, size_t len) {
|
||||
|
||||
static int
|
||||
run_receiver(void *data) {
|
||||
struct receiver *receiver = data;
|
||||
struct sc_receiver *receiver = data;
|
||||
|
||||
static unsigned char buf[DEVICE_MSG_MAX_SIZE];
|
||||
size_t head = 0;
|
||||
@ -108,7 +108,7 @@ run_receiver(void *data) {
|
||||
}
|
||||
|
||||
bool
|
||||
receiver_start(struct receiver *receiver) {
|
||||
sc_receiver_start(struct sc_receiver *receiver) {
|
||||
LOGD("Starting receiver thread");
|
||||
|
||||
bool ok = sc_thread_create(&receiver->thread, run_receiver,
|
||||
@ -122,6 +122,6 @@ receiver_start(struct receiver *receiver) {
|
||||
}
|
||||
|
||||
void
|
||||
receiver_join(struct receiver *receiver) {
|
||||
sc_receiver_join(struct sc_receiver *receiver) {
|
||||
sc_thread_join(&receiver->thread, NULL);
|
||||
}
|
||||
|
@ -11,7 +11,7 @@
|
||||
|
||||
// receive events from the device
|
||||
// managed by the controller
|
||||
struct receiver {
|
||||
struct sc_receiver {
|
||||
sc_socket control_socket;
|
||||
sc_thread thread;
|
||||
sc_mutex mutex;
|
||||
@ -20,18 +20,18 @@ struct receiver {
|
||||
};
|
||||
|
||||
bool
|
||||
receiver_init(struct receiver *receiver, sc_socket control_socket,
|
||||
sc_receiver_init(struct sc_receiver *receiver, sc_socket control_socket,
|
||||
struct sc_acksync *acksync);
|
||||
|
||||
void
|
||||
receiver_destroy(struct receiver *receiver);
|
||||
sc_receiver_destroy(struct sc_receiver *receiver);
|
||||
|
||||
bool
|
||||
receiver_start(struct receiver *receiver);
|
||||
sc_receiver_start(struct sc_receiver *receiver);
|
||||
|
||||
// no receiver_stop(), it will automatically stop on control_socket shutdown
|
||||
// no sc_receiver_stop(), it will automatically stop on control_socket shutdown
|
||||
|
||||
void
|
||||
receiver_join(struct receiver *receiver);
|
||||
sc_receiver_join(struct sc_receiver *receiver);
|
||||
|
||||
#endif
|
||||
|
@ -8,8 +8,11 @@
|
||||
#include "util/log.h"
|
||||
#include "util/str.h"
|
||||
|
||||
/** Downcast packet_sink to recorder */
|
||||
#define DOWNCAST(SINK) container_of(SINK, struct sc_recorder, packet_sink)
|
||||
/** 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)
|
||||
|
||||
static const AVRational SCRCPY_TIME_BASE = {1, 1000000}; // timestamps in us
|
||||
|
||||
@ -78,9 +81,7 @@ sc_recorder_get_format_name(enum sc_record_format format) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_write_header(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
AVStream *ostream = recorder->ctx->streams[0];
|
||||
|
||||
sc_recorder_set_extradata(AVStream *ostream, const AVPacket *packet) {
|
||||
uint8_t *extradata = av_malloc(packet->size * sizeof(uint8_t));
|
||||
if (!extradata) {
|
||||
LOG_OOM();
|
||||
@ -92,170 +93,56 @@ sc_recorder_write_header(struct sc_recorder *recorder, 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 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 inline void
|
||||
sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) {
|
||||
av_packet_rescale_ts(packet, SCRCPY_TIME_BASE, stream->time_base);
|
||||
}
|
||||
|
||||
static bool
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
if (packet->pts == AV_NOPTS_VALUE) {
|
||||
// ignore config packets
|
||||
return true;
|
||||
static inline bool
|
||||
sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return sc_recorder_write_stream(recorder, recorder->video_stream_index,
|
||||
packet);
|
||||
}
|
||||
|
||||
sc_recorder_rescale_packet(recorder, packet);
|
||||
return av_write_frame(recorder->ctx, packet) >= 0;
|
||||
}
|
||||
|
||||
static int
|
||||
run_recorder(void *data) {
|
||||
struct sc_recorder *recorder = data;
|
||||
|
||||
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 inline bool
|
||||
sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) {
|
||||
return sc_recorder_write_stream(recorder, recorder->audio_stream_index,
|
||||
packet);
|
||||
}
|
||||
|
||||
static bool
|
||||
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;
|
||||
|
||||
sc_recorder_open_output_file(struct sc_recorder *recorder) {
|
||||
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");
|
||||
goto error_cond_destroy;
|
||||
return false;
|
||||
}
|
||||
|
||||
recorder->ctx = avformat_alloc_context();
|
||||
if (!recorder->ctx) {
|
||||
LOG_OOM();
|
||||
goto error_cond_destroy;
|
||||
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;
|
||||
}
|
||||
|
||||
// contrary to the deprecated API (av_oformat_next()), av_muxer_iterate()
|
||||
@ -267,71 +154,436 @@ sc_recorder_open(struct sc_recorder *recorder, const AVCodec *input_codec) {
|
||||
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;
|
||||
|
||||
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);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_close(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
|
||||
sc_thread_join(&recorder->thread, NULL);
|
||||
|
||||
sc_recorder_close_output_file(struct sc_recorder *recorder) {
|
||||
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_push(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
sc_recorder_wait_video_stream(struct sc_recorder *recorder) {
|
||||
sc_mutex_lock(&recorder->mutex);
|
||||
assert(!recorder->stopped);
|
||||
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 (recorder->failed) {
|
||||
// reject any new packet (this will stop the stream)
|
||||
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;
|
||||
stream->codecpar->ch_layout.nb_channels = 2;
|
||||
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_mutex_lock(&recorder->mutex);
|
||||
// EOS also stops the recorder
|
||||
recorder->stopped = true;
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
sc_mutex_unlock(&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_mutex_lock(&recorder->mutex);
|
||||
|
||||
if (recorder->stopped) {
|
||||
// reject any new packet
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
return false;
|
||||
}
|
||||
@ -343,7 +595,9 @@ sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sc_queue_push(&recorder->queue, next, rec);
|
||||
rec->packet->stream_index = 0;
|
||||
|
||||
sc_queue_push(&recorder->video_queue, next, rec);
|
||||
sc_cond_signal(&recorder->queue_cond);
|
||||
|
||||
sc_mutex_unlock(&recorder->mutex);
|
||||
@ -351,51 +605,191 @@ sc_recorder_push(struct sc_recorder *recorder, const AVPacket *packet) {
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_packet_sink_open(struct sc_packet_sink *sink,
|
||||
sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
|
||||
const AVCodec *codec) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
return sc_recorder_open(recorder, 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;
|
||||
}
|
||||
|
||||
static void
|
||||
sc_recorder_packet_sink_close(struct sc_packet_sink *sink) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
sc_recorder_close(recorder);
|
||||
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);
|
||||
}
|
||||
|
||||
static bool
|
||||
sc_recorder_packet_sink_push(struct sc_packet_sink *sink,
|
||||
sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
|
||||
const AVPacket *packet) {
|
||||
struct sc_recorder *recorder = DOWNCAST(sink);
|
||||
return sc_recorder_push(recorder, 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);
|
||||
}
|
||||
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder,
|
||||
const char *filename,
|
||||
enum sc_record_format format,
|
||||
struct sc_size declared_frame_size) {
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format, bool audio,
|
||||
struct sc_size declared_frame_size,
|
||||
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
|
||||
recorder->filename = strdup(filename);
|
||||
if (!recorder->filename) {
|
||||
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;
|
||||
|
||||
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,
|
||||
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,
|
||||
};
|
||||
|
||||
recorder->packet_sink.ops = &ops;
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
@ -20,32 +20,66 @@ struct sc_record_packet {
|
||||
struct sc_recorder_queue SC_QUEUE(struct sc_record_packet);
|
||||
|
||||
struct sc_recorder {
|
||||
struct sc_packet_sink packet_sink; // packet sink trait
|
||||
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;
|
||||
|
||||
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;
|
||||
bool stopped; // set on recorder_close()
|
||||
bool failed; // set on packet write failure
|
||||
struct sc_recorder_queue queue;
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
// 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);
|
||||
};
|
||||
|
||||
bool
|
||||
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
|
||||
enum sc_record_format format,
|
||||
struct sc_size declared_frame_size);
|
||||
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);
|
||||
|
||||
void
|
||||
sc_recorder_destroy(struct sc_recorder *recorder);
|
||||
|
274
app/src/scrcpy.c
274
app/src/scrcpy.c
@ -13,6 +13,7 @@
|
||||
# include <windows.h>
|
||||
#endif
|
||||
|
||||
#include "audio_player.h"
|
||||
#include "controller.h"
|
||||
#include "decoder.h"
|
||||
#include "demuxer.h"
|
||||
@ -40,8 +41,11 @@
|
||||
struct scrcpy {
|
||||
struct sc_server server;
|
||||
struct sc_screen screen;
|
||||
struct sc_demuxer demuxer;
|
||||
struct sc_decoder decoder;
|
||||
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_recorder recorder;
|
||||
#ifdef HAVE_V4L2
|
||||
struct sc_v4l2_sink v4l2_sink;
|
||||
@ -155,9 +159,15 @@ event_loop(struct scrcpy *s) {
|
||||
SDL_Event event;
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case EVENT_STREAM_STOPPED:
|
||||
case SC_EVENT_DEVICE_DISCONNECTED:
|
||||
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;
|
||||
@ -176,15 +186,16 @@ await_for_server(bool *connected) {
|
||||
while (SDL_WaitEvent(&event)) {
|
||||
switch (event.type) {
|
||||
case SDL_QUIT:
|
||||
LOGD("User requested to quit");
|
||||
if (connected) {
|
||||
*connected = false;
|
||||
}
|
||||
return true;
|
||||
case EVENT_SERVER_CONNECTION_FAILED:
|
||||
LOGE("Server connection failed");
|
||||
case SC_EVENT_SERVER_CONNECTION_FAILED:
|
||||
return false;
|
||||
case EVENT_SERVER_CONNECTED:
|
||||
LOGD("Server connected");
|
||||
case SC_EVENT_SERVER_CONNECTED:
|
||||
if (connected) {
|
||||
*connected = true;
|
||||
}
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
@ -195,49 +206,58 @@ await_for_server(bool *connected) {
|
||||
return false;
|
||||
}
|
||||
|
||||
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;
|
||||
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);
|
||||
}
|
||||
// do not forward others, which are too verbose
|
||||
return 0;
|
||||
}
|
||||
|
||||
static void
|
||||
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;
|
||||
}
|
||||
sc_audio_player_on_ended(struct sc_audio_player *ap, bool success,
|
||||
void *userdata) {
|
||||
(void) ap;
|
||||
(void) userdata;
|
||||
|
||||
size_t fmt_len = strlen(fmt);
|
||||
char *local_fmt = malloc(fmt_len + 10);
|
||||
if (!local_fmt) {
|
||||
LOG_OOM();
|
||||
return;
|
||||
if (!success) {
|
||||
// TODO
|
||||
}
|
||||
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_demuxer_on_eos(struct sc_demuxer *demuxer, void *userdata) {
|
||||
sc_video_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
|
||||
void *userdata) {
|
||||
(void) demuxer;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(EVENT_STREAM_STOPPED);
|
||||
if (eos) {
|
||||
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
|
||||
} else {
|
||||
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer, bool eos,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
static void
|
||||
@ -245,7 +265,7 @@ sc_server_on_connection_failed(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(EVENT_SERVER_CONNECTION_FAILED);
|
||||
PUSH_EVENT(SC_EVENT_SERVER_CONNECTION_FAILED);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -253,7 +273,7 @@ sc_server_on_connected(struct sc_server *server, void *userdata) {
|
||||
(void) server;
|
||||
(void) userdata;
|
||||
|
||||
PUSH_EVENT(EVENT_SERVER_CONNECTED);
|
||||
PUSH_EVENT(SC_EVENT_SERVER_CONNECTED);
|
||||
}
|
||||
|
||||
static void
|
||||
@ -266,8 +286,9 @@ 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_uid() {
|
||||
scrcpy_generate_scid() {
|
||||
struct sc_rand rand;
|
||||
sc_rand_init(&rand);
|
||||
// Only use 31 bits to avoid issues with signed values on the Java-side
|
||||
@ -292,10 +313,13 @@ scrcpy(struct scrcpy_options *options) {
|
||||
bool server_started = false;
|
||||
bool file_pusher_initialized = false;
|
||||
bool recorder_initialized = false;
|
||||
bool recorder_started = false;
|
||||
bool audio_player_initialized = false;
|
||||
#ifdef HAVE_V4L2
|
||||
bool v4l2_sink_initialized = false;
|
||||
#endif
|
||||
bool demuxer_started = false;
|
||||
bool video_demuxer_started = false;
|
||||
bool audio_demuxer_started = false;
|
||||
#ifdef HAVE_USB
|
||||
bool aoa_hid_initialized = false;
|
||||
bool hid_keyboard_initialized = false;
|
||||
@ -307,28 +331,34 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
struct sc_acksync *acksync = NULL;
|
||||
|
||||
uint32_t uid = scrcpy_generate_uid();
|
||||
uint32_t scid = scrcpy_generate_scid();
|
||||
|
||||
struct sc_server_params params = {
|
||||
.uid = uid,
|
||||
.scid = scid,
|
||||
.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,
|
||||
.bit_rate = options->bit_rate,
|
||||
.video_bit_rate = options->video_bit_rate,
|
||||
.audio_bit_rate = options->audio_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,
|
||||
.codec_options = options->codec_options,
|
||||
.encoder_name = options->encoder_name,
|
||||
.video_codec_options = options->video_codec_options,
|
||||
.audio_codec_options = options->audio_codec_options,
|
||||
.video_encoder = options->video_encoder,
|
||||
.audio_encoder = options->audio_encoder,
|
||||
.force_adb_forward = options->force_adb_forward,
|
||||
.power_off_on_close = options->power_off_on_close,
|
||||
.clipboard_autosync = options->clipboard_autosync,
|
||||
@ -337,6 +367,8 @@ 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 = {
|
||||
@ -354,30 +386,47 @@ 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 && SDL_Init(SDL_INIT_VIDEO)) {
|
||||
LOGE("Could not initialize SDL: %s", SDL_GetError());
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
sdl_configure(options->display, options->disable_screensaver);
|
||||
|
||||
// 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;
|
||||
|
||||
@ -395,41 +444,55 @@ scrcpy(struct scrcpy_options *options) {
|
||||
file_pusher_initialized = true;
|
||||
}
|
||||
|
||||
struct sc_decoder *dec = NULL;
|
||||
bool needs_decoder = options->display;
|
||||
#ifdef HAVE_V4L2
|
||||
needs_decoder |= !!options->v4l2_device;
|
||||
#endif
|
||||
if (needs_decoder) {
|
||||
sc_decoder_init(&s->decoder);
|
||||
dec = &s->decoder;
|
||||
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;
|
||||
#ifdef HAVE_V4L2
|
||||
needs_video_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);
|
||||
}
|
||||
|
||||
struct sc_recorder *rec = NULL;
|
||||
if (options->record_filename) {
|
||||
if (!sc_recorder_init(&s->recorder,
|
||||
options->record_filename,
|
||||
options->record_format,
|
||||
info->frame_size)) {
|
||||
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)) {
|
||||
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);
|
||||
|
||||
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);
|
||||
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);
|
||||
}
|
||||
|
||||
if (rec) {
|
||||
sc_demuxer_add_sink(&s->demuxer, &rec->packet_sink);
|
||||
}
|
||||
|
||||
struct sc_controller *controller = NULL;
|
||||
@ -620,7 +683,20 @@ aoa_hid_end:
|
||||
}
|
||||
screen_initialized = true;
|
||||
|
||||
sc_decoder_add_sink(&s->decoder, &s->screen.frame_sink);
|
||||
sc_decoder_add_sink(&s->video_decoder, &s->screen.frame_sink);
|
||||
|
||||
if (options->audio) {
|
||||
static const struct sc_audio_player_callbacks audio_player_cbs = {
|
||||
.on_ended = sc_audio_player_on_ended,
|
||||
};
|
||||
if (!sc_audio_player_init(&s->audio_player,
|
||||
&audio_player_cbs, NULL)) {
|
||||
goto end;
|
||||
}
|
||||
audio_player_initialized = true;
|
||||
|
||||
sc_decoder_add_sink(&s->audio_decoder, &s->audio_player.frame_sink);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
@ -630,24 +706,31 @@ aoa_hid_end:
|
||||
goto end;
|
||||
}
|
||||
|
||||
sc_decoder_add_sink(&s->decoder, &s->v4l2_sink.frame_sink);
|
||||
sc_decoder_add_sink(&s->video_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 demuxer
|
||||
if (!sc_demuxer_start(&s->demuxer)) {
|
||||
// start the video demuxer
|
||||
if (!sc_demuxer_start(&s->video_demuxer)) {
|
||||
goto end;
|
||||
}
|
||||
demuxer_started = true;
|
||||
video_demuxer_started = true;
|
||||
|
||||
if (options->audio) {
|
||||
if (!sc_demuxer_start(&s->audio_demuxer)) {
|
||||
goto end;
|
||||
}
|
||||
audio_demuxer_started = true;
|
||||
}
|
||||
|
||||
ret = event_loop(s);
|
||||
LOGD("quit...");
|
||||
|
||||
// Close the window immediately on closing, because screen_destroy() may
|
||||
// only be called once the demuxer thread is joined (it may take time)
|
||||
// only be called once the video demuxer thread is joined (it may take time)
|
||||
sc_screen_hide_window(&s->screen);
|
||||
|
||||
end:
|
||||
@ -674,6 +757,9 @@ 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);
|
||||
}
|
||||
@ -685,8 +771,12 @@ end:
|
||||
|
||||
// now that the sockets are shutdown, the demuxer and controller are
|
||||
// interrupted, we can join them
|
||||
if (demuxer_started) {
|
||||
sc_demuxer_join(&s->demuxer);
|
||||
if (video_demuxer_started) {
|
||||
sc_demuxer_join(&s->video_demuxer);
|
||||
}
|
||||
|
||||
if (audio_demuxer_started) {
|
||||
sc_demuxer_join(&s->audio_demuxer);
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
@ -705,8 +795,9 @@ end:
|
||||
}
|
||||
#endif
|
||||
|
||||
// Destroy the screen only after the demuxer is guaranteed to be finished,
|
||||
// because otherwise the screen could receive new frames after destruction
|
||||
// Destroy the screen only after the video 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);
|
||||
@ -719,15 +810,26 @@ end:
|
||||
sc_controller_destroy(&s->controller);
|
||||
}
|
||||
|
||||
if (recorder_started) {
|
||||
sc_recorder_join(&s->recorder);
|
||||
}
|
||||
if (recorder_initialized) {
|
||||
sc_recorder_destroy(&s->recorder);
|
||||
}
|
||||
|
||||
if (audio_player_initialized) {
|
||||
sc_audio_player_destroy(&s->audio_player);
|
||||
}
|
||||
|
||||
if (file_pusher_initialized) {
|
||||
sc_file_pusher_join(&s->file_pusher);
|
||||
sc_file_pusher_destroy(&s->file_pusher);
|
||||
}
|
||||
|
||||
if (server_started) {
|
||||
sc_server_join(&s->server);
|
||||
}
|
||||
|
||||
sc_server_destroy(&s->server);
|
||||
|
||||
return ret;
|
||||
|
@ -330,7 +330,10 @@ event_watcher(void *data, SDL_Event *event) {
|
||||
#endif
|
||||
|
||||
static bool
|
||||
sc_screen_frame_sink_open(struct sc_frame_sink *sink) {
|
||||
sc_screen_frame_sink_open(struct sc_frame_sink *sink,
|
||||
const AVCodecContext *ctx) {
|
||||
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||
|
||||
struct sc_screen *screen = DOWNCAST(sink);
|
||||
(void) screen;
|
||||
#ifndef NDEBUG
|
||||
@ -371,7 +374,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 EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||
// The SC_EVENT_NEW_FRAME triggered for the previous frame will consume
|
||||
// this new frame instead, unless the previous event failed
|
||||
need_new_event = screen->event_failed;
|
||||
} else {
|
||||
@ -380,7 +383,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 = EVENT_NEW_FRAME,
|
||||
.type = SC_EVENT_NEW_FRAME,
|
||||
};
|
||||
|
||||
// Post the event on the UI thread
|
||||
@ -820,7 +823,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 EVENT_NEW_FRAME: {
|
||||
case SC_EVENT_NEW_FRAME: {
|
||||
bool ok = sc_screen_update_frame(screen);
|
||||
if (!ok) {
|
||||
LOGW("Frame update failed\n");
|
||||
|
145
app/src/server.c
145
app/src/server.c
@ -8,6 +8,7 @@
|
||||
#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"
|
||||
@ -70,8 +71,10 @@ 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->codec_options);
|
||||
free((char *) params->encoder_name);
|
||||
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->tcpip_dst);
|
||||
}
|
||||
|
||||
@ -94,8 +97,10 @@ sc_server_params_copy(struct sc_server_params *dst,
|
||||
|
||||
COPY(req_serial);
|
||||
COPY(crop);
|
||||
COPY(codec_options);
|
||||
COPY(encoder_name);
|
||||
COPY(video_codec_options);
|
||||
COPY(audio_codec_options);
|
||||
COPY(video_encoder);
|
||||
COPY(audio_encoder);
|
||||
COPY(tcpip_dst);
|
||||
#undef COPY
|
||||
|
||||
@ -155,6 +160,24 @@ 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) {
|
||||
@ -198,10 +221,25 @@ execute_server(struct sc_server *server,
|
||||
cmd[count++] = p; \
|
||||
}
|
||||
|
||||
ADD_PARAM("uid=%08x", params->uid);
|
||||
ADD_PARAM("scid=%08x", params->scid);
|
||||
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);
|
||||
}
|
||||
@ -231,11 +269,17 @@ execute_server(struct sc_server *server,
|
||||
if (params->stay_awake) {
|
||||
ADD_PARAM("stay_awake=true");
|
||||
}
|
||||
if (params->codec_options) {
|
||||
ADD_PARAM("codec_options=%s", params->codec_options);
|
||||
if (params->video_codec_options) {
|
||||
ADD_PARAM("video_codec_options=%s", params->video_codec_options);
|
||||
}
|
||||
if (params->encoder_name) {
|
||||
ADD_PARAM("encoder_name=%s", params->encoder_name);
|
||||
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->power_off_on_close) {
|
||||
ADD_PARAM("power_off_on_close=true");
|
||||
@ -256,6 +300,12 @@ 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
|
||||
|
||||
@ -370,6 +420,7 @@ 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);
|
||||
@ -398,10 +449,9 @@ device_read_info(struct sc_intr *intr, sc_socket device_socket,
|
||||
buf[SC_DEVICE_NAME_FIELD_LENGTH - 1] = '\0';
|
||||
memcpy(info->device_name, (char *) buf, sizeof(info->device_name));
|
||||
|
||||
info->frame_size.width = (buf[SC_DEVICE_NAME_FIELD_LENGTH] << 8)
|
||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 1];
|
||||
info->frame_size.height = (buf[SC_DEVICE_NAME_FIELD_LENGTH + 2] << 8)
|
||||
| buf[SC_DEVICE_NAME_FIELD_LENGTH + 3];
|
||||
unsigned char *fields = &buf[SC_DEVICE_NAME_FIELD_LENGTH];
|
||||
info->frame_size.width = sc_read16be(fields);
|
||||
info->frame_size.height = sc_read16be(&fields[2]);
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -414,9 +464,11 @@ 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);
|
||||
@ -424,6 +476,14 @@ 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);
|
||||
@ -450,6 +510,18 @@ 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
|
||||
@ -476,9 +548,11 @@ 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;
|
||||
@ -490,6 +564,12 @@ 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");
|
||||
@ -769,8 +849,27 @@ 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->uid);
|
||||
params->scid);
|
||||
if (r == -1) {
|
||||
LOG_OOM();
|
||||
goto error_connection_failed;
|
||||
@ -778,11 +877,6 @@ 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);
|
||||
@ -835,6 +929,11 @@ 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);
|
||||
@ -887,7 +986,10 @@ 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);
|
||||
}
|
||||
|
||||
@ -896,6 +998,9 @@ 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);
|
||||
}
|
||||
|
@ -22,21 +22,27 @@ struct sc_server_info {
|
||||
};
|
||||
|
||||
struct sc_server_params {
|
||||
uint32_t uid;
|
||||
uint32_t scid;
|
||||
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 *codec_options;
|
||||
const char *encoder_name;
|
||||
const char *video_codec_options;
|
||||
const char *audio_codec_options;
|
||||
const char *video_encoder;
|
||||
const char *audio_encoder;
|
||||
struct sc_port_range port_range;
|
||||
uint32_t tunnel_host;
|
||||
uint16_t tunnel_port;
|
||||
uint16_t max_size;
|
||||
uint32_t bit_rate;
|
||||
uint32_t video_bit_rate;
|
||||
uint32_t audio_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;
|
||||
@ -49,6 +55,8 @@ struct sc_server_params {
|
||||
bool select_tcpip;
|
||||
bool cleanup;
|
||||
bool power_on;
|
||||
bool list_encoders;
|
||||
bool list_displays;
|
||||
};
|
||||
|
||||
struct sc_server {
|
||||
@ -68,6 +76,7 @@ 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;
|
||||
@ -107,6 +116,10 @@ sc_server_start(struct sc_server *server);
|
||||
void
|
||||
sc_server_stop(struct sc_server *server);
|
||||
|
||||
// join the server thread
|
||||
void
|
||||
sc_server_join(struct sc_server *server);
|
||||
|
||||
// close and release sockets
|
||||
void
|
||||
sc_server_destroy(struct sc_server *server);
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
#include <assert.h>
|
||||
#include <stdbool.h>
|
||||
#include <libavcodec/avcodec.h>
|
||||
|
||||
typedef struct AVFrame AVFrame;
|
||||
|
||||
@ -18,7 +19,7 @@ struct sc_frame_sink {
|
||||
};
|
||||
|
||||
struct sc_frame_sink_ops {
|
||||
bool (*open)(struct sc_frame_sink *sink);
|
||||
bool (*open)(struct sc_frame_sink *sink, const AVCodecContext *ctx);
|
||||
void (*close)(struct sc_frame_sink *sink);
|
||||
bool (*push)(struct sc_frame_sink *sink, const AVFrame *frame);
|
||||
};
|
||||
|
@ -19,9 +19,20 @@ 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
|
||||
|
@ -22,7 +22,7 @@ sc_usb_on_disconnected(struct sc_usb *usb, void *userdata) {
|
||||
(void) userdata;
|
||||
|
||||
SDL_Event event;
|
||||
event.type = EVENT_USB_DEVICE_DISCONNECTED;
|
||||
event.type = SC_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 EVENT_USB_DEVICE_DISCONNECTED:
|
||||
case SC_EVENT_USB_DEVICE_DISCONNECTED:
|
||||
LOGW("Device disconnected");
|
||||
return SCRCPY_EXIT_DISCONNECTED;
|
||||
case SDL_QUIT:
|
||||
|
114
app/src/util/bytebuf.c
Normal file
114
app/src/util/bytebuf.c
Normal file
@ -0,0 +1,114 @@
|
||||
#include "bytebuf.h"
|
||||
|
||||
#include <assert.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;
|
||||
}
|
104
app/src/util/bytebuf.h
Normal file
104
app/src/util/bytebuf.h
Normal file
@ -0,0 +1,104 @@
|
||||
#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
|
@ -4,6 +4,7 @@
|
||||
# include <windows.h>
|
||||
#endif
|
||||
#include <assert.h>
|
||||
#include <libavformat/avformat.h>
|
||||
|
||||
static SDL_LogPriority
|
||||
log_level_sc_to_sdl(enum sc_log_level level) {
|
||||
@ -47,6 +48,7 @@ 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
|
||||
@ -85,3 +87,46 @@ 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);
|
||||
}
|
||||
|
@ -35,4 +35,7 @@ bool
|
||||
sc_log_windows_error(const char *prefix, int error);
|
||||
#endif
|
||||
|
||||
void
|
||||
sc_log_configure();
|
||||
|
||||
#endif
|
||||
|
@ -156,7 +156,9 @@ 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) {
|
||||
sc_v4l2_sink_open(struct sc_v4l2_sink *vs, const AVCodecContext *ctx) {
|
||||
assert(ctx->pix_fmt == AV_PIX_FMT_YUV420P);
|
||||
|
||||
static const struct sc_video_buffer_callbacks cbs = {
|
||||
.on_new_frame = sc_video_buffer_on_new_frame,
|
||||
};
|
||||
|
154
app/tests/test_bytebuf.c
Normal file
154
app/tests/test_bytebuf.c
Normal file
@ -0,0 +1,154 @@
|
||||
#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;
|
||||
}
|
@ -46,7 +46,7 @@ static void test_options(void) {
|
||||
char *argv[] = {
|
||||
"scrcpy",
|
||||
"--always-on-top",
|
||||
"--bit-rate", "5M",
|
||||
"--video-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->bit_rate == 5000000);
|
||||
assert(opts->video_bit_rate == 5000000);
|
||||
assert(!strcmp(opts->crop, "100:200:300:400"));
|
||||
assert(opts->fullscreen);
|
||||
assert(opts->max_fps == 30);
|
||||
|
@ -90,13 +90,14 @@ 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 == 28);
|
||||
assert(size == 32);
|
||||
|
||||
const unsigned char expected[] = {
|
||||
SC_CONTROL_MSG_TYPE_INJECT_TOUCH_EVENT,
|
||||
@ -105,7 +106,8 @@ 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
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (action button)
|
||||
0x00, 0x00, 0x00, 0x01, // AMOTION_EVENT_BUTTON_PRIMARY (buttons)
|
||||
};
|
||||
assert(!memcmp(buf, expected, sizeof(expected)));
|
||||
}
|
||||
|
47
server/src/main/java/com/genymobile/scrcpy/AudioCodec.java
Normal file
47
server/src/main/java/com/genymobile/scrcpy/AudioCodec.java
Normal file
@ -0,0 +1,47 @@
|
||||
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;
|
||||
}
|
||||
}
|
376
server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java
Normal file
376
server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java
Normal file
@ -0,0 +1,376 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.annotation.TargetApi;
|
||||
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 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 CHANNELS = 1;
|
||||
|
||||
private static final int BUFFER_MS = 10; // milliseconds
|
||||
private static final int BUFFER_SIZE = SAMPLE_RATE * CHANNELS * 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(AudioFormat.ENCODING_PCM_16BIT);
|
||||
builder.setSampleRate(SAMPLE_RATE);
|
||||
builder.setChannelMask(CHANNELS == 2 ? AudioFormat.CHANNEL_IN_STEREO : AudioFormat.CHANNEL_IN_MONO);
|
||||
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());
|
||||
builder.setBufferSizeInBytes(1024 * 1024);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@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);
|
||||
recorder = createAudioRecord();
|
||||
|
||||
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);
|
||||
|
||||
recorder.startRecording();
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
@ -139,7 +139,7 @@ public final class CleanUp {
|
||||
builder.start();
|
||||
}
|
||||
|
||||
private static void unlinkSelf() {
|
||||
public static void unlinkSelf() {
|
||||
try {
|
||||
new File(SERVER_PATH).delete();
|
||||
} catch (Exception e) {
|
||||
|
17
server/src/main/java/com/genymobile/scrcpy/Codec.java
Normal file
17
server/src/main/java/com/genymobile/scrcpy/Codec.java
Normal file
@ -0,0 +1,17 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public interface Codec {
|
||||
|
||||
enum Type {
|
||||
VIDEO,
|
||||
AUDIO,
|
||||
}
|
||||
|
||||
Type getType();
|
||||
|
||||
int getId();
|
||||
|
||||
String getName();
|
||||
|
||||
String getMimeType();
|
||||
}
|
78
server/src/main/java/com/genymobile/scrcpy/CodecUtils.java
Normal file
78
server/src/main/java/com/genymobile/scrcpy/CodecUtils.java
Normal file
@ -0,0 +1,78 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
public class ConfigurationException extends Exception {
|
||||
public ConfigurationException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
@ -29,6 +29,7 @@ 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;
|
||||
@ -60,13 +61,15 @@ public final class ControlMessage {
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
public static ControlMessage createInjectTouchEvent(int action, long pointerId, Position position, float pressure, int actionButton,
|
||||
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;
|
||||
}
|
||||
@ -140,6 +143,10 @@ public final class ControlMessage {
|
||||
return keycode;
|
||||
}
|
||||
|
||||
public int getActionButton() {
|
||||
return actionButton;
|
||||
}
|
||||
|
||||
public int getButtons() {
|
||||
return buttons;
|
||||
}
|
||||
|
@ -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 = 27;
|
||||
static final int INJECT_TOUCH_EVENT_PAYLOAD_LENGTH = 31;
|
||||
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,8 +140,9 @@ 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, buttons);
|
||||
return ControlMessage.createInjectTouchEvent(action, pointerId, position, pressure, actionButton, buttons);
|
||||
}
|
||||
|
||||
private ControlMessage parseInjectScrollEvent() {
|
||||
|
@ -1,5 +1,7 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import com.genymobile.scrcpy.wrappers.InputManager;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.SystemClock;
|
||||
import android.view.InputDevice;
|
||||
@ -22,6 +24,8 @@ 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;
|
||||
@ -60,7 +64,7 @@ public class Controller {
|
||||
}
|
||||
}
|
||||
|
||||
public void control() throws IOException {
|
||||
private void control() throws IOException {
|
||||
// on start, power on the device
|
||||
if (powerOn && !Device.isScreenOn()) {
|
||||
device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC);
|
||||
@ -80,6 +84,34 @@ 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;
|
||||
}
|
||||
@ -99,7 +131,7 @@ public class Controller {
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_TOUCH_EVENT:
|
||||
if (device.supportsInputEvents()) {
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getButtons());
|
||||
injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons());
|
||||
}
|
||||
break;
|
||||
case ControlMessage.TYPE_INJECT_SCROLL_EVENT:
|
||||
@ -179,7 +211,7 @@ public class Controller {
|
||||
return successCount;
|
||||
}
|
||||
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int buttons) {
|
||||
private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) {
|
||||
long now = SystemClock.uptimeMillis();
|
||||
|
||||
Point point = device.getPhysicalPoint(position);
|
||||
@ -196,22 +228,23 @@ 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;
|
||||
@ -225,6 +258,62 @@ 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);
|
||||
|
@ -20,6 +20,9 @@ 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;
|
||||
@ -27,9 +30,10 @@ public final class DesktopConnection implements Closeable {
|
||||
private final ControlMessageReader reader = new ControlMessageReader();
|
||||
private final DeviceMessageWriter writer = new DeviceMessageWriter();
|
||||
|
||||
private DesktopConnection(LocalSocket videoSocket, LocalSocket controlSocket) throws IOException {
|
||||
private DesktopConnection(LocalSocket videoSocket, LocalSocket audioSocket, LocalSocket controlSocket) throws IOException {
|
||||
this.videoSocket = videoSocket;
|
||||
this.controlSocket = controlSocket;
|
||||
this.audioSocket = audioSocket;
|
||||
if (controlSocket != null) {
|
||||
controlInputStream = controlSocket.getInputStream();
|
||||
controlOutputStream = controlSocket.getOutputStream();
|
||||
@ -38,6 +42,7 @@ 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 {
|
||||
@ -46,20 +51,22 @@ public final class DesktopConnection implements Closeable {
|
||||
return localSocket;
|
||||
}
|
||||
|
||||
private static String getSocketName(int uid) {
|
||||
if (uid == -1) {
|
||||
// If no UID is set, use "scrcpy" to simplify using scrcpy-server alone
|
||||
private static String getSocketName(int scid) {
|
||||
if (scid == -1) {
|
||||
// If no SCID is set, use "scrcpy" to simplify using scrcpy-server alone
|
||||
return SOCKET_NAME_PREFIX;
|
||||
}
|
||||
|
||||
return SOCKET_NAME_PREFIX + String.format("_%08x", uid);
|
||||
return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
|
||||
}
|
||||
|
||||
public static DesktopConnection open(int uid, boolean tunnelForward, boolean control, boolean sendDummyByte) throws IOException {
|
||||
String socketName = getSocketName(uid);
|
||||
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException {
|
||||
String socketName = getSocketName(scid);
|
||||
|
||||
LocalSocket videoSocket;
|
||||
LocalSocket videoSocket = null;
|
||||
LocalSocket audioSocket = null;
|
||||
LocalSocket controlSocket = null;
|
||||
try {
|
||||
if (tunnelForward) {
|
||||
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
|
||||
videoSocket = localServerSocket.accept();
|
||||
@ -67,28 +74,36 @@ public final class DesktopConnection implements Closeable {
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
videoSocket.getOutputStream().write(0);
|
||||
}
|
||||
if (control) {
|
||||
try {
|
||||
controlSocket = localServerSocket.accept();
|
||||
} catch (IOException | RuntimeException e) {
|
||||
videoSocket.close();
|
||||
throw e;
|
||||
if (audio) {
|
||||
audioSocket = localServerSocket.accept();
|
||||
}
|
||||
if (control) {
|
||||
controlSocket = localServerSocket.accept();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
videoSocket = connect(socketName);
|
||||
if (audio) {
|
||||
audioSocket = connect(socketName);
|
||||
}
|
||||
if (control) {
|
||||
try {
|
||||
controlSocket = connect(socketName);
|
||||
}
|
||||
}
|
||||
} catch (IOException | RuntimeException e) {
|
||||
if (videoSocket != null) {
|
||||
videoSocket.close();
|
||||
}
|
||||
if (audioSocket != null) {
|
||||
audioSocket.close();
|
||||
}
|
||||
if (controlSocket != null) {
|
||||
controlSocket.close();
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new DesktopConnection(videoSocket, controlSocket);
|
||||
return new DesktopConnection(videoSocket, audioSocket, controlSocket);
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
@ -121,6 +136,10 @@ 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) {
|
||||
|
@ -61,12 +61,12 @@ public final class Device {
|
||||
|
||||
private final boolean supportsInputEvents;
|
||||
|
||||
public Device(Options options) {
|
||||
public Device(Options options) throws ConfigurationException {
|
||||
displayId = options.getDisplayId();
|
||||
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
|
||||
if (displayInfo == null) {
|
||||
int[] displayIds = ServiceManager.getDisplayManager().getDisplayIds();
|
||||
throw new InvalidDisplayIdException(displayId, displayIds);
|
||||
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
|
||||
throw new ConfigurationException("Unknown display id: " + displayId);
|
||||
}
|
||||
|
||||
int displayInfoFlags = displayInfo.getFlags();
|
||||
@ -277,6 +277,26 @@ 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");
|
||||
|
@ -6,6 +6,8 @@ public final class DeviceMessageSender {
|
||||
|
||||
private final DesktopConnection connection;
|
||||
|
||||
private Thread thread;
|
||||
|
||||
private String clipboardText;
|
||||
|
||||
private long ack;
|
||||
@ -24,7 +26,7 @@ public final class DeviceMessageSender {
|
||||
notify();
|
||||
}
|
||||
|
||||
public void loop() throws IOException, InterruptedException {
|
||||
private void loop() throws IOException, InterruptedException {
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
String text;
|
||||
long sequence;
|
||||
@ -49,4 +51,28 @@ 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
40
server/src/main/java/com/genymobile/scrcpy/FakeContext.java
Normal file
40
server/src/main/java/com/genymobile/scrcpy/FakeContext.java
Normal file
@ -0,0 +1,40 @@
|
||||
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";
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
@ -48,4 +48,9 @@ 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;
|
||||
}
|
||||
}
|
||||
|
@ -1,21 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
63
server/src/main/java/com/genymobile/scrcpy/LogUtils.java
Normal file
63
server/src/main/java/com/genymobile/scrcpy/LogUtils.java
Normal file
@ -0,0 +1,63 @@
|
||||
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();
|
||||
}
|
||||
}
|
@ -5,10 +5,15 @@ import android.graphics.Rect;
|
||||
import java.util.List;
|
||||
|
||||
public class Options {
|
||||
|
||||
private Ln.Level logLevel = Ln.Level.DEBUG;
|
||||
private int uid = -1; // 31-bit non-negative value, or -1
|
||||
private int scid = -1; // 31-bit non-negative value, or -1
|
||||
private boolean audio = true;
|
||||
private int maxSize;
|
||||
private int bitRate = 8000000;
|
||||
private VideoCodec videoCodec = VideoCodec.H264;
|
||||
private AudioCodec audioCodec = AudioCodec.OPUS;
|
||||
private int videoBitRate = 8000000;
|
||||
private int audioBitRate = 196000;
|
||||
private int maxFps;
|
||||
private int lockVideoOrientation = -1;
|
||||
private boolean tunnelForward;
|
||||
@ -17,18 +22,25 @@ public class Options {
|
||||
private int displayId;
|
||||
private boolean showTouches;
|
||||
private boolean stayAwake;
|
||||
private List<CodecOption> codecOptions;
|
||||
private String encoderName;
|
||||
private List<CodecOption> videoCodecOptions;
|
||||
private List<CodecOption> audioCodecOptions;
|
||||
|
||||
private String videoEncoder;
|
||||
private String audioEncoder;
|
||||
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;
|
||||
@ -38,12 +50,20 @@ public class Options {
|
||||
this.logLevel = logLevel;
|
||||
}
|
||||
|
||||
public int getUid() {
|
||||
return uid;
|
||||
public int getScid() {
|
||||
return scid;
|
||||
}
|
||||
|
||||
public void setUid(int uid) {
|
||||
this.uid = uid;
|
||||
public void setScid(int scid) {
|
||||
this.scid = scid;
|
||||
}
|
||||
|
||||
public boolean getAudio() {
|
||||
return audio;
|
||||
}
|
||||
|
||||
public void setAudio(boolean audio) {
|
||||
this.audio = audio;
|
||||
}
|
||||
|
||||
public int getMaxSize() {
|
||||
@ -54,12 +74,36 @@ public class Options {
|
||||
this.maxSize = maxSize;
|
||||
}
|
||||
|
||||
public int getBitRate() {
|
||||
return bitRate;
|
||||
public VideoCodec getVideoCodec() {
|
||||
return videoCodec;
|
||||
}
|
||||
|
||||
public void setBitRate(int bitRate) {
|
||||
this.bitRate = 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 int getMaxFps() {
|
||||
@ -126,20 +170,36 @@ public class Options {
|
||||
this.stayAwake = stayAwake;
|
||||
}
|
||||
|
||||
public List<CodecOption> getCodecOptions() {
|
||||
return codecOptions;
|
||||
public List<CodecOption> getVideoCodecOptions() {
|
||||
return videoCodecOptions;
|
||||
}
|
||||
|
||||
public void setCodecOptions(List<CodecOption> codecOptions) {
|
||||
this.codecOptions = codecOptions;
|
||||
public void setVideoCodecOptions(List<CodecOption> videoCodecOptions) {
|
||||
this.videoCodecOptions = videoCodecOptions;
|
||||
}
|
||||
|
||||
public String getEncoderName() {
|
||||
return encoderName;
|
||||
public List<CodecOption> getAudioCodecOptions() {
|
||||
return audioCodecOptions;
|
||||
}
|
||||
|
||||
public void setEncoderName(String encoderName) {
|
||||
this.encoderName = 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 setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
|
||||
@ -182,6 +242,22 @@ 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;
|
||||
}
|
||||
@ -205,4 +281,12 @@ public class Options {
|
||||
public void setSendDummyByte(boolean sendDummyByte) {
|
||||
this.sendDummyByte = sendDummyByte;
|
||||
}
|
||||
|
||||
public boolean getSendCodecId() {
|
||||
return sendCodecId;
|
||||
}
|
||||
|
||||
public void setSendCodecId(boolean sendCodecId) {
|
||||
this.sendCodecId = sendCodecId;
|
||||
}
|
||||
}
|
||||
|
@ -5,17 +5,14 @@ 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;
|
||||
|
||||
@ -27,27 +24,26 @@ 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 long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||
private static final int MAX_CONSECUTIVE_ERRORS = 3;
|
||||
|
||||
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 bitRate;
|
||||
private final int videoBitRate;
|
||||
private final int maxFps;
|
||||
private final boolean sendFrameMeta;
|
||||
private final boolean downsizeOnError;
|
||||
private long ptsOrigin;
|
||||
|
||||
private boolean firstFrameSent;
|
||||
private int consecutiveErrors;
|
||||
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||
public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
|
||||
boolean downsizeOnError) {
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
this.bitRate = bitRate;
|
||||
this.device = device;
|
||||
this.streamer = streamer;
|
||||
this.videoBitRate = videoBitRate;
|
||||
this.maxFps = maxFps;
|
||||
this.codecOptions = codecOptions;
|
||||
this.encoderName = encoderName;
|
||||
@ -63,22 +59,15 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return rotationChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
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);
|
||||
public void streamScreen() throws IOException, ConfigurationException {
|
||||
Codec codec = streamer.getCodec();
|
||||
MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
|
||||
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
|
||||
IBinder display = createDisplay();
|
||||
device.setRotationListener(this);
|
||||
|
||||
streamer.writeHeader();
|
||||
|
||||
boolean alive;
|
||||
try {
|
||||
do {
|
||||
@ -92,8 +81,8 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
Surface surface = null;
|
||||
try {
|
||||
codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
surface = codec.createInputSurface();
|
||||
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
|
||||
surface = mediaCodec.createInputSurface();
|
||||
|
||||
// does not include the locked video orientation
|
||||
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
|
||||
@ -101,42 +90,65 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
int layerStack = device.getLayerStack();
|
||||
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
|
||||
|
||||
codec.start();
|
||||
mediaCodec.start();
|
||||
|
||||
alive = encode(codec, fd);
|
||||
alive = encode(mediaCodec, streamer);
|
||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||
codec.stop();
|
||||
mediaCodec.stop();
|
||||
} catch (IllegalStateException | IllegalArgumentException e) {
|
||||
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
|
||||
if (!downsizeOnError || firstFrameSent) {
|
||||
// Fail immediately
|
||||
if (!prepareRetry(device, screenInfo)) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
int newMaxSize = chooseMaxSizeFallback(screenInfo.getVideoSize());
|
||||
if (newMaxSize == 0) {
|
||||
// Definitively fail
|
||||
throw e;
|
||||
}
|
||||
|
||||
// Retry with a smaller device size
|
||||
Ln.i("Retrying with -m" + newMaxSize + "...");
|
||||
device.setMaxSize(newMaxSize);
|
||||
Ln.i("Retrying...");
|
||||
alive = true;
|
||||
} finally {
|
||||
codec.reset();
|
||||
mediaCodec.reset();
|
||||
if (surface != null) {
|
||||
surface.release();
|
||||
}
|
||||
}
|
||||
} while (alive);
|
||||
} finally {
|
||||
codec.release();
|
||||
mediaCodec.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) {
|
||||
// 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) {
|
||||
// Must definitively fail
|
||||
return false;
|
||||
}
|
||||
|
||||
// Retry with a smaller device size
|
||||
Ln.i("Retrying with -m" + newMaxSize + "...");
|
||||
device.setMaxSize(newMaxSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int chooseMaxSizeFallback(Size failedSize) {
|
||||
int currentMaxSize = Math.max(failedSize.getWidth(), failedSize.getHeight());
|
||||
for (int value : MAX_SIZE_FALLBACK) {
|
||||
@ -149,30 +161,30 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return 0;
|
||||
}
|
||||
|
||||
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
|
||||
private boolean encode(MediaCodec codec, Streamer streamer) 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);
|
||||
|
||||
if (sendFrameMeta) {
|
||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||
}
|
||||
|
||||
IO.writeFully(fd, codecBuffer);
|
||||
if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) == 0) {
|
||||
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;
|
||||
}
|
||||
|
||||
streamer.writePacket(codecBuffer, bufferInfo);
|
||||
}
|
||||
} finally {
|
||||
if (outputBufferId >= 0) {
|
||||
@ -184,74 +196,24 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
return !eof;
|
||||
}
|
||||
|
||||
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 {
|
||||
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
|
||||
if (encoderName != null) {
|
||||
Ln.d("Creating encoder by name: '" + encoderName + "'");
|
||||
try {
|
||||
return MediaCodec.createByCodecName(encoderName);
|
||||
} catch (IllegalArgumentException e) {
|
||||
MediaCodecInfo[] encoders = listEncoders();
|
||||
throw new InvalidEncoderException(encoderName, encoders);
|
||||
Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
|
||||
throw new ConfigurationException("Unknown encoder: " + encoderName);
|
||||
}
|
||||
}
|
||||
MediaCodec codec = MediaCodec.createEncoderByType(MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
Ln.d("Using encoder: '" + codec.getName() + "'");
|
||||
return codec;
|
||||
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
|
||||
Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
|
||||
return mediaCodec;
|
||||
}
|
||||
|
||||
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) {
|
||||
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {
|
||||
MediaFormat format = new MediaFormat();
|
||||
format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_VIDEO_AVC);
|
||||
format.setString(MediaFormat.KEY_MIME, videoMimeType);
|
||||
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);
|
||||
@ -268,7 +230,10 @@ public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
if (codecOptions != null) {
|
||||
for (CodecOption option : codecOptions) {
|
||||
setCodecOption(format, option);
|
||||
String key = option.getKey();
|
||||
Object value = option.getValue();
|
||||
CodecUtils.setCodecOption(format, key, value);
|
||||
Ln.d("Video codec option set: " + key + " (" + value.getClass().getSimpleName() + ") = " + value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.os.BatteryManager;
|
||||
import android.os.Build;
|
||||
|
||||
@ -59,52 +58,96 @@ public final class Server {
|
||||
}
|
||||
}
|
||||
|
||||
private static void scrcpy(Options options) throws IOException {
|
||||
private static void scrcpy(Options options) throws IOException, ConfigurationException {
|
||||
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 uid = options.getUid();
|
||||
int scid = options.getScid();
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
boolean control = options.getControl();
|
||||
boolean audio = options.getAudio();
|
||||
boolean sendDummyByte = options.getSendDummyByte();
|
||||
|
||||
try (DesktopConnection connection = DesktopConnection.open(uid, tunnelForward, control, sendDummyByte)) {
|
||||
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)) {
|
||||
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) {
|
||||
final Controller controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||
controller = new Controller(device, connection, options.getClipboardAutosync(), options.getPowerOn());
|
||||
controller.start();
|
||||
|
||||
// asynchronous
|
||||
controllerThread = startController(controller);
|
||||
deviceMessageSenderThread = startDeviceMessageSender(controller.getSender());
|
||||
final Controller controllerRef = controller;
|
||||
device.setClipboardListener(text -> controllerRef.getSender().pushClipboardText(text));
|
||||
}
|
||||
|
||||
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();
|
||||
} 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);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Ln.d("Screen streaming stopped");
|
||||
initThread.interrupt();
|
||||
if (audioEncoder != null) {
|
||||
audioEncoder.stop();
|
||||
}
|
||||
if (controller != null) {
|
||||
controller.stop();
|
||||
}
|
||||
|
||||
try {
|
||||
// synchronous
|
||||
screenEncoder.streamScreen(device, connection.getVideoFd());
|
||||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Screen streaming stopped");
|
||||
} finally {
|
||||
initThread.interrupt();
|
||||
if (controllerThread != null) {
|
||||
controllerThread.interrupt();
|
||||
initThread.join();
|
||||
if (audioEncoder != null) {
|
||||
audioEncoder.join();
|
||||
}
|
||||
if (deviceMessageSenderThread != null) {
|
||||
deviceMessageSenderThread.interrupt();
|
||||
if (controller != null) {
|
||||
controller.join();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -115,32 +158,7 @@ public final class Server {
|
||||
return thread;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@SuppressWarnings("MethodLength")
|
||||
private static Options createOptions(String... args) {
|
||||
if (args.length < 1) {
|
||||
throw new IllegalArgumentException("Missing client version");
|
||||
@ -163,24 +181,46 @@ public final class Server {
|
||||
String key = arg.substring(0, equalIndex);
|
||||
String value = arg.substring(equalIndex + 1);
|
||||
switch (key) {
|
||||
case "uid":
|
||||
int uid = Integer.parseInt(value, 0x10);
|
||||
if (uid < -1) {
|
||||
throw new IllegalArgumentException("uid may not be negative (except -1 for 'none'): " + uid);
|
||||
case "scid":
|
||||
int scid = Integer.parseInt(value, 0x10);
|
||||
if (scid < -1) {
|
||||
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
|
||||
}
|
||||
options.setUid(uid);
|
||||
options.setScid(scid);
|
||||
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 "bit_rate":
|
||||
int bitRate = Integer.parseInt(value);
|
||||
options.setBitRate(bitRate);
|
||||
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);
|
||||
break;
|
||||
case "max_fps":
|
||||
int maxFps = Integer.parseInt(value);
|
||||
@ -214,15 +254,23 @@ public final class Server {
|
||||
boolean stayAwake = Boolean.parseBoolean(value);
|
||||
options.setStayAwake(stayAwake);
|
||||
break;
|
||||
case "codec_options":
|
||||
List<CodecOption> codecOptions = CodecOption.parse(value);
|
||||
options.setCodecOptions(codecOptions);
|
||||
case "video_codec_options":
|
||||
List<CodecOption> videoCodecOptions = CodecOption.parse(value);
|
||||
options.setVideoCodecOptions(videoCodecOptions);
|
||||
break;
|
||||
case "encoder_name":
|
||||
case "audio_codec_options":
|
||||
List<CodecOption> audioCodecOptions = CodecOption.parse(value);
|
||||
options.setAudioCodecOptions(audioCodecOptions);
|
||||
break;
|
||||
case "video_encoder":
|
||||
if (!value.isEmpty()) {
|
||||
options.setEncoderName(value);
|
||||
options.setVideoEncoder(value);
|
||||
}
|
||||
break;
|
||||
case "audio_encoder":
|
||||
if (!value.isEmpty()) {
|
||||
options.setAudioEncoder(value);
|
||||
}
|
||||
case "power_off_on_close":
|
||||
boolean powerOffScreenOnClose = Boolean.parseBoolean(value);
|
||||
options.setPowerOffScreenOnClose(powerOffScreenOnClose);
|
||||
@ -243,6 +291,14 @@ 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);
|
||||
@ -255,12 +311,17 @@ 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:
|
||||
@ -288,38 +349,35 @@ 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
129
server/src/main/java/com/genymobile/scrcpy/Streamer.java
Normal file
129
server/src/main/java/com/genymobile/scrcpy/Streamer.java
Normal file
@ -0,0 +1,129 @@
|
||||
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);
|
||||
}
|
||||
}
|
48
server/src/main/java/com/genymobile/scrcpy/VideoCodec.java
Normal file
48
server/src/main/java/com/genymobile/scrcpy/VideoCodec.java
Normal file
@ -0,0 +1,48 @@
|
||||
package com.genymobile.scrcpy;
|
||||
|
||||
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),
|
||||
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;
|
||||
}
|
||||
}
|
@ -2,14 +2,12 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.Application;
|
||||
import android.app.Instrumentation;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
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() {
|
||||
@ -50,7 +48,7 @@ public final class Workarounds {
|
||||
Object appBindData = appBindDataConstructor.newInstance();
|
||||
|
||||
ApplicationInfo applicationInfo = new ApplicationInfo();
|
||||
applicationInfo.packageName = "com.genymobile.scrcpy";
|
||||
applicationInfo.packageName = FakeContext.PACKAGE_NAME;
|
||||
|
||||
// appBindData.appInfo = applicationInfo;
|
||||
Field appInfoField = appBindDataClass.getDeclaredField("appInfo");
|
||||
@ -62,11 +60,10 @@ public final class Workarounds {
|
||||
mBoundApplicationField.setAccessible(true);
|
||||
mBoundApplicationField.set(activityThread, appBindData);
|
||||
|
||||
// Context ctx = activityThread.getSystemContext();
|
||||
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
||||
Context ctx = (Context) getSystemContextMethod.invoke(activityThread);
|
||||
|
||||
Application app = Instrumentation.newApplication(Application.class, ctx);
|
||||
Application app = Application.class.newInstance();
|
||||
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||
baseField.setAccessible(true);
|
||||
baseField.set(app, FakeContext.get());
|
||||
|
||||
// activityThread.mInitialApplication = app;
|
||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
||||
|
@ -5,6 +5,7 @@ import com.genymobile.scrcpy.Ln;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.os.IInterface;
|
||||
import android.os.Process;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
@ -48,10 +49,10 @@ public class ActivityManager {
|
||||
Object[] args;
|
||||
if (getContentProviderExternalMethodNewVersion) {
|
||||
// new version
|
||||
args = new Object[]{name, ServiceManager.USER_ID, token, null};
|
||||
args = new Object[]{name, Process.ROOT_UID, token, null};
|
||||
} else {
|
||||
// old version
|
||||
args = new Object[]{name, ServiceManager.USER_ID, token};
|
||||
args = new Object[]{name, Process.ROOT_UID, token};
|
||||
}
|
||||
// ContentProviderHolder providerHolder = getContentProviderExternal(...);
|
||||
Object providerHolder = method.invoke(manager, args);
|
||||
|
@ -1,11 +1,13 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.FakeContext;
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
|
||||
import android.content.ClipData;
|
||||
import android.content.IOnPrimaryClipChangedListener;
|
||||
import android.os.Build;
|
||||
import android.os.IInterface;
|
||||
import android.os.Process;
|
||||
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.Method;
|
||||
@ -58,22 +60,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, ServiceManager.PACKAGE_NAME);
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME);
|
||||
}
|
||||
if (alternativeMethod) {
|
||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||
}
|
||||
return (ClipData) method.invoke(manager, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||
}
|
||||
|
||||
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, ServiceManager.PACKAGE_NAME);
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME);
|
||||
} else if (alternativeMethod) {
|
||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||
} else {
|
||||
method.invoke(manager, clipData, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||
}
|
||||
}
|
||||
|
||||
@ -106,11 +108,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, ServiceManager.PACKAGE_NAME);
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME);
|
||||
} else if (alternativeMethod) {
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, null, ServiceManager.USER_ID);
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, Process.ROOT_UID);
|
||||
} else {
|
||||
method.invoke(manager, listener, ServiceManager.PACKAGE_NAME, ServiceManager.USER_ID);
|
||||
method.invoke(manager, listener, FakeContext.PACKAGE_NAME, Process.ROOT_UID);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,15 @@
|
||||
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;
|
||||
import android.os.Process;
|
||||
|
||||
import java.io.Closeable;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
@ -51,11 +55,10 @@ public class ContentProvider implements Closeable {
|
||||
@SuppressLint("PrivateApi")
|
||||
private Method getCallMethod() throws NoSuchMethodException {
|
||||
if (callMethod == null) {
|
||||
try {
|
||||
Class<?> attributionSourceClass = Class.forName("android.content.AttributionSource");
|
||||
callMethod = provider.getClass().getMethod("call", attributionSourceClass, String.class, String.class, String.class, Bundle.class);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class);
|
||||
callMethodVersion = 0;
|
||||
} catch (NoSuchMethodException | ClassNotFoundException e0) {
|
||||
} else {
|
||||
// old versions
|
||||
try {
|
||||
callMethod = provider.getClass()
|
||||
@ -75,40 +78,29 @@ 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 ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
|
||||
throws NoSuchMethodException, InvocationTargetException, 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 0:
|
||||
args = new Object[]{getAttributionSource(), "settings", callMethod, arg, extras};
|
||||
break;
|
||||
case 1:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
||||
args = new Object[]{FakeContext.PACKAGE_NAME, null, "settings", callMethod, arg, extras};
|
||||
break;
|
||||
case 2:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||
args = new Object[]{FakeContext.PACKAGE_NAME, "settings", callMethod, arg, extras};
|
||||
break;
|
||||
default:
|
||||
args = new Object[]{ServiceManager.PACKAGE_NAME, callMethod, arg, extras};
|
||||
args = new Object[]{FakeContext.PACKAGE_NAME, callMethod, arg, extras};
|
||||
break;
|
||||
}
|
||||
}
|
||||
return (Bundle) method.invoke(provider, args);
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException | ClassNotFoundException | InstantiationException e) {
|
||||
} catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) {
|
||||
Ln.e("Could not invoke method", e);
|
||||
throw e;
|
||||
}
|
||||
@ -147,7 +139,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, ServiceManager.USER_ID);
|
||||
arg.putInt(CALL_METHOD_USER_KEY, Process.ROOT_UID);
|
||||
try {
|
||||
Bundle bundle = call(method, key, arg);
|
||||
if (bundle == null) {
|
||||
@ -163,7 +155,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, ServiceManager.USER_ID);
|
||||
arg.putInt(CALL_METHOD_USER_KEY, Process.ROOT_UID);
|
||||
arg.putString(NAME_VALUE_TABLE_VALUE, value);
|
||||
try {
|
||||
call(method, key, arg);
|
||||
|
@ -3,6 +3,7 @@ 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;
|
||||
@ -17,6 +18,7 @@ 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;
|
||||
@ -56,4 +58,22 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,6 @@ 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 {
|
||||
|
@ -30,6 +30,8 @@ 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
|
||||
@ -98,7 +100,6 @@ public final class SurfaceControl {
|
||||
}
|
||||
|
||||
public static IBinder getBuiltInDisplay() {
|
||||
|
||||
try {
|
||||
Method method = getGetBuiltInDisplayMethod();
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
|
||||
@ -114,6 +115,40 @@ 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);
|
||||
|
@ -94,7 +94,8 @@ public class ControlMessageReaderTest {
|
||||
dos.writeShort(1080);
|
||||
dos.writeShort(1920);
|
||||
dos.writeShort(0xffff); // pressure
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY);
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // action button
|
||||
dos.writeInt(MotionEvent.BUTTON_PRIMARY); // buttons
|
||||
|
||||
byte[] packet = bos.toByteArray();
|
||||
|
||||
@ -112,6 +113,7 @@ 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());
|
||||
}
|
||||
|
||||
|
BIN
u/.ninja_deps
Normal file
BIN
u/.ninja_deps
Normal file
Binary file not shown.
62
u/.ninja_log
Normal file
62
u/.ninja_log
Normal file
@ -0,0 +1,62 @@
|
||||
# ninja log v5
|
||||
0 243 1542027815 build.ninja ef03cd8523486e97
|
||||
0 58 1542027815 app/app@@scrcpy@exe/src_str_util.c.o 2aa692e7aa83914
|
||||
0 87 1542027815 app/app@@scrcpy@exe/src_sys_unix_net.c.o 7ea14bd07e90ff97
|
||||
0 91 1542027815 app/app@@scrcpy@exe/src_sys_unix_command.c.o dd44ba15cc3d6a7e
|
||||
1 113 1542027815 app/app@@scrcpy@exe/src_command.c.o 1eaa0f061a5c0447
|
||||
0 114 1542027815 app/app@@scrcpy@exe/src_server.c.o 8b376071b5e0aaf1
|
||||
1 117 1542027815 app/app@@scrcpy@exe/src_controller.c.o 907de440054c77e7
|
||||
1 146 1542027815 app/app@@scrcpy@exe/src_control_event.c.o fcfa5a6c322ebf8b
|
||||
91 202 1542027815 app/app@@scrcpy@exe/src_device.c.o 9ac9441f4f2e4d54
|
||||
58 215 1542027815 app/app@@scrcpy@exe/src_convert.c.o 9de268e9b915094e
|
||||
115 219 1542027815 app/app@@scrcpy@exe/src_fps_counter.c.o 22b968c51acd256b
|
||||
114 235 1542027815 app/app@@scrcpy@exe/src_file_handler.c.o 11e303a26f189d9a
|
||||
117 286 1542027815 app/app@@scrcpy@exe/src_frames.c.o 3c5c4dbee035e5ab
|
||||
88 319 1542027815 app/app@@scrcpy@exe/src_decoder.c.o 60c1438cf7786895
|
||||
202 338 1542027815 app/app@@scrcpy@exe/src_lock_util.c.o 9265bcc92f144427
|
||||
215 367 1542027815 app/app@@scrcpy@exe/src_net.c.o 718f65aa73583163
|
||||
220 408 1542027815 app/app@@scrcpy@exe/src_recorder.c.o 676a7500fb0d45cb
|
||||
1 470 1542027815 app/app@@scrcpy@exe/src_tiny_xpm.c.o 91851ad29940a4b1
|
||||
286 485 1542027815 app/app@@test_control_event_queue@exe/tests_test_control_event_queue.c.o 46bff52a98c0b5ca
|
||||
319 487 1542027815 app/app@@test_control_event_queue@exe/src_control_event.c.o 76492af89a914173
|
||||
1 488 1542027815 app/app@@scrcpy@exe/src_main.c.o e7dc8583797471c5
|
||||
367 488 1542027815 app/app@@test_control_event_serialize@exe/src_control_event.c.o fcff2e1105474edf
|
||||
0 497 1542027815 app/app@@scrcpy@exe/src_screen.c.o 329c18ec2111c8ff
|
||||
408 515 1542027815 app/app@@test_strutil@exe/tests_test_strutil.c.o 15440f4bca20c50d
|
||||
338 517 1542027815 app/app@@test_control_event_serialize@exe/tests_test_control_event_serialize.c.o baa0b48891372fcc
|
||||
470 525 1542027815 app/app@@test_strutil@exe/src_str_util.c.o fcb3a91d36e23e11
|
||||
525 561 1542027815 app/test_strutil 3448478dadf99adf
|
||||
487 582 1542027815 app/test_control_event_queue bfca00bc894d3c4f
|
||||
517 606 1542027815 app/test_control_event_serialize e06ab4ce04dd4fad
|
||||
147 638 1542027815 app/app@@scrcpy@exe/src_input_manager.c.o 1fe285b256bf5908
|
||||
236 713 1542027815 app/app@@scrcpy@exe/src_scrcpy.c.o 8b0bae90b272da98
|
||||
713 891 1542027816 app/scrcpy 8fba96817bb2802c
|
||||
485 5716 1542027820 server/scrcpy-server.jar 8511d30842df298f
|
||||
0 264 1542027826 build.ninja ef03cd8523486e97
|
||||
1 31 1542027826 app/app@@scrcpy@exe/src_fps_counter.c.o 22b968c51acd256b
|
||||
1 44 1542027826 app/app@@scrcpy@exe/src_file_handler.c.o 11e303a26f189d9a
|
||||
1 47 1542027826 app/app@@scrcpy@exe/src_controller.c.o 907de440054c77e7
|
||||
2 50 1542027826 app/app@@scrcpy@exe/src_frames.c.o 3c5c4dbee035e5ab
|
||||
2 50 1542027826 app/app@@scrcpy@exe/src_recorder.c.o 676a7500fb0d45cb
|
||||
1 65 1542027826 app/app@@scrcpy@exe/src_decoder.c.o 60c1438cf7786895
|
||||
31 82 1542027826 app/app@@scrcpy@exe/src_server.c.o 8b376071b5e0aaf1
|
||||
2 108 1542027826 app/app@@scrcpy@exe/src_input_manager.c.o 1fe285b256bf5908
|
||||
2 129 1542027826 app/app@@scrcpy@exe/src_screen.c.o 329c18ec2111c8ff
|
||||
2 162 1542027826 app/app@@scrcpy@exe/src_scrcpy.c.o 8b0bae90b272da98
|
||||
1 339 1542027826 app/app@@scrcpy@exe/src_main.c.o e7dc8583797471c5
|
||||
339 538 1542027827 app/scrcpy 8fba96817bb2802c
|
||||
44 753 1542027827 server/scrcpy-server.jar 8511d30842df298f
|
||||
0 276 1542027871 build.ninja ef03cd8523486e97
|
||||
1 37 1542027872 app/app@@scrcpy@exe/src_file_handler.c.o 11e303a26f189d9a
|
||||
1 42 1542027872 app/app@@scrcpy@exe/src_controller.c.o 907de440054c77e7
|
||||
1 45 1542027872 app/app@@scrcpy@exe/src_fps_counter.c.o 22b968c51acd256b
|
||||
2 49 1542027872 app/app@@scrcpy@exe/src_recorder.c.o 676a7500fb0d45cb
|
||||
1 52 1542027872 app/app@@scrcpy@exe/src_frames.c.o 3c5c4dbee035e5ab
|
||||
0 64 1542027872 app/app@@scrcpy@exe/src_decoder.c.o 60c1438cf7786895
|
||||
37 80 1542027872 app/app@@scrcpy@exe/src_server.c.o 8b376071b5e0aaf1
|
||||
1 128 1542027872 app/app@@scrcpy@exe/src_input_manager.c.o 1fe285b256bf5908
|
||||
2 138 1542027872 app/app@@scrcpy@exe/src_screen.c.o 329c18ec2111c8ff
|
||||
2 150 1542027872 app/app@@scrcpy@exe/src_scrcpy.c.o 8b0bae90b272da98
|
||||
1 370 1542027872 app/app@@scrcpy@exe/src_main.c.o e7dc8583797471c5
|
||||
370 578 1542027872 app/scrcpy 8fba96817bb2802c
|
||||
42 688 1542027872 server/scrcpy-server.jar 8511d30842df298f
|
BIN
u/app/app@@scrcpy@exe/src_command.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_command.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_control_event.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_control_event.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_controller.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_controller.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_convert.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_convert.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_decoder.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_decoder.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_device.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_device.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_file_handler.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_file_handler.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_fps_counter.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_fps_counter.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_frames.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_frames.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_input_manager.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_input_manager.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_lock_util.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_lock_util.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_main.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_main.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_net.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_net.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_recorder.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_recorder.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_scrcpy.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_scrcpy.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_screen.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_screen.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_server.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_server.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_str_util.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_str_util.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_sys_unix_command.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_sys_unix_command.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_sys_unix_net.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_sys_unix_net.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@scrcpy@exe/src_tiny_xpm.c.o
Normal file
BIN
u/app/app@@scrcpy@exe/src_tiny_xpm.c.o
Normal file
Binary file not shown.
BIN
u/app/app@@test_control_event_queue@exe/src_control_event.c.o
Normal file
BIN
u/app/app@@test_control_event_queue@exe/src_control_event.c.o
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user