Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f11b096a | ||
|
|
25e33566f5 |
@@ -97,7 +97,7 @@ _scrcpy() {
|
||||
return
|
||||
;;
|
||||
--audio-codec)
|
||||
COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur"))
|
||||
COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur"))
|
||||
return
|
||||
;;
|
||||
--video-source)
|
||||
|
||||
@@ -11,7 +11,7 @@ arguments=(
|
||||
'--always-on-top[Make scrcpy window always on top \(above other windows\)]'
|
||||
'--audio-bit-rate=[Encode the audio at the given bit-rate]'
|
||||
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
|
||||
'--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)'
|
||||
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
|
||||
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
|
||||
'--audio-encoder=[Use a specific MediaCodec audio encoder]'
|
||||
'--audio-source=[Select the audio source]:source:(output mic)'
|
||||
|
||||
@@ -6,11 +6,11 @@ cd "$DIR"
|
||||
mkdir -p "$PREBUILT_DATA_DIR"
|
||||
cd "$PREBUILT_DATA_DIR"
|
||||
|
||||
VERSION=6.1-scrcpy
|
||||
VERSION=6.0-scrcpy-4
|
||||
DEP_DIR="ffmpeg-$VERSION"
|
||||
|
||||
FILENAME="$DEP_DIR".7z
|
||||
SHA256SUM=b41726e603f4624bb9ed7d2836e3e59d9d20b000e22a9ebd27055f4e99e48219
|
||||
SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60
|
||||
|
||||
if [[ -d "$DEP_DIR" ]]
|
||||
then
|
||||
|
||||
@@ -35,7 +35,7 @@ Default is 50.
|
||||
|
||||
.TP
|
||||
.BI "\-\-audio\-codec " name
|
||||
Select an audio codec (opus, aac, flac or raw).
|
||||
Select an audio codec (opus, aac or raw).
|
||||
|
||||
Default is opus.
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ static const struct sc_option options[] = {
|
||||
.longopt_id = OPT_AUDIO_CODEC,
|
||||
.longopt = "audio-codec",
|
||||
.argdesc = "name",
|
||||
.text = "Select an audio codec (opus, aac, flac or raw).\n"
|
||||
.text = "Select an audio codec (opus, aac or raw).\n"
|
||||
"Default is opus.",
|
||||
},
|
||||
{
|
||||
@@ -1626,9 +1626,6 @@ get_record_format(const char *name) {
|
||||
if (!strcmp(name, "aac")) {
|
||||
return SC_RECORD_FORMAT_AAC;
|
||||
}
|
||||
if (!strcmp(name, "flac")) {
|
||||
return SC_RECORD_FORMAT_FLAC;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -1698,15 +1695,11 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) {
|
||||
*codec = SC_CODEC_AAC;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "flac")) {
|
||||
*codec = SC_CODEC_FLAC;
|
||||
return true;
|
||||
}
|
||||
if (!strcmp(optarg, "raw")) {
|
||||
*codec = SC_CODEC_RAW;
|
||||
return true;
|
||||
}
|
||||
LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", optarg);
|
||||
LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2264,19 +2257,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
opts->require_audio = true;
|
||||
}
|
||||
|
||||
if (opts->audio_playback && opts->audio_buffer == -1) {
|
||||
if (opts->audio_codec == SC_CODEC_FLAC) {
|
||||
// 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 = SC_TICK_FROM_MS(120);
|
||||
} else {
|
||||
opts->audio_buffer = SC_TICK_FROM_MS(50);
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef HAVE_V4L2
|
||||
if (v4l2) {
|
||||
if (opts->lock_video_orientation ==
|
||||
@@ -2373,7 +2353,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
}
|
||||
|
||||
if (opts->audio_codec == SC_CODEC_RAW) {
|
||||
LOGE("Recording does not support RAW audio codec");
|
||||
LOGW("Recording does not support RAW audio codec");
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -2396,16 +2376,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
|
||||
"(try with --audio-codec=aac)");
|
||||
return false;
|
||||
}
|
||||
if (opts->record_format == SC_RECORD_FORMAT_FLAC
|
||||
&& opts->audio_codec != SC_CODEC_FLAC) {
|
||||
LOGE("Recording to FLAC file requires a 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) {
|
||||
|
||||
@@ -25,8 +25,7 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
||||
#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_OPUS UINT32_C(0x6f707573) // "opus" 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_AAC UINT32_C(0x00616163) // "aac in ASCII"
|
||||
#define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII
|
||||
switch (codec_id) {
|
||||
case SC_CODEC_ID_H264:
|
||||
@@ -44,8 +43,6 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) {
|
||||
return AV_CODEC_ID_OPUS;
|
||||
case SC_CODEC_ID_AAC:
|
||||
return AV_CODEC_ID_AAC;
|
||||
case SC_CODEC_ID_FLAC:
|
||||
return AV_CODEC_ID_FLAC;
|
||||
case SC_CODEC_ID_RAW:
|
||||
return AV_CODEC_ID_PCM_S16LE;
|
||||
default:
|
||||
@@ -210,11 +207,6 @@ run_demuxer(void *data) {
|
||||
codec_ctx->channels = 2;
|
||||
#endif
|
||||
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) {
|
||||
|
||||
@@ -46,7 +46,7 @@ const struct scrcpy_options scrcpy_options_default = {
|
||||
.window_height = 0,
|
||||
.display_id = 0,
|
||||
.display_buffer = 0,
|
||||
.audio_buffer = -1, // depends on the audio format,
|
||||
.audio_buffer = SC_TICK_FROM_MS(50),
|
||||
.audio_output_buffer = SC_TICK_FROM_MS(5),
|
||||
.time_limit = 0,
|
||||
#ifdef HAVE_V4L2
|
||||
|
||||
@@ -25,7 +25,6 @@ enum sc_record_format {
|
||||
SC_RECORD_FORMAT_MKA,
|
||||
SC_RECORD_FORMAT_OPUS,
|
||||
SC_RECORD_FORMAT_AAC,
|
||||
SC_RECORD_FORMAT_FLAC,
|
||||
};
|
||||
|
||||
static inline bool
|
||||
@@ -33,8 +32,7 @@ sc_record_format_is_audio_only(enum sc_record_format fmt) {
|
||||
return fmt == SC_RECORD_FORMAT_M4A
|
||||
|| fmt == SC_RECORD_FORMAT_MKA
|
||||
|| fmt == SC_RECORD_FORMAT_OPUS
|
||||
|| fmt == SC_RECORD_FORMAT_AAC
|
||||
|| fmt == SC_RECORD_FORMAT_FLAC;
|
||||
|| fmt == SC_RECORD_FORMAT_AAC;
|
||||
}
|
||||
|
||||
enum sc_codec {
|
||||
@@ -43,7 +41,6 @@ enum sc_codec {
|
||||
SC_CODEC_AV1,
|
||||
SC_CODEC_OPUS,
|
||||
SC_CODEC_AAC,
|
||||
SC_CODEC_FLAC,
|
||||
SC_CODEC_RAW,
|
||||
};
|
||||
|
||||
|
||||
@@ -69,8 +69,6 @@ sc_recorder_get_format_name(enum sc_record_format format) {
|
||||
return "matroska";
|
||||
case SC_RECORD_FORMAT_OPUS:
|
||||
return "opus";
|
||||
case SC_RECORD_FORMAT_FLAC:
|
||||
return "flac";
|
||||
default:
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -417,14 +417,10 @@ scrcpy(struct scrcpy_options *options) {
|
||||
|
||||
if (options->video_playback) {
|
||||
sdl_set_hints(options->render_driver);
|
||||
}
|
||||
|
||||
// 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 (SDL_Init(SDL_INIT_VIDEO)) {
|
||||
LOGE("Could not initialize SDL video: %s", SDL_GetError());
|
||||
goto end;
|
||||
}
|
||||
}
|
||||
|
||||
if (options->audio_playback) {
|
||||
|
||||
@@ -178,8 +178,6 @@ sc_server_get_codec_name(enum sc_codec codec) {
|
||||
return "opus";
|
||||
case SC_CODEC_AAC:
|
||||
return "aac";
|
||||
case SC_CODEC_FLAC:
|
||||
return "flac";
|
||||
case SC_CODEC_RAW:
|
||||
return "raw";
|
||||
default:
|
||||
|
||||
@@ -16,6 +16,6 @@ cpu = 'i686'
|
||||
endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win32'
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32'
|
||||
prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32'
|
||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32'
|
||||
|
||||
@@ -16,6 +16,6 @@ cpu = 'x86_64'
|
||||
endian = 'little'
|
||||
|
||||
[properties]
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win64'
|
||||
prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64'
|
||||
prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32'
|
||||
prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64'
|
||||
|
||||
12
doc/audio.md
12
doc/audio.md
@@ -62,13 +62,12 @@ scrcpy --audio-source=mic --no-video --no-playback --record=file.opus
|
||||
|
||||
## Codec
|
||||
|
||||
The audio codec can be selected. The possible values are `opus` (default),
|
||||
`aac`, `flac` and `raw` (uncompressed PCM 16-bit LE):
|
||||
The audio codec can be selected. The possible values are `opus` (default), `aac`
|
||||
and `raw` (uncompressed PCM 16-bit LE):
|
||||
|
||||
```bash
|
||||
scrcpy --audio-codec=opus # default
|
||||
scrcpy --audio-codec=aac
|
||||
scrcpy --audio-codec=flac
|
||||
scrcpy --audio-codec=raw
|
||||
```
|
||||
|
||||
@@ -81,14 +80,7 @@ then your device has no Opus encoder: try `scrcpy --audio-codec=aac`.
|
||||
For advanced usage, to pass arbitrary parameters to the [`MediaFormat`],
|
||||
check `--audio-codec-options` in the manpage or in `scrcpy --help`.
|
||||
|
||||
For example, to change the [FLAC compression level]:
|
||||
|
||||
```bash
|
||||
scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8
|
||||
```
|
||||
|
||||
[`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat
|
||||
[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL
|
||||
|
||||
|
||||
## Encoder
|
||||
|
||||
@@ -18,6 +18,17 @@ scrcpy --video-source=display --audio-source=mic # force display AND micropho
|
||||
scrcpy --video-source=camera --audio-source=output # force camera AND device audio output
|
||||
```
|
||||
|
||||
Audio can be disabled:
|
||||
|
||||
```bash
|
||||
# audio not captured at all
|
||||
scrcpy --video-source=camera --no-audio
|
||||
scrcpy --video-source=camera --no-audio --record=file.mp4
|
||||
|
||||
# audio captured and recorded, but not played
|
||||
scrcpy --video-source=camera --no-audio-playback --record=file.mp4
|
||||
```
|
||||
|
||||
|
||||
## List
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ To record only the audio:
|
||||
```bash
|
||||
scrcpy --no-video --record=file.opus
|
||||
scrcpy --no-video --audio-codec=aac --record=file.aac
|
||||
scrcpy --no-video --audio-codec=flac --record=file.flac
|
||||
# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac
|
||||
# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac
|
||||
```
|
||||
|
||||
Timestamps are captured on the device, so [packet delay variation] does not
|
||||
|
||||
@@ -21,6 +21,13 @@ This will create a new video device in `/dev/videoN`, where `N` is an integer
|
||||
(more [options](https://github.com/umlaeute/v4l2loopback#options) are available
|
||||
to create several devices or devices with specific IDs).
|
||||
|
||||
If you encounter problems detecting your device with Chrome/WebRTC, you can try
|
||||
`exclusive_caps` mode:
|
||||
|
||||
```
|
||||
sudo modprobe v4l2loopback exclusive_caps=1
|
||||
```
|
||||
|
||||
To list the enabled devices:
|
||||
|
||||
```bash
|
||||
|
||||
16
release.mk
16
release.mk
@@ -94,10 +94,10 @@ dist-win32: build-server build-win32
|
||||
cp app/data/scrcpy-noconsole.vbs "$(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/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avutil-58.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.1-scrcpy/win32/bin/avformat-60.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/ffmpeg-6.0-scrcpy-4/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.0-scrcpy-4/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/platform-tools-34.0.5/adb.exe "$(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.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
|
||||
@@ -112,10 +112,10 @@ dist-win64: build-server build-win64
|
||||
cp app/data/scrcpy-noconsole.vbs "$(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/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avutil-58.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.1-scrcpy/win64/bin/avformat-60.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/ffmpeg-6.0-scrcpy-4/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.0-scrcpy-4/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/platform-tools-34.0.5/adb.exe "$(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.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
|
||||
|
||||
@@ -5,7 +5,6 @@ import android.media.MediaFormat;
|
||||
public enum AudioCodec implements Codec {
|
||||
OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS),
|
||||
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);
|
||||
|
||||
private final int id; // 4-byte ASCII representation of the name
|
||||
|
||||
@@ -32,13 +32,7 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
||||
final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
try {
|
||||
try {
|
||||
capture.start();
|
||||
} catch (Throwable t) {
|
||||
// Notify the client that the audio could not be captured
|
||||
streamer.writeDisableStream(false);
|
||||
throw t;
|
||||
}
|
||||
capture.start();
|
||||
|
||||
streamer.writeAudioHeader();
|
||||
while (!Thread.currentThread().isInterrupted()) {
|
||||
@@ -51,11 +45,10 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
||||
|
||||
streamer.writePacket(buffer, bufferInfo);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
// Broken pipe is expected on close, because the socket is closed by the client
|
||||
if (!IO.isBrokenPipe(e)) {
|
||||
Ln.e("Audio capture error", e);
|
||||
}
|
||||
} catch (Throwable e) {
|
||||
// Notify the client that the audio could not be captured
|
||||
streamer.writeDisableStream(false);
|
||||
throw e;
|
||||
} finally {
|
||||
capture.stop();
|
||||
}
|
||||
@@ -69,8 +62,8 @@ public final class AudioRawRecorder implements AsyncProcessor {
|
||||
record();
|
||||
} catch (AudioCaptureForegroundException e) {
|
||||
// Do not print stack trace, a user-friendly error-message has already been logged
|
||||
} catch (Throwable t) {
|
||||
Ln.e("Audio recording error", t);
|
||||
} catch (IOException e) {
|
||||
Ln.e("Audio recording error", e);
|
||||
fatalError = true;
|
||||
} finally {
|
||||
Ln.d("Audio recorder stopped");
|
||||
|
||||
@@ -2,12 +2,11 @@ package com.genymobile.scrcpy;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.AttributionSource;
|
||||
import android.content.Context;
|
||||
import android.content.ContextWrapper;
|
||||
import android.content.MutableContextWrapper;
|
||||
import android.os.Build;
|
||||
import android.os.Process;
|
||||
|
||||
public final class FakeContext extends ContextWrapper {
|
||||
public final class FakeContext extends MutableContextWrapper {
|
||||
|
||||
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
|
||||
@@ -19,7 +18,7 @@ public final class FakeContext extends ContextWrapper {
|
||||
}
|
||||
|
||||
private FakeContext() {
|
||||
super(Workarounds.getSystemContext());
|
||||
super(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -45,9 +44,4 @@ public final class FakeContext extends ContextWrapper {
|
||||
public int getDeviceId() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Context getApplicationContext() {
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,26 +93,19 @@ public final class LogUtils {
|
||||
builder.append("\n (none)");
|
||||
} else {
|
||||
for (String id : cameraIds) {
|
||||
builder.append("\n --camera-id=").append(id);
|
||||
builder.append("\n --video-source=camera --camera-id=").append(id);
|
||||
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id);
|
||||
|
||||
int facing = characteristics.get(CameraCharacteristics.LENS_FACING);
|
||||
builder.append(" (").append(getCameraFacingName(facing)).append(", ");
|
||||
|
||||
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
|
||||
builder.append(activeSize.width()).append("x").append(activeSize.height());
|
||||
builder.append(activeSize.width()).append("x").append(activeSize.height()).append(", ");
|
||||
|
||||
try {
|
||||
// Capture frame rates for low-FPS mode are the same for every resolution
|
||||
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
|
||||
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
|
||||
builder.append(", fps=").append(uniqueLowFps);
|
||||
} catch (Exception e) {
|
||||
// Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper"
|
||||
Ln.w("Could not get available frame rates for camera " + id, e);
|
||||
}
|
||||
|
||||
builder.append(')');
|
||||
// Capture frame rates for low-FPS mode are the same for every resolution
|
||||
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES);
|
||||
SortedSet<Integer> uniqueLowFps = getUniqueSet(lowFpsRanges);
|
||||
builder.append("fps=").append(uniqueLowFps).append(')');
|
||||
|
||||
if (includeSizes) {
|
||||
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
|
||||
|
||||
@@ -5,14 +5,14 @@ import android.media.MediaCodec;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.ByteOrder;
|
||||
import java.util.Arrays;
|
||||
|
||||
public final class Streamer {
|
||||
|
||||
private static final long PACKET_FLAG_CONFIG = 1L << 63;
|
||||
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 Codec codec;
|
||||
private final boolean sendCodecMeta;
|
||||
@@ -30,7 +30,6 @@ public final class Streamer {
|
||||
public Codec getCodec() {
|
||||
return codec;
|
||||
}
|
||||
|
||||
public void writeAudioHeader() throws IOException {
|
||||
if (sendCodecMeta) {
|
||||
ByteBuffer buffer = ByteBuffer.allocate(4);
|
||||
@@ -63,12 +62,8 @@ public final class Streamer {
|
||||
}
|
||||
|
||||
public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException {
|
||||
if (config) {
|
||||
if (codec == AudioCodec.OPUS) {
|
||||
fixOpusConfigPacket(buffer);
|
||||
} else if (codec == AudioCodec.FLAC) {
|
||||
fixFlacConfigPacket(buffer);
|
||||
}
|
||||
if (config && codec == AudioCodec.OPUS) {
|
||||
fixOpusConfigPacket(buffer);
|
||||
}
|
||||
|
||||
if (sendFrameMeta) {
|
||||
@@ -125,14 +120,11 @@ public final class Streamer {
|
||||
throw new IOException("Not enough data in OPUS config packet");
|
||||
}
|
||||
|
||||
final byte[] opusHeaderId = {'A', 'O', 'P', 'U', 'S', 'H', 'D', 'R'};
|
||||
byte[] idBuffer = new byte[8];
|
||||
buffer.get(idBuffer);
|
||||
if (!Arrays.equals(idBuffer, opusHeaderId)) {
|
||||
long id = buffer.getLong();
|
||||
if (id != AOPUSHDR) {
|
||||
throw new IOException("OPUS header not found");
|
||||
}
|
||||
|
||||
// The size is in native byte-order
|
||||
long sizeLong = buffer.getLong();
|
||||
if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) {
|
||||
throw new IOException("Invalid block size in OPUS header: " + sizeLong);
|
||||
@@ -146,41 +138,4 @@ public final class Streamer {
|
||||
// Set the buffer to point to the OPUS header slice
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,34 +21,18 @@ import java.lang.reflect.Method;
|
||||
|
||||
public final class Workarounds {
|
||||
|
||||
private static final Class<?> ACTIVITY_THREAD_CLASS;
|
||||
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 static Class<?> activityThreadClass;
|
||||
private static Object activityThread;
|
||||
|
||||
private Workarounds() {
|
||||
// not instantiable
|
||||
}
|
||||
|
||||
public static void apply(boolean audio, boolean camera) {
|
||||
Workarounds.prepareMainLooper();
|
||||
|
||||
boolean mustFillAppInfo = false;
|
||||
boolean mustFillBaseContext = false;
|
||||
boolean mustFillAppContext = false;
|
||||
|
||||
if (Build.BRAND.equalsIgnoreCase("meizu")) {
|
||||
@@ -69,6 +53,7 @@ public final class Workarounds {
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/4015#issuecomment-1595382142>
|
||||
// - <https://github.com/Genymobile/scrcpy/issues/3805#issuecomment-1596148031>
|
||||
mustFillAppInfo = true;
|
||||
mustFillBaseContext = true;
|
||||
mustFillAppContext = true;
|
||||
}
|
||||
|
||||
@@ -81,12 +66,15 @@ public final class Workarounds {
|
||||
|
||||
if (camera) {
|
||||
mustFillAppInfo = true;
|
||||
mustFillAppContext = true;
|
||||
mustFillBaseContext = true;
|
||||
}
|
||||
|
||||
if (mustFillAppInfo) {
|
||||
Workarounds.fillAppInfo();
|
||||
}
|
||||
if (mustFillBaseContext) {
|
||||
Workarounds.fillBaseContext();
|
||||
}
|
||||
if (mustFillAppContext) {
|
||||
Workarounds.fillAppContext();
|
||||
}
|
||||
@@ -105,9 +93,27 @@ public final class Workarounds {
|
||||
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")
|
||||
private static void fillAppInfo() {
|
||||
try {
|
||||
fillActivityThread();
|
||||
|
||||
// ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData();
|
||||
Class<?> appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData");
|
||||
Constructor<?> appBindDataConstructor = appBindDataClass.getDeclaredConstructor();
|
||||
@@ -123,9 +129,9 @@ public final class Workarounds {
|
||||
appInfoField.set(appBindData, applicationInfo);
|
||||
|
||||
// activityThread.mBoundApplication = appBindData;
|
||||
Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication");
|
||||
Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication");
|
||||
mBoundApplicationField.setAccessible(true);
|
||||
mBoundApplicationField.set(ACTIVITY_THREAD, appBindData);
|
||||
mBoundApplicationField.set(activityThread, appBindData);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.d("Could not fill app info: " + throwable.getMessage());
|
||||
@@ -135,29 +141,33 @@ public final class Workarounds {
|
||||
@SuppressLint("PrivateApi,DiscouragedPrivateApi")
|
||||
private static void fillAppContext() {
|
||||
try {
|
||||
Application app = new Application();
|
||||
fillActivityThread();
|
||||
|
||||
Application app = Application.class.newInstance();
|
||||
Field baseField = ContextWrapper.class.getDeclaredField("mBase");
|
||||
baseField.setAccessible(true);
|
||||
baseField.set(app, FakeContext.get());
|
||||
|
||||
// activityThread.mInitialApplication = app;
|
||||
Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication");
|
||||
Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication");
|
||||
mInitialApplicationField.setAccessible(true);
|
||||
mInitialApplicationField.set(ACTIVITY_THREAD, app);
|
||||
mInitialApplicationField.set(activityThread, app);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.d("Could not fill app context: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
static Context getSystemContext() {
|
||||
private static void fillBaseContext() {
|
||||
try {
|
||||
Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext");
|
||||
return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD);
|
||||
fillActivityThread();
|
||||
|
||||
Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext");
|
||||
Context context = (Context) getSystemContextMethod.invoke(activityThread);
|
||||
FakeContext.get().setBaseContext(context);
|
||||
} catch (Throwable throwable) {
|
||||
// this is a workaround, so failing is not an error
|
||||
Ln.d("Could not get system context: " + throwable.getMessage());
|
||||
return null;
|
||||
Ln.d("Could not fill base context: " + throwable.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user