Compare commits

..

3 Commits

Author SHA1 Message Date
Romain Vimont
dedbc3f8cd Recover on invalid camera FPS ranges
Some devices may provide invalid ranges, causing an
IllegalArgumentException "lower must be less than or equal to upper".

Catch the exception to list the cameras anyway.

Refs #4403 <https://github.com/Genymobile/scrcpy/issues/4403>
2023-11-05 15:14:04 +01:00
Romain Vimont
ad8ced5b1e Log physical cameras 2023-11-05 12:16:14 +01:00
Romain Vimont
36b17ee068 Simplify --list-cameras ouptut
Remove --video-source=camera from the output of --list-cameras (this is
implicit).
2023-11-05 11:52:33 +01:00
23 changed files with 98 additions and 219 deletions

View File

@@ -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)
@@ -125,7 +125,7 @@ _scrcpy() {
return
;;
--record-format)
COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac wav' -- "$cur"))
COMPREPLY=($(compgen -W 'mkv mp4' -- "$cur"))
return
;;
--render-driver)

View File

@@ -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)'
@@ -65,7 +65,7 @@ arguments=(
'--push-target=[Set the target directory for pushing files to the device by drag and drop]'
{-r,--record=}'[Record screen to file]:record file:_files'
'--raw-key-events[Inject key events for all input keys, and ignore text events]'
'--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)'
'--record-format=[Force recording format]:format:(mp4 mkv)'
'--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)'
'--require-audio=[Make scrcpy fail if audio is enabled but does not work]'
'--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)'

View File

@@ -6,11 +6,11 @@ cd "$DIR"
mkdir -p "$PREBUILT_DATA_DIR"
cd "$PREBUILT_DATA_DIR"
VERSION=6.1-scrcpy-2
VERSION=6.0-scrcpy-4
DEP_DIR="ffmpeg-$VERSION"
FILENAME="$DEP_DIR".7z
SHA256SUM=7f25f638dc24a0f5d4af07a088b6a604cf33548900bbfd2f6ce0bae050b7664d
SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60
if [[ -d "$DEP_DIR" ]]
then

View File

@@ -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.
@@ -347,7 +347,7 @@ Record screen to
The format is determined by the
.B \-\-record\-format
option if set, or by the file extension.
option if set, or by the file extension (.mp4 or .mkv).
.TP
.B \-\-raw\-key\-events
@@ -355,7 +355,7 @@ Inject key events for all input keys, and ignore text events.
.TP
.BI "\-\-record\-format " format
Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav).
Force recording format (either mp4 or mkv).
.TP
.BI "\-\-render\-driver " name

View File

@@ -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.",
},
{
@@ -583,7 +583,7 @@ static const struct sc_option options[] = {
.argdesc = "file.mp4",
.text = "Record screen to file.\n"
"The format is determined by the --record-format option if "
"set, or by the file extension.",
"set, or by the file extension (.mp4 or .mkv).",
},
{
.longopt_id = OPT_RAW_KEY_EVENTS,
@@ -594,8 +594,7 @@ static const struct sc_option options[] = {
.longopt_id = OPT_RECORD_FORMAT,
.longopt = "record-format",
.argdesc = "format",
.text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac "
"or wav).",
.text = "Force recording format (either mp4 or mkv).",
},
{
.longopt_id = OPT_RENDER_DRIVER,
@@ -1627,12 +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;
}
if (!strcmp(name, "wav")) {
return SC_RECORD_FORMAT_WAV;
}
return 0;
}
@@ -1702,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;
}
@@ -2268,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 ==
@@ -2376,6 +2352,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[],
}
}
if (opts->audio_codec == SC_CODEC_RAW) {
LOGW("Recording does not support RAW audio codec");
return false;
}
if (opts->video
&& sc_record_format_is_audio_only(opts->record_format)) {
LOGE("Audio container does not support video stream");
@@ -2395,30 +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->record_format == SC_RECORD_FORMAT_WAV
&& opts->audio_codec != SC_CODEC_RAW) {
LOGE("Recording to WAV file requires a RAW audio stream "
"(try with --audio-codec=raw)");
return false;
}
if ((opts->record_format == SC_RECORD_FORMAT_MP4 ||
opts->record_format == SC_RECORD_FORMAT_M4A)
&& opts->audio_codec == SC_CODEC_RAW) {
LOGE("Recording to MP4 container does not support RAW audio");
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) {

View File

@@ -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) {

View File

@@ -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

View File

@@ -25,8 +25,6 @@ enum sc_record_format {
SC_RECORD_FORMAT_MKA,
SC_RECORD_FORMAT_OPUS,
SC_RECORD_FORMAT_AAC,
SC_RECORD_FORMAT_FLAC,
SC_RECORD_FORMAT_WAV,
};
static inline bool
@@ -34,9 +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_WAV;
|| fmt == SC_RECORD_FORMAT_AAC;
}
enum sc_codec {
@@ -45,7 +41,6 @@ enum sc_codec {
SC_CODEC_AV1,
SC_CODEC_OPUS,
SC_CODEC_AAC,
SC_CODEC_FLAC,
SC_CODEC_RAW,
};

View File

@@ -69,10 +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";
case SC_RECORD_FORMAT_WAV:
return "wav";
default:
return NULL;
}
@@ -105,7 +101,7 @@ sc_recorder_write_stream(struct sc_recorder *recorder,
AVStream *stream = recorder->ctx->streams[st->index];
sc_recorder_rescale_packet(stream, packet);
if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) {
LOGD("Fixing PTS non monotonically increasing in stream %d "
LOGW("Fixing PTS non monotonically increasing in stream %d "
"(%" PRIi64 " >= %" PRIi64 ")",
st->index, st->last_pts, packet->pts);
packet->pts = ++st->last_pts;
@@ -170,14 +166,13 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) {
}
static inline bool
sc_recorder_must_wait_for_config_packets(struct sc_recorder *recorder) {
sc_recorder_has_empty_queues(struct sc_recorder *recorder) {
if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) {
// The video queue is empty
return true;
}
if (recorder->audio && recorder->audio_expects_config_packet
&& sc_vecdeque_is_empty(&recorder->audio_queue)) {
if (recorder->audio && sc_vecdeque_is_empty(&recorder->audio_queue)) {
// The audio queue is empty (when audio is enabled)
return true;
}
@@ -193,7 +188,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
while (!recorder->stopped &&
((recorder->video && !recorder->video_init)
|| (recorder->audio && !recorder->audio_init)
|| sc_recorder_must_wait_for_config_packets(recorder))) {
|| sc_recorder_has_empty_queues(recorder))) {
sc_cond_wait(&recorder->cond, &recorder->mutex);
}
@@ -212,8 +207,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) {
}
AVPacket *audio_pkt = NULL;
if (recorder->audio_expects_config_packet &&
!sc_vecdeque_is_empty(&recorder->audio_queue)) {
if (!sc_vecdeque_is_empty(&recorder->audio_queue)) {
assert(recorder->audio);
audio_pkt = sc_vecdeque_pop(&recorder->audio_queue);
}
@@ -601,10 +595,6 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink,
recorder->audio_stream.index = stream->index;
// A config packet is provided for all formats supported except raw audio
recorder->audio_expects_config_packet =
ctx->codec_id != AV_CODEC_ID_PCM_S16LE;
recorder->audio_init = true;
sc_cond_signal(&recorder->cond);
sc_mutex_unlock(&recorder->mutex);
@@ -717,8 +707,6 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename,
recorder->video_init = false;
recorder->audio_init = false;
recorder->audio_expects_config_packet = false;
sc_recorder_stream_init(&recorder->video_stream);
sc_recorder_stream_init(&recorder->audio_stream);

View File

@@ -50,8 +50,6 @@ struct sc_recorder {
bool video_init;
bool audio_init;
bool audio_expects_config_packet;
struct sc_recorder_stream video_stream;
struct sc_recorder_stream audio_stream;

View File

@@ -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) {

View File

@@ -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:

View File

@@ -16,6 +16,6 @@ cpu = 'i686'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/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'

View File

@@ -16,6 +16,6 @@ cpu = 'x86_64'
endian = 'little'
[properties]
prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/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'

View File

@@ -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

View File

@@ -18,9 +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
scrcpy --no-video --audio-codec=raw --record=file.wav
# .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
@@ -33,17 +31,14 @@ course, not if you capture your scrcpy window and audio output on the computer).
## Format
The video and audio streams are encoded on the device, but are muxed on the
client side. Several formats (containers) are supported:
- MP4 (`.mp4`, `.m4a`, `.aac`)
- Matroska (`.mkv`, `.mka`)
- OPUS (`.opus`)
- FLAC (`.flac`)
- WAV (`.wav`)
client side. Two formats (containers) are supported:
- Matroska (`.mkv`)
- MP4 (`.mp4`)
The container is automatically selected based on the filename.
It is also possible to explicitly select a container (in that case the filename
needs not end with a known extension):
needs not end with `.mkv` or `.mp4`):
```
scrcpy --record=file --record-format=mkv

View File

@@ -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-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/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-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/"
cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/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)/"

View File

@@ -24,19 +24,11 @@ public final class AudioCapture {
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 final int audioSource;
private AudioRecord recorder;
private final AudioTimestamp timestamp = new AudioTimestamp();
private long previousRecorderTimestamp = -1;
private long previousPts = 0;
private long nextPts = 0;
@@ -44,6 +36,10 @@ public final class AudioCapture {
this.audioSource = audioSource.value();
}
public static int millisToBytes(int millis) {
return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000;
}
private static AudioFormat createAudioFormat() {
AudioFormat.Builder builder = new AudioFormat.Builder();
builder.setEncoding(ENCODING);
@@ -139,8 +135,8 @@ public final class AudioCapture {
}
@TargetApi(Build.VERSION_CODES.N)
public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, MAX_READ_SIZE);
public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) {
int r = recorder.read(directBuffer, size);
if (r <= 0) {
return r;
}
@@ -148,9 +144,8 @@ public final class AudioCapture {
long pts;
int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC);
if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) {
if (ret == AudioRecord.SUCCESS) {
pts = timestamp.nanoTime / 1000;
previousRecorderTimestamp = timestamp.nanoTime;
} else {
if (nextPts == 0) {
Ln.w("Could not get any audio timestamp");
@@ -162,13 +157,13 @@ public final class AudioCapture {
long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE);
nextPts = pts + durationUs;
if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) {
if (previousPts != 0 && pts < previousPts) {
// 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;
pts = previousPts + 1;
}
previousPts = pts;

View File

@@ -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

View File

@@ -37,6 +37,9 @@ 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 READ_MS = 5; // milliseconds
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
private final AudioCapture capture;
private final Streamer streamer;
private final int bitRate;
@@ -90,7 +93,7 @@ public final class AudioEncoder implements AsyncProcessor {
while (!Thread.currentThread().isInterrupted()) {
InputTask task = inputTasks.take();
ByteBuffer buffer = mediaCodec.getInputBuffer(task.index);
int r = capture.read(buffer, bufferInfo);
int r = capture.read(buffer, READ_SIZE, bufferInfo);
if (r <= 0) {
throw new IOException("Could not read audio: " + r);
}

View File

@@ -13,6 +13,9 @@ public final class AudioRawRecorder implements AsyncProcessor {
private Thread thread;
private static final int READ_MS = 5; // milliseconds
private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS);
public AudioRawRecorder(AudioCapture capture, Streamer streamer) {
this.capture = capture;
this.streamer = streamer;
@@ -25,22 +28,16 @@ public final class AudioRawRecorder implements AsyncProcessor {
return;
}
final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE);
final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE);
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()) {
buffer.position(0);
int r = capture.read(buffer, bufferInfo);
int r = capture.read(buffer, READ_SIZE, bufferInfo);
if (r < 0) {
throw new IOException("Could not read audio: " + r);
}
@@ -48,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();
}
@@ -66,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");

View File

@@ -3,14 +3,17 @@ package com.genymobile.scrcpy;
import com.genymobile.scrcpy.wrappers.DisplayManager;
import com.genymobile.scrcpy.wrappers.ServiceManager;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCharacteristics;
import android.hardware.camera2.CameraManager;
import android.hardware.camera2.params.StreamConfigurationMap;
import android.media.MediaCodec;
import android.os.Build;
import android.util.Range;
import java.util.Arrays;
import java.util.List;
import java.util.SortedSet;
import java.util.TreeSet;
@@ -84,6 +87,7 @@ public final class LogUtils {
}
}
@TargetApi(Build.VERSION_CODES.P)
public static String buildCameraListMessage(boolean includeSizes) {
StringBuilder builder = new StringBuilder("List of cameras:");
CameraManager cameraManager = ServiceManager.getCameraManager();
@@ -114,6 +118,15 @@ public final class LogUtils {
builder.append(')');
int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES);
if (Arrays.asList(capabilities).contains(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_LOGICAL_MULTI_CAMERA)) {
builder.append("\n Logical multi-camera, backed by physical cameras:");
for (String phyId : characteristics.getPhysicalCameraIds()) {
CameraCharacteristics phyCharacteristics = cameraManager.getCameraCharacteristics(phyId);
appendPhysicalCamera(builder, phyId, phyCharacteristics);
}
}
if (includeSizes) {
StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP);
@@ -141,6 +154,13 @@ public final class LogUtils {
return builder.toString();
}
private static void appendPhysicalCamera(StringBuilder builder, String id, CameraCharacteristics characteristics) {
builder.append("\n --camera-id=").append(id).append(" (");
Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE);
builder.append(activeSize.width()).append("x").append(activeSize.height()).append(")");
}
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) {
SortedSet<Integer> set = new TreeSet<>();
for (Range<Integer> range : ranges) {

View File

@@ -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);
}
}