Compare commits

...

24 Commits

Author SHA1 Message Date
Romain Vimont
2b6089cbfc Enable workarounds by default
Workarounds were disabled by default, and only enabled for some devices
or under specific conditions.

But it seems they are needed for more and more devices, so enable them
by default. They could be disabled for specific devices if necessary in
the future.

In the past, these workarounds caused a (harmless) exception to be
printed on some Xiaomi devices [1]. But this is not a problem anymore
since commit b8c5853aa6.

They also caused problems for audio on Vivo devices [2], but it seems
this is not the case anymore [3].

They might also impact an old Nvidia Shield [4], but hopefully this is
fixed now.

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

PR #5154 <https://github.com/Genymobile/scrcpy/pull/5154>
2024-08-01 12:16:35 +02:00
Al Grimes
f691ebb1b4 Add workaround for TCL Android 12 Smart TVs
Fixes #5140 <https://github.com/Genymobile/scrcpy/issues/5140>
PR #5148 <https://github.com/Genymobile/scrcpy/pull/5148>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-07-31 14:55:00 +02:00
Romain Vimont
071d459ad7 Fix --no-audio
By default, the audio source is initialized to SC_AUDIO_SOURCE_AUTO, and
is "resolved" only if audio is enabled.

But the server arguments were built assuming that the audio source was
never SC_AUDIO_SOURCE_AUTO (even with audio disabled), causing a crash.

Regression introduced by a10f8cd798.
2024-07-29 20:03:44 +02:00
Romain Vimont
ed4066902d Update documentation for audio playback capture
PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
127a271d34 Switch audio source if audio-dup is set
Automatically switch implicit audio source to "playback" if --audio-dup
is passed.

This allows to run:

    scrcpy --audio-dup

without specifying explicitly:

    scrcpy --audio-source=playback --audio-dup

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
31116a60d7 Add --audio-dup
Add an option to duplicate audio on the device, compatible with the new
audio playback capture (--audio-source=playback).

Fixes #3875 <https://github.com/Genymobile/scrcpy/issues/3875>
Fixes #4380 <https://github.com/Genymobile/scrcpy/issues/4380>
PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2024-07-19 17:48:39 +02:00
Romain Vimont
a10f8cd798 Add audio playback capture method
Add a new method to capture audio playback.

It requires Android 13 (where the Shell app has MODIFY_AUDIO_ROUTING
permission).

The main benefit is that it supports keeping audio playing on the device
(implemented in a further commit).

Fixes #4380 <https://github.com/Genymobile/scrcpy/issues/4380>
PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>

Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com>
2024-07-19 17:48:39 +02:00
Romain Vimont
53c6eb66ea Move audio source value
The MediaRecorder constant should not belong to the AudioSource enum.

This will allow to add a new AudioSource which has no meaningful
MediaRecorder audio source value.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
0f076083e8 Extract AudioCapture interface
Move the implementation to AudioDirectCapture and extract an
AudioCapture interface.

This will allow to provide another AudioCapture implementation.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
053bf83f58 Extract AudioRecordReader
Move the logic to read from an AudioRecord and handle all corner cases
for PTS. This simplifies AudioCapture.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
414ce4c754 Move createAudioFormat() to AudioConfig
This will allow to reuse this method.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
a2f3a5cf18 Move hardcoded audio configuration to AudioConfig
This will allow to use these constants from different classes not
directly related to AudioCapture.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
5e605b9b8f Move audio compatibility check
The compatibility depends on the capture constraints, not the encoding.

This will allow to add a new capture implementation with different
constraints.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
cf09e78323 Throw AudioCaptureException on workaround error
Replace a RuntimeException by a specific AudioCaptureException.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
3b8ec0c38d Rename audio capture exception
The AudioCaptureForegroundException was very specific. Rename it to
AudioCaptureException to support other capture failures.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Romain Vimont
39132ff2dd Make encode() method private
It is only used from AudioEncoder.

PR #5102 <https://github.com/Genymobile/scrcpy/pull/5102>
2024-07-19 17:48:39 +02:00
Kaiming Hu
9d1d79b004 Fix "turn screen off" for Honor Android 14 devices
Fixes #4823 <https://github.com/Genymobile/scrcpy/issues/4823>
PR #5109 <https://github.com/Genymobile/scrcpy/pull/5109>

Signed-off-by: Romain Vimont <rom@rom1v.com>
2024-07-17 18:02:29 +02:00
Romain Vimont
e0cdc2ace3 Fix method name
The method indicates whether GetPhysicalDisplayIds() exists. The "Get"
was missing.
2024-07-17 18:02:26 +02:00
Romain Vimont
bbcd763612 Exclude install-release tags from git describe
The install_release.sh script is updated one commit after the release
tag, which may be confusing.

For convenience, new lightweight tags have been added (for example
v2.5-install-release) to point to the commit where install_release.sh is
updated.

But these tags interfere with "git describe" to generate pretty
filenames when executing ./release.sh on a development branch, so ignore
them.

Before:

    release-v2.5-install-release-17-gc57a0512b

After:

    release-v2.5-18-gc57a0512b

Refs #4098 comment <https://github.com/Genymobile/scrcpy/issues/4098#issuecomment-1600332180>
2024-07-17 18:00:27 +02:00
Romain Vimont
c57a0512ba Add assertions
Passing an unknown enum value to convert them to string would return
NULL without any error, possibly causing undefined behavior later.

Add assertions to catch such programming errors early.
2024-07-16 21:31:31 +02:00
Romain Vimont
e84db2914d Reorganize server packages
There are now a lot of classes in the server, reorganize them into
subpackages.
2024-07-11 22:38:00 +02:00
Romain Vimont
80ca7b15e5 Extract sources paths in build_without_gradle.sh
This avoids duplication, and will be useful to add more packages.
2024-07-11 22:34:58 +02:00
Romain Vimont
79242957a0 Add clipboard workaround for Honor device
Fixes #5073 <https://github.com/Genymobile/scrcpy/issues/5073>
2024-07-11 12:21:38 +02:00
Romain Vimont
fe7494c492 Linearize try-catch blocks
There are many possible method signatures for getPrimaryClip() and
setPrimaryClip().

Avoid the nested try-catch blocks.
2024-07-11 12:19:47 +02:00
84 changed files with 813 additions and 309 deletions

View File

@@ -6,6 +6,7 @@ _scrcpy() {
--audio-buffer=
--audio-codec=
--audio-codec-options=
--audio-dup
--audio-encoder=
--audio-source=
--audio-output-buffer=
@@ -111,7 +112,7 @@ _scrcpy() {
return
;;
--audio-source)
COMPREPLY=($(compgen -W 'output mic' -- "$cur"))
COMPREPLY=($(compgen -W 'output mic playback' -- "$cur"))
return
;;
--camera-facing)

View File

@@ -13,8 +13,9 @@ arguments=(
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--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-dup=[Duplicate audio]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
'--audio-source=[Select the audio source]:source:(output mic)'
'--audio-source=[Select the audio source]:source:(output mic playback)'
'--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]'
'--camera-ar=[Select the camera size by its aspect ratio]'

View File

@@ -49,6 +49,12 @@ The list of possible codec options is available in the Android documentation:
<https://d.android.com/reference/android/media/MediaFormat>
.TP
.B \-\-audio\-dup
Duplicate audio (capture and keep playing on the device).
This feature is only available with --audio-source=playback.
.TP
.BI "\-\-audio\-encoder " name
Use a specific MediaCodec audio encoder (depending on the codec provided by \fB\-\-audio\-codec\fR).
@@ -57,7 +63,13 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR.
.TP
.BI "\-\-audio\-source " source
Select the audio source (output or mic).
Select the audio source (output, mic or playback).
The "output" source forwards the whole audio output, and disables playback on the device.
The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured).
The "mic" source captures the microphone.
Default is output.

View File

@@ -100,6 +100,7 @@ enum {
OPT_NO_WINDOW,
OPT_MOUSE_BIND,
OPT_NO_MOUSE_HOVER,
OPT_AUDIO_DUP,
};
struct sc_option {
@@ -177,6 +178,13 @@ static const struct sc_option options[] = {
"Android documentation: "
"<https://d.android.com/reference/android/media/MediaFormat>",
},
{
.longopt_id = OPT_AUDIO_DUP,
.longopt = "audio-dup",
.text = "Duplicate audio (capture and keep playing on the device).\n"
"This feature is only available with --audio-source=playback."
},
{
.longopt_id = OPT_AUDIO_ENCODER,
.longopt = "audio-encoder",
@@ -189,7 +197,13 @@ static const struct sc_option options[] = {
.longopt_id = OPT_AUDIO_SOURCE,
.longopt = "audio-source",
.argdesc = "source",
.text = "Select the audio source (output or mic).\n"
.text = "Select the audio source (output, mic or playback).\n"
"The \"output\" source forwards the whole audio output, and "
"disables playback on the device.\n"
"The \"playback\" source captures the audio playback (Android "
"apps can opt-out, so the whole output is not necessarily "
"captured).\n"
"The \"mic\" source captures the microphone.\n"
"Default is output.",
},
{
@@ -1931,7 +1945,13 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) {
return true;
}
LOGE("Unsupported audio source: %s (expected output or mic)", optarg);
if (!strcmp(optarg, "playback")) {
*source = SC_AUDIO_SOURCE_PLAYBACK;
return true;
}
LOGE("Unsupported audio source: %s (expected output, mic or playback)",
optarg);
return false;
}
@@ -2603,6 +2623,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
case OPT_NO_WINDOW:
opts->window = false;
break;
case OPT_AUDIO_DUP:
opts->audio_dup = true;
break;
default:
// getopt prints the error message on stderr
return false;
@@ -2872,13 +2895,31 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) {
// Select the audio source according to the video source
if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
if (opts->audio_dup) {
LOGI("Audio duplication enabled: audio source switched to "
"\"playback\"");
opts->audio_source = SC_AUDIO_SOURCE_PLAYBACK;
} else {
opts->audio_source = SC_AUDIO_SOURCE_OUTPUT;
}
} else {
opts->audio_source = SC_AUDIO_SOURCE_MIC;
LOGI("Camera video source: microphone audio source selected");
}
}
if (opts->audio_dup) {
if (!opts->audio) {
LOGE("--audio-dup not supported if audio is disabled");
return false;
}
if (opts->audio_source != SC_AUDIO_SOURCE_PLAYBACK) {
LOGE("--audio-dup is specific to --audio-source=playback");
return false;
}
}
if (opts->record_format && !opts->record_filename) {
LOGE("Record format specified without recording");
return false;

View File

@@ -101,6 +101,7 @@ const struct scrcpy_options scrcpy_options_default = {
.list = 0,
.window = true,
.mouse_hover = true,
.audio_dup = false,
};
enum sc_orientation

View File

@@ -59,6 +59,7 @@ enum sc_audio_source {
SC_AUDIO_SOURCE_AUTO, // OUTPUT for video DISPLAY, MIC for video CAMERA
SC_AUDIO_SOURCE_OUTPUT,
SC_AUDIO_SOURCE_MIC,
SC_AUDIO_SOURCE_PLAYBACK,
};
enum sc_camera_facing {
@@ -296,6 +297,7 @@ struct scrcpy_options {
uint8_t list;
bool window;
bool mouse_hover;
bool audio_dup;
};
extern const struct scrcpy_options scrcpy_options_default;

View File

@@ -394,6 +394,7 @@ scrcpy(struct scrcpy_options *options) {
.display_id = options->display_id,
.video = options->video,
.audio = options->audio,
.audio_dup = options->audio_dup,
.show_touches = options->show_touches,
.stay_awake = options->stay_awake,
.video_codec_options = options->video_codec_options,

View File

@@ -147,7 +147,7 @@ log_level_to_server_string(enum sc_log_level level) {
return "error";
default:
assert(!"unexpected log level");
return "(unknown)";
return NULL;
}
}
@@ -183,6 +183,7 @@ sc_server_get_codec_name(enum sc_codec codec) {
case SC_CODEC_RAW:
return "raw";
default:
assert(!"unexpected codec");
return NULL;
}
}
@@ -197,6 +198,22 @@ sc_server_get_camera_facing_name(enum sc_camera_facing camera_facing) {
case SC_CAMERA_FACING_EXTERNAL:
return "external";
default:
assert(!"unexpected camera facing");
return NULL;
}
}
static const char *
sc_server_get_audio_source_name(enum sc_audio_source audio_source) {
switch (audio_source) {
case SC_AUDIO_SOURCE_OUTPUT:
return "output";
case SC_AUDIO_SOURCE_MIC:
return "mic";
case SC_AUDIO_SOURCE_PLAYBACK:
return "playback";
default:
assert(!"unexpected audio source");
return NULL;
}
}
@@ -271,8 +288,14 @@ execute_server(struct sc_server *server,
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 audio is enabled, an "auto" audio source must have been resolved
assert(params->audio_source != SC_AUDIO_SOURCE_AUTO || !params->audio);
if (params->audio_source != SC_AUDIO_SOURCE_OUTPUT && params->audio) {
ADD_PARAM("audio_source=%s",
sc_server_get_audio_source_name(params->audio_source));
}
if (params->audio_dup) {
ADD_PARAM("audio_dup=true");
}
if (params->max_size) {
ADD_PARAM("max_size=%" PRIu16, params->max_size);

View File

@@ -50,6 +50,7 @@ struct sc_server_params {
uint32_t display_id;
bool video;
bool audio;
bool audio_dup;
bool show_touches;
bool stay_awake;
bool force_adb_forward;

View File

@@ -66,6 +66,30 @@ the computer:
scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
```
### Duplication
An alternative device audio capture method is also available (only for Android
13 and above):
```
scrcpy --audio-source=playback
```
This audio source supports keeping the audio playing on the device while
mirroring, with `--audio-dup`:
```bash
scrcpy --audio-source=playback --audio-dup
# or simply:
scrcpy --audio-dup # --audio-source=playback is implied
```
However, it requires Android 13, and Android apps can opt-out (so they are not
captured).
See [#4380](https://github.com/Genymobile/scrcpy/issues/4380).
## Codec

View File

@@ -24,7 +24,7 @@ SERVER_BUILD_DIR := build-server
WIN32_BUILD_DIR := build-win32
WIN64_BUILD_DIR := build-win64
VERSION := $(shell git describe --tags --always)
VERSION := $(shell git describe --tags --exclude='*install-release' --always)
DIST := dist
WIN32_TARGET_DIR := scrcpy-win32-$(VERSION)

View File

@@ -50,14 +50,29 @@ cd "$SERVER_DIR/src/main/aidl"
android/content/IOnPrimaryClipChangedListener.aidl
"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl
SRC=( \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/audio/*.java \
com/genymobile/scrcpy/control/*.java \
com/genymobile/scrcpy/device/*.java \
com/genymobile/scrcpy/util/*.java \
com/genymobile/scrcpy/video/*.java \
com/genymobile/scrcpy/wrappers/*.java \
)
CLASSES=()
for src in "${SRC[@]}"
do
CLASSES+=("${src%.java}.class")
done
echo "Compiling java sources..."
cd ../java
javac -bootclasspath "$ANDROID_JAR" \
-cp "$LAMBDA_JAR:$GEN_DIR" \
-d "$CLASSES_DIR" \
-source 1.8 -target 1.8 \
com/genymobile/scrcpy/*.java \
com/genymobile/scrcpy/wrappers/*.java
${SRC[@]}
echo "Dexing..."
cd "$CLASSES_DIR"
@@ -68,8 +83,7 @@ then
"$BUILD_TOOLS_DIR/dx" --dex --output "$BUILD_DIR/classes.dex" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
${CLASSES[@]}
echo "Archiving..."
cd "$BUILD_DIR"
@@ -81,8 +95,7 @@ else
--output "$BUILD_DIR/classes.zip" \
android/view/*.class \
android/content/*.class \
com/genymobile/scrcpy/*.class \
com/genymobile/scrcpy/wrappers/*.class
${CLASSES[@]}
cd "$BUILD_DIR"
mv classes.zip "$SERVER_BINARY"

View File

@@ -1,7 +0,0 @@
package com.genymobile.scrcpy;
/**
* Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground.
*/
public class AudioCaptureForegroundException extends Exception {
}

View File

@@ -1,30 +0,0 @@
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

@@ -1,5 +1,10 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;

View File

@@ -1,5 +1,15 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.video.CameraAspectRatio;
import com.genymobile.scrcpy.video.CameraFacing;
import com.genymobile.scrcpy.video.VideoCodec;
import com.genymobile.scrcpy.video.VideoSource;
import android.graphics.Rect;
import java.util.List;
@@ -16,6 +26,7 @@ public class Options {
private AudioCodec audioCodec = AudioCodec.OPUS;
private VideoSource videoSource = VideoSource.DISPLAY;
private AudioSource audioSource = AudioSource.OUTPUT;
private boolean audioDup;
private int videoBitRate = 8000000;
private int audioBitRate = 128000;
private int maxFps;
@@ -90,6 +101,10 @@ public class Options {
return audioSource;
}
public boolean getAudioDup() {
return audioDup;
}
public int getVideoBitRate() {
return videoBitRate;
}
@@ -293,6 +308,9 @@ public class Options {
}
options.audioSource = audioSource;
break;
case "audio_dup":
options.audioDup = Boolean.parseBoolean(value);
break;
case "max_size":
options.maxSize = Integer.parseInt(value) & ~7; // multiple of 8
break;

View File

@@ -1,5 +1,29 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCapture;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.audio.AudioDirectCapture;
import com.genymobile.scrcpy.audio.AudioEncoder;
import com.genymobile.scrcpy.audio.AudioPlaybackCapture;
import com.genymobile.scrcpy.audio.AudioRawRecorder;
import com.genymobile.scrcpy.audio.AudioSource;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.control.Controller;
import com.genymobile.scrcpy.control.DeviceMessage;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.device.DesktopConnection;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.device.Streamer;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.util.Settings;
import com.genymobile.scrcpy.util.SettingsException;
import com.genymobile.scrcpy.video.CameraCapture;
import com.genymobile.scrcpy.video.ScreenCapture;
import com.genymobile.scrcpy.video.SurfaceCapture;
import com.genymobile.scrcpy.video.SurfaceEncoder;
import com.genymobile.scrcpy.video.VideoSource;
import android.os.BatteryManager;
import android.os.Build;
@@ -120,7 +144,7 @@ public final class Server {
final Device device = camera ? null : new Device(options);
Workarounds.apply(audio, camera);
Workarounds.apply();
List<AsyncProcessor> asyncProcessors = new ArrayList<>();
@@ -142,7 +166,14 @@ public final class Server {
if (audio) {
AudioCodec audioCodec = options.getAudioCodec();
AudioCapture audioCapture = new AudioCapture(options.getAudioSource());
AudioSource audioSource = options.getAudioSource();
AudioCapture audioCapture;
if (audioSource.isDirect()) {
audioCapture = new AudioDirectCapture(audioSource);
} else {
audioCapture = new AudioPlaybackCapture(options.getAudioDup());
}
Streamer audioStreamer = new Streamer(connection.getAudioFd(), audioCodec, options.getSendCodecMeta(), options.getSendFrameMeta());
AsyncProcessor audioRecorder;
if (audioCodec == AudioCodec.RAW) {
@@ -248,7 +279,7 @@ public final class Server {
Ln.i(LogUtils.buildDisplayListMessage());
}
if (options.getListCameras() || options.getListCameraSizes()) {
Workarounds.apply(false, true);
Workarounds.apply();
Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes()));
}
// Just print the requested data, do not mirror

View File

@@ -1,5 +1,8 @@
package com.genymobile.scrcpy;
import com.genymobile.scrcpy.audio.AudioCaptureException;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.Application;
@@ -48,64 +51,18 @@ public final class Workarounds {
// not instantiable
}
public static void apply(boolean audio, boolean camera) {
boolean mustFillConfigurationController = false;
boolean mustFillAppInfo = false;
boolean mustFillAppContext = false;
if (Build.BRAND.equalsIgnoreCase("meizu")) {
// Workarounds must be applied for Meizu phones:
// - <https://github.com/Genymobile/scrcpy/issues/240>
// - <https://github.com/Genymobile/scrcpy/issues/365>
// - <https://github.com/Genymobile/scrcpy/issues/2656>
//
// But only apply when strictly necessary, since workarounds can cause other issues:
// - <https://github.com/Genymobile/scrcpy/issues/940>
// - <https://github.com/Genymobile/scrcpy/issues/994>
mustFillAppInfo = true;
} else if (Build.BRAND.equalsIgnoreCase("honor") || Build.MANUFACTURER.equalsIgnoreCase("skyworth")) {
// More workarounds must be applied for Honor devices:
// - <https://github.com/Genymobile/scrcpy/issues/4015>
// and Skyworth devices:
// - <https://github.com/Genymobile/scrcpy/issues/4922>
//
// The system context must not be set for all devices, because it would cause other problems:
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
mustFillAppInfo = true;
mustFillAppContext = true;
}
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// 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.
mustFillAppContext = true;
}
if (camera) {
mustFillAppInfo = true;
mustFillAppContext = true;
}
public static void apply() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(),
// which requires a non-null ConfigurationController.
// ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions.
// <https://github.com/Genymobile/scrcpy/issues/4467>
mustFillConfigurationController = true;
}
if (mustFillConfigurationController) {
// Must be call before fillAppContext() because it is necessary to get a valid system context
// Must be called before fillAppContext() because it is necessary to get a valid system context.
fillConfigurationController();
}
if (mustFillAppInfo) {
fillAppInfo();
}
if (mustFillAppContext) {
fillAppContext();
}
fillAppInfo();
fillAppContext();
}
@SuppressWarnings("deprecation")
@@ -193,7 +150,8 @@ public final class Workarounds {
@TargetApi(Build.VERSION_CODES.R)
@SuppressLint("WrongConstant,MissingPermission")
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) {
public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws
AudioCaptureException {
// Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment.
//
// This method invokes the `AudioRecord(long nativeRecordInJavaObj)` constructor to create an empty `AudioRecord` instance, then uses
@@ -334,8 +292,8 @@ public final class Workarounds {
return audioRecord;
} catch (Exception e) {
Ln.e("Failed to invoke AudioRecord.<init>.", e);
throw new RuntimeException("Cannot create AudioRecord");
Ln.e("Cannot create AudioRecord", e);
throw new AudioCaptureException();
}
}
}

View File

@@ -0,0 +1,20 @@
package com.genymobile.scrcpy.audio;
import android.media.MediaCodec;
import java.nio.ByteBuffer;
public interface AudioCapture {
void checkCompatibility() throws AudioCaptureException;
void start() throws AudioCaptureException;
void stop();
/**
* Read a chunk of {@link AudioConfig#MAX_READ_SIZE} samples.
*
* @param outDirectBuffer The target buffer
* @param outBufferInfo The info to provide to MediaCodec
* @return the number of bytes actually read.
*/
int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo);
}

View File

@@ -0,0 +1,12 @@
package com.genymobile.scrcpy.audio;
/**
* Exception for any audio capture issue.
* <p/>
* This includes the case where audio capture failed on Android 11 specifically because the running App (Shell) was not in foreground.
* <p/>
* Its purpose is to disable audio without errors (that's why the exception is empty, any error message must be printed by the caller before
* throwing the exception).
*/
public class AudioCaptureException extends Exception {
}

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.util.Codec;
import android.media.MediaFormat;

View File

@@ -0,0 +1,29 @@
package com.genymobile.scrcpy.audio;
import android.media.AudioFormat;
public final class AudioConfig {
public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2;
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;
// 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 AudioConfig() {
// Not instantiable
}
public static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
}
}

View File

@@ -1,55 +1,48 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Workarounds;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.media.MediaRecorder;
import android.os.Build;
import android.os.SystemClock;
import java.nio.ByteBuffer;
public final class AudioCapture {
public class AudioDirectCapture implements AudioCapture {
public static final int SAMPLE_RATE = 48000;
public static final int CHANNEL_CONFIG = AudioFormat.CHANNEL_IN_STEREO;
public static final int CHANNELS = 2;
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;
// 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 static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
private static final int CHANNEL_CONFIG = AudioConfig.CHANNEL_CONFIG;
private static final int CHANNELS = AudioConfig.CHANNELS;
private static final int CHANNEL_MASK = AudioConfig.CHANNEL_MASK;
private static final int ENCODING = AudioConfig.ENCODING;
private final int audioSource;
private AudioRecord recorder;
private AudioRecordReader reader;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0;
private long nextPts = 0;
public AudioCapture(AudioSource audioSource) {
this.audioSource = audioSource.value();
public AudioDirectCapture(AudioSource audioSource) {
this.audioSource = getAudioSourceValue(audioSource);
}
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING);
builder.setSampleRate(SAMPLE_RATE);
builder.setChannelMask(CHANNEL_CONFIG);
return builder.build();
private static int getAudioSourceValue(AudioSource audioSource) {
switch (audioSource) {
case OUTPUT:
return MediaRecorder.AudioSource.REMOTE_SUBMIX;
case MIC:
return MediaRecorder.AudioSource.MIC;
default:
throw new IllegalArgumentException("Unsupported audio source: " + audioSource);
}
}
@TargetApi(Build.VERSION_CODES.M)
@@ -61,7 +54,7 @@ public final class AudioCapture {
builder.setContext(FakeContext.get());
}
builder.setAudioSource(audioSource);
builder.setAudioFormat(createAudioFormat());
builder.setAudioFormat(AudioConfig.createAudioFormat());
int minBufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE, CHANNEL_CONFIG, ENCODING);
// This buffer size does not impact latency
builder.setBufferSizeInBytes(8 * minBufferSize);
@@ -86,7 +79,7 @@ public final class AudioCapture {
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureException {
while (attempts-- > 0) {
// Wait for activity to start
SystemClock.sleep(delayMs);
@@ -98,7 +91,7 @@ public final class AudioCapture {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting "
+ "scrcpy.");
throw new AudioCaptureForegroundException();
throw new AudioCaptureException();
} else {
Ln.d("Failed to start audio capture, retrying...");
}
@@ -106,7 +99,7 @@ public final class AudioCapture {
}
}
private void startRecording() {
private void startRecording() throws AudioCaptureException {
try {
recorder = createAudioRecord(audioSource);
} catch (NullPointerException e) {
@@ -116,9 +109,19 @@ public final class AudioCapture {
recorder = Workarounds.createAudioRecord(audioSource, SAMPLE_RATE, CHANNEL_CONFIG, CHANNELS, CHANNEL_MASK, ENCODING);
}
recorder.startRecording();
reader = new AudioRecordReader(recorder);
}
public void start() throws AudioCaptureForegroundException {
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
throw new AudioCaptureException();
}
}
@Override
public void start() throws AudioCaptureException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
startWorkaroundAndroid11();
try {
@@ -131,6 +134,7 @@ public final class AudioCapture {
}
}
@Override
public void stop() {
if (recorder != null) {
// Will call .stop() if necessary, without throwing an IllegalStateException
@@ -138,42 +142,9 @@ public final class AudioCapture {
}
}
@Override
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, MAX_READ_SIZE);
if (r <= 0) {
return r;
}
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else {
if (nextPts == 0) {
Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationUs = r * 1000000L / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + ONE_SAMPLE_US;
}
previousPts = pts;
outBufferInfo.set(0, r, pts, 0);
return r;
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}
}

View File

@@ -1,4 +1,14 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.CodecUtils;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.device.Streamer;
import android.annotation.TargetApi;
import android.media.MediaCodec;
@@ -34,8 +44,8 @@ public final class AudioEncoder implements AsyncProcessor {
}
}
private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE;
private static final int CHANNELS = AudioCapture.CHANNELS;
private static final int SAMPLE_RATE = AudioConfig.SAMPLE_RATE;
private static final int CHANNELS = AudioConfig.CHANNELS;
private final AudioCapture capture;
private final Streamer streamer;
@@ -122,7 +132,7 @@ public final class AudioEncoder implements AsyncProcessor {
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
fatalError = true;
} catch (AudioCaptureForegroundException e) {
} catch (AudioCaptureException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
Ln.e("Audio encoding error", e);
@@ -166,7 +176,7 @@ public final class AudioEncoder implements AsyncProcessor {
}
@TargetApi(Build.VERSION_CODES.M)
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
private void encode() throws IOException, ConfigurationException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
@@ -177,6 +187,8 @@ public final class AudioEncoder implements AsyncProcessor {
boolean mediaCodecStarted = false;
try {
capture.checkCompatibility(); // throws an AudioCaptureException on error
Codec codec = streamer.getCodec();
mediaCodec = createMediaCodec(codec, encoderName);

View File

@@ -0,0 +1,137 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioAttributes;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.MediaCodec;
import android.os.Build;
import java.lang.reflect.Method;
import java.nio.ByteBuffer;
public final class AudioPlaybackCapture implements AudioCapture {
private final boolean keepPlayingOnDevice;
private AudioRecord recorder;
private AudioRecordReader reader;
public AudioPlaybackCapture(boolean keepPlayingOnDevice) {
this.keepPlayingOnDevice = keepPlayingOnDevice;
}
@SuppressLint("PrivateApi")
private AudioRecord createAudioRecord() throws AudioCaptureException {
// See <https://github.com/Genymobile/scrcpy/issues/4380>
try {
Class<?> audioMixingRuleClass = Class.forName("android.media.audiopolicy.AudioMixingRule");
Class<?> audioMixingRuleBuilderClass = Class.forName("android.media.audiopolicy.AudioMixingRule$Builder");
// AudioMixingRule.Builder audioMixingRuleBuilder = new AudioMixingRule.Builder();
Object audioMixingRuleBuilder = audioMixingRuleBuilderClass.getConstructor().newInstance();
// audioMixingRuleBuilder.setTargetMixRole(AudioMixingRule.MIX_ROLE_PLAYERS);
int mixRolePlayersConstant = audioMixingRuleClass.getField("MIX_ROLE_PLAYERS").getInt(null);
Method setTargetMixRoleMethod = audioMixingRuleBuilderClass.getMethod("setTargetMixRole", int.class);
setTargetMixRoleMethod.invoke(audioMixingRuleBuilder, mixRolePlayersConstant);
AudioAttributes attributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).build();
// audioMixingRuleBuilder.addMixRule(AudioMixingRule.RULE_MATCH_ATTRIBUTE_USAGE, attributes);
int ruleMatchAttributeUsageConstant = audioMixingRuleClass.getField("RULE_MATCH_ATTRIBUTE_USAGE").getInt(null);
Method addMixRuleMethod = audioMixingRuleBuilderClass.getMethod("addMixRule", int.class, Object.class);
addMixRuleMethod.invoke(audioMixingRuleBuilder, ruleMatchAttributeUsageConstant, attributes);
// AudioMixingRule audioMixingRule = builder.build();
Object audioMixingRule = audioMixingRuleBuilderClass.getMethod("build").invoke(audioMixingRuleBuilder);
// audioMixingRuleBuilder.voiceCommunicationCaptureAllowed(true);
Method voiceCommunicationCaptureAllowedMethod = audioMixingRuleBuilderClass.getMethod("voiceCommunicationCaptureAllowed", boolean.class);
voiceCommunicationCaptureAllowedMethod.invoke(audioMixingRuleBuilder, true);
Class<?> audioMixClass = Class.forName("android.media.audiopolicy.AudioMix");
Class<?> audioMixBuilderClass = Class.forName("android.media.audiopolicy.AudioMix$Builder");
// AudioMix.Builder audioMixBuilder = new AudioMix.Builder(audioMixingRule);
Object audioMixBuilder = audioMixBuilderClass.getConstructor(audioMixingRuleClass).newInstance(audioMixingRule);
// audioMixBuilder.setFormat(createAudioFormat());
Method setFormat = audioMixBuilder.getClass().getMethod("setFormat", AudioFormat.class);
setFormat.invoke(audioMixBuilder, AudioConfig.createAudioFormat());
String routeFlagName = keepPlayingOnDevice ? "ROUTE_FLAG_LOOP_BACK_RENDER" : "ROUTE_FLAG_LOOP_BACK";
int routeFlags = audioMixClass.getField(routeFlagName).getInt(null);
// audioMixBuilder.setRouteFlags(routeFlag);
Method setRouteFlags = audioMixBuilder.getClass().getMethod("setRouteFlags", int.class);
setRouteFlags.invoke(audioMixBuilder, routeFlags);
// AudioMix audioMix = audioMixBuilder.build();
Object audioMix = audioMixBuilderClass.getMethod("build").invoke(audioMixBuilder);
Class<?> audioPolicyClass = Class.forName("android.media.audiopolicy.AudioPolicy");
Class<?> audioPolicyBuilderClass = Class.forName("android.media.audiopolicy.AudioPolicy$Builder");
// AudioPolicy.Builder audioPolicyBuilder = new AudioPolicy.Builder();
Object audioPolicyBuilder = audioPolicyBuilderClass.getConstructor(Context.class).newInstance(FakeContext.get());
// audioPolicyBuilder.addMix(audioMix);
Method addMixMethod = audioPolicyBuilderClass.getMethod("addMix", audioMixClass);
addMixMethod.invoke(audioPolicyBuilder, audioMix);
// AudioPolicy audioPolicy = audioPolicyBuilder.build();
Object audioPolicy = audioPolicyBuilderClass.getMethod("build").invoke(audioPolicyBuilder);
// AudioManager.registerAudioPolicyStatic(audioPolicy);
Method registerAudioPolicyStaticMethod = AudioManager.class.getDeclaredMethod("registerAudioPolicyStatic", audioPolicyClass);
registerAudioPolicyStaticMethod.setAccessible(true);
int result = (int) registerAudioPolicyStaticMethod.invoke(null, audioPolicy);
if (result != 0) {
throw new RuntimeException("registerAudioPolicy() returned " + result);
}
// audioPolicy.createAudioRecordSink(audioPolicy);
Method createAudioRecordSinkClass = audioPolicyClass.getMethod("createAudioRecordSink", audioMixClass);
return (AudioRecord) createAudioRecordSinkClass.invoke(audioPolicy, audioMix);
} catch (Exception e) {
Ln.e("Could not capture audio playback", e);
throw new AudioCaptureException();
}
}
@Override
public void checkCompatibility() throws AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) {
Ln.w("Audio disabled: audio playback capture source not supported before Android 13");
throw new AudioCaptureException();
}
}
@Override
public void start() throws AudioCaptureException {
recorder = createAudioRecord();
recorder.startRecording();
reader = new AudioRecordReader(recorder);
}
@Override
public void stop() {
if (recorder != null) {
// Will call .stop() if necessary, without throwing an IllegalStateException
recorder.release();
}
}
@Override
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
return reader.read(outDirectBuffer, outBufferInfo);
}
}

View File

@@ -1,4 +1,9 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Streamer;
import android.media.MediaCodec;
import android.os.Build;
@@ -18,14 +23,14 @@ public final class AudioRawRecorder implements AsyncProcessor {
this.streamer = streamer;
}
private void record() throws IOException, AudioCaptureForegroundException {
private void record() throws IOException, AudioCaptureException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
return;
}
final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE);
final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioConfig.MAX_READ_SIZE);
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
try {
@@ -64,7 +69,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
boolean fatalError = false;
try {
record();
} catch (AudioCaptureForegroundException e) {
} catch (AudioCaptureException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (Throwable t) {
Ln.e("Audio recording error", t);

View File

@@ -0,0 +1,67 @@
package com.genymobile.scrcpy.audio;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
import android.media.MediaCodec;
import android.os.Build;
import java.nio.ByteBuffer;
public class AudioRecordReader {
private static final long ONE_SAMPLE_US =
(1000000 + AudioConfig.SAMPLE_RATE - 1) / AudioConfig.SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS)
private final AudioRecord recorder;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0;
private long nextPts = 0;
public AudioRecordReader(AudioRecord recorder) {
this.recorder = recorder;
}
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE);
if (r <= 0) {
return r;
}
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else {
if (nextPts == 0) {
Ln.w("Could not get initial audio timestamp");
nextPts = System.nanoTime() / 1000;
}
// compute from previous timestamp and packet size
pts = nextPts;
}
long durationUs = r * 1000000L / (AudioConfig.CHANNELS * AudioConfig.BYTES_PER_SAMPLE * AudioConfig.SAMPLE_RATE);
nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
// Audio PTS may come from two sources:
// - recorder.getTimestamp() if the call works;
// - an estimation from the previous PTS and the packet size as a fallback.
//
// Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it.
pts = previousPts + ONE_SAMPLE_US;
}
previousPts = pts;
outBufferInfo.set(0, r, pts, 0);
return r;
}
}

View File

@@ -0,0 +1,27 @@
package com.genymobile.scrcpy.audio;
public enum AudioSource {
OUTPUT("output"),
MIC("mic"),
PLAYBACK("playback");
private final String name;
AudioSource(String name) {
this.name = name;
}
public boolean isDirect() {
return this != PLAYBACK;
}
public static AudioSource findByName(String name) {
for (AudioSource audioSource : AudioSource.values()) {
if (name.equals(audioSource.name)) {
return audioSource;
}
}
return null;
}
}

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import android.net.LocalSocket;

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Position;
/**
* Union of all supported event types, identified by their {@code type}.

View File

@@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Binary;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Position;
import java.io.EOFException;
import java.io.IOException;

View File

@@ -1,5 +1,11 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.CleanUp;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Point;
import com.genymobile.scrcpy.device.Position;
import com.genymobile.scrcpy.wrappers.InputManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
public final class DeviceMessage {

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import java.io.IOException;
import java.util.concurrent.ArrayBlockingQueue;

View File

@@ -1,4 +1,7 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.StringUtils;
import java.io.IOException;
import java.io.OutputStream;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import java.util.HashMap;
import java.util.Map;

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Point;
public class Pointer {

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Point;
import android.view.MotionEvent;

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.util.Ln;
import android.os.Build;
import android.os.HandlerThread;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
public class ConfigurationException extends Exception {
public ConfigurationException(String message) {

View File

@@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.control.ControlChannel;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.StringUtils;
import android.net.LocalServerSocket;
import android.net.LocalSocket;

View File

@@ -1,5 +1,9 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.Options;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.video.ScreenInfo;
import com.genymobile.scrcpy.wrappers.ClipboardManager;
import com.genymobile.scrcpy.wrappers.DisplayControl;
import com.genymobile.scrcpy.wrappers.InputManager;
@@ -319,10 +323,22 @@ public final class Device {
* @param mode one of the {@code POWER_MODE_*} constants
*/
public static boolean setScreenPowerMode(int mode) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q;
if (applyToMultiPhysicalDisplays
&& Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE
&& Build.BRAND.equalsIgnoreCase("honor")
&& SurfaceControl.hasGetBuildInDisplayMethod()) {
// Workaround for Honor devices with Android 14:
// - <https://github.com/Genymobile/scrcpy/issues/4823>
// - <https://github.com/Genymobile/scrcpy/issues/4943>
applyToMultiPhysicalDisplays = false;
}
if (applyToMultiPhysicalDisplays) {
// 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();
Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod();
// Change the power mode for all physical displays
long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds();

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
public final class DisplayInfo {
private final int displayId;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import java.util.Objects;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import java.util.Objects;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import android.graphics.Rect;

View File

@@ -1,4 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.device;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.IO;
import android.media.MediaCodec;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public final class Binary {
private Binary() {

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public interface Codec {

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import java.util.ArrayList;
import java.util.List;

View File

@@ -1,4 +1,7 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.audio.AudioCodec;
import com.genymobile.scrcpy.video.VideoCodec;
import android.media.MediaCodecInfo;
import android.media.MediaCodecList;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import java.io.IOException;
import java.util.Arrays;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import android.os.Handler;

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.BuildConfig;
import android.system.ErrnoException;
import android.system.Os;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import android.util.Log;
@@ -19,7 +19,7 @@ public final class Ln {
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 {
public enum Level {
VERBOSE, DEBUG, INFO, WARN, ERROR
}

View File

@@ -1,5 +1,7 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.wrappers.ContentProvider;
import com.genymobile.scrcpy.wrappers.ServiceManager;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public class SettingsException extends Exception {
private static String createMessage(String method, String table, String key, String value) {

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
public final class StringUtils {
private StringUtils() {

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
public final class CameraAspectRatio {
private static final float SENSOR = -1;

View File

@@ -1,5 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.util.HandlerExecutor;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.SuppressLint;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import android.annotation.SuppressLint;
import android.hardware.camera2.CameraCharacteristics;
@@ -21,7 +21,7 @@ public enum CameraFacing {
return value;
}
static CameraFacing findByName(String name) {
public static CameraFacing findByName(String name) {
for (CameraFacing facing : CameraFacing.values()) {
if (name.equals(facing.name)) {
return facing;

View File

@@ -1,5 +1,8 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import com.genymobile.scrcpy.wrappers.SurfaceControl;

View File

@@ -1,4 +1,9 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.BuildConfig;
import com.genymobile.scrcpy.device.Device;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Size;
import android.graphics.Rect;

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.device.Size;
import android.view.Surface;

View File

@@ -1,4 +1,15 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.AsyncProcessor;
import com.genymobile.scrcpy.util.Codec;
import com.genymobile.scrcpy.util.CodecOption;
import com.genymobile.scrcpy.util.CodecUtils;
import com.genymobile.scrcpy.device.ConfigurationException;
import com.genymobile.scrcpy.util.IO;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.LogUtils;
import com.genymobile.scrcpy.device.Size;
import com.genymobile.scrcpy.device.Streamer;
import android.media.MediaCodec;
import android.media.MediaCodecInfo;

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
import com.genymobile.scrcpy.util.Codec;
import android.annotation.SuppressLint;
import android.media.MediaFormat;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.video;
public enum VideoSource {
DISPLAY("display"),
@@ -10,7 +10,7 @@ public enum VideoSource {
this.name = name;
}
static VideoSource findByName(String name) {
public static VideoSource findByName(String name) {
for (VideoSource videoSource : VideoSource.values()) {
if (name.equals(videoSource.name)) {
return videoSource;

View File

@@ -1,7 +1,7 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;

View File

@@ -1,7 +1,7 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.content.ClipData;
import android.content.IOnPrimaryClipChangedListener;
@@ -38,38 +38,61 @@ public final class ClipboardManager {
if (getPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class);
} else {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
getMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
} catch (NoSuchMethodException e2) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
} catch (NoSuchMethodException e3) {
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
} catch (NoSuchMethodException e4) {
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
} catch (NoSuchMethodException e5) {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class,
boolean.class);
getMethodVersion = 5;
}
}
}
}
}
return getPrimaryClipMethod;
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class);
getMethodVersion = 0;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class);
getMethodVersion = 1;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class);
getMethodVersion = 2;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class);
getMethodVersion = 3;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 4;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
try {
getPrimaryClipMethod = manager.getClass()
.getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class);
getMethodVersion = 5;
return getPrimaryClipMethod;
} catch (NoSuchMethodException e) {
// fall-through
}
getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class);
getMethodVersion = 6;
}
return getPrimaryClipMethod;
}
@@ -78,27 +101,37 @@ public final class ClipboardManager {
if (setPrimaryClipMethod == null) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class);
} else {
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
setMethodVersion = 0;
} catch (NoSuchMethodException e1) {
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1;
} catch (NoSuchMethodException e2) {
try {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
} catch (NoSuchMethodException e3) {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
setMethodVersion = 3;
}
}
}
return setPrimaryClipMethod;
}
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class);
setMethodVersion = 0;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e1) {
// fall-through
}
try {
setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class);
setMethodVersion = 1;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e2) {
// fall-through
}
try {
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class);
setMethodVersion = 2;
return setPrimaryClipMethod;
} catch (NoSuchMethodException e3) {
// fall-through
}
setPrimaryClipMethod = manager.getClass()
.getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class);
setMethodVersion = 3;
}
return setPrimaryClipMethod;
}
@@ -120,8 +153,10 @@ public final class ClipboardManager {
case 4:
// The last boolean parameter is "userOperate"
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true);
default:
case 5:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true);
default:
return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null);
}
}

View File

@@ -1,8 +1,8 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.FakeContext;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.SettingsException;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.util.SettingsException;
import android.annotation.SuppressLint;
import android.content.AttributionSource;

View File

@@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.annotation.TargetApi;

View File

@@ -1,9 +1,9 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Command;
import com.genymobile.scrcpy.DisplayInfo;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.Size;
import com.genymobile.scrcpy.util.Command;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.util.Ln;
import com.genymobile.scrcpy.device.Size;
import android.annotation.SuppressLint;
import android.hardware.display.VirtualDisplay;

View File

@@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.view.InputEvent;

View File

@@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.os.Build;

View File

@@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.os.IInterface;

View File

@@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.SuppressLint;
import android.graphics.Rect;
@@ -94,6 +94,15 @@ public final class SurfaceControl {
return getBuiltInDisplayMethod;
}
public static boolean hasGetBuildInDisplayMethod() {
try {
getGetBuiltInDisplayMethod();
return true;
} catch (NoSuchMethodException e) {
return false;
}
}
public static IBinder getBuiltInDisplay() {
try {
Method method = getGetBuiltInDisplayMethod();
@@ -134,7 +143,7 @@ public final class SurfaceControl {
return getPhysicalDisplayIdsMethod;
}
public static boolean hasPhysicalDisplayIdsMethod() {
public static boolean hasGetPhysicalDisplayIdsMethod() {
try {
getGetPhysicalDisplayIdsMethod();
return true;

View File

@@ -1,6 +1,6 @@
package com.genymobile.scrcpy.wrappers;
import com.genymobile.scrcpy.Ln;
import com.genymobile.scrcpy.util.Ln;
import android.annotation.TargetApi;
import android.os.IInterface;

View File

@@ -1,4 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import com.genymobile.scrcpy.device.Device;
import android.view.KeyEvent;
import android.view.MotionEvent;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.control;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import org.junit.Assert;
import org.junit.Test;

View File

@@ -1,5 +1,6 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import com.genymobile.scrcpy.device.DisplayInfo;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import android.view.Display;

View File

@@ -1,4 +1,4 @@
package com.genymobile.scrcpy;
package com.genymobile.scrcpy.util;
import org.junit.Assert;
import org.junit.Test;