mirror of
https://github.com/Genymobile/scrcpy.git
synced 2026-03-04 11:14:28 +01:00
Compare commits
24 Commits
bindings_s
...
workaround
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b6089cbfc | ||
|
|
f691ebb1b4 | ||
|
|
071d459ad7 | ||
|
|
ed4066902d | ||
|
|
127a271d34 | ||
|
|
31116a60d7 | ||
|
|
a10f8cd798 | ||
|
|
53c6eb66ea | ||
|
|
0f076083e8 | ||
|
|
053bf83f58 | ||
|
|
414ce4c754 | ||
|
|
a2f3a5cf18 | ||
|
|
5e605b9b8f | ||
|
|
cf09e78323 | ||
|
|
3b8ec0c38d | ||
|
|
39132ff2dd | ||
|
|
9d1d79b004 | ||
|
|
e0cdc2ace3 | ||
|
|
bbcd763612 | ||
|
|
c57a0512ba | ||
|
|
e84db2914d | ||
|
|
80ca7b15e5 | ||
|
|
79242957a0 | ||
|
|
fe7494c492 |
@@ -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)
|
||||
|
||||
@@ -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]'
|
||||
|
||||
14
app/scrcpy.1
14
app/scrcpy.1
@@ -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.
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -101,6 +101,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.list = 0,
|
||||
.window = true,
|
||||
.mouse_hover = true,
|
||||
.audio_dup = false,
|
||||
};
|
||||
|
||||
enum sc_orientation
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
24
doc/audio.md
24
doc/audio.md
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.audio;
|
||||
|
||||
import com.genymobile.scrcpy.util.Codec;
|
||||
|
||||
import android.media.MediaFormat;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import android.net.LocalSocket;
|
||||
|
||||
@@ -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}.
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
public final class DeviceMessage {
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
@@ -1,4 +1,6 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
|
||||
public class Pointer {
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import com.genymobile.scrcpy.device.Point;
|
||||
|
||||
import android.view.MotionEvent;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
public class ConfigurationException extends Exception {
|
||||
public ConfigurationException(String message) {
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
public final class DisplayInfo {
|
||||
private final int displayId;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.device;
|
||||
|
||||
import android.graphics.Rect;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
public final class Binary {
|
||||
private Binary() {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
public interface Codec {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import android.os.Handler;
|
||||
|
||||
@@ -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;
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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) {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
public final class StringUtils {
|
||||
private StringUtils() {
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
public final class CameraAspectRatio {
|
||||
private static final float SENSOR = -1;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.video;
|
||||
|
||||
import com.genymobile.scrcpy.device.Size;
|
||||
|
||||
import android.view.Surface;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.genymobile.scrcpy.wrappers;
|
||||
|
||||
import com.genymobile.scrcpy.Ln;
|
||||
import com.genymobile.scrcpy.util.Ln;
|
||||
|
||||
import android.os.IInterface;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.control;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
@@ -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;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.genymobile.scrcpy;
|
||||
package com.genymobile.scrcpy.util;
|
||||
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
Reference in New Issue
Block a user