Compare commits

...

31 Commits

Author SHA1 Message Date
9bff8ccadb Apply workaround on "honor" devices
This makes audio work on those devices.

Fixes #4015 <https://github.com/Genymobile/scrcpy/issues/4015>
2023-06-17 00:25:10 +02:00
0d4157357a Use system context as base context
DONOTMERGE: it causes #994 on Xiaomi devices

This allows to make Context.getPackageManager() work.

Fixes #4015 <https://github.com/Genymobile/scrcpy/issues/4015>
Refs <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1594262721>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-06-17 00:25:10 +02:00
95e61e2a0b Move ActivityThread instance
An ActivityThread instance will be needed from several classes.
2023-06-17 00:25:10 +02:00
48a00fb481 Log device BRAND
The BRAND value is not always the same as the MANUFACTURER value.
2023-06-17 00:25:01 +02:00
3b7e2ca9c8 Fix lint warning
Suppress lint "DiscouragedPrivateApi" in Workarounds.java.
2023-06-16 23:24:08 +02:00
5bd7514871 Add InputManagerGlobal for Android 14 beta 3
Parts of the InputManager class have been moved to a new
InputManagerGlobal class in Android 14 preview.

Fixes #4074 <https://github.com/Genymobile/scrcpy/issues/4074>
PR #4075 <https://github.com/Genymobile/scrcpy/pull/4075>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-06-13 15:19:24 +02:00
d3c2955fb9 Add --time-limit
Add an option to stop scrcpy automatically after a given delay.

PR #4052 <https://github.com/Genymobile/scrcpy/pull/4052>
Fixes #3752 <https://github.com/Genymobile/scrcpy/issues/3752>
2023-06-10 16:04:51 +02:00
5042f8de93 Improve recording documentation 2023-06-10 16:03:27 +02:00
7536f95d1c Rename raw_video_stream to raw_stream
This server-specific option impacts both the video and audio streams.
2023-06-10 12:09:43 +02:00
6832e8d629 Remove spurious empty line 2023-06-10 12:07:35 +02:00
28313631e5 Reformat Java code
Fix code style.
2023-06-09 22:28:01 +02:00
fdbc9397a7 Name Java threads
Give a user-friendly name to Java threads created by the server.
2023-06-09 22:27:35 +02:00
a3cdf1a6b8 Add option to kill adb on close
Killing adb on close by default would be incorrect, since it would break
any other usage of adb in parallel.

It could be easily done manually by calling "adb kill-server" once
scrcpy terminates, but add an option --kill-adb-on-close for
convenience.

Fixes #205 <https://github.com/Genymobile/scrcpy/issues/205>
Fixes #2580 <https://github.com/Genymobile/scrcpy/issues/2580>
Fixes #4049 <https://github.com/Genymobile/scrcpy/issues/4049>
2023-06-05 19:48:21 +02:00
b16d4d1835 Fix adb server vs adb daemon confusion
The adb daemon runs on the device, the adb server runs as a background
process on the computer.
2023-06-05 19:45:20 +02:00
b8d43866d2 Fix options alphabetical order
Commit fc52b24503 missed this one.
2023-06-05 19:44:15 +02:00
2d79aeb117 Simplify command in documentation
If --no-video is passed, --no-playback is equivalent to
--no-audio-playback.
2023-06-04 18:43:35 +02:00
888a5aae7d Fix typo in recording documentation
The option is --record, not --record-file.
2023-06-04 18:40:55 +02:00
323ea2f1d9 Fix PTS when not monotonically increasing
Some decoders fail to guarantee that PTS is strictly monotonically
increasing. Fix the (rescaled) PTS when it does not respect this
constraint.

Fixes #4054 <https://github.com/Genymobile/scrcpy/issues/4054>
2023-06-03 18:50:28 +02:00
9ca554ca41 Extract stream-specific structure in recorder
For now, it only contains the stream index, but more fields will be
added.
2023-06-03 18:48:01 +02:00
9d3c656414 Fix recorder waiting when stream disabled
In the recorder, if the video or audio stream is disabled, do not wait
for its initialization (it will never happen) to process the header.

In that case (scrcpy --no-audio --record=file.mp4), this caused the
whole content to be buffered in memory, and written only on exit.
2023-06-03 18:46:39 +02:00
379caf8551 Use a single condvar in recorder
The sc_cond_wait() in sc_recorder_process_header() needs to be notified
of changes to video_init/audio_init (protected by stream_cond) and
video_queue/audio_queue (protected by queue_cond).

Use only one condition variable to simplify.
2023-06-03 15:10:42 +02:00
2aec7b4c9d Mention how to interrupt scrcpy without video
There is no window to close if video playback is disabled.
2023-06-02 09:00:33 +02:00
fc52b24503 Reorder options in alphabetical order
Fix the options order, using the short option as key first (if any) in
all cases for consistency.
2023-06-01 12:52:48 +02:00
ff5ffc892f Add option to select audio source
Pass --audio-source=mic to capture the microphone instead of the device
audio output.
2023-06-01 09:21:09 +02:00
360f2fea1e Extract AudioCapture creation
This will allow to pass capture options without code duplication.
2023-06-01 09:21:09 +02:00
24999d0d32 Reset video capture on folding event
Handle folding event the same way as rotation events.

Fixes #3960 <https://github.com/Genymobile/scrcpy/issues/3960>
PR #3979 <https://github.com/Genymobile/scrcpy/pull/3979>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-06-01 09:20:00 +02:00
8e2c0d6407 Rename rotationChanged to resetCapture
The flag is used to reset the capture (restart the encoding) on rotation
change. It will also be used for other events (on folding change), so
rename it.

PR #3979 <https://github.com/Genymobile/scrcpy/pull/3979>
2023-06-01 09:20:00 +02:00
9a2abba098 Update demuxer comment
The comment was outdated:
 - the "meta" header is now always present (not only when recording is
   enabled);
 - it is not only used for the video stream, but also for the audio
   stream.
2023-06-01 09:04:00 +02:00
b2d860382f Fix stream offset on audio buffer underflow
The `read` variable is in number of samples, while the offset must be in
bytes.

PR #4045 <https://github.com/Genymobile/scrcpy/pull/4045>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-05-31 10:09:56 +02:00
4c4a03ebe1 Reorder options to maintain alphabetical order 2023-05-30 21:36:48 +02:00
Yan
798dfd240e Turn device screen off after set up
Sometimes it can take quite a while for everything to get set up and
the screen to appear.

PR #3902 <https://github.com/Genymobile/scrcpy/pull/3902>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-05-27 10:11:42 +02:00
39 changed files with 782 additions and 254 deletions

View File

@ -7,6 +7,7 @@ _scrcpy() {
--audio-codec=
--audio-codec-options=
--audio-encoder=
--audio-source=
--audio-output-buffer=
-b --video-bit-rate=
--crop=
@ -15,26 +16,27 @@ _scrcpy() {
--display=
--display-buffer=
-e --select-tcpip
-f --fullscreen
--force-adb-forward
--forward-all-clicks
-f --fullscreen
-K --hid-keyboard
-h --help
--kill-adb-on-close
-K --hid-keyboard
--legacy-paste
--list-displays
--list-encoders
--lock-video-orientation
--lock-video-orientation=
--max-fps=
-M --hid-mouse
-m --max-size=
-M --hid-mouse
--max-fps=
-n --no-control
-N --no-playback
--no-audio
--no-audio-playback
--no-cleanup
--no-clipboard-autosync
--no-downsize-on-error
-n --no-control
-N --no-playback
--no-key-repeat
--no-mipmaps
--no-power-on
@ -46,24 +48,25 @@ _scrcpy() {
--prefer-text
--print-fps
--push-target=
--raw-key-events
-r --record=
--raw-key-events
--record-format=
--render-driver=
--require-audio
--rotation=
-s --serial=
--shortcut-mod=
-S --turn-screen-off
--shortcut-mod=
-t --show-touches
--tcpip
--tcpip=
--time-limit=
--tunnel-host=
--tunnel-port=
--v4l2-buffer=
--v4l2-sink=
-V --verbosity=
-v --version
-V --verbosity=
--video-codec=
--video-codec-options=
--video-encoder=
@ -86,6 +89,10 @@ _scrcpy() {
COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur"))
return
;;
--audio-source)
COMPREPLY=($(compgen -W 'output mic' -- "$cur"))
return
;;
--lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur"))
return

View File

@ -14,6 +14,7 @@ arguments=(
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
'--audio-source=[Select the audio source]:source:(output mic)'
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
@ -22,25 +23,26 @@ arguments=(
'--display=[Specify the display id to mirror]'
'--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
{-e,--select-tcpip}'[Use TCP/IP device]'
{-f,--fullscreen}'[Start in fullscreen]'
'--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]'
'--kill-adb-on-close[Kill adb when scrcpy terminates]'
{-K,--hid-keyboard}'[Simulate a physical keyboard by using HID over AOAv2]'
'--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]'
{-M,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]'
'--max-fps=[Limit the frame rate of screen capture]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-playback}'[Disable video and audio playback]'
'--no-audio[Disable audio forwarding]'
'--no-audio-playback[Disable audio playback]'
'--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]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-playback}'[Disable video and audio playback]'
'--no-key-repeat[Do not forward repeated key events when a key is held down]'
'--no-mipmaps[Disable the generation of mipmaps]'
'--no-power-on[Do not power on the device on start]'
@ -52,23 +54,24 @@ arguments=(
'--prefer-text[Inject alpha characters and space as text events instead of key events]'
'--print-fps[Start FPS counter, to print frame logs to the console]'
'--push-target=[Set the target directory for pushing files to the device by drag and drop]'
'--raw-key-events[Inject key events for all input keys, and ignore text events]'
{-r,--record=}'[Record screen to file]:record file:_files'
'--raw-key-events[Inject key events for all input keys, and ignore text events]'
'--record-format=[Force recording format]:format:(mp4 mkv)'
'--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)'
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
'--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)'
{-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))'
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
{-S,--turn-screen-off}'[Turn the device screen off immediately]'
'--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)'
{-t,--show-touches}'[Show physical touches]'
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]'
'--time-limit=[Set the maximum mirroring time, in seconds]'
'--tunnel-host=[Set the IP address of the adb tunnel to reach the scrcpy server]'
'--tunnel-port=[Set the TCP port of the adb tunnel to reach the scrcpy server]'
'--v4l2-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]'
'--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]'
{-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)'
'--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]'

View File

@ -51,6 +51,7 @@ src = [
'src/util/term.c',
'src/util/thread.c',
'src/util/tick.c',
'src/util/timeout.c',
]
conf = configuration_data()

View File

@ -33,14 +33,6 @@ Lower values decrease the latency, but increase the likelyhood of buffer underru
Default is 50.
.TP
.BI "\-\-audio\-output\-buffer ms
Configure the size of the SDL audio output buffer (in milliseconds).
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
Default is 5.
.TP
.BI "\-\-audio\-codec " name
Select an audio codec (opus, aac or raw).
@ -63,6 +55,20 @@ Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\
The available encoders can be listed by \-\-list\-encoders.
.TP
.BI "\-\-audio\-source " source
Select the audio source (output or mic).
Default is output.
.TP
.BI "\-\-audio\-output\-buffer ms
Configure the size of the SDL audio output buffer (in milliseconds).
If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise.
Default is 5.
.TP
.BI "\-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).
@ -107,6 +113,10 @@ Use TCP/IP device (if there is exactly one, like adb -e).
Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
.TP
.B \-f, \-\-fullscreen
Start in fullscreen.
.TP
.B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device.
@ -115,14 +125,14 @@ Do not attempt to use "adb reverse" to connect to the device.
.B \-\-forward\-all\-clicks
By default, right-click triggers BACK (or POWER on) and middle-click triggers HOME. This option disables these shortcuts and forward the clicks to the device instead.
.TP
.B \-f, \-\-fullscreen
Start in fullscreen.
.TP
.B \-h, \-\-help
Print this help.
.TP
.B \-\-kill\-adb\-on\-close
Kill adb when scrcpy terminates.
.TP
.B \-K, \-\-hid\-keyboard
Simulate a physical keyboard by using HID over AOAv2.
@ -161,10 +171,6 @@ Default is "unlocked".
Passing the option without argument is equivalent to passing "initial".
.TP
.BI "\-\-max\-fps " value
Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).
.TP
.BI "\-m, \-\-max\-size " value
Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved.
@ -183,6 +189,18 @@ It may only work over USB.
Also see \fB\-\-hid\-keyboard\fR.
.TP
.BI "\-\-max\-fps " value
Limit the framerate of screen capture (officially supported since Android 10, but may work on earlier versions).
.TP
.B \-n, \-\-no\-control
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-playback
Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback).
.TP
.B \-\-no\-audio
Disable audio forwarding.
@ -209,14 +227,6 @@ By default, on MediaCodec error, scrcpy automatically tries again with a lower d
This option disables this behavior.
.TP
.B \-n, \-\-no\-control
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-playback
Disable video and audio playback on the computer (equivalent to --no-video-playback --no-audio-playback).
.TP
.B \-\-no\-key\-repeat
Do not forward repeated key events when a key is held down.
@ -278,10 +288,6 @@ Set the target directory for pushing files to the device by drag & drop. It is p
Default is "/sdcard/Download/".
.TP
.B \-\-raw\-key\-events
Inject key events for all input keys, and ignore text events.
.TP
.BI "\-r, \-\-record " file
Record screen to
@ -291,6 +297,10 @@ The format is determined by the
.B \-\-record\-format
option if set, or by the file extension (.mp4 or .mkv).
.TP
.B \-\-raw\-key\-events
Inject key events for all input keys, and ignore text events.
.TP
.BI "\-\-record\-format " format
Force recording format (either mp4 or mkv).
@ -316,6 +326,10 @@ Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each incre
.BI "\-s, \-\-serial " number
The device serial number. Mandatory only if several devices are connected to adb.
.TP
.B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately.
.TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
@ -326,6 +340,12 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
Default is "lalt,lsuper" (left-Alt or left-Super).
.TP
.B \-t, \-\-show\-touches
Enable "show touches" on start, restore the initial value on exit.
It only shows physical touches (not clicks from scrcpy).
.TP
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
Configure and reconnect the device over TCP/IP.
@ -335,14 +355,8 @@ If a destination address is provided, then scrcpy connects to this address befor
If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting.
.TP
.B \-S, \-\-turn\-screen\-off
Turn the device screen off immediately.
.TP
.B \-t, \-\-show\-touches
Enable "show touches" on start, restore the initial value on exit.
It only shows physical touches (not clicks from scrcpy).
.BI "\-\-time\-limit " seconds
Set the maximum mirroring time, in seconds.
.TP
.BI "\-\-tunnel\-host " ip
@ -356,6 +370,16 @@ Set the TCP port of the adb tunnel to reach the scrcpy server. This option autom
Default is 0 (not forced): the local port used for establishing the tunnel will be used.
.TP
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.BI "\-V, \-\-verbosity " value
Set the log level ("verbose", "debug", "info", "warn" or "error").
Default is "info" for release builds, "debug" for debug builds.
.TP
.BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device.
@ -370,16 +394,6 @@ This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
Default is 0 (no buffering).
.TP
.BI "\-V, \-\-verbosity " value
Set the log level ("verbose", "debug", "info", "warn" or "error").
Default is "info" for release builds, "debug" for debug builds.
.TP
.B \-v, \-\-version
Print the version of scrcpy.
.TP
.BI "\-\-video\-codec " name
Select a video codec (h264, h265 or av1).

View File

@ -107,7 +107,7 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) {
// latency.
LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples",
silence);
memset(stream + read, 0, TO_BYTES(silence));
memset(stream + TO_BYTES(read), 0, TO_BYTES(silence));
if (ap->received) {
// Inserting additional samples immediately increases buffering

View File

@ -76,6 +76,9 @@ enum {
OPT_NO_VIDEO,
OPT_NO_AUDIO_PLAYBACK,
OPT_NO_VIDEO_PLAYBACK,
OPT_AUDIO_SOURCE,
OPT_KILL_ADB_ON_CLOSE,
OPT_TIME_LIMIT,
};
struct sc_option {
@ -134,16 +137,6 @@ static const struct sc_option options[] = {
"likelyhood of buffer underrun (causing audio glitches).\n"
"Default is 50.",
},
{
.longopt_id = OPT_AUDIO_OUTPUT_BUFFER,
.longopt = "audio-output-buffer",
.argdesc = "ms",
.text = "Configure the size of the SDL audio output buffer (in "
"milliseconds).\n"
"If you get \"robotic\" audio playback, you should test with "
"a higher value (10). Do not change this setting otherwise.\n"
"Default is 5.",
},
{
.longopt_id = OPT_AUDIO_CODEC,
.longopt = "audio-codec",
@ -171,6 +164,23 @@ static const struct sc_option options[] = {
"codec provided by --audio-codec).\n"
"The available encoders can be listed by --list-encoders.",
},
{
.longopt_id = OPT_AUDIO_SOURCE,
.longopt = "audio-source",
.argdesc = "source",
.text = "Select the audio source (output or mic).\n"
"Default is output.",
},
{
.longopt_id = OPT_AUDIO_OUTPUT_BUFFER,
.longopt = "audio-output-buffer",
.argdesc = "ms",
.text = "Configure the size of the SDL audio output buffer (in "
"milliseconds).\n"
"If you get \"robotic\" audio playback, you should test with "
"a higher value (10). Do not change this setting otherwise.\n"
"Default is 5.",
},
{
.shortopt = 'b',
.longopt = "video-bit-rate",
@ -249,6 +259,11 @@ static const struct sc_option options[] = {
.longopt = "encoder",
.argdesc = "name",
},
{
.shortopt = 'f',
.longopt = "fullscreen",
.text = "Start in fullscreen.",
},
{
.longopt_id = OPT_FORCE_ADB_FORWARD,
.longopt = "force-adb-forward",
@ -263,9 +278,14 @@ static const struct sc_option options[] = {
"shortcuts and forwards the clicks to the device instead.",
},
{
.shortopt = 'f',
.longopt = "fullscreen",
.text = "Start in fullscreen.",
.shortopt = 'h',
.longopt = "help",
.text = "Print this help.",
},
{
.longopt_id = OPT_KILL_ADB_ON_CLOSE,
.longopt = "kill-adb-on-close",
.text = "Kill adb when scrcpy terminates.",
},
{
.shortopt = 'K',
@ -284,11 +304,6 @@ static const struct sc_option options[] = {
"is enabled (or a physical keyboard is connected).\n"
"Also see --hid-mouse.",
},
{
.shortopt = 'h',
.longopt = "help",
.text = "Print this help.",
},
{
.longopt_id = OPT_LEGACY_PASTE,
.longopt = "legacy-paste",
@ -322,11 +337,13 @@ static const struct sc_option options[] = {
"\"initial\".",
},
{
.longopt_id = OPT_MAX_FPS,
.longopt = "max-fps",
.shortopt = 'm',
.longopt = "max-size",
.argdesc = "value",
.text = "Limit the frame rate of screen capture (officially supported "
"since Android 10, but may work on earlier versions).",
.text = "Limit both the width and height of the video to value. The "
"other dimension is computed so that the device aspect-ratio "
"is preserved.\n"
"Default is 0 (unlimited).",
},
{
.shortopt = 'M',
@ -340,13 +357,22 @@ static const struct sc_option options[] = {
"Also see --hid-keyboard.",
},
{
.shortopt = 'm',
.longopt = "max-size",
.longopt_id = OPT_MAX_FPS,
.longopt = "max-fps",
.argdesc = "value",
.text = "Limit both the width and height of the video to value. The "
"other dimension is computed so that the device aspect-ratio "
"is preserved.\n"
"Default is 0 (unlimited).",
.text = "Limit the frame rate of screen capture (officially supported "
"since Android 10, but may work on earlier versions).",
},
{
.shortopt = 'n',
.longopt = "no-control",
.text = "Disable device control (mirror the device in read-only).",
},
{
.shortopt = 'N',
.longopt = "no-playback",
.text = "Disable video and audio playback on the computer (equivalent "
"to --no-video-playback --no-audio-playback).",
},
{
.longopt_id = OPT_NO_AUDIO,
@ -382,17 +408,6 @@ static const struct sc_option options[] = {
"again with a lower definition.\n"
"This option disables this behavior.",
},
{
.shortopt = 'n',
.longopt = "no-control",
.text = "Disable device control (mirror the device in read-only).",
},
{
.shortopt = 'N',
.longopt = "no-playback",
.text = "Disable video and audio playback on the computer (equivalent "
"to --no-video-playback --no-audio-playback).",
},
{
// deprecated
.longopt_id = OPT_NO_DISPLAY,
@ -476,11 +491,6 @@ static const struct sc_option options[] = {
"drag & drop. It is passed as is to \"adb push\".\n"
"Default is \"/sdcard/Download/\".",
},
{
.longopt_id = OPT_RAW_KEY_EVENTS,
.longopt = "raw-key-events",
.text = "Inject key events for all input keys, and ignore text events."
},
{
.shortopt = 'r',
.longopt = "record",
@ -489,6 +499,11 @@ static const struct sc_option options[] = {
"The format is determined by the --record-format option if "
"set, or by the file extension (.mp4 or .mkv).",
},
{
.longopt_id = OPT_RAW_KEY_EVENTS,
.longopt = "raw-key-events",
.text = "Inject key events for all input keys, and ignore text events."
},
{
.longopt_id = OPT_RECORD_FORMAT,
.longopt = "record-format",
@ -527,6 +542,11 @@ static const struct sc_option options[] = {
.text = "The device serial number. Mandatory only if several devices "
"are connected to adb.",
},
{
.shortopt = 'S',
.longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.",
},
{
.longopt_id = OPT_SHORTCUT_MOD,
.longopt = "shortcut-mod",
@ -540,11 +560,6 @@ static const struct sc_option options[] = {
"shortcuts, pass \"lctrl+lalt,lsuper\".\n"
"Default is \"lalt,lsuper\" (left-Alt or left-Super).",
},
{
.shortopt = 'S',
.longopt = "turn-screen-off",
.text = "Turn the device screen off immediately.",
},
{
.shortopt = 't',
.longopt = "show-touches",
@ -566,6 +581,12 @@ static const struct sc_option options[] = {
"connected over USB), enables TCP/IP mode, then connects to "
"this address before starting.",
},
{
.longopt_id = OPT_TIME_LIMIT,
.longopt = "time-limit",
.argdesc = "seconds",
.text = "Set the maximum mirroring time, in seconds.",
},
{
.longopt_id = OPT_TUNNEL_HOST,
.longopt = "tunnel-host",
@ -585,6 +606,22 @@ static const struct sc_option options[] = {
"Default is 0 (not forced): the local port used for "
"establishing the tunnel will be used.",
},
{
.shortopt = 'v',
.longopt = "version",
.text = "Print the version of scrcpy.",
},
{
.shortopt = 'V',
.longopt = "verbosity",
.argdesc = "value",
.text = "Set the log level (verbose, debug, info, warn or error).\n"
#ifndef NDEBUG
"Default is debug.",
#else
"Default is info.",
#endif
},
{
.longopt_id = OPT_V4L2_SINK,
.longopt = "v4l2-sink",
@ -605,22 +642,6 @@ static const struct sc_option options[] = {
"Default is 0 (no buffering).\n"
"This option is only available on Linux.",
},
{
.shortopt = 'V',
.longopt = "verbosity",
.argdesc = "value",
.text = "Set the log level (verbose, debug, info, warn or error).\n"
#ifndef NDEBUG
"Default is debug.",
#else
"Default is info.",
#endif
},
{
.shortopt = 'v',
.longopt = "version",
.text = "Print the version of scrcpy.",
},
{
.longopt_id = OPT_VIDEO_CODEC,
.longopt = "video-codec",
@ -1588,6 +1609,34 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) {
return false;
}
static bool
parse_audio_source(const char *optarg, enum sc_audio_source *source) {
if (!strcmp(optarg, "mic")) {
*source = SC_AUDIO_SOURCE_MIC;
return true;
}
if (!strcmp(optarg, "output")) {
*source = SC_AUDIO_SOURCE_OUTPUT;
return true;
}
LOGE("Unsupported audio source: %s (expected output or mic)", optarg);
return false;
}
static bool
parse_time_limit(const char *s, sc_tick *tick) {
long value;
bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF, "time limit");
if (!ok) {
return false;
}
*tick = SC_TICK_FROM_SEC(value);
return true;
}
static bool
parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
const char *optstring, const struct option *longopts) {
@ -1915,6 +1964,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
return false;
}
break;
case OPT_AUDIO_SOURCE:
if (!parse_audio_source(optarg, &opts->audio_source)) {
return false;
}
break;
case OPT_KILL_ADB_ON_CLOSE:
opts->kill_adb_on_close = true;
break;
case OPT_TIME_LIMIT:
if (!parse_time_limit(optarg, &opts->time_limit)) {
return false;
}
break;
default:
// getopt prints the error message on stderr
return false;

View File

@ -79,9 +79,8 @@ sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width,
static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we
// record, we retrieve the timestamps separately, from a "meta" header
// added by the server before each raw packet.
// The video and audio streams contain a sequence of raw packets (as
// provided by MediaCodec), each prefixed with a "meta" header.
//
// The "meta" header length is 12 bytes:
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...

View File

@ -6,3 +6,4 @@
#define SC_EVENT_DEMUXER_ERROR (SDL_USEREVENT + 5)
#define SC_EVENT_RECORDER_ERROR (SDL_USEREVENT + 6)
#define SC_EVENT_SCREEN_INIT_SIZE (SDL_USEREVENT + 7)
#define SC_EVENT_TIME_LIMIT_REACHED (SDL_USEREVENT + 8)

View File

@ -14,6 +14,7 @@ const struct scrcpy_options scrcpy_options_default = {
.log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264,
.audio_codec = SC_CODEC_OPUS,
.audio_source = SC_AUDIO_SOURCE_OUTPUT,
.record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT,
@ -41,6 +42,7 @@ const struct scrcpy_options scrcpy_options_default = {
.display_buffer = 0,
.audio_buffer = SC_TICK_FROM_MS(50),
.audio_output_buffer = SC_TICK_FROM_MS(5),
.time_limit = 0,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
.v4l2_buffer = 0,
@ -79,4 +81,5 @@ const struct scrcpy_options scrcpy_options_default = {
.require_audio = false,
.list_encoders = false,
.list_displays = false,
.kill_adb_on_close = false,
};

View File

@ -44,6 +44,11 @@ enum sc_codec {
SC_CODEC_RAW,
};
enum sc_audio_source {
SC_AUDIO_SOURCE_OUTPUT,
SC_AUDIO_SOURCE_MIC,
};
enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts
@ -115,6 +120,7 @@ struct scrcpy_options {
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
enum sc_audio_source audio_source;
enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode;
@ -136,6 +142,7 @@ struct scrcpy_options {
sc_tick display_buffer;
sc_tick audio_buffer;
sc_tick audio_output_buffer;
sc_tick time_limit;
#ifdef HAVE_V4L2
const char *v4l2_device;
sc_tick v4l2_buffer;
@ -174,6 +181,7 @@ struct scrcpy_options {
bool require_audio;
bool list_encoders;
bool list_displays;
bool kill_adb_on_close;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@ -96,23 +96,30 @@ sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) {
}
static bool
sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index,
AVPacket *packet) {
AVStream *stream = recorder->ctx->streams[stream_index];
sc_recorder_write_stream(struct sc_recorder *recorder,
struct sc_recorder_stream *st, AVPacket *packet) {
AVStream *stream = recorder->ctx->streams[st->index];
sc_recorder_rescale_packet(stream, packet);
if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) {
LOGW("Fixing PTS non monotonically increasing in stream %d "
"(%" PRIi64 " >= %" PRIi64 ")",
st->index, st->last_pts, packet->pts);
packet->pts = ++st->last_pts;
packet->dts = packet->pts;
} else {
st->last_pts = packet->pts;
}
return av_interleaved_write_frame(recorder->ctx, packet) >= 0;
}
static inline bool
sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) {
return sc_recorder_write_stream(recorder, recorder->video_stream_index,
packet);
return sc_recorder_write_stream(recorder, &recorder->video_stream, packet);
}
static inline bool
sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) {
return sc_recorder_write_stream(recorder, recorder->audio_stream_index,
packet);
return sc_recorder_write_stream(recorder, &recorder->audio_stream, packet);
}
static bool
@ -178,10 +185,11 @@ static bool
sc_recorder_process_header(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped && (!recorder->video_init
|| !recorder->audio_init
|| sc_recorder_has_empty_queues(recorder))) {
sc_cond_wait(&recorder->stream_cond, &recorder->mutex);
while (!recorder->stopped &&
((recorder->video && !recorder->video_init)
|| (recorder->audio && !recorder->audio_init)
|| sc_recorder_has_empty_queues(recorder))) {
sc_cond_wait(&recorder->cond, &recorder->mutex);
}
if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
@ -214,9 +222,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
goto end;
}
assert(recorder->video_stream_index >= 0);
assert(recorder->video_stream.index >= 0);
AVStream *video_stream =
recorder->ctx->streams[recorder->video_stream_index];
recorder->ctx->streams[recorder->video_stream.index];
bool ok = sc_recorder_set_extradata(video_stream, video_pkt);
if (!ok) {
goto end;
@ -229,9 +237,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
goto end;
}
assert(recorder->audio_stream_index >= 0);
assert(recorder->audio_stream.index >= 0);
AVStream *audio_stream =
recorder->ctx->streams[recorder->audio_stream_index];
recorder->ctx->streams[recorder->audio_stream.index];
bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
if (!ok) {
goto end;
@ -289,7 +297,7 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
// A new packet may be assigned to audio_pkt and be processed
break;
}
sc_cond_wait(&recorder->queue_cond, &recorder->mutex);
sc_cond_wait(&recorder->cond, &recorder->mutex);
}
// If stopped is set, continue to process the remaining events (to
@ -504,10 +512,10 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
return false;
}
recorder->video_stream_index = stream->index;
recorder->video_stream.index = stream->index;
recorder->video_init = true;
sc_cond_signal(&recorder->stream_cond);
sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
return true;
@ -522,7 +530,7 @@ sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
}
@ -548,7 +556,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
return false;
}
rec->stream_index = recorder->video_stream_index;
rec->stream_index = recorder->video_stream.index;
bool ok = sc_vecdeque_push(&recorder->video_queue, rec);
if (!ok) {
@ -557,7 +565,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
return false;
}
sc_cond_signal(&recorder->queue_cond);
sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
return true;
@ -585,10 +593,10 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
return false;
}
recorder->audio_stream_index = stream->index;
recorder->audio_stream.index = stream->index;
recorder->audio_init = true;
sc_cond_signal(&recorder->stream_cond);
sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
return true;
@ -604,7 +612,7 @@ sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) {
sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder
recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond);
sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
}
@ -631,7 +639,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
return false;
}
rec->stream_index = recorder->audio_stream_index;
rec->stream_index = recorder->audio_stream.index;
bool ok = sc_vecdeque_push(&recorder->audio_queue, rec);
if (!ok) {
@ -640,7 +648,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
return false;
}
sc_cond_signal(&recorder->queue_cond);
sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
return true;
@ -658,10 +666,16 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
sc_mutex_lock(&recorder->mutex);
recorder->audio = false;
recorder->audio_init = true;
sc_cond_signal(&recorder->stream_cond);
sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
}
static void
sc_recorder_stream_init(struct sc_recorder_stream *stream) {
stream->index = -1;
stream->last_pts = AV_NOPTS_VALUE;
}
bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool video, bool audio,
@ -677,16 +691,11 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
goto error_free_filename;
}
ok = sc_cond_init(&recorder->queue_cond);
ok = sc_cond_init(&recorder->cond);
if (!ok) {
goto error_mutex_destroy;
}
ok = sc_cond_init(&recorder->stream_cond);
if (!ok) {
goto error_queue_cond_destroy;
}
assert(video || audio);
recorder->video = video;
recorder->audio = audio;
@ -698,8 +707,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
recorder->video_init = false;
recorder->audio_init = false;
recorder->video_stream_index = -1;
recorder->audio_stream_index = -1;
sc_recorder_stream_init(&recorder->video_stream);
sc_recorder_stream_init(&recorder->audio_stream);
recorder->format = format;
@ -730,8 +739,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
return true;
error_queue_cond_destroy:
sc_cond_destroy(&recorder->queue_cond);
error_mutex_destroy:
sc_mutex_destroy(&recorder->mutex);
error_free_filename:
@ -756,8 +763,7 @@ 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_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
}
@ -768,8 +774,7 @@ sc_recorder_join(struct sc_recorder *recorder) {
void
sc_recorder_destroy(struct sc_recorder *recorder) {
sc_cond_destroy(&recorder->stream_cond);
sc_cond_destroy(&recorder->queue_cond);
sc_cond_destroy(&recorder->cond);
sc_mutex_destroy(&recorder->mutex);
free(recorder->filename);
}

View File

@ -14,6 +14,11 @@
struct sc_recorder_queue SC_VECDEQUE(AVPacket *);
struct sc_recorder_stream {
int index;
int64_t last_pts;
};
struct sc_recorder {
struct sc_packet_sink video_packet_sink;
struct sc_packet_sink audio_packet_sink;
@ -35,19 +40,18 @@ struct sc_recorder {
sc_thread thread;
sc_mutex mutex;
sc_cond queue_cond;
sc_cond cond;
// set on sc_recorder_stop(), packet_sink close or recording failure
bool stopped;
struct sc_recorder_queue video_queue;
struct sc_recorder_queue audio_queue;
// wake up the recorder thread once the video or audio codec is known
sc_cond stream_cond;
bool video_init;
bool audio_init;
int video_stream_index;
int audio_stream_index;
struct sc_recorder_stream video_stream;
struct sc_recorder_stream audio_stream;
const struct sc_recorder_callbacks *cbs;
void *cbs_userdata;

View File

@ -35,6 +35,7 @@
#include "util/log.h"
#include "util/net.h"
#include "util/rand.h"
#include "util/timeout.h"
#ifdef HAVE_V4L2
# include "v4l2_sink.h"
#endif
@ -73,6 +74,7 @@ struct scrcpy {
struct sc_hid_mouse mouse_hid;
#endif
};
struct sc_timeout timeout;
};
static inline void
@ -171,6 +173,9 @@ event_loop(struct scrcpy *s) {
case SC_EVENT_RECORDER_ERROR:
LOGE("Recorder error");
return SCRCPY_EXIT_FAILURE;
case SC_EVENT_TIME_LIMIT_REACHED:
LOGI("Time limit reached");
return SCRCPY_EXIT_SUCCESS;
case SDL_QUIT:
LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS;
@ -280,6 +285,14 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
// event
}
static void
sc_timeout_on_timeout(struct sc_timeout *timeout, void *userdata) {
(void) timeout;
(void) userdata;
PUSH_EVENT(SC_EVENT_TIME_LIMIT_REACHED);
}
// Generate a scrcpy id to differentiate multiple running scrcpy instances
static uint32_t
scrcpy_generate_scid() {
@ -321,6 +334,8 @@ scrcpy(struct scrcpy_options *options) {
bool controller_initialized = false;
bool controller_started = false;
bool screen_initialized = false;
bool timeout_initialized = false;
bool timeout_started = false;
struct sc_acksync *acksync = NULL;
@ -334,6 +349,7 @@ scrcpy(struct scrcpy_options *options) {
.log_level = options->log_level,
.video_codec = options->video_codec,
.audio_codec = options->audio_codec,
.audio_source = options->audio_source,
.crop = options->crop,
.port_range = options->port_range,
.tunnel_host = options->tunnel_host,
@ -363,6 +379,7 @@ scrcpy(struct scrcpy_options *options) {
.power_on = options->power_on,
.list_encoders = options->list_encoders,
.list_displays = options->list_displays,
.kill_adb_on_close = options->kill_adb_on_close,
};
static const struct sc_server_callbacks cbs = {
@ -639,17 +656,6 @@ aoa_hid_end:
}
controller_started = true;
controller = &s->controller;
if (options->turn_screen_off) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
}
// There is a controller if and only if control is enabled
@ -740,6 +746,39 @@ aoa_hid_end:
audio_demuxer_started = true;
}
// If the device screen is to be turned off, send the control message after
// everything is set up
if (options->control && options->turn_screen_off) {
struct sc_control_msg msg;
msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE;
msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF;
if (!sc_controller_push_msg(&s->controller, &msg)) {
LOGW("Could not request 'set screen power mode'");
}
}
if (options->time_limit) {
bool ok = sc_timeout_init(&s->timeout);
if (!ok) {
goto end;
}
timeout_initialized = true;
sc_tick deadline = sc_tick_now() + options->time_limit;
static const struct sc_timeout_callbacks cbs = {
.on_timeout = sc_timeout_on_timeout,
};
ok = sc_timeout_start(&s->timeout, deadline, &cbs, NULL);
if (!ok) {
goto end;
}
timeout_started = true;
}
ret = event_loop(s);
LOGD("quit...");
@ -748,6 +787,10 @@ aoa_hid_end:
sc_screen_hide_window(&s->screen);
end:
if (timeout_started) {
sc_timeout_stop(&s->timeout);
}
// The demuxer is not stopped explicitly, because it will stop by itself on
// end-of-stream
#ifdef HAVE_USB
@ -783,6 +826,13 @@ end:
sc_server_stop(&s->server);
}
if (timeout_started) {
sc_timeout_join(&s->timeout);
}
if (timeout_initialized) {
sc_timeout_destroy(&s->timeout);
}
// now that the sockets are shutdown, the demuxer and controller are
// interrupted, we can join them
if (video_demuxer_started) {

View File

@ -246,6 +246,10 @@ execute_server(struct sc_server *server,
ADD_PARAM("audio_codec=%s",
sc_server_get_codec_name(params->audio_codec));
}
if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT) {
assert(params->audio_source == SC_AUDIO_SOURCE_MIC);
ADD_PARAM("audio_source=mic");
}
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);
}
@ -790,6 +794,15 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server,
return sc_server_connect_to_tcpip(server, ip_port);
}
static void
sc_server_kill_adb_if_requested(struct sc_server *server) {
if (server->params.kill_adb_on_close) {
LOGI("Killing adb server...");
unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
sc_adb_kill_server(&server->intr, flags);
}
}
static int
run_server(void *data) {
struct sc_server *server = data;
@ -801,7 +814,7 @@ run_server(void *data) {
// is parsed, so it is not output)
bool ok = sc_adb_start_server(&server->intr, 0);
if (!ok) {
LOGE("Could not start adb daemon");
LOGE("Could not start adb server");
goto error_connection_failed;
}
@ -989,9 +1002,12 @@ run_server(void *data) {
sc_process_close(pid);
sc_server_kill_adb_if_requested(server);
return 0;
error_connection_failed:
sc_server_kill_adb_if_requested(server);
server->cbs->on_connection_failed(server, server->cbs_userdata);
return -1;
}

View File

@ -26,6 +26,7 @@ struct sc_server_params {
enum sc_log_level log_level;
enum sc_codec video_codec;
enum sc_codec audio_codec;
enum sc_audio_source audio_source;
const char *crop;
const char *video_codec_options;
const char *audio_codec_options;
@ -57,6 +58,7 @@ struct sc_server_params {
bool power_on;
bool list_encoders;
bool list_displays;
bool kill_adb_on_close;
};
struct sc_server {

View File

@ -83,7 +83,7 @@ scrcpy_otg(struct scrcpy_options *options) {
#ifdef _WIN32
// On Windows, only one process could open a USB device
// <https://github.com/Genymobile/scrcpy/issues/2773>
LOGI("Killing adb daemon (if any)...");
LOGI("Killing adb server (if any)...");
unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
// uninterruptible (intr == NULL), but in practice it's very quick
sc_adb_kill_server(NULL, flags);

77
app/src/util/timeout.c Normal file
View File

@ -0,0 +1,77 @@
#include "timeout.h"
#include <assert.h>
#include "log.h"
bool
sc_timeout_init(struct sc_timeout *timeout) {
bool ok = sc_mutex_init(&timeout->mutex);
if (!ok) {
return false;
}
ok = sc_cond_init(&timeout->cond);
if (!ok) {
return false;
}
timeout->stopped = false;
return true;
}
static int
run_timeout(void *data) {
struct sc_timeout *timeout = data;
sc_tick deadline = timeout->deadline;
sc_mutex_lock(&timeout->mutex);
bool timed_out = false;
while (!timeout->stopped && !timed_out) {
timed_out = !sc_cond_timedwait(&timeout->cond, &timeout->mutex,
deadline);
}
sc_mutex_unlock(&timeout->mutex);
timeout->cbs->on_timeout(timeout, timeout->cbs_userdata);
return 0;
}
bool
sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline,
const struct sc_timeout_callbacks *cbs, void *cbs_userdata) {
bool ok = sc_thread_create(&timeout->thread, run_timeout, "scrcpy-timeout",
timeout);
if (!ok) {
LOGE("Timeout: could not start thread");
return false;
}
timeout->deadline = deadline;
assert(cbs && cbs->on_timeout);
timeout->cbs = cbs;
timeout->cbs_userdata = cbs_userdata;
return true;
}
void
sc_timeout_stop(struct sc_timeout *timeout) {
sc_mutex_lock(&timeout->mutex);
timeout->stopped = true;
sc_mutex_unlock(&timeout->mutex);
}
void
sc_timeout_join(struct sc_timeout *timeout) {
sc_thread_join(&timeout->thread, NULL);
}
void
sc_timeout_destroy(struct sc_timeout *timeout) {
sc_mutex_destroy(&timeout->mutex);
sc_cond_destroy(&timeout->cond);
}

43
app/src/util/timeout.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef SC_TIMEOUT_H
#define SC_TIMEOUT_H
#include "common.h"
#include <stdbool.h>
#include "thread.h"
#include "tick.h"
struct sc_timeout {
sc_thread thread;
sc_tick deadline;
sc_mutex mutex;
sc_cond cond;
bool stopped;
const struct sc_timeout_callbacks *cbs;
void *cbs_userdata;
};
struct sc_timeout_callbacks {
void (*on_timeout)(struct sc_timeout *timeout, void *userdata);
};
bool
sc_timeout_init(struct sc_timeout *timeout);
void
sc_timeout_destroy(struct sc_timeout *timeout);
bool
sc_timeout_start(struct sc_timeout *timeout, sc_tick deadline,
const struct sc_timeout_callbacks *cbs, void *cbs_userdata);
void
sc_timeout_stop(struct sc_timeout *timeout);
void
sc_timeout_join(struct sc_timeout *timeout);
#endif

View File

@ -30,8 +30,9 @@ To disable only the audio playback, see [no playback](video.md#no-playback).
To play audio only, disable the video:
```
```bash
scrcpy --no-video
# interrupt with Ctrl+C
```
Without video, the audio latency is typically not criticial, so it might be
@ -41,6 +42,24 @@ interesting to add [buffering](#buffering) to minimize glitches:
scrcpy --no-video --audio-buffer=200
```
## Source
By default, the device audio output is forwarded.
It is possible to capture the device microphone instead:
```
scrcpy --audio-source=mic
```
For example, to use the device as a dictaphone and record a capture directly on
the computer:
```
scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
```
## Codec
The audio codec can be selected. The possible values are `opus` (default), `aac`

View File

@ -17,24 +17,19 @@ To record only the audio:
```bash
scrcpy --no-video --record=file.opus
scrcpy --no-video --audio-codec=aac --record-file=file.aac
scrcpy --no-video --audio-codec=aac --record=file.aac
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
```
To disable playback while recording:
```bash
scrcpy --no-playback --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
```
Timestamps are captured on the device, so [packet delay variation] does not
impact the recorded file, which is always clean (only if you use `--record` of
course, not if you capture your scrcpy window and audio output on the computer).
[packet delay variation]: https://en.wikipedia.org/wiki/Packet_delay_variation
## Format
The video and audio streams are encoded on the device, but are muxed on the
client side. Two formats (containers) are supported:
- Matroska (`.mkv`)
@ -48,3 +43,36 @@ needs not end with `.mkv` or `.mp4`):
```
scrcpy --record=file --record-format=mkv
```
## No playback
To disable playback while recording:
```bash
scrcpy --no-playback --record=file.mp4
scrcpy -Nr file.mkv
# interrupt recording with Ctrl+C
```
It is also possible to disable video and audio playback separately:
```bash
# Record both video and audio, but only play video
scrcpy --record=file.mkv --no-audio-playback
```
## Time limit
To limit the recording time:
```bash
scrcpy --record=file.mkv --time-limit=20 # in seconds
```
The `--time-limit` option is not limited to recording, it also impacts simple
mirroring:
```
scrcpy --time-limit=20
```

View File

@ -168,6 +168,7 @@ the computer. This option is useful when [recording](recording.md) or when
```bash
scrcpy --v4l2-sink=/dev/video2 --no-playback
scrcpy --record=file.mkv --no-playback
# interrupt with Ctrl+C
```
It is also possible to disable video and audio playback separately:

View File

@ -0,0 +1,26 @@
/*
* Copyright (C) 2019 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view;
/**
* {@hide}
*/
oneway interface IDisplayFoldListener
{
/** Called when the foldedness of a display changes */
void onDisplayFoldChanged(int displayId, boolean folded);
}

View File

@ -10,7 +10,6 @@ import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.SystemClock;
@ -18,7 +17,6 @@ import java.nio.ByteBuffer;
public final class AudioCapture {
public static final int SOURCE = MediaRecorder.AudioSource.REMOTE_SUBMIX;
public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2;
@ -26,12 +24,18 @@ public final class AudioCapture {
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final int BYTES_PER_SAMPLE = 2;
private final int audioSource;
private AudioRecord recorder;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousPts = 0;
private long nextPts = 0;
public AudioCapture(AudioSource audioSource) {
this.audioSource = audioSource.value();
}
public static int millisToBytes(int millis) {
return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000;
}
@ -46,13 +50,13 @@ public final class AudioCapture {
@TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() {
private static AudioRecord createAudioRecord(int audioSource) {
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(SOURCE);
builder.setAudioSource(audioSource);
builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
// This buffer size does not impact latency
@ -100,12 +104,12 @@ public final class AudioCapture {
private void startRecording() {
try {
recorder = createAudioRecord();
recorder = createAudioRecord(audioSource);
} catch (NullPointerException e) {
// Creating an AudioRecord using an AudioRecord.Builder does not work on Vivo phones:
// - <https://github.com/Genymobile/scrcpy/issues/3805>
// - <https://github.com/Genymobile/scrcpy/pull/3862>
recorder = Workarounds.createAudioRecord(SOURCE, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
}
recorder.startRecording();
}

View File

@ -40,6 +40,7 @@ public final class AudioEncoder implements AsyncProcessor {
private static final int READ_MS = 5; // milliseconds
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
private final AudioCapture capture;
private final Streamer streamer;
private final int bitRate;
private final List<CodecOption> codecOptions;
@ -58,7 +59,8 @@ public final class AudioEncoder implements AsyncProcessor {
private boolean ended;
public AudioEncoder(Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List<CodecOption> codecOptions, String encoderName) {
this.capture = capture;
this.streamer = streamer;
this.bitRate = bitRate;
this.codecOptions = codecOptions;
@ -132,7 +134,7 @@ public final class AudioEncoder implements AsyncProcessor {
Ln.d("Audio encoder stopped");
listener.onTerminated(fatalError);
}
});
}, "audio-encoder");
thread.start();
}
@ -175,14 +177,13 @@ public final class AudioEncoder implements AsyncProcessor {
}
MediaCodec mediaCodec = null;
AudioCapture capture = new AudioCapture();
boolean mediaCodecStarted = false;
try {
Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName);
mediaCodecThread = new HandlerThread("AudioEncoder");
mediaCodecThread = new HandlerThread("media-codec");
mediaCodecThread.start();
MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions);
@ -192,16 +193,15 @@ public final class AudioEncoder implements AsyncProcessor {
capture.start();
final MediaCodec mediaCodecRef = mediaCodec;
final AudioCapture captureRef = capture;
inputThread = new Thread(() -> {
try {
inputThread(mediaCodecRef, captureRef);
inputThread(mediaCodecRef, capture);
} catch (IOException | InterruptedException e) {
Ln.e("Audio capture error", e);
} finally {
end();
}
});
}, "audio-in");
outputThread = new Thread(() -> {
try {
@ -216,7 +216,7 @@ public final class AudioEncoder implements AsyncProcessor {
} finally {
end();
}
});
}, "audio-out");
mediaCodec.start();
mediaCodecStarted = true;

View File

@ -8,6 +8,7 @@ import java.nio.ByteBuffer;
public final class AudioRawRecorder implements AsyncProcessor {
private final AudioCapture capture;
private final Streamer streamer;
private Thread thread;
@ -15,7 +16,8 @@ public final class AudioRawRecorder implements AsyncProcessor {
private static final int READ_MS = 5; // milliseconds
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
public AudioRawRecorder(Streamer streamer) {
public AudioRawRecorder(AudioCapture capture, Streamer streamer) {
this.capture = capture;
this.streamer = streamer;
}
@ -29,7 +31,6 @@ public final class AudioRawRecorder implements AsyncProcessor {
final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
AudioCapture capture = new AudioCapture();
try {
capture.start();
@ -68,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
Ln.d("Audio recorder stopped");
listener.onTerminated(fatalError);
}
});
}, "audio-raw");
thread.start();
}

View File

@ -0,0 +1,30 @@
package com.genymobile.scrcpy;
import android.media.MediaRecorder;
public enum AudioSource {
OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX),
MIC("mic", MediaRecorder.AudioSource.MIC);
private final String name;
private final int value;
AudioSource(String name, int value) {
this.name = name;
this.value = value;
}
int value() {
return value;
}
static AudioSource findByName(String name) {
for (AudioSource audioSource : AudioSource.values()) {
if (name.equals(audioSource.name)) {
return audioSource;
}
}
return null;
}
}

View File

@ -95,7 +95,7 @@ public class Controller implements AsyncProcessor {
Ln.d("Controller stopped");
listener.onTerminated(true);
}
});
}, "control-recv");
thread.start();
sender.start();
}

View File

@ -12,6 +12,7 @@ import android.os.Build;
import android.os.IBinder;
import android.os.SystemClock;
import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import android.view.InputDevice;
import android.view.InputEvent;
import android.view.KeyCharacterMap;
@ -35,6 +36,10 @@ public final class Device {
void onRotationChanged(int rotation);
}
public interface FoldListener {
void onFoldChanged(int displayId, boolean folded);
}
public interface ClipboardListener {
void onClipboardTextChanged(String text);
}
@ -46,6 +51,7 @@ public final class Device {
private ScreenInfo screenInfo;
private RotationListener rotationListener;
private FoldListener foldListener;
private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
@ -93,6 +99,26 @@ public final class Device {
}
}, displayId);
ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() {
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
synchronized (Device.this) {
DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId);
if (displayInfo == null) {
Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage());
return;
}
screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), options.getCrop(),
options.getMaxSize(), options.getLockVideoOrientation());
// notify
if (foldListener != null) {
foldListener.onFoldChanged(displayId, folded);
}
}
}
});
if (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
@ -224,6 +250,10 @@ public final class Device {
this.rotationListener = rotationListener;
}
public synchronized void setFoldListener(FoldListener foldlistener) {
this.foldListener = foldlistener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener;
}

View File

@ -60,7 +60,7 @@ public final class DeviceMessageSender {
} finally {
Ln.d("Device message sender stopped");
}
});
}, "control-send");
thread.start();
}

View File

@ -1,11 +1,16 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ActivityThread;
import android.annotation.TargetApi;
import android.content.AttributionSource;
import android.content.Context;
import android.content.ContextWrapper;
import android.os.Build;
import android.os.Process;
import java.lang.reflect.Method;
public final class FakeContext extends ContextWrapper {
public static final String PACKAGE_NAME = "com.android.shell";
@ -13,12 +18,25 @@ public final class FakeContext extends ContextWrapper {
private static final FakeContext INSTANCE = new FakeContext();
private static Context retrieveSystemContext() {
try {
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass();
Object activityThread = ActivityThread.getActivityThread();
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
return (Context) getSystemContextMethod.invoke(activityThread);
} catch (Exception e) {
Ln.e("Cannot retrieve system context", e);
return null;
}
}
public static FakeContext get() {
return INSTANCE;
}
private FakeContext() {
super(null);
super(retrieveSystemContext());
}
@Override

View File

@ -14,6 +14,7 @@ public class Options {
private int maxSize;
private VideoCodec videoCodec = VideoCodec.H264;
private AudioCodec audioCodec = AudioCodec.OPUS;
private AudioSource audioSource = AudioSource.OUTPUT;
private int videoBitRate = 8000000;
private int audioBitRate = 128000;
private int maxFps;
@ -72,6 +73,10 @@ public class Options {
return audioCodec;
}
public AudioSource getAudioSource() {
return audioSource;
}
public int getVideoBitRate() {
return videoBitRate;
}
@ -225,6 +230,13 @@ public class Options {
}
options.audioCodec = audioCodec;
break;
case "audio_source":
AudioSource audioSource = AudioSource.findByName(value);
if (audioSource == null) {
throw new IllegalArgumentException("Audio source " + value + " not supported");
}
options.audioSource = audioSource;
break;
case "max_size":
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
break;
@ -306,9 +318,9 @@ public class Options {
case "send_codec_meta":
options.sendCodecMeta = Boolean.parseBoolean(value);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
case "raw_stream":
boolean rawStream = Boolean.parseBoolean(value);
if (rawStream) {
options.sendDeviceMeta = false;
options.sendFrameMeta = false;
options.sendDummyByte = false;

View File

@ -16,7 +16,7 @@ import java.nio.ByteBuffer;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
public class ScreenEncoder implements Device.RotationListener, Device.FoldListener, AsyncProcessor {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
@ -26,7 +26,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final int MAX_CONSECUTIVE_ERRORS = 3;
private final AtomicBoolean rotationChanged = new AtomicBoolean();
private final AtomicBoolean resetCapture = new AtomicBoolean();
private final Device device;
private final Streamer streamer;
@ -54,12 +54,17 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
}
@Override
public void onRotationChanged(int rotation) {
rotationChanged.set(true);
public void onFoldChanged(int displayId, boolean folded) {
resetCapture.set(true);
}
private boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
@Override
public void onRotationChanged(int rotation) {
resetCapture.set(true);
}
private boolean consumeResetCapture() {
return resetCapture.getAndSet(false);
}
private void streamScreen() throws IOException, ConfigurationException {
@ -68,6 +73,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
device.setFoldListener(this);
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize());
@ -115,6 +121,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
} finally {
mediaCodec.release();
device.setRotationListener(null);
device.setFoldListener(null);
SurfaceControl.destroyDisplay(display);
}
}
@ -169,14 +176,14 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
boolean alive = true;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) {
while (!consumeResetCapture() && !eof) {
if (stopped.get()) {
alive = false;
break;
}
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try {
if (consumeRotationChange()) {
if (consumeResetCapture()) {
// must restart encoding with new size
break;
}
@ -292,7 +299,7 @@ public class ScreenEncoder implements Device.RotationListener, AsyncProcessor {
Ln.d("Screen streaming stopped");
listener.onTerminated(true);
}
});
}, "video");
thread.start();
}

View File

@ -87,7 +87,7 @@ public final class Server {
}
private static void scrcpy(Options options) throws IOException, ConfigurationException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
final Device device = new Device(options);
Thread initThread = startInitThread(options);
@ -109,7 +109,7 @@ public final class Server {
// But only apply when strictly necessary, since workarounds can cause other issues:
// - <https://github.com/Genymobile/scrcpy/issues/940>
// - <https://github.com/Genymobile/scrcpy/issues/994>
if (Build.BRAND.equalsIgnoreCase("meizu")) {
if (Build.BRAND.equalsIgnoreCase("meizu") || Build.BRAND.equalsIgnoreCase("honor")) {
Workarounds.fillAppInfo();
}
@ -136,13 +136,13 @@ public final class Server {
if (audio) {
AudioCodec audioCodec = options.getAudioCodec();
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(),
options.getSendFrameMeta());
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioStreamer);
audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else {
audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
options.getAudioEncoder());
}
asyncProcessors.add(audioRecorder);
@ -184,7 +184,7 @@ public final class Server {
}
private static Thread startInitThread(final Options options) {
Thread thread = new Thread(() -> initAndCleanUp(options));
Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup");
thread.start();
return thread;
}

View File

@ -1,5 +1,7 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ActivityThread;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
@ -20,8 +22,7 @@ import java.lang.reflect.Method;
public final class Workarounds {
private static Class<?> activityThreadClass;
private static Object activityThread;
private static boolean activityThreadFilled;
private Workarounds() {
// not instantiable
@ -42,17 +43,16 @@ public final class Workarounds {
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
private static void fillActivityThread() throws Exception {
if (activityThread == null) {
// ActivityThread activityThread = new ActivityThread();
activityThreadClass = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
activityThread = activityThreadConstructor.newInstance();
if (!activityThreadFilled) {
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass();
Object activityThread = ActivityThread.getActivityThread();
// ActivityThread.sCurrentActivityThread = activityThread;
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
sCurrentActivityThreadField.set(null, activityThread);
activityThreadFilled = true;
}
}
@ -75,6 +75,9 @@ public final class Workarounds {
appInfoField.setAccessible(true);
appInfoField.set(appBindData, applicationInfo);
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass();
Object activityThread = ActivityThread.getActivityThread();
// activityThread.mBoundApplication = appBindData;
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
mBoundApplicationField.setAccessible(true);
@ -95,6 +98,9 @@ public final class Workarounds {
baseField.setAccessible(true);
baseField.set(app, FakeContext.get());
Class<?> activityThreadClass = ActivityThread.getActivityThreadClass();
Object activityThread = ActivityThread.getActivityThread();
// activityThread.mInitialApplication = app;
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
mInitialApplicationField.setAccessible(true);
@ -106,7 +112,7 @@ public final class Workarounds {
}
@TargetApi(Build.VERSION_CODES.R)
@SuppressLint({"WrongConstant", "MissingPermission", "BlockedPrivateApi", "SoonBlockedPrivateApi"})
@SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
//

View File

@ -0,0 +1,32 @@
package com.genymobile.scrcpy.wrappers;
import java.lang.reflect.Constructor;
public class ActivityThread {
private static final Class<?> activityThreadClass;
private static final Object activityThread;
static {
try {
activityThreadClass = Class.forName("android.app.ActivityThread");
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
activityThreadConstructor.setAccessible(true);
activityThread = activityThreadConstructor.newInstance();
} catch (Exception e) {
throw new AssertionError(e);
}
}
private ActivityThread() {
// only static methods
}
public static Object getActivityThread() {
return activityThread;
}
public static Class<?> getActivityThreadClass() {
return activityThreadClass;
}
}

View File

@ -14,13 +14,13 @@ public final class InputManager {
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1;
public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2;
private final android.hardware.input.InputManager manager;
private final Object manager;
private Method injectInputEventMethod;
private static Method setDisplayIdMethod;
private static Method setActionButtonMethod;
public InputManager(android.hardware.input.InputManager manager) {
public InputManager(Object manager) {
this.manager = manager;
}

View File

@ -62,11 +62,21 @@ public final class ServiceManager {
return displayManager;
}
public static Class<?> getInputManagerClass() {
try {
// Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview
return Class.forName("android.hardware.input.InputManagerGlobal");
} catch (ClassNotFoundException e) {
return android.hardware.input.InputManager.class;
}
}
public static InputManager getInputManager() {
if (inputManager == null) {
try {
Method getInstanceMethod = android.hardware.input.InputManager.class.getDeclaredMethod("getInstance");
android.hardware.input.InputManager im = (android.hardware.input.InputManager) getInstanceMethod.invoke(null);
Class<?> inputManagerClass = getInputManagerClass();
Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance");
Object im = getInstanceMethod.invoke(null);
inputManager = new InputManager(im);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new AssertionError(e);

View File

@ -4,6 +4,7 @@ import com.genymobile.scrcpy.Ln;
import android.os.IInterface;
import android.view.IRotationWatcher;
import android.view.IDisplayFoldListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
@ -108,4 +109,13 @@ public final class WindowManager {
throw new AssertionError(e);
}
}
public void registerDisplayFoldListener(IDisplayFoldListener foldListener) {
try {
Class<?> cls = manager.getClass();
cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener);
} catch (Exception e) {
throw new AssertionError(e);
}
}
}

View File

@ -12,7 +12,6 @@ import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
public class ControlMessageReaderTest {
@Test