Compare commits

...

214 Commits

Author SHA1 Message Date
fdc58722b3 Adapt to display API changes
The method SurfaceControl.createDisplay() has been removed in AOSP.

Use DisplayManager to create a VirtualDisplay object instead.

Fixes #4646 <https://github.com/Genymobile/scrcpy/issues/4646>
Fixes #4656 <https://github.com/Genymobile/scrcpy/issues/4656>
PR #4657 <https://github.com/Genymobile/scrcpy/pull/4657>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-02-09 18:31:20 +01:00
5a6b8310ca Add note about official website 2023-12-13 12:55:14 +01:00
c6ff78f414 Update links to v2.3.1 2023-12-02 12:39:05 +01:00
40f2560d98 Bump version to 2.3.1 2023-12-02 12:30:19 +01:00
26aa28c998 Merge branch 'master' into release 2023-12-02 12:29:31 +01:00
ef79fcbbd2 Fix AV1 demuxing
For AV1, the config packet must not be merged with the next non-config
packet.

This fixes the following error when passing --video-codec=av1:

> INFO: [FFmpeg] libdav1d 1.3.0
> ERROR: [FFmpeg] Unknown OBU type 0 of size 29393
> ERROR: [FFmpeg] Error parsing OBU data
> ERROR: Decoder 'video': could not send video packet: -1094995529

PR #4487 <https://github.com/Genymobile/scrcpy/pull/4487>
2023-12-02 12:20:01 +01:00
9497f39fb4 Do not fail if SDL_INIT_VIDEO fails without video
The SDL video subsystem may be initialized so that clipboard
synchronization works even without video playback.

But if the video subsystem initialization fails (e.g. because no video
device is available), consider it as an error only if video playback is
enabled.

Refs 5e59ed3135
Fixes #4477 <https://github.com/Genymobile/scrcpy/issues/4477>
2023-11-29 12:16:05 +01:00
bf056b1fee Do not initialize SDL video when not necessary
The SDL video subsystem is required for video playback and clipboard
synchronization.

If neither is used, it is not necessary to initialize it.

Refs 5e59ed3135
Refs 110b3a16f6
Refs #4418 <https://github.com/Genymobile/scrcpy/issues/4418>
Refs #4477 <https://github.com/Genymobile/scrcpy/issues/4477>
2023-11-29 12:14:07 +01:00
bd9292931e Mention exclusive_caps mode in v4l2 documentation
PR #4435 <https://github.com/Genymobile/scrcpy/pull/4435>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-28 08:32:28 +01:00
140a49b8be Add workaround for Samsung devices issues
On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked()
calls ActivityThread.currentActivityThread().getConfiguration(), which
requires a non-null ConfigurationController.

Fixes <https://github.com/Genymobile/scrcpy/issues/4467>
2023-11-27 09:29:06 +01:00
4135c411af Fix compilation error
Fix the following warning/error:

    ../app/src/cli.c:2158:17: warning: a label can only be part of a
    statement and a declaration is not a statement [-Wpedantic]

With some compilers, this is an error rather than a pedantic warning.

Refs <https://github.com/Genymobile/scrcpy/issues/2256#issuecomment-1467008307>
2023-11-25 23:56:46 +01:00
5e061636f6 Update links to v2.3 2023-11-25 22:15:07 +01:00
5f3fb843f5 Bump version to 2.3
The previous version bump to 2.2 was incorrect, it was updated by:

    ./bump_version v2.2

instead of:

    ./bump_version 2.2

Correctly bump to version 2.3.

Refs #4433 <https://github.com/Genymobile/scrcpy/issues/4433#issuecomment-1816830875>
2023-11-25 21:40:27 +01:00
ce8126f322 Merge branch 'master' into release 2023-11-25 21:37:37 +01:00
d037b02cc2 Fix scrcpy-console.desktop
The argument passed to scrcpy was not applied, the full command must be
passed as a single argument.

PR #4448 <https://github.com/Genymobile/scrcpy/pull/4448>
2023-11-25 21:35:04 +01:00
Kid
89761213c3 Do not quote $SHELL in .desktop files
This does not work properly on some desktop environments (KDE), and
$SHELL is unlikely to require quoting.

Fixes #4367 <https://github.com/Genymobile/scrcpy/issues/4367>
PR #4448 <https://github.com/Genymobile/scrcpy/pull/4448>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-25 21:28:43 +01:00
Kid
8db4e78b34 Fix Linux desktop files
There were too many backslashes in the Exec line.

Fixes #4367 <https://github.com/Genymobile/scrcpy/issues/4367>
PR #4448 <https://github.com/Genymobile/scrcpy/pull/4448>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-25 21:28:43 +01:00
5d4b8a7e6d Fix turn screen off on Android 14
On Android 14, the methods to access the display have been moved to
DisplayControl, which is not in the core framework. Use a specific
ClassLoader to access this class and its native dependencies.

Fixes #3927 <https://github.com/Genymobile/scrcpy/issues/3927>
Refs #3927 comment <https://github.com/Genymobile/scrcpy/issues/3927#issuecomment-1790031953>
Refs #4446 comment <https://github.com/Genymobile/scrcpy/pull/4446#issuecomment-1824660915>
PR #4456 <https://github.com/Genymobile/scrcpy/pull/4456>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-25 21:25:26 +01:00
eed06b141a Upgrade sdl (2.28.5) for Windows
Include the latest version of SDL in Windows releases.
2023-11-25 21:06:45 +01:00
825d7f72c0 Extract $VERSION for dependency scripts
This will allow to update the version only once in these files.
2023-11-25 21:06:45 +01:00
2370298b61 Download SDL prebuilt binaries from github
The server is faster than libsdl.org.
2023-11-25 21:06:45 +01:00
67f356f881 Improve crossbuild
Install all the prebuilt dependencies for Windows to a specific folder,
and use meson command line options to specify their location.

This removes crossbuild-specific code from the meson scripts and will
simplify dependency upgrades.

PR #4460 <https://github.com/Genymobile/scrcpy/pull/4460>
2023-11-25 21:06:37 +01:00
c573bd2a33 Fix java code style 2023-11-23 23:50:00 +01:00
acb2988837 Do not hardcode server path on the device
The path can be retrieved from the classpath.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-23 23:39:40 +01:00
85a94dd4b5 Fix meson deprecated 'pkgconfig' to 'pkg-config'
When running ./release.sh:

> DEPRECATION: "pkgconfig" entry is deprecated and should be replaced by
> "pkg-config"
2023-11-23 23:39:40 +01:00
94031dfe97 Update documentation about video orientation
PR #4441 <https://github.com/Genymobile/scrcpy/pull/4441>
2023-11-23 23:34:46 +01:00
b43a9e8e7a Add --orientation
Add a shortcut to set both the display and record orientations.

PR #4441 <https://github.com/Genymobile/scrcpy/pull/4441>
2023-11-23 23:34:46 +01:00
a9d6cb5837 Add --record-orientation
Add an option to store the orientation to apply in a recorded file.

Only rotations are supported (not flips).

PR #4441 <https://github.com/Genymobile/scrcpy/pull/4441>
2023-11-23 23:34:46 +01:00
2f92686930 Pass --lock-video-orientation argument in degrees
For consistency with the new --display-orientation option, express the
--lock-video-orientation in degrees clockwise:

 * --lock-video-orientation=0 -> --lock-video-orientation=0
 * --lock-video-orientation=3 -> --lock-video-orientation=90
 * --lock-video-orientation=2 -> --lock-video-orientation=180
 * --lock-video-orientation=1 -> --lock-video-orientation=270

PR #4441 <https://github.com/Genymobile/scrcpy/pull/4441>
2023-11-23 23:27:32 +01:00
bb88b60227 Add --display-orientation
Deprecate the option --rotation and introduce a new option
--display-orientation with the 8 possible orientations (0, 90, 180, 270,
flip0, flip90, flip180 and flip270).

New shortcuts MOD+Shift+(arrow) dynamically change the display
(horizontal or vertical) flip.

Fixes #1380 <https://github.com/Genymobile/scrcpy/issues/1380>
Fixes #3819 <https://github.com/Genymobile/scrcpy/issues/3819>
PR #4441 <https://github.com/Genymobile/scrcpy/pull/4441>
2023-11-23 23:27:28 +01:00
25e33566f5 Mention turning off audio in camera documentation 2023-11-21 08:46:38 +01:00
9df92ebe37 Fix manpage style syntax 2023-11-20 14:05:54 +01:00
0801cf0627 Fix options alphabetical order
Renaming --display to --display-id broke the alphabetical order.

Refs 23e116064d
2023-11-20 14:03:51 +01:00
4658c0e5d2 Update record format error message
Recording now supports formats other than mp4 and mkv.

Refs e637feba51
2023-11-16 10:02:25 +01:00
45a073a333 Do not create Device instance for camera
The device instance manages the display and the injection of input
events. It is not necessary for camera capture.
2023-11-16 09:10:28 +01:00
7e3b935932 Recreate the display on rotation
On Android 14 (Pixel 8), a device rotation while the camera app was
running resulted in an incorrect capture.

Destroying and recreating the display fixes the issue.
2023-11-16 08:56:04 +01:00
abcb100597 Upgrade Android SDK to 34 2023-11-15 21:13:12 +01:00
e8801cc3c0 Upgrade AGP (8.1.3) and Gradle to 8.4
Android Gradle Plugin 8.1.3.
Gradle 8.4.

From now on, Java 17 is required.
2023-11-15 21:13:12 +01:00
86808e8114 Upgrade Android checkstyle to 10.12.5
Upgrade to the latest version.
2023-11-15 21:13:12 +01:00
15a3bad4ab Log PTS fixing at debug level
Audio PTS are retrieved by AudioRecord.getTimestamp(), so they do not
necessarily exactly match the number of samples (this allows to take
drift and lag into account).

As a consequence, two consecutive timestamps in microseconds may
sometimes end up within the same millisecond, causing the warning. This
is particularly true for the Matroska muxer which uses a timebase of
1/1000 (1 ms precision).

Since this is "expected", lower the log level from warning to debug.
2023-11-15 21:06:53 +01:00
200488111e Add support for RAW audio (WAV) recording
RAW audio forwarding was supported but not for recording.

Add support for recording a raw audio stream to a `.wav` file (and
`.mkv`).
2023-11-15 21:05:38 +01:00
1713422c13 Upgrade FFmpeg build to 6.1-scrcpy-2
Use a build with WAV muxer.
2023-11-15 12:02:57 +01:00
4b4f045e19 Fix audio PTS by the duration of 1 sample
If the difference of PTS between two consecutive blocks of audio is less
than 1 sample, then it will be considered as non-increasing by FFmpeg
muxers having a time_base of 1/sample_rate.

Increase the PTS by 1 sample instead.
2023-11-15 12:02:57 +01:00
a402eac7f2 Compute PTS of intermediate blocks
If several reads are performed for a single captured audio block (e.g.
if the read size is smaller than the captured block), then the provided
timestamp was the same for all packets.

Recompute the timestamp for each of them.
2023-11-15 12:02:57 +01:00
3bb6b0cb9f Read audio by blocks of 1024 samples
In practice, the system captures audio samples by blocks of 1024
samples.

Remplace the hardcoded value of 5 milliseconds (240 samples), and let
AudioRecord fill the input buffer provided by MediaCodec (or by
AudioRawRecorder), with a maximum size of 1024 samples (just in case).
2023-11-15 12:02:57 +01:00
258eaaae2a Increase default audio buffer for FLAC
FLAC is not low latency: the default encoder produces blocks of 4096
samples, which represent ~85.333ms.

Increase the audio buffer by default so that audio playback works.
2023-11-15 12:02:57 +01:00
4857c5dd59 Add support for FLAC audio codec
PR #4410 <#https://github.com/Genymobile/scrcpy/pull/4410>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-11-15 12:02:57 +01:00
f23be823fd Upgrade FFmpeg build to 6.1-scrcpy
Upgrade to FFmpeg 6.1, and with FLAC support enabled.
2023-11-15 12:02:57 +01:00
783719c72e Fix OPUS packet in an endian-independent way
Reading the header id as an int assumed that the current endianness was
little endian. Read to a byte array to remove this assumption.
2023-11-15 12:02:57 +01:00
80defdd8aa Suppress private APIs lints to Workarounds class
The whole class need them (including the static block).
2023-11-15 12:01:10 +01:00
e637feba51 Update muxers documentation
Recording now supports formats other than mp4 and mkv.
2023-11-14 09:08:24 +01:00
5e59ed3135 Always initialize SDL with the video subsystem
Clipboard synchronization requires SDL_INIT_VIDEO, so always initialize
the video subsystem, even if --no-video or --no-video-playback is
passed.

Refs caf594c90e
Fixes #4418 <https://github.com/Genymobile/scrcpy/issues/4418>
2023-11-11 11:41:15 +01:00
4eb33054cd Do not log EPIPE on close for raw audio
Handle EPIPE the same way in AudioRawRecorder as in AudioEncoder.

This prevents useless errors on close.
2023-11-11 11:24:47 +01:00
420d3a40dd Fix error handling in raw audio recorder
It is incorret to ever call:

    streamer.writeDisableStream(...);

after:

    streamer.writeAudioHeader();

Move the try-catch block so that it can never happen.
2023-11-11 11:24:47 +01:00
9d5f53caa7 Stop capture on any RAW audio error
The server was stopped only if an IOException occurred during RAW audio
capture, but it did not catch RuntimeExceptions.
2023-11-11 11:24:47 +01:00
3c45625324 Log recording RAW audio codec as error
It is not possible to record with a RAW audio codec, so the log before
exiting should be an error rather than a warning.
2023-11-11 11:24:47 +01:00
11d738321f Recover on invalid camera FPS ranges
Some devices may provide invalid ranges, causing an
IllegalArgumentException "lower must be less than or equal to upper".

Catch the exception to list the cameras anyway.

Refs #4403 <https://github.com/Genymobile/scrcpy/issues/4403>
2023-11-05 21:45:15 +01:00
ccaa832f48 Simplify --list-cameras output
Remove --video-source=camera from the output of --list-cameras (this is
implicit).
2023-11-05 21:44:33 +01:00
4e4ddc499f Return the FakeContext as application context
This avoids getApplicationContext() to return null and cause
NullPointerException.

Fixes #4392 <https://github.com/Genymobile/scrcpy/issues/4392#issuecomment-1792806080>
2023-11-03 19:07:15 +01:00
8d76b3e06d Fill application context for camera
Using the camera fails on some devices without a proper application
context.

Fixes #4392 <https://github.com/Genymobile/scrcpy/issues/4392>
2023-11-03 19:07:08 +01:00
85a0b935c9 Always assign a system context as base context
FakeContext used ActivityThread.getSystemContext() as base context only
in some cases, because it caused problems on some devices:
 - warnings on Xiaomi devices [1], which are now fixed by
   b8c5853aa6
 - issues related to Looper [2], which are solved by just calling
   Looper.prepare*()

Therefore, we can now always assign a base context, which simplifies and
helps to solve camera issues on some devices (#4392).

[1] <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
[2] <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>

Fixes #4392 <https://github.com/Genymobile/scrcpy/issues/4392>
2023-11-03 19:05:50 +01:00
8c3e2bae7b Simplify Application instantiation
The constructor is public.
2023-11-03 19:05:28 +01:00
446ea818a4 Update links to v2.2 2023-11-01 18:47:58 +01:00
c3c7bf7af3 Bump version to v2.2 2023-11-01 18:36:33 +01:00
5000368c2f Merge branch 'master' into release 2023-11-01 18:36:13 +01:00
855ae4adb1 Upgrade SDL (2.28.4) for Windows
Include the latest version of SDL in Windows releases.
2023-11-01 18:36:10 +01:00
a8db3ec9e2 Upgrade platform-tools (34.0.5) for Windows
Include the latest version of adb in Windows releases.
2023-11-01 18:36:10 +01:00
ff579990c2 Shutdown connection before joining threads
Interrupting async processors may require to shutdown the connection to
wake up blocking calls.

Therefore, shutdown the connection first, then join the threads, then
close the connection.

Refs commit 9c08eb79cb
2023-11-01 18:36:10 +01:00
b8c5853aa6 Disable default stdout/stderr
Some devices (mostly Xiaomi) print internal errors using
e.printStackTrace(), flooding the console with irrelevant errors.

Disable system streams used via System.out and System.err streams, to
print only the logs from scrcpy.

Refs #994 <https://github.com/Genymobile/scrcpy/issues/994>
Refs #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-11-01 18:36:04 +01:00
c64d150202 Improve manpage formatting 2023-10-31 19:20:59 +01:00
8350a61926 Simplify URLs in manpage
The .UR-formatted URLs are not always rendered correctly. Use simple
brackets instead.
2023-10-31 19:19:34 +01:00
5580803406 Always print device model and version
Print the log before checking for --list-* options so that it is
printed in all cases.
2023-10-31 19:19:33 +01:00
9bfc749803 Add camera documentation
PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
6af4bd601f Add support for high frame rate camera capture
Add --camera-high-speed to enable high frame rate camera capture. If
the option is enabled, then --camera-fps is mandatory.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
4722bff423 Add --camera-fps
Add a new option for specifying the camera frame rate.

By default, Android's default frame rate (30 fps) is used.

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

Signed-off-by: Andrew Gunnerson <accounts+github@chiller3.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
928f8b8eb3 Do not arbitrary limit --max-fps to 1000
Limit to the variable type size, for consistency.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
9fc5835485 Fail-fast camera mirroring on Android 11 and older
PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
dd36d6135f Support camera size selection using -m/--camera-ar
In addition to --camera-size to specify an explicit size, make it
possible to select the camera size automatically, respecting the maximum
size (already used for display mirroring) and an aspect ratio.

For example, "scrcpy --video-source=camera" followed by:
 - (no additional arguments)
    : mirrors at the maximum size, any a-r
 - -m1920
    : only consider valid sizes having both dimensions not above 1920
 - --camera-ar=4:3
    : only consider valid sizes having an aspect ratio of 4:3 (+/- 10%)
 - -m2048 --camera-ar=1.6
    : only consider valid sizes having both dimensions not above 2048
      and an aspect ratio of 1.6 (+/- 10%)

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

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-10-31 15:57:06 +01:00
faebb7d70a Add --camera-facing
Add an option to select the camera by its lens facing (front, back or
external).

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
7f8d079c8c Make camera id optional
If no camera id is provided, use the first camera available.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
64930e71b9 Handle camera disconnection
Stop mirroring on camera disconnection.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
d544e577c0 Automatically select audio source
If --audio-source is not specified, select the default value
according to the video source:
 - for display mirroring, use device audio by default;
 - for camera mirroring, use microphone by default.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
bfeecc0131 Add camera mirroring
Add --video-source=camera, and related options:
 - --camera-id=<id>: select the camera by its id (see --list-cameras);
 - --camera-size=<width>x<height>: select the capture size.

Fixed #241 <https://github.com/Genymobile/scrcpy/issues/241>
PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:57:06 +01:00
f032262cd7 Add --list-camera-sizes
Add an option to list the device camera declared sizes.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 15:57:06 +01:00
cd63896d63 Add --list-cameras
Add an option to list the device cameras.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 15:56:25 +01:00
f085765e04 Factorize --list- options handling
This will limit code duplication as more list options will be added.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 12:45:40 +01:00
a2fb1b40f6 Extract SurfaceCapture from ScreenEncoder
Extract an interface SurfaceCapture from ScreenEncoder, representing a
video source which can be rendered to a Surface for encoding.

Split ScreenEncoder into:
 - ScreenCapture, implementing SurfaceCapture to capture the device
   screen,
 - SurfaceEncoder, to encode any SurfaceCapture.

This separation prepares the introduction of another SurfaceCapture
implementation to capture the camera instead of the device screen.

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

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-31 12:45:40 +01:00
41ccb5883e Force server exit at the end of main()
By default, the Java process exits when all non-daemon threads are
terminated.

The Android SDK might start some non-daemon threads internally,
preventing the scrcpy server to exit in some cases.

So force the process to exit explicitly.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 12:45:40 +01:00
23e116064d Rename --display to --display-id
The option is named "display id" everywhere.

This will be consistent with --camera-id (there will be many camera
options, so an option --camera would be confusing).

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 12:45:40 +01:00
3432029a3d Make separator configurable for parsing integers
The separator was hardcoded to ':'. This will allow to reuse the
function to parse sizes as WIDTHxHEIGHT.

PR #4213 <https://github.com/Genymobile/scrcpy/pull/4213>
2023-10-31 12:45:31 +01:00
7a2b756f1e Fix incorrect comment about AV1 constant
MediaFormat.MIMETYPE_VIDEO_AV1 has been added in API 29, not 21.
2023-10-31 12:35:04 +01:00
b7ad652a75 Move empty string test for crop option parsing
For consistency with other options.
2023-10-26 22:42:46 +02:00
76a99a7fcd Replace raw number by its name
PR #4373 <https://github.com/Genymobile/scrcpy/pull/4373>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-25 16:13:36 +02:00
68b55ef2fe Replace sprintf() with safer snprintf()
PR #4373 <https://github.com/Genymobile/scrcpy/pull/4373>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-25 16:13:34 +02:00
bc8913e12b Use char * for pointer arithmetic
PR #4374 <https://github.com/Genymobile/scrcpy/pull/4374>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-24 23:12:36 +02:00
3c2013de10 Enable missing-prototypes warning
Warn if a global function is defined without a previous prototype
declaration. It is not enabled by default at warning_level=2.
2023-10-24 23:06:57 +02:00
8cef8bac94 Declare local functions as static
PR #4374 <https://github.com/Genymobile/scrcpy/pull/4374>

Co-authored-by: Romain Vimont <rom@rom1v.com>
Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-24 23:06:57 +02:00
0bbe8a7007 Wrap macros in do-while(0)
To fix the warnings of stray `;`.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-24 22:58:33 +02:00
9fdb882509 Fix --pause-on-exit parsing
The function incorrectly returned `false` instead of a valid (and
expected) enum value.
2023-10-24 22:53:41 +02:00
8e7b041f35 Add missing voids for empty parameter list 2023-10-23 21:50:40 +02:00
9ade389069 Make sc_usb_devices_destroy() static
It is only called from the implementation file.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-23 15:39:19 +02:00
90ba885547 Remove redundant ;
PR #4371 <https://github.com/Genymobile/scrcpy/pull/4371>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-23 15:09:26 +02:00
7adf98e9d4 Use void for empty function parameter list
PR #4371 <https://github.com/Genymobile/scrcpy/pull/4371>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-23 15:07:24 +02:00
90ca46ee41 Add scrcpy-server to .gitignore
The script install_release.sh downloads a file named scrcpy-server to
the repo root directory. Add it to .gitignore so that it is ignored.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-10-21 09:01:50 +02:00
1c864a88eb Use --pause-on-exit from launchers
The terminal opened by scrcpy-console (.bat or .desktop) must not close
if scrcpy terminates with an error, so that error messages can be read.

Refs #3817 <https://github.com/Genymobile/scrcpy/pull/3817>
Refs #3822 <https://github.com/Genymobile/scrcpy/pull/3822>
PR #4130 <https://github.com/Genymobile/scrcpy/pull/4130>
2023-10-11 09:43:44 +02:00
1650b7c058 Add --pause-on-exit
Add an option to make scrcpy pause on exit.

Three behaviors are possible:
 - always pause on exit:
    --pause-on-exit
    --pause-on-exit=true
 - never pause on exit:
    (no option)
    --pause-on-exit=false
 - pause when scrcpy returns with an error (a non-zero exit code):
    --pause-on-exit=if-error

This is useful to prevent the terminal window from automatically
closing, so that error messages can be read.

Refs #3817 <https://github.com/Genymobile/scrcpy/pull/3817>
Refs #3822 <https://github.com/Genymobile/scrcpy/pull/3822>
PR #4130 <https://github.com/Genymobile/scrcpy/pull/4130>
2023-10-11 09:43:44 +02:00
a7c3c9a54c Make fillBaseContext() method private
This is consistent with fillAppInfo() and fillAppContext(), which are
also private.
2023-08-22 20:10:06 +02:00
111d02fca4 Add missing 'final' in Java classes
For consistency.
2023-08-22 18:52:29 +02:00
36670dda40 Fix warning typo
A parenthesis was missing.
2023-08-07 20:22:17 +02:00
0983f0a194 Report device disconnection on audio EOS
If --no-video was set, then device disconnection was not reported. To
avoid the problem, report device disconnection also on audio
end-of-stream (EOS).

If both video and audio are enabled, then a device disconnection event
will be sent twice, but only the first one will be handled (since it
makes scrcpy exit).

Fixes #4207 <https://github.com/Genymobile/scrcpy/issues/4207>
2023-08-01 12:05:16 +02:00
110b3a16f6 Do not disable controls without video playback
Some control messages can still be used even when video playback is
disabled (i.e. there is no window), for example to turn the screen off.

This reverts commit 92483fe11b
(semantically).

Fixes #4175 <https://github.com/Genymobile/scrcpy/issues/4175>
2023-07-28 14:45:33 +02:00
1ee46970e3 Fix TCP/IP link in README
Refs 328ed3650d
PR #4173 <https://github.com/Genymobile/scrcpy/pull/4173>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-07-26 19:58:13 +02:00
fcdf847dd3 Add missing syntax highlighting in audio doc 2023-07-14 23:37:19 +02:00
ad05a01800 Add Encoder section
This will allow to reference the encoder section directly in issues.
2023-07-14 23:36:21 +02:00
328ed3650d Extract device connection to a separate doc page
Create a new "Connection" documentation page.
2023-07-14 23:31:26 +02:00
c14668b177 Move display section to video documentation 2023-07-14 23:26:52 +02:00
637f48f360 Update links to v2.1.1 2023-07-14 23:09:44 +02:00
d391fc3b69 Bump version to 2.1.1 2023-07-14 18:58:58 +02:00
75ad925423 Merge branch 'master' into release 2023-07-14 18:58:34 +02:00
7e936fa879 Fix meizu deadlock
Some devices (Meizu) assume that the video encoding thread has a
Looper. By moving video encoding to a separate thread, commit
feab87053a broke this assumption.

Call Looper.prepare() from this thread to fix the problem.

Fixes #4143 <https://github.com/Genymobile/scrcpy/issues/4143>
2023-07-13 21:46:50 +02:00
01d785d9a3 Increase attempts to start AudioRecord
Making the shell app foreground (specific for Android 11) may take more
than 300ms on some devices, so increase the number of attempts from 3 to
5 (separated by 100ms).

Fixes #4147 <https://github.com/Genymobile/scrcpy/issues/4147>
Refs #3796 <https://github.com/Genymobile/scrcpy/issues/3796>
Refs 02f4ff7534
2023-07-07 18:21:17 +02:00
fe6e9acb36 Log device selection at INFO level
The selected device should be logged by default.
2023-07-04 18:22:33 +02:00
625934fb1b Fix fedora package in build instructions
In Fedora, the package is libusb1-devel.

Fixes #4131 <https://github.com/Genymobile/scrcpy/issues/4131>
PR #4132 <https://github.com/Genymobile/scrcpy/pull/4132>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-06-30 21:21:13 +02:00
85b55b3c4e Fix possible division by zero
On sway (a window manager), SDL_WINDOWEVENT_EXPOSED and
SDL_WINDOWEVENT_SIZE_CHANGED might not be called before a mouse event is
triggered. As a consequence, the "content rectangle" might not be
initialized when the mouse event is processed, causing a division by
zero.

To avoid the problem, initialize the content rect immediately when the
window is shown.

Fixes #4115 <https://github.com/Genymobile/scrcpy/issues/4115>
2023-06-29 19:18:32 +02:00
7b7076ef85 Add direct links to donations 2023-06-28 10:58:00 +02:00
808bd14e30 Ignore fold change events for other display ids
Scrcpy mirrors a specific display id, it must ignore events for other
display ids.

Fixes #4120 <https://github.com/Genymobile/scrcpy/issues/4120>
2023-06-27 18:43:22 +02:00
0049b3ce07 Remove superfluous log
This line was committed by error in commit
a52053421a.
2023-06-23 08:23:29 +02:00
5764f47fee Update links to v2.1 2023-06-22 01:18:17 +02:00
2dab1f7024 Bump version to 2.1 2023-06-22 01:15:44 +02:00
744312ec64 Merge branch 'master' into release 2023-06-22 01:15:39 +02:00
b9315620e2 Fix adb forward initialization
In forward mode, the dummy byte must be written immediately after the
first accept(), otherwise the client will wait indefinitely, causing a
deadlock (or a timeout).

Regression introduced by 8c650e53cd.
2023-06-22 01:13:53 +02:00
ea59d525bd Fix code style
The code should fit in 80 columns.
2023-06-22 01:07:09 +02:00
0ffcfa0f5c Accept failure in rotation or fold registration
Do not make scrcpy fail if rotation or display fold listeners could not
be registered.
2023-06-22 00:52:54 +02:00
c0f3c080b6 Register DisplayFoldListener only for Android 10+
This listener does not exist on Android < 10, and it makes scrcpy fail.
2023-06-22 00:49:11 +02:00
d046678f85 Upgrade platform-tools (34.0.3) for Windows
Include the latest version of adb in Windows releases.
2023-06-22 00:10:37 +02:00
fae3fbc934 Update developer documentation 2023-06-22 00:03:26 +02:00
5061b7e02c Fix build without gradle
Add missing class generation from IDisplayFoldListener.aidl.

Refs 24999d0d32
2023-06-22 00:03:26 +02:00
09009c2aa7 Upgrade SDL (2.28.0) for Windows
Include the latest version of SDL in Windows releases.

Fixes #3825 <https://github.com/Genymobile/scrcpy/issues/3825>
Refs libsdl/#7478 <https://github.com/libsdl-org/SDL/issues/7478>
2023-06-20 21:45:14 +02:00
fb21bbf763 Add workarounds for Honor devices
Audio did not work on Honor devices.

To make it work, a system context must be set as a base context of
FakeContext (so that a PackageManager is available), and a current
Application and ActivityThread must be set.

These workarounds must not be applied for all devices, because they
might cause other issues.

Fixes #4015 <https://github.com/Genymobile/scrcpy/issues/4015>
Refs #3085 <https://github.com/Genymobile/scrcpy/issues/3805>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2023-06-19 18:45:40 +02:00
0f1afff7a6 Move workarounds execution
Expose a single public static method in the Workarounds class to apply
all necessary workarounds.
2023-06-19 18:45:11 +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
4ad7479425 Add missing shortcut in documentation
MOD+Backspace also triggers BACK.
2023-06-08 08:52:21 +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
c4caa6b81d Document --no-{video,audio}-playback
PR #4033 <https://github.com/Genymobile/scrcpy/pull/4033>
2023-05-27 10:08:10 +02:00
1efbfe1175 Add separate video and audio playback options
Add --no-video-playback and --no-audio-playback. The option
--no-playback is now an alias for both.

PR #4033 <https://github.com/Genymobile/scrcpy/pull/4033>
2023-05-27 10:08:10 +02:00
751c09f47a Simplify V4L2/USB ifdefs
Define local variables whose value depends on ifdefs, to avoid
cluttering all conditions with ifdefs.
2023-05-27 09:55:49 +02:00
6ad46d70b8 Define v4l2_buffer only if HAVE_V4L2
If V4L2 support is disabled, there is no v4l2 buffer option.
2023-05-27 09:55:49 +02:00
f46758d1c5 Fix V4L2 error message when disabled
For consistency, use the same error message for --v4l2-sink and
--v4l2-buffer.
2023-05-27 09:55:49 +02:00
e71f5358b3 Reorder command line options checks
Perform checks that impact the options first.
2023-05-27 09:55:49 +02:00
a2c8910006 Rename --no-mirror to --no-playback
This option impacts video and audio _playback_. For example, if we use
V4L2, the device is still "mirrored" (via V4L2), even if playback is
disabled. Therefore, "playback" is more approriate than "mirror".

The initial option --no-display option was renamed to --no-mirror by
commit 6928acdeac, but this has never been
released, so it is ok to rename it one more time.

Refs #3978 <https://github.com/Genymobile/scrcpy/pull/3978#issuecomment-1549420103>
PR #4033 <https://github.com/Genymobile/scrcpy/pull/4033>
2023-05-27 09:55:38 +02:00
cab354102d Create AudioRecord by reflection as a fallback
Some devices (Vivo phones) fail to create an AudioRecord from an
AudioRecord.Builder (which throws a NullPointerException).

In that case, create an AudioRecord instance directly by reflection.

The AOSP version of AudioRecord constructor code can be found at:
 - Android 11 (R):
   <https://cs.android.com/android/platform/superproject/+/android-11.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=335;drc=64ed2ec38a511bbbd048985fe413268335e072f8>
 - Android 12 (S):
   <https://cs.android.com/android/platform/superproject/+/android-12.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=388;drc=2eebf929650e0d320a21f0d13677a27d7ab278e9>
 - Android 13 (T, functionally identical to Android 12):
   <https://cs.android.com/android/platform/superproject/+/android-13.0.0_r1:frameworks/base/media/java/android/media/AudioRecord.java;l=382;drc=ed242da52f975a1dd18671afb346b18853d729f2>
 - Android 14 (U): Not released, but expected to change

PR #3862 <https://github.com/Genymobile/scrcpy/pull/3862>
Fixes #3805 <https://github.com/Genymobile/scrcpy/issues/3805>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-05-26 18:56:43 +02:00
597d2ccc01 Rename FORMAT to ENCODING
The AudioFormat contains several properties. This specific value is
named "encoding".
2023-05-24 22:13:09 +02:00
38900d7730 Extract audio source to a static constant
For consistency with the other parameters.
2023-05-24 22:13:09 +02:00
e926bf1fe8 Delay window resize when minimized
On some window managers (e.g. on Windows), performing a resize while the
window is minimized does nothing (the restored window keeps its old
size).

Therefore, like for maximized and fullscreen states, wait for the window
to be restored to apply a resize.

Refs #3947 <https://github.com/Genymobile/scrcpy/issues/3947>
2023-05-22 18:22:45 +02:00
6298ef095f Accept texture failures
When the scrcpy window is minimized on Windows with D3D9, texture
creation and update fail.

In that case, do not terminate scrcpy. Instead, store the pending size
or frame to update, to attempt again during the next update or
rendering.

Fixes #3947 <https://github.com/Genymobile/scrcpy/issues/3947>
2023-05-22 18:21:10 +02:00
958f22490b Document installation via winget on Windows
PR #4005 <https://github.com/Genymobile/scrcpy/pull/4005>
Refs #1444 <https://github.com/Genymobile/scrcpy/issues/1444>
Refs #3932 <https://github.com/Genymobile/scrcpy/issues/3932>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-05-17 19:36:39 +02:00
7d33798b40 Upgrade FFmpeg build to 6.0-scrcpy-4
Use FFmpeg DLLs which do not depend on zlib1.dll.
2023-05-15 21:55:22 +02:00
d500550212 Update audio recording documentation
Document how to record audio-only to .opus and .aac files.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-15 14:28:55 +02:00
a166eee909 Upgrade FFmpeg build to 6.0-scrcpy-3
Use a build which includes the opus muxer, to support recording to .opus
files.

Refs <https://github.com/rom1v/scrcpy-deps/commits/6.0-scrcpy-3>
PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-15 14:28:53 +02:00
b11b363e8e Add recording to aac file
It is just an alias for mp4.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 17:11:34 +02:00
7321db6f28 Add recording to opus file
Use the FFmpeg opus muxer to record an opus file.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 17:11:34 +02:00
d6bcde565f Accept .m4a and .mka
These are just aliases for mp4 and mkv when there is no video stream.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 17:11:34 +02:00
98f4f4e68a Refactor command line checks
Several checks are performed when opts->record_filename is not NULL.
Group them in a single block.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 17:11:34 +02:00
be86e14e05 Factorize record format parsing
Convert either the filename extension or the explicit record format
to a sc_record_format using the same function.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 17:11:34 +02:00
8c650e53cd Add --no-video
Similar to --no-audio, add --no-video to play audio only.

Fixes #3842 <https://github.com/Genymobile/scrcpy/issues/3842>
PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 17:11:34 +02:00
e89e772c7c Remove unnecessary 'else'
Some server parameters may depend on one another. For example,
audio_bit_rate is meaningless if audio is false.

But it is inconsistent to disable some parameters based on these
dependencies checks, but not others. Handling all dependencies between
parameters would add too much complexity for no benefit.

So just pass individual parameters independently.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 16:41:12 +02:00
feab87053a Convert screen encoder to async processor
Contrary to the other tasks (controller and audio capture/encoding), the
screen encoder was executed synchronously. As a consequence,
scrcpy-server could not terminate until the screen encoder returned.

Convert it to an async processor. This allows to terminate on controller
error, and this paves the way to disable video mirroring.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 16:41:09 +02:00
751a3653a0 Add missing @Override annotations
PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 16:41:06 +02:00
9c08eb79cb Close connection at the end of finally-block
The async processors use the socket file descriptors from the
connection. Therefore, the connection must not be closed before all
async processor threads are joined.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 16:41:04 +02:00
92483fe11b Disable controls on --no-mirror
If mirroring is disabled, control must also be disabled.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 16:41:01 +02:00
6928acdeac Rename --no-display to --no-mirror
The option impacts both video and audio playback, so "no display" is not
an appropriate name.

PR #3978 <https://github.com/Genymobile/scrcpy/pull/3978>
2023-05-08 16:40:58 +02:00
cb20bcb16f Clarify API versions that support Audio Forwarding
Reword the supported API versions for audio forwarding sentence to
clarify that it supports API >= 30

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-04-26 19:33:28 +02:00
0f3af2d20b Fix build for FFmpeg < 3.3
The constant AV_CODEC_ID_AV1 was introduced in FFmpeg 3.3. Add an ifdef
to support older versions.

Fixes #3939 <https://github.com/Genymobile/scrcpy/issues/3939>
2023-04-23 12:26:46 +02:00
Yan
c083a7cc90 Force OpenGL Core Profile context on macOS
By default, SDL creates an OpenGL 2.1 context on macOS for an OpenGL
renderer. As a consequence, mipmapping is not supported.

Force to use a core profile context, to get a higher version.

Before:

    INFO: Renderer: opengl
    INFO: OpenGL version: 2.1 NVIDIA-14.0.32 355.11.11.10.10.143
    WARN: Trilinear filtering disabled (OpenGL 3.0+ or ES 2.0+ required)

After:

    INFO: Renderer: opengl
    DEBUG: Creating OpenGL Core Profile context
    INFO: OpenGL version: 4.1 NVIDIA-14.0.32 355.11.11.10.10.143
    INFO: Trilinear filtering enabled

when running with:

    scrcpy --verbosity=debug --render-driver=opengl

Note: Since SDL_CreateRenderer() causes a fallback to OpenGL 2.1, the
profile and version attributes have to be set and the context created
_after_.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-04-12 21:26:24 +02:00
9eb6591913 Add missing --no-audio option in manpage 2023-04-10 19:30:04 +02:00
9cfea347d0 Remove Options setters
Now that options parsing is performed from the Options class, setters
are not necessary anymore.
2023-04-09 20:02:39 +02:00
ce064fb5e0 Move options parsing to Options class 2023-04-09 20:02:39 +02:00
afcdfc7fd7 Fix checkstyle violation
Checkstyle reported this error:

    [ant:checkstyle] [ERROR] AudioCapture.java:89:145: '+' should be on
    a new line. [OperatorWrap]
2023-04-09 20:01:58 +02:00
8f0b38cc4f Specify in README that OTG does not require adb 2023-03-31 07:55:13 +02:00
a1e8a34001 Fix documentation link in FAQ 2023-03-28 08:32:07 +02:00
00534b0b2d Fix typo in FAQ 2023-03-28 08:31:01 +02:00
21df2c240e Mention necessary reboot
After setting "USB debugging (security settings)", a reboot is
necessary.
2023-03-23 19:06:44 +01:00
2d3059e1ab Reference FAQ from HID/OTG documentation
Reference the FAQ section about "HID/OTG issues on Windows" from the
HID/OTG documentation.
2023-03-20 19:42:23 +01:00
478aece68f Replace "bit-rate" with "bit rate" 2023-03-20 08:35:13 +01:00
55899c091e Fix typo in doc/audio.md
The documentation is about audio bit rate, not video bit rate.

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-03-20 08:32:58 +01:00
d9a644df9c Clarify V4L2 feature in README
The previous formulation could suggest that the device camera could be
used as a webcam. This is not the case (yet?).
2023-03-15 10:46:36 +01:00
45717733a1 Document missing Opus encoder error
And how to solve it.
2023-03-15 00:41:07 +01:00
6ad037ea04 Update Gentoo instructions
scrcpy is available directly in the distro, drop link to the overlay
(which only contains older versions).

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

Signed-off-by: Romain Vimont <rom@rom1v.com>
2023-03-14 22:03:21 +01:00
115 changed files with 5367 additions and 1683 deletions

1
.gitignore vendored
View File

@ -7,3 +7,4 @@ build/
.gradle/ .gradle/
/x/ /x/
local.properties local.properties
/scrcpy-server

8
FAQ.md
View File

@ -159,6 +159,8 @@ In developer options, enable:
> **USB debugging (Security settings)** > **USB debugging (Security settings)**
> _Allow granting permissions and simulating input via USB debugging_ > _Allow granting permissions and simulating input via USB debugging_
Rebooting the device is necessary once this option is set.
[simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 [simulating input]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
@ -168,12 +170,12 @@ The default text injection method is [limited to ASCII characters][text-input].
A trick allows to also inject some [accented characters][accented-characters], A trick allows to also inject some [accented characters][accented-characters],
but that's all. See [#37]. but that's all. See [#37].
Since scrcpy v1.20, it is possible to simulate a [physical keyboard][hid] (HID). It is also possible to simulate a [physical keyboard][hid] (HID).
[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode
[accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters
[#37]: https://github.com/Genymobile/scrcpy/issues/37 [#37]: https://github.com/Genymobile/scrcpy/issues/37
[hid]: README.md#physical-keyboard-simulation-hid [hid]: doc/hid-otg.md
## Client issues ## Client issues
@ -229,4 +231,4 @@ Translations of this FAQ in other languages are available in the [wiki].
[wiki]: https://github.com/Genymobile/scrcpy/wiki [wiki]: https://github.com/Genymobile/scrcpy/wiki
Only this README file is guaranteed to be up-to-date. Only this FAQ file is guaranteed to be up-to-date.

View File

@ -1,11 +1,15 @@
# scrcpy (v2.0) **This GitHub repo (<https://github.com/Genymobile/scrcpy>) is the only official
source for the project. Do not download releases from random websites, even if
their name contains `scrcpy`.**
# scrcpy (v2.3.1)
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" /> <img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
_pronounced "**scr**een **c**o**py**"_ _pronounced "**scr**een **c**o**py**"_
This application mirrors Android devices (video and audio) connected via This application mirrors Android devices (video and audio) connected via
USB or [over TCP/IP](doc/device.md#tcpip-wireless), and allows to control the USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the
device with the keyboard and the mouse of the computer. It does not require any device with the keyboard and the mouse of the computer. It does not require any
_root_ access. It works on _Linux_, _Windows_ and _macOS_. _root_ access. It works on _Linux_, _Windows_ and _macOS_.
@ -25,12 +29,13 @@ It focuses on:
[lowlatency]: https://github.com/Genymobile/scrcpy/pull/646 [lowlatency]: https://github.com/Genymobile/scrcpy/pull/646
Its features include: Its features include:
- [audio forwarding](doc/audio.md) (Android >= 11) - [audio forwarding](doc/audio.md) (Android 11+)
- [recording](doc/recording.md) - [recording](doc/recording.md)
- mirroring with [Android device screen off](doc/device.md#turn-screen-off) - mirroring with [Android device screen off](doc/device.md#turn-screen-off)
- [copy-paste](doc/control.md#copy-paste) in both directions - [copy-paste](doc/control.md#copy-paste) in both directions
- [configurable quality](doc/video.md) - [configurable quality](doc/video.md)
- Android device [as a webcam (V4L2)](doc/v4l2.md) (Linux-only) - [camera mirroring](doc/camera.md) (Android 12+)
- [mirroring as a webcam (V4L2)](doc/v4l2.md) (Linux-only)
- [physical keyboard/mouse simulation (HID)](doc/hid-otg.md) - [physical keyboard/mouse simulation (HID)](doc/hid-otg.md)
- [OTG mode](doc/hid-otg.md#otg) - [OTG mode](doc/hid-otg.md#otg)
- and more… - and more…
@ -39,7 +44,7 @@ Its features include:
The Android device requires at least API 21 (Android 5.0). The Android device requires at least API 21 (Android 5.0).
[Audio forwarding](doc/audio.md) is supported from API 30 (Android 11). [Audio forwarding](doc/audio.md) is supported for API >= 30 (Android 11+).
Make sure you [enabled USB debugging][enable-adb] on your device(s). Make sure you [enabled USB debugging][enable-adb] on your device(s).
@ -47,10 +52,14 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s).
On some devices, you also need to enable [an additional option][control] `USB On some devices, you also need to enable [an additional option][control] `USB
debugging (Security Settings)` (this is an item different from `USB debugging`) debugging (Security Settings)` (this is an item different from `USB debugging`)
to control it using a keyboard and mouse. to control it using a keyboard and mouse. Rebooting the device is necessary once
this option is set.
[control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323 [control]: https://github.com/Genymobile/scrcpy/issues/70#issuecomment-373286323
Note that USB debugging is not required to run scrcpy in [OTG
mode](doc/hid-otg.md#otg).
## Get the app ## Get the app
@ -64,14 +73,16 @@ to control it using a keyboard and mouse.
The application provides a lot of features and configuration options. They are The application provides a lot of features and configuration options. They are
documented in the following pages: documented in the following pages:
- [Device](doc/device.md) - [Connection](doc/connection.md)
- [Video](doc/video.md) - [Video](doc/video.md)
- [Audio](doc/audio.md) - [Audio](doc/audio.md)
- [Control](doc/control.md) - [Control](doc/control.md)
- [Device](doc/device.md)
- [Window](doc/window.md) - [Window](doc/window.md)
- [Recording](doc/recording.md) - [Recording](doc/recording.md)
- [Tunnels](doc/tunnels.md) - [Tunnels](doc/tunnels.md)
- [HID/OTG](doc/hid-otg.md) - [HID/OTG](doc/hid-otg.md)
- [Camera](doc/camera.md)
- [Video4Linux](doc/v4l2.md) - [Video4Linux](doc/v4l2.md)
- [Shortcuts](doc/shortcuts.md) - [Shortcuts](doc/shortcuts.md)
@ -113,7 +124,10 @@ For general questions or discussions, you can also use:
I'm [@rom1v](https://github.com/rom1v), the author and maintainer of _scrcpy_. I'm [@rom1v](https://github.com/rom1v), the author and maintainer of _scrcpy_.
If you appreciate this application, you can [support my open source If you appreciate this application, you can [support my open source
work][donate]. work][donate]:
- [GitHub Sponsors](https://github.com/sponsors/rom1v)
- [Liberapay](https://liberapay.com/rom1v/)
- [PayPal](https://paypal.me/rom2v)
[donate]: https://blog.rom1v.com/about/#support-my-open-source-work [donate]: https://blog.rom1v.com/about/#support-my-open-source-work

View File

@ -7,63 +7,83 @@ _scrcpy() {
--audio-codec= --audio-codec=
--audio-codec-options= --audio-codec-options=
--audio-encoder= --audio-encoder=
--audio-source=
--audio-output-buffer= --audio-output-buffer=
-b --video-bit-rate= -b --video-bit-rate=
--camera-ar=
--camera-id=
--camera-facing=
--camera-fps=
--camera-high-speed
--camera-size=
--crop= --crop=
-d --select-usb -d --select-usb
--disable-screensaver --disable-screensaver
--display=
--display-buffer= --display-buffer=
--display-id=
--display-orientation=
-e --select-tcpip -e --select-tcpip
-f --fullscreen
--force-adb-forward --force-adb-forward
--forward-all-clicks --forward-all-clicks
-f --fullscreen
-K --hid-keyboard
-h --help -h --help
--kill-adb-on-close
-K --hid-keyboard
--legacy-paste --legacy-paste
--list-camera-sizes
--list-cameras
--list-displays --list-displays
--list-encoders --list-encoders
--lock-video-orientation --lock-video-orientation
--lock-video-orientation= --lock-video-orientation=
--max-fps=
-M --hid-mouse
-m --max-size= -m --max-size=
-M --hid-mouse
--max-fps=
-n --no-control
-N --no-playback
--no-audio --no-audio
--no-audio-playback
--no-cleanup --no-cleanup
--no-clipboard-autosync --no-clipboard-autosync
--no-downsize-on-error --no-downsize-on-error
-n --no-control
-N --no-display
--no-key-repeat --no-key-repeat
--no-mipmaps --no-mipmaps
--no-power-on --no-power-on
--no-video
--no-video-playback
--orientation=
--otg --otg
-p --port= -p --port=
--pause-on-exit
--pause-on-exit=
--power-off-on-close --power-off-on-close
--prefer-text --prefer-text
--print-fps --print-fps
--push-target= --push-target=
--raw-key-events
-r --record= -r --record=
--raw-key-events
--record-format= --record-format=
--record-orientation=
--render-driver= --render-driver=
--require-audio --require-audio
--rotation= --rotation=
-s --serial= -s --serial=
--shortcut-mod=
-S --turn-screen-off -S --turn-screen-off
--shortcut-mod=
-t --show-touches -t --show-touches
--tcpip --tcpip
--tcpip= --tcpip=
--time-limit=
--tunnel-host= --tunnel-host=
--tunnel-port= --tunnel-port=
--v4l2-buffer= --v4l2-buffer=
--v4l2-sink= --v4l2-sink=
-V --verbosity=
-v --version -v --version
-V --verbosity=
--video-codec= --video-codec=
--video-codec-options= --video-codec-options=
--video-encoder= --video-encoder=
--video-source=
-w --stay-awake -w --stay-awake
--window-borderless --window-borderless
--window-title= --window-title=
@ -80,11 +100,36 @@ _scrcpy() {
return return
;; ;;
--audio-codec) --audio-codec)
COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur")) COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur"))
return
;;
--video-source)
COMPREPLY=($(compgen -W 'display camera' -- "$cur"))
return
;;
--audio-source)
COMPREPLY=($(compgen -W 'output mic' -- "$cur"))
return
;;
--camera-facing)
COMPREPLY=($(compgen -W 'front back external' -- "$cur"))
return
;;
--orientation
--display-orientation)
COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur"))
return
;;
--record-orientation)
COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur"))
return return
;; ;;
--lock-video-orientation) --lock-video-orientation)
COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur"))
return
;;
--pause-on-exit)
COMPREPLY=($(compgen -W 'true false if-error' -- "$cur"))
return return
;; ;;
-r|--record) -r|--record)
@ -92,17 +137,13 @@ _scrcpy() {
return return
;; ;;
--record-format) --record-format)
COMPREPLY=($(compgen -W 'mkv mp4' -- "$cur")) COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac wav' -- "$cur"))
return return
;; ;;
--render-driver) --render-driver)
COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur"))
return return
;; ;;
--rotation)
COMPREPLY=($(compgen -W '0 1 2 3' -- "$cur"))
return
;;
--shortcut-mod) --shortcut-mod)
# Only auto-complete a single key # Only auto-complete a single key
COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur"))
@ -123,8 +164,12 @@ _scrcpy() {
|--audio-codec-options \ |--audio-codec-options \
|--audio-encoder \ |--audio-encoder \
|--audio-output-buffer \ |--audio-output-buffer \
|--camera-ar \
|--camera-id \
|--camera-fps \
|--camera-size \
|--crop \ |--crop \
|--display \ |--display-id \
|--display-buffer \ |--display-buffer \
|--max-fps \ |--max-fps \
|-m|--max-size \ |-m|--max-size \

View File

@ -1,4 +1,2 @@
@echo off @echo off
scrcpy.exe %* scrcpy.exe --pause-on-exit=if-error %*
:: if the exit code is >= 1, then pause
if errorlevel 1 pause

View File

@ -5,7 +5,7 @@ Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell # For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get # startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized. # environment correctly initialized.
Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter to quit...'" Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'"
Icon=scrcpy Icon=scrcpy
Terminal=true Terminal=true
Type=Application Type=Application

View File

@ -5,7 +5,7 @@ Comment=Display and control your Android device
# For some users, the PATH or ADB environment variables are set from the shell # For some users, the PATH or ADB environment variables are set from the shell
# startup file, like .bashrc or .zshrc… Run an interactive shell to get # startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized. # environment correctly initialized.
Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy" Exec=/bin/sh -c "\\$SHELL -i -c scrcpy"
Icon=scrcpy Icon=scrcpy
Terminal=false Terminal=false
Type=Application Type=Application

View File

@ -11,64 +11,82 @@ arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]' '--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)' '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--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-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)]' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--camera-ar=[Select the camera size by its aspect ratio]'
'--camera-high-speed=[Enable high-speed camera capture mode]'
'--camera-id=[Specify the camera id to mirror]'
'--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)'
'--camera-fps=[Specify the camera capture frame rate]'
'--camera-size=[Specify an explicit camera capture size]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]' {-d,--select-usb}'[Use USB device]'
'--disable-screensaver[Disable screensaver while scrcpy is running]' '--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]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]'
'--display-id=[Specify the display id to mirror]'
'--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
{-e,--select-tcpip}'[Use TCP/IP device]' {-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]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]'
'--forward-all-clicks[Forward clicks to 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]' {-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]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]'
'--list-camera-sizes[List the valid camera capture sizes]'
'--list-cameras[List cameras available on the device]'
'--list-displays[List displays available on the device]' '--list-displays[List displays available on the device]'
'--list-encoders[List video and audio encoders 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)' '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)'
'--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,--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[Disable audio forwarding]'
'--no-audio-playback[Disable audio playback]'
'--no-cleanup[Disable device cleanup actions on exit]' '--no-cleanup[Disable device cleanup actions on exit]'
'--no-clipboard-autosync[Disable automatic clipboard synchronization]' '--no-clipboard-autosync[Disable automatic clipboard synchronization]'
'--no-downsize-on-error[Disable lowering definition on MediaCodec error]' '--no-downsize-on-error[Disable lowering definition on MediaCodec error]'
{-n,--no-control}'[Disable device control \(mirror the device in read only\)]'
{-N,--no-display}'[Do not display device \(during screen recording or when V4L2 sink is enabled\)]'
'--no-key-repeat[Do not forward repeated key events when a key is held down]' '--no-key-repeat[Do not forward repeated key events when a key is held down]'
'--no-mipmaps[Disable the generation of mipmaps]' '--no-mipmaps[Disable the generation of mipmaps]'
'--no-power-on[Do not power on the device on start]' '--no-power-on[Do not power on the device on start]'
'--no-video[Disable video forwarding]'
'--no-video-playback[Disable video playback]'
'--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)'
'--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]'
{-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]'
'--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)'
'--power-off-on-close[Turn the device screen off when closing scrcpy]' '--power-off-on-close[Turn the device screen off when closing scrcpy]'
'--prefer-text[Inject alpha characters and space as text events instead of key events]' '--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]' '--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]' '--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' {-r,--record=}'[Record screen to file]:record file:_files'
'--record-format=[Force recording format]:format:(mp4 mkv)' '--raw-key-events[Inject key events for all input keys, and ignore text events]'
'--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)'
'--record-orientation=[Set the record orientation]:orientation values:(0 90 180 270)'
'--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--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]' '--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}'\''))' {-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]' {-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]' {-t,--show-touches}'[Show physical touches]'
'--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' '--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-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]' '--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-buffer=[Add a buffering delay \(in milliseconds\) before pushing frames]'
'--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' '--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,--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=[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-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]' '--video-encoder=[Use a specific MediaCodec video encoder]'
'--video-source=[Select the video source]:source:(display camera)'
{-w,--stay-awake}'[Keep the device on while scrcpy is running, when the device is plugged in]' {-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-borderless[Disable window decorations \(display borderless window\)]'
'--window-title=[Set a custom window title]' '--window-title=[Set a custom window title]'

View File

@ -51,6 +51,7 @@ src = [
'src/util/term.c', 'src/util/term.c',
'src/util/thread.c', 'src/util/thread.c',
'src/util/tick.c', 'src/util/tick.c',
'src/util/timeout.c',
] ]
conf = configuration_data() conf = configuration_data()
@ -97,77 +98,24 @@ endif
cc = meson.get_compiler('c') cc = meson.get_compiler('c')
crossbuild_windows = meson.is_cross_build() and host_machine.system() == 'windows' dependencies = [
dependency('libavformat', version: '>= 57.33'),
dependency('libavcodec', version: '>= 57.37'),
dependency('libavutil'),
dependency('libswresample'),
dependency('sdl2', version: '>= 2.0.5'),
]
if not crossbuild_windows if v4l2_support
dependencies += dependency('libavdevice')
# native build endif
dependencies = [
dependency('libavformat', version: '>= 57.33'),
dependency('libavcodec', version: '>= 57.37'),
dependency('libavutil'),
dependency('libswresample'),
dependency('sdl2', version: '>= 2.0.5'),
]
if v4l2_support
dependencies += dependency('libavdevice')
endif
if usb_support
dependencies += dependency('libusb-1.0')
endif
else
# cross-compile mingw32 build (from Linux to Windows)
prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2')
sdl2_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/bin'
sdl2_lib_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/lib'
sdl2_include_dir = 'prebuilt-deps/data/' + prebuilt_sdl2 + '/include'
sdl2 = declare_dependency(
dependencies: [
cc.find_library('SDL2', dirs: sdl2_bin_dir),
cc.find_library('SDL2main', dirs: sdl2_lib_dir),
],
include_directories: include_directories(sdl2_include_dir)
)
prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg')
ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin'
ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include'
ffmpeg = declare_dependency(
dependencies: [
cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir),
cc.find_library('avformat-60', dirs: ffmpeg_bin_dir),
cc.find_library('avutil-58', dirs: ffmpeg_bin_dir),
cc.find_library('swresample-4', dirs: ffmpeg_bin_dir),
],
include_directories: include_directories(ffmpeg_include_dir)
)
prebuilt_libusb = meson.get_cross_property('prebuilt_libusb')
libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin'
libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include'
libusb = declare_dependency(
dependencies: [
cc.find_library('msys-usb-1.0', dirs: libusb_bin_dir),
],
include_directories: include_directories(libusb_include_dir)
)
dependencies = [
ffmpeg,
sdl2,
libusb,
cc.find_library('mingw32')
]
if usb_support
dependencies += dependency('libusb-1.0')
endif endif
if host_machine.system() == 'windows' if host_machine.system() == 'windows'
dependencies += cc.find_library('mingw32')
dependencies += cc.find_library('ws2_32') dependencies += cc.find_library('ws2_32')
endif endif
@ -288,6 +236,10 @@ if get_option('buildtype') == 'debug'
'tests/test_device_msg_deserialize.c', 'tests/test_device_msg_deserialize.c',
'src/device_msg.c', 'src/device_msg.c',
]], ]],
['test_orientation', [
'tests/test_orientation.c',
'src/options.c',
]],
['test_strbuf', [ ['test_strbuf', [
'tests/test_strbuf.c', 'tests/test_strbuf.c',
'src/util/strbuf.c', 'src/util/strbuf.c',

View File

@ -6,10 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
DEP_DIR=platform-tools-34.0.1 DEP_DIR=platform-tools-34.0.5
FILENAME=platform-tools_r34.0.1-windows.zip FILENAME=platform-tools_r34.0.5-windows.zip
SHA256SUM=5dd9c2be744c224fa3a7cbe30ba02d2cb378c763bd0f797a7e47e9f3156a5daa SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
then then

View File

@ -6,11 +6,11 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
VERSION=6.0-scrcpy-2 VERSION=6.1-scrcpy-3
DEP_DIR="ffmpeg-$VERSION" DEP_DIR="ffmpeg-$VERSION"
FILENAME="$DEP_DIR".7z FILENAME="$DEP_DIR".7z
SHA256SUM=98ef97f8607c97a5c4f9c5a0a991b78f105d002a3619145011d16ffb92501b14 SHA256SUM=b646d18a3d543a4e4c46881568213499f22e4454a464e1552f03f2ac9cc3a05a
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
then then

View File

@ -6,9 +6,10 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
DEP_DIR=libusb-1.0.26 VERSION=1.0.26
DEP_DIR="libusb-$VERSION"
FILENAME=libusb-1.0.26-binaries.7z FILENAME="libusb-$VERSION-binaries.7z"
SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5 SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
@ -17,17 +18,22 @@ then
exit 0 exit 0
fi fi
get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" "$FILENAME" "$SHA256SUM" get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR" mkdir "$DEP_DIR"
cd "$DEP_DIR" cd "$DEP_DIR"
7z x "../$FILENAME" \ 7z x "../$FILENAME" \
libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \ "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \
libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \ "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \
libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \ "libusb-$VERSION-binaries/libusb-MinGW-x64/" \
libusb-1.0.26-binaries/libusb-MinGW-x64/include/ "libusb-$VERSION-binaries/libusb-MinGW-x64/"
mv libusb-1.0.26-binaries/libusb-MinGW-Win32 . mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" .
mv libusb-1.0.26-binaries/libusb-MinGW-x64 . mv "libusb-$VERSION-binaries/libusb-MinGW-x64" .
rm -rf libusb-1.0.26-binaries rm -rf "libusb-$VERSION-binaries"
# Rename the dll to get the same library name on all platforms
mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll
mv libusb-MinGW-x64/bin/msys-usb-1.0.dll libusb-MinGW-x64/bin/libusb-1.0.dll

View File

@ -6,10 +6,11 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR" mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR"
DEP_DIR=SDL2-2.26.4 VERSION=2.28.5
DEP_DIR="SDL2-$VERSION"
FILENAME=SDL2-devel-2.26.4-mingw.tar.gz FILENAME="SDL2-devel-$VERSION-mingw.tar.gz"
SHA256SUM=fe899c8642caac2f180b1ee6f786857ddcaa0adc1fa82474312b09dd47d74712 SHA256SUM=3c0c655c2ebf67cad48fead72761d1601740ded30808952c3274ba223d226c21
if [[ -d "$DEP_DIR" ]] if [[ -d "$DEP_DIR" ]]
then then
@ -17,7 +18,8 @@ then
exit 0 exit 0
fi fi
get_file "https://libsdl.org/release/$FILENAME" "$FILENAME" "$SHA256SUM" get_file "https://github.com/libsdl-org/SDL/releases/download/release-$VERSION/$FILENAME" \
"$FILENAME" "$SHA256SUM"
mkdir "$DEP_DIR" mkdir "$DEP_DIR"
cd "$DEP_DIR" cd "$DEP_DIR"

View File

@ -13,7 +13,7 @@ BEGIN
VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "LegalCopyright", "Romain Vimont, Genymobile"
VALUE "OriginalFilename", "scrcpy.exe" VALUE "OriginalFilename", "scrcpy.exe"
VALUE "ProductName", "scrcpy" VALUE "ProductName", "scrcpy"
VALUE "ProductVersion", "2.0" VALUE "ProductVersion", "2.3.1"
END END
END END
BLOCK "VarFileInfo" BLOCK "VarFileInfo"

View File

@ -21,29 +21,21 @@ Make scrcpy window always on top (above other windows).
.TP .TP
.BI "\-\-audio\-bit\-rate " value .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). 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 128K (128000). Default is 128K (128000).
.TP .TP
.BI "\-\-audio\-buffer ms .BI "\-\-audio\-buffer " ms
Configure the audio buffering delay (in milliseconds). Configure the audio buffering delay (in milliseconds).
Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches).
Default is 50. 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 .TP
.BI "\-\-audio\-codec " name .BI "\-\-audio\-codec " name
Select an audio codec (opus, aac or raw). Select an audio codec (opus, aac, flac or raw).
Default is opus. Default is opus.
@ -53,22 +45,70 @@ Set a list of comma-separated key:type=value options for the device audio encode
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
The list of possible codec options is available in the Android documentation The list of possible codec options is available in the Android documentation:
.UR https://d.android.com/reference/android/media/MediaFormat
.UE . <https://d.android.com/reference/android/media/MediaFormat>
.TP .TP
.BI "\-\-audio\-encoder " name .BI "\-\-audio\-encoder " name
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR). 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. The available encoders can be listed by \fB\-\-list\-encoders\fR.
.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 .TP
.BI "\-b, \-\-video\-bit\-rate " value .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). Encode the video at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000).
Default is 8M (8000000). Default is 8M (8000000).
.TP
.BI "\-\-camera\-ar " ar
Select the camera size by its aspect ratio (+/- 10%).
Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6").
.TP
.B \-\-camera\-high\-speed
Enable high-speed camera capture mode.
This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR.
.TP
.BI "\-\-camera\-id " id
Specify the device camera id to mirror.
The available camera ids can be listed by \fB\-\-list\-cameras\fR.
.TP
.BI "\-\-camera\-facing " facing
Select the device camera by its facing direction.
Possible values are "front", "back" and "external".
.TP
.BI "\-\-camera\-fps " fps
Specify the camera capture frame rate.
If not specified, Android's default frame rate (30 fps) is used.
.TP
.BI "\-\-camera\-size " width\fRx\fIheight
Specify an explicit camera capture size.
.TP .TP
.BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy
Crop the device screen on the server. Crop the device screen on the server.
@ -88,18 +128,26 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR).
Disable screensaver while scrcpy is running. Disable screensaver while scrcpy is running.
.TP .TP
.BI "\-\-display " id .BI "\-\-display\-buffer " ms
Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter.
Default is 0 (no buffering).
.TP
.BI "\-\-display\-id " id
Specify the device display id to mirror. Specify the device display id to mirror.
The available display ids can be listed by \-\-list\-displays. The available display ids can be listed by \fB\-\-list\-displays\fR.
Default is 0. Default is 0.
.TP .TP
.BI "\-\-display\-buffer ms .BI "\-\-display\-orientation " value
Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. Set the initial display orientation.
Default is 0 (no buffering). Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation.
Default is 0.
.TP .TP
.B \-e, \-\-select\-tcpip .B \-e, \-\-select\-tcpip
@ -107,6 +155,10 @@ Use TCP/IP device (if there is exactly one, like adb -e).
Also see \fB\-d\fR (\fB\-\-select\-usb\fR). Also see \fB\-d\fR (\fB\-\-select\-usb\fR).
.TP
.B \-f, \-\-fullscreen
Start in fullscreen.
.TP .TP
.B \-\-force\-adb\-forward .B \-\-force\-adb\-forward
Do not attempt to use "adb reverse" to connect to the device. Do not attempt to use "adb reverse" to connect to the device.
@ -115,14 +167,14 @@ Do not attempt to use "adb reverse" to connect to the device.
.B \-\-forward\-all\-clicks .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. 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 .TP
.B \-h, \-\-help .B \-h, \-\-help
Print this help. Print this help.
.TP
.B \-\-kill\-adb\-on\-close
Kill adb when scrcpy terminates.
.TP .TP
.B \-K, \-\-hid\-keyboard .B \-K, \-\-hid\-keyboard
Simulate a physical keyboard by using HID over AOAv2. Simulate a physical keyboard by using HID over AOAv2.
@ -145,6 +197,14 @@ 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. This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically.
.TP
.B \-\-list\-camera\-sizes
List the valid camera capture sizes.
.TP
.B \-\-list\-cameras
List cameras available on the device.
.TP .TP
.B \-\-list\-encoders .B \-\-list\-encoders
List video and audio encoders available on the device. List video and audio encoders available on the device.
@ -155,16 +215,14 @@ List displays available on the device.
.TP .TP
\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] \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. Lock capture video orientation to \fIvalue\fR.
Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees.
Default is "unlocked". Default is "unlocked".
Passing the option without argument is equivalent to passing "initial". 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 .TP
.BI "\-m, \-\-max\-size " value .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. 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 +241,26 @@ It may only work over USB.
Also see \fB\-\-hid\-keyboard\fR. 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 \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR).
.TP
.B \-\-no\-audio
Disable audio forwarding.
.TP
.B \-\-no\-audio\-playback
Disable audio playback on the computer.
.TP .TP
.B \-\-no\-cleanup .B \-\-no\-cleanup
By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit. By default, scrcpy removes the server binary from the device and restores the device state (show touches, stay awake and power mode) on exit.
@ -201,14 +279,6 @@ By default, on MediaCodec error, scrcpy automatically tries again with a lower d
This option disables this behavior. This option disables this behavior.
.TP
.B \-n, \-\-no\-control
Disable device control (mirror the device in read\-only).
.TP
.B \-N, \-\-no\-display
Do not display device (only when screen recording is enabled).
.TP .TP
.B \-\-no\-key\-repeat .B \-\-no\-key\-repeat
Do not forward repeated key events when a key is held down. Do not forward repeated key events when a key is held down.
@ -221,6 +291,18 @@ If the renderer is OpenGL 3.0+ or OpenGL ES 2.0+, then mipmaps are automatically
.B \-\-no\-power\-on .B \-\-no\-power\-on
Do not power on the device on start. Do not power on the device on start.
.TP
.B \-\-no\-video
Disable video forwarding.
.TP
.B \-\-no\-video\-playback
Disable video playback on the computer.
.TP
.BI "\-\-orientation " value
Same as --display-orientation=value --record-orientation=value.
.TP .TP
.B \-\-otg .B \-\-otg
Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable.
@ -241,6 +323,16 @@ Set the TCP port (range) used by the client to listen.
Default is 27183:27199. Default is 27183:27199.
.TP
\fB\-\-pause\-on\-exit\fR[=\fImode\fR]
Configure pause on exit. Possible values are "true" (always pause on exit), "false" (never pause on exit) and "if-error" (pause only if an error occured).
This is useful to prevent the terminal window from automatically closing, so that error messages can be read.
Default is "false".
Passing the option without argument is equivalent to passing "true".
.TP .TP
.B \-\-power\-off\-on\-close .B \-\-power\-off\-on\-close
Turn the device screen off when closing scrcpy. Turn the device screen off when closing scrcpy.
@ -262,10 +354,6 @@ Set the target directory for pushing files to the device by drag & drop. It is p
Default is "/sdcard/Download/". Default is "/sdcard/Download/".
.TP
.B \-\-raw\-key\-events
Inject key events for all input keys, and ignore text events.
.TP .TP
.BI "\-r, \-\-record " file .BI "\-r, \-\-record " file
Record screen to Record screen to
@ -273,11 +361,23 @@ Record screen to
The format is determined by the The format is determined by the
.B \-\-record\-format .B \-\-record\-format
option if set, or by the file extension (.mp4 or .mkv). option if set, or by the file extension.
.TP
.B \-\-raw\-key\-events
Inject key events for all input keys, and ignore text events.
.TP .TP
.BI "\-\-record\-format " format .BI "\-\-record\-format " format
Force recording format (either mp4 or mkv). Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav).
.TP
.BI "\-\-record\-orientation " value
Set the record orientation.
Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees.
Default is 0.
.TP .TP
.BI "\-\-render\-driver " name .BI "\-\-render\-driver " name
@ -285,21 +385,20 @@ Request SDL to use the given render driver (this is just a hint).
Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software". Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "metal" and "software".
.UR https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER <https://wiki.libsdl.org/SDL_HINT_RENDER_DRIVER>
.UE
.TP .TP
.B \-\-require\-audio .B \-\-require\-audio
By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work.
.TP
.BI "\-\-rotation " value
Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise.
.TP .TP
.BI "\-s, \-\-serial " number .BI "\-s, \-\-serial " number
The device serial number. Mandatory only if several devices are connected to adb. 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 .TP
.BI "\-\-shortcut\-mod " key\fR[+...]][,...] .BI "\-\-shortcut\-mod " key\fR[+...]][,...]
Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper".
@ -310,6 +409,12 @@ For example, to use either LCtrl+LAlt or LSuper for scrcpy shortcuts, pass "lctr
Default is "lalt,lsuper" (left-Alt or left-Super). 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 .TP
.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]] .BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]]
Configure and reconnect the device over TCP/IP. Configure and reconnect the device over TCP/IP.
@ -319,27 +424,31 @@ 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. 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 .TP
.B \-S, \-\-turn\-screen\-off .BI "\-\-time\-limit " seconds
Turn the device screen off immediately. Set the maximum mirroring time, in seconds.
.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 .TP
.BI "\-\-tunnel\-host " ip .BI "\-\-tunnel\-host " ip
Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. Set the IP address of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR.
Default is localhost. Default is localhost.
.TP .TP
.BI "\-\-tunnel\-port " port .BI "\-\-tunnel\-port " port
Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables --force-adb-forward. Set the TCP port of the adb tunnel to reach the scrcpy server. This option automatically enables \fB\-\-force\-adb\-forward\fR.
Default is 0 (not forced): the local port used for establishing the tunnel will be used. 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 .TP
.BI "\-\-v4l2-sink " /dev/videoN .BI "\-\-v4l2-sink " /dev/videoN
Output to v4l2loopback device. Output to v4l2loopback device.
@ -354,16 +463,6 @@ This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink.
Default is 0 (no buffering). 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 .TP
.BI "\-\-video\-codec " name .BI "\-\-video\-codec " name
Select a video codec (h264, h265 or av1). Select a video codec (h264, h265 or av1).
@ -376,15 +475,23 @@ Set a list of comma-separated key:type=value options for the device video encode
The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'. The possible values for 'type' are 'int' (default), 'long', 'float' and 'string'.
The list of possible codec options is available in the Android documentation The list of possible codec options is available in the Android documentation:
.UR https://d.android.com/reference/android/media/MediaFormat
.UE . <https://d.android.com/reference/android/media/MediaFormat>
.TP .TP
.BI "\-\-video\-encoder " name .BI "\-\-video\-encoder " name
Use a specific MediaCodec video encoder (depending on the codec provided by \fB\-\-video\-codec\fR). 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. The available encoders can be listed by \fB\-\-list\-encoders\fR.
.TP
.BI "\-\-video\-source " source
Select the video source (display or camera).
Camera mirroring requires Android 12+.
Default is display.
.TP .TP
.B \-w, \-\-stay-awake .B \-w, \-\-stay-awake
@ -445,6 +552,14 @@ Rotate display left
.B MOD+Right .B MOD+Right
Rotate display right Rotate display right
.TP
.B MOD+Shift+Left, MOD+Shift+Right
Flip display horizontally
.TP
.B MOD+Shift+Up, MOD+Shift+Down
Flip display vertically
.TP .TP
.B MOD+g .B MOD+g
Resize window to 1:1 (pixel\-perfect) Resize window to 1:1 (pixel\-perfect)
@ -546,7 +661,7 @@ Path to adb.
.TP .TP
.B ANDROID_SERIAL .B ANDROID_SERIAL
Device serial to use if no selector (-s, -d, -e or --tcpip=<addr>) is specified. Device serial to use if no selector (\fB-s\fR, \fB-d\fR, \fB-e\fR or \fB\-\-tcpip=\fIaddr\fR) is specified.
.TP .TP
.B SCRCPY_ICON_PATH .B SCRCPY_ICON_PATH
@ -569,23 +684,14 @@ for the Debian Project (and may be used by others).
.SH "REPORTING BUGS" .SH "REPORTING BUGS"
Report bugs to Report bugs to <https://github.com/Genymobile/scrcpy/issues>.
.UR https://github.com/Genymobile/scrcpy/issues
.UE .
.SH COPYRIGHT .SH COPYRIGHT
Copyright \(co 2018 Genymobile Copyright \(co 2018 Genymobile <https://www.genymobile.com>
.UR https://www.genymobile.com
Genymobile
.UE
Copyright \(co 2018\-2023 Copyright \(co 2018\-2023 Romain Vimont <rom@rom1v.com>
.MT rom@rom1v.com
Romain Vimont
.ME
Licensed under the Apache License, Version 2.0. Licensed under the Apache License, Version 2.0.
.SH WWW .SH WWW
.UR https://github.com/Genymobile/scrcpy <https://github.com/Genymobile/scrcpy>
.UE

View File

@ -70,7 +70,7 @@ argv_to_string(const char *const *argv, char *buf, size_t bufsize) {
} }
static void static void
show_adb_installation_msg() { show_adb_installation_msg(void) {
#ifndef __WINDOWS__ #ifndef __WINDOWS__
static const struct { static const struct {
const char *binary; const char *binary;
@ -218,8 +218,16 @@ sc_adb_forward(struct sc_intr *intr, const char *serial, uint16_t local_port,
const char *device_socket_name, unsigned flags) { const char *device_socket_name, unsigned flags) {
char local[4 + 5 + 1]; // tcp:PORT char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port);
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port);
assert(r >= 0 && (size_t) r < sizeof(local));
r = snprintf(remote, sizeof(remote), "localabstract:%s",
device_socket_name);
if (r < 0 || (size_t) r >= sizeof(remote)) {
LOGE("Could not write socket name");
return false;
}
assert(serial); assert(serial);
const char *const argv[] = const char *const argv[] =
@ -233,7 +241,9 @@ bool
sc_adb_forward_remove(struct sc_intr *intr, const char *serial, sc_adb_forward_remove(struct sc_intr *intr, const char *serial,
uint16_t local_port, unsigned flags) { uint16_t local_port, unsigned flags) {
char local[4 + 5 + 1]; // tcp:PORT char local[4 + 5 + 1]; // tcp:PORT
sprintf(local, "tcp:%" PRIu16, local_port); int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port);
assert(r >= 0 && (size_t) r < sizeof(local));
(void) r;
assert(serial); assert(serial);
const char *const argv[] = const char *const argv[] =
@ -249,8 +259,16 @@ sc_adb_reverse(struct sc_intr *intr, const char *serial,
unsigned flags) { unsigned flags) {
char local[4 + 5 + 1]; // tcp:PORT char local[4 + 5 + 1]; // tcp:PORT
char remote[108 + 14 + 1]; // localabstract:NAME char remote[108 + 14 + 1]; // localabstract:NAME
sprintf(local, "tcp:%" PRIu16, local_port); int r = snprintf(local, sizeof(local), "tcp:%" PRIu16, local_port);
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); assert(r >= 0 && (size_t) r < sizeof(local));
r = snprintf(remote, sizeof(remote), "localabstract:%s",
device_socket_name);
if (r < 0 || (size_t) r >= sizeof(remote)) {
LOGE("Could not write socket name");
return false;
}
assert(serial); assert(serial);
const char *const argv[] = const char *const argv[] =
SC_ADB_COMMAND("-s", serial, "reverse", remote, local); SC_ADB_COMMAND("-s", serial, "reverse", remote, local);
@ -263,7 +281,12 @@ bool
sc_adb_reverse_remove(struct sc_intr *intr, const char *serial, sc_adb_reverse_remove(struct sc_intr *intr, const char *serial,
const char *device_socket_name, unsigned flags) { const char *device_socket_name, unsigned flags) {
char remote[108 + 14 + 1]; // localabstract:NAME char remote[108 + 14 + 1]; // localabstract:NAME
snprintf(remote, sizeof(remote), "localabstract:%s", device_socket_name); int r = snprintf(remote, sizeof(remote), "localabstract:%s",
device_socket_name);
if (r < 0 || (size_t) r >= sizeof(remote)) {
LOGE("Device socket name too long");
return false;
}
assert(serial); assert(serial);
const char *const argv[] = const char *const argv[] =
@ -333,7 +356,9 @@ bool
sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port, sc_adb_tcpip(struct sc_intr *intr, const char *serial, uint16_t port,
unsigned flags) { unsigned flags) {
char port_string[5 + 1]; char port_string[5 + 1];
sprintf(port_string, "%" PRIu16, port); int r = snprintf(port_string, sizeof(port_string), "%" PRIu16, port);
assert(r >= 0 && (size_t) r < sizeof(port_string));
(void) r;
assert(serial); assert(serial);
const char *const argv[] = const char *const argv[] =
@ -628,8 +653,8 @@ sc_adb_select_device(struct sc_intr *intr,
return false; return false;
} }
LOGD("ADB device found:"); LOGI("ADB device found:");
sc_adb_devices_log(SC_LOG_LEVEL_DEBUG, vec.data, vec.size); sc_adb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size);
// Move devics into out_device (do not destroy device) // Move devics into out_device (do not destroy device)
sc_adb_device_move(out_device, device); sc_adb_device_move(out_device, device);

View File

@ -7,7 +7,7 @@
#include "util/log.h" #include "util/log.h"
#include "util/str.h" #include "util/str.h"
bool static bool
sc_adb_parse_device(char *line, struct sc_adb_device *device) { sc_adb_parse_device(char *line, struct sc_adb_device *device) {
// One device line looks like: // One device line looks like:
// "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel " // "0123456789abcdef device usb:2-1 product:MyProduct model:MyModel "

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -7,10 +7,17 @@
#include "options.h" #include "options.h"
enum sc_pause_on_exit {
SC_PAUSE_ON_EXIT_TRUE,
SC_PAUSE_ON_EXIT_FALSE,
SC_PAUSE_ON_EXIT_IF_ERROR,
};
struct scrcpy_cli_args { struct scrcpy_cli_args {
struct scrcpy_options opts; struct scrcpy_options opts;
bool help; bool help;
bool version; bool version;
enum sc_pause_on_exit pause_on_exit;
}; };
void void

View File

@ -3,7 +3,9 @@
#include "config.h" #include "config.h"
#include <libavcodec/version.h>
#include <libavformat/version.h> #include <libavformat/version.h>
#include <libavutil/version.h>
#include <SDL2/SDL_version.h> #include <SDL2/SDL_version.h>
#ifndef __WIN32 #ifndef __WIN32
@ -25,6 +27,12 @@
# define SCRCPY_LAVF_REQUIRES_REGISTER_ALL # define SCRCPY_LAVF_REQUIRES_REGISTER_ALL
#endif #endif
// Not documented in ffmpeg/doc/APIchanges, but AV_CODEC_ID_AV1 has been added
// by FFmpeg commit d42809f9835a4e9e5c7c63210abb09ad0ef19cfb (included in tag
// n3.3).
#if LIBAVFORMAT_VERSION_INT >= AV_VERSION_INT(57, 89, 100)
# define SCRCPY_LAVC_HAS_AV1
#endif
// In ffmpeg/doc/APIchanges: // In ffmpeg/doc/APIchanges:
// 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h // 2018-01-28 - ea3672b7d6 - lavf 58.7.100 - avformat.h
@ -44,6 +52,15 @@
# define SCRCPY_LAVU_HAS_CHLAYOUT # define SCRCPY_LAVU_HAS_CHLAYOUT
#endif #endif
// In ffmpeg/doc/APIchanges:
// 2023-10-06 - 5432d2aacad - lavc 60.15.100 - avformat.h
// Deprecate AVFormatContext.{nb_,}side_data, av_stream_add_side_data(),
// av_stream_new_side_data(), and av_stream_get_side_data(). Side data fields
// from AVFormatContext.codecpar should be used from now on.
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 15, 100)
# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA
#endif
#if SDL_VERSION_ATLEAST(2, 0, 6) #if SDL_VERSION_ATLEAST(2, 0, 6)
// <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc> // <https://github.com/libsdl-org/SDL/commit/d7a318de563125e5bb465b1000d6bc9576fbc6fc>
# define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS # define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS

View File

@ -25,7 +25,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" 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_AV1 UINT32_C(0x00617631) // "av1" in ASCII
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII #define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII" #define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII
#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII #define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
switch (codec_id) { switch (codec_id) {
case SC_CODEC_ID_H264: case SC_CODEC_ID_H264:
@ -33,11 +34,18 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
case SC_CODEC_ID_H265: case SC_CODEC_ID_H265:
return AV_CODEC_ID_HEVC; return AV_CODEC_ID_HEVC;
case SC_CODEC_ID_AV1: case SC_CODEC_ID_AV1:
#ifdef SCRCPY_LAVC_HAS_AV1
return AV_CODEC_ID_AV1; return AV_CODEC_ID_AV1;
#else
LOGE("AV1 not supported by this FFmpeg version");
return AV_CODEC_ID_NONE;
#endif
case SC_CODEC_ID_OPUS: case SC_CODEC_ID_OPUS:
return AV_CODEC_ID_OPUS; return AV_CODEC_ID_OPUS;
case SC_CODEC_ID_AAC: case SC_CODEC_ID_AAC:
return AV_CODEC_ID_AAC; return AV_CODEC_ID_AAC;
case SC_CODEC_ID_FLAC:
return AV_CODEC_ID_FLAC;
case SC_CODEC_ID_RAW: case SC_CODEC_ID_RAW:
return AV_CODEC_ID_PCM_S16LE; return AV_CODEC_ID_PCM_S16LE;
default: default:
@ -74,9 +82,8 @@ sc_demuxer_recv_video_size(struct sc_demuxer *demuxer, uint32_t *width,
static bool static bool
sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) { sc_demuxer_recv_packet(struct sc_demuxer *demuxer, AVPacket *packet) {
// The video stream contains raw packets, without time information. When we // The video and audio streams contain a sequence of raw packets (as
// record, we retrieve the timestamps separately, from a "meta" header // provided by MediaCodec), each prefixed with a "meta" header.
// added by the server before each raw packet.
// //
// The "meta" header length is 12 bytes: // The "meta" header length is 12 bytes:
// [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ... // [. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
@ -203,6 +210,11 @@ run_demuxer(void *data) {
codec_ctx->channels = 2; codec_ctx->channels = 2;
#endif #endif
codec_ctx->sample_rate = 48000; codec_ctx->sample_rate = 48000;
if (raw_codec_id == SC_CODEC_ID_FLAC) {
// The sample_fmt is not set by the FLAC decoder
codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;
}
} }
if (avcodec_open2(codec_ctx, codec, NULL) < 0) { if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
@ -215,8 +227,9 @@ run_demuxer(void *data) {
} }
// Config packets must be merged with the next non-config packet only for // Config packets must be merged with the next non-config packet only for
// video streams // H.26x
bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO; bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264
|| raw_codec_id == SC_CODEC_ID_H265;
struct sc_packet_merger merger; struct sc_packet_merger merger;

View File

@ -23,6 +23,22 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
// starts with "opengl" // starts with "opengl"
bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6); bool use_opengl = renderer_name && !strncmp(renderer_name, "opengl", 6);
if (use_opengl) { if (use_opengl) {
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
// Persuade macOS to give us something better than OpenGL 2.1.
// If we create a Core Profile context, we get the best OpenGL version.
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK,
SDL_GL_CONTEXT_PROFILE_CORE);
LOGD("Creating OpenGL Core Profile context");
display->gl_context = SDL_GL_CreateContext(window);
if (!display->gl_context) {
LOGE("Could not create OpenGL context: %s", SDL_GetError());
SDL_DestroyRenderer(display->renderer);
return false;
}
#endif
struct sc_opengl *gl = &display->gl; struct sc_opengl *gl = &display->gl;
sc_opengl_init(gl); sc_opengl_init(gl);
@ -37,7 +53,7 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
display->mipmaps = true; display->mipmaps = true;
} else { } else {
LOGW("Trilinear filtering disabled " LOGW("Trilinear filtering disabled "
"(OpenGL 3.0+ or ES 2.0+ required"); "(OpenGL 3.0+ or ES 2.0+ required)");
} }
} else { } else {
LOGI("Trilinear filtering disabled"); LOGI("Trilinear filtering disabled");
@ -46,11 +62,20 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps) {
LOGD("Trilinear filtering disabled (not an OpenGL renderer"); LOGD("Trilinear filtering disabled (not an OpenGL renderer");
} }
display->pending.flags = 0;
display->pending.frame = NULL;
return true; return true;
} }
void void
sc_display_destroy(struct sc_display *display) { sc_display_destroy(struct sc_display *display) {
if (display->pending.frame) {
av_frame_free(&display->pending.frame);
}
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GL_DeleteContext(display->gl_context);
#endif
if (display->texture) { if (display->texture) {
SDL_DestroyTexture(display->texture); SDL_DestroyTexture(display->texture);
} }
@ -65,7 +90,7 @@ sc_display_create_texture(struct sc_display *display,
SDL_TEXTUREACCESS_STREAMING, SDL_TEXTUREACCESS_STREAMING,
size.width, size.height); size.width, size.height);
if (!texture) { if (!texture) {
LOGE("Could not create texture: %s", SDL_GetError()); LOGD("Could not create texture: %s", SDL_GetError());
return NULL; return NULL;
} }
@ -85,8 +110,66 @@ sc_display_create_texture(struct sc_display *display,
return texture; return texture;
} }
bool static inline void
sc_display_set_texture_size(struct sc_display *display, struct sc_size size) { sc_display_set_pending_size(struct sc_display *display, struct sc_size size) {
assert(!display->texture);
display->pending.size = size;
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_SIZE;
}
static bool
sc_display_set_pending_frame(struct sc_display *display, const AVFrame *frame) {
if (!display->pending.frame) {
display->pending.frame = av_frame_alloc();
if (!display->pending.frame) {
LOG_OOM();
return false;
}
}
int r = av_frame_ref(display->pending.frame, frame);
if (r) {
LOGE("Could not ref frame: %d", r);
return false;
}
display->pending.flags |= SC_DISPLAY_PENDING_FLAG_FRAME;
return true;
}
static bool
sc_display_apply_pending(struct sc_display *display) {
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_SIZE) {
assert(!display->texture);
display->texture =
sc_display_create_texture(display, display->pending.size);
if (!display->texture) {
return false;
}
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_SIZE;
}
if (display->pending.flags & SC_DISPLAY_PENDING_FLAG_FRAME) {
assert(display->pending.frame);
bool ok = sc_display_update_texture(display, display->pending.frame);
if (!ok) {
return false;
}
av_frame_unref(display->pending.frame);
display->pending.flags &= ~SC_DISPLAY_PENDING_FLAG_FRAME;
}
return true;
}
static bool
sc_display_set_texture_size_internal(struct sc_display *display,
struct sc_size size) {
assert(size.width && size.height);
if (display->texture) { if (display->texture) {
SDL_DestroyTexture(display->texture); SDL_DestroyTexture(display->texture);
} }
@ -100,14 +183,27 @@ sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
return true; return true;
} }
bool enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { sc_display_set_texture_size(struct sc_display *display, struct sc_size size) {
bool ok = sc_display_set_texture_size_internal(display, size);
if (!ok) {
sc_display_set_pending_size(display, size);
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
static bool
sc_display_update_texture_internal(struct sc_display *display,
const AVFrame *frame) {
int ret = SDL_UpdateYUVTexture(display->texture, NULL, int ret = SDL_UpdateYUVTexture(display->texture, NULL,
frame->data[0], frame->linesize[0], frame->data[0], frame->linesize[0],
frame->data[1], frame->linesize[1], frame->data[1], frame->linesize[1],
frame->data[2], frame->linesize[2]); frame->data[2], frame->linesize[2]);
if (ret) { if (ret) {
LOGE("Could not update texture: %s", SDL_GetError()); LOGD("Could not update texture: %s", SDL_GetError());
return false; return false;
} }
@ -120,47 +216,70 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
return true; return true;
} }
bool enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame) {
bool ok = sc_display_update_texture_internal(display, frame);
if (!ok) {
ok = sc_display_set_pending_frame(display, frame);
if (!ok) {
LOGE("Could not set pending frame");
return SC_DISPLAY_RESULT_ERROR;
}
return SC_DISPLAY_RESULT_PENDING;
}
return SC_DISPLAY_RESULT_OK;
}
enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry, sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
unsigned rotation) { enum sc_orientation orientation) {
SDL_RenderClear(display->renderer); SDL_RenderClear(display->renderer);
if (display->pending.flags) {
bool ok = sc_display_apply_pending(display);
if (!ok) {
return SC_DISPLAY_RESULT_PENDING;
}
}
SDL_Renderer *renderer = display->renderer; SDL_Renderer *renderer = display->renderer;
SDL_Texture *texture = display->texture; SDL_Texture *texture = display->texture;
if (rotation == 0) { if (orientation == SC_ORIENTATION_0) {
int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); int ret = SDL_RenderCopy(renderer, texture, NULL, geometry);
if (ret) { if (ret) {
LOGE("Could not render texture: %s", SDL_GetError()); LOGE("Could not render texture: %s", SDL_GetError());
return false; return SC_DISPLAY_RESULT_ERROR;
} }
} else { } else {
// rotation in RenderCopyEx() is clockwise, while screen->rotation is unsigned cw_rotation = sc_orientation_get_rotation(orientation);
// counterclockwise (to be consistent with --lock-video-orientation)
int cw_rotation = (4 - rotation) % 4;
double angle = 90 * cw_rotation; double angle = 90 * cw_rotation;
const SDL_Rect *dstrect = NULL; const SDL_Rect *dstrect = NULL;
SDL_Rect rect; SDL_Rect rect;
if (rotation & 1) { if (sc_orientation_is_swap(orientation)) {
rect.x = geometry->x + (geometry->w - geometry->h) / 2; rect.x = geometry->x + (geometry->w - geometry->h) / 2;
rect.y = geometry->y + (geometry->h - geometry->w) / 2; rect.y = geometry->y + (geometry->h - geometry->w) / 2;
rect.w = geometry->h; rect.w = geometry->h;
rect.h = geometry->w; rect.h = geometry->w;
dstrect = &rect; dstrect = &rect;
} else { } else {
assert(rotation == 2);
dstrect = geometry; dstrect = geometry;
} }
SDL_RendererFlip flip = sc_orientation_is_mirror(orientation)
? SDL_FLIP_HORIZONTAL : 0;
int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle,
NULL, 0); NULL, flip);
if (ret) { if (ret) {
LOGE("Could not render texture: %s", SDL_GetError()); LOGE("Could not render texture: %s", SDL_GetError());
return false; return SC_DISPLAY_RESULT_ERROR;
} }
} }
SDL_RenderPresent(display->renderer); SDL_RenderPresent(display->renderer);
return true; return SC_DISPLAY_RESULT_OK;
} }

View File

@ -9,13 +9,36 @@
#include "coords.h" #include "coords.h"
#include "opengl.h" #include "opengl.h"
#include "options.h"
#ifdef __APPLE__
# define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
#endif
struct sc_display { struct sc_display {
SDL_Renderer *renderer; SDL_Renderer *renderer;
SDL_Texture *texture; SDL_Texture *texture;
struct sc_opengl gl; struct sc_opengl gl;
#ifdef SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE
SDL_GLContext *gl_context;
#endif
bool mipmaps; bool mipmaps;
struct {
#define SC_DISPLAY_PENDING_FLAG_SIZE 1
#define SC_DISPLAY_PENDING_FLAG_FRAME 2
int8_t flags;
struct sc_size size;
AVFrame *frame;
} pending;
};
enum sc_display_result {
SC_DISPLAY_RESULT_OK,
SC_DISPLAY_RESULT_PENDING,
SC_DISPLAY_RESULT_ERROR,
}; };
bool bool
@ -24,14 +47,14 @@ sc_display_init(struct sc_display *display, SDL_Window *window, bool mipmaps);
void void
sc_display_destroy(struct sc_display *display); sc_display_destroy(struct sc_display *display);
bool enum sc_display_result
sc_display_set_texture_size(struct sc_display *display, struct sc_size size); sc_display_set_texture_size(struct sc_display *display, struct sc_size size);
bool enum sc_display_result
sc_display_update_texture(struct sc_display *display, const AVFrame *frame); sc_display_update_texture(struct sc_display *display, const AVFrame *frame);
bool enum sc_display_result
sc_display_render(struct sc_display *display, const SDL_Rect *geometry, sc_display_render(struct sc_display *display, const SDL_Rect *geometry,
unsigned rotation); enum sc_orientation orientation);
#endif #endif

View File

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

View File

@ -271,7 +271,7 @@ error:
} }
SDL_Surface * SDL_Surface *
scrcpy_icon_load() { scrcpy_icon_load(void) {
char *icon_path = get_icon_path(); char *icon_path = get_icon_path();
if (!icon_path) { if (!icon_path) {
return NULL; return NULL;

View File

@ -293,15 +293,11 @@ rotate_device(struct sc_controller *controller) {
} }
static void static void
rotate_client_left(struct sc_screen *screen) { apply_orientation_transform(struct sc_screen *screen,
unsigned new_rotation = (screen->rotation + 1) % 4; enum sc_orientation transform) {
sc_screen_set_rotation(screen, new_rotation); enum sc_orientation new_orientation =
} sc_orientation_apply(screen->orientation, transform);
sc_screen_set_orientation(screen, new_orientation);
static void
rotate_client_right(struct sc_screen *screen) {
unsigned new_rotation = (screen->rotation + 3) % 4;
sc_screen_set_rotation(screen, new_rotation);
} }
static void static void
@ -421,25 +417,47 @@ sc_input_manager_process_key(struct sc_input_manager *im,
} }
return; return;
case SDLK_DOWN: case SDLK_DOWN:
if (controller && !shift) { if (shift) {
if (!repeat & down) {
apply_orientation_transform(im->screen,
SC_ORIENTATION_FLIP_180);
}
} else if (controller) {
// forward repeated events // forward repeated events
action_volume_down(controller, action); action_volume_down(controller, action);
} }
return; return;
case SDLK_UP: case SDLK_UP:
if (controller && !shift) { if (shift) {
if (!repeat & down) {
apply_orientation_transform(im->screen,
SC_ORIENTATION_FLIP_180);
}
} else if (controller) {
// forward repeated events // forward repeated events
action_volume_up(controller, action); action_volume_up(controller, action);
} }
return; return;
case SDLK_LEFT: case SDLK_LEFT:
if (!shift && !repeat && down) { if (!repeat && down) {
rotate_client_left(im->screen); if (shift) {
apply_orientation_transform(im->screen,
SC_ORIENTATION_FLIP_0);
} else {
apply_orientation_transform(im->screen,
SC_ORIENTATION_270);
}
} }
return; return;
case SDLK_RIGHT: case SDLK_RIGHT:
if (!shift && !repeat && down) { if (!repeat && down) {
rotate_client_right(im->screen); if (shift) {
apply_orientation_transform(im->screen,
SC_ORIENTATION_FLIP_0);
} else {
apply_orientation_transform(im->screen,
SC_ORIENTATION_90);
}
} }
return; return;
case SDLK_c: case SDLK_c:

View File

@ -23,7 +23,7 @@
#include "util/str.h" #include "util/str.h"
#endif #endif
int static int
main_scrcpy(int argc, char *argv[]) { main_scrcpy(int argc, char *argv[]) {
#ifdef _WIN32 #ifdef _WIN32
// disable buffering, we want logs immediately // disable buffering, we want logs immediately
@ -39,26 +39,32 @@ main_scrcpy(int argc, char *argv[]) {
.opts = scrcpy_options_default, .opts = scrcpy_options_default,
.help = false, .help = false,
.version = false, .version = false,
.pause_on_exit = SC_PAUSE_ON_EXIT_FALSE,
}; };
#ifndef NDEBUG #ifndef NDEBUG
args.opts.log_level = SC_LOG_LEVEL_DEBUG; args.opts.log_level = SC_LOG_LEVEL_DEBUG;
#endif #endif
enum scrcpy_exit_code ret;
if (!scrcpy_parse_args(&args, argc, argv)) { if (!scrcpy_parse_args(&args, argc, argv)) {
return SCRCPY_EXIT_FAILURE; ret = SCRCPY_EXIT_FAILURE;
goto end;
} }
sc_set_log_level(args.opts.log_level); sc_set_log_level(args.opts.log_level);
if (args.help) { if (args.help) {
scrcpy_print_usage(argv[0]); scrcpy_print_usage(argv[0]);
return SCRCPY_EXIT_SUCCESS; ret = SCRCPY_EXIT_SUCCESS;
goto end;
} }
if (args.version) { if (args.version) {
scrcpy_print_version(); scrcpy_print_version();
return SCRCPY_EXIT_SUCCESS; ret = SCRCPY_EXIT_SUCCESS;
goto end;
} }
#ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL #ifdef SCRCPY_LAVF_REQUIRES_REGISTER_ALL
@ -72,18 +78,26 @@ main_scrcpy(int argc, char *argv[]) {
#endif #endif
if (!net_init()) { if (!net_init()) {
return SCRCPY_EXIT_FAILURE; ret = SCRCPY_EXIT_FAILURE;
goto end;
} }
sc_log_configure(); sc_log_configure();
#ifdef HAVE_USB #ifdef HAVE_USB
enum scrcpy_exit_code ret = args.opts.otg ? scrcpy_otg(&args.opts) ret = args.opts.otg ? scrcpy_otg(&args.opts) : scrcpy(&args.opts);
: scrcpy(&args.opts);
#else #else
enum scrcpy_exit_code ret = scrcpy(&args.opts); ret = scrcpy(&args.opts);
#endif #endif
end:
if (args.pause_on_exit == SC_PAUSE_ON_EXIT_TRUE ||
(args.pause_on_exit == SC_PAUSE_ON_EXIT_IF_ERROR &&
ret != SCRCPY_EXIT_SUCCESS)) {
printf("Press Enter to continue...\n");
getchar();
}
return ret; return ret;
} }

View File

@ -11,15 +11,19 @@ const struct scrcpy_options scrcpy_options_default = {
.audio_codec_options = NULL, .audio_codec_options = NULL,
.video_encoder = NULL, .video_encoder = NULL,
.audio_encoder = NULL, .audio_encoder = NULL,
#ifdef HAVE_V4L2 .camera_id = NULL,
.v4l2_device = NULL, .camera_size = NULL,
#endif .camera_ar = NULL,
.camera_fps = 0,
.log_level = SC_LOG_LEVEL_INFO, .log_level = SC_LOG_LEVEL_INFO,
.video_codec = SC_CODEC_H264, .video_codec = SC_CODEC_H264,
.audio_codec = SC_CODEC_OPUS, .audio_codec = SC_CODEC_OPUS,
.video_source = SC_VIDEO_SOURCE_DISPLAY,
.audio_source = SC_AUDIO_SOURCE_AUTO,
.record_format = SC_RECORD_FORMAT_AUTO, .record_format = SC_RECORD_FORMAT_AUTO,
.keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT, .keyboard_input_mode = SC_KEYBOARD_INPUT_MODE_INJECT,
.mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT, .mouse_input_mode = SC_MOUSE_INPUT_MODE_INJECT,
.camera_facing = SC_CAMERA_FACING_ANY,
.port_range = { .port_range = {
.first = DEFAULT_LOCAL_PORT_RANGE_FIRST, .first = DEFAULT_LOCAL_PORT_RANGE_FIRST,
.last = DEFAULT_LOCAL_PORT_RANGE_LAST, .last = DEFAULT_LOCAL_PORT_RANGE_LAST,
@ -35,16 +39,21 @@ const struct scrcpy_options scrcpy_options_default = {
.audio_bit_rate = 0, .audio_bit_rate = 0,
.max_fps = 0, .max_fps = 0,
.lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED,
.rotation = 0, .display_orientation = SC_ORIENTATION_0,
.record_orientation = SC_ORIENTATION_0,
.window_x = SC_WINDOW_POSITION_UNDEFINED, .window_x = SC_WINDOW_POSITION_UNDEFINED,
.window_y = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED,
.window_width = 0, .window_width = 0,
.window_height = 0, .window_height = 0,
.display_id = 0, .display_id = 0,
.display_buffer = 0, .display_buffer = 0,
.v4l2_buffer = 0, .audio_buffer = -1, // depends on the audio format,
.audio_buffer = SC_TICK_FROM_MS(50),
.audio_output_buffer = SC_TICK_FROM_MS(5), .audio_output_buffer = SC_TICK_FROM_MS(5),
.time_limit = 0,
#ifdef HAVE_V4L2
.v4l2_device = NULL,
.v4l2_buffer = 0,
#endif
#ifdef HAVE_USB #ifdef HAVE_USB
.otg = false, .otg = false,
#endif #endif
@ -52,7 +61,8 @@ const struct scrcpy_options scrcpy_options_default = {
.fullscreen = false, .fullscreen = false,
.always_on_top = false, .always_on_top = false,
.control = true, .control = true,
.display = true, .video_playback = true,
.audio_playback = true,
.turn_screen_off = false, .turn_screen_off = false,
.key_inject_mode = SC_KEY_INJECT_MODE_MIXED, .key_inject_mode = SC_KEY_INJECT_MODE_MIXED,
.window_borderless = false, .window_borderless = false,
@ -73,8 +83,46 @@ const struct scrcpy_options scrcpy_options_default = {
.cleanup = true, .cleanup = true,
.start_fps_counter = false, .start_fps_counter = false,
.power_on = true, .power_on = true,
.video = true,
.audio = true, .audio = true,
.require_audio = false, .require_audio = false,
.list_encoders = false, .kill_adb_on_close = false,
.list_displays = false, .camera_high_speed = false,
.list = 0,
}; };
enum sc_orientation
sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) {
assert(!(src & ~7));
assert(!(transform & ~7));
unsigned transform_hflip = transform & 4;
unsigned transform_rotation = transform & 3;
unsigned src_hflip = src & 4;
unsigned src_rotation = src & 3;
unsigned src_swap = src & 1;
if (src_swap && transform_hflip) {
// If the src is rotated by 90 or 270 degrees, applying a flipped
// transformation requires an additional 180 degrees rotation to
// compensate for the inversion of the order of multiplication:
//
// hflip1 × rotate1 × hflip2 × rotate2
// `--------------' `--------------'
// src transform
//
// In the final result, we want all the hflips then all the rotations,
// so we must move hflip2 to the left:
//
// hflip1 × hflip2 × rotate1' × rotate2
//
// with rotate1' = | rotate1 if src is 0° or 180°
// | rotate1 + 180° if src is 90° or 270°
src_rotation += 2;
}
unsigned result_hflip = src_hflip ^ transform_hflip;
unsigned result_rotation = (transform_rotation + src_rotation) % 4;
enum sc_orientation result = result_hflip | result_rotation;
return result;
}

View File

@ -3,6 +3,7 @@
#include "common.h" #include "common.h"
#include <assert.h>
#include <stdbool.h> #include <stdbool.h>
#include <stddef.h> #include <stddef.h>
#include <stdint.h> #include <stdint.h>
@ -21,25 +22,121 @@ enum sc_record_format {
SC_RECORD_FORMAT_AUTO, SC_RECORD_FORMAT_AUTO,
SC_RECORD_FORMAT_MP4, SC_RECORD_FORMAT_MP4,
SC_RECORD_FORMAT_MKV, SC_RECORD_FORMAT_MKV,
SC_RECORD_FORMAT_M4A,
SC_RECORD_FORMAT_MKA,
SC_RECORD_FORMAT_OPUS,
SC_RECORD_FORMAT_AAC,
SC_RECORD_FORMAT_FLAC,
SC_RECORD_FORMAT_WAV,
}; };
static inline bool
sc_record_format_is_audio_only(enum sc_record_format fmt) {
return fmt == SC_RECORD_FORMAT_M4A
|| fmt == SC_RECORD_FORMAT_MKA
|| fmt == SC_RECORD_FORMAT_OPUS
|| fmt == SC_RECORD_FORMAT_AAC
|| fmt == SC_RECORD_FORMAT_FLAC
|| fmt == SC_RECORD_FORMAT_WAV;
}
enum sc_codec { enum sc_codec {
SC_CODEC_H264, SC_CODEC_H264,
SC_CODEC_H265, SC_CODEC_H265,
SC_CODEC_AV1, SC_CODEC_AV1,
SC_CODEC_OPUS, SC_CODEC_OPUS,
SC_CODEC_AAC, SC_CODEC_AAC,
SC_CODEC_FLAC,
SC_CODEC_RAW, SC_CODEC_RAW,
}; };
enum sc_video_source {
SC_VIDEO_SOURCE_DISPLAY,
SC_VIDEO_SOURCE_CAMERA,
};
enum sc_audio_source {
SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA
SC_AUDIO_SOURCE_OUTPUT,
SC_AUDIO_SOURCE_MIC,
};
enum sc_camera_facing {
SC_CAMERA_FACING_ANY,
SC_CAMERA_FACING_FRONT,
SC_CAMERA_FACING_BACK,
SC_CAMERA_FACING_EXTERNAL,
};
// ,----- hflip (applied before the rotation)
// | ,--- 180°
// | | ,- 90° clockwise
// | | |
enum sc_orientation { // v v v
SC_ORIENTATION_0, // 0 0 0
SC_ORIENTATION_90, // 0 0 1
SC_ORIENTATION_180, // 0 1 0
SC_ORIENTATION_270, // 0 1 1
SC_ORIENTATION_FLIP_0, // 1 0 0
SC_ORIENTATION_FLIP_90, // 1 0 1
SC_ORIENTATION_FLIP_180, // 1 1 0
SC_ORIENTATION_FLIP_270, // 1 1 1
};
static inline bool
sc_orientation_is_mirror(enum sc_orientation orientation) {
assert(!(orientation & ~7));
return orientation & 4;
}
// Does the orientation swap width and height?
static inline bool
sc_orientation_is_swap(enum sc_orientation orientation) {
assert(!(orientation & ~7));
return orientation & 1;
}
static inline enum sc_orientation
sc_orientation_get_rotation(enum sc_orientation orientation) {
assert(!(orientation & ~7));
return orientation & 3;
}
enum sc_orientation
sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform);
static inline const char *
sc_orientation_get_name(enum sc_orientation orientation) {
switch (orientation) {
case SC_ORIENTATION_0:
return "0";
case SC_ORIENTATION_90:
return "90";
case SC_ORIENTATION_180:
return "180";
case SC_ORIENTATION_270:
return "270";
case SC_ORIENTATION_FLIP_0:
return "flip0";
case SC_ORIENTATION_FLIP_90:
return "flip90";
case SC_ORIENTATION_FLIP_180:
return "flip180";
case SC_ORIENTATION_FLIP_270:
return "flip270";
default:
return "(unknown)";
}
}
enum sc_lock_video_orientation { enum sc_lock_video_orientation {
SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1,
// lock the current orientation when scrcpy starts // lock the current orientation when scrcpy starts
SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2,
SC_LOCK_VIDEO_ORIENTATION_0 = 0, SC_LOCK_VIDEO_ORIENTATION_0 = 0,
SC_LOCK_VIDEO_ORIENTATION_1, SC_LOCK_VIDEO_ORIENTATION_90 = 3,
SC_LOCK_VIDEO_ORIENTATION_2, SC_LOCK_VIDEO_ORIENTATION_180 = 2,
SC_LOCK_VIDEO_ORIENTATION_3, SC_LOCK_VIDEO_ORIENTATION_270 = 1,
}; };
enum sc_keyboard_input_mode { enum sc_keyboard_input_mode {
@ -100,15 +197,19 @@ struct scrcpy_options {
const char *audio_codec_options; const char *audio_codec_options;
const char *video_encoder; const char *video_encoder;
const char *audio_encoder; const char *audio_encoder;
#ifdef HAVE_V4L2 const char *camera_id;
const char *v4l2_device; const char *camera_size;
#endif const char *camera_ar;
uint16_t camera_fps;
enum sc_log_level log_level; enum sc_log_level log_level;
enum sc_codec video_codec; enum sc_codec video_codec;
enum sc_codec audio_codec; enum sc_codec audio_codec;
enum sc_video_source video_source;
enum sc_audio_source audio_source;
enum sc_record_format record_format; enum sc_record_format record_format;
enum sc_keyboard_input_mode keyboard_input_mode; enum sc_keyboard_input_mode keyboard_input_mode;
enum sc_mouse_input_mode mouse_input_mode; enum sc_mouse_input_mode mouse_input_mode;
enum sc_camera_facing camera_facing;
struct sc_port_range port_range; struct sc_port_range port_range;
uint32_t tunnel_host; uint32_t tunnel_host;
uint16_t tunnel_port; uint16_t tunnel_port;
@ -118,16 +219,21 @@ struct scrcpy_options {
uint32_t audio_bit_rate; uint32_t audio_bit_rate;
uint16_t max_fps; uint16_t max_fps;
enum sc_lock_video_orientation lock_video_orientation; enum sc_lock_video_orientation lock_video_orientation;
uint8_t rotation; enum sc_orientation display_orientation;
enum sc_orientation record_orientation;
int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto"
int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto"
uint16_t window_width; uint16_t window_width;
uint16_t window_height; uint16_t window_height;
uint32_t display_id; uint32_t display_id;
sc_tick display_buffer; sc_tick display_buffer;
sc_tick v4l2_buffer;
sc_tick audio_buffer; sc_tick audio_buffer;
sc_tick audio_output_buffer; sc_tick audio_output_buffer;
sc_tick time_limit;
#ifdef HAVE_V4L2
const char *v4l2_device;
sc_tick v4l2_buffer;
#endif
#ifdef HAVE_USB #ifdef HAVE_USB
bool otg; bool otg;
#endif #endif
@ -135,7 +241,8 @@ struct scrcpy_options {
bool fullscreen; bool fullscreen;
bool always_on_top; bool always_on_top;
bool control; bool control;
bool display; bool video_playback;
bool audio_playback;
bool turn_screen_off; bool turn_screen_off;
enum sc_key_inject_mode key_inject_mode; enum sc_key_inject_mode key_inject_mode;
bool window_borderless; bool window_borderless;
@ -156,10 +263,16 @@ struct scrcpy_options {
bool cleanup; bool cleanup;
bool start_fps_counter; bool start_fps_counter;
bool power_on; bool power_on;
bool video;
bool audio; bool audio;
bool require_audio; bool require_audio;
bool list_encoders; bool kill_adb_on_close;
bool list_displays; bool camera_high_speed;
#define SC_OPTION_LIST_ENCODERS 0x1
#define SC_OPTION_LIST_DISPLAYS 0x2
#define SC_OPTION_LIST_CAMERAS 0x4
#define SC_OPTION_LIST_CAMERA_SIZES 0x8
uint8_t list;
}; };
extern const struct scrcpy_options scrcpy_options_default; extern const struct scrcpy_options scrcpy_options_default;

View File

@ -4,6 +4,7 @@
#include <libavcodec/avcodec.h> #include <libavcodec/avcodec.h>
#include <libavformat/avformat.h> #include <libavformat/avformat.h>
#include <libavutil/time.h> #include <libavutil/time.h>
#include <libavutil/display.h>
#include "util/log.h" #include "util/log.h"
#include "util/str.h" #include "util/str.h"
@ -60,9 +61,21 @@ sc_recorder_queue_clear(struct sc_recorder_queue *queue) {
static const char * static const char *
sc_recorder_get_format_name(enum sc_record_format format) { sc_recorder_get_format_name(enum sc_record_format format) {
switch (format) { switch (format) {
case SC_RECORD_FORMAT_MP4: return "mp4"; case SC_RECORD_FORMAT_MP4:
case SC_RECORD_FORMAT_MKV: return "matroska"; case SC_RECORD_FORMAT_M4A:
default: return NULL; case SC_RECORD_FORMAT_AAC:
return "mp4";
case SC_RECORD_FORMAT_MKV:
case SC_RECORD_FORMAT_MKA:
return "matroska";
case SC_RECORD_FORMAT_OPUS:
return "opus";
case SC_RECORD_FORMAT_FLAC:
return "flac";
case SC_RECORD_FORMAT_WAV:
return "wav";
default:
return NULL;
} }
} }
@ -88,23 +101,30 @@ sc_recorder_rescale_packet(AVStream *stream, AVPacket *packet) {
} }
static bool static bool
sc_recorder_write_stream(struct sc_recorder *recorder, int stream_index, sc_recorder_write_stream(struct sc_recorder *recorder,
AVPacket *packet) { struct sc_recorder_stream *st, AVPacket *packet) {
AVStream *stream = recorder->ctx->streams[stream_index]; AVStream *stream = recorder->ctx->streams[st->index];
sc_recorder_rescale_packet(stream, packet); sc_recorder_rescale_packet(stream, packet);
if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) {
LOGD("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; return av_interleaved_write_frame(recorder->ctx, packet) >= 0;
} }
static inline bool static inline bool
sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) { sc_recorder_write_video(struct sc_recorder *recorder, AVPacket *packet) {
return sc_recorder_write_stream(recorder, recorder->video_stream_index, return sc_recorder_write_stream(recorder, &recorder->video_stream, packet);
packet);
} }
static inline bool static inline bool
sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) { sc_recorder_write_audio(struct sc_recorder *recorder, AVPacket *packet) {
return sc_recorder_write_stream(recorder, recorder->audio_stream_index, return sc_recorder_write_stream(recorder, &recorder->audio_stream, packet);
packet);
} }
static bool static bool
@ -151,13 +171,14 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) {
} }
static inline bool static inline bool
sc_recorder_has_empty_queues(struct sc_recorder *recorder) { sc_recorder_must_wait_for_config_packets(struct sc_recorder *recorder) {
if (sc_vecdeque_is_empty(&recorder->video_queue)) { if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
// The video queue is empty // The video queue is empty
return true; return true;
} }
if (recorder->audio && sc_vecdeque_is_empty(&recorder->audio_queue)) { if (recorder->audio && recorder->audio_expects_config_packet
&& sc_vecdeque_is_empty(&recorder->audio_queue)) {
// The audio queue is empty (when audio is enabled) // The audio queue is empty (when audio is enabled)
return true; return true;
} }
@ -170,13 +191,14 @@ static bool
sc_recorder_process_header(struct sc_recorder *recorder) { sc_recorder_process_header(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex); sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped && (!recorder->video_init while (!recorder->stopped &&
|| !recorder->audio_init ((recorder->video && !recorder->video_init)
|| sc_recorder_has_empty_queues(recorder))) { || (recorder->audio && !recorder->audio_init)
sc_cond_wait(&recorder->stream_cond, &recorder->mutex); || sc_recorder_must_wait_for_config_packets(recorder))) {
sc_cond_wait(&recorder->cond, &recorder->mutex);
} }
if (sc_vecdeque_is_empty(&recorder->video_queue)) { if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
assert(recorder->stopped); assert(recorder->stopped);
// If the recorder is stopped, don't process anything if there are not // If the recorder is stopped, don't process anything if there are not
// at least video packets // at least video packets
@ -184,10 +206,15 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
return false; return false;
} }
AVPacket *video_pkt = sc_vecdeque_pop(&recorder->video_queue); AVPacket *video_pkt = NULL;
if (!sc_vecdeque_is_empty(&recorder->video_queue)) {
assert(recorder->video);
video_pkt = sc_vecdeque_pop(&recorder->video_queue);
}
AVPacket *audio_pkt = NULL; AVPacket *audio_pkt = NULL;
if (!sc_vecdeque_is_empty(&recorder->audio_queue)) { if (recorder->audio_expects_config_packet &&
!sc_vecdeque_is_empty(&recorder->audio_queue)) {
assert(recorder->audio); assert(recorder->audio);
audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); audio_pkt = sc_vecdeque_pop(&recorder->audio_queue);
} }
@ -196,17 +223,19 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
int ret = false; int ret = false;
if (video_pkt->pts != AV_NOPTS_VALUE) { if (video_pkt) {
LOGE("The first video packet is not a config packet"); if (video_pkt->pts != AV_NOPTS_VALUE) {
goto end; LOGE("The first video packet is not a config packet");
} goto end;
}
assert(recorder->video_stream_index >= 0); assert(recorder->video_stream.index >= 0);
AVStream *video_stream = 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); bool ok = sc_recorder_set_extradata(video_stream, video_pkt);
if (!ok) { if (!ok) {
goto end; goto end;
}
} }
if (audio_pkt) { if (audio_pkt) {
@ -215,16 +244,16 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
goto end; goto end;
} }
assert(recorder->audio_stream_index >= 0); assert(recorder->audio_stream.index >= 0);
AVStream *audio_stream = AVStream *audio_stream =
recorder->ctx->streams[recorder->audio_stream_index]; recorder->ctx->streams[recorder->audio_stream.index];
ok = sc_recorder_set_extradata(audio_stream, audio_pkt); bool ok = sc_recorder_set_extradata(audio_stream, audio_pkt);
if (!ok) { if (!ok) {
goto end; goto end;
} }
} }
ok = avformat_write_header(recorder->ctx, NULL) >= 0; bool ok = avformat_write_header(recorder->ctx, NULL) >= 0;
if (!ok) { if (!ok) {
LOGE("Failed to write header to %s", recorder->filename); LOGE("Failed to write header to %s", recorder->filename);
goto end; goto end;
@ -233,7 +262,9 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
ret = true; ret = true;
end: end:
av_packet_free(&video_pkt); if (video_pkt) {
av_packet_free(&video_pkt);
}
if (audio_pkt) { if (audio_pkt) {
av_packet_free(&audio_pkt); av_packet_free(&audio_pkt);
} }
@ -263,7 +294,8 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex); sc_mutex_lock(&recorder->mutex);
while (!recorder->stopped) { while (!recorder->stopped) {
if (!video_pkt && !sc_vecdeque_is_empty(&recorder->video_queue)) { if (recorder->video && !video_pkt &&
!sc_vecdeque_is_empty(&recorder->video_queue)) {
// A new packet may be assigned to video_pkt and be processed // A new packet may be assigned to video_pkt and be processed
break; break;
} }
@ -272,12 +304,17 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
// A new packet may be assigned to audio_pkt and be processed // A new packet may be assigned to audio_pkt and be processed
break; 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 // If stopped is set, continue to process the remaining events (to
// finish the recording) before actually stopping. // finish the recording) before actually stopping.
// If there is no video, then the video_queue will remain empty forever
// and video_pkt will always be NULL.
assert(recorder->video || (!video_pkt
&& sc_vecdeque_is_empty(&recorder->video_queue)));
// If there is no audio, then the audio_queue will remain empty forever // If there is no audio, then the audio_queue will remain empty forever
// and audio_pkt will always be NULL. // and audio_pkt will always be NULL.
assert(recorder->audio || (!audio_pkt assert(recorder->audio || (!audio_pkt
@ -319,6 +356,9 @@ sc_recorder_process_packets(struct sc_recorder *recorder) {
if (!recorder->audio) { if (!recorder->audio) {
assert(video_pkt); assert(video_pkt);
pts_origin = video_pkt->pts; pts_origin = video_pkt->pts;
} else if (!recorder->video) {
assert(audio_pkt);
pts_origin = audio_pkt->pts;
} else if (video_pkt && audio_pkt) { } else if (video_pkt && audio_pkt) {
pts_origin = MIN(video_pkt->pts, audio_pkt->pts); pts_origin = MIN(video_pkt->pts, audio_pkt->pts);
} else if (recorder->stopped) { } else if (recorder->stopped) {
@ -454,6 +494,42 @@ run_recorder(void *data) {
return 0; return 0;
} }
static bool
sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) {
assert(!sc_orientation_is_mirror(orientation));
uint8_t *raw_data;
#ifdef SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA
AVPacketSideData *sd =
av_packet_side_data_new(&stream->codecpar->coded_side_data,
&stream->codecpar->nb_coded_side_data,
AV_PKT_DATA_DISPLAYMATRIX,
sizeof(int32_t) * 9, 0);
if (!sd) {
LOG_OOM();
return false;
}
raw_data = sd->data;
#else
raw_data = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX,
sizeof(int32_t) * 9);
if (!raw_data) {
LOG_OOM();
return false;
}
#endif
int32_t *matrix = (int32_t *) raw_data;
unsigned rotation = orientation;
unsigned angle = rotation * 90;
av_display_rotation_set(matrix, angle);
return true;
}
static bool static bool
sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
AVCodecContext *ctx) { AVCodecContext *ctx) {
@ -479,10 +555,20 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink,
return false; return false;
} }
recorder->video_stream_index = stream->index; recorder->video_stream.index = stream->index;
if (recorder->orientation != SC_ORIENTATION_0) {
if (!sc_recorder_set_orientation(stream, recorder->orientation)) {
sc_mutex_unlock(&recorder->mutex);
return false;
}
LOGI("Record orientation set to %s",
sc_orientation_get_name(recorder->orientation));
}
recorder->video_init = true; recorder->video_init = true;
sc_cond_signal(&recorder->stream_cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
return true; return true;
@ -497,7 +583,7 @@ sc_recorder_video_packet_sink_close(struct sc_packet_sink *sink) {
sc_mutex_lock(&recorder->mutex); sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder // EOS also stops the recorder
recorder->stopped = true; recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
} }
@ -523,7 +609,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
return false; 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); bool ok = sc_vecdeque_push(&recorder->video_queue, rec);
if (!ok) { if (!ok) {
@ -532,7 +618,7 @@ sc_recorder_video_packet_sink_push(struct sc_packet_sink *sink,
return false; return false;
} }
sc_cond_signal(&recorder->queue_cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
return true; return true;
@ -560,10 +646,14 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
return false; return false;
} }
recorder->audio_stream_index = stream->index; recorder->audio_stream.index = stream->index;
// A config packet is provided for all supported formats except raw audio
recorder->audio_expects_config_packet =
ctx->codec_id != AV_CODEC_ID_PCM_S16LE;
recorder->audio_init = true; recorder->audio_init = true;
sc_cond_signal(&recorder->stream_cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
return true; return true;
@ -579,7 +669,7 @@ sc_recorder_audio_packet_sink_close(struct sc_packet_sink *sink) {
sc_mutex_lock(&recorder->mutex); sc_mutex_lock(&recorder->mutex);
// EOS also stops the recorder // EOS also stops the recorder
recorder->stopped = true; recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
} }
@ -606,7 +696,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
return false; 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); bool ok = sc_vecdeque_push(&recorder->audio_queue, rec);
if (!ok) { if (!ok) {
@ -615,7 +705,7 @@ sc_recorder_audio_packet_sink_push(struct sc_packet_sink *sink,
return false; return false;
} }
sc_cond_signal(&recorder->queue_cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
return true; return true;
@ -633,14 +723,23 @@ sc_recorder_audio_packet_sink_disable(struct sc_packet_sink *sink) {
sc_mutex_lock(&recorder->mutex); sc_mutex_lock(&recorder->mutex);
recorder->audio = false; recorder->audio = false;
recorder->audio_init = true; recorder->audio_init = true;
sc_cond_signal(&recorder->stream_cond); sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex); 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 bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename, sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool audio, enum sc_record_format format, bool video, bool audio,
enum sc_orientation orientation,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { const struct sc_recorder_callbacks *cbs, void *cbs_userdata) {
assert(!sc_orientation_is_mirror(orientation));
recorder->filename = strdup(filename); recorder->filename = strdup(filename);
if (!recorder->filename) { if (!recorder->filename) {
LOG_OOM(); LOG_OOM();
@ -652,18 +751,17 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
goto error_free_filename; goto error_free_filename;
} }
ok = sc_cond_init(&recorder->queue_cond); ok = sc_cond_init(&recorder->cond);
if (!ok) { if (!ok) {
goto error_mutex_destroy; goto error_mutex_destroy;
} }
ok = sc_cond_init(&recorder->stream_cond); assert(video || audio);
if (!ok) { recorder->video = video;
goto error_queue_cond_destroy;
}
recorder->audio = audio; recorder->audio = audio;
recorder->orientation = orientation;
sc_vecdeque_init(&recorder->video_queue); sc_vecdeque_init(&recorder->video_queue);
sc_vecdeque_init(&recorder->audio_queue); sc_vecdeque_init(&recorder->audio_queue);
recorder->stopped = false; recorder->stopped = false;
@ -671,8 +769,10 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
recorder->video_init = false; recorder->video_init = false;
recorder->audio_init = false; recorder->audio_init = false;
recorder->video_stream_index = -1; recorder->audio_expects_config_packet = false;
recorder->audio_stream_index = -1;
sc_recorder_stream_init(&recorder->video_stream);
sc_recorder_stream_init(&recorder->audio_stream);
recorder->format = format; recorder->format = format;
@ -680,13 +780,15 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
recorder->cbs = cbs; recorder->cbs = cbs;
recorder->cbs_userdata = cbs_userdata; recorder->cbs_userdata = cbs_userdata;
static const struct sc_packet_sink_ops video_ops = { if (video) {
.open = sc_recorder_video_packet_sink_open, static const struct sc_packet_sink_ops video_ops = {
.close = sc_recorder_video_packet_sink_close, .open = sc_recorder_video_packet_sink_open,
.push = sc_recorder_video_packet_sink_push, .close = sc_recorder_video_packet_sink_close,
}; .push = sc_recorder_video_packet_sink_push,
};
recorder->video_packet_sink.ops = &video_ops; recorder->video_packet_sink.ops = &video_ops;
}
if (audio) { if (audio) {
static const struct sc_packet_sink_ops audio_ops = { static const struct sc_packet_sink_ops audio_ops = {
@ -701,8 +803,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
return true; return true;
error_queue_cond_destroy:
sc_cond_destroy(&recorder->queue_cond);
error_mutex_destroy: error_mutex_destroy:
sc_mutex_destroy(&recorder->mutex); sc_mutex_destroy(&recorder->mutex);
error_free_filename: error_free_filename:
@ -727,8 +827,7 @@ void
sc_recorder_stop(struct sc_recorder *recorder) { sc_recorder_stop(struct sc_recorder *recorder) {
sc_mutex_lock(&recorder->mutex); sc_mutex_lock(&recorder->mutex);
recorder->stopped = true; recorder->stopped = true;
sc_cond_signal(&recorder->queue_cond); sc_cond_signal(&recorder->cond);
sc_cond_signal(&recorder->stream_cond);
sc_mutex_unlock(&recorder->mutex); sc_mutex_unlock(&recorder->mutex);
} }
@ -739,8 +838,7 @@ sc_recorder_join(struct sc_recorder *recorder) {
void void
sc_recorder_destroy(struct sc_recorder *recorder) { sc_recorder_destroy(struct sc_recorder *recorder) {
sc_cond_destroy(&recorder->stream_cond); sc_cond_destroy(&recorder->cond);
sc_cond_destroy(&recorder->queue_cond);
sc_mutex_destroy(&recorder->mutex); sc_mutex_destroy(&recorder->mutex);
free(recorder->filename); free(recorder->filename);
} }

View File

@ -14,6 +14,11 @@
struct sc_recorder_queue SC_VECDEQUE(AVPacket *); struct sc_recorder_queue SC_VECDEQUE(AVPacket *);
struct sc_recorder_stream {
int index;
int64_t last_pts;
};
struct sc_recorder { struct sc_recorder {
struct sc_packet_sink video_packet_sink; struct sc_packet_sink video_packet_sink;
struct sc_packet_sink audio_packet_sink; struct sc_packet_sink audio_packet_sink;
@ -27,6 +32,9 @@ struct sc_recorder {
* may access it without data races. * may access it without data races.
*/ */
bool audio; bool audio;
bool video;
enum sc_orientation orientation;
char *filename; char *filename;
enum sc_record_format format; enum sc_record_format format;
@ -34,19 +42,20 @@ struct sc_recorder {
sc_thread thread; sc_thread thread;
sc_mutex mutex; sc_mutex mutex;
sc_cond queue_cond; sc_cond cond;
// set on sc_recorder_stop(), packet_sink close or recording failure // set on sc_recorder_stop(), packet_sink close or recording failure
bool stopped; bool stopped;
struct sc_recorder_queue video_queue; struct sc_recorder_queue video_queue;
struct sc_recorder_queue audio_queue; struct sc_recorder_queue audio_queue;
// wake up the recorder thread once the video or audio codec is known // wake up the recorder thread once the video or audio codec is known
sc_cond stream_cond;
bool video_init; bool video_init;
bool audio_init; bool audio_init;
int video_stream_index; bool audio_expects_config_packet;
int audio_stream_index;
struct sc_recorder_stream video_stream;
struct sc_recorder_stream audio_stream;
const struct sc_recorder_callbacks *cbs; const struct sc_recorder_callbacks *cbs;
void *cbs_userdata; void *cbs_userdata;
@ -59,7 +68,8 @@ struct sc_recorder_callbacks {
bool bool
sc_recorder_init(struct sc_recorder *recorder, const char *filename, sc_recorder_init(struct sc_recorder *recorder, const char *filename,
enum sc_record_format format, bool audio, enum sc_record_format format, bool video, bool audio,
enum sc_orientation orientation,
const struct sc_recorder_callbacks *cbs, void *cbs_userdata); const struct sc_recorder_callbacks *cbs, void *cbs_userdata);
bool bool

View File

@ -35,6 +35,7 @@
#include "util/log.h" #include "util/log.h"
#include "util/net.h" #include "util/net.h"
#include "util/rand.h" #include "util/rand.h"
#include "util/timeout.h"
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
# include "v4l2_sink.h" # include "v4l2_sink.h"
#endif #endif
@ -73,6 +74,7 @@ struct scrcpy {
struct sc_hid_mouse mouse_hid; struct sc_hid_mouse mouse_hid;
#endif #endif
}; };
struct sc_timeout timeout;
}; };
static inline void static inline void
@ -88,7 +90,7 @@ push_event(uint32_t type, const char *name) {
#define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE) #define PUSH_EVENT(TYPE) push_event(TYPE, # TYPE)
#ifdef _WIN32 #ifdef _WIN32
BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) { static BOOL WINAPI windows_ctrl_handler(DWORD ctrl_type) {
if (ctrl_type == CTRL_C_EVENT) { if (ctrl_type == CTRL_C_EVENT) {
PUSH_EVENT(SDL_QUIT); PUSH_EVENT(SDL_QUIT);
return TRUE; return TRUE;
@ -137,7 +139,7 @@ sdl_set_hints(const char *render_driver) {
} }
static void static void
sdl_configure(bool display, bool disable_screensaver) { sdl_configure(bool video_playback, bool disable_screensaver) {
#ifdef _WIN32 #ifdef _WIN32
// Clean up properly on Ctrl+C on Windows // Clean up properly on Ctrl+C on Windows
bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE); bool ok = SetConsoleCtrlHandler(windows_ctrl_handler, TRUE);
@ -146,7 +148,7 @@ sdl_configure(bool display, bool disable_screensaver) {
} }
#endif // _WIN32 #endif // _WIN32
if (!display) { if (!video_playback) {
return; return;
} }
@ -171,6 +173,9 @@ event_loop(struct scrcpy *s) {
case SC_EVENT_RECORDER_ERROR: case SC_EVENT_RECORDER_ERROR:
LOGE("Recorder error"); LOGE("Recorder error");
return SCRCPY_EXIT_FAILURE; return SCRCPY_EXIT_FAILURE;
case SC_EVENT_TIME_LIMIT_REACHED:
LOGI("Time limit reached");
return SCRCPY_EXIT_SUCCESS;
case SDL_QUIT: case SDL_QUIT:
LOGD("User requested to quit"); LOGD("User requested to quit");
return SCRCPY_EXIT_SUCCESS; return SCRCPY_EXIT_SUCCESS;
@ -247,7 +252,9 @@ sc_audio_demuxer_on_ended(struct sc_demuxer *demuxer,
// Contrary to the video demuxer, keep mirroring if only the audio fails // Contrary to the video demuxer, keep mirroring if only the audio fails
// (unless --require-audio is set). // (unless --require-audio is set).
if (status == SC_DEMUXER_STATUS_ERROR if (status == SC_DEMUXER_STATUS_EOS) {
PUSH_EVENT(SC_EVENT_DEVICE_DISCONNECTED);
} else if (status == SC_DEMUXER_STATUS_ERROR
|| (status == SC_DEMUXER_STATUS_DISABLED || (status == SC_DEMUXER_STATUS_DISABLED
&& options->require_audio)) { && options->require_audio)) {
PUSH_EVENT(SC_EVENT_DEMUXER_ERROR); PUSH_EVENT(SC_EVENT_DEMUXER_ERROR);
@ -280,9 +287,17 @@ sc_server_on_disconnected(struct sc_server *server, void *userdata) {
// event // 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 // Generate a scrcpy id to differentiate multiple running scrcpy instances
static uint32_t static uint32_t
scrcpy_generate_scid() { scrcpy_generate_scid(void) {
struct sc_rand rand; struct sc_rand rand;
sc_rand_init(&rand); sc_rand_init(&rand);
// Only use 31 bits to avoid issues with signed values on the Java-side // Only use 31 bits to avoid issues with signed values on the Java-side
@ -321,6 +336,8 @@ scrcpy(struct scrcpy_options *options) {
bool controller_initialized = false; bool controller_initialized = false;
bool controller_started = false; bool controller_started = false;
bool screen_initialized = false; bool screen_initialized = false;
bool timeout_initialized = false;
bool timeout_started = false;
struct sc_acksync *acksync = NULL; struct sc_acksync *acksync = NULL;
@ -334,6 +351,9 @@ scrcpy(struct scrcpy_options *options) {
.log_level = options->log_level, .log_level = options->log_level,
.video_codec = options->video_codec, .video_codec = options->video_codec,
.audio_codec = options->audio_codec, .audio_codec = options->audio_codec,
.video_source = options->video_source,
.audio_source = options->audio_source,
.camera_facing = options->camera_facing,
.crop = options->crop, .crop = options->crop,
.port_range = options->port_range, .port_range = options->port_range,
.tunnel_host = options->tunnel_host, .tunnel_host = options->tunnel_host,
@ -345,6 +365,7 @@ scrcpy(struct scrcpy_options *options) {
.lock_video_orientation = options->lock_video_orientation, .lock_video_orientation = options->lock_video_orientation,
.control = options->control, .control = options->control,
.display_id = options->display_id, .display_id = options->display_id,
.video = options->video,
.audio = options->audio, .audio = options->audio,
.show_touches = options->show_touches, .show_touches = options->show_touches,
.stay_awake = options->stay_awake, .stay_awake = options->stay_awake,
@ -352,6 +373,10 @@ scrcpy(struct scrcpy_options *options) {
.audio_codec_options = options->audio_codec_options, .audio_codec_options = options->audio_codec_options,
.video_encoder = options->video_encoder, .video_encoder = options->video_encoder,
.audio_encoder = options->audio_encoder, .audio_encoder = options->audio_encoder,
.camera_id = options->camera_id,
.camera_size = options->camera_size,
.camera_ar = options->camera_ar,
.camera_fps = options->camera_fps,
.force_adb_forward = options->force_adb_forward, .force_adb_forward = options->force_adb_forward,
.power_off_on_close = options->power_off_on_close, .power_off_on_close = options->power_off_on_close,
.clipboard_autosync = options->clipboard_autosync, .clipboard_autosync = options->clipboard_autosync,
@ -360,8 +385,9 @@ scrcpy(struct scrcpy_options *options) {
.tcpip_dst = options->tcpip_dst, .tcpip_dst = options->tcpip_dst,
.cleanup = options->cleanup, .cleanup = options->cleanup,
.power_on = options->power_on, .power_on = options->power_on,
.list_encoders = options->list_encoders, .kill_adb_on_close = options->kill_adb_on_close,
.list_displays = options->list_displays, .camera_high_speed = options->camera_high_speed,
.list = options->list,
}; };
static const struct sc_server_callbacks cbs = { static const struct sc_server_callbacks cbs = {
@ -379,30 +405,45 @@ scrcpy(struct scrcpy_options *options) {
server_started = true; server_started = true;
if (options->list_encoders || options->list_displays) { if (options->list) {
bool ok = await_for_server(NULL); bool ok = await_for_server(NULL);
ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE; ret = ok ? SCRCPY_EXIT_SUCCESS : SCRCPY_EXIT_FAILURE;
goto end; goto end;
} }
if (options->display) { // playback implies capture
assert(!options->video_playback || options->video);
assert(!options->audio_playback || options->audio);
if (options->video_playback) {
sdl_set_hints(options->render_driver); sdl_set_hints(options->render_driver);
} }
// Initialize SDL video in addition if display is enabled if (options->video_playback ||
if (options->display) { (options->control && options->clipboard_autosync)) {
// Initialize the video subsystem even if --no-video or
// --no-video-playback is passed so that clipboard synchronization
// still works.
// <https://github.com/Genymobile/scrcpy/issues/4418>
if (SDL_Init(SDL_INIT_VIDEO)) { if (SDL_Init(SDL_INIT_VIDEO)) {
LOGE("Could not initialize SDL video: %s", SDL_GetError()); // If it fails, it is an error only if video playback is enabled
goto end; if (options->video_playback) {
LOGE("Could not initialize SDL video: %s", SDL_GetError());
goto end;
} else {
LOGW("Could not initialize SDL video: %s", SDL_GetError());
}
} }
}
if (options->audio && SDL_Init(SDL_INIT_AUDIO)) { if (options->audio_playback) {
if (SDL_Init(SDL_INIT_AUDIO)) {
LOGE("Could not initialize SDL audio: %s", SDL_GetError()); LOGE("Could not initialize SDL audio: %s", SDL_GetError());
goto end; goto end;
} }
} }
sdl_configure(options->display, options->disable_screensaver); sdl_configure(options->video_playback, options->disable_screensaver);
// Await for server without blocking Ctrl+C handling // Await for server without blocking Ctrl+C handling
bool connected; bool connected;
@ -428,7 +469,7 @@ scrcpy(struct scrcpy_options *options) {
struct sc_file_pusher *fp = NULL; struct sc_file_pusher *fp = NULL;
if (options->display && options->control) { if (options->video_playback && options->control) {
if (!sc_file_pusher_init(&s->file_pusher, serial, if (!sc_file_pusher_init(&s->file_pusher, serial,
options->push_target)) { options->push_target)) {
goto end; goto end;
@ -437,11 +478,13 @@ scrcpy(struct scrcpy_options *options) {
file_pusher_initialized = true; file_pusher_initialized = true;
} }
static const struct sc_demuxer_callbacks video_demuxer_cbs = { if (options->video) {
.on_ended = sc_video_demuxer_on_ended, 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); sc_demuxer_init(&s->video_demuxer, "video", s->server.video_socket,
&video_demuxer_cbs, NULL);
}
if (options->audio) { if (options->audio) {
static const struct sc_demuxer_callbacks audio_demuxer_cbs = { static const struct sc_demuxer_callbacks audio_demuxer_cbs = {
@ -451,8 +494,8 @@ scrcpy(struct scrcpy_options *options) {
&audio_demuxer_cbs, options); &audio_demuxer_cbs, options);
} }
bool needs_video_decoder = options->display; bool needs_video_decoder = options->video_playback;
bool needs_audio_decoder = options->audio && options->display; bool needs_audio_decoder = options->audio_playback;
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
needs_video_decoder |= !!options->v4l2_device; needs_video_decoder |= !!options->v4l2_device;
#endif #endif
@ -472,7 +515,8 @@ scrcpy(struct scrcpy_options *options) {
.on_ended = sc_recorder_on_ended, .on_ended = sc_recorder_on_ended,
}; };
if (!sc_recorder_init(&s->recorder, options->record_filename, if (!sc_recorder_init(&s->recorder, options->record_filename,
options->record_format, options->audio, options->record_format, options->video,
options->audio, options->record_orientation,
&recorder_cbs, NULL)) { &recorder_cbs, NULL)) {
goto end; goto end;
} }
@ -483,8 +527,10 @@ scrcpy(struct scrcpy_options *options) {
} }
recorder_started = true; recorder_started = true;
sc_packet_source_add_sink(&s->video_demuxer.packet_source, if (options->video) {
&s->recorder.video_packet_sink); sc_packet_source_add_sink(&s->video_demuxer.packet_source,
&s->recorder.video_packet_sink);
}
if (options->audio) { if (options->audio) {
sc_packet_source_add_sink(&s->audio_demuxer.packet_source, sc_packet_source_add_sink(&s->audio_demuxer.packet_source,
&s->recorder.audio_packet_sink); &s->recorder.audio_packet_sink);
@ -630,23 +676,12 @@ aoa_hid_end:
} }
controller_started = true; controller_started = true;
controller = &s->controller; 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 // There is a controller if and only if control is enabled
assert(options->control == !!controller); assert(options->control == !!controller);
if (options->display) { if (options->video_playback) {
const char *window_title = const char *window_title =
options->window_title ? options->window_title : info->device_name; options->window_title ? options->window_title : info->device_name;
@ -666,17 +701,12 @@ aoa_hid_end:
.window_width = options->window_width, .window_width = options->window_width,
.window_height = options->window_height, .window_height = options->window_height,
.window_borderless = options->window_borderless, .window_borderless = options->window_borderless,
.rotation = options->rotation, .orientation = options->display_orientation,
.mipmaps = options->mipmaps, .mipmaps = options->mipmaps,
.fullscreen = options->fullscreen, .fullscreen = options->fullscreen,
.start_fps_counter = options->start_fps_counter, .start_fps_counter = options->start_fps_counter,
}; };
if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
}
screen_initialized = true;
struct sc_frame_source *src = &s->video_decoder.frame_source; struct sc_frame_source *src = &s->video_decoder.frame_source;
if (options->display_buffer) { if (options->display_buffer) {
sc_delay_buffer_init(&s->display_buffer, options->display_buffer, sc_delay_buffer_init(&s->display_buffer, options->display_buffer,
@ -685,14 +715,19 @@ aoa_hid_end:
src = &s->display_buffer.frame_source; src = &s->display_buffer.frame_source;
} }
sc_frame_source_add_sink(src, &s->screen.frame_sink); if (!sc_screen_init(&s->screen, &screen_params)) {
goto end;
if (options->audio) {
sc_audio_player_init(&s->audio_player, options->audio_buffer,
options->audio_output_buffer);
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
&s->audio_player.frame_sink);
} }
screen_initialized = true;
sc_frame_source_add_sink(src, &s->screen.frame_sink);
}
if (options->audio_playback) {
sc_audio_player_init(&s->audio_player, options->audio_buffer,
options->audio_output_buffer);
sc_frame_source_add_sink(&s->audio_decoder.frame_source,
&s->audio_player.frame_sink);
} }
#ifdef HAVE_V4L2 #ifdef HAVE_V4L2
@ -714,12 +749,15 @@ aoa_hid_end:
} }
#endif #endif
// now we consumed the header values, the socket receives the video stream // Now that the header values have been consumed, the socket(s) will
// start the video demuxer // receive the stream(s). Start the demuxer(s).
if (!sc_demuxer_start(&s->video_demuxer)) {
goto end; if (options->video) {
if (!sc_demuxer_start(&s->video_demuxer)) {
goto end;
}
video_demuxer_started = true;
} }
video_demuxer_started = true;
if (options->audio) { if (options->audio) {
if (!sc_demuxer_start(&s->audio_demuxer)) { if (!sc_demuxer_start(&s->audio_demuxer)) {
@ -728,6 +766,39 @@ aoa_hid_end:
audio_demuxer_started = true; 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); ret = event_loop(s);
LOGD("quit..."); LOGD("quit...");
@ -736,6 +807,10 @@ aoa_hid_end:
sc_screen_hide_window(&s->screen); sc_screen_hide_window(&s->screen);
end: end:
if (timeout_started) {
sc_timeout_stop(&s->timeout);
}
// The demuxer is not stopped explicitly, because it will stop by itself on // The demuxer is not stopped explicitly, because it will stop by itself on
// end-of-stream // end-of-stream
#ifdef HAVE_USB #ifdef HAVE_USB
@ -771,6 +846,13 @@ end:
sc_server_stop(&s->server); 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 // now that the sockets are shutdown, the demuxer and controller are
// interrupted, we can join them // interrupted, we can join them
if (video_demuxer_started) { if (video_demuxer_started) {

View File

@ -14,16 +14,16 @@
#define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink)
static inline struct sc_size static inline struct sc_size
get_rotated_size(struct sc_size size, int rotation) { get_oriented_size(struct sc_size size, enum sc_orientation orientation) {
struct sc_size rotated_size; struct sc_size oriented_size;
if (rotation & 1) { if (sc_orientation_is_swap(orientation)) {
rotated_size.width = size.height; oriented_size.width = size.height;
rotated_size.height = size.width; oriented_size.height = size.width;
} else { } else {
rotated_size.width = size.width; oriented_size.width = size.width;
rotated_size.height = size.height; oriented_size.height = size.height;
} }
return rotated_size; return oriented_size;
} }
// get the window size in a struct sc_size // get the window size in a struct sc_size
@ -56,6 +56,7 @@ static void
set_window_size(struct sc_screen *screen, struct sc_size new_size) { set_window_size(struct sc_screen *screen, struct sc_size new_size) {
assert(!screen->fullscreen); assert(!screen->fullscreen);
assert(!screen->maximized); assert(!screen->maximized);
assert(!screen->minimized);
SDL_SetWindowSize(screen->window, new_size.width, new_size.height); SDL_SetWindowSize(screen->window, new_size.width, new_size.height);
} }
@ -249,9 +250,9 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) {
sc_screen_update_content_rect(screen); sc_screen_update_content_rect(screen);
} }
bool ok = sc_display_render(&screen->display, &screen->rect, enum sc_display_result res =
screen->rotation); sc_display_render(&screen->display, &screen->rect, screen->orientation);
(void) ok; // error already logged (void) res; // any error already logged
} }
#if defined(__APPLE__) || defined(__WINDOWS__) #if defined(__APPLE__) || defined(__WINDOWS__)
@ -359,6 +360,7 @@ sc_screen_init(struct sc_screen *screen,
screen->has_frame = false; screen->has_frame = false;
screen->fullscreen = false; screen->fullscreen = false;
screen->maximized = false; screen->maximized = false;
screen->minimized = false;
screen->mouse_capture_key_pressed = 0; screen->mouse_capture_key_pressed = 0;
screen->req.x = params->window_x; screen->req.x = params->window_x;
@ -377,9 +379,10 @@ sc_screen_init(struct sc_screen *screen,
goto error_destroy_frame_buffer; goto error_destroy_frame_buffer;
} }
screen->rotation = params->rotation; screen->orientation = params->orientation;
if (screen->rotation) { if (screen->orientation != SC_ORIENTATION_0) {
LOGI("Initial display rotation set to %u", screen->rotation); LOGI("Initial display orientation set to %s",
sc_orientation_get_name(screen->orientation));
} }
uint32_t window_flags = SDL_WINDOW_HIDDEN uint32_t window_flags = SDL_WINDOW_HIDDEN
@ -486,6 +489,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) {
} }
SDL_ShowWindow(screen->window); SDL_ShowWindow(screen->window);
sc_screen_update_content_rect(screen);
} }
void void
@ -531,11 +535,11 @@ resize_for_content(struct sc_screen *screen, struct sc_size old_content_size,
static void static void
set_content_size(struct sc_screen *screen, struct sc_size new_content_size) { set_content_size(struct sc_screen *screen, struct sc_size new_content_size) {
if (!screen->fullscreen && !screen->maximized) { if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
resize_for_content(screen, screen->content_size, new_content_size); resize_for_content(screen, screen->content_size, new_content_size);
} else if (!screen->resize_pending) { } else if (!screen->resize_pending) {
// Store the windowed size to be able to compute the optimal size once // Store the windowed size to be able to compute the optimal size once
// fullscreen and maximized are disabled // fullscreen/maximized/minimized are disabled
screen->windowed_content_size = screen->content_size; screen->windowed_content_size = screen->content_size;
screen->resize_pending = true; screen->resize_pending = true;
} }
@ -547,6 +551,7 @@ static void
apply_pending_resize(struct sc_screen *screen) { apply_pending_resize(struct sc_screen *screen) {
assert(!screen->fullscreen); assert(!screen->fullscreen);
assert(!screen->maximized); assert(!screen->maximized);
assert(!screen->minimized);
if (screen->resize_pending) { if (screen->resize_pending) {
resize_for_content(screen, screen->windowed_content_size, resize_for_content(screen, screen->windowed_content_size,
screen->content_size); screen->content_size);
@ -555,19 +560,19 @@ apply_pending_resize(struct sc_screen *screen) {
} }
void void
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) { sc_screen_set_orientation(struct sc_screen *screen,
assert(rotation < 4); enum sc_orientation orientation) {
if (rotation == screen->rotation) { if (orientation == screen->orientation) {
return; return;
} }
struct sc_size new_content_size = struct sc_size new_content_size =
get_rotated_size(screen->frame_size, rotation); get_oriented_size(screen->frame_size, orientation);
set_content_size(screen, new_content_size); set_content_size(screen, new_content_size);
screen->rotation = rotation; screen->orientation = orientation;
LOGI("Display rotation set to %u", rotation); LOGI("Display orientation set to %s", sc_orientation_get_name(orientation));
sc_screen_render(screen, true); sc_screen_render(screen, true);
} }
@ -580,25 +585,27 @@ sc_screen_init_size(struct sc_screen *screen) {
// The requested size is passed via screen->frame_size // The requested size is passed via screen->frame_size
struct sc_size content_size = struct sc_size content_size =
get_rotated_size(screen->frame_size, screen->rotation); get_oriented_size(screen->frame_size, screen->orientation);
screen->content_size = content_size; screen->content_size = content_size;
return sc_display_set_texture_size(&screen->display, screen->frame_size); enum sc_display_result res =
sc_display_set_texture_size(&screen->display, screen->frame_size);
return res != SC_DISPLAY_RESULT_ERROR;
} }
// recreate the texture and resize the window if the frame size has changed // recreate the texture and resize the window if the frame size has changed
static bool static enum sc_display_result
prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) {
if (screen->frame_size.width == new_frame_size.width if (screen->frame_size.width == new_frame_size.width
&& screen->frame_size.height == new_frame_size.height) { && screen->frame_size.height == new_frame_size.height) {
return true; return SC_DISPLAY_RESULT_OK;
} }
// frame dimension changed // frame dimension changed
screen->frame_size = new_frame_size; screen->frame_size = new_frame_size;
struct sc_size new_content_size = struct sc_size new_content_size =
get_rotated_size(new_frame_size, screen->rotation); get_oriented_size(new_frame_size, screen->orientation);
set_content_size(screen, new_content_size); set_content_size(screen, new_content_size);
sc_screen_update_content_rect(screen); sc_screen_update_content_rect(screen);
@ -615,13 +622,23 @@ sc_screen_update_frame(struct sc_screen *screen) {
sc_fps_counter_add_rendered_frame(&screen->fps_counter); sc_fps_counter_add_rendered_frame(&screen->fps_counter);
struct sc_size new_frame_size = {frame->width, frame->height}; struct sc_size new_frame_size = {frame->width, frame->height};
if (!prepare_for_frame(screen, new_frame_size)) { enum sc_display_result res = prepare_for_frame(screen, new_frame_size);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false; return false;
} }
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
if (!sc_display_update_texture(&screen->display, frame)) { res = sc_display_update_texture(&screen->display, frame);
if (res == SC_DISPLAY_RESULT_ERROR) {
return false; return false;
} }
if (res == SC_DISPLAY_RESULT_PENDING) {
// Not an error, but do not continue
return true;
}
if (!screen->has_frame) { if (!screen->has_frame) {
screen->has_frame = true; screen->has_frame = true;
@ -647,7 +664,7 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
} }
screen->fullscreen = !screen->fullscreen; screen->fullscreen = !screen->fullscreen;
if (!screen->fullscreen && !screen->maximized) { if (!screen->fullscreen && !screen->maximized && !screen->minimized) {
apply_pending_resize(screen); apply_pending_resize(screen);
} }
@ -657,7 +674,7 @@ sc_screen_switch_fullscreen(struct sc_screen *screen) {
void void
sc_screen_resize_to_fit(struct sc_screen *screen) { sc_screen_resize_to_fit(struct sc_screen *screen) {
if (screen->fullscreen || screen->maximized) { if (screen->fullscreen || screen->maximized || screen->minimized) {
return; return;
} }
@ -681,7 +698,7 @@ sc_screen_resize_to_fit(struct sc_screen *screen) {
void void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) {
if (screen->fullscreen) { if (screen->fullscreen || screen->minimized) {
return; return;
} }
@ -738,6 +755,9 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
case SDL_WINDOWEVENT_MAXIMIZED: case SDL_WINDOWEVENT_MAXIMIZED:
screen->maximized = true; screen->maximized = true;
break; break;
case SDL_WINDOWEVENT_MINIMIZED:
screen->minimized = true;
break;
case SDL_WINDOWEVENT_RESTORED: case SDL_WINDOWEVENT_RESTORED:
if (screen->fullscreen) { if (screen->fullscreen) {
// On Windows, in maximized+fullscreen, disabling // On Windows, in maximized+fullscreen, disabling
@ -748,6 +768,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
break; break;
} }
screen->maximized = false; screen->maximized = false;
screen->minimized = false;
apply_pending_resize(screen); apply_pending_resize(screen);
sc_screen_render(screen, true); sc_screen_render(screen, true);
break; break;
@ -823,37 +844,54 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) {
struct sc_point struct sc_point
sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen,
int32_t x, int32_t y) { int32_t x, int32_t y) {
unsigned rotation = screen->rotation; enum sc_orientation orientation = screen->orientation;
assert(rotation < 4);
int32_t w = screen->content_size.width; int32_t w = screen->content_size.width;
int32_t h = screen->content_size.height; int32_t h = screen->content_size.height;
// screen->rect must be initialized to avoid a division by zero
assert(screen->rect.w && screen->rect.h);
x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; x = (int64_t) (x - screen->rect.x) * w / screen->rect.w;
y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h;
// rotate
struct sc_point result; struct sc_point result;
switch (rotation) { switch (orientation) {
case 0: case SC_ORIENTATION_0:
result.x = x; result.x = x;
result.y = y; result.y = y;
break; break;
case 1: case SC_ORIENTATION_90:
result.x = h - y;
result.y = x;
break;
case 2:
result.x = w - x;
result.y = h - y;
break;
default:
assert(rotation == 3);
result.x = y; result.x = y;
result.y = w - x; result.y = w - x;
break; break;
case SC_ORIENTATION_180:
result.x = w - x;
result.y = h - y;
break;
case SC_ORIENTATION_270:
result.x = h - y;
result.y = x;
break;
case SC_ORIENTATION_FLIP_0:
result.x = w - x;
result.y = y;
break;
case SC_ORIENTATION_FLIP_90:
result.x = h - y;
result.y = w - x;
break;
case SC_ORIENTATION_FLIP_180:
result.x = x;
result.y = h - y;
break;
default:
assert(orientation == SC_ORIENTATION_FLIP_270);
result.x = y;
result.y = x;
break;
} }
return result; return result;
} }

View File

@ -14,6 +14,7 @@
#include "frame_buffer.h" #include "frame_buffer.h"
#include "input_manager.h" #include "input_manager.h"
#include "opengl.h" #include "opengl.h"
#include "options.h"
#include "trait/key_processor.h" #include "trait/key_processor.h"
#include "trait/frame_sink.h" #include "trait/frame_sink.h"
#include "trait/mouse_processor.h" #include "trait/mouse_processor.h"
@ -49,13 +50,14 @@ struct sc_screen {
// fullscreen (meaningful only when resize_pending is true) // fullscreen (meaningful only when resize_pending is true)
struct sc_size windowed_content_size; struct sc_size windowed_content_size;
// client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) // client orientation
unsigned rotation; enum sc_orientation orientation;
// rectangle of the content (excluding black borders) // rectangle of the content (excluding black borders)
struct SDL_Rect rect; struct SDL_Rect rect;
bool has_frame; bool has_frame;
bool fullscreen; bool fullscreen;
bool maximized; bool maximized;
bool minimized;
// To enable/disable mouse capture, a mouse capture key (LALT, LGUI or // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or
// RGUI) must be pressed. This variable tracks the pressed capture key. // RGUI) must be pressed. This variable tracks the pressed capture key.
@ -85,7 +87,7 @@ struct sc_screen_params {
bool window_borderless; bool window_borderless;
uint8_t rotation; enum sc_orientation orientation;
bool mipmaps; bool mipmaps;
bool fullscreen; bool fullscreen;
@ -128,9 +130,10 @@ sc_screen_resize_to_fit(struct sc_screen *screen);
void void
sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); sc_screen_resize_to_pixel_perfect(struct sc_screen *screen);
// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) // set the display orientation
void void
sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); sc_screen_set_orientation(struct sc_screen *screen,
enum sc_orientation orientation);
// react to SDL events // react to SDL events
// If this function returns false, scrcpy must exit with an error. // If this function returns false, scrcpy must exit with an error.

View File

@ -76,6 +76,8 @@ sc_server_params_destroy(struct sc_server_params *params) {
free((char *) params->video_encoder); free((char *) params->video_encoder);
free((char *) params->audio_encoder); free((char *) params->audio_encoder);
free((char *) params->tcpip_dst); free((char *) params->tcpip_dst);
free((char *) params->camera_id);
free((char *) params->camera_ar);
} }
static bool static bool
@ -86,14 +88,15 @@ sc_server_params_copy(struct sc_server_params *dst,
// The params reference user-allocated memory, so we must copy them to // The params reference user-allocated memory, so we must copy them to
// handle them from another thread // handle them from another thread
#define COPY(FIELD) \ #define COPY(FIELD) do { \
dst->FIELD = NULL; \ dst->FIELD = NULL; \
if (src->FIELD) { \ if (src->FIELD) { \
dst->FIELD = strdup(src->FIELD); \ dst->FIELD = strdup(src->FIELD); \
if (!dst->FIELD) { \ if (!dst->FIELD) { \
goto error; \ goto error; \
} \ } \
} } \
} while(0)
COPY(req_serial); COPY(req_serial);
COPY(crop); COPY(crop);
@ -102,6 +105,8 @@ sc_server_params_copy(struct sc_server_params *dst,
COPY(video_encoder); COPY(video_encoder);
COPY(audio_encoder); COPY(audio_encoder);
COPY(tcpip_dst); COPY(tcpip_dst);
COPY(camera_id);
COPY(camera_ar);
#undef COPY #undef COPY
return true; return true;
@ -173,6 +178,8 @@ sc_server_get_codec_name(enum sc_codec codec) {
return "opus"; return "opus";
case SC_CODEC_AAC: case SC_CODEC_AAC:
return "aac"; return "aac";
case SC_CODEC_FLAC:
return "flac";
case SC_CODEC_RAW: case SC_CODEC_RAW:
return "raw"; return "raw";
default: default:
@ -180,6 +187,20 @@ sc_server_get_codec_name(enum sc_codec codec) {
} }
} }
static const char *
sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) {
switch (camera_facing) {
case SC_CAMERA_FACING_FRONT:
return "front";
case SC_CAMERA_FACING_BACK:
return "back";
case SC_CAMERA_FACING_EXTERNAL:
return "external";
default:
return NULL;
}
}
static sc_pid static sc_pid
execute_server(struct sc_server *server, execute_server(struct sc_server *server,
const struct sc_server_params *params) { const struct sc_server_params *params) {
@ -215,23 +236,27 @@ execute_server(struct sc_server *server,
cmd[count++] = SCRCPY_VERSION; cmd[count++] = SCRCPY_VERSION;
unsigned dyn_idx = count; // from there, the strings are allocated unsigned dyn_idx = count; // from there, the strings are allocated
#define ADD_PARAM(fmt, ...) { \ #define ADD_PARAM(fmt, ...) do { \
char *p; \ char *p; \
if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \ if (asprintf(&p, fmt, ## __VA_ARGS__) == -1) { \
goto end; \ goto end; \
} \ } \
cmd[count++] = p; \ cmd[count++] = p; \
} } while(0)
ADD_PARAM("scid=%08x", params->scid); ADD_PARAM("scid=%08x", params->scid);
ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level)); ADD_PARAM("log_level=%s", log_level_to_server_string(params->log_level));
if (!params->video) {
ADD_PARAM("video=false");
}
if (params->video_bit_rate) { if (params->video_bit_rate) {
ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate); ADD_PARAM("video_bit_rate=%" PRIu32, params->video_bit_rate);
} }
if (!params->audio) { if (!params->audio) {
ADD_PARAM("audio=false"); ADD_PARAM("audio=false");
} else if (params->audio_bit_rate) { }
if (params->audio_bit_rate) {
ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate); ADD_PARAM("audio_bit_rate=%" PRIu32, params->audio_bit_rate);
} }
if (params->video_codec != SC_CODEC_H264) { if (params->video_codec != SC_CODEC_H264) {
@ -242,6 +267,13 @@ execute_server(struct sc_server *server,
ADD_PARAM("audio_codec=%s", ADD_PARAM("audio_codec=%s",
sc_server_get_codec_name(params->audio_codec)); sc_server_get_codec_name(params->audio_codec));
} }
if (params->video_source != SC_VIDEO_SOURCE_DISPLAY) {
assert(params->video_source == SC_VIDEO_SOURCE_CAMERA);
ADD_PARAM("video_source=camera");
}
if (params->audio_source == SC_AUDIO_SOURCE_MIC) {
ADD_PARAM("audio_source=mic");
}
if (params->max_size) { if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size); ADD_PARAM("max_size=%" PRIu16, params->max_size);
} }
@ -265,6 +297,25 @@ execute_server(struct sc_server *server,
if (params->display_id) { if (params->display_id) {
ADD_PARAM("display_id=%" PRIu32, params->display_id); ADD_PARAM("display_id=%" PRIu32, params->display_id);
} }
if (params->camera_id) {
ADD_PARAM("camera_id=%s", params->camera_id);
}
if (params->camera_size) {
ADD_PARAM("camera_size=%s", params->camera_size);
}
if (params->camera_facing != SC_CAMERA_FACING_ANY) {
ADD_PARAM("camera_facing=%s",
sc_server_get_camera_facing_name(params->camera_facing));
}
if (params->camera_ar) {
ADD_PARAM("camera_ar=%s", params->camera_ar);
}
if (params->camera_fps) {
ADD_PARAM("camera_fps=%" PRIu16, params->camera_fps);
}
if (params->camera_high_speed) {
ADD_PARAM("camera_high_speed=true");
}
if (params->show_touches) { if (params->show_touches) {
ADD_PARAM("show_touches=true"); ADD_PARAM("show_touches=true");
} }
@ -302,12 +353,18 @@ execute_server(struct sc_server *server,
// By default, power_on is true // By default, power_on is true
ADD_PARAM("power_on=false"); ADD_PARAM("power_on=false");
} }
if (params->list_encoders) { if (params->list & SC_OPTION_LIST_ENCODERS) {
ADD_PARAM("list_encoders=true"); ADD_PARAM("list_encoders=true");
} }
if (params->list_displays) { if (params->list & SC_OPTION_LIST_DISPLAYS) {
ADD_PARAM("list_displays=true"); ADD_PARAM("list_displays=true");
} }
if (params->list & SC_OPTION_LIST_CAMERAS) {
ADD_PARAM("list_cameras=true");
}
if (params->list & SC_OPTION_LIST_CAMERA_SIZES) {
ADD_PARAM("list_camera_sizes=true");
}
#undef ADD_PARAM #undef ADD_PARAM
@ -463,6 +520,7 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
const char *serial = server->serial; const char *serial = server->serial;
assert(serial); assert(serial);
bool video = server->params.video;
bool audio = server->params.audio; bool audio = server->params.audio;
bool control = server->params.control; bool control = server->params.control;
@ -470,9 +528,12 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
sc_socket audio_socket = SC_SOCKET_NONE; sc_socket audio_socket = SC_SOCKET_NONE;
sc_socket control_socket = SC_SOCKET_NONE; sc_socket control_socket = SC_SOCKET_NONE;
if (!tunnel->forward) { if (!tunnel->forward) {
video_socket = net_accept_intr(&server->intr, tunnel->server_socket); if (video) {
if (video_socket == SC_SOCKET_NONE) { video_socket =
goto fail; net_accept_intr(&server->intr, tunnel->server_socket);
if (video_socket == SC_SOCKET_NONE) {
goto fail;
}
} }
if (audio) { if (audio) {
@ -503,35 +564,45 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
unsigned attempts = 100; unsigned attempts = 100;
sc_tick delay = SC_TICK_FROM_MS(100); sc_tick delay = SC_TICK_FROM_MS(100);
video_socket = connect_to_server(server, attempts, delay, tunnel_host, sc_socket first_socket = connect_to_server(server, attempts, delay,
tunnel_port); tunnel_host, tunnel_port);
if (video_socket == SC_SOCKET_NONE) { if (first_socket == SC_SOCKET_NONE) {
goto fail; goto fail;
} }
if (video) {
video_socket = first_socket;
}
if (audio) { if (audio) {
audio_socket = net_socket(); if (!video) {
if (audio_socket == SC_SOCKET_NONE) { audio_socket = first_socket;
goto fail; } else {
} audio_socket = net_socket();
bool ok = net_connect_intr(&server->intr, audio_socket, tunnel_host, if (audio_socket == SC_SOCKET_NONE) {
tunnel_port); goto fail;
if (!ok) { }
goto fail; bool ok = net_connect_intr(&server->intr, audio_socket,
tunnel_host, tunnel_port);
if (!ok) {
goto fail;
}
} }
} }
if (control) { if (control) {
// we know that the device is listening, we don't need several if (!video && !audio) {
// attempts control_socket = first_socket;
control_socket = net_socket(); } else {
if (control_socket == SC_SOCKET_NONE) { control_socket = net_socket();
goto fail; if (control_socket == SC_SOCKET_NONE) {
} goto fail;
bool ok = net_connect_intr(&server->intr, control_socket, }
tunnel_host, tunnel_port); bool ok = net_connect_intr(&server->intr, control_socket,
if (!ok) { tunnel_host, tunnel_port);
goto fail; if (!ok) {
goto fail;
}
} }
} }
} }
@ -540,13 +611,17 @@ sc_server_connect_to(struct sc_server *server, struct sc_server_info *info) {
sc_adb_tunnel_close(tunnel, &server->intr, serial, sc_adb_tunnel_close(tunnel, &server->intr, serial,
server->device_socket_name); server->device_socket_name);
sc_socket first_socket = video ? video_socket
: audio ? audio_socket
: control_socket;
// The sockets will be closed on stop if device_read_info() fails // The sockets will be closed on stop if device_read_info() fails
bool ok = device_read_info(&server->intr, video_socket, info); bool ok = device_read_info(&server->intr, first_socket, info);
if (!ok) { if (!ok) {
goto fail; goto fail;
} }
assert(video_socket != SC_SOCKET_NONE); assert(!video || video_socket != SC_SOCKET_NONE);
assert(!audio || audio_socket != SC_SOCKET_NONE); assert(!audio || audio_socket != SC_SOCKET_NONE);
assert(!control || control_socket != SC_SOCKET_NONE); assert(!control || control_socket != SC_SOCKET_NONE);
@ -768,6 +843,15 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server,
return sc_server_connect_to_tcpip(server, ip_port); 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 static int
run_server(void *data) { run_server(void *data) {
struct sc_server *server = data; struct sc_server *server = data;
@ -779,7 +863,7 @@ run_server(void *data) {
// is parsed, so it is not output) // is parsed, so it is not output)
bool ok = sc_adb_start_server(&server->intr, 0); bool ok = sc_adb_start_server(&server->intr, 0);
if (!ok) { if (!ok) {
LOGE("Could not start adb daemon"); LOGE("Could not start adb server");
goto error_connection_failed; goto error_connection_failed;
} }
@ -860,7 +944,7 @@ run_server(void *data) {
// If --list-* is passed, then the server just prints the requested data // If --list-* is passed, then the server just prints the requested data
// then exits. // then exits.
if (params->list_encoders || params->list_displays) { if (params->list) {
sc_pid pid = execute_server(server, params); sc_pid pid = execute_server(server, params);
if (pid == SC_PROCESS_NONE) { if (pid == SC_PROCESS_NONE) {
goto error_connection_failed; goto error_connection_failed;
@ -930,8 +1014,11 @@ run_server(void *data) {
sc_mutex_unlock(&server->mutex); sc_mutex_unlock(&server->mutex);
// Interrupt sockets to wake up socket blocking calls on the server // Interrupt sockets to wake up socket blocking calls on the server
assert(server->video_socket != SC_SOCKET_NONE);
net_interrupt(server->video_socket); if (server->video_socket != SC_SOCKET_NONE) {
// There is no video_socket if --no-video is set
net_interrupt(server->video_socket);
}
if (server->audio_socket != SC_SOCKET_NONE) { if (server->audio_socket != SC_SOCKET_NONE) {
// There is no audio_socket if --no-audio is set // There is no audio_socket if --no-audio is set
@ -964,9 +1051,12 @@ run_server(void *data) {
sc_process_close(pid); sc_process_close(pid);
sc_server_kill_adb_if_requested(server);
return 0; return 0;
error_connection_failed: error_connection_failed:
sc_server_kill_adb_if_requested(server);
server->cbs->on_connection_failed(server, server->cbs_userdata); server->cbs->on_connection_failed(server, server->cbs_userdata);
return -1; return -1;
} }

View File

@ -26,11 +26,18 @@ struct sc_server_params {
enum sc_log_level log_level; enum sc_log_level log_level;
enum sc_codec video_codec; enum sc_codec video_codec;
enum sc_codec audio_codec; enum sc_codec audio_codec;
enum sc_video_source video_source;
enum sc_audio_source audio_source;
enum sc_camera_facing camera_facing;
const char *crop; const char *crop;
const char *video_codec_options; const char *video_codec_options;
const char *audio_codec_options; const char *audio_codec_options;
const char *video_encoder; const char *video_encoder;
const char *audio_encoder; const char *audio_encoder;
const char *camera_id;
const char *camera_size;
const char *camera_ar;
uint16_t camera_fps;
struct sc_port_range port_range; struct sc_port_range port_range;
uint32_t tunnel_host; uint32_t tunnel_host;
uint16_t tunnel_port; uint16_t tunnel_port;
@ -41,6 +48,7 @@ struct sc_server_params {
int8_t lock_video_orientation; int8_t lock_video_orientation;
bool control; bool control;
uint32_t display_id; uint32_t display_id;
bool video;
bool audio; bool audio;
bool show_touches; bool show_touches;
bool stay_awake; bool stay_awake;
@ -54,8 +62,9 @@ struct sc_server_params {
bool select_tcpip; bool select_tcpip;
bool cleanup; bool cleanup;
bool power_on; bool power_on;
bool list_encoders; bool kill_adb_on_close;
bool list_displays; bool camera_high_speed;
uint8_t list;
}; };
struct sc_server { struct sc_server {

View File

@ -27,7 +27,8 @@
// keyboard support, though OS could support more keys via modifying the report // keyboard support, though OS could support more keys via modifying the report
// desc. 6 should be enough for scrcpy. // desc. 6 should be enough for scrcpy.
#define HID_KEYBOARD_MAX_KEYS 6 #define HID_KEYBOARD_MAX_KEYS 6
#define HID_KEYBOARD_EVENT_SIZE (2 + HID_KEYBOARD_MAX_KEYS) #define HID_KEYBOARD_EVENT_SIZE \
(HID_KEYBOARD_INDEX_KEYS + HID_KEYBOARD_MAX_KEYS)
#define HID_RESERVED 0x00 #define HID_RESERVED 0x00
#define HID_ERROR_ROLL_OVER 0x01 #define HID_ERROR_ROLL_OVER 0x01

View File

@ -83,7 +83,7 @@ scrcpy_otg(struct scrcpy_options *options) {
#ifdef _WIN32 #ifdef _WIN32
// On Windows, only one process could open a USB device // On Windows, only one process could open a USB device
// <https://github.com/Genymobile/scrcpy/issues/2773> // <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; unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR;
// uninterruptible (intr == NULL), but in practice it's very quick // uninterruptible (intr == NULL), but in practice it's very quick
sc_adb_kill_server(NULL, flags); sc_adb_kill_server(NULL, flags);
@ -105,10 +105,6 @@ scrcpy_otg(struct scrcpy_options *options) {
usb_device_initialized = true; usb_device_initialized = true;
LOGI("USB device: %s (%04x:%04x) %s %s", usb_device.serial,
(unsigned) usb_device.vid, (unsigned) usb_device.pid,
usb_device.manufacturer, usb_device.product);
ok = sc_usb_connect(&s->usb, usb_device.device, &cbs, NULL); ok = sc_usb_connect(&s->usb, usb_device.device, &cbs, NULL);
if (!ok) { if (!ok) {
goto end; goto end;

View File

@ -93,7 +93,7 @@ sc_usb_device_move(struct sc_usb_device *dst, struct sc_usb_device *src) {
src->product = NULL; src->product = NULL;
} }
void static void
sc_usb_devices_destroy(struct sc_vec_usb_devices *usb_devices) { sc_usb_devices_destroy(struct sc_vec_usb_devices *usb_devices) {
for (size_t i = 0; i < usb_devices->size; ++i) { for (size_t i = 0; i < usb_devices->size; ++i) {
sc_usb_device_destroy(&usb_devices->data[i]); sc_usb_device_destroy(&usb_devices->data[i]);
@ -213,8 +213,8 @@ sc_usb_select_device(struct sc_usb *usb, const char *serial,
assert(sel_count == 1); // sel_idx is valid only if sel_count == 1 assert(sel_count == 1); // sel_idx is valid only if sel_count == 1
struct sc_usb_device *device = &vec.data[sel_idx]; struct sc_usb_device *device = &vec.data[sel_idx];
LOGD("USB device found:"); LOGI("USB device found:");
sc_usb_devices_log(SC_LOG_LEVEL_DEBUG, vec.data, vec.size); sc_usb_devices_log(SC_LOG_LEVEL_INFO, vec.data, vec.size);
// Move device into out_device (do not destroy device) // Move device into out_device (do not destroy device)
sc_usb_device_move(out_device, device); sc_usb_device_move(out_device, device);

View File

@ -147,7 +147,7 @@ sc_sdl_log_print(void *userdata, int category, SDL_LogPriority priority,
} }
void void
sc_log_configure() { sc_log_configure(void) {
SDL_LogSetOutputFunction(sc_sdl_log_print, NULL); SDL_LogSetOutputFunction(sc_sdl_log_print, NULL);
// Redirect FFmpeg logs to SDL logs // Redirect FFmpeg logs to SDL logs
av_log_set_callback(sc_av_log_callback); av_log_set_callback(sc_av_log_callback);

View File

@ -36,6 +36,6 @@ sc_log_windows_error(const char *prefix, int error);
#endif #endif
void void
sc_log_configure(); sc_log_configure(void);
#endif #endif

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

@ -190,10 +190,10 @@ sc_vecdeque_reallocdata_(void *ptr, size_t newcap, size_t item_size,
size_t right_len = MIN(size, oldcap - oldorigin); size_t right_len = MIN(size, oldcap - oldorigin);
assert(right_len); assert(right_len);
memcpy(newptr, ptr + (oldorigin * item_size), right_len * item_size); memcpy(newptr, (char *) ptr + (oldorigin * item_size), right_len * item_size);
if (size > right_len) { if (size > right_len) {
memcpy(newptr + (right_len * item_size), ptr, memcpy((char *) newptr + (right_len * item_size), ptr,
(size - right_len) * item_size); (size - right_len) * item_size);
} }

View File

@ -5,7 +5,7 @@
#include "util/bytebuf.h" #include "util/bytebuf.h"
void test_bytebuf_simple(void) { static void test_bytebuf_simple(void) {
struct sc_bytebuf buf; struct sc_bytebuf buf;
uint8_t data[20]; uint8_t data[20];
@ -34,7 +34,7 @@ void test_bytebuf_simple(void) {
sc_bytebuf_destroy(&buf); sc_bytebuf_destroy(&buf);
} }
void test_bytebuf_boundaries(void) { static void test_bytebuf_boundaries(void) {
struct sc_bytebuf buf; struct sc_bytebuf buf;
uint8_t data[20]; uint8_t data[20];
@ -71,7 +71,7 @@ void test_bytebuf_boundaries(void) {
sc_bytebuf_destroy(&buf); sc_bytebuf_destroy(&buf);
} }
void test_bytebuf_two_steps_write(void) { static void test_bytebuf_two_steps_write(void) {
struct sc_bytebuf buf; struct sc_bytebuf buf;
uint8_t data[20]; uint8_t data[20];

View File

@ -53,7 +53,7 @@ static void test_options(void) {
"--max-size", "1024", "--max-size", "1024",
"--lock-video-orientation=2", // optional arguments require '=' "--lock-video-orientation=2", // optional arguments require '='
// "--no-control" is not compatible with "--turn-screen-off" // "--no-control" is not compatible with "--turn-screen-off"
// "--no-display" is not compatible with "--fulscreen" // "--no-playback" is not compatible with "--fulscreen"
"--port", "1234:1236", "--port", "1234:1236",
"--push-target", "/sdcard/Movies", "--push-target", "/sdcard/Movies",
"--record", "file", "--record", "file",
@ -108,8 +108,8 @@ static void test_options2(void) {
char *argv[] = { char *argv[] = {
"scrcpy", "scrcpy",
"--no-control", "--no-control",
"--no-display", "--no-playback",
"--record", "file.mp4", // cannot enable --no-display without recording "--record", "file.mp4", // cannot enable --no-playback without recording
}; };
bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv); bool ok = scrcpy_parse_args(&args, ARRAY_LEN(argv), argv);
@ -117,7 +117,8 @@ static void test_options2(void) {
const struct scrcpy_options *opts = &args.opts; const struct scrcpy_options *opts = &args.opts;
assert(!opts->control); assert(!opts->control);
assert(!opts->display); assert(!opts->video_playback);
assert(!opts->audio_playback);
assert(!strcmp(opts->record_filename, "file.mp4")); assert(!strcmp(opts->record_filename, "file.mp4"));
assert(opts->record_format == SC_RECORD_FORMAT_MP4); assert(opts->record_format == SC_RECORD_FORMAT_MP4);
} }

View File

@ -0,0 +1,91 @@
#include "common.h"
#include <assert.h>
#include "options.h"
static void test_transforms(void) {
#define O(X) SC_ORIENTATION_ ## X
#define ASSERT_TRANSFORM(SRC, TR, RES) \
assert(sc_orientation_apply(O(SRC), O(TR)) == O(RES));
ASSERT_TRANSFORM(0, 0, 0);
ASSERT_TRANSFORM(0, 90, 90);
ASSERT_TRANSFORM(0, 180, 180);
ASSERT_TRANSFORM(0, 270, 270);
ASSERT_TRANSFORM(0, FLIP_0, FLIP_0);
ASSERT_TRANSFORM(0, FLIP_90, FLIP_90);
ASSERT_TRANSFORM(0, FLIP_180, FLIP_180);
ASSERT_TRANSFORM(0, FLIP_270, FLIP_270);
ASSERT_TRANSFORM(90, 0, 90);
ASSERT_TRANSFORM(90, 90, 180);
ASSERT_TRANSFORM(90, 180, 270);
ASSERT_TRANSFORM(90, 270, 0);
ASSERT_TRANSFORM(90, FLIP_0, FLIP_270);
ASSERT_TRANSFORM(90, FLIP_90, FLIP_0);
ASSERT_TRANSFORM(90, FLIP_180, FLIP_90);
ASSERT_TRANSFORM(90, FLIP_270, FLIP_180);
ASSERT_TRANSFORM(180, 0, 180);
ASSERT_TRANSFORM(180, 90, 270);
ASSERT_TRANSFORM(180, 180, 0);
ASSERT_TRANSFORM(180, 270, 90);
ASSERT_TRANSFORM(180, FLIP_0, FLIP_180);
ASSERT_TRANSFORM(180, FLIP_90, FLIP_270);
ASSERT_TRANSFORM(180, FLIP_180, FLIP_0);
ASSERT_TRANSFORM(180, FLIP_270, FLIP_90);
ASSERT_TRANSFORM(270, 0, 270);
ASSERT_TRANSFORM(270, 90, 0);
ASSERT_TRANSFORM(270, 180, 90);
ASSERT_TRANSFORM(270, 270, 180);
ASSERT_TRANSFORM(270, FLIP_0, FLIP_90);
ASSERT_TRANSFORM(270, FLIP_90, FLIP_180);
ASSERT_TRANSFORM(270, FLIP_180, FLIP_270);
ASSERT_TRANSFORM(270, FLIP_270, FLIP_0);
ASSERT_TRANSFORM(FLIP_0, 0, FLIP_0);
ASSERT_TRANSFORM(FLIP_0, 90, FLIP_90);
ASSERT_TRANSFORM(FLIP_0, 180, FLIP_180);
ASSERT_TRANSFORM(FLIP_0, 270, FLIP_270);
ASSERT_TRANSFORM(FLIP_0, FLIP_0, 0);
ASSERT_TRANSFORM(FLIP_0, FLIP_90, 90);
ASSERT_TRANSFORM(FLIP_0, FLIP_180, 180);
ASSERT_TRANSFORM(FLIP_0, FLIP_270, 270);
ASSERT_TRANSFORM(FLIP_90, 0, FLIP_90);
ASSERT_TRANSFORM(FLIP_90, 90, FLIP_180);
ASSERT_TRANSFORM(FLIP_90, 180, FLIP_270);
ASSERT_TRANSFORM(FLIP_90, 270, FLIP_0);
ASSERT_TRANSFORM(FLIP_90, FLIP_0, 270);
ASSERT_TRANSFORM(FLIP_90, FLIP_90, 0);
ASSERT_TRANSFORM(FLIP_90, FLIP_180, 90);
ASSERT_TRANSFORM(FLIP_90, FLIP_270, 180);
ASSERT_TRANSFORM(FLIP_180, 0, FLIP_180);
ASSERT_TRANSFORM(FLIP_180, 90, FLIP_270);
ASSERT_TRANSFORM(FLIP_180, 180, FLIP_0);
ASSERT_TRANSFORM(FLIP_180, 270, FLIP_90);
ASSERT_TRANSFORM(FLIP_180, FLIP_0, 180);
ASSERT_TRANSFORM(FLIP_180, FLIP_90, 270);
ASSERT_TRANSFORM(FLIP_180, FLIP_180, 0);
ASSERT_TRANSFORM(FLIP_180, FLIP_270, 90);
ASSERT_TRANSFORM(FLIP_270, 0, FLIP_270);
ASSERT_TRANSFORM(FLIP_270, 90, FLIP_0);
ASSERT_TRANSFORM(FLIP_270, 180, FLIP_90);
ASSERT_TRANSFORM(FLIP_270, 270, FLIP_180);
ASSERT_TRANSFORM(FLIP_270, FLIP_0, 90);
ASSERT_TRANSFORM(FLIP_270, FLIP_90, 180);
ASSERT_TRANSFORM(FLIP_270, FLIP_180, 270);
ASSERT_TRANSFORM(FLIP_270, FLIP_270, 0);
}
int main(int argc, char *argv[]) {
(void) argc;
(void) argv;
test_transforms();
return 0;
}

View File

@ -269,21 +269,25 @@ static void test_parse_integer_with_suffix(void) {
char buf[32]; char buf[32];
sprintf(buf, "%ldk", LONG_MAX / 2000); int r = snprintf(buf, sizeof(buf), "%ldk", LONG_MAX / 2000);
assert(r >= 0 && (size_t) r < sizeof(buf));
ok = sc_str_parse_integer_with_suffix(buf, &value); ok = sc_str_parse_integer_with_suffix(buf, &value);
assert(ok); assert(ok);
assert(value == LONG_MAX / 2000 * 1000); assert(value == LONG_MAX / 2000 * 1000);
sprintf(buf, "%ldm", LONG_MAX / 2000); r = snprintf(buf, sizeof(buf), "%ldm", LONG_MAX / 2000);
assert(r >= 0 && (size_t) r < sizeof(buf));
ok = sc_str_parse_integer_with_suffix(buf, &value); ok = sc_str_parse_integer_with_suffix(buf, &value);
assert(!ok); assert(!ok);
sprintf(buf, "%ldk", LONG_MIN / 2000); r = snprintf(buf, sizeof(buf), "%ldk", LONG_MIN / 2000);
assert(r >= 0 && (size_t) r < sizeof(buf));
ok = sc_str_parse_integer_with_suffix(buf, &value); ok = sc_str_parse_integer_with_suffix(buf, &value);
assert(ok); assert(ok);
assert(value == LONG_MIN / 2000 * 1000); assert(value == LONG_MIN / 2000 * 1000);
sprintf(buf, "%ldm", LONG_MIN / 2000); r = snprintf(buf, sizeof(buf), "%ldm", LONG_MIN / 2000);
assert(r >= 0 && (size_t) r < sizeof(buf));
ok = sc_str_parse_integer_with_suffix(buf, &value); ok = sc_str_parse_integer_with_suffix(buf, &value);
assert(!ok); assert(!ok);
} }
@ -358,7 +362,7 @@ static void test_index_of_column(void) {
assert(sc_str_index_of_column(" a bc d", 1, " ") == 2); assert(sc_str_index_of_column(" a bc d", 1, " ") == 2);
} }
static void test_remove_trailing_cr() { static void test_remove_trailing_cr(void) {
char s[] = "abc\r"; char s[] = "abc\r";
sc_str_remove_trailing_cr(s, sizeof(s) - 1); sc_str_remove_trailing_cr(s, sizeof(s) - 1);
assert(!strcmp(s, "abc")); assert(!strcmp(s, "abc"));

View File

@ -102,7 +102,7 @@ static void test_vecdeque_reserve(void) {
sc_vecdeque_destroy(&vdq); sc_vecdeque_destroy(&vdq);
} }
static void test_vecdeque_grow() { static void test_vecdeque_grow(void) {
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
bool ok = sc_vecdeque_reserve(&vdq, 20); bool ok = sc_vecdeque_reserve(&vdq, 20);
@ -142,7 +142,7 @@ static void test_vecdeque_grow() {
sc_vecdeque_destroy(&vdq); sc_vecdeque_destroy(&vdq);
} }
static void test_vecdeque_push_hole() { static void test_vecdeque_push_hole(void) {
struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER; struct SC_VECDEQUE(int) vdq = SC_VECDEQUE_INITIALIZER;
bool ok = sc_vecdeque_reserve(&vdq, 20); bool ok = sc_vecdeque_reserve(&vdq, 20);

View File

@ -187,7 +187,7 @@ static void test_vector_index_of(void) {
sc_vector_destroy(&vec); sc_vector_destroy(&vec);
} }
static void test_vector_grow() { static void test_vector_grow(void) {
struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER; struct SC_VECTOR(int) vec = SC_VECTOR_INITIALIZER;
bool ok; bool ok;

View File

@ -7,7 +7,7 @@ buildscript {
mavenCentral() mavenCentral()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:7.4.0' classpath 'com.android.tools.build:gradle:8.1.3'
// NOTE: Do not place your application dependencies here; they belong // NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files // in the individual module build.gradle files
@ -23,7 +23,3 @@ allprojects {
options.compilerArgs << "-Xlint:deprecation" options.compilerArgs << "-Xlint:deprecation"
} }
} }
task clean(type: Delete) {
delete rootProject.buildDir
}

View File

@ -2,7 +2,7 @@ apply plugin: 'checkstyle'
check.dependsOn 'checkstyle' check.dependsOn 'checkstyle'
checkstyle { checkstyle {
toolVersion = '9.0.1' toolVersion = '10.12.5'
} }
task checkstyle(type: Checkstyle) { task checkstyle(type: Checkstyle) {

View File

@ -6,7 +6,7 @@ c = 'i686-w64-mingw32-gcc'
cpp = 'i686-w64-mingw32-g++' cpp = 'i686-w64-mingw32-g++'
ar = 'i686-w64-mingw32-ar' ar = 'i686-w64-mingw32-ar'
strip = 'i686-w64-mingw32-strip' strip = 'i686-w64-mingw32-strip'
pkgconfig = 'i686-w64-mingw32-pkg-config' pkg-config = 'i686-w64-mingw32-pkg-config'
windres = 'i686-w64-mingw32-windres' windres = 'i686-w64-mingw32-windres'
[host_machine] [host_machine]
@ -14,8 +14,3 @@ system = 'windows'
cpu_family = 'x86' cpu_family = 'x86'
cpu = 'i686' cpu = 'i686'
endian = 'little' endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win32'
prebuilt_sdl2 = 'SDL2-2.26.4/i686-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'

View File

@ -6,7 +6,7 @@ c = 'x86_64-w64-mingw32-gcc'
cpp = 'x86_64-w64-mingw32-g++' cpp = 'x86_64-w64-mingw32-g++'
ar = 'x86_64-w64-mingw32-ar' ar = 'x86_64-w64-mingw32-ar'
strip = 'x86_64-w64-mingw32-strip' strip = 'x86_64-w64-mingw32-strip'
pkgconfig = 'x86_64-w64-mingw32-pkg-config' pkg-config = 'x86_64-w64-mingw32-pkg-config'
windres = 'x86_64-w64-mingw32-windres' windres = 'x86_64-w64-mingw32-windres'
[host_machine] [host_machine]
@ -14,8 +14,3 @@ system = 'windows'
cpu_family = 'x86' cpu_family = 'x86'
cpu = 'x86_64' cpu = 'x86_64'
endian = 'little' endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-2/win64'
prebuilt_sdl2 = 'SDL2-2.26.4/x86_64-w64-mingw32'
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'

View File

@ -24,17 +24,75 @@ To disable audio:
scrcpy --no-audio scrcpy --no-audio
``` ```
To disable only the audio playback, see [no playback](video.md#no-playback).
## Audio only
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
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 ## Codec
The audio codec can be selected. The possible values are `opus` (default), `aac` The audio codec can be selected. The possible values are `opus` (default),
and `raw` (uncompressed PCM 16-bit LE): `aac`, `flac` and `raw` (uncompressed PCM 16-bit LE):
```bash ```bash
scrcpy --audio-codec=opus # default scrcpy --audio-codec=opus # default
scrcpy --audio-codec=aac scrcpy --audio-codec=aac
scrcpy --audio-codec=flac
scrcpy --audio-codec=raw scrcpy --audio-codec=raw
``` ```
In particular, if you get the following error:
> Failed to initialize audio/opus, error 0xfffffffe
then your device has no Opus encoder: try `scrcpy --audio-codec=aac`.
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
check `--audio-codec-options` in the manpage or in `scrcpy --help`.
For example, to change the [FLAC compression level]:
```bash
scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8
```
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL
## Encoder
Several encoders may be available on the device. They can be listed by: Several encoders may be available on the device. They can be listed by:
```bash ```bash
@ -43,19 +101,14 @@ scrcpy --list-encoders
To select a specific encoder: To select a specific encoder:
``` ```bash
scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder' scrcpy --audio-codec=opus --audio-encoder='c2.android.opus.encoder'
``` ```
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
check `--audio-codec-options` in the manpage or in `scrcpy --help`.
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
## Bit rate ## Bit rate
The default video bit-rate is 128Kbps. To change it: The default audio bit rate is 128Kbps. To change it:
```bash ```bash
scrcpy --audio-bit-rate=64K scrcpy --audio-bit-rate=64K

View File

@ -58,7 +58,7 @@ sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \
libswresample-dev libusb-1.0-0-dev libswresample-dev libusb-1.0-0-dev
# server build dependencies # server build dependencies
sudo apt install openjdk-11-jdk sudo apt install openjdk-17-jdk
``` ```
On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install
@ -77,7 +77,7 @@ pip3 install meson
sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm
# client build dependencies # client build dependencies
sudo dnf install SDL2-devel ffms2-devel libusb-devel meson gcc make sudo dnf install SDL2-devel ffms2-devel libusb1-devel meson gcc make
# server build dependencies # server build dependencies
sudo dnf install java-devel sudo dnf install java-devel
@ -100,7 +100,7 @@ sudo apt install mingw-w64 mingw-w64-tools
You also need the JDK to build the server: You also need the JDK to build the server:
```bash ```bash
sudo apt install openjdk-11-jdk sudo apt install openjdk-17-jdk
``` ```
Then generate the releases: Then generate the releases:
@ -168,13 +168,13 @@ brew install sdl2 ffmpeg libusb
brew install pkg-config meson brew install pkg-config meson
``` ```
Additionally, if you want to build the server, install Java 8 from Caskroom, and Additionally, if you want to build the server, install Java 17 from Caskroom, and
make it available from the `PATH`: make it available from the `PATH`:
```bash ```bash
brew tap homebrew/cask-versions brew tap homebrew/cask-versions
brew install adoptopenjdk/openjdk/adoptopenjdk11 brew install adoptopenjdk/openjdk/adoptopenjdk17
export JAVA_HOME="$(/usr/libexec/java_home --version 1.11)" export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)"
export PATH="$JAVA_HOME/bin:$PATH" export PATH="$JAVA_HOME/bin:$PATH"
``` ```
@ -233,10 +233,10 @@ install` must be run as root)._
#### Option 2: Use prebuilt server #### Option 2: Use prebuilt server
- [`scrcpy-server-v2.0`][direct-scrcpy-server] - [`scrcpy-server-v2.3.1`][direct-scrcpy-server]
<sub>SHA-256: `9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2`</sub> <sub>SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b`</sub>
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0 [direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1
Download the prebuilt server somewhere, and specify its path during the Meson Download the prebuilt server somewhere, and specify its path during the Meson
configuration: configuration:

171
doc/camera.md Normal file
View File

@ -0,0 +1,171 @@
# Camera
Camera mirroring is supported for devices with Android 12 or higher.
To capture the camera instead of the device screen:
```
scrcpy --video-source=camera
```
By default, it automatically switches [audio source](audio.md#source) to
microphone (as if `--audio-source=mic` were also passed).
```bash
scrcpy --video-source=display # default is --audio-source=output
scrcpy --video-source=camera # default is --audio-source=mic
scrcpy --video-source=display --audio-source=mic # force display AND microphone
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output
```
Audio can be disabled:
```bash
# audio not captured at all
scrcpy --video-source=camera --no-audio
scrcpy --video-source=camera --no-audio --record=file.mp4
# audio captured and recorded, but not played
scrcpy --video-source=camera --no-audio-playback --record=file.mp4
```
## List
To list the cameras available (with their declared valid sizes and frame rates):
```
scrcpy --list-cameras
scrcpy --list-camera-sizes
```
_Note that the sizes and frame rates are declarative. They are not accurate on
all devices: some of them are declared but not supported, while some others are
not declared but supported._
## Selection
It is possible to pass an explicit camera id (as listed by `--list-cameras`):
```
scrcpy --video-source=camera --camera-id=0
```
Alternatively, the camera may be selected automatically:
```bash
scrcpy --video-source=camera # use the first camera
scrcpy --video-source=camera --camera-facing=front # use the first front camera
scrcpy --video-source=camera --camera-facing=back # use the first back camera
scrcpy --video-source=camera --camera-facing=external # use the first external camera
```
If `--camera-id` is specified, then `--camera-facing` is forbidden (the id
already determines the camera):
```bash
scrcpy --video-source=camera --camera-id=0 --camera-facing=front # error
```
### Size selection
It is possible to pass an explicit camera size:
```
scrcpy --video-source=camera --camera-size=1920x1080
```
The given size may be listed among the declared valid sizes
(`--list-camera-sizes`), but may also be anything else (some devices support
arbitrary sizes):
```
scrcpy --video-source=camera --camera-size=1840x444
```
Alternatively, a declared valid size (among the ones listed by
`list-camera-sizes`) may be selected automatically.
Two constraints are supported:
- `-m`/`--max-size` (already used for display mirroring), for example `-m1920`;
- `--camera-ar` to specify an aspect ratio (`<num>:<den>`, `<value>` or
`sensor`).
Some examples:
```bash
scrcpy --video-source=camera # use the greatest width and the greatest associated height
scrcpy --video-source=camera -m1920 # use the greatest width not above 1920 and the greatest associated height
scrcpy --video-source=camera --camera-ar=4:3 # use the greatest size with an aspect ratio of 4:3 (+/- 10%)
scrcpy --video-source=camera --camera-ar=1.6 # use the greatest size with an aspect ratio of 1.6 (+/- 10%)
scrcpy --video-source=camera --camera-ar=sensor # use the greatest size with the aspect ratio of the camera sensor (+/- 10%)
scrcpy --video-source=camera -m1920 --camera-ar=16:9 # use the greatest width not above 1920 and the closest to 16:9 aspect ratio
```
If `--camera-size` is specified, then `-m`/`--max-size` and `--camera-ar` are
forbidden (the size is determined by the value given explicitly):
```bash
scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error
```
## Rotation
To rotate the captured video, use the [video orientation](video.md#orientation)
option:
```
scrcpy --video-source=camera --camera-size=1920x1080 --orientation=90
```
## Frame rate
By default, camera is captured at Android's default frame rate (30 fps).
To configure a different frame rate:
```
scrcpy --video-source=camera --camera-fps=60
```
## High speed capture
The Android camera API also supports a [high speed capture mode][high speed].
This mode is restricted to specific resolutions and frame rates, listed by
`--list-camera-sizes`.
```
scrcpy --video-source=camera --camera-size=1920x1080 --camera-fps=240
```
[high speed]: https://developer.android.com/reference/android/hardware/camera2/CameraConstrainedHighSpeedCaptureSession
## Brace expansion tip
All camera options start with `--camera-`, so if your shell supports it, you can
benefit from [brace expansion] (for example, it is supported _bash_ and _zsh_):
```bash
scrcpy --video-source=camera --camera-{facing=back,ar=16:9,high-speed,fps=120}
```
This will be expanded as:
```bash
scrcpy --video-source=camera --camera-facing=back --camera-ar=16:9 --camera-high-speed --camera-fps=120
```
[brace expansion]: https://www.gnu.org/software/bash/manual/html_node/Brace-Expansion.html
## Webcam
Combined with the [V4L2](v4l2.md) feature on Linux, the Android device camera
may be used as a webcam on the computer.

125
doc/connection.md Normal file
View File

@ -0,0 +1,125 @@
# Connection
## Selection
If exactly one device is connected (i.e. listed by `adb devices`), then it is
automatically selected.
However, if there are multiple devices connected, you must specify the one to
use in one of 4 ways:
- by its serial:
```bash
scrcpy --serial=0123456789abcdef
scrcpy -s 0123456789abcdef # short version
# the serial is the ip:port if connected over TCP/IP (same behavior as adb)
scrcpy --serial=192.168.1.1:5555
```
- the one connected over USB (if there is exactly one):
```bash
scrcpy --select-usb
scrcpy -d # short version
```
- the one connected over TCP/IP (if there is exactly one):
```bash
scrcpy --select-tcpip
scrcpy -e # short version
```
- a device already listening on TCP/IP (see [below](#tcpip-wireless)):
```bash
scrcpy --tcpip=192.168.1.1:5555
scrcpy --tcpip=192.168.1.1 # default port is 5555
```
The serial may also be provided via the environment variable `ANDROID_SERIAL`
(also used by `adb`):
```bash
# in bash
export ANDROID_SERIAL=0123456789abcdef
scrcpy
```
```cmd
:: in cmd
set ANDROID_SERIAL=0123456789abcdef
scrcpy
```
```powershell
# in PowerShell
$env:ANDROID_SERIAL = '0123456789abcdef'
scrcpy
```
## TCP/IP (wireless)
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP. The device must be connected on the same network as the
computer.
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
### Automatic
An option `--tcpip` allows to configure the connection automatically. There are
two variants.
If the device (accessible at 192.168.1.1 in this example) already listens on a
port (typically 5555) for incoming _adb_ connections, then run:
```bash
scrcpy --tcpip=192.168.1.1 # default port is 5555
scrcpy --tcpip=192.168.1.1:5555
```
If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP
address), connect the device over USB, then run:
```bash
scrcpy --tcpip # without arguments
```
It will automatically find the device IP address and adb port, enable TCP/IP
mode if necessary, then connect to the device before starting.
### Manual
Alternatively, it is possible to enable the TCP/IP connection manually using
`adb`:
1. Plug the device into a USB port on your computer.
2. Connect the device to the same Wi-Fi network as your computer.
3. Get your device IP address, in Settings → About phone → Status, or by
executing this command:
```bash
adb shell ip route | awk '{print $9}'
```
4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`.
5. Unplug your device.
6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`
with the device IP address you found)_.
7. Run `scrcpy` as usual.
8. Run `adb disconnect` once you're done.
Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass
having to physically connect your device directly to your computer.
[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line
## Autostart
A small tool (by the scrcpy author) allows to run arbitrary commands whenever a
new Android device is connected: [AutoAdb]. It can be used to start scrcpy:
```bash
autoadb scrcpy -s '{}'
```
[AutoAdb]: https://github.com/rom1v/autoadb

View File

@ -9,16 +9,52 @@ This application is composed of two parts:
The client is responsible to push the server to the device and start its The client is responsible to push the server to the device and start its
execution. execution.
Once the client and the server are connected to each other, the server initially The client and the server establish communication using separate sockets for
sends device information (name and initial screen dimensions), then starts to video, audio and controls. Any of them may be disabled (but not all), so
send a raw H.264 video stream of the device screen. The client decodes the video there are 1, 2 or 3 socket(s).
frames, and display them as soon as possible, without buffering, to minimize
latency. The client is not aware of the device rotation (which is handled by the
server), it just knows the dimensions of the video frames.
The client captures relevant keyboard and mouse events, that it transmits to the The server initially sends the device name on the first socket (it is used for
server, which injects them to the device. the scrcpy window title), then each socket is used for its own purpose. All
reads and writes are performed from a dedicated thread for each socket, both on
the client and on the server.
If video is enabled, then the server sends a raw video stream (H.264 by default)
of the device screen, with some additional headers for each packet. The client
decodes the video frames, and displays them as soon as possible, without
buffering (unless `--display-buffer=delay` is specified) to minimize latency.
The client is not aware of the device rotation (which is handled by the server),
it just knows the dimensions of the video frames it receives.
Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS
by default) of the device audio output (or the microphone if
`--audio-source=mic` is specified), with some additional headers for each
packet. The client decodes the stream, attempts to keep a minimal latency by
maintaining an average buffering. The [blog post][scrcpy2] of the scrcpy v2.0
release gives more details about the audio feature.
If control is enabled, then the client captures relevant keyboard and mouse
events, that it transmits to the server, which injects them to the device. This
is the only socket which is used in both direction: input events are sent from
the client to the device, and when the device clipboard changes, the new content
is sent from the device to the client to support seamless copy-paste.
[scrcpy2]: https://blog.rom1v.com/2023/03/scrcpy-2-0-with-audio/
Note that the client-server roles are expressed at the application level:
- the server _serves_ video and audio streams, and handle requests from the
client,
- the client _controls_ the device through the server.
However, by default (when `--force-adb-forward` is not set), the roles are
reversed at the network level:
- the client opens a server socket and listen on a port before starting the
server,
- the server connects to the client.
This role inversion guarantees that the connection will not fail due to race
conditions without polling.
## Server ## Server
@ -32,15 +68,14 @@ The server is a Java application (with a [`public static void main(String...
args)`][main] method), compiled against the Android framework, and executed as args)`][main] method), compiled against the Android framework, and executed as
`shell` on the Android device. `shell` on the Android device.
[main]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Server.java#L123 [main]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Server.java#L193
To run such a Java application, the classes must be [_dexed_][dex] (typically, To run such a Java application, the classes must be [_dexed_][dex] (typically,
to `classes.dex`). If `my.package.MainClass` is the main class, compiled to to `classes.dex`). If `my.package.MainClass` is the main class, compiled to
`classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run `classes.dex`, pushed to the device in `/data/local/tmp`, then it can be run
with: with:
adb shell CLASSPATH=/data/local/tmp/classes.dex \ adb shell CLASSPATH=/data/local/tmp/classes.dex app_process / my.package.MainClass
app_process / my.package.MainClass
_The path `/data/local/tmp` is a good candidate to push the server, since it's _The path `/data/local/tmp` is a good candidate to push the server, since it's
readable and writable by `shell`, but not world-writable, so a malicious readable and writable by `shell`, but not world-writable, so a malicious
@ -49,7 +84,7 @@ application may not replace the server just before the client executes it._
Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing Instead of a raw _dex_ file, `app_process` accepts a _jar_ containing
`classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle `classes.dex` (e.g. an [APK]). For simplicity, and to benefit from the gradle
build system, the server is built to an (unsigned) APK (renamed to build system, the server is built to an (unsigned) APK (renamed to
`scrcpy-server`). `scrcpy-server.jar`).
[dex]: https://en.wikipedia.org/wiki/Dalvik_(software) [dex]: https://en.wikipedia.org/wiki/Dalvik_(software)
[apk]: https://en.wikipedia.org/wiki/Android_application_package [apk]: https://en.wikipedia.org/wiki/Android_application_package
@ -65,42 +100,77 @@ They can be called using reflection though. The communication with hidden
components is provided by [_wrappers_ classes][wrappers] and [aidl]. components is provided by [_wrappers_ classes][wrappers] and [aidl].
[hidden]: https://stackoverflow.com/a/31908373/1987178 [hidden]: https://stackoverflow.com/a/31908373/1987178
[wrappers]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers [wrappers]: https://github.com/Genymobile/scrcpy/tree/master/server/src/main/java/com/genymobile/scrcpy/wrappers
[aidl]: https://github.com/Genymobile/scrcpy/tree/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/aidl/android/view [aidl]: https://github.com/Genymobile/scrcpy/tree/master/server/src/main/aidl
### Threading
The server uses 3 threads: ### Execution
- the **main** thread, encoding and streaming the video to the client; The server is started by the client basically by executing the following
- the **controller** thread, listening for _control messages_ (typically, commands:
keyboard and mouse events) from the client;
- the **receiver** thread (managed by the controller), sending _device messages_
to the clients (currently, it is only used to send the device clipboard
content).
Since the video encoding is typically hardware, there would be no benefit in ```bash
encoding and streaming in two different threads. adb push scrcpy-server /data/local/tmp/scrcpy-server.jar
adb forward tcp:27183 localabstract:scrcpy
adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1
```
The first argument (`2.1` in the example) is the client scrcpy version. The
server fails if the client and the server do not have the exact same version.
The protocol between the client and the server may change from version to
version (see [protocol](#protocol) below), and there is no backward or forward
compatibility (there is no point to use different client and server versions).
This check allows to detect misconfiguration (running an older or newer server
by mistake).
It is followed by any number of arguments, in the form of `key=value` pairs.
Their order is irrelevant. The possible keys and associated value types can be
found in the [server][server-options] and [client][client-options] code.
[server-options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L181
[client-options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/server.c#L226
For example, if we execute `scrcpy -m1920 --no-audio`, then the server
execution will look like this:
```bash
# scid is a random number to identify different clients running on the same device
adb shell CLASSPATH=/data/local/tmp/scrcpy-server.jar app_process / com.genymobile.scrcpy.Server 2.1 scid=12345678 log_level=info audio=false max_size=1920
```
### Components
When executed, its [`main()`][main] method is executed (on the "main" thread).
It parses the arguments, establishes the connection with the client and starts
the other "components":
- the **video** streamer: it captures the video screen and send encoded video
packets on the _video_ socket (from the _video_ thread).
- the **audio** streamer: it uses several threads to capture raw packets,
submits them to encoding and retrieve encoded packets, which it sends on the
_audio_ socket.
- the **controller**: it receives _control messages_ (typically input events)
on the _control_ socket from one thread, and sends _device messages_ (e.g. to
transmit the device clipboard content to the client) on the same _control
socket_ from another thread. Thus, the _control_ socket is used in both
directions (contrary to the _video_ and _audio_ sockets).
### Screen video encoding ### Screen video encoding
The encoding is managed by [`ScreenEncoder`]. The encoding is managed by [`ScreenEncoder`].
The video is encoded using the [`MediaCodec`] API. The codec takes its input The video is encoded using the [`MediaCodec`] API. The codec encodes the content
from a [surface] associated to the display, and writes the resulting H.264 of a `Surface` associated to the display, and writes the encoding packets to the
stream to the provided output stream (the socket connected to the client). client (on the _video_ socket).
[`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java [`ScreenEncoder`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java
[`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html [`MediaCodec`]: https://developer.android.com/reference/android/media/MediaCodec.html
[surface]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L68-L69
On device [rotation], the codec, surface and display are reinitialized, and a On device rotation (or folding), the encoding session is [reset] and restarted.
new video stream is produced.
New frames are produced only when changes occur on the surface. This is good New frames are produced only when changes occur on the surface. This avoids to
because it avoids to send unnecessary frames, but there are drawbacks: send unnecessary frames, but by default there might be drawbacks:
- it does not send any frame on start if the device screen does not change, - it does not send any frame on start if the device screen does not change,
- after fast motion changes, the last frame may have poor quality. - after fast motion changes, the last frame may have poor quality.
@ -108,11 +178,24 @@ because it avoids to send unnecessary frames, but there are drawbacks:
Both problems are [solved][repeat] by the flag Both problems are [solved][repeat] by the flag
[`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag]. [`KEY_REPEAT_PREVIOUS_FRAME_AFTER`][repeat-flag].
[reset]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L179
[rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90 [rotation]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L90
[repeat]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L147-L148 [repeat]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/ScreenEncoder.java#L246-L247
[repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER [repeat-flag]: https://developer.android.com/reference/android/media/MediaFormat.html#KEY_REPEAT_PREVIOUS_FRAME_AFTER
### Audio encoding
Similarly, the audio is [captured] using an [`AudioRecord`], and [encoded] using
the [`MediaCodec`] asynchronous API.
More details are available on the [blog post][scrcpy2] introducing the audio feature.
[captured]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
[encoded]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java
[`AudioRecord`]: https://developer.android.com/reference/android/media/AudioRecord
### Input events injection ### Input events injection
_Control messages_ are received from the client by the [`Controller`] (run in a _Control messages_ are received from the client by the [`Controller`] (run in a
@ -124,13 +207,13 @@ separate thread). There are several types of input events:
- other commands (e.g. to switch the screen on or to copy the clipboard). - other commands (e.g. to switch the screen on or to copy the clipboard).
Some of them need to inject input events to the system. To do so, they use the Some of them need to inject input events to the system. To do so, they use the
_hidden_ method [`InputManager.injectInputEvent`] (exposed by our _hidden_ method [`InputManager.injectInputEvent()`] (exposed by the
[`InputManager` wrapper][inject-wrapper]). [`InputManager` wrapper][inject-wrapper]).
[`Controller`]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/Controller.java#L81 [`Controller`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Controller.java
[`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html [`KeyEvent`]: https://developer.android.com/reference/android/view/KeyEvent.html
[`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html [`MotionEvent`]: https://developer.android.com/reference/android/view/MotionEvent.html
[`InputManager.injectInputEvent`]: https://android.googlesource.com/platform/frameworks/base/+/oreo-release/core/java/android/hardware/input/InputManager.java#857 [`InputManager.injectInputEvent()`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L34
[inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27 [inject-wrapper]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java#L27
@ -140,126 +223,222 @@ _hidden_ method [`InputManager.injectInputEvent`] (exposed by our
The client relies on [SDL], which provides cross-platform API for UI, input The client relies on [SDL], which provides cross-platform API for UI, input
events, threading, etc. events, threading, etc.
The video stream is decoded by [libav] (FFmpeg). The video and audio streams are decoded by [FFmpeg].
[SDL]: https://www.libsdl.org [SDL]: https://www.libsdl.org
[libav]: https://www.libav.org/ [ffmpeg]: https://ffmpeg.org/
### Initialization ### Initialization
On startup, in addition to _libav_ and _SDL_ initialization, the client must The client parses the command line arguments, then [runs one of two code
push and start the server on the device, and open two sockets (one for the video paths][run]:
stream, one for control) so that they may communicate. - scrcpy in "normal" mode ([`scrcpy.c`])
- scrcpy in [OTG mode](hid-otg.md) ([`scrcpy_otg.c`])
Note that the client-server roles are expressed at the application level: [run]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/main.c#L81-L82
[`scrcpy.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/scrcpy.c#L292-L293
[`scrcpy_otg.c`]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/usb/scrcpy_otg.c#L51-L52
- the server _serves_ video stream and handle requests from the client, In the remaining of this document, we assume that the "normal" mode is used
- the client _controls_ the device through the server. (read the code for the OTG mode).
However, the roles are reversed at the network level: On startup, the client:
- opens the _video_, _audio_ and _control_ sockets;
- the client opens a server socket and listen on a port before starting the - pushes and starts the server on the device;
server, - initializes its components (demuxers, decoders, recorder…).
- the server connects to the client.
This role inversion guarantees that the connection will not fail due to race
conditions, and avoids polling.
_(Note that over TCP/IP, the roles are not reversed, due to a bug in `adb
reverse`. See commit [1038bad] and [issue #5].)_
Once the server is connected, it sends the device information (name and initial
screen dimensions). Thus, the client may init the window and renderer, before
the first frame is available.
To minimize startup time, SDL initialization is performed while listening for
the connection from the server (see commit [90a46b4]).
[1038bad]: https://github.com/Genymobile/scrcpy/commit/1038bad3850f18717a048a4d5c0f8110e54ee172
[issue #5]: https://github.com/Genymobile/scrcpy/issues/5
[90a46b4]: https://github.com/Genymobile/scrcpy/commit/90a46b4c45637d083e877020d85ade52a9a5fa8e
### Threading ### Video and audio streams
The client uses 4 threads: Depending on the arguments passed to `scrcpy`, several components may be used.
Here is an overview of the video and audio components:
- the **main** thread, executing the SDL event loop,
- the **stream** thread, receiving the video and used for decoding and
recording,
- the **controller** thread, sending _control messages_ to the server,
- the **receiver** thread (managed by the controller), receiving _device
messages_ from the server.
In addition, another thread can be started if necessary to handle APK
installation or file push requests (via drag&drop on the main window) or to
print the framerate regularly in the console.
### Stream
The video [stream] is received from the socket (connected to the server on the
device) in a separate thread.
If a [decoder] is present (i.e. `--no-display` is not set), then it uses _libav_
to decode the H.264 stream from the socket, and notifies the main thread when a
new frame is available.
There are two [frames][video_buffer] simultaneously in memory:
- the **decoding** frame, written by the decoder from the decoder thread,
- the **rendering** frame, rendered in a texture from the main thread.
When a new decoded frame is available, the decoder _swaps_ the decoding and
rendering frame (with proper synchronization). Thus, it immediately starts
to decode a new frame while the main thread renders the last one.
If a [recorder] is present (i.e. `--record` is enabled), then it muxes the raw
H.264 packet to the output video file.
[stream]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/stream.h
[decoder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/decoder.h
[video_buffer]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/video_buffer.h
[recorder]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/recorder.h
``` ```
+----------+ +----------+ V4L2 sink
---> | decoder | ---> | screen | /
+---------+ / +----------+ +----------+ decoder
socket ---> | stream | ---- / \
+---------+ \ +----------+ VIDEO -------------> demuxer display
---> | recorder | \
+----------+ recorder
/
AUDIO -------------> demuxer
\
decoder --- audio player
``` ```
The _demuxer_ is responsible to extract video and audio packets (read some
header, split the video stream into packets at correct boundaries, etc.).
The demuxed packets may be sent to a _decoder_ (one per stream, to produce
frames) and to a recorder (receiving both video and audio stream to record a
single file). The packets are encoded on the device (by `MediaCodec`), but when
recording, they are _muxed_ (asynchronously) into a container (MKV or MP4) on
the client side.
Video frames are sent to the screen/display to be rendered in the scrcpy window.
They may also be sent to a [V4L2 sink](v4l2.md).
Audio "frames" (an array of decoded samples) are sent to the audio player.
### Controller ### Controller
The [controller] is responsible to send _control messages_ to the device. It The _controller_ is responsible to send _control messages_ to the device. It
runs in a separate thread, to avoid I/O on the main thread. runs in a separate thread, to avoid I/O on the main thread.
On SDL event, received on the main thread, the [input manager][inputmanager] On SDL event, received on the main thread, the _input manager_ creates
creates appropriate [_control messages_][controlmsg]. It is responsible to appropriate _control messages_. It is responsible to convert SDL events to
convert SDL events to Android events (using [convert]). It pushes the _control Android events. It then pushes the _control messages_ to a queue hold by the
messages_ to a queue hold by the controller. On its own thread, the controller controller. On its own thread, the controller takes messages from the queue,
takes messages from the queue, that it serializes and sends to the client. that it serializes and sends to the client.
[controller]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/controller.h
[controlmsg]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/control_msg.h
[inputmanager]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/input_manager.h
[convert]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/convert.h
### UI and event loop ## Protocol
Initialization, input events and rendering are all [managed][scrcpy] in the main The protocol between the client and the server must be considered _internal_: it
thread. may (and will) change at any time for any reason. Everything may change (the
number of sockets, the order in which the sockets must be opened, the data
format on the wire…) from version to version. A client must always be run with a
matching server version.
Events are handled in the [event loop], which either updates the [screen] or This section documents the current protocol in scrcpy v2.1.
delegates to the [input manager][inputmanager].
[scrcpy]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c ### Connection
[event loop]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/scrcpy.c#L201
[screen]: https://github.com/Genymobile/scrcpy/blob/ffe0417228fb78ab45b7ee4e202fc06fc8875bf3/app/src/screen.h Firstly, the client sets up an adb tunnel:
```bash
# By default, a reverse redirection: the computer listens, the device connects
adb reverse localabstract:scrcpy_<SCID> tcp:27183
# As a fallback (or if --force-adb forward is set), a forward redirection:
# the device listens, the computer connects
adb forward tcp:27183 localabstract:scrcpy_<SCID>
```
(`<SCID>` is a 31-bit random number, so that it does not fail when several
scrcpy instances start "at the same time" for the same device.)
Then, up to 3 sockets are opened, in that order:
- a _video_ socket
- an _audio_ socket
- a _control_ socket
Each one may be disabled (respectively by `--no-video`, `--no-audio` and
`--no-control`, directly or indirectly). For example, if `--no-audio` is set,
then the _video_ socket is opened first, then the _control_ socket.
On the _first_ socket opened (whichever it is), if the tunnel is _forward_, then
a [dummy byte] is sent from the device to the client. This allows to detect a
connection error (the client connection does not fail as long as there is an adb
forward redirection, even if nothing is listening on the device side).
Still on this _first_ socket, the device sends some [metadata][device meta] to
the client (currently only the device name, used as the window title, but there
might be other fields in the future).
[dummy byte]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L93
[device meta]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L151
You can read the [client][client-connection] and [server][server-connection]
code for more details.
[client-connection]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/app/src/server.c#L465-L466
[server-connection]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/DesktopConnection.java#L63
Then each socket is used for its intended purpose.
### Video and audio
On the _video_ and _audio_ sockets, the device first sends some [codec
metadata]:
- On the _video_ socket, 12 bytes:
- the codec id (`u32`) (H264, H265 or AV1)
- the initial video width (`u32`)
- the initial video height (`u32`)
- On the _audio_ socket, 4 bytes:
- the codec id (`u32`) (OPUS, AAC or RAW)
[codec metadata]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Streamer.java#L33-L51
Then each packet produced by `MediaCodec` is sent, prefixed by a 12-byte [frame
header]:
- config packet flag (`u1`)
- key frame flag (`u1`)
- PTS (`u62`)
- packet size (`u32`)
Here is a schema describing the frame header:
```
[. . . . . . . .|. . . .]. . . . . . . . . . . . . . . ...
<-------------> <-----> <-----------------------------...
PTS packet raw packet
size
<--------------------->
frame header
The most significant bits of the PTS are used for packet flags:
byte 7 byte 6 byte 5 byte 4 byte 3 byte 2 byte 1 byte 0
CK...... ........ ........ ........ ........ ........ ........ ........
^^<------------------------------------------------------------------->
|| PTS
| `- key frame
`-- config packet
```
[frame header]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Streamer.java#L83
### Controls
Controls messages are sent via a custom binary protocol.
The only documentation for this protocol is the set of unit tests on both sides:
- `ControlMessage` (from client to device): [serialization](https://github.com/Genymobile/scrcpy/blob/master/app/tests/test_control_msg_serialize.c) | [deserialization](https://github.com/Genymobile/scrcpy/blob/master/server/src/test/java/com/genymobile/scrcpy/ControlMessageReaderTest.java)
- `DeviceMessage` (from device to client) [serialization](https://github.com/Genymobile/scrcpy/blob/master/server/src/test/java/com/genymobile/scrcpy/DeviceMessageWriterTest.java) | [deserialization](https://github.com/Genymobile/scrcpy/blob/master/app/tests/test_device_msg_deserialize.c)
## Standalone server
Although the server is designed to work for the scrcpy client, it can be used
with any client which uses the same protocol.
For simplicity, some [server-specific options] have been added to produce raw
streams easily:
- `send_device_meta=false`: disable the device metata (in practice, the device
name) sent on the _first_ socket
- `send_frame_meta=false`: disable the 12-byte header for each packet
- `send_dummy_byte`: disable the dummy byte sent on forward connections
- `send_codec_meta`: disable the codec information (and initial device size for
video)
- `raw_stream`: disable all the above
[server-specific options]: https://github.com/Genymobile/scrcpy/blob/a3cdf1a6b86ea22786e1f7d09b9c202feabc6949/server/src/main/java/com/genymobile/scrcpy/Options.java#L309-L329
Concretely, here is how to expose a raw H.264 stream on a TCP socket:
```bash
adb push scrcpy-server-v2.1 /data/local/tmp/scrcpy-server-manual.jar
adb forward tcp:1234 localabstract:scrcpy
adb shell CLASSPATH=/data/local/tmp/scrcpy-server-manual.jar \
app_process / com.genymobile.scrcpy.Server 2.1 \
tunnel_forward=true audio=false control=false cleanup=false \
raw_stream=true max_size=1920
```
As soon as a client connects over TCP on port 1234, the device will start
streaming the video. For example, VLC can play the video (although you will
experience a very high latency, more details [here][vlc-0latency]):
```
vlc -Idummy --demux=h264 --network-caching=0 tcp://localhost:1234
```
[vlc-0latency]: https://code.videolan.org/rom1v/vlc/-/merge_requests/20
## Hack ## Hack

View File

@ -1,156 +1,9 @@
# Device # Device
## Selection
If exactly one device is connected (i.e. listed by `adb devices`), then it is
automatically selected.
However, if there are multiple devices connected, you must specify the one to
use in one of 4 ways:
- by its serial:
```bash
scrcpy --serial=0123456789abcdef
scrcpy -s 0123456789abcdef # short version
# the serial is the ip:port if connected over TCP/IP (same behavior as adb)
scrcpy --serial=192.168.1.1:5555
```
- the one connected over USB (if there is exactly one):
```bash
scrcpy --select-usb
scrcpy -d # short version
```
- the one connected over TCP/IP (if there is exactly one):
```bash
scrcpy --select-tcpip
scrcpy -e # short version
```
- a device already listening on TCP/IP (see [below](#tcpip-wireless)):
```bash
scrcpy --tcpip=192.168.1.1:5555
scrcpy --tcpip=192.168.1.1 # default port is 5555
```
The serial may also be provided via the environment variable `ANDROID_SERIAL`
(also used by `adb`):
```bash
# in bash
export ANDROID_SERIAL=0123456789abcdef
scrcpy
```
```cmd
:: in cmd
set ANDROID_SERIAL=0123456789abcdef
scrcpy
```
```powershell
# in PowerShell
$env:ANDROID_SERIAL = '0123456789abcdef'
scrcpy
```
## TCP/IP (wireless)
_Scrcpy_ uses `adb` to communicate with the device, and `adb` can [connect] to a
device over TCP/IP. The device must be connected on the same network as the
computer.
[connect]: https://developer.android.com/studio/command-line/adb.html#wireless
### Automatic
An option `--tcpip` allows to configure the connection automatically. There are
two variants.
If the device (accessible at 192.168.1.1 in this example) already listens on a
port (typically 5555) for incoming _adb_ connections, then run:
```bash
scrcpy --tcpip=192.168.1.1 # default port is 5555
scrcpy --tcpip=192.168.1.1:5555
```
If _adb_ TCP/IP mode is disabled on the device (or if you don't know the IP
address), connect the device over USB, then run:
```bash
scrcpy --tcpip # without arguments
```
It will automatically find the device IP address and adb port, enable TCP/IP
mode if necessary, then connect to the device before starting.
### Manual
Alternatively, it is possible to enable the TCP/IP connection manually using
`adb`:
1. Plug the device into a USB port on your computer.
2. Connect the device to the same Wi-Fi network as your computer.
3. Get your device IP address, in Settings → About phone → Status, or by
executing this command:
```bash
adb shell ip route | awk '{print $9}'
```
4. Enable `adb` over TCP/IP on your device: `adb tcpip 5555`.
5. Unplug your device.
6. Connect to your device: `adb connect DEVICE_IP:5555` _(replace `DEVICE_IP`
with the device IP address you found)_.
7. Run `scrcpy` as usual.
8. Run `adb disconnect` once you're done.
Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass
having to physically connect your device directly to your computer.
[adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line
## Autostart
A small tool (by the scrcpy author) allows to run arbitrary commands whenever a
new Android device is connected: [AutoAdb]. It can be used to start scrcpy:
```bash
autoadb scrcpy -s '{}'
```
[AutoAdb]: https://github.com/rom1v/autoadb
## Display
If several displays are available on the Android device, it is possible to
select the display to mirror:
```bash
scrcpy --display=1
```
The list of display ids can be retrieved by:
```bash
scrcpy --list-displays
```
A secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored as read-only).
## Actions
Some command line arguments perform actions on the device itself while scrcpy is Some command line arguments perform actions on the device itself while scrcpy is
running. running.
## Stay awake
### Stay awake
To prevent the device from sleeping after a delay **when the device is plugged To prevent the device from sleeping after a delay **when the device is plugged
in**: in**:
@ -166,7 +19,7 @@ If the device is not plugged in (i.e. only connected over TCP/IP),
`--stay-awake` has no effect (this is the Android behavior). `--stay-awake` has no effect (this is the Android behavior).
### Turn screen off ## Turn screen off
It is possible to turn the device screen off while mirroring on start with a It is possible to turn the device screen off while mirroring on start with a
command-line option: command-line option:
@ -194,7 +47,7 @@ scrcpy -Sw # short version
``` ```
### Show touches ## Show touches
For presentations, it may be useful to show physical touches (on the physical For presentations, it may be useful to show physical touches (on the physical
device). Android exposes this feature in _Developers options_. device). Android exposes this feature in _Developers options_.
@ -210,7 +63,7 @@ scrcpy -t # short version
Note that it only shows _physical_ touches (by a finger on the device). Note that it only shows _physical_ touches (by a finger on the device).
### Power off on close ## Power off on close
To turn the device screen off when closing _scrcpy_: To turn the device screen off when closing _scrcpy_:
@ -218,11 +71,10 @@ To turn the device screen off when closing _scrcpy_:
scrcpy --power-off-on-close scrcpy --power-off-on-close
``` ```
### Power on on start ## Power on on start
By default, on start, the device is powered on. To prevent this behavior: By default, on start, the device is powered on. To prevent this behavior:
```bash ```bash
scrcpy --no-power-on scrcpy --no-power-on
``` ```

View File

@ -106,3 +106,7 @@ scrcpy --otg # keyboard and mouse
Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is Like `--hid-keyboard` and `--hid-mouse`, it only works if the device is
connected over USB. connected over USB.
## HID/OTG issues on Windows
See [FAQ](/FAQ.md#hidotg-issues-on-windows).

View File

@ -9,12 +9,10 @@ Scrcpy is packaged in several distributions and package managers:
- Debian/Ubuntu: `apt install scrcpy` - Debian/Ubuntu: `apt install scrcpy`
- Arch Linux: `pacman -S scrcpy` - Arch Linux: `pacman -S scrcpy`
- Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy`
- Gentoo: [ebuild][ebuild-link] file - Gentoo: `emerge scrcpy`
- Snap: `snap install scrcpy` - Snap: `snap install scrcpy`
- … (see [repology](https://repology.org/project/scrcpy/versions)) - … (see [repology](https://repology.org/project/scrcpy/versions))
[ebuild-link]: https://github.com/maggu2810/maggu2810-overlay/tree/master/app-mobilephone/scrcpy
### Latest version ### Latest version
However, the packaged version is not always the latest release. To install the However, the packaged version is not always the latest release. To install the

View File

@ -13,14 +13,14 @@ To record only the video:
scrcpy --no-audio --record=file.mp4 scrcpy --no-audio --record=file.mp4
``` ```
_It is currently not possible to record only the audio._ To record only the audio:
To disable mirroring while recording:
```bash ```bash
scrcpy --no-display --record=file.mp4 scrcpy --no-video --record=file.opus
scrcpy -Nr file.mkv scrcpy --no-video --audio-codec=aac --record=file.aac
# interrupt recording with Ctrl+C scrcpy --no-video --audio-codec=flac --record=file.flac
scrcpy --no-video --audio-codec=raw --record=file.wav
# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac
``` ```
Timestamps are captured on the device, so [packet delay variation] does not Timestamps are captured on the device, so [packet delay variation] does not
@ -29,16 +29,61 @@ 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 [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 The video and audio streams are encoded on the device, but are muxed on the
client side. Two formats (containers) are supported: client side. Several formats (containers) are supported:
- Matroska (`.mkv`) - MP4 (`.mp4`, `.m4a`, `.aac`)
- MP4 (`.mp4`) - Matroska (`.mkv`, `.mka`)
- OPUS (`.opus`)
- FLAC (`.flac`)
- WAV (`.wav`)
The container is automatically selected based on the filename. The container is automatically selected based on the filename.
It is also possible to explicitly select a container (in that case the filename It is also possible to explicitly select a container (in that case the filename
needs not end with `.mkv` or `.mp4`): needs not end with a known extension):
``` ```
scrcpy --record=file --record-format=mkv scrcpy --record=file --record-format=mkv
``` ```
## Rotation
The video can be recorded rotated. See [video
orientation](video.md#orientation).
## 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

@ -26,10 +26,12 @@ _<kbd>[Super]</kbd> is typically the <kbd>Windows</kbd> or <kbd>Cmd</kbd> key._
| Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd> | Switch fullscreen mode | <kbd>MOD</kbd>+<kbd>f</kbd>
| Rotate display left | <kbd>MOD</kbd>+<kbd></kbd> _(left)_ | Rotate display left | <kbd>MOD</kbd>+<kbd></kbd> _(left)_
| Rotate display right | <kbd>MOD</kbd>+<kbd></kbd> _(right)_ | Rotate display right | <kbd>MOD</kbd>+<kbd></kbd> _(right)_
| Flip display horizontally | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(left)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(right)_
| Flip display vertically | <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(up)_ \| <kbd>MOD</kbd>+<kbd>Shift</kbd>+<kbd></kbd> _(down)_
| Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd> | Resize window to 1:1 (pixel-perfect) | <kbd>MOD</kbd>+<kbd>g</kbd>
| Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_ | Resize window to remove black borders | <kbd>MOD</kbd>+<kbd>w</kbd> \| _Double-left-click¹_
| Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_ | Click on `HOME` | <kbd>MOD</kbd>+<kbd>h</kbd> \| _Middle-click_
| Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| _Right-click²_ | Click on `BACK` | <kbd>MOD</kbd>+<kbd>b</kbd> \| <kbd>MOD</kbd>+<kbd>Backspace</kbd> \| _Right-click²_
| Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4th-click³_ | Click on `APP_SWITCH` | <kbd>MOD</kbd>+<kbd>s</kbd> \| _4th-click³_
| Click on `MENU` (unlock screen)⁴ | <kbd>MOD</kbd>+<kbd>m</kbd> | Click on `MENU` (unlock screen)⁴ | <kbd>MOD</kbd>+<kbd>m</kbd>
| Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd></kbd> _(up)_ | Click on `VOLUME_UP` | <kbd>MOD</kbd>+<kbd></kbd> _(up)_

View File

@ -21,6 +21,13 @@ This will create a new video device in `/dev/videoN`, where `N` is an integer
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available (more [options](https://github.com/umlaeute/v4l2loopback#options) are available
to create several devices or devices with specific IDs). to create several devices or devices with specific IDs).
If you encounter problems detecting your device with Chrome/WebRTC, you can try
`exclusive_caps` mode:
```
sudo modprobe v4l2loopback exclusive_caps=1
```
To list the enabled devices: To list the enabled devices:
```bash ```bash
@ -35,7 +42,7 @@ To start `scrcpy` using a v4l2 sink:
```bash ```bash
scrcpy --v4l2-sink=/dev/videoN scrcpy --v4l2-sink=/dev/videoN
scrcpy --v4l2-sink=/dev/videoN --no-display # disable mirroring window scrcpy --v4l2-sink=/dev/videoN --no-video-playback # disable playback window
``` ```
(replace `N` with the device ID, check with `ls /dev/video*`) (replace `N` with the device ID, check with `ls /dev/video*`)

View File

@ -1,5 +1,14 @@
# Video # Video
## Source
By default, scrcpy mirrors the device screen.
It is possible to capture the device camera instead.
See the dedicated [camera](camera.md) page.
## Size ## Size
By default, scrcpy attempts to mirror at the Android device resolution. By default, scrcpy attempts to mirror at the Android device resolution.
@ -21,7 +30,7 @@ If encoding fails, scrcpy automatically tries again with a lower definition
## Bit rate ## Bit rate
The default video bit-rate is 8 Mbps. To change it: The default video bit rate is 8 Mbps. To change it:
```bash ```bash
scrcpy --video-bit-rate=2M scrcpy --video-bit-rate=2M
@ -66,6 +75,14 @@ scrcpy --video-codec=av1
H265 may provide better quality, but H264 should provide lower latency. H265 may provide better quality, but H264 should provide lower latency.
AV1 encoders are not common on current Android devices. AV1 encoders are not common on current Android devices.
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
check `--video-codec-options` in the manpage or in `scrcpy --help`.
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
## Encoder
Several encoders may be available on the device. They can be listed by: Several encoders may be available on the device. They can be listed by:
```bash ```bash
@ -79,45 +96,51 @@ try another one:
scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc'
``` ```
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
check `--video-codec-options` in the manpage or in `scrcpy --help`.
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat ## Orientation
The orientation may be applied at 3 different levels:
## Rotation
The rotation may be applied at 3 different levels:
- The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the - The [shortcut](shortcuts.md) <kbd>MOD</kbd>+<kbd>r</kbd> requests the
device to switch between portrait and landscape (the current running app may device to switch between portrait and landscape (the current running app may
refuse, if it does not support the requested orientation). refuse, if it does not support the requested orientation).
- `--lock-video-orientation` changes the mirroring orientation (the orientation - `--lock-video-orientation` changes the mirroring orientation (the orientation
of the video sent from the device to the computer). This affects the of the video sent from the device to the computer). This affects the
recording. recording.
- `--rotation` rotates only the window content. This only affects the display, - `--orientation` is applied on the client side, and affects display and
not the recording. It may be changed dynamically at any time using the recording. For the display, it can be changed dynamically using
[shortcuts](shortcuts.md) <kbd>MOD</kbd>+<kbd></kbd> and [shortcuts](shortcuts.md).
<kbd>MOD</kbd>+<kbd></kbd>.
To lock the mirroring orientation: To lock the mirroring orientation (on the capture side):
```bash ```bash
scrcpy --lock-video-orientation # initial (current) orientation scrcpy --lock-video-orientation # initial (current) orientation
scrcpy --lock-video-orientation=0 # natural orientation scrcpy --lock-video-orientation=0 # natural orientation
scrcpy --lock-video-orientation=1 # 90° counterclockwise scrcpy --lock-video-orientation=90 # 90° clockwise
scrcpy --lock-video-orientation=2 # 180° scrcpy --lock-video-orientation=180 # 180°
scrcpy --lock-video-orientation=3 # 90° clockwise scrcpy --lock-video-orientation=270 # 270° clockwise
``` ```
To set an initial window rotation: To orient the video (on the rendering side):
```bash ```bash
scrcpy --rotation=0 # no rotation scrcpy --orientation=0
scrcpy --rotation=1 # 90 degrees counterclockwise scrcpy --orientation=90 # 90° clockwise
scrcpy --rotation=2 # 180 degrees scrcpy --orientation=180 # 180°
scrcpy --rotation=3 # 90 degrees clockwise scrcpy --orientation=270 # 270° clockwise
scrcpy --orientation=flip0 # hflip
scrcpy --orientation=flip90 # hflip + 90° clockwise
scrcpy --orientation=flip180 # vflip (hflip + 180°)
scrcpy --orientation=flip270 # hflip + 270° clockwise
``` ```
The orientation can be set separately for display and record if necessary, via
`--display-orientation` and `--record-orientation`.
The rotation is applied to a recorded file by writing a display transformation
to the MP4 or MKV target file. Flipping is not supported, so only the 4 first
values are allowed when recording.
## Crop ## Crop
The device screen may be cropped to mirror only part of the screen. The device screen may be cropped to mirror only part of the screen.
@ -134,6 +157,25 @@ phone, landscape for a tablet).
If `--max-size` is also specified, resizing is applied after cropping. If `--max-size` is also specified, resizing is applied after cropping.
## Display
If several displays are available on the Android device, it is possible to
select the display to mirror:
```bash
scrcpy --display-id=1
```
The list of display ids can be retrieved by:
```bash
scrcpy --list-displays
```
A secondary display may only be controlled if the device runs at least Android
10 (otherwise it is mirrored as read-only).
## Buffering ## Buffering
By default, there is no video buffering, to get the lowest possible latency. By default, there is no video buffering, to get the lowest possible latency.
@ -159,17 +201,38 @@ scrcpy --display-buffer=50 --v4l2-buffer=300
``` ```
## No display ## No playback
It is possible to capture an Android device without displaying a mirroring It is possible to capture an Android device without playing video or audio on
window. This option is available if either [recording](recording.md) or the computer. This option is useful when [recording](recording.md) or when
[v4l2](#video4linux) is enabled: [v4l2](#video4linux) is enabled:
```bash ```bash
scrcpy --v4l2-sink=/dev/video2 --no-display scrcpy --v4l2-sink=/dev/video2 --no-playback
scrcpy --record=file.mkv --no-display scrcpy --record=file.mkv --no-playback
# interrupt with Ctrl+C
``` ```
It is also possible to disable video and audio playback separately:
```bash
# Send video to V4L2 sink without playing it, but keep audio playback
scrcpy --v4l2-sink=/dev/video2 --no-video-playback
# Record both video and audio, but only play video
scrcpy --record=file.mkv --no-audio-playback
```
## No video
To disable video forwarding completely, so that only audio is forwarded:
```
scrcpy --no-video
```
## Video4Linux ## Video4Linux
See the dedicated [Video4Linux](v4l2.md) page. See the dedicated [Video4Linux](v4l2.md) page.

View File

@ -4,18 +4,24 @@
Download the [latest release]: Download the [latest release]:
- [`scrcpy-win64-v2.0.zip`][direct-win64] (64-bit) - [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit)
<sub>SHA-256: `ae4c8d37a496b43f8974ba8f07f708e22a9570ba0cddc3dc3a36edbccd4d2a20`</sub> <sub>SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d`</sub>
- [`scrcpy-win32-v2.0.zip`][direct-win32] (32-bit) - [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit)
<sub>SHA-256: `15d98c02cb0e0bbd84f8b5d54991e0f6925569b1286a86a40743944fcb1c2d8c`</sub> <sub>SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff`</sub>
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest [latest release]: https://github.com/Genymobile/scrcpy/releases/latest
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win64-v2.0.zip [direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-win32-v2.0.zip [direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win32-v2.3.1.zip
and extract it. and extract it.
Alternatively, you could install it from packages manager, like [Chocolatey]: Alternatively, you could install it from packages manager, like [Winget]:
```bash
winget install scrcpy
```
or [Chocolatey]:
```bash ```bash
choco install scrcpy choco install scrcpy
@ -30,6 +36,7 @@ scoop install scrcpy
scoop install adb # if you don't have it yet scoop install adb # if you don't have it yet
``` ```
[Winget]: https://github.com/microsoft/winget-cli
[Chocolatey]: https://chocolatey.org/ [Chocolatey]: https://chocolatey.org/
[Scoop]: https://scoop.sh [Scoop]: https://scoop.sh

View File

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

View File

@ -2,8 +2,8 @@
set -e set -e
BUILDDIR=build-auto BUILDDIR=build-auto
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.0/scrcpy-server-v2.0 PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1
PREBUILT_SERVER_SHA256=9e241615f578cd690bb43311000debdecf6a9c50a7082b001952f18f6f21ddc2 PREBUILT_SERVER_SHA256=f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b
echo "[scrcpy] Downloading prebuilt server..." echo "[scrcpy] Downloading prebuilt server..."
wget "$PREBUILT_SERVER_URL" -O scrcpy-server wget "$PREBUILT_SERVER_URL" -O scrcpy-server

View File

@ -1,5 +1,5 @@
project('scrcpy', 'c', project('scrcpy', 'c',
version: '2.0', version: '2.3.1',
meson_version: '>= 0.48', meson_version: '>= 0.48',
default_options: [ default_options: [
'c_std=c11', 'c_std=c11',
@ -7,6 +7,8 @@ project('scrcpy', 'c',
'b_ndebug=if-release', 'b_ndebug=if-release',
]) ])
add_project_arguments('-Wmissing-prototypes', language: 'c')
if get_option('compile_app') if get_option('compile_app')
subdir('app') subdir('app')
endif endif

View File

@ -69,60 +69,62 @@ prepare-deps:
@app/prebuilt-deps/prepare-libusb.sh @app/prebuilt-deps/prepare-libusb.sh
build-win32: prepare-deps build-win32: prepare-deps
[ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ rm -rf "$(WIN32_BUILD_DIR)"
meson setup "$(WIN32_BUILD_DIR)" \ mkdir -p "$(WIN32_BUILD_DIR)/local"
--cross-file cross_win32.txt \ cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/"
--buildtype release --strip -Db_lto=true \ cp -r app/prebuilt-deps/data/SDL2-2.28.5/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/"
-Dcompile_server=false \ cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/"
-Dportable=true ) meson setup "$(WIN32_BUILD_DIR)" \
--pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \
-Dc_args="-I$(PWD)/$(WIN32_BUILD_DIR)/local/include" \
-Dc_link_args="-L$(PWD)/$(WIN32_BUILD_DIR)/local/lib" \
--cross-file=cross_win32.txt \
--buildtype=release --strip -Db_lto=true \
-Dcompile_server=false \
-Dportable=true
ninja -C "$(WIN32_BUILD_DIR)" ninja -C "$(WIN32_BUILD_DIR)"
build-win64: prepare-deps build-win64: prepare-deps
[ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ rm -rf "$(WIN64_BUILD_DIR)"
meson setup "$(WIN64_BUILD_DIR)" \ mkdir -p "$(WIN64_BUILD_DIR)/local"
--cross-file cross_win64.txt \ cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/"
--buildtype release --strip -Db_lto=true \ cp -r app/prebuilt-deps/data/SDL2-2.28.5/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/"
-Dcompile_server=false \ cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/"
-Dportable=true ) meson setup "$(WIN64_BUILD_DIR)" \
--pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \
-Dc_args="-I$(PWD)/$(WIN64_BUILD_DIR)/local/include" \
-Dc_link_args="-L$(PWD)/$(WIN64_BUILD_DIR)/local/lib" \
--cross-file=cross_win64.txt \
--buildtype=release --strip -Db_lto=true \
-Dcompile_server=false \
-Dportable=true
ninja -C "$(WIN64_BUILD_DIR)" ninja -C "$(WIN64_BUILD_DIR)"
dist-win32: build-server build-win32 dist-win32: build-server build-win32
mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/"
cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/scrcpy-console.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp "$(WIN32_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win32/bin/zlib1.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.26.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
dist-win64: build-server build-win64 dist-win64: build-server build-win64
mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)"
cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/"
cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/scrcpy-console.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp "$(WIN64_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-2/win64/bin/zlib1.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/platform-tools-34.0.1/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/SDL2-2.26.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
zip-win32: dist-win32 zip-win32: dist-win32
cd "$(DIST)"; \ cd "$(DIST)"; \

View File

@ -2,13 +2,13 @@ apply plugin: 'com.android.application'
android { android {
namespace 'com.genymobile.scrcpy' namespace 'com.genymobile.scrcpy'
compileSdkVersion 33 compileSdk 34
defaultConfig { defaultConfig {
applicationId "com.genymobile.scrcpy" applicationId "com.genymobile.scrcpy"
minSdkVersion 21 minSdkVersion 21
targetSdkVersion 33 targetSdkVersion 34
versionCode 20000 versionCode 20301
versionName "2.0" versionName "2.3.1"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
} }
buildTypes { buildTypes {
@ -17,6 +17,10 @@ android {
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
buildFeatures {
buildConfig true
aidl true
}
} }
dependencies { dependencies {

View File

@ -12,10 +12,10 @@
set -e set -e
SCRCPY_DEBUG=false SCRCPY_DEBUG=false
SCRCPY_VERSION_NAME=2.0 SCRCPY_VERSION_NAME=2.3.1
PLATFORM=${ANDROID_PLATFORM:-33} PLATFORM=${ANDROID_PLATFORM:-34}
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0}
BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS"
BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})"
@ -48,6 +48,7 @@ cd "$SERVER_DIR/src/main/aidl"
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \
android/content/IOnPrimaryClipChangedListener.aidl android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl
echo "Compiling java sources..." echo "Compiling java sources..."
cd ../java cd ../java

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

@ -1,7 +1,18 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
public interface AsyncProcessor { public interface AsyncProcessor {
void start(); interface TerminationListener {
/**
* Notify processor termination
*
* @param fatalError {@code true} if this must cause the termination of the whole scrcpy-server.
*/
void onTerminated(boolean fatalError);
}
void start(TerminationListener listener);
void stop(); void stop();
void join() throws InterruptedException; void join() throws InterruptedException;
} }

View File

@ -10,7 +10,6 @@ import android.media.AudioFormat;
import android.media.AudioRecord; import android.media.AudioRecord;
import android.media.AudioTimestamp; import android.media.AudioTimestamp;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.os.Build; import android.os.Build;
import android.os.SystemClock; import android.os.SystemClock;
@ -21,22 +20,33 @@ public final class AudioCapture {
public static final int SAMPLE_RATE = 48000; public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO; public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2; public static final int CHANNELS = 2;
public static final int FORMAT = AudioFormat.ENCODING_PCM_16BIT; public static final int CHANNEL_MASK = AudioFormat.CHANNEL_IN_LEFT | AudioFormat.CHANNEL_IN_RIGHT;
public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT;
public static final int BYTES_PER_SAMPLE = 2; public static final int BYTES_PER_SAMPLE = 2;
// Never read more than 1024 samples, even if the buffer is bigger (that would increase latency).
// A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we
// receive 4 successive blocks without waiting, then we wait for the 4 next ones).
public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE;
private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
private final int audioSource;
private AudioRecord recorder; private AudioRecord recorder;
private final AudioTimestamp timestamp = new AudioTimestamp(); private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0; private long previousPts = 0;
private long nextPts = 0; private long nextPts = 0;
public static int millisToBytes(int millis) { public AudioCapture(AudioSource audioSource) {
return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000; this.audioSource = audioSource.value();
} }
private static AudioFormat createAudioFormat() { private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder(); AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(FORMAT); builder.setEncoding(ENCODING);
builder.setSampleRate(SAMPLE_RATE); builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG); builder.setChannelMask(CHANNEL_CONFIG);
return builder.build(); return builder.build();
@ -44,15 +54,15 @@ public final class AudioCapture {
@TargetApi(Build.VERSION_CODES.M) @TargetApi(Build.VERSION_CODES.M)
@SuppressLint({"WrongConstant", "MissingPermission"}) @SuppressLint({"WrongConstant", "MissingPermission"})
private static AudioRecord createAudioRecord() { private static AudioRecord createAudioRecord(int audioSource) {
AudioRecord.Builder builder = new AudioRecord.Builder(); AudioRecord.Builder builder = new AudioRecord.Builder();
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On older APIs, Workarounds.fillAppInfo() must be called beforehand // On older APIs, Workarounds.fillAppInfo() must be called beforehand
builder.setContext(FakeContext.get()); builder.setContext(FakeContext.get());
} }
builder.setAudioSource(MediaRecorder.AudioSource.REMOTE_SUBMIX); builder.setAudioSource(audioSource);
builder.setAudioFormat(createAudioFormat()); builder.setAudioFormat(createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, FORMAT); int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
// This buffer size does not impact latency // This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize); builder.setBufferSizeInBytes(8 * minBufferSize);
return builder.build(); return builder.build();
@ -86,8 +96,8 @@ public final class AudioCapture {
} catch (UnsupportedOperationException e) { } catch (UnsupportedOperationException e) {
if (attempts == 0) { if (attempts == 0) {
Ln.e("Failed to start audio capture"); Ln.e("Failed to start audio capture");
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " + Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
"scrcpy."); + "scrcpy.");
throw new AudioCaptureForegroundException(); throw new AudioCaptureForegroundException();
} else { } else {
Ln.d("Failed to start audio capture, retrying..."); Ln.d("Failed to start audio capture, retrying...");
@ -97,7 +107,14 @@ public final class AudioCapture {
} }
private void startRecording() { private void startRecording() {
recorder = createAudioRecord(); try {
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(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
}
recorder.startRecording(); recorder.startRecording();
} }
@ -105,7 +122,7 @@ public final class AudioCapture {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
startWorkaroundAndroid11(); startWorkaroundAndroid11();
try { try {
tryStartRecording(3, 100); tryStartRecording(5, 100);
} finally { } finally {
stopWorkaroundAndroid11(); stopWorkaroundAndroid11();
} }
@ -122,8 +139,8 @@ public final class AudioCapture {
} }
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) { public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, size); int r = recorder.read(directBuffer, MAX_READ_SIZE);
if (r <= 0) { if (r <= 0) {
return r; return r;
} }
@ -131,8 +148,9 @@ public final class AudioCapture {
long pts; long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS) { if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
pts = timestamp.nanoTime / 1000; pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else { } else {
if (nextPts == 0) { if (nextPts == 0) {
Ln.w("Could not get any audio timestamp"); Ln.w("Could not get any audio timestamp");
@ -144,13 +162,13 @@ public final class AudioCapture {
long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs; nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts) { if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
// Audio PTS may come from two sources: // Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works; // - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback. // - 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. // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + 1; pts = previousPts + ONE_SAMPLE_US;
} }
previousPts = pts; previousPts = pts;

View File

@ -5,6 +5,7 @@ import android.media.MediaFormat;
public enum AudioCodec implements Codec { public enum AudioCodec implements Codec {
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC), AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC),
FLAC(0x66_6c_61_63, "flac", MediaFormat.MIMETYPE_AUDIO_FLAC),
RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW); RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW);
private final int id; // 4-byte ASCII representation of the name private final int id; // 4-byte ASCII representation of the name

View File

@ -37,9 +37,7 @@ public final class AudioEncoder implements AsyncProcessor {
private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE; private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE;
private static final int CHANNELS = AudioCapture.CHANNELS; private static final int CHANNELS = AudioCapture.CHANNELS;
private static final int READ_MS = 5; // milliseconds private final AudioCapture capture;
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
private final Streamer streamer; private final Streamer streamer;
private final int bitRate; private final int bitRate;
private final List<CodecOption> codecOptions; private final List<CodecOption> codecOptions;
@ -58,7 +56,8 @@ public final class AudioEncoder implements AsyncProcessor {
private boolean ended; 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.streamer = streamer;
this.bitRate = bitRate; this.bitRate = bitRate;
this.codecOptions = codecOptions; this.codecOptions = codecOptions;
@ -91,7 +90,7 @@ public final class AudioEncoder implements AsyncProcessor {
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
InputTask task = inputTasks.take(); InputTask task = inputTasks.take();
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
int r = capture.read(buffer, READ_SIZE, bufferInfo); int r = capture.read(buffer, bufferInfo);
if (r <= 0) { if (r <= 0) {
throw new IOException("Could not read audio: " + r); throw new IOException("Could not read audio: " + r);
} }
@ -114,21 +113,29 @@ public final class AudioEncoder implements AsyncProcessor {
} }
} }
public void start() { @Override
public void start(TerminationListener listener) {
thread = new Thread(() -> { thread = new Thread(() -> {
boolean fatalError = false;
try { try {
encode(); encode();
} catch (ConfigurationException | AudioCaptureForegroundException e) { } catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
fatalError = true;
} catch (AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been logged // Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) { } catch (IOException e) {
Ln.e("Audio encoding error", e); Ln.e("Audio encoding error", e);
fatalError = true;
} finally { } finally {
Ln.d("Audio encoder stopped"); Ln.d("Audio encoder stopped");
listener.onTerminated(fatalError);
} }
}); }, "audio-encoder");
thread.start(); thread.start();
} }
@Override
public void stop() { public void stop() {
if (thread != null) { if (thread != null) {
// Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates // Just wake up the blocking wait from the thread, so that it properly releases all its resources and terminates
@ -136,6 +143,7 @@ public final class AudioEncoder implements AsyncProcessor {
} }
} }
@Override
public void join() throws InterruptedException { public void join() throws InterruptedException {
if (thread != null) { if (thread != null) {
thread.join(); thread.join();
@ -166,14 +174,13 @@ public final class AudioEncoder implements AsyncProcessor {
} }
MediaCodec mediaCodec = null; MediaCodec mediaCodec = null;
AudioCapture capture = new AudioCapture();
boolean mediaCodecStarted = false; boolean mediaCodecStarted = false;
try { try {
Codec codec = streamer.getCodec(); Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName); mediaCodec = createMediaCodec(codec, encoderName);
mediaCodecThread = new HandlerThread("AudioEncoder"); mediaCodecThread = new HandlerThread("media-codec");
mediaCodecThread.start(); mediaCodecThread.start();
MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions); MediaFormat format = createFormat(codec.getMimeType(), bitRate, codecOptions);
@ -183,16 +190,15 @@ public final class AudioEncoder implements AsyncProcessor {
capture.start(); capture.start();
final MediaCodec mediaCodecRef = mediaCodec; final MediaCodec mediaCodecRef = mediaCodec;
final AudioCapture captureRef = capture;
inputThread = new Thread(() -> { inputThread = new Thread(() -> {
try { try {
inputThread(mediaCodecRef, captureRef); inputThread(mediaCodecRef, capture);
} catch (IOException | InterruptedException e) { } catch (IOException | InterruptedException e) {
Ln.e("Audio capture error", e); Ln.e("Audio capture error", e);
} finally { } finally {
end(); end();
} }
}); }, "audio-in");
outputThread = new Thread(() -> { outputThread = new Thread(() -> {
try { try {
@ -207,7 +213,7 @@ public final class AudioEncoder implements AsyncProcessor {
} finally { } finally {
end(); end();
} }
}); }, "audio-out");
mediaCodec.start(); mediaCodec.start();
mediaCodecStarted = true; mediaCodecStarted = true;
@ -289,7 +295,7 @@ public final class AudioEncoder implements AsyncProcessor {
} }
} }
private class EncoderCallback extends MediaCodec.Callback { private final class EncoderCallback extends MediaCodec.Callback {
@TargetApi(Build.VERSION_CODES.N) @TargetApi(Build.VERSION_CODES.N)
@Override @Override
public void onInputBufferAvailable(MediaCodec codec, int index) { public void onInputBufferAvailable(MediaCodec codec, int index) {

View File

@ -8,14 +8,13 @@ import java.nio.ByteBuffer;
public final class AudioRawRecorder implements AsyncProcessor { public final class AudioRawRecorder implements AsyncProcessor {
private final AudioCapture capture;
private final Streamer streamer; private final Streamer streamer;
private Thread thread; private Thread thread;
private static final int READ_MS = 5; // milliseconds public AudioRawRecorder(AudioCapture capture, Streamer streamer) {
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); this.capture = capture;
public AudioRawRecorder(Streamer streamer) {
this.streamer = streamer; this.streamer = streamer;
} }
@ -26,17 +25,22 @@ public final class AudioRawRecorder implements AsyncProcessor {
return; return;
} }
final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE); final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
AudioCapture capture = new AudioCapture();
try { try {
capture.start(); try {
capture.start();
} catch (Throwable t) {
// Notify the client that the audio could not be captured
streamer.writeDisableStream(false);
throw t;
}
streamer.writeAudioHeader(); streamer.writeAudioHeader();
while (!Thread.currentThread().isInterrupted()) { while (!Thread.currentThread().isInterrupted()) {
buffer.position(0); buffer.position(0);
int r = capture.read(buffer, READ_SIZE, bufferInfo); int r = capture.read(buffer, bufferInfo);
if (r < 0) { if (r < 0) {
throw new IOException("Could not read audio: " + r); throw new IOException("Could not read audio: " + r);
} }
@ -44,36 +48,43 @@ public final class AudioRawRecorder implements AsyncProcessor {
streamer.writePacket(buffer, bufferInfo); streamer.writePacket(buffer, bufferInfo);
} }
} catch (Throwable e) { } catch (IOException e) {
// Notify the client that the audio could not be captured // Broken pipe is expected on close, because the socket is closed by the client
streamer.writeDisableStream(false); if (!IO.isBrokenPipe(e)) {
throw e; Ln.e("Audio capture error", e);
}
} finally { } finally {
capture.stop(); capture.stop();
} }
} }
public void start() { @Override
public void start(TerminationListener listener) {
thread = new Thread(() -> { thread = new Thread(() -> {
boolean fatalError = false;
try { try {
record(); record();
} catch (AudioCaptureForegroundException e) { } catch (AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been logged // Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) { } catch (Throwable t) {
Ln.e("Audio recording error", e); Ln.e("Audio recording error", t);
fatalError = true;
} finally { } finally {
Ln.d("Audio recorder stopped"); Ln.d("Audio recorder stopped");
listener.onTerminated(fatalError);
} }
}); }, "audio-raw");
thread.start(); thread.start();
} }
@Override
public void stop() { public void stop() {
if (thread != null) { if (thread != null) {
thread.interrupt(); thread.interrupt();
} }
} }
@Override
public void join() throws InterruptedException { public void join() throws InterruptedException {
if (thread != null) { if (thread != null) {
thread.join(); thread.join();

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

@ -0,0 +1,37 @@
package com.genymobile.scrcpy;
public final class CameraAspectRatio {
private static final float SENSOR = -1;
private float ar;
private CameraAspectRatio(float ar) {
this.ar = ar;
}
public static CameraAspectRatio fromFloat(float ar) {
if (ar < 0) {
throw new IllegalArgumentException("Invalid aspect ratio: " + ar);
}
return new CameraAspectRatio(ar);
}
public static CameraAspectRatio fromFraction(int w, int h) {
if (w <= 0 || h <= 0) {
throw new IllegalArgumentException("Invalid aspect ratio: " + w + ":" + h);
}
return new CameraAspectRatio((float) w / h);
}
public static CameraAspectRatio sensorAspectRatio() {
return new CameraAspectRatio(SENSOR);
}
public boolean isSensor() {
return ar == SENSOR;
}
public float getAspectRatio() {
return ar;
}
}

View File

@ -0,0 +1,351 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraConstrainedHighSpeedCaptureSession;
import android.hardware.camera2.CameraDevice;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.CaptureFailure;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.params.SessionConfiguration;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodec;
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.util.Range;
import android.view.Surface;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Stream;
public class CameraCapture extends SurfaceCapture {
private final String explicitCameraId;
private final CameraFacing cameraFacing;
private final Size explicitSize;
private int maxSize;
private final CameraAspectRatio aspectRatio;
private final int fps;
private final boolean highSpeed;
private String cameraId;
private Size size;
private HandlerThread cameraThread;
private Handler cameraHandler;
private CameraDevice cameraDevice;
private Executor cameraExecutor;
private final AtomicBoolean disconnected = new AtomicBoolean();
public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps,
boolean highSpeed) {
this.explicitCameraId = explicitCameraId;
this.cameraFacing = cameraFacing;
this.explicitSize = explicitSize;
this.maxSize = maxSize;
this.aspectRatio = aspectRatio;
this.fps = fps;
this.highSpeed = highSpeed;
}
@Override
public void init() throws IOException {
cameraThread = new HandlerThread("camera");
cameraThread.start();
cameraHandler = new Handler(cameraThread.getLooper());
cameraExecutor = new HandlerExecutor(cameraHandler);
try {
cameraId = selectCamera(explicitCameraId, cameraFacing);
if (cameraId == null) {
throw new IOException("No matching camera found");
}
size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed);
if (size == null) {
throw new IOException("Could not select camera size");
}
Ln.i("Using camera '" + cameraId + "'");
cameraDevice = openCamera(cameraId);
} catch (CameraAccessException | InterruptedException e) {
throw new IOException(e);
}
}
private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException {
if (explicitCameraId != null) {
return explicitCameraId;
}
CameraManager cameraManager = ServiceManager.getCameraManager();
String[] cameraIds = cameraManager.getCameraIdList();
if (cameraFacing == null) {
// Use the first one
return cameraIds.length > 0 ? cameraIds[0] : null;
}
for (String cameraId : cameraIds) {
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
if (cameraFacing.value() == facing) {
return cameraId;
}
}
// Not found
return null;
}
@TargetApi(Build.VERSION_CODES.N)
private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed)
throws CameraAccessException {
if (explicitSize != null) {
return explicitSize;
}
CameraManager cameraManager = ServiceManager.getCameraManager();
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(cameraId);
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
android.util.Size[] sizes = highSpeed ? configs.getHighSpeedVideoSizes() : configs.getOutputSizes(MediaCodec.class);
Stream<android.util.Size> stream = Arrays.stream(sizes);
if (maxSize > 0) {
stream = stream.filter(it -> it.getWidth() <= maxSize && it.getHeight() <= maxSize);
}
Float targetAspectRatio = resolveAspectRatio(aspectRatio, characteristics);
if (targetAspectRatio != null) {
stream = stream.filter(it -> {
float ar = ((float) it.getWidth() / it.getHeight());
float arRatio = ar / targetAspectRatio;
// Accept if the aspect ratio is the target aspect ratio + or - 10%
return arRatio >= 0.9f && arRatio <= 1.1f;
});
}
Optional<android.util.Size> selected = stream.max((s1, s2) -> {
// Greater width is better
int cmp = Integer.compare(s1.getWidth(), s2.getWidth());
if (cmp != 0) {
return cmp;
}
if (targetAspectRatio != null) {
// Closer to the target aspect ratio is better
float ar1 = ((float) s1.getWidth() / s1.getHeight());
float arRatio1 = ar1 / targetAspectRatio;
float distance1 = Math.abs(1 - arRatio1);
float ar2 = ((float) s2.getWidth() / s2.getHeight());
float arRatio2 = ar2 / targetAspectRatio;
float distance2 = Math.abs(1 - arRatio2);
// Reverse the order because lower distance is better
cmp = Float.compare(distance2, distance1);
if (cmp != 0) {
return cmp;
}
}
// Greater height is better
return Integer.compare(s1.getHeight(), s2.getHeight());
});
if (selected.isPresent()) {
android.util.Size size = selected.get();
return new Size(size.getWidth(), size.getHeight());
}
// Not found
return null;
}
private static Float resolveAspectRatio(CameraAspectRatio ratio, CameraCharacteristics characteristics) {
if (ratio == null) {
return null;
}
if (ratio.isSensor()) {
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
return (float) activeSize.width() / activeSize.height();
}
return ratio.getAspectRatio();
}
@Override
public void start(Surface surface) throws IOException {
try {
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
CaptureRequest request = createCaptureRequest(surface);
setRepeatingRequest(session, request);
} catch (CameraAccessException | InterruptedException e) {
throw new IOException(e);
}
}
@Override
public void release() {
if (cameraDevice != null) {
cameraDevice.close();
}
if (cameraThread != null) {
cameraThread.quitSafely();
}
}
@Override
public Size getSize() {
return size;
}
@Override
public boolean setMaxSize(int maxSize) {
if (explicitSize != null) {
return false;
}
this.maxSize = maxSize;
try {
size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed);
return size != null;
} catch (CameraAccessException e) {
Ln.w("Could not select camera size", e);
return false;
}
}
@SuppressLint("MissingPermission")
@TargetApi(Build.VERSION_CODES.S)
private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException {
CompletableFuture<CameraDevice> future = new CompletableFuture<>();
ServiceManager.getCameraManager().openCamera(id, new CameraDevice.StateCallback() {
@Override
public void onOpened(CameraDevice camera) {
Ln.d("Camera opened successfully");
future.complete(camera);
}
@Override
public void onDisconnected(CameraDevice camera) {
Ln.w("Camera disconnected");
disconnected.set(true);
requestReset();
}
@Override
public void onError(CameraDevice camera, int error) {
int cameraAccessExceptionErrorCode;
switch (error) {
case CameraDevice.StateCallback.ERROR_CAMERA_IN_USE:
cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_IN_USE;
break;
case CameraDevice.StateCallback.ERROR_MAX_CAMERAS_IN_USE:
cameraAccessExceptionErrorCode = CameraAccessException.MAX_CAMERAS_IN_USE;
break;
case CameraDevice.StateCallback.ERROR_CAMERA_DISABLED:
cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_DISABLED;
break;
case CameraDevice.StateCallback.ERROR_CAMERA_DEVICE:
case CameraDevice.StateCallback.ERROR_CAMERA_SERVICE:
default:
cameraAccessExceptionErrorCode = CameraAccessException.CAMERA_ERROR;
break;
}
future.completeExceptionally(new CameraAccessException(cameraAccessExceptionErrorCode));
}
}, cameraHandler);
try {
return future.get();
} catch (ExecutionException e) {
throw (CameraAccessException) e.getCause();
}
}
@TargetApi(Build.VERSION_CODES.S)
private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException {
CompletableFuture<CameraCaptureSession> future = new CompletableFuture<>();
OutputConfiguration outputConfig = new OutputConfiguration(surface);
List<OutputConfiguration> outputs = Arrays.asList(outputConfig);
int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR;
SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() {
@Override
public void onConfigured(CameraCaptureSession session) {
future.complete(session);
}
@Override
public void onConfigureFailed(CameraCaptureSession session) {
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
}
});
camera.createCaptureSession(sessionConfig);
try {
return future.get();
} catch (ExecutionException e) {
throw (CameraAccessException) e.getCause();
}
}
private CaptureRequest createCaptureRequest(Surface surface) throws CameraAccessException {
CaptureRequest.Builder requestBuilder = cameraDevice.createCaptureRequest(CameraDevice.TEMPLATE_RECORD);
requestBuilder.addTarget(surface);
if (fps > 0) {
requestBuilder.set(CaptureRequest.CONTROL_AE_TARGET_FPS_RANGE, new Range<>(fps, fps));
}
return requestBuilder.build();
}
@TargetApi(Build.VERSION_CODES.S)
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException {
CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request, long timestamp, long frameNumber) {
// Called for each frame captured, do nothing
}
@Override
public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request, CaptureFailure failure) {
Ln.w("Camera capture failed: frame " + failure.getFrameNumber());
}
};
if (highSpeed) {
CameraConstrainedHighSpeedCaptureSession highSpeedSession = (CameraConstrainedHighSpeedCaptureSession) session;
List<CaptureRequest> requests = highSpeedSession.createHighSpeedRequestList(request);
highSpeedSession.setRepeatingBurst(requests, callback, cameraHandler);
} else {
session.setRepeatingRequest(request, callback, cameraHandler);
}
}
@Override
public boolean isClosed() {
return disconnected.get();
}
}

View File

@ -0,0 +1,33 @@
package com.genymobile.scrcpy;
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
public enum CameraFacing {
FRONT("front", CameraCharacteristics.LENS_FACING_FRONT),
BACK("back", CameraCharacteristics.LENS_FACING_BACK),
@SuppressLint("InlinedApi") // introduced in API 23
EXTERNAL("external", CameraCharacteristics.LENS_FACING_EXTERNAL);
private final String name;
private final int value;
CameraFacing(String name, int value) {
this.name = name;
this.value = value;
}
int value() {
return value;
}
static CameraFacing findByName(String name) {
for (CameraFacing facing : CameraFacing.values()) {
if (name.equals(facing.name)) {
return facing;
}
}
return null;
}
}

View File

@ -14,8 +14,6 @@ import java.io.IOException;
*/ */
public final class CleanUp { public final class CleanUp {
public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar";
// A simple struct to be passed from the main process to the cleanup process // A simple struct to be passed from the main process to the cleanup process
public static class Config implements Parcelable { public static class Config implements Parcelable {
@ -135,13 +133,13 @@ public final class CleanUp {
String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()};
ProcessBuilder builder = new ProcessBuilder(cmd); ProcessBuilder builder = new ProcessBuilder(cmd);
builder.environment().put("CLASSPATH", SERVER_PATH); builder.environment().put("CLASSPATH", Server.SERVER_PATH);
builder.start(); builder.start();
} }
public static void unlinkSelf() { public static void unlinkSelf() {
try { try {
new File(SERVER_PATH).delete(); new File(Server.SERVER_PATH).delete();
} catch (Exception e) { } catch (Exception e) {
Ln.e("Could not unlink server", e); Ln.e("Could not unlink server", e);
} }

View File

@ -84,7 +84,8 @@ public class Controller implements AsyncProcessor {
} }
} }
public void start() { @Override
public void start(TerminationListener listener) {
thread = new Thread(() -> { thread = new Thread(() -> {
try { try {
control(); control();
@ -92,12 +93,14 @@ public class Controller implements AsyncProcessor {
// this is expected on close // this is expected on close
} finally { } finally {
Ln.d("Controller stopped"); Ln.d("Controller stopped");
listener.onTerminated(true);
} }
}); }, "control-recv");
thread.start(); thread.start();
sender.start(); sender.start();
} }
@Override
public void stop() { public void stop() {
if (thread != null) { if (thread != null) {
thread.interrupt(); thread.interrupt();
@ -105,6 +108,7 @@ public class Controller implements AsyncProcessor {
sender.stop(); sender.stop();
} }
@Override
public void join() throws InterruptedException { public void join() throws InterruptedException {
if (thread != null) { if (thread != null) {
thread.join(); thread.join();
@ -314,9 +318,8 @@ public class Controller implements AsyncProcessor {
} }
} }
MotionEvent event = MotionEvent MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, DEFAULT_DEVICE_ID, 0, source, 0);
0);
return device.injectEvent(event, Device.INJECT_MODE_ASYNC); return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
} }
@ -337,9 +340,8 @@ public class Controller implements AsyncProcessor {
coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll);
coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll);
MotionEvent event = MotionEvent MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f,
.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0);
InputDevice.SOURCE_MOUSE, 0);
return device.injectEvent(event, Device.INJECT_MODE_ASYNC); return device.injectEvent(event, Device.INJECT_MODE_ASYNC);
} }

View File

@ -41,7 +41,7 @@ public final class DesktopConnection implements Closeable {
controlInputStream = null; controlInputStream = null;
controlOutputStream = null; controlOutputStream = null;
} }
videoFd = videoSocket.getFileDescriptor(); videoFd = videoSocket != null ? videoSocket.getFileDescriptor() : null;
audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null; audioFd = audioSocket != null ? audioSocket.getFileDescriptor() : null;
} }
@ -60,7 +60,8 @@ public final class DesktopConnection implements Closeable {
return SOCKET_NAME_PREFIX + String.format("_%08x", scid); return SOCKET_NAME_PREFIX + String.format("_%08x", scid);
} }
public static DesktopConnection open(int scid, boolean tunnelForward, boolean audio, boolean control, boolean sendDummyByte) throws IOException { public static DesktopConnection open(int scid, boolean tunnelForward, boolean video, boolean audio, boolean control, boolean sendDummyByte)
throws IOException {
String socketName = getSocketName(scid); String socketName = getSocketName(scid);
LocalSocket videoSocket = null; LocalSocket videoSocket = null;
@ -69,20 +70,35 @@ public final class DesktopConnection implements Closeable {
try { try {
if (tunnelForward) { if (tunnelForward) {
try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) { try (LocalServerSocket localServerSocket = new LocalServerSocket(socketName)) {
videoSocket = localServerSocket.accept(); if (video) {
if (sendDummyByte) { videoSocket = localServerSocket.accept();
// send one byte so the client may read() to detect a connection error if (sendDummyByte) {
videoSocket.getOutputStream().write(0); // send one byte so the client may read() to detect a connection error
videoSocket.getOutputStream().write(0);
sendDummyByte = false;
}
} }
if (audio) { if (audio) {
audioSocket = localServerSocket.accept(); audioSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
audioSocket.getOutputStream().write(0);
sendDummyByte = false;
}
} }
if (control) { if (control) {
controlSocket = localServerSocket.accept(); controlSocket = localServerSocket.accept();
if (sendDummyByte) {
// send one byte so the client may read() to detect a connection error
controlSocket.getOutputStream().write(0);
sendDummyByte = false;
}
} }
} }
} else { } else {
videoSocket = connect(socketName); if (video) {
videoSocket = connect(socketName);
}
if (audio) { if (audio) {
audioSocket = connect(socketName); audioSocket = connect(socketName);
} }
@ -106,18 +122,39 @@ public final class DesktopConnection implements Closeable {
return new DesktopConnection(videoSocket, audioSocket, controlSocket); return new DesktopConnection(videoSocket, audioSocket, controlSocket);
} }
public void close() throws IOException { private LocalSocket getFirstSocket() {
videoSocket.shutdownInput(); if (videoSocket != null) {
videoSocket.shutdownOutput(); return videoSocket;
videoSocket.close(); }
if (audioSocket != null) {
return audioSocket;
}
return controlSocket;
}
public void shutdown() throws IOException {
if (videoSocket != null) {
videoSocket.shutdownInput();
videoSocket.shutdownOutput();
}
if (audioSocket != null) { if (audioSocket != null) {
audioSocket.shutdownInput(); audioSocket.shutdownInput();
audioSocket.shutdownOutput(); audioSocket.shutdownOutput();
audioSocket.close();
} }
if (controlSocket != null) { if (controlSocket != null) {
controlSocket.shutdownInput(); controlSocket.shutdownInput();
controlSocket.shutdownOutput(); controlSocket.shutdownOutput();
}
}
public void close() throws IOException {
if (videoSocket != null) {
videoSocket.close();
}
if (audioSocket != null) {
audioSocket.close();
}
if (controlSocket != null) {
controlSocket.close(); controlSocket.close();
} }
} }
@ -130,7 +167,8 @@ public final class DesktopConnection implements Closeable {
System.arraycopy(deviceNameBytes, 0, buffer, 0, len); System.arraycopy(deviceNameBytes, 0, buffer, 0, len);
// byte[] are always 0-initialized in java, no need to set '\0' explicitly // byte[] are always 0-initialized in java, no need to set '\0' explicitly
IO.writeFully(videoFd, buffer, 0, buffer.length); FileDescriptor fd = getFirstSocket().getFileDescriptor();
IO.writeFully(fd, buffer, 0, buffer.length);
} }
public FileDescriptor getVideoFd() { public FileDescriptor getVideoFd() {

View File

@ -1,6 +1,7 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.DisplayControl;
import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.SurfaceControl;
@ -11,6 +12,7 @@ import android.graphics.Rect;
import android.os.Build; import android.os.Build;
import android.os.IBinder; import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.IDisplayFoldListener;
import android.view.IRotationWatcher; import android.view.IRotationWatcher;
import android.view.InputDevice; import android.view.InputDevice;
import android.view.InputEvent; import android.view.InputEvent;
@ -35,6 +37,10 @@ public final class Device {
void onRotationChanged(int rotation); void onRotationChanged(int rotation);
} }
public interface FoldListener {
void onFoldChanged(int displayId, boolean folded);
}
public interface ClipboardListener { public interface ClipboardListener {
void onClipboardTextChanged(String text); void onClipboardTextChanged(String text);
} }
@ -46,6 +52,7 @@ public final class Device {
private ScreenInfo screenInfo; private ScreenInfo screenInfo;
private RotationListener rotationListener; private RotationListener rotationListener;
private FoldListener foldListener;
private ClipboardListener clipboardListener; private ClipboardListener clipboardListener;
private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); private final AtomicBoolean isSettingClipboard = new AtomicBoolean();
@ -93,6 +100,33 @@ public final class Device {
} }
}, displayId); }, displayId);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() {
@Override
public void onDisplayFoldChanged(int displayId, boolean folded) {
if (Device.this.displayId != displayId) {
// Ignore events related to other display ids
return;
}
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 (options.getControl() && options.getClipboardAutosync()) {
// If control and autosync are enabled, synchronize Android clipboard to the computer automatically // If control and autosync are enabled, synchronize Android clipboard to the computer automatically
ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); ClipboardManager clipboardManager = ServiceManager.getClipboardManager();
@ -130,6 +164,10 @@ public final class Device {
} }
} }
public int getDisplayId() {
return displayId;
}
public synchronized void setMaxSize(int newMaxSize) { public synchronized void setMaxSize(int newMaxSize) {
maxSize = newMaxSize; maxSize = newMaxSize;
screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation);
@ -224,6 +262,10 @@ public final class Device {
this.rotationListener = rotationListener; this.rotationListener = rotationListener;
} }
public synchronized void setFoldListener(FoldListener foldlistener) {
this.foldListener = foldlistener;
}
public synchronized void setClipboardListener(ClipboardListener clipboardListener) { public synchronized void setClipboardListener(ClipboardListener clipboardListener) {
this.clipboardListener = clipboardListener; this.clipboardListener = clipboardListener;
} }
@ -278,8 +320,12 @@ public final class Device {
*/ */
public static boolean setScreenPowerMode(int mode) { public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// On Android 14, these internal methods have been moved to DisplayControl
boolean useDisplayControl =
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod();
// Change the power mode for all physical displays // Change the power mode for all physical displays
long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds(); long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds();
if (physicalDisplayIds == null) { if (physicalDisplayIds == null) {
Ln.e("Could not get physical display ids"); Ln.e("Could not get physical display ids");
return false; return false;
@ -287,7 +333,8 @@ public final class Device {
boolean allOk = true; boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) { for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken(
physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); allOk &= SurfaceControl.setDisplayPowerMode(binder, mode);
} }
return allOk; return allOk;

View File

@ -51,6 +51,7 @@ public final class DeviceMessageSender {
} }
} }
} }
public void start() { public void start() {
thread = new Thread(() -> { thread = new Thread(() -> {
try { try {
@ -60,7 +61,7 @@ public final class DeviceMessageSender {
} finally { } finally {
Ln.d("Device message sender stopped"); Ln.d("Device message sender stopped");
} }
}); }, "control-send");
thread.start(); thread.start();
} }

View File

@ -2,6 +2,7 @@ package com.genymobile.scrcpy;
import android.annotation.TargetApi; import android.annotation.TargetApi;
import android.content.AttributionSource; import android.content.AttributionSource;
import android.content.Context;
import android.content.ContextWrapper; import android.content.ContextWrapper;
import android.os.Build; import android.os.Build;
import android.os.Process; import android.os.Process;
@ -18,7 +19,7 @@ public final class FakeContext extends ContextWrapper {
} }
private FakeContext() { private FakeContext() {
super(null); super(Workarounds.getSystemContext());
} }
@Override @Override
@ -44,4 +45,9 @@ public final class FakeContext extends ContextWrapper {
public int getDeviceId() { public int getDeviceId() {
return 0; return 0;
} }
@Override
public Context getApplicationContext() {
return this;
}
} }

View File

@ -0,0 +1,23 @@
package com.genymobile.scrcpy;
import android.os.Handler;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
// Inspired from hidden android.os.HandlerExecutor
public class HandlerExecutor implements Executor {
private final Handler handler;
public HandlerExecutor(Handler handler) {
this.handler = handler;
}
@Override
public void execute(Runnable command) {
if (!handler.post(command)) {
throw new RejectedExecutionException(handler + " is shutting down");
}
}
}

View File

@ -2,6 +2,11 @@ package com.genymobile.scrcpy;
import android.util.Log; import android.util.Log;
import java.io.FileDescriptor;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.io.PrintStream;
/** /**
* Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal * Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal
* directly). * directly).
@ -11,6 +16,9 @@ public final class Ln {
private static final String TAG = "scrcpy"; private static final String TAG = "scrcpy";
private static final String PREFIX = "[server] "; private static final String PREFIX = "[server] ";
private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out));
private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err));
enum Level { enum Level {
VERBOSE, DEBUG, INFO, WARN, ERROR VERBOSE, DEBUG, INFO, WARN, ERROR
} }
@ -21,6 +29,12 @@ public final class Ln {
// not instantiable // not instantiable
} }
public static void disableSystemStreams() {
PrintStream nullStream = new PrintStream(new NullOutputStream());
System.setOut(nullStream);
System.setErr(nullStream);
}
/** /**
* Initialize the log level. * Initialize the log level.
* <p> * <p>
@ -39,30 +53,30 @@ public final class Ln {
public static void v(String message) { public static void v(String message) {
if (isEnabled(Level.VERBOSE)) { if (isEnabled(Level.VERBOSE)) {
Log.v(TAG, message); Log.v(TAG, message);
System.out.print(PREFIX + "VERBOSE: " + message + '\n'); CONSOLE_OUT.print(PREFIX + "VERBOSE: " + message + '\n');
} }
} }
public static void d(String message) { public static void d(String message) {
if (isEnabled(Level.DEBUG)) { if (isEnabled(Level.DEBUG)) {
Log.d(TAG, message); Log.d(TAG, message);
System.out.print(PREFIX + "DEBUG: " + message + '\n'); CONSOLE_OUT.print(PREFIX + "DEBUG: " + message + '\n');
} }
} }
public static void i(String message) { public static void i(String message) {
if (isEnabled(Level.INFO)) { if (isEnabled(Level.INFO)) {
Log.i(TAG, message); Log.i(TAG, message);
System.out.print(PREFIX + "INFO: " + message + '\n'); CONSOLE_OUT.print(PREFIX + "INFO: " + message + '\n');
} }
} }
public static void w(String message, Throwable throwable) { public static void w(String message, Throwable throwable) {
if (isEnabled(Level.WARN)) { if (isEnabled(Level.WARN)) {
Log.w(TAG, message, throwable); Log.w(TAG, message, throwable);
System.err.print(PREFIX + "WARN: " + message + '\n'); CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
if (throwable != null) { if (throwable != null) {
throwable.printStackTrace(); throwable.printStackTrace(CONSOLE_ERR);
} }
} }
} }
@ -74,9 +88,9 @@ public final class Ln {
public static void e(String message, Throwable throwable) { public static void e(String message, Throwable throwable) {
if (isEnabled(Level.ERROR)) { if (isEnabled(Level.ERROR)) {
Log.e(TAG, message, throwable); Log.e(TAG, message, throwable);
System.err.print(PREFIX + "ERROR: " + message + "\n"); CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
if (throwable != null) { if (throwable != null) {
throwable.printStackTrace(); throwable.printStackTrace(CONSOLE_ERR);
} }
} }
} }
@ -84,4 +98,21 @@ public final class Ln {
public static void e(String message) { public static void e(String message) {
e(message, null); e(message, null);
} }
static class NullOutputStream extends OutputStream {
@Override
public void write(byte[] b) {
// ignore
}
@Override
public void write(byte[] b, int off, int len) {
// ignore
}
@Override
public void write(int b) {
// ignore
}
}
} }

View File

@ -3,7 +3,17 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.graphics.Rect;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodec;
import android.util.Range;
import java.util.List; import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
public final class LogUtils { public final class LogUtils {
@ -47,7 +57,7 @@ public final class LogUtils {
builder.append("\n (none)"); builder.append("\n (none)");
} else { } else {
for (int id : displayIds) { for (int id : displayIds) {
builder.append("\n --display=").append(id).append(" ("); builder.append("\n --display-id=").append(id).append(" (");
DisplayInfo displayInfo = displayManager.getDisplayInfo(id); DisplayInfo displayInfo = displayManager.getDisplayInfo(id);
if (displayInfo != null) { if (displayInfo != null) {
Size size = displayInfo.getSize(); Size size = displayInfo.getSize();
@ -60,4 +70,82 @@ public final class LogUtils {
} }
return builder.toString(); return builder.toString();
} }
private static String getCameraFacingName(int facing) {
switch (facing) {
case CameraCharacteristics.LENS_FACING_FRONT:
return "front";
case CameraCharacteristics.LENS_FACING_BACK:
return "back";
case CameraCharacteristics.LENS_FACING_EXTERNAL:
return "external";
default:
return "unknown";
}
}
public static String buildCameraListMessage(boolean includeSizes) {
StringBuilder builder = new StringBuilder("List of cameras:");
CameraManager cameraManager = ServiceManager.getCameraManager();
try {
String[] cameraIds = cameraManager.getCameraIdList();
if (cameraIds == null || cameraIds.length == 0) {
builder.append("\n (none)");
} else {
for (String id : cameraIds) {
builder.append("\n --camera-id=").append(id);
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
builder.append(" (").append(getCameraFacingName(facing)).append(", ");
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
builder.append(activeSize.width()).append("x").append(activeSize.height());
try {
// Capture frame rates for low-FPS mode are the same for every resolution
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
builder.append(", fps=").append(uniqueLowFps);
} catch (Exception e) {
// Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper"
Ln.w("Could not get available frame rates for camera " + id, e);
}
builder.append(')');
if (includeSizes) {
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class);
for (android.util.Size size : sizes) {
builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight());
}
android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes();
if (highSpeedSizes.length > 0) {
builder.append("\n High speed capture (--camera-high-speed):");
for (android.util.Size size : highSpeedSizes) {
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges();
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges);
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight());
builder.append(" (fps=").append(uniqueHighFps).append(')');
}
}
}
}
}
} catch (CameraAccessException e) {
builder.append("\n (access denied)");
}
return builder.toString();
}
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) {
SortedSet<Integer> set = new TreeSet<>();
for (Range<Integer> range : ranges) {
set.add(range.getUpper());
}
return set;
}
} }

View File

@ -3,15 +3,19 @@ package com.genymobile.scrcpy;
import android.graphics.Rect; import android.graphics.Rect;
import java.util.List; import java.util.List;
import java.util.Locale;
public class Options { public class Options {
private Ln.Level logLevel = Ln.Level.DEBUG; private Ln.Level logLevel = Ln.Level.DEBUG;
private int scid = -1; // 31-bit non-negative value, or -1 private int scid = -1; // 31-bit non-negative value, or -1
private boolean video = true;
private boolean audio = true; private boolean audio = true;
private int maxSize; private int maxSize;
private VideoCodec videoCodec = VideoCodec.H264; private VideoCodec videoCodec = VideoCodec.H264;
private AudioCodec audioCodec = AudioCodec.OPUS; private AudioCodec audioCodec = AudioCodec.OPUS;
private VideoSource videoSource = VideoSource.DISPLAY;
private AudioSource audioSource = AudioSource.OUTPUT;
private int videoBitRate = 8000000; private int videoBitRate = 8000000;
private int audioBitRate = 128000; private int audioBitRate = 128000;
private int maxFps; private int maxFps;
@ -20,6 +24,12 @@ public class Options {
private Rect crop; private Rect crop;
private boolean control = true; private boolean control = true;
private int displayId; private int displayId;
private String cameraId;
private Size cameraSize;
private CameraFacing cameraFacing;
private CameraAspectRatio cameraAspectRatio;
private int cameraFps;
private boolean cameraHighSpeed;
private boolean showTouches; private boolean showTouches;
private boolean stayAwake; private boolean stayAwake;
private List<CodecOption> videoCodecOptions; private List<CodecOption> videoCodecOptions;
@ -35,6 +45,8 @@ public class Options {
private boolean listEncoders; private boolean listEncoders;
private boolean listDisplays; private boolean listDisplays;
private boolean listCameras;
private boolean listCameraSizes;
// Options not used by the scrcpy client, but useful to use scrcpy-server directly // 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 sendDeviceMeta = true; // send device name and size
@ -46,166 +58,118 @@ public class Options {
return logLevel; return logLevel;
} }
public void setLogLevel(Ln.Level logLevel) {
this.logLevel = logLevel;
}
public int getScid() { public int getScid() {
return scid; return scid;
} }
public void setScid(int scid) { public boolean getVideo() {
this.scid = scid; return video;
} }
public boolean getAudio() { public boolean getAudio() {
return audio; return audio;
} }
public void setAudio(boolean audio) {
this.audio = audio;
}
public int getMaxSize() { public int getMaxSize() {
return maxSize; return maxSize;
} }
public void setMaxSize(int maxSize) {
this.maxSize = maxSize;
}
public VideoCodec getVideoCodec() { public VideoCodec getVideoCodec() {
return videoCodec; return videoCodec;
} }
public void setVideoCodec(VideoCodec videoCodec) {
this.videoCodec = videoCodec;
}
public AudioCodec getAudioCodec() { public AudioCodec getAudioCodec() {
return audioCodec; return audioCodec;
} }
public void setAudioCodec(AudioCodec audioCodec) { public VideoSource getVideoSource() {
this.audioCodec = audioCodec; return videoSource;
}
public AudioSource getAudioSource() {
return audioSource;
} }
public int getVideoBitRate() { public int getVideoBitRate() {
return videoBitRate; return videoBitRate;
} }
public void setVideoBitRate(int videoBitRate) {
this.videoBitRate = videoBitRate;
}
public int getAudioBitRate() { public int getAudioBitRate() {
return audioBitRate; return audioBitRate;
} }
public void setAudioBitRate(int audioBitRate) {
this.audioBitRate = audioBitRate;
}
public int getMaxFps() { public int getMaxFps() {
return maxFps; return maxFps;
} }
public void setMaxFps(int maxFps) {
this.maxFps = maxFps;
}
public int getLockVideoOrientation() { public int getLockVideoOrientation() {
return lockVideoOrientation; return lockVideoOrientation;
} }
public void setLockVideoOrientation(int lockVideoOrientation) {
this.lockVideoOrientation = lockVideoOrientation;
}
public boolean isTunnelForward() { public boolean isTunnelForward() {
return tunnelForward; return tunnelForward;
} }
public void setTunnelForward(boolean tunnelForward) {
this.tunnelForward = tunnelForward;
}
public Rect getCrop() { public Rect getCrop() {
return crop; return crop;
} }
public void setCrop(Rect crop) {
this.crop = crop;
}
public boolean getControl() { public boolean getControl() {
return control; return control;
} }
public void setControl(boolean control) {
this.control = control;
}
public int getDisplayId() { public int getDisplayId() {
return displayId; return displayId;
} }
public void setDisplayId(int displayId) { public String getCameraId() {
this.displayId = displayId; return cameraId;
}
public Size getCameraSize() {
return cameraSize;
}
public CameraFacing getCameraFacing() {
return cameraFacing;
}
public CameraAspectRatio getCameraAspectRatio() {
return cameraAspectRatio;
}
public int getCameraFps() {
return cameraFps;
}
public boolean getCameraHighSpeed() {
return cameraHighSpeed;
} }
public boolean getShowTouches() { public boolean getShowTouches() {
return showTouches; return showTouches;
} }
public void setShowTouches(boolean showTouches) {
this.showTouches = showTouches;
}
public boolean getStayAwake() { public boolean getStayAwake() {
return stayAwake; return stayAwake;
} }
public void setStayAwake(boolean stayAwake) {
this.stayAwake = stayAwake;
}
public List<CodecOption> getVideoCodecOptions() { public List<CodecOption> getVideoCodecOptions() {
return videoCodecOptions; return videoCodecOptions;
} }
public void setVideoCodecOptions(List<CodecOption> videoCodecOptions) {
this.videoCodecOptions = videoCodecOptions;
}
public List<CodecOption> getAudioCodecOptions() { public List<CodecOption> getAudioCodecOptions() {
return audioCodecOptions; return audioCodecOptions;
} }
public void setAudioCodecOptions(List<CodecOption> audioCodecOptions) {
this.audioCodecOptions = audioCodecOptions;
}
public String getVideoEncoder() { public String getVideoEncoder() {
return videoEncoder; return videoEncoder;
} }
public void setVideoEncoder(String videoEncoder) {
this.videoEncoder = videoEncoder;
}
public String getAudioEncoder() { public String getAudioEncoder() {
return audioEncoder; return audioEncoder;
} }
public void setAudioEncoder(String audioEncoder) {
this.audioEncoder = audioEncoder;
}
public void setPowerOffScreenOnClose(boolean powerOffScreenOnClose) {
this.powerOffScreenOnClose = powerOffScreenOnClose;
}
public boolean getPowerOffScreenOnClose() { public boolean getPowerOffScreenOnClose() {
return this.powerOffScreenOnClose; return this.powerOffScreenOnClose;
} }
@ -214,79 +178,295 @@ public class Options {
return clipboardAutosync; return clipboardAutosync;
} }
public void setClipboardAutosync(boolean clipboardAutosync) {
this.clipboardAutosync = clipboardAutosync;
}
public boolean getDownsizeOnError() { public boolean getDownsizeOnError() {
return downsizeOnError; return downsizeOnError;
} }
public void setDownsizeOnError(boolean downsizeOnError) {
this.downsizeOnError = downsizeOnError;
}
public boolean getCleanup() { public boolean getCleanup() {
return cleanup; return cleanup;
} }
public void setCleanup(boolean cleanup) {
this.cleanup = cleanup;
}
public boolean getPowerOn() { public boolean getPowerOn() {
return powerOn; return powerOn;
} }
public void setPowerOn(boolean powerOn) { public boolean getList() {
this.powerOn = powerOn; return listEncoders || listDisplays || listCameras || listCameraSizes;
} }
public boolean getListEncoders() { public boolean getListEncoders() {
return listEncoders; return listEncoders;
} }
public void setListEncoders(boolean listEncoders) {
this.listEncoders = listEncoders;
}
public boolean getListDisplays() { public boolean getListDisplays() {
return listDisplays; return listDisplays;
} }
public void setListDisplays(boolean listDisplays) { public boolean getListCameras() {
this.listDisplays = listDisplays; return listCameras;
}
public boolean getListCameraSizes() {
return listCameraSizes;
} }
public boolean getSendDeviceMeta() { public boolean getSendDeviceMeta() {
return sendDeviceMeta; return sendDeviceMeta;
} }
public void setSendDeviceMeta(boolean sendDeviceMeta) {
this.sendDeviceMeta = sendDeviceMeta;
}
public boolean getSendFrameMeta() { public boolean getSendFrameMeta() {
return sendFrameMeta; return sendFrameMeta;
} }
public void setSendFrameMeta(boolean sendFrameMeta) {
this.sendFrameMeta = sendFrameMeta;
}
public boolean getSendDummyByte() { public boolean getSendDummyByte() {
return sendDummyByte; return sendDummyByte;
} }
public void setSendDummyByte(boolean sendDummyByte) {
this.sendDummyByte = sendDummyByte;
}
public boolean getSendCodecMeta() { public boolean getSendCodecMeta() {
return sendCodecMeta; return sendCodecMeta;
} }
public void setSendCodecMeta(boolean sendCodecMeta) { @SuppressWarnings("MethodLength")
this.sendCodecMeta = sendCodecMeta; public static Options parse(String... args) {
if (args.length < 1) {
throw new IllegalArgumentException("Missing client version");
}
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
Options options = new Options();
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "scid":
int scid = Integer.parseInt(value, 0x10);
if (scid < -1) {
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
}
options.scid = scid;
break;
case "log_level":
options.logLevel = Ln.Level.valueOf(value.toUpperCase(Locale.ENGLISH));
break;
case "video":
options.video = Boolean.parseBoolean(value);
break;
case "audio":
options.audio = Boolean.parseBoolean(value);
break;
case "video_codec":
VideoCodec videoCodec = VideoCodec.findByName(value);
if (videoCodec == null) {
throw new IllegalArgumentException("Video codec " + value + " not supported");
}
options.videoCodec = videoCodec;
break;
case "audio_codec":
AudioCodec audioCodec = AudioCodec.findByName(value);
if (audioCodec == null) {
throw new IllegalArgumentException("Audio codec " + value + " not supported");
}
options.audioCodec = audioCodec;
break;
case "video_source":
VideoSource videoSource = VideoSource.findByName(value);
if (videoSource == null) {
throw new IllegalArgumentException("Video source " + value + " not supported");
}
options.videoSource = videoSource;
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;
case "video_bit_rate":
options.videoBitRate = Integer.parseInt(value);
break;
case "audio_bit_rate":
options.audioBitRate = Integer.parseInt(value);
break;
case "max_fps":
options.maxFps = Integer.parseInt(value);
break;
case "lock_video_orientation":
options.lockVideoOrientation = Integer.parseInt(value);
break;
case "tunnel_forward":
options.tunnelForward = Boolean.parseBoolean(value);
break;
case "crop":
if (!value.isEmpty()) {
options.crop = parseCrop(value);
}
break;
case "control":
options.control = Boolean.parseBoolean(value);
break;
case "display_id":
options.displayId = Integer.parseInt(value);
break;
case "show_touches":
options.showTouches = Boolean.parseBoolean(value);
break;
case "stay_awake":
options.stayAwake = Boolean.parseBoolean(value);
break;
case "video_codec_options":
options.videoCodecOptions = CodecOption.parse(value);
break;
case "audio_codec_options":
options.audioCodecOptions = CodecOption.parse(value);
break;
case "video_encoder":
if (!value.isEmpty()) {
options.videoEncoder = value;
}
break;
case "audio_encoder":
if (!value.isEmpty()) {
options.audioEncoder = value;
}
case "power_off_on_close":
options.powerOffScreenOnClose = Boolean.parseBoolean(value);
break;
case "clipboard_autosync":
options.clipboardAutosync = Boolean.parseBoolean(value);
break;
case "downsize_on_error":
options.downsizeOnError = Boolean.parseBoolean(value);
break;
case "cleanup":
options.cleanup = Boolean.parseBoolean(value);
break;
case "power_on":
options.powerOn = Boolean.parseBoolean(value);
break;
case "list_encoders":
options.listEncoders = Boolean.parseBoolean(value);
break;
case "list_displays":
options.listDisplays = Boolean.parseBoolean(value);
break;
case "list_cameras":
options.listCameras = Boolean.parseBoolean(value);
break;
case "list_camera_sizes":
options.listCameraSizes = Boolean.parseBoolean(value);
break;
case "camera_id":
if (!value.isEmpty()) {
options.cameraId = value;
}
break;
case "camera_size":
if (!value.isEmpty()) {
options.cameraSize = parseSize(value);
}
break;
case "camera_facing":
if (!value.isEmpty()) {
CameraFacing facing = CameraFacing.findByName(value);
if (facing == null) {
throw new IllegalArgumentException("Camera facing " + value + " not supported");
}
options.cameraFacing = facing;
}
break;
case "camera_ar":
if (!value.isEmpty()) {
options.cameraAspectRatio = parseCameraAspectRatio(value);
}
break;
case "camera_fps":
options.cameraFps = Integer.parseInt(value);
break;
case "camera_high_speed":
options.cameraHighSpeed = Boolean.parseBoolean(value);
break;
case "send_device_meta":
options.sendDeviceMeta = Boolean.parseBoolean(value);
break;
case "send_frame_meta":
options.sendFrameMeta = Boolean.parseBoolean(value);
break;
case "send_dummy_byte":
options.sendDummyByte = Boolean.parseBoolean(value);
break;
case "send_codec_meta":
options.sendCodecMeta = Boolean.parseBoolean(value);
break;
case "raw_stream":
boolean rawStream = Boolean.parseBoolean(value);
if (rawStream) {
options.sendDeviceMeta = false;
options.sendFrameMeta = false;
options.sendDummyByte = false;
options.sendCodecMeta = false;
}
break;
default:
Ln.w("Unknown server option: " + key);
break;
}
}
return options;
}
private static Rect parseCrop(String crop) {
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
}
private static Size parseSize(String size) {
// input format: "<width>x<height>"
String[] tokens = size.split("x");
if (tokens.length != 2) {
throw new IllegalArgumentException("Invalid size format (expected <width>x<height>): \"" + size + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
return new Size(width, height);
}
private static CameraAspectRatio parseCameraAspectRatio(String ar) {
if ("sensor".equals(ar)) {
return CameraAspectRatio.sensorAspectRatio();
}
String[] tokens = ar.split(":");
if (tokens.length == 2) {
int w = Integer.parseInt(tokens[0]);
int h = Integer.parseInt(tokens[1]);
return CameraAspectRatio.fromFraction(w, h);
}
float floatAr = Float.parseFloat(tokens[0]);
return CameraAspectRatio.fromFloat(floatAr);
} }
} }

View File

@ -0,0 +1,113 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.hardware.display.VirtualDisplay;
import android.os.Build;
import android.os.IBinder;
import android.view.Surface;
public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener {
private final Device device;
private IBinder display;
private VirtualDisplay virtualDisplay;
public ScreenCapture(Device device) {
this.device = device;
}
@Override
public void init() {
device.setRotationListener(this);
device.setFoldListener(this);
}
@Override
public void start(Surface surface) {
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// does not include the locked video orientation
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
if (display != null) {
SurfaceControl.destroyDisplay(display);
display = null;
}
if (virtualDisplay != null) {
virtualDisplay.release();
virtualDisplay = null;
}
try {
display = createDisplay();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
Ln.d("Display: using SurfaceControl API");
} catch (Exception surfaceControlException) {
Rect videoRect = screenInfo.getVideoSize().toRect();
try {
virtualDisplay = ServiceManager.getDisplayManager()
.createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface);
Ln.d("Display: using DisplayManager API");
} catch (Exception displayManagerException) {
Ln.e("Could not create display using SurfaceControl", surfaceControlException);
Ln.e("Could not create display using DisplayManager", displayManagerException);
throw new AssertionError("Could not create display");
}
}
}
@Override
public void release() {
device.setRotationListener(null);
device.setFoldListener(null);
if (display != null) {
SurfaceControl.destroyDisplay(display);
}
}
@Override
public Size getSize() {
return device.getScreenInfo().getVideoSize();
}
@Override
public boolean setMaxSize(int maxSize) {
device.setMaxSize(maxSize);
return true;
}
@Override
public void onFoldChanged(int displayId, boolean folded) {
requestReset();
}
@Override
public void onRotationChanged(int rotation) {
requestReset();
}
private static IBinder createDisplay() throws Exception {
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore.
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S".
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals(
Build.VERSION.CODENAME));
return SurfaceControl.createDisplay("scrcpy", secure);
}
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) {
SurfaceControl.openTransaction();
try {
SurfaceControl.setDisplaySurface(display, surface);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect);
SurfaceControl.setDisplayLayerStack(display, layerStack);
} finally {
SurfaceControl.closeTransaction();
}
}
}

View File

@ -1,16 +1,52 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import android.graphics.Rect;
import android.os.BatteryManager; import android.os.BatteryManager;
import android.os.Build; import android.os.Build;
import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale;
public final class Server { public final class Server {
public static final String SERVER_PATH;
static {
String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator);
// By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath
SERVER_PATH = classPaths[0];
}
private static class Completion {
private int running;
private boolean fatalError;
Completion(int running) {
this.running = running;
}
synchronized void addCompleted(boolean fatalError) {
--running;
if (fatalError) {
this.fatalError = true;
}
if (running == 0 || this.fatalError) {
notify();
}
}
synchronized void await() {
try {
while (running > 0 && !fatalError) {
wait();
}
} catch (InterruptedException e) {
// ignore
}
}
}
private Server() { private Server() {
// not instantiable // not instantiable
} }
@ -60,41 +96,29 @@ public final class Server {
} }
private static void scrcpy(Options options) throws IOException, ConfigurationException { private static void scrcpy(Options options) throws IOException, ConfigurationException {
Ln.i("Device: " + Build.MANUFACTURER + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")"); if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) {
final Device device = new Device(options); Ln.e("Camera mirroring is not supported before Android 12");
throw new ConfigurationException("Camera mirroring is not supported");
}
Thread initThread = startInitThread(options); Thread initThread = startInitThread(options);
int scid = options.getScid(); int scid = options.getScid();
boolean tunnelForward = options.isTunnelForward(); boolean tunnelForward = options.isTunnelForward();
boolean control = options.getControl(); boolean control = options.getControl();
boolean video = options.getVideo();
boolean audio = options.getAudio(); boolean audio = options.getAudio();
boolean sendDummyByte = options.getSendDummyByte(); boolean sendDummyByte = options.getSendDummyByte();
boolean camera = video && options.getVideoSource() == VideoSource.CAMERA;
Workarounds.prepareMainLooper(); final Device device = camera ? null : new Device(options);
// Workarounds must be applied for Meizu phones: Workarounds.apply(audio, camera);
// - <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>
if (Build.BRAND.equalsIgnoreCase("meizu")) {
Workarounds.fillAppInfo();
}
// Before Android 11, audio is not supported.
// Since Android 12, we can properly set a context on the AudioRecord.
// Only on Android 11 we must fill the application context for the AudioRecord to work.
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Workarounds.fillAppContext();
}
List<AsyncProcessor> asyncProcessors = new ArrayList<>(); List<AsyncProcessor> asyncProcessors = new ArrayList<>();
try (DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, audio, control, sendDummyByte)) { DesktopConnection connection = DesktopConnection.open(scid, tunnelForward, video, audio, control, sendDummyByte);
try {
if (options.getSendDeviceMeta()) { if (options.getSendDeviceMeta()) {
connection.sendDeviceMeta(Device.getDeviceName()); connection.sendDeviceMeta(Device.getDeviceName());
} }
@ -107,43 +131,49 @@ public final class Server {
if (audio) { if (audio) {
AudioCodec audioCodec = options.getAudioCodec(); AudioCodec audioCodec = options.getAudioCodec();
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
options.getSendFrameMeta()); Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
AsyncProcessor audioRecorder; AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) { if (audioCodec == AudioCodec.RAW) {
audioRecorder = new AudioRawRecorder(audioStreamer); audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer);
} else { } else {
audioRecorder = new AudioEncoder(audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(),
options.getAudioEncoder()); options.getAudioEncoder());
} }
asyncProcessors.add(audioRecorder); asyncProcessors.add(audioRecorder);
} }
Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(), if (video) {
options.getSendFrameMeta()); Streamer videoStreamer = new Streamer(connection.getVideoFd(), options.getVideoCodec(), options.getSendCodecMeta(),
ScreenEncoder screenEncoder = new ScreenEncoder(device, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getSendFrameMeta());
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); SurfaceCapture surfaceCapture;
if (options.getVideoSource() == VideoSource.DISPLAY) {
for (AsyncProcessor asyncProcessor : asyncProcessors) { surfaceCapture = new ScreenCapture(device);
asyncProcessor.start(); } else {
} surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(),
options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed());
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);
} }
SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(),
options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError());
asyncProcessors.add(surfaceEncoder);
} }
Completion completion = new Completion(asyncProcessors.size());
for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.start((fatalError) -> {
completion.addCompleted(fatalError);
});
}
completion.await();
} finally { } finally {
Ln.d("Screen streaming stopped");
initThread.interrupt(); initThread.interrupt();
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
asyncProcessor.stop(); asyncProcessor.stop();
} }
connection.shutdown();
try { try {
initThread.join(); initThread.join();
for (AsyncProcessor asyncProcessor : asyncProcessors) { for (AsyncProcessor asyncProcessor : asyncProcessors) {
@ -152,216 +182,45 @@ public final class Server {
} catch (InterruptedException e) { } catch (InterruptedException e) {
// ignore // ignore
} }
connection.close();
} }
} }
private static Thread startInitThread(final Options options) { private static Thread startInitThread(final Options options) {
Thread thread = new Thread(() -> initAndCleanUp(options)); Thread thread = new Thread(() -> initAndCleanUp(options), "init-cleanup");
thread.start(); thread.start();
return thread; return thread;
} }
@SuppressWarnings("MethodLength") public static void main(String... args) {
private static Options createOptions(String... args) { int status = 0;
if (args.length < 1) { try {
throw new IllegalArgumentException("Missing client version"); internalMain(args);
} catch (Throwable t) {
Ln.e(t.getMessage(), t);
status = 1;
} finally {
// By default, the Java process exits when all non-daemon threads are terminated.
// The Android SDK might start some non-daemon threads internally, preventing the scrcpy server to exit.
// So force the process to exit explicitly.
System.exit(status);
} }
String clientVersion = args[0];
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
throw new IllegalArgumentException(
"The server version (" + BuildConfig.VERSION_NAME + ") does not match the client " + "(" + clientVersion + ")");
}
Options options = new Options();
for (int i = 1; i < args.length; ++i) {
String arg = args[i];
int equalIndex = arg.indexOf('=');
if (equalIndex == -1) {
throw new IllegalArgumentException("Invalid key=value pair: \"" + arg + "\"");
}
String key = arg.substring(0, equalIndex);
String value = arg.substring(equalIndex + 1);
switch (key) {
case "scid":
int scid = Integer.parseInt(value, 0x10);
if (scid < -1) {
throw new IllegalArgumentException("scid may not be negative (except -1 for 'none'): " + scid);
}
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 "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);
options.setMaxFps(maxFps);
break;
case "lock_video_orientation":
int lockVideoOrientation = Integer.parseInt(value);
options.setLockVideoOrientation(lockVideoOrientation);
break;
case "tunnel_forward":
boolean tunnelForward = Boolean.parseBoolean(value);
options.setTunnelForward(tunnelForward);
break;
case "crop":
Rect crop = parseCrop(value);
options.setCrop(crop);
break;
case "control":
boolean control = Boolean.parseBoolean(value);
options.setControl(control);
break;
case "display_id":
int displayId = Integer.parseInt(value);
options.setDisplayId(displayId);
break;
case "show_touches":
boolean showTouches = Boolean.parseBoolean(value);
options.setShowTouches(showTouches);
break;
case "stay_awake":
boolean stayAwake = Boolean.parseBoolean(value);
options.setStayAwake(stayAwake);
break;
case "video_codec_options":
List<CodecOption> videoCodecOptions = CodecOption.parse(value);
options.setVideoCodecOptions(videoCodecOptions);
break;
case "audio_codec_options":
List<CodecOption> audioCodecOptions = CodecOption.parse(value);
options.setAudioCodecOptions(audioCodecOptions);
break;
case "video_encoder":
if (!value.isEmpty()) {
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);
break;
case "clipboard_autosync":
boolean clipboardAutosync = Boolean.parseBoolean(value);
options.setClipboardAutosync(clipboardAutosync);
break;
case "downsize_on_error":
boolean downsizeOnError = Boolean.parseBoolean(value);
options.setDownsizeOnError(downsizeOnError);
break;
case "cleanup":
boolean cleanup = Boolean.parseBoolean(value);
options.setCleanup(cleanup);
break;
case "power_on":
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);
break;
case "send_frame_meta":
boolean sendFrameMeta = Boolean.parseBoolean(value);
options.setSendFrameMeta(sendFrameMeta);
break;
case "send_dummy_byte":
boolean sendDummyByte = Boolean.parseBoolean(value);
options.setSendDummyByte(sendDummyByte);
break;
case "send_codec_meta":
boolean sendCodecMeta = Boolean.parseBoolean(value);
options.setSendCodecMeta(sendCodecMeta);
break;
case "raw_video_stream":
boolean rawVideoStream = Boolean.parseBoolean(value);
if (rawVideoStream) {
options.setSendDeviceMeta(false);
options.setSendFrameMeta(false);
options.setSendDummyByte(false);
options.setSendCodecMeta(false);
}
break;
default:
Ln.w("Unknown server option: " + key);
break;
}
}
return options;
} }
private static Rect parseCrop(String crop) { private static void internalMain(String... args) throws Exception {
if (crop.isEmpty()) {
return null;
}
// input format: "width:height:x:y"
String[] tokens = crop.split(":");
if (tokens.length != 4) {
throw new IllegalArgumentException("Crop must contains 4 values separated by colons: \"" + crop + "\"");
}
int width = Integer.parseInt(tokens[0]);
int height = Integer.parseInt(tokens[1]);
int x = Integer.parseInt(tokens[2]);
int y = Integer.parseInt(tokens[3]);
return new Rect(x, y, x + width, y + height);
}
public static void main(String... args) throws Exception {
Thread.setDefaultUncaughtExceptionHandler((t, e) -> { Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
Ln.e("Exception on thread " + t, e); Ln.e("Exception on thread " + t, e);
}); });
Options options = createOptions(args); Options options = Options.parse(args);
Ln.disableSystemStreams();
Ln.initLogLevel(options.getLogLevel()); Ln.initLogLevel(options.getLogLevel());
if (options.getListEncoders() || options.getListDisplays()) { Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
if (options.getList()) {
if (options.getCleanup()) { if (options.getCleanup()) {
CleanUp.unlinkSelf(); CleanUp.unlinkSelf();
} }
@ -373,6 +232,10 @@ public final class Server {
if (options.getListDisplays()) { if (options.getListDisplays()) {
Ln.i(LogUtils.buildDisplayListMessage()); Ln.i(LogUtils.buildDisplayListMessage());
} }
if (options.getListCameras() || options.getListCameraSizes()) {
Workarounds.apply(false, true);
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
}
// Just print the requested data, do not mirror // Just print the requested data, do not mirror
return; return;
} }

View File

@ -75,7 +75,7 @@ public final class Settings {
String oldValue = getValue(table, key); String oldValue = getValue(table, key);
if (!value.equals(oldValue)) { if (!value.equals(oldValue)) {
putValue(table, key, value); putValue(table, key, value);
} }
return oldValue; return oldValue;
} }

View File

@ -5,14 +5,14 @@ import android.media.MediaCodec;
import java.io.FileDescriptor; import java.io.FileDescriptor;
import java.io.IOException; import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.util.Arrays;
public final class Streamer { public final class Streamer {
private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_CONFIG = 1L << 63;
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; 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 FileDescriptor fd;
private final Codec codec; private final Codec codec;
private final boolean sendCodecMeta; private final boolean sendCodecMeta;
@ -30,6 +30,7 @@ public final class Streamer {
public Codec getCodec() { public Codec getCodec() {
return codec; return codec;
} }
public void writeAudioHeader() throws IOException { public void writeAudioHeader() throws IOException {
if (sendCodecMeta) { if (sendCodecMeta) {
ByteBuffer buffer = ByteBuffer.allocate(4); ByteBuffer buffer = ByteBuffer.allocate(4);
@ -62,8 +63,12 @@ public final class Streamer {
} }
public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException {
if (config && codec == AudioCodec.OPUS) { if (config) {
fixOpusConfigPacket(buffer); if (codec == AudioCodec.OPUS) {
fixOpusConfigPacket(buffer);
} else if (codec == AudioCodec.FLAC) {
fixFlacConfigPacket(buffer);
}
} }
if (sendFrameMeta) { if (sendFrameMeta) {
@ -120,11 +125,14 @@ public final class Streamer {
throw new IOException("Not enough data in OPUS config packet"); throw new IOException("Not enough data in OPUS config packet");
} }
long id = buffer.getLong(); final byte[] opusHeaderId = {'A', 'O', 'P', 'U', 'S', 'H', 'D', 'R'};
if (id != AOPUSHDR) { byte[] idBuffer = new byte[8];
buffer.get(idBuffer);
if (!Arrays.equals(idBuffer, opusHeaderId)) {
throw new IOException("OPUS header not found"); throw new IOException("OPUS header not found");
} }
// The size is in native byte-order
long sizeLong = buffer.getLong(); long sizeLong = buffer.getLong();
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) { if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
throw new IOException("Invalid block size in OPUS header: " + sizeLong); throw new IOException("Invalid block size in OPUS header: " + sizeLong);
@ -138,4 +146,41 @@ public final class Streamer {
// Set the buffer to point to the OPUS header slice // Set the buffer to point to the OPUS header slice
buffer.limit(buffer.position() + size); buffer.limit(buffer.position() + size);
} }
private static void fixFlacConfigPacket(ByteBuffer buffer) throws IOException {
// 00000000 66 4c 61 43 00 00 00 22 |fLaC..." |
// -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA -------------------
// 00000000 10 00 10 00 00 00 00 00 | ........|
// 00000010 00 00 0b b8 02 f0 00 00 00 00 00 00 00 00 00 00 |................|
// 00000020 00 00 00 00 00 00 00 00 00 00 |.......... |
// ------------------------------------------------------------------------------
// 00000020 84 00 00 28 20 00 | ...( .|
// 00000030 00 00 72 65 66 65 72 65 6e 63 65 20 6c 69 62 46 |..reference libF|
// 00000040 4c 41 43 20 31 2e 33 2e 32 20 32 30 32 32 31 30 |LAC 1.3.2 202210|
// 00000050 32 32 00 00 00 00 |22....|
//
// <https://developer.android.com/reference/android/media/MediaCodec#CSD>
if (buffer.remaining() < 8) {
throw new IOException("Not enough data in FLAC config packet");
}
final byte[] flacHeaderId = {'f', 'L', 'a', 'C'};
byte[] idBuffer = new byte[4];
buffer.get(idBuffer);
if (!Arrays.equals(idBuffer, flacHeaderId)) {
throw new IOException("FLAC header not found");
}
// The size is in big-endian
buffer.order(ByteOrder.BIG_ENDIAN);
int size = buffer.getInt();
if (buffer.remaining() < size) {
throw new IOException("Not enough data in FLAC header (invalid size: " + size + ")");
}
// Set the buffer to point to the FLAC header slice
buffer.limit(buffer.position() + size);
}
} }

View File

@ -0,0 +1,71 @@
package com.genymobile.scrcpy;
import android.view.Surface;
import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* A video source which can be rendered on a Surface for encoding.
*/
public abstract class SurfaceCapture {
private final AtomicBoolean resetCapture = new AtomicBoolean();
/**
* Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on
* device rotation for example).
*/
protected void requestReset() {
resetCapture.set(true);
}
/**
* Consume the reset request (intended to be called by the encoder).
*
* @return {@code true} if a reset request was pending, {@code false} otherwise.
*/
public boolean consumeReset() {
return resetCapture.getAndSet(false);
}
/**
* Called once before the capture starts.
*/
public abstract void init() throws IOException;
/**
* Called after the capture ends (if and only if {@link #init()} has been called).
*/
public abstract void release();
/**
* Start the capture to the target surface.
*
* @param surface the surface which will be encoded
*/
public abstract void start(Surface surface) throws IOException;
/**
* Return the video size
*
* @return the video size
*/
public abstract Size getSize();
/**
* Set the maximum capture size (set by the encoder if it does not support the current size).
*
* @param maxSize Maximum size
*/
public abstract boolean setMaxSize(int maxSize);
/**
* Indicate if the capture has been closed internally.
*
* @return {@code true} is the capture is closed, {@code false} otherwise.
*/
public boolean isClosed() {
return false;
}
}

View File

@ -1,13 +1,9 @@
package com.genymobile.scrcpy; package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.SurfaceControl;
import android.graphics.Rect;
import android.media.MediaCodec; import android.media.MediaCodec;
import android.media.MediaCodecInfo; import android.media.MediaCodecInfo;
import android.media.MediaFormat; import android.media.MediaFormat;
import android.os.Build; import android.os.Looper;
import android.os.IBinder;
import android.os.SystemClock; import android.os.SystemClock;
import android.view.Surface; import android.view.Surface;
@ -16,7 +12,7 @@ import java.nio.ByteBuffer;
import java.util.List; import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicBoolean;
public class ScreenEncoder implements Device.RotationListener { public class SurfaceEncoder implements AsyncProcessor {
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms private static final int REPEAT_FRAME_DELAY_US = 100_000; // repeat after 100ms
@ -26,9 +22,7 @@ public class ScreenEncoder implements Device.RotationListener {
private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800}; private static final int[] MAX_SIZE_FALLBACK = {2560, 1920, 1600, 1280, 1024, 800};
private static final int MAX_CONSECUTIVE_ERRORS = 3; private static final int MAX_CONSECUTIVE_ERRORS = 3;
private final AtomicBoolean rotationChanged = new AtomicBoolean(); private final SurfaceCapture capture;
private final Device device;
private final Streamer streamer; private final Streamer streamer;
private final String encoderName; private final String encoderName;
private final List<CodecOption> codecOptions; private final List<CodecOption> codecOptions;
@ -39,9 +33,12 @@ public class ScreenEncoder implements Device.RotationListener {
private boolean firstFrameSent; private boolean firstFrameSent;
private int consecutiveErrors; private int consecutiveErrors;
public ScreenEncoder(Device device, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, private Thread thread;
private final AtomicBoolean stopped = new AtomicBoolean();
public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName,
boolean downsizeOnError) { boolean downsizeOnError) {
this.device = device; this.capture = capture;
this.streamer = streamer; this.streamer = streamer;
this.videoBitRate = videoBitRate; this.videoBitRate = videoBitRate;
this.maxFps = maxFps; this.maxFps = maxFps;
@ -50,45 +47,29 @@ public class ScreenEncoder implements Device.RotationListener {
this.downsizeOnError = downsizeOnError; this.downsizeOnError = downsizeOnError;
} }
@Override private void streamScreen() throws IOException, ConfigurationException {
public void onRotationChanged(int rotation) {
rotationChanged.set(true);
}
public boolean consumeRotationChange() {
return rotationChanged.getAndSet(false);
}
public void streamScreen() throws IOException, ConfigurationException {
Codec codec = streamer.getCodec(); Codec codec = streamer.getCodec();
MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaCodec mediaCodec = createMediaCodec(codec, encoderName);
MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions);
IBinder display = createDisplay();
device.setRotationListener(this);
streamer.writeVideoHeader(device.getScreenInfo().getVideoSize()); capture.init();
boolean alive;
try { try {
do { streamer.writeVideoHeader(capture.getSize());
ScreenInfo screenInfo = device.getScreenInfo();
Rect contentRect = screenInfo.getContentRect();
// include the locked video orientation boolean alive;
Rect videoRect = screenInfo.getVideoSize().toRect();
format.setInteger(MediaFormat.KEY_WIDTH, videoRect.width()); do {
format.setInteger(MediaFormat.KEY_HEIGHT, videoRect.height()); Size size = capture.getSize();
format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth());
format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight());
Surface surface = null; Surface surface = null;
try { try {
mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE);
surface = mediaCodec.createInputSurface(); surface = mediaCodec.createInputSurface();
// does not include the locked video orientation capture.start(surface);
Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect();
int videoRotation = screenInfo.getVideoRotation();
int layerStack = device.getLayerStack();
setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack);
mediaCodec.start(); mediaCodec.start();
@ -97,7 +78,7 @@ public class ScreenEncoder implements Device.RotationListener {
mediaCodec.stop(); mediaCodec.stop();
} catch (IllegalStateException | IllegalArgumentException e) { } catch (IllegalStateException | IllegalArgumentException e) {
Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage());
if (!prepareRetry(device, screenInfo)) { if (!prepareRetry(size)) {
throw e; throw e;
} }
Ln.i("Retrying..."); Ln.i("Retrying...");
@ -111,12 +92,11 @@ public class ScreenEncoder implements Device.RotationListener {
} while (alive); } while (alive);
} finally { } finally {
mediaCodec.release(); mediaCodec.release();
device.setRotationListener(null); capture.release();
SurfaceControl.destroyDisplay(display);
} }
} }
private boolean prepareRetry(Device device, ScreenInfo screenInfo) { private boolean prepareRetry(Size currentSize) {
if (firstFrameSent) { if (firstFrameSent) {
++consecutiveErrors; ++consecutiveErrors;
if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) { if (consecutiveErrors >= MAX_CONSECUTIVE_ERRORS) {
@ -136,16 +116,19 @@ public class ScreenEncoder implements Device.RotationListener {
// Downsizing on error is only enabled if an encoding failure occurs before the first frame (downsizing later could be surprising) // 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()); int newMaxSize = chooseMaxSizeFallback(currentSize);
Ln.i("newMaxSize = " + newMaxSize);
if (newMaxSize == 0) { if (newMaxSize == 0) {
// Must definitively fail // Must definitively fail
return false; return false;
} }
// Retry with a smaller device size boolean accepted = capture.setMaxSize(newMaxSize);
if (!accepted) {
return false;
}
// Retry with a smaller size
Ln.i("Retrying with -m" + newMaxSize + "..."); Ln.i("Retrying with -m" + newMaxSize + "...");
device.setMaxSize(newMaxSize);
return true; return true;
} }
@ -163,12 +146,17 @@ public class ScreenEncoder implements Device.RotationListener {
private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { private boolean encode(MediaCodec codec, Streamer streamer) throws IOException {
boolean eof = false; boolean eof = false;
boolean alive = true;
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
while (!consumeRotationChange() && !eof) { while (!capture.consumeReset() && !eof) {
if (stopped.get()) {
alive = false;
break;
}
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
try { try {
if (consumeRotationChange()) { if (capture.consumeReset()) {
// must restart encoding with new size // must restart encoding with new size
break; break;
} }
@ -193,7 +181,12 @@ public class ScreenEncoder implements Device.RotationListener {
} }
} }
return !eof; if (capture.isClosed()) {
// The capture might have been closed internally (for example if the camera is disconnected)
alive = false;
}
return !eof && alive;
} }
private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException {
@ -249,22 +242,41 @@ public class ScreenEncoder implements Device.RotationListener {
return format; return format;
} }
private static IBinder createDisplay() { @Override
// Since Android 12 (preview), secure displays could not be created with shell permissions anymore. public void start(TerminationListener listener) {
// On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". thread = new Thread(() -> {
boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S" // Some devices (Meizu) deadlock if the video encoding thread has no Looper
.equals(Build.VERSION.CODENAME)); // <https://github.com/Genymobile/scrcpy/issues/4143>
return SurfaceControl.createDisplay("scrcpy", secure); Looper.prepare();
try {
streamScreen();
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} 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");
listener.onTerminated(true);
}
}, "video");
thread.start();
} }
private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { @Override
SurfaceControl.openTransaction(); public void stop() {
try { if (thread != null) {
SurfaceControl.setDisplaySurface(display, surface); stopped.set(true);
SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); }
SurfaceControl.setDisplayLayerStack(display, layerStack); }
} finally {
SurfaceControl.closeTransaction(); @Override
public void join() throws InterruptedException {
if (thread != null) {
thread.join();
} }
} }
} }

Some files were not shown because too many files have changed in this diff Show More