Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd03f92c68 | ||
|
|
394b8e61ad | ||
|
|
409192cdff | ||
|
|
5d6df2e744 | ||
|
|
5e59ed3135 | ||
|
|
4eb33054cd | ||
|
|
420d3a40dd | ||
|
|
9d5f53caa7 | ||
|
|
3c45625324 | ||
|
|
11d738321f | ||
|
|
ccaa832f48 | ||
|
|
4e4ddc499f | ||
|
|
8d76b3e06d | ||
|
|
85a0b935c9 | ||
|
|
8c3e2bae7b | ||
|
|
446ea818a4 | ||
|
|
c3c7bf7af3 | ||
|
|
5000368c2f | ||
|
|
855ae4adb1 | ||
|
|
a8db3ec9e2 | ||
|
|
ff579990c2 | ||
|
|
b8c5853aa6 | ||
|
|
90ca46ee41 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -7,3 +7,4 @@ build/
|
|||||||
.gradle/
|
.gradle/
|
||||||
/x/
|
/x/
|
||||||
local.properties
|
local.properties
|
||||||
|
/scrcpy-server
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# scrcpy (v2.1.1)
|
# scrcpy (v2.2)
|
||||||
|
|
||||||
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
<img src="app/data/icon.svg" width="128" height="128" alt="scrcpy" align="right" />
|
||||||
|
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ _scrcpy() {
|
|||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--audio-codec)
|
--audio-codec)
|
||||||
COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur"))
|
COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur"))
|
||||||
return
|
return
|
||||||
;;
|
;;
|
||||||
--video-source)
|
--video-source)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ arguments=(
|
|||||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
||||||
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
|
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
|
||||||
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
||||||
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
||||||
'--audio-source=[Select the audio source]:source:(output mic)'
|
'--audio-source=[Select the audio source]:source:(output mic)'
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
DEP_DIR=platform-tools-34.0.3
|
DEP_DIR=platform-tools-34.0.5
|
||||||
|
|
||||||
FILENAME=platform-tools_r34.0.3-windows.zip
|
FILENAME=platform-tools_r34.0.5-windows.zip
|
||||||
SHA256SUM=fce992e93eb786fc9f47df93d83a7b912c46742d45c39d712c02e06d05b72e2b
|
SHA256SUM=3f8320152704377de150418a3c4c9d07d16d80a6c0d0d8f7289c22c499e33571
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
VERSION=6.0-scrcpy-4
|
VERSION=6.1-scrcpy
|
||||||
DEP_DIR="ffmpeg-$VERSION"
|
DEP_DIR="ffmpeg-$VERSION"
|
||||||
|
|
||||||
FILENAME="$DEP_DIR".7z
|
FILENAME="$DEP_DIR".7z
|
||||||
SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60
|
SHA256SUM=b41726e603f4624bb9ed7d2836e3e59d9d20b000e22a9ebd27055f4e99e48219
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ cd "$DIR"
|
|||||||
mkdir -p "$PREBUILT_DATA_DIR"
|
mkdir -p "$PREBUILT_DATA_DIR"
|
||||||
cd "$PREBUILT_DATA_DIR"
|
cd "$PREBUILT_DATA_DIR"
|
||||||
|
|
||||||
DEP_DIR=SDL2-2.28.0
|
DEP_DIR=SDL2-2.28.4
|
||||||
|
|
||||||
FILENAME=SDL2-devel-2.28.0-mingw.tar.gz
|
FILENAME=SDL2-devel-2.28.4-mingw.tar.gz
|
||||||
SHA256SUM=b91ce59eeacd4a9db403f976fd2337d9360b21ada374124417d716065c380e20
|
SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35
|
||||||
|
|
||||||
if [[ -d "$DEP_DIR" ]]
|
if [[ -d "$DEP_DIR" ]]
|
||||||
then
|
then
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ BEGIN
|
|||||||
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
VALUE "LegalCopyright", "Romain Vimont, Genymobile"
|
||||||
VALUE "OriginalFilename", "scrcpy.exe"
|
VALUE "OriginalFilename", "scrcpy.exe"
|
||||||
VALUE "ProductName", "scrcpy"
|
VALUE "ProductName", "scrcpy"
|
||||||
VALUE "ProductVersion", "2.1.1"
|
VALUE "ProductVersion", "v2.2"
|
||||||
END
|
END
|
||||||
END
|
END
|
||||||
BLOCK "VarFileInfo"
|
BLOCK "VarFileInfo"
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ Default is 50.
|
|||||||
|
|
||||||
.TP
|
.TP
|
||||||
.BI "\-\-audio\-codec " name
|
.BI "\-\-audio\-codec " name
|
||||||
Select an audio codec (opus, aac or raw).
|
Select an audio codec (opus, aac, flac or raw).
|
||||||
|
|
||||||
Default is opus.
|
Default is opus.
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ static const struct sc_option options[] = {
|
|||||||
.longopt_id = OPT_AUDIO_CODEC,
|
.longopt_id = OPT_AUDIO_CODEC,
|
||||||
.longopt = "audio-codec",
|
.longopt = "audio-codec",
|
||||||
.argdesc = "name",
|
.argdesc = "name",
|
||||||
.text = "Select an audio codec (opus, aac or raw).\n"
|
.text = "Select an audio codec (opus, aac, flac or raw).\n"
|
||||||
"Default is opus.",
|
"Default is opus.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -1626,6 +1626,9 @@ get_record_format(const char *name) {
|
|||||||
if (!strcmp(name, "aac")) {
|
if (!strcmp(name, "aac")) {
|
||||||
return SC_RECORD_FORMAT_AAC;
|
return SC_RECORD_FORMAT_AAC;
|
||||||
}
|
}
|
||||||
|
if (!strcmp(name, "flac")) {
|
||||||
|
return SC_RECORD_FORMAT_FLAC;
|
||||||
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1695,11 +1698,15 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
|||||||
*codec = SC_CODEC_AAC;
|
*codec = SC_CODEC_AAC;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (!strcmp(optarg, "flac")) {
|
||||||
|
*codec = SC_CODEC_FLAC;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (!strcmp(optarg, "raw")) {
|
if (!strcmp(optarg, "raw")) {
|
||||||
*codec = SC_CODEC_RAW;
|
*codec = SC_CODEC_RAW;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg);
|
LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", optarg);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2257,6 +2264,17 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
opts->require_audio = true;
|
opts->require_audio = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (opts->audio_playback && opts->audio_buffer == -1) {
|
||||||
|
// Use 50 ms audio buffer by default, but use a higher value for FLAC,
|
||||||
|
// which is not low latency (the default encoder produces blocks of
|
||||||
|
// 4096 samples, which represent ~85.333ms).
|
||||||
|
LOGI("FLAC audio: audio buffer increased to 120 ms (use "
|
||||||
|
"--audio-buffer to set a custom value)");
|
||||||
|
opts->audio_buffer = opts->audio_codec == SC_CODEC_FLAC
|
||||||
|
? SC_TICK_FROM_MS(120)
|
||||||
|
: SC_TICK_FROM_MS(50);
|
||||||
|
}
|
||||||
|
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
if (v4l2) {
|
if (v4l2) {
|
||||||
if (opts->lock_video_orientation ==
|
if (opts->lock_video_orientation ==
|
||||||
@@ -2353,7 +2371,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (opts->audio_codec == SC_CODEC_RAW) {
|
if (opts->audio_codec == SC_CODEC_RAW) {
|
||||||
LOGW("Recording does not support RAW audio codec");
|
LOGE("Recording does not support RAW audio codec");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2376,6 +2394,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
|||||||
"(try with --audio-codec=aac)");
|
"(try with --audio-codec=aac)");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (opts->record_format == SC_RECORD_FORMAT_FLAC
|
||||||
|
&& opts->audio_codec != SC_CODEC_FLAC) {
|
||||||
|
LOGE("Recording to FLAC file requires an FLAC audio stream "
|
||||||
|
"(try with --audio-codec=flac)");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) {
|
||||||
|
LOGW("--audio-bit-rate is ignored for FLAC audio codec");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (opts->audio_codec == SC_CODEC_RAW) {
|
if (opts->audio_codec == SC_CODEC_RAW) {
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|||||||
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
#define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII
|
||||||
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
#define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII
|
||||||
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
#define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII
|
||||||
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII
|
||||||
|
#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII
|
||||||
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
|
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
|
||||||
switch (codec_id) {
|
switch (codec_id) {
|
||||||
case SC_CODEC_ID_H264:
|
case SC_CODEC_ID_H264:
|
||||||
@@ -43,6 +44,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
|||||||
return AV_CODEC_ID_OPUS;
|
return AV_CODEC_ID_OPUS;
|
||||||
case SC_CODEC_ID_AAC:
|
case SC_CODEC_ID_AAC:
|
||||||
return AV_CODEC_ID_AAC;
|
return AV_CODEC_ID_AAC;
|
||||||
|
case SC_CODEC_ID_FLAC:
|
||||||
|
return AV_CODEC_ID_FLAC;
|
||||||
case SC_CODEC_ID_RAW:
|
case SC_CODEC_ID_RAW:
|
||||||
return AV_CODEC_ID_PCM_S16LE;
|
return AV_CODEC_ID_PCM_S16LE;
|
||||||
default:
|
default:
|
||||||
@@ -207,6 +210,11 @@ run_demuxer(void *data) {
|
|||||||
codec_ctx->channels = 2;
|
codec_ctx->channels = 2;
|
||||||
#endif
|
#endif
|
||||||
codec_ctx->sample_rate = 48000;
|
codec_ctx->sample_rate = 48000;
|
||||||
|
|
||||||
|
if (raw_codec_id == SC_CODEC_ID_FLAC) {
|
||||||
|
// The sample_fmt is not set by the FLAC decoder
|
||||||
|
codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
if (avcodec_open2(codec_ctx, codec, NULL) < 0) {
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
|||||||
.window_height = 0,
|
.window_height = 0,
|
||||||
.display_id = 0,
|
.display_id = 0,
|
||||||
.display_buffer = 0,
|
.display_buffer = 0,
|
||||||
.audio_buffer = SC_TICK_FROM_MS(50),
|
.audio_buffer = -1, // depends on the audio format,
|
||||||
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
||||||
.time_limit = 0,
|
.time_limit = 0,
|
||||||
#ifdef HAVE_V4L2
|
#ifdef HAVE_V4L2
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ enum sc_record_format {
|
|||||||
SC_RECORD_FORMAT_MKA,
|
SC_RECORD_FORMAT_MKA,
|
||||||
SC_RECORD_FORMAT_OPUS,
|
SC_RECORD_FORMAT_OPUS,
|
||||||
SC_RECORD_FORMAT_AAC,
|
SC_RECORD_FORMAT_AAC,
|
||||||
|
SC_RECORD_FORMAT_FLAC,
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
@@ -32,7 +33,8 @@ sc_record_format_is_audio_only(enum sc_record_format fmt) {
|
|||||||
return fmt == SC_RECORD_FORMAT_M4A
|
return fmt == SC_RECORD_FORMAT_M4A
|
||||||
|| fmt == SC_RECORD_FORMAT_MKA
|
|| fmt == SC_RECORD_FORMAT_MKA
|
||||||
|| fmt == SC_RECORD_FORMAT_OPUS
|
|| fmt == SC_RECORD_FORMAT_OPUS
|
||||||
|| fmt == SC_RECORD_FORMAT_AAC;
|
|| fmt == SC_RECORD_FORMAT_AAC
|
||||||
|
|| fmt == SC_RECORD_FORMAT_FLAC;
|
||||||
}
|
}
|
||||||
|
|
||||||
enum sc_codec {
|
enum sc_codec {
|
||||||
@@ -41,6 +43,7 @@ enum sc_codec {
|
|||||||
SC_CODEC_AV1,
|
SC_CODEC_AV1,
|
||||||
SC_CODEC_OPUS,
|
SC_CODEC_OPUS,
|
||||||
SC_CODEC_AAC,
|
SC_CODEC_AAC,
|
||||||
|
SC_CODEC_FLAC,
|
||||||
SC_CODEC_RAW,
|
SC_CODEC_RAW,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ sc_recorder_get_format_name(enum sc_record_format format) {
|
|||||||
return "matroska";
|
return "matroska";
|
||||||
case SC_RECORD_FORMAT_OPUS:
|
case SC_RECORD_FORMAT_OPUS:
|
||||||
return "opus";
|
return "opus";
|
||||||
|
case SC_RECORD_FORMAT_FLAC:
|
||||||
|
return "flac";
|
||||||
default:
|
default:
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -417,10 +417,14 @@ scrcpy(struct scrcpy_options *options) {
|
|||||||
|
|
||||||
if (options->video_playback) {
|
if (options->video_playback) {
|
||||||
sdl_set_hints(options->render_driver);
|
sdl_set_hints(options->render_driver);
|
||||||
if (SDL_Init(SDL_INIT_VIDEO)) {
|
}
|
||||||
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
|
||||||
goto end;
|
// Initialize the video subsystem even if --no-video or --no-video-playback
|
||||||
}
|
// is passed so that clipboard synchronization still works.
|
||||||
|
// <https://github.com/Genymobile/scrcpy/issues/4418>
|
||||||
|
if (SDL_Init(SDL_INIT_VIDEO)) {
|
||||||
|
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
||||||
|
goto end;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options->audio_playback) {
|
if (options->audio_playback) {
|
||||||
|
|||||||
@@ -178,6 +178,8 @@ sc_server_get_codec_name(enum sc_codec codec) {
|
|||||||
return "opus";
|
return "opus";
|
||||||
case SC_CODEC_AAC:
|
case SC_CODEC_AAC:
|
||||||
return "aac";
|
return "aac";
|
||||||
|
case SC_CODEC_FLAC:
|
||||||
|
return "flac";
|
||||||
case SC_CODEC_RAW:
|
case SC_CODEC_RAW:
|
||||||
return "raw";
|
return "raw";
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ cpu = 'i686'
|
|||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
[properties]
|
[properties]
|
||||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32'
|
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win32'
|
||||||
prebuilt_sdl2 = 'SDL2-2.28.0/i686-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32'
|
||||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
||||||
|
|||||||
@@ -16,6 +16,6 @@ cpu = 'x86_64'
|
|||||||
endian = 'little'
|
endian = 'little'
|
||||||
|
|
||||||
[properties]
|
[properties]
|
||||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64'
|
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win64'
|
||||||
prebuilt_sdl2 = 'SDL2-2.28.0/x86_64-w64-mingw32'
|
prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32'
|
||||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
|
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
|
||||||
|
|||||||
12
doc/audio.md
12
doc/audio.md
@@ -62,12 +62,13 @@ scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
|
|||||||
|
|
||||||
## Codec
|
## Codec
|
||||||
|
|
||||||
The audio codec can be selected. The possible values are `opus` (default), `aac`
|
The audio codec can be selected. The possible values are `opus` (default),
|
||||||
and `raw` (uncompressed PCM 16-bit LE):
|
`aac`, `flac` and `raw` (uncompressed PCM 16-bit LE):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
scrcpy --audio-codec=opus # default
|
scrcpy --audio-codec=opus # default
|
||||||
scrcpy --audio-codec=aac
|
scrcpy --audio-codec=aac
|
||||||
|
scrcpy --audio-codec=flac
|
||||||
scrcpy --audio-codec=raw
|
scrcpy --audio-codec=raw
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -80,7 +81,14 @@ then your device has no Opus encoder: try `scrcpy --audio-codec=aac`.
|
|||||||
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
|
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
|
||||||
check `--audio-codec-options` in the manpage or in `scrcpy --help`.
|
check `--audio-codec-options` in the manpage or in `scrcpy --help`.
|
||||||
|
|
||||||
|
For example, to change the [FLAC compression level]:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8
|
||||||
|
```
|
||||||
|
|
||||||
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
|
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
|
||||||
|
[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL
|
||||||
|
|
||||||
|
|
||||||
## Encoder
|
## Encoder
|
||||||
|
|||||||
@@ -233,10 +233,10 @@ install` must be run as root)._
|
|||||||
|
|
||||||
#### Option 2: Use prebuilt server
|
#### Option 2: Use prebuilt server
|
||||||
|
|
||||||
- [`scrcpy-server-v2.1.1`][direct-scrcpy-server]
|
- [`scrcpy-server-v2.2`][direct-scrcpy-server]
|
||||||
<sub>SHA-256: `9558db6c56743a1dc03b38f59801fb40e91cc891f8fc0c89e5b0b067761f148e`</sub>
|
<sub>SHA-256: `c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874`</sub>
|
||||||
|
|
||||||
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-server-v2.1.1
|
[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2
|
||||||
|
|
||||||
Download the prebuilt server somewhere, and specify its path during the Meson
|
Download the prebuilt server somewhere, and specify its path during the Meson
|
||||||
configuration:
|
configuration:
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ To record only the audio:
|
|||||||
```bash
|
```bash
|
||||||
scrcpy --no-video --record=file.opus
|
scrcpy --no-video --record=file.opus
|
||||||
scrcpy --no-video --audio-codec=aac --record=file.aac
|
scrcpy --no-video --audio-codec=aac --record=file.aac
|
||||||
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
|
scrcpy --no-video --audio-codec=flac --record=file.flac
|
||||||
|
# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac
|
||||||
```
|
```
|
||||||
|
|
||||||
Timestamps are captured on the device, so [packet delay variation] does not
|
Timestamps are captured on the device, so [packet delay variation] does not
|
||||||
|
|||||||
@@ -4,14 +4,14 @@
|
|||||||
|
|
||||||
Download the [latest release]:
|
Download the [latest release]:
|
||||||
|
|
||||||
- [`scrcpy-win64-v2.1.1.zip`][direct-win64] (64-bit)
|
- [`scrcpy-win64-v2.2.zip`][direct-win64] (64-bit)
|
||||||
<sub>SHA-256: `f77281e1bce2f9934617699c581f063d5b327f012eff602ee98fb2ef550c25c2`</sub>
|
<sub>SHA-256: `9f9da88ac4c8319dcb9bf852f2d9bba942bac663413383419cddf64eaa5685bd`</sub>
|
||||||
- [`scrcpy-win32-v2.1.1.zip`][direct-win32] (32-bit)
|
- [`scrcpy-win32-v2.2.zip`][direct-win32] (32-bit)
|
||||||
<sub>SHA-256: `ef7ae7fbe9449f2643febdc2244fb186d1a746a3c736394150cfd14f06d3c943`</sub>
|
<sub>SHA-256: `cb84269fc847b8b880e320879492a1ae6c017b42175f03e199530f7a53be9d74`</sub>
|
||||||
|
|
||||||
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
[latest release]: https://github.com/Genymobile/scrcpy/releases/latest
|
||||||
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-win64-v2.1.1.zip
|
[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win64-v2.2.zip
|
||||||
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-win32-v2.1.1.zip
|
[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win32-v2.2.zip
|
||||||
|
|
||||||
and extract it.
|
and extract it.
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
BUILDDIR=build-auto
|
BUILDDIR=build-auto
|
||||||
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.1.1/scrcpy-server-v2.1.1
|
PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2
|
||||||
PREBUILT_SERVER_SHA256=9558db6c56743a1dc03b38f59801fb40e91cc891f8fc0c89e5b0b067761f148e
|
PREBUILT_SERVER_SHA256=c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874
|
||||||
|
|
||||||
echo "[scrcpy] Downloading prebuilt server..."
|
echo "[scrcpy] Downloading prebuilt server..."
|
||||||
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
wget "$PREBUILT_SERVER_URL" -O scrcpy-server
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
project('scrcpy', 'c',
|
project('scrcpy', 'c',
|
||||||
version: '2.1.1',
|
version: 'v2.2',
|
||||||
meson_version: '>= 0.48',
|
meson_version: '>= 0.48',
|
||||||
default_options: [
|
default_options: [
|
||||||
'c_std=c11',
|
'c_std=c11',
|
||||||
|
|||||||
32
release.mk
32
release.mk
@@ -94,14 +94,14 @@ dist-win32: build-server build-win32
|
|||||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
|
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
|
cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
|
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.3/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/SDL2-2.28.0/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||||
|
|
||||||
dist-win64: build-server build-win64
|
dist-win64: build-server build-win64
|
||||||
@@ -112,14 +112,14 @@ dist-win64: build-server build-win64
|
|||||||
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
|
cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
|
cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.3/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/platform-tools-34.0.3/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/SDL2-2.28.0/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||||
|
|
||||||
zip-win32: dist-win32
|
zip-win32: dist-win32
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ android {
|
|||||||
applicationId "com.genymobile.scrcpy"
|
applicationId "com.genymobile.scrcpy"
|
||||||
minSdkVersion 21
|
minSdkVersion 21
|
||||||
targetSdkVersion 33
|
targetSdkVersion 33
|
||||||
versionCode 20101
|
versionCode 200
|
||||||
versionName "2.1.1"
|
versionName "v2.2"
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
set -e
|
set -e
|
||||||
|
|
||||||
SCRCPY_DEBUG=false
|
SCRCPY_DEBUG=false
|
||||||
SCRCPY_VERSION_NAME=2.1.1
|
SCRCPY_VERSION_NAME=v2.2
|
||||||
|
|
||||||
PLATFORM=${ANDROID_PLATFORM:-33}
|
PLATFORM=${ANDROID_PLATFORM:-33}
|
||||||
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
|
BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import android.media.MediaFormat;
|
|||||||
public enum AudioCodec implements Codec {
|
public enum AudioCodec implements Codec {
|
||||||
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
||||||
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC),
|
AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC),
|
||||||
|
FLAC(0x66_6c_61_63, "flac", MediaFormat.MIMETYPE_AUDIO_FLAC),
|
||||||
RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW);
|
RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW);
|
||||||
|
|
||||||
private final int id; // 4-byte ASCII representation of the name
|
private final int id; // 4-byte ASCII representation of the name
|
||||||
|
|||||||
@@ -32,7 +32,13 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
|||||||
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
capture.start();
|
try {
|
||||||
|
capture.start();
|
||||||
|
} catch (Throwable t) {
|
||||||
|
// Notify the client that the audio could not be captured
|
||||||
|
streamer.writeDisableStream(false);
|
||||||
|
throw t;
|
||||||
|
}
|
||||||
|
|
||||||
streamer.writeAudioHeader();
|
streamer.writeAudioHeader();
|
||||||
while (!Thread.currentThread().isInterrupted()) {
|
while (!Thread.currentThread().isInterrupted()) {
|
||||||
@@ -45,10 +51,11 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
|||||||
|
|
||||||
streamer.writePacket(buffer, bufferInfo);
|
streamer.writePacket(buffer, bufferInfo);
|
||||||
}
|
}
|
||||||
} catch (Throwable e) {
|
} catch (IOException e) {
|
||||||
// Notify the client that the audio could not be captured
|
// Broken pipe is expected on close, because the socket is closed by the client
|
||||||
streamer.writeDisableStream(false);
|
if (!IO.isBrokenPipe(e)) {
|
||||||
throw e;
|
Ln.e("Audio capture error", e);
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
capture.stop();
|
capture.stop();
|
||||||
}
|
}
|
||||||
@@ -62,8 +69,8 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
|||||||
record();
|
record();
|
||||||
} catch (AudioCaptureForegroundException e) {
|
} catch (AudioCaptureForegroundException e) {
|
||||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||||
} catch (IOException e) {
|
} catch (Throwable t) {
|
||||||
Ln.e("Audio recording error", e);
|
Ln.e("Audio recording error", t);
|
||||||
fatalError = true;
|
fatalError = true;
|
||||||
} finally {
|
} finally {
|
||||||
Ln.d("Audio recorder stopped");
|
Ln.d("Audio recorder stopped");
|
||||||
|
|||||||
@@ -132,20 +132,29 @@ public final class DesktopConnection implements Closeable {
|
|||||||
return controlSocket;
|
return controlSocket;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void close() throws IOException {
|
public void shutdown() throws IOException {
|
||||||
if (videoSocket != null) {
|
if (videoSocket != null) {
|
||||||
videoSocket.shutdownInput();
|
videoSocket.shutdownInput();
|
||||||
videoSocket.shutdownOutput();
|
videoSocket.shutdownOutput();
|
||||||
videoSocket.close();
|
|
||||||
}
|
}
|
||||||
if (audioSocket != null) {
|
if (audioSocket != null) {
|
||||||
audioSocket.shutdownInput();
|
audioSocket.shutdownInput();
|
||||||
audioSocket.shutdownOutput();
|
audioSocket.shutdownOutput();
|
||||||
audioSocket.close();
|
|
||||||
}
|
}
|
||||||
if (controlSocket != null) {
|
if (controlSocket != null) {
|
||||||
controlSocket.shutdownInput();
|
controlSocket.shutdownInput();
|
||||||
controlSocket.shutdownOutput();
|
controlSocket.shutdownOutput();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void close() throws IOException {
|
||||||
|
if (videoSocket != null) {
|
||||||
|
videoSocket.close();
|
||||||
|
}
|
||||||
|
if (audioSocket != null) {
|
||||||
|
audioSocket.close();
|
||||||
|
}
|
||||||
|
if (controlSocket != null) {
|
||||||
controlSocket.close();
|
controlSocket.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package com.genymobile.scrcpy;
|
|||||||
|
|
||||||
import android.annotation.TargetApi;
|
import android.annotation.TargetApi;
|
||||||
import android.content.AttributionSource;
|
import android.content.AttributionSource;
|
||||||
import android.content.MutableContextWrapper;
|
import android.content.Context;
|
||||||
|
import android.content.ContextWrapper;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Process;
|
import android.os.Process;
|
||||||
|
|
||||||
public final class FakeContext extends MutableContextWrapper {
|
public final class FakeContext extends ContextWrapper {
|
||||||
|
|
||||||
public static final String PACKAGE_NAME = "com.android.shell";
|
public static final String PACKAGE_NAME = "com.android.shell";
|
||||||
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
|
public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29
|
||||||
@@ -18,7 +19,7 @@ public final class FakeContext extends MutableContextWrapper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private FakeContext() {
|
private FakeContext() {
|
||||||
super(null);
|
super(Workarounds.getSystemContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -44,4 +45,9 @@ public final class FakeContext extends MutableContextWrapper {
|
|||||||
public int getDeviceId() {
|
public int getDeviceId() {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Context getApplicationContext() {
|
||||||
|
return this;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,33 +2,37 @@ package com.genymobile.scrcpy;
|
|||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.io.FileDescriptor;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.io.PrintStream;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal
|
* Log both to Android logger (so that logs are visible in "adb logcat") and standard output/error (so that they are visible in the terminal
|
||||||
* directly).
|
* directly).
|
||||||
*/
|
*/
|
||||||
public final class Ln {
|
public final class Ln {
|
||||||
|
|
||||||
public interface ConsolePrinter {
|
|
||||||
void printOut(String message);
|
|
||||||
void printErr(String message, Throwable throwable);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static final String TAG = "scrcpy";
|
private static final String TAG = "scrcpy";
|
||||||
private static final String PREFIX = "[server] ";
|
private static final String PREFIX = "[server] ";
|
||||||
|
|
||||||
|
private static final PrintStream CONSOLE_OUT = new PrintStream(new FileOutputStream(FileDescriptor.out));
|
||||||
|
private static final PrintStream CONSOLE_ERR = new PrintStream(new FileOutputStream(FileDescriptor.err));
|
||||||
|
|
||||||
enum Level {
|
enum Level {
|
||||||
VERBOSE, DEBUG, INFO, WARN, ERROR
|
VERBOSE, DEBUG, INFO, WARN, ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ConsolePrinter consolePrinter = new DefaultConsolePrinter();
|
|
||||||
private static Level threshold = Level.INFO;
|
private static Level threshold = Level.INFO;
|
||||||
|
|
||||||
private Ln() {
|
private Ln() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void setConsolePrinter(ConsolePrinter consolePrinter) {
|
public static void disableSystemStreams() {
|
||||||
Ln.consolePrinter = consolePrinter;
|
PrintStream nullStream = new PrintStream(new NullOutputStream());
|
||||||
|
System.setOut(nullStream);
|
||||||
|
System.setErr(nullStream);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,28 +53,31 @@ public final class Ln {
|
|||||||
public static void v(String message) {
|
public static void v(String message) {
|
||||||
if (isEnabled(Level.VERBOSE)) {
|
if (isEnabled(Level.VERBOSE)) {
|
||||||
Log.v(TAG, message);
|
Log.v(TAG, message);
|
||||||
consolePrinter.printOut(PREFIX + "VERBOSE: " + message + '\n');
|
CONSOLE_OUT.print(PREFIX + "VERBOSE: " + message + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void d(String message) {
|
public static void d(String message) {
|
||||||
if (isEnabled(Level.DEBUG)) {
|
if (isEnabled(Level.DEBUG)) {
|
||||||
Log.d(TAG, message);
|
Log.d(TAG, message);
|
||||||
consolePrinter.printOut(PREFIX + "DEBUG: " + message + '\n');
|
CONSOLE_OUT.print(PREFIX + "DEBUG: " + message + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void i(String message) {
|
public static void i(String message) {
|
||||||
if (isEnabled(Level.INFO)) {
|
if (isEnabled(Level.INFO)) {
|
||||||
Log.i(TAG, message);
|
Log.i(TAG, message);
|
||||||
consolePrinter.printOut(PREFIX + "INFO: " + message + '\n');
|
CONSOLE_OUT.print(PREFIX + "INFO: " + message + '\n');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void w(String message, Throwable throwable) {
|
public static void w(String message, Throwable throwable) {
|
||||||
if (isEnabled(Level.WARN)) {
|
if (isEnabled(Level.WARN)) {
|
||||||
Log.w(TAG, message, throwable);
|
Log.w(TAG, message, throwable);
|
||||||
consolePrinter.printErr(PREFIX + "WARN: " + message + '\n', throwable);
|
CONSOLE_ERR.print(PREFIX + "WARN: " + message + '\n');
|
||||||
|
if (throwable != null) {
|
||||||
|
throwable.printStackTrace(CONSOLE_ERR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,7 +88,10 @@ public final class Ln {
|
|||||||
public static void e(String message, Throwable throwable) {
|
public static void e(String message, Throwable throwable) {
|
||||||
if (isEnabled(Level.ERROR)) {
|
if (isEnabled(Level.ERROR)) {
|
||||||
Log.e(TAG, message, throwable);
|
Log.e(TAG, message, throwable);
|
||||||
consolePrinter.printErr(PREFIX + "ERROR: " + message + '\n', throwable);
|
CONSOLE_ERR.print(PREFIX + "ERROR: " + message + '\n');
|
||||||
|
if (throwable != null) {
|
||||||
|
throwable.printStackTrace(CONSOLE_ERR);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,18 +99,20 @@ public final class Ln {
|
|||||||
e(message, null);
|
e(message, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class DefaultConsolePrinter implements ConsolePrinter {
|
static class NullOutputStream extends OutputStream {
|
||||||
@Override
|
@Override
|
||||||
public void printOut(String message) {
|
public void write(byte[] b) {
|
||||||
System.out.print(message);
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void printErr(String message, Throwable throwable) {
|
public void write(byte[] b, int off, int len) {
|
||||||
System.err.print(message);
|
// ignore
|
||||||
if (throwable != null) {
|
}
|
||||||
throwable.printStackTrace();
|
|
||||||
}
|
@Override
|
||||||
|
public void write(int b) {
|
||||||
|
// ignore
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,19 +93,26 @@ public final class LogUtils {
|
|||||||
builder.append("\n (none)");
|
builder.append("\n (none)");
|
||||||
} else {
|
} else {
|
||||||
for (String id : cameraIds) {
|
for (String id : cameraIds) {
|
||||||
builder.append("\n --video-source=camera --camera-id=").append(id);
|
builder.append("\n --camera-id=").append(id);
|
||||||
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
|
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
|
||||||
|
|
||||||
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
|
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
|
||||||
builder.append(" (").append(getCameraFacingName(facing)).append(", ");
|
builder.append(" (").append(getCameraFacingName(facing)).append(", ");
|
||||||
|
|
||||||
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
|
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
|
||||||
builder.append(activeSize.width()).append("x").append(activeSize.height()).append(", ");
|
builder.append(activeSize.width()).append("x").append(activeSize.height());
|
||||||
|
|
||||||
// Capture frame rates for low-FPS mode are the same for every resolution
|
try {
|
||||||
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
|
// Capture frame rates for low-FPS mode are the same for every resolution
|
||||||
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
|
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
|
||||||
builder.append("fps=").append(uniqueLowFps).append(')');
|
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
|
||||||
|
builder.append(", fps=").append(uniqueLowFps);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper"
|
||||||
|
Ln.w("Could not get available frame rates for camera " + id, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.append(')');
|
||||||
|
|
||||||
if (includeSizes) {
|
if (includeSizes) {
|
||||||
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
|
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
|
||||||
|
|||||||
@@ -163,6 +163,8 @@ public final class Server {
|
|||||||
asyncProcessor.stop();
|
asyncProcessor.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connection.shutdown();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
initThread.join();
|
initThread.join();
|
||||||
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
for (AsyncProcessor asyncProcessor : asyncProcessors) {
|
||||||
@@ -187,7 +189,7 @@ public final class Server {
|
|||||||
try {
|
try {
|
||||||
internalMain(args);
|
internalMain(args);
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
t.printStackTrace();
|
Ln.e(t.getMessage(), t);
|
||||||
status = 1;
|
status = 1;
|
||||||
} finally {
|
} finally {
|
||||||
// By default, the Java process exits when all non-daemon threads are terminated.
|
// By default, the Java process exits when all non-daemon threads are terminated.
|
||||||
@@ -204,6 +206,7 @@ public final class Server {
|
|||||||
|
|
||||||
Options options = Options.parse(args);
|
Options options = Options.parse(args);
|
||||||
|
|
||||||
|
Ln.disableSystemStreams();
|
||||||
Ln.initLogLevel(options.getLogLevel());
|
Ln.initLogLevel(options.getLogLevel());
|
||||||
|
|
||||||
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
Ln.i("Device: [" + Build.MANUFACTURER + "] " + Build.BRAND + " " + Build.MODEL + " (Android " + Build.VERSION.RELEASE + ")");
|
||||||
|
|||||||
@@ -5,14 +5,14 @@ import android.media.MediaCodec;
|
|||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.ByteBuffer;
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
public final class Streamer {
|
public final class Streamer {
|
||||||
|
|
||||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||||
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
private static final long PACKET_FLAG_KEY_FRAME = 1L << 62;
|
||||||
|
|
||||||
private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian)
|
|
||||||
|
|
||||||
private final FileDescriptor fd;
|
private final FileDescriptor fd;
|
||||||
private final Codec codec;
|
private final Codec codec;
|
||||||
private final boolean sendCodecMeta;
|
private final boolean sendCodecMeta;
|
||||||
@@ -30,6 +30,7 @@ public final class Streamer {
|
|||||||
public Codec getCodec() {
|
public Codec getCodec() {
|
||||||
return codec;
|
return codec;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeAudioHeader() throws IOException {
|
public void writeAudioHeader() throws IOException {
|
||||||
if (sendCodecMeta) {
|
if (sendCodecMeta) {
|
||||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||||
@@ -62,8 +63,12 @@ public final class Streamer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException {
|
public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException {
|
||||||
if (config && codec == AudioCodec.OPUS) {
|
if (config) {
|
||||||
fixOpusConfigPacket(buffer);
|
if (codec == AudioCodec.OPUS) {
|
||||||
|
fixOpusConfigPacket(buffer);
|
||||||
|
} else if (codec == AudioCodec.FLAC) {
|
||||||
|
fixFlacConfigPacket(buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sendFrameMeta) {
|
if (sendFrameMeta) {
|
||||||
@@ -120,11 +125,14 @@ public final class Streamer {
|
|||||||
throw new IOException("Not enough data in OPUS config packet");
|
throw new IOException("Not enough data in OPUS config packet");
|
||||||
}
|
}
|
||||||
|
|
||||||
long id = buffer.getLong();
|
final byte[] opusHeaderId = {'A', 'O', 'P', 'U', 'S', 'H', 'D', 'R'};
|
||||||
if (id != AOPUSHDR) {
|
byte[] idBuffer = new byte[8];
|
||||||
|
buffer.get(idBuffer);
|
||||||
|
if (!Arrays.equals(idBuffer, opusHeaderId)) {
|
||||||
throw new IOException("OPUS header not found");
|
throw new IOException("OPUS header not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The size is in native byte-order
|
||||||
long sizeLong = buffer.getLong();
|
long sizeLong = buffer.getLong();
|
||||||
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
|
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
|
||||||
throw new IOException("Invalid block size in OPUS header: " + sizeLong);
|
throw new IOException("Invalid block size in OPUS header: " + sizeLong);
|
||||||
@@ -138,4 +146,41 @@ public final class Streamer {
|
|||||||
// Set the buffer to point to the OPUS header slice
|
// Set the buffer to point to the OPUS header slice
|
||||||
buffer.limit(buffer.position() + size);
|
buffer.limit(buffer.position() + size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void fixFlacConfigPacket(ByteBuffer buffer) throws IOException {
|
||||||
|
// 00000000 66 4c 61 43 00 00 00 22 |fLaC..." |
|
||||||
|
// -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA -------------------
|
||||||
|
// 00000000 10 00 10 00 00 00 00 00 | ........|
|
||||||
|
// 00000010 00 00 0b b8 02 f0 00 00 00 00 00 00 00 00 00 00 |................|
|
||||||
|
// 00000020 00 00 00 00 00 00 00 00 00 00 |.......... |
|
||||||
|
// ------------------------------------------------------------------------------
|
||||||
|
// 00000020 84 00 00 28 20 00 | ...( .|
|
||||||
|
// 00000030 00 00 72 65 66 65 72 65 6e 63 65 20 6c 69 62 46 |..reference libF|
|
||||||
|
// 00000040 4c 41 43 20 31 2e 33 2e 32 20 32 30 32 32 31 30 |LAC 1.3.2 202210|
|
||||||
|
// 00000050 32 32 00 00 00 00 |22....|
|
||||||
|
//
|
||||||
|
// <https://developer.android.com/reference/android/media/MediaCodec#CSD>
|
||||||
|
|
||||||
|
if (buffer.remaining() < 8) {
|
||||||
|
throw new IOException("Not enough data in FLAC config packet");
|
||||||
|
}
|
||||||
|
|
||||||
|
final byte[] flacHeaderId = {'f', 'L', 'a', 'C'};
|
||||||
|
byte[] idBuffer = new byte[4];
|
||||||
|
buffer.get(idBuffer);
|
||||||
|
if (!Arrays.equals(idBuffer, flacHeaderId)) {
|
||||||
|
throw new IOException("FLAC header not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
// The size is in big-endian
|
||||||
|
buffer.order(ByteOrder.BIG_ENDIAN);
|
||||||
|
|
||||||
|
int size = buffer.getInt();
|
||||||
|
if (buffer.remaining() < size) {
|
||||||
|
throw new IOException("Not enough data in FLAC header (invalid size: " + size + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the buffer to point to the FLAC header slice
|
||||||
|
buffer.limit(buffer.position() + size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,12 +14,6 @@ import android.os.Build;
|
|||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
|
|
||||||
import java.io.FileDescriptor;
|
|
||||||
import java.io.FileOutputStream;
|
|
||||||
import java.io.OutputStream;
|
|
||||||
import java.io.PrintStream;
|
|
||||||
import java.io.PrintWriter;
|
|
||||||
import java.io.StringWriter;
|
|
||||||
import java.lang.ref.WeakReference;
|
import java.lang.ref.WeakReference;
|
||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.Field;
|
import java.lang.reflect.Field;
|
||||||
@@ -27,18 +21,34 @@ import java.lang.reflect.Method;
|
|||||||
|
|
||||||
public final class Workarounds {
|
public final class Workarounds {
|
||||||
|
|
||||||
private static Class<?> activityThreadClass;
|
private static final Class<?> ACTIVITY_THREAD_CLASS;
|
||||||
private static Object activityThread;
|
private static final Object ACTIVITY_THREAD;
|
||||||
|
|
||||||
|
static {
|
||||||
|
prepareMainLooper();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ActivityThread activityThread = new ActivityThread();
|
||||||
|
ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread");
|
||||||
|
Constructor<?> activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor();
|
||||||
|
activityThreadConstructor.setAccessible(true);
|
||||||
|
ACTIVITY_THREAD = activityThreadConstructor.newInstance();
|
||||||
|
|
||||||
|
// ActivityThread.sCurrentActivityThread = activityThread;
|
||||||
|
Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread");
|
||||||
|
sCurrentActivityThreadField.setAccessible(true);
|
||||||
|
sCurrentActivityThreadField.set(null, ACTIVITY_THREAD);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new AssertionError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private Workarounds() {
|
private Workarounds() {
|
||||||
// not instantiable
|
// not instantiable
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void apply(boolean audio, boolean camera) {
|
public static void apply(boolean audio, boolean camera) {
|
||||||
Workarounds.prepareMainLooper();
|
|
||||||
|
|
||||||
boolean mustFillAppInfo = false;
|
boolean mustFillAppInfo = false;
|
||||||
boolean mustFillBaseContext = false;
|
|
||||||
boolean mustFillAppContext = false;
|
boolean mustFillAppContext = false;
|
||||||
|
|
||||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||||
@@ -59,14 +69,7 @@ public final class Workarounds {
|
|||||||
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
|
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
|
||||||
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
|
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
|
||||||
mustFillAppInfo = true;
|
mustFillAppInfo = true;
|
||||||
mustFillBaseContext = true;
|
|
||||||
mustFillAppContext = true;
|
mustFillAppContext = true;
|
||||||
} else if (Build.MANUFACTURER.equalsIgnoreCase("xiaomi")) {
|
|
||||||
// Trash the messages printed to the console by direct calls to System.out and System.err.
|
|
||||||
// Xiaomi device ROMs may print internal errors using e.printStackTrace(), flooding the console with irrelevant errors.
|
|
||||||
ExclusiveConsolePrinter exclusiveConsolePrinter = new ExclusiveConsolePrinter();
|
|
||||||
exclusiveConsolePrinter.installNullSystemStreams();
|
|
||||||
Ln.setConsolePrinter(new ExclusiveConsolePrinter());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
if (audio && Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
|
||||||
@@ -78,15 +81,12 @@ public final class Workarounds {
|
|||||||
|
|
||||||
if (camera) {
|
if (camera) {
|
||||||
mustFillAppInfo = true;
|
mustFillAppInfo = true;
|
||||||
mustFillBaseContext = true;
|
mustFillAppContext = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mustFillAppInfo) {
|
if (mustFillAppInfo) {
|
||||||
Workarounds.fillAppInfo();
|
Workarounds.fillAppInfo();
|
||||||
}
|
}
|
||||||
if (mustFillBaseContext) {
|
|
||||||
Workarounds.fillBaseContext();
|
|
||||||
}
|
|
||||||
if (mustFillAppContext) {
|
if (mustFillAppContext) {
|
||||||
Workarounds.fillAppContext();
|
Workarounds.fillAppContext();
|
||||||
}
|
}
|
||||||
@@ -105,27 +105,9 @@ public final class Workarounds {
|
|||||||
Looper.prepareMainLooper();
|
Looper.prepareMainLooper();
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
|
||||||
private static void fillActivityThread() throws Exception {
|
|
||||||
if (activityThread == null) {
|
|
||||||
// ActivityThread activityThread = new ActivityThread();
|
|
||||||
activityThreadClass = Class.forName("android.app.ActivityThread");
|
|
||||||
Constructor<?> activityThreadConstructor = activityThreadClass.getDeclaredConstructor();
|
|
||||||
activityThreadConstructor.setAccessible(true);
|
|
||||||
activityThread = activityThreadConstructor.newInstance();
|
|
||||||
|
|
||||||
// ActivityThread.sCurrentActivityThread = activityThread;
|
|
||||||
Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread");
|
|
||||||
sCurrentActivityThreadField.setAccessible(true);
|
|
||||||
sCurrentActivityThreadField.set(null, activityThread);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
private static void fillAppInfo() {
|
private static void fillAppInfo() {
|
||||||
try {
|
try {
|
||||||
fillActivityThread();
|
|
||||||
|
|
||||||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||||
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
|
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
|
||||||
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
|
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
|
||||||
@@ -141,9 +123,9 @@ public final class Workarounds {
|
|||||||
appInfoField.set(appBindData, applicationInfo);
|
appInfoField.set(appBindData, applicationInfo);
|
||||||
|
|
||||||
// activityThread.mBoundApplication = appBindData;
|
// activityThread.mBoundApplication = appBindData;
|
||||||
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication");
|
||||||
mBoundApplicationField.setAccessible(true);
|
mBoundApplicationField.setAccessible(true);
|
||||||
mBoundApplicationField.set(activityThread, appBindData);
|
mBoundApplicationField.set(ACTIVITY_THREAD, appBindData);
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
// this is a workaround, so failing is not an error
|
// this is a workaround, so failing is not an error
|
||||||
Ln.d("Could not fill app info: " + throwable.getMessage());
|
Ln.d("Could not fill app info: " + throwable.getMessage());
|
||||||
@@ -153,33 +135,29 @@ public final class Workarounds {
|
|||||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||||
private static void fillAppContext() {
|
private static void fillAppContext() {
|
||||||
try {
|
try {
|
||||||
fillActivityThread();
|
Application app = new Application();
|
||||||
|
|
||||||
Application app = Application.class.newInstance();
|
|
||||||
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||||
baseField.setAccessible(true);
|
baseField.setAccessible(true);
|
||||||
baseField.set(app, FakeContext.get());
|
baseField.set(app, FakeContext.get());
|
||||||
|
|
||||||
// activityThread.mInitialApplication = app;
|
// activityThread.mInitialApplication = app;
|
||||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication");
|
||||||
mInitialApplicationField.setAccessible(true);
|
mInitialApplicationField.setAccessible(true);
|
||||||
mInitialApplicationField.set(activityThread, app);
|
mInitialApplicationField.set(ACTIVITY_THREAD, app);
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
// this is a workaround, so failing is not an error
|
// this is a workaround, so failing is not an error
|
||||||
Ln.d("Could not fill app context: " + throwable.getMessage());
|
Ln.d("Could not fill app context: " + throwable.getMessage());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void fillBaseContext() {
|
static Context getSystemContext() {
|
||||||
try {
|
try {
|
||||||
fillActivityThread();
|
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext");
|
||||||
|
return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD);
|
||||||
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
|
||||||
Context context = (Context) getSystemContextMethod.invoke(activityThread);
|
|
||||||
FakeContext.get().setBaseContext(context);
|
|
||||||
} catch (Throwable throwable) {
|
} catch (Throwable throwable) {
|
||||||
// this is a workaround, so failing is not an error
|
// this is a workaround, so failing is not an error
|
||||||
Ln.d("Could not fill base context: " + throwable.getMessage());
|
Ln.d("Could not get system context: " + throwable.getMessage());
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -318,48 +296,4 @@ public final class Workarounds {
|
|||||||
throw new RuntimeException("Cannot create AudioRecord");
|
throw new RuntimeException("Cannot create AudioRecord");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ExclusiveConsolePrinter implements Ln.ConsolePrinter {
|
|
||||||
|
|
||||||
static class NullOutputStream extends OutputStream {
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(byte[] b, int off, int len) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void write(int b) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private final PrintStream realOut = new PrintStream(new FileOutputStream(FileDescriptor.out));
|
|
||||||
private final PrintStream realErr = new PrintStream(new FileOutputStream(FileDescriptor.err));
|
|
||||||
|
|
||||||
void installNullSystemStreams() {
|
|
||||||
PrintStream nullStream = new PrintStream(new NullOutputStream());
|
|
||||||
System.setOut(nullStream);
|
|
||||||
System.setErr(nullStream);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void printOut(String message) {
|
|
||||||
realOut.print(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void printErr(String message, Throwable throwable) {
|
|
||||||
realErr.print(message);
|
|
||||||
if (throwable != null) {
|
|
||||||
StringWriter errors = new StringWriter();
|
|
||||||
throwable.printStackTrace(new PrintWriter(errors));
|
|
||||||
realErr.print(errors);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user