From f69ac405340efb0b53849b96a32f389cf1c1d54e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Sep 2024 08:43:42 +0200 Subject: [PATCH 001/278] Reorganize server imports Moving classes into subpackages changed the expected imports order. Reorganize them all at once automatically to avoid spurious changes in future commits. --- .../main/java/com/genymobile/scrcpy/audio/AudioEncoder.java | 4 ++-- .../java/com/genymobile/scrcpy/audio/AudioRawRecorder.java | 2 +- .../com/genymobile/scrcpy/control/ControlMessageReader.java | 2 +- .../main/java/com/genymobile/scrcpy/control/Controller.java | 2 +- .../java/com/genymobile/scrcpy/video/CameraCapture.java | 2 +- .../java/com/genymobile/scrcpy/video/ScreenCapture.java | 2 +- .../main/java/com/genymobile/scrcpy/video/ScreenInfo.java | 2 +- .../java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 6 +++--- .../java/com/genymobile/scrcpy/wrappers/DisplayManager.java | 4 ++-- 9 files changed, 13 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 8230e054..672403b8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -1,14 +1,14 @@ package com.genymobile.scrcpy.audio; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecUtils; -import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.device.Streamer; import android.annotation.TargetApi; import android.media.MediaCodec; diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 323caae4..3924c205 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -1,9 +1,9 @@ package com.genymobile.scrcpy.audio; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.os.Build; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 45116935..17e121c2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -1,7 +1,7 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.util.Binary; import com.genymobile.scrcpy.device.Position; +import com.genymobile.scrcpy.util.Binary; import java.io.BufferedInputStream; import java.io.DataInputStream; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 38251655..b445427d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -3,9 +3,9 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; +import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 7d2e2055..3b8fc59b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index fbeca2af..62afb263 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index ba537b17..bd0a3b62 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -2,8 +2,8 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.BuildConfig; import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Ln; import android.graphics.Rect; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index a5f2d1e9..7800e4bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -1,15 +1,15 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Codec; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.CodecUtils; -import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.util.IO; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.device.Streamer; import android.media.MediaCodec; import android.media.MediaCodecInfo; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index dd92330c..00a39274 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,9 +1,9 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.util.Command; import com.genymobile.scrcpy.device.DisplayInfo; -import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Command; +import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.hardware.display.VirtualDisplay; From 0cc6f6aa09f0fe5913ec66276e7ea3681fa81cd7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Sep 2024 08:17:48 +0200 Subject: [PATCH 002/278] Detect codec/encoder mismatch Fail with an explicit error when the requested encoder does not match the requested codec. Refs #5066 --- .../java/com/genymobile/scrcpy/audio/AudioEncoder.java | 8 +++++++- .../src/main/java/com/genymobile/scrcpy/util/Codec.java | 7 +++++++ .../java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 8 +++++++- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 672403b8..f462431a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -287,7 +287,13 @@ public final class AudioEncoder implements AsyncProcessor { if (encoderName != null) { Ln.d("Creating audio encoder by name: '" + encoderName + "'"); try { - return MediaCodec.createByCodecName(encoderName); + MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName); + String mimeType = Codec.getMimeType(mediaCodec); + if (!codec.getMimeType().equals(mimeType)) { + Ln.e("Audio encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")"); + throw new ConfigurationException("Incorrect encoder type: " + encoderName); + } + return mediaCodec; } catch (IllegalArgumentException e) { Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Codec.java b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java index a363bd8b..b350409b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Codec.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Codec.java @@ -1,5 +1,7 @@ package com.genymobile.scrcpy.util; +import android.media.MediaCodec; + public interface Codec { enum Type { @@ -14,4 +16,9 @@ public interface Codec { String getName(); String getMimeType(); + + static String getMimeType(MediaCodec codec) { + String[] types = codec.getCodecInfo().getSupportedTypes(); + return types.length > 0 ? types[0] : null; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 7800e4bb..41c38642 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -205,7 +205,13 @@ public class SurfaceEncoder implements AsyncProcessor { if (encoderName != null) { Ln.d("Creating encoder by name: '" + encoderName + "'"); try { - return MediaCodec.createByCodecName(encoderName); + MediaCodec mediaCodec = MediaCodec.createByCodecName(encoderName); + String mimeType = Codec.getMimeType(mediaCodec); + if (!codec.getMimeType().equals(mimeType)) { + Ln.e("Video encoder type for \"" + encoderName + "\" (" + mimeType + ") does not match codec type (" + codec.getMimeType() + ")"); + throw new ConfigurationException("Incorrect encoder type: " + encoderName); + } + return mediaCodec; } catch (IllegalArgumentException e) { Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); throw new ConfigurationException("Unknown encoder: " + encoderName); From a7e61fb8712316e0375bf156f3243d7ced333e10 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 22:28:04 +0200 Subject: [PATCH 003/278] Remove unused audio player callbacks The callbacks were never used: the player can report errors directly from sc_audio_player_frame_sink_push(). --- app/src/audio_player.h | 7 ------- 1 file changed, 7 deletions(-) diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 0c677363..3d468999 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -68,13 +68,6 @@ struct sc_audio_player { // Set to true the first time the SDL callback is called atomic_bool played; - - const struct sc_audio_player_callbacks *cbs; - void *cbs_userdata; -}; - -struct sc_audio_player_callbacks { - void (*on_ended)(struct sc_audio_player *ap, bool success, void *userdata); }; void From 2e7a15a9987615b10460e7717f3a0dd31774936e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 22:32:02 +0200 Subject: [PATCH 004/278] Remove unused audio player fields They are only used locally. --- app/src/audio_player.c | 4 +--- app/src/audio_player.h | 4 ---- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 274b6948..24144483 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -351,8 +351,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, assert(out_bytes_per_sample > 0); ap->sample_rate = ctx->sample_rate; - ap->nb_channels = nb_channels; - ap->out_bytes_per_sample = out_bytes_per_sample; ap->target_buffering = ap->target_buffering_delay * ap->sample_rate / SC_TICK_FREQ; @@ -413,7 +411,7 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, // without locking. uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; - size_t sample_size = ap->nb_channels * ap->out_bytes_per_sample; + size_t sample_size = nb_channels * out_bytes_per_sample; bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); if (!ok) { goto error_free_swr_ctx; diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 3d468999..e638e601 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -41,10 +41,6 @@ struct sc_audio_player { // The sample rate is the same for input and output unsigned sample_rate; - // The number of channels is the same for input and output - unsigned nb_channels; - // The number of bytes per sample for a single channel - size_t out_bytes_per_sample; // Target buffer for resampling (only used by the receiver thread) uint8_t *swr_buf; From 42fb947780e1054a72de7b4baf02f1a73beda3c6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 22:39:45 +0200 Subject: [PATCH 005/278] Use local mutex for audio player Replace SDL_LockAudioDevice() by a local mutex, to minimize the lock section and to make the code independent of SDL. --- app/src/audio_player.c | 29 +++++++++++++++++++++-------- app/src/audio_player.h | 2 ++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 24144483..fe007832 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -66,8 +66,6 @@ static void SDLCALL sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { struct sc_audio_player *ap = userdata; - // This callback is called with the lock used by SDL_LockAudioDevice() - assert(len_int > 0); size_t len = len_int; uint32_t count = TO_SAMPLES(len); @@ -76,6 +74,10 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); #endif + // A lock is necessary in the rare case where the producer needs to drop + // samples already pushed (when the buffer is full) + sc_mutex_lock(&ap->mutex); + bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); if (!played) { uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); @@ -88,12 +90,15 @@ sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { // whole buffer with silence (len is small compared to the // arbitrary margin value). memset(stream, 0, len); + sc_mutex_unlock(&ap->mutex); return; } } uint32_t read = sc_audiobuf_read(&ap->buf, stream, count); + sc_mutex_unlock(&ap->mutex); + if (read < count) { uint32_t silence = count - read; // Insert silence. In theory, the inserted silent samples replace the @@ -183,7 +188,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, // All samples that could be written without locking have been written, // now we need to lock to drop/consume old samples - SDL_LockAudioDevice(ap->device); + sc_mutex_lock(&ap->mutex); // Retry with the lock written += sc_audiobuf_write(&ap->buf, @@ -196,7 +201,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, assert(skipped_samples == remaining); } - SDL_UnlockAudioDevice(ap->device); + sc_mutex_unlock(&ap->mutex); if (written < samples) { // Now there is enough space @@ -229,7 +234,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, if (can_read > max_buffered_samples) { uint32_t skip_samples = 0; - SDL_LockAudioDevice(ap->device); + sc_mutex_lock(&ap->mutex); can_read = sc_audiobuf_can_read(&ap->buf); if (can_read > max_buffered_samples) { skip_samples = can_read - max_buffered_samples; @@ -238,7 +243,7 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, (void) r; skipped_samples += skip_samples; } - SDL_UnlockAudioDevice(ap->device); + sc_mutex_unlock(&ap->mutex); if (skip_samples) { if (played) { @@ -411,12 +416,17 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, // without locking. uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; - size_t sample_size = nb_channels * out_bytes_per_sample; - bool ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); + bool ok = sc_mutex_init(&ap->mutex); if (!ok) { goto error_free_swr_ctx; } + size_t sample_size = nb_channels * out_bytes_per_sample; + ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); + if (!ok) { + goto error_destroy_mutex; + } + size_t initial_swr_buf_size = TO_BYTES(4096); ap->swr_buf = malloc(initial_swr_buf_size); if (!ap->swr_buf) { @@ -450,6 +460,8 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, error_destroy_audiobuf: sc_audiobuf_destroy(&ap->buf); +error_destroy_mutex: + sc_mutex_destroy(&ap->mutex); error_free_swr_ctx: swr_free(&ap->swr_ctx); error_close_audio_device: @@ -468,6 +480,7 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { free(ap->swr_buf); sc_audiobuf_destroy(&ap->buf); + sc_mutex_destroy(&ap->mutex); swr_free(&ap->swr_ctx); } diff --git a/app/src/audio_player.h b/app/src/audio_player.h index e638e601..7ebb43db 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -20,6 +20,8 @@ struct sc_audio_player { SDL_AudioDeviceID device; + sc_mutex mutex; + // The target buffering between the producer and the consumer. This value // is directly use for compensation. // Since audio capture and/or encoding on the device typically produce From 10f60054aca70047c128fc801d103dac4d5d7896 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 23:11:03 +0200 Subject: [PATCH 006/278] Use exact-width integer types --- app/src/audio_player.c | 8 ++++---- app/src/audio_player.h | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index fe007832..d72dac25 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -342,12 +342,12 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { struct sc_audio_player *ap = DOWNCAST(sink); #ifdef SCRCPY_LAVU_HAS_CHLAYOUT - assert(ctx->ch_layout.nb_channels > 0); - unsigned nb_channels = ctx->ch_layout.nb_channels; + assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256); + uint8_t nb_channels = ctx->ch_layout.nb_channels; #else int tmp = av_get_channel_layout_nb_channels(ctx->channel_layout); - assert(tmp > 0); - unsigned nb_channels = tmp; + assert(tmp > 0 && tmp < 256); + uint8_t nb_channels = tmp; #endif assert(ctx->sample_rate > 0); diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 7ebb43db..c02a0d20 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -42,7 +42,7 @@ struct sc_audio_player { struct SwrContext *swr_ctx; // The sample rate is the same for input and output - unsigned sample_rate; + uint32_t sample_rate; // Target buffer for resampling (only used by the receiver thread) uint8_t *swr_buf; From 62776fb2617a3c23190d3549b673c44b65fe12ce Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 23:16:05 +0200 Subject: [PATCH 007/278] Make audio buffering independant of output buffer This will allow to extract the "audio regulator" part from the audio player. --- app/src/audio_player.c | 9 ++++----- app/src/audio_player.h | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/app/src/audio_player.c b/app/src/audio_player.c index d72dac25..9e856181 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -220,14 +220,14 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, underflow = atomic_exchange_explicit(&ap->underflow, 0, memory_order_relaxed); - max_buffered_samples = ap->target_buffering - + 12 * ap->output_buffer - + ap->target_buffering / 10; + max_buffered_samples = ap->target_buffering * 11 / 10 + + 60 * ap->sample_rate / 1000 /* 60 ms */; } else { // SDL playback not started yet, do not accumulate more than // max_initial_buffering samples, this would cause unnecessary delay // (and glitches to compensate) on start. - max_buffered_samples = ap->target_buffering + 2 * ap->output_buffer; + max_buffered_samples = ap->target_buffering + + 10 * ap->sample_rate / 1000 /* 10 ms */; } uint32_t can_read = sc_audiobuf_can_read(&ap->buf); @@ -363,7 +363,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate / SC_TICK_FREQ; assert(aout_samples <= 0xFFFF); - ap->output_buffer = (uint16_t) aout_samples; SDL_AudioSpec desired = { .freq = ctx->sample_rate, diff --git a/app/src/audio_player.h b/app/src/audio_player.h index c02a0d20..4ad40306 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -32,7 +32,6 @@ struct sc_audio_player { // SDL audio output buffer size. sc_tick output_buffer_duration; - uint16_t output_buffer; // Audio buffer to communicate between the receiver and the SDL audio // callback From 0bb3955b958752e1ec220a1045e5185c027fc56b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Sep 2024 23:58:02 +0200 Subject: [PATCH 008/278] Split audio player The audio player had 2 roles: - handle the SDL audio output device; - resample input samples to maintain a target latency. Extract the latter to a separate component (an "audio regulator"), independent of SDL. --- app/meson.build | 1 + app/src/audio_player.c | 413 ++----------------------------------- app/src/audio_player.h | 47 +---- app/src/audio_regulator.c | 415 ++++++++++++++++++++++++++++++++++++++ app/src/audio_regulator.h | 71 +++++++ 5 files changed, 507 insertions(+), 440 deletions(-) create mode 100644 app/src/audio_regulator.c create mode 100644 app/src/audio_regulator.h diff --git a/app/meson.build b/app/meson.build index fc752e86..99e7e3a2 100644 --- a/app/meson.build +++ b/app/meson.build @@ -5,6 +5,7 @@ src = [ 'src/adb/adb_parser.c', 'src/adb/adb_tunnel.c', 'src/audio_player.c', + 'src/audio_regulator.c', 'src/cli.c', 'src/clock.c', 'src/compat.c', diff --git a/app/src/audio_player.c b/app/src/audio_player.c index 9e856181..9413c2ea 100644 --- a/app/src/audio_player.c +++ b/app/src/audio_player.c @@ -1,143 +1,23 @@ #include "audio_player.h" -#include -#include - #include "util/log.h" -//#define SC_AUDIO_PLAYER_DEBUG // uncomment to debug - -/** - * Real-time audio player with configurable latency - * - * As input, the player regularly receives AVFrames of decoded audio samples. - * As output, an SDL callback regularly requests audio samples to be played. - * In the middle, an audio buffer stores the samples produced but not consumed - * yet. - * - * The goal of the player is to feed the audio output with a latency as low as - * possible while avoiding buffer underrun (i.e. not being able to provide - * samples when requested). - * - * The player aims to feed the audio output with as little latency as possible - * while avoiding buffer underrun. To achieve this, it attempts to maintain the - * average buffering (the number of samples present in the buffer) around a - * target value. If this target buffering is too low, then buffer underrun will - * occur frequently. If it is too high, then latency will become unacceptable. - * This target value is configured using the scrcpy option --audio-buffer. - * - * The player cannot adjust the sample input rate (it receives samples produced - * in real-time) or the sample output rate (it must provide samples as - * requested by the audio output callback). Therefore, it may only apply - * compensation by resampling (converting _m_ input samples to _n_ output - * samples). - * - * The compensation itself is applied by libswresample (FFmpeg). It is - * configured using swr_set_compensation(). An important work for the player - * is to estimate the compensation value regularly and apply it. - * - * The estimated buffering level is the result of averaging the "natural" - * buffering (samples are produced and consumed by blocks, so it must be - * smoothed), and making instant adjustments resulting of its own actions - * (explicit compensation and silence insertion on underflow), which are not - * smoothed. - * - * Buffer underflow events can occur when packets arrive too late. In that case, - * the player inserts silence. Once the packets finally arrive (late), one - * strategy could be to drop the samples that were replaced by silence, in - * order to keep a minimal latency. However, dropping samples in case of buffer - * underflow is inadvisable, as it would temporarily increase the underflow - * even more and cause very noticeable audio glitches. - * - * Therefore, the player doesn't drop any sample on underflow. The compensation - * mechanism will absorb the delay introduced by the inserted silence. - */ - /** Downcast frame_sink to sc_audio_player */ #define DOWNCAST(SINK) container_of(SINK, struct sc_audio_player, frame_sink) -#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT #define SC_SDL_SAMPLE_FMT AUDIO_F32 -#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ap->buf, (SAMPLES)) -#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ap->buf, (BYTES)) - static void SDLCALL sc_audio_player_sdl_callback(void *userdata, uint8_t *stream, int len_int) { struct sc_audio_player *ap = userdata; assert(len_int > 0); size_t len = len_int; - uint32_t count = TO_SAMPLES(len); -#ifdef SC_AUDIO_PLAYER_DEBUG - LOGD("[Audio] SDL callback requests %" PRIu32 " samples", count); -#endif + assert(len % ap->audioreg.sample_size == 0); + uint32_t out_samples = len / ap->audioreg.sample_size; - // A lock is necessary in the rare case where the producer needs to drop - // samples already pushed (when the buffer is full) - sc_mutex_lock(&ap->mutex); - - bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); - if (!played) { - uint32_t buffered_samples = sc_audiobuf_can_read(&ap->buf); - // Wait until the buffer is filled up to at least target_buffering - // before playing - if (buffered_samples < ap->target_buffering) { - LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 - " samples", count); - // Delay playback starting to reach the target buffering. Fill the - // whole buffer with silence (len is small compared to the - // arbitrary margin value). - memset(stream, 0, len); - sc_mutex_unlock(&ap->mutex); - return; - } - } - - uint32_t read = sc_audiobuf_read(&ap->buf, stream, count); - - sc_mutex_unlock(&ap->mutex); - - if (read < count) { - uint32_t silence = count - read; - // Insert silence. In theory, the inserted silent samples replace the - // missing real samples, which will arrive later, so they should be - // dropped to keep the latency minimal. However, this would cause very - // audible glitches, so let the clock compensation restore the target - // latency. - LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", - silence); - memset(stream + TO_BYTES(read), 0, TO_BYTES(silence)); - - bool received = atomic_load_explicit(&ap->received, - memory_order_relaxed); - if (received) { - // Inserting additional samples immediately increases buffering - atomic_fetch_add_explicit(&ap->underflow, silence, - memory_order_relaxed); - } - } - - atomic_store_explicit(&ap->played, true, memory_order_relaxed); -} - -static uint8_t * -sc_audio_player_get_swr_buf(struct sc_audio_player *ap, uint32_t min_samples) { - size_t min_buf_size = TO_BYTES(min_samples); - if (min_buf_size > ap->swr_buf_alloc_size) { - size_t new_size = min_buf_size + 4096; - uint8_t *buf = realloc(ap->swr_buf, new_size); - if (!buf) { - LOG_OOM(); - // Could not realloc to the requested size - return NULL; - } - ap->swr_buf = buf; - ap->swr_buf_alloc_size = new_size; - } - - return ap->swr_buf; + sc_audio_regulator_pull(&ap->audioreg, stream, out_samples); } static bool @@ -145,202 +25,14 @@ sc_audio_player_frame_sink_push(struct sc_frame_sink *sink, const AVFrame *frame) { struct sc_audio_player *ap = DOWNCAST(sink); - SwrContext *swr_ctx = ap->swr_ctx; - - int64_t swr_delay = swr_get_delay(swr_ctx, ap->sample_rate); - // No need to av_rescale_rnd(), input and output sample rates are the same. - // Add more space (256) for clock compensation. - int dst_nb_samples = swr_delay + frame->nb_samples + 256; - - uint8_t *swr_buf = sc_audio_player_get_swr_buf(ap, dst_nb_samples); - if (!swr_buf) { - return false; - } - - int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, - (const uint8_t **) frame->data, frame->nb_samples); - if (ret < 0) { - LOGE("Resampling failed: %d", ret); - return false; - } - - // swr_convert() returns the number of samples which would have been - // written if the buffer was big enough. - uint32_t samples = MIN(ret, dst_nb_samples); -#ifdef SC_AUDIO_PLAYER_DEBUG - LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); -#endif - - uint32_t cap = sc_audiobuf_capacity(&ap->buf); - if (samples > cap) { - // Very very unlikely: a single resampled frame should never - // exceed the audio buffer size (or something is very wrong). - // Ignore the first bytes in swr_buf to avoid memory corruption anyway. - swr_buf += TO_BYTES(samples - cap); - samples = cap; - } - - uint32_t skipped_samples = 0; - - uint32_t written = sc_audiobuf_write(&ap->buf, swr_buf, samples); - if (written < samples) { - uint32_t remaining = samples - written; - - // All samples that could be written without locking have been written, - // now we need to lock to drop/consume old samples - sc_mutex_lock(&ap->mutex); - - // Retry with the lock - written += sc_audiobuf_write(&ap->buf, - swr_buf + TO_BYTES(written), - remaining); - if (written < samples) { - remaining = samples - written; - // Still insufficient, drop old samples to make space - skipped_samples = sc_audiobuf_read(&ap->buf, NULL, remaining); - assert(skipped_samples == remaining); - } - - sc_mutex_unlock(&ap->mutex); - - if (written < samples) { - // Now there is enough space - uint32_t w = sc_audiobuf_write(&ap->buf, - swr_buf + TO_BYTES(written), - remaining); - assert(w == remaining); - (void) w; - } - } - - uint32_t underflow = 0; - uint32_t max_buffered_samples; - bool played = atomic_load_explicit(&ap->played, memory_order_relaxed); - if (played) { - underflow = atomic_exchange_explicit(&ap->underflow, 0, - memory_order_relaxed); - - max_buffered_samples = ap->target_buffering * 11 / 10 - + 60 * ap->sample_rate / 1000 /* 60 ms */; - } else { - // SDL playback not started yet, do not accumulate more than - // max_initial_buffering samples, this would cause unnecessary delay - // (and glitches to compensate) on start. - max_buffered_samples = ap->target_buffering - + 10 * ap->sample_rate / 1000 /* 10 ms */; - } - - uint32_t can_read = sc_audiobuf_can_read(&ap->buf); - if (can_read > max_buffered_samples) { - uint32_t skip_samples = 0; - - sc_mutex_lock(&ap->mutex); - can_read = sc_audiobuf_can_read(&ap->buf); - if (can_read > max_buffered_samples) { - skip_samples = can_read - max_buffered_samples; - uint32_t r = sc_audiobuf_read(&ap->buf, NULL, skip_samples); - assert(r == skip_samples); - (void) r; - skipped_samples += skip_samples; - } - sc_mutex_unlock(&ap->mutex); - - if (skip_samples) { - if (played) { - LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 - " samples", skip_samples); -#ifdef SC_AUDIO_PLAYER_DEBUG - } else { - LOGD("[Audio] Playback not started, skipping %" PRIu32 - " samples", skip_samples); -#endif - } - } - } - - atomic_store_explicit(&ap->received, true, memory_order_relaxed); - if (!played) { - // Nothing more to do - return true; - } - - // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = (int32_t) written - frame->nb_samples; - // Inserting silence instantly increases buffering - int32_t inserted_silence = (int32_t) underflow; - // Dropping input samples instantly decreases buffering - int32_t dropped = (int32_t) skipped_samples; - - // The compensation must apply instantly, it must not be smoothed - ap->avg_buffering.avg += instant_compensation + inserted_silence - dropped; - if (ap->avg_buffering.avg < 0) { - // Since dropping samples instantly reduces buffering, the difference - // is applied immediately to the average value, assuming that the delay - // between the producer and the consumer will be caught up. - // - // However, when this assumption is not valid, the average buffering - // may decrease indefinitely. Prevent it to become negative to limit - // the consequences. - ap->avg_buffering.avg = 0; - } - - // However, the buffering level must be smoothed - sc_average_push(&ap->avg_buffering, can_read); - -#ifdef SC_AUDIO_PLAYER_DEBUG - LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", - can_read, sc_average_get(&ap->avg_buffering)); -#endif - - ap->samples_since_resync += written; - if (ap->samples_since_resync >= ap->sample_rate) { - // Recompute compensation every second - ap->samples_since_resync = 0; - - float avg = sc_average_get(&ap->avg_buffering); - int diff = ap->target_buffering - avg; - - // Enable compensation when the difference exceeds +/- 4ms. - // Disable compensation when the difference is lower than +/- 1ms. - int threshold = ap->compensation != 0 - ? ap->sample_rate / 1000 /* 1ms */ - : ap->sample_rate * 4 / 1000; /* 4ms */ - - if (abs(diff) < threshold) { - // Do not compensate for small values, the error is just noise - diff = 0; - } else if (diff < 0 && can_read < ap->target_buffering) { - // Do not accelerate if the instant buffering level is below the - // target, this would increase underflow - diff = 0; - } - // Compensate the diff over 4 seconds (but will be recomputed after 1 - // second) - int distance = 4 * ap->sample_rate; - // Limit compensation rate to 2% - int abs_max_diff = distance / 50; - diff = CLAMP(diff, -abs_max_diff, abs_max_diff); - LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d", ap->target_buffering, avg, can_read, diff); - - if (diff != ap->compensation) { - int ret = swr_set_compensation(swr_ctx, diff, distance); - if (ret < 0) { - LOGW("Resampling compensation failed: %d", ret); - // not fatal - } else { - ap->compensation = diff; - } - } - } - - return true; + return sc_audio_regulator_push(&ap->audioreg, frame); } static bool sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, const AVCodecContext *ctx) { struct sc_audio_player *ap = DOWNCAST(sink); + #ifdef SCRCPY_LAVU_HAS_CHLAYOUT assert(ctx->ch_layout.nb_channels > 0 && ctx->ch_layout.nb_channels < 256); uint8_t nb_channels = ctx->ch_layout.nb_channels; @@ -355,12 +47,17 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, int out_bytes_per_sample = av_get_bytes_per_sample(SC_AV_SAMPLE_FMT); assert(out_bytes_per_sample > 0); - ap->sample_rate = ctx->sample_rate; + uint32_t target_buffering_samples = + ap->target_buffering_delay * ctx->sample_rate / SC_TICK_FREQ; - ap->target_buffering = ap->target_buffering_delay * ap->sample_rate - / SC_TICK_FREQ; + size_t sample_size = nb_channels * out_bytes_per_sample; + bool ok = sc_audio_regulator_init(&ap->audioreg, sample_size, ctx, + target_buffering_samples); + if (!ok) { + return false; + } - uint64_t aout_samples = ap->output_buffer_duration * ap->sample_rate + uint64_t aout_samples = ap->output_buffer_duration * ctx->sample_rate / SC_TICK_FREQ; assert(aout_samples <= 0xFFFF); @@ -377,74 +74,10 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, ap->device = SDL_OpenAudioDevice(NULL, 0, &desired, &obtained, 0); if (!ap->device) { LOGE("Could not open audio device: %s", SDL_GetError()); + sc_audio_regulator_destroy(&ap->audioreg); return false; } - SwrContext *swr_ctx = swr_alloc(); - if (!swr_ctx) { - LOG_OOM(); - goto error_close_audio_device; - } - ap->swr_ctx = swr_ctx; - -#ifdef SCRCPY_LAVU_HAS_CHLAYOUT - av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); - av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); -#else - av_opt_set_channel_layout(swr_ctx, "in_channel_layout", - ctx->channel_layout, 0); - av_opt_set_channel_layout(swr_ctx, "out_channel_layout", - ctx->channel_layout, 0); -#endif - - av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); - av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); - - av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); - av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); - - int ret = swr_init(swr_ctx); - if (ret) { - LOGE("Failed to initialize the resampling context"); - goto error_free_swr_ctx; - } - - // Use a ring-buffer of the target buffering size plus 1 second between the - // producer and the consumer. It's too big on purpose, to guarantee that - // the producer and the consumer will be able to access it in parallel - // without locking. - uint32_t audiobuf_samples = ap->target_buffering + ap->sample_rate; - - bool ok = sc_mutex_init(&ap->mutex); - if (!ok) { - goto error_free_swr_ctx; - } - - size_t sample_size = nb_channels * out_bytes_per_sample; - ok = sc_audiobuf_init(&ap->buf, sample_size, audiobuf_samples); - if (!ok) { - goto error_destroy_mutex; - } - - size_t initial_swr_buf_size = TO_BYTES(4096); - ap->swr_buf = malloc(initial_swr_buf_size); - if (!ap->swr_buf) { - LOG_OOM(); - goto error_destroy_audiobuf; - } - ap->swr_buf_alloc_size = initial_swr_buf_size; - - // Samples are produced and consumed by blocks, so the buffering must be - // smoothed to get a relatively stable value. - sc_average_init(&ap->avg_buffering, 128); - ap->samples_since_resync = 0; - - ap->received = false; - atomic_init(&ap->played, false); - atomic_init(&ap->received, false); - atomic_init(&ap->underflow, 0); - ap->compensation = 0; - // The thread calling open() is the thread calling push(), which fills the // audio buffer consumed by the SDL audio thread. ok = sc_thread_set_priority(SC_THREAD_PRIORITY_TIME_CRITICAL); @@ -456,17 +89,6 @@ sc_audio_player_frame_sink_open(struct sc_frame_sink *sink, SDL_PauseAudioDevice(ap->device, 0); return true; - -error_destroy_audiobuf: - sc_audiobuf_destroy(&ap->buf); -error_destroy_mutex: - sc_mutex_destroy(&ap->mutex); -error_free_swr_ctx: - swr_free(&ap->swr_ctx); -error_close_audio_device: - SDL_CloseAudioDevice(ap->device); - - return false; } static void @@ -477,10 +99,7 @@ sc_audio_player_frame_sink_close(struct sc_frame_sink *sink) { SDL_PauseAudioDevice(ap->device, 1); SDL_CloseAudioDevice(ap->device); - free(ap->swr_buf); - sc_audiobuf_destroy(&ap->buf); - sc_mutex_destroy(&ap->mutex); - swr_free(&ap->swr_ctx); + sc_audio_regulator_destroy(&ap->audioreg); } void diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 4ad40306..9133c24a 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -5,66 +5,27 @@ #include #include -#include -#include #include +#include "audio_regulator.h" #include "trait/frame_sink.h" -#include "util/audiobuf.h" -#include "util/average.h" -#include "util/thread.h" #include "util/tick.h" struct sc_audio_player { struct sc_frame_sink frame_sink; - SDL_AudioDeviceID device; - - sc_mutex mutex; - // The target buffering between the producer and the consumer. This value // is directly use for compensation. // Since audio capture and/or encoding on the device typically produce // blocks of 960 samples (20ms) or 1024 samples (~21.3ms), this target // value should be higher. sc_tick target_buffering_delay; - uint32_t target_buffering; // in samples - // SDL audio output buffer size. + // SDL audio output buffer size sc_tick output_buffer_duration; - // Audio buffer to communicate between the receiver and the SDL audio - // callback - struct sc_audiobuf buf; - - // Resampler (only used from the receiver thread) - struct SwrContext *swr_ctx; - - // The sample rate is the same for input and output - uint32_t sample_rate; - - // Target buffer for resampling (only used by the receiver thread) - uint8_t *swr_buf; - size_t swr_buf_alloc_size; - - // Number of buffered samples (may be negative on underflow) (only used by - // the receiver thread) - struct sc_average avg_buffering; - // Count the number of samples to trigger a compensation update regularly - // (only used by the receiver thread) - uint32_t samples_since_resync; - - // Number of silence samples inserted since the last received packet - atomic_uint_least32_t underflow; - - // Current applied compensation value (only used by the receiver thread) - int compensation; - - // Set to true the first time a sample is received - atomic_bool received; - - // Set to true the first time the SDL callback is called - atomic_bool played; + SDL_AudioDeviceID device; + struct sc_audio_regulator audioreg; }; void diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c new file mode 100644 index 00000000..911b2bfa --- /dev/null +++ b/app/src/audio_regulator.c @@ -0,0 +1,415 @@ +#include "audio_regulator.h" + +#include +#include + +#include "util/log.h" + +//#define SC_AUDIO_REGULATOR_DEBUG // uncomment to debug + +/** + * Real-time audio regulator with configurable latency + * + * As input, the regulator regularly receives AVFrames of decoded audio samples. + * As output, the audio player regularly requests audio samples to be played. + * In the middle, an audio buffer stores the samples produced but not consumed + * yet. + * + * The goal of the regulator is to feed the audio player with a latency as low + * as possible while avoiding buffer underrun (i.e. not being able to provide + * samples when requested). + * + * To achieve this, it attempts to maintain the average buffering (the number + * of samples present in the buffer) around a target value. If this target + * buffering is too low, then buffer underrun will occur frequently. If it is + * too high, then latency will become unacceptable. This target value is + * configured using the scrcpy option --audio-buffer. + * + * The regulator cannot adjust the sample input rate (it receives samples + * produced in real-time) or the sample output rate (it must provide samples as + * requested by the audio player). Therefore, it may only apply compensation by + * resampling (converting _m_ input samples to _n_ output samples). + * + * The compensation itself is applied by libswresample (FFmpeg). It is + * configured using swr_set_compensation(). An important work for the regulator + * is to estimate the compensation value regularly and apply it. + * + * The estimated buffering level is the result of averaging the "natural" + * buffering (samples are produced and consumed by blocks, so it must be + * smoothed), and making instant adjustments resulting of its own actions + * (explicit compensation and silence insertion on underflow), which are not + * smoothed. + * + * Buffer underflow events can occur when packets arrive too late. In that case, + * the regulator inserts silence. Once the packets finally arrive (late), one + * strategy could be to drop the samples that were replaced by silence, in + * order to keep a minimal latency. However, dropping samples in case of buffer + * underflow is inadvisable, as it would temporarily increase the underflow + * even more and cause very noticeable audio glitches. + * + * Therefore, the regulator doesn't drop any sample on underflow. The + * compensation mechanism will absorb the delay introduced by the inserted + * silence. + */ + +#define TO_BYTES(SAMPLES) sc_audiobuf_to_bytes(&ar->buf, (SAMPLES)) +#define TO_SAMPLES(BYTES) sc_audiobuf_to_samples(&ar->buf, (BYTES)) + +void +sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, + uint32_t out_samples) { +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] Audio regulator pulls %" PRIu32 " samples", out_samples); +#endif + + // A lock is necessary in the rare case where the producer needs to drop + // samples already pushed (when the buffer is full) + sc_mutex_lock(&ar->mutex); + + bool played = atomic_load_explicit(&ar->played, memory_order_relaxed); + if (!played) { + uint32_t buffered_samples = sc_audiobuf_can_read(&ar->buf); + // Wait until the buffer is filled up to at least target_buffering + // before playing + if (buffered_samples < ar->target_buffering) { + LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 + " samples", out_samples); + // Delay playback starting to reach the target buffering. Fill the + // whole buffer with silence (len is small compared to the + // arbitrary margin value). + memset(out, 0, out_samples * ar->sample_size); + sc_mutex_unlock(&ar->mutex); + return; + } + } + + uint32_t read = sc_audiobuf_read(&ar->buf, out, out_samples); + + sc_mutex_unlock(&ar->mutex); + + if (read < out_samples) { + uint32_t silence = out_samples - read; + // Insert silence. In theory, the inserted silent samples replace the + // missing real samples, which will arrive later, so they should be + // dropped to keep the latency minimal. However, this would cause very + // audible glitches, so let the clock compensation restore the target + // latency. + LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", + silence); + memset(out + TO_BYTES(read), 0, TO_BYTES(silence)); + + bool received = atomic_load_explicit(&ar->received, + memory_order_relaxed); + if (received) { + // Inserting additional samples immediately increases buffering + atomic_fetch_add_explicit(&ar->underflow, silence, + memory_order_relaxed); + } + } + + atomic_store_explicit(&ar->played, true, memory_order_relaxed); +} + +static uint8_t * +sc_audio_regulator_get_swr_buf(struct sc_audio_regulator *ar, + uint32_t min_samples) { + size_t min_buf_size = TO_BYTES(min_samples); + if (min_buf_size > ar->swr_buf_alloc_size) { + size_t new_size = min_buf_size + 4096; + uint8_t *buf = realloc(ar->swr_buf, new_size); + if (!buf) { + LOG_OOM(); + // Could not realloc to the requested size + return NULL; + } + ar->swr_buf = buf; + ar->swr_buf_alloc_size = new_size; + } + + return ar->swr_buf; +} + +bool +sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { + SwrContext *swr_ctx = ar->swr_ctx; + + int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate); + // No need to av_rescale_rnd(), input and output sample rates are the same. + // Add more space (256) for clock compensation. + int dst_nb_samples = swr_delay + frame->nb_samples + 256; + + uint8_t *swr_buf = sc_audio_regulator_get_swr_buf(ar, dst_nb_samples); + if (!swr_buf) { + return false; + } + + int ret = swr_convert(swr_ctx, &swr_buf, dst_nb_samples, + (const uint8_t **) frame->data, frame->nb_samples); + if (ret < 0) { + LOGE("Resampling failed: %d", ret); + return false; + } + + // swr_convert() returns the number of samples which would have been + // written if the buffer was big enough. + uint32_t samples = MIN(ret, dst_nb_samples); +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] %" PRIu32 " samples written to buffer", samples); +#endif + + uint32_t cap = sc_audiobuf_capacity(&ar->buf); + if (samples > cap) { + // Very very unlikely: a single resampled frame should never + // exceed the audio buffer size (or something is very wrong). + // Ignore the first bytes in swr_buf to avoid memory corruption anyway. + swr_buf += TO_BYTES(samples - cap); + samples = cap; + } + + uint32_t skipped_samples = 0; + + uint32_t written = sc_audiobuf_write(&ar->buf, swr_buf, samples); + if (written < samples) { + uint32_t remaining = samples - written; + + // All samples that could be written without locking have been written, + // now we need to lock to drop/consume old samples + sc_mutex_lock(&ar->mutex); + + // Retry with the lock + written += sc_audiobuf_write(&ar->buf, + swr_buf + TO_BYTES(written), + remaining); + if (written < samples) { + remaining = samples - written; + // Still insufficient, drop old samples to make space + skipped_samples = sc_audiobuf_read(&ar->buf, NULL, remaining); + assert(skipped_samples == remaining); + } + + sc_mutex_unlock(&ar->mutex); + + if (written < samples) { + // Now there is enough space + uint32_t w = sc_audiobuf_write(&ar->buf, + swr_buf + TO_BYTES(written), + remaining); + assert(w == remaining); + (void) w; + } + } + + uint32_t underflow = 0; + uint32_t max_buffered_samples; + bool played = atomic_load_explicit(&ar->played, memory_order_relaxed); + if (played) { + underflow = atomic_exchange_explicit(&ar->underflow, 0, + memory_order_relaxed); + + max_buffered_samples = ar->target_buffering * 11 / 10 + + 60 * ar->sample_rate / 1000 /* 60 ms */; + } else { + // Playback not started yet, do not accumulate more than + // max_initial_buffering samples, this would cause unnecessary delay + // (and glitches to compensate) on start. + max_buffered_samples = ar->target_buffering + + 10 * ar->sample_rate / 1000 /* 10 ms */; + } + + uint32_t can_read = sc_audiobuf_can_read(&ar->buf); + if (can_read > max_buffered_samples) { + uint32_t skip_samples = 0; + + sc_mutex_lock(&ar->mutex); + can_read = sc_audiobuf_can_read(&ar->buf); + if (can_read > max_buffered_samples) { + skip_samples = can_read - max_buffered_samples; + uint32_t r = sc_audiobuf_read(&ar->buf, NULL, skip_samples); + assert(r == skip_samples); + (void) r; + skipped_samples += skip_samples; + } + sc_mutex_unlock(&ar->mutex); + + if (skip_samples) { + if (played) { + LOGD("[Audio] Buffering threshold exceeded, skipping %" PRIu32 + " samples", skip_samples); +#ifdef SC_AUDIO_REGULATOR_DEBUG + } else { + LOGD("[Audio] Playback not started, skipping %" PRIu32 + " samples", skip_samples); +#endif + } + } + } + + atomic_store_explicit(&ar->received, true, memory_order_relaxed); + if (!played) { + // Nothing more to do + return true; + } + + // Number of samples added (or removed, if negative) for compensation + int32_t instant_compensation = (int32_t) written - frame->nb_samples; + // Inserting silence instantly increases buffering + int32_t inserted_silence = (int32_t) underflow; + // Dropping input samples instantly decreases buffering + int32_t dropped = (int32_t) skipped_samples; + + // The compensation must apply instantly, it must not be smoothed + ar->avg_buffering.avg += instant_compensation + inserted_silence - dropped; + if (ar->avg_buffering.avg < 0) { + // Since dropping samples instantly reduces buffering, the difference + // is applied immediately to the average value, assuming that the delay + // between the producer and the consumer will be caught up. + // + // However, when this assumption is not valid, the average buffering + // may decrease indefinitely. Prevent it to become negative to limit + // the consequences. + ar->avg_buffering.avg = 0; + } + + // However, the buffering level must be smoothed + sc_average_push(&ar->avg_buffering, can_read); + +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] can_read=%" PRIu32 " avg_buffering=%f", + can_read, sc_average_get(&ar->avg_buffering)); +#endif + + ar->samples_since_resync += written; + if (ar->samples_since_resync >= ar->sample_rate) { + // Recompute compensation every second + ar->samples_since_resync = 0; + + float avg = sc_average_get(&ar->avg_buffering); + int diff = ar->target_buffering - avg; + + // Enable compensation when the difference exceeds +/- 4ms. + // Disable compensation when the difference is lower than +/- 1ms. + int threshold = ar->compensation != 0 + ? ar->sample_rate / 1000 /* 1ms */ + : ar->sample_rate * 4 / 1000; /* 4ms */ + + if (abs(diff) < threshold) { + // Do not compensate for small values, the error is just noise + diff = 0; + } else if (diff < 0 && can_read < ar->target_buffering) { + // Do not accelerate if the instant buffering level is below the + // target, this would increase underflow + diff = 0; + } + // Compensate the diff over 4 seconds (but will be recomputed after 1 + // second) + int distance = 4 * ar->sample_rate; + // Limit compensation rate to 2% + int abs_max_diff = distance / 50; + diff = CLAMP(diff, -abs_max_diff, abs_max_diff); + LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 + " compensation=%d", ar->target_buffering, avg, can_read, diff); + + if (diff != ar->compensation) { + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } else { + ar->compensation = diff; + } + } + } + + return true; +} + +bool +sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, + const AVCodecContext *ctx, uint32_t target_buffering) { + SwrContext *swr_ctx = swr_alloc(); + if (!swr_ctx) { + LOG_OOM(); + return false; + } + ar->swr_ctx = swr_ctx; + +#ifdef SCRCPY_LAVU_HAS_CHLAYOUT + av_opt_set_chlayout(swr_ctx, "in_chlayout", &ctx->ch_layout, 0); + av_opt_set_chlayout(swr_ctx, "out_chlayout", &ctx->ch_layout, 0); +#else + av_opt_set_channel_layout(swr_ctx, "in_channel_layout", + ctx->channel_layout, 0); + av_opt_set_channel_layout(swr_ctx, "out_channel_layout", + ctx->channel_layout, 0); +#endif + + av_opt_set_int(swr_ctx, "in_sample_rate", ctx->sample_rate, 0); + av_opt_set_int(swr_ctx, "out_sample_rate", ctx->sample_rate, 0); + + av_opt_set_sample_fmt(swr_ctx, "in_sample_fmt", ctx->sample_fmt, 0); + av_opt_set_sample_fmt(swr_ctx, "out_sample_fmt", SC_AV_SAMPLE_FMT, 0); + + int ret = swr_init(swr_ctx); + if (ret) { + LOGE("Failed to initialize the resampling context"); + goto error_free_swr_ctx; + } + + bool ok = sc_mutex_init(&ar->mutex); + if (!ok) { + goto error_free_swr_ctx; + } + + ar->target_buffering = target_buffering; + ar->sample_size = sample_size; + ar->sample_rate = ctx->sample_rate; + + // Use a ring-buffer of the target buffering size plus 1 second between the + // producer and the consumer. It's too big on purpose, to guarantee that + // the producer and the consumer will be able to access it in parallel + // without locking. + uint32_t audiobuf_samples = target_buffering + ar->sample_rate; + + ok = sc_audiobuf_init(&ar->buf, sample_size, audiobuf_samples); + if (!ok) { + goto error_destroy_mutex; + } + + size_t initial_swr_buf_size = TO_BYTES(4096); + ar->swr_buf = malloc(initial_swr_buf_size); + if (!ar->swr_buf) { + LOG_OOM(); + goto error_destroy_audiobuf; + } + ar->swr_buf_alloc_size = initial_swr_buf_size; + + // Samples are produced and consumed by blocks, so the buffering must be + // smoothed to get a relatively stable value. + sc_average_init(&ar->avg_buffering, 128); + ar->samples_since_resync = 0; + + ar->received = false; + atomic_init(&ar->played, false); + atomic_init(&ar->received, false); + atomic_init(&ar->underflow, 0); + ar->compensation = 0; + + return true; + +error_destroy_audiobuf: + sc_audiobuf_destroy(&ar->buf); +error_destroy_mutex: + sc_mutex_destroy(&ar->mutex); +error_free_swr_ctx: + swr_free(&ar->swr_ctx); + + return false; +} + +void +sc_audio_regulator_destroy(struct sc_audio_regulator *ar) { + free(ar->swr_buf); + sc_audiobuf_destroy(&ar->buf); + sc_mutex_destroy(&ar->mutex); + swr_free(&ar->swr_ctx); +} diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h new file mode 100644 index 00000000..7daa1b05 --- /dev/null +++ b/app/src/audio_regulator.h @@ -0,0 +1,71 @@ +#ifndef SC_AUDIO_REGULATOR_H +#define SC_AUDIO_REGULATOR_H + +#include "common.h" + +#include +#include +#include +#include +#include "util/audiobuf.h" +#include "util/average.h" +#include "util/thread.h" + +#define SC_AV_SAMPLE_FMT AV_SAMPLE_FMT_FLT + +struct sc_audio_regulator { + sc_mutex mutex; + + // Target buffering between the producer and the consumer (in samples) + uint32_t target_buffering; + + // Audio buffer to communicate between the receiver and the player + struct sc_audiobuf buf; + + // Resampler (only used from the receiver thread) + struct SwrContext *swr_ctx; + + // The sample rate is the same for input and output + uint32_t sample_rate; + // The number of bytes per sample (for all channels) + size_t sample_size; + + // Target buffer for resampling (only used by the receiver thread) + uint8_t *swr_buf; + size_t swr_buf_alloc_size; + + // Number of buffered samples (may be negative on underflow) (only used by + // the receiver thread) + struct sc_average avg_buffering; + // Count the number of samples to trigger a compensation update regularly + // (only used by the receiver thread) + uint32_t samples_since_resync; + + // Number of silence samples inserted since the last received packet + atomic_uint_least32_t underflow; + + // Current applied compensation value (only used by the receiver thread) + int compensation; + + // Set to true the first time a sample is received + atomic_bool received; + + // Set to true the first time samples are pulled by the player + atomic_bool played; +}; + +bool +sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, + const AVCodecContext *ctx, uint32_t target_buffering); + +void +sc_audio_regulator_destroy(struct sc_audio_regulator *ar); + +bool +sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame); + +void +sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, + uint32_t samples); + +#endif From d92b7a60243f1e08141d8d9bfbc94dadd1b19ac8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 19:59:49 +0200 Subject: [PATCH 009/278] Rename switch_fullscreen() to toggle_fullscreen() Toggle means to switch between two states. --- app/src/input_manager.c | 2 +- app/src/screen.c | 4 ++-- app/src/screen.h | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 77cb4f1d..b1d7e9b9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -536,7 +536,7 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_f: if (video && !shift && !repeat && down) { - sc_screen_switch_fullscreen(im->screen); + sc_screen_toggle_fullscreen(im->screen); } return; case SDLK_w: diff --git a/app/src/screen.c b/app/src/screen.c index cb455cb1..ce730f19 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -538,7 +538,7 @@ sc_screen_show_initial_window(struct sc_screen *screen) { SDL_SetWindowPosition(screen->window, x, y); if (screen->req.fullscreen) { - sc_screen_switch_fullscreen(screen); + sc_screen_toggle_fullscreen(screen); } if (screen->req.start_fps_counter) { @@ -774,7 +774,7 @@ sc_screen_set_paused(struct sc_screen *screen, bool paused) { } void -sc_screen_switch_fullscreen(struct sc_screen *screen) { +sc_screen_toggle_fullscreen(struct sc_screen *screen) { assert(screen->video); uint32_t new_mode = screen->fullscreen ? 0 : SDL_WINDOW_FULLSCREEN_DESKTOP; diff --git a/app/src/screen.h b/app/src/screen.h index 7e1f7e6e..6d5964bd 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -126,9 +126,9 @@ sc_screen_destroy(struct sc_screen *screen); void sc_screen_hide_window(struct sc_screen *screen); -// switch the fullscreen mode +// toggle the fullscreen mode void -sc_screen_switch_fullscreen(struct sc_screen *screen); +sc_screen_toggle_fullscreen(struct sc_screen *screen); // resize window to optimal size (remove black borders) void From 7a9ea5c66fedbb5b3b1d02f51695aa4ab259dfe3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 21:38:09 +0200 Subject: [PATCH 010/278] Add shortcut for horizontal tilt Use Ctrl+Shift for horizontal tilt. Refs #4529 comment Fixes #5317 --- app/scrcpy.1 | 6 +++++- app/src/cli.c | 6 +++++- app/src/input_manager.c | 20 ++++++++++++++++---- doc/control.md | 8 ++++++-- doc/shortcuts.md | 3 ++- 5 files changed, 34 insertions(+), 9 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index a256c40e..3fd3eb29 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -727,7 +727,11 @@ Pinch-to-zoom and rotate from the center of the screen .TP .B Shift+click-and-move -Tilt (slide vertically with two fingers) +Tilt vertically (slide with 2 fingers) + +.TP +.B Ctrl+Shift+click-and-move +Tilt horizontally (slide with 2 fingers) .TP .B Drag & drop APK file diff --git a/app/src/cli.c b/app/src/cli.c index 3c1f9a1b..4fc3c534 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1072,7 +1072,11 @@ static const struct sc_shortcut shortcuts[] = { }, { .shortcuts = { "Shift+click-and-move" }, - .text = "Tilt (slide vertically with two fingers)", + .text = "Tilt vertically (slide with 2 fingers)", + }, + { + .shortcuts = { "Ctrl+Shift+click-and-move" }, + .text = "Tilt horizontally (slide with 2 fingers)", }, { .shortcuts = { "Drag & drop APK file" }, diff --git a/app/src/input_manager.c b/app/src/input_manager.c index b1d7e9b9..444a5f16 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -836,7 +836,7 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, } bool change_vfinger = event->button == SDL_BUTTON_LEFT && - ((down && !im->vfinger_down && (ctrl_pressed ^ shift_pressed)) || + ((down && !im->vfinger_down && (ctrl_pressed || shift_pressed)) || (!down && im->vfinger_down)); bool use_finger = im->vfinger_down || change_vfinger; @@ -868,16 +868,28 @@ sc_input_manager_process_mouse_button(struct sc_input_manager *im, // In other words, the center of the rotation/scaling is the center of the // screen. // - // To simulate a tilt gesture (a vertical slide with two fingers), Shift - // can be used instead of Ctrl. The "virtual finger" has a position + // To simulate a vertical tilt gesture (a vertical slide with two fingers), + // Shift can be used instead of Ctrl. The "virtual finger" has a position // inverted with respect to the vertical axis of symmetry in the middle of // the screen. + // + // To simulate a horizontal tilt gesture (a horizontal slide with two + // fingers), Ctrl+Shift can be used. The "virtual finger" has a position + // inverted with respect to the horizontal axis of symmetry in the middle + // of the screen. It is expected to be less frequently used, that's why the + // one-mod shortcuts are assigned to rotation and vertical tilt. if (change_vfinger) { struct sc_point mouse = sc_screen_convert_window_to_frame_coords(im->screen, event->x, event->y); if (down) { - im->vfinger_invert_x = ctrl_pressed || shift_pressed; + // Ctrl Shift invert_x invert_y + // ---- ----- ==> -------- -------- + // 0 0 0 0 - + // 0 1 1 0 vertical tilt + // 1 0 1 1 rotate + // 1 1 0 1 horizontal tilt + im->vfinger_invert_x = ctrl_pressed ^ shift_pressed; im->vfinger_invert_y = ctrl_pressed; } struct sc_point vfinger = inverse_point(mouse, im->screen->frame_size, diff --git a/doc/control.md b/doc/control.md index 34eb7a6a..26805346 100644 --- a/doc/control.md +++ b/doc/control.md @@ -94,14 +94,18 @@ the content (if supported by the app) relative to the center of the screen. https://github.com/Genymobile/scrcpy/assets/543275/26c4a920-9805-43f1-8d4c-608752d04767 -To simulate a tilt gesture: Shift+_click-and-move-up-or-down_. +To simulate a vertical tilt gesture: Shift+_click-and-move-up-or-down_. https://github.com/Genymobile/scrcpy/assets/543275/1e252341-4a90-4b29-9d11-9153b324669f +Similarly, to simulate a horizontal tilt gesture: +Ctrl+Shift+_click-and-move-left-or-right_. + Technically, _scrcpy_ generates additional touch events from a "virtual finger" at a location inverted through the center of the screen. When pressing Ctrl the _x_ and _y_ coordinates are inverted. Using Shift -only inverts _x_. +only inverts _x_, whereas using Ctrl+Shift only inverts +_y_. This only works for the default mouse mode (`--mouse=sdk`). diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 841ceaa6..4ea37257 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -53,7 +53,8 @@ _[Super] is typically the Windows or Cmd key._ | Open keyboard settings (HID keyboard only) | MOD+k | Enable/disable FPS counter (on stdout) | MOD+i | Pinch-to-zoom/rotate | Ctrl+_click-and-move_ - | Tilt (slide vertically with 2 fingers) | Shift+_click-and-move_ + | Tilt vertically (slide with 2 fingers) | Shift+_click-and-move_ + | Tilt horizontally (slide with 2 fingers) | Ctrl+Shift+_click-and-move_ | Drag & drop APK file | Install APK from computer | Drag & drop non-APK file | [Push file to device](control.md#push-file-to-device) From ec602a0334357982d75b374f7ac753c5bef1216a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 30 Sep 2024 08:12:08 +0200 Subject: [PATCH 011/278] Suggest command line arguments without quotes Replace argument suggestion: --video-encoder='c2.android.avc.encoder' by: --video-encoder=c2.android.avc.encoder On Linux, the quotes are interpreted by the shell, but on Windows they are passed as is. This was harmless, because even transmitted as is, they were interpreted by the shell on the device. However, special characters are now validated since commit bec3321fff4c6dc3b3dbc61fdc6fd98913988a78, making the command fail. Fixes #5329 --- server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index aee1594a..45ab4eba 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -31,7 +31,7 @@ public final class LogUtils { } else { for (CodecUtils.DeviceEncoder encoder : videoEncoders) { builder.append("\n --video-codec=").append(encoder.getCodec().getName()); - builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'"); + builder.append(" --video-encoder=").append(encoder.getInfo().getName()); } } return builder.toString(); @@ -45,7 +45,7 @@ public final class LogUtils { } else { for (CodecUtils.DeviceEncoder encoder : audioEncoders) { builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); - builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'"); + builder.append(" --audio-encoder=").append(encoder.getInfo().getName()); } } return builder.toString(); From c0a6432967c54d739cb0f01e87c834c3927f84f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Oct 2024 22:39:06 +0200 Subject: [PATCH 012/278] Extract EINTR handling for Os.write() Expose a function which retries automatically on EINTR, and throws an IOException on other errors. --- .../java/com/genymobile/scrcpy/util/IO.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index ab3fa59f..5c558c1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -17,24 +17,30 @@ public final class IO { // not instantiable } + private static int write(FileDescriptor fd, ByteBuffer from) throws IOException { + while (true) { + try { + return Os.write(fd, from); + } catch (ErrnoException e) { + if (e.errno != OsConstants.EINTR) { + throw new IOException(e); + } + } + } + } + public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so // count the remaining bytes manually. // See . int remaining = from.remaining(); while (remaining > 0) { - try { - int w = Os.write(fd, from); - if (BuildConfig.DEBUG && w < 0) { - // w should not be negative, since an exception is thrown on error - throw new AssertionError("Os.write() returned a negative value (" + w + ")"); - } - remaining -= w; - } catch (ErrnoException e) { - if (e.errno != OsConstants.EINTR) { - throw new IOException(e); - } + int w = write(fd, from); + if (BuildConfig.DEBUG && w < 0) { + // w should not be negative, since an exception is thrown on error + throw new AssertionError("Os.write() returned a negative value (" + w + ")"); } + remaining -= w; } } From 79014143b9cc958ed4b36b8e9a49676243ca68b7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Oct 2024 22:49:55 +0200 Subject: [PATCH 013/278] Fix IO.writeFully() on Android 5 Os.write() did not update the ByteBuffer position before Android 6. A workaround was added by commit b882322f7371b16acd53677c4a3adbaaed0aef77, which fixed part of the problem, but the position was still not updated across calls, causing the wrong chunk to be written. Refs --- server/src/main/java/com/genymobile/scrcpy/util/IO.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index 5c558c1b..8ef1500d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -31,8 +31,9 @@ public final class IO { public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so - // count the remaining bytes manually. + // handle the position and the remaining bytes manually. // See . + int position = from.position(); int remaining = from.remaining(); while (remaining > 0) { int w = write(fd, from); @@ -41,6 +42,8 @@ public final class IO { throw new AssertionError("Os.write() returned a negative value (" + w + ")"); } remaining -= w; + position += w; + from.position(position); } } From e724ff43490661d9b1c7f92632303a4f08768f03 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Oct 2024 22:50:34 +0200 Subject: [PATCH 014/278] Simplify IO.writeFully() for Android >= 6 Do not handle buffer properties manually for Android >= 6 (where it is already handled by Os.write()). Refs --- .../java/com/genymobile/scrcpy/util/IO.java | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index 8ef1500d..d9247a98 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.util; import com.genymobile.scrcpy.BuildConfig; +import android.os.Build; import android.system.ErrnoException; import android.system.Os; import android.system.OsConstants; @@ -30,20 +31,26 @@ public final class IO { } public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { - // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so - // handle the position and the remaining bytes manually. - // See . - int position = from.position(); - int remaining = from.remaining(); - while (remaining > 0) { - int w = write(fd, from); - if (BuildConfig.DEBUG && w < 0) { - // w should not be negative, since an exception is thrown on error - throw new AssertionError("Os.write() returned a negative value (" + w + ")"); + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + while (from.hasRemaining()) { + write(fd, from); + } + } else { + // ByteBuffer position is not updated as expected by Os.write() on old Android versions, so + // handle the position and the remaining bytes manually. + // See . + int position = from.position(); + int remaining = from.remaining(); + while (remaining > 0) { + int w = write(fd, from); + if (BuildConfig.DEBUG && w < 0) { + // w should not be negative, since an exception is thrown on error + throw new AssertionError("Os.write() returned a negative value (" + w + ")"); + } + remaining -= w; + position += w; + from.position(position); } - remaining -= w; - position += w; - from.position(position); } } From a6f74d72f52c96fa20ccd49e216383321e4a9efa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 22:26:07 +0200 Subject: [PATCH 015/278] Forward Alt and Super with SDK keyboard Alt and Super (also named Meta) modifier keys are captured for shortcuts by default (cf --shortcut-mod). However, when shortcut modifiers are changed, Alt and Super should be forwarded to the device. This is the case for AOA and UHID keyboards, but it was not the case for SDK keyboard. Fixes #5318 PR #5322 --- app/src/keyboard_sdk.c | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 00b7f92a..2d9ca85b 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -45,6 +45,10 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, {SC_KEYCODE_RCTRL, AKEYCODE_CTRL_RIGHT}, {SC_KEYCODE_LSHIFT, AKEYCODE_SHIFT_LEFT}, {SC_KEYCODE_RSHIFT, AKEYCODE_SHIFT_RIGHT}, + {SC_KEYCODE_LALT, AKEYCODE_ALT_LEFT}, + {SC_KEYCODE_RALT, AKEYCODE_ALT_RIGHT}, + {SC_KEYCODE_LGUI, AKEYCODE_META_LEFT}, + {SC_KEYCODE_RGUI, AKEYCODE_META_RIGHT}, }; // Numpad navigation keys. @@ -166,11 +170,7 @@ convert_keycode(enum sc_keycode from, enum android_keycode *to, uint16_t mod, return false; } - if (mod & (SC_MOD_LALT | SC_MOD_RALT | SC_MOD_LGUI | SC_MOD_RGUI)) { - return false; - } - - // if ALT and META are not pressed, also handle letters and space + // Handle letters and space entry = SC_INTMAP_FIND_ENTRY(alphaspace_keys, from); if (entry) { *to = entry->value; From 65fc53eace19392426631ba2f5bcbd9aec88d796 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 22:39:22 +0200 Subject: [PATCH 016/278] Simplify (and inline) is_shortcut_mod() Masking was unnecessary (im->sdl_shortcut_mods is implicitly masked). PR #5322 --- app/src/input_manager.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 444a5f16..0f121da9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -33,10 +33,10 @@ to_sdl_mod(uint8_t shortcut_mod) { return sdl_mod; } -static bool +static inline bool is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { - // keep only the relevant modifier keys - sdl_mod &= SC_SDL_SHORTCUT_MODS_MASK; + // im->sdl_shortcut_mods is within the mask + assert(!(im->sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK)); // at least one shortcut mod pressed? return sdl_mod & im->sdl_shortcut_mods; From 281fcc705254653edc4b418ab68951c6fd069622 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 25 Sep 2024 22:50:44 +0200 Subject: [PATCH 017/278] Extract mouse capture Factorize mouse capture for relative mouse mode to reduce code duplication between normal and OTG modes. PR #5322 --- app/meson.build | 1 + app/src/mouse_capture.c | 120 ++++++++++++++++++++++++++++++++++++++ app/src/mouse_capture.h | 35 +++++++++++ app/src/screen.c | 123 ++++----------------------------------- app/src/screen.h | 6 +- app/src/usb/screen_otg.c | 104 ++++----------------------------- app/src/usb/screen_otg.h | 4 +- 7 files changed, 183 insertions(+), 210 deletions(-) create mode 100644 app/src/mouse_capture.c create mode 100644 app/src/mouse_capture.h diff --git a/app/meson.build b/app/meson.build index 99e7e3a2..9d179101 100644 --- a/app/meson.build +++ b/app/meson.build @@ -23,6 +23,7 @@ src = [ 'src/frame_buffer.c', 'src/input_manager.c', 'src/keyboard_sdk.c', + 'src/mouse_capture.c', 'src/mouse_sdk.c', 'src/opengl.c', 'src/options.c', diff --git a/app/src/mouse_capture.c b/app/src/mouse_capture.c new file mode 100644 index 00000000..1420bad6 --- /dev/null +++ b/app/src/mouse_capture.c @@ -0,0 +1,120 @@ +#include "mouse_capture.h" + +#include "util/log.h" + +void +sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window) { + mc->window = window; + mc->mouse_capture_key_pressed = SDLK_UNKNOWN; +} + +static inline bool +sc_mouse_capture_is_capture_key(SDL_Keycode key) { + return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +} + +bool +sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, + const SDL_Event *event) { + switch (event->type) { + case SDL_WINDOWEVENT: + if (event->window.event == SDL_WINDOWEVENT_FOCUS_LOST) { + sc_mouse_capture_set_active(mc, false); + return true; + } + break; + case SDL_KEYDOWN: { + SDL_Keycode key = event->key.keysym.sym; + if (sc_mouse_capture_is_capture_key(key)) { + if (!mc->mouse_capture_key_pressed) { + mc->mouse_capture_key_pressed = key; + } else { + // Another mouse capture key has been pressed, cancel + // mouse (un)capture + mc->mouse_capture_key_pressed = 0; + } + // Mouse capture keys are never forwarded to the device + return true; + } + break; + } + case SDL_KEYUP: { + SDL_Keycode key = event->key.keysym.sym; + SDL_Keycode cap = mc->mouse_capture_key_pressed; + mc->mouse_capture_key_pressed = 0; + if (sc_mouse_capture_is_capture_key(key)) { + if (key == cap) { + // A mouse capture key has been pressed then released: + // toggle the capture mouse mode + sc_mouse_capture_toggle(mc); + } + // Mouse capture keys are never forwarded to the device + return true; + } + break; + } + case SDL_MOUSEWHEEL: + case SDL_MOUSEMOTION: + case SDL_MOUSEBUTTONDOWN: + if (!sc_mouse_capture_is_active(mc)) { + // The mouse will be captured on SDL_MOUSEBUTTONUP, so consume + // the event + return true; + } + break; + case SDL_MOUSEBUTTONUP: + if (!sc_mouse_capture_is_active(mc)) { + sc_mouse_capture_set_active(mc, true); + return true; + } + break; + case SDL_FINGERMOTION: + case SDL_FINGERDOWN: + case SDL_FINGERUP: + // Touch events are not compatible with relative mode + // (coordinates are not relative), so consume the event + return true; + } + + return false; +} + +void +sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) { +#ifdef __APPLE__ + // Workaround for SDL bug on macOS: + // + if (capture) { + int mouse_x, mouse_y; + SDL_GetGlobalMouseState(&mouse_x, &mouse_y); + + int x, y, w, h; + SDL_GetWindowPosition(window, &x, &y); + SDL_GetWindowSize(window, &w, &h); + + bool outside_window = mouse_x < x || mouse_x >= x + w + || mouse_y < y || mouse_y >= y + h; + if (outside_window) { + SDL_WarpMouseInWindow(mc->window, w / 2, h / 2); + } + } +#else + (void) mc; +#endif + if (SDL_SetRelativeMouseMode(capture)) { + LOGE("Could not set relative mouse mode to %s: %s", + capture ? "true" : "false", SDL_GetError()); + } +} + +bool +sc_mouse_capture_is_active(struct sc_mouse_capture *mc) { + (void) mc; + return SDL_GetRelativeMouseMode(); +} + +void +sc_mouse_capture_toggle(struct sc_mouse_capture *mc) { + bool new_value = !sc_mouse_capture_is_active(mc); + sc_mouse_capture_set_active(mc, new_value); +} diff --git a/app/src/mouse_capture.h b/app/src/mouse_capture.h new file mode 100644 index 00000000..53018c19 --- /dev/null +++ b/app/src/mouse_capture.h @@ -0,0 +1,35 @@ +#ifndef SC_MOUSE_CAPTURE_H +#define SC_MOUSE_CAPTURE_H + +#include "common.h" + +#include + +#include + +struct sc_mouse_capture { + SDL_Window *window; + + // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or + // RGUI) must be pressed. This variable tracks the pressed capture key. + SDL_Keycode mouse_capture_key_pressed; +}; + +void +sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window); + +void +sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture); + +bool +sc_mouse_capture_is_active(struct sc_mouse_capture *mc); + +void +sc_mouse_capture_toggle(struct sc_mouse_capture *mc); + +// Return true if it consumed the event +bool +sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, + const SDL_Event *event); + +#endif diff --git a/app/src/screen.c b/app/src/screen.c index ce730f19..146f10a5 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -162,47 +162,6 @@ sc_screen_is_relative_mode(struct sc_screen *screen) { return screen->im.mp && screen->im.mp->relative_mode; } -static void -sc_screen_set_mouse_capture(struct sc_screen *screen, bool capture) { -#ifdef __APPLE__ - // Workaround for SDL bug on macOS: - // - if (capture) { - int mouse_x, mouse_y; - SDL_GetGlobalMouseState(&mouse_x, &mouse_y); - - int x, y, w, h; - SDL_GetWindowPosition(screen->window, &x, &y); - SDL_GetWindowSize(screen->window, &w, &h); - - bool outside_window = mouse_x < x || mouse_x >= x + w - || mouse_y < y || mouse_y >= y + h; - if (outside_window) { - SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); - } - } -#else - (void) screen; -#endif - if (SDL_SetRelativeMouseMode(capture)) { - LOGE("Could not set relative mouse mode to %s: %s", - capture ? "true" : "false", SDL_GetError()); - } -} - -static inline bool -sc_screen_get_mouse_capture(struct sc_screen *screen) { - (void) screen; - return SDL_GetRelativeMouseMode(); -} - -static inline void -sc_screen_toggle_mouse_capture(struct sc_screen *screen) { - (void) screen; - bool new_value = !sc_screen_get_mouse_capture(screen); - sc_screen_set_mouse_capture(screen, new_value); -} - static void sc_screen_update_content_rect(struct sc_screen *screen) { assert(screen->video); @@ -371,7 +330,6 @@ sc_screen_init(struct sc_screen *screen, screen->fullscreen = false; screen->maximized = false; screen->minimized = false; - screen->mouse_capture_key_pressed = 0; screen->paused = false; screen->resume_frame = NULL; screen->orientation = SC_ORIENTATION_0; @@ -486,6 +444,9 @@ sc_screen_init(struct sc_screen *screen, sc_input_manager_init(&screen->im, &im_params); + // Initialize even if not used for simplicity + sc_mouse_capture_init(&screen->mc, screen->window); + #ifdef CONTINUOUS_RESIZING_WORKAROUND if (screen->video) { SDL_AddEventWatch(event_watcher, screen); @@ -506,7 +467,7 @@ sc_screen_init(struct sc_screen *screen, if (!screen->video && sc_screen_is_relative_mode(screen)) { // Capture mouse immediately if video mirroring is disabled - sc_screen_set_mouse_capture(screen, true); + sc_mouse_capture_set_active(&screen->mc, true); } return true; @@ -713,7 +674,7 @@ sc_screen_apply_frame(struct sc_screen *screen) { if (sc_screen_is_relative_mode(screen)) { // Capture mouse on start - sc_screen_set_mouse_capture(screen, true); + sc_mouse_capture_set_active(&screen->mc, true); } } @@ -837,15 +798,8 @@ sc_screen_resize_to_pixel_perfect(struct sc_screen *screen) { content_size.height); } -static inline bool -sc_screen_is_mouse_capture_key(SDL_Keycode key) { - return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; -} - bool sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { - bool relative_mode = sc_screen_is_relative_mode(screen); - switch (event->type) { case SC_EVENT_SCREEN_INIT_SIZE: { // The initial size is passed via screen->frame_size @@ -903,69 +857,14 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { apply_pending_resize(screen); sc_screen_render(screen, true); break; - case SDL_WINDOWEVENT_FOCUS_LOST: - if (relative_mode) { - sc_screen_set_mouse_capture(screen, false); - } - break; } return true; - case SDL_KEYDOWN: - if (relative_mode) { - SDL_Keycode key = event->key.keysym.sym; - if (sc_screen_is_mouse_capture_key(key)) { - if (!screen->mouse_capture_key_pressed) { - screen->mouse_capture_key_pressed = key; - } else { - // Another mouse capture key has been pressed, cancel - // mouse (un)capture - screen->mouse_capture_key_pressed = 0; - } - // Mouse capture keys are never forwarded to the device - return true; - } - } - break; - case SDL_KEYUP: - if (relative_mode) { - SDL_Keycode key = event->key.keysym.sym; - SDL_Keycode cap = screen->mouse_capture_key_pressed; - screen->mouse_capture_key_pressed = 0; - if (sc_screen_is_mouse_capture_key(key)) { - if (key == cap) { - // A mouse capture key has been pressed then released: - // toggle the capture mouse mode - sc_screen_toggle_mouse_capture(screen); - } - // Mouse capture keys are never forwarded to the device - return true; - } - } - break; - case SDL_MOUSEWHEEL: - case SDL_MOUSEMOTION: - case SDL_MOUSEBUTTONDOWN: - if (relative_mode && !sc_screen_get_mouse_capture(screen)) { - // Do not forward to input manager, the mouse will be captured - // on SDL_MOUSEBUTTONUP - return true; - } - break; - case SDL_FINGERMOTION: - case SDL_FINGERDOWN: - case SDL_FINGERUP: - if (relative_mode) { - // Touch events are not compatible with relative mode - // (coordinates are not relative) - return true; - } - break; - case SDL_MOUSEBUTTONUP: - if (relative_mode && !sc_screen_get_mouse_capture(screen)) { - sc_screen_set_mouse_capture(screen, true); - return true; - } - break; + } + + if (sc_screen_is_relative_mode(screen) + && sc_mouse_capture_handle_event(&screen->mc, event)) { + // The mouse capture handler consumed the event + return true; } sc_input_manager_handle_event(&screen->im, event); diff --git a/app/src/screen.h b/app/src/screen.h index 6d5964bd..c716c399 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -13,6 +13,7 @@ #include "fps_counter.h" #include "frame_buffer.h" #include "input_manager.h" +#include "mouse_capture.h" #include "opengl.h" #include "options.h" #include "trait/key_processor.h" @@ -30,6 +31,7 @@ struct sc_screen { struct sc_display display; struct sc_input_manager im; + struct sc_mouse_capture mc; // only used in mouse relative mode struct sc_frame_buffer fb; struct sc_fps_counter fps_counter; @@ -61,10 +63,6 @@ struct sc_screen { bool maximized; bool minimized; - // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or - // RGUI) must be pressed. This variable tracks the pressed capture key. - SDL_Keycode mouse_capture_key_pressed; - AVFrame *frame; bool paused; diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index b13f8d04..aabb8a7f 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -4,47 +4,6 @@ #include "options.h" #include "util/log.h" -static void -sc_screen_otg_set_mouse_capture(struct sc_screen_otg *screen, bool capture) { -#ifdef __APPLE__ - // Workaround for SDL bug on macOS: - // - if (capture) { - int mouse_x, mouse_y; - SDL_GetGlobalMouseState(&mouse_x, &mouse_y); - - int x, y, w, h; - SDL_GetWindowPosition(screen->window, &x, &y); - SDL_GetWindowSize(screen->window, &w, &h); - - bool outside_window = mouse_x < x || mouse_x >= x + w - || mouse_y < y || mouse_y >= y + h; - if (outside_window) { - SDL_WarpMouseInWindow(screen->window, w / 2, h / 2); - } - } -#else - (void) screen; -#endif - if (SDL_SetRelativeMouseMode(capture)) { - LOGE("Could not set relative mouse mode to %s: %s", - capture ? "true" : "false", SDL_GetError()); - } -} - -static inline bool -sc_screen_otg_get_mouse_capture(struct sc_screen_otg *screen) { - (void) screen; - return SDL_GetRelativeMouseMode(); -} - -static inline void -sc_screen_otg_toggle_mouse_capture(struct sc_screen_otg *screen) { - (void) screen; - bool new_value = !sc_screen_otg_get_mouse_capture(screen); - sc_screen_otg_set_mouse_capture(screen, new_value); -} - static void sc_screen_otg_render(struct sc_screen_otg *screen) { SDL_RenderClear(screen->renderer); @@ -61,8 +20,6 @@ sc_screen_otg_init(struct sc_screen_otg *screen, screen->mouse = params->mouse; screen->gamepad = params->gamepad; - screen->mouse_capture_key_pressed = 0; - const char *title = params->window_title; assert(title); @@ -113,9 +70,11 @@ sc_screen_otg_init(struct sc_screen_otg *screen, LOGW("Could not load icon"); } + sc_mouse_capture_init(&screen->mc, screen->window); + if (screen->mouse) { // Capture mouse on start - sc_screen_otg_set_mouse_capture(screen, true); + sc_mouse_capture_set_active(&screen->mc, true); } return true; @@ -137,11 +96,6 @@ sc_screen_otg_destroy(struct sc_screen_otg *screen) { SDL_DestroyWindow(screen->window); } -static inline bool -sc_screen_otg_is_mouse_capture_key(SDL_Keycode key) { - return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; -} - static void sc_screen_otg_process_key(struct sc_screen_otg *screen, const SDL_KeyboardEvent *event) { @@ -298,80 +252,46 @@ sc_screen_otg_process_gamepad_button(struct sc_screen_otg *screen, void sc_screen_otg_handle_event(struct sc_screen_otg *screen, SDL_Event *event) { + if (sc_mouse_capture_handle_event(&screen->mc, event)) { + // The mouse capture handler consumed the event + return; + } + switch (event->type) { case SDL_WINDOWEVENT: switch (event->window.event) { case SDL_WINDOWEVENT_EXPOSED: sc_screen_otg_render(screen); break; - case SDL_WINDOWEVENT_FOCUS_LOST: - if (screen->mouse) { - sc_screen_otg_set_mouse_capture(screen, false); - } - break; } return; case SDL_KEYDOWN: - if (screen->mouse) { - SDL_Keycode key = event->key.keysym.sym; - if (sc_screen_otg_is_mouse_capture_key(key)) { - if (!screen->mouse_capture_key_pressed) { - screen->mouse_capture_key_pressed = key; - } else { - // Another mouse capture key has been pressed, cancel - // mouse (un)capture - screen->mouse_capture_key_pressed = 0; - } - // Mouse capture keys are never forwarded to the device - return; - } - } - if (screen->keyboard) { sc_screen_otg_process_key(screen, &event->key); } break; case SDL_KEYUP: - if (screen->mouse) { - SDL_Keycode key = event->key.keysym.sym; - SDL_Keycode cap = screen->mouse_capture_key_pressed; - screen->mouse_capture_key_pressed = 0; - if (sc_screen_otg_is_mouse_capture_key(key)) { - if (key == cap) { - // A mouse capture key has been pressed then released: - // toggle the capture mouse mode - sc_screen_otg_toggle_mouse_capture(screen); - } - // Mouse capture keys are never forwarded to the device - return; - } - } - if (screen->keyboard) { sc_screen_otg_process_key(screen, &event->key); } break; case SDL_MOUSEMOTION: - if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + if (screen->mouse) { sc_screen_otg_process_mouse_motion(screen, &event->motion); } break; case SDL_MOUSEBUTTONDOWN: - if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + if (screen->mouse) { sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEBUTTONUP: if (screen->mouse) { - if (sc_screen_otg_get_mouse_capture(screen)) { - sc_screen_otg_process_mouse_button(screen, &event->button); - } else { - sc_screen_otg_set_mouse_capture(screen, true); - } + sc_screen_otg_process_mouse_button(screen, &event->button); } break; case SDL_MOUSEWHEEL: - if (screen->mouse && sc_screen_otg_get_mouse_capture(screen)) { + if (screen->mouse) { sc_screen_otg_process_mouse_wheel(screen, &event->wheel); } break; diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 2ea76eda..850a6ae5 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -8,6 +8,7 @@ #include "keyboard_aoa.h" #include "mouse_aoa.h" +#include "mouse_capture.h" #include "gamepad_aoa.h" struct sc_screen_otg { @@ -19,8 +20,7 @@ struct sc_screen_otg { SDL_Renderer *renderer; SDL_Texture *texture; - // See equivalent mechanism in screen.h - SDL_Keycode mouse_capture_key_pressed; + struct sc_mouse_capture mc; }; struct sc_screen_otg_params { From a36de26969791d054cfd3729e1c29923fec61b32 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Sep 2024 18:24:21 +0200 Subject: [PATCH 018/278] Move shortcut mod functions to a separate header This will allow to reuse it for mouse capture keys, which are handled by a component separate from the input manager. PR #5322 --- app/src/input_manager.c | 56 +++++--------------------------------- app/src/shortcut_mod.h | 60 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 49 deletions(-) create mode 100644 app/src/shortcut_mod.h diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 0f121da9..969196e3 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -5,53 +5,9 @@ #include "input_events.h" #include "screen.h" +#include "shortcut_mod.h" #include "util/log.h" -#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) - -static inline uint16_t -to_sdl_mod(uint8_t shortcut_mod) { - uint16_t sdl_mod = 0; - if (shortcut_mod & SC_SHORTCUT_MOD_LCTRL) { - sdl_mod |= KMOD_LCTRL; - } - if (shortcut_mod & SC_SHORTCUT_MOD_RCTRL) { - sdl_mod |= KMOD_RCTRL; - } - if (shortcut_mod & SC_SHORTCUT_MOD_LALT) { - sdl_mod |= KMOD_LALT; - } - if (shortcut_mod & SC_SHORTCUT_MOD_RALT) { - sdl_mod |= KMOD_RALT; - } - if (shortcut_mod & SC_SHORTCUT_MOD_LSUPER) { - sdl_mod |= KMOD_LGUI; - } - if (shortcut_mod & SC_SHORTCUT_MOD_RSUPER) { - sdl_mod |= KMOD_RGUI; - } - return sdl_mod; -} - -static inline bool -is_shortcut_mod(struct sc_input_manager *im, uint16_t sdl_mod) { - // im->sdl_shortcut_mods is within the mask - assert(!(im->sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK)); - - // at least one shortcut mod pressed? - return sdl_mod & im->sdl_shortcut_mods; -} - -static bool -is_shortcut_key(struct sc_input_manager *im, SDL_Keycode keycode) { - return (im->sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) - || (im->sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) - || (im->sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) - || (im->sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) - || (im->sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) - || (im->sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); -} - void sc_input_manager_init(struct sc_input_manager *im, const struct sc_input_manager_params *params) { @@ -73,7 +29,7 @@ sc_input_manager_init(struct sc_input_manager *im, im->legacy_paste = params->legacy_paste; im->clipboard_autosync = params->clipboard_autosync; - im->sdl_shortcut_mods = to_sdl_mod(params->shortcut_mods); + im->sdl_shortcut_mods = sc_shortcut_mods_to_sdl(params->shortcut_mods); im->vfinger_down = false; im->vfinger_invert_x = false; @@ -346,7 +302,8 @@ sc_input_manager_process_text_input(struct sc_input_manager *im, return; } - if (is_shortcut_mod(im, SDL_GetModState())) { + if (sc_shortcut_mods_is_shortcut_mod(im->sdl_shortcut_mods, + SDL_GetModState())) { // A shortcut must never generate text events return; } @@ -413,8 +370,9 @@ sc_input_manager_process_key(struct sc_input_manager *im, // press/release is a modifier key. // The second condition is necessary to ignore the release of the modifier // key (because in this case mod is 0). - bool is_shortcut = is_shortcut_mod(im, mod) - || is_shortcut_key(im, sdl_keycode); + uint16_t mods = im->sdl_shortcut_mods; + bool is_shortcut = sc_shortcut_mods_is_shortcut_mod(mods, mod) + || sc_shortcut_mods_is_shortcut_key(mods, sdl_keycode); if (down && !repeat) { if (sdl_keycode == im->last_keycode && mod == im->last_mod) { diff --git a/app/src/shortcut_mod.h b/app/src/shortcut_mod.h new file mode 100644 index 00000000..b685e987 --- /dev/null +++ b/app/src/shortcut_mod.h @@ -0,0 +1,60 @@ +#ifndef SC_SHORTCUT_MOD_H +#define SC_SHORTCUT_MOD_H + +#include "common.h" + +#include +#include +#include + +#include "options.h" + +#define SC_SDL_SHORTCUT_MODS_MASK (KMOD_CTRL | KMOD_ALT | KMOD_GUI) + +// input: OR of enum sc_shortcut_mod +// output: OR of SDL_Keymod +static inline uint16_t +sc_shortcut_mods_to_sdl(uint8_t shortcut_mods) { + uint16_t sdl_mod = 0; + if (shortcut_mods & SC_SHORTCUT_MOD_LCTRL) { + sdl_mod |= KMOD_LCTRL; + } + if (shortcut_mods & SC_SHORTCUT_MOD_RCTRL) { + sdl_mod |= KMOD_RCTRL; + } + if (shortcut_mods & SC_SHORTCUT_MOD_LALT) { + sdl_mod |= KMOD_LALT; + } + if (shortcut_mods & SC_SHORTCUT_MOD_RALT) { + sdl_mod |= KMOD_RALT; + } + if (shortcut_mods & SC_SHORTCUT_MOD_LSUPER) { + sdl_mod |= KMOD_LGUI; + } + if (shortcut_mods & SC_SHORTCUT_MOD_RSUPER) { + sdl_mod |= KMOD_RGUI; + } + return sdl_mod; +} + +static inline bool +sc_shortcut_mods_is_shortcut_mod(uint16_t sdl_shortcut_mods, uint16_t sdl_mod) { + // sdl_shortcut_mods must be within the mask + assert(!(sdl_shortcut_mods & ~SC_SDL_SHORTCUT_MODS_MASK)); + + // at least one shortcut mod pressed? + return sdl_mod & sdl_shortcut_mods; +} + +static inline bool +sc_shortcut_mods_is_shortcut_key(uint16_t sdl_shortcut_mods, + SDL_Keycode keycode) { + return (sdl_shortcut_mods & KMOD_LCTRL && keycode == SDLK_LCTRL) + || (sdl_shortcut_mods & KMOD_RCTRL && keycode == SDLK_RCTRL) + || (sdl_shortcut_mods & KMOD_LALT && keycode == SDLK_LALT) + || (sdl_shortcut_mods & KMOD_RALT && keycode == SDLK_RALT) + || (sdl_shortcut_mods & KMOD_LGUI && keycode == SDLK_LGUI) + || (sdl_shortcut_mods & KMOD_RGUI && keycode == SDLK_RGUI); +} + +#endif From ff9fb5994dbe555be8835a5f8d06b03e9f3b1b27 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 27 Sep 2024 18:32:39 +0200 Subject: [PATCH 019/278] Use shortcut mods as mouse capture keys Instead of using separate hardcoded keys for mouse capture/uncapture, use the shortcut mods. By changing the shortcut mods (for example --shortcut-mod=rctrl), it allows to forward Alt and Super to the device. Fixes #5318 PR #5322 --- app/src/mouse_capture.c | 13 ++++++++----- app/src/mouse_capture.h | 5 ++++- app/src/screen.c | 2 +- app/src/usb/scrcpy_otg.c | 1 + app/src/usb/screen_otg.c | 2 +- app/src/usb/screen_otg.h | 1 + 6 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/src/mouse_capture.c b/app/src/mouse_capture.c index 1420bad6..ee96ae60 100644 --- a/app/src/mouse_capture.c +++ b/app/src/mouse_capture.c @@ -1,16 +1,19 @@ #include "mouse_capture.h" +#include "shortcut_mod.h" #include "util/log.h" void -sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window) { +sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window, + uint8_t shortcut_mods) { mc->window = window; + mc->sdl_mouse_capture_keys = sc_shortcut_mods_to_sdl(shortcut_mods); mc->mouse_capture_key_pressed = SDLK_UNKNOWN; } static inline bool -sc_mouse_capture_is_capture_key(SDL_Keycode key) { - return key == SDLK_LALT || key == SDLK_LGUI || key == SDLK_RGUI; +sc_mouse_capture_is_capture_key(struct sc_mouse_capture *mc, SDL_Keycode key) { + return sc_shortcut_mods_is_shortcut_key(mc->sdl_mouse_capture_keys, key); } bool @@ -25,7 +28,7 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, break; case SDL_KEYDOWN: { SDL_Keycode key = event->key.keysym.sym; - if (sc_mouse_capture_is_capture_key(key)) { + if (sc_mouse_capture_is_capture_key(mc, key)) { if (!mc->mouse_capture_key_pressed) { mc->mouse_capture_key_pressed = key; } else { @@ -42,7 +45,7 @@ sc_mouse_capture_handle_event(struct sc_mouse_capture *mc, SDL_Keycode key = event->key.keysym.sym; SDL_Keycode cap = mc->mouse_capture_key_pressed; mc->mouse_capture_key_pressed = 0; - if (sc_mouse_capture_is_capture_key(key)) { + if (sc_mouse_capture_is_capture_key(mc, key)) { if (key == cap) { // A mouse capture key has been pressed then released: // toggle the capture mouse mode diff --git a/app/src/mouse_capture.h b/app/src/mouse_capture.h index 53018c19..f352cc13 100644 --- a/app/src/mouse_capture.h +++ b/app/src/mouse_capture.h @@ -9,14 +9,17 @@ struct sc_mouse_capture { SDL_Window *window; + uint16_t sdl_mouse_capture_keys; // To enable/disable mouse capture, a mouse capture key (LALT, LGUI or // RGUI) must be pressed. This variable tracks the pressed capture key. SDL_Keycode mouse_capture_key_pressed; + }; void -sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window); +sc_mouse_capture_init(struct sc_mouse_capture *mc, SDL_Window *window, + uint8_t shortcut_mods); void sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture); diff --git a/app/src/screen.c b/app/src/screen.c index 146f10a5..1d694f12 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -445,7 +445,7 @@ sc_screen_init(struct sc_screen *screen, sc_input_manager_init(&screen->im, &im_params); // Initialize even if not used for simplicity - sc_mouse_capture_init(&screen->mc, screen->window); + sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods); #ifdef CONTINUOUS_RESIZING_WORKAROUND if (screen->video) { diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 9595face..1a7e9544 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -185,6 +185,7 @@ scrcpy_otg(struct scrcpy_options *options) { .window_width = options->window_width, .window_height = options->window_height, .window_borderless = options->window_borderless, + .shortcut_mods = options->shortcut_mods, }; ok = sc_screen_otg_init(&s->screen_otg, ¶ms); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index aabb8a7f..18377074 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -70,7 +70,7 @@ sc_screen_otg_init(struct sc_screen_otg *screen, LOGW("Could not load icon"); } - sc_mouse_capture_init(&screen->mc, screen->window); + sc_mouse_capture_init(&screen->mc, screen->window, params->shortcut_mods); if (screen->mouse) { // Capture mouse on start diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 850a6ae5..427723ad 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -35,6 +35,7 @@ struct sc_screen_otg_params { uint16_t window_width; uint16_t window_height; bool window_borderless; + uint8_t shortcut_mods; // OR of enum sc_shortcut_mod values }; bool From 064670ab4c7d740ba6a02a1242d24edd267d2b76 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 29 Sep 2024 17:20:35 +0200 Subject: [PATCH 020/278] Add missing include common.h --- app/src/usb/aoa_hid.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 00961c28..9cc6355e 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -1,6 +1,8 @@ #ifndef SC_AOA_HID_H #define SC_AOA_HID_H +#include "common.h" + #include #include From 0d8014be5260b64daf3d7b3516d37a0dfafca7e1 Mon Sep 17 00:00:00 2001 From: Yan Date: Mon, 7 Oct 2024 16:43:01 +0200 Subject: [PATCH 021/278] Fix build error on macOS Fix window access typo for macOS. PR #5348 Signed-off-by: Romain Vimont --- app/src/mouse_capture.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/mouse_capture.c b/app/src/mouse_capture.c index ee96ae60..25345faa 100644 --- a/app/src/mouse_capture.c +++ b/app/src/mouse_capture.c @@ -92,8 +92,8 @@ sc_mouse_capture_set_active(struct sc_mouse_capture *mc, bool capture) { SDL_GetGlobalMouseState(&mouse_x, &mouse_y); int x, y, w, h; - SDL_GetWindowPosition(window, &x, &y); - SDL_GetWindowSize(window, &w, &h); + SDL_GetWindowPosition(mc->window, &x, &y); + SDL_GetWindowSize(mc->window, &w, &h); bool outside_window = mouse_x < x || mouse_x >= x + w || mouse_y < y || mouse_y >= y + h; From 5b10650f22b218b2fb42415aefe4bd6fec6c7e9e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 8 Oct 2024 18:12:55 +0200 Subject: [PATCH 022/278] Fix time-limit early interruption If a value for --time-limit was set, then the thread was not interrupted on stop (the condvar was not signaled). --- app/src/util/timeout.c | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c index a1665373..159a4681 100644 --- a/app/src/util/timeout.c +++ b/app/src/util/timeout.c @@ -62,6 +62,7 @@ void sc_timeout_stop(struct sc_timeout *timeout) { sc_mutex_lock(&timeout->mutex); timeout->stopped = true; + sc_cond_signal(&timeout->cond); sc_mutex_unlock(&timeout->mutex); } From afbaf59abba79a79ab6f4c659ff3d832e02a8e7f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 8 Oct 2024 18:18:05 +0200 Subject: [PATCH 023/278] Cast to sc_tick type in conversion macros With the old macros definitions, the type of the result depended on the type of `sec`. In particular, if sec is a 32-bit type, sec * 1000000 was likely to overflow (even if the result was assigned to a sc_tick by the caller of the macro). This was the case on Windows, where the long type is a 32-bit signed integer: the --time-limit argument, expressed in seconds, was first parsed to a long value, then multiplied by 1000000 by the SC_TICK_FROM_SEC() macro, causing an overflow when the value was greater than 2147 (2^31 / 1000000). Fixes #5355 --- app/src/util/tick.h | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/app/src/util/tick.h b/app/src/util/tick.h index 2d941f23..b037734b 100644 --- a/app/src/util/tick.h +++ b/app/src/util/tick.h @@ -10,14 +10,14 @@ typedef int64_t sc_tick; #define SC_TICK_FREQ 1000000 // microsecond // To be adapted if SC_TICK_FREQ changes -#define SC_TICK_TO_NS(tick) ((tick) * 1000) -#define SC_TICK_TO_US(tick) (tick) -#define SC_TICK_TO_MS(tick) ((tick) / 1000) -#define SC_TICK_TO_SEC(tick) ((tick) / 1000000) -#define SC_TICK_FROM_NS(ns) ((ns) / 1000) -#define SC_TICK_FROM_US(us) (us) -#define SC_TICK_FROM_MS(ms) ((ms) * 1000) -#define SC_TICK_FROM_SEC(sec) ((sec) * 1000000) +#define SC_TICK_TO_NS(tick) ((sc_tick) (tick) * 1000) +#define SC_TICK_TO_US(tick) ((sc_tick) tick) +#define SC_TICK_TO_MS(tick) ((sc_tick) (tick) / 1000) +#define SC_TICK_TO_SEC(tick) ((sc_tick) (tick) / 1000000) +#define SC_TICK_FROM_NS(ns) ((sc_tick) (ns) / 1000) +#define SC_TICK_FROM_US(us) ((sc_tick) us) +#define SC_TICK_FROM_MS(ms) ((sc_tick) (ms) * 1000) +#define SC_TICK_FROM_SEC(sec) ((sc_tick) (sec) * 1000000) sc_tick sc_tick_now(void); From 09741bc8051fc0d131c00690088390b8b36dd672 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Oct 2024 22:42:39 +0200 Subject: [PATCH 024/278] Do not duplicate server string params The server params were passed from the main thread to the server thread, so a deep copy was performed in case the caller instance was destroyed. But in practice, it only contains memory that lives until the end of the program (command line arguments), so simply reference it. Several copies of string fields were missing anyway. --- app/src/server.c | 64 +++--------------------------------------------- 1 file changed, 4 insertions(+), 60 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 90a0ac5d..b7f3b56d 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -66,56 +66,6 @@ get_server_path(void) { return server_path; } -static void -sc_server_params_destroy(struct sc_server_params *params) { - // The server stores a copy of the params provided by the user - free((char *) params->req_serial); - free((char *) params->crop); - free((char *) params->video_codec_options); - free((char *) params->audio_codec_options); - free((char *) params->video_encoder); - free((char *) params->audio_encoder); - free((char *) params->tcpip_dst); - free((char *) params->camera_id); - free((char *) params->camera_ar); -} - -static bool -sc_server_params_copy(struct sc_server_params *dst, - const struct sc_server_params *src) { - *dst = *src; - - // The params reference user-allocated memory, so we must copy them to - // handle them from another thread - -#define COPY(FIELD) do { \ - dst->FIELD = NULL; \ - if (src->FIELD) { \ - dst->FIELD = strdup(src->FIELD); \ - if (!dst->FIELD) { \ - goto error; \ - } \ - } \ -} while(0) - - COPY(req_serial); - COPY(crop); - COPY(video_codec_options); - COPY(audio_codec_options); - COPY(video_encoder); - COPY(audio_encoder); - COPY(tcpip_dst); - COPY(camera_id); - COPY(camera_ar); -#undef COPY - - return true; - -error: - sc_server_params_destroy(dst); - return false; -} - static bool push_server(struct sc_intr *intr, const char *serial) { char *server_path = get_server_path(); @@ -499,22 +449,18 @@ connect_to_server(struct sc_server *server, unsigned attempts, sc_tick delay, bool sc_server_init(struct sc_server *server, const struct sc_server_params *params, const struct sc_server_callbacks *cbs, void *cbs_userdata) { - bool ok = sc_server_params_copy(&server->params, params); - if (!ok) { - LOG_OOM(); - return false; - } + // The allocated data in params (const char *) must remain valid until the + // end of the program + server->params = *params; - ok = sc_mutex_init(&server->mutex); + bool ok = sc_mutex_init(&server->mutex); if (!ok) { - sc_server_params_destroy(&server->params); return false; } ok = sc_cond_init(&server->cond_stopped); if (!ok) { sc_mutex_destroy(&server->mutex); - sc_server_params_destroy(&server->params); return false; } @@ -522,7 +468,6 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, if (!ok) { sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); - sc_server_params_destroy(&server->params); return false; } @@ -1161,7 +1106,6 @@ sc_server_destroy(struct sc_server *server) { free(server->serial); free(server->device_socket_name); - sc_server_params_destroy(&server->params); sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); From c15df01171793dca7074a012f4703a185d99169d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 11 Oct 2024 22:51:15 +0200 Subject: [PATCH 025/278] Reject non-positive camera sizes early Throw an exception on parsing if the camera size dimensions are not both positive. --- server/src/main/java/com/genymobile/scrcpy/Options.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 51daeced..9eab1d90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -475,6 +475,9 @@ public class Options { } int width = Integer.parseInt(tokens[0]); int height = Integer.parseInt(tokens[1]); + if (width <= 0 || height <= 0) { + throw new IllegalArgumentException("Invalid non-positive size dimension: \"" + size + "\""); + } return new Size(width, height); } From e33be3d288fb6ec764a07c56eb4299b44c4793ac Mon Sep 17 00:00:00 2001 From: dillonfrederica Date: Sat, 12 Oct 2024 18:12:46 +0800 Subject: [PATCH 026/278] Fix SDL_events.h include All SDL includes must be prefixed by "SDL2/". Fixed #5372 Signed-off-by: Romain Vimont --- app/src/events.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/events.h b/app/src/events.h index 59c55de4..2fe4d3a7 100644 --- a/app/src/events.h +++ b/app/src/events.h @@ -5,7 +5,7 @@ #include #include -#include +#include enum { SC_EVENT_NEW_FRAME = SDL_USEREVENT, From 3acffaae57238ee47e05f97f8e762a04550fdad8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 13:01:52 +0200 Subject: [PATCH 027/278] Use explicit constants for Android versions Who remembers code names? This avoids to check the mapping every time. --- .../genymobile/scrcpy/AndroidVersions.java | 32 +++++++++++++++++++ .../com/genymobile/scrcpy/FakeContext.java | 3 +- .../java/com/genymobile/scrcpy/Server.java | 2 +- .../com/genymobile/scrcpy/Workarounds.java | 8 ++--- .../scrcpy/audio/AudioDirectCapture.java | 11 ++++--- .../genymobile/scrcpy/audio/AudioEncoder.java | 9 +++--- .../scrcpy/audio/AudioPlaybackCapture.java | 5 +-- .../scrcpy/audio/AudioRawRecorder.java | 3 +- .../scrcpy/audio/AudioRecordReader.java | 4 +-- .../genymobile/scrcpy/control/Controller.java | 7 ++-- .../scrcpy/control/UhidManager.java | 7 ++-- .../com/genymobile/scrcpy/device/Device.java | 15 +++++---- .../java/com/genymobile/scrcpy/util/IO.java | 3 +- .../com/genymobile/scrcpy/util/Settings.java | 7 ++-- .../scrcpy/video/CameraCapture.java | 10 +++--- .../scrcpy/video/ScreenCapture.java | 5 +-- .../scrcpy/video/SurfaceEncoder.java | 3 +- .../scrcpy/wrappers/ActivityManager.java | 4 +-- .../scrcpy/wrappers/ClipboardManager.java | 13 ++++---- .../scrcpy/wrappers/ContentProvider.java | 5 +-- .../scrcpy/wrappers/DisplayControl.java | 4 +-- .../scrcpy/wrappers/PowerManager.java | 3 +- .../scrcpy/wrappers/SurfaceControl.java | 7 ++-- 23 files changed, 108 insertions(+), 62 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java diff --git a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java new file mode 100644 index 00000000..8acad7ee --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java @@ -0,0 +1,32 @@ +package com.genymobile.scrcpy; + +import android.os.Build; + +/** + * Android version code constants, done right. + *

+ * API levels + */ +public final class AndroidVersions { + + private AndroidVersions() { + // not instantiable + } + + public static final int API_20_ANDROID_4_4 = Build.VERSION_CODES.KITKAT_WATCH; + public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP; + public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1; + public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M; + public static final int API_24_ANDROID_7_0 = Build.VERSION_CODES.N; + public static final int API_25_ANDROID_7_1 = Build.VERSION_CODES.N_MR1; + public static final int API_26_ANDROID_8_0 = Build.VERSION_CODES.O; + public static final int API_27_ANDROID_8_1 = Build.VERSION_CODES.O_MR1; + public static final int API_28_ANDROID_9 = Build.VERSION_CODES.P; + public static final int API_29_ANDROID_10 = Build.VERSION_CODES.Q; + public static final int API_30_ANDROID_11 = Build.VERSION_CODES.R; + public static final int API_31_ANDROID_12 = Build.VERSION_CODES.S; + public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2; + public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU; + public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + +} diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 2ea7bf4a..0b086cc5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -4,7 +4,6 @@ import android.annotation.TargetApi; import android.content.AttributionSource; import android.content.Context; import android.content.ContextWrapper; -import android.os.Build; import android.os.Process; public final class FakeContext extends ContextWrapper { @@ -32,7 +31,7 @@ public final class FakeContext extends ContextWrapper { return PACKAGE_NAME; } - @TargetApi(Build.VERSION_CODES.S) + @TargetApi(AndroidVersions.API_31_ANDROID_12) @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 7817fdf5..555cf97a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -121,7 +121,7 @@ public final class Server { } private static void scrcpy(Options options) throws IOException, ConfigurationException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S && options.getVideoSource() == VideoSource.CAMERA) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) { Ln.e("Camera mirroring is not supported before Android 12"); throw new ConfigurationException("Camera mirroring is not supported"); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 7de98b72..eec00a04 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -52,7 +52,7 @@ public final class Workarounds { } public static void apply() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), // which requires a non-null ConfigurationController. // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. @@ -155,7 +155,7 @@ public final class Workarounds { } } - @TargetApi(Build.VERSION_CODES.R) + @TargetApi(AndroidVersions.API_30_ANDROID_11) @SuppressLint("WrongConstant,MissingPermission") public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) throws AudioCaptureException { @@ -226,7 +226,7 @@ public final class Workarounds { int[] session = new int[]{AudioManager.AUDIO_SESSION_ID_GENERATE}; int initResult; - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12) { // private native final int native_setup(Object audiorecord_this, // Object /*AudioAttributes*/ attributes, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, @@ -252,7 +252,7 @@ public final class Workarounds { Method getParcelMethod = attributionSourceState.getClass().getDeclaredMethod("getParcel"); Parcel attributionSourceParcel = (Parcel) getParcelMethod.invoke(attributionSourceState); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14) { // private native int native_setup(Object audiorecordThis, // Object /*AudioAttributes*/ attributes, // int[] sampleRate, int channelMask, int channelIndexMask, int audioFormat, diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 8d4a4c2d..5c859738 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.Workarounds; import com.genymobile.scrcpy.util.Ln; @@ -45,11 +46,11 @@ public class AudioDirectCapture implements AudioCapture { } } - @TargetApi(Build.VERSION_CODES.M) + @TargetApi(AndroidVersions.API_23_ANDROID_6_0) @SuppressLint({"WrongConstant", "MissingPermission"}) private static AudioRecord createAudioRecord(int audioSource) { AudioRecord.Builder builder = new AudioRecord.Builder(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { // On older APIs, Workarounds.fillAppInfo() must be called beforehand builder.setContext(FakeContext.get()); } @@ -117,7 +118,7 @@ public class AudioDirectCapture implements AudioCapture { @Override public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { Ln.w("Audio disabled: it is not supported before Android 11"); throw new AudioCaptureException(); } @@ -125,7 +126,7 @@ public class AudioDirectCapture implements AudioCapture { @Override public void start() throws AudioCaptureException { - if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11) { startWorkaroundAndroid11(); try { tryStartRecording(5, 100); @@ -146,7 +147,7 @@ public class AudioDirectCapture implements AudioCapture { } @Override - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { return reader.read(outDirectBuffer, outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index f462431a..fcc0c52f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Streamer; @@ -93,7 +94,7 @@ public final class AudioEncoder implements AsyncProcessor { return format; } - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) private void inputThread(MediaCodec mediaCodec, AudioCapture capture) throws IOException, InterruptedException { final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); @@ -175,9 +176,9 @@ public final class AudioEncoder implements AsyncProcessor { } } - @TargetApi(Build.VERSION_CODES.M) + @TargetApi(AndroidVersions.API_23_ANDROID_6_0) private void encode() throws IOException, ConfigurationException, AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); return; @@ -314,7 +315,7 @@ public final class AudioEncoder implements AsyncProcessor { } private final class EncoderCallback extends MediaCodec.Callback { - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) @Override public void onInputBufferAvailable(MediaCodec codec, int index) { try { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java index e38493f2..009a239a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioPlaybackCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; @@ -108,7 +109,7 @@ public final class AudioPlaybackCapture implements AudioCapture { @Override public void checkCompatibility() throws AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_33_ANDROID_13) { Ln.w("Audio disabled: audio playback capture source not supported before Android 13"); throw new AudioCaptureException(); } @@ -130,7 +131,7 @@ public final class AudioPlaybackCapture implements AudioCapture { } @Override - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { return reader.read(outDirectBuffer, outBufferInfo); } diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java index 3924c205..9645bbbd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRawRecorder.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.IO; @@ -24,7 +25,7 @@ public final class AudioRawRecorder implements AsyncProcessor { } private void record() throws IOException, AudioCaptureException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11) { Ln.w("Audio disabled: it is not supported before Android 11"); streamer.writeDisableStream(false); return; diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java index 80286831..32b42257 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioRecordReader.java @@ -1,12 +1,12 @@ package com.genymobile.scrcpy.audio; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.media.AudioRecord; import android.media.AudioTimestamp; import android.media.MediaCodec; -import android.os.Build; import java.nio.ByteBuffer; @@ -26,7 +26,7 @@ public class AudioRecordReader { this.recorder = recorder; } - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) public int read(ByteBuffer outDirectBuffer, MediaCodec.BufferInfo outBufferInfo) { int r = recorder.read(outDirectBuffer, AudioConfig.MAX_READ_SIZE); if (r <= 0) { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b445427d..8fa27e81 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.control; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.device.Device; @@ -318,7 +319,7 @@ public class Controller implements AsyncProcessor { * * Otherwise, Chrome does not work properly: */ - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && source == InputDevice.SOURCE_MOUSE) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0 && source == InputDevice.SOURCE_MOUSE) { if (action == MotionEvent.ACTION_DOWN) { if (actionButton == buttons) { // First button pressed: ACTION_DOWN @@ -423,7 +424,7 @@ public class Controller implements AsyncProcessor { private void getClipboard(int copyKey) { // On Android >= 7, press the COPY or CUT key if requested - if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) { int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); @@ -448,7 +449,7 @@ public class Controller implements AsyncProcessor { } // On Android >= 7, also press the PASTE key if requested - if (paste && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && device.supportsInputEvents()) { + if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) { device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index d8cfd81f..8121adfc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.control; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; @@ -38,7 +39,7 @@ public final class UhidManager { public UhidManager(DeviceMessageSender sender) { this.sender = sender; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { HandlerThread thread = new HandlerThread("UHidManager"); thread.start(); queue = thread.getLooper().getQueue(); @@ -71,7 +72,7 @@ public final class UhidManager { } private void registerUhidListener(int id, FileDescriptor fd) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { queue.addOnFileDescriptorEventListener(fd, MessageQueue.OnFileDescriptorEventListener.EVENT_INPUT, (fd2, events) -> { try { buffer.clear(); @@ -97,7 +98,7 @@ public final class UhidManager { } private void unregisterUhidListener(FileDescriptor fd) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { queue.removeOnFileDescriptorEventListener(fd); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 5a1083fd..1f375942 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.device; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; @@ -104,7 +105,7 @@ public final class Device { } }, displayId); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { @Override public void onDisplayFoldChanged(int displayId, boolean folded) { @@ -161,8 +162,8 @@ public final class Device { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } - // main display or any display on Android >= Q - supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + // main display or any display on Android >= 10 + supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (!supportsInputEvents) { Ln.w("Input events are not supported for secondary displays before Android 10"); } @@ -215,7 +216,7 @@ public final class Device { } public static boolean supportsInputEvents(int displayId) { - return displayId == 0 || Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; } public boolean supportsInputEvents() { @@ -323,10 +324,10 @@ public final class Device { * @param mode one of the {@code POWER_MODE_*} constants */ public static boolean setScreenPowerMode(int mode) { - boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q; + boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (applyToMultiPhysicalDisplays - && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE + && Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 && Build.BRAND.equalsIgnoreCase("honor") && SurfaceControl.hasGetBuildInDisplayMethod()) { // Workaround for Honor devices with Android 14: @@ -338,7 +339,7 @@ public final class Device { if (applyToMultiPhysicalDisplays) { // On Android 14, these internal methods have been moved to DisplayControl boolean useDisplayControl = - Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); + Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14 && !SurfaceControl.hasGetPhysicalDisplayIdsMethod(); // Change the power mode for all physical displays long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index d9247a98..b953f290 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.BuildConfig; import android.os.Build; @@ -31,7 +32,7 @@ public final class IO { } public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { while (from.hasRemaining()) { write(fd, from); } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java index d9e82d62..e6465525 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/Settings.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.wrappers.ContentProvider; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -34,7 +35,7 @@ public final class Settings { } public static String getValue(String table, String key) throws SettingsException { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { return provider.getValue(table, key); @@ -47,7 +48,7 @@ public final class Settings { } public static void putValue(String table, String key, String value) throws SettingsException { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { provider.putValue(table, key, value); @@ -60,7 +61,7 @@ public final class Settings { } public static String getAndPutValue(String table, String key, String value) throws SettingsException { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { + if (Build.VERSION.SDK_INT <= AndroidVersions.API_30_ANDROID_11) { // on Android >= 12, it always fails: try (ContentProvider provider = ServiceManager.getActivityManager().createSettingsProvider()) { String oldValue = provider.getValue(table, key); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 3b8fc59b..a5fa4b06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; @@ -20,7 +21,6 @@ import android.hardware.camera2.params.OutputConfiguration; import android.hardware.camera2.params.SessionConfiguration; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; -import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.util.Range; @@ -118,7 +118,7 @@ public class CameraCapture extends SurfaceCapture { return null; } - @TargetApi(Build.VERSION_CODES.N) + @TargetApi(AndroidVersions.API_24_ANDROID_7_0) private static Size selectSize(String cameraId, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, boolean highSpeed) throws CameraAccessException { if (explicitSize != null) { @@ -242,7 +242,7 @@ public class CameraCapture extends SurfaceCapture { } @SuppressLint("MissingPermission") - @TargetApi(Build.VERSION_CODES.S) + @TargetApi(AndroidVersions.API_31_ANDROID_12) private CameraDevice openCamera(String id) throws CameraAccessException, InterruptedException { CompletableFuture future = new CompletableFuture<>(); ServiceManager.getCameraManager().openCamera(id, new CameraDevice.StateCallback() { @@ -289,7 +289,7 @@ public class CameraCapture extends SurfaceCapture { } } - @TargetApi(Build.VERSION_CODES.S) + @TargetApi(AndroidVersions.API_31_ANDROID_12) private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface) throws CameraAccessException, InterruptedException { CompletableFuture future = new CompletableFuture<>(); OutputConfiguration outputConfig = new OutputConfiguration(surface); @@ -328,7 +328,7 @@ public class CameraCapture extends SurfaceCapture { return requestBuilder.build(); } - @TargetApi(Build.VERSION_CODES.S) + @TargetApi(AndroidVersions.API_31_ANDROID_12) private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request) throws CameraAccessException, InterruptedException { CameraCaptureSession.CaptureCallback callback = new CameraCaptureSession.CaptureCallback() { @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 62afb263..e6357410 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; @@ -103,8 +104,8 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList private static IBinder createDisplay() throws Exception { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". - boolean secure = Build.VERSION.SDK_INT < Build.VERSION_CODES.R || (Build.VERSION.SDK_INT == Build.VERSION_CODES.R && !"S".equals( - Build.VERSION.CODENAME)); + boolean secure = Build.VERSION.SDK_INT < AndroidVersions.API_30_ANDROID_11 || (Build.VERSION.SDK_INT == AndroidVersions.API_30_ANDROID_11 + && !"S".equals(Build.VERSION.CODENAME)); return SurfaceControl.createDisplay("scrcpy", secure); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 41c38642..5a9417da 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; @@ -238,7 +239,7 @@ public class SurfaceEncoder implements AsyncProcessor { // must be present to configure the encoder, but does not impact the actual frame rate, which is variable format.setInteger(MediaFormat.KEY_FRAME_RATE, 60); format.setInteger(MediaFormat.KEY_COLOR_FORMAT, MediaCodecInfo.CodecCapabilities.COLOR_FormatSurface); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0) { format.setInteger(MediaFormat.KEY_COLOR_RANGE, MediaFormat.COLOR_RANGE_LIMITED); } format.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, DEFAULT_I_FRAME_INTERVAL); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index bb1ca0d4..c907e12f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; @@ -7,7 +8,6 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Intent; import android.os.Binder; -import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.os.IInterface; @@ -63,7 +63,7 @@ public final class ActivityManager { return removeContentProviderExternalMethod; } - @TargetApi(Build.VERSION_CODES.Q) + @TargetApi(AndroidVersions.API_29_ANDROID_10) private ContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index c5f007fe..791df0f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; @@ -36,7 +37,7 @@ public final class ClipboardManager { private Method getGetPrimaryClipMethod() throws NoSuchMethodException { if (getPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); return getPrimaryClipMethod; } @@ -99,7 +100,7 @@ public final class ClipboardManager { private Method getSetPrimaryClipMethod() throws NoSuchMethodException { if (setPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); return setPrimaryClipMethod; } @@ -137,7 +138,7 @@ public final class ClipboardManager { } private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); } @@ -161,7 +162,7 @@ public final class ClipboardManager { } private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); return; } @@ -210,7 +211,7 @@ public final class ClipboardManager { private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { method.invoke(manager, listener, FakeContext.PACKAGE_NAME); return; } @@ -230,7 +231,7 @@ public final class ClipboardManager { private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { if (addPrimaryClipChangedListener == null) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { addPrimaryClipChangedListener = manager.getClass() .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); } else { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java index 7e92ac50..f625b398 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ContentProvider.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.SettingsException; @@ -51,7 +52,7 @@ public final class ContentProvider implements Closeable { @SuppressLint("PrivateApi") private Method getCallMethod() throws NoSuchMethodException { if (callMethod == null) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { callMethod = provider.getClass().getMethod("call", AttributionSource.class, String.class, String.class, String.class, Bundle.class); callMethodVersion = 0; } else { @@ -79,7 +80,7 @@ public final class ContentProvider implements Closeable { Method method = getCallMethod(); Object[] args; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && callMethodVersion == 0) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12 && callMethodVersion == 0) { args = new Object[]{FakeContext.get().getAttributionSource(), "settings", callMethod, arg, extras}; } else { switch (callMethodVersion) { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java index cc9d5526..a57f7948 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -1,16 +1,16 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; -import android.os.Build; import android.os.IBinder; import java.lang.reflect.Method; @SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) -@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +@TargetApi(AndroidVersions.API_34_ANDROID_14) public final class DisplayControl { private static final Class CLASS; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 0a56f347..615ceb42 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -24,7 +25,7 @@ public final class PowerManager { private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; + String methodName = Build.VERSION.SDK_INT >= AndroidVersions.API_20_ANDROID_4_4 ? "isInteractive" : "isScreenOn"; isScreenOnMethod = manager.getClass().getMethod(methodName); } return isScreenOnMethod; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java index 038e7ca0..3bae4a37 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -83,9 +84,9 @@ public final class SurfaceControl { private static Method getGetBuiltInDisplayMethod() throws NoSuchMethodException { if (getBuiltInDisplayMethod == null) { - // the method signature has changed in Android Q + // the method signature has changed in Android 10 // - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { getBuiltInDisplayMethod = CLASS.getMethod("getBuiltInDisplay", int.class); } else { getBuiltInDisplayMethod = CLASS.getMethod("getInternalDisplayToken"); @@ -106,7 +107,7 @@ public final class SurfaceControl { public static IBinder getBuiltInDisplay() { try { Method method = getGetBuiltInDisplayMethod(); - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { // call getBuiltInDisplay(0) return (IBinder) method.invoke(null, 0); } From a46150f753c47d0fb180040448629d229ae74581 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 15:06:54 +0200 Subject: [PATCH 028/278] Upgrade Android SDK to 35 Also adapt the call to build-tools/35.0.0/aidl, which now requires an import path (-I. for the current directory). Otherwise, it fails with: ERROR: android/view/IRotationWatcher.aidl:23.1-10: directory ./ is not found in any of the import paths: - Also upgrade AGP (8.7.1) and Gradle (8.9), required for SDK 35. --- build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +++- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 10 +++++----- .../java/com/genymobile/scrcpy/AndroidVersions.java | 1 + 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index f81f7d27..81c91d37 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:8.3.0' + classpath 'com.android.tools.build:gradle:8.7.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e411586a..b34b7096 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip +# https://gradle.org/release-checksums/ +distributionSha256Sum=d725d707bfabd4dfdc958c624003b3c80accc03f7037b5122c4b1d0ef15cecab zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/server/build.gradle b/server/build.gradle index 655298a9..2781a2db 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -2,11 +2,11 @@ apply plugin: 'com.android.application' android { namespace 'com.genymobile.scrcpy' - compileSdk 34 + compileSdk 35 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 34 + targetSdkVersion 35 versionCode 20700 versionName "2.7" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index ab6c821d..14534700 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -14,8 +14,8 @@ set -e SCRCPY_DEBUG=false SCRCPY_VERSION_NAME=2.7 -PLATFORM=${ANDROID_PLATFORM:-34} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} +PLATFORM=${ANDROID_PLATFORM:-35} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" @@ -45,10 +45,10 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IRotationWatcher.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" \ +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ android/content/IOnPrimaryClipChangedListener.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" android/view/IDisplayFoldListener.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl SRC=( \ com/genymobile/scrcpy/*.java \ diff --git a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java index 8acad7ee..98fa6dc3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java +++ b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java @@ -28,5 +28,6 @@ public final class AndroidVersions { public static final int API_32_ANDROID_12L = Build.VERSION_CODES.S_V2; public static final int API_33_ANDROID_13 = Build.VERSION_CODES.TIRAMISU; public static final int API_34_ANDROID_14 = Build.VERSION_CODES.UPSIDE_DOWN_CAKE; + public static final int API_35_ANDROID_15 = Build.VERSION_CODES.VANILLA_ICE_CREAM; } From 7b3dd595b493449b4e93281205c375b1472d3b87 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 22:53:32 +0200 Subject: [PATCH 029/278] Remove useless version check Scrcpy requires Android 5.0+, so there is no point testing for older versions. Btw, there were two mistakes: - the constant name in AndroidVersions should have been API_20_ANDROID_4_4W (Android 4.4 without 'W' is API 19) - the method isInteractive() was introduced in Android 5.0, not 4.4W: --- .../main/java/com/genymobile/scrcpy/AndroidVersions.java | 1 - .../java/com/genymobile/scrcpy/wrappers/PowerManager.java | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java index 98fa6dc3..5303924a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java +++ b/server/src/main/java/com/genymobile/scrcpy/AndroidVersions.java @@ -13,7 +13,6 @@ public final class AndroidVersions { // not instantiable } - public static final int API_20_ANDROID_4_4 = Build.VERSION_CODES.KITKAT_WATCH; public static final int API_21_ANDROID_5_0 = Build.VERSION_CODES.LOLLIPOP; public static final int API_22_ANDROID_5_1 = Build.VERSION_CODES.LOLLIPOP_MR1; public static final int API_23_ANDROID_6_0 = Build.VERSION_CODES.M; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index 615ceb42..f62e5b8e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,10 +1,7 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; -import android.annotation.SuppressLint; -import android.os.Build; import android.os.IInterface; import java.lang.reflect.Method; @@ -24,9 +21,7 @@ public final class PowerManager { private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { - @SuppressLint("ObsoleteSdkInt") // we may lower minSdkVersion in the future - String methodName = Build.VERSION.SDK_INT >= AndroidVersions.API_20_ANDROID_4_4 ? "isInteractive" : "isScreenOn"; - isScreenOnMethod = manager.getClass().getMethod(methodName); + isScreenOnMethod = manager.getClass().getMethod("isInteractive"); } return isScreenOnMethod; } From 9578aae34eca49f448d0312838c2b4011e977810 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 22 Oct 2024 19:47:56 +0200 Subject: [PATCH 030/278] Use explicit constant for @TargetApi --- .../java/com/genymobile/scrcpy/wrappers/WindowManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 4c769e85..5894b836 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; @@ -200,7 +201,7 @@ public final class WindowManager { } } - @TargetApi(29) + @TargetApi(AndroidVersions.API_29_ANDROID_10) public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { try { Class cls = manager.getClass(); From 67d4dfb5ffcfeaddf3363e73fdf3b2c500fd5842 Mon Sep 17 00:00:00 2001 From: Anwar Fuadi Date: Sun, 28 Jul 2024 14:24:51 +0700 Subject: [PATCH 031/278] Add missing client build dependency in Fedora PR #5147 Signed-off-by: Romain Vimont --- doc/build.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/build.md b/doc/build.md index 63bd7ca7..0c70f598 100644 --- a/doc/build.md +++ b/doc/build.md @@ -77,7 +77,7 @@ pip3 install meson sudo dnf install https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-$(rpm -E %fedora).noarch.rpm # client build dependencies -sudo dnf install SDL2-devel ffms2-devel libusb1-devel meson gcc make +sudo dnf install SDL2-devel ffms2-devel libusb1-devel libavdevice-free-devel meson gcc make # server build dependencies sudo dnf install java-devel From 538a32a53973d5ab1e393a36286de094a97aaf9a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 22:32:28 +0200 Subject: [PATCH 032/278] Fix .PHONY in release.mk The prepare-deps recipe does not exist anymore. It has been split into prepare-deps-win32 and prepare-deps-win64. PR #5306 --- release.mk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release.mk b/release.mk index 7f082144..099db85e 100644 --- a/release.mk +++ b/release.mk @@ -11,7 +11,7 @@ .PHONY: default clean \ test \ build-server \ - prepare-deps \ + prepare-deps-win32 prepare-deps-win64 \ build-win32 build-win64 \ dist-win32 dist-win64 \ zip-win32 zip-win64 \ From 02ef3d57ce9542493ea7efb4ace66c4300d472fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 23:39:45 +0200 Subject: [PATCH 033/278] Split client and server tests in release.mk This will allow to run server tests separately on the CI. PR #5306 --- release.mk | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/release.mk b/release.mk index 099db85e..91a8f41d 100644 --- a/release.mk +++ b/release.mk @@ -9,7 +9,7 @@ # the server to the device. .PHONY: default clean \ - test \ + test test-client test-server \ build-server \ prepare-deps-win32 prepare-deps-win64 \ build-win32 build-win64 \ @@ -51,12 +51,16 @@ clean: rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" -test: +test-client: [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address ) ninja -C "$(TEST_BUILD_DIR)" + +test-server: $(GRADLE) -p server check +test: test-client test-server + build-server: [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) From 9c0a32849897d98e17bf0f8d7603454e558746cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 23:40:27 +0200 Subject: [PATCH 034/278] Build server without meson in release.mk This avoids to install meson/ninja to build scrcpy-server on the CI. PR #5306 --- release.mk | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/release.mk b/release.mk index 91a8f41d..f99eaf4f 100644 --- a/release.mk +++ b/release.mk @@ -62,9 +62,10 @@ test-server: test: test-client test-server build-server: - [ -d "$(SERVER_BUILD_DIR)" ] || ( mkdir "$(SERVER_BUILD_DIR)" && \ - meson setup "$(SERVER_BUILD_DIR)" --buildtype release -Dcompile_app=false ) - ninja -C "$(SERVER_BUILD_DIR)" + $(GRADLE) -p server assembleRelease + mkdir -p "$(SERVER_BUILD_DIR)/server" + cp server/build/outputs/apk/release/server-release-unsigned.apk \ + "$(SERVER_BUILD_DIR)/server/scrcpy-server" prepare-deps-win32: @app/deps/adb.sh win32 From 2687d202809dfaafe8f40f613aec131ad9501433 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 23:51:52 +0200 Subject: [PATCH 035/278] Rework release.mk for CI Make it possible to build scrcpy-server and Windows binaries in parallel from different GitHub Actions workflows, and to package everything as a final step. PR #5306 --- release.mk | 87 +++++++++++++++++++++++++++++------------------------- 1 file changed, 46 insertions(+), 41 deletions(-) diff --git a/release.mk b/release.mk index f99eaf4f..61145002 100644 --- a/release.mk +++ b/release.mk @@ -13,9 +13,8 @@ build-server \ prepare-deps-win32 prepare-deps-win64 \ build-win32 build-win64 \ - dist-win32 dist-win64 \ zip-win32 zip-win64 \ - release + package release GRADLE ?= ./gradlew @@ -26,7 +25,7 @@ WIN64_BUILD_DIR := build-win64 VERSION ?= $(shell git describe --tags --exclude='*install-release' --always) -DIST := dist +ZIP := zip WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) WIN64_TARGET_DIR := scrcpy-win64-$(VERSION) WIN32_TARGET := $(WIN32_TARGET_DIR).zip @@ -34,21 +33,11 @@ WIN64_TARGET := $(WIN64_TARGET_DIR).zip RELEASE_DIR := release-$(VERSION) -release: clean test build-server zip-win32 zip-win64 - mkdir -p "$(RELEASE_DIR)" - cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ - "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" - cp "$(DIST)/$(WIN32_TARGET)" "$(RELEASE_DIR)" - cp "$(DIST)/$(WIN64_TARGET)" "$(RELEASE_DIR)" - cd "$(RELEASE_DIR)" && \ - sha256sum "scrcpy-server-$(VERSION)" \ - "scrcpy-win32-$(VERSION).zip" \ - "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt - @echo "Release generated in $(RELEASE_DIR)/" +release: clean test build-server build-win32 build-win64 package clean: $(GRADLE) clean - rm -rf "$(DIST)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ + rm -rf "$(ZIP)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" test-client: @@ -91,6 +80,15 @@ build-win32: prepare-deps-win32 -Dcompile_server=false \ -Dportable=true ninja -C "$(WIN32_BUILD_DIR)" + # Group intermediate outputs into a 'dist' directory + mkdir -p "$(WIN32_BUILD_DIR)/dist" + cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(WIN32_BUILD_DIR)/dist/" + cp app/data/scrcpy-console.bat "$(WIN32_BUILD_DIR)/dist/" + cp app/data/scrcpy-noconsole.vbs "$(WIN32_BUILD_DIR)/dist/" + cp app/data/icon.png "$(WIN32_BUILD_DIR)/dist/" + cp app/data/open_a_terminal_here.bat "$(WIN32_BUILD_DIR)/dist/" + cp app/deps/work/install/win32/bin/*.dll "$(WIN32_BUILD_DIR)/dist/" + cp app/deps/work/install/win32/bin/adb.exe "$(WIN32_BUILD_DIR)/dist/" build-win64: prepare-deps-win64 rm -rf "$(WIN64_BUILD_DIR)" @@ -104,33 +102,40 @@ build-win64: prepare-deps-win64 -Dcompile_server=false \ -Dportable=true ninja -C "$(WIN64_BUILD_DIR)" + # Group intermediate outputs into a 'dist' directory + mkdir -p "$(WIN64_BUILD_DIR)/dist" + cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(WIN64_BUILD_DIR)/dist/" + cp app/data/scrcpy-console.bat "$(WIN64_BUILD_DIR)/dist/" + cp app/data/scrcpy-noconsole.vbs "$(WIN64_BUILD_DIR)/dist/" + cp app/data/icon.png "$(WIN64_BUILD_DIR)/dist/" + cp app/data/open_a_terminal_here.bat "$(WIN64_BUILD_DIR)/dist/" + cp app/deps/work/install/win64/bin/*.dll "$(WIN64_BUILD_DIR)/dist/" + cp app/deps/work/install/win64/bin/adb.exe "$(WIN64_BUILD_DIR)/dist/" -dist-win32: build-server build-win32 - mkdir -p "$(DIST)/$(WIN32_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN32_TARGET_DIR)/" - cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/data/scrcpy-console.bat "$(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/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/deps/work/install/win32/bin/*.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/deps/work/install/win32/bin/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" - -dist-win64: build-server build-win64 - mkdir -p "$(DIST)/$(WIN64_TARGET_DIR)" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(DIST)/$(WIN64_TARGET_DIR)/" - cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/data/scrcpy-console.bat "$(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/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/deps/work/install/win64/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/deps/work/install/win64/bin/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" - -zip-win32: dist-win32 - cd "$(DIST)"; \ +zip-win32: + mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)" + cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/" + cd "$(ZIP)"; \ zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" + rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)" -zip-win64: dist-win64 - cd "$(DIST)"; \ +zip-win64: + mkdir -p "$(ZIP)/$(WIN64_TARGET_DIR)" + cp -r "$(WIN64_BUILD_DIR)/dist/." "$(ZIP)/$(WIN64_TARGET_DIR)/" + cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN64_TARGET_DIR)/" + cd "$(ZIP)"; \ zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" + rm -rf "$(ZIP)/$(WIN64_TARGET_DIR)" + +package: zip-win32 zip-win64 + mkdir -p "$(RELEASE_DIR)" + cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ + "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" + cp "$(ZIP)/$(WIN32_TARGET)" "$(RELEASE_DIR)" + cp "$(ZIP)/$(WIN64_TARGET)" "$(RELEASE_DIR)" + cd "$(RELEASE_DIR)" && \ + sha256sum "scrcpy-server-$(VERSION)" \ + "scrcpy-win32-$(VERSION).zip" \ + "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt + @echo "Release generated in $(RELEASE_DIR)/" From a5844e198e9bc53653c8658d53b7b9df4e122cc0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Sep 2024 23:58:42 +0200 Subject: [PATCH 036/278] Add GitHub Actions release workflow Fixes #4490 PR #5306 --- .github/workflows/release.yml | 147 ++++++++++++++++++++++++++++++++++ 1 file changed, 147 insertions(+) create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..e67c1c21 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,147 @@ +name: Build + +on: + workflow_dispatch: + inputs: + name: + description: 'Version name (default is ref name)' + +jobs: + build-scrcpy-server: + runs-on: ubuntu-latest + env: + GRADLE: gradle # use native gradle instead of ./gradlew in release.mk + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Test scrcpy-server + run: make -f release.mk test-server + + - name: Build scrcpy-server + run: make -f release.mk build-server + + - name: Upload scrcpy-server artifact + uses: actions/upload-artifact@v4 + with: + name: scrcpy-server + path: build-server/server/scrcpy-server + + test-client: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ + libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev + + - name: Build + run: | + meson setup d -Db_sanitize=address,undefined + + - name: Test + run: | + meson test -Cd + + build-win32: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ + libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ + mingw-w64 mingw-w64-tools libz-mingw-w64-dev + + - name: Workaround for old meson version run by Github Actions + run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt + + - name: Build scrcpy win32 + run: make -f release.mk build-win32 + + - name: Upload build-win32 artifact + uses: actions/upload-artifact@v4 + with: + name: build-win32-intermediate + path: build-win32/dist/ + + build-win64: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ + libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ + mingw-w64 mingw-w64-tools libz-mingw-w64-dev + + - name: Workaround for old meson version run by Github Actions + run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt + + - name: Build scrcpy win64 + run: make -f release.mk build-win64 + + - name: Upload build-win64 artifact + uses: actions/upload-artifact@v4 + with: + name: build-win64-intermediate + path: build-win64/dist/ + + package: + needs: + - build-scrcpy-server + - build-win32 + - build-win64 + runs-on: ubuntu-latest + env: + # $VERSION is used by release.mk + VERSION: ${{ github.event.inputs.name || github.ref_name }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: build-server/server/ + + - name: Download build-win32 + uses: actions/download-artifact@v4 + with: + name: build-win32-intermediate + path: build-win32/dist/ + + - name: Download build-win64 + uses: actions/download-artifact@v4 + with: + name: build-win64-intermediate + path: build-win64/dist/ + + - name: Package + run: make -f release.mk package + + - name: Upload release artifact + uses: actions/upload-artifact@v4 + with: + name: scrcpy-release-${{ env.VERSION }} + path: release-${{ env.VERSION }} From 14e5439dee5486f870bda95add4102eaba39971c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 28 Oct 2024 14:52:05 +0100 Subject: [PATCH 037/278] Update mouse documentation about capture key The mouse capture keys are not hardcoded anymore, they use the configured shortcut modifiers. Refs ff9fb5994dbe555be8835a5f8d06b03e9f3b1b27 --- doc/mouse.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/mouse.md b/doc/mouse.md index ae7c6834..3607a92c 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -34,9 +34,9 @@ Two modes allow to simulate a physical HID mouse on the device. In these modes, the computer mouse is "captured": the mouse pointer disappears from the computer and appears on the Android device instead. -Special capture keys, either Alt or Super, toggle -(disable or enable) the mouse capture. Use one of them to give the control of -the mouse back to the computer. +The [shortcut mod](shortcuts.md) (either Alt or Super by +default) toggle (disable or enable) the mouse capture. Use one of them to give +the control of the mouse back to the computer. ### UHID From 874eaec487369f7fcaa9ed8c5f85569659565d4f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 038/278] Move screen-related features out of Device.java Move the code related to screen size and rotation/fold to ScreenCapture. For now, keep the ScreenInfo instance in the Device class to communicate with the Controller, but it will be removed by further commits. PR #5370 --- .../java/com/genymobile/scrcpy/Server.java | 3 +- .../com/genymobile/scrcpy/device/Device.java | 127 ++---------------- .../scrcpy/video/ScreenCapture.java | 123 +++++++++++++---- .../scrcpy/video/SurfaceCapture.java | 3 +- .../scrcpy/wrappers/WindowManager.java | 20 ++- 5 files changed, 135 insertions(+), 141 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 555cf97a..9802e0f5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -190,7 +190,8 @@ public final class Server { options.getSendFrameMeta()); SurfaceCapture surfaceCapture; if (options.getVideoSource() == VideoSource.DISPLAY) { - surfaceCapture = new ScreenCapture(device); + surfaceCapture = new ScreenCapture(device, options.getDisplayId(), options.getMaxSize(), options.getCrop(), + options.getLockVideoOrientation()); } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed()); diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 1f375942..63e33988 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -3,7 +3,6 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; @@ -17,14 +16,13 @@ import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; -import android.view.IDisplayFoldListener; -import android.view.IRotationWatcher; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; public final class Device { @@ -38,26 +36,10 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - public interface RotationListener { - void onRotationChanged(int rotation); - } - - public interface FoldListener { - void onFoldChanged(int displayId, boolean folded); - } - public interface ClipboardListener { void onClipboardTextChanged(String text); } - private final Rect crop; - private int maxSize; - private final int lockVideoOrientation; - - private Size deviceSize; - private ScreenInfo screenInfo; - private RotationListener rotationListener; - private FoldListener foldListener; private ClipboardListener clipboardListener; private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); @@ -66,71 +48,12 @@ public final class Device { */ private final int displayId; - /** - * The surface flinger layer stack associated with this logical display - */ - private final int layerStack; - private final boolean supportsInputEvents; - public Device(Options options) throws ConfigurationException { + private final AtomicReference screenInfo = new AtomicReference<>(); // set by the ScreenCapture instance + + public Device(Options options) { displayId = options.getDisplayId(); - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - throw new ConfigurationException("Unknown display id: " + displayId); - } - - int displayInfoFlags = displayInfo.getFlags(); - - deviceSize = displayInfo.getSize(); - crop = options.getCrop(); - maxSize = options.getMaxSize(); - lockVideoOrientation = options.getLockVideoOrientation(); - - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); - layerStack = displayInfo.getLayerStack(); - - ServiceManager.getWindowManager().registerRotationWatcher(new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - synchronized (Device.this) { - screenInfo = screenInfo.withDeviceRotation(rotation); - - // notify - if (rotationListener != null) { - rotationListener.onRotationChanged(rotation); - } - } - } - }, displayId); - - if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { - ServiceManager.getWindowManager().registerDisplayFoldListener(new IDisplayFoldListener.Stub() { - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - if (Device.this.displayId != displayId) { - // Ignore events related to other display ids - return; - } - - synchronized (Device.this) { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - return; - } - - deviceSize = displayInfo.getSize(); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); - // notify - if (foldListener != null) { - foldListener.onFoldChanged(displayId, folded); - } - } - } - }); - } if (options.getControl() && options.getClipboardAutosync()) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically @@ -158,38 +81,20 @@ public final class Device { } } - if ((displayInfoFlags & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { - Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); - } - // main display or any display on Android >= 10 - supportsInputEvents = displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; + supportsInputEvents = options.getDisplayId() == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (!supportsInputEvents) { Ln.w("Input events are not supported for secondary displays before Android 10"); } } - public int getDisplayId() { - return displayId; - } - - public synchronized void setMaxSize(int newMaxSize) { - maxSize = newMaxSize; - screenInfo = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); - } - - public synchronized ScreenInfo getScreenInfo() { - return screenInfo; - } - - public int getLayerStack() { - return layerStack; - } - public Point getPhysicalPoint(Position position) { - // it hides the field on purpose, to read it with a lock + // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") - ScreenInfo screenInfo = getScreenInfo(); // read with synchronization + ScreenInfo screenInfo = this.screenInfo.get(); + if (screenInfo == null) { + return null; + } // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); @@ -223,6 +128,10 @@ public final class Device { return supportsInputEvents; } + public void setScreenInfo(ScreenInfo screenInfo) { + this.screenInfo.set(screenInfo); + } + public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { if (!supportsInputEvents(displayId)) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); @@ -263,14 +172,6 @@ public final class Device { return ServiceManager.getPowerManager().isScreenOn(); } - public synchronized void setRotationListener(RotationListener rotationListener) { - this.rotationListener = rotationListener; - } - - public synchronized void setFoldListener(FoldListener foldlistener) { - this.foldListener = foldlistener; - } - public synchronized void setClipboardListener(ClipboardListener clipboardListener) { this.clipboardListener = clipboardListener; } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index e6357410..af9a9283 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,9 +1,12 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -11,33 +14,104 @@ import android.graphics.Rect; import android.hardware.display.VirtualDisplay; import android.os.Build; import android.os.IBinder; +import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; import android.view.Surface; -public class ScreenCapture extends SurfaceCapture implements Device.RotationListener, Device.FoldListener { +public class ScreenCapture extends SurfaceCapture { private final Device device; + + private final int displayId; + private int maxSize; + private final Rect crop; + private final int lockVideoOrientation; + private int layerStack; + + private Size deviceSize; + private ScreenInfo screenInfo; + private IBinder display; private VirtualDisplay virtualDisplay; - public ScreenCapture(Device device) { + private IRotationWatcher rotationWatcher; + private IDisplayFoldListener displayFoldListener; + + public ScreenCapture(Device device, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { this.device = device; + this.displayId = displayId; + this.maxSize = maxSize; + this.crop = crop; + this.lockVideoOrientation = lockVideoOrientation; } @Override - public void init() { - device.setRotationListener(this); - device.setFoldListener(this); + public void init() throws ConfigurationException { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + throw new ConfigurationException("Unknown display id: " + displayId); + } + + deviceSize = displayInfo.getSize(); + ScreenInfo si = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); + setScreenInfo(si); + layerStack = displayInfo.getLayerStack(); + + if (displayId == 0) { + rotationWatcher = new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + synchronized (ScreenCapture.this) { + ScreenInfo si = screenInfo.withDeviceRotation(rotation); + setScreenInfo(si); + } + + requestReset(); + } + }; + ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); + } + + if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { + displayFoldListener = new IDisplayFoldListener.Stub() { + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + if (ScreenCapture.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + + synchronized (ScreenCapture.this) { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + return; + } + + deviceSize = displayInfo.getSize(); + ScreenInfo si = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); + setScreenInfo(si); + } + + requestReset(); + } + }; + ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); + } + + if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { + Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); + } } @Override public void start(Surface surface) { - ScreenInfo screenInfo = device.getScreenInfo(); Rect contentRect = screenInfo.getContentRect(); // does not include the locked video orientation Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); - int layerStack = device.getLayerStack(); if (display != null) { SurfaceControl.destroyDisplay(display); @@ -51,7 +125,7 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList try { Rect videoRect = screenInfo.getVideoSize().toRect(); virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), device.getDisplayId(), surface); + .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), displayId, surface); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -68,8 +142,12 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList @Override public void release() { - device.setRotationListener(null); - device.setFoldListener(null); + if (rotationWatcher != null) { + ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); + } + if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { + ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + } if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -81,26 +159,18 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList } @Override - public Size getSize() { - return device.getScreenInfo().getVideoSize(); + public synchronized Size getSize() { + return screenInfo.getVideoSize(); } @Override - public boolean setMaxSize(int maxSize) { - device.setMaxSize(maxSize); + public synchronized boolean setMaxSize(int newMaxSize) { + maxSize = newMaxSize; + ScreenInfo si = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); + setScreenInfo(si); return true; } - @Override - public void onFoldChanged(int displayId, boolean folded) { - requestReset(); - } - - @Override - public void onRotationChanged(int rotation) { - requestReset(); - } - private static IBinder createDisplay() throws Exception { // Since Android 12 (preview), secure displays could not be created with shell permissions anymore. // On Android 12 preview, SDK_INT is still R (not S), but CODENAME is "S". @@ -119,4 +189,9 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList SurfaceControl.closeTransaction(); } } + + private void setScreenInfo(ScreenInfo si) { + screenInfo = si; + device.setScreenInfo(si); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index 3118ddc8..fe679beb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import android.view.Surface; @@ -34,7 +35,7 @@ public abstract class SurfaceCapture { /** * Called once before the capture starts. */ - public abstract void init() throws IOException; + public abstract void init() throws ConfigurationException, IOException; /** * Called after the capture ends (if and only if {@link #init()} has been called). diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 5894b836..ee36139a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -201,13 +201,29 @@ public final class WindowManager { } } + public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) { + try { + manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher); + } catch (Exception e) { + Ln.e("Could not unregister rotation watcher", e); + } + } + @TargetApi(AndroidVersions.API_29_ANDROID_10) public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { try { - Class cls = manager.getClass(); - cls.getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); + manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); } catch (Exception e) { Ln.e("Could not register display fold listener", e); } } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) { + try { + manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); + } catch (Exception e) { + Ln.e("Could not unregister display fold listener", e); + } + } } From 5f0480c0398bb3370d3a68182a0a4f950c56d824 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 039/278] Ignore first displayFoldChanged event An event is posted on registration to signal the initial state. This had no impact when the listener was registered from Device (before it was moved to ScreenCapture), because this first initial event was already triggered when ScreenCapture started listening. But now, it causes the first encoding to be reset immediately. To avoid that, ignore the first event. Refs PR #5370 --- .../java/com/genymobile/scrcpy/video/ScreenCapture.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index af9a9283..df7cd8f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -75,8 +75,17 @@ public class ScreenCapture extends SurfaceCapture { if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { displayFoldListener = new IDisplayFoldListener.Stub() { + + private boolean first = true; + @Override public void onDisplayFoldChanged(int displayId, boolean folded) { + if (first) { + // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. + first = false; + return; + } + if (ScreenCapture.this.displayId != displayId) { // Ignore events related to other display ids return; From 68e54d9b0b393b53539218679b21a105fba4ef8f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 040/278] Refactor to call getSize() only once Avoid to call capture.getSize() (provided by the SurfaceCapture implementation) twice. PR #5370 --- .../java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 5a9417da..4da1454d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -68,12 +68,16 @@ public class SurfaceEncoder implements AsyncProcessor { capture.init(); try { - streamer.writeVideoHeader(capture.getSize()); - boolean alive; + boolean headerWritten = false; do { Size size = capture.getSize(); + if (!headerWritten) { + streamer.writeVideoHeader(size); + headerWritten = true; + } + format.setInteger(MediaFormat.KEY_WIDTH, size.getWidth()); format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight()); From 12d5ca4d5ee870ed6112b5fa396924a090b206b2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 041/278] Move local variables in ScreenCapture Do not initialize variables when they are not used. PR #5370 --- .../com/genymobile/scrcpy/video/ScreenCapture.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index df7cd8f2..e279f569 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -116,12 +116,6 @@ public class ScreenCapture extends SurfaceCapture { @Override public void start(Surface surface) { - Rect contentRect = screenInfo.getContentRect(); - - // does not include the locked video orientation - Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); - int videoRotation = screenInfo.getVideoRotation(); - if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -139,6 +133,13 @@ public class ScreenCapture extends SurfaceCapture { } catch (Exception displayManagerException) { try { display = createDisplay(); + + Rect contentRect = screenInfo.getContentRect(); + + // does not include the locked video orientation + Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); + int videoRotation = screenInfo.getVideoRotation(); + setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { From 5851b6258037d2b2c7d763fac17d33d131eb8922 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 042/278] Simplify virtual display video size Do not use an unnecessary intermediate Rect object. PR #5370 --- .../main/java/com/genymobile/scrcpy/video/ScreenCapture.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index e279f569..05d349da 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -126,9 +126,9 @@ public class ScreenCapture extends SurfaceCapture { } try { - Rect videoRect = screenInfo.getVideoSize().toRect(); + Size videoSize = screenInfo.getVideoSize(); virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", videoRect.width(), videoRect.height(), displayId, surface); + .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { From b60e1747809cce58793a8c0d54b499df87a6a975 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 043/278] Add capture prepare() step Add a function called before each capture starts (before getSize() is called). This allows to compute the ScreenInfo instance once exactly when needed. PR #5370 --- .../scrcpy/video/ScreenCapture.java | 58 ++++++------------- .../genymobile/scrcpy/video/ScreenInfo.java | 22 ------- .../scrcpy/video/SurfaceCapture.java | 11 +++- .../scrcpy/video/SurfaceEncoder.java | 1 + 4 files changed, 28 insertions(+), 64 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 05d349da..f71ff020 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -26,9 +26,8 @@ public class ScreenCapture extends SurfaceCapture { private int maxSize; private final Rect crop; private final int lockVideoOrientation; - private int layerStack; - private Size deviceSize; + private DisplayInfo displayInfo; private ScreenInfo screenInfo; private IBinder display; @@ -46,27 +45,11 @@ public class ScreenCapture extends SurfaceCapture { } @Override - public void init() throws ConfigurationException { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - throw new ConfigurationException("Unknown display id: " + displayId); - } - - deviceSize = displayInfo.getSize(); - ScreenInfo si = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); - setScreenInfo(si); - layerStack = displayInfo.getLayerStack(); - + public void init() { if (displayId == 0) { rotationWatcher = new IRotationWatcher.Stub() { @Override public void onRotationChanged(int rotation) { - synchronized (ScreenCapture.this) { - ScreenInfo si = screenInfo.withDeviceRotation(rotation); - setScreenInfo(si); - } - requestReset(); } }; @@ -91,27 +74,26 @@ public class ScreenCapture extends SurfaceCapture { return; } - synchronized (ScreenCapture.this) { - DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (displayInfo == null) { - Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); - return; - } - - deviceSize = displayInfo.getSize(); - ScreenInfo si = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), deviceSize, crop, maxSize, lockVideoOrientation); - setScreenInfo(si); - } - requestReset(); } }; ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); } + } + + @Override + public void prepare() throws ConfigurationException { + displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (displayInfo == null) { + Ln.e("Display " + displayId + " not found\n" + LogUtils.buildDisplayListMessage()); + throw new ConfigurationException("Unknown display id: " + displayId); + } if ((displayInfo.getFlags() & DisplayInfo.FLAG_SUPPORTS_PROTECTED_BUFFERS) == 0) { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } + + screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation); } @Override @@ -139,6 +121,7 @@ public class ScreenCapture extends SurfaceCapture { // does not include the locked video orientation Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); + int layerStack = displayInfo.getLayerStack(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); Ln.d("Display: using SurfaceControl API"); @@ -148,6 +131,8 @@ public class ScreenCapture extends SurfaceCapture { throw new AssertionError("Could not create display"); } } + + device.setScreenInfo(screenInfo); } @Override @@ -169,15 +154,13 @@ public class ScreenCapture extends SurfaceCapture { } @Override - public synchronized Size getSize() { + public Size getSize() { return screenInfo.getVideoSize(); } @Override - public synchronized boolean setMaxSize(int newMaxSize) { + public boolean setMaxSize(int newMaxSize) { maxSize = newMaxSize; - ScreenInfo si = ScreenInfo.computeScreenInfo(screenInfo.getReverseVideoRotation(), deviceSize, crop, newMaxSize, lockVideoOrientation); - setScreenInfo(si); return true; } @@ -199,9 +182,4 @@ public class ScreenCapture extends SurfaceCapture { SurfaceControl.closeTransaction(); } } - - private void setScreenInfo(ScreenInfo si) { - screenInfo = si; - device.setScreenInfo(si); - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index bd0a3b62..1f74ce34 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -63,28 +63,6 @@ public final class ScreenInfo { return unlockedVideoSize.rotate(); } - public int getDeviceRotation() { - return deviceRotation; - } - - public ScreenInfo withDeviceRotation(int newDeviceRotation) { - if (newDeviceRotation == deviceRotation) { - return this; - } - // true if changed between portrait and landscape - boolean orientationChanged = (deviceRotation + newDeviceRotation) % 2 != 0; - Rect newContentRect; - Size newUnlockedVideoSize; - if (orientationChanged) { - newContentRect = flipRect(contentRect); - newUnlockedVideoSize = unlockedVideoSize.rotate(); - } else { - newContentRect = contentRect; - newUnlockedVideoSize = unlockedVideoSize; - } - return new ScreenInfo(newContentRect, newUnlockedVideoSize, newDeviceRotation, lockedVideoOrientation); - } - public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index fe679beb..0ee93c92 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -33,15 +33,22 @@ public abstract class SurfaceCapture { } /** - * Called once before the capture starts. + * Called once before the first capture starts. */ public abstract void init() throws ConfigurationException, IOException; /** - * Called after the capture ends (if and only if {@link #init()} has been called). + * Called after the last capture ends (if and only if {@link #init()} has been called). */ public abstract void release(); + /** + * Called once before each capture starts, before {@link #getSize()}. + */ + public void prepare() throws ConfigurationException { + // empty by default + } + /** * Start the capture to the target surface. * diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 4da1454d..84bda1ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -72,6 +72,7 @@ public class SurfaceEncoder implements AsyncProcessor { boolean headerWritten = false; do { + capture.prepare(); Size size = capture.getSize(); if (!headerWritten) { streamer.writeVideoHeader(size); From 7cfefae5e110f32940f6ad35dbd45813d066f735 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 044/278] Move implicit displayId to Controller Remove from Device the functions using an implicit displayId. Move them to Controller, which knows best which displayId it must use. This will allow to properly dispatch events either to the origin display or to the virtual display created for mirroring. PR #5370 --- .../java/com/genymobile/scrcpy/Server.java | 3 +- .../genymobile/scrcpy/control/Controller.java | 67 ++++++++++++------- .../com/genymobile/scrcpy/device/Device.java | 34 +--------- 3 files changed, 48 insertions(+), 56 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 9802e0f5..ed3ae669 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -156,7 +156,8 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); - Controller controller = new Controller(device, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + Controller controller = new Controller( + device, options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); device.setClipboardListener(text -> { DeviceMessage msg = DeviceMessage.createClipboard(text); controller.getSender().send(msg); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 8fa27e81..ee2e1749 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -14,6 +14,7 @@ import android.content.Intent; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; +import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -37,6 +38,8 @@ public class Controller implements AsyncProcessor { private UhidManager uhidManager; private final Device device; + private final int displayId; + private final boolean supportsInputEvents; private final ControlChannel controlChannel; private final CleanUp cleanUp; private final DeviceMessageSender sender; @@ -52,14 +55,20 @@ public class Controller implements AsyncProcessor { private boolean keepPowerModeOff; - public Controller(Device device, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { + public Controller(Device device, int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.device = device; + this.displayId = displayId; this.controlChannel = controlChannel; this.cleanUp = cleanUp; this.clipboardAutosync = clipboardAutosync; this.powerOn = powerOn; initPointers(); sender = new DeviceMessageSender(controlChannel); + + supportsInputEvents = Device.supportsInputEvents(displayId); + if (!supportsInputEvents) { + Ln.w("Input events are not supported for secondary displays before Android 10"); + } } private UhidManager getUhidManager() { @@ -86,7 +95,7 @@ public class Controller implements AsyncProcessor { private void control() throws IOException { // on start, power on the device if (powerOn && !Device.isScreenOn()) { - device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); + Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); // dirty hack // After POWER is injected, the device is powered on asynchronously. @@ -154,27 +163,27 @@ public class Controller implements AsyncProcessor { switch (msg.getType()) { case ControlMessage.TYPE_INJECT_KEYCODE: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { injectKeycode(msg.getAction(), msg.getKeycode(), msg.getRepeat(), msg.getMetaState()); } break; case ControlMessage.TYPE_INJECT_TEXT: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { injectText(msg.getText()); } break; case ControlMessage.TYPE_INJECT_TOUCH_EVENT: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { injectTouch(msg.getAction(), msg.getPointerId(), msg.getPosition(), msg.getPressure(), msg.getActionButton(), msg.getButtons()); } break; case ControlMessage.TYPE_INJECT_SCROLL_EVENT: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { injectScroll(msg.getPosition(), msg.getHScroll(), msg.getVScroll(), msg.getButtons()); } break; case ControlMessage.TYPE_BACK_OR_SCREEN_ON: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { pressBackOrTurnScreenOn(msg.getAction()); } break; @@ -194,7 +203,7 @@ public class Controller implements AsyncProcessor { setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - if (device.supportsInputEvents()) { + if (supportsInputEvents) { int mode = msg.getAction(); boolean setPowerModeOk = Device.setScreenPowerMode(mode); if (setPowerModeOk) { @@ -208,7 +217,7 @@ public class Controller implements AsyncProcessor { } break; case ControlMessage.TYPE_ROTATE_DEVICE: - device.rotateDevice(); + Device.rotateDevice(displayId); break; case ControlMessage.TYPE_UHID_CREATE: getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); @@ -233,7 +242,7 @@ public class Controller implements AsyncProcessor { if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { schedulePowerModeOff(); } - return device.injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); + return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } private boolean injectChar(char c) { @@ -244,7 +253,7 @@ public class Controller implements AsyncProcessor { return false; } for (KeyEvent event : events) { - if (!device.injectEvent(event, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(event, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -325,7 +334,7 @@ public class Controller implements AsyncProcessor { // First button pressed: ACTION_DOWN MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!device.injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -336,7 +345,7 @@ public class Controller implements AsyncProcessor { if (!InputManager.setActionButton(pressEvent, actionButton)) { return false; } - if (!device.injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { return false; } @@ -350,7 +359,7 @@ public class Controller implements AsyncProcessor { if (!InputManager.setActionButton(releaseEvent, actionButton)) { return false; } - if (!device.injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { return false; } @@ -358,7 +367,7 @@ public class Controller implements AsyncProcessor { // Last button released: ACTION_UP MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!device.injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { + if (!injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -369,7 +378,7 @@ public class Controller implements AsyncProcessor { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - return device.injectEvent(event, Device.INJECT_MODE_ASYNC); + return injectEvent(event, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { @@ -391,7 +400,7 @@ public class Controller implements AsyncProcessor { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); - return device.injectEvent(event, Device.INJECT_MODE_ASYNC); + return injectEvent(event, Device.INJECT_MODE_ASYNC); } /** @@ -406,7 +415,7 @@ public class Controller implements AsyncProcessor { private boolean pressBackOrTurnScreenOn(int action) { if (Device.isScreenOn()) { - return device.injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); + return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); } // Screen is off @@ -419,15 +428,15 @@ public class Controller implements AsyncProcessor { if (keepPowerModeOff) { schedulePowerModeOff(); } - return device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); + return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } private void getClipboard(int copyKey) { // On Android >= 7, press the COPY or CUT key if requested - if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) { + if (copyKey != ControlMessage.COPY_KEY_NONE && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) { int key = copyKey == ControlMessage.COPY_KEY_COPY ? KeyEvent.KEYCODE_COPY : KeyEvent.KEYCODE_CUT; // Wait until the event is finished, to ensure that the clipboard text we read just after is the correct one - device.pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); + pressReleaseKeycode(key, Device.INJECT_MODE_WAIT_FOR_FINISH); } // If clipboard autosync is enabled, then the device clipboard is synchronized to the computer clipboard whenever it changes, in @@ -449,8 +458,8 @@ public class Controller implements AsyncProcessor { } // On Android >= 7, also press the PASTE key if requested - if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && device.supportsInputEvents()) { - device.pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); + if (paste && Build.VERSION.SDK_INT >= AndroidVersions.API_24_ANDROID_7_0 && supportsInputEvents) { + pressReleaseKeycode(KeyEvent.KEYCODE_PASTE, Device.INJECT_MODE_ASYNC); } if (sequence != ControlMessage.SEQUENCE_INVALID) { @@ -466,4 +475,16 @@ public class Controller implements AsyncProcessor { Intent intent = new Intent("android.settings.HARD_KEYBOARD_SETTINGS"); ServiceManager.getActivityManager().startActivity(intent); } + + private boolean injectEvent(InputEvent event, int injectMode) { + return Device.injectEvent(event, displayId, injectMode); + } + + private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { + return Device.injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); + } + + private boolean pressReleaseKeycode(int keyCode, int injectMode) { + return Device.pressReleaseKeycode(keyCode, displayId, injectMode); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 63e33988..7972d740 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -43,18 +43,9 @@ public final class Device { private ClipboardListener clipboardListener; private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); - /** - * Logical display identifier - */ - private final int displayId; - - private final boolean supportsInputEvents; - private final AtomicReference screenInfo = new AtomicReference<>(); // set by the ScreenCapture instance public Device(Options options) { - displayId = options.getDisplayId(); - if (options.getControl() && options.getClipboardAutosync()) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); @@ -80,12 +71,6 @@ public final class Device { Ln.w("No clipboard manager, copy-paste between device and computer will not work"); } } - - // main display or any display on Android >= 10 - supportsInputEvents = options.getDisplayId() == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; - if (!supportsInputEvents) { - Ln.w("Input events are not supported for secondary displays before Android 10"); - } } public Point getPhysicalPoint(Position position) { @@ -121,13 +106,10 @@ public final class Device { } public static boolean supportsInputEvents(int displayId) { + // main display or any display on Android >= 10 return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; } - public boolean supportsInputEvents() { - return supportsInputEvents; - } - public void setScreenInfo(ScreenInfo screenInfo) { this.screenInfo.set(screenInfo); } @@ -144,10 +126,6 @@ public final class Device { return ServiceManager.getInputManager().injectInputEvent(inputEvent, injectMode); } - public boolean injectEvent(InputEvent event, int injectMode) { - return injectEvent(event, displayId, injectMode); - } - public static boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int displayId, int injectMode) { long now = SystemClock.uptimeMillis(); KeyEvent event = new KeyEvent(now, now, action, keyCode, repeat, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 0, @@ -155,19 +133,11 @@ public final class Device { return injectEvent(event, displayId, injectMode); } - public boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { - return injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); - } - public static boolean pressReleaseKeycode(int keyCode, int displayId, int injectMode) { return injectKeyEvent(KeyEvent.ACTION_DOWN, keyCode, 0, 0, displayId, injectMode) && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); } - public boolean pressReleaseKeycode(int keyCode, int injectMode) { - return pressReleaseKeycode(keyCode, displayId, injectMode); - } - public static boolean isScreenOn() { return ServiceManager.getPowerManager().isScreenOn(); } @@ -277,7 +247,7 @@ public final class Device { /** * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ - public void rotateDevice() { + public static void rotateDevice(int displayId) { WindowManager wm = ServiceManager.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(displayId); From d9164295666ef9a27b45eecc8608ca1fc0e0fbd8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 045/278] Move clipboard management to Controller Continue to declutter the global Device. PR #5370 --- .../java/com/genymobile/scrcpy/Server.java | 7 +-- .../genymobile/scrcpy/control/Controller.java | 36 ++++++++++++-- .../com/genymobile/scrcpy/device/Device.java | 49 +------------------ 3 files changed, 34 insertions(+), 58 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ed3ae669..0b60dbdc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -9,7 +9,6 @@ import com.genymobile.scrcpy.audio.AudioRawRecorder; import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.control.ControlChannel; import com.genymobile.scrcpy.control.Controller; -import com.genymobile.scrcpy.control.DeviceMessage; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.DesktopConnection; import com.genymobile.scrcpy.device.Device; @@ -142,7 +141,7 @@ public final class Server { boolean sendDummyByte = options.getSendDummyByte(); boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; - final Device device = camera ? null : new Device(options); + final Device device = camera ? null : new Device(); Workarounds.apply(); @@ -158,10 +157,6 @@ public final class Server { ControlChannel controlChannel = connection.getControlChannel(); Controller controller = new Controller( device, options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); - device.setClipboardListener(text -> { - DeviceMessage msg = DeviceMessage.createClipboard(text); - controller.getSender().send(msg); - }); asyncProcessors.add(controller); } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index ee2e1749..e6463563 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -7,9 +7,11 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; import android.os.Build; import android.os.SystemClock; @@ -23,6 +25,7 @@ import java.io.IOException; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; public class Controller implements AsyncProcessor { @@ -48,6 +51,8 @@ public class Controller implements AsyncProcessor { private final KeyCharacterMap charMap = KeyCharacterMap.load(KeyCharacterMap.VIRTUAL_KEYBOARD); + private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); + private long lastTouchDown; private final PointersState pointersState = new PointersState(); private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; @@ -69,6 +74,29 @@ public class Controller implements AsyncProcessor { if (!supportsInputEvents) { Ln.w("Input events are not supported for secondary displays before Android 10"); } + + if (clipboardAutosync) { + // If control and autosync are enabled, synchronize Android clipboard to the computer automatically + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); + if (clipboardManager != null) { + clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { + @Override + public void dispatchPrimaryClipChanged() { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + String text = Device.getClipboardText(); + if (text != null) { + DeviceMessage msg = DeviceMessage.createClipboard(text); + sender.send(msg); + } + } + }); + } else { + Ln.w("No clipboard manager, copy-paste between device and computer will not work"); + } + } } private UhidManager getUhidManager() { @@ -148,10 +176,6 @@ public class Controller implements AsyncProcessor { sender.join(); } - public DeviceMessageSender getSender() { - return sender; - } - private boolean handleEvent() throws IOException { ControlMessage msg; try { @@ -452,7 +476,9 @@ public class Controller implements AsyncProcessor { } private boolean setClipboard(String text, boolean paste, long sequence) { - boolean ok = device.setClipboardText(text); + isSettingClipboard.set(true); + boolean ok = Device.setClipboardText(text); + isSettingClipboard.set(false); if (ok) { Ln.i("Device clipboard set"); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 7972d740..1765ccf2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; @@ -11,7 +10,6 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; -import android.content.IOnPrimaryClipChangedListener; import android.graphics.Rect; import android.os.Build; import android.os.IBinder; @@ -21,7 +19,6 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; public final class Device { @@ -36,43 +33,8 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - public interface ClipboardListener { - void onClipboardTextChanged(String text); - } - - private ClipboardListener clipboardListener; - private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); - private final AtomicReference screenInfo = new AtomicReference<>(); // set by the ScreenCapture instance - public Device(Options options) { - if (options.getControl() && options.getClipboardAutosync()) { - // If control and autosync are enabled, synchronize Android clipboard to the computer automatically - ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); - if (clipboardManager != null) { - clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { - @Override - public void dispatchPrimaryClipChanged() { - if (isSettingClipboard.get()) { - // This is a notification for the change we are currently applying, ignore it - return; - } - synchronized (Device.this) { - if (clipboardListener != null) { - String text = getClipboardText(); - if (text != null) { - clipboardListener.onClipboardTextChanged(text); - } - } - } - } - }); - } else { - Ln.w("No clipboard manager, copy-paste between device and computer will not work"); - } - } - } - public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") @@ -142,10 +104,6 @@ public final class Device { return ServiceManager.getPowerManager().isScreenOn(); } - public synchronized void setClipboardListener(ClipboardListener clipboardListener) { - this.clipboardListener = clipboardListener; - } - public static void expandNotificationPanel() { ServiceManager.getStatusBarManager().expandNotificationsPanel(); } @@ -170,7 +128,7 @@ public final class Device { return s.toString(); } - public boolean setClipboardText(String text) { + public static boolean setClipboardText(String text) { ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager == null) { return false; @@ -185,10 +143,7 @@ public final class Device { return false; } - isSettingClipboard.set(true); - boolean ok = clipboardManager.setText(text); - isSettingClipboard.set(false); - return ok; + return clipboardManager.setText(text); } /** From f1368d9a8f936ab47d51de5af30c7e64bf5285bc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 046/278] Introduce PositionMapper Extract the function that converts coordinates from video space to display space into a separate component. It only requires the specific data it uses and does not need a full ScreenInfo object (although it can be created from a ScreenInfo instance). PR #5370 --- .../scrcpy/control/PositionMapper.java | 48 +++++++++++++++++++ .../com/genymobile/scrcpy/device/Device.java | 32 +++---------- .../scrcpy/video/ScreenCapture.java | 4 +- 3 files changed, 58 insertions(+), 26 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java new file mode 100644 index 00000000..2ebb5961 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -0,0 +1,48 @@ +package com.genymobile.scrcpy.control; + +import com.genymobile.scrcpy.device.Point; +import com.genymobile.scrcpy.device.Position; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.video.ScreenInfo; + +import android.graphics.Rect; + +public final class PositionMapper { + + private final Size videoSize; + private final Rect contentRect; + private final int coordsRotation; + + public PositionMapper(Size videoSize, Rect contentRect, int videoRotation) { + this.videoSize = videoSize; + this.contentRect = contentRect; + this.coordsRotation = reverseRotation(videoRotation); + } + + public static PositionMapper from(ScreenInfo screenInfo) { + // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation + Size videoSize = screenInfo.getUnlockedVideoSize(); + return new PositionMapper(videoSize, screenInfo.getContentRect(), screenInfo.getVideoRotation()); + } + + private static int reverseRotation(int rotation) { + return (4 - rotation) % 4; + } + + public Point map(Position position) { + // reverse the video rotation to apply the events + Position devicePosition = position.rotate(coordsRotation); + + Size clientVideoSize = devicePosition.getScreenSize(); + if (!videoSize.equals(clientVideoSize)) { + // The client sends a click relative to a video with wrong dimensions, + // the device may have been rotated since the event was generated, so ignore the event + return null; + } + + Point point = devicePosition.getPoint(); + int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); + int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); + return new Point(convertedX, convertedY); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 1765ccf2..0977a2b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,8 +1,8 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.util.Ln; -import com.genymobile.scrcpy.video.ScreenInfo; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; @@ -10,7 +10,6 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; -import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; @@ -33,34 +32,17 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - private final AtomicReference screenInfo = new AtomicReference<>(); // set by the ScreenCapture instance + private final AtomicReference positionMapper = new AtomicReference<>(); // set by the ScreenCapture instance public Point getPhysicalPoint(Position position) { // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") - ScreenInfo screenInfo = this.screenInfo.get(); - if (screenInfo == null) { + PositionMapper positionMapper = this.positionMapper.get(); + if (positionMapper == null) { return null; } - // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation - Size unlockedVideoSize = screenInfo.getUnlockedVideoSize(); - - int reverseVideoRotation = screenInfo.getReverseVideoRotation(); - // reverse the video rotation to apply the events - Position devicePosition = position.rotate(reverseVideoRotation); - - Size clientVideoSize = devicePosition.getScreenSize(); - if (!unlockedVideoSize.equals(clientVideoSize)) { - // The client sends a click relative to a video with wrong dimensions, - // the device may have been rotated since the event was generated, so ignore the event - return null; - } - Rect contentRect = screenInfo.getContentRect(); - Point point = devicePosition.getPoint(); - int convertedX = contentRect.left + point.getX() * contentRect.width() / unlockedVideoSize.getWidth(); - int convertedY = contentRect.top + point.getY() * contentRect.height() / unlockedVideoSize.getHeight(); - return new Point(convertedX, convertedY); + return positionMapper.map(position); } public static String getDeviceName() { @@ -72,8 +54,8 @@ public final class Device { return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; } - public void setScreenInfo(ScreenInfo screenInfo) { - this.screenInfo.set(screenInfo); + public void setPositionMapper(PositionMapper positionMapper) { + this.positionMapper.set(positionMapper); } public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index f71ff020..066c9ae4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; @@ -132,7 +133,8 @@ public class ScreenCapture extends SurfaceCapture { } } - device.setScreenInfo(screenInfo); + PositionMapper positionMapper = PositionMapper.from(screenInfo); + device.setPositionMapper(positionMapper); } @Override From 7024d38199206e1a4e7c02e2c9016e856de4a3c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 047/278] Send PositionMapper to Controller directly When a new capture starts, send a new PositionMapper to the Controller without using the global Device as an intermediate. Now all Device methods are static. PR #5370 --- .../java/com/genymobile/scrcpy/Server.java | 10 +++--- .../genymobile/scrcpy/control/Controller.java | 32 +++++++++++++++---- .../com/genymobile/scrcpy/device/Device.java | 20 ++---------- .../scrcpy/video/ScreenCapture.java | 14 ++++---- .../scrcpy/video/VirtualDisplayListener.java | 7 ++++ 5 files changed, 45 insertions(+), 38 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 0b60dbdc..91e7ce6c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -139,9 +139,6 @@ public final class Server { boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); - boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; - - final Device device = camera ? null : new Device(); Workarounds.apply(); @@ -153,10 +150,11 @@ public final class Server { connection.sendDeviceMeta(Device.getDeviceName()); } + Controller controller = null; + if (control) { ControlChannel controlChannel = connection.getControlChannel(); - Controller controller = new Controller( - device, options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); asyncProcessors.add(controller); } @@ -186,7 +184,7 @@ public final class Server { options.getSendFrameMeta()); SurfaceCapture surfaceCapture; if (options.getVideoSource() == VideoSource.DISPLAY) { - surfaceCapture = new ScreenCapture(device, options.getDisplayId(), options.getMaxSize(), options.getCrop(), + surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(), options.getLockVideoOrientation()); } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index e6463563..ac870f27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -7,6 +7,7 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -26,8 +27,9 @@ import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; -public class Controller implements AsyncProcessor { +public class Controller implements AsyncProcessor, VirtualDisplayListener { private static final int DEFAULT_DEVICE_ID = 0; @@ -40,7 +42,6 @@ public class Controller implements AsyncProcessor { private UhidManager uhidManager; - private final Device device; private final int displayId; private final boolean supportsInputEvents; private final ControlChannel controlChannel; @@ -53,6 +54,8 @@ public class Controller implements AsyncProcessor { private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); + private final AtomicReference positionMapper = new AtomicReference<>(); + private long lastTouchDown; private final PointersState pointersState = new PointersState(); private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; @@ -60,8 +63,7 @@ public class Controller implements AsyncProcessor { private boolean keepPowerModeOff; - public Controller(Device device, int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { - this.device = device; + public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.displayId = displayId; this.controlChannel = controlChannel; this.cleanUp = cleanUp; @@ -99,6 +101,11 @@ public class Controller implements AsyncProcessor { } } + @Override + public void onNewVirtualDisplay(PositionMapper positionMapper) { + this.positionMapper.set(positionMapper); + } + private UhidManager getUhidManager() { if (uhidManager == null) { uhidManager = new UhidManager(sender); @@ -299,7 +306,7 @@ public class Controller implements AsyncProcessor { private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { long now = SystemClock.uptimeMillis(); - Point point = device.getPhysicalPoint(position); + Point point = getPhysicalPoint(position); if (point == null) { Ln.w("Ignore touch event, it was generated for a different device size"); return false; @@ -407,9 +414,9 @@ public class Controller implements AsyncProcessor { private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { long now = SystemClock.uptimeMillis(); - Point point = device.getPhysicalPoint(position); + Point point = getPhysicalPoint(position); if (point == null) { - // ignore event + Ln.w("Ignore scroll event, it was generated for a different device size"); return false; } @@ -427,6 +434,17 @@ public class Controller implements AsyncProcessor { return injectEvent(event, Device.INJECT_MODE_ASYNC); } + private Point getPhysicalPoint(Position position) { + // it hides the field on purpose, to read it with atomic access + @SuppressWarnings("checkstyle:HiddenField") + PositionMapper positionMapper = this.positionMapper.get(); + if (positionMapper == null) { + return null; + } + + return positionMapper.map(position); + } + /** * Schedule a call to set power mode to off after a small delay. */ diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 0977a2b7..35266d0e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,7 +1,6 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; -import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; @@ -18,8 +17,6 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import java.util.concurrent.atomic.AtomicReference; - public final class Device { public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; @@ -32,17 +29,8 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - private final AtomicReference positionMapper = new AtomicReference<>(); // set by the ScreenCapture instance - - public Point getPhysicalPoint(Position position) { - // it hides the field on purpose, to read it with atomic access - @SuppressWarnings("checkstyle:HiddenField") - PositionMapper positionMapper = this.positionMapper.get(); - if (positionMapper == null) { - return null; - } - - return positionMapper.map(position); + private Device() { + // not instantiable } public static String getDeviceName() { @@ -54,10 +42,6 @@ public final class Device { return displayId == 0 || Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; } - public void setPositionMapper(PositionMapper positionMapper) { - this.positionMapper.set(positionMapper); - } - public static boolean injectEvent(InputEvent inputEvent, int displayId, int injectMode) { if (!supportsInputEvents(displayId)) { throw new AssertionError("Could not inject input event if !supportsInputEvents()"); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 066c9ae4..95faaf39 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -3,7 +3,6 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.ConfigurationException; -import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; @@ -21,8 +20,7 @@ import android.view.Surface; public class ScreenCapture extends SurfaceCapture { - private final Device device; - + private final VirtualDisplayListener vdListener; private final int displayId; private int maxSize; private final Rect crop; @@ -37,8 +35,8 @@ public class ScreenCapture extends SurfaceCapture { private IRotationWatcher rotationWatcher; private IDisplayFoldListener displayFoldListener; - public ScreenCapture(Device device, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { - this.device = device; + public ScreenCapture(VirtualDisplayListener vdListener, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { + this.vdListener = vdListener; this.displayId = displayId; this.maxSize = maxSize; this.crop = crop; @@ -133,8 +131,10 @@ public class ScreenCapture extends SurfaceCapture { } } - PositionMapper positionMapper = PositionMapper.from(screenInfo); - device.setPositionMapper(positionMapper); + if (vdListener != null) { + PositionMapper positionMapper = PositionMapper.from(screenInfo); + vdListener.onNewVirtualDisplay(positionMapper); + } } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java new file mode 100644 index 00000000..d978361e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java @@ -0,0 +1,7 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.control.PositionMapper; + +public interface VirtualDisplayListener { + void onNewVirtualDisplay(PositionMapper positionMapper); +} From d19396718ee0c0ba7fb578f595a6553c0458da59 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 048/278] Inject display-related events to virtual display Mouse and touch events must be sent to the virtual display id (used for mirroring), other events (like key events) must be sent to the original display id. Fixes #4598 Fixes #5137 PR #5370 Co-authored-by: nightmare --- .../genymobile/scrcpy/control/Controller.java | 74 ++++++++++++------- .../scrcpy/video/ScreenCapture.java | 11 ++- .../scrcpy/video/VirtualDisplayListener.java | 2 +- 3 files changed, 56 insertions(+), 31 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index ac870f27..5175ed5e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -17,7 +17,6 @@ import android.content.Intent; import android.os.Build; import android.os.SystemClock; import android.view.InputDevice; -import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; @@ -31,6 +30,28 @@ import java.util.concurrent.atomic.AtomicReference; public class Controller implements AsyncProcessor, VirtualDisplayListener { + /* + * For event injection, there are two display ids: + * - the displayId passed to the constructor (which comes from --display-id passed by the client, 0 for the main display); + * - the virtualDisplayId used for mirroring, notified by the capture instance via the VirtualDisplayListener interface. + * + * (In case the ScreenCapture uses the "SurfaceControl API", then both ids are equals, but this is an implementation detail.) + * + * In order to make events work correctly in all cases: + * - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates); + * - displayId must be used for other events (like key events). + */ + + private static final class DisplayData { + private final int virtualDisplayId; + private final PositionMapper positionMapper; + + private DisplayData(int virtualDisplayId, PositionMapper positionMapper) { + this.virtualDisplayId = virtualDisplayId; + this.positionMapper = positionMapper; + } + } + private static final int DEFAULT_DEVICE_ID = 0; // control_msg.h values of the pointerId field in inject_touch_event message @@ -54,7 +75,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); - private final AtomicReference positionMapper = new AtomicReference<>(); + private final AtomicReference displayData = new AtomicReference<>(); private long lastTouchDown; private final PointersState pointersState = new PointersState(); @@ -102,8 +123,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } @Override - public void onNewVirtualDisplay(PositionMapper positionMapper) { - this.positionMapper.set(positionMapper); + public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) { + DisplayData data = new DisplayData(virtualDisplayId, positionMapper); + this.displayData.set(data); } private UhidManager getUhidManager() { @@ -284,7 +306,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return false; } for (KeyEvent event : events) { - if (!injectEvent(event, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(event, displayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -306,7 +328,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { long now = SystemClock.uptimeMillis(); - Point point = getPhysicalPoint(position); + // it hides the field on purpose, to read it with atomic access + @SuppressWarnings("checkstyle:HiddenField") + DisplayData displayData = this.displayData.get(); + assert displayData != null : "Cannot receive a touch event without a display"; + + Point point = displayData.positionMapper.map(position); if (point == null) { Ln.w("Ignore touch event, it was generated for a different device size"); return false; @@ -365,7 +392,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // First button pressed: ACTION_DOWN MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!injectEvent(downEvent, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(downEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -376,7 +403,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(pressEvent, actionButton)) { return false; } - if (!injectEvent(pressEvent, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(pressEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } @@ -390,7 +417,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(releaseEvent, actionButton)) { return false; } - if (!injectEvent(releaseEvent, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(releaseEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } @@ -398,7 +425,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // Last button released: ACTION_UP MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!injectEvent(upEvent, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(upEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -409,12 +436,18 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - return injectEvent(event, Device.INJECT_MODE_ASYNC); + return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { long now = SystemClock.uptimeMillis(); - Point point = getPhysicalPoint(position); + + // it hides the field on purpose, to read it with atomic access + @SuppressWarnings("checkstyle:HiddenField") + DisplayData displayData = this.displayData.get(); + assert displayData != null : "Cannot receive a scroll event without a display"; + + Point point = displayData.positionMapper.map(position); if (point == null) { Ln.w("Ignore scroll event, it was generated for a different device size"); return false; @@ -431,18 +464,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); - return injectEvent(event, Device.INJECT_MODE_ASYNC); - } - - private Point getPhysicalPoint(Position position) { - // it hides the field on purpose, to read it with atomic access - @SuppressWarnings("checkstyle:HiddenField") - PositionMapper positionMapper = this.positionMapper.get(); - if (positionMapper == null) { - return null; - } - - return positionMapper.map(position); + return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); } /** @@ -520,10 +542,6 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { ServiceManager.getActivityManager().startActivity(intent); } - private boolean injectEvent(InputEvent event, int injectMode) { - return Device.injectEvent(event, displayId, injectMode); - } - private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { return Device.injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 95faaf39..7e516909 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -106,10 +106,16 @@ public class ScreenCapture extends SurfaceCapture { virtualDisplay = null; } + int virtualDisplayId; + PositionMapper positionMapper; try { Size videoSize = screenInfo.getVideoSize(); virtualDisplay = ServiceManager.getDisplayManager() .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + Rect contentRect = new Rect(0, 0, videoSize.getWidth(), videoSize.getHeight()); + // The position are relative to the virtual display, not the original display + positionMapper = new PositionMapper(videoSize, contentRect, 0); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -123,6 +129,8 @@ public class ScreenCapture extends SurfaceCapture { int layerStack = displayInfo.getLayerStack(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + virtualDisplayId = displayId; + positionMapper = PositionMapper.from(screenInfo); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -132,8 +140,7 @@ public class ScreenCapture extends SurfaceCapture { } if (vdListener != null) { - PositionMapper positionMapper = PositionMapper.from(screenInfo); - vdListener.onNewVirtualDisplay(positionMapper); + vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java index d978361e..c079265e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VirtualDisplayListener.java @@ -3,5 +3,5 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.control.PositionMapper; public interface VirtualDisplayListener { - void onNewVirtualDisplay(PositionMapper positionMapper); + void onNewVirtualDisplay(int displayId, PositionMapper positionMapper); } From 5d0e012a4c198633df8c578e626e2160d148d3d6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 049/278] Add DPI to DisplayInfo It will be useful to automatically set an appropriate DPI for new virtual displays. PR #5370 --- .../java/com/genymobile/scrcpy/device/DisplayInfo.java | 8 +++++++- .../com/genymobile/scrcpy/wrappers/DisplayManager.java | 10 ++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index 2973710d..cdd4bab9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -6,15 +6,17 @@ public final class DisplayInfo { private final int rotation; private final int layerStack; private final int flags; + private final int dpi; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; - public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags) { + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; + this.dpi = dpi; } public int getDisplayId() { @@ -36,5 +38,9 @@ public final class DisplayInfo { public int getFlags() { return flags; } + + public int getDpi() { + return dpi; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 00a39274..b91b7146 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -39,7 +39,7 @@ public final class DisplayManager { public static DisplayInfo parseDisplayInfo(String dumpsysDisplayOutput, int displayId) { Pattern regex = Pattern.compile( "^ mOverrideDisplayInfo=DisplayInfo\\{\".*?, displayId " + displayId + ".*?(, FLAG_.*)?, real ([0-9]+) x ([0-9]+).*?, " - + "rotation ([0-9]+).*?, layerStack ([0-9]+)", + + "rotation ([0-9]+).*?, density ([0-9]+).*?, layerStack ([0-9]+)", Pattern.MULTILINE); Matcher m = regex.matcher(dumpsysDisplayOutput); if (!m.find()) { @@ -49,9 +49,10 @@ public final class DisplayManager { int width = Integer.parseInt(m.group(2)); int height = Integer.parseInt(m.group(3)); int rotation = Integer.parseInt(m.group(4)); - int layerStack = Integer.parseInt(m.group(5)); + int density = Integer.parseInt(m.group(5)); + int layerStack = Integer.parseInt(m.group(6)); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -98,7 +99,8 @@ public final class DisplayManager { int rotation = cls.getDeclaredField("rotation").getInt(displayInfo); int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags); + int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } From 98ed5eb643553b6886babfd083931bef4fb4a5b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 050/278] Add virtual display feature Add a feature to create a new (separate) virtual display instead of mirroring the device screen: scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display # use the main display size and density scrcpy --new-display -m1920 # scaled to fit a max size of 1920 scrcpy --new-display=/240 # use the main display size and 240 dpi Fixes #1887 PR #5370 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> Co-authored-by: anirudhb --- app/data/bash-completion/scrcpy | 2 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 12 ++ app/src/cli.c | 43 ++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 4 + app/src/server.h | 1 + .../java/com/genymobile/scrcpy/CleanUp.java | 6 +- .../java/com/genymobile/scrcpy/Options.java | 42 +++++ .../java/com/genymobile/scrcpy/Server.java | 18 ++- .../genymobile/scrcpy/control/Controller.java | 31 +++- .../com/genymobile/scrcpy/device/Device.java | 8 + .../genymobile/scrcpy/device/NewDisplay.java | 31 ++++ .../com/genymobile/scrcpy/device/Size.java | 4 + .../scrcpy/video/NewDisplayCapture.java | 146 ++++++++++++++++++ .../genymobile/scrcpy/video/ScreenInfo.java | 2 +- .../scrcpy/wrappers/DisplayManager.java | 11 ++ 19 files changed, 353 insertions(+), 12 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index db825ecc..4f40d466 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -46,6 +46,8 @@ _scrcpy() { --mouse-bind= -n --no-control -N --no-playback + --new-display + --new-display= --no-audio --no-audio-playback --no-cleanup diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index fa0fa84f..f65430e0 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -52,6 +52,7 @@ arguments=( '--mouse-bind=[Configure bindings of secondary clicks]' {-n,--no-control}'[Disable device control \(mirror the device in read only\)]' {-N,--no-playback}'[Disable video and audio playback]' + '--new-display=[Create a new display]' '--no-audio[Disable audio forwarding]' '--no-audio-playback[Disable audio playback]' '--no-cleanup[Disable device cleanup actions on exit]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 3fd3eb29..8a0d09aa 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -314,6 +314,18 @@ Disable device control (mirror the device in read\-only). .B \-N, \-\-no\-playback Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video\-playback \-\-no\-audio\-playback\fR). +.TP +\fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] +Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI, and \fB\-\-max\-size\fR is considered. + +Examples: + + \-\-new\-display=1920x1080 + \-\-new\-display=1920x1080/420 + \-\-new\-display # main display size and density + \-\-new\-display -m1920 # scaled to fit a max size of 1920 + \-\-new\-display=/240 # main display size and 240 dpi + .TP .B \-\-no\-audio Disable audio forwarding. diff --git a/app/src/cli.c b/app/src/cli.c index 4fc3c534..88477c00 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -102,6 +102,7 @@ enum { OPT_NO_MOUSE_HOVER, OPT_AUDIO_DUP, OPT_GAMEPAD, + OPT_NEW_DISPLAY, }; struct sc_option { @@ -557,6 +558,21 @@ static const struct sc_option options[] = { .text = "Disable video and audio playback on the computer (equivalent " "to --no-video-playback --no-audio-playback).", }, + { + .longopt_id = OPT_NEW_DISPLAY, + .longopt = "new-display", + .argdesc = "[x][/]", + .optional_arg = true, + .text = "Create a new display with the specified resolution and " + "density. If not provided, they default to the main display " + "dimensions and DPI, and --max-size is considered.\n" + "Examples:\n" + " --new-display=1920x1080\n" + " --new-display=1920x1080/420 # force 420 dpi\n" + " --new-display # main display size and density\n" + " --new-display -m1920 # scaled to fit a max size of 1920\n" + " --new-display=/240 # main display size and 240 dpi", + }, { .longopt_id = OPT_NO_AUDIO, .longopt = "no-audio", @@ -2668,6 +2684,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_NEW_DISPLAY: + opts->new_display = optarg ? optarg : ""; + break; default: // getopt prints the error message on stderr return false; @@ -2848,6 +2867,25 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->new_display) { + if (opts->video_source != SC_VIDEO_SOURCE_DISPLAY) { + LOGE("--new-display is only available with --video-source=display"); + return false; + } + + if (!opts->video) { + LOGE("--new-display is incompatible with --no-video"); + return false; + } + + if (opts->max_size && opts->new_display[0] != '\0' + && opts->new_display[0] != '/') { + // An explicit size is defined (not "" nor "/") + LOGE("Cannot specify both --new-display size and -m/--max-size"); + return false; + } + } + if (otg) { if (!opts->control) { LOGE("--no-control is not allowed in OTG mode"); @@ -2954,6 +2992,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_id != 0 && opts->new_display) { + LOGE("Cannot specify both --display-id and --new-display"); + return false; + } + if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { // Select the audio source according to the video source if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { diff --git a/app/src/options.c b/app/src/options.c index f8448792..62fcd925 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -103,6 +103,7 @@ const struct scrcpy_options scrcpy_options_default = { .window = true, .mouse_hover = true, .audio_dup = false, + .new_display = NULL, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 5f6726e0..f3d27a88 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -308,6 +308,7 @@ struct scrcpy_options { bool window; bool mouse_hover; bool audio_dup; + const char *new_display; // [x][/] parsed by the server }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 854657fb..502498ad 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -431,6 +431,7 @@ scrcpy(struct scrcpy_options *options) { .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, + .new_display = options->new_display, .video = options->video, .audio = options->audio, .audio_dup = options->audio_dup, diff --git a/app/src/server.c b/app/src/server.c index b7f3b56d..26725fa0 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -355,6 +355,10 @@ execute_server(struct sc_server *server, // By default, power_on is true ADD_PARAM("power_on=false"); } + if (params->new_display) { + VALIDATE_STRING(params->new_display); + ADD_PARAM("new_display=%s", params->new_display); + } if (params->list & SC_OPTION_LIST_ENCODERS) { ADD_PARAM("list_encoders=true"); } diff --git a/app/src/server.h b/app/src/server.h index d9d42582..4ff5539d 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -48,6 +48,7 @@ struct sc_server_params { int8_t lock_video_orientation; bool control; uint32_t display_id; + const char *new_display; bool video; bool audio; bool audio_dup; diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 1b8d4248..a47aae90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -139,8 +139,10 @@ public final class CleanUp { if (Device.isScreenOn()) { if (powerOffScreen) { - Ln.i("Power off screen"); - Device.powerOffScreen(displayId); + if (displayId != Device.DISPLAY_ID_NONE) { + Ln.i("Power off screen"); + Device.powerOffScreen(displayId); + } } else if (restoreNormalPowerMode) { Ln.i("Restoring normal power mode"); Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 9eab1d90..a4b9d28b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; +import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.Ln; @@ -54,6 +55,8 @@ public class Options { private boolean cleanup = true; private boolean powerOn = true; + private NewDisplay newDisplay; + private boolean listEncoders; private boolean listDisplays; private boolean listCameras; @@ -205,6 +208,10 @@ public class Options { return powerOn; } + public NewDisplay getNewDisplay() { + return newDisplay; + } + public boolean getList() { return listEncoders || listDisplays || listCameras || listCameraSizes; } @@ -418,6 +425,9 @@ public class Options { case "camera_high_speed": options.cameraHighSpeed = Boolean.parseBoolean(value); break; + case "new_display": + options.newDisplay = parseNewDisplay(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -504,4 +514,36 @@ public class Options { throw new IllegalArgumentException("Invalid float value for " + key + ": \"" + value + "\""); } } + + private static NewDisplay parseNewDisplay(String newDisplay) { + // Possible inputs: + // - "" (empty string) + // - "x/" + // - "x" + // - "/" + if (newDisplay.isEmpty()) { + return new NewDisplay(); + } + + String[] tokens = newDisplay.split("/"); + + Size size; + if (!tokens[0].isEmpty()) { + size = parseSize(tokens[0]); + } else { + size = null; + } + + int dpi; + if (tokens.length >= 2) { + dpi = Integer.parseInt(tokens[1]); + if (dpi <= 0) { + throw new IllegalArgumentException("Invalid non-positive dpi: " + tokens[1]); + } + } else { + dpi = 0; + } + + return new NewDisplay(size, dpi); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 91e7ce6c..fd854e06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -12,12 +12,14 @@ import com.genymobile.scrcpy.control.Controller; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.DesktopConnection; import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.video.CameraCapture; +import com.genymobile.scrcpy.video.NewDisplayCapture; import com.genymobile.scrcpy.video.ScreenCapture; import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; @@ -128,8 +130,11 @@ public final class Server { CleanUp cleanUp = null; Thread initThread = null; + NewDisplay newDisplay = options.getNewDisplay(); + int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE; + if (options.getCleanup()) { - cleanUp = CleanUp.configure(options.getDisplayId()); + cleanUp = CleanUp.configure(displayId); initThread = startInitThread(options, cleanUp); } @@ -154,7 +159,7 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); - controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); asyncProcessors.add(controller); } @@ -184,8 +189,13 @@ public final class Server { options.getSendFrameMeta()); SurfaceCapture surfaceCapture; if (options.getVideoSource() == VideoSource.DISPLAY) { - surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(), - options.getLockVideoOrientation()); + if (newDisplay != null) { + surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize()); + } else { + assert displayId != Device.DISPLAY_ID_NONE; + surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(), + options.getLockVideoOrientation()); + } } else { surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed()); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 5175ed5e..1bc4c692 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -40,6 +40,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { * In order to make events work correctly in all cases: * - virtualDisplayId must be used for events relative to the display (mouse and touch events with coordinates); * - displayId must be used for other events (like key events). + * + * If a new separate virtual display is created (using --new-display), then displayId == Device.DISPLAY_ID_NONE. In that case, all events are + * sent to the virtual display id. */ private static final class DisplayData { @@ -151,7 +154,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private void control() throws IOException { // on start, power on the device - if (powerOn && !Device.isScreenOn()) { + if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) { Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); // dirty hack @@ -270,7 +273,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } break; case ControlMessage.TYPE_ROTATE_DEVICE: - Device.rotateDevice(displayId); + Device.rotateDevice(getActionDisplayId()); break; case ControlMessage.TYPE_UHID_CREATE: getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); @@ -305,8 +308,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (events == null) { return false; } + + int actionDisplayId = getActionDisplayId(); for (KeyEvent event : events) { - if (!Device.injectEvent(event, displayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(event, actionDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -543,10 +548,26 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private boolean injectKeyEvent(int action, int keyCode, int repeat, int metaState, int injectMode) { - return Device.injectKeyEvent(action, keyCode, repeat, metaState, displayId, injectMode); + return Device.injectKeyEvent(action, keyCode, repeat, metaState, getActionDisplayId(), injectMode); } private boolean pressReleaseKeycode(int keyCode, int injectMode) { - return Device.pressReleaseKeycode(keyCode, displayId, injectMode); + return Device.pressReleaseKeycode(keyCode, getActionDisplayId(), injectMode); + } + + private int getActionDisplayId() { + if (displayId != Device.DISPLAY_ID_NONE) { + // Real screen mirrored, use the source display id + return displayId; + } + + // Virtual display created by --new-display, use the virtualDisplayId + DisplayData data = displayData.get(); + if (data == null) { + // If no virtual display id is initialized yet, use the main display id + return 0; + } + + return data.virtualDisplayId; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 35266d0e..1cf9aae5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -19,6 +19,8 @@ import android.view.KeyEvent; public final class Device { + public static final int DISPLAY_ID_NONE = -1; + public static final int POWER_MODE_OFF = SurfaceControl.POWER_MODE_OFF; public static final int POWER_MODE_NORMAL = SurfaceControl.POWER_MODE_NORMAL; @@ -159,6 +161,8 @@ public final class Device { } public static boolean powerOffScreen(int displayId) { + assert displayId != DISPLAY_ID_NONE; + if (!isScreenOn()) { return true; } @@ -169,6 +173,8 @@ public final class Device { * Disable auto-rotation (if enabled), set the screen rotation and re-enable auto-rotation (if it was enabled). */ public static void rotateDevice(int displayId) { + assert displayId != DISPLAY_ID_NONE; + WindowManager wm = ServiceManager.getWindowManager(); boolean accelerometerRotation = !wm.isRotationFrozen(displayId); @@ -187,6 +193,8 @@ public final class Device { } private static int getCurrentRotation(int displayId) { + assert displayId != DISPLAY_ID_NONE; + if (displayId == 0) { return ServiceManager.getWindowManager().getRotation(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java new file mode 100644 index 00000000..3aa2996a --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/device/NewDisplay.java @@ -0,0 +1,31 @@ +package com.genymobile.scrcpy.device; + +public final class NewDisplay { + private Size size; + private int dpi; + + public NewDisplay() { + // Auto size and dpi + } + + public NewDisplay(Size size, int dpi) { + this.size = size; + this.dpi = dpi; + } + + public Size getSize() { + return size; + } + + public int getDpi() { + return dpi; + } + + public boolean hasExplicitSize() { + return size != null; + } + + public boolean hasExplicitDpi() { + return dpi != 0; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index bc9dce1c..230fd29e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -21,6 +21,10 @@ public final class Size { return height; } + public int getMax() { + return Math.max(width, height); + } + public Size rotate() { return new Size(height, width); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java new file mode 100644 index 00000000..8f507fdf --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -0,0 +1,146 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.control.PositionMapper; +import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.device.NewDisplay; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.graphics.Rect; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.os.Build; +import android.view.Surface; + +public class NewDisplayCapture extends SurfaceCapture { + + // Internal fields copied from android.hardware.display.DisplayManager + private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; + private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; + private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; + private static final int VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS = 1 << 9; + private static final int VIRTUAL_DISPLAY_FLAG_TRUSTED = 1 << 10; + private static final int VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP = 1 << 11; + private static final int VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED = 1 << 12; + private static final int VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED = 1 << 13; + private static final int VIRTUAL_DISPLAY_FLAG_OWN_FOCUS = 1 << 14; + private static final int VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP = 1 << 15; + + private final VirtualDisplayListener vdListener; + private final NewDisplay newDisplay; + + private Size mainDisplaySize; + private int mainDisplayDpi; + private int maxSize; // only used if newDisplay.getSize() != null + + private VirtualDisplay virtualDisplay; + private Size size; + private int dpi; + + public NewDisplayCapture(VirtualDisplayListener vdListener, NewDisplay newDisplay, int maxSize) { + this.vdListener = vdListener; + this.newDisplay = newDisplay; + this.maxSize = maxSize; + } + + @Override + public void init() { + size = newDisplay.getSize(); + dpi = newDisplay.getDpi(); + if (size == null || dpi == 0) { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); + if (displayInfo != null) { + mainDisplaySize = displayInfo.getSize(); + mainDisplayDpi = displayInfo.getDpi(); + } else { + Ln.w("Main display not found, fallback to 1920x1080 240dpi"); + mainDisplaySize = new Size(1920, 1080); + mainDisplayDpi = 240; + } + } + } + + @Override + public void prepare() { + if (!newDisplay.hasExplicitSize()) { + size = ScreenInfo.computeVideoSize(mainDisplaySize.getWidth(), mainDisplaySize.getHeight(), maxSize); + } + if (!newDisplay.hasExplicitDpi()) { + dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); + } + } + + @Override + public void start(Surface surface) { + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + + int virtualDisplayId; + try { + int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC + | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH + | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT + | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL + | VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) { + flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED + | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP + | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED + | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS + | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; + } + } + virtualDisplay = ServiceManager.getDisplayManager() + .createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags); + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + } catch (Exception e) { + Ln.e("Could not create display", e); + throw new AssertionError("Could not create display"); + } + + if (vdListener != null) { + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight()); + PositionMapper positionMapper = new PositionMapper(size, contentRect, 0); + vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); + } + } + + @Override + public void release() { + if (virtualDisplay != null) { + virtualDisplay.release(); + virtualDisplay = null; + } + } + + @Override + public synchronized Size getSize() { + return size; + } + + @Override + public synchronized boolean setMaxSize(int newMaxSize) { + if (newDisplay.hasExplicitSize()) { + // Cannot retry with a different size if the display size was explicitly provided + return false; + } + + maxSize = newMaxSize; + return true; + } + + private static int scaleDpi(Size initialSize, int initialDpi, Size size) { + int den = initialSize.getMax(); + int num = size.getMax(); + return initialDpi * num / den; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index 1f74ce34..cc82a654 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -90,7 +90,7 @@ public final class ScreenInfo { return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; } - private static Size computeVideoSize(int w, int h, int maxSize) { + public static Size computeVideoSize(int w, int h, int maxSize) { // Compute the video size and the padding of the content inside this video. // Principle: // - scale down the great side of the screen to maxSize (if necessary); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index b91b7146..c8c405bb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,15 +1,18 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Command; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.content.Context; import android.hardware.display.VirtualDisplay; import android.view.Display; import android.view.Surface; +import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.util.regex.Matcher; @@ -126,4 +129,12 @@ public final class DisplayManager { Method method = getCreateVirtualDisplayMethod(); return (VirtualDisplay) method.invoke(null, name, width, height, displayIdToMirror, surface); } + + public VirtualDisplay createNewVirtualDisplay(String name, int width, int height, int dpi, Surface surface, int flags) throws Exception { + Constructor ctor = android.hardware.display.DisplayManager.class.getDeclaredConstructor( + Context.class); + ctor.setAccessible(true); + android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); + return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); + } } From 408a388fc5115483248c3444062d52565fd1f906 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 12 Oct 2024 09:23:31 +0200 Subject: [PATCH 051/278] Reject --new-display for Android <= 10 Fail explicitly if a new virtual display is requested on an Android version lower than 10. PR #5370 --- server/src/main/java/com/genymobile/scrcpy/Server.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index fd854e06..68f3c4ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -127,6 +127,11 @@ public final class Server { throw new ConfigurationException("Camera mirroring is not supported"); } + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) { + Ln.e("New virtual display is not supported before Android 10"); + throw new ConfigurationException("New virtual display is not supported"); + } + CleanUp cleanUp = null; Thread initThread = null; From 9c9d92fb1c75dcf7631ef1408dfdce7a01c29db4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Oct 2024 17:16:08 +0200 Subject: [PATCH 052/278] Add --list-apps Add an option to list all apps installed on the device: scrcpy --list-apps PR #5370 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++ app/src/cli.c | 9 ++++ app/src/options.h | 1 + app/src/server.c | 3 ++ .../java/com/genymobile/scrcpy/Options.java | 10 +++- .../java/com/genymobile/scrcpy/Server.java | 5 ++ .../com/genymobile/scrcpy/device/Device.java | 41 ++++++++++++++ .../genymobile/scrcpy/device/DeviceApp.java | 26 +++++++++ .../com/genymobile/scrcpy/util/LogUtils.java | 54 +++++++++++++++++++ 11 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 4f40d466..f37da13a 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -33,6 +33,7 @@ _scrcpy() { --keyboard= --kill-adb-on-close --legacy-paste + --list-apps --list-camera-sizes --list-cameras --list-displays diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index f65430e0..3f25b88d 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -40,6 +40,7 @@ arguments=( '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' + '--list-apps[List Android apps installed on the device]' '--list-camera-sizes[List the valid camera capture sizes]' '--list-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 8a0d09aa..258c125d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -227,6 +227,10 @@ Inject computer clipboard text as a sequence of key events on Ctrl+v (like MOD+S This is a workaround for some devices not behaving as expected when setting the device clipboard programmatically. +.TP +.B \-\-list\-apps +List Android apps installed on the device. + .TP .B \-\-list\-camera\-sizes List the valid camera capture sizes. diff --git a/app/src/cli.c b/app/src/cli.c index 88477c00..ac58364e 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -103,6 +103,7 @@ enum { OPT_AUDIO_DUP, OPT_GAMEPAD, OPT_NEW_DISPLAY, + OPT_LIST_APPS, }; struct sc_option { @@ -443,6 +444,11 @@ static const struct sc_option options[] = { "This is a workaround for some devices not behaving as " "expected when setting the device clipboard programmatically.", }, + { + .longopt_id = OPT_LIST_APPS, + .longopt = "list-apps", + .text = "List Android apps installed on the device.", + }, { .longopt_id = OPT_LIST_CAMERAS, .longopt = "list-cameras", @@ -2611,6 +2617,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_LIST_CAMERA_SIZES: opts->list |= SC_OPTION_LIST_CAMERA_SIZES; break; + case OPT_LIST_APPS: + opts->list |= SC_OPTION_LIST_APPS; + break; case OPT_REQUIRE_AUDIO: opts->require_audio = true; break; diff --git a/app/src/options.h b/app/src/options.h index f3d27a88..7cbe2e5b 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -304,6 +304,7 @@ struct scrcpy_options { #define SC_OPTION_LIST_DISPLAYS 0x2 #define SC_OPTION_LIST_CAMERAS 0x4 #define SC_OPTION_LIST_CAMERA_SIZES 0x8 +#define SC_OPTION_LIST_APPS 0x10 uint8_t list; bool window; bool mouse_hover; diff --git a/app/src/server.c b/app/src/server.c index 26725fa0..167582e4 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -371,6 +371,9 @@ execute_server(struct sc_server *server, if (params->list & SC_OPTION_LIST_CAMERA_SIZES) { ADD_PARAM("list_camera_sizes=true"); } + if (params->list & SC_OPTION_LIST_APPS) { + ADD_PARAM("list_apps=true"); + } #undef ADD_PARAM diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index a4b9d28b..65702b42 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -61,6 +61,7 @@ public class Options { private boolean listDisplays; private boolean listCameras; private boolean listCameraSizes; + private boolean listApps; // Options not used by the scrcpy client, but useful to use scrcpy-server directly private boolean sendDeviceMeta = true; // send device name and size @@ -213,7 +214,7 @@ public class Options { } public boolean getList() { - return listEncoders || listDisplays || listCameras || listCameraSizes; + return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; } public boolean getListEncoders() { @@ -232,6 +233,10 @@ public class Options { return listCameraSizes; } + public boolean getListApps() { + return listApps; + } + public boolean getSendDeviceMeta() { return sendDeviceMeta; } @@ -395,6 +400,9 @@ public class Options { case "list_camera_sizes": options.listCameraSizes = Boolean.parseBoolean(value); break; + case "list_apps": + options.listApps = Boolean.parseBoolean(value); + break; case "camera_id": if (!value.isEmpty()) { options.cameraId = value; diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 68f3c4ee..35d317f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -292,6 +292,11 @@ public final class Server { Workarounds.apply(); Ln.i(LogUtils.buildCameraListMessage(options.getListCameraSizes())); } + if (options.getListApps()) { + Workarounds.apply(); + Ln.i("Processing Android apps... (this may take some time)"); + Ln.i(LogUtils.buildAppListMessage()); + } // Just print the requested data, do not mirror return; } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 1cf9aae5..f7931141 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; @@ -9,6 +10,10 @@ import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import com.genymobile.scrcpy.wrappers.WindowManager; +import android.annotation.SuppressLint; +import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; @@ -17,6 +22,9 @@ import android.view.InputEvent; import android.view.KeyCharacterMap; import android.view.KeyEvent; +import java.util.ArrayList; +import java.util.List; + public final class Device { public static final int DISPLAY_ID_NONE = -1; @@ -202,4 +210,37 @@ public final class Device { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(displayId); return displayInfo.getRotation(); } + + public static List listApps() { + List apps = new ArrayList<>(); + PackageManager pm = FakeContext.get().getPackageManager(); + for (ApplicationInfo appInfo : getLaunchableApps(pm)) { + String name = pm.getApplicationLabel(appInfo).toString(); + boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + apps.add(new DeviceApp(appInfo.packageName, name, system)); + } + + return apps; + } + + @SuppressLint("QueryPermissionsNeeded") + private static List getLaunchableApps(PackageManager pm) { + List result = new ArrayList<>(); + for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { + if (appInfo.enabled && getLaunchIntent(pm, appInfo.packageName) != null) { + result.add(appInfo); + } + } + + return result; + } + + public static Intent getLaunchIntent(PackageManager pm, String packageName) { + Intent launchIntent = pm.getLaunchIntentForPackage(packageName); + if (launchIntent != null) { + return launchIntent; + } + + return pm.getLeanbackLaunchIntentForPackage(packageName); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java new file mode 100644 index 00000000..ed292efa --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/device/DeviceApp.java @@ -0,0 +1,26 @@ +package com.genymobile.scrcpy.device; + +public final class DeviceApp { + + private final String packageName; + private final String name; + private final boolean system; + + public DeviceApp(String packageName, String name, boolean system) { + this.packageName = packageName; + this.name = name; + this.system = system; + } + + public String getPackageName() { + return packageName; + } + + public String getName() { + return name; + } + + public boolean isSystem() { + return system; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 45ab4eba..e25f140c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,10 +1,13 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.annotation.SuppressLint; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; @@ -13,7 +16,9 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; import android.util.Range; +import java.util.Collections; import java.util.List; +import java.util.Objects; import java.util.SortedSet; import java.util.TreeSet; @@ -154,4 +159,53 @@ public final class LogUtils { } return set; } + + @SuppressLint("QueryPermissionsNeeded") + public static String buildAppListMessage() { + StringBuilder builder = new StringBuilder("List of apps:"); + + List apps = Device.listApps(); + + // Sort by: + // 1. system flag (system apps are before non-system apps) + // 2. name + // 3. package name + // Comparator.comparing() was introduced in API 24, so it cannot be used here to simplify the code + Collections.sort(apps, (thisApp, otherApp) -> { + // System apps first + int cmp = -Boolean.compare(thisApp.isSystem(), otherApp.isSystem()); + if (cmp != 0) { + return cmp; + } + + cmp = Objects.compare(thisApp.getName(), otherApp.getName(), String::compareTo); + if (cmp != 0) { + return cmp; + } + + return Objects.compare(thisApp.getPackageName(), otherApp.getPackageName(), String::compareTo); + }); + + final int column = 30; + for (DeviceApp app : apps) { + String name = app.getName(); + int padding = column - name.length(); + builder.append("\n "); + if (app.isSystem()) { + builder.append("* "); + } else { + builder.append("- "); + + } + builder.append(name); + if (padding > 0) { + builder.append(String.format("%" + padding + "s", " ")); + } else { + builder.append("\n ").append(String.format("%" + column + "s", " ")); + } + builder.append(" [").append(app.getPackageName()).append(']'); + } + + return builder.toString(); + } } From 13ce277e1f1eec6312350c5a3a3ac3be7b9be6e1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Oct 2024 18:19:10 +0200 Subject: [PATCH 053/278] Add --start-app Add a command line option --start-app=name to start an Android app by its package name. For example: scrcpy --start-app=org.mozilla.firefox The app will be started on the correct target display: scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc PR #5370 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 + app/src/cli.c | 14 ++++ app/src/control_msg.c | 10 +++ app/src/control_msg.h | 4 + app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 19 +++++ .../scrcpy/control/ControlMessage.java | 8 ++ .../scrcpy/control/ControlMessageReader.java | 7 ++ .../genymobile/scrcpy/control/Controller.java | 79 ++++++++++++++++++- .../com/genymobile/scrcpy/device/Device.java | 48 ++++++++++- .../com/genymobile/scrcpy/util/LogUtils.java | 10 ++- .../scrcpy/wrappers/ActivityManager.java | 8 +- .../control/ControlMessageReaderTest.java | 21 +++++ 16 files changed, 227 insertions(+), 9 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f37da13a..223c5264 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -79,6 +79,7 @@ _scrcpy() { -s --serial= -S --turn-screen-off --shortcut-mod= + --start-app= -t --show-touches --tcpip --tcpip= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 3f25b88d..8d1189c0 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -82,6 +82,7 @@ arguments=( {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' {-S,--turn-screen-off}'[Turn the device screen off immediately]' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' + '--start-app=[Start an Android app]' {-t,--show-touches}'[Show physical touches]' '--tcpip[\(optional \[ip\:port\]\) Configure and connect the device over TCP/IP]' '--time-limit=[Set the maximum mirroring time, in seconds]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 258c125d..35abd0d1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -494,6 +494,10 @@ For example, to use either LCtrl or LSuper for scrcpy shortcuts, pass "lctrl,lsu Default is "lalt,lsuper" (left-Alt or left-Super). +.TP +.BI "\-\-start\-app " name +Start an Android app, by its exact package name. + .TP .B \-t, \-\-show\-touches Enable "show touches" on start, restore the initial value on exit. diff --git a/app/src/cli.c b/app/src/cli.c index ac58364e..ba272393 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -104,6 +104,7 @@ enum { OPT_GAMEPAD, OPT_NEW_DISPLAY, OPT_LIST_APPS, + OPT_START_APP, }; struct sc_option { @@ -806,6 +807,12 @@ static const struct sc_option options[] = { "shortcuts, pass \"lctrl,lsuper\".\n" "Default is \"lalt,lsuper\" (left-Alt or left-Super).", }, + { + .longopt_id = OPT_START_APP, + .longopt = "start-app", + .argdesc = "name", + .text = "Start an Android app, by its exact package name.", + }, { .shortopt = 't', .longopt = "show-touches", @@ -2696,6 +2703,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NEW_DISPLAY: opts->new_display = optarg ? optarg : ""; break; + case OPT_START_APP: + opts->start_app = optarg; + break; default: // getopt prints the error message on stderr return false; @@ -3138,6 +3148,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("Cannot request power off on close if control is disabled"); return false; } + if (opts->start_app) { + LOGE("Cannot start an Android app if control is disabled"); + return false; + } } # ifdef _WIN32 diff --git a/app/src/control_msg.c b/app/src/control_msg.c index d599b62d..a71bf445 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -183,6 +183,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_UHID_DESTROY: sc_write16be(&buf[1], msg->uhid_destroy.id); return 3; + case SC_CONTROL_MSG_TYPE_START_APP: { + size_t len = write_string_tiny(&buf[1], msg->start_app.name, 255); + return 1 + len; + } case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: case SC_CONTROL_MSG_TYPE_EXPAND_SETTINGS_PANEL: case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: @@ -308,6 +312,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: LOG_CMSG("open hard keyboard settings"); break; + case SC_CONTROL_MSG_TYPE_START_APP: + LOG_CMSG("start app \"%s\"", msg->start_app.name); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; @@ -333,6 +340,9 @@ sc_control_msg_destroy(struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_SET_CLIPBOARD: free(msg->set_clipboard.text); break; + case SC_CONTROL_MSG_TYPE_START_APP: + free(msg->start_app.name); + break; default: // do nothing break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 1ae8cae4..a809a154 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -41,6 +41,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_UHID_INPUT, SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, + SC_CONTROL_MSG_TYPE_START_APP, }; enum sc_screen_power_mode { @@ -110,6 +111,9 @@ struct sc_control_msg { struct { uint16_t id; } uhid_destroy; + struct { + char *name; + } start_app; }; }; diff --git a/app/src/options.c b/app/src/options.c index 62fcd925..8106ce3d 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -104,6 +104,7 @@ const struct scrcpy_options scrcpy_options_default = { .mouse_hover = true, .audio_dup = false, .new_display = NULL, + .start_app = NULL, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 7cbe2e5b..ec5e71ea 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -310,6 +310,7 @@ struct scrcpy_options { bool mouse_hover; bool audio_dup; const char *new_display; // [x][/] parsed by the server + const char *start_app; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 502498ad..64a2fa10 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -907,6 +907,25 @@ aoa_complete: init_sdl_gamepads(); } + if (options->control && options->start_app) { + assert(controller); + + char *name = strdup(options->start_app); + if (!name) { + LOG_OOM(); + goto end; + } + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_START_APP; + msg.start_app.name = name; + + if (!sc_controller_push_msg(controller, &msg)) { + LOGW("Could not request start app '%s'", name); + free(name); + } + } + ret = event_loop(s); terminate_event_loop(); LOGD("quit..."); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index d1406ed0..36dbd03a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -23,6 +23,7 @@ public final class ControlMessage { public static final int TYPE_UHID_INPUT = 13; public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; + public static final int TYPE_START_APP = 16; public static final long SEQUENCE_INVALID = 0; @@ -155,6 +156,13 @@ public final class ControlMessage { return msg; } + public static ControlMessage createStartApp(String name) { + ControlMessage msg = new ControlMessage(); + msg.type = TYPE_START_APP; + msg.text = name; + return msg; + } + public int getType() { return type; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index 17e121c2..eb5dc787 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -53,6 +53,8 @@ public class ControlMessageReader { return parseUhidInput(); case ControlMessage.TYPE_UHID_DESTROY: return parseUhidDestroy(); + case ControlMessage.TYPE_START_APP: + return parseStartApp(); default: throw new ControlProtocolException("Unknown event type: " + type); } @@ -155,6 +157,11 @@ public class ControlMessageReader { return ControlMessage.createUhidDestroy(id); } + private ControlMessage parseStartApp() throws IOException { + String name = parseString(1); + return ControlMessage.createStartApp(name); + } + private Position parsePosition() throws IOException { int x = dis.readInt(); int y = dis.readInt(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 1bc4c692..ccdb85e1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; @@ -22,6 +23,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; @@ -61,6 +63,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private static final int POINTER_ID_MOUSE = -1; private static final ScheduledExecutorService EXECUTOR = Executors.newSingleThreadScheduledExecutor(); + private ExecutorService startAppExecutor; private Thread thread; @@ -79,6 +82,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private final AtomicBoolean isSettingClipboard = new AtomicBoolean(); private final AtomicReference displayData = new AtomicReference<>(); + private final Object displayDataAvailable = new Object(); // condition variable private long lastTouchDown; private final PointersState pointersState = new PointersState(); @@ -128,7 +132,13 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { @Override public void onNewVirtualDisplay(int virtualDisplayId, PositionMapper positionMapper) { DisplayData data = new DisplayData(virtualDisplayId, positionMapper); - this.displayData.set(data); + DisplayData old = this.displayData.getAndSet(data); + if (old == null) { + // The very first time the Controller is notified of a new virtual display + synchronized (displayDataAvailable) { + displayDataAvailable.notify(); + } + } } private UhidManager getUhidManager() { @@ -287,6 +297,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: openHardKeyboardSettings(); break; + case ControlMessage.TYPE_START_APP: + startAppAsync(msg.getText()); + break; default: // do nothing } @@ -570,4 +583,68 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return data.virtualDisplayId; } + + private void startAppAsync(String name) { + if (startAppExecutor == null) { + startAppExecutor = Executors.newSingleThreadExecutor(); + } + + // Listing and selecting the app may take a lot of time + startAppExecutor.submit(() -> startApp(name)); + } + + private void startApp(String name) { + DeviceApp app = Device.findByPackageName(name); + if (app == null) { + Ln.w("No app found for package \"" + name + "\""); + return; + } + + int startAppDisplayId = getStartAppDisplayId(); + if (startAppDisplayId == Device.DISPLAY_ID_NONE) { + Ln.e("No known display id to start app \"" + name + "\""); + return; + } + + Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "..."); + Device.startApp(app.getPackageName(), startAppDisplayId); + } + + private int getStartAppDisplayId() { + if (displayId != Device.DISPLAY_ID_NONE) { + return displayId; + } + + // Mirroring a new virtual display id (using --new-display-id feature) + try { + // Wait for at most 1 second until a virtual display id is known + DisplayData data = waitDisplayData(1000); + if (data != null) { + return data.virtualDisplayId; + } + } catch (InterruptedException e) { + // do nothing + } + + // No display id available + return Device.DISPLAY_ID_NONE; + } + + private DisplayData waitDisplayData(long timeoutMillis) throws InterruptedException { + long deadline = System.currentTimeMillis() + timeoutMillis; + + synchronized (displayDataAvailable) { + DisplayData data = displayData.get(); + while (data == null) { + long timeout = deadline - System.currentTimeMillis(); + if (timeout < 0) { + return null; + } + displayDataAvailable.wait(timeout); + data = displayData.get(); + } + + return data; + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index f7931141..496865e4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.device; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.ActivityManager; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; @@ -12,9 +13,11 @@ import com.genymobile.scrcpy.wrappers.WindowManager; import android.annotation.SuppressLint; import android.content.Intent; +import android.app.ActivityOptions; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager; import android.os.Build; +import android.os.Bundle; import android.os.IBinder; import android.os.SystemClock; import android.view.InputDevice; @@ -215,9 +218,7 @@ public final class Device { List apps = new ArrayList<>(); PackageManager pm = FakeContext.get().getPackageManager(); for (ApplicationInfo appInfo : getLaunchableApps(pm)) { - String name = pm.getApplicationLabel(appInfo).toString(); - boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; - apps.add(new DeviceApp(appInfo.packageName, name, system)); + apps.add(toApp(pm, appInfo)); } return apps; @@ -243,4 +244,45 @@ public final class Device { return pm.getLeanbackLaunchIntentForPackage(packageName); } + + private static DeviceApp toApp(PackageManager pm, ApplicationInfo appInfo) { + String name = pm.getApplicationLabel(appInfo).toString(); + boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + return new DeviceApp(appInfo.packageName, name, system); + } + + @SuppressLint("QueryPermissionsNeeded") + public static DeviceApp findByPackageName(String packageName) { + PackageManager pm = FakeContext.get().getPackageManager(); + // No need to filter by "launchable" apps, an error will be reported on start if the app is not launchable + for (ApplicationInfo appInfo : pm.getInstalledApplications(PackageManager.GET_META_DATA)) { + if (packageName.equals(appInfo.packageName)) { + return toApp(pm, appInfo); + } + } + + return null; + } + + public static void startApp(String packageName, int displayId) { + PackageManager pm = FakeContext.get().getPackageManager(); + + Intent launchIntent = getLaunchIntent(pm, packageName); + if (launchIntent == null) { + Ln.w("Cannot create launch intent for app " + packageName); + return; + } + + launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + + Bundle options = null; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_26_ANDROID_8_0) { + ActivityOptions launchOptions = ActivityOptions.makeBasic(); + launchOptions.setLaunchDisplayId(displayId); + options = launchOptions.toBundle(); + } + + ActivityManager am = ServiceManager.getActivityManager(); + am.startActivity(launchIntent, options); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index e25f140c..6b813135 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -160,11 +160,15 @@ public final class LogUtils { return set; } - @SuppressLint("QueryPermissionsNeeded") - public static String buildAppListMessage() { - StringBuilder builder = new StringBuilder("List of apps:"); + public static String buildAppListMessage() { List apps = Device.listApps(); + return buildAppListMessage("List of apps:", apps); + } + + @SuppressLint("QueryPermissionsNeeded") + public static String buildAppListMessage(String title, List apps) { + StringBuilder builder = new StringBuilder(title); // Sort by: // 1. system flag (system apps are before non-system apps) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index c907e12f..f052dee0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -118,8 +118,12 @@ public final class ActivityManager { return startActivityAsUserMethod; } - @SuppressWarnings("ConstantConditions") public int startActivity(Intent intent) { + return startActivity(intent, null); + } + + @SuppressWarnings("ConstantConditions") + public int startActivity(Intent intent, Bundle options) { try { Method method = getStartActivityAsUserMethod(); return (int) method.invoke( @@ -133,7 +137,7 @@ public final class ActivityManager { /* requestCode */ 0, /* startFlags */ 0, /* profilerInfo */ null, - /* bOptions */ null, + /* bOptions */ options, /* userId */ /* UserHandle.USER_CURRENT */ -2); } catch (Throwable e) { Ln.e("Could not invoke method", e); diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index f29be2f4..d8489fc3 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -399,6 +399,27 @@ public class ControlMessageReaderTest { Assert.assertEquals(-1, bis.read()); // EOS } + @Test + public void testParseStartApp() throws IOException { + byte[] name = "firefox".getBytes(StandardCharsets.UTF_8); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + DataOutputStream dos = new DataOutputStream(bos); + dos.writeByte(ControlMessage.TYPE_START_APP); + dos.writeByte(name.length); + dos.write(name); + byte[] packet = bos.toByteArray(); + + ByteArrayInputStream bis = new ByteArrayInputStream(packet); + ControlMessageReader reader = new ControlMessageReader(bis); + + ControlMessage event = reader.read(); + Assert.assertEquals(ControlMessage.TYPE_START_APP, event.getType()); + Assert.assertEquals("firefox", event.getText()); + + Assert.assertEquals(-1, bis.read()); // EOS + } + @Test public void testMultiEvents() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); From dd20efa41cd7d1f2220e38a45e0d7413d19f5c09 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 19 Oct 2024 19:01:54 +0200 Subject: [PATCH 054/278] Add option to force-stop app before starting The previous commit introduced: scrcpy --start-app=name By adding a '+' prefix, the app is stopped beforehand: scrcpy --start-app=+name This may be useful to start a fresh app on a new virtual display: scrcpy --new-display --start-app=+org.mozilla.firefox PR #5370 --- app/scrcpy.1 | 4 ++++ app/src/cli.c | 4 +++- .../java/com/genymobile/scrcpy/control/Controller.java | 7 ++++++- .../src/main/java/com/genymobile/scrcpy/device/Device.java | 5 ++++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 35abd0d1..802dab5e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -498,6 +498,10 @@ Default is "lalt,lsuper" (left-Alt or left-Super). .BI "\-\-start\-app " name Start an Android app, by its exact package name. +Add a '+' prefix to force-stop before starting the app: + + scrcpy --new-display --start-app=+org.mozilla.firefox + .TP .B \-t, \-\-show\-touches Enable "show touches" on start, restore the initial value on exit. diff --git a/app/src/cli.c b/app/src/cli.c index ba272393..d715a385 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -811,7 +811,9 @@ static const struct sc_option options[] = { .longopt_id = OPT_START_APP, .longopt = "start-app", .argdesc = "name", - .text = "Start an Android app, by its exact package name.", + .text = "Start an Android app, by its exact package name.\n" + "Add a '+' prefix to force-stop before starting the app:\n" + " scrcpy --new-display --start-app=+org.mozilla.firefox", }, { .shortopt = 't', diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index ccdb85e1..b3ab34c3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -594,6 +594,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private void startApp(String name) { + boolean forceStopBeforeStart = name.startsWith("+"); + if (forceStopBeforeStart) { + name = name.substring(1); + } + DeviceApp app = Device.findByPackageName(name); if (app == null) { Ln.w("No app found for package \"" + name + "\""); @@ -607,7 +612,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } Ln.i("Starting app \"" + app.getName() + "\" [" + app.getPackageName() + "] on display " + startAppDisplayId + "..."); - Device.startApp(app.getPackageName(), startAppDisplayId); + Device.startApp(app.getPackageName(), startAppDisplayId, forceStopBeforeStart); } private int getStartAppDisplayId() { diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 496865e4..f51a433e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -264,7 +264,7 @@ public final class Device { return null; } - public static void startApp(String packageName, int displayId) { + public static void startApp(String packageName, int displayId, boolean forceStop) { PackageManager pm = FakeContext.get().getPackageManager(); Intent launchIntent = getLaunchIntent(pm, packageName); @@ -283,6 +283,9 @@ public final class Device { } ActivityManager am = ServiceManager.getActivityManager(); + if (forceStop) { + am.forceStopPackage(packageName); + } am.startActivity(launchIntent, options); } } From 566b5be0f69dae6b98f5df849a75d709ef3bd1c2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 15:49:25 +0200 Subject: [PATCH 055/278] Add option to start an app by its name By adding the '?' prefix, the app is searched by its name instead of its package name (retrieving app names on the device may take some time): scrcpy --start-app=?firefox An app matches if its label starts with the given name, case-insensitive. If '+' is also passed to force-stop the app before starting, then the prefixes must be in that order: scrcpy --start-app=+?firefox PR #5370 --- app/scrcpy.1 | 8 +++++ app/src/cli.c | 8 ++++- .../genymobile/scrcpy/control/Controller.java | 31 ++++++++++++++++--- .../com/genymobile/scrcpy/device/Device.java | 18 +++++++++++ 4 files changed, 60 insertions(+), 5 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 802dab5e..1b81d05e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -498,10 +498,18 @@ Default is "lalt,lsuper" (left-Alt or left-Super). .BI "\-\-start\-app " name Start an Android app, by its exact package name. +Add a '?' prefix to select an app whose name starts with the given name, case-insensitive (retrieving app names on the device may take some time): + + scrcpy --start-app=?firefox + Add a '+' prefix to force-stop before starting the app: scrcpy --new-display --start-app=+org.mozilla.firefox +Both prefixes can be used, in that order: + + scrcpy --start-app=+?firefox + .TP .B \-t, \-\-show\-touches Enable "show touches" on start, restore the initial value on exit. diff --git a/app/src/cli.c b/app/src/cli.c index d715a385..4b9be5d8 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -812,8 +812,14 @@ static const struct sc_option options[] = { .longopt = "start-app", .argdesc = "name", .text = "Start an Android app, by its exact package name.\n" + "Add a '?' prefix to select an app whose name starts with the " + "given name, case-insensitive (retrieving app names on the " + "device may take some time):\n" + " scrcpy --start-app=?firefox\n" "Add a '+' prefix to force-stop before starting the app:\n" - " scrcpy --new-display --start-app=+org.mozilla.firefox", + " scrcpy --new-display --start-app=+org.mozilla.firefox\n" + "Both prefixes can be used, in that order:\n" + " scrcpy --start-app=+?firefox", }, { .shortopt = 't', diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b3ab34c3..0fdb6064 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -8,6 +8,7 @@ import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; @@ -23,6 +24,7 @@ import android.view.KeyEvent; import android.view.MotionEvent; import java.io.IOException; +import java.util.List; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; @@ -599,10 +601,31 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { name = name.substring(1); } - DeviceApp app = Device.findByPackageName(name); - if (app == null) { - Ln.w("No app found for package \"" + name + "\""); - return; + DeviceApp app; + boolean searchByName = name.startsWith("?"); + if (searchByName) { + name = name.substring(1); + + Ln.i("Processing Android apps... (this may take some time)"); + List apps = Device.findByName(name); + if (apps.isEmpty()) { + Ln.w("No app found for name \"" + name + "\""); + return; + } + + if (apps.size() > 1) { + String title = "No unique app found for name \"" + name + "\":"; + Ln.w(LogUtils.buildAppListMessage(title, apps)); + return; + } + + app = apps.get(0); + } else { + app = Device.findByPackageName(name); + if (app == null) { + Ln.w("No app found for package \"" + name + "\""); + return; + } } int startAppDisplayId = getStartAppDisplayId(); diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index f51a433e..a2699076 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -27,6 +27,7 @@ import android.view.KeyEvent; import java.util.ArrayList; import java.util.List; +import java.util.Locale; public final class Device { @@ -264,6 +265,23 @@ public final class Device { return null; } + @SuppressLint("QueryPermissionsNeeded") + public static List findByName(String searchName) { + List result = new ArrayList<>(); + searchName = searchName.toLowerCase(Locale.getDefault()); + + PackageManager pm = FakeContext.get().getPackageManager(); + for (ApplicationInfo appInfo : getLaunchableApps(pm)) { + String name = pm.getApplicationLabel(appInfo).toString(); + if (name.toLowerCase(Locale.getDefault()).startsWith(searchName)) { + boolean system = (appInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0; + result.add(new DeviceApp(appInfo.packageName, name, system)); + } + } + + return result; + } + public static void startApp(String packageName, int displayId, boolean forceStop) { PackageManager pm = FakeContext.get().getPackageManager(); From 381fe95867fb51cfd463f798e2ddbc122e4884b3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 23 Oct 2024 18:52:35 +0200 Subject: [PATCH 056/278] Document virtual display and "start app" features PR #5370 --- README.md | 8 ++++++++ doc/device.md | 45 ++++++++++++++++++++++++++++++++++++++++++ doc/virtual_display.md | 26 ++++++++++++++++++++++++ 3 files changed, 79 insertions(+) create mode 100644 doc/virtual_display.md diff --git a/README.md b/README.md index 44f3d740..6e4e513a 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ It focuses on: Its features include: - [audio forwarding](doc/audio.md) (Android 11+) - [recording](doc/recording.md) + - [virtual display](doc/virtual_display.md) - mirroring with [Android device screen off](doc/device.md#turn-screen-off) - [copy-paste](doc/control.md#copy-paste) in both directions - [configurable quality](doc/video.md) @@ -91,6 +92,12 @@ Here are just some common examples. scrcpy --video-codec=h265 -m1920 --max-fps=60 --no-audio -K # short version ``` + - Start VLC in a new virtual display (separate from the device display): + + ```bash + scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc + ``` + - Record the device camera in H.265 at 1920x1080 (and microphone) to an MP4 file: @@ -134,6 +141,7 @@ documented in the following pages: - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) + - [Virtual display](doc/virtual_displays.md) - [Tunnels](doc/tunnels.md) - [OTG](doc/otg.md) - [Camera](doc/camera.md) diff --git a/doc/device.md b/doc/device.md index 988ad417..ee86c359 100644 --- a/doc/device.md +++ b/doc/device.md @@ -78,3 +78,48 @@ By default, on start, the device is powered on. To prevent this behavior: ```bash scrcpy --no-power-on ``` + + +## Start Android app + +To list the Android apps installed on the device: + +```bash +scrcpy --list-apps +``` + +An app, selected by its package name, can be launched on start: + +``` +scrcpy --start-app=org.mozilla.firefox +``` + +This feature can be used to run an app in a [virtual +display](virtual_display.md): + +``` +scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc +``` + +The app can be optionally forced-stop before being started, by adding a `+` +prefix: + +``` +scrcpy --start-app=+org.mozilla.firefox +``` + +For convenience, it is also possible to select an app by its name, by adding a +`?` prefix: + +``` +scrcpy --start-app=?firefox +``` + +But retrieving app names may take some time (sometimes several seconds), so +passing the package name is recommended. + +The `+` and `?` prefixes can be combined (in that order): + +``` +scrcpy --start-app=+?firefox +``` diff --git a/doc/virtual_display.md b/doc/virtual_display.md new file mode 100644 index 00000000..4ed5961f --- /dev/null +++ b/doc/virtual_display.md @@ -0,0 +1,26 @@ +# Virtual display + +## New display + +To mirror a new virtual display instead of the device screen: + +```bash +scrcpy --new-display=1920x1080 +scrcpy --new-display=1920x1080/420 # force 420 dpi +scrcpy --new-display # use the main display size and density +scrcpy --new-display -m1920 # ... scaled to fit a max size of 1920 +scrcpy --new-display=/240 # use the main display size and 240 dpi +``` + +## Start app + +On some devices, a launcher is available in the virtual display. + +When no launcher is available, the virtual display is empty. In that case, you +must [start an Android app](device.md#start-android-app). + +For example: + +```bash +scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc +``` From ce21f515e318474ca979a38a05814fc748c2bc85 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 18:58:54 +0100 Subject: [PATCH 057/278] Remove unnecessary '\n' in log --- app/src/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index 4b9be5d8..d4caaa89 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2822,7 +2822,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->v4l2_buffer && !opts->v4l2_device) { - LOGE("V4L2 buffer value without V4L2 sink\n"); + LOGE("V4L2 buffer value without V4L2 sink"); return false; } #endif From 2c25fd7a8082307da19645a690c31403903fbb1e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 18:59:29 +0100 Subject: [PATCH 058/278] Disable mouse by default if no video playback If video playback is disabled, then SDK mouse (which uses absolute positions) could not be used, so the default mouse mode was automatically switched to UHID. But UHID does not work on all devices, so it could make the whole scrcpy session fail. Instead, disable the mouse by default. It is still possible to pass -M or --mouse=uhid to enable it explicitly. Fixes #5410 --- app/src/cli.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index d4caaa89..2437d5fc 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2841,8 +2841,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], if (otg) { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_AOA; } else if (!opts->video_playback) { - LOGI("No video mirroring, mouse mode switched to UHID"); - opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_UHID; + LOGI("No video mirroring, SDK mouse disabled"); + opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_DISABLED; } else { opts->mouse_input_mode = SC_MOUSE_INPUT_MODE_SDK; } From 5474ae6bd62f65e6aa25a8685a7e5db9571390ac Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Oct 2024 23:50:54 +0200 Subject: [PATCH 059/278] Factorize codec info listing Make the listing of video and audio encoders share the same code. PR #5416 --- .../genymobile/scrcpy/util/CodecUtils.java | 45 +------------------ .../com/genymobile/scrcpy/util/LogUtils.java | 38 ++++++++-------- 2 files changed, 19 insertions(+), 64 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java index 5b0c95e8..3a01256a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/CodecUtils.java @@ -1,8 +1,5 @@ package com.genymobile.scrcpy.util; -import com.genymobile.scrcpy.audio.AudioCodec; -import com.genymobile.scrcpy.video.VideoCodec; - import android.media.MediaCodecInfo; import android.media.MediaCodecList; import android.media.MediaFormat; @@ -13,24 +10,6 @@ import java.util.List; public final class CodecUtils { - public static final class DeviceEncoder { - private final Codec codec; - private final MediaCodecInfo info; - - DeviceEncoder(Codec codec, MediaCodecInfo info) { - this.codec = codec; - this.info = info; - } - - public Codec getCodec() { - return codec; - } - - public MediaCodecInfo getInfo() { - return info; - } - } - private CodecUtils() { // not instantiable } @@ -47,7 +26,7 @@ public final class CodecUtils { } } - private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { + public static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { List result = new ArrayList<>(); for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) { if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { @@ -56,26 +35,4 @@ public final class CodecUtils { } return result.toArray(new MediaCodecInfo[result.size()]); } - - public static List listVideoEncoders() { - List encoders = new ArrayList<>(); - MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - for (VideoCodec codec : VideoCodec.values()) { - for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { - encoders.add(new DeviceEncoder(codec, info)); - } - } - return encoders; - } - - public static List listAudioEncoders() { - List encoders = new ArrayList<>(); - MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - for (AudioCodec codec : AudioCodec.values()) { - for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { - encoders.add(new DeviceEncoder(codec, info)); - } - } - return encoders; - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 6b813135..f2837f40 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,9 +1,11 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; @@ -14,6 +16,8 @@ import android.hardware.camera2.CameraCharacteristics; import android.hardware.camera2.CameraManager; import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; +import android.media.MediaCodecInfo; +import android.media.MediaCodecList; import android.util.Range; import java.util.Collections; @@ -28,32 +32,26 @@ public final class LogUtils { // not instantiable } - public static String buildVideoEncoderListMessage() { - StringBuilder builder = new StringBuilder("List of video encoders:"); - List videoEncoders = CodecUtils.listVideoEncoders(); - if (videoEncoders.isEmpty()) { - builder.append("\n (none)"); - } else { - for (CodecUtils.DeviceEncoder encoder : videoEncoders) { - builder.append("\n --video-codec=").append(encoder.getCodec().getName()); - builder.append(" --video-encoder=").append(encoder.getInfo().getName()); + private static String buildEncoderListMessage(String type, Codec[] codecs) { + StringBuilder builder = new StringBuilder("List of ").append(type).append(" encoders:"); + MediaCodecList codecList = new MediaCodecList(MediaCodecList.REGULAR_CODECS); + for (Codec codec : codecs) { + MediaCodecInfo[] encoders = CodecUtils.getEncoders(codecList, codec.getMimeType()); + for (MediaCodecInfo info : encoders) { + builder.append("\n --").append(type).append("-codec=").append(codec.getName()); + builder.append(" --").append(type).append("-encoder=").append(info.getName()); } } + return builder.toString(); } + public static String buildVideoEncoderListMessage() { + return buildEncoderListMessage("video", VideoCodec.values()); + } + public static String buildAudioEncoderListMessage() { - StringBuilder builder = new StringBuilder("List of audio encoders:"); - List audioEncoders = CodecUtils.listAudioEncoders(); - if (audioEncoders.isEmpty()) { - builder.append("\n (none)"); - } else { - for (CodecUtils.DeviceEncoder encoder : audioEncoders) { - builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); - builder.append(" --audio-encoder=").append(encoder.getInfo().getName()); - } - } - return builder.toString(); + return buildEncoderListMessage("audio", AudioCodec.values()); } public static String buildDisplayListMessage() { From acff5b005ce9f2efae80adad09d2e77ba3052460 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Oct 2024 23:50:54 +0200 Subject: [PATCH 060/278] Add more details to --list-encoders output Add more information about each codec (hw/sw, vendor, alias). Before: [server] INFO: List of video encoders: --video-codec=h264 --video-encoder=c2.exynos.h264.encoder --video-codec=h264 --video-encoder=c2.android.avc.encoder --video-codec=h264 --video-encoder=OMX.google.h264.encoder --video-codec=h265 --video-encoder=c2.exynos.hevc.encoder --video-codec=h265 --video-encoder=c2.android.hevc.encoder --video-codec=av1 --video-encoder=c2.google.av1.encoder --video-codec=av1 --video-encoder=c2.android.av1.encoder // audio encoders omitted After: [server] INFO: List of video encoders: --video-codec=h264 --video-encoder=c2.exynos.h264.encoder (hw) [vendor] --video-codec=h264 --video-encoder=c2.android.avc.encoder (sw) --video-codec=h264 --video-encoder=OMX.google.h264.encoder (sw) (alias for c2.android.avc.encoder) --video-codec=h265 --video-encoder=c2.exynos.hevc.encoder (hw) [vendor] --video-codec=h265 --video-encoder=c2.android.hevc.encoder (sw) --video-codec=av1 --video-encoder=c2.google.av1.encoder (hw) [vendor] --video-codec=av1 --video-encoder=c2.android.av1.encoder (sw) // audio encoders omitted PR #5416 --- .../com/genymobile/scrcpy/util/LogUtils.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index f2837f40..2b780caf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.util; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; @@ -10,6 +11,7 @@ import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.graphics.Rect; import android.hardware.camera2.CameraAccessException; import android.hardware.camera2.CameraCharacteristics; @@ -18,6 +20,7 @@ import android.hardware.camera2.params.StreamConfigurationMap; import android.media.MediaCodec; import android.media.MediaCodecInfo; import android.media.MediaCodecList; +import android.os.Build; import android.util.Range; import java.util.Collections; @@ -38,8 +41,25 @@ public final class LogUtils { for (Codec codec : codecs) { MediaCodecInfo[] encoders = CodecUtils.getEncoders(codecList, codec.getMimeType()); for (MediaCodecInfo info : encoders) { + int lineStart = builder.length(); builder.append("\n --").append(type).append("-codec=").append(codec.getName()); builder.append(" --").append(type).append("-encoder=").append(info.getName()); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { + int lineLength = builder.length() - lineStart; + final int column = 70; + if (lineLength < column) { + int padding = column - lineLength; + builder.append(String.format("%" + padding + "s", " ")); + } + builder.append(" (").append(getHwCodecType(info)).append(')'); + if (info.isVendor()) { + builder.append(" [vendor]"); + } + if (info.isAlias()) { + builder.append(" (alias for ").append(info.getCanonicalName()).append(')'); + } + } + } } @@ -54,6 +74,17 @@ public final class LogUtils { return buildEncoderListMessage("audio", AudioCodec.values()); } + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private static String getHwCodecType(MediaCodecInfo info) { + if (info.isSoftwareOnly()) { + return "sw"; + } + if (info.isHardwareAccelerated()) { + return "hw"; + } + return "hybrid"; + } + public static String buildDisplayListMessage() { StringBuilder builder = new StringBuilder("List of displays:"); DisplayManager displayManager = ServiceManager.getDisplayManager(); From 58a0fbbf2e0c7912ad487196c2f46ca6c0bd79cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 28 Oct 2024 23:56:17 +0100 Subject: [PATCH 061/278] Refactor display power mode Accept a single boolean "on" rather than a "mode" (which, in practice, could only take 2 values: NORMAL and OFF). Also rename "screen power mode" to "display power". PR #5418 --- app/src/control_msg.c | 21 +++--------- app/src/control_msg.h | 12 ++----- app/src/input_manager.c | 13 +++---- app/src/scrcpy.c | 6 ++-- app/tests/test_control_msg_serialize.c | 14 ++++---- .../java/com/genymobile/scrcpy/CleanUp.java | 18 +++++----- .../scrcpy/control/ControlMessage.java | 18 +++++----- .../scrcpy/control/ControlMessageReader.java | 10 +++--- .../genymobile/scrcpy/control/Controller.java | 34 +++++++++---------- .../com/genymobile/scrcpy/device/Device.java | 6 ++-- .../control/ControlMessageReaderTest.java | 12 +++---- 11 files changed, 71 insertions(+), 93 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index a71bf445..e04fbd3c 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -22,9 +22,6 @@ #define MOTIONEVENT_ACTION_LABEL(value) \ ENUM_TO_LABEL(android_motionevent_action_labels, value) -#define SCREEN_POWER_MODE_LABEL(value) \ - ENUM_TO_LABEL(screen_power_mode_labels, value) - static const char *const android_keyevent_action_labels[] = { "down", "up", @@ -47,14 +44,6 @@ static const char *const android_motionevent_action_labels[] = { "btn-release", }; -static const char *const screen_power_mode_labels[] = { - "off", - "doze", - "normal", - "doze-suspend", - "suspend", -}; - static const char *const copy_key_labels[] = { "none", "copy", @@ -158,8 +147,8 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { size_t len = write_string(&buf[10], msg->set_clipboard.text, SC_CONTROL_MSG_CLIPBOARD_TEXT_MAX_LENGTH); return 10 + len; - case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: - buf[1] = msg->set_screen_power_mode.mode; + case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: + buf[1] = msg->set_display_power.on; return 2; case SC_CONTROL_MSG_TYPE_UHID_CREATE: sc_write16be(&buf[1], msg->uhid_create.id); @@ -268,9 +257,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { msg->set_clipboard.paste ? "paste" : "nopaste", msg->set_clipboard.text); break; - case SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE: - LOG_CMSG("power mode %s", - SCREEN_POWER_MODE_LABEL(msg->set_screen_power_mode.mode)); + case SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER: + LOG_CMSG("display power %s", + msg->set_display_power.on ? "on" : "off"); break; case SC_CONTROL_MSG_TYPE_EXPAND_NOTIFICATION_PANEL: LOG_CMSG("expand notification panel"); diff --git a/app/src/control_msg.h b/app/src/control_msg.h index a809a154..9eef7e82 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -35,7 +35,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS, SC_CONTROL_MSG_TYPE_GET_CLIPBOARD, SC_CONTROL_MSG_TYPE_SET_CLIPBOARD, - SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, + SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, SC_CONTROL_MSG_TYPE_ROTATE_DEVICE, SC_CONTROL_MSG_TYPE_UHID_CREATE, SC_CONTROL_MSG_TYPE_UHID_INPUT, @@ -44,12 +44,6 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_START_APP, }; -enum sc_screen_power_mode { - // see - SC_SCREEN_POWER_MODE_OFF = 0, - SC_SCREEN_POWER_MODE_NORMAL = 2, -}; - enum sc_copy_key { SC_COPY_KEY_NONE, SC_COPY_KEY_COPY, @@ -95,8 +89,8 @@ struct sc_control_msg { bool paste; } set_clipboard; struct { - enum sc_screen_power_mode mode; - } set_screen_power_mode; + bool on; + } set_display_power; struct { uint16_t id; const char *name; // pointer to static data diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 969196e3..140b50ac 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -203,13 +203,12 @@ set_device_clipboard(struct sc_input_manager *im, bool paste, } static void -set_screen_power_mode(struct sc_input_manager *im, - enum sc_screen_power_mode mode) { +set_display_power(struct sc_input_manager *im, bool on) { assert(im->controller); struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; - msg.set_screen_power_mode.mode = mode; + msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; + msg.set_display_power.on = on; if (!sc_controller_push_msg(im->controller, &msg)) { LOGW("Could not request 'set screen power mode'"); @@ -415,10 +414,8 @@ sc_input_manager_process_key(struct sc_input_manager *im, return; case SDLK_o: if (control && !repeat && down && !paused) { - enum sc_screen_power_mode mode = shift - ? SC_SCREEN_POWER_MODE_NORMAL - : SC_SCREEN_POWER_MODE_OFF; - set_screen_power_mode(im, mode); + bool on = shift; + set_display_power(im, on); } return; case SDLK_z: diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 64a2fa10..f0ce1959 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -873,11 +873,11 @@ aoa_complete: // everything is set up if (options->control && options->turn_screen_off) { struct sc_control_msg msg; - msg.type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE; - msg.set_screen_power_mode.mode = SC_SCREEN_POWER_MODE_OFF; + msg.type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER; + msg.set_display_power.on = false; if (!sc_controller_push_msg(&s->controller, &msg)) { - LOGW("Could not request 'set screen power mode'"); + LOGW("Could not request 'set display power'"); } } diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 72ec61ee..73bca901 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -289,11 +289,11 @@ static void test_serialize_set_clipboard_long(void) { assert(!memcmp(buf, expected, sizeof(expected))); } -static void test_serialize_set_screen_power_mode(void) { +static void test_serialize_set_display_power(void) { struct sc_control_msg msg = { - .type = SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, - .set_screen_power_mode = { - .mode = SC_SCREEN_POWER_MODE_NORMAL, + .type = SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, + .set_display_power = { + .on = true, }, }; @@ -302,8 +302,8 @@ static void test_serialize_set_screen_power_mode(void) { assert(size == 2); const uint8_t expected[] = { - SC_CONTROL_MSG_TYPE_SET_SCREEN_POWER_MODE, - 0x02, // SC_SCREEN_POWER_MODE_NORMAL + SC_CONTROL_MSG_TYPE_SET_DISPLAY_POWER, + 0x01, // true }; assert(!memcmp(buf, expected, sizeof(expected))); } @@ -423,7 +423,7 @@ int main(int argc, char *argv[]) { test_serialize_get_clipboard(); test_serialize_set_clipboard(); test_serialize_set_clipboard_long(); - test_serialize_set_screen_power_mode(); + test_serialize_set_display_power(); test_serialize_rotate_device(); test_serialize_uhid_create(); test_serialize_uhid_input(); diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index a47aae90..7fbb6cc6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -19,7 +19,7 @@ public final class CleanUp { private static final int MSG_TYPE_MASK = 0b11; private static final int MSG_TYPE_RESTORE_STAY_ON = 0; private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; - private static final int MSG_TYPE_RESTORE_NORMAL_POWER_MODE = 2; + private static final int MSG_TYPE_RESTORE_DISPLAY_POWER = 2; private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; private static final int MSG_PARAM_SHIFT = 2; @@ -63,8 +63,8 @@ public final class CleanUp { return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0); } - public boolean setRestoreNormalPowerMode(boolean restoreOnExit) { - return sendMessage(MSG_TYPE_RESTORE_NORMAL_POWER_MODE, restoreOnExit ? 1 : 0); + public boolean setRestoreDisplayPower(boolean restoreOnExit) { + return sendMessage(MSG_TYPE_RESTORE_DISPLAY_POWER, restoreOnExit ? 1 : 0); } public boolean setPowerOffScreen(boolean powerOffScreenOnExit) { @@ -86,7 +86,7 @@ public final class CleanUp { int restoreStayOn = -1; boolean disableShowTouches = false; - boolean restoreNormalPowerMode = false; + boolean restoreDisplayPower = false; boolean powerOffScreen = false; try { @@ -102,8 +102,8 @@ public final class CleanUp { case MSG_TYPE_DISABLE_SHOW_TOUCHES: disableShowTouches = param != 0; break; - case MSG_TYPE_RESTORE_NORMAL_POWER_MODE: - restoreNormalPowerMode = param != 0; + case MSG_TYPE_RESTORE_DISPLAY_POWER: + restoreDisplayPower = param != 0; break; case MSG_TYPE_POWER_OFF_SCREEN: powerOffScreen = param != 0; @@ -143,9 +143,9 @@ public final class CleanUp { Ln.i("Power off screen"); Device.powerOffScreen(displayId); } - } else if (restoreNormalPowerMode) { - Ln.i("Restoring normal power mode"); - Device.setScreenPowerMode(Device.POWER_MODE_NORMAL); + } else if (restoreDisplayPower) { + Ln.i("Restoring display power"); + Device.setDisplayPower(true); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index 36dbd03a..eec5f67f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -17,7 +17,7 @@ public final class ControlMessage { public static final int TYPE_COLLAPSE_PANELS = 7; public static final int TYPE_GET_CLIPBOARD = 8; public static final int TYPE_SET_CLIPBOARD = 9; - public static final int TYPE_SET_SCREEN_POWER_MODE = 10; + public static final int TYPE_SET_DISPLAY_POWER = 10; public static final int TYPE_ROTATE_DEVICE = 11; public static final int TYPE_UHID_CREATE = 12; public static final int TYPE_UHID_INPUT = 13; @@ -34,7 +34,7 @@ public final class ControlMessage { private int type; private String text; private int metaState; // KeyEvent.META_* - private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* or POWER_MODE_* + private int action; // KeyEvent.ACTION_* or MotionEvent.ACTION_* private int keycode; // KeyEvent.KEYCODE_* private int actionButton; // MotionEvent.BUTTON_* private int buttons; // MotionEvent.BUTTON_* @@ -49,6 +49,7 @@ public final class ControlMessage { private long sequence; private int id; private byte[] data; + private boolean on; private ControlMessage() { } @@ -116,13 +117,10 @@ public final class ControlMessage { return msg; } - /** - * @param mode one of the {@code Device.SCREEN_POWER_MODE_*} constants - */ - public static ControlMessage createSetScreenPowerMode(int mode) { + public static ControlMessage createSetDisplayPower(boolean on) { ControlMessage msg = new ControlMessage(); - msg.type = TYPE_SET_SCREEN_POWER_MODE; - msg.action = mode; + msg.type = TYPE_SET_DISPLAY_POWER; + msg.on = on; return msg; } @@ -234,4 +232,8 @@ public final class ControlMessage { public byte[] getData() { return data; } + + public boolean getOn() { + return on; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index eb5dc787..ae167690 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -39,8 +39,8 @@ public class ControlMessageReader { return parseGetClipboard(); case ControlMessage.TYPE_SET_CLIPBOARD: return parseSetClipboard(); - case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: - return parseSetScreenPowerMode(); + case ControlMessage.TYPE_SET_DISPLAY_POWER: + return parseSetDisplayPower(); case ControlMessage.TYPE_EXPAND_NOTIFICATION_PANEL: case ControlMessage.TYPE_EXPAND_SETTINGS_PANEL: case ControlMessage.TYPE_COLLAPSE_PANELS: @@ -134,9 +134,9 @@ public class ControlMessageReader { return ControlMessage.createSetClipboard(sequence, text, paste); } - private ControlMessage parseSetScreenPowerMode() throws IOException { - int mode = dis.readUnsignedByte(); - return ControlMessage.createSetScreenPowerMode(mode); + private ControlMessage parseSetDisplayPower() throws IOException { + boolean on = dis.readBoolean(); + return ControlMessage.createSetDisplayPower(on); } private ControlMessage parseUhidCreate() throws IOException { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 0fdb6064..81da1800 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -91,7 +91,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private final MotionEvent.PointerProperties[] pointerProperties = new MotionEvent.PointerProperties[PointersState.MAX_POINTERS]; private final MotionEvent.PointerCoords[] pointerCoords = new MotionEvent.PointerCoords[PointersState.MAX_POINTERS]; - private boolean keepPowerModeOff; + private boolean keepDisplayPowerOff; public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.displayId = displayId; @@ -270,16 +270,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_SET_CLIPBOARD: setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; - case ControlMessage.TYPE_SET_SCREEN_POWER_MODE: + case ControlMessage.TYPE_SET_DISPLAY_POWER: if (supportsInputEvents) { - int mode = msg.getAction(); - boolean setPowerModeOk = Device.setScreenPowerMode(mode); - if (setPowerModeOk) { - keepPowerModeOff = mode == Device.POWER_MODE_OFF; - Ln.i("Device screen turned " + (mode == Device.POWER_MODE_OFF ? "off" : "on")); + boolean on = msg.getOn(); + boolean setDisplayPowerOk = Device.setDisplayPower(on); + if (setDisplayPowerOk) { + keepDisplayPowerOff = !on; + Ln.i("Device display turned " + (on ? "on" : "off")); if (cleanUp != null) { - boolean mustRestoreOnExit = mode != Device.POWER_MODE_NORMAL; - cleanUp.setRestoreNormalPowerMode(mustRestoreOnExit); + boolean mustRestoreOnExit = !on; + cleanUp.setRestoreDisplayPower(mustRestoreOnExit); } } } @@ -310,8 +310,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { - if (keepPowerModeOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { - schedulePowerModeOff(); + if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + scheduleDisplayPowerOff(); } return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } @@ -488,12 +488,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } /** - * Schedule a call to set power mode to off after a small delay. + * Schedule a call to set display power to off after a small delay. */ - private static void schedulePowerModeOff() { + private static void scheduleDisplayPowerOff() { EXECUTOR.schedule(() -> { - Ln.i("Forcing screen off"); - Device.setScreenPowerMode(Device.POWER_MODE_OFF); + Ln.i("Forcing display off"); + Device.setDisplayPower(false); }, 200, TimeUnit.MILLISECONDS); } @@ -509,8 +509,8 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return true; } - if (keepPowerModeOff) { - schedulePowerModeOff(); + if (keepDisplayPowerOff) { + scheduleDisplayPowerOff(); } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index a2699076..cbc1bc81 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -126,10 +126,7 @@ public final class Device { return clipboardManager.setText(text); } - /** - * @param mode one of the {@code POWER_MODE_*} constants - */ - public static boolean setScreenPowerMode(int mode) { + public static boolean setDisplayPower(boolean on) { boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (applyToMultiPhysicalDisplays @@ -142,6 +139,7 @@ public final class Device { applyToMultiPhysicalDisplays = false; } + int mode = on ? POWER_MODE_NORMAL : POWER_MODE_OFF; if (applyToMultiPhysicalDisplays) { // On Android 14, these internal methods have been moved to DisplayControl boolean useDisplayControl = diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index d8489fc3..a25507b4 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -1,7 +1,5 @@ package com.genymobile.scrcpy.control; -import com.genymobile.scrcpy.device.Device; - import android.view.KeyEvent; import android.view.MotionEvent; import org.junit.Assert; @@ -285,19 +283,19 @@ public class ControlMessageReaderTest { } @Test - public void testParseSetScreenPowerMode() throws IOException { + public void testParseSetDisplayPower() throws IOException { ByteArrayOutputStream bos = new ByteArrayOutputStream(); DataOutputStream dos = new DataOutputStream(bos); - dos.writeByte(ControlMessage.TYPE_SET_SCREEN_POWER_MODE); - dos.writeByte(Device.POWER_MODE_NORMAL); + dos.writeByte(ControlMessage.TYPE_SET_DISPLAY_POWER); + dos.writeBoolean(true); byte[] packet = bos.toByteArray(); ByteArrayInputStream bis = new ByteArrayInputStream(packet); ControlMessageReader reader = new ControlMessageReader(bis); ControlMessage event = reader.read(); - Assert.assertEquals(ControlMessage.TYPE_SET_SCREEN_POWER_MODE, event.getType()); - Assert.assertEquals(Device.POWER_MODE_NORMAL, event.getAction()); + Assert.assertEquals(ControlMessage.TYPE_SET_DISPLAY_POWER, event.getType()); + Assert.assertTrue(event.getOn()); Assert.assertEquals(-1, bis.read()); // EOS } From 569c37cec159a67593b01c52aaa4cb92d1826bab Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 00:41:02 +0100 Subject: [PATCH 062/278] Disable display power for virtual displays If displayId == Device.DISPLAY_ID_NONE, then the display is virtual: its power mode cannot be changed. PR #5418 --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 8 +++----- .../java/com/genymobile/scrcpy/control/Controller.java | 4 +++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 7fbb6cc6..f561a10f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -137,12 +137,10 @@ public final class CleanUp { } } - if (Device.isScreenOn()) { + if (Device.isScreenOn() && displayId != Device.DISPLAY_ID_NONE) { if (powerOffScreen) { - if (displayId != Device.DISPLAY_ID_NONE) { - Ln.i("Power off screen"); - Device.powerOffScreen(displayId); - } + Ln.i("Power off screen"); + Device.powerOffScreen(displayId); } else if (restoreDisplayPower) { Ln.i("Restoring display power"); Device.setDisplayPower(true); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 81da1800..fbe0691e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -271,7 +271,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_DISPLAY_POWER: - if (supportsInputEvents) { + if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { boolean on = msg.getOn(); boolean setDisplayPowerOk = Device.setDisplayPower(on); if (setDisplayPowerOk) { @@ -311,6 +311,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { + assert displayId != Device.DISPLAY_ID_NONE; scheduleDisplayPowerOff(); } return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); @@ -510,6 +511,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } if (keepDisplayPowerOff) { + assert displayId != Device.DISPLAY_ID_NONE; scheduleDisplayPowerOff(); } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); From 58ba00fa060c9a1f439120f8869ed106e1c935f9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 00:45:26 +0100 Subject: [PATCH 063/278] Adapt "turn screen off" for Android 15 Android 15 introduced an easy way to set the display power: Refs #3927 Refs PR #5418 --- .../java/com/genymobile/scrcpy/CleanUp.java | 2 +- .../genymobile/scrcpy/control/Controller.java | 10 ++++----- .../com/genymobile/scrcpy/device/Device.java | 8 ++++++- .../scrcpy/wrappers/DisplayManager.java | 21 +++++++++++++++++++ 4 files changed, 34 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f561a10f..c8ee3ef4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -143,7 +143,7 @@ public final class CleanUp { Device.powerOffScreen(displayId); } else if (restoreDisplayPower) { Ln.i("Restoring display power"); - Device.setDisplayPower(true); + Device.setDisplayPower(displayId, true); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index fbe0691e..7add4ea9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -273,7 +273,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_SET_DISPLAY_POWER: if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { boolean on = msg.getOn(); - boolean setDisplayPowerOk = Device.setDisplayPower(on); + boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on); if (setDisplayPowerOk) { keepDisplayPowerOff = !on; Ln.i("Device display turned " + (on ? "on" : "off")); @@ -312,7 +312,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean injectKeycode(int action, int keycode, int repeat, int metaState) { if (keepDisplayPowerOff && action == KeyEvent.ACTION_UP && (keycode == KeyEvent.KEYCODE_POWER || keycode == KeyEvent.KEYCODE_WAKEUP)) { assert displayId != Device.DISPLAY_ID_NONE; - scheduleDisplayPowerOff(); + scheduleDisplayPowerOff(displayId); } return injectKeyEvent(action, keycode, repeat, metaState, Device.INJECT_MODE_ASYNC); } @@ -491,10 +491,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { /** * Schedule a call to set display power to off after a small delay. */ - private static void scheduleDisplayPowerOff() { + private static void scheduleDisplayPowerOff(int displayId) { EXECUTOR.schedule(() -> { Ln.i("Forcing display off"); - Device.setDisplayPower(false); + Device.setDisplayPower(displayId, false); }, 200, TimeUnit.MILLISECONDS); } @@ -512,7 +512,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (keepDisplayPowerOff) { assert displayId != Device.DISPLAY_ID_NONE; - scheduleDisplayPowerOff(); + scheduleDisplayPowerOff(displayId); } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index cbc1bc81..1cf96714 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -126,7 +126,13 @@ public final class Device { return clipboardManager.setText(text); } - public static boolean setDisplayPower(boolean on) { + public static boolean setDisplayPower(int displayId, boolean on) { + assert displayId != Device.DISPLAY_ID_NONE; + + if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { + return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on); + } + boolean applyToMultiPhysicalDisplays = Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10; if (applyToMultiPhysicalDisplays diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index c8c405bb..37d82c33 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; @@ -7,6 +8,7 @@ import com.genymobile.scrcpy.util.Command; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.content.Context; import android.hardware.display.VirtualDisplay; import android.view.Display; @@ -22,6 +24,7 @@ import java.util.regex.Pattern; public final class DisplayManager { private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private Method createVirtualDisplayMethod; + private Method requestDisplayPowerMethod; static DisplayManager create() { try { @@ -137,4 +140,22 @@ public final class DisplayManager { android.hardware.display.DisplayManager dm = ctor.newInstance(FakeContext.get()); return dm.createVirtualDisplay(name, width, height, dpi, surface, flags); } + + private Method getRequestDisplayPowerMethod() throws NoSuchMethodException { + if (requestDisplayPowerMethod == null) { + requestDisplayPowerMethod = manager.getClass().getMethod("requestDisplayPower", int.class, boolean.class); + } + return requestDisplayPowerMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public boolean requestDisplayPower(int displayId, boolean on) { + try { + Method method = getRequestDisplayPowerMethod(); + return (boolean) method.invoke(manager, displayId, on); + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return false; + } + } } From 1f6634ea87b36c2c6fe08af90b983c69294ee74d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 30 Oct 2024 22:23:53 +0100 Subject: [PATCH 064/278] Document adb shell settings commands Some scrcpy features change Android settings with `adb shell settings`. Document the commands to execute manually. --- doc/device.md | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/doc/device.md b/doc/device.md index ee86c359..06210e18 100644 --- a/doc/device.md +++ b/doc/device.md @@ -18,6 +18,21 @@ The initial state is restored when _scrcpy_ is closed. If the device is not plugged in (i.e. only connected over TCP/IP), `--stay-awake` has no effect (this is the Android behavior). +This changes the value of [`stay_on_while_plugged_in`], setting which can be +changed manually: + +[`stay_on_while_plugged_in`]: https://developer.android.com/reference/android/provider/Settings.Global#STAY_ON_WHILE_PLUGGED_IN + + +```bash +# get the current show_touches value +adb shell settings get global stay_on_while_plugged_in +# enable for AC/USB/wireless chargers +adb shell settings put global stay_on_while_plugged_in 7 +# disable +adb shell settings put global stay_on_while_plugged_in 0 +``` + ## Turn screen off @@ -46,6 +61,15 @@ scrcpy --turn-screen-off --stay-awake scrcpy -Sw # short version ``` +Since Android 15, it is possible to change this setting manually: + +``` +# turn screen off (0 for main display) +adb shell cmd display power-off 0 +# turn screen on +adb shell cmd display power-on 0 +``` + ## Show touches @@ -62,6 +86,16 @@ scrcpy -t # short version Note that it only shows _physical_ touches (by a finger on the device). +It is possible to change this setting manually: + +```bash +# get the current show_touches value +adb shell settings get system show_touches +# enable show_touches +adb shell settings put system show_touches 1 +# disable show_touches +adb shell settings put system show_touches 0 +``` ## Power off on close From d62fa8880e03e8823057a5d4d9659d5f19132806 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 28 Oct 2024 23:31:27 +0100 Subject: [PATCH 065/278] Disable broken options on Android 14 The options --lock-video-orientation and --crop are broken since Android 14. Hopefully, they will be reimplemented differently. Meanwhile, when running Android >= 14, fail with an error to prevent incorrect behavior. Refs #4011 Refs #4162 PR #5417 --- app/src/cli.c | 3 ++- app/src/options.h | 2 ++ .../java/com/genymobile/scrcpy/Options.java | 7 ++++++- .../main/java/com/genymobile/scrcpy/Server.java | 17 +++++++++++++++++ .../com/genymobile/scrcpy/device/Device.java | 2 ++ .../com/genymobile/scrcpy/video/ScreenInfo.java | 2 +- 6 files changed, 30 insertions(+), 3 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 2437d5fc..95035836 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2812,7 +2812,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " "See --lock-video-orientation."); - opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; + opts->lock_video_orientation = + SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO; } // V4L2 could not handle size change. diff --git a/app/src/options.h b/app/src/options.h index ec5e71ea..2d458ab4 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -134,6 +134,8 @@ enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, + // like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically + SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3, SC_LOCK_VIDEO_ORIENTATION_0 = 0, SC_LOCK_VIDEO_ORIENTATION_90 = 3, SC_LOCK_VIDEO_ORIENTATION_180 = 2, diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 65702b42..659a6948 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; +import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.CodecOption; @@ -31,7 +32,7 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; - private int lockVideoOrientation = -1; + private int lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -253,6 +254,10 @@ public class Options { return sendCodecMeta; } + public void resetLockVideoOrientation() { + this.lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED; + } + @SuppressWarnings("MethodLength") public static Options parse(String... args) { if (args.length < 1) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 35d317f2..a0a48806 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -132,6 +132,23 @@ public final class Server { throw new ConfigurationException("New virtual display is not supported"); } + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + int lockVideoOrientation = options.getLockVideoOrientation(); + if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { + if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) { + Ln.e("--lock-video-orientation is broken on Android >= 14: "); + throw new ConfigurationException("--lock-video-orientation is broken on Android >= 14"); + } else { + // If the flag has been set automatically (because v4l2 sink is enabled), do not fail + Ln.w("--lock-video-orientation is ignored on Android >= 14: "); + } + } + if (options.getCrop() != null) { + Ln.e("--crop is broken on Android >= 14: "); + throw new ConfigurationException("Crop is not broken on Android >= 14"); + } + } + CleanUp cleanUp = null; Thread initThread = null; diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 1cf96714..5cd7a52a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -42,6 +42,8 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; + // like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically + public static final int LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3; private Device() { // not instantiable diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index cc82a654..602bd8ab 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -64,7 +64,7 @@ public final class ScreenInfo { } public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { - if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL || lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) { // The user requested to lock the video orientation to the current orientation lockedVideoOrientation = rotation; } From c29ecd0314352661e826d8474b5d275d5ac660f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 29 Oct 2024 19:09:01 +0100 Subject: [PATCH 066/278] Rename --display-buffer to --video-buffer For consistency with --audio-buffer, rename --display-buffer to --video-buffer. Fixes #5403 PR #5420 --- app/data/bash-completion/scrcpy | 4 ++-- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 16 +++++++++------- app/src/cli.c | 22 +++++++++++++++++----- app/src/options.c | 2 +- app/src/options.h | 2 +- app/src/scrcpy.c | 12 ++++++------ doc/audio.md | 2 +- doc/develop.md | 6 +++--- doc/video.md | 6 +++--- 10 files changed, 44 insertions(+), 30 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 223c5264..d9a7be60 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -20,7 +20,6 @@ _scrcpy() { --crop= -d --select-usb --disable-screensaver - --display-buffer= --display-id= --display-orientation= -e --select-tcpip @@ -90,6 +89,7 @@ _scrcpy() { --v4l2-sink= -v --version -V --verbosity= + --video-buffer= --video-codec= --video-codec-options= --video-encoder= @@ -191,7 +191,6 @@ _scrcpy() { |--camera-size \ |--crop \ |--display-id \ - |--display-buffer \ |--max-fps \ |-m|--max-size \ |-p|--port \ @@ -201,6 +200,7 @@ _scrcpy() { |--tunnel-port \ |--v4l2-buffer \ |--v4l2-sink \ + |--video-buffer \ |--video-codec-options \ |--video-encoder \ |--tcpip \ diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 8d1189c0..c4e0e60c 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -27,7 +27,6 @@ arguments=( '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' - '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' '--display-id=[Specify the display id to mirror]' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' {-e,--select-tcpip}'[Use TCP/IP device]' @@ -92,6 +91,7 @@ arguments=( '--v4l2-sink=[\[\/dev\/videoN\] Output to v4l2loopback device]' {-v,--version}'[Print the version of scrcpy]' {-V,--verbosity=}'[Set the log level]:verbosity:(verbose debug info warn error)' + '--video-buffer=[Add a buffering delay \(in milliseconds\) before displaying video frames]' '--video-codec=[Select the video codec]:codec:(h264 h265 av1)' '--video-codec-options=[Set a list of comma-separated key\:type=value options for the device video encoder]' '--video-encoder=[Use a specific MediaCodec video encoder]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1b81d05e..f517ad4c 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -139,12 +139,6 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). .BI "\-\-disable\-screensaver" Disable screensaver while scrcpy is running. -.TP -.BI "\-\-display\-buffer " ms -Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. - -Default is 0 (no buffering). - .TP .BI "\-\-display\-id " id Specify the device display id to mirror. @@ -560,7 +554,15 @@ It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\f .BI "\-\-v4l2-buffer " ms Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. -This option is similar to \fB\-\-display\-buffer\fR, but specific to V4L2 sink. +This option is similar to \fB\-\-video\-buffer\fR, but specific to V4L2 sink. + +Default is 0 (no buffering). + +.TP +.BI "\-\-video\-buffer " ms +Add a buffering delay (in milliseconds) before displaying video frames. + +This increases latency to compensate for jitter. Default is 0 (no buffering). diff --git a/app/src/cli.c b/app/src/cli.c index 95035836..77747e93 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -50,6 +50,7 @@ enum { OPT_POWER_OFF_ON_CLOSE, OPT_V4L2_SINK, OPT_DISPLAY_BUFFER, + OPT_VIDEO_BUFFER, OPT_V4L2_BUFFER, OPT_TUNNEL_HOST, OPT_TUNNEL_PORT, @@ -321,12 +322,10 @@ static const struct sc_option options[] = { .argdesc = "id", }, { + // deprecated .longopt_id = OPT_DISPLAY_BUFFER, .longopt = "display-buffer", .argdesc = "ms", - .text = "Add a buffering delay (in milliseconds) before displaying. " - "This increases latency to compensate for jitter.\n" - "Default is 0 (no buffering).", }, { .longopt_id = OPT_DISPLAY_ID, @@ -898,11 +897,20 @@ static const struct sc_option options[] = { .argdesc = "ms", .text = "Add a buffering delay (in milliseconds) before pushing " "frames. This increases latency to compensate for jitter.\n" - "This option is similar to --display-buffer, but specific to " + "This option is similar to --video-buffer, but specific to " "V4L2 sink.\n" "Default is 0 (no buffering).\n" "This option is only available on Linux.", }, + { + .longopt_id = OPT_VIDEO_BUFFER, + .longopt = "video-buffer", + .argdesc = "ms", + .text = "Add a buffering delay (in milliseconds) before displaying " + "video frames.\n" + "This increases latency to compensate for jitter.\n" + "Default is 0 (no buffering).", + }, { .longopt_id = OPT_VIDEO_CODEC, .longopt = "video-codec", @@ -2549,7 +2557,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->power_off_on_close = true; break; case OPT_DISPLAY_BUFFER: - if (!parse_buffering_time(optarg, &opts->display_buffer)) { + LOGW("--display-buffer is deprecated, use --video-buffer " + "instead."); + // fall through + case OPT_VIDEO_BUFFER: + if (!parse_buffering_time(optarg, &opts->video_buffer)) { return false; } break; diff --git a/app/src/options.c b/app/src/options.c index 8106ce3d..b1a3b739 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -58,7 +58,7 @@ const struct scrcpy_options scrcpy_options_default = { .window_width = 0, .window_height = 0, .display_id = 0, - .display_buffer = 0, + .video_buffer = 0, .audio_buffer = -1, // depends on the audio format, .audio_output_buffer = SC_TICK_FROM_MS(5), .time_limit = 0, diff --git a/app/src/options.h b/app/src/options.h index 2d458ab4..d37ac0a2 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -261,7 +261,7 @@ struct scrcpy_options { uint16_t window_width; uint16_t window_height; uint32_t display_id; - sc_tick display_buffer; + sc_tick video_buffer; sc_tick audio_buffer; sc_tick audio_output_buffer; sc_tick time_limit; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f0ce1959..8d135394 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -53,7 +53,7 @@ struct scrcpy { struct sc_decoder video_decoder; struct sc_decoder audio_decoder; struct sc_recorder recorder; - struct sc_delay_buffer display_buffer; + struct sc_delay_buffer video_buffer; #ifdef HAVE_V4L2 struct sc_v4l2_sink v4l2_sink; struct sc_delay_buffer v4l2_buffer; @@ -815,11 +815,11 @@ aoa_complete: if (options->video_playback) { struct sc_frame_source *src = &s->video_decoder.frame_source; - if (options->display_buffer) { - sc_delay_buffer_init(&s->display_buffer, - options->display_buffer, true); - sc_frame_source_add_sink(src, &s->display_buffer.frame_sink); - src = &s->display_buffer.frame_source; + if (options->video_buffer) { + sc_delay_buffer_init(&s->video_buffer, + options->video_buffer, true); + sc_frame_source_add_sink(src, &s->video_buffer.frame_sink); + src = &s->video_buffer.frame_source; } sc_frame_source_add_sink(src, &s->screen.frame_sink); diff --git a/doc/audio.md b/doc/audio.md index 750163e0..85f76ac5 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -170,7 +170,7 @@ latency (for both [video](video.md#buffering) and audio) might be preferable to avoid glitches and smooth the playback: ``` -scrcpy --display-buffer=200 --audio-buffer=200 +scrcpy --video-buffer=200 --audio-buffer=200 ``` It is also possible to configure another audio buffer (the audio output buffer), diff --git a/doc/develop.md b/doc/develop.md index e5274783..a094aa32 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -21,9 +21,9 @@ the client and on the server. If video is enabled, then the server sends a raw video stream (H.264 by default) of the device screen, with some additional headers for each packet. The client decodes the video frames, and displays them as soon as possible, without -buffering (unless `--display-buffer=delay` is specified) to minimize latency. -The client is not aware of the device rotation (which is handled by the server), -it just knows the dimensions of the video frames it receives. +buffering (unless `--video-buffer=delay` is specified) to minimize latency. The +client is not aware of the device rotation (which is handled by the server), it +just knows the dimensions of the video frames it receives. Similarly, if audio is enabled, then the server sends a raw audio stream (OPUS by default) of the device audio output (or the microphone if diff --git a/doc/video.md b/doc/video.md index ed92cb22..74ec74dd 100644 --- a/doc/video.md +++ b/doc/video.md @@ -189,15 +189,15 @@ The configuration is available independently for the display, [v4l2 sinks](video.md#video4linux) and [audio](audio.md#buffering) playback. ```bash -scrcpy --display-buffer=50 # add 50ms buffering for display -scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink +scrcpy --video-buffer=50 # add 50ms buffering for video playback scrcpy --audio-buffer=200 # set 200ms buffering for audio playback +scrcpy --v4l2-buffer=300 # add 300ms buffering for v4l2 sink ``` They can be applied simultaneously: ```bash -scrcpy --display-buffer=50 --v4l2-buffer=300 +scrcpy --video-buffer=50 --v4l2-buffer=300 ``` From 04a3e6fb06377fc75eef805147e55f93877b3e86 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 28 Oct 2024 18:30:52 +0100 Subject: [PATCH 067/278] Consume reset request on encoding start If a reset request is pending when a new encoding starts, then it is implicitly fulfilled. PR #5415 --- .../main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 84bda1ce..6a58d791 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -72,6 +72,7 @@ public class SurfaceEncoder implements AsyncProcessor { boolean headerWritten = false; do { + capture.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled capture.prepare(); Size size = capture.getSize(); if (!headerWritten) { From e26bdb07a21493d096ea5c8cfd870fc5a3f015dc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 21 Oct 2024 18:25:08 +0200 Subject: [PATCH 068/278] Listen to display changed events Replace RotationWatcher and DisplayFoldListener by a single DisplayListener, which is notified whenever the display size or dpi changes. However, the DisplayListener mechanism is broken in the first versions of Android 14 (it is fixed in android-14.0.0_r29 by commit [1]), so continue to use the old mechanism specifically for Android 14 (where DisplayListener may be broken), until we receive the first "display changed" event (which proves that it works). [1]: Fixes #161 Fixes #1918 Fixes #4152 Fixes #5362 comment Refs #4469 PR #5415 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../com/genymobile/scrcpy/device/Size.java | 2 +- .../scrcpy/video/ScreenCapture.java | 170 ++++++++++++++---- .../scrcpy/wrappers/DisplayManager.java | 69 +++++++ 3 files changed, 206 insertions(+), 35 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 230fd29e..558deb00 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -52,6 +52,6 @@ public final class Size { @Override public String toString() { - return "Size{" + "width=" + width + ", height=" + height + '}'; + return "Size{" + width + 'x' + height + '}'; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 7e516909..d190bdde 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -7,12 +7,15 @@ import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.hardware.display.VirtualDisplay; import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; import android.os.IBinder; import android.view.IDisplayFoldListener; import android.view.IRotationWatcher; @@ -29,9 +32,19 @@ public class ScreenCapture extends SurfaceCapture { private DisplayInfo displayInfo; private ScreenInfo screenInfo; + // Source display size (before resizing/crop) for the current session + private Size sessionDisplaySize; + private IBinder display; private VirtualDisplay virtualDisplay; + private DisplayManager.DisplayListenerHandle displayListenerHandle; + private HandlerThread handlerThread; + + // On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really + // detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from + // DisplayListener (which proves that it works). + private boolean displayListenerWorks; // only accessed from the display listener thread private IRotationWatcher rotationWatcher; private IDisplayFoldListener displayFoldListener; @@ -45,39 +58,57 @@ public class ScreenCapture extends SurfaceCapture { @Override public void init() { - if (displayId == 0) { - rotationWatcher = new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - requestReset(); - } - }; - ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + registerDisplayListenerFallbacks(); } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { - displayFoldListener = new IDisplayFoldListener.Stub() { - - private boolean first = true; - - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - if (first) { - // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. - first = false; - return; - } - - if (ScreenCapture.this.displayId != displayId) { - // Ignore events related to other display ids - return; - } - - requestReset(); + handlerThread = new HandlerThread("DisplayListener"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")"); + } + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + if (!displayListenerWorks) { + // On the first display listener event, we know it works, we can unregister the fallbacks + displayListenerWorks = true; + unregisterDisplayListenerFallbacks(); } - }; - ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); - } + } + if (this.displayId == displayId) { + DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (di == null) { + Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); + // We can't compare with the current size, so reset unconditionally + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); + } + setSessionDisplaySize(null); + requestReset(); + } else { + Size size = di.getSize(); + + // The field is hidden on purpose, to read it with synchronization + @SuppressWarnings("checkstyle:HiddenField") + Size sessionDisplaySize = getSessionDisplaySize(); // synchronized + + // .equals() also works if sessionDisplaySize == null + if (!size.equals(sessionDisplaySize)) { + // Reset only if the size is different + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: requestReset(): " + sessionDisplaySize + " -> " + size); + } + // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() + // considers that the current size is the requested size (to avoid a duplicate requestReset()) + setSessionDisplaySize(size); + requestReset(); + } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()"); + } + } + } + }, handler); } @Override @@ -92,6 +123,7 @@ public class ScreenCapture extends SurfaceCapture { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } + setSessionDisplaySize(displayInfo.getSize()); screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation); } @@ -146,12 +178,19 @@ public class ScreenCapture extends SurfaceCapture { @Override public void release() { - if (rotationWatcher != null) { - ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + unregisterDisplayListenerFallbacks(); } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10) { - ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + + handlerThread.quitSafely(); + handlerThread = null; + + // displayListenerHandle may be null if registration failed + if (displayListenerHandle != null) { + ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); + displayListenerHandle = null; } + if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -191,4 +230,67 @@ public class ScreenCapture extends SurfaceCapture { SurfaceControl.closeTransaction(); } } + + private synchronized Size getSessionDisplaySize() { + return sessionDisplaySize; + } + + private synchronized void setSessionDisplaySize(Size sessionDisplaySize) { + this.sessionDisplaySize = sessionDisplaySize; + } + + private void registerDisplayListenerFallbacks() { + if (displayId == 0) { + rotationWatcher = new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); + } + requestReset(); + } + }; + ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); + } + + // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) + displayFoldListener = new IDisplayFoldListener.Stub() { + + private boolean first = true; + + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + if (first) { + // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. + first = false; + return; + } + + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: onDisplayFoldChanged(" + displayId + ", " + folded + ")"); + } + + if (ScreenCapture.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + requestReset(); + } + }; + ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); + } + + private void unregisterDisplayListenerFallbacks() { + synchronized (this) { + if (rotationWatcher != null) { + ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); + rotationWatcher = null; + } + if (displayFoldListener != null) { + // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) + ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + displayFoldListener = null; + } + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 37d82c33..b497e97f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -11,17 +11,40 @@ import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.Context; import android.hardware.display.VirtualDisplay; +import android.os.Handler; import android.view.Display; import android.view.Surface; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.regex.Matcher; import java.util.regex.Pattern; @SuppressLint("PrivateApi,DiscouragedPrivateApi") public final class DisplayManager { + + // android.hardware.display.DisplayManager.EVENT_FLAG_DISPLAY_CHANGED + public static final long EVENT_FLAG_DISPLAY_CHANGED = 1L << 2; + + public interface DisplayListener { + /** + * Called whenever the properties of a logical {@link android.view.Display}, + * such as size and density, have changed. + * + * @param displayId The id of the logical display that changed. + */ + void onDisplayChanged(int displayId); + } + + public static final class DisplayListenerHandle { + private final Object displayListenerProxy; + private DisplayListenerHandle(Object displayListenerProxy) { + this.displayListenerProxy = displayListenerProxy; + } + } + private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal private Method createVirtualDisplayMethod; private Method requestDisplayPowerMethod; @@ -158,4 +181,50 @@ public final class DisplayManager { return false; } } + + public DisplayListenerHandle registerDisplayListener(DisplayListener listener, Handler handler) { + try { + Class displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener"); + Object displayListenerProxy = Proxy.newProxyInstance( + ClassLoader.getSystemClassLoader(), + new Class[] {displayListenerClass}, + (proxy, method, args) -> { + if ("onDisplayChanged".equals(method.getName())) { + listener.onDisplayChanged((int) args[0]); + } + return null; + }); + try { + manager.getClass() + .getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class, String.class) + .invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED, FakeContext.PACKAGE_NAME); + } catch (NoSuchMethodException e) { + try { + manager.getClass() + .getMethod("registerDisplayListener", displayListenerClass, Handler.class, long.class) + .invoke(manager, displayListenerProxy, handler, EVENT_FLAG_DISPLAY_CHANGED); + } catch (NoSuchMethodException e2) { + manager.getClass() + .getMethod("registerDisplayListener", displayListenerClass, Handler.class) + .invoke(manager, displayListenerProxy, handler); + } + } + + return new DisplayListenerHandle(displayListenerProxy); + } catch (Exception e) { + // Rotation and screen size won't be updated, not a fatal error + Ln.e("Could not register display listener", e); + } + + return null; + } + + public void unregisterDisplayListener(DisplayListenerHandle listener) { + try { + Class displayListenerClass = Class.forName("android.hardware.display.DisplayManager$DisplayListener"); + manager.getClass().getMethod("unregisterDisplayListener", displayListenerClass).invoke(manager, listener.displayListenerProxy); + } catch (Exception e) { + Ln.e("Could not unregister display listener", e); + } + } } From c7378f4dc843d24699349812661867c07af3954d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 20:23:11 +0100 Subject: [PATCH 069/278] Extract setting display power to a separate method For consistency with the other actions. --- .../genymobile/scrcpy/control/Controller.java | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 7add4ea9..76d62fa6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -272,16 +272,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { break; case ControlMessage.TYPE_SET_DISPLAY_POWER: if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { - boolean on = msg.getOn(); - boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on); - if (setDisplayPowerOk) { - keepDisplayPowerOff = !on; - Ln.i("Device display turned " + (on ? "on" : "off")); - if (cleanUp != null) { - boolean mustRestoreOnExit = !on; - cleanUp.setRestoreDisplayPower(mustRestoreOnExit); - } - } + setDisplayPower(msg.getOn()); } break; case ControlMessage.TYPE_ROTATE_DEVICE: @@ -677,4 +668,16 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return data; } } + + private void setDisplayPower(boolean on) { + boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on); + if (setDisplayPowerOk) { + keepDisplayPowerOff = !on; + Ln.i("Device display turned " + (on ? "on" : "off")); + if (cleanUp != null) { + boolean mustRestoreOnExit = !on; + cleanUp.setRestoreDisplayPower(mustRestoreOnExit); + } + } + } } From 3ac4b64461716ff472e62ef13b0947dfff98cb2e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Nov 2024 18:40:23 +0100 Subject: [PATCH 070/278] Register rotation watcher for non-main displays While moving code, commit 874eaec487369f7fcaa9ed8c5f85569659565d4f added a condition `if (displayId == 0)` to register a rotation watcher, without good reasons. This condition was kept when the rotation watcher was moved to a fallback in e26bdb07a21493d096ea5c8cfd870fc5a3f015dc. Note: use `git show -b` to show this commit ignoring whitespace changes. Refs #5428 --- .../scrcpy/video/ScreenCapture.java | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index d190bdde..04e42800 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -240,18 +240,16 @@ public class ScreenCapture extends SurfaceCapture { } private void registerDisplayListenerFallbacks() { - if (displayId == 0) { - rotationWatcher = new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); - } - requestReset(); + rotationWatcher = new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); } - }; - ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); - } + requestReset(); + } + }; + ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) displayFoldListener = new IDisplayFoldListener.Stub() { From f08a6d86c5d84aa48c1c197d8f92f322768515cb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 20:19:58 +0100 Subject: [PATCH 071/278] Power on the device only for main display Power on the device on start only if scrcpy is mirroring the main display. --- .../src/main/java/com/genymobile/scrcpy/control/Controller.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 76d62fa6..533faa68 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -166,7 +166,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private void control() throws IOException { // on start, power on the device - if (powerOn && displayId != Device.DISPLAY_ID_NONE && !Device.isScreenOn()) { + if (powerOn && displayId == 0 && !Device.isScreenOn()) { Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); // dirty hack From c905fbba8d383af3a128390a95f26f9ab986486e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 19:01:39 +0100 Subject: [PATCH 072/278] Fix indentation --- .../java/com/genymobile/scrcpy/video/NewDisplayCapture.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 8f507fdf..8c47ba43 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -93,8 +93,8 @@ public class NewDisplayCapture extends SurfaceCapture { | VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED | VIRTUAL_DISPLAY_FLAG_TOUCH_FEEDBACK_DISABLED; if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { - flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS - | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; + flags |= VIRTUAL_DISPLAY_FLAG_OWN_FOCUS + | VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP; } } virtualDisplay = ServiceManager.getDisplayManager() From 1270997f6beb9574dc0ce46e4961f031ca7fb7c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 19:02:57 +0100 Subject: [PATCH 073/278] Remove useless assignment The local variable virtualDisplayId was already initialized to the exact same value. --- .../main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 8c47ba43..5cbdb792 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -107,7 +107,6 @@ public class NewDisplayCapture extends SurfaceCapture { } if (vdListener != null) { - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight()); PositionMapper positionMapper = new PositionMapper(size, contentRect, 0); vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); From 790ea5e58c97a8635ba0326b2b3268ef77bdaae0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 12:33:33 +0100 Subject: [PATCH 074/278] Check screen on for current displayId Since Android 14, the "screen on" state can be checked per-display. Refs PR #5442 --- .../main/java/com/genymobile/scrcpy/CleanUp.java | 2 +- .../com/genymobile/scrcpy/control/Controller.java | 4 ++-- .../java/com/genymobile/scrcpy/device/Device.java | 7 ++++--- .../genymobile/scrcpy/wrappers/PowerManager.java | 14 ++++++++++++-- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index c8ee3ef4..343d854a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -137,7 +137,7 @@ public final class CleanUp { } } - if (Device.isScreenOn() && displayId != Device.DISPLAY_ID_NONE) { + if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) { if (powerOffScreen) { Ln.i("Power off screen"); Device.powerOffScreen(displayId); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 533faa68..67a0115d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -166,7 +166,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private void control() throws IOException { // on start, power on the device - if (powerOn && displayId == 0 && !Device.isScreenOn()) { + if (powerOn && displayId == 0 && !Device.isScreenOn(displayId)) { Device.pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); // dirty hack @@ -490,7 +490,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private boolean pressBackOrTurnScreenOn(int action) { - if (Device.isScreenOn()) { + if (displayId == Device.DISPLAY_ID_NONE || Device.isScreenOn(displayId)) { return injectKeyEvent(action, KeyEvent.KEYCODE_BACK, 0, 0, Device.INJECT_MODE_ASYNC); } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 5cd7a52a..e7a743f8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -82,8 +82,9 @@ public final class Device { && injectKeyEvent(KeyEvent.ACTION_UP, keyCode, 0, 0, displayId, injectMode); } - public static boolean isScreenOn() { - return ServiceManager.getPowerManager().isScreenOn(); + public static boolean isScreenOn(int displayId) { + assert displayId != DISPLAY_ID_NONE; + return ServiceManager.getPowerManager().isScreenOn(displayId); } public static void expandNotificationPanel() { @@ -181,7 +182,7 @@ public final class Device { public static boolean powerOffScreen(int displayId) { assert displayId != DISPLAY_ID_NONE; - if (!isScreenOn()) { + if (!isScreenOn(displayId)) { return true; } return pressReleaseKeycode(KeyEvent.KEYCODE_POWER, displayId, Device.INJECT_MODE_ASYNC); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java index f62e5b8e..b5fefdd8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -1,7 +1,9 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; +import android.os.Build; import android.os.IInterface; import java.lang.reflect.Method; @@ -21,14 +23,22 @@ public final class PowerManager { private Method getIsScreenOnMethod() throws NoSuchMethodException { if (isScreenOnMethod == null) { - isScreenOnMethod = manager.getClass().getMethod("isInteractive"); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + isScreenOnMethod = manager.getClass().getMethod("isDisplayInteractive", int.class); + } else { + isScreenOnMethod = manager.getClass().getMethod("isInteractive"); + } } return isScreenOnMethod; } - public boolean isScreenOn() { + public boolean isScreenOn(int displayId) { + try { Method method = getIsScreenOnMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { + return (boolean) method.invoke(manager, displayId); + } return (boolean) method.invoke(manager); } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); From 69b836930a1d33d2aeebd17eed67c264c084c37d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 20:43:13 +0100 Subject: [PATCH 075/278] Handle capture reset via listener When the capture source becomes "invalid" (because the display size changes for example), a reset request is performed to restart the encoder. The reset state was stored in SurfaceCapture. The capture implementation set the flag, and the encoder consumed it. However, this mechanism did not allow a reset request to _interrupt_ the encoder, which may be waiting on a blocking call (until a new frame is produced). To be able to interrupt the encoder, a reset request must not only set a flag, but run a callback provided by the encoder. For that purpose, introduce the CaptureListener interface, which is notified by the SurfaceCapture implementation whenever the capture is invalidated. For now, the listener implementation just set a flag as before, so the behavior is unchanged. It lays the groundwork for the next commits. PR #5432 --- .../scrcpy/video/CameraCapture.java | 4 +-- .../genymobile/scrcpy/video/CaptureReset.java | 21 +++++++++++++ .../scrcpy/video/NewDisplayCapture.java | 2 +- .../scrcpy/video/ScreenCapture.java | 8 ++--- .../scrcpy/video/SurfaceCapture.java | 31 ++++++++++--------- .../scrcpy/video/SurfaceEncoder.java | 10 +++--- 6 files changed, 50 insertions(+), 26 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index a5fa4b06..92663f79 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -68,7 +68,7 @@ public class CameraCapture extends SurfaceCapture { } @Override - public void init() throws IOException { + protected void init() throws IOException { cameraThread = new HandlerThread("camera"); cameraThread.start(); cameraHandler = new Handler(cameraThread.getLooper()); @@ -256,7 +256,7 @@ public class CameraCapture extends SurfaceCapture { public void onDisconnected(CameraDevice camera) { Ln.w("Camera disconnected"); disconnected.set(true); - requestReset(); + invalidate(); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java new file mode 100644 index 00000000..20256d1e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy.video; + +import java.util.concurrent.atomic.AtomicBoolean; + +public class CaptureReset implements SurfaceCapture.CaptureListener { + + private final AtomicBoolean reset = new AtomicBoolean(); + + public boolean consumeReset() { + return reset.getAndSet(false); + } + + public void reset() { + reset.set(true); + } + + @Override + public void onInvalidated() { + reset(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 5cbdb792..5d61c4bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -46,7 +46,7 @@ public class NewDisplayCapture extends SurfaceCapture { } @Override - public void init() { + protected void init() { size = newDisplay.getSize(); dpi = newDisplay.getDpi(); if (size == null || dpi == 0) { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 04e42800..c0d49f60 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -85,7 +85,7 @@ public class ScreenCapture extends SurfaceCapture { Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); } setSessionDisplaySize(null); - requestReset(); + invalidate(); } else { Size size = di.getSize(); @@ -102,7 +102,7 @@ public class ScreenCapture extends SurfaceCapture { // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() // considers that the current size is the requested size (to avoid a duplicate requestReset()) setSessionDisplaySize(size); - requestReset(); + invalidate(); } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()"); } @@ -246,7 +246,7 @@ public class ScreenCapture extends SurfaceCapture { if (Ln.isEnabled(Ln.Level.VERBOSE)) { Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); } - requestReset(); + invalidate(); } }; ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); @@ -272,7 +272,7 @@ public class ScreenCapture extends SurfaceCapture { // Ignore events related to other display ids return; } - requestReset(); + invalidate(); } }; ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index 0ee93c92..172bd78f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -6,36 +6,37 @@ import com.genymobile.scrcpy.device.Size; import android.view.Surface; import java.io.IOException; -import java.util.concurrent.atomic.AtomicBoolean; /** * A video source which can be rendered on a Surface for encoding. */ public abstract class SurfaceCapture { - private final AtomicBoolean resetCapture = new AtomicBoolean(); - - /** - * Request the encoding session to be restarted, for example if the capture implementation detects that the video source size has changed (on - * device rotation for example). - */ - protected void requestReset() { - resetCapture.set(true); + public interface CaptureListener { + void onInvalidated(); } + private CaptureListener listener; + /** - * Consume the reset request (intended to be called by the encoder). - * - * @return {@code true} if a reset request was pending, {@code false} otherwise. + * Notify the listener that the capture has been invalidated (for example, because its size changed). */ - public boolean consumeReset() { - return resetCapture.getAndSet(false); + protected void invalidate() { + listener.onInvalidated(); } /** * Called once before the first capture starts. */ - public abstract void init() throws ConfigurationException, IOException; + public final void init(CaptureListener listener) throws ConfigurationException, IOException { + this.listener = listener; + init(); + } + + /** + * Called once before the first capture starts. + */ + protected abstract void init() throws ConfigurationException, IOException; /** * Called after the last capture ends (if and only if {@link #init()} has been called). diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 6a58d791..3a1c481e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -49,6 +49,8 @@ public class SurfaceEncoder implements AsyncProcessor { private Thread thread; private final AtomicBoolean stopped = new AtomicBoolean(); + private final CaptureReset reset = new CaptureReset(); + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List codecOptions, String encoderName, boolean downsizeOnError) { this.capture = capture; @@ -65,14 +67,14 @@ public class SurfaceEncoder implements AsyncProcessor { MediaCodec mediaCodec = createMediaCodec(codec, encoderName); MediaFormat format = createFormat(codec.getMimeType(), videoBitRate, maxFps, codecOptions); - capture.init(); + capture.init(reset); try { boolean alive; boolean headerWritten = false; do { - capture.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled + reset.consumeReset(); // If a capture reset was requested, it is implicitly fulfilled capture.prepare(); Size size = capture.getSize(); if (!headerWritten) { @@ -168,14 +170,14 @@ public class SurfaceEncoder implements AsyncProcessor { boolean alive = true; MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - while (!capture.consumeReset() && !eof) { + while (!reset.consumeReset() && !eof) { if (stopped.get()) { alive = false; break; } int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); try { - if (capture.consumeReset()) { + if (reset.consumeReset()) { // must restart encoding with new size break; } From 9958302e6f8780c7aa547186c70b493137986701 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 21:42:58 +0100 Subject: [PATCH 076/278] Interrupt MediaCodec blocking call on reset When the MediaCodec input is a Surface, no EOS (end-of-stream) will never occur automatically: it may only be triggered manually by MediaCodec.signalEndOfInputStream(). Use this signal to interrupt the blocking call to dequeueOutputBuffer() immediately on reset, without waiting for the next frame to be dequeued. PR #5432 --- .../genymobile/scrcpy/video/CaptureReset.java | 14 +++++- .../scrcpy/video/SurfaceEncoder.java | 48 +++++++++---------- 2 files changed, 37 insertions(+), 25 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java index 20256d1e..c11e2e80 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java @@ -1,17 +1,29 @@ package com.genymobile.scrcpy.video; +import android.media.MediaCodec; + import java.util.concurrent.atomic.AtomicBoolean; public class CaptureReset implements SurfaceCapture.CaptureListener { private final AtomicBoolean reset = new AtomicBoolean(); + // Current instance of MediaCodec to "interrupt" on reset + private MediaCodec runningMediaCodec; + public boolean consumeReset() { return reset.getAndSet(false); } - public void reset() { + public synchronized void reset() { reset.set(true); + if (runningMediaCodec != null) { + runningMediaCodec.signalEndOfInputStream(); + } + } + + public synchronized void setRunningMediaCodec(MediaCodec runningMediaCodec) { + this.runningMediaCodec = runningMediaCodec; } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 3a1c481e..8fadfa7b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -94,7 +94,21 @@ public class SurfaceEncoder implements AsyncProcessor { mediaCodec.start(); - alive = encode(mediaCodec, streamer); + // Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset + reset.setRunningMediaCodec(mediaCodec); + + if (stopped.get()) { + alive = false; + } else { + boolean resetRequested = reset.consumeReset(); + if (!resetRequested) { + // If a reset is requested during encode(), it will interrupt the encoding by an EOS + encode(mediaCodec, streamer); + } + // The capture might have been closed internally (for example if the camera is disconnected) + alive = !stopped.get() && !capture.isClosed(); + } + // do not call stop() on exception, it would trigger an IllegalStateException mediaCodec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { @@ -105,6 +119,7 @@ public class SurfaceEncoder implements AsyncProcessor { Ln.i("Retrying..."); alive = true; } finally { + reset.setRunningMediaCodec(null); mediaCodec.reset(); if (surface != null) { surface.release(); @@ -165,25 +180,16 @@ public class SurfaceEncoder implements AsyncProcessor { return 0; } - private boolean encode(MediaCodec codec, Streamer streamer) throws IOException { - boolean eof = false; - boolean alive = true; + private void encode(MediaCodec codec, Streamer streamer) throws IOException { MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); - while (!reset.consumeReset() && !eof) { - if (stopped.get()) { - alive = false; - break; - } + boolean eos; + do { int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1); try { - if (reset.consumeReset()) { - // must restart encoding with new size - break; - } - - eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; - if (outputBufferId >= 0) { + eos = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0; + // On EOS, there might be data or not, depending on bufferInfo.size + if (outputBufferId >= 0 && bufferInfo.size > 0) { ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId); boolean isConfig = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0; @@ -200,14 +206,7 @@ public class SurfaceEncoder implements AsyncProcessor { codec.releaseOutputBuffer(outputBufferId, false); } } - } - - if (capture.isClosed()) { - // The capture might have been closed internally (for example if the camera is disconnected) - alive = false; - } - - return !eof && alive; + } while (!eos); } private static MediaCodec createMediaCodec(Codec codec, String encoderName) throws IOException, ConfigurationException { @@ -300,6 +299,7 @@ public class SurfaceEncoder implements AsyncProcessor { public void stop() { if (thread != null) { stopped.set(true); + reset.reset(); } } From 104195fc3bdcd69481ede5ff2808be0a1f1d6e5e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 31 Oct 2024 22:47:35 +0100 Subject: [PATCH 077/278] Add shortcut to reset video capture/encoding Reset video capture/encoding on MOD+Shift+r. Like on device rotation, this starts a new encoding session which produces a video stream starting by a key frame. PR #5432 --- app/scrcpy.1 | 4 ++++ app/src/cli.c | 4 ++++ app/src/control_msg.c | 4 ++++ app/src/control_msg.h | 1 + app/src/input_manager.c | 20 ++++++++++++++-- app/tests/test_control_msg_serialize.c | 16 +++++++++++++ doc/shortcuts.md | 1 + .../java/com/genymobile/scrcpy/Server.java | 4 ++++ .../scrcpy/control/ControlMessage.java | 1 + .../scrcpy/control/ControlMessageReader.java | 1 + .../genymobile/scrcpy/control/Controller.java | 18 +++++++++++++++ .../scrcpy/video/CameraCapture.java | 5 ++++ .../scrcpy/video/NewDisplayCapture.java | 23 ++++++++++++++----- .../scrcpy/video/ScreenCapture.java | 5 ++++ .../scrcpy/video/SurfaceCapture.java | 7 ++++++ 15 files changed, 106 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index f517ad4c..76e36dcb 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -671,6 +671,10 @@ Pause or re-pause display .B MOD+Shift+z Unpause display +.TP +.B MOD+Shift+r +Reset video capture/encoding + .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) diff --git a/app/src/cli.c b/app/src/cli.c index 77747e93..7cc68085 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1022,6 +1022,10 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Shift+z" }, .text = "Unpause display", }, + { + .shortcuts = { "MOD+Shift+r" }, + .text = "Reset video capture/encoding", + }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e04fbd3c..0defda92 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -181,6 +181,7 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { case SC_CONTROL_MSG_TYPE_COLLAPSE_PANELS: case SC_CONTROL_MSG_TYPE_ROTATE_DEVICE: case SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + case SC_CONTROL_MSG_TYPE_RESET_VIDEO: // no additional data return 1; default: @@ -304,6 +305,9 @@ sc_control_msg_log(const struct sc_control_msg *msg) { case SC_CONTROL_MSG_TYPE_START_APP: LOG_CMSG("start app \"%s\"", msg->start_app.name); break; + case SC_CONTROL_MSG_TYPE_RESET_VIDEO: + LOG_CMSG("reset video"); + break; default: LOG_CMSG("unknown type: %u", (unsigned) msg->type); break; diff --git a/app/src/control_msg.h b/app/src/control_msg.h index 9eef7e82..f0a2e373 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -42,6 +42,7 @@ enum sc_control_msg_type { SC_CONTROL_MSG_TYPE_UHID_DESTROY, SC_CONTROL_MSG_TYPE_OPEN_HARD_KEYBOARD_SETTINGS, SC_CONTROL_MSG_TYPE_START_APP, + SC_CONTROL_MSG_TYPE_RESET_VIDEO, }; enum sc_copy_key { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 140b50ac..3955c211 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -284,6 +284,18 @@ open_hard_keyboard_settings(struct sc_input_manager *im) { } } +static void +reset_video(struct sc_input_manager *im) { + assert(im->controller); + + struct sc_control_msg msg; + msg.type = SC_CONTROL_MSG_TYPE_RESET_VIDEO; + + if (!sc_controller_push_msg(im->controller, &msg)) { + LOGW("Could not request reset video"); + } +} + static void apply_orientation_transform(struct sc_input_manager *im, enum sc_orientation transform) { @@ -521,8 +533,12 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_r: - if (control && !shift && !repeat && down && !paused) { - rotate_device(im); + if (control && !repeat && down && !paused) { + if (shift) { + reset_video(im); + } else { + rotate_device(im); + } } return; case SDLK_k: diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 73bca901..9adf2a3d 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -407,6 +407,21 @@ static void test_serialize_open_hard_keyboard(void) { assert(!memcmp(buf, expected, sizeof(expected))); } +static void test_serialize_reset_video(void) { + struct sc_control_msg msg = { + .type = SC_CONTROL_MSG_TYPE_RESET_VIDEO, + }; + + uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; + size_t size = sc_control_msg_serialize(&msg, buf); + assert(size == 1); + + const uint8_t expected[] = { + SC_CONTROL_MSG_TYPE_RESET_VIDEO, + }; + assert(!memcmp(buf, expected, sizeof(expected))); +} + int main(int argc, char *argv[]) { (void) argc; (void) argv; @@ -429,5 +444,6 @@ int main(int argc, char *argv[]) { test_serialize_uhid_input(); test_serialize_uhid_destroy(); test_serialize_open_hard_keyboard(); + test_serialize_reset_video(); return 0; } diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 4ea37257..d22eb473 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -30,6 +30,7 @@ _[Super] is typically the Windows or Cmd key._ | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ | Pause or re-pause display | MOD+z | Unpause display | MOD+Shift+z + | Reset video capture/encoding | MOD+Shift+r | Resize window to 1:1 (pixel-perfect) | MOD+g | Resize window to remove black borders | MOD+w \| _Double-left-click¹_ | Click on `HOME` | MOD+h \| _Middle-click_ diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a0a48806..e0adeea0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -225,6 +225,10 @@ public final class Server { SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); asyncProcessors.add(surfaceEncoder); + + if (controller != null) { + controller.setSurfaceCapture(surfaceCapture); + } } Completion completion = new Completion(asyncProcessors.size()); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index eec5f67f..7455cdf8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -24,6 +24,7 @@ public final class ControlMessage { public static final int TYPE_UHID_DESTROY = 14; public static final int TYPE_OPEN_HARD_KEYBOARD_SETTINGS = 15; public static final int TYPE_START_APP = 16; + public static final int TYPE_RESET_VIDEO = 17; public static final long SEQUENCE_INVALID = 0; diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index ae167690..b82615ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -46,6 +46,7 @@ public class ControlMessageReader { case ControlMessage.TYPE_COLLAPSE_PANELS: case ControlMessage.TYPE_ROTATE_DEVICE: case ControlMessage.TYPE_OPEN_HARD_KEYBOARD_SETTINGS: + case ControlMessage.TYPE_RESET_VIDEO: return ControlMessage.createEmpty(type); case ControlMessage.TYPE_UHID_CREATE: return parseUhidCreate(); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 67a0115d..b4ae07b6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -9,6 +9,7 @@ import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; +import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.VirtualDisplayListener; import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; @@ -93,6 +94,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private boolean keepDisplayPowerOff; + // Used for resetting video encoding on RESET_VIDEO message + private SurfaceCapture surfaceCapture; + public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { this.displayId = displayId; this.controlChannel = controlChannel; @@ -143,6 +147,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } } + public void setSurfaceCapture(SurfaceCapture surfaceCapture) { + this.surfaceCapture = surfaceCapture; + } + private UhidManager getUhidManager() { if (uhidManager == null) { uhidManager = new UhidManager(sender); @@ -293,6 +301,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { case ControlMessage.TYPE_START_APP: startAppAsync(msg.getText()); break; + case ControlMessage.TYPE_RESET_VIDEO: + resetVideo(); + break; default: // do nothing } @@ -680,4 +691,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } } } + + private void resetVideo() { + if (surfaceCapture != null) { + Ln.i("Video capture reset"); + surfaceCapture.requestInvalidate(); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 92663f79..7385283e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -355,4 +355,9 @@ public class CameraCapture extends SurfaceCapture { public boolean isClosed() { return disconnected.get(); } + + @Override + public void requestInvalidate() { + // do nothing (the user could not request a reset anyway for now, since there is no controller for camera mirroring) + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 5d61c4bd..f6561a5f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -14,6 +14,8 @@ import android.hardware.display.VirtualDisplay; import android.os.Build; import android.view.Surface; +import java.io.IOException; + public class NewDisplayCapture extends SurfaceCapture { // Internal fields copied from android.hardware.display.DisplayManager @@ -72,13 +74,8 @@ public class NewDisplayCapture extends SurfaceCapture { } } - @Override - public void start(Surface surface) { - if (virtualDisplay != null) { - virtualDisplay.release(); - virtualDisplay = null; - } + public void startNew(Surface surface) { int virtualDisplayId; try { int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC @@ -113,6 +110,15 @@ public class NewDisplayCapture extends SurfaceCapture { } } + @Override + public void start(Surface surface) throws IOException { + if (virtualDisplay == null) { + startNew(surface); + } else { + virtualDisplay.setSurface(surface); + } + } + @Override public void release() { if (virtualDisplay != null) { @@ -142,4 +148,9 @@ public class NewDisplayCapture extends SurfaceCapture { int num = size.getMax(); return initialDpi * num / den; } + + @Override + public void requestInvalidate() { + invalidate(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index c0d49f60..48e594b7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -291,4 +291,9 @@ public class ScreenCapture extends SurfaceCapture { } } } + + @Override + public void requestInvalidate() { + invalidate(); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index 172bd78f..de9e1b27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -79,4 +79,11 @@ public abstract class SurfaceCapture { public boolean isClosed() { return false; } + + /** + * Manually request to invalidate (typically a user request). + *

+ * The capture implementation is free to ignore the request and do nothing. + */ + public abstract void requestInvalidate(); } From e9dd0f68adc59bae67f04bc859a8287785771ace Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 4 Nov 2024 23:06:19 +0100 Subject: [PATCH 078/278] Fix audio regulator compensation A call to swr_set_compensation() configures the resampler to drop or duplicate "diff" samples over an interval of "distance" samples. If the function is not called again, then after "distance" samples, no more compensation will be applied. So it must always be called, even if the new computed diff value happens to be the same as the previous one. In practice, it is unlikely that the diff value is exactly the same every second, except when it is actively clamped (to 2% of the sample rate). --- app/src/audio_regulator.c | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index 911b2bfa..fb0c3758 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -309,14 +309,12 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 " compensation=%d", ar->target_buffering, avg, can_read, diff); - if (diff != ar->compensation) { - int ret = swr_set_compensation(swr_ctx, diff, distance); - if (ret < 0) { - LOGW("Resampling compensation failed: %d", ret); - // not fatal - } else { - ar->compensation = diff; - } + int ret = swr_set_compensation(swr_ctx, diff, distance); + if (ret < 0) { + LOGW("Resampling compensation failed: %d", ret); + // not fatal + } else { + ar->compensation = diff; } } From 5936167ff77b543a533c7f6a5fa42d78ca5d724f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 4 Nov 2024 23:17:43 +0100 Subject: [PATCH 079/278] Store compensation state as a boolean We don't need to store the last compensation value anymore, we just need to know if it's non-zero. --- app/src/audio_regulator.c | 6 +++--- app/src/audio_regulator.h | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index fb0c3758..3e4f78ad 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -288,7 +288,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { // Enable compensation when the difference exceeds +/- 4ms. // Disable compensation when the difference is lower than +/- 1ms. - int threshold = ar->compensation != 0 + int threshold = ar->compensation_active ? ar->sample_rate / 1000 /* 1ms */ : ar->sample_rate * 4 / 1000; /* 4ms */ @@ -314,7 +314,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { LOGW("Resampling compensation failed: %d", ret); // not fatal } else { - ar->compensation = diff; + ar->compensation_active = diff != 0; } } @@ -390,7 +390,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, atomic_init(&ar->played, false); atomic_init(&ar->received, false); atomic_init(&ar->underflow, 0); - ar->compensation = 0; + ar->compensation_active = false; return true; diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 7daa1b05..1c0eeb9f 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -44,8 +44,8 @@ struct sc_audio_regulator { // Number of silence samples inserted since the last received packet atomic_uint_least32_t underflow; - // Current applied compensation value (only used by the receiver thread) - int compensation; + // Non-zero compensation applied (only used by the receiver thread) + bool compensation_active; // Set to true the first time a sample is received atomic_bool received; From d3db9c40653d4c1eaafda1ece2abe57acad94906 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 22:34:02 +0100 Subject: [PATCH 080/278] Refactor clean up configuration to simplify All options were configured dynamically by sending a single byte to an output stream. But in practice, only the power mode must be changed dynamically, the others are configured once on start. For simplicity, pass the value of static options as command line arguments, and handle dynamic options in a loop only from a separate thread once the clean up process is started. This will allow to easily add cleanup options with values which do not fit in 1 byte. Also handle the clean up thread (and the loading of initial settings values) from the CleanUp class, to expose a simpler clean up API. Refs 9efa162949c2a3e3e42564862ff390700270394d PR #5447 --- .../java/com/genymobile/scrcpy/CleanUp.java | 164 +++++++++++------- .../java/com/genymobile/scrcpy/Server.java | 66 +------ 2 files changed, 106 insertions(+), 124 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 343d854a..a8a5784d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -5,6 +5,8 @@ import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; +import android.os.BatteryManager; + import java.io.File; import java.io.IOException; import java.io.OutputStream; @@ -16,59 +18,110 @@ import java.io.OutputStream; */ public final class CleanUp { - private static final int MSG_TYPE_MASK = 0b11; - private static final int MSG_TYPE_RESTORE_STAY_ON = 0; - private static final int MSG_TYPE_DISABLE_SHOW_TOUCHES = 1; - private static final int MSG_TYPE_RESTORE_DISPLAY_POWER = 2; - private static final int MSG_TYPE_POWER_OFF_SCREEN = 3; + // Dynamic options + private static final int PENDING_CHANGE_DISPLAY_POWER = 1 << 0; + private int pendingChanges; + private boolean pendingRestoreDisplayPower; - private static final int MSG_PARAM_SHIFT = 2; + private Thread thread; - private final OutputStream out; - - public CleanUp(OutputStream out) { - this.out = out; + private CleanUp(int displayId, Options options) { + thread = new Thread(() -> runCleanUp(displayId, options), "cleanup"); + thread.start(); } - public static CleanUp configure(int displayId) throws IOException { - String[] cmd = {"app_process", "/", CleanUp.class.getName(), String.valueOf(displayId)}; + public static CleanUp start(int displayId, Options options) { + return new CleanUp(displayId, options); + } + + public void interrupt() { + thread.interrupt(); + } + + public void join() throws InterruptedException { + thread.join(); + } + + private void runCleanUp(int displayId, Options options) { + boolean disableShowTouches = false; + if (options.getShowTouches()) { + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); + // If "show touches" was disabled, it must be disabled back on clean up + disableShowTouches = !"1".equals(oldValue); + } catch (SettingsException e) { + Ln.e("Could not change \"show_touches\"", e); + } + } + + int restoreStayOn = -1; + if (options.getStayAwake()) { + int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); + try { + int currentStayOn = Integer.parseInt(oldValue); + // Restore only if the current value is different + if (currentStayOn != stayOn) { + restoreStayOn = currentStayOn; + } + } catch (NumberFormatException e) { + // ignore + } + } catch (SettingsException e) { + Ln.e("Could not change \"stay_on_while_plugged_in\"", e); + } + } + + boolean powerOffScreen = options.getPowerOffScreenOnClose(); + + try { + run(displayId, restoreStayOn, disableShowTouches, powerOffScreen); + } catch (InterruptedException e) { + // ignore + } catch (IOException e) { + Ln.e("Clean up I/O exception", e); + } + } + + private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen) throws IOException, InterruptedException { + String[] cmd = { + "app_process", + "/", + CleanUp.class.getName(), + String.valueOf(displayId), + String.valueOf(restoreStayOn), + String.valueOf(disableShowTouches), + String.valueOf(powerOffScreen), + }; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", Server.SERVER_PATH); Process process = builder.start(); - return new CleanUp(process.getOutputStream()); - } + OutputStream out = process.getOutputStream(); - private boolean sendMessage(int type, int param) { - assert (type & ~MSG_TYPE_MASK) == 0; - int msg = type | param << MSG_PARAM_SHIFT; - try { - out.write(msg); - out.flush(); - return true; - } catch (IOException e) { - Ln.w("Could not configure cleanup (type=" + type + ", param=" + param + ")", e); - return false; + while (true) { + int localPendingChanges; + boolean localPendingRestoreDisplayPower; + synchronized (this) { + while (pendingChanges == 0) { + wait(); + } + localPendingChanges = pendingChanges; + localPendingRestoreDisplayPower = pendingRestoreDisplayPower; + pendingChanges = 0; + } + if ((localPendingChanges & PENDING_CHANGE_DISPLAY_POWER) != 0) { + out.write(localPendingRestoreDisplayPower ? 1 : 0); + out.flush(); + } } } - public boolean setRestoreStayOn(int restoreValue) { - // Restore the value (between 0 and 7), -1 to not restore - // - assert restoreValue >= -1 && restoreValue <= 7; - return sendMessage(MSG_TYPE_RESTORE_STAY_ON, restoreValue & 0b1111); - } - - public boolean setDisableShowTouches(boolean disableOnExit) { - return sendMessage(MSG_TYPE_DISABLE_SHOW_TOUCHES, disableOnExit ? 1 : 0); - } - - public boolean setRestoreDisplayPower(boolean restoreOnExit) { - return sendMessage(MSG_TYPE_RESTORE_DISPLAY_POWER, restoreOnExit ? 1 : 0); - } - - public boolean setPowerOffScreen(boolean powerOffScreenOnExit) { - return sendMessage(MSG_TYPE_POWER_OFF_SCREEN, powerOffScreenOnExit ? 1 : 0); + public synchronized void setRestoreDisplayPower(boolean restoreDisplayPower) { + pendingRestoreDisplayPower = restoreDisplayPower; + pendingChanges |= PENDING_CHANGE_DISPLAY_POWER; + notify(); } public static void unlinkSelf() { @@ -83,35 +136,20 @@ public final class CleanUp { unlinkSelf(); int displayId = Integer.parseInt(args[0]); + int restoreStayOn = Integer.parseInt(args[1]); + boolean disableShowTouches = Boolean.parseBoolean(args[2]); + boolean powerOffScreen = Boolean.parseBoolean(args[3]); - int restoreStayOn = -1; - boolean disableShowTouches = false; + // Dynamic option boolean restoreDisplayPower = false; - boolean powerOffScreen = false; try { // Wait for the server to die int msg; while ((msg = System.in.read()) != -1) { - int type = msg & MSG_TYPE_MASK; - int param = msg >> MSG_PARAM_SHIFT; - switch (type) { - case MSG_TYPE_RESTORE_STAY_ON: - restoreStayOn = param > 7 ? -1 : param; - break; - case MSG_TYPE_DISABLE_SHOW_TOUCHES: - disableShowTouches = param != 0; - break; - case MSG_TYPE_RESTORE_DISPLAY_POWER: - restoreDisplayPower = param != 0; - break; - case MSG_TYPE_POWER_OFF_SCREEN: - powerOffScreen = param != 0; - break; - default: - Ln.w("Unexpected msg type: " + type); - break; - } + // Only restore display power + assert msg == 0 || msg == 1; + restoreDisplayPower = msg != 0; } } catch (IOException e) { // Expected when the server is dead diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index e0adeea0..a093fdf0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -16,8 +16,6 @@ import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.util.Settings; -import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.video.CameraCapture; import com.genymobile.scrcpy.video.NewDisplayCapture; import com.genymobile.scrcpy.video.ScreenCapture; @@ -25,7 +23,6 @@ import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; -import android.os.BatteryManager; import android.os.Build; import java.io.File; @@ -76,51 +73,6 @@ public final class Server { // not instantiable } - private static void initAndCleanUp(Options options, CleanUp cleanUp) { - // This method is called from its own thread, so it may only configure cleanup actions which are NOT dynamic (i.e. they are configured once - // and for all, they cannot be changed from another thread) - - if (options.getShowTouches()) { - try { - String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "show_touches", "1"); - // If "show touches" was disabled, it must be disabled back on clean up - if (!"1".equals(oldValue)) { - if (!cleanUp.setDisableShowTouches(true)) { - Ln.e("Could not disable show touch on exit"); - } - } - } catch (SettingsException e) { - Ln.e("Could not change \"show_touches\"", e); - } - } - - if (options.getStayAwake()) { - int stayOn = BatteryManager.BATTERY_PLUGGED_AC | BatteryManager.BATTERY_PLUGGED_USB | BatteryManager.BATTERY_PLUGGED_WIRELESS; - try { - String oldValue = Settings.getAndPutValue(Settings.TABLE_GLOBAL, "stay_on_while_plugged_in", String.valueOf(stayOn)); - try { - int restoreStayOn = Integer.parseInt(oldValue); - if (restoreStayOn != stayOn) { - // Restore only if the current value is different - if (!cleanUp.setRestoreStayOn(restoreStayOn)) { - Ln.e("Could not restore stay on on exit"); - } - } - } catch (NumberFormatException e) { - // ignore - } - } catch (SettingsException e) { - Ln.e("Could not change \"stay_on_while_plugged_in\"", e); - } - } - - if (options.getPowerOffScreenOnClose()) { - if (!cleanUp.setPowerOffScreen(true)) { - Ln.e("Could not power off screen on exit"); - } - } - } - private static void scrcpy(Options options) throws IOException, ConfigurationException { if (Build.VERSION.SDK_INT < AndroidVersions.API_31_ANDROID_12 && options.getVideoSource() == VideoSource.CAMERA) { Ln.e("Camera mirroring is not supported before Android 12"); @@ -150,14 +102,12 @@ public final class Server { } CleanUp cleanUp = null; - Thread initThread = null; NewDisplay newDisplay = options.getNewDisplay(); int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE; if (options.getCleanup()) { - cleanUp = CleanUp.configure(displayId); - initThread = startInitThread(options, cleanUp); + cleanUp = CleanUp.start(displayId, options); } int scid = options.getScid(); @@ -240,8 +190,8 @@ public final class Server { completion.await(); } finally { - if (initThread != null) { - initThread.interrupt(); + if (cleanUp != null) { + cleanUp.interrupt(); } for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.stop(); @@ -250,8 +200,8 @@ public final class Server { connection.shutdown(); try { - if (initThread != null) { - initThread.join(); + if (cleanUp != null) { + cleanUp.join(); } for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.join(); @@ -264,12 +214,6 @@ public final class Server { } } - private static Thread startInitThread(final Options options, final CleanUp cleanUp) { - Thread thread = new Thread(() -> initAndCleanUp(options, cleanUp), "init-cleanup"); - thread.start(); - return thread; - } - public static void main(String... args) { int status = 0; try { From eff5b4b219be6043a3baf51149b1d6752569a173 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 3 Nov 2024 22:46:21 +0100 Subject: [PATCH 081/278] Add --screen-off-timeout Change the Android "screen off timeout" (the idle delay before the screen automatically turns off) and restore the initial value on exit. PR #5447 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/src/cli.c | 28 +++++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 5 +++ app/src/server.h | 1 + doc/device.md | 25 +++++++++++++ .../java/com/genymobile/scrcpy/CleanUp.java | 35 +++++++++++++++++-- .../java/com/genymobile/scrcpy/Options.java | 11 ++++++ 11 files changed, 108 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index d9a7be60..d9ad4c8d 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -77,6 +77,7 @@ _scrcpy() { --rotation= -s --serial= -S --turn-screen-off + --screen-off-timeout= --shortcut-mod= --start-app= -t --show-touches diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c4e0e60c..430e8000 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -80,6 +80,7 @@ arguments=( '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' {-S,--turn-screen-off}'[Turn the device screen off immediately]' + '--screen-off-timeout=[Set the screen off timeout in seconds]' '--shortcut-mod=[\[key1,key2+key3,...\] Specify the modifiers to use for scrcpy shortcuts]:shortcut mod:(lctrl rctrl lalt ralt lsuper rsuper)' '--start-app=[Start an Android app]' {-t,--show-touches}'[Show physical touches]' diff --git a/app/src/cli.c b/app/src/cli.c index 7cc68085..ebf0f6f6 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -106,6 +106,7 @@ enum { OPT_NEW_DISPLAY, OPT_LIST_APPS, OPT_START_APP, + OPT_SCREEN_OFF_TIMEOUT, }; struct sc_option { @@ -793,6 +794,13 @@ static const struct sc_option options[] = { .longopt = "turn-screen-off", .text = "Turn the device screen off immediately.", }, + { + .longopt_id = OPT_SCREEN_OFF_TIMEOUT, + .longopt = "screen-off-timeout", + .argdesc = "seconds", + .text = "Set the screen off timeout while scrcpy is running (restore " + "the initial value on exit).", + }, { .longopt_id = OPT_SHORTCUT_MOD, .longopt = "shortcut-mod", @@ -2155,6 +2163,20 @@ parse_time_limit(const char *s, sc_tick *tick) { return true; } +static bool +parse_screen_off_timeout(const char *s, sc_tick *tick) { + long value; + // value in seconds, but must fit in 31 bits in milliseconds + bool ok = parse_integer_arg(s, &value, false, 0, 0x7FFFFFFF / 1000, + "screen off timeout"); + if (!ok) { + return false; + } + + *tick = SC_TICK_FROM_SEC(value); + return true; +} + static bool parse_pause_on_exit(const char *s, enum sc_pause_on_exit *pause_on_exit) { if (!s || !strcmp(s, "true")) { @@ -2730,6 +2752,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_START_APP: opts->start_app = optarg; break; + case OPT_SCREEN_OFF_TIMEOUT: + if (!parse_screen_off_timeout(optarg, + &opts->screen_off_timeout)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index b1a3b739..3cad9d9f 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -62,6 +62,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_buffer = -1, // depends on the audio format, .audio_output_buffer = SC_TICK_FROM_MS(5), .time_limit = 0, + .screen_off_timeout = -1, #ifdef HAVE_V4L2 .v4l2_device = NULL, .v4l2_buffer = 0, diff --git a/app/src/options.h b/app/src/options.h index d37ac0a2..5662719a 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -265,6 +265,7 @@ struct scrcpy_options { sc_tick audio_buffer; sc_tick audio_output_buffer; sc_tick time_limit; + sc_tick screen_off_timeout; #ifdef HAVE_V4L2 const char *v4l2_device; sc_tick v4l2_buffer; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 8d135394..2721c0d8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -428,6 +428,7 @@ scrcpy(struct scrcpy_options *options) { .video_bit_rate = options->video_bit_rate, .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, + .screen_off_timeout = options->screen_off_timeout, .lock_video_orientation = options->lock_video_orientation, .control = options->control, .display_id = options->display_id, diff --git a/app/src/server.c b/app/src/server.c index 167582e4..41f0bf27 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -320,6 +320,11 @@ execute_server(struct sc_server *server, if (params->stay_awake) { ADD_PARAM("stay_awake=true"); } + if (params->screen_off_timeout != -1) { + assert(params->screen_off_timeout >= 0); + uint64_t ms = SC_TICK_TO_MS(params->screen_off_timeout); + ADD_PARAM("screen_off_timeout=%" PRIu64, ms); + } if (params->video_codec_options) { VALIDATE_STRING(params->video_codec_options); ADD_PARAM("video_codec_options=%s", params->video_codec_options); diff --git a/app/src/server.h b/app/src/server.h index 4ff5539d..7059be7f 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -45,6 +45,7 @@ struct sc_server_params { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server + sc_tick screen_off_timeout; int8_t lock_video_orientation; bool control; uint32_t display_id; diff --git a/doc/device.md b/doc/device.md index 06210e18..42208faa 100644 --- a/doc/device.md +++ b/doc/device.md @@ -71,6 +71,31 @@ adb shell cmd display power-on 0 ``` +## Screen off timeout + +The Android screen automatically turns off after some delay. + +To change this delay while scrcpy is running: + +```bash +scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) +``` + +The initial value is restored on exit. + +It is possible to change this setting manually: + +```bash +# get the current screen_off_timeout value +adb shell settings get system screen_off_timeout +# set a new value (in milliseconds) +adb shell settings put system screen_off_timeout 30000 +``` + +Note that the Android value is in milliseconds, but the scrcpy command line +argument is in seconds. + + ## Show touches For presentations, it may be useful to show physical touches (on the physical diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index a8a5784d..90de8c2c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -73,10 +73,29 @@ public final class CleanUp { } } + int restoreScreenOffTimeout = -1; + int screenOffTimeout = options.getScreenOffTimeout(); + if (screenOffTimeout != -1) { + try { + String oldValue = Settings.getAndPutValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(screenOffTimeout)); + try { + int currentScreenOffTimeout = Integer.parseInt(oldValue); + // Restore only if the current value is different + if (currentScreenOffTimeout != screenOffTimeout) { + restoreScreenOffTimeout = currentScreenOffTimeout; + } + } catch (NumberFormatException e) { + // ignore + } + } catch (SettingsException e) { + Ln.e("Could not change \"screen_off_timeout\"", e); + } + } + boolean powerOffScreen = options.getPowerOffScreenOnClose(); try { - run(displayId, restoreStayOn, disableShowTouches, powerOffScreen); + run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); } catch (InterruptedException e) { // ignore } catch (IOException e) { @@ -84,7 +103,8 @@ public final class CleanUp { } } - private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen) throws IOException, InterruptedException { + private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) + throws IOException, InterruptedException { String[] cmd = { "app_process", "/", @@ -93,6 +113,7 @@ public final class CleanUp { String.valueOf(restoreStayOn), String.valueOf(disableShowTouches), String.valueOf(powerOffScreen), + String.valueOf(restoreScreenOffTimeout), }; ProcessBuilder builder = new ProcessBuilder(cmd); @@ -139,6 +160,7 @@ public final class CleanUp { int restoreStayOn = Integer.parseInt(args[1]); boolean disableShowTouches = Boolean.parseBoolean(args[2]); boolean powerOffScreen = Boolean.parseBoolean(args[3]); + int restoreScreenOffTimeout = Integer.parseInt(args[4]); // Dynamic option boolean restoreDisplayPower = false; @@ -175,6 +197,15 @@ public final class CleanUp { } } + if (restoreScreenOffTimeout != -1) { + Ln.i("Restoring \"screen off timeout\""); + try { + Settings.putValue(Settings.TABLE_SYSTEM, "screen_off_timeout", String.valueOf(restoreScreenOffTimeout)); + } catch (SettingsException e) { + Ln.e("Could not restore \"screen_off_timeout\"", e); + } + } + if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) { if (powerOffScreen) { Ln.i("Power off screen"); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 659a6948..e75321e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -45,6 +45,7 @@ public class Options { private boolean cameraHighSpeed; private boolean showTouches; private boolean stayAwake; + private int screenOffTimeout = -1; private List videoCodecOptions; private List audioCodecOptions; @@ -174,6 +175,10 @@ public class Options { return stayAwake; } + public int getScreenOffTimeout() { + return screenOffTimeout; + } + public List getVideoCodecOptions() { return videoCodecOptions; } @@ -363,6 +368,12 @@ public class Options { case "stay_awake": options.stayAwake = Boolean.parseBoolean(value); break; + case "screen_off_timeout": + options.screenOffTimeout = Integer.parseInt(value); + if (options.screenOffTimeout < -1) { + throw new IllegalArgumentException("Invalid screen off timeout: " + options.screenOffTimeout); + } + break; case "video_codec_options": options.videoCodecOptions = CodecOption.parse(value); break; From c0e2e27cf9eb38a95b324ba9aeeb34dab6d2e72f Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 11 Nov 2024 12:16:18 +0800 Subject: [PATCH 082/278] Force javac to use UTF-8 The source files are encoded in UTF-8. Refs Signed-off-by: Romain Vimont --- server/build_without_gradle.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 14534700..ddb98b21 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -68,7 +68,7 @@ done echo "Compiling java sources..." cd ../java -javac -bootclasspath "$ANDROID_JAR" \ +javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \ -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ From 762816cac62b39a58bebf129fb6e00bd24a4594b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Nov 2024 12:54:25 +0100 Subject: [PATCH 083/278] Remove quotes for --video-encoder in documentation Refs ec602a0334357982d75b374f7ac753c5bef1216a --- doc/video.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/video.md b/doc/video.md index ed92cb22..844fa516 100644 --- a/doc/video.md +++ b/doc/video.md @@ -93,7 +93,7 @@ Sometimes, the default encoder may have issues or even crash, so it is useful to try another one: ```bash -scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' +scrcpy --video-codec=h264 --video-encoder=OMX.qcom.video.encoder.avc ``` From 04dd72b5944dbe8de2a2fa438f920e88c03e4ff8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 13 Nov 2024 12:56:35 +0100 Subject: [PATCH 084/278] Add "how to run" link for Windows Reference the documentation explaining how to run scrcpy on Windows directly in the main README. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 44f3d740..7b29f3a4 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). ## Get the app - [Linux](doc/linux.md) - - [Windows](doc/windows.md) + - [Windows](doc/windows.md) (read [how to run](doc/windows.md#run)) - [macOS](doc/macos.md) From 91373d906b100349de959f49172d4605f66f64b2 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sat, 9 Nov 2024 14:34:55 +0800 Subject: [PATCH 085/278] Add FakeContext.getContentResolver() This avoids the following error on some devices: Given calling package android does not match caller's uid 2000 Refs #4639 comment Fixes #4639 PR #5476 Signed-off-by: Romain Vimont --- server/build_without_gradle.sh | 6 +++ .../android/content/IContentProvider.java | 5 +++ .../com/genymobile/scrcpy/FakeContext.java | 42 +++++++++++++++++++ .../scrcpy/wrappers/ActivityManager.java | 16 +++---- 4 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 server/src/main/java/android/content/IContentProvider.java diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index ddb98b21..e0fc3a95 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -50,6 +50,11 @@ cd "$SERVER_DIR/src/main/aidl" android/content/IOnPrimaryClipChangedListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl +# Fake sources to expose hidden Android types to the project +FAKE_SRC=( \ + android/content/*java \ +) + SRC=( \ com/genymobile/scrcpy/*.java \ com/genymobile/scrcpy/audio/*.java \ @@ -72,6 +77,7 @@ javac -encoding UTF-8 -bootclasspath "$ANDROID_JAR" \ -cp "$LAMBDA_JAR:$GEN_DIR" \ -d "$CLASSES_DIR" \ -source 1.8 -target 1.8 \ + ${FAKE_SRC[@]} \ ${SRC[@]} echo "Dexing..." diff --git a/server/src/main/java/android/content/IContentProvider.java b/server/src/main/java/android/content/IContentProvider.java new file mode 100644 index 00000000..bb907dd3 --- /dev/null +++ b/server/src/main/java/android/content/IContentProvider.java @@ -0,0 +1,5 @@ +package android.content; + +public interface IContentProvider { + // android.content.IContentProvider is hidden, this is a fake one to expose the type to the project +} diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 0b086cc5..2b83e397 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -1,9 +1,14 @@ package com.genymobile.scrcpy; +import com.genymobile.scrcpy.wrappers.ServiceManager; + import android.annotation.TargetApi; import android.content.AttributionSource; +import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; +import android.content.IContentProvider; +import android.os.Binder; import android.os.Process; public final class FakeContext extends ContextWrapper { @@ -17,6 +22,38 @@ public final class FakeContext extends ContextWrapper { return INSTANCE; } + private final ContentResolver contentResolver = new ContentResolver(this) { + @SuppressWarnings({"unused", "ProtectedMemberInFinalClass"}) + // @Override (but super-class method not visible) + protected IContentProvider acquireProvider(Context c, String name) { + return ServiceManager.getActivityManager().getContentProviderExternal(name, new Binder()); + } + + @SuppressWarnings("unused") + // @Override (but super-class method not visible) + public boolean releaseProvider(IContentProvider icp) { + return false; + } + + @SuppressWarnings({"unused", "ProtectedMemberInFinalClass"}) + // @Override (but super-class method not visible) + protected IContentProvider acquireUnstableProvider(Context c, String name) { + return null; + } + + @SuppressWarnings("unused") + // @Override (but super-class method not visible) + public boolean releaseUnstableProvider(IContentProvider icp) { + return false; + } + + @SuppressWarnings("unused") + // @Override (but super-class method not visible) + public void unstableProviderDied(IContentProvider icp) { + // ignore + } + }; + private FakeContext() { super(Workarounds.getSystemContext()); } @@ -49,4 +86,9 @@ public final class FakeContext extends ContextWrapper { public Context getApplicationContext() { return this; } + + @Override + public ContentResolver getContentResolver() { + return contentResolver; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java index f052dee0..255483c6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ActivityManager.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; +import android.content.IContentProvider; import android.content.Intent; import android.os.Binder; import android.os.Bundle; @@ -64,7 +65,7 @@ public final class ActivityManager { } @TargetApi(AndroidVersions.API_29_ANDROID_10) - private ContentProvider getContentProviderExternal(String name, IBinder token) { + public IContentProvider getContentProviderExternal(String name, IBinder token) { try { Method method = getGetContentProviderExternalMethod(); Object[] args; @@ -83,11 +84,7 @@ public final class ActivityManager { // IContentProvider provider = providerHolder.provider; Field providerField = providerHolder.getClass().getDeclaredField("provider"); providerField.setAccessible(true); - Object provider = providerField.get(providerHolder); - if (provider == null) { - return null; - } - return new ContentProvider(this, provider, name, token); + return (IContentProvider) providerField.get(providerHolder); } catch (ReflectiveOperationException e) { Ln.e("Could not invoke method", e); return null; @@ -104,7 +101,12 @@ public final class ActivityManager { } public ContentProvider createSettingsProvider() { - return getContentProviderExternal("settings", new Binder()); + IBinder token = new Binder(); + IContentProvider provider = getContentProviderExternal("settings", token); + if (provider == null) { + return null; + } + return new ContentProvider(this, provider, "settings", token); } private Method getStartActivityAsUserMethod() throws NoSuchMethodException, ClassNotFoundException { From df74cceb6fe115bd39e862612a14a1e1483b1529 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 18:46:51 +0100 Subject: [PATCH 086/278] Use camera prepare() step For consistency with screen capture. Refs b60e1747809cce58793a8c0d54b499df87a6a975 --- .../scrcpy/video/CameraCapture.java | 23 ++++++++++--------- .../scrcpy/video/SurfaceCapture.java | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 7385283e..0ec404eb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -80,14 +80,21 @@ public class CameraCapture extends SurfaceCapture { throw new IOException("No matching camera found"); } + Ln.i("Using camera '" + cameraId + "'"); + cameraDevice = openCamera(cameraId); + } catch (CameraAccessException | InterruptedException e) { + throw new IOException(e); + } + } + + @Override + public void prepare() throws IOException { + try { size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); if (size == null) { throw new IOException("Could not select camera size"); } - - Ln.i("Using camera '" + cameraId + "'"); - cameraDevice = openCamera(cameraId); - } catch (CameraAccessException | InterruptedException e) { + } catch (CameraAccessException e) { throw new IOException(e); } } @@ -232,13 +239,7 @@ public class CameraCapture extends SurfaceCapture { } this.maxSize = maxSize; - try { - size = selectSize(cameraId, null, maxSize, aspectRatio, highSpeed); - return size != null; - } catch (CameraAccessException e) { - Ln.w("Could not select camera size", e); - return false; - } + return true; } @SuppressLint("MissingPermission") diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index de9e1b27..d0d93f54 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -46,7 +46,7 @@ public abstract class SurfaceCapture { /** * Called once before each capture starts, before {@link #getSize()}. */ - public void prepare() throws ConfigurationException { + public void prepare() throws ConfigurationException, IOException { // empty by default } From 2337f524d167e210a2985d691e5695e5b7e3249f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 19:17:04 +0100 Subject: [PATCH 087/278] Improve error message on unknown camera id If the camera id is explicitly provided (via --camera-id), report a user-friendly error if no camera with this id is found. --- .../scrcpy/video/CameraCapture.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 0ec404eb..01afad7b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,9 +1,11 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.annotation.SuppressLint; @@ -68,7 +70,7 @@ public class CameraCapture extends SurfaceCapture { } @Override - protected void init() throws IOException { + protected void init() throws ConfigurationException, IOException { cameraThread = new HandlerThread("camera"); cameraThread.start(); cameraHandler = new Handler(cameraThread.getLooper()); @@ -77,7 +79,7 @@ public class CameraCapture extends SurfaceCapture { try { cameraId = selectCamera(explicitCameraId, cameraFacing); if (cameraId == null) { - throw new IOException("No matching camera found"); + throw new ConfigurationException("No matching camera found"); } Ln.i("Using camera '" + cameraId + "'"); @@ -99,14 +101,18 @@ public class CameraCapture extends SurfaceCapture { } } - private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException { - if (explicitCameraId != null) { - return explicitCameraId; - } - + private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException { CameraManager cameraManager = ServiceManager.getCameraManager(); String[] cameraIds = cameraManager.getCameraIdList(); + if (explicitCameraId != null) { + if (!Arrays.asList(cameraIds).contains(explicitCameraId)) { + Ln.e("Camera with id " + explicitCameraId + " not found\n" + LogUtils.buildCameraListMessage(false)); + throw new ConfigurationException("Camera id not found"); + } + return explicitCameraId; + } + if (cameraFacing == null) { // Use the first one return cameraIds.length > 0 ? cameraIds[0] : null; From 0e399b65bd2f32f93a0372ed0d64cdc2ba223d86 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 17:03:39 +0100 Subject: [PATCH 088/278] Remove [] around app package names This simplifies copy-pasting from the result of: scrcpy --list-apps --- server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 2b780caf..088be7e7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -236,7 +236,7 @@ public final class LogUtils { } else { builder.append("\n ").append(String.format("%" + column + "s", " ")); } - builder.append(" [").append(app.getPackageName()).append(']'); + builder.append(" ").append(app.getPackageName()); } return builder.toString(); From 5e10c37f02cb41054257dc895a5aae7d9df7dd89 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 11:53:12 +0100 Subject: [PATCH 089/278] Define all DisplayManager flags locally For consistency. --- .../com/genymobile/scrcpy/video/NewDisplayCapture.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index f6561a5f..3dc05ce7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -19,6 +19,8 @@ import java.io.IOException; public class NewDisplayCapture extends SurfaceCapture { // Internal fields copied from android.hardware.display.DisplayManager + private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; + private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; @@ -74,12 +76,11 @@ public class NewDisplayCapture extends SurfaceCapture { } } - public void startNew(Surface surface) { int virtualDisplayId; try { - int flags = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC - | DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY + int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC + | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL From 794595e3f0db8208a2a1df30c1c5e9442deb4112 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 20:20:10 +0100 Subject: [PATCH 090/278] Set displayId to NONE in Options on new display If a new display is set, force options.getDisplayId() to return Device.DISPLAY_ID_NONE, to avoid any confusion between a local displayId and options.getDisplayId(). --- .../src/main/java/com/genymobile/scrcpy/CleanUp.java | 11 ++++++----- .../src/main/java/com/genymobile/scrcpy/Options.java | 5 +++++ .../src/main/java/com/genymobile/scrcpy/Server.java | 12 +++++------- 3 files changed, 16 insertions(+), 12 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 90de8c2c..352f7c6b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -25,13 +25,13 @@ public final class CleanUp { private Thread thread; - private CleanUp(int displayId, Options options) { - thread = new Thread(() -> runCleanUp(displayId, options), "cleanup"); + private CleanUp(Options options) { + thread = new Thread(() -> runCleanUp(options), "cleanup"); thread.start(); } - public static CleanUp start(int displayId, Options options) { - return new CleanUp(displayId, options); + public static CleanUp start(Options options) { + return new CleanUp(options); } public void interrupt() { @@ -42,7 +42,7 @@ public final class CleanUp { thread.join(); } - private void runCleanUp(int displayId, Options options) { + private void runCleanUp(Options options) { boolean disableShowTouches = false; if (options.getShowTouches()) { try { @@ -93,6 +93,7 @@ public final class CleanUp { } boolean powerOffScreen = options.getPowerOffScreenOnClose(); + int displayId = options.getDisplayId(); try { run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index e75321e6..54888827 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -479,6 +479,11 @@ public class Options { } } + if (options.newDisplay != null) { + assert options.displayId == 0 : "Must not set both displayId and newDisplay"; + options.displayId = Device.DISPLAY_ID_NONE; + } + return options; } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a093fdf0..d0a340da 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -103,11 +103,8 @@ public final class Server { CleanUp cleanUp = null; - NewDisplay newDisplay = options.getNewDisplay(); - int displayId = newDisplay == null ? options.getDisplayId() : Device.DISPLAY_ID_NONE; - if (options.getCleanup()) { - cleanUp = CleanUp.start(displayId, options); + cleanUp = CleanUp.start(options); } int scid = options.getScid(); @@ -131,7 +128,7 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); - controller = new Controller(displayId, controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); asyncProcessors.add(controller); } @@ -161,11 +158,12 @@ public final class Server { options.getSendFrameMeta()); SurfaceCapture surfaceCapture; if (options.getVideoSource() == VideoSource.DISPLAY) { + NewDisplay newDisplay = options.getNewDisplay(); if (newDisplay != null) { surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize()); } else { - assert displayId != Device.DISPLAY_ID_NONE; - surfaceCapture = new ScreenCapture(controller, displayId, options.getMaxSize(), options.getCrop(), + assert options.getDisplayId() != Device.DISPLAY_ID_NONE; + surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(), options.getLockVideoOrientation()); } } else { From bd9d93194b04970655b22f1eb96d5ad2bd4f2c75 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 20:25:12 +0100 Subject: [PATCH 091/278] Pass Options instance directly Many constructors take a lot of parameters copied from Options. For simplicity, just pass the Options instance. --- .../java/com/genymobile/scrcpy/Server.java | 16 ++++++---------- .../genymobile/scrcpy/audio/AudioEncoder.java | 9 +++++---- .../genymobile/scrcpy/control/Controller.java | 9 +++++---- .../genymobile/scrcpy/video/CameraCapture.java | 18 +++++++++--------- .../scrcpy/video/NewDisplayCapture.java | 8 +++++--- .../genymobile/scrcpy/video/ScreenCapture.java | 13 ++++++++----- .../scrcpy/video/SurfaceEncoder.java | 14 +++++++------- 7 files changed, 45 insertions(+), 42 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index d0a340da..dae73b64 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -128,7 +128,7 @@ public final class Server { if (control) { ControlChannel controlChannel = connection.getControlChannel(); - controller = new Controller(options.getDisplayId(), controlChannel, cleanUp, options.getClipboardAutosync(), options.getPowerOn()); + controller = new Controller(controlChannel, cleanUp, options); asyncProcessors.add(controller); } @@ -147,8 +147,7 @@ public final class Server { if (audioCodec == AudioCodec.RAW) { audioRecorder = new AudioRawRecorder(audioCapture, audioStreamer); } else { - audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options.getAudioBitRate(), options.getAudioCodecOptions(), - options.getAudioEncoder()); + audioRecorder = new AudioEncoder(audioCapture, audioStreamer, options); } asyncProcessors.add(audioRecorder); } @@ -160,18 +159,15 @@ public final class Server { if (options.getVideoSource() == VideoSource.DISPLAY) { NewDisplay newDisplay = options.getNewDisplay(); if (newDisplay != null) { - surfaceCapture = new NewDisplayCapture(controller, newDisplay, options.getMaxSize()); + surfaceCapture = new NewDisplayCapture(controller, options); } else { assert options.getDisplayId() != Device.DISPLAY_ID_NONE; - surfaceCapture = new ScreenCapture(controller, options.getDisplayId(), options.getMaxSize(), options.getCrop(), - options.getLockVideoOrientation()); + surfaceCapture = new ScreenCapture(controller, options); } } else { - surfaceCapture = new CameraCapture(options.getCameraId(), options.getCameraFacing(), options.getCameraSize(), - options.getMaxSize(), options.getCameraAspectRatio(), options.getCameraFps(), options.getCameraHighSpeed()); + surfaceCapture = new CameraCapture(options); } - SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options.getVideoBitRate(), options.getMaxFps(), - options.getVideoCodecOptions(), options.getVideoEncoder(), options.getDownsizeOnError()); + SurfaceEncoder surfaceEncoder = new SurfaceEncoder(surfaceCapture, videoStreamer, options); asyncProcessors.add(surfaceEncoder); if (controller != null) { diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index fcc0c52f..267be60a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.audio; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Streamer; import com.genymobile.scrcpy.util.Codec; @@ -67,12 +68,12 @@ public final class AudioEncoder implements AsyncProcessor { private boolean ended; - public AudioEncoder(AudioCapture capture, Streamer streamer, int bitRate, List codecOptions, String encoderName) { + public AudioEncoder(AudioCapture capture, Streamer streamer, Options options) { this.capture = capture; this.streamer = streamer; - this.bitRate = bitRate; - this.codecOptions = codecOptions; - this.encoderName = encoderName; + this.bitRate = options.getAudioBitRate(); + this.codecOptions = options.getAudioCodecOptions(); + this.encoderName = options.getAudioEncoder(); } private static MediaFormat createFormat(String mimeType, int bitRate, List codecOptions) { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index b4ae07b6..573e8f52 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; import com.genymobile.scrcpy.CleanUp; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.Point; @@ -97,12 +98,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // Used for resetting video encoding on RESET_VIDEO message private SurfaceCapture surfaceCapture; - public Controller(int displayId, ControlChannel controlChannel, CleanUp cleanUp, boolean clipboardAutosync, boolean powerOn) { - this.displayId = displayId; + public Controller(ControlChannel controlChannel, CleanUp cleanUp, Options options) { + this.displayId = options.getDisplayId(); this.controlChannel = controlChannel; this.cleanUp = cleanUp; - this.clipboardAutosync = clipboardAutosync; - this.powerOn = powerOn; + this.clipboardAutosync = options.getClipboardAutosync(); + this.powerOn = options.getPowerOn(); initPointers(); sender = new DeviceMessageSender(controlChannel); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 01afad7b..ee4085e9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.HandlerExecutor; @@ -58,15 +59,14 @@ public class CameraCapture extends SurfaceCapture { private final AtomicBoolean disconnected = new AtomicBoolean(); - public CameraCapture(String explicitCameraId, CameraFacing cameraFacing, Size explicitSize, int maxSize, CameraAspectRatio aspectRatio, int fps, - boolean highSpeed) { - this.explicitCameraId = explicitCameraId; - this.cameraFacing = cameraFacing; - this.explicitSize = explicitSize; - this.maxSize = maxSize; - this.aspectRatio = aspectRatio; - this.fps = fps; - this.highSpeed = highSpeed; + public CameraCapture(Options options) { + this.explicitCameraId = options.getCameraId(); + this.cameraFacing = options.getCameraFacing(); + this.explicitSize = options.getCameraSize(); + this.maxSize = options.getMaxSize(); + this.aspectRatio = options.getCameraAspectRatio(); + this.fps = options.getCameraFps(); + this.highSpeed = options.getCameraHighSpeed(); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 3dc05ce7..9b1c9933 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.NewDisplay; @@ -43,10 +44,11 @@ public class NewDisplayCapture extends SurfaceCapture { private Size size; private int dpi; - public NewDisplayCapture(VirtualDisplayListener vdListener, NewDisplay newDisplay, int maxSize) { + public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; - this.newDisplay = newDisplay; - this.maxSize = maxSize; + this.newDisplay = options.getNewDisplay(); + assert newDisplay != null; + this.maxSize = options.getMaxSize(); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 48e594b7..00d855bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -1,8 +1,10 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; @@ -48,12 +50,13 @@ public class ScreenCapture extends SurfaceCapture { private IRotationWatcher rotationWatcher; private IDisplayFoldListener displayFoldListener; - public ScreenCapture(VirtualDisplayListener vdListener, int displayId, int maxSize, Rect crop, int lockVideoOrientation) { + public ScreenCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; - this.displayId = displayId; - this.maxSize = maxSize; - this.crop = crop; - this.lockVideoOrientation = lockVideoOrientation; + this.displayId = options.getDisplayId(); + assert displayId != Device.DISPLAY_ID_NONE; + this.maxSize = options.getMaxSize(); + this.crop = options.getCrop(); + this.lockVideoOrientation = options.getLockVideoOrientation(); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 8fadfa7b..62581d3d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.AsyncProcessor; +import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.device.Streamer; @@ -51,15 +52,14 @@ public class SurfaceEncoder implements AsyncProcessor { private final CaptureReset reset = new CaptureReset(); - public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, int videoBitRate, float maxFps, List codecOptions, - String encoderName, boolean downsizeOnError) { + public SurfaceEncoder(SurfaceCapture capture, Streamer streamer, Options options) { this.capture = capture; this.streamer = streamer; - this.videoBitRate = videoBitRate; - this.maxFps = maxFps; - this.codecOptions = codecOptions; - this.encoderName = encoderName; - this.downsizeOnError = downsizeOnError; + this.videoBitRate = options.getVideoBitRate(); + this.maxFps = options.getMaxFps(); + this.codecOptions = options.getVideoCodecOptions(); + this.encoderName = options.getVideoEncoder(); + this.downsizeOnError = options.getDownsizeOnError(); } private void streamCapture() throws IOException, ConfigurationException { From 5694562a74e068df17193f25c9b1dbdd9342e0c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 18 Nov 2024 18:47:57 +0100 Subject: [PATCH 092/278] Remove duplicate log The function prepareRetry() already logs a more detailed message: Retrying with -mXXXX... --- .../main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 62581d3d..a00a8236 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -116,7 +116,6 @@ public class SurfaceEncoder implements AsyncProcessor { if (!prepareRetry(size)) { throw e; } - Ln.i("Retrying..."); alive = true; } finally { reset.setRunningMediaCodec(null); From e411b74a16a12f95c52f0c7a4c9c4f0ab1a3bc8d Mon Sep 17 00:00:00 2001 From: Matthias Stock Date: Mon, 18 Nov 2024 18:10:17 +0100 Subject: [PATCH 093/278] Use explicit file protocol for AVIO AVIO expects a `url` to locate a resource. Use the file protocol to handle filenames containing colons. Fixes #5487 PR #5499 Signed-off-by: Romain Vimont --- app/src/recorder.c | 10 ++++++++-- app/src/util/str.c | 20 ++++++++++++++++++++ app/src/util/str.h | 9 +++++++++ app/tests/test_str.c | 11 +++++++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 9e0b3395..15f27157 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -143,8 +143,14 @@ sc_recorder_open_output_file(struct sc_recorder *recorder) { return false; } - int ret = avio_open(&recorder->ctx->pb, recorder->filename, - AVIO_FLAG_WRITE); + char *file_url = sc_str_concat("file:", recorder->filename); + if (!file_url) { + avformat_free_context(recorder->ctx); + return false; + } + + int ret = avio_open(&recorder->ctx->pb, file_url, AVIO_FLAG_WRITE); + free(file_url); if (ret < 0) { LOGE("Failed to open output file: %s", recorder->filename); avformat_free_context(recorder->ctx); diff --git a/app/src/util/str.c b/app/src/util/str.c index 755369d8..304cd302 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -64,6 +64,26 @@ sc_str_quote(const char *src) { return quoted; } +char * +sc_str_concat(const char *start, const char *end) { + assert(start); + assert(end); + + size_t start_len = strlen(start); + size_t end_len = strlen(end); + + char *result = malloc(start_len + end_len + 1); + if (!result) { + LOG_OOM(); + return NULL; + } + + memcpy(result, start, start_len); + memcpy(result + start_len, end, end_len + 1); + + return result; +} + bool sc_str_parse_integer(const char *s, long *out) { char *endptr; diff --git a/app/src/util/str.h b/app/src/util/str.h index 20da26f0..d20f1b28 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -38,6 +38,15 @@ sc_str_join(char *dst, const char *const tokens[], char sep, size_t n); char * sc_str_quote(const char *src); +/** + * Concat two strings + * + * Return a new allocated string, contanining the concatenation of the two + * input strings. + */ +char * +sc_str_concat(const char *start, const char *end); + /** * Parse `s` as an integer into `out` * diff --git a/app/tests/test_str.c b/app/tests/test_str.c index 5d365ef5..4a906d92 100644 --- a/app/tests/test_str.c +++ b/app/tests/test_str.c @@ -141,6 +141,16 @@ static void test_quote(void) { free(out); } +static void test_concat(void) { + const char *s = "2024:11"; + char *out = sc_str_concat("my-prefix:", s); + + // contains the concat + assert(!strcmp("my-prefix:2024:11", out)); + + free(out); +} + static void test_utf8_truncate(void) { const char *s = "aÉbÔc"; assert(strlen(s) == 7); // É and Ô are 2 bytes-wide @@ -389,6 +399,7 @@ int main(int argc, char *argv[]) { test_join_truncated_before_sep(); test_join_truncated_after_sep(); test_quote(); + test_concat(); test_utf8_truncate(); test_parse_integer(); test_parse_integers(); From 2a04858a2271fdb44a0eeaeeadaedfc966eab48d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 20 Oct 2024 16:51:32 +0200 Subject: [PATCH 094/278] Add on-device OpenGL video filter architecture MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce several key components to perform OpenGL filters: - OpenGLRunner: a tool for running a filter to be rendered to a Surface from an OpenGL-dedicated thread - OpenGLFilter: a simple OpenGL filter API - AffineOpenGLFilter: a generic OpenGL implementation to apply any 2D affine transform - AffineMatrix: an affine transform matrix, with helpers to build matrices from semantic transformations (rotate, scale, translate…) PR #5455 --- server/build_without_gradle.sh | 1 + .../java/com/genymobile/scrcpy/Server.java | 4 + .../scrcpy/opengl/AffineOpenGLFilter.java | 135 +++++++ .../com/genymobile/scrcpy/opengl/GLUtils.java | 124 ++++++ .../scrcpy/opengl/OpenGLException.java | 13 + .../scrcpy/opengl/OpenGLFilter.java | 21 + .../scrcpy/opengl/OpenGLRunner.java | 246 ++++++++++++ .../genymobile/scrcpy/util/AffineMatrix.java | 368 ++++++++++++++++++ 8 files changed, 912 insertions(+) create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java create mode 100644 server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index e0fc3a95..206aa604 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -60,6 +60,7 @@ SRC=( \ com/genymobile/scrcpy/audio/*.java \ com/genymobile/scrcpy/control/*.java \ com/genymobile/scrcpy/device/*.java \ + com/genymobile/scrcpy/opengl/*.java \ com/genymobile/scrcpy/util/*.java \ com/genymobile/scrcpy/video/*.java \ com/genymobile/scrcpy/wrappers/*.java \ diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index dae73b64..ca53d861 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -14,6 +14,7 @@ import com.genymobile.scrcpy.device.DesktopConnection; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Streamer; +import com.genymobile.scrcpy.opengl.OpenGLRunner; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.video.CameraCapture; @@ -191,6 +192,8 @@ public final class Server { asyncProcessor.stop(); } + OpenGLRunner.quit(); // quit the OpenGL thread, if any + connection.shutdown(); try { @@ -200,6 +203,7 @@ public final class Server { for (AsyncProcessor asyncProcessor : asyncProcessors) { asyncProcessor.join(); } + OpenGLRunner.join(); } catch (InterruptedException e) { // ignore } diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java b/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java new file mode 100644 index 00000000..7608a574 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/AffineOpenGLFilter.java @@ -0,0 +1,135 @@ +package com.genymobile.scrcpy.opengl; + +import com.genymobile.scrcpy.util.AffineMatrix; + +import android.opengl.GLES11Ext; +import android.opengl.GLES20; + +import java.nio.FloatBuffer; + +public class AffineOpenGLFilter implements OpenGLFilter { + + private int program; + private FloatBuffer vertexBuffer; + private FloatBuffer texCoordsBuffer; + private final float[] userMatrix; + + private int vertexPosLoc; + private int texCoordsInLoc; + + private int texLoc; + private int texMatrixLoc; + private int userMatrixLoc; + + public AffineOpenGLFilter(AffineMatrix transform) { + userMatrix = transform.to4x4(); + } + + @Override + public void init() throws OpenGLException { + // @formatter:off + String vertexShaderCode = "#version 100\n" + + "attribute vec4 vertex_pos;\n" + + "attribute vec4 tex_coords_in;\n" + + "varying vec2 tex_coords;\n" + + "uniform mat4 tex_matrix;\n" + + "uniform mat4 user_matrix;\n" + + "void main() {\n" + + " gl_Position = vertex_pos;\n" + + " tex_coords = (tex_matrix * user_matrix * tex_coords_in).xy;\n" + + "}"; + + // @formatter:off + String fragmentShaderCode = "#version 100\n" + + "#extension GL_OES_EGL_image_external : require\n" + + "precision highp float;\n" + + "uniform samplerExternalOES tex;\n" + + "varying vec2 tex_coords;\n" + + "void main() {\n" + + " if (tex_coords.x >= 0.0 && tex_coords.x <= 1.0\n" + + " && tex_coords.y >= 0.0 && tex_coords.y <= 1.0) {\n" + + " gl_FragColor = texture2D(tex, tex_coords);\n" + + " } else {\n" + + " gl_FragColor = vec4(0.0);\n" + + " }\n" + + "}"; + + program = GLUtils.createProgram(vertexShaderCode, fragmentShaderCode); + if (program == 0) { + throw new OpenGLException("Cannot create OpenGL program"); + } + + float[] vertices = { + -1, -1, // Bottom-left + 1, -1, // Bottom-right + -1, 1, // Top-left + 1, 1, // Top-right + }; + + float[] texCoords = { + 0, 0, // Bottom-left + 1, 0, // Bottom-right + 0, 1, // Top-left + 1, 1, // Top-right + }; + + // OpenGL will fill the 3rd and 4th coordinates of the vec4 automatically with 0.0 and 1.0 respectively + vertexBuffer = GLUtils.createFloatBuffer(vertices); + texCoordsBuffer = GLUtils.createFloatBuffer(texCoords); + + vertexPosLoc = GLES20.glGetAttribLocation(program, "vertex_pos"); + assert vertexPosLoc != -1; + + texCoordsInLoc = GLES20.glGetAttribLocation(program, "tex_coords_in"); + assert texCoordsInLoc != -1; + + texLoc = GLES20.glGetUniformLocation(program, "tex"); + assert texLoc != -1; + + texMatrixLoc = GLES20.glGetUniformLocation(program, "tex_matrix"); + assert texMatrixLoc != -1; + + userMatrixLoc = GLES20.glGetUniformLocation(program, "user_matrix"); + assert userMatrixLoc != -1; + } + + @Override + public void draw(int textureId, float[] texMatrix) { + GLES20.glUseProgram(program); + GLUtils.checkGlError(); + + GLES20.glEnableVertexAttribArray(vertexPosLoc); + GLUtils.checkGlError(); + GLES20.glEnableVertexAttribArray(texCoordsInLoc); + GLUtils.checkGlError(); + + GLES20.glVertexAttribPointer(vertexPosLoc, 2, GLES20.GL_FLOAT, false, 0, vertexBuffer); + GLUtils.checkGlError(); + GLES20.glVertexAttribPointer(texCoordsInLoc, 2, GLES20.GL_FLOAT, false, 0, texCoordsBuffer); + GLUtils.checkGlError(); + + GLES20.glActiveTexture(GLES20.GL_TEXTURE0); + GLUtils.checkGlError(); + GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, textureId); + GLUtils.checkGlError(); + GLES20.glUniform1i(texLoc, 0); + GLUtils.checkGlError(); + + GLES20.glUniformMatrix4fv(texMatrixLoc, 1, false, texMatrix, 0); + GLUtils.checkGlError(); + + GLES20.glUniformMatrix4fv(userMatrixLoc, 1, false, userMatrix, 0); + GLUtils.checkGlError(); + + GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT); + GLUtils.checkGlError(); + GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4); + GLUtils.checkGlError(); + } + + @Override + public void release() { + GLES20.glDeleteProgram(program); + GLUtils.checkGlError(); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java b/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java new file mode 100644 index 00000000..72a3f400 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/GLUtils.java @@ -0,0 +1,124 @@ +package com.genymobile.scrcpy.opengl; + +import com.genymobile.scrcpy.BuildConfig; +import com.genymobile.scrcpy.util.Ln; + +import android.opengl.GLES20; +import android.opengl.GLU; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.nio.FloatBuffer; + +public final class GLUtils { + + private static final boolean DEBUG = BuildConfig.DEBUG; + + private GLUtils() { + // not instantiable + } + + public static int createProgram(String vertexSource, String fragmentSource) { + int vertexShader = createShader(GLES20.GL_VERTEX_SHADER, vertexSource); + if (vertexShader == 0) { + return 0; + } + + int fragmentShader = createShader(GLES20.GL_FRAGMENT_SHADER, fragmentSource); + if (fragmentShader == 0) { + GLES20.glDeleteShader(vertexShader); + return 0; + } + + int program = GLES20.glCreateProgram(); + if (program == 0) { + GLES20.glDeleteShader(fragmentShader); + GLES20.glDeleteShader(vertexShader); + return 0; + } + + GLES20.glAttachShader(program, vertexShader); + checkGlError(); + GLES20.glAttachShader(program, fragmentShader); + checkGlError(); + GLES20.glLinkProgram(program); + checkGlError(); + + int[] linkStatus = new int[1]; + GLES20.glGetProgramiv(program, GLES20.GL_LINK_STATUS, linkStatus, 0); + if (linkStatus[0] == 0) { + Ln.e("Could not link program: " + GLES20.glGetProgramInfoLog(program)); + GLES20.glDeleteProgram(program); + GLES20.glDeleteShader(fragmentShader); + GLES20.glDeleteShader(vertexShader); + return 0; + } + + return program; + } + + public static int createShader(int type, String source) { + int shader = GLES20.glCreateShader(type); + if (shader == 0) { + Ln.e(getGlErrorMessage("Could not create shader")); + return 0; + } + + GLES20.glShaderSource(shader, source); + GLES20.glCompileShader(shader); + + int[] compileStatus = new int[1]; + GLES20.glGetShaderiv(shader, GLES20.GL_COMPILE_STATUS, compileStatus, 0); + if (compileStatus[0] == 0) { + Ln.e("Could not compile " + getShaderTypeString(type) + ": " + GLES20.glGetShaderInfoLog(shader)); + GLES20.glDeleteShader(shader); + return 0; + } + + return shader; + } + + private static String getShaderTypeString(int type) { + switch (type) { + case GLES20.GL_VERTEX_SHADER: + return "vertex shader"; + case GLES20.GL_FRAGMENT_SHADER: + return "fragment shader"; + default: + return "shader"; + } + } + + /** + * Throws a runtime exception if {@link GLES20#glGetError()} returns an error (useful for debugging). + */ + public static void checkGlError() { + if (DEBUG) { + int error = GLES20.glGetError(); + if (error != GLES20.GL_NO_ERROR) { + throw new RuntimeException(toErrorString(error)); + } + } + } + + public static String getGlErrorMessage(String userError) { + int glError = GLES20.glGetError(); + if (glError == GLES20.GL_NO_ERROR) { + return userError; + } + + return userError + " (" + toErrorString(glError) + ")"; + } + + private static String toErrorString(int glError) { + String errorString = GLU.gluErrorString(glError); + return "glError 0x" + Integer.toHexString(glError) + " " + errorString; + } + + public static FloatBuffer createFloatBuffer(float[] values) { + FloatBuffer fb = ByteBuffer.allocateDirect(values.length * 4).order(ByteOrder.nativeOrder()).asFloatBuffer(); + fb.put(values); + fb.position(0); + return fb; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java new file mode 100644 index 00000000..cbc9539b --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLException.java @@ -0,0 +1,13 @@ +package com.genymobile.scrcpy.opengl; + +import java.io.IOException; + +public class OpenGLException extends IOException { + public OpenGLException(String message) { + super(message); + } + + public OpenGLException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java new file mode 100644 index 00000000..6f27777e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLFilter.java @@ -0,0 +1,21 @@ +package com.genymobile.scrcpy.opengl; + +public interface OpenGLFilter { + + /** + * Initialize the OpenGL filter (typically compile the shaders and create the program). + * + * @throws OpenGLException if an initialization error occurs + */ + void init() throws OpenGLException; + + /** + * Render a frame (call for each frame). + */ + void draw(int textureId, float[] texMatrix); + + /** + * Release resources. + */ + void release(); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java new file mode 100644 index 00000000..a3f9335c --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java @@ -0,0 +1,246 @@ +package com.genymobile.scrcpy.opengl; + +import com.genymobile.scrcpy.device.Size; + +import android.graphics.SurfaceTexture; +import android.opengl.EGL14; +import android.opengl.EGLConfig; +import android.opengl.EGLContext; +import android.opengl.EGLDisplay; +import android.opengl.EGLExt; +import android.opengl.EGLSurface; +import android.opengl.GLES11Ext; +import android.opengl.GLES20; +import android.os.Handler; +import android.os.HandlerThread; +import android.view.Surface; + +import java.util.concurrent.Semaphore; + +public final class OpenGLRunner { + + private static HandlerThread handlerThread; + private static Handler handler; + private static boolean quit; + + private EGLDisplay eglDisplay; + private EGLContext eglContext; + private EGLSurface eglSurface; + + private final OpenGLFilter filter; + + private SurfaceTexture surfaceTexture; + private Surface inputSurface; + private int textureId; + + private boolean stopped; + + public OpenGLRunner(OpenGLFilter filter) { + this.filter = filter; + } + + public static synchronized void initOnce() { + if (handlerThread == null) { + if (quit) { + throw new IllegalStateException("Could not init OpenGLRunner after it is quit"); + } + handlerThread = new HandlerThread("OpenGLRunner"); + handlerThread.start(); + handler = new Handler(handlerThread.getLooper()); + } + } + + public static void quit() { + HandlerThread thread; + synchronized (OpenGLRunner.class) { + thread = handlerThread; + quit = true; + } + if (thread != null) { + thread.quitSafely(); + } + } + + public static void join() throws InterruptedException { + HandlerThread thread; + synchronized (OpenGLRunner.class) { + thread = handlerThread; + } + if (thread != null) { + thread.join(); + } + } + + public Surface start(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { + initOnce(); + + // Simulate CompletableFuture, but working for all Android versions + final Semaphore sem = new Semaphore(0); + Throwable[] throwableRef = new Throwable[1]; + + // The whole OpenGL execution must be performed on a Handler, so that SurfaceTexture.setOnFrameAvailableListener() works correctly. + // See + handler.post(() -> { + try { + run(inputSize, outputSize, outputSurface); + } catch (Throwable throwable) { + throwableRef[0] = throwable; + } finally { + sem.release(); + } + }); + + try { + sem.acquire(); + } catch (InterruptedException e) { + // Behave as if this method call was synchronous + Thread.currentThread().interrupt(); + } + + Throwable throwable = throwableRef[0]; + if (throwable != null) { + if (throwable instanceof OpenGLException) { + throw (OpenGLException) throwable; + } + throw new OpenGLException("Asynchronous OpenGL runner init failed", throwable); + } + + // Synchronization is ok: inputSurface is written before sem.release() and read after sem.acquire() + return inputSurface; + } + + private void run(Size inputSize, Size outputSize, Surface outputSurface) throws OpenGLException { + eglDisplay = EGL14.eglGetDisplay(EGL14.EGL_DEFAULT_DISPLAY); + if (eglDisplay == EGL14.EGL_NO_DISPLAY) { + throw new OpenGLException("Unable to get EGL14 display"); + } + + int[] version = new int[2]; + if (!EGL14.eglInitialize(eglDisplay, version, 0, version, 1)) { + throw new OpenGLException("Unable to initialize EGL14"); + } + + // @formatter:off + int[] attribList = { + EGL14.EGL_RED_SIZE, 8, + EGL14.EGL_GREEN_SIZE, 8, + EGL14.EGL_BLUE_SIZE, 8, + EGL14.EGL_ALPHA_SIZE, 8, + EGL14.EGL_RENDERABLE_TYPE, EGL14.EGL_OPENGL_ES2_BIT, + EGL14.EGL_NONE + }; + + EGLConfig[] configs = new EGLConfig[1]; + int[] numConfigs = new int[1]; + EGL14.eglChooseConfig(eglDisplay, attribList, 0, configs, 0, configs.length, numConfigs, 0); + if (numConfigs[0] <= 0) { + EGL14.eglTerminate(eglDisplay); + throw new OpenGLException("Unable to find ES2 EGL config"); + } + EGLConfig eglConfig = configs[0]; + + // @formatter:off + int[] contextAttribList = { + EGL14.EGL_CONTEXT_CLIENT_VERSION, 2, + EGL14.EGL_NONE + }; + eglContext = EGL14.eglCreateContext(eglDisplay, eglConfig, EGL14.EGL_NO_CONTEXT, contextAttribList, 0); + if (eglContext == null) { + EGL14.eglTerminate(eglDisplay); + throw new OpenGLException("Failed to create EGL context"); + } + + int[] surfaceAttribList = { + EGL14.EGL_NONE + }; + eglSurface = EGL14.eglCreateWindowSurface(eglDisplay, eglConfig, outputSurface, surfaceAttribList, 0); + if (eglSurface == null) { + EGL14.eglDestroyContext(eglDisplay, eglContext); + EGL14.eglTerminate(eglDisplay); + throw new OpenGLException("Failed to create EGL window surface"); + } + + if (!EGL14.eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) { + EGL14.eglDestroySurface(eglDisplay, eglSurface); + EGL14.eglDestroyContext(eglDisplay, eglContext); + EGL14.eglTerminate(eglDisplay); + throw new OpenGLException("Failed to make EGL context current"); + } + + int[] textures = new int[1]; + GLES20.glGenTextures(1, textures, 0); + GLUtils.checkGlError(); + textureId = textures[0]; + + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR); + GLUtils.checkGlError(); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR); + GLUtils.checkGlError(); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_S, GLES20.GL_CLAMP_TO_EDGE); + GLUtils.checkGlError(); + GLES20.glTexParameteri(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, GLES20.GL_TEXTURE_WRAP_T, GLES20.GL_CLAMP_TO_EDGE); + GLUtils.checkGlError(); + + surfaceTexture = new SurfaceTexture(textureId); + surfaceTexture.setDefaultBufferSize(inputSize.getWidth(), inputSize.getHeight()); + inputSurface = new Surface(surfaceTexture); + + filter.init(); + + surfaceTexture.setOnFrameAvailableListener(surfaceTexture -> { + if (stopped) { + // Make sure to never render after resources have been released + return; + } + + render(outputSize); + }, handler); + } + + private void render(Size outputSize) { + GLES20.glViewport(0, 0, outputSize.getWidth(), outputSize.getHeight()); + GLUtils.checkGlError(); + + surfaceTexture.updateTexImage(); + float[] matrix = new float[16]; + surfaceTexture.getTransformMatrix(matrix); + + filter.draw(textureId, matrix); + + EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, surfaceTexture.getTimestamp()); + EGL14.eglSwapBuffers(eglDisplay, eglSurface); + } + + public void stopAndRelease() { + final Semaphore sem = new Semaphore(0); + + handler.post(() -> { + stopped = true; + surfaceTexture.setOnFrameAvailableListener(null, handler); + + filter.release(); + + int[] textures = {textureId}; + GLES20.glDeleteTextures(1, textures, 0); + GLUtils.checkGlError(); + + EGL14.eglDestroySurface(eglDisplay, eglSurface); + EGL14.eglDestroyContext(eglDisplay, eglContext); + EGL14.eglTerminate(eglDisplay); + eglDisplay = EGL14.EGL_NO_DISPLAY; + eglContext = EGL14.EGL_NO_CONTEXT; + eglSurface = EGL14.EGL_NO_SURFACE; + surfaceTexture.release(); + inputSurface.release(); + + sem.release(); + }); + + try { + sem.acquire(); + } catch (InterruptedException e) { + // Behave as if this method call was synchronous + Thread.currentThread().interrupt(); + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java b/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java new file mode 100644 index 00000000..0db74af6 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/util/AffineMatrix.java @@ -0,0 +1,368 @@ +package com.genymobile.scrcpy.util; + +import com.genymobile.scrcpy.device.Point; +import com.genymobile.scrcpy.device.Size; + +/** + * Represents a 2D affine transform (a 3x3 matrix): + * + *

+ *     / a c e \
+ *     | b d f |
+ *     \ 0 0 1 /
+ * 
+ *

+ * Or, a 4x4 matrix if we add a z axis: + * + *

+ *     / a c 0 e \
+ *     | b d 0 f |
+ *     | 0 0 1 0 |
+ *     \ 0 0 0 1 /
+ * 
+ */ +public class AffineMatrix { + + private final double a, b, c, d, e, f; + + /** + * The identity matrix. + */ + public static final AffineMatrix IDENTITY = new AffineMatrix(1, 0, 0, 1, 0, 0); + + /** + * Create a new matrix: + * + *
+     *     / a c e \
+     *     | b d f |
+     *     \ 0 0 1 /
+     * 
+ */ + public AffineMatrix(double a, double b, double c, double d, double e, double f) { + this.a = a; + this.b = b; + this.c = c; + this.d = d; + this.e = e; + this.f = f; + } + + @Override + public String toString() { + return "[" + a + ", " + c + ", " + e + "; " + b + ", " + d + ", " + f + "]"; + } + + /** + * Return a matrix which converts from Normalized Device Coordinates to pixels. + * + * @param size the target size + * @return the transform matrix + */ + public static AffineMatrix ndcFromPixels(Size size) { + double w = size.getWidth(); + double h = size.getHeight(); + return new AffineMatrix(1 / w, 0, 0, -1 / h, 0, 1); + } + + /** + * Return a matrix which converts from pixels to Normalized Device Coordinates. + * + * @param size the source size + * @return the transform matrix + */ + public static AffineMatrix ndcToPixels(Size size) { + double w = size.getWidth(); + double h = size.getHeight(); + return new AffineMatrix(w, 0, 0, -h, 0, h); + } + + /** + * Apply the transform to a point ({@code this} should be a matrix converted to pixels coordinates via {@link #ndcToPixels(Size)}). + * + * @param point the source point + * @return the converted point + */ + public Point apply(Point point) { + int x = point.getX(); + int y = point.getY(); + int xx = (int) (a * x + c * y + e); + int yy = (int) (b * x + d * y + f); + return new Point(xx, yy); + } + + /** + * Compute this * rhs. + * + * @param rhs the matrix to multiply + * @return the product + */ + public AffineMatrix multiply(AffineMatrix rhs) { + if (rhs == null) { + // For convenience + return this; + } + + double aa = this.a * rhs.a + this.c * rhs.b; + double bb = this.b * rhs.a + this.d * rhs.b; + double cc = this.a * rhs.c + this.c * rhs.d; + double dd = this.b * rhs.c + this.d * rhs.d; + double ee = this.a * rhs.e + this.c * rhs.f + this.e; + double ff = this.b * rhs.e + this.d * rhs.f + this.f; + return new AffineMatrix(aa, bb, cc, dd, ee, ff); + } + + /** + * Multiply all matrices from left to right, ignoring any {@code null} matrix (for convenience). + * + * @param matrices the matrices + * @return the product + */ + public static AffineMatrix multiplyAll(AffineMatrix... matrices) { + AffineMatrix result = null; + for (AffineMatrix matrix : matrices) { + if (result == null) { + result = matrix; + } else { + result = result.multiply(matrix); + } + } + return result; + } + + /** + * Invert the matrix. + * + * @return the inverse matrix (or {@code null} if not invertible). + */ + public AffineMatrix invert() { + // The 3x3 matrix M can be decomposed into M = M1 * M2: + // M1 M2 + // / 1 0 e \ / a c 0 \ + // | 0 1 f | * | b d 0 | + // \ 0 0 1 / \ 0 0 1 / + // + // The inverse of an invertible 2x2 matrix is given by this formula: + // + // / A B \⁻¹ 1 / D -B \ + // \ C D / = ----- \ -C A / + // AD-BC + // + // Let B=c and C=b (to apply the general formula with the same letters). + // + // M⁻¹ = (M1 * M2)⁻¹ = M2⁻¹ * M1⁻¹ + // + // M2⁻¹ M1⁻¹ + // /----------------\ + // 1 / d -B 0 \ / 1 0 -e \ + // = ----- | -C a 0 | * | 0 1 -f | + // ad-BC \ 0 0 1 / \ 0 0 1 / + // + // With the original letters: + // + // 1 / d -c 0 \ / 1 0 -e \ + // M⁻¹ = ----- | -b a 0 | * | 0 1 -f | + // ad-cb \ 0 0 1 / \ 0 0 1 / + // + // 1 / d -c cf-de \ + // = ----- | -b a be-af | + // ad-cb \ 0 0 1 / + + double det = a * d - c * b; + if (det == 0) { + // Not invertible + return null; + } + + double aa = d / det; + double bb = -b / det; + double cc = -c / det; + double dd = a / det; + double ee = (c * f - d * e) / det; + double ff = (b * e - a * f) / det; + + return new AffineMatrix(aa, bb, cc, dd, ee, ff); + } + + /** + * Return this transform applied from the center (0.5, 0.5). + * + * @return the resulting matrix + */ + public AffineMatrix fromCenter() { + return translate(0.5, 0.5).multiply(this).multiply(translate(-0.5, -0.5)); + } + + /** + * Return this transform with the specified aspect ratio. + * + * @param ar the aspect ratio + * @return the resulting matrix + */ + public AffineMatrix withAspectRatio(double ar) { + return scale(1 / ar, 1).multiply(this).multiply(scale(ar, 1)); + } + + /** + * Return this transform with the specified aspect ratio. + * + * @param size the size describing the aspect ratio + * @return the transform + */ + public AffineMatrix withAspectRatio(Size size) { + double ar = (double) size.getWidth() / size.getHeight(); + return withAspectRatio(ar); + } + + /** + * Return a translation matrix. + * + * @param x the horizontal translation + * @param y the vertical translation + * @return the matrix + */ + public static AffineMatrix translate(double x, double y) { + return new AffineMatrix(1, 0, 0, 1, x, y); + } + + /** + * Return a scaling matrix. + * + * @param x the horizontal scaling + * @param y the vertical scaling + * @return the matrix + */ + public static AffineMatrix scale(double x, double y) { + return new AffineMatrix(x, 0, 0, y, 0, 0); + } + + /** + * Return a scaling matrix. + * + * @param from the source size + * @param to the destination size + * @return the matrix + */ + public static AffineMatrix scale(Size from, Size to) { + double scaleX = (double) to.getWidth() / from.getWidth(); + double scaleY = (double) to.getHeight() / from.getHeight(); + return scale(scaleX, scaleY); + } + + /** + * Return a matrix applying a "reframing" (cropping a rectangle). + *

+ * (x, y) is the bottom-left corner, (w, h) is the size of the rectangle. + * + * @param x horizontal coordinate (increasing to the right) + * @param y vertical coordinate (increasing upwards) + * @param w width + * @param h height + * @return the matrix + */ + public static AffineMatrix reframe(double x, double y, double w, double h) { + if (w == 0 || h == 0) { + throw new IllegalArgumentException("Cannot reframe to an empty area: " + w + "x" + h); + } + return scale(1 / w, 1 / h).multiply(translate(-x, -y)); + } + + /** + * Return an orthogonal rotation matrix. + * + * @param ccwRotation the counter-clockwise rotation + * @return the matrix + */ + public static AffineMatrix rotateOrtho(int ccwRotation) { + switch (ccwRotation) { + case 0: + return IDENTITY; + case 1: + // 90° counter-clockwise + return new AffineMatrix(0, 1, -1, 0, 1, 0); + case 2: + // 180° + return new AffineMatrix(-1, 0, 0, -1, 1, 1); + case 3: + // 90° clockwise + return new AffineMatrix(0, -1, 1, 0, 0, 1); + default: + throw new IllegalArgumentException("Invalid rotation: " + ccwRotation); + } + } + + /** + * Return an horizontal flip matrix. + * + * @return the matrix + */ + public static AffineMatrix hflip() { + return new AffineMatrix(-1, 0, 0, 1, 1, 0); + } + + /** + * Return a vertical flip matrix. + * + * @return the matrix + */ + public static AffineMatrix vflip() { + return new AffineMatrix(1, 0, 0, -1, 0, 1); + } + + /** + * Return a rotation matrix. + * + * @param ccwDegrees the angle, in degrees (counter-clockwise) + * @return the matrix + */ + public static AffineMatrix rotate(double ccwDegrees) { + double radians = Math.toRadians(ccwDegrees); + double cos = Math.cos(radians); + double sin = Math.sin(radians); + return new AffineMatrix(cos, sin, -sin, cos, 0, 0); + } + + /** + * Export this affine transform to a 4x4 column-major order matrix. + * + * @param matrix output 4x4 matrix + */ + public void to4x4(float[] matrix) { + // matrix is a 4x4 matrix in column-major order + + // Column 0 + matrix[0] = (float) a; + matrix[1] = (float) b; + matrix[2] = 0; + matrix[3] = 0; + + // Column 1 + matrix[4] = (float) c; + matrix[5] = (float) d; + matrix[6] = 0; + matrix[7] = 0; + + // Column 2 + matrix[8] = 0; + matrix[9] = 0; + matrix[10] = 1; + matrix[11] = 0; + + // Column 3 + matrix[12] = (float) e; + matrix[13] = (float) f; + matrix[14] = 0; + matrix[15] = 1; + } + + /** + * Export this affine transform to a 4x4 column-major order matrix. + * + * @return 4x4 matrix + */ + public float[] to4x4() { + float[] matrix = new float[16]; + to4x4(matrix); + return matrix; + } +} From 89518f49adb59dfa961f76be3f4414bffdc22cc5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 09:08:49 +0100 Subject: [PATCH 095/278] Revert "Disable broken options on Android 14" This reverts commit d62fa8880e03e8823057a5d4d9659d5f19132806. These options will be reimplemented differently. Refs #4011 Refs #4162 PR #5455 --- app/src/cli.c | 3 +-- app/src/options.h | 2 -- .../java/com/genymobile/scrcpy/Options.java | 6 +----- .../main/java/com/genymobile/scrcpy/Server.java | 17 ----------------- .../com/genymobile/scrcpy/device/Device.java | 2 -- .../com/genymobile/scrcpy/video/ScreenInfo.java | 2 +- 6 files changed, 3 insertions(+), 29 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index ebf0f6f6..e67192bf 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2856,8 +2856,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { LOGI("Video orientation is locked for v4l2 sink. " "See --lock-video-orientation."); - opts->lock_video_orientation = - SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO; + opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; } // V4L2 could not handle size change. diff --git a/app/src/options.h b/app/src/options.h index 5662719a..9236c3f8 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -134,8 +134,6 @@ enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, - // like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically - SC_LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3, SC_LOCK_VIDEO_ORIENTATION_0 = 0, SC_LOCK_VIDEO_ORIENTATION_90 = 3, SC_LOCK_VIDEO_ORIENTATION_180 = 2, diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 54888827..c1620432 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -32,7 +32,7 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; - private int lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED; + private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -259,10 +259,6 @@ public class Options { return sendCodecMeta; } - public void resetLockVideoOrientation() { - this.lockVideoOrientation = Device.LOCK_VIDEO_ORIENTATION_UNLOCKED; - } - @SuppressWarnings("MethodLength") public static Options parse(String... args) { if (args.length < 1) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index ca53d861..eb8b533a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -85,23 +85,6 @@ public final class Server { throw new ConfigurationException("New virtual display is not supported"); } - if (Build.VERSION.SDK_INT >= AndroidVersions.API_34_ANDROID_14) { - int lockVideoOrientation = options.getLockVideoOrientation(); - if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { - if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) { - Ln.e("--lock-video-orientation is broken on Android >= 14: "); - throw new ConfigurationException("--lock-video-orientation is broken on Android >= 14"); - } else { - // If the flag has been set automatically (because v4l2 sink is enabled), do not fail - Ln.w("--lock-video-orientation is ignored on Android >= 14: "); - } - } - if (options.getCrop() != null) { - Ln.e("--crop is broken on Android >= 14: "); - throw new ConfigurationException("Crop is not broken on Android >= 14"); - } - } - CleanUp cleanUp = null; if (options.getCleanup()) { diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index e7a743f8..09c7d2b6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -42,8 +42,6 @@ public final class Device { public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - // like SC_LOCK_VIDEO_ORIENTATION_INITIAL, but set automatically - public static final int LOCK_VIDEO_ORIENTATION_INITIAL_AUTO = -3; private Device() { // not instantiable diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index 602bd8ab..cc82a654 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -64,7 +64,7 @@ public final class ScreenInfo { } public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { - if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL || lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL_AUTO) { + if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation lockedVideoOrientation = rotation; } From d6033d28f5a326409218bab15acc432b305cb23f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 09:11:57 +0100 Subject: [PATCH 096/278] Split computeVideoSize() into limit() and round8() Expose two methods on Size directly: - limit() to downscale a size; - round8() to round both dimensions to multiples of 8. This will allow removing ScreenInfo completely. PR #5455 --- .../com/genymobile/scrcpy/device/Size.java | 51 +++++++++++++++++++ .../scrcpy/video/NewDisplayCapture.java | 2 +- .../genymobile/scrcpy/video/ScreenInfo.java | 30 +---------- 3 files changed, 53 insertions(+), 30 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 558deb00..3baa1bdd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -29,6 +29,57 @@ public final class Size { return new Size(height, width); } + public Size limit(int maxSize) { + assert maxSize >= 0 : "Max size may not be negative"; + assert maxSize % 8 == 0 : "Max size must be a multiple of 8"; + + if (maxSize == 0) { + // No limit + return this; + } + + boolean portrait = height > width; + int major = portrait ? height : width; + if (major <= maxSize) { + return this; + } + + int minor = portrait ? width : height; + + int newMajor = maxSize; + int newMinor = maxSize * minor / major; + + int w = portrait ? newMinor : newMajor; + int h = portrait ? newMajor : newMinor; + return new Size(w, h); + } + + /** + * Round both dimensions of this size to be a multiple of 8 (as required by many encoders). + * + * @return The current size rounded. + */ + public Size round8() { + if ((width & 7) == 0 && (height & 7) == 0) { + // Already a multiple of 8 + return this; + } + + boolean portrait = height > width; + int major = portrait ? height : width; + int minor = portrait ? width : height; + + major &= ~7; // round down to not exceed the initial size + minor = (minor + 4) & ~7; // round to the nearest to minimize aspect ratio distortion + if (minor > major) { + minor = major; + } + + int w = portrait ? minor : major; + int h = portrait ? major : minor; + return new Size(w, h); + } + public Rect toRect() { return new Rect(0, 0, width, height); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 9b1c9933..b9cecbe4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -71,7 +71,7 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public void prepare() { if (!newDisplay.hasExplicitSize()) { - size = ScreenInfo.computeVideoSize(mainDisplaySize.getWidth(), mainDisplaySize.getHeight(), maxSize); + size = mainDisplaySize.limit(maxSize).round8(); } if (!newDisplay.hasExplicitDpi()) { dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java index cc82a654..010ab59a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java @@ -1,6 +1,5 @@ package com.genymobile.scrcpy.video; -import com.genymobile.scrcpy.BuildConfig; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; @@ -82,7 +81,7 @@ public final class ScreenInfo { } } - Size videoSize = computeVideoSize(contentRect.width(), contentRect.height(), maxSize); + Size videoSize = new Size(contentRect.width(), contentRect.height()).limit(maxSize).round8(); return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); } @@ -90,33 +89,6 @@ public final class ScreenInfo { return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; } - public static Size computeVideoSize(int w, int h, int maxSize) { - // Compute the video size and the padding of the content inside this video. - // Principle: - // - scale down the great side of the screen to maxSize (if necessary); - // - scale down the other side so that the aspect ratio is preserved; - // - round this value to the nearest multiple of 8 (H.264 only accepts multiples of 8) - w &= ~7; // in case it's not a multiple of 8 - h &= ~7; - if (maxSize > 0) { - if (BuildConfig.DEBUG && maxSize % 8 != 0) { - throw new AssertionError("Max size must be a multiple of 8"); - } - boolean portrait = h > w; - int major = portrait ? h : w; - int minor = portrait ? w : h; - if (major > maxSize) { - int minorExact = minor * maxSize / major; - // +4 to round the value to the nearest multiple of 8 - minor = (minorExact + 4) & ~7; - major = maxSize; - } - w = portrait ? minor : major; - h = portrait ? major : minor; - } - return new Size(w, h); - } - private static Rect flipRect(Rect crop) { return new Rect(crop.top, crop.left, crop.bottom, crop.right); } From 019ce5eea4e8d0655ab481ab59ac498c3be51d0f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 10:04:01 +0100 Subject: [PATCH 097/278] Temporarily ignore lock video orientation and crop Get rid of old code implementing --lock-video-orientation and --crop features on the device side. They will be reimplemented differently. Refs #4011 Refs #4162 PR #5455 --- .../scrcpy/control/PositionMapper.java | 32 +---- .../scrcpy/video/NewDisplayCapture.java | 4 +- .../scrcpy/video/ScreenCapture.java | 31 ++--- .../genymobile/scrcpy/video/ScreenInfo.java | 121 ------------------ 4 files changed, 19 insertions(+), 169 deletions(-) delete mode 100644 server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java index 2ebb5961..7b546652 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -3,46 +3,28 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.video.ScreenInfo; - -import android.graphics.Rect; public final class PositionMapper { + private final Size sourceSize; private final Size videoSize; - private final Rect contentRect; - private final int coordsRotation; - public PositionMapper(Size videoSize, Rect contentRect, int videoRotation) { + public PositionMapper(Size sourceSize, Size videoSize) { + this.sourceSize = sourceSize; this.videoSize = videoSize; - this.contentRect = contentRect; - this.coordsRotation = reverseRotation(videoRotation); - } - - public static PositionMapper from(ScreenInfo screenInfo) { - // ignore the locked video orientation, the events will apply in coordinates considered in the physical device orientation - Size videoSize = screenInfo.getUnlockedVideoSize(); - return new PositionMapper(videoSize, screenInfo.getContentRect(), screenInfo.getVideoRotation()); - } - - private static int reverseRotation(int rotation) { - return (4 - rotation) % 4; } public Point map(Position position) { - // reverse the video rotation to apply the events - Position devicePosition = position.rotate(coordsRotation); - - Size clientVideoSize = devicePosition.getScreenSize(); + Size clientVideoSize = position.getScreenSize(); if (!videoSize.equals(clientVideoSize)) { // The client sends a click relative to a video with wrong dimensions, // the device may have been rotated since the event was generated, so ignore the event return null; } - Point point = devicePosition.getPoint(); - int convertedX = contentRect.left + point.getX() * contentRect.width() / videoSize.getWidth(); - int convertedY = contentRect.top + point.getY() * contentRect.height() / videoSize.getHeight(); + Point point = position.getPoint(); + int convertedX = point.getX() * sourceSize.getWidth() / videoSize.getWidth(); + int convertedY = point.getY() * sourceSize.getHeight() / videoSize.getHeight(); return new Point(convertedX, convertedY); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index b9cecbe4..f657d1a8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -9,7 +9,6 @@ import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.graphics.Rect; import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.os.Build; @@ -107,8 +106,7 @@ public class NewDisplayCapture extends SurfaceCapture { } if (vdListener != null) { - Rect contentRect = new Rect(0, 0, size.getWidth(), size.getHeight()); - PositionMapper positionMapper = new PositionMapper(size, contentRect, 0); + PositionMapper positionMapper = new PositionMapper(size, size); vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 00d855bd..00ee89d8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -28,11 +28,9 @@ public class ScreenCapture extends SurfaceCapture { private final VirtualDisplayListener vdListener; private final int displayId; private int maxSize; - private final Rect crop; - private final int lockVideoOrientation; private DisplayInfo displayInfo; - private ScreenInfo screenInfo; + private Size videoSize; // Source display size (before resizing/crop) for the current session private Size sessionDisplaySize; @@ -55,8 +53,6 @@ public class ScreenCapture extends SurfaceCapture { this.displayId = options.getDisplayId(); assert displayId != Device.DISPLAY_ID_NONE; this.maxSize = options.getMaxSize(); - this.crop = options.getCrop(); - this.lockVideoOrientation = options.getLockVideoOrientation(); } @Override @@ -126,8 +122,9 @@ public class ScreenCapture extends SurfaceCapture { Ln.w("Display doesn't have FLAG_SUPPORTS_PROTECTED_BUFFERS flag, mirroring can be restricted"); } - setSessionDisplaySize(displayInfo.getSize()); - screenInfo = ScreenInfo.computeScreenInfo(displayInfo.getRotation(), displayInfo.getSize(), crop, maxSize, lockVideoOrientation); + Size displaySize = displayInfo.getSize(); + setSessionDisplaySize(displaySize); + videoSize = displaySize.limit(maxSize).round8(); } @Override @@ -144,28 +141,22 @@ public class ScreenCapture extends SurfaceCapture { int virtualDisplayId; PositionMapper positionMapper; try { - Size videoSize = screenInfo.getVideoSize(); virtualDisplay = ServiceManager.getDisplayManager() .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - Rect contentRect = new Rect(0, 0, videoSize.getWidth(), videoSize.getHeight()); // The position are relative to the virtual display, not the original display - positionMapper = new PositionMapper(videoSize, contentRect, 0); + positionMapper = new PositionMapper(videoSize, videoSize); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { display = createDisplay(); - Rect contentRect = screenInfo.getContentRect(); - - // does not include the locked video orientation - Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); - int videoRotation = screenInfo.getVideoRotation(); + Size deviceSize = displayInfo.getSize(); int layerStack = displayInfo.getLayerStack(); - setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); + setDisplaySurface(display, surface, deviceSize.toRect(), videoSize.toRect(), layerStack); virtualDisplayId = displayId; - positionMapper = PositionMapper.from(screenInfo); + positionMapper = new PositionMapper(deviceSize, videoSize); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -206,7 +197,7 @@ public class ScreenCapture extends SurfaceCapture { @Override public Size getSize() { - return screenInfo.getVideoSize(); + return videoSize; } @Override @@ -223,11 +214,11 @@ public class ScreenCapture extends SurfaceCapture { return SurfaceControl.createDisplay("scrcpy", secure); } - private static void setDisplaySurface(IBinder display, Surface surface, int orientation, Rect deviceRect, Rect displayRect, int layerStack) { + private static void setDisplaySurface(IBinder display, Surface surface, Rect deviceRect, Rect displayRect, int layerStack) { SurfaceControl.openTransaction(); try { SurfaceControl.setDisplaySurface(display, surface); - SurfaceControl.setDisplayProjection(display, orientation, deviceRect, displayRect); + SurfaceControl.setDisplayProjection(display, 0, deviceRect, displayRect); SurfaceControl.setDisplayLayerStack(display, layerStack); } finally { SurfaceControl.closeTransaction(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java deleted file mode 100644 index 010ab59a..00000000 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenInfo.java +++ /dev/null @@ -1,121 +0,0 @@ -package com.genymobile.scrcpy.video; - -import com.genymobile.scrcpy.device.Device; -import com.genymobile.scrcpy.device.Size; -import com.genymobile.scrcpy.util.Ln; - -import android.graphics.Rect; - -public final class ScreenInfo { - /** - * Device (physical) size, possibly cropped - */ - private final Rect contentRect; // device size, possibly cropped - - /** - * Video size, possibly smaller than the device size, already taking the device rotation and crop into account. - *

- * However, it does not include the locked video orientation. - */ - private final Size unlockedVideoSize; - - /** - * Device rotation, related to the natural device orientation (0, 1, 2 or 3) - */ - private final int deviceRotation; - - /** - * The locked video orientation (-1: disabled, 0: normal, 1: 90° CCW, 2: 180°, 3: 90° CW) - */ - private final int lockedVideoOrientation; - - public ScreenInfo(Rect contentRect, Size unlockedVideoSize, int deviceRotation, int lockedVideoOrientation) { - this.contentRect = contentRect; - this.unlockedVideoSize = unlockedVideoSize; - this.deviceRotation = deviceRotation; - this.lockedVideoOrientation = lockedVideoOrientation; - } - - public Rect getContentRect() { - return contentRect; - } - - /** - * Return the video size as if locked video orientation was not set. - * - * @return the unlocked video size - */ - public Size getUnlockedVideoSize() { - return unlockedVideoSize; - } - - /** - * Return the actual video size if locked video orientation is set. - * - * @return the actual video size - */ - public Size getVideoSize() { - if (getVideoRotation() % 2 == 0) { - return unlockedVideoSize; - } - - return unlockedVideoSize.rotate(); - } - - public static ScreenInfo computeScreenInfo(int rotation, Size deviceSize, Rect crop, int maxSize, int lockedVideoOrientation) { - if (lockedVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { - // The user requested to lock the video orientation to the current orientation - lockedVideoOrientation = rotation; - } - - Rect contentRect = new Rect(0, 0, deviceSize.getWidth(), deviceSize.getHeight()); - if (crop != null) { - if (rotation % 2 != 0) { // 180s preserve dimensions - // the crop (provided by the user) is expressed in the natural orientation - crop = flipRect(crop); - } - if (!contentRect.intersect(crop)) { - // intersect() changes contentRect so that it is intersected with crop - Ln.w("Crop rectangle (" + formatCrop(crop) + ") does not intersect device screen (" + formatCrop(deviceSize.toRect()) + ")"); - contentRect = new Rect(); // empty - } - } - - Size videoSize = new Size(contentRect.width(), contentRect.height()).limit(maxSize).round8(); - return new ScreenInfo(contentRect, videoSize, rotation, lockedVideoOrientation); - } - - private static String formatCrop(Rect rect) { - return rect.width() + ":" + rect.height() + ":" + rect.left + ":" + rect.top; - } - - private static Rect flipRect(Rect crop) { - return new Rect(crop.top, crop.left, crop.bottom, crop.right); - } - - /** - * Return the rotation to apply to the device rotation to get the requested locked video orientation - * - * @return the rotation offset - */ - public int getVideoRotation() { - if (lockedVideoOrientation == -1) { - // no offset - return 0; - } - return (deviceRotation + 4 - lockedVideoOrientation) % 4; - } - - /** - * Return the rotation to apply to the requested locked video orientation to get the device rotation - * - * @return the (reverse) rotation offset - */ - public int getReverseVideoRotation() { - if (lockedVideoOrientation == -1) { - // no offset - return 0; - } - return (lockedVideoOrientation + 4 - deviceRotation) % 4; - } -} From e226950cfab9b9e8f2fe8620dc9970bcf13133ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 13:47:14 +0100 Subject: [PATCH 098/278] Make PositionMapper use affine transforms This will allow applying transformations performed by video filters. PR #5455 --- .../genymobile/scrcpy/control/PositionMapper.java | 14 ++++++++------ .../genymobile/scrcpy/video/NewDisplayCapture.java | 2 +- .../com/genymobile/scrcpy/video/ScreenCapture.java | 7 +++++-- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java index 7b546652..cf9b25ab 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -3,15 +3,16 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.AffineMatrix; public final class PositionMapper { - private final Size sourceSize; private final Size videoSize; + private final AffineMatrix videoToDeviceMatrix; - public PositionMapper(Size sourceSize, Size videoSize) { - this.sourceSize = sourceSize; + public PositionMapper(Size videoSize, AffineMatrix videoToDeviceMatrix) { this.videoSize = videoSize; + this.videoToDeviceMatrix = videoToDeviceMatrix; } public Point map(Position position) { @@ -23,8 +24,9 @@ public final class PositionMapper { } Point point = position.getPoint(); - int convertedX = point.getX() * sourceSize.getWidth() / videoSize.getWidth(); - int convertedY = point.getY() * sourceSize.getHeight() / videoSize.getHeight(); - return new Point(convertedX, convertedY); + if (videoToDeviceMatrix != null) { + point = videoToDeviceMatrix.apply(point); + } + return point; } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index f657d1a8..cc54876a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -106,7 +106,7 @@ public class NewDisplayCapture extends SurfaceCapture { } if (vdListener != null) { - PositionMapper positionMapper = new PositionMapper(size, size); + PositionMapper positionMapper = new PositionMapper(size, null); vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 00ee89d8..8873cb6d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -7,6 +7,7 @@ import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.wrappers.DisplayManager; @@ -145,7 +146,7 @@ public class ScreenCapture extends SurfaceCapture { .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); // The position are relative to the virtual display, not the original display - positionMapper = new PositionMapper(videoSize, videoSize); + positionMapper = new PositionMapper(videoSize, null); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -156,7 +157,9 @@ public class ScreenCapture extends SurfaceCapture { setDisplaySurface(display, surface, deviceSize.toRect(), videoSize.toRect(), layerStack); virtualDisplayId = displayId; - positionMapper = new PositionMapper(deviceSize, videoSize); + + AffineMatrix videoToDeviceMatrix = videoSize.equals(deviceSize) ? null : AffineMatrix.scale(videoSize, deviceSize); + positionMapper = new PositionMapper(videoSize, videoToDeviceMatrix); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); From 904f86152ea7693f481e29cdfd8327ac58eb8c34 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 09:00:32 +0100 Subject: [PATCH 099/278] Move mediaCodec.stop() to finally block This will allow stopping MediaCodec only after the cleanup of other components which must be performed beforehand. PR #5455 --- .../com/genymobile/scrcpy/video/SurfaceEncoder.java | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index a00a8236..dcb5d648 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -86,6 +86,7 @@ public class SurfaceEncoder implements AsyncProcessor { format.setInteger(MediaFormat.KEY_HEIGHT, size.getHeight()); Surface surface = null; + boolean mediaCodecStarted = false; try { mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); surface = mediaCodec.createInputSurface(); @@ -93,6 +94,7 @@ public class SurfaceEncoder implements AsyncProcessor { capture.start(surface); mediaCodec.start(); + mediaCodecStarted = true; // Set the MediaCodec instance to "interrupt" (by signaling an EOS) on reset reset.setRunningMediaCodec(mediaCodec); @@ -108,9 +110,6 @@ public class SurfaceEncoder implements AsyncProcessor { // The capture might have been closed internally (for example if the camera is disconnected) alive = !stopped.get() && !capture.isClosed(); } - - // do not call stop() on exception, it would trigger an IllegalStateException - mediaCodec.stop(); } catch (IllegalStateException | IllegalArgumentException e) { Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!prepareRetry(size)) { @@ -119,6 +118,13 @@ public class SurfaceEncoder implements AsyncProcessor { alive = true; } finally { reset.setRunningMediaCodec(null); + if (mediaCodecStarted) { + try { + mediaCodec.stop(); + } catch (IllegalStateException e) { + // ignore (just in case) + } + } mediaCodec.reset(); if (surface != null) { surface.release(); From 23960ca11ab676dc85f6c0f6e6f447efed92bba9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 11:40:31 +0100 Subject: [PATCH 100/278] Ignore signalEndOfStream() error This may be called at any time to interrupt the current encoding, including when MediaCodec is in an expected state. PR #5455 --- .../main/java/com/genymobile/scrcpy/video/CaptureReset.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java index c11e2e80..79d32d7c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CaptureReset.java @@ -18,7 +18,11 @@ public class CaptureReset implements SurfaceCapture.CaptureListener { public synchronized void reset() { reset.set(true); if (runningMediaCodec != null) { - runningMediaCodec.signalEndOfInputStream(); + try { + runningMediaCodec.signalEndOfInputStream(); + } catch (IllegalStateException e) { + // ignore + } } } From 9fb0a3dac1770c51758f578bca0b8e349e164e84 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 9 Nov 2024 15:14:52 +0100 Subject: [PATCH 101/278] Reimplement crop using transforms Reimplement the --crop feature using affine transforms. Fixes #4162 PR #5455 --- .../scrcpy/control/PositionMapper.java | 12 ++++ .../scrcpy/video/ScreenCapture.java | 56 ++++++++++++--- .../scrcpy/video/SurfaceCapture.java | 7 ++ .../scrcpy/video/SurfaceEncoder.java | 5 ++ .../genymobile/scrcpy/video/VideoFilter.java | 69 +++++++++++++++++++ 5 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java index cf9b25ab..4d3b8875 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -15,6 +15,18 @@ public final class PositionMapper { this.videoToDeviceMatrix = videoToDeviceMatrix; } + public static PositionMapper create(Size videoSize, AffineMatrix filterTransform, Size targetSize) { + boolean convertToPixels = !videoSize.equals(targetSize) || filterTransform != null; + AffineMatrix transform = filterTransform; + if (convertToPixels) { + AffineMatrix inputTransform = AffineMatrix.ndcFromPixels(videoSize); + AffineMatrix outputTransform = AffineMatrix.ndcToPixels(targetSize); + transform = outputTransform.multiply(transform).multiply(inputTransform); + } + + return new PositionMapper(videoSize, transform); + } + public Point map(Position position) { Size clientVideoSize = position.getScreenSize(); if (!videoSize.equals(clientVideoSize)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 8873cb6d..79d4974d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -7,6 +7,9 @@ import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLRunner; import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; @@ -24,11 +27,14 @@ import android.view.IDisplayFoldListener; import android.view.IRotationWatcher; import android.view.Surface; +import java.io.IOException; + public class ScreenCapture extends SurfaceCapture { private final VirtualDisplayListener vdListener; private final int displayId; private int maxSize; + private final Rect crop; private DisplayInfo displayInfo; private Size videoSize; @@ -39,6 +45,9 @@ public class ScreenCapture extends SurfaceCapture { private IBinder display; private VirtualDisplay virtualDisplay; + private AffineMatrix transform; + private OpenGLRunner glRunner; + private DisplayManager.DisplayListenerHandle displayListenerHandle; private HandlerThread handlerThread; @@ -54,6 +63,7 @@ public class ScreenCapture extends SurfaceCapture { this.displayId = options.getDisplayId(); assert displayId != Device.DISPLAY_ID_NONE; this.maxSize = options.getMaxSize(); + this.crop = options.getCrop(); } @Override @@ -125,11 +135,20 @@ public class ScreenCapture extends SurfaceCapture { Size displaySize = displayInfo.getSize(); setSessionDisplaySize(displaySize); - videoSize = displaySize.limit(maxSize).round8(); + + VideoFilter filter = new VideoFilter(displaySize); + + if (crop != null) { + boolean transposed = (displayInfo.getRotation() % 2) != 0; + filter.addCrop(crop, transposed); + } + + transform = filter.getInverseTransform(); + videoSize = filter.getOutputSize().limit(maxSize).round8(); } @Override - public void start(Surface surface) { + public void start(Surface surface) throws IOException { if (display != null) { SurfaceControl.destroyDisplay(display); display = null; @@ -139,14 +158,28 @@ public class ScreenCapture extends SurfaceCapture { virtualDisplay = null; } + Size inputSize; + if (transform != null) { + // If there is a filter, it must receive the full display content + inputSize = displayInfo.getSize(); + assert glRunner == null; + OpenGLFilter glFilter = new AffineOpenGLFilter(transform); + glRunner = new OpenGLRunner(glFilter); + surface = glRunner.start(inputSize, videoSize, surface); + } else { + // If there is no filter, the display must be rendered at target video size directly + inputSize = videoSize; + } + int virtualDisplayId; PositionMapper positionMapper; try { virtualDisplay = ServiceManager.getDisplayManager() - .createVirtualDisplay("scrcpy", videoSize.getWidth(), videoSize.getHeight(), displayId, surface); + .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - // The position are relative to the virtual display, not the original display - positionMapper = new PositionMapper(videoSize, null); + + // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) + positionMapper = PositionMapper.create(videoSize, transform, inputSize); Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -155,11 +188,10 @@ public class ScreenCapture extends SurfaceCapture { Size deviceSize = displayInfo.getSize(); int layerStack = displayInfo.getLayerStack(); - setDisplaySurface(display, surface, deviceSize.toRect(), videoSize.toRect(), layerStack); + setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack); virtualDisplayId = displayId; - AffineMatrix videoToDeviceMatrix = videoSize.equals(deviceSize) ? null : AffineMatrix.scale(videoSize, deviceSize); - positionMapper = new PositionMapper(videoSize, videoToDeviceMatrix); + positionMapper = PositionMapper.create(videoSize, transform, deviceSize); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -173,6 +205,14 @@ public class ScreenCapture extends SurfaceCapture { } } + @Override + public void stop() { + if (glRunner != null) { + glRunner.stopAndRelease(); + glRunner = null; + } + } + @Override public void release() { if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java index d0d93f54..39d3bdb8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceCapture.java @@ -57,6 +57,13 @@ public abstract class SurfaceCapture { */ public abstract void start(Surface surface) throws IOException; + /** + * Stop the capture. + */ + public void stop() { + // Do nothing by default + } + /** * Return the video size * diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index dcb5d648..bc120107 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -87,11 +87,13 @@ public class SurfaceEncoder implements AsyncProcessor { Surface surface = null; boolean mediaCodecStarted = false; + boolean captureStarted = false; try { mediaCodec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); surface = mediaCodec.createInputSurface(); capture.start(surface); + captureStarted = true; mediaCodec.start(); mediaCodecStarted = true; @@ -118,6 +120,9 @@ public class SurfaceEncoder implements AsyncProcessor { alive = true; } finally { reset.setRunningMediaCodec(null); + if (captureStarted) { + capture.stop(); + } if (mediaCodecStarted) { try { mediaCodec.stop(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java new file mode 100644 index 00000000..5a52231f --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -0,0 +1,69 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.AffineMatrix; + +import android.graphics.Rect; + +public class VideoFilter { + + private Size size; + private AffineMatrix transform; + + public VideoFilter(Size inputSize) { + this.size = inputSize; + } + + public Size getOutputSize() { + return size; + } + + public AffineMatrix getTransform() { + return transform; + } + + /** + * Return the inverse transform. + *

+ * The direct affine transform describes how the input image is transformed. + *

+ * It is often useful to retrieve the inverse transform instead: + *

    + *
  • The OpenGL filter expects the matrix to transform the image coordinates, which is the inverse transform;
  • + *
  • The click positions must be transformed back to the device positions, using the inverse transform too.
  • + *
+ * + * @return the inverse transform + */ + public AffineMatrix getInverseTransform() { + if (transform == null) { + return null; + } + return transform.invert(); + } + + private static Rect transposeRect(Rect rect) { + return new Rect(rect.top, rect.left, rect.bottom, rect.right); + } + + public void addCrop(Rect crop, boolean transposed) { + if (transposed) { + crop = transposeRect(crop); + } + + double inputWidth = size.getWidth(); + double inputHeight = size.getHeight(); + + if (crop.left < 0 || crop.top < 0 || crop.right > inputWidth || crop.bottom > inputHeight) { + throw new IllegalArgumentException("Crop " + crop + " exceeds the input area (" + size + ")"); + } + + double x = crop.left / inputWidth; + double y = 1 - (crop.bottom / inputHeight); // OpenGL origin is bottom-left + double w = crop.width() / inputWidth; + double h = crop.height() / inputHeight; + + transform = AffineMatrix.reframe(x, y, w, h).multiply(transform); + size = new Size(crop.width(), crop.height()); + } +} From 06385ce83bcfd57ac82e2f5ca4d9575fe1f1649f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 16:21:59 +0100 Subject: [PATCH 102/278] Reimplement lock orientation using transforms Reimplement the --lock-video-orientation feature using affine transforms. Fixes #4011 PR #5455 --- .../genymobile/scrcpy/video/ScreenCapture.java | 11 +++++++++++ .../com/genymobile/scrcpy/video/VideoFilter.java | 16 ++++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 79d4974d..bc0f825a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -35,6 +35,7 @@ public class ScreenCapture extends SurfaceCapture { private final int displayId; private int maxSize; private final Rect crop; + private int lockVideoOrientation; private DisplayInfo displayInfo; private Size videoSize; @@ -64,6 +65,7 @@ public class ScreenCapture extends SurfaceCapture { assert displayId != Device.DISPLAY_ID_NONE; this.maxSize = options.getMaxSize(); this.crop = options.getCrop(); + this.lockVideoOrientation = options.getLockVideoOrientation(); } @Override @@ -136,6 +138,11 @@ public class ScreenCapture extends SurfaceCapture { Size displaySize = displayInfo.getSize(); setSessionDisplaySize(displaySize); + if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + // The user requested to lock the video orientation to the current orientation + lockVideoOrientation = displayInfo.getRotation(); + } + VideoFilter filter = new VideoFilter(displaySize); if (crop != null) { @@ -143,6 +150,10 @@ public class ScreenCapture extends SurfaceCapture { filter.addCrop(crop, transposed); } + if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { + filter.addLockVideoOrientation(lockVideoOrientation, displayInfo.getRotation()); + } + transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 5a52231f..2d570446 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -66,4 +66,20 @@ public class VideoFilter { transform = AffineMatrix.reframe(x, y, w, h).multiply(transform); size = new Size(crop.width(), crop.height()); } + + public void addRotation(int ccwRotation) { + if (ccwRotation == 0) { + return; + } + + transform = AffineMatrix.rotateOrtho(ccwRotation).multiply(transform); + if (ccwRotation % 2 != 0) { + size = size.rotate(); + } + } + + public void addLockVideoOrientation(int lockVideoOrientation, int displayRotation) { + int ccwRotation = (4 + lockVideoOrientation - displayRotation) % 4; + addRotation(ccwRotation); + } } From d72686c867dcde973f4666117128f2fc38888a1b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Nov 2024 21:47:07 +0100 Subject: [PATCH 103/278] Extract display size monitor Detecting display size changes is not straightforward: - from a DisplayListener, "display changed" events are received, but this does not imply that the size has changed (it must be checked); - on Android 14 (see e26bdb07a21493d096ea5c8cfd870fc5a3f015dc), "display changed" events are not received on some versions, so as a fallback, a RotationWatcher and a DisplayFoldListener are registered, but unregistered as soon as a "display changed" event is actually received, which means that the problem is fixed. Extract a "display size monitor" to share the code between screen capture and virtual display capture. PR #5455 --- .../scrcpy/video/DisplaySizeMonitor.java | 189 ++++++++++++++++++ .../scrcpy/video/ScreenCapture.java | 146 +------------- 2 files changed, 193 insertions(+), 142 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java new file mode 100644 index 00000000..df8be323 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java @@ -0,0 +1,189 @@ +package com.genymobile.scrcpy.video; + +import com.genymobile.scrcpy.AndroidVersions; +import com.genymobile.scrcpy.device.Device; +import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.util.Ln; +import com.genymobile.scrcpy.wrappers.DisplayManager; +import com.genymobile.scrcpy.wrappers.ServiceManager; + +import android.os.Build; +import android.os.Handler; +import android.os.HandlerThread; +import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; + +public class DisplaySizeMonitor { + + public interface Listener { + void onDisplaySizeChanged(); + } + + private DisplayManager.DisplayListenerHandle displayListenerHandle; + private HandlerThread handlerThread; + + // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really + // detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from + // DisplayListener (which proves that it works). + private boolean displayListenerWorks; // only accessed from the display listener thread + private IRotationWatcher rotationWatcher; + private IDisplayFoldListener displayFoldListener; + + private int displayId = Device.DISPLAY_ID_NONE; + + private Size sessionDisplaySize; + + private Listener listener; + + public void start(int displayId, Listener listener) { + // Once started, the listener and the displayId must never change + assert listener != null; + this.listener = listener; + + assert this.displayId == Device.DISPLAY_ID_NONE; + this.displayId = displayId; + + handlerThread = new HandlerThread("DisplayListener"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + registerDisplayListenerFallbacks(); + } + + displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); + } + + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + if (!displayListenerWorks) { + // On the first display listener event, we know it works, we can unregister the fallbacks + displayListenerWorks = true; + unregisterDisplayListenerFallbacks(); + } + } + + if (eventDisplayId == displayId) { + checkDisplaySizeChanged(); + } + }, handler); + } + + /** + * Stop and release the monitor. + *

+ * It must not be used anymore. + * It is ok to call this method even if {@link #start(int, Listener)} was not called. + */ + public void stopAndRelease() { + if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { + unregisterDisplayListenerFallbacks(); + } + + // displayListenerHandle may be null if registration failed + if (displayListenerHandle != null) { + ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); + displayListenerHandle = null; + } + + if (handlerThread != null) { + handlerThread.quitSafely(); + } + } + + private synchronized Size getSessionDisplaySize() { + return sessionDisplaySize; + } + + public synchronized void setSessionDisplaySize(Size sessionDisplaySize) { + this.sessionDisplaySize = sessionDisplaySize; + } + + private void checkDisplaySizeChanged() { + DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); + if (di == null) { + Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); + // We can't compare with the current size, so reset unconditionally + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); + } + setSessionDisplaySize(null); + listener.onDisplaySizeChanged(); + } else { + Size size = di.getSize(); + + // The field is hidden on purpose, to read it with synchronization + @SuppressWarnings("checkstyle:HiddenField") + Size sessionDisplaySize = getSessionDisplaySize(); // synchronized + + // .equals() also works if sessionDisplaySize == null + if (!size.equals(sessionDisplaySize)) { + // Reset only if the size is different + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: requestReset(): " + sessionDisplaySize + " -> " + size); + } + // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() + // considers that the current size is the requested size (to avoid a duplicate requestReset()) + setSessionDisplaySize(size); + listener.onDisplaySizeChanged(); + } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: Size not changed (" + size + "): do not requestReset()"); + } + } + } + + private void registerDisplayListenerFallbacks() { + rotationWatcher = new IRotationWatcher.Stub() { + @Override + public void onRotationChanged(int rotation) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onRotationChanged(" + rotation + ")"); + } + + checkDisplaySizeChanged(); + } + }; + ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); + + // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) + displayFoldListener = new IDisplayFoldListener.Stub() { + + private boolean first = true; + + @Override + public void onDisplayFoldChanged(int displayId, boolean folded) { + if (first) { + // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. + first = false; + return; + } + + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onDisplayFoldChanged(" + displayId + ", " + folded + ")"); + } + + if (DisplaySizeMonitor.this.displayId != displayId) { + // Ignore events related to other display ids + return; + } + + checkDisplaySizeChanged(); + } + }; + ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); + } + + private synchronized void unregisterDisplayListenerFallbacks() { + if (rotationWatcher != null) { + ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); + rotationWatcher = null; + } + if (displayFoldListener != null) { + // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) + ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); + displayFoldListener = null; + } + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index bc0f825a..2a705fa0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -13,18 +13,13 @@ import com.genymobile.scrcpy.opengl.OpenGLRunner; import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; -import com.genymobile.scrcpy.wrappers.DisplayManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; import android.graphics.Rect; import android.hardware.display.VirtualDisplay; import android.os.Build; -import android.os.Handler; -import android.os.HandlerThread; import android.os.IBinder; -import android.view.IDisplayFoldListener; -import android.view.IRotationWatcher; import android.view.Surface; import java.io.IOException; @@ -40,8 +35,7 @@ public class ScreenCapture extends SurfaceCapture { private DisplayInfo displayInfo; private Size videoSize; - // Source display size (before resizing/crop) for the current session - private Size sessionDisplaySize; + private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); private IBinder display; private VirtualDisplay virtualDisplay; @@ -49,16 +43,6 @@ public class ScreenCapture extends SurfaceCapture { private AffineMatrix transform; private OpenGLRunner glRunner; - private DisplayManager.DisplayListenerHandle displayListenerHandle; - private HandlerThread handlerThread; - - // On Android 14, the DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really - // detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from - // DisplayListener (which proves that it works). - private boolean displayListenerWorks; // only accessed from the display listener thread - private IRotationWatcher rotationWatcher; - private IDisplayFoldListener displayFoldListener; - public ScreenCapture(VirtualDisplayListener vdListener, Options options) { this.vdListener = vdListener; this.displayId = options.getDisplayId(); @@ -70,57 +54,7 @@ public class ScreenCapture extends SurfaceCapture { @Override public void init() { - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - registerDisplayListenerFallbacks(); - } - - handlerThread = new HandlerThread("DisplayListener"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(displayId -> { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: onDisplayChanged(" + displayId + ")"); - } - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - if (!displayListenerWorks) { - // On the first display listener event, we know it works, we can unregister the fallbacks - displayListenerWorks = true; - unregisterDisplayListenerFallbacks(); - } - } - if (this.displayId == displayId) { - DisplayInfo di = ServiceManager.getDisplayManager().getDisplayInfo(displayId); - if (di == null) { - Ln.w("DisplayInfo for " + displayId + " cannot be retrieved"); - // We can't compare with the current size, so reset unconditionally - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: requestReset(): " + getSessionDisplaySize() + " -> (unknown)"); - } - setSessionDisplaySize(null); - invalidate(); - } else { - Size size = di.getSize(); - - // The field is hidden on purpose, to read it with synchronization - @SuppressWarnings("checkstyle:HiddenField") - Size sessionDisplaySize = getSessionDisplaySize(); // synchronized - - // .equals() also works if sessionDisplaySize == null - if (!size.equals(sessionDisplaySize)) { - // Reset only if the size is different - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: requestReset(): " + sessionDisplaySize + " -> " + size); - } - // Set the new size immediately, so that a future onDisplayChanged() event called before the asynchronous prepare() - // considers that the current size is the requested size (to avoid a duplicate requestReset()) - setSessionDisplaySize(size); - invalidate(); - } else if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: Size not changed (" + size + "): do not requestReset()"); - } - } - } - }, handler); + displaySizeMonitor.start(displayId, this::invalidate); } @Override @@ -136,7 +70,7 @@ public class ScreenCapture extends SurfaceCapture { } Size displaySize = displayInfo.getSize(); - setSessionDisplaySize(displaySize); + displaySizeMonitor.setSessionDisplaySize(displaySize); if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { // The user requested to lock the video orientation to the current orientation @@ -226,18 +160,7 @@ public class ScreenCapture extends SurfaceCapture { @Override public void release() { - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - unregisterDisplayListenerFallbacks(); - } - - handlerThread.quitSafely(); - handlerThread = null; - - // displayListenerHandle may be null if registration failed - if (displayListenerHandle != null) { - ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); - displayListenerHandle = null; - } + displaySizeMonitor.stopAndRelease(); if (display != null) { SurfaceControl.destroyDisplay(display); @@ -279,67 +202,6 @@ public class ScreenCapture extends SurfaceCapture { } } - private synchronized Size getSessionDisplaySize() { - return sessionDisplaySize; - } - - private synchronized void setSessionDisplaySize(Size sessionDisplaySize) { - this.sessionDisplaySize = sessionDisplaySize; - } - - private void registerDisplayListenerFallbacks() { - rotationWatcher = new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: onRotationChanged(" + rotation + ")"); - } - invalidate(); - } - }; - ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); - - // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) - displayFoldListener = new IDisplayFoldListener.Stub() { - - private boolean first = true; - - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - if (first) { - // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. - first = false; - return; - } - - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("ScreenCapture: onDisplayFoldChanged(" + displayId + ", " + folded + ")"); - } - - if (ScreenCapture.this.displayId != displayId) { - // Ignore events related to other display ids - return; - } - invalidate(); - } - }; - ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); - } - - private void unregisterDisplayListenerFallbacks() { - synchronized (this) { - if (rotationWatcher != null) { - ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); - rotationWatcher = null; - } - if (displayFoldListener != null) { - // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) - ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); - displayFoldListener = null; - } - } - } - @Override public void requestInvalidate() { invalidate(); From 39d51ff2cc2f3e201ad433d48372b548e5dd11d3 Mon Sep 17 00:00:00 2001 From: Anric Date: Sun, 17 Nov 2024 21:43:32 +0800 Subject: [PATCH 104/278] Use DisplayWindowListener for Android 14 On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really detect it directly. As a workaround, a RotationWatcher and DisplayFoldListener were registered as a fallback, until a first "display changed" event was triggered. To simplify, on Android 14, register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. Refs #5455 comment PR #5455 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- .../android/view/IDisplayWindowListener.aidl | 66 +++++++++ .../scrcpy/video/DisplaySizeMonitor.java | 140 ++++++------------ .../wrappers/DisplayWindowListener.java | 39 +++++ .../scrcpy/wrappers/WindowManager.java | 20 +++ 4 files changed, 170 insertions(+), 95 deletions(-) create mode 100644 server/src/main/aidl/android/view/IDisplayWindowListener.aidl create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java diff --git a/server/src/main/aidl/android/view/IDisplayWindowListener.aidl b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl new file mode 100644 index 00000000..2b331175 --- /dev/null +++ b/server/src/main/aidl/android/view/IDisplayWindowListener.aidl @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view; + +import android.graphics.Rect; +import android.content.res.Configuration; + +import java.util.List; + +/** + * Interface to listen for changes to display window-containers. + * + * This differs from DisplayManager's DisplayListener in a couple ways: + * - onDisplayAdded is always called after the display is actually added to the WM hierarchy. + * This corresponds to the DisplayContent and not the raw Dislay from DisplayManager. + * - onDisplayConfigurationChanged is called for all configuration changes, not just changes + * to displayinfo (eg. windowing-mode). + * + */ +oneway interface IDisplayWindowListener { + + /** + * Called when a new display is added to the WM hierarchy. The existing display ids are returned + * when this listener is registered with WM via {@link #registerDisplayWindowListener}. + */ + void onDisplayAdded(int displayId); + + /** + * Called when a display's window-container configuration has changed. + */ + void onDisplayConfigurationChanged(int displayId, in Configuration newConfig); + + /** + * Called when a display is removed from the hierarchy. + */ + void onDisplayRemoved(int displayId); + + /** + * Called when fixed rotation is started on a display. + */ + void onFixedRotationStarted(int displayId, int newRotation); + + /** + * Called when the previous fixed rotation on a display is finished. + */ + void onFixedRotationFinished(int displayId); + + /** + * Called when the keep clear ares on a display have changed. + */ + void onKeepClearAreasChanged(int displayId, in List restricted, in List unrestricted); +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java index df8be323..ff863aa8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java @@ -6,13 +6,14 @@ import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.DisplayManager; +import com.genymobile.scrcpy.wrappers.DisplayWindowListener; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.content.res.Configuration; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; -import android.view.IDisplayFoldListener; -import android.view.IRotationWatcher; +import android.view.IDisplayWindowListener; public class DisplaySizeMonitor { @@ -20,15 +21,14 @@ public class DisplaySizeMonitor { void onDisplaySizeChanged(); } + // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really + // detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. + private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT != AndroidVersions.API_34_ANDROID_14; + private DisplayManager.DisplayListenerHandle displayListenerHandle; private HandlerThread handlerThread; - // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really - // detect it directly, so register a RotationWatcher and a DisplayFoldListener as a fallback, until we receive the first event from - // DisplayListener (which proves that it works). - private boolean displayListenerWorks; // only accessed from the display listener thread - private IRotationWatcher rotationWatcher; - private IDisplayFoldListener displayFoldListener; + private IDisplayWindowListener displayWindowListener; private int displayId = Device.DISPLAY_ID_NONE; @@ -44,31 +44,34 @@ public class DisplaySizeMonitor { assert this.displayId == Device.DISPLAY_ID_NONE; this.displayId = displayId; - handlerThread = new HandlerThread("DisplayListener"); - handlerThread.start(); - Handler handler = new Handler(handlerThread.getLooper()); - - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - registerDisplayListenerFallbacks(); - } - - displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); - } - - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - if (!displayListenerWorks) { - // On the first display listener event, we know it works, we can unregister the fallbacks - displayListenerWorks = true; - unregisterDisplayListenerFallbacks(); + if (USE_DEFAULT_METHOD) { + handlerThread = new HandlerThread("DisplayListener"); + handlerThread.start(); + Handler handler = new Handler(handlerThread.getLooper()); + displayListenerHandle = ServiceManager.getDisplayManager().registerDisplayListener(eventDisplayId -> { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onDisplayChanged(" + eventDisplayId + ")"); } - } - if (eventDisplayId == displayId) { - checkDisplaySizeChanged(); - } - }, handler); + if (eventDisplayId == displayId) { + checkDisplaySizeChanged(); + } + }, handler); + } else { + displayWindowListener = new DisplayWindowListener() { + @Override + public void onDisplayConfigurationChanged(int eventDisplayId, Configuration newConfig) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Ln.v("DisplaySizeMonitor: onDisplayConfigurationChanged(" + eventDisplayId + ")"); + } + + if (eventDisplayId == displayId) { + checkDisplaySizeChanged(); + } + } + }; + ServiceManager.getWindowManager().registerDisplayWindowListener(displayWindowListener); + } } /** @@ -78,18 +81,18 @@ public class DisplaySizeMonitor { * It is ok to call this method even if {@link #start(int, Listener)} was not called. */ public void stopAndRelease() { - if (Build.VERSION.SDK_INT == AndroidVersions.API_34_ANDROID_14) { - unregisterDisplayListenerFallbacks(); - } + if (USE_DEFAULT_METHOD) { + // displayListenerHandle may be null if registration failed + if (displayListenerHandle != null) { + ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); + displayListenerHandle = null; + } - // displayListenerHandle may be null if registration failed - if (displayListenerHandle != null) { - ServiceManager.getDisplayManager().unregisterDisplayListener(displayListenerHandle); - displayListenerHandle = null; - } - - if (handlerThread != null) { - handlerThread.quitSafely(); + if (handlerThread != null) { + handlerThread.quitSafely(); + } + } else if (displayWindowListener != null) { + ServiceManager.getWindowManager().unregisterDisplayWindowListener(displayWindowListener); } } @@ -133,57 +136,4 @@ public class DisplaySizeMonitor { } } } - - private void registerDisplayListenerFallbacks() { - rotationWatcher = new IRotationWatcher.Stub() { - @Override - public void onRotationChanged(int rotation) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onRotationChanged(" + rotation + ")"); - } - - checkDisplaySizeChanged(); - } - }; - ServiceManager.getWindowManager().registerRotationWatcher(rotationWatcher, displayId); - - // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) - displayFoldListener = new IDisplayFoldListener.Stub() { - - private boolean first = true; - - @Override - public void onDisplayFoldChanged(int displayId, boolean folded) { - if (first) { - // An event is posted on registration to signal the initial state. Ignore it to avoid restarting encoding. - first = false; - return; - } - - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Ln.v("DisplaySizeMonitor: onDisplayFoldChanged(" + displayId + ", " + folded + ")"); - } - - if (DisplaySizeMonitor.this.displayId != displayId) { - // Ignore events related to other display ids - return; - } - - checkDisplaySizeChanged(); - } - }; - ServiceManager.getWindowManager().registerDisplayFoldListener(displayFoldListener); - } - - private synchronized void unregisterDisplayListenerFallbacks() { - if (rotationWatcher != null) { - ServiceManager.getWindowManager().unregisterRotationWatcher(rotationWatcher); - rotationWatcher = null; - } - if (displayFoldListener != null) { - // Build.VERSION.SDK_INT >= AndroidVersions.API_29_ANDROID_10 (but implied by == API_34_ANDROID 14) - ServiceManager.getWindowManager().unregisterDisplayFoldListener(displayFoldListener); - displayFoldListener = null; - } - } } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java new file mode 100644 index 00000000..f2ecb158 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayWindowListener.java @@ -0,0 +1,39 @@ +package com.genymobile.scrcpy.wrappers; + +import android.content.res.Configuration; +import android.graphics.Rect; +import android.view.IDisplayWindowListener; + +import java.util.List; + +public class DisplayWindowListener extends IDisplayWindowListener.Stub { + @Override + public void onDisplayAdded(int displayId) { + // empty default implementation + } + + @Override + public void onDisplayConfigurationChanged(int displayId, Configuration newConfig) { + // empty default implementation + } + + @Override + public void onDisplayRemoved(int displayId) { + // empty default implementation + } + + @Override + public void onFixedRotationStarted(int displayId, int newRotation) { + // empty default implementation + } + + @Override + public void onFixedRotationFinished(int displayId) { + // empty default implementation + } + + @Override + public void onKeepClearAreasChanged(int displayId, List restricted, List unrestricted) { + // empty default implementation + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index ee36139a..86dd83f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.os.IInterface; import android.view.IDisplayFoldListener; +import android.view.IDisplayWindowListener; import android.view.IRotationWatcher; import java.lang.reflect.Method; @@ -226,4 +227,23 @@ public final class WindowManager { Ln.e("Could not unregister display fold listener", e); } } + + @TargetApi(AndroidVersions.API_30_ANDROID_11) + public int[] registerDisplayWindowListener(IDisplayWindowListener listener) { + try { + return (int[]) manager.getClass().getMethod("registerDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); + } catch (Exception e) { + Ln.e("Could not register display window listener", e); + } + return null; + } + + @TargetApi(AndroidVersions.API_30_ANDROID_11) + public void unregisterDisplayWindowListener(IDisplayWindowListener listener) { + try { + manager.getClass().getMethod("unregisterDisplayWindowListener", IDisplayWindowListener.class).invoke(manager, listener); + } catch (Exception e) { + Ln.e("Could not unregister display window listener", e); + } + } } From 9b03bfc3ae881f639f9c4bb381eef7365b785437 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 10 Nov 2024 13:47:02 +0100 Subject: [PATCH 105/278] Handle virtual display rotation Listen to display size changes and rotate the virtual display accordingly. Note: use `git show -b` to Show this commit ignoring whitespace changes. Fixes #5428 Refs #5370 PR #5455 --- .../scrcpy/video/NewDisplayCapture.java | 73 +++++++++++++++---- 1 file changed, 59 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index cc54876a..6ce50521 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -6,10 +6,13 @@ import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.NewDisplay; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLRunner; +import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.hardware.display.DisplayManager; import android.hardware.display.VirtualDisplay; import android.os.Build; import android.view.Surface; @@ -19,8 +22,8 @@ import java.io.IOException; public class NewDisplayCapture extends SurfaceCapture { // Internal fields copied from android.hardware.display.DisplayManager - private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; - private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; + private static final int VIRTUAL_DISPLAY_FLAG_PUBLIC = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC; + private static final int VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY = android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY; private static final int VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH = 1 << 6; private static final int VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT = 1 << 7; private static final int VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL = 1 << 8; @@ -35,12 +38,18 @@ public class NewDisplayCapture extends SurfaceCapture { private final VirtualDisplayListener vdListener; private final NewDisplay newDisplay; + private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); + + private AffineMatrix displayTransform; + private OpenGLRunner glRunner; + private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; // only used if newDisplay.getSize() != null private VirtualDisplay virtualDisplay; - private Size size; + private Size size; // the logical size of the display (including rotation) + private Size physicalSize; // the physical size of the display (without rotation) private int dpi; public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { @@ -69,11 +78,27 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public void prepare() { - if (!newDisplay.hasExplicitSize()) { - size = mainDisplaySize.limit(maxSize).round8(); - } - if (!newDisplay.hasExplicitDpi()) { - dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); + if (virtualDisplay == null) { + if (!newDisplay.hasExplicitSize()) { + size = mainDisplaySize.limit(maxSize).round8(); + } + if (!newDisplay.hasExplicitDpi()) { + dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); + } + + physicalSize = size; + // Set the current display size to avoid an unnecessary call to invalidate() + displaySizeMonitor.setSessionDisplaySize(size); + } else { + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId()); + size = displayInfo.getSize(); + dpi = displayInfo.getDpi(); + + VideoFilter displayFilter = new VideoFilter(size); + displayFilter.addRotation(displayInfo.getRotation()); + // The display info gives the oriented size, but the virtual display video always remains in the origin orientation + displayTransform = displayFilter.getInverseTransform(); + physicalSize = displayFilter.getOutputSize(); } } @@ -100,28 +125,48 @@ public class NewDisplayCapture extends SurfaceCapture { .createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + + displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { Ln.e("Could not create display", e); throw new AssertionError("Could not create display"); } - - if (vdListener != null) { - PositionMapper positionMapper = new PositionMapper(size, null); - vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); - } } @Override public void start(Surface surface) throws IOException { + if (displayTransform != null) { + assert glRunner == null; + OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform); + glRunner = new OpenGLRunner(glFilter); + surface = glRunner.start(physicalSize, size, surface); + } + if (virtualDisplay == null) { startNew(surface); } else { virtualDisplay.setSurface(surface); } + + if (vdListener != null) { + // The virtual display rotation must only be applied to video, it is already taken into account when injecting events! + PositionMapper positionMapper = PositionMapper.create(size, null, size); + vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper); + } + } + + @Override + public void stop() { + if (glRunner != null) { + glRunner.stopAndRelease(); + glRunner = null; + } } @Override public void release() { + displaySizeMonitor.stopAndRelease(); + if (virtualDisplay != null) { virtualDisplay.release(); virtualDisplay = null; From 45382e3f017ea17925243b5cdcdeb3b1a5a44a37 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 20:19:40 +0100 Subject: [PATCH 106/278] Add --capture-orientation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Deprecate --lock-video-orientation in favor of a more general option --capture-orientation, which supports all possible orientations (0, 90, 180, 270, flip0, flip90, flip180, flip270), and a "locked" flag via a '@' prefix. All the old "locked video orientations" are supported: - --lock-video-orientation -> --capture-orientation=@ - --lock-video-orientation=0 -> --capture-orientation=@0 - --lock-video-orientation=90 -> --capture-orientation=@90 - --lock-video-orientation=180 -> --capture-orientation=@180 - --lock-video-orientation=270 -> --capture-orientation=@270 In addition, --capture-orientation can rotate/flip the display without locking, so that it follows the physical device rotation. For example: scrcpy --capture-orientation=flip90 always flips and rotates the capture by 90° clockwise. The arguments are consistent with --display-orientation and --record-orientation and --orientation (which provide separate client-side orientation settings). Refs #4011 PR #5455 --- app/data/bash-completion/scrcpy | 11 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 24 ++-- app/src/cli.c | 132 +++++++----------- app/src/options.c | 3 +- app/src/options.h | 19 ++- app/src/scrcpy.c | 3 +- app/src/server.c | 14 +- app/src/server.h | 3 +- app/tests/test_cli.c | 2 - doc/video.md | 32 ++++- .../java/com/genymobile/scrcpy/Options.java | 47 +++++-- .../com/genymobile/scrcpy/device/Device.java | 3 - .../genymobile/scrcpy/device/Orientation.java | 47 +++++++ .../scrcpy/video/ScreenCapture.java | 19 ++- .../genymobile/scrcpy/video/VideoFilter.java | 17 ++- 16 files changed, 233 insertions(+), 145 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/device/Orientation.java diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index d9ad4c8d..c2f32ad0 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -17,6 +17,7 @@ _scrcpy() { --camera-fps= --camera-high-speed --camera-size= + --capture-orientation= --crop= -d --select-usb --disable-screensaver @@ -37,8 +38,6 @@ _scrcpy() { --list-cameras --list-displays --list-encoders - --lock-video-orientation - --lock-video-orientation= -m --max-size= -M --max-fps= @@ -138,6 +137,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'disabled uhid aoa' -- "$cur")) return ;; + --capture-orientation) + COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270' -- "$cur")) + return + ;; --orientation|--display-orientation) COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return @@ -146,10 +149,6 @@ _scrcpy() { COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) return ;; - --lock-video-orientation) - COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) - return - ;; --pause-on-exit) COMPREPLY=($(compgen -W 'true false if-error' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 430e8000..59019904 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -24,6 +24,7 @@ arguments=( '--camera-facing=[Select the device camera by its facing direction]:facing:(front back external)' '--camera-fps=[Specify the camera capture frame rate]' '--camera-size=[Specify an explicit camera capture size]' + '--capture-orientation=[Set the capture video orientation]:orientation:(0 90 180 270 flip0 flip90 flip180 flip270 @0 @90 @180 @270 @flip0 @flip90 @flip180 @flip270)' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' @@ -44,7 +45,6 @@ arguments=( '--list-cameras[List cameras available on the device]' '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' - '--lock-video-orientation=[Lock video orientation]:orientation:(unlocked initial 0 90 180 270)' {-m,--max-size=}'[Limit both the width and height of the video to value]' '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' '--max-fps=[Limit the frame rate of screen capture]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 76e36dcb..f0c1e0f1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -121,6 +121,18 @@ If not specified, Android's default frame rate (30 fps) is used. .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. +.TP +.BI "\-\-capture\-orientation " value +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270, possibly prefixed by '@'. + +The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +If a leading '@' is passed (@90) for display capture, then the rotation is locked, and is relative to the natural device orientation. + +If '@' is passed alone, then the rotation is locked to the initial device orientation. + +Default is 0. + .TP .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. @@ -241,16 +253,6 @@ List video and audio encoders available on the device. .B \-\-list\-displays List displays available on the device. -.TP -\fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] -Lock capture video orientation to \fIvalue\fR. - -Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 90, 180, and 270. The values represent the clockwise rotation from the natural device orientation, in degrees. - -Default is "unlocked". - -Passing the option without argument is equivalent to passing "initial". - .TP .BI "\-m, \-\-max\-size " value Limit both the width and height of the video to \fIvalue\fR. The other dimension is computed so that the device aspect\-ratio is preserved. @@ -548,8 +550,6 @@ Default is "info" for release builds, "debug" for debug builds. .BI "\-\-v4l2-sink " /dev/videoN Output to v4l2loopback device. -It requires to lock the video orientation (see \fB\-\-lock\-video\-orientation\fR). - .TP .BI "\-\-v4l2-buffer " ms Add a buffering delay (in milliseconds) before pushing frames. This increases latency to compensate for jitter. diff --git a/app/src/cli.c b/app/src/cli.c index e67192bf..55ccfc0d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -107,6 +107,7 @@ enum { OPT_LIST_APPS, OPT_START_APP, OPT_SCREEN_OFF_TIMEOUT, + OPT_CAPTURE_ORIENTATION, }; struct sc_option { @@ -471,18 +472,27 @@ static const struct sc_option options[] = { .text = "List video and audio encoders available on the device.", }, { + .longopt_id = OPT_CAPTURE_ORIENTATION, + .longopt = "capture-orientation", + .argdesc = "value", + .text = "Set the capture video orientation.\n" + "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " + "and flip270, possibly prefixed by '@'.\n" + "The number represents the clockwise rotation in degrees; the " + "flip\" keyword applies a horizontal flip before the " + "rotation.\n" + "If a leading '@' is passed (@90) for display capture, then " + "the rotation is locked, and is relative to the natural device " + "orientation.\n" + "If '@' is passed alone, then the rotation is locked to the " + "initial device orientation.\n" + "Default is 0.", + }, + { + // deprecated .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, .longopt = "lock-video-orientation", .argdesc = "value", - .optional_arg = true, - .text = "Lock capture video orientation to value.\n" - "Possible values are \"unlocked\", \"initial\" (locked to the " - "initial orientation), 0, 90, 180 and 270. The values " - "represent the clockwise rotation from the natural device " - "orientation, in degrees.\n" - "Default is \"unlocked\".\n" - "Passing the option without argument is equivalent to passing " - "\"initial\".", }, { .shortopt = 'm', @@ -895,8 +905,6 @@ static const struct sc_option options[] = { .longopt = "v4l2-sink", .argdesc = "/dev/videoN", .text = "Output to v4l2loopback device.\n" - "It requires to lock the video orientation (see " - "--lock-video-orientation).\n" "This feature is only available on Linux.", }, { @@ -1582,66 +1590,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { return true; } -static bool -parse_lock_video_orientation(const char *s, - enum sc_lock_video_orientation *lock_mode) { - if (!s || !strcmp(s, "initial")) { - // Without argument, lock the initial orientation - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_INITIAL; - return true; - } - - if (!strcmp(s, "unlocked")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED; - return true; - } - - if (!strcmp(s, "0")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_0; - return true; - } - - if (!strcmp(s, "90")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; - return true; - } - - if (!strcmp(s, "180")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; - return true; - } - - if (!strcmp(s, "270")) { - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; - return true; - } - - if (!strcmp(s, "1")) { - LOGW("--lock-video-orientation=1 is deprecated, use " - "--lock-video-orientation=270 instead."); - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_270; - return true; - } - - if (!strcmp(s, "2")) { - LOGW("--lock-video-orientation=2 is deprecated, use " - "--lock-video-orientation=180 instead."); - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_180; - return true; - } - - if (!strcmp(s, "3")) { - LOGW("--lock-video-orientation=3 is deprecated, use " - "--lock-video-orientation=90 instead."); - *lock_mode = SC_LOCK_VIDEO_ORIENTATION_90; - return true; - } - - LOGE("Unsupported --lock-video-orientation value: %s (expected initial, " - "unlocked, 0, 90, 180 or 270).", s); - return false; -} - static bool parse_rotation(const char *s, uint8_t *rotation) { long value; @@ -1693,6 +1641,32 @@ parse_orientation(const char *s, enum sc_orientation *orientation) { return false; } +static bool +parse_capture_orientation(const char *s, enum sc_orientation *orientation, + enum sc_orientation_lock *lock) { + if (*s == '\0') { + LOGE("Capture orientation may not be empty (expected 0, 90, 180, 270, " + "flip0, flip90, flip180 or flip270, possibly prefixed by '@')"); + return false; + } + + // Lock the orientation by a leading '@' + if (s[0] == '@') { + // Consume '@' + ++s; + if (*s == '\0') { + // Only '@': lock to the initial orientation (orientation is unused) + *lock = SC_ORIENTATION_LOCKED_INITIAL; + return true; + } + *lock = SC_ORIENTATION_LOCKED_VALUE; + } else { + *lock = SC_ORIENTATION_UNLOCKED; + } + + return parse_orientation(s, orientation); +} + static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" @@ -2367,8 +2341,13 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "--mouse=uhid instead."); return false; case OPT_LOCK_VIDEO_ORIENTATION: - if (!parse_lock_video_orientation(optarg, - &opts->lock_video_orientation)) { + LOGE("--lock-video-orientation has been removed, use " + "--capture-orientation instead."); + return false; + case OPT_CAPTURE_ORIENTATION: + if (!parse_capture_orientation(optarg, + &opts->capture_orientation, + &opts->capture_orientation_lock)) { return false; } break; @@ -2852,13 +2831,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } - if (opts->lock_video_orientation == - SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { - LOGI("Video orientation is locked for v4l2 sink. " - "See --lock-video-orientation."); - opts->lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_INITIAL; - } - // V4L2 could not handle size change. // Do not log because downsizing on error is the default behavior, // not an explicit request from the user. diff --git a/app/src/options.c b/app/src/options.c index 3cad9d9f..69f8f64d 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -50,7 +50,8 @@ const struct scrcpy_options scrcpy_options_default = { .video_bit_rate = 0, .audio_bit_rate = 0, .max_fps = NULL, - .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, + .capture_orientation = SC_ORIENTATION_0, + .capture_orientation_lock = SC_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, .window_x = SC_WINDOW_POSITION_UNDEFINED, diff --git a/app/src/options.h b/app/src/options.h index 9236c3f8..945fcdf7 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -84,6 +84,12 @@ enum sc_orientation { // v v v SC_ORIENTATION_FLIP_270, // 1 1 1 }; +enum sc_orientation_lock { + SC_ORIENTATION_UNLOCKED, + SC_ORIENTATION_LOCKED_VALUE, // lock to specified orientation + SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation +}; + static inline bool sc_orientation_is_mirror(enum sc_orientation orientation) { assert(!(orientation & ~7)); @@ -130,16 +136,6 @@ sc_orientation_get_name(enum sc_orientation orientation) { } } -enum sc_lock_video_orientation { - SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, - // lock the current orientation when scrcpy starts - SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, - SC_LOCK_VIDEO_ORIENTATION_0 = 0, - SC_LOCK_VIDEO_ORIENTATION_90 = 3, - SC_LOCK_VIDEO_ORIENTATION_180 = 2, - SC_LOCK_VIDEO_ORIENTATION_270 = 1, -}; - enum sc_keyboard_input_mode { SC_KEYBOARD_INPUT_MODE_AUTO, SC_KEYBOARD_INPUT_MODE_UHID_OR_AOA, // normal vs otg mode @@ -251,7 +247,8 @@ struct scrcpy_options { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server - enum sc_lock_video_orientation lock_video_orientation; + enum sc_orientation capture_orientation; + enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; enum sc_orientation record_orientation; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 2721c0d8..5528910a 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -429,7 +429,8 @@ scrcpy(struct scrcpy_options *options) { .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, .screen_off_timeout = options->screen_off_timeout, - .lock_video_orientation = options->lock_video_orientation, + .capture_orientation = options->capture_orientation, + .capture_orientation_lock = options->capture_orientation_lock, .control = options->control, .display_id = options->display_id, .new_display = options->new_display, diff --git a/app/src/server.c b/app/src/server.c index 41f0bf27..9c12500e 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -274,9 +274,17 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps); } - if (params->lock_video_orientation != SC_LOCK_VIDEO_ORIENTATION_UNLOCKED) { - ADD_PARAM("lock_video_orientation=%" PRIi8, - params->lock_video_orientation); + if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED + || params->capture_orientation != SC_ORIENTATION_0) { + if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) { + ADD_PARAM("capture_orientation=@"); + } else { + const char *orient = + sc_orientation_get_name(params->capture_orientation); + bool locked = + params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED; + ADD_PARAM("capture_orientation=%s%s", locked ? "@" : "", orient); + } } if (server->tunnel.forward) { ADD_PARAM("tunnel_forward=true"); diff --git a/app/src/server.h b/app/src/server.h index 7059be7f..20d998e9 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -46,7 +46,8 @@ struct sc_server_params { uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server sc_tick screen_off_timeout; - int8_t lock_video_orientation; + enum sc_orientation capture_orientation; + enum sc_orientation_lock capture_orientation_lock; bool control; uint32_t display_id; const char *new_display; diff --git a/app/tests/test_cli.c b/app/tests/test_cli.c index 14765792..de605cb9 100644 --- a/app/tests/test_cli.c +++ b/app/tests/test_cli.c @@ -51,7 +51,6 @@ static void test_options(void) { "--fullscreen", "--max-fps", "30", "--max-size", "1024", - "--lock-video-orientation=2", // optional arguments require '=' // "--no-control" is not compatible with "--turn-screen-off" // "--no-playback" is not compatible with "--fulscreen" "--port", "1234:1236", @@ -80,7 +79,6 @@ static void test_options(void) { assert(opts->fullscreen); assert(!strcmp(opts->max_fps, "30")); assert(opts->max_size == 1024); - assert(opts->lock_video_orientation == 2); assert(opts->port_range.first == 1234); assert(opts->port_range.last == 1236); assert(!strcmp(opts->push_target, "/sdcard/Movies")); diff --git a/doc/video.md b/doc/video.md index 74ec74dd..c00b6602 100644 --- a/doc/video.md +++ b/doc/video.md @@ -103,21 +103,39 @@ The orientation may be applied at 3 different levels: - The [shortcut](shortcuts.md) MOD+r requests the device to switch between portrait and landscape (the current running app may refuse, if it does not support the requested orientation). - - `--lock-video-orientation` changes the mirroring orientation (the orientation + - `--capture-orientation` changes the mirroring orientation (the orientation of the video sent from the device to the computer). This affects the recording. - `--orientation` is applied on the client side, and affects display and recording. For the display, it can be changed dynamically using [shortcuts](shortcuts.md). -To lock the mirroring orientation (on the capture side): +To capture the video with a specific orientation: ```bash -scrcpy --lock-video-orientation # initial (current) orientation -scrcpy --lock-video-orientation=0 # natural orientation -scrcpy --lock-video-orientation=90 # 90° clockwise -scrcpy --lock-video-orientation=180 # 180° -scrcpy --lock-video-orientation=270 # 270° clockwise +scrcpy --capture-orientation=0 +scrcpy --capture-orientation=90 # 90° clockwise +scrcpy --capture-orientation=180 # 180° +scrcpy --capture-orientation=270 # 270° clockwise +scrcpy --capture-orientation=flip0 # hflip +scrcpy --capture-orientation=flip90 # hflip + 90° clockwise +scrcpy --capture-orientation=flip180 # hflip + 180° +scrcpy --capture-orientation=flip270 # hflip + 270° clockwise +``` + +The capture orientation can be locked by using `@`, so that a physical device +rotation does not change the captured video orientation: + +```bash +scrcpy --capture-orientation=@ # locked to the initial orientation +scrcpy --capture-orientation=@0 # locked to 0° +scrcpy --capture-orientation=@90 # locked to 90° clockwise +scrcpy --capture-orientation=@180 # locked to 180° +scrcpy --capture-orientation=@270 # locked to 270° clockwise +scrcpy --capture-orientation=@flip0 # locked to hflip +scrcpy --capture-orientation=@flip90 # locked to hflip + 90° clockwise +scrcpy --capture-orientation=@flip180 # locked to hflip + 180° +scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise ``` To orient the video (on the rendering side): diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index c1620432..e1b3b9af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.audio.AudioCodec; import com.genymobile.scrcpy.audio.AudioSource; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.NewDisplay; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.CodecOption; import com.genymobile.scrcpy.util.Ln; @@ -13,6 +14,7 @@ import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.video.VideoSource; import android.graphics.Rect; +import android.util.Pair; import java.util.List; import java.util.Locale; @@ -32,7 +34,6 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; - private int lockVideoOrientation = -1; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -59,6 +60,9 @@ public class Options { private NewDisplay newDisplay; + private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked; + private Orientation captureOrientation = Orientation.Orient0; + private boolean listEncoders; private boolean listDisplays; private boolean listCameras; @@ -123,10 +127,6 @@ public class Options { return maxFps; } - public int getLockVideoOrientation() { - return lockVideoOrientation; - } - public boolean isTunnelForward() { return tunnelForward; } @@ -219,6 +219,14 @@ public class Options { return newDisplay; } + public Orientation getCaptureOrientation() { + return captureOrientation; + } + + public Orientation.Lock getCaptureOrientationLock() { + return captureOrientationLock; + } + public boolean getList() { return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; } @@ -341,9 +349,6 @@ public class Options { case "max_fps": options.maxFps = parseFloat("max_fps", value); break; - case "lock_video_orientation": - options.lockVideoOrientation = Integer.parseInt(value); - break; case "tunnel_forward": options.tunnelForward = Boolean.parseBoolean(value); break; @@ -448,6 +453,11 @@ public class Options { case "new_display": options.newDisplay = parseNewDisplay(value); break; + case "capture_orientation": + Pair pair = parseCaptureOrientation(value); + options.captureOrientationLock = pair.first; + options.captureOrientation = pair.second; + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -571,4 +581,25 @@ public class Options { return new NewDisplay(size, dpi); } + + private static Pair parseCaptureOrientation(String value) { + if (value.isEmpty()) { + throw new IllegalArgumentException("Empty capture orientation string"); + } + + Orientation.Lock lock; + if (value.charAt(0) == '@') { + // Consume '@' + value = value.substring(1); + if (value.isEmpty()) { + // Only '@': lock to the initial orientation (orientation is unused) + return Pair.create(Orientation.Lock.LockedInitial, Orientation.Orient0); + } + lock = Orientation.Lock.LockedValue; + } else { + lock = Orientation.Lock.Unlocked; + } + + return Pair.create(lock, Orientation.getByName(value)); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index 09c7d2b6..cd713499 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -40,9 +40,6 @@ public final class Device { public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; - public static final int LOCK_VIDEO_ORIENTATION_UNLOCKED = -1; - public static final int LOCK_VIDEO_ORIENTATION_INITIAL = -2; - private Device() { // not instantiable } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java new file mode 100644 index 00000000..c269750e --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java @@ -0,0 +1,47 @@ +package com.genymobile.scrcpy.device; + +public enum Orientation { + + // @formatter:off + Orient0("0"), + Orient90("90"), + Orient180("180"), + Orient270("270"), + Flip0("flip0"), + Flip90("flip90"), + Flip180("flip180"), + Flip270("flip270"); + + public enum Lock { + Unlocked, LockedInitial, LockedValue, + } + + private final String name; + + Orientation(String name) { + this.name = name; + } + + public static Orientation getByName(String name) { + for (Orientation orientation : values()) { + if (orientation.name.equals(name)) { + return orientation; + } + } + + throw new IllegalArgumentException("Unknown orientation: " + name); + } + + public static Orientation fromRotation(int rotation) { + assert rotation >= 0 && rotation < 4; + return values()[rotation]; + } + + public boolean isFlipped() { + return (ordinal() & 4) != 0; + } + + public int getRotation() { + return ordinal() & 3; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 2a705fa0..432d0ae8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.ConfigurationException; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DisplayInfo; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; import com.genymobile.scrcpy.opengl.OpenGLFilter; @@ -30,7 +31,8 @@ public class ScreenCapture extends SurfaceCapture { private final int displayId; private int maxSize; private final Rect crop; - private int lockVideoOrientation; + private Orientation.Lock captureOrientationLock; + private Orientation captureOrientation; private DisplayInfo displayInfo; private Size videoSize; @@ -49,7 +51,10 @@ public class ScreenCapture extends SurfaceCapture { assert displayId != Device.DISPLAY_ID_NONE; this.maxSize = options.getMaxSize(); this.crop = options.getCrop(); - this.lockVideoOrientation = options.getLockVideoOrientation(); + this.captureOrientationLock = options.getCaptureOrientationLock(); + this.captureOrientation = options.getCaptureOrientation(); + assert captureOrientationLock != null; + assert captureOrientation != null; } @Override @@ -72,9 +77,10 @@ public class ScreenCapture extends SurfaceCapture { Size displaySize = displayInfo.getSize(); displaySizeMonitor.setSessionDisplaySize(displaySize); - if (lockVideoOrientation == Device.LOCK_VIDEO_ORIENTATION_INITIAL) { + if (captureOrientationLock == Orientation.Lock.LockedInitial) { // The user requested to lock the video orientation to the current orientation - lockVideoOrientation = displayInfo.getRotation(); + captureOrientationLock = Orientation.Lock.LockedValue; + captureOrientation = Orientation.fromRotation(displayInfo.getRotation()); } VideoFilter filter = new VideoFilter(displaySize); @@ -84,9 +90,8 @@ public class ScreenCapture extends SurfaceCapture { filter.addCrop(crop, transposed); } - if (lockVideoOrientation != Device.LOCK_VIDEO_ORIENTATION_UNLOCKED) { - filter.addLockVideoOrientation(lockVideoOrientation, displayInfo.getRotation()); - } + boolean locked = captureOrientationLock != Orientation.Lock.Unlocked; + filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation); transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 2d570446..8aadaa0d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.video; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.AffineMatrix; @@ -78,8 +79,20 @@ public class VideoFilter { } } - public void addLockVideoOrientation(int lockVideoOrientation, int displayRotation) { - int ccwRotation = (4 + lockVideoOrientation - displayRotation) % 4; + public void addOrientation(Orientation captureOrientation) { + if (captureOrientation.isFlipped()) { + transform = AffineMatrix.hflip().multiply(transform); + } + int ccwRotation = (4 - captureOrientation.getRotation()) % 4; addRotation(ccwRotation); } + + public void addOrientation(int displayRotation, boolean locked, Orientation captureOrientation) { + if (locked) { + // flip/rotate the current display from the natural device orientation (i.e. where display rotation is 0) + int reverseDisplayRotation = (4 - displayRotation) % 4; + addRotation(reverseDisplayRotation); + } + addOrientation(captureOrientation); + } } From 456fa510f25039d6cd016eb9293a82be1f7a2653 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 20:50:41 +0100 Subject: [PATCH 107/278] Apply filters to camera capture Apply crop and orientation to camera capture. Fixes #4426 PR #5455 --- .../scrcpy/opengl/OpenGLRunner.java | 18 +++++- .../scrcpy/video/CameraCapture.java | 60 +++++++++++++++++-- 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java index a3f9335c..86bd1859 100644 --- a/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java +++ b/server/src/main/java/com/genymobile/scrcpy/opengl/OpenGLRunner.java @@ -28,6 +28,7 @@ public final class OpenGLRunner { private EGLSurface eglSurface; private final OpenGLFilter filter; + private final float[] overrideTransformMatrix; private SurfaceTexture surfaceTexture; private Surface inputSurface; @@ -35,8 +36,13 @@ public final class OpenGLRunner { private boolean stopped; - public OpenGLRunner(OpenGLFilter filter) { + public OpenGLRunner(OpenGLFilter filter, float[] overrideTransformMatrix) { this.filter = filter; + this.overrideTransformMatrix = overrideTransformMatrix; + } + + public OpenGLRunner(OpenGLFilter filter) { + this(filter, null); } public static synchronized void initOnce() { @@ -202,8 +208,14 @@ public final class OpenGLRunner { GLUtils.checkGlError(); surfaceTexture.updateTexImage(); - float[] matrix = new float[16]; - surfaceTexture.getTransformMatrix(matrix); + + float[] matrix; + if (overrideTransformMatrix != null) { + matrix = overrideTransformMatrix; + } else { + matrix = new float[16]; + surfaceTexture.getTransformMatrix(matrix); + } filter.draw(textureId, matrix); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index ee4085e9..5a18aeac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -3,7 +3,12 @@ package com.genymobile.scrcpy.video; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.ConfigurationException; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; +import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLFilter; +import com.genymobile.scrcpy.opengl.OpenGLRunner; +import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.HandlerExecutor; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; @@ -41,6 +46,13 @@ import java.util.stream.Stream; public class CameraCapture extends SurfaceCapture { + public static final float[] VFLIP_MATRIX = { + 1, 0, 0, 0, // column 1 + 0, -1, 0, 0, // column 2 + 0, 0, 1, 0, // column 3 + 0, 1, 0, 1, // column 4 + }; + private final String explicitCameraId; private final CameraFacing cameraFacing; private final Size explicitSize; @@ -48,9 +60,15 @@ public class CameraCapture extends SurfaceCapture { private final CameraAspectRatio aspectRatio; private final int fps; private final boolean highSpeed; + private final Rect crop; + private final Orientation captureOrientation; private String cameraId; - private Size size; + private Size captureSize; + private Size videoSize; // after OpenGL transforms + + private AffineMatrix transform; + private OpenGLRunner glRunner; private HandlerThread cameraThread; private Handler cameraHandler; @@ -67,6 +85,9 @@ public class CameraCapture extends SurfaceCapture { this.aspectRatio = options.getCameraAspectRatio(); this.fps = options.getCameraFps(); this.highSpeed = options.getCameraHighSpeed(); + this.crop = options.getCrop(); + this.captureOrientation = options.getCaptureOrientation(); + assert captureOrientation != null; } @Override @@ -92,13 +113,26 @@ public class CameraCapture extends SurfaceCapture { @Override public void prepare() throws IOException { try { - size = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); - if (size == null) { + captureSize = selectSize(cameraId, explicitSize, maxSize, aspectRatio, highSpeed); + if (captureSize == null) { throw new IOException("Could not select camera size"); } } catch (CameraAccessException e) { throw new IOException(e); } + + VideoFilter filter = new VideoFilter(captureSize); + + if (crop != null) { + filter.addCrop(crop, false); + } + + if (captureOrientation != Orientation.Orient0) { + filter.addOrientation(captureOrientation); + } + + transform = filter.getInverseTransform(); + videoSize = filter.getOutputSize().limit(maxSize).round8(); } private static String selectCamera(String explicitCameraId, CameraFacing cameraFacing) throws CameraAccessException, ConfigurationException { @@ -214,15 +248,33 @@ public class CameraCapture extends SurfaceCapture { @Override public void start(Surface surface) throws IOException { + if (transform != null) { + assert glRunner == null; + OpenGLFilter glFilter = new AffineOpenGLFilter(transform); + // The transform matrix returned by SurfaceTexture is incorrect for camera capture (it often contains an additional unexpected 90° + // rotation). Use a vertical flip transform matrix instead. + glRunner = new OpenGLRunner(glFilter, VFLIP_MATRIX); + surface = glRunner.start(captureSize, videoSize, surface); + } + try { CameraCaptureSession session = createCaptureSession(cameraDevice, surface); CaptureRequest request = createCaptureRequest(surface); setRepeatingRequest(session, request); } catch (CameraAccessException | InterruptedException e) { + stop(); throw new IOException(e); } } + @Override + public void stop() { + if (glRunner != null) { + glRunner.stopAndRelease(); + glRunner = null; + } + } + @Override public void release() { if (cameraDevice != null) { @@ -235,7 +287,7 @@ public class CameraCapture extends SurfaceCapture { @Override public Size getSize() { - return size; + return videoSize; } @Override From 371ff3122590f35996f904d99d02a58986a8c617 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 14 Nov 2024 23:54:20 +0100 Subject: [PATCH 108/278] Apply filters to virtual display capture Apply crop and orientation to virtual display capture. PR #5455 --- .../scrcpy/video/NewDisplayCapture.java | 81 ++++++++++++++----- 1 file changed, 61 insertions(+), 20 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 6ce50521..bd4cf033 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -5,6 +5,7 @@ import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.control.PositionMapper; import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.NewDisplay; +import com.genymobile.scrcpy.device.Orientation; import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.opengl.AffineOpenGLFilter; import com.genymobile.scrcpy.opengl.OpenGLFilter; @@ -13,6 +14,7 @@ import com.genymobile.scrcpy.util.AffineMatrix; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.graphics.Rect; import android.hardware.display.VirtualDisplay; import android.os.Build; import android.view.Surface; @@ -41,15 +43,21 @@ public class NewDisplayCapture extends SurfaceCapture { private final DisplaySizeMonitor displaySizeMonitor = new DisplaySizeMonitor(); private AffineMatrix displayTransform; + private AffineMatrix eventTransform; private OpenGLRunner glRunner; private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; // only used if newDisplay.getSize() != null + private final Rect crop; + private final boolean captureOrientationLocked; + private final Orientation captureOrientation; private VirtualDisplay virtualDisplay; - private Size size; // the logical size of the display (including rotation) + private Size videoSize; + private Size displaySize; // the logical size of the display (including rotation) private Size physicalSize; // the physical size of the display (without rotation) + private int dpi; public NewDisplayCapture(VirtualDisplayListener vdListener, Options options) { @@ -57,13 +65,18 @@ public class NewDisplayCapture extends SurfaceCapture { this.newDisplay = options.getNewDisplay(); assert newDisplay != null; this.maxSize = options.getMaxSize(); + this.crop = options.getCrop(); + assert options.getCaptureOrientationLock() != null; + this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; + this.captureOrientation = options.getCaptureOrientation(); + assert captureOrientation != null; } @Override protected void init() { - size = newDisplay.getSize(); + displaySize = newDisplay.getSize(); dpi = newDisplay.getDpi(); - if (size == null || dpi == 0) { + if (displaySize == null || dpi == 0) { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); if (displayInfo != null) { mainDisplaySize = displayInfo.getSize(); @@ -78,28 +91,57 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public void prepare() { + int displayRotation; if (virtualDisplay == null) { if (!newDisplay.hasExplicitSize()) { - size = mainDisplaySize.limit(maxSize).round8(); + displaySize = mainDisplaySize.limit(maxSize).round8(); } if (!newDisplay.hasExplicitDpi()) { - dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, size); + dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize); } - physicalSize = size; + videoSize = displaySize; + displayRotation = 0; // Set the current display size to avoid an unnecessary call to invalidate() - displaySizeMonitor.setSessionDisplaySize(size); + displaySizeMonitor.setSessionDisplaySize(displaySize); } else { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(virtualDisplay.getDisplay().getDisplayId()); - size = displayInfo.getSize(); + displaySize = displayInfo.getSize(); dpi = displayInfo.getDpi(); - - VideoFilter displayFilter = new VideoFilter(size); - displayFilter.addRotation(displayInfo.getRotation()); - // The display info gives the oriented size, but the virtual display video always remains in the origin orientation - displayTransform = displayFilter.getInverseTransform(); - physicalSize = displayFilter.getOutputSize(); + displayRotation = displayInfo.getRotation(); } + + VideoFilter filter = new VideoFilter(displaySize); + + if (crop != null) { + boolean transposed = (displayRotation % 2) != 0; + filter.addCrop(crop, transposed); + } + + filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); + + eventTransform = filter.getInverseTransform(); + + // DisplayInfo gives the oriented size (so videoSize includes the display rotation) + videoSize = filter.getOutputSize().limit(maxSize).round8(); + + // But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually). + // This additional display rotation must not be included in the input events transform (the expected coordinates are already in the + // physical display size) + if ((displayRotation % 2) == 0) { + physicalSize = displaySize; + } else { + physicalSize = displaySize.rotate(); + } + VideoFilter displayFilter = new VideoFilter(physicalSize); + displayFilter.addRotation(displayRotation); + AffineMatrix displayRotationMatrix = displayFilter.getInverseTransform(); + + // Take care of multiplication order: + // displayTransform = (FILTER_MATRIX * DISPLAY_FILTER_MATRIX)⁻¹ + // = DISPLAY_FILTER_MATRIX⁻¹ * FILTER_MATRIX⁻¹ + // = displayRotationMatrix * eventTransform + displayTransform = AffineMatrix.multiplyAll(displayRotationMatrix, eventTransform); } public void startNew(Surface surface) { @@ -122,9 +164,9 @@ public class NewDisplayCapture extends SurfaceCapture { } } virtualDisplay = ServiceManager.getDisplayManager() - .createNewVirtualDisplay("scrcpy", size.getWidth(), size.getHeight(), dpi, surface, flags); + .createNewVirtualDisplay("scrcpy", displaySize.getWidth(), displaySize.getHeight(), dpi, surface, flags); virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - Ln.i("New display: " + size.getWidth() + "x" + size.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { @@ -139,7 +181,7 @@ public class NewDisplayCapture extends SurfaceCapture { assert glRunner == null; OpenGLFilter glFilter = new AffineOpenGLFilter(displayTransform); glRunner = new OpenGLRunner(glFilter); - surface = glRunner.start(physicalSize, size, surface); + surface = glRunner.start(physicalSize, videoSize, surface); } if (virtualDisplay == null) { @@ -149,8 +191,7 @@ public class NewDisplayCapture extends SurfaceCapture { } if (vdListener != null) { - // The virtual display rotation must only be applied to video, it is already taken into account when injecting events! - PositionMapper positionMapper = PositionMapper.create(size, null, size); + PositionMapper positionMapper = PositionMapper.create(videoSize, eventTransform, displaySize); vdListener.onNewVirtualDisplay(virtualDisplay.getDisplay().getDisplayId(), positionMapper); } } @@ -175,7 +216,7 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public synchronized Size getSize() { - return size; + return videoSize; } @Override From 4348f12194b5e44e710e61786b452ebe4b9eb850 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2024 18:41:04 +0100 Subject: [PATCH 109/278] Improve mismatching event size warning Include both the event size and the current size in the warning message. PR #5455 --- .../java/com/genymobile/scrcpy/control/Controller.java | 9 +++++++-- .../com/genymobile/scrcpy/control/PositionMapper.java | 4 ++++ .../src/main/java/com/genymobile/scrcpy/device/Size.java | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 573e8f52..cafa11bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -8,6 +8,7 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; +import com.genymobile.scrcpy.device.Size; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.LogUtils; import com.genymobile.scrcpy.video.SurfaceCapture; @@ -359,7 +360,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Point point = displayData.positionMapper.map(position); if (point == null) { - Ln.w("Ignore touch event, it was generated for a different device size"); + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.w("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")"); return false; } @@ -473,7 +476,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Point point = displayData.positionMapper.map(position); if (point == null) { - Ln.w("Ignore scroll event, it was generated for a different device size"); + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.w("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")"); return false; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java index 4d3b8875..60109b51 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/PositionMapper.java @@ -27,6 +27,10 @@ public final class PositionMapper { return new PositionMapper(videoSize, transform); } + public Size getVideoSize() { + return videoSize; + } + public Point map(Position position) { Size clientVideoSize = position.getScreenSize(); if (!videoSize.equals(clientVideoSize)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 3baa1bdd..6500b74e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -103,6 +103,6 @@ public final class Size { @Override public String toString() { - return "Size{" + width + 'x' + height + '}'; + return width + "x" + height; } } From 090488081652593a795ea40697ce703bb5b2a59c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Nov 2024 22:16:17 +0100 Subject: [PATCH 110/278] Log event size mismatch as verbose On rotation, it is expected that many successive events are ignored due to size mismatch, when an event was generated from the mirroring window having the old size, but was received on the device with the new size (especially since mouse hover events are forwarded). Do not flood the console with warnings. PR #5455 --- .../genymobile/scrcpy/control/Controller.java | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index cafa11bd..f0e4c037 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -360,9 +360,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Point point = displayData.positionMapper.map(position); if (point == null) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.w("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")"); + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.v("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")"); + } return false; } @@ -476,9 +478,11 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Point point = displayData.positionMapper.map(position); if (point == null) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.w("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")"); + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.v("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")"); + } return false; } From 443f315f609b2bbd7d7c4f8e9ffc2b41bdc381f7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2024 18:46:08 +0100 Subject: [PATCH 111/278] Use natural device orientation for --new-display If no size is provided with --new-display, the main display size is used. But the actual size depended on the current device orientation. To make it deterministic, use the size of the natural device orientation (portrait for phones, landscape for tablets). PR #5455 --- .../java/com/genymobile/scrcpy/video/NewDisplayCapture.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index bd4cf033..3530cce8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -80,6 +80,9 @@ public class NewDisplayCapture extends SurfaceCapture { DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(0); if (displayInfo != null) { mainDisplaySize = displayInfo.getSize(); + if ((displayInfo.getRotation() % 2) != 0) { + mainDisplaySize = mainDisplaySize.rotate(); // Use the natural device orientation (at rotation 0), not the current one + } mainDisplayDpi = displayInfo.getDpi(); } else { Ln.w("Main display not found, fallback to 1920x1080 240dpi"); From d19045628e422d8e969a137580bb9e6496ec51fc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2024 18:51:40 +0100 Subject: [PATCH 112/278] Remove deprecated options PR #5455 --- app/src/cli.c | 72 +++++++++------------------------------------------ 1 file changed, 12 insertions(+), 60 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 55ccfc0d..291157c1 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1590,18 +1590,6 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { return true; } -static bool -parse_rotation(const char *s, uint8_t *rotation) { - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 3, "rotation"); - if (!ok) { - return false; - } - - *rotation = (uint8_t) value; - return true; -} - static bool parse_orientation(const char *s, enum sc_orientation *orientation) { if (!strcmp(s, "0")) { @@ -2276,8 +2264,8 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->crop = optarg; break; case OPT_DISPLAY: - LOGW("--display is deprecated, use --display-id instead."); - // fall through + LOGE("--display has been removed, use --display-id instead."); + return false; case OPT_DISPLAY_ID: if (!parse_display_id(optarg, &opts->display_id)) { return false; @@ -2365,8 +2353,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->control = false; break; case OPT_NO_DISPLAY: - LOGW("--no-display is deprecated, use --no-playback instead."); - // fall through + LOGE("--no-display has been removed, use --no-playback " + "instead."); + return false; case 'N': opts->video_playback = false; opts->audio_playback = false; @@ -2452,32 +2441,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->key_inject_mode = SC_KEY_INJECT_MODE_RAW; break; case OPT_ROTATION: - LOGW("--rotation is deprecated, use --display-orientation " - "instead."); - uint8_t rotation; - if (!parse_rotation(optarg, &rotation)) { - return false; - } - assert(rotation <= 3); - switch (rotation) { - case 0: - opts->display_orientation = SC_ORIENTATION_0; - break; - case 1: - // rotation 1 was 90° counterclockwise, but orientation - // is expressed clockwise - opts->display_orientation = SC_ORIENTATION_270; - break; - case 2: - opts->display_orientation = SC_ORIENTATION_180; - break; - case 3: - // rotation 3 was 270° counterclockwise, but orientation - // is expressed clockwise - opts->display_orientation = SC_ORIENTATION_90; - break; - } - break; + LOGE("--rotation has been removed, use --orientation or " + "--capture-orientation instead."); + return false; case OPT_DISPLAY_ORIENTATION: if (!parse_orientation(optarg, &opts->display_orientation)) { return false; @@ -2538,23 +2504,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } break; case OPT_FORWARD_ALL_CLICKS: - LOGW("--forward-all-clicks is deprecated, " + LOGE("--forward-all-clicks has been removed, " "use --mouse-bind=++++ instead."); - opts->mouse_bindings = (struct sc_mouse_bindings) { - .pri = { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, - }, - .sec = { - .right_click = SC_MOUSE_BINDING_CLICK, - .middle_click = SC_MOUSE_BINDING_CLICK, - .click4 = SC_MOUSE_BINDING_CLICK, - .click5 = SC_MOUSE_BINDING_CLICK, - }, - }; - break; + return false; case OPT_LEGACY_PASTE: opts->legacy_paste = true; break; @@ -2562,9 +2514,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->power_off_on_close = true; break; case OPT_DISPLAY_BUFFER: - LOGW("--display-buffer is deprecated, use --video-buffer " + LOGE("--display-buffer has been removed, use --video-buffer " "instead."); - // fall through + return false; case OPT_VIDEO_BUFFER: if (!parse_buffering_time(optarg, &opts->video_buffer)) { return false; From adb674a5c890bf2553149059bfc22ba9df86a04b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 15 Nov 2024 19:17:04 +0100 Subject: [PATCH 113/278] Add --angle Add an option to rotate the video content by a custom angle. Fixes #4135 Fixes #4345 Refs #4658 PR #5455 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 11 +++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 4 ++++ app/src/server.h | 1 + doc/video.md | 11 +++++++++++ .../src/main/java/com/genymobile/scrcpy/Options.java | 8 ++++++++ .../com/genymobile/scrcpy/video/CameraCapture.java | 4 ++++ .../genymobile/scrcpy/video/NewDisplayCapture.java | 3 +++ .../com/genymobile/scrcpy/video/ScreenCapture.java | 3 +++ .../java/com/genymobile/scrcpy/video/VideoFilter.java | 8 ++++++++ 15 files changed, 62 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index c2f32ad0..cddfc4a6 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -2,6 +2,7 @@ _scrcpy() { local cur prev words cword local opts=" --always-on-top + --angle --audio-bit-rate= --audio-buffer= --audio-codec= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 59019904..cda49e8e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -9,6 +9,7 @@ local arguments arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' + '--angle=[Rotate the video content by a custom angle, in degrees]' '--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)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index f0c1e0f1..543801bc 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -19,6 +19,10 @@ provides display and control of Android devices connected on USB (or over TCP/IP .B \-\-always\-on\-top Make scrcpy window always on top (above other windows). +.TP +.BI "\-\-angle " degrees +Rotate the video content by a custom angle, in degrees (clockwise). + .TP .BI "\-\-audio\-bit\-rate " value Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are supported: '\fBK\fR' (x1000) and '\fBM\fR' (x1000000). diff --git a/app/src/cli.c b/app/src/cli.c index 291157c1..95dad3d7 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -108,6 +108,7 @@ enum { OPT_START_APP, OPT_SCREEN_OFF_TIMEOUT, OPT_CAPTURE_ORIENTATION, + OPT_ANGLE, }; struct sc_option { @@ -149,6 +150,13 @@ static const struct sc_option options[] = { .longopt = "always-on-top", .text = "Make scrcpy window always on top (above other windows).", }, + { + .longopt_id = OPT_ANGLE, + .longopt = "angle", + .argdesc = "degrees", + .text = "Rotate the video content by a custom angle, in degrees " + "(clockwise).", + }, { .longopt_id = OPT_AUDIO_BIT_RATE, .longopt = "audio-bit-rate", @@ -2689,6 +2697,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_ANGLE: + opts->angle = optarg; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index 69f8f64d..adc7ba0c 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -107,6 +107,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_dup = false, .new_display = NULL, .start_app = NULL, + .angle = NULL, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 945fcdf7..0692276e 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -247,6 +247,7 @@ struct scrcpy_options { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server + const char *angle; // float to be parsed by the server enum sc_orientation capture_orientation; enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 5528910a..48befb1d 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -428,6 +428,7 @@ scrcpy(struct scrcpy_options *options) { .video_bit_rate = options->video_bit_rate, .audio_bit_rate = options->audio_bit_rate, .max_fps = options->max_fps, + .angle = options->angle, .screen_off_timeout = options->screen_off_timeout, .capture_orientation = options->capture_orientation, .capture_orientation_lock = options->capture_orientation_lock, diff --git a/app/src/server.c b/app/src/server.c index 9c12500e..9c81a7f6 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -274,6 +274,10 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->max_fps); ADD_PARAM("max_fps=%s", params->max_fps); } + if (params->angle) { + VALIDATE_STRING(params->angle); + ADD_PARAM("angle=%s", params->angle); + } if (params->capture_orientation_lock != SC_ORIENTATION_UNLOCKED || params->capture_orientation != SC_ORIENTATION_0) { if (params->capture_orientation_lock == SC_ORIENTATION_LOCKED_INITIAL) { diff --git a/app/src/server.h b/app/src/server.h index 20d998e9..9d46b354 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -45,6 +45,7 @@ struct sc_server_params { uint32_t video_bit_rate; uint32_t audio_bit_rate; const char *max_fps; // float to be parsed by the server + const char *angle; // float to be parsed by the server sc_tick screen_off_timeout; enum sc_orientation capture_orientation; enum sc_orientation_lock capture_orientation_lock; diff --git a/doc/video.md b/doc/video.md index c00b6602..9e57e1af 100644 --- a/doc/video.md +++ b/doc/video.md @@ -159,6 +159,17 @@ to the MP4 or MKV target file. Flipping is not supported, so only the 4 first values are allowed when recording. +## Angle + +To rotate the video content by a custom angle (in degrees, clockwise): + +``` +scrcpy --angle=23 +``` + +The center of rotation is the center of the visible area (after cropping). + + ## Crop The device screen may be cropped to mirror only part of the screen. diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index e1b3b9af..6a59fbe7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -34,6 +34,7 @@ public class Options { private int videoBitRate = 8000000; private int audioBitRate = 128000; private float maxFps; + private float angle; private boolean tunnelForward; private Rect crop; private boolean control = true; @@ -127,6 +128,10 @@ public class Options { return maxFps; } + public float getAngle() { + return angle; + } + public boolean isTunnelForward() { return tunnelForward; } @@ -349,6 +354,9 @@ public class Options { case "max_fps": options.maxFps = parseFloat("max_fps", value); break; + case "angle": + options.angle = parseFloat("angle", value); + break; case "tunnel_forward": options.tunnelForward = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java index 5a18aeac..0e147cb7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/CameraCapture.java @@ -62,6 +62,7 @@ public class CameraCapture extends SurfaceCapture { private final boolean highSpeed; private final Rect crop; private final Orientation captureOrientation; + private final float angle; private String cameraId; private Size captureSize; @@ -88,6 +89,7 @@ public class CameraCapture extends SurfaceCapture { this.crop = options.getCrop(); this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -131,6 +133,8 @@ public class CameraCapture extends SurfaceCapture { filter.addOrientation(captureOrientation); } + filter.addAngle(angle); + transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 3530cce8..6a70704e 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -52,6 +52,7 @@ public class NewDisplayCapture extends SurfaceCapture { private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; + private final float angle; private VirtualDisplay virtualDisplay; private Size videoSize; @@ -70,6 +71,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -122,6 +124,7 @@ public class NewDisplayCapture extends SurfaceCapture { } filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); + filter.addAngle(angle); eventTransform = filter.getInverseTransform(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 432d0ae8..47425d09 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -33,6 +33,7 @@ public class ScreenCapture extends SurfaceCapture { private final Rect crop; private Orientation.Lock captureOrientationLock; private Orientation captureOrientation; + private final float angle; private DisplayInfo displayInfo; private Size videoSize; @@ -55,6 +56,7 @@ public class ScreenCapture extends SurfaceCapture { this.captureOrientation = options.getCaptureOrientation(); assert captureOrientationLock != null; assert captureOrientation != null; + this.angle = options.getAngle(); } @Override @@ -92,6 +94,7 @@ public class ScreenCapture extends SurfaceCapture { boolean locked = captureOrientationLock != Orientation.Lock.Unlocked; filter.addOrientation(displayInfo.getRotation(), locked, captureOrientation); + filter.addAngle(angle); transform = filter.getInverseTransform(); videoSize = filter.getOutputSize().limit(maxSize).round8(); diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 8aadaa0d..6bffb51a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -95,4 +95,12 @@ public class VideoFilter { } addOrientation(captureOrientation); } + + public void addAngle(double cwAngle) { + if (cwAngle == 0) { + return; + } + double ccwAngle = -cwAngle; + transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform); + } } From f95a5f97b1944b948a45218d349d2acf8e19ac3b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2024 18:30:19 +0100 Subject: [PATCH 114/278] Document filter order Matrix multiplication is not commutative, so the order of filters matters. PR #5455 --- app/scrcpy.1 | 4 +--- app/src/cli.c | 3 +-- doc/video.md | 18 +++++++++++++++--- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 543801bc..bb77d25e 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -141,9 +141,7 @@ Default is 0. .BI "\-\-crop " width\fR:\fIheight\fR:\fIx\fR:\fIy Crop the device screen on the server. -The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). Any -.B \-\-max\-size -value is computed on the cropped size. +The values are expressed in the device natural orientation (typically, portrait for a phone, landscape for a tablet). .TP .B \-d, \-\-select\-usb diff --git a/app/src/cli.c b/app/src/cli.c index 95dad3d7..08f1db2b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -311,8 +311,7 @@ static const struct sc_option options[] = { .argdesc = "width:height:x:y", .text = "Crop the device screen on the server.\n" "The values are expressed in the device natural orientation " - "(typically, portrait for a phone, landscape for a tablet). " - "Any --max-size value is computed on the cropped size.", + "(typically, portrait for a phone, landscape for a tablet).", }, { .shortopt = 'd', diff --git a/doc/video.md b/doc/video.md index 9e57e1af..5f3a42cb 100644 --- a/doc/video.md +++ b/doc/video.md @@ -27,6 +27,9 @@ preserved. That way, a device in 1920×1080 will be mirrored at 1024×576. If encoding fails, scrcpy automatically tries again with a lower definition (unless `--no-downsize-on-error` is enabled). +For camera mirroring, the `--max-size` value is used to select the camera source +size instead (among the available resolutions). + ## Bit rate @@ -138,7 +141,10 @@ scrcpy --capture-orientation=@flip180 # locked to hflip + 180° scrcpy --capture-orientation=@flip270 # locked to hflip + 270° clockwise ``` -To orient the video (on the rendering side): +The capture orientation transform is applied after `--crop`, but before +`--angle`. + +To orient the video (on the client side): ```bash scrcpy --orientation=0 @@ -167,7 +173,9 @@ To rotate the video content by a custom angle (in degrees, clockwise): scrcpy --angle=23 ``` -The center of rotation is the center of the visible area (after cropping). +The center of rotation is the center of the visible area. + +This transformation is applied after `--crop` and `--capture-orientation`. ## Crop @@ -183,7 +191,11 @@ scrcpy --crop=1224:1440:0:0 # 1224x1440 at offset (0,0) The values are expressed in the device natural orientation (portrait for a phone, landscape for a tablet). -If `--max-size` is also specified, resizing is applied after cropping. +Cropping is performed before `--capture-orientation` and `--angle`. + +For screen mirroring, `--max-size` is applied after cropping. For camera and +virtual display mirroring, `--max-size` is applied first (because it selects the +source size rather than resizing it). ## Display From 36d61f9ecd853104ba838d8df18102c31320fd0c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 19 Nov 2024 18:45:05 +0100 Subject: [PATCH 115/278] Reference virtual display documentation Reference the documentation about virtual displays from the "Display" section of video.md. --- doc/video.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/video.md b/doc/video.md index 5f3a42cb..63b6078c 100644 --- a/doc/video.md +++ b/doc/video.md @@ -216,6 +216,8 @@ scrcpy --list-displays A secondary display may only be controlled if the device runs at least Android 10 (otherwise it is mirrored as read-only). +It is also possible to create a [virtual display](virtual_display.md). + ## Buffering From 28d64ef319337f2313d74be4d16653774ec8185e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Nov 2024 07:45:15 +0100 Subject: [PATCH 116/278] Fix --new-display bash completion The option --new-display accepts an optional argument, but bash must not try to auto-complete it with unrelated content. --- app/data/bash-completion/scrcpy | 1 + 1 file changed, 1 insertion(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index cddfc4a6..8fae972f 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -194,6 +194,7 @@ _scrcpy() { |--display-id \ |--max-fps \ |-m|--max-size \ + |--new-display \ |-p|--port \ |--push-target \ |--rotation \ From 145b823b1d029a34f640d5a3f231439e45b0eaf7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 16 Nov 2024 22:45:38 +0100 Subject: [PATCH 117/278] Add --no-vd-system-decorations Add an option to disable the following flag for virtual displays: DisplayManager.VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS Some devices render a broken UI when this flag is enabled. Fixes #5494 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 9 +++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 +++ app/src/server.h | 1 + doc/virtual_display.md | 10 ++++++++++ .../src/main/java/com/genymobile/scrcpy/Options.java | 8 ++++++++ .../com/genymobile/scrcpy/video/NewDisplayCapture.java | 8 ++++++-- 12 files changed, 46 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 8fae972f..6c88927e 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -57,6 +57,7 @@ _scrcpy() { --no-mipmaps --no-mouse-hover --no-power-on + --no-vd-system-decorations --no-video --no-video-playback --orientation= diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index cda49e8e..e0c5e265 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -63,6 +63,7 @@ arguments=( '--no-mipmaps[Disable the generation of mipmaps]' '--no-mouse-hover[Do not forward mouse hover events]' '--no-power-on[Do not power on the device on start]' + '--no-vd-system-decorations[Disable virtual display system decorations flag]' '--no-video[Disable video forwarding]' '--no-video-playback[Disable video playback]' '--orientation=[Set the video orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index bb77d25e..711c53c6 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -370,6 +370,10 @@ Do not forward mouse hover (mouse motion without any clicks) events. .B \-\-no\-power\-on Do not power on the device on start. +.TP +.B \-\-no\-vd\-system\-decorations +Disable virtual display system decorations flag. + .TP .B \-\-no\-video Disable video forwarding. diff --git a/app/src/cli.c b/app/src/cli.c index 08f1db2b..177bf934 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -109,6 +109,7 @@ enum { OPT_SCREEN_OFF_TIMEOUT, OPT_CAPTURE_ORIENTATION, OPT_ANGLE, + OPT_NO_VD_SYSTEM_DECORATIONS, }; struct sc_option { @@ -659,6 +660,11 @@ static const struct sc_option options[] = { .longopt = "no-power-on", .text = "Do not power on the device on start.", }, + { + .longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS, + .longopt = "no-vd-system-decorations", + .text = "Disable virtual display system decorations flag.", + }, { .longopt_id = OPT_NO_VIDEO, .longopt = "no-video", @@ -2699,6 +2705,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_ANGLE: opts->angle = optarg; break; + case OPT_NO_VD_SYSTEM_DECORATIONS: + opts->vd_system_decorations = optarg; + break; default: // getopt prints the error message on stderr return false; diff --git a/app/src/options.c b/app/src/options.c index adc7ba0c..be3cf8d1 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -108,6 +108,7 @@ const struct scrcpy_options scrcpy_options_default = { .new_display = NULL, .start_app = NULL, .angle = NULL, + .vd_system_decorations = true, }; enum sc_orientation diff --git a/app/src/options.h b/app/src/options.h index 0692276e..eaeba2f2 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -310,6 +310,7 @@ struct scrcpy_options { bool audio_dup; const char *new_display; // [x][/] parsed by the server const char *start_app; + bool vd_system_decorations; }; extern const struct scrcpy_options scrcpy_options_default; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 48befb1d..dc9e237f 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -458,6 +458,7 @@ scrcpy(struct scrcpy_options *options) { .power_on = options->power_on, .kill_adb_on_close = options->kill_adb_on_close, .camera_high_speed = options->camera_high_speed, + .vd_system_decorations = options->vd_system_decorations, .list = options->list, }; diff --git a/app/src/server.c b/app/src/server.c index 9c81a7f6..ce7b1aaf 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -376,6 +376,9 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->new_display); ADD_PARAM("new_display=%s", params->new_display); } + if (!params->vd_system_decorations) { + ADD_PARAM("vd_system_decorations=false"); + } if (params->list & SC_OPTION_LIST_ENCODERS) { ADD_PARAM("list_encoders=true"); } diff --git a/app/src/server.h b/app/src/server.h index 9d46b354..6d9dbd4d 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -69,6 +69,7 @@ struct sc_server_params { bool power_on; bool kill_adb_on_close; bool camera_high_speed; + bool vd_system_decorations; uint8_t list; }; diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 4ed5961f..97ac01b2 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -24,3 +24,13 @@ For example: ```bash scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc ``` + +## System decorations + +By default, virtual display system decorations are enabled. But some devices +might display a broken UI; + +Use `--no-vd-system-decorations` to disable it. + +Note that if no app is started, no content will be rendered, so no video frame +will be produced at all. diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 6a59fbe7..43cc790d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -60,6 +60,7 @@ public class Options { private boolean powerOn = true; private NewDisplay newDisplay; + private boolean vdSystemDecorations = true; private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked; private Orientation captureOrientation = Orientation.Orient0; @@ -232,6 +233,10 @@ public class Options { return captureOrientationLock; } + public boolean getVDSystemDecorations() { + return vdSystemDecorations; + } + public boolean getList() { return listEncoders || listDisplays || listCameras || listCameraSizes || listApps; } @@ -461,6 +466,9 @@ public class Options { case "new_display": options.newDisplay = parseNewDisplay(value); break; + case "vd_system_decorations": + options.vdSystemDecorations = Boolean.parseBoolean(value); + break; case "capture_orientation": Pair pair = parseCaptureOrientation(value); options.captureOrientationLock = pair.first; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 6a70704e..dc9c8897 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -53,6 +53,7 @@ public class NewDisplayCapture extends SurfaceCapture { private final boolean captureOrientationLocked; private final Orientation captureOrientation; private final float angle; + private final boolean vdSystemDecorations; private VirtualDisplay virtualDisplay; private Size videoSize; @@ -72,6 +73,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; this.angle = options.getAngle(); + this.vdSystemDecorations = options.getVDSystemDecorations(); } @Override @@ -157,8 +159,10 @@ public class NewDisplayCapture extends SurfaceCapture { | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT - | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL - | VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; + | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; + if (vdSystemDecorations) { + flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; + } if (Build.VERSION.SDK_INT >= AndroidVersions.API_33_ANDROID_13) { flags |= VIRTUAL_DISPLAY_FLAG_TRUSTED | VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP From 2ec30bdf8038899b0b0c0fdb4924725561072c66 Mon Sep 17 00:00:00 2001 From: backryun Date: Wed, 2 Oct 2024 04:17:09 +0900 Subject: [PATCH 118/278] Upgrade FFmpeg (7.1) for Windows PR #5332 Signed-off-by: Romain Vimont --- app/deps/ffmpeg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 89431542..93612c1b 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=7.0.2 +VERSION=7.1 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=8646515b638a3ad303e23af6a3587734447cb8fc0a0c064ecdb8e95c4fd8b389 +SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6 cd "$SOURCES_DIR" From eeb04292a47f7ef7519b2eb9fd5c06a81bb69352 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Nov 2024 07:57:35 +0100 Subject: [PATCH 119/278] Upgrade SDL (2.30.9) for Windows --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index c8b62746..55866ccd 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=2.30.7 +VERSION=2.30.9 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=1578c96f62c9ae36b64e431b2aa0e0b0fd07c275dedbc694afc38e19056688f5 +SHA256SUM=682a055004081e37d81a7d4ce546c3ee3ef2e0e6a675ed2651e430ccd14eb407 cd "$SOURCES_DIR" From f1f27116269ecbe422fd853ec192e3a80e168f76 Mon Sep 17 00:00:00 2001 From: Gutem Date: Tue, 22 Oct 2024 21:43:22 -0300 Subject: [PATCH 120/278] Document missing --cask option for macOS Installing android-platform-tools via brew install requires the option --cask. Refs #2004 Refs #2231 PR #5398 Signed-off-by: Romain Vimont --- doc/macos.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/macos.md b/doc/macos.md index 35d90e9d..2c7c6071 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -13,7 +13,7 @@ brew install scrcpy You need `adb`, accessible from your `PATH`. If you don't have it yet: ```bash -brew install android-platform-tools +brew install --cask android-platform-tools ``` Alternatively, Scrcpy is also available in [MacPorts], which sets up `adb` for you: From 4608a19a1313c3c2805ca05a3106798504ecb642 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Nov 2024 08:14:04 +0100 Subject: [PATCH 121/278] Upgrade platform-tools (35.0.2) for Windows Since 35.0.1, the filename has changed on the server from -windows.zip to -win.zip The links are referenced from this file: Refs --- app/deps/adb.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/deps/adb.sh b/app/deps/adb.sh index 58a54659..b07f29b3 100755 --- a/app/deps/adb.sh +++ b/app/deps/adb.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.0 -FILENAME=platform-tools_r$VERSION-windows.zip +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-win.zip PROJECT_DIR=platform-tools-$VERSION -SHA256SUM=7ab78a8f8b305ae4d0de647d99c43599744de61a0838d3a47bda0cdffefee87e +SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 cd "$SOURCES_DIR" From 264110fd70cbdc1350c08618d158d34bf4c55bbd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 20 Nov 2024 13:01:57 +0100 Subject: [PATCH 122/278] Dissociate virtual display size and capture size Allow capturing virtual displays at a lower resolution using -m/--max-size. In the original implementation in #5370, the virtual display size was necessarily the same as the capture size. The --max-size value was only allowed to determine the virtual display size when no explicit size was provided. Since the dpi was scaled down accordingly, it is often better to create a virtual display at the target capture size directly. However, not everything is rendered according to the virtual display DPI. For example, a page in Firefox is rendered too big on small virtual displays. Thus, it makes sense to be able create a virtual display at a given size, and capture it at a lower resolution with --max-size. This is now possible using OpenGL filters. Therefore, change the behavior of --max-size for virtual displays: - --max-size does not impact --new-display without size argument anymore (the virtual display size is the main display size); - it is used to limit the capture size (whether an explicit size is provided or not). This new behavior is consistent with main display capture. Refs #5370 comment Refs #5370 PR #5506 --- app/scrcpy.1 | 3 +-- app/src/cli.c | 10 +--------- doc/video.md | 6 +++--- doc/virtual_display.md | 1 - .../com/genymobile/scrcpy/device/Size.java | 6 +++++- .../scrcpy/video/NewDisplayCapture.java | 20 +++++++++++-------- .../genymobile/scrcpy/video/VideoFilter.java | 13 ++++++++++++ 7 files changed, 35 insertions(+), 24 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 711c53c6..95d5133d 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -318,14 +318,13 @@ Disable video and audio playback on the computer (equivalent to \fB\-\-no\-video .TP \fB\-\-new\-display\fR[=[\fIwidth\fRx\fIheight\fR][/\fIdpi\fR]] -Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI, and \fB\-\-max\-size\fR is considered. +Create a new display with the specified resolution and density. If not provided, they default to the main display dimensions and DPI. Examples: \-\-new\-display=1920x1080 \-\-new\-display=1920x1080/420 \-\-new\-display # main display size and density - \-\-new\-display -m1920 # scaled to fit a max size of 1920 \-\-new\-display=/240 # main display size and 240 dpi .TP diff --git a/app/src/cli.c b/app/src/cli.c index 177bf934..3f2d23cb 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -590,12 +590,11 @@ static const struct sc_option options[] = { .optional_arg = true, .text = "Create a new display with the specified resolution and " "density. If not provided, they default to the main display " - "dimensions and DPI, and --max-size is considered.\n" + "dimensions and DPI.\n" "Examples:\n" " --new-display=1920x1080\n" " --new-display=1920x1080/420 # force 420 dpi\n" " --new-display # main display size and density\n" - " --new-display -m1920 # scaled to fit a max size of 1920\n" " --new-display=/240 # main display size and 240 dpi", }, { @@ -2891,13 +2890,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], LOGE("--new-display is incompatible with --no-video"); return false; } - - if (opts->max_size && opts->new_display[0] != '\0' - && opts->new_display[0] != '/') { - // An explicit size is defined (not "" nor "/") - LOGE("Cannot specify both --new-display size and -m/--max-size"); - return false; - } } if (otg) { diff --git a/doc/video.md b/doc/video.md index 63b6078c..db9571f7 100644 --- a/doc/video.md +++ b/doc/video.md @@ -193,9 +193,9 @@ phone, landscape for a tablet). Cropping is performed before `--capture-orientation` and `--angle`. -For screen mirroring, `--max-size` is applied after cropping. For camera and -virtual display mirroring, `--max-size` is applied first (because it selects the -source size rather than resizing it). +For display mirroring, `--max-size` is applied after cropping. For camera, +`--max-size` is applied first (because it selects the source size rather than +resizing the content). ## Display diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 97ac01b2..7523c118 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -8,7 +8,6 @@ To mirror a new virtual display instead of the device screen: scrcpy --new-display=1920x1080 scrcpy --new-display=1920x1080/420 # force 420 dpi scrcpy --new-display # use the main display size and density -scrcpy --new-display -m1920 # ... scaled to fit a max size of 1920 scrcpy --new-display=/240 # use the main display size and 240 dpi ``` diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Size.java b/server/src/main/java/com/genymobile/scrcpy/device/Size.java index 6500b74e..b448273d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Size.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Size.java @@ -60,7 +60,7 @@ public final class Size { * @return The current size rounded. */ public Size round8() { - if ((width & 7) == 0 && (height & 7) == 0) { + if (isMultipleOf8()) { // Already a multiple of 8 return this; } @@ -80,6 +80,10 @@ public final class Size { return new Size(w, h); } + public boolean isMultipleOf8() { + return (width & 7) == 0 && (height & 7) == 0; + } + public Rect toRect() { return new Rect(0, 0, width, height); } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index dc9c8897..d92141af 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -48,7 +48,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size mainDisplaySize; private int mainDisplayDpi; - private int maxSize; // only used if newDisplay.getSize() != null + private int maxSize; private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; @@ -101,7 +101,7 @@ public class NewDisplayCapture extends SurfaceCapture { int displayRotation; if (virtualDisplay == null) { if (!newDisplay.hasExplicitSize()) { - displaySize = mainDisplaySize.limit(maxSize).round8(); + displaySize = mainDisplaySize; } if (!newDisplay.hasExplicitDpi()) { dpi = scaleDpi(mainDisplaySize, mainDisplayDpi, displaySize); @@ -128,10 +128,19 @@ public class NewDisplayCapture extends SurfaceCapture { filter.addOrientation(displayRotation, captureOrientationLocked, captureOrientation); filter.addAngle(angle); + Size filteredSize = filter.getOutputSize(); + if (!filteredSize.isMultipleOf8() || (maxSize != 0 && filteredSize.getMax() > maxSize)) { + if (maxSize != 0) { + filteredSize = filteredSize.limit(maxSize); + } + filteredSize = filteredSize.round8(); + filter.addResize(filteredSize); + } + eventTransform = filter.getInverseTransform(); // DisplayInfo gives the oriented size (so videoSize includes the display rotation) - videoSize = filter.getOutputSize().limit(maxSize).round8(); + videoSize = filter.getOutputSize(); // But the virtual display video always remains in the origin orientation (the video itself is not rotated, so it must rotated manually). // This additional display rotation must not be included in the input events transform (the expected coordinates are already in the @@ -231,11 +240,6 @@ public class NewDisplayCapture extends SurfaceCapture { @Override public synchronized boolean setMaxSize(int newMaxSize) { - if (newDisplay.hasExplicitSize()) { - // Cannot retry with a different size if the display size was explicitly provided - return false; - } - maxSize = newMaxSize; return true; } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java index 6bffb51a..a27915ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/VideoFilter.java @@ -103,4 +103,17 @@ public class VideoFilter { double ccwAngle = -cwAngle; transform = AffineMatrix.rotate(ccwAngle).withAspectRatio(size).fromCenter().multiply(transform); } + + public void addResize(Size targetSize) { + if (size.equals(targetSize)) { + return; + } + + if (transform == null) { + // The requested scaling is performed by the viewport (by changing the output size), but the OpenGL filter must still run, even if + // resizing is not performed by the shader. So transform MUST NOT be null. + transform = AffineMatrix.IDENTITY; + } + size = targetSize; + } } From 0e50d1e7dba940bdf3875bfa3855b4227f40a861 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 07:45:04 +0100 Subject: [PATCH 123/278] Extract PLATFORM_TOOLS in build_without_gradle.sh Refs #5512 --- server/build_without_gradle.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 206aa604..6add5a69 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -16,6 +16,7 @@ SCRCPY_VERSION_NAME=2.7 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} +PLATFORM_TOOLS="$ANDROID_HOME/platforms/android-$PLATFORM" BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" @@ -23,7 +24,7 @@ CLASSES_DIR="$BUILD_DIR/classes" GEN_DIR="$BUILD_DIR/gen" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server -ANDROID_JAR="$ANDROID_HOME/platforms/android-$PLATFORM/android.jar" +ANDROID_JAR="$PLATFORM_TOOLS/android.jar" LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" echo "Platform: android-$PLATFORM" From 24588cb637496c04330bf12f6350a1369b08a718 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 07:48:00 +0100 Subject: [PATCH 124/278] Add missing aidl in build_without_gradle.sh Refs 39d51ff2cc2f3e201ad433d48372b548e5dd11d3 Fixes #5512 --- server/build_without_gradle.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 6add5a69..7b293e02 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -25,6 +25,7 @@ GEN_DIR="$BUILD_DIR/gen" SERVER_DIR=$(dirname "$0") SERVER_BINARY=scrcpy-server ANDROID_JAR="$PLATFORM_TOOLS/android.jar" +ANDROID_AIDL="$PLATFORM_TOOLS/framework.aidl" LAMBDA_JAR="$BUILD_TOOLS_DIR/core-lambda-stubs.jar" echo "Platform: android-$PLATFORM" @@ -50,6 +51,8 @@ cd "$SERVER_DIR/src/main/aidl" "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ android/content/IOnPrimaryClipChangedListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl +"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \ + android/view/IDisplayWindowListener.aidl # Fake sources to expose hidden Android types to the project FAKE_SRC=( \ From 9f39a5f2d6b89b2f57bf003c81163affc3bf0c60 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 17 Nov 2024 14:42:02 +0100 Subject: [PATCH 125/278] Determine debugger command at runtime When server_debugger is enabled, retrieve the device SDK version to execute the correct command. PR #5466 --- app/meson.build | 3 --- app/src/adb/adb.c | 18 ++++++++++++++++++ app/src/adb/adb.h | 6 ++++++ app/src/server.c | 28 ++++++++++++++++++---------- doc/develop.md | 9 --------- meson_options.txt | 1 - 6 files changed, 42 insertions(+), 23 deletions(-) diff --git a/app/meson.build b/app/meson.build index 9d179101..444cf98e 100644 --- a/app/meson.build +++ b/app/meson.build @@ -167,9 +167,6 @@ conf.set('DEFAULT_LOCAL_PORT_RANGE_LAST', '27199') # run a server debugger and wait for a client to be attached conf.set('SERVER_DEBUGGER', get_option('server_debugger')) -# select the debugger method ('old' for Android < 9, 'new' for Android >= 9) -conf.set('SERVER_DEBUGGER_METHOD_NEW', get_option('server_debugger_method') == 'new') - # enable V4L2 support (linux only) conf.set('HAVE_V4L2', v4l2_support) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 15c9c85a..b3e90b2f 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -739,3 +739,21 @@ sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags) { return sc_adb_parse_device_ip(buf); } + +uint16_t +sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial) { + char *sdk_version = + sc_adb_getprop(intr, serial, "ro.build.version.sdk", SC_ADB_SILENT); + if (!sdk_version) { + return 0; + } + + long value; + bool ok = sc_str_parse_integer(sdk_version, &value); + free(sdk_version); + if (!ok || value < 0 || value > 0xFFFF) { + return 0; + } + + return value; +} diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index ffd532ea..0292dea1 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -114,4 +114,10 @@ sc_adb_getprop(struct sc_intr *intr, const char *serial, const char *prop, char * sc_adb_get_device_ip(struct sc_intr *intr, const char *serial, unsigned flags); +/** + * Return the device SDK version. + */ +uint16_t +sc_adb_get_device_sdk_version(struct sc_intr *intr, const char *serial); + #endif diff --git a/app/src/server.c b/app/src/server.c index ce7b1aaf..9101aee9 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -201,18 +201,26 @@ execute_server(struct sc_server *server, cmd[count++] = "app_process"; #ifdef SERVER_DEBUGGER + uint16_t sdk_version = sc_adb_get_device_sdk_version(&server->intr, serial); + if (!sdk_version) { + LOGE("Could not determine SDK version"); + return 0; + } + # define SERVER_DEBUGGER_PORT "5005" - cmd[count++] = -# ifdef SERVER_DEBUGGER_METHOD_NEW - /* Android 9 and above */ - "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket,suspend=y," - "server=y,address=" -# else - /* Android 8 and below */ - "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" -# endif - SERVER_DEBUGGER_PORT; + const char *dbg; + if (sdk_version < 28) { + // Android < 9 + dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" + SERVER_DEBUGGER_PORT; + } else { + // Android >= 9 + dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket," + "suspend=y,server=y,address=" SERVER_DEBUGGER_PORT; + } + cmd[count++] = dbg; #endif + cmd[count++] = "/"; // unused cmd[count++] = "com.genymobile.scrcpy.Server"; cmd[count++] = SCRCPY_VERSION; diff --git a/doc/develop.md b/doc/develop.md index a094aa32..fb75f471 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -461,15 +461,6 @@ meson setup x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` -If your device runs Android 8 or below, set the `server_debugger_method` to -`old` in addition: - -```bash -meson setup x -Dserver_debugger=true -Dserver_debugger_method=old -# or, if x is already configured -meson configure x -Dserver_debugger=true -Dserver_debugger_method=old -``` - Then recompile. When you start scrcpy, it will start a debugger on port 5005 on the device. diff --git a/meson_options.txt b/meson_options.txt index d1030694..76075b3a 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -3,6 +3,5 @@ option('compile_server', type: 'boolean', value: true, description: 'Build the s option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') -option('server_debugger_method', type: 'combo', choices: ['old', 'new'], value: 'new', description: 'Select the debugger method (Android < 9: "old", Android >= 9: "new")') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') From dc82425769ab0fce545a5c76658fc7ba774468ad Mon Sep 17 00:00:00 2001 From: Enno Boland Date: Sun, 10 Nov 2024 19:17:45 +0100 Subject: [PATCH 126/278] Add debugging method for Android >= 11 Fixes #5346 PR #5466 Signed-off-by: Romain Vimont --- app/src/server.c | 21 +++++++++++++++------ doc/develop.md | 21 +++++++++++++++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/app/src/server.c b/app/src/server.c index 9101aee9..584a3c34 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -213,10 +213,15 @@ execute_server(struct sc_server *server, // Android < 9 dbg = "-agentlib:jdwp=transport=dt_socket,suspend=y,server=y,address=" SERVER_DEBUGGER_PORT; - } else { - // Android >= 9 + } else if (sdk_version < 30) { + // Android >= 9 && Android < 11 dbg = "-XjdwpProvider:internal -XjdwpOptions:transport=dt_socket," "suspend=y,server=y,address=" SERVER_DEBUGGER_PORT; + } else { + // Android >= 11 + // Contrary to the other methods, this does not suspend on start. + // + dbg = "-XjdwpProvider:adbconnection"; } cmd[count++] = dbg; #endif @@ -408,10 +413,14 @@ execute_server(struct sc_server *server, cmd[count++] = NULL; #ifdef SERVER_DEBUGGER - LOGI("Server debugger waiting for a client on device port " - SERVER_DEBUGGER_PORT "..."); - // From the computer, run - // adb forward tcp:5005 tcp:5005 + LOGI("Server debugger listening%s...", + sdk_version < 30 ? " on port " SERVER_DEBUGGER_PORT : ""); + // For Android < 11, from the computer: + // - run `adb forward tcp:5005 tcp:5005` + // For Android >= 11: + // - execute `adb jdwp` to get the jdwp port + // - run `adb forward tcp:5005 jdwp:XXXX` (replace XXXX) + // // Then, from Android Studio: Run > Debug > Edit configurations... // On the left, click on '+', "Remote", with: // Host: localhost diff --git a/doc/develop.md b/doc/develop.md index fb75f471..21949ea6 100644 --- a/doc/develop.md +++ b/doc/develop.md @@ -461,17 +461,30 @@ meson setup x -Dserver_debugger=true meson configure x -Dserver_debugger=true ``` -Then recompile. +Then recompile, and run scrcpy. -When you start scrcpy, it will start a debugger on port 5005 on the device. +For Android < 11, it will start a debugger on port 5005 on the device and wait: Redirect that port to the computer: ```bash adb forward tcp:5005 tcp:5005 ``` -In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click on -`+`, _Remote_, and fill the form: +For Android >= 11, first find the listening port: + +```bash +adb jdwp +# press Ctrl+C to interrupt +``` + +Then redirect the resulting PID: + +```bash +adb forward tcp:5005 jdwp:XXXX # replace XXXX +``` + +In Android Studio, _Run_ > _Debug_ > _Edit configurations..._ On the left, click +on `+`, _Remote_, and fill the form: - Host: `localhost` - Port: `5005` From 26bf209617d9bf633307eaf9051c03fa0a9f631c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 08:25:15 +0100 Subject: [PATCH 127/278] Replace release.mk by release scripts Since commit 2687d202809dfaafe8f40f613aec131ad9501433, the Makefile named release.mk stopped handling dependencies between recipes, because they have to be executed separately (from different Github Actions jobs). Using a Makefile no longer provides any real benefit. Replace it by several individual release scripts for simplicity and readability. Refs #5306 PR #5515 --- .github/workflows/release.yml | 55 +++++++------ release.mk | 141 ---------------------------------- release.sh | 2 - release/.gitignore | 2 + release/build_common | 5 ++ release/build_server.sh | 14 ++++ release/build_windows.sh | 51 ++++++++++++ release/generate_checksums.sh | 11 +++ release/package_client.sh | 32 ++++++++ release/package_server.sh | 10 +++ release/release.sh | 22 ++++++ release/test_client.sh | 12 +++ release/test_server.sh | 9 +++ 13 files changed, 198 insertions(+), 168 deletions(-) delete mode 100644 release.mk delete mode 100755 release.sh create mode 100644 release/.gitignore create mode 100644 release/build_common create mode 100755 release/build_server.sh create mode 100755 release/build_windows.sh create mode 100755 release/generate_checksums.sh create mode 100755 release/package_client.sh create mode 100755 release/package_server.sh create mode 100755 release/release.sh create mode 100755 release/test_client.sh create mode 100755 release/test_server.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e67c1c21..30984ae3 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,11 +6,15 @@ on: name: description: 'Version name (default is ref name)' +env: + # $VERSION is used by release scripts + VERSION: ${{ github.event.inputs.name || github.ref_name }} + jobs: build-scrcpy-server: runs-on: ubuntu-latest env: - GRADLE: gradle # use native gradle instead of ./gradlew in release.mk + GRADLE: gradle # use native gradle instead of ./gradlew in scripts steps: - name: Checkout code uses: actions/checkout@v4 @@ -22,16 +26,16 @@ jobs: java-version: '17' - name: Test scrcpy-server - run: make -f release.mk test-server + run: release/test_server.sh - name: Build scrcpy-server - run: make -f release.mk build-server + run: release/build_server.sh - name: Upload scrcpy-server artifact uses: actions/upload-artifact@v4 with: name: scrcpy-server - path: build-server/server/scrcpy-server + path: release/work/build-server/server/scrcpy-server test-client: runs-on: ubuntu-latest @@ -46,13 +50,8 @@ jobs: libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev - - name: Build - run: | - meson setup d -Db_sanitize=address,undefined - - name: Test - run: | - meson test -Cd + run: release/test_client.sh build-win32: runs-on: ubuntu-latest @@ -71,14 +70,14 @@ jobs: - name: Workaround for old meson version run by Github Actions run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt - - name: Build scrcpy win32 - run: make -f release.mk build-win32 + - name: Build win32 + run: release/build_windows.sh 32 - name: Upload build-win32 artifact uses: actions/upload-artifact@v4 with: name: build-win32-intermediate - path: build-win32/dist/ + path: release/work/build-win32/dist/ build-win64: runs-on: ubuntu-latest @@ -97,14 +96,14 @@ jobs: - name: Workaround for old meson version run by Github Actions run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt - - name: Build scrcpy win64 - run: make -f release.mk build-win64 + - name: Build win64 + run: release/build_windows.sh 64 - name: Upload build-win64 artifact uses: actions/upload-artifact@v4 with: name: build-win64-intermediate - path: build-win64/dist/ + path: release/work/build-win64/dist/ package: needs: @@ -112,9 +111,6 @@ jobs: - build-win32 - build-win64 runs-on: ubuntu-latest - env: - # $VERSION is used by release.mk - VERSION: ${{ github.event.inputs.name || github.ref_name }} steps: - name: Checkout code uses: actions/checkout@v4 @@ -123,25 +119,34 @@ jobs: uses: actions/download-artifact@v4 with: name: scrcpy-server - path: build-server/server/ + path: release/work/build-server/server/ - name: Download build-win32 uses: actions/download-artifact@v4 with: name: build-win32-intermediate - path: build-win32/dist/ + path: release/work/build-win32/dist/ - name: Download build-win64 uses: actions/download-artifact@v4 with: name: build-win64-intermediate - path: build-win64/dist/ + path: release/work/build-win64/dist/ - - name: Package - run: make -f release.mk package + - name: Package server + run: release/package_server.sh + + - name: Package win32 + run: release/package_client.sh win32 + + - name: Package win64 + run: release/package_client.sh win64 + + - name: Generate checksums + run: release/generate_checksums.sh - name: Upload release artifact uses: actions/upload-artifact@v4 with: name: scrcpy-release-${{ env.VERSION }} - path: release-${{ env.VERSION }} + path: release/output diff --git a/release.mk b/release.mk deleted file mode 100644 index 61145002..00000000 --- a/release.mk +++ /dev/null @@ -1,141 +0,0 @@ -# This makefile provides recipes to build a "portable" version of scrcpy for -# Windows. -# -# Here, "portable" means that the client and server binaries are expected to be -# anywhere, but in the same directory, instead of well-defined separate -# locations (e.g. /usr/bin/scrcpy and /usr/share/scrcpy/scrcpy-server). -# -# In particular, this implies to change the location from where the client push -# the server to the device. - -.PHONY: default clean \ - test test-client test-server \ - build-server \ - prepare-deps-win32 prepare-deps-win64 \ - build-win32 build-win64 \ - zip-win32 zip-win64 \ - package release - -GRADLE ?= ./gradlew - -TEST_BUILD_DIR := build-test -SERVER_BUILD_DIR := build-server -WIN32_BUILD_DIR := build-win32 -WIN64_BUILD_DIR := build-win64 - -VERSION ?= $(shell git describe --tags --exclude='*install-release' --always) - -ZIP := zip -WIN32_TARGET_DIR := scrcpy-win32-$(VERSION) -WIN64_TARGET_DIR := scrcpy-win64-$(VERSION) -WIN32_TARGET := $(WIN32_TARGET_DIR).zip -WIN64_TARGET := $(WIN64_TARGET_DIR).zip - -RELEASE_DIR := release-$(VERSION) - -release: clean test build-server build-win32 build-win64 package - -clean: - $(GRADLE) clean - rm -rf "$(ZIP)" "$(TEST_BUILD_DIR)" "$(SERVER_BUILD_DIR)" \ - "$(WIN32_BUILD_DIR)" "$(WIN64_BUILD_DIR)" - -test-client: - [ -d "$(TEST_BUILD_DIR)" ] || ( mkdir "$(TEST_BUILD_DIR)" && \ - meson setup "$(TEST_BUILD_DIR)" -Db_sanitize=address ) - ninja -C "$(TEST_BUILD_DIR)" - -test-server: - $(GRADLE) -p server check - -test: test-client test-server - -build-server: - $(GRADLE) -p server assembleRelease - mkdir -p "$(SERVER_BUILD_DIR)/server" - cp server/build/outputs/apk/release/server-release-unsigned.apk \ - "$(SERVER_BUILD_DIR)/server/scrcpy-server" - -prepare-deps-win32: - @app/deps/adb.sh win32 - @app/deps/sdl.sh win32 - @app/deps/ffmpeg.sh win32 - @app/deps/libusb.sh win32 - -prepare-deps-win64: - @app/deps/adb.sh win64 - @app/deps/sdl.sh win64 - @app/deps/ffmpeg.sh win64 - @app/deps/libusb.sh win64 - -build-win32: prepare-deps-win32 - rm -rf "$(WIN32_BUILD_DIR)" - mkdir -p "$(WIN32_BUILD_DIR)/local" - meson setup "$(WIN32_BUILD_DIR)" \ - --pkg-config-path="app/deps/work/install/win32/lib/pkgconfig" \ - -Dc_args="-I$(PWD)/app/deps/work/install/win32/include" \ - -Dc_link_args="-L$(PWD)/app/deps/work/install/win32/lib" \ - --cross-file=cross_win32.txt \ - --buildtype=release --strip -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true - ninja -C "$(WIN32_BUILD_DIR)" - # Group intermediate outputs into a 'dist' directory - mkdir -p "$(WIN32_BUILD_DIR)/dist" - cp "$(WIN32_BUILD_DIR)"/app/scrcpy.exe "$(WIN32_BUILD_DIR)/dist/" - cp app/data/scrcpy-console.bat "$(WIN32_BUILD_DIR)/dist/" - cp app/data/scrcpy-noconsole.vbs "$(WIN32_BUILD_DIR)/dist/" - cp app/data/icon.png "$(WIN32_BUILD_DIR)/dist/" - cp app/data/open_a_terminal_here.bat "$(WIN32_BUILD_DIR)/dist/" - cp app/deps/work/install/win32/bin/*.dll "$(WIN32_BUILD_DIR)/dist/" - cp app/deps/work/install/win32/bin/adb.exe "$(WIN32_BUILD_DIR)/dist/" - -build-win64: prepare-deps-win64 - rm -rf "$(WIN64_BUILD_DIR)" - mkdir -p "$(WIN64_BUILD_DIR)/local" - meson setup "$(WIN64_BUILD_DIR)" \ - --pkg-config-path="app/deps/work/install/win64/lib/pkgconfig" \ - -Dc_args="-I$(PWD)/app/deps/work/install/win64/include" \ - -Dc_link_args="-L$(PWD)/app/deps/work/install/win64/lib" \ - --cross-file=cross_win64.txt \ - --buildtype=release --strip -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true - ninja -C "$(WIN64_BUILD_DIR)" - # Group intermediate outputs into a 'dist' directory - mkdir -p "$(WIN64_BUILD_DIR)/dist" - cp "$(WIN64_BUILD_DIR)"/app/scrcpy.exe "$(WIN64_BUILD_DIR)/dist/" - cp app/data/scrcpy-console.bat "$(WIN64_BUILD_DIR)/dist/" - cp app/data/scrcpy-noconsole.vbs "$(WIN64_BUILD_DIR)/dist/" - cp app/data/icon.png "$(WIN64_BUILD_DIR)/dist/" - cp app/data/open_a_terminal_here.bat "$(WIN64_BUILD_DIR)/dist/" - cp app/deps/work/install/win64/bin/*.dll "$(WIN64_BUILD_DIR)/dist/" - cp app/deps/work/install/win64/bin/adb.exe "$(WIN64_BUILD_DIR)/dist/" - -zip-win32: - mkdir -p "$(ZIP)/$(WIN32_TARGET_DIR)" - cp -r "$(WIN32_BUILD_DIR)/dist/." "$(ZIP)/$(WIN32_TARGET_DIR)/" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN32_TARGET_DIR)/" - cd "$(ZIP)"; \ - zip -r "$(WIN32_TARGET)" "$(WIN32_TARGET_DIR)" - rm -rf "$(ZIP)/$(WIN32_TARGET_DIR)" - -zip-win64: - mkdir -p "$(ZIP)/$(WIN64_TARGET_DIR)" - cp -r "$(WIN64_BUILD_DIR)/dist/." "$(ZIP)/$(WIN64_TARGET_DIR)/" - cp "$(SERVER_BUILD_DIR)"/server/scrcpy-server "$(ZIP)/$(WIN64_TARGET_DIR)/" - cd "$(ZIP)"; \ - zip -r "$(WIN64_TARGET)" "$(WIN64_TARGET_DIR)" - rm -rf "$(ZIP)/$(WIN64_TARGET_DIR)" - -package: zip-win32 zip-win64 - mkdir -p "$(RELEASE_DIR)" - cp "$(SERVER_BUILD_DIR)/server/scrcpy-server" \ - "$(RELEASE_DIR)/scrcpy-server-$(VERSION)" - cp "$(ZIP)/$(WIN32_TARGET)" "$(RELEASE_DIR)" - cp "$(ZIP)/$(WIN64_TARGET)" "$(RELEASE_DIR)" - cd "$(RELEASE_DIR)" && \ - sha256sum "scrcpy-server-$(VERSION)" \ - "scrcpy-win32-$(VERSION).zip" \ - "scrcpy-win64-$(VERSION).zip" > SHA256SUMS.txt - @echo "Release generated in $(RELEASE_DIR)/" diff --git a/release.sh b/release.sh deleted file mode 100755 index 51ce2e38..00000000 --- a/release.sh +++ /dev/null @@ -1,2 +0,0 @@ -#!/bin/bash -make -f release.mk diff --git a/release/.gitignore b/release/.gitignore new file mode 100644 index 00000000..ed363cdf --- /dev/null +++ b/release/.gitignore @@ -0,0 +1,2 @@ +/work +/output diff --git a/release/build_common b/release/build_common new file mode 100644 index 00000000..199a80b6 --- /dev/null +++ b/release/build_common @@ -0,0 +1,5 @@ +# This file must be sourced from the release scripts directory +WORK_DIR="$PWD/work" +OUTPUT_DIR="$PWD/output" + +VERSION="${VERSION:-$(git describe --tags --always)}" diff --git a/release/build_server.sh b/release/build_server.sh new file mode 100755 index 00000000..f52672de --- /dev/null +++ b/release/build_server.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +GRADLE="${GRADLE:-./gradlew}" +SERVER_BUILD_DIR="$WORK_DIR/build-server" + +rm -rf "$SERVER_BUILD_DIR" +"$GRADLE" -p server assembleRelease +mkdir -p "$SERVER_BUILD_DIR/server" +cp server/build/outputs/apk/release/server-release-unsigned.apk \ + "$SERVER_BUILD_DIR/server/scrcpy-server" diff --git a/release/build_windows.sh b/release/build_windows.sh new file mode 100755 index 00000000..74bd32fc --- /dev/null +++ b/release/build_windows.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -ex + +case "$1" in + 32) + WINXX=win32 + ;; + 64) + WINXX=win64 + ;; + *) + echo "ERROR: $0 must be called with one argument: 32 or 64" >&2 + exit 1 + ;; +esac + +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" + +app/deps/adb.sh $WINXX +app/deps/sdl.sh $WINXX +app/deps/ffmpeg.sh $WINXX +app/deps/libusb.sh $WINXX + +DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX" + +rm -rf "$WINXX_BUILD_DIR" +meson setup "$WINXX_BUILD_DIR" \ + --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ + -Dc_args="-I$DEPS_INSTALL_DIR/include" \ + -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ + --cross-file=cross_$WINXX.txt \ + --buildtype=release \ + --strip \ + -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true +ninja -C "$WINXX_BUILD_DIR" + +# Group intermediate outputs into a 'dist' directory +mkdir -p "$WINXX_BUILD_DIR/dist" +cp "$WINXX_BUILD_DIR"/app/scrcpy.exe "$WINXX_BUILD_DIR/dist/" +cp app/data/scrcpy-console.bat "$WINXX_BUILD_DIR/dist/" +cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/" +cp app/data/icon.png "$WINXX_BUILD_DIR/dist/" +cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/" +cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/" +cp "$DEPS_INSTALL_DIR"/bin/adb.exe "$WINXX_BUILD_DIR/dist/" diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh new file mode 100755 index 00000000..a57f1523 --- /dev/null +++ b/release/generate_checksums.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common + +cd "$OUTPUT_DIR" +sha256sum "scrcpy-server-$VERSION" \ + "scrcpy-win32-$VERSION.zip" \ + "scrcpy-win64-$VERSION.zip" \ + | tee SHA256SUMS.txt +echo "Release checksums generated in $PWD/SHA256SUMS.txt" diff --git a/release/package_client.sh b/release/package_client.sh new file mode 100755 index 00000000..f69b2332 --- /dev/null +++ b/release/package_client.sh @@ -0,0 +1,32 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +if [[ $# != 1 ]] +then + # : for example win64 + echo "Syntax: $0 " >&2 + exit 1 + +fi + +BUILD_DIR="$WORK_DIR/build-$1" +ARCHIVE_DIR="$BUILD_DIR/release-archive" +TARGET="scrcpy-$1-$VERSION" + +rm -rf "$ARCHIVE_DIR/$TARGET" +mkdir -p "$ARCHIVE_DIR/$TARGET" + +cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET/" +cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET/" + +mkdir -p "$OUTPUT_DIR" + +cd "$ARCHIVE_DIR" +rm -f "$OUTPUT_DIR/$TARGET.zip" +zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET" +rm -rf "$TARGET" +cd - +echo "Generated '$OUTPUT_DIR/$TARGET.zip'" diff --git a/release/package_server.sh b/release/package_server.sh new file mode 100755 index 00000000..a856cebb --- /dev/null +++ b/release/package_server.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +OUTPUT_DIR="$PWD/output" +. build_common +cd .. # root project dir + +mkdir -p "$OUTPUT_DIR" +cp "$WORK_DIR/build-server/server/scrcpy-server" "$OUTPUT_DIR/scrcpy-server-$VERSION" +echo "Generated '$OUTPUT_DIR/scrcpy-server-$VERSION'" diff --git a/release/release.sh b/release/release.sh new file mode 100755 index 00000000..0760089f --- /dev/null +++ b/release/release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# To customize the version name: +# VERSION=myversion ./release.sh +set -e + +cd "$(dirname ${BASH_SOURCE[0]})" +rm -rf output + +./test_server.sh +./test_client.sh + +./build_server.sh +./build_windows.sh 32 +./build_windows.sh 64 + +./package_server.sh +./package_client.sh win32 +./package_client.sh win64 + +./generate_checksums.sh + +echo "Release generated in $PWD/output" diff --git a/release/test_client.sh b/release/test_client.sh new file mode 100755 index 00000000..6059541d --- /dev/null +++ b/release/test_client.sh @@ -0,0 +1,12 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +TEST_BUILD_DIR="$WORK_DIR/build-test" + +rm -rf "$TEST_BUILD_DIR" +meson setup "$TEST_BUILD_DIR" -Dcompile_server=false \ + -Db_sanitize=address,undefined +ninja -C "$TEST_BUILD_DIR" test diff --git a/release/test_server.sh b/release/test_server.sh new file mode 100755 index 00000000..940e8c1a --- /dev/null +++ b/release/test_server.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +GRADLE="${GRADLE:-./gradlew}" + +"$GRADLE" -p server check From 5df218d8f9419f555000fb21758bad46350064fb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 08:25:33 +0100 Subject: [PATCH 128/278] Test scrcpy-server in a separate CI job Use a separate GitHub Action job to build and test the server. PR #5515 --- .github/workflows/release.yml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 30984ae3..994c55fa 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ env: VERSION: ${{ github.event.inputs.name || github.ref_name }} jobs: - build-scrcpy-server: + test-scrcpy-server: runs-on: ubuntu-latest env: GRADLE: gradle # use native gradle instead of ./gradlew in scripts @@ -28,6 +28,20 @@ jobs: - name: Test scrcpy-server run: release/test_server.sh + build-scrcpy-server: + runs-on: ubuntu-latest + env: + GRADLE: gradle # use native gradle instead of ./gradlew in scripts + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + - name: Build scrcpy-server run: release/build_server.sh From a57180047c58b37ad135c6bd322b02d7122f6dd4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 21:43:57 +0100 Subject: [PATCH 129/278] Split packaging for each target on CI Create separate jobs for packaging win32 and win64 releases. PR #5515 --- .github/workflows/release.yml | 70 +++++++++++++++++++++++++++++++---- 1 file changed, 62 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 994c55fa..f7ac87cb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -119,11 +119,10 @@ jobs: name: build-win64-intermediate path: release/work/build-win64/dist/ - package: + package-win32: needs: - build-scrcpy-server - build-win32 - - build-win64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -141,21 +140,76 @@ jobs: name: build-win32-intermediate path: release/work/build-win32/dist/ + - name: Package win32 + run: release/package_client.sh win32 + + - name: Upload win32 release + uses: actions/upload-artifact@v4 + with: + name: release-win32 + path: release/output/ + + package-win64: + needs: + - build-scrcpy-server + - build-win64 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + - name: Download build-win64 uses: actions/download-artifact@v4 with: name: build-win64-intermediate path: release/work/build-win64/dist/ - - name: Package server - run: release/package_server.sh - - - name: Package win32 - run: release/package_client.sh win32 - - name: Package win64 run: release/package_client.sh win64 + - name: Upload win64 release + uses: actions/upload-artifact@v4 + with: + name: release-win64 + path: release/output + + release: + needs: + - build-scrcpy-server + - package-win32 + - package-win64 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + + - name: Download release-win32 + uses: actions/download-artifact@v4 + with: + name: release-win32 + path: release/output/ + + - name: Download release-win64 + uses: actions/download-artifact@v4 + with: + name: release-win64 + path: release/output/ + + - name: Package server + run: release/package_server.sh + - name: Generate checksums run: release/generate_checksums.sh From 7fc694328483319105408e0bb767c6eb2341632b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 21:51:58 +0100 Subject: [PATCH 130/278] Preserve file permissions in GitHub Actions The upload-artifact action does not preserve file permissions: Even if it is not critical for Windows releases, it will be for other platforms. Wrap everything in a tarball to keep original permissions. PR #5515 --- .github/workflows/release.yml | 36 +++++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7ac87cb..703bb777 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,11 +87,19 @@ jobs: - name: Build win32 run: release/build_windows.sh 32 + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-win32 + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + - name: Upload build-win32 artifact uses: actions/upload-artifact@v4 with: name: build-win32-intermediate - path: release/work/build-win32/dist/ + path: release/work/build-win32/dist-tar/ build-win64: runs-on: ubuntu-latest @@ -113,11 +121,19 @@ jobs: - name: Build win64 run: release/build_windows.sh 64 + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-win64 + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + - name: Upload build-win64 artifact uses: actions/upload-artifact@v4 with: name: build-win64-intermediate - path: release/work/build-win64/dist/ + path: release/work/build-win64/dist-tar/ package-win32: needs: @@ -138,7 +154,13 @@ jobs: uses: actions/download-artifact@v4 with: name: build-win32-intermediate - path: release/work/build-win32/dist/ + path: release/work/build-win32/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-win32 + tar xf dist-tar/dist.tar.gz - name: Package win32 run: release/package_client.sh win32 @@ -168,7 +190,13 @@ jobs: uses: actions/download-artifact@v4 with: name: build-win64-intermediate - path: release/work/build-win64/dist/ + path: release/work/build-win64/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-win64 + tar xf dist-tar/dist.tar.gz - name: Package win64 run: release/package_client.sh win64 From d74f564f563f17a807106b4d2507a6cd4b6cbc3f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:08:18 +0100 Subject: [PATCH 131/278] Reorder FFmpeg configure args All --disable, then all --enable. PR #5515 --- app/deps/ffmpeg.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 93612c1b..fa170046 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -65,6 +65,7 @@ else --disable-avdevice \ --disable-network \ --disable-everything \ + --disable-vulkan \ --enable-swresample \ --enable-decoder=h264 \ --enable-decoder=hevc \ @@ -83,7 +84,6 @@ else --enable-muxer=opus \ --enable-muxer=flac \ --enable-muxer=wav \ - --disable-vulkan fi make -j From 73b595c806db12e78a5f37de2476309b56a0fac9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:09:06 +0100 Subject: [PATCH 132/278] Disable VDPAU and VAAPI for FFmpeg build They are not used, and this prevents Linux builds from working if the dependencies are unavailable. PR #5515 --- app/deps/ffmpeg.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index fa170046..c676664e 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -66,6 +66,8 @@ else --disable-network \ --disable-everything \ --disable-vulkan \ + --disable-vaapi \ + --disable-vdpau \ --enable-swresample \ --enable-decoder=h264 \ --enable-decoder=hevc \ From cf0098abf0f4198e199f373f7831673f86be44c0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Nov 2024 22:44:12 +0100 Subject: [PATCH 133/278] Store dependencies configure args in bash arrays This will make it easy to conditionally add items. PR #5515 --- app/deps/ffmpeg.sh | 81 ++++++++++++++++++++++++---------------------- app/deps/libusb.sh | 13 +++++--- app/deps/sdl.sh | 11 ++++--- 3 files changed, 57 insertions(+), 48 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index c676664e..94fb06d2 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -47,45 +47,48 @@ else mkdir "$HOST" cd "$HOST" - "$SOURCES_DIR/$PROJECT_DIR"/configure \ - --prefix="$INSTALL_DIR/$HOST" \ - --enable-cross-compile \ - --target-os=mingw32 \ - --arch="$ARCH" \ - --cross-prefix="${HOST_TRIPLET}-" \ - --cc="${HOST_TRIPLET}-gcc" \ - --extra-cflags="-O2 -fPIC" \ - --enable-shared \ - --disable-static \ - --disable-programs \ - --disable-doc \ - --disable-swscale \ - --disable-postproc \ - --disable-avfilter \ - --disable-avdevice \ - --disable-network \ - --disable-everything \ - --disable-vulkan \ - --disable-vaapi \ - --disable-vdpau \ - --enable-swresample \ - --enable-decoder=h264 \ - --enable-decoder=hevc \ - --enable-decoder=av1 \ - --enable-decoder=pcm_s16le \ - --enable-decoder=opus \ - --enable-decoder=aac \ - --enable-decoder=flac \ - --enable-decoder=png \ - --enable-protocol=file \ - --enable-demuxer=image2 \ - --enable-parser=png \ - --enable-zlib \ - --enable-muxer=matroska \ - --enable-muxer=mp4 \ - --enable-muxer=opus \ - --enable-muxer=flac \ - --enable-muxer=wav \ + conf=( + --prefix="$INSTALL_DIR/$HOST" + --enable-cross-compile + --target-os=mingw32 + --arch="$ARCH" + --cross-prefix="${HOST_TRIPLET}-" + --cc="${HOST_TRIPLET}-gcc" + --extra-cflags="-O2 -fPIC" + --enable-shared + --disable-static + --disable-programs + --disable-doc + --disable-swscale + --disable-postproc + --disable-avfilter + --disable-avdevice + --disable-network + --disable-everything + --disable-vulkan + --disable-vaapi + --disable-vdpau + --enable-swresample + --enable-decoder=h264 + --enable-decoder=hevc + --enable-decoder=av1 + --enable-decoder=pcm_s16le + --enable-decoder=opus + --enable-decoder=aac + --enable-decoder=flac + --enable-decoder=png + --enable-protocol=file + --enable-demuxer=image2 + --enable-parser=png + --enable-zlib + --enable-muxer=matroska + --enable-muxer=mp4 + --enable-muxer=opus + --enable-muxer=flac + --enable-muxer=wav + ) + + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi make -j diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 26f0140b..77a904b2 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -33,12 +33,15 @@ else mkdir "$HOST" cd "$HOST" - "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh - "$SOURCES_DIR/$PROJECT_DIR"/configure \ - --prefix="$INSTALL_DIR/$HOST" \ - --host="$HOST_TRIPLET" \ - --enable-shared \ + conf=( + --prefix="$INSTALL_DIR/$HOST" + --host="$HOST_TRIPLET" + --enable-shared --disable-static + ) + + "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi make -j diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 55866ccd..1bdd9a4b 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -33,11 +33,14 @@ else mkdir "$HOST" cd "$HOST" - "$SOURCES_DIR/$PROJECT_DIR"/configure \ - --prefix="$INSTALL_DIR/$HOST" \ - --host="$HOST_TRIPLET" \ - --enable-shared \ + conf=( + --prefix="$INSTALL_DIR/$HOST" + --host="$HOST_TRIPLET" + --enable-shared --disable-static + ) + + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi make -j From 6a81fc438b6457eaab6efe61c41c194791b9439f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:14:05 +0100 Subject: [PATCH 134/278] Extract args processing in deps scripts Extract the code that processes arguments into a function. This will make it optional, so the script that only downloads the official ADB binaries will not use arguments. PR #5515 --- app/deps/adb.sh | 1 + app/deps/common | 36 +++++++++++++++++++----------------- app/deps/ffmpeg.sh | 1 + app/deps/libusb.sh | 1 + app/deps/sdl.sh | 1 + 5 files changed, 23 insertions(+), 17 deletions(-) diff --git a/app/deps/adb.sh b/app/deps/adb.sh index b07f29b3..25727535 100755 --- a/app/deps/adb.sh +++ b/app/deps/adb.sh @@ -3,6 +3,7 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common +process_args "$@" VERSION=35.0.2 FILENAME=platform-tools_r$VERSION-win.zip diff --git a/app/deps/common b/app/deps/common index c1cc7729..6f8f80dc 100644 --- a/app/deps/common +++ b/app/deps/common @@ -1,25 +1,27 @@ #!/usr/bin/env bash # This file is intended to be sourced by other scripts, not executed -if [[ $# != 1 ]] -then - # : win32 or win64 - echo "Syntax: $0 " >&2 - exit 1 -fi +process_args() { + if [[ $# != 1 ]] + then + # : win32 or win64 + echo "Syntax: $0 " >&2 + exit 1 + fi -HOST="$1" + HOST="$1" -if [[ "$HOST" = win32 ]] -then - HOST_TRIPLET=i686-w64-mingw32 -elif [[ "$HOST" = win64 ]] -then - HOST_TRIPLET=x86_64-w64-mingw32 -else - echo "Unsupported host: $HOST" >&2 - exit 1 -fi + if [[ "$HOST" = win32 ]] + then + HOST_TRIPLET=i686-w64-mingw32 + elif [[ "$HOST" = win64 ]] + then + HOST_TRIPLET=x86_64-w64-mingw32 + else + echo "Unsupported host: $HOST" >&2 + exit 1 + fi +} DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 94fb06d2..20e59375 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -3,6 +3,7 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common +process_args "$@" VERSION=7.1 FILENAME=ffmpeg-$VERSION.tar.xz diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 77a904b2..ee36d141 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -3,6 +3,7 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common +process_args "$@" VERSION=1.0.27 FILENAME=libusb-$VERSION.tar.gz diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 1bdd9a4b..d8d0d734 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -3,6 +3,7 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common +process_args "$@" VERSION=2.30.9 FILENAME=SDL-$VERSION.tar.gz From 98d2065d6d06d44b3c7af3d199e29cabc6f58b80 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:17:41 +0100 Subject: [PATCH 135/278] Make the ADB dependency script Windows-specific This will allow adding similar scripts for other platforms. PR #5515 --- app/deps/{adb.sh => adb_windows.sh} | 9 ++++----- release/build_windows.sh | 5 +++-- 2 files changed, 7 insertions(+), 7 deletions(-) rename app/deps/{adb.sh => adb_windows.sh} (79%) diff --git a/app/deps/adb.sh b/app/deps/adb_windows.sh similarity index 79% rename from app/deps/adb.sh rename to app/deps/adb_windows.sh index 25727535..d36706b0 100755 --- a/app/deps/adb.sh +++ b/app/deps/adb_windows.sh @@ -3,11 +3,10 @@ set -ex DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -process_args "$@" VERSION=35.0.2 FILENAME=platform-tools_r$VERSION-win.zip -PROJECT_DIR=platform-tools-$VERSION +PROJECT_DIR=platform-tools-$VERSION-windows SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 cd "$SOURCES_DIR" @@ -28,6 +27,6 @@ else rmdir "$ZIP_PREFIX" fi -mkdir -p "$INSTALL_DIR/$HOST/bin" -cd "$INSTALL_DIR/$HOST/bin" -cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/$HOST/bin/" +mkdir -p "$INSTALL_DIR/adb-windows" +cd "$INSTALL_DIR/adb-windows" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-windows/" diff --git a/release/build_windows.sh b/release/build_windows.sh index 74bd32fc..1b738ea3 100755 --- a/release/build_windows.sh +++ b/release/build_windows.sh @@ -20,12 +20,13 @@ cd .. # root project dir WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" -app/deps/adb.sh $WINXX +app/deps/adb_windows.sh app/deps/sdl.sh $WINXX app/deps/ffmpeg.sh $WINXX app/deps/libusb.sh $WINXX DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX" +ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows" rm -rf "$WINXX_BUILD_DIR" meson setup "$WINXX_BUILD_DIR" \ @@ -48,4 +49,4 @@ cp app/data/scrcpy-noconsole.vbs "$WINXX_BUILD_DIR/dist/" cp app/data/icon.png "$WINXX_BUILD_DIR/dist/" cp app/data/open_a_terminal_here.bat "$WINXX_BUILD_DIR/dist/" cp "$DEPS_INSTALL_DIR"/bin/*.dll "$WINXX_BUILD_DIR/dist/" -cp "$DEPS_INSTALL_DIR"/bin/adb.exe "$WINXX_BUILD_DIR/dist/" +cp -r "$ADB_INSTALL_DIR"/. "$WINXX_BUILD_DIR/dist/" From 360936248c0fb59bdc211e298d1aaea49af5ee07 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Nov 2024 23:16:18 +0100 Subject: [PATCH 136/278] Add support for build and link types for deps Make dependencies build scripts more flexible, to accept a build type (native or cross) and a link type (static or shared). This lays the groundwork for building binaries for Linux and macOS. PR #5515 --- app/deps/common | 38 +++++++++++++----- app/deps/ffmpeg.sh | 85 ++++++++++++++++++++++++++-------------- app/deps/libusb.sh | 35 ++++++++++++----- app/deps/sdl.sh | 40 ++++++++++++++----- release/build_windows.sh | 8 ++-- 5 files changed, 145 insertions(+), 61 deletions(-) diff --git a/app/deps/common b/app/deps/common index 6f8f80dc..49587e17 100644 --- a/app/deps/common +++ b/app/deps/common @@ -2,25 +2,45 @@ # This file is intended to be sourced by other scripts, not executed process_args() { - if [[ $# != 1 ]] + if [[ $# != 3 ]] then # : win32 or win64 - echo "Syntax: $0 " >&2 + # : native or cross + # : static or shared + echo "Syntax: $0 " >&2 exit 1 fi HOST="$1" + BUILD_TYPE="$2" # native or cross + LINK_TYPE="$3" # static or shared + DIRNAME="$HOST-$BUILD_TYPE-$LINK_TYPE" - if [[ "$HOST" = win32 ]] + if [[ "$BUILD_TYPE" != native && "$BUILD_TYPE" != cross ]] then - HOST_TRIPLET=i686-w64-mingw32 - elif [[ "$HOST" = win64 ]] - then - HOST_TRIPLET=x86_64-w64-mingw32 - else - echo "Unsupported host: $HOST" >&2 + echo "Unsupported build type (expected native or cross): $BUILD_TYPE" >&2 exit 1 fi + + if [[ "$LINK_TYPE" != static && "$LINK_TYPE" != shared ]] + then + echo "Unsupported link type (expected static or shared): $LINK_TYPE" >&2 + exit 1 + fi + + if [[ "$BUILD_TYPE" == cross ]] + then + if [[ "$HOST" = win32 ]] + then + HOST_TRIPLET=i686-w64-mingw32 + elif [[ "$HOST" = win64 ]] + then + HOST_TRIPLET=x86_64-w64-mingw32 + else + echo "Unsupported cross-build to host: $HOST" >&2 + exit 1 + fi + fi } DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 20e59375..2484da23 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -23,41 +23,26 @@ fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" cd "$BUILD_DIR/$PROJECT_DIR" -if [[ "$HOST" = win32 ]] +if [[ -d "$DIRNAME" ]] then - ARCH=x86 -elif [[ "$HOST" = win64 ]] -then - ARCH=x86_64 + echo "'$PWD/$DIRNAME' already exists, not reconfigured" + cd "$DIRNAME" else - echo "Unsupported host: $HOST" >&2 - exit 1 -fi + mkdir "$DIRNAME" + cd "$DIRNAME" -# -static-libgcc to avoid missing libgcc_s_dw2-1.dll -# -static to avoid dynamic dependency to zlib -export CFLAGS='-static-libgcc -static' -export CXXFLAGS="$CFLAGS" -export LDFLAGS='-static-libgcc -static' - -if [[ -d "$HOST" ]] -then - echo "'$PWD/$HOST' already exists, not reconfigured" - cd "$HOST" -else - mkdir "$HOST" - cd "$HOST" + if [[ "$HOST" == win* ]] + then + # -static-libgcc to avoid missing libgcc_s_dw2-1.dll + # -static to avoid dynamic dependency to zlib + export CFLAGS='-static-libgcc -static' + export CXXFLAGS="$CFLAGS" + export LDFLAGS='-static-libgcc -static' + fi conf=( - --prefix="$INSTALL_DIR/$HOST" - --enable-cross-compile - --target-os=mingw32 - --arch="$ARCH" - --cross-prefix="${HOST_TRIPLET}-" - --cc="${HOST_TRIPLET}-gcc" + --prefix="$INSTALL_DIR/$DIRNAME" --extra-cflags="-O2 -fPIC" - --enable-shared - --disable-static --disable-programs --disable-doc --disable-swscale @@ -89,6 +74,48 @@ else --enable-muxer=wav ) + if [[ "$LINK_TYPE" == static ]] + then + conf+=( + --enable-static + --disable-shared + ) + else + conf+=( + --disable-static + --enable-shared + ) + fi + + if [[ "$BUILD_TYPE" == cross ]] + then + conf+=( + --enable-cross-compile + --cross-prefix="${HOST_TRIPLET}-" + --cc="${HOST_TRIPLET}-gcc" + ) + + case "$HOST" in + win32) + conf+=( + --target-os=mingw32 + --arch=x86 + ) + ;; + + win64) + conf+=( + --target-os=mingw32 + --arch=x86_64 + ) + ;; + + *) + echo "Unsupported host: $HOST" >&2 + exit 1 + esac + fi + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index ee36d141..340b0f70 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -26,21 +26,38 @@ cd "$BUILD_DIR/$PROJECT_DIR" export CFLAGS='-O2' export CXXFLAGS="$CFLAGS" -if [[ -d "$HOST" ]] +if [[ -d "$DIRNAME" ]] then - echo "'$PWD/$HOST' already exists, not reconfigured" - cd "$HOST" + echo "'$PWD/$DIRNAME' already exists, not reconfigured" + cd "$DIRNAME" else - mkdir "$HOST" - cd "$HOST" + mkdir "$DIRNAME" + cd "$DIRNAME" conf=( - --prefix="$INSTALL_DIR/$HOST" - --host="$HOST_TRIPLET" - --enable-shared - --disable-static + --prefix="$INSTALL_DIR/$DIRNAME" ) + if [[ "$LINK_TYPE" == static ]] + then + conf+=( + --enable-static + --disable-shared + ) + else + conf+=( + --disable-static + --enable-shared + ) + fi + + if [[ "$BUILD_TYPE" == cross ]] + then + conf+=( + --host="$HOST_TRIPLET" + ) + fi + "$SOURCES_DIR/$PROJECT_DIR"/bootstrap.sh "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index d8d0d734..71314118 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -26,21 +26,38 @@ cd "$BUILD_DIR/$PROJECT_DIR" export CFLAGS='-O2' export CXXFLAGS="$CFLAGS" -if [[ -d "$HOST" ]] +if [[ -d "$DIRNAME" ]] then - echo "'$PWD/$HOST' already exists, not reconfigured" - cd "$HOST" + echo "'$PWD/$HDIRNAME' already exists, not reconfigured" + cd "$DIRNAME" else - mkdir "$HOST" - cd "$HOST" + mkdir "$DIRNAME" + cd "$DIRNAME" conf=( - --prefix="$INSTALL_DIR/$HOST" - --host="$HOST_TRIPLET" - --enable-shared - --disable-static + --prefix="$INSTALL_DIR/$DIRNAME" ) + if [[ "$LINK_TYPE" == static ]] + then + conf+=( + --enable-static + --disable-shared + ) + else + conf+=( + --disable-static + --enable-shared + ) + fi + + if [[ "$BUILD_TYPE" == cross ]] + then + conf+=( + --host="$HOST_TRIPLET" + ) + fi + "$SOURCES_DIR/$PROJECT_DIR"/configure "${conf[@]}" fi @@ -48,4 +65,7 @@ make -j # There is no "make install-strip" make install # Strip manually -${HOST_TRIPLET}-strip "$INSTALL_DIR/$HOST/bin/SDL2.dll" +if [[ "$LINK_TYPE" == shared && "$HOST" == win* ]] +then + ${HOST_TRIPLET}-strip "$INSTALL_DIR/$DIRNAME/bin/SDL2.dll" +fi diff --git a/release/build_windows.sh b/release/build_windows.sh index 1b738ea3..dbd6cbf4 100755 --- a/release/build_windows.sh +++ b/release/build_windows.sh @@ -21,11 +21,11 @@ cd .. # root project dir WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" app/deps/adb_windows.sh -app/deps/sdl.sh $WINXX -app/deps/ffmpeg.sh $WINXX -app/deps/libusb.sh $WINXX +app/deps/sdl.sh $WINXX cross shared +app/deps/ffmpeg.sh $WINXX cross shared +app/deps/libusb.sh $WINXX cross shared -DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX" +DEPS_INSTALL_DIR="$PWD/app/deps/work/install/$WINXX-cross-shared" ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-windows" rm -rf "$WINXX_BUILD_DIR" From 179c664e2b78f7b1406dc1eed28031591c20934c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 21 Nov 2024 23:28:33 +0100 Subject: [PATCH 137/278] Add static build option Use static dependencies if the option is set. PR #5515 --- app/meson.build | 16 +++++++++------- meson_options.txt | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/app/meson.build b/app/meson.build index 444cf98e..f089ffb1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -109,20 +109,22 @@ endif cc = meson.get_compiler('c') +static = get_option('static') + dependencies = [ - dependency('libavformat', version: '>= 57.33'), - dependency('libavcodec', version: '>= 57.37'), - dependency('libavutil'), - dependency('libswresample'), - dependency('sdl2', version: '>= 2.0.5'), + dependency('libavformat', version: '>= 57.33', static: static), + dependency('libavcodec', version: '>= 57.37', static: static), + dependency('libavutil', static: static), + dependency('libswresample', static: static), + dependency('sdl2', version: '>= 2.0.5', static: static), ] if v4l2_support - dependencies += dependency('libavdevice') + dependencies += dependency('libavdevice', static: static) endif if usb_support - dependencies += dependency('libusb-1.0') + dependencies += dependency('libusb-1.0', static: static) endif if host_machine.system() == 'windows' diff --git a/meson_options.txt b/meson_options.txt index 76075b3a..fd347734 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -2,6 +2,7 @@ option('compile_app', type: 'boolean', value: true, description: 'Build the clie option('compile_server', type: 'boolean', value: true, description: 'Build the server') option('prebuilt_server', type: 'string', description: 'Path of the prebuilt server') option('portable', type: 'boolean', value: false, description: 'Use scrcpy-server from the same directory as the scrcpy executable') +option('static', type: 'boolean', value: false, description: 'Use static dependencies') option('server_debugger', type: 'boolean', value: false, description: 'Run a server debugger and wait for a client to be attached') option('v4l2', type: 'boolean', value: true, description: 'Enable V4L2 feature when supported') option('usb', type: 'boolean', value: true, description: 'Enable HID/OTG features when supported') From 93da693e8c15fe2a1065c37636ab9fa945cb1254 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 21:17:00 +0100 Subject: [PATCH 138/278] Add support for .tar.gz packaging Make package_client.sh accept an archive format. PR #5515 --- .github/workflows/release.yml | 4 ++-- release/package_client.sh | 30 +++++++++++++++++++++++++----- release/release.sh | 4 ++-- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 703bb777..54722c9f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -163,7 +163,7 @@ jobs: tar xf dist-tar/dist.tar.gz - name: Package win32 - run: release/package_client.sh win32 + run: release/package_client.sh win32 zip - name: Upload win32 release uses: actions/upload-artifact@v4 @@ -199,7 +199,7 @@ jobs: tar xf dist-tar/dist.tar.gz - name: Package win64 - run: release/package_client.sh win64 + run: release/package_client.sh win64 zip - name: Upload win64 release uses: actions/upload-artifact@v4 diff --git a/release/package_client.sh b/release/package_client.sh index f69b2332..c6d430b2 100755 --- a/release/package_client.sh +++ b/release/package_client.sh @@ -4,12 +4,20 @@ cd "$(dirname ${BASH_SOURCE[0]})" . build_common cd .. # root project dir -if [[ $# != 1 ]] +if [[ $# != 2 ]] then # : for example win64 - echo "Syntax: $0 " >&2 + # : zip or tar.gz + echo "Syntax: $0 " >&2 exit 1 +fi +FORMAT=$2 + +if [[ "$2" != zip && "$2" != tar.gz ]] +then + echo "Invalid format (expected zip or tar.gz): $2" >&2 + exit 1 fi BUILD_DIR="$WORK_DIR/build-$1" @@ -25,8 +33,20 @@ cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET/" mkdir -p "$OUTPUT_DIR" cd "$ARCHIVE_DIR" -rm -f "$OUTPUT_DIR/$TARGET.zip" -zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET" +rm -f "$OUTPUT_DIR/$TARGET.$FORMAT" + +case "$FORMAT" in + zip) + zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET" + ;; + tar.gz) + tar cvf "$OUTPUT_DIR/$TARGET.tar.gz" "$TARGET" + ;; + *) + echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 + exit 1 +esac + rm -rf "$TARGET" cd - -echo "Generated '$OUTPUT_DIR/$TARGET.zip'" +echo "Generated '$OUTPUT_DIR/$TARGET.$FORMAT'" diff --git a/release/release.sh b/release/release.sh index 0760089f..e07b51c0 100755 --- a/release/release.sh +++ b/release/release.sh @@ -14,8 +14,8 @@ rm -rf output ./build_windows.sh 64 ./package_server.sh -./package_client.sh win32 -./package_client.sh win64 +./package_client.sh win32 zip +./package_client.sh win64 zip ./generate_checksums.sh From cb19686d7950113039f4f478998df8d9ae7ed985 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 21:41:56 +0100 Subject: [PATCH 139/278] Add script to release Linux static binary Provide a prebuilt binary for Linux. Fixes #5327 PR #5515 --- .github/workflows/release.yml | 73 +++++++++++++++++++++++++++++++ app/data/scrcpy_static_wrapper.sh | 6 +++ app/deps/adb_linux.sh | 29 ++++++++++++ app/deps/ffmpeg.sh | 9 +++- app/deps/sdl.sh | 8 ++++ release/build_linux.sh | 35 +++++++++++++++ release/generate_checksums.sh | 1 + release/release.sh | 2 + 8 files changed, 162 insertions(+), 1 deletion(-) create mode 100755 app/data/scrcpy_static_wrapper.sh create mode 100755 app/deps/adb_linux.sh create mode 100755 release/build_linux.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54722c9f..3bc62a3a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -67,6 +67,36 @@ jobs: - name: Test run: release/test_client.sh + build-linux: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ + libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev + + - name: Build linux + run: release/build_linux.sh + + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-linux + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + + - name: Upload build-linux artifact + uses: actions/upload-artifact@v4 + with: + name: build-linux-intermediate + path: release/work/build-linux/dist-tar/ + build-win32: runs-on: ubuntu-latest steps: @@ -135,6 +165,42 @@ jobs: name: build-win64-intermediate path: release/work/build-win64/dist-tar/ + package-linux: + needs: + - build-scrcpy-server + - build-linux + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + + - name: Download build-linux + uses: actions/download-artifact@v4 + with: + name: build-linux-intermediate + path: release/work/build-linux/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-linux + tar xf dist-tar/dist.tar.gz + + - name: Package linux + run: release/package_client.sh linux tar.gz + + - name: Upload linux release + uses: actions/upload-artifact@v4 + with: + name: release-linux + path: release/output/ + package-win32: needs: - build-scrcpy-server @@ -210,6 +276,7 @@ jobs: release: needs: - build-scrcpy-server + - package-linux - package-win32 - package-win64 runs-on: ubuntu-latest @@ -223,6 +290,12 @@ jobs: name: scrcpy-server path: release/work/build-server/server/ + - name: Download release-linux + uses: actions/download-artifact@v4 + with: + name: release-linux + path: release/output/ + - name: Download release-win32 uses: actions/download-artifact@v4 with: diff --git a/app/data/scrcpy_static_wrapper.sh b/app/data/scrcpy_static_wrapper.sh new file mode 100755 index 00000000..ac1e7a95 --- /dev/null +++ b/app/data/scrcpy_static_wrapper.sh @@ -0,0 +1,6 @@ +#!/bin/bash +cd "$(dirname ${BASH_SOURCE[0]})" +export ADB="${ADB:-./adb}" +export SCRCPY_SERVER_PATH="${SCRCPY_SERVER_PATH:-./scrcpy-server}" +export SCRCPY_ICON_PATH="${SCRCPY_ICON_PATH:-./icon.png}" +./scrcpy_bin "$@" diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh new file mode 100755 index 00000000..17b5641d --- /dev/null +++ b/app/deps/adb_linux.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-linux.zip +PROJECT_DIR=platform-tools-$VERSION-linux +SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + mkdir -p "$PROJECT_DIR" + cd "$PROJECT_DIR" + ZIP_PREFIX=platform-tools + unzip "../$FILENAME" "$ZIP_PREFIX"/adb + mv "$ZIP_PREFIX"/* . + rmdir "$ZIP_PREFIX" +fi + +mkdir -p "$INSTALL_DIR/adb-linux" +cd "$INSTALL_DIR/adb-linux" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-linux/" diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 2484da23..90ffa855 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -48,7 +48,6 @@ else --disable-swscale --disable-postproc --disable-avfilter - --disable-avdevice --disable-network --disable-everything --disable-vulkan @@ -74,6 +73,14 @@ else --enable-muxer=wav ) + if [[ "$HOST" != linux ]] + then + # libavdevice is only used for V4L2 on Linux + conf+=( + --disable-avdevice + ) + fi + if [[ "$LINK_TYPE" == static ]] then conf+=( diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 71314118..8698e120 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -38,6 +38,14 @@ else --prefix="$INSTALL_DIR/$DIRNAME" ) + if [[ "$HOST" == linux ]] + then + conf+=( + --enable-video-wayland + --enable-video-x11 + ) + fi + if [[ "$LINK_TYPE" == static ]] then conf+=( diff --git a/release/build_linux.sh b/release/build_linux.sh new file mode 100755 index 00000000..2f2fb62f --- /dev/null +++ b/release/build_linux.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +LINUX_BUILD_DIR="$WORK_DIR/build-linux" + +app/deps/adb_linux.sh +app/deps/sdl.sh linux native static +app/deps/ffmpeg.sh linux native static +app/deps/libusb.sh linux native static + +DEPS_INSTALL_DIR="$PWD/app/deps/work/install/linux-native-static" +ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-linux" + +rm -rf "$LINUX_BUILD_DIR" +meson setup "$LINUX_BUILD_DIR" \ + --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ + -Dc_args="-I$DEPS_INSTALL_DIR/include" \ + -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ + --buildtype=release \ + --strip \ + -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true \ + -Dstatic=true +ninja -C "$LINUX_BUILD_DIR" + +# Group intermediate outputs into a 'dist' directory +mkdir -p "$LINUX_BUILD_DIR/dist" +cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/scrcpy_bin" +cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" +cp app/data/scrcpy_static_wrapper.sh "$LINUX_BUILD_DIR/dist/scrcpy" +cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh index a57f1523..d13120de 100755 --- a/release/generate_checksums.sh +++ b/release/generate_checksums.sh @@ -5,6 +5,7 @@ cd "$(dirname ${BASH_SOURCE[0]})" cd "$OUTPUT_DIR" sha256sum "scrcpy-server-$VERSION" \ + "scrcpy-linux-$VERSION.tar.gz" \ "scrcpy-win32-$VERSION.zip" \ "scrcpy-win64-$VERSION.zip" \ | tee SHA256SUMS.txt diff --git a/release/release.sh b/release/release.sh index e07b51c0..8bef11ab 100755 --- a/release/release.sh +++ b/release/release.sh @@ -12,10 +12,12 @@ rm -rf output ./build_server.sh ./build_windows.sh 32 ./build_windows.sh 64 +./build_linux.sh ./package_server.sh ./package_client.sh win32 zip ./package_client.sh win64 zip +./package_client.sh linux tar.gz ./generate_checksums.sh From 28c372e8387c3473613e6653f58c934f81d8fb2f Mon Sep 17 00:00:00 2001 From: Muvaffak Onus Date: Sat, 23 Nov 2024 18:00:45 +0300 Subject: [PATCH 140/278] Use generic command for SHA-256 The command sha256sum does not exist on macOS, but `shasum -a256` works both on Linux and macOS. PR #5515 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/deps/common | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/deps/common b/app/deps/common index 49587e17..daaa96c0 100644 --- a/app/deps/common +++ b/app/deps/common @@ -59,7 +59,7 @@ checksum() { local file="$1" local sum="$2" echo "$file: verifying checksum..." - echo "$sum $file" | sha256sum -c + echo "$sum $file" | shasum -a256 -c } get_file() { From a7efb180b9befa576513c31e2db0340d0ee33aeb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 22 Nov 2024 22:23:27 +0100 Subject: [PATCH 141/278] Add script to release macOS static binary Provide a prebuilt binary for macOS. Fixes #1733 Fixes #3235 Fixes #4489 Fixes #5327 PR #5515 Co-authored-by: Muvaffak Onus --- .github/workflows/release.yml | 71 +++++++++++++++++++++++++++++++++++ app/deps/adb_macos.sh | 29 ++++++++++++++ app/deps/ffmpeg.sh | 8 ++++ release/build_macos.sh | 35 +++++++++++++++++ release/generate_checksums.sh | 1 + 5 files changed, 144 insertions(+) create mode 100755 app/deps/adb_macos.sh create mode 100755 release/build_macos.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3bc62a3a..40508b7d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -165,6 +165,34 @@ jobs: name: build-win64-intermediate path: release/work/build-win64/dist-tar/ + build-macos: + runs-on: macos-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: | + brew install meson ninja nasm libiconv zlib automake autoconf \ + libtool + + - name: Build macOS + run: release/build_macos.sh + + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-macos + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + + - name: Upload build-macos artifact + uses: actions/upload-artifact@v4 + with: + name: build-macos-intermediate + path: release/work/build-macos/dist-tar/ + package-linux: needs: - build-scrcpy-server @@ -273,12 +301,49 @@ jobs: name: release-win64 path: release/output + package-macos: + needs: + - build-scrcpy-server + - build-macos + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + + - name: Download build-macos + uses: actions/download-artifact@v4 + with: + name: build-macos-intermediate + path: release/work/build-macos/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-macos + tar xf dist-tar/dist.tar.gz + + - name: Package macos + run: release/package_client.sh macos tar.gz + + - name: Upload macos release + uses: actions/upload-artifact@v4 + with: + name: release-macos + path: release/output/ + release: needs: - build-scrcpy-server - package-linux - package-win32 - package-win64 + - package-macos runs-on: ubuntu-latest steps: - name: Checkout code @@ -308,6 +373,12 @@ jobs: name: release-win64 path: release/output/ + - name: Download release-macos + uses: actions/download-artifact@v4 + with: + name: release-macos + path: release/output/ + - name: Package server run: release/package_server.sh diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh new file mode 100755 index 00000000..8a25915e --- /dev/null +++ b/app/deps/adb_macos.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common + +VERSION=35.0.2 +FILENAME=platform-tools_r$VERSION-darwin.zip +PROJECT_DIR=platform-tools-$VERSION-darwin +SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78 + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://dl.google.com/android/repository/$FILENAME" "$FILENAME" "$SHA256SUM" + mkdir -p "$PROJECT_DIR" + cd "$PROJECT_DIR" + ZIP_PREFIX=platform-tools + unzip "../$FILENAME" "$ZIP_PREFIX"/adb + mv "$ZIP_PREFIX"/* . + rmdir "$ZIP_PREFIX" +fi + +mkdir -p "$INSTALL_DIR/adb-macos" +cd "$INSTALL_DIR/adb-macos" +cp -r "$SOURCES_DIR/$PROJECT_DIR"/. "$INSTALL_DIR/adb-macos/" diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 90ffa855..cc71ab13 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -38,6 +38,14 @@ else export CFLAGS='-static-libgcc -static' export CXXFLAGS="$CFLAGS" export LDFLAGS='-static-libgcc -static' + elif [[ "$HOST" == "macos" ]] + then + export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib" + export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include" + + export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib" + export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include" + export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig" fi conf=( diff --git a/release/build_macos.sh b/release/build_macos.sh new file mode 100755 index 00000000..a42c7e88 --- /dev/null +++ b/release/build_macos.sh @@ -0,0 +1,35 @@ +#!/bin/bash +set -ex +cd "$(dirname ${BASH_SOURCE[0]})" +. build_common +cd .. # root project dir + +MACOS_BUILD_DIR="$WORK_DIR/build-macos" + +app/deps/adb_macos.sh +app/deps/sdl.sh macos native static +app/deps/ffmpeg.sh macos native static +app/deps/libusb.sh macos native static + +DEPS_INSTALL_DIR="$PWD/app/deps/work/install/macos-native-static" +ADB_INSTALL_DIR="$PWD/app/deps/work/install/adb-macos" + +rm -rf "$MACOS_BUILD_DIR" +meson setup "$MACOS_BUILD_DIR" \ + --pkg-config-path="$DEPS_INSTALL_DIR/lib/pkgconfig" \ + -Dc_args="-I$DEPS_INSTALL_DIR/include" \ + -Dc_link_args="-L$DEPS_INSTALL_DIR/lib" \ + --buildtype=release \ + --strip \ + -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true \ + -Dstatic=true +ninja -C "$MACOS_BUILD_DIR" + +# Group intermediate outputs into a 'dist' directory +mkdir -p "$MACOS_BUILD_DIR/dist" +cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/scrcpy_bin" +cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" +cp app/data/scrcpy_static_wrapper.sh "$MACOS_BUILD_DIR/dist/scrcpy" +cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh index d13120de..b0464bed 100755 --- a/release/generate_checksums.sh +++ b/release/generate_checksums.sh @@ -8,5 +8,6 @@ sha256sum "scrcpy-server-$VERSION" \ "scrcpy-linux-$VERSION.tar.gz" \ "scrcpy-win32-$VERSION.zip" \ "scrcpy-win64-$VERSION.zip" \ + "scrcpy-macos-$VERSION.tar.gz" \ | tee SHA256SUMS.txt echo "Release checksums generated in $PWD/SHA256SUMS.txt" From 6f9520f3e2f7df6061bfc06053d7d92aba9a24b8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 10:12:25 +0100 Subject: [PATCH 142/278] Test build_without_gradle.sh in GitHub Actions Build the server without gradle to make sure that the script works. PR #5515 --- .github/workflows/release.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 40508b7d..13f4accf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -51,6 +51,21 @@ jobs: name: scrcpy-server path: release/work/build-server/server/scrcpy-server + test-build-scrcpy-server-without-gradle: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JDK + uses: actions/setup-java@v4 + with: + distribution: 'zulu' + java-version: '17' + + - name: Build scrcpy-server without gradle + run: server/build_without_gradle.sh + test-client: runs-on: ubuntu-latest steps: From d40224f299b29d614bbdb2ff0e160b6cccd9af03 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 16:32:56 +0100 Subject: [PATCH 143/278] Fix alphabetic order of cli args --- app/scrcpy.1 | 24 ++++++++++----------- app/src/cli.c | 60 +++++++++++++++++++++++++-------------------------- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 95d5133d..c513dc9a 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -97,18 +97,6 @@ Select the camera size by its aspect ratio (+/- 10%). Possible values are "sensor" (use the camera sensor aspect ratio), "\fInum\fR:\fIden\fR" (e.g. "4:3") and "\fIvalue\fR" (e.g. "1.6"). -.TP -.B \-\-camera\-high\-speed -Enable high-speed camera capture mode. - -This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. - -.TP -.BI "\-\-camera\-id " id -Specify the device camera id to mirror. - -The available camera ids can be listed by \fB\-\-list\-cameras\fR. - .TP .BI "\-\-camera\-facing " facing Select the device camera by its facing direction. @@ -121,6 +109,18 @@ Specify the camera capture frame rate. If not specified, Android's default frame rate (30 fps) is used. +.TP +.B \-\-camera\-high\-speed +Enable high-speed camera capture mode. + +This mode is restricted to specific resolutions and frame rates, listed by \fB\-\-list\-camera\-sizes\fR. + +.TP +.BI "\-\-camera\-id " id +Specify the device camera id to mirror. + +The available camera ids can be listed by \fB\-\-list\-cameras\fR. + .TP .BI "\-\-camera\-size " width\fRx\fIheight Specify an explicit camera capture size. diff --git a/app/src/cli.c b/app/src/cli.c index 3f2d23cb..ee86b34b 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -255,14 +255,6 @@ static const struct sc_option options[] = { "ratio), \":\" (e.g. \"4:3\") or \"\" (e.g. " "\"1.6\")." }, - { - .longopt_id = OPT_CAMERA_ID, - .longopt = "camera-id", - .argdesc = "id", - .text = "Specify the device camera id to mirror.\n" - "The available camera ids can be listed by:\n" - " scrcpy --list-cameras", - }, { .longopt_id = OPT_CAMERA_FACING, .longopt = "camera-facing", @@ -270,6 +262,14 @@ static const struct sc_option options[] = { .text = "Select the device camera by its facing direction.\n" "Possible values are \"front\", \"back\" and \"external\".", }, + { + .longopt_id = OPT_CAMERA_FPS, + .longopt = "camera-fps", + .argdesc = "value", + .text = "Specify the camera capture frame rate.\n" + "If not specified, Android's default frame rate (30 fps) is " + "used.", + }, { .longopt_id = OPT_CAMERA_HIGH_SPEED, .longopt = "camera-high-speed", @@ -277,6 +277,14 @@ static const struct sc_option options[] = { "This mode is restricted to specific resolutions and frame " "rates, listed by --list-camera-sizes.", }, + { + .longopt_id = OPT_CAMERA_ID, + .longopt = "camera-id", + .argdesc = "id", + .text = "Specify the device camera id to mirror.\n" + "The available camera ids can be listed by:\n" + " scrcpy --list-cameras", + }, { .longopt_id = OPT_CAMERA_SIZE, .longopt = "camera-size", @@ -284,12 +292,21 @@ static const struct sc_option options[] = { .text = "Specify an explicit camera capture size.", }, { - .longopt_id = OPT_CAMERA_FPS, - .longopt = "camera-fps", + .longopt_id = OPT_CAPTURE_ORIENTATION, + .longopt = "capture-orientation", .argdesc = "value", - .text = "Specify the camera capture frame rate.\n" - "If not specified, Android's default frame rate (30 fps) is " - "used.", + .text = "Set the capture video orientation.\n" + "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " + "and flip270, possibly prefixed by '@'.\n" + "The number represents the clockwise rotation in degrees; the " + "flip\" keyword applies a horizontal flip before the " + "rotation.\n" + "If a leading '@' is passed (@90) for display capture, then " + "the rotation is locked, and is relative to the natural device " + "orientation.\n" + "If '@' is passed alone, then the rotation is locked to the " + "initial device orientation.\n" + "Default is 0.", }, { // Not really deprecated (--codec has never been released), but without @@ -479,23 +496,6 @@ static const struct sc_option options[] = { .longopt = "list-encoders", .text = "List video and audio encoders available on the device.", }, - { - .longopt_id = OPT_CAPTURE_ORIENTATION, - .longopt = "capture-orientation", - .argdesc = "value", - .text = "Set the capture video orientation.\n" - "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " - "and flip270, possibly prefixed by '@'.\n" - "The number represents the clockwise rotation in degrees; the " - "flip\" keyword applies a horizontal flip before the " - "rotation.\n" - "If a leading '@' is passed (@90) for display capture, then " - "the rotation is locked, and is relative to the natural device " - "orientation.\n" - "If '@' is passed alone, then the rotation is locked to the " - "initial device orientation.\n" - "Default is 0.", - }, { // deprecated .longopt_id = OPT_LOCK_VIDEO_ORIENTATION, From 54e1f8e060ca77f9b5dac1a2f57ea7cdf0de83c3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 16:39:56 +0100 Subject: [PATCH 144/278] Include scrcpy manpage in Linux and macOS releases --- release/build_linux.sh | 1 + release/build_macos.sh | 1 + 2 files changed, 2 insertions(+) diff --git a/release/build_linux.sh b/release/build_linux.sh index 2f2fb62f..445240ce 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -32,4 +32,5 @@ mkdir -p "$LINUX_BUILD_DIR/dist" cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/scrcpy_bin" cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" cp app/data/scrcpy_static_wrapper.sh "$LINUX_BUILD_DIR/dist/scrcpy" +cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/build_macos.sh b/release/build_macos.sh index a42c7e88..58010704 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -32,4 +32,5 @@ mkdir -p "$MACOS_BUILD_DIR/dist" cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/scrcpy_bin" cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" cp app/data/scrcpy_static_wrapper.sh "$MACOS_BUILD_DIR/dist/scrcpy" +cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" From 3d478d7d5b19839b10b581b14be0140793fcbeb0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 17:01:37 +0100 Subject: [PATCH 145/278] Build FFmpeg with v4l2 support for Linux So that --v4l2-sink works with Linux static builds. --- .github/workflows/release.yml | 6 ++++-- app/deps/ffmpeg.sh | 8 +++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 13f4accf..4f7d0241 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -77,7 +77,8 @@ jobs: sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ + libv4l-dev - name: Test run: release/test_client.sh @@ -93,7 +94,8 @@ jobs: sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ - libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev + libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ + libv4l-dev - name: Build linux run: release/build_linux.sh diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index cc71ab13..386de190 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -81,8 +81,14 @@ else --enable-muxer=wav ) - if [[ "$HOST" != linux ]] + if [[ "$HOST" == linux ]] then + conf+=( + --enable-libv4l2 + --enable-outdev=v4l2 + --enable-encoder=rawvideo + ) + else # libavdevice is only used for V4L2 on Linux conf+=( --disable-avdevice From 5e05f2a25bfdc35840bef81583f02b0ba127b32b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 16:01:16 +0100 Subject: [PATCH 146/278] Bump version to 3.0 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 6454c88e..b80e01b9 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "2.7" + VALUE "ProductVersion", "3.0" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index f76d5ecf..b3ad3c75 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.7', + version: '3.0', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 2781a2db..72c74a5a 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 20700 - versionName "2.7" + versionCode 30000 + versionName "3.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 7b293e02..d0572615 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=2.7 +SCRCPY_VERSION_NAME=3.0 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From 74aecc00b512969fa3b067ed3cf20e12194206d2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 16:22:40 +0100 Subject: [PATCH 147/278] Update links to 3.0 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 23 ++++++++++++++++++++--- doc/macos.md | 17 +++++++++++++++++ doc/windows.md | 22 +++++++++++++--------- install_release.sh | 4 ++-- 6 files changed, 56 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 7dada742..253b9254 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v2.7) +# scrcpy (v3.0) scrcpy diff --git a/doc/build.md b/doc/build.md index 0c70f598..43841268 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v2.7`][direct-scrcpy-server] - SHA-256: `a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba` + - [`scrcpy-server-v3.0`][direct-scrcpy-server] + SHA-256: `800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 6bfe3454..79cbb286 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -2,6 +2,23 @@ ## Install +### From the official release + +Download a static build of the [latest release]: + + - [`scrcpy-linux-v3.0.tar.gz`][direct-linux] (x86_64) + SHA-256: `06cb74e22f758228c944cea048b78e42b2925c2affe2b5aca901cfd6a649e503` + +[latest release]: https://github.com/Genymobile/scrcpy/releases/latest +[direct-linux]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-linux-v3.0.tar.gz + +and extract it. + +_Static builds of scrcpy for Linux are still experimental._ + + +### From your package manager + Packaging status Scrcpy is packaged in several distributions and package managers: @@ -13,10 +30,10 @@ Scrcpy is packaged in several distributions and package managers: - Snap: `snap install scrcpy` - … (see [repology](https://repology.org/project/scrcpy/versions)) -### Latest version -However, the packaged version is not always the latest release. To install the -latest release from `master`, follow this simplified process. +### From an install script + +To install the latest release from `master`, follow this simplified process. First, you need to install the required packages: diff --git a/doc/macos.md b/doc/macos.md index 2c7c6071..ee3c23be 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -2,6 +2,23 @@ ## Install +### From the official release + +Download a static build of the [latest release]: + + - [`scrcpy-macos-v3.0.tar.gz`][direct-macos] (arm64) + SHA-256: `5db9821918537eb3aaf0333cdd05baf85babdd851972d5f1b71f86da0530b4bf` + +[latest release]: https://github.com/Genymobile/scrcpy/releases/latest +[direct-macos]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-macos-v3.0.tar.gz + +and extract it. + +_Static builds of scrcpy for macOS are still experimental._ + + +### From a package manager + Scrcpy is available in [Homebrew]: ```bash diff --git a/doc/windows.md b/doc/windows.md index 36e59178..330b4fbd 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -2,27 +2,32 @@ ## Install +### From the official release + Download the [latest release]: - - [`scrcpy-win64-v2.7.zip`][direct-win64] (64-bit) - SHA-256: `5910bc18d5a16f42d84185ddc7e16a4cee6a6f5f33451559c1a1d6d0099bd5f5` - - [`scrcpy-win32-v2.7.zip`][direct-win32] (32-bit) - SHA-256: `ef4daf89d500f33d78b830625536ecb18481429dd94433e7634c824292059d06` + - [`scrcpy-win64-v3.0.zip`][direct-win64] (64-bit) + SHA-256: `dfbe8a8fef6535197acc506936bfd59d0aa0427e9b44fb2e5c550eae642f72be` + - [`scrcpy-win32-v3.0.zip`][direct-win32] (32-bit) + SHA-256: `7cbf8d7a6ebfdca7b3b161e29a481c11088305f3e0a89d28e8e62f70c7bd0028` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win64-v2.7.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-win32-v2.7.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win64-v3.0.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win32-v3.0.zip and extract it. -Alternatively, you could install it from packages manager, like [Chocolatey]: + +### From a package manager + +From [Chocolatey]: ```bash choco install scrcpy choco install adb # if you don't have it yet ``` -or [Scoop]: +From [Scoop]: ```bash @@ -30,7 +35,6 @@ scoop install scrcpy scoop install adb # if you don't have it yet ``` -[Winget]: https://github.com/microsoft/winget-cli [Chocolatey]: https://chocolatey.org/ [Scoop]: https://scoop.sh diff --git a/install_release.sh b/install_release.sh index 3cf3490c..46b7dd43 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.7/scrcpy-server-v2.7 -PREBUILT_SERVER_SHA256=a23c5659f36c260f105c022d27bcb3eafffa26070e7baa9eda66d01377a1adba +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 +PREBUILT_SERVER_SHA256=800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From da8ade88fd0b1c15c5ae07cd276b3cffa2e5382e Mon Sep 17 00:00:00 2001 From: Wouter Schoot Date: Sun, 24 Nov 2024 23:18:54 +0100 Subject: [PATCH 148/278] Fix link to virtual display doc in README PR #5525 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 253b9254..5075e7ed 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ documented in the following pages: - [Device](doc/device.md) - [Window](doc/window.md) - [Recording](doc/recording.md) - - [Virtual display](doc/virtual_displays.md) + - [Virtual display](doc/virtual_display.md) - [Tunnels](doc/tunnels.md) - [OTG](doc/otg.md) - [Camera](doc/camera.md) From 7fef05197674aa82b7c81541411115704fc9599f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 20:06:32 +0100 Subject: [PATCH 149/278] Add BlueSky link Scrcpy now has a BlueSky account. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5075e7ed..85023a1d 100644 --- a/README.md +++ b/README.md @@ -181,6 +181,7 @@ to your problem immediately. You can also use: - Reddit: [`r/scrcpy`](https://www.reddit.com/r/scrcpy) + - BlueSky: [`@scrcpy.bsky.social`](https://bsky.app/profile/scrcpy.bsky.social) - Twitter: [`@scrcpy_app`](https://twitter.com/scrcpy_app) From 1d2f16dbb53facdb2ea174578437a2a5afb6aede Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2024 14:06:36 +0100 Subject: [PATCH 150/278] Fix documentation about default mouse mode When video playback is turned off, the default mouse mode has changed from "uhid" to "disabled" in 2c25fd7a8082307da19645a690c31403903fbb1e. Update the documentation accordingly. Refs #5410 Refs #5542 --- doc/control.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/doc/control.md b/doc/control.md index 26805346..86c0efe6 100644 --- a/doc/control.md +++ b/doc/control.md @@ -23,14 +23,20 @@ To control the device without mirroring: scrcpy --no-video --no-audio ``` -By default, mouse mode is switched to UHID if video mirroring is disabled (a -relative mouse mode is required). +By default, the mouse is disabled when video playback is turned off. + +To control the device using a relative mouse, enable UHID mouse mode: + +```bash +scrcpy --no-video --no-audio --mouse=uhid +scrcpy --no-video --no-audio -M # short version +``` To also use a UHID keyboard, set it explicitly: ```bash -scrcpy --no-video --no-audio --keyboard=uhid -scrcpy --no-video --no-audio -K # short version +scrcpy --no-video --no-audio --mouse=uhid --keyboard=uhid +scrcpy --no-video --no-audio -MK # short version ``` To use AOA instead (over USB only): From 3d5294c1e5819535dfa94bd399e53191283105cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 24 Nov 2024 21:36:28 +0100 Subject: [PATCH 151/278] Set main display power for virtual display Change the display power of the main display when mirroring a virtual display, to make it possible to turn off the screen. Fixes #5522 Refs #5530 --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 8 +++++--- .../java/com/genymobile/scrcpy/control/Controller.java | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 352f7c6b..1c6f1701 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -207,13 +207,15 @@ public final class CleanUp { } } - if (displayId != Device.DISPLAY_ID_NONE && Device.isScreenOn(displayId)) { + // Change the power of the main display when mirroring a virtual display + int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; + if (Device.isScreenOn(targetDisplayId)) { if (powerOffScreen) { Ln.i("Power off screen"); - Device.powerOffScreen(displayId); + Device.powerOffScreen(targetDisplayId); } else if (restoreDisplayPower) { Ln.i("Restoring display power"); - Device.setDisplayPower(displayId, true); + Device.setDisplayPower(targetDisplayId, true); } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index f0e4c037..34c613e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -281,7 +281,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { setClipboard(msg.getText(), msg.getPaste(), msg.getSequence()); break; case ControlMessage.TYPE_SET_DISPLAY_POWER: - if (supportsInputEvents && displayId != Device.DISPLAY_ID_NONE) { + if (supportsInputEvents) { setDisplayPower(msg.getOn()); } break; @@ -691,9 +691,12 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { } private void setDisplayPower(boolean on) { - boolean setDisplayPowerOk = Device.setDisplayPower(displayId, on); + // Change the power of the main display when mirroring a virtual display + int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; + boolean setDisplayPowerOk = Device.setDisplayPower(targetDisplayId, on); if (setDisplayPowerOk) { - keepDisplayPowerOff = !on; + // Do not keep display power off for virtual displays: MOD+p must wake up the physical device + keepDisplayPowerOff = displayId != Device.DISPLAY_ID_NONE && !on; Ln.i("Device display turned " + (on ? "on" : "off")); if (cleanUp != null) { boolean mustRestoreOnExit = !on; From 3d1f036c04412e17a694e6a0b857b7f9e9217ab3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2024 15:47:27 +0100 Subject: [PATCH 152/278] Rollback to old --turn-screen-off for Android 15 When the screen is turned off with the new display power method introduced in Android 15, video mirroring freezes. Use the Android 14 method for Android 15. Refs 58ba00fa060c9a1f439120f8869ed106e1c935f9 Refs #5418 Fixes #5530 --- .../src/main/java/com/genymobile/scrcpy/device/Device.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Device.java b/server/src/main/java/com/genymobile/scrcpy/device/Device.java index cd713499..3553dc27 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Device.java @@ -40,6 +40,10 @@ public final class Device { public static final int INJECT_MODE_WAIT_FOR_RESULT = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT; public static final int INJECT_MODE_WAIT_FOR_FINISH = InputManager.INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH; + // The new display power method introduced in Android 15 does not work as expected: + // + private static final boolean USE_ANDROID_15_DISPLAY_POWER = false; + private Device() { // not instantiable } @@ -127,7 +131,7 @@ public final class Device { public static boolean setDisplayPower(int displayId, boolean on) { assert displayId != Device.DISPLAY_ID_NONE; - if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { + if (USE_ANDROID_15_DISPLAY_POWER && Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { return ServiceManager.getDisplayManager().requestDisplayPower(displayId, on); } From 3e689020baa1b3ea1b66cba3260a7a33be458a06 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2024 07:45:35 +0100 Subject: [PATCH 153/278] Fix null return value in DisplayManager.toString() Ensure DisplayListener.toString() returns a non-null value to prevent a NullPointerException on certain devices. Fixes #5537 --- .../java/com/genymobile/scrcpy/wrappers/DisplayManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index b497e97f..d44ac608 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -192,6 +192,9 @@ public final class DisplayManager { if ("onDisplayChanged".equals(method.getName())) { listener.onDisplayChanged((int) args[0]); } + if ("toString".equals(method.getName())) { + return "DisplayListener"; + } return null; }); try { From 678025b31672c230575fe2dbc4a0d487d5010bb1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2024 00:13:02 +0100 Subject: [PATCH 154/278] Remove apt update on GitHub Actions Assume the image is up-to-date. --- .github/workflows/release.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4f7d0241..390b99a0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,6 @@ jobs: - name: Install dependencies run: | - sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -91,7 +90,6 @@ jobs: - name: Install dependencies run: | - sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -122,7 +120,6 @@ jobs: - name: Install dependencies run: | - sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -156,7 +153,6 @@ jobs: - name: Install dependencies run: | - sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ From a18ed1ee7ab10143239e8bf979cfa9bf938a4ea3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 10:21:19 +0100 Subject: [PATCH 155/278] Simplify GitHub actions step descriptions Each step is executed within the context of an action, so mentioning the name of the action is unnecessary. --- .github/workflows/release.yml | 38 +++++++++++++++++------------------ 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 390b99a0..8816fbbc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -42,10 +42,10 @@ jobs: distribution: 'zulu' java-version: '17' - - name: Build scrcpy-server + - name: Build run: release/build_server.sh - - name: Upload scrcpy-server artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: scrcpy-server @@ -63,7 +63,7 @@ jobs: distribution: 'zulu' java-version: '17' - - name: Build scrcpy-server without gradle + - name: Build without gradle run: server/build_without_gradle.sh test-client: @@ -95,7 +95,7 @@ jobs: libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ libv4l-dev - - name: Build linux + - name: Build run: release/build_linux.sh # upload-artifact does not preserve permissions @@ -106,7 +106,7 @@ jobs: cd dist-tar tar -C .. -cvf dist.tar.gz dist/ - - name: Upload build-linux artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build-linux-intermediate @@ -128,7 +128,7 @@ jobs: - name: Workaround for old meson version run by Github Actions run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt - - name: Build win32 + - name: Build run: release/build_windows.sh 32 # upload-artifact does not preserve permissions @@ -139,7 +139,7 @@ jobs: cd dist-tar tar -C .. -cvf dist.tar.gz dist/ - - name: Upload build-win32 artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build-win32-intermediate @@ -161,7 +161,7 @@ jobs: - name: Workaround for old meson version run by Github Actions run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt - - name: Build win64 + - name: Build run: release/build_windows.sh 64 # upload-artifact does not preserve permissions @@ -172,7 +172,7 @@ jobs: cd dist-tar tar -C .. -cvf dist.tar.gz dist/ - - name: Upload build-win64 artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build-win64-intermediate @@ -189,7 +189,7 @@ jobs: brew install meson ninja nasm libiconv zlib automake autoconf \ libtool - - name: Build macOS + - name: Build run: release/build_macos.sh # upload-artifact does not preserve permissions @@ -200,7 +200,7 @@ jobs: cd dist-tar tar -C .. -cvf dist.tar.gz dist/ - - name: Upload build-macos artifact + - name: Upload artifact uses: actions/upload-artifact@v4 with: name: build-macos-intermediate @@ -233,10 +233,10 @@ jobs: cd release/work/build-linux tar xf dist-tar/dist.tar.gz - - name: Package linux + - name: Package run: release/package_client.sh linux tar.gz - - name: Upload linux release + - name: Upload release uses: actions/upload-artifact@v4 with: name: release-linux @@ -269,10 +269,10 @@ jobs: cd release/work/build-win32 tar xf dist-tar/dist.tar.gz - - name: Package win32 + - name: Package run: release/package_client.sh win32 zip - - name: Upload win32 release + - name: Upload release uses: actions/upload-artifact@v4 with: name: release-win32 @@ -305,10 +305,10 @@ jobs: cd release/work/build-win64 tar xf dist-tar/dist.tar.gz - - name: Package win64 + - name: Package run: release/package_client.sh win64 zip - - name: Upload win64 release + - name: Upload release uses: actions/upload-artifact@v4 with: name: release-win64 @@ -341,10 +341,10 @@ jobs: cd release/work/build-macos tar xf dist-tar/dist.tar.gz - - name: Package macos + - name: Package run: release/package_client.sh macos tar.gz - - name: Upload macos release + - name: Upload release uses: actions/upload-artifact@v4 with: name: release-macos From ee9f7126ffba412d7a7b3119c55b968d5d6e7502 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 09:54:55 +0100 Subject: [PATCH 156/278] Use FORMAT variable name in package_client.sh The format is used several times, avoid using "$2" directly. --- release/package_client.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/release/package_client.sh b/release/package_client.sh index c6d430b2..e92cffa9 100755 --- a/release/package_client.sh +++ b/release/package_client.sh @@ -14,9 +14,9 @@ fi FORMAT=$2 -if [[ "$2" != zip && "$2" != tar.gz ]] +if [[ "$FORMAT" != zip && "$FORMAT" != tar.gz ]] then - echo "Invalid format (expected zip or tar.gz): $2" >&2 + echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 exit 1 fi From acddd811bf9192fce2ca3332b6f7f654b3d3a0e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 10:00:11 +0100 Subject: [PATCH 157/278] Rename TARGET to TARGET_DIRNAME This avoids confusion with "$1", which is also documented as "". If "$1" (the target) is "linux", then TARGET_DIRNAME is "scrcpy-linux-v3.0". --- release/package_client.sh | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/release/package_client.sh b/release/package_client.sh index e92cffa9..1c0bf801 100755 --- a/release/package_client.sh +++ b/release/package_client.sh @@ -22,31 +22,31 @@ fi BUILD_DIR="$WORK_DIR/build-$1" ARCHIVE_DIR="$BUILD_DIR/release-archive" -TARGET="scrcpy-$1-$VERSION" +TARGET_DIRNAME="scrcpy-$1-$VERSION" -rm -rf "$ARCHIVE_DIR/$TARGET" -mkdir -p "$ARCHIVE_DIR/$TARGET" +rm -rf "$ARCHIVE_DIR/$TARGET_DIRNAME" +mkdir -p "$ARCHIVE_DIR/$TARGET_DIRNAME" -cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET/" -cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET/" +cp -r "$BUILD_DIR/dist/." "$ARCHIVE_DIR/$TARGET_DIRNAME/" +cp "$WORK_DIR/build-server/server/scrcpy-server" "$ARCHIVE_DIR/$TARGET_DIRNAME/" mkdir -p "$OUTPUT_DIR" cd "$ARCHIVE_DIR" -rm -f "$OUTPUT_DIR/$TARGET.$FORMAT" +rm -f "$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT" case "$FORMAT" in zip) - zip -r "$OUTPUT_DIR/$TARGET.zip" "$TARGET" + zip -r "$OUTPUT_DIR/$TARGET_DIRNAME.zip" "$TARGET_DIRNAME" ;; tar.gz) - tar cvf "$OUTPUT_DIR/$TARGET.tar.gz" "$TARGET" + tar cvf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME" ;; *) echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 exit 1 esac -rm -rf "$TARGET" +rm -rf "$TARGET_DIRNAME" cd - -echo "Generated '$OUTPUT_DIR/$TARGET.$FORMAT'" +echo "Generated '$OUTPUT_DIR/$TARGET_DIRNAME.$FORMAT'" From 618a978f5b3a37ba08ee7f9832a77fba4e19c667 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 25 Nov 2024 10:28:39 +0100 Subject: [PATCH 158/278] Specify architecture for Linux and macOS releases PR #5526 Co-authored-by: Genxster1998 --- .github/workflows/release.yml | 64 +++++++++++++++++------------------ release/build_linux.sh | 9 ++++- release/build_macos.sh | 9 ++++- release/generate_checksums.sh | 4 +-- release/release.sh | 4 +-- 5 files changed, 52 insertions(+), 38 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 8816fbbc..1ee8eb35 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -82,7 +82,7 @@ jobs: - name: Test run: release/test_client.sh - build-linux: + build-linux-x86_64: runs-on: ubuntu-latest steps: - name: Checkout code @@ -96,12 +96,12 @@ jobs: libv4l-dev - name: Build - run: release/build_linux.sh + run: release/build_linux.sh x86_64 # upload-artifact does not preserve permissions - name: Tar run: | - cd release/work/build-linux + cd release/work/build-linux-x86_64 mkdir dist-tar cd dist-tar tar -C .. -cvf dist.tar.gz dist/ @@ -109,8 +109,8 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: build-linux-intermediate - path: release/work/build-linux/dist-tar/ + name: build-linux-x86_64-intermediate + path: release/work/build-linux-x86_64/dist-tar/ build-win32: runs-on: ubuntu-latest @@ -178,7 +178,7 @@ jobs: name: build-win64-intermediate path: release/work/build-win64/dist-tar/ - build-macos: + build-macos-aarch64: runs-on: macos-latest steps: - name: Checkout code @@ -190,12 +190,12 @@ jobs: libtool - name: Build - run: release/build_macos.sh + run: release/build_macos.sh aarch64 # upload-artifact does not preserve permissions - name: Tar run: | - cd release/work/build-macos + cd release/work/build-macos-aarch64 mkdir dist-tar cd dist-tar tar -C .. -cvf dist.tar.gz dist/ @@ -203,13 +203,13 @@ jobs: - name: Upload artifact uses: actions/upload-artifact@v4 with: - name: build-macos-intermediate - path: release/work/build-macos/dist-tar/ + name: build-macos-aarch64-intermediate + path: release/work/build-macos-aarch64/dist-tar/ - package-linux: + package-linux-x86_64: needs: - build-scrcpy-server - - build-linux + - build-linux-x86_64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -221,25 +221,25 @@ jobs: name: scrcpy-server path: release/work/build-server/server/ - - name: Download build-linux + - name: Download build-linux-x86_64 uses: actions/download-artifact@v4 with: - name: build-linux-intermediate - path: release/work/build-linux/dist-tar/ + name: build-linux-x86_64-intermediate + path: release/work/build-linux-x86_64/dist-tar/ # upload-artifact does not preserve permissions - name: Detar run: | - cd release/work/build-linux + cd release/work/build-linux-x86_64 tar xf dist-tar/dist.tar.gz - name: Package - run: release/package_client.sh linux tar.gz + run: release/package_client.sh linux-x86_64 tar.gz - name: Upload release uses: actions/upload-artifact@v4 with: - name: release-linux + name: release-linux-x86_64 path: release/output/ package-win32: @@ -314,10 +314,10 @@ jobs: name: release-win64 path: release/output - package-macos: + package-macos-aarch64: needs: - build-scrcpy-server - - build-macos + - build-macos-aarch64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -329,34 +329,34 @@ jobs: name: scrcpy-server path: release/work/build-server/server/ - - name: Download build-macos + - name: Download build-macos-aarch64 uses: actions/download-artifact@v4 with: - name: build-macos-intermediate - path: release/work/build-macos/dist-tar/ + name: build-macos-aarch64-intermediate + path: release/work/build-macos-aarch64/dist-tar/ # upload-artifact does not preserve permissions - name: Detar run: | - cd release/work/build-macos + cd release/work/build-macos-aarch64 tar xf dist-tar/dist.tar.gz - name: Package - run: release/package_client.sh macos tar.gz + run: release/package_client.sh macos-aarch64 tar.gz - name: Upload release uses: actions/upload-artifact@v4 with: - name: release-macos + name: release-macos-aarch64 path: release/output/ release: needs: - build-scrcpy-server - - package-linux + - package-linux-x86_64 - package-win32 - package-win64 - - package-macos + - package-macos-aarch64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -368,10 +368,10 @@ jobs: name: scrcpy-server path: release/work/build-server/server/ - - name: Download release-linux + - name: Download release-linux-x86_64 uses: actions/download-artifact@v4 with: - name: release-linux + name: release-linux-x86_64 path: release/output/ - name: Download release-win32 @@ -386,10 +386,10 @@ jobs: name: release-win64 path: release/output/ - - name: Download release-macos + - name: Download release-macos-aarch64 uses: actions/download-artifact@v4 with: - name: release-macos + name: release-macos-aarch64 path: release/output/ - name: Package server diff --git a/release/build_linux.sh b/release/build_linux.sh index 445240ce..39308828 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -4,7 +4,14 @@ cd "$(dirname ${BASH_SOURCE[0]})" . build_common cd .. # root project dir -LINUX_BUILD_DIR="$WORK_DIR/build-linux" +if [[ $# != 1 ]] +then + echo "Syntax: $0 " >&2 + exit 1 +fi + +ARCH="$1" +LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH" app/deps/adb_linux.sh app/deps/sdl.sh linux native static diff --git a/release/build_macos.sh b/release/build_macos.sh index 58010704..4794d97d 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -4,7 +4,14 @@ cd "$(dirname ${BASH_SOURCE[0]})" . build_common cd .. # root project dir -MACOS_BUILD_DIR="$WORK_DIR/build-macos" +if [[ $# != 1 ]] +then + echo "Syntax: $0 " >&2 + exit 1 +fi + +ARCH="$1" +MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH" app/deps/adb_macos.sh app/deps/sdl.sh macos native static diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh index b0464bed..f4305703 100755 --- a/release/generate_checksums.sh +++ b/release/generate_checksums.sh @@ -5,9 +5,9 @@ cd "$(dirname ${BASH_SOURCE[0]})" cd "$OUTPUT_DIR" sha256sum "scrcpy-server-$VERSION" \ - "scrcpy-linux-$VERSION.tar.gz" \ + "scrcpy-linux-x86_64-$VERSION.tar.gz" \ "scrcpy-win32-$VERSION.zip" \ "scrcpy-win64-$VERSION.zip" \ - "scrcpy-macos-$VERSION.tar.gz" \ + "scrcpy-macos-aarch64-$VERSION.tar.gz" \ | tee SHA256SUMS.txt echo "Release checksums generated in $PWD/SHA256SUMS.txt" diff --git a/release/release.sh b/release/release.sh index 8bef11ab..ddba585b 100755 --- a/release/release.sh +++ b/release/release.sh @@ -12,12 +12,12 @@ rm -rf output ./build_server.sh ./build_windows.sh 32 ./build_windows.sh 64 -./build_linux.sh +./build_linux.sh x86_64 ./package_server.sh ./package_client.sh win32 zip ./package_client.sh win64 zip -./package_client.sh linux tar.gz +./package_client.sh linux-x86_64 tar.gz ./generate_checksums.sh From c1351b250e4824017d876143b39a0d643d7a0f65 Mon Sep 17 00:00:00 2001 From: Genxster1998 Date: Mon, 25 Nov 2024 04:18:46 +0530 Subject: [PATCH 159/278] Build macOS x86_64 release Add actions to build a release for macOS x86_64 in addition to the aarch64 version. PR #5526 Signed-off-by: Romain Vimont --- .github/workflows/release.yml | 70 +++++++++++++++++++++++++++++++++++ release/generate_checksums.sh | 1 + 2 files changed, 71 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1ee8eb35..c6187ccb 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -206,6 +206,33 @@ jobs: name: build-macos-aarch64-intermediate path: release/work/build-macos-aarch64/dist-tar/ + build-macos-x86_64: + runs-on: macos-13 + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install dependencies + run: brew install meson ninja nasm libiconv zlib automake + # autoconf and libtool are already installed on macos-13 + + - name: Build + run: release/build_macos.sh x86_64 + + # upload-artifact does not preserve permissions + - name: Tar + run: | + cd release/work/build-macos-x86_64 + mkdir dist-tar + cd dist-tar + tar -C .. -cvf dist.tar.gz dist/ + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: build-macos-x86_64-intermediate + path: release/work/build-macos-x86_64/dist-tar/ + package-linux-x86_64: needs: - build-scrcpy-server @@ -350,6 +377,42 @@ jobs: name: release-macos-aarch64 path: release/output/ + package-macos-x86_64: + needs: + - build-scrcpy-server + - build-macos-x86_64 + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download scrcpy-server + uses: actions/download-artifact@v4 + with: + name: scrcpy-server + path: release/work/build-server/server/ + + - name: Download build-macos + uses: actions/download-artifact@v4 + with: + name: build-macos-x86_64-intermediate + path: release/work/build-macos-x86_64/dist-tar/ + + # upload-artifact does not preserve permissions + - name: Detar + run: | + cd release/work/build-macos-x86_64 + tar xf dist-tar/dist.tar.gz + + - name: Package + run: release/package_client.sh macos-x86_64 tar.gz + + - name: Upload release + uses: actions/upload-artifact@v4 + with: + name: release-macos-x86_64 + path: release/output/ + release: needs: - build-scrcpy-server @@ -357,6 +420,7 @@ jobs: - package-win32 - package-win64 - package-macos-aarch64 + - package-macos-x86_64 runs-on: ubuntu-latest steps: - name: Checkout code @@ -392,6 +456,12 @@ jobs: name: release-macos-aarch64 path: release/output/ + - name: Download release-macos-x86_64 + uses: actions/download-artifact@v4 + with: + name: release-macos-x86_64 + path: release/output/ + - name: Package server run: release/package_server.sh diff --git a/release/generate_checksums.sh b/release/generate_checksums.sh index f4305703..2785c6c3 100755 --- a/release/generate_checksums.sh +++ b/release/generate_checksums.sh @@ -9,5 +9,6 @@ sha256sum "scrcpy-server-$VERSION" \ "scrcpy-win32-$VERSION.zip" \ "scrcpy-win64-$VERSION.zip" \ "scrcpy-macos-aarch64-$VERSION.tar.gz" \ + "scrcpy-macos-x86_64-$VERSION.tar.gz" \ | tee SHA256SUMS.txt echo "Release checksums generated in $PWD/SHA256SUMS.txt" From 017a3672a49b55c7943217089f506a55fea8d834 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Nov 2024 19:44:21 +0100 Subject: [PATCH 160/278] Check GitHub runner architecture Make sure that the releases are built for the expected target arch. --- .github/workflows/release.yml | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c6187ccb..a77b7ff1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -85,6 +85,15 @@ jobs: build-linux-x86_64: runs-on: ubuntu-latest steps: + - name: Check architecture + run: | + arch=$(uname -m) + if [[ "$arch" != x86_64 ]] + then + echo "Unexpected architecture: $arch" >&2 + exit 1 + fi + - name: Checkout code uses: actions/checkout@v4 @@ -181,6 +190,15 @@ jobs: build-macos-aarch64: runs-on: macos-latest steps: + - name: Check architecture + run: | + arch=$(uname -m) + if [[ "$arch" != arm64 ]] + then + echo "Unexpected architecture: $arch" >&2 + exit 1 + fi + - name: Checkout code uses: actions/checkout@v4 @@ -209,6 +227,15 @@ jobs: build-macos-x86_64: runs-on: macos-13 steps: + - name: Check architecture + run: | + arch=$(uname -m) + if [[ "$arch" != x86_64 ]] + then + echo "Unexpected architecture: $arch" >&2 + exit 1 + fi + - name: Checkout code uses: actions/checkout@v4 From ff06b6dcc1dde8fb750191e07ba476b2de0c9927 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 26 Nov 2024 19:43:37 +0100 Subject: [PATCH 161/278] Split network macro conditions On Windows, interrupting a socket with shutdown() does not wake up accept() or read() calls, the socket must be closed. Introduce a new macro constant SC_SOCKET_CLOSE_ON_INTERRUPT, distinct of _WIN32, because Windows will not be the only platform exhibiting this behavior. Refs #5536 --- app/src/util/net.c | 46 ++++++++++++++++++++-------------------------- app/src/util/net.h | 33 ++++++++++++++++++++++++--------- 2 files changed, 44 insertions(+), 35 deletions(-) diff --git a/app/src/util/net.c b/app/src/util/net.c index d43d1c7a..d68b0af6 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -9,8 +9,6 @@ #ifdef _WIN32 # include typedef int socklen_t; - typedef SOCKET sc_raw_socket; -# define SC_RAW_SOCKET_NONE INVALID_SOCKET #else # include # include @@ -23,8 +21,6 @@ typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; - typedef int sc_raw_socket; -# define SC_RAW_SOCKET_NONE -1 #endif bool @@ -47,17 +43,26 @@ net_cleanup(void) { #endif } +static inline bool +sc_raw_socket_close(sc_raw_socket raw_sock) { +#ifndef _WIN32 + return !close(raw_sock); +#else + return !closesocket(raw_sock); +#endif +} + static inline sc_socket wrap(sc_raw_socket sock) { -#ifdef _WIN32 - if (sock == INVALID_SOCKET) { +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT + if (sock == SC_RAW_SOCKET_NONE) { return SC_SOCKET_NONE; } - struct sc_socket_windows *socket = malloc(sizeof(*socket)); + struct sc_socket_wrapper *socket = malloc(sizeof(*socket)); if (!socket) { LOG_OOM(); - closesocket(sock); + sc_raw_socket_close(sock); return SC_SOCKET_NONE; } @@ -72,9 +77,9 @@ wrap(sc_raw_socket sock) { static inline sc_raw_socket unwrap(sc_socket socket) { -#ifdef _WIN32 +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT if (socket == SC_SOCKET_NONE) { - return INVALID_SOCKET; + return SC_RAW_SOCKET_NONE; } return socket->socket; @@ -83,17 +88,6 @@ unwrap(sc_socket socket) { #endif } -#ifndef HAVE_SOCK_CLOEXEC // avoid unused-function warning -static inline bool -sc_raw_socket_close(sc_raw_socket raw_sock) { -#ifndef _WIN32 - return !close(raw_sock); -#else - return !closesocket(raw_sock); -#endif -} -#endif - #ifndef HAVE_SOCK_CLOEXEC // If SOCK_CLOEXEC does not exist, the flag must be set manually once the // socket is created @@ -248,9 +242,9 @@ net_interrupt(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); -#ifdef _WIN32 +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT if (!atomic_flag_test_and_set(&socket->closed)) { - return !closesocket(raw_sock); + return sc_raw_socket_close(raw_sock); } return true; #else @@ -262,15 +256,15 @@ bool net_close(sc_socket socket) { sc_raw_socket raw_sock = unwrap(socket); -#ifdef _WIN32 +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT bool ret = true; if (!atomic_flag_test_and_set(&socket->closed)) { - ret = !closesocket(raw_sock); + ret = sc_raw_socket_close(raw_sock); } free(socket); return ret; #else - return !close(raw_sock); + return sc_raw_socket_close(raw_sock); #endif } diff --git a/app/src/util/net.h b/app/src/util/net.h index ea54b793..9f23bac9 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -7,21 +7,36 @@ #include #ifdef _WIN32 - # include + typedef SOCKET sc_raw_socket; +# define SC_RAW_SOCKET_NONE INVALID_SOCKET +#else // not _WIN32 +# include + typedef int sc_raw_socket; +# define SC_RAW_SOCKET_NONE -1 +#endif + +#ifdef _WIN32 +// On Windows, shutdown() does not interrupt accept() or read() calls, so +// net_interrupt() must call close() instead, and net_close() must behave +// accordingly. +// This causes a small race condition (once the socket is closed, its +// handle becomes invalid and may in theory be reassigned before another +// thread calls accept() or read()), but it is deemed acceptable as a +// workaround. +# define SC_SOCKET_CLOSE_ON_INTERRUPT +#endif + +#ifdef SC_SOCKET_CLOSE_ON_INTERRUPT # include # define SC_SOCKET_NONE NULL - typedef struct sc_socket_windows { - SOCKET socket; + typedef struct sc_socket_wrapper { + sc_raw_socket socket; atomic_flag closed; } *sc_socket; - -#else // not _WIN32 - -# include +#else # define SC_SOCKET_NONE -1 - typedef int sc_socket; - + typedef sc_raw_socket sc_socket; #endif #define IPV4_LOCALHOST 0x7F000001 From d01373c03c135c0882c49c8bf95006d32fef041c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 27 Nov 2024 10:10:18 +0100 Subject: [PATCH 162/278] Enable close-on-interrupt for macOS This behavior is also necessary on macOS. Fixes #5536 --- app/src/util/net.h | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/util/net.h b/app/src/util/net.h index 9f23bac9..94789954 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -16,10 +16,10 @@ # define SC_RAW_SOCKET_NONE -1 #endif -#ifdef _WIN32 -// On Windows, shutdown() does not interrupt accept() or read() calls, so -// net_interrupt() must call close() instead, and net_close() must behave -// accordingly. +#if defined(_WIN32) || defined(__APPLE__) +// On Windows and macOS, shutdown() does not interrupt accept() or read() +// calls, so net_interrupt() must call close() instead, and net_close() must +// behave accordingly. // This causes a small race condition (once the socket is closed, its // handle becomes invalid and may in theory be reassigned before another // thread calls accept() or read()), but it is deemed acceptable as a From b2cdaa4bdce0adc256b87c3271c39a1482817dc6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Nov 2024 21:27:00 +0100 Subject: [PATCH 163/278] Factorize position mapper resolution The code was duplicated for touch and scroll events. Extract it to a private function. Refs #5542 --- .../genymobile/scrcpy/control/Controller.java | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 34c613e6..e6901f4b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -21,6 +21,7 @@ import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; import android.os.Build; import android.os.SystemClock; +import android.util.Pair; import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; @@ -350,24 +351,36 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { return successCount; } - private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { - long now = SystemClock.uptimeMillis(); - + private Pair getEventPointAndDisplayId(Position position) { // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") DisplayData displayData = this.displayData.get(); - assert displayData != null : "Cannot receive a touch event without a display"; + assert displayData != null : "Cannot receive a positional event without a display"; Point point = displayData.positionMapper.map(position); if (point == null) { if (Ln.isEnabled(Ln.Level.VERBOSE)) { Size eventSize = position.getScreenSize(); Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.v("Ignore touch event generated for size " + eventSize + " (current size is " + currentSize + ")"); + Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")"); } + return null; + } + + return Pair.create(point, displayData.virtualDisplayId); + } + + private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { + long now = SystemClock.uptimeMillis(); + + Pair pair = getEventPointAndDisplayId(position); + if (pair == null) { return false; } + Point point = pair.first; + int targetDisplayId = pair.second; + int pointerIndex = pointersState.getPointerIndex(pointerId); if (pointerIndex == -1) { Ln.w("Too many pointers for touch event"); @@ -421,7 +434,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // First button pressed: ACTION_DOWN MotionEvent downEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_DOWN, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!Device.injectEvent(downEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(downEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -432,7 +445,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(pressEvent, actionButton)) { return false; } - if (!Device.injectEvent(pressEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(pressEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } @@ -446,7 +459,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (!InputManager.setActionButton(releaseEvent, actionButton)) { return false; } - if (!Device.injectEvent(releaseEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(releaseEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } @@ -454,7 +467,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // Last button released: ACTION_UP MotionEvent upEvent = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_UP, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - if (!Device.injectEvent(upEvent, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC)) { + if (!Device.injectEvent(upEvent, targetDisplayId, Device.INJECT_MODE_ASYNC)) { return false; } } @@ -465,27 +478,20 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, action, pointerCount, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, source, 0); - return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); + return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC); } private boolean injectScroll(Position position, float hScroll, float vScroll, int buttons) { long now = SystemClock.uptimeMillis(); - // it hides the field on purpose, to read it with atomic access - @SuppressWarnings("checkstyle:HiddenField") - DisplayData displayData = this.displayData.get(); - assert displayData != null : "Cannot receive a scroll event without a display"; - - Point point = displayData.positionMapper.map(position); - if (point == null) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.v("Ignore scroll event generated for size " + eventSize + " (current size is " + currentSize + ")"); - } + Pair pair = getEventPointAndDisplayId(position); + if (pair == null) { return false; } + Point point = pair.first; + int targetDisplayId = pair.second; + MotionEvent.PointerProperties props = pointerProperties[0]; props.id = 0; @@ -497,7 +503,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { MotionEvent event = MotionEvent.obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, InputDevice.SOURCE_MOUSE, 0); - return Device.injectEvent(event, displayData.virtualDisplayId, Device.INJECT_MODE_ASYNC); + return Device.injectEvent(event, targetDisplayId, Device.INJECT_MODE_ASYNC); } /** From 3b2b3625e478855392c98367818a360dfba239bf Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 28 Nov 2024 21:10:06 +0100 Subject: [PATCH 164/278] Accept positional control events without display The position of touch and scroll must normally be "resolved" with a "position mapper" associated to the display. But to support the injection of such events with scrcpy-server alone without video, handle the case where there is no display. Fixes #5542 --- .../genymobile/scrcpy/control/Controller.java | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index e6901f4b..a0bdc584 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -355,19 +355,30 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // it hides the field on purpose, to read it with atomic access @SuppressWarnings("checkstyle:HiddenField") DisplayData displayData = this.displayData.get(); - assert displayData != null : "Cannot receive a positional event without a display"; + // In scrcpy, displayData should never be null (a touch event can only be generated from the client when a video frame is present). + // However, it is possible to send events without video playback when using scrcpy-server alone (except for virtual displays). + assert displayData != null || displayId != Device.DISPLAY_ID_NONE : "Cannot receive a positional event without a display"; - Point point = displayData.positionMapper.map(position); - if (point == null) { - if (Ln.isEnabled(Ln.Level.VERBOSE)) { - Size eventSize = position.getScreenSize(); - Size currentSize = displayData.positionMapper.getVideoSize(); - Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")"); + Point point; + int targetDisplayId; + if (displayData != null) { + point = displayData.positionMapper.map(position); + if (point == null) { + if (Ln.isEnabled(Ln.Level.VERBOSE)) { + Size eventSize = position.getScreenSize(); + Size currentSize = displayData.positionMapper.getVideoSize(); + Ln.v("Ignore positional event generated for size " + eventSize + " (current size is " + currentSize + ")"); + } + return null; } - return null; + targetDisplayId = displayData.virtualDisplayId; + } else { + // No display, use the raw coordinates + point = position.getPoint(); + targetDisplayId = displayId; } - return Pair.create(point, displayData.virtualDisplayId); + return Pair.create(point, targetDisplayId); } private boolean injectTouch(int action, long pointerId, Position position, float pressure, int actionButton, int buttons) { From 36574d2ee7b5084a64c54951e7c97f5a46ed0388 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 2 Dec 2024 08:54:22 +0100 Subject: [PATCH 165/278] Fix .tar.gz compression The generated .tar.gz releases were in fact non-gzipped tarballs. Fixes #5581 --- release/package_client.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/release/package_client.sh b/release/package_client.sh index 1c0bf801..51997e75 100755 --- a/release/package_client.sh +++ b/release/package_client.sh @@ -40,7 +40,7 @@ case "$FORMAT" in zip -r "$OUTPUT_DIR/$TARGET_DIRNAME.zip" "$TARGET_DIRNAME" ;; tar.gz) - tar cvf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME" + tar cvzf "$OUTPUT_DIR/$TARGET_DIRNAME.tar.gz" "$TARGET_DIRNAME" ;; *) echo "Invalid format (expected zip or tar.gz): $FORMAT" >&2 From 0fd7534bd5a5bb3e3556b964f58a4082d941fa81 Mon Sep 17 00:00:00 2001 From: Genxster1998 Date: Mon, 25 Nov 2024 22:35:03 +0530 Subject: [PATCH 166/278] Add method to get executable path on MacOS PR #5560 Signed-off-by: Romain Vimont --- app/src/sys/unix/file.c | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 9c3f7333..6123c788 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -6,6 +6,9 @@ #include #include #include +#ifdef __APPLE__ +# include // for _NSGetExecutablePath() +#endif #include "util/log.h" @@ -60,11 +63,22 @@ sc_file_get_executable_path(void) { } buf[len] = '\0'; return strdup(buf); +#elif defined(__APPLE__) + char buf[PATH_MAX]; + uint32_t bufsize = PATH_MAX; + if (_NSGetExecutablePath(buf, &bufsize) != 0) { + LOGE("Executable path buffer too small; need %u bytes", bufsize); + return NULL; + } + return realpath(buf, NULL); #else - // in practice, we only need this feature for portable builds, only used on - // Windows, so we don't care implementing it for every platform - // (it's useful to have a working version on Linux for debugging though) - return NULL; + // "_" is often used to store the full path of the command being executed + char *path = getenv("_"); + if (!path) { + LOGE("Could not determine executable path"); + return NULL; + } + return strdup(path); #endif } From 131372d2c4b430b01b137e2a0debb21a33963bba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 30 Nov 2024 16:10:48 +0100 Subject: [PATCH 167/278] Expose sc_get_env() to read environment variable Contrary to getenv(), sc_get_env() returns an allocated string that is guaranteed to be encoded in UTF-8 on all platforms (it uses _wgetenv() internally on Windows and converts the strings). PR #5560 --- app/meson.build | 1 + app/src/icon.c | 22 +++++----------------- app/src/server.c | 22 +++++----------------- app/src/util/env.c | 29 +++++++++++++++++++++++++++++ app/src/util/env.h | 12 ++++++++++++ 5 files changed, 52 insertions(+), 34 deletions(-) create mode 100644 app/src/util/env.c create mode 100644 app/src/util/env.h diff --git a/app/meson.build b/app/meson.build index f089ffb1..be02ebc1 100644 --- a/app/meson.build +++ b/app/meson.build @@ -46,6 +46,7 @@ src = [ 'src/util/acksync.c', 'src/util/audiobuf.c', 'src/util/average.c', + 'src/util/env.c', 'src/util/file.c', 'src/util/intmap.c', 'src/util/intr.c', diff --git a/app/src/icon.c b/app/src/icon.c index a76a85c9..4f3a9a39 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -9,6 +9,7 @@ #include "config.h" #include "compat.h" +#include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/str.h" @@ -19,35 +20,22 @@ static char * get_icon_path(void) { -#ifdef __WINDOWS__ - const wchar_t *icon_path_env = _wgetenv(L"SCRCPY_ICON_PATH"); -#else - const char *icon_path_env = getenv("SCRCPY_ICON_PATH"); -#endif - if (icon_path_env) { + char *icon_path = sc_get_env("SCRCPY_ICON_PATH"); + if (icon_path) { // if the envvar is set, use it -#ifdef __WINDOWS__ - char *icon_path = sc_str_from_wchars(icon_path_env); -#else - char *icon_path = strdup(icon_path_env); -#endif - if (!icon_path) { - LOG_OOM(); - return NULL; - } LOGD("Using SCRCPY_ICON_PATH: %s", icon_path); return icon_path; } #ifndef PORTABLE LOGD("Using icon: " SCRCPY_DEFAULT_ICON_PATH); - char *icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); + icon_path = strdup(SCRCPY_DEFAULT_ICON_PATH); if (!icon_path) { LOG_OOM(); return NULL; } #else - char *icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); + icon_path = sc_file_get_local_path(SCRCPY_PORTABLE_ICON_FILENAME); if (!icon_path) { LOGE("Could not get icon path"); return NULL; diff --git a/app/src/server.c b/app/src/server.c index 584a3c34..fe55baa2 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -9,6 +9,7 @@ #include "adb/adb.h" #include "util/binary.h" +#include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" @@ -25,35 +26,22 @@ static char * get_server_path(void) { -#ifdef __WINDOWS__ - const wchar_t *server_path_env = _wgetenv(L"SCRCPY_SERVER_PATH"); -#else - const char *server_path_env = getenv("SCRCPY_SERVER_PATH"); -#endif - if (server_path_env) { + char *server_path = sc_get_env("SCRCPY_SERVER_PATH"); + if (server_path) { // if the envvar is set, use it -#ifdef __WINDOWS__ - char *server_path = sc_str_from_wchars(server_path_env); -#else - char *server_path = strdup(server_path_env); -#endif - if (!server_path) { - LOG_OOM(); - return NULL; - } LOGD("Using SCRCPY_SERVER_PATH: %s", server_path); return server_path; } #ifndef PORTABLE LOGD("Using server: " SC_SERVER_PATH_DEFAULT); - char *server_path = strdup(SC_SERVER_PATH_DEFAULT); + server_path = strdup(SC_SERVER_PATH_DEFAULT); if (!server_path) { LOG_OOM(); return NULL; } #else - char *server_path = sc_file_get_local_path(SC_SERVER_FILENAME); + server_path = sc_file_get_local_path(SC_SERVER_FILENAME); if (!server_path) { LOGE("Could not get local file path, " "using " SC_SERVER_FILENAME " from current directory"); diff --git a/app/src/util/env.c b/app/src/util/env.c new file mode 100644 index 00000000..1128e5ea --- /dev/null +++ b/app/src/util/env.c @@ -0,0 +1,29 @@ +#include "env.h" + +#include +#include +#include "util/str.h" + +char * +sc_get_env(const char *varname) { +#ifdef _WIN32 + wchar_t *w_varname = sc_str_to_wchars(varname); + if (!w_varname) { + return NULL; + } + const wchar_t *value = _wgetenv(w_varname); + free(w_varname); + if (!value) { + return NULL; + } + + return sc_str_from_wchars(value); +#else + const char *value = getenv(varname); + if (!value) { + return NULL; + } + + return strdup(value); +#endif +} diff --git a/app/src/util/env.h b/app/src/util/env.h new file mode 100644 index 00000000..50a31165 --- /dev/null +++ b/app/src/util/env.h @@ -0,0 +1,12 @@ +#ifndef SC_ENV_H +#define SC_ENV_H + +#include "common.h" + +// Return the value of the environment variable (may be NULL). +// +// The returned value must be freed by the caller. +char * +sc_get_env(const char *varname); + +#endif From beee42fb065a4852082e3f949cb4ee20c184f104 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 1 Dec 2024 15:47:40 +0100 Subject: [PATCH 168/278] Load ADB value using sc_get_env() Contrary to getenv(), the result of sc_get_env() is encoded in UTF-8 on all platforms. Since it is allocated, it requires an explicit init() and destroy() functions. PR #5560 --- app/src/adb/adb.c | 29 +++++++++++++++++++++++------ app/src/adb/adb.h | 6 ++++++ app/src/server.c | 12 +++++++++++- app/src/usb/scrcpy_otg.c | 11 ++++++++--- 4 files changed, 48 insertions(+), 10 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index b3e90b2f..4bb209be 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -7,6 +7,7 @@ #include "adb_device.h" #include "adb_parser.h" +#include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/process_intr.h" @@ -24,15 +25,31 @@ */ #define SC_ADB_COMMAND(...) { sc_adb_get_executable(), __VA_ARGS__, NULL } -static const char *adb_executable; +static char *adb_executable; + +bool +sc_adb_init(void) { + adb_executable = sc_get_env("ADB"); + if (adb_executable) { + return true; + } + + adb_executable = strdup("adb"); + if (!adb_executable) { + LOG_OOM(); + return false; + } + + return true; +} + +void +sc_adb_destroy(void) { + free(adb_executable); +} const char * sc_adb_get_executable(void) { - if (!adb_executable) { - adb_executable = getenv("ADB"); - if (!adb_executable) - adb_executable = "adb"; - } return adb_executable; } diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 0292dea1..43310fb9 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -15,6 +15,12 @@ #define SC_ADB_SILENT (SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR) +bool +sc_adb_init(void); + +void +sc_adb_destroy(void); + const char * sc_adb_get_executable(void); diff --git a/app/src/server.c b/app/src/server.c index fe55baa2..923b5671 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -485,14 +485,21 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, // end of the program server->params = *params; - bool ok = sc_mutex_init(&server->mutex); + bool ok = sc_adb_init(); if (!ok) { return false; } + ok = sc_mutex_init(&server->mutex); + if (!ok) { + sc_adb_destroy(); + return false; + } + ok = sc_cond_init(&server->cond_stopped); if (!ok) { sc_mutex_destroy(&server->mutex); + sc_adb_destroy(); return false; } @@ -500,6 +507,7 @@ sc_server_init(struct sc_server *server, const struct sc_server_params *params, if (!ok) { sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); + sc_adb_destroy(); return false; } @@ -1141,4 +1149,6 @@ sc_server_destroy(struct sc_server *server) { sc_intr_destroy(&server->intr); sc_cond_destroy(&server->cond_stopped); sc_mutex_destroy(&server->mutex); + + sc_adb_destroy(); } diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 1a7e9544..6ef2fc2a 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -95,9 +95,14 @@ scrcpy_otg(struct scrcpy_options *options) { // On Windows, only one process could open a USB device // LOGI("Killing adb server (if any)..."); - unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; - // uninterruptible (intr == NULL), but in practice it's very quick - sc_adb_kill_server(NULL, flags); + if (sc_adb_init()) { + unsigned flags = SC_ADB_NO_STDOUT | SC_ADB_NO_STDERR | SC_ADB_NO_LOGERR; + // uninterruptible (intr == NULL), but in practice it's very quick + sc_adb_kill_server(NULL, flags); + sc_adb_destroy(); + } else { + LOGW("Could not call adb executable, adb server not killed"); + } #endif static const struct sc_usb_callbacks cbs = { From 6d0ac3626dbc8a5ffa6693a00f370f4f93660cd8 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 1 Dec 2024 15:48:50 +0100 Subject: [PATCH 169/278] Use local adb in portable builds For non-Windows portable builds, use the absolute path to the adb executable located in the same directory as scrcpy. On Windows, just use "adb", which is sufficient to use the local one. PR #5560 --- app/src/adb/adb.c | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 4bb209be..ce7cdec1 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -34,11 +34,22 @@ sc_adb_init(void) { return true; } +#if !defined(PORTABLE) || defined(_WIN32) adb_executable = strdup("adb"); if (!adb_executable) { LOG_OOM(); return false; } +#else + // For portable builds, use the absolute path to the adb executable + // in the same directory as scrcpy (except on Windows, where "adb" + // is sufficient) + adb_executable = sc_file_get_local_path("adb"); + if (!adb_executable) { + // Error already logged + return false; + } +#endif return true; } From dc6c279b1e500e4c39fd0adcad425e6eab2f6944 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 1 Dec 2024 16:08:03 +0100 Subject: [PATCH 170/278] Log adb executable path Log the ADB executable path (at the DEBUG level) if it is not the default one. PR #5560 --- app/src/adb/adb.c | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index ce7cdec1..ed3b1ea4 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -31,6 +31,7 @@ bool sc_adb_init(void) { adb_executable = sc_get_env("ADB"); if (adb_executable) { + LOGD("Using adb: %s", adb_executable); return true; } @@ -49,6 +50,8 @@ sc_adb_init(void) { // Error already logged return false; } + + LOGD("Using adb (portable): %s", adb_executable); #endif return true; From aea6a371aa3f5278c8b10cf6bec7bbe215ae1518 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 1 Dec 2024 16:00:20 +0100 Subject: [PATCH 171/278] Remove scrcpy wrapper script for static builds All portable builds now use the files located in the same directory as the scrcpy executable by default. PR #5560 --- app/data/scrcpy_static_wrapper.sh | 6 ------ release/build_linux.sh | 3 +-- release/build_macos.sh | 3 +-- 3 files changed, 2 insertions(+), 10 deletions(-) delete mode 100755 app/data/scrcpy_static_wrapper.sh diff --git a/app/data/scrcpy_static_wrapper.sh b/app/data/scrcpy_static_wrapper.sh deleted file mode 100755 index ac1e7a95..00000000 --- a/app/data/scrcpy_static_wrapper.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -cd "$(dirname ${BASH_SOURCE[0]})" -export ADB="${ADB:-./adb}" -export SCRCPY_SERVER_PATH="${SCRCPY_SERVER_PATH:-./scrcpy-server}" -export SCRCPY_ICON_PATH="${SCRCPY_ICON_PATH:-./icon.png}" -./scrcpy_bin "$@" diff --git a/release/build_linux.sh b/release/build_linux.sh index 39308828..ccf24575 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -36,8 +36,7 @@ ninja -C "$LINUX_BUILD_DIR" # Group intermediate outputs into a 'dist' directory mkdir -p "$LINUX_BUILD_DIR/dist" -cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/scrcpy_bin" +cp "$LINUX_BUILD_DIR"/app/scrcpy "$LINUX_BUILD_DIR/dist/" cp app/data/icon.png "$LINUX_BUILD_DIR/dist/" -cp app/data/scrcpy_static_wrapper.sh "$LINUX_BUILD_DIR/dist/scrcpy" cp app/scrcpy.1 "$LINUX_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$LINUX_BUILD_DIR/dist/" diff --git a/release/build_macos.sh b/release/build_macos.sh index 4794d97d..2c41d04e 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -36,8 +36,7 @@ ninja -C "$MACOS_BUILD_DIR" # Group intermediate outputs into a 'dist' directory mkdir -p "$MACOS_BUILD_DIR/dist" -cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/scrcpy_bin" +cp "$MACOS_BUILD_DIR"/app/scrcpy "$MACOS_BUILD_DIR/dist/" cp app/data/icon.png "$MACOS_BUILD_DIR/dist/" -cp app/data/scrcpy_static_wrapper.sh "$MACOS_BUILD_DIR/dist/scrcpy" cp app/scrcpy.1 "$MACOS_BUILD_DIR/dist/" cp -r "$ADB_INSTALL_DIR"/. "$MACOS_BUILD_DIR/dist/" From 9555d3a537a828731ad89ef5258ba537acf8cc11 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Dec 2024 23:06:33 +0100 Subject: [PATCH 172/278] Retry capture on IOException If the capture fails with an IOException, retry with a lower resolution. Fixes #5539 --- .../main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index bc120107..1402eceb 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -112,8 +112,8 @@ public class SurfaceEncoder implements AsyncProcessor { // The capture might have been closed internally (for example if the camera is disconnected) alive = !stopped.get() && !capture.isClosed(); } - } catch (IllegalStateException | IllegalArgumentException e) { - Ln.e("Encoding error: " + e.getClass().getName() + ": " + e.getMessage()); + } catch (IllegalStateException | IllegalArgumentException | IOException e) { + Ln.e("Capture/encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!prepareRetry(size)) { throw e; } From b26b4fb7458b9e6c361b60198d38b28fbf5e329d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 12:59:48 +0100 Subject: [PATCH 173/278] Document launchers in virtual displays Mention how to start a launcher in a virtual display. Refs #5592 --- doc/virtual_display.md | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 7523c118..5036f35b 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -15,8 +15,10 @@ scrcpy --new-display=/240 # use the main display size and 240 dpi On some devices, a launcher is available in the virtual display. -When no launcher is available, the virtual display is empty. In that case, you -must [start an Android app](device.md#start-android-app). +When no launcher is available (or if is explicitly disabled by +[`--no-vd-system-decorations`](#system-decorations)), the virtual display is +empty. In that case, you must [start an Android +app](device.md#start-android-app). For example: @@ -24,12 +26,27 @@ For example: scrcpy --new-display=1920x1080 --start-app=org.videolan.vlc ``` +The app may itself be a launcher. For example, to run the open source [Fossify +Launcher]: + +```bash +scrcpy --new-display=1920x1080 --no-vd-system-decorations --start-app=org.fossify.home +``` + +[Fossify Launcher]: https://f-droid.org/en/packages/org.fossify.home/ + + ## System decorations -By default, virtual display system decorations are enabled. But some devices -might display a broken UI; +By default, virtual display system decorations are enabled. To disable them, use +`--no-vd-system-decorations`: -Use `--no-vd-system-decorations` to disable it. +``` +scrcpy --new-display --no-vd-system-decorations +``` + +This is useful for some devices which might display a broken UI, or to disable +any default launcher UI available in virtual displays. Note that if no app is started, no content will be rendered, so no video frame will be produced at all. From 0e473eb0051e7210e10fb9acef92dad3b956ca1f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 13:11:43 +0100 Subject: [PATCH 174/278] Reset TCP/IP connection with a '+' prefix When running scrcpy with --tcpip=xx.xx.xx.xx, to make sure a new working connection is established, it was first disconnected by a call to: adb disconnect However, this caused all running instances connected to that address to be killed. Running several instances of scrcpy on the same device is now useful with virtual displays, so change the default behavior to NOT disconnect. To force a reconnection, a '+' prefix can be added: scrcpy --tcpip=+192.168.0.x Fixes #5562 --- app/scrcpy.1 | 6 ++++-- app/src/adb/adb.c | 5 +++-- app/src/cli.c | 7 ++++--- app/src/server.c | 23 ++++++++++++++++------- doc/connection.md | 6 ++++++ 5 files changed, 33 insertions(+), 14 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index c513dc9a..326cb23f 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -518,13 +518,15 @@ Enable "show touches" on start, restore the initial value on exit. It only shows physical touches (not clicks from scrcpy). .TP -.BI "\-\-tcpip\fR[=\fIip\fR[:\fIport\fR]] -Configure and reconnect the device over TCP/IP. +.BI "\-\-tcpip\fR[=[+]\fIip\fR[:\fIport\fR]] +Configure and connect the device over TCP/IP. If a destination address is provided, then scrcpy connects to this address before starting. The device must listen on the given TCP port (default is 5555). If no destination address is provided, then scrcpy attempts to find the IP address and adb port of the current device (typically connected over USB), enables TCP/IP mode if necessary, then connects to this address before starting. +Prefix the address with a '+' to force a reconnection. + .TP .BI "\-\-time\-limit " seconds Set the maximum mirroring time, in seconds. diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index ed3b1ea4..0cd3c0fd 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -412,7 +412,7 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { // "adb connect" always returns successfully (with exit code 0), even in // case of failure. As a workaround, check if its output starts with - // "connected". + // "connected" or "already connected". char buf[128]; ssize_t r = sc_pipe_read_all_intr(intr, pid, pout, buf, sizeof(buf) - 1); sc_pipe_close(pout); @@ -429,7 +429,8 @@ sc_adb_connect(struct sc_intr *intr, const char *ip_port, unsigned flags) { assert((size_t) r < sizeof(buf)); buf[r] = '\0'; - ok = !strncmp("connected", buf, sizeof("connected") - 1); + ok = !strncmp("connected", buf, sizeof("connected") - 1) + || !strncmp("already connected", buf, sizeof("already connected") - 1); if (!ok && !(flags & SC_ADB_NO_STDERR)) { // "adb connect" also prints errors to stdout. Since we capture it, // re-print the error to stderr. diff --git a/app/src/cli.c b/app/src/cli.c index ee86b34b..fa46c4e4 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -860,16 +860,17 @@ static const struct sc_option options[] = { { .longopt_id = OPT_TCPIP, .longopt = "tcpip", - .argdesc = "ip[:port]", + .argdesc = "[+]ip[:port]", .optional_arg = true, - .text = "Configure and reconnect the device over TCP/IP.\n" + .text = "Configure and connect the device over TCP/IP.\n" "If a destination address is provided, then scrcpy connects to " "this address before starting. The device must listen on the " "given TCP port (default is 5555).\n" "If no destination address is provided, then scrcpy attempts " "to find the IP address of the current device (typically " "connected over USB), enables TCP/IP mode, then connects to " - "this address before starting.", + "this address before starting.\n" + "Prefix the address with a '+' to force a reconnection.", }, { .longopt_id = OPT_TIME_LIMIT, diff --git a/app/src/server.c b/app/src/server.c index 923b5671..8bdf9501 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -829,11 +829,14 @@ sc_server_switch_to_tcpip(struct sc_server *server, const char *serial) { } static bool -sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { +sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port, + bool disconnect) { struct sc_intr *intr = &server->intr; - // Error expected if not connected, do not report any error - sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); + if (disconnect) { + // Error expected if not connected, do not report any error + sc_adb_disconnect(intr, ip_port, SC_ADB_SILENT); + } LOGI("Connecting to %s...", ip_port); @@ -849,7 +852,7 @@ sc_server_connect_to_tcpip(struct sc_server *server, const char *ip_port) { static bool sc_server_configure_tcpip_known_address(struct sc_server *server, - const char *addr) { + const char *addr, bool disconnect) { // Append ":5555" if no port is present bool contains_port = strchr(addr, ':'); char *ip_port = contains_port ? strdup(addr) @@ -860,7 +863,7 @@ sc_server_configure_tcpip_known_address(struct sc_server *server, } server->serial = ip_port; - return sc_server_connect_to_tcpip(server, ip_port); + return sc_server_connect_to_tcpip(server, ip_port, disconnect); } static bool @@ -885,7 +888,7 @@ sc_server_configure_tcpip_unknown_address(struct sc_server *server, } server->serial = ip_port; - return sc_server_connect_to_tcpip(server, ip_port); + return sc_server_connect_to_tcpip(server, ip_port, false); } static void @@ -972,7 +975,13 @@ run_server(void *data) { sc_adb_device_destroy(&device); } } else { - ok = sc_server_configure_tcpip_known_address(server, params->tcpip_dst); + // If the user passed a '+' (--tcpip=+ip), then disconnect first + const char *tcpip_dst = params->tcpip_dst; + bool plus = tcpip_dst[0] == '+'; + if (plus) { + ++tcpip_dst; + } + ok = sc_server_configure_tcpip_known_address(server, tcpip_dst, plus); if (!ok) { goto error_connection_failed; } diff --git a/doc/connection.md b/doc/connection.md index 17efbbdc..2c3d37e1 100644 --- a/doc/connection.md +++ b/doc/connection.md @@ -85,6 +85,12 @@ scrcpy --tcpip=192.168.1.1 # default port is 5555 scrcpy --tcpip=192.168.1.1:5555 ``` +Prefix the address with a '+' to force a reconnection: + +```bash +scrcpy --tcpip=+192.168.1.1 +``` + ### Manual From 5c3626ed47b983625481f852073cef859e7fcaf9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 18:38:23 +0100 Subject: [PATCH 175/278] Handle broken pipe errors specifically Since 9555d3a537a828731ad89ef5258ba537acf8cc11, a capture/encoding error was sometimes logged on exit. --- server/src/main/java/com/genymobile/scrcpy/util/IO.java | 4 ++++ .../main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/IO.java b/server/src/main/java/com/genymobile/scrcpy/util/IO.java index b953f290..16ddaedd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/IO.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/IO.java @@ -72,4 +72,8 @@ public final class IO { Throwable cause = e.getCause(); return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE; } + + public static boolean isBrokenPipe(Exception e) { + return e instanceof IOException && isBrokenPipe((IOException) e); + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java index 1402eceb..236a5f48 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/SurfaceEncoder.java @@ -113,6 +113,10 @@ public class SurfaceEncoder implements AsyncProcessor { alive = !stopped.get() && !capture.isClosed(); } } catch (IllegalStateException | IllegalArgumentException | IOException e) { + if (IO.isBrokenPipe(e)) { + // Do not retry on broken pipe, which is expected on close because the socket is closed by the client + throw e; + } Ln.e("Capture/encoding error: " + e.getClass().getName() + ": " + e.getMessage()); if (!prepareRetry(size)) { throw e; From 5febb1e9fba646d8fe76f891fed001c49ed59b0d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 20:29:21 +0100 Subject: [PATCH 176/278] Update links to 3.0.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 10 +++++++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 85023a1d..ba4730af 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.0) +# scrcpy (v3.0.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 43841268..2d1b4763 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.0`][direct-scrcpy-server] - SHA-256: `800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea` + - [`scrcpy-server-v3.0.1`][direct-scrcpy-server] + SHA-256: `86c4ef31f5acb060a24d5d63d8dd262ef83384e19ae5f9ad78e6408a50743d17` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-server-v3.0.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 79cbb286..4446c8c9 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-v3.0.tar.gz`][direct-linux] (x86_64) - SHA-256: `06cb74e22f758228c944cea048b78e42b2925c2affe2b5aca901cfd6a649e503` + - [`scrcpy-linux-x86_64-v3.0.1.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `6cb7fb16efbe3afd6db19b1ee31ee9f6e104a4735dc1f41c4a478cabbeac3f77` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-linux-v3.0.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-linux-x86_64-v3.0.1.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index ee3c23be..b1a1e2fc 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,11 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-v3.0.tar.gz`][direct-macos] (arm64) - SHA-256: `5db9821918537eb3aaf0333cdd05baf85babdd851972d5f1b71f86da0530b4bf` + - [`scrcpy-macos-aarch64-v3.0.1.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `e1af70898b6881b3e714ee0e15a7664bfab5eda3ea27c101163a09a36e1df753` + + - [`scrcpy-macos-x86_64-v3.0.1.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `d6f9fad290e0142a6dfb0a405a8d1bfbe1698bbb146c1c0c33e38da53762e442` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-macos-v3.0.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-macos-aarch64-v3.0.1.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-macos-x86_64-v3.0.1.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 330b4fbd..115100e6 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.0.zip`][direct-win64] (64-bit) - SHA-256: `dfbe8a8fef6535197acc506936bfd59d0aa0427e9b44fb2e5c550eae642f72be` - - [`scrcpy-win32-v3.0.zip`][direct-win32] (32-bit) - SHA-256: `7cbf8d7a6ebfdca7b3b161e29a481c11088305f3e0a89d28e8e62f70c7bd0028` + - [`scrcpy-win64-v3.0.1.zip`][direct-win64] (64-bit) + SHA-256: `2d2485cead6bb9d80ec337a660a571fc4b3c2e15034ad73c6a2867442206a5f4` + - [`scrcpy-win32-v3.0.1.zip`][direct-win32] (32-bit) + SHA-256: `43296f8bf34dd408c65463d45ca367febe68cec3ae34b78917a8f3ecbf321829` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win64-v3.0.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-win32-v3.0.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-win64-v3.0.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-win32-v3.0.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 46b7dd43..0bb42035 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0/scrcpy-server-v3.0 -PREBUILT_SERVER_SHA256=800044c62a94d5fc16f5ab9c86d45b1050eae3eb436514d1b0d2fe2646b894ea +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-server-v3.0.1 +PREBUILT_SERVER_SHA256=86c4ef31f5acb060a24d5d63d8dd262ef83384e19ae5f9ad78e6408a50743d17 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 2ed2247e8fe90eefe6070384aa9895a70569e1b6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 22:35:25 +0100 Subject: [PATCH 177/278] Bump version to 3.0.2 The version was not bumped for 3.0.1. --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index b80e01b9..0f1caf87 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.0" + VALUE "ProductVersion", "3.0.2" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index b3ad3c75..badc1adb 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.0', + version: '3.0.2', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 72c74a5a..4b7b0254 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30000 - versionName "3.0" + versionCode 30002 + versionName "3.0.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d0572615..f2420f64 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=3.0 +SCRCPY_VERSION_NAME=3.0.2 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From baa10ed0a36ec775712be85f22d3db3f0a6e19f2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 22:48:27 +0100 Subject: [PATCH 178/278] Update links to 3.0.2 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index ba4730af..601085da 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.0.1) +# scrcpy (v3.0.2) scrcpy diff --git a/doc/build.md b/doc/build.md index 2d1b4763..20d1f0f5 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.0.1`][direct-scrcpy-server] - SHA-256: `86c4ef31f5acb060a24d5d63d8dd262ef83384e19ae5f9ad78e6408a50743d17` + - [`scrcpy-server-v3.0.2`][direct-scrcpy-server] + SHA-256: `e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-server-v3.0.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-server-v3.0.2 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 4446c8c9..db4d7977 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.0.1.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `6cb7fb16efbe3afd6db19b1ee31ee9f6e104a4735dc1f41c4a478cabbeac3f77` + - [`scrcpy-linux-x86_64-v3.0.2.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `20b69dcd379bb7d7208bf1e4858cf04162fc856697be0e6c03863d7b3c1e734a` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-linux-x86_64-v3.0.1.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-linux-x86_64-v3.0.2.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index b1a1e2fc..af92f6be 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.0.1.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `e1af70898b6881b3e714ee0e15a7664bfab5eda3ea27c101163a09a36e1df753` + - [`scrcpy-macos-aarch64-v3.0.2.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `811ba2f4e856146bdd161e24c3490d78efbec2339ca783fac791d041c0aecfb6` - - [`scrcpy-macos-x86_64-v3.0.1.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `d6f9fad290e0142a6dfb0a405a8d1bfbe1698bbb146c1c0c33e38da53762e442` + - [`scrcpy-macos-x86_64-v3.0.2.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `8effff54dca3a3e46eaaec242771a13a7f81af2e18670b3d0d8ed6b461bb4f79` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-macos-aarch64-v3.0.1.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-macos-x86_64-v3.0.1.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-aarch64-v3.0.2.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-x86_64-v3.0.2.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 115100e6..e0f0a1b3 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.0.1.zip`][direct-win64] (64-bit) - SHA-256: `2d2485cead6bb9d80ec337a660a571fc4b3c2e15034ad73c6a2867442206a5f4` - - [`scrcpy-win32-v3.0.1.zip`][direct-win32] (32-bit) - SHA-256: `43296f8bf34dd408c65463d45ca367febe68cec3ae34b78917a8f3ecbf321829` + - [`scrcpy-win64-v3.0.2.zip`][direct-win64] (64-bit) + SHA-256: `f0de59f5d46127c87cd822d39d6665e016b86db4cd048101b262f6adb6766832` + - [`scrcpy-win32-v3.0.2.zip`][direct-win32] (32-bit) + SHA-256: `8db8d4984d642012c55802de71f507f8ff9f68a8cfed456d7a1982d47e065f64` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-win64-v3.0.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-win32-v3.0.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win64-v3.0.2.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win32-v3.0.2.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 0bb42035..5a6eaa7b 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0.1/scrcpy-server-v3.0.1 -PREBUILT_SERVER_SHA256=86c4ef31f5acb060a24d5d63d8dd262ef83384e19ae5f9ad78e6408a50743d17 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-server-v3.0.2 +PREBUILT_SERVER_SHA256=e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 97fa77c76c5502105a3d128a0be2477a04f4fd1b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 23:35:43 +0100 Subject: [PATCH 179/278] Inject main display events to the original display When mirroring a secondary display, touch and scroll events must be sent to the mirroring virtual display id (with coordinates relative to the virtual display size), rather than to the original display (with coordinates relative to the original display size). This behavior, introduced by d19396718ee0c0ba7fb578f595a6553c0458da59, was also applied for the main display for consistency. However, it causes some UI elements to become unclickable. To minimize inconveniences, restore the previous behavior when mirroring the main display: send all events to the original display id (0) with coordinates relative to the original display size. Fixes #5545 Fixes #5605 Fixes #5616 Refs #4598 Refs #5137 Refs #5370 PR #5614 --- .../com/genymobile/scrcpy/video/ScreenCapture.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 47425d09..5d026a73 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -129,10 +129,18 @@ public class ScreenCapture extends SurfaceCapture { try { virtualDisplay = ServiceManager.getDisplayManager() .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface); - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) - positionMapper = PositionMapper.create(videoSize, transform, inputSize); + + if (displayId == 0) { + // Main display: send all events to the original display, relative to the device size + Size deviceSize = displayInfo.getSize(); + positionMapper = PositionMapper.create(videoSize, transform, deviceSize); + virtualDisplayId = 0; + } else { + // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) + positionMapper = PositionMapper.create(videoSize, transform, inputSize); + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + } Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { From f90dc216d10ac062ab1a06e14e574acc5569d2c3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 4 Dec 2024 23:58:22 +0100 Subject: [PATCH 180/278] Refactor virtual display properties initialization Following the changes from the previous commit, the behavior is now identical when mirroring the main display or using the SurfaceControl API. Factorize the code to perform the initialization in a single location. Refs #5605 PR #5614 --- .../scrcpy/video/ScreenCapture.java | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java index 5d026a73..5f4e1803 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/ScreenCapture.java @@ -124,23 +124,9 @@ public class ScreenCapture extends SurfaceCapture { inputSize = videoSize; } - int virtualDisplayId; - PositionMapper positionMapper; try { virtualDisplay = ServiceManager.getDisplayManager() .createVirtualDisplay("scrcpy", inputSize.getWidth(), inputSize.getHeight(), displayId, surface); - - - if (displayId == 0) { - // Main display: send all events to the original display, relative to the device size - Size deviceSize = displayInfo.getSize(); - positionMapper = PositionMapper.create(videoSize, transform, deviceSize); - virtualDisplayId = 0; - } else { - // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) - positionMapper = PositionMapper.create(videoSize, transform, inputSize); - virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); - } Ln.d("Display: using DisplayManager API"); } catch (Exception displayManagerException) { try { @@ -148,11 +134,7 @@ public class ScreenCapture extends SurfaceCapture { Size deviceSize = displayInfo.getSize(); int layerStack = displayInfo.getLayerStack(); - setDisplaySurface(display, surface, deviceSize.toRect(), inputSize.toRect(), layerStack); - virtualDisplayId = displayId; - - positionMapper = PositionMapper.create(videoSize, transform, deviceSize); Ln.d("Display: using SurfaceControl API"); } catch (Exception surfaceControlException) { Ln.e("Could not create display using DisplayManager", displayManagerException); @@ -162,6 +144,18 @@ public class ScreenCapture extends SurfaceCapture { } if (vdListener != null) { + int virtualDisplayId; + PositionMapper positionMapper; + if (virtualDisplay == null || displayId == 0) { + // Surface control or main display: send all events to the original display, relative to the device size + Size deviceSize = displayInfo.getSize(); + positionMapper = PositionMapper.create(videoSize, transform, deviceSize); + virtualDisplayId = displayId; + } else { + // The positions are relative to the virtual display, not the original display (so use inputSize, not deviceSize!) + positionMapper = PositionMapper.create(videoSize, transform, inputSize); + virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); + } vdListener.onNewVirtualDisplay(virtualDisplayId, positionMapper); } } From 988174805c1942c3a06caa6f715d896764b1fb00 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2024 20:50:12 +0100 Subject: [PATCH 181/278] Fix boolean assignment On --no-vd-system-decoration, the boolean option must be set to false. It was wrongly assigned from optarg (this worked because optarg is NULL at this point, so it was converted to false). PR #5615 --- app/src/cli.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index fa46c4e4..cd0fa1c5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2706,7 +2706,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->angle = optarg; break; case OPT_NO_VD_SYSTEM_DECORATIONS: - opts->vd_system_decorations = optarg; + opts->vd_system_decorations = false; break; default: // getopt prints the error message on stderr From 6c6607d404b0e8e886852537c382d3732f3454d7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2024 21:02:50 +0100 Subject: [PATCH 182/278] Add --no-vd-destroy-content Add an option to disable the following flag for virtual displays: DisplayManager.VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed. PR #5615 --- app/data/bash-completion/scrcpy | 1 + app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 6 ++++++ app/src/cli.c | 13 +++++++++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/scrcpy.c | 1 + app/src/server.c | 3 +++ app/src/server.h | 1 + doc/virtual_display.md | 11 +++++++++++ .../main/java/com/genymobile/scrcpy/Options.java | 8 ++++++++ .../genymobile/scrcpy/video/NewDisplayCapture.java | 8 ++++++-- 12 files changed, 53 insertions(+), 2 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 6c88927e..29130892 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -57,6 +57,7 @@ _scrcpy() { --no-mipmaps --no-mouse-hover --no-power-on + --no-vd-destroy-content --no-vd-system-decorations --no-video --no-video-playback diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index e0c5e265..0897b9cc 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -63,6 +63,7 @@ arguments=( '--no-mipmaps[Disable the generation of mipmaps]' '--no-mouse-hover[Do not forward mouse hover events]' '--no-power-on[Do not power on the device on start]' + '--no-vd-destroy-content[Disable virtual display "destroy content on removal" flag]' '--no-vd-system-decorations[Disable virtual display system decorations flag]' '--no-video[Disable video forwarding]' '--no-video-playback[Disable video playback]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 326cb23f..924905e4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -369,6 +369,12 @@ Do not forward mouse hover (mouse motion without any clicks) events. .B \-\-no\-power\-on Do not power on the device on start. +.TP +.B \-\-no\-vd\-destroy\-content +Disable virtual display "destroy content on removal" flag. + +With this option, when the virtual display is closed, the running apps are moved to the main display rather than being destroyed. + .TP .B \-\-no\-vd\-system\-decorations Disable virtual display system decorations flag. diff --git a/app/src/cli.c b/app/src/cli.c index cd0fa1c5..ed1970d4 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -110,6 +110,7 @@ enum { OPT_CAPTURE_ORIENTATION, OPT_ANGLE, OPT_NO_VD_SYSTEM_DECORATIONS, + OPT_NO_VD_DESTROY_CONTENT, }; struct sc_option { @@ -659,6 +660,15 @@ static const struct sc_option options[] = { .longopt = "no-power-on", .text = "Do not power on the device on start.", }, + { + .longopt_id = OPT_NO_VD_DESTROY_CONTENT, + .longopt = "no-vd-destroy-content", + .text = "Disable virtual display \"destroy content on removal\" " + "flag.\n" + "With this option, when the virtual display is closed, the " + "running apps are moved to the main display rather than being " + "destroyed.", + }, { .longopt_id = OPT_NO_VD_SYSTEM_DECORATIONS, .longopt = "no-vd-system-decorations", @@ -2705,6 +2715,9 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_ANGLE: opts->angle = optarg; break; + case OPT_NO_VD_DESTROY_CONTENT: + opts->vd_destroy_content = false; + break; case OPT_NO_VD_SYSTEM_DECORATIONS: opts->vd_system_decorations = false; break; diff --git a/app/src/options.c b/app/src/options.c index be3cf8d1..df8033e9 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -108,6 +108,7 @@ const struct scrcpy_options scrcpy_options_default = { .new_display = NULL, .start_app = NULL, .angle = NULL, + .vd_destroy_content = true, .vd_system_decorations = true, }; diff --git a/app/src/options.h b/app/src/options.h index eaeba2f2..152881d8 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -310,6 +310,7 @@ struct scrcpy_options { bool audio_dup; const char *new_display; // [x][/] parsed by the server const char *start_app; + bool vd_destroy_content; bool vd_system_decorations; }; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index dc9e237f..f1942e43 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -458,6 +458,7 @@ scrcpy(struct scrcpy_options *options) { .power_on = options->power_on, .kill_adb_on_close = options->kill_adb_on_close, .camera_high_speed = options->camera_high_speed, + .vd_destroy_content = options->vd_destroy_content, .vd_system_decorations = options->vd_system_decorations, .list = options->list, }; diff --git a/app/src/server.c b/app/src/server.c index 8bdf9501..22ddd372 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -377,6 +377,9 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->new_display); ADD_PARAM("new_display=%s", params->new_display); } + if (!params->vd_destroy_content) { + ADD_PARAM("vd_destroy_content=false"); + } if (!params->vd_system_decorations) { ADD_PARAM("vd_system_decorations=false"); } diff --git a/app/src/server.h b/app/src/server.h index 6d9dbd4d..3c78b9ed 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -69,6 +69,7 @@ struct sc_server_params { bool power_on; bool kill_adb_on_close; bool camera_high_speed; + bool vd_destroy_content; bool vd_system_decorations; uint8_t list; }; diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 5036f35b..5d1673e8 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -50,3 +50,14 @@ any default launcher UI available in virtual displays. Note that if no app is started, no content will be rendered, so no video frame will be produced at all. + + +## Destroy on close + +By default, when the virtual display is closed, the running apps are destroyed. + +To move them to the main display instead, use: + +``` +scrcpy --new-display --no-vd-destroy-content +``` diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 43cc790d..8a438750 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -60,6 +60,7 @@ public class Options { private boolean powerOn = true; private NewDisplay newDisplay; + private boolean vdDestroyContent = true; private boolean vdSystemDecorations = true; private Orientation.Lock captureOrientationLock = Orientation.Lock.Unlocked; @@ -233,6 +234,10 @@ public class Options { return captureOrientationLock; } + public boolean getVDDestroyContent() { + return vdDestroyContent; + } + public boolean getVDSystemDecorations() { return vdSystemDecorations; } @@ -466,6 +471,9 @@ public class Options { case "new_display": options.newDisplay = parseNewDisplay(value); break; + case "vd_destroy_content": + options.vdDestroyContent = Boolean.parseBoolean(value); + break; case "vd_system_decorations": options.vdSystemDecorations = Boolean.parseBoolean(value); break; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index d92141af..033d6b9a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -53,6 +53,7 @@ public class NewDisplayCapture extends SurfaceCapture { private final boolean captureOrientationLocked; private final Orientation captureOrientation; private final float angle; + private final boolean vdDestroyContent; private final boolean vdSystemDecorations; private VirtualDisplay virtualDisplay; @@ -73,6 +74,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.captureOrientation = options.getCaptureOrientation(); assert captureOrientation != null; this.angle = options.getAngle(); + this.vdDestroyContent = options.getVDDestroyContent(); this.vdSystemDecorations = options.getVDSystemDecorations(); } @@ -167,8 +169,10 @@ public class NewDisplayCapture extends SurfaceCapture { int flags = VIRTUAL_DISPLAY_FLAG_PUBLIC | VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY | VIRTUAL_DISPLAY_FLAG_SUPPORTS_TOUCH - | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT - | VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; + | VIRTUAL_DISPLAY_FLAG_ROTATES_WITH_CONTENT; + if (vdDestroyContent) { + flags |= VIRTUAL_DISPLAY_FLAG_DESTROY_CONTENT_ON_REMOVAL; + } if (vdSystemDecorations) { flags |= VIRTUAL_DISPLAY_FLAG_SHOULD_SHOW_SYSTEM_DECORATIONS; } From 2780e0bd7b7c1d4bdbbfc07e7a2b978b48abf3c2 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2024 19:56:53 +0100 Subject: [PATCH 183/278] Do not interrupt cleanup configuration Some options, such as --show-touches or --stay-awake, modify Android settings and must be restored upon exit. If scrcpy terminates (e.g. due to an early error) in the middle of the clean up configuration, the device may be left in an inconsistent state (some settings might be changed but not restored). This issue can be reproduced with high probability by forcing scrcpy to fail: scrcpy --show-touches --video-encoder=fail To prevent this problem, ensure that the clean up thread is not interrupted until the clean up process is started. Refs #5601 PR #5613 --- .../java/com/genymobile/scrcpy/CleanUp.java | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 1c6f1701..f372855b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -24,6 +24,7 @@ public final class CleanUp { private boolean pendingRestoreDisplayPower; private Thread thread; + private boolean interrupted; private CleanUp(Options options) { thread = new Thread(() -> runCleanUp(options), "cleanup"); @@ -34,8 +35,10 @@ public final class CleanUp { return new CleanUp(options); } - public void interrupt() { - thread.interrupt(); + public synchronized void interrupt() { + // Do not use thread.interrupt() because only the wait() call must be interrupted, not Command.exec() + interrupted = true; + notify(); } public void join() throws InterruptedException { @@ -97,15 +100,13 @@ public final class CleanUp { try { run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); - } catch (InterruptedException e) { - // ignore } catch (IOException e) { Ln.e("Clean up I/O exception", e); } } private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) - throws IOException, InterruptedException { + throws IOException { String[] cmd = { "app_process", "/", @@ -126,8 +127,15 @@ public final class CleanUp { int localPendingChanges; boolean localPendingRestoreDisplayPower; synchronized (this) { - while (pendingChanges == 0) { - wait(); + while (!interrupted && pendingChanges == 0) { + try { + wait(); + } catch (InterruptedException e) { + throw new AssertionError("Clean up thread MUST NOT be interrupted"); + } + } + if (interrupted) { + break; } localPendingChanges = pendingChanges; localPendingRestoreDisplayPower = pendingRestoreDisplayPower; From c59a3c3169973abb4ce236e06990d58ae6567481 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Dec 2024 20:08:21 +0100 Subject: [PATCH 184/278] Start cleanup process with setsid or nohup If available, start the cleanup process in a new session to reduce the likelihood of it being terminated along with the scrcpy server process on some devices. The binaries setsid and nohup are often available, but it is not guaranteed. Refs #5601 PR #5613 --- .../java/com/genymobile/scrcpy/CleanUp.java | 28 ++++++++++++------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f372855b..ac265229 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -10,6 +10,8 @@ import android.os.BatteryManager; import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; +import java.util.List; /** * Handle the cleanup of scrcpy, even if the main process is killed. @@ -107,16 +109,22 @@ public final class CleanUp { private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) throws IOException { - String[] cmd = { - "app_process", - "/", - CleanUp.class.getName(), - String.valueOf(displayId), - String.valueOf(restoreStayOn), - String.valueOf(disableShowTouches), - String.valueOf(powerOffScreen), - String.valueOf(restoreScreenOffTimeout), - }; + + List cmd = new ArrayList<>(); + if (new File("/system/bin/setsid").exists()) { + cmd.add("/system/bin/setsid"); + } else if (new File("/system/bin/nohup").exists()) { + cmd.add("/system/bin/nohup"); + } + + cmd.add("app_process"); + cmd.add("/"); + cmd.add(CleanUp.class.getName()); + cmd.add(String.valueOf(displayId)); + cmd.add(String.valueOf(restoreStayOn)); + cmd.add(String.valueOf(disableShowTouches)); + cmd.add(String.valueOf(powerOffScreen)); + cmd.add(String.valueOf(restoreScreenOffTimeout)); ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", Server.SERVER_PATH); From 4bd1c5981db307155452fa7594945e069542ddb3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2024 14:12:53 +0100 Subject: [PATCH 185/278] Split gamepad device added/removed events Use two separate callbacks for gamepad device added and gamepad device removed. It looks cleaner. PR #5623 --- app/src/input_events.h | 16 ----------- app/src/input_manager.c | 19 +++++++------ app/src/trait/gamepad_processor.h | 15 ++++++++-- app/src/uhid/gamepad_uhid.c | 43 +++++++++++++++------------- app/src/usb/gamepad_aoa.c | 47 ++++++++++++++++--------------- app/src/usb/screen_otg.c | 22 +++++++-------- 6 files changed, 80 insertions(+), 82 deletions(-) diff --git a/app/src/input_events.h b/app/src/input_events.h index c8966a35..ad3afa81 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -412,18 +412,12 @@ struct sc_touch_event { float pressure; }; -enum sc_gamepad_device_event_type { - SC_GAMEPAD_DEVICE_ADDED, - SC_GAMEPAD_DEVICE_REMOVED, -}; - // As documented in : // The ID value starts at 0 and increments from there. The value -1 is an // invalid ID. #define SC_GAMEPAD_ID_INVALID UINT32_C(-1) struct sc_gamepad_device_event { - enum sc_gamepad_device_event_type type; uint32_t gamepad_id; }; @@ -503,16 +497,6 @@ sc_mouse_buttons_state_from_sdl(uint32_t buttons_state) { return buttons_state; } -static inline enum sc_gamepad_device_event_type -sc_gamepad_device_event_type_from_sdl_type(uint32_t type) { - assert(type == SDL_CONTROLLERDEVICEADDED - || type == SDL_CONTROLLERDEVICEREMOVED); - if (type == SDL_CONTROLLERDEVICEADDED) { - return SC_GAMEPAD_DEVICE_ADDED; - } - return SC_GAMEPAD_DEVICE_REMOVED; -} - static inline enum sc_gamepad_axis sc_gamepad_axis_from_sdl(uint8_t axis) { if (axis <= SDL_CONTROLLER_AXIS_TRIGGERRIGHT) { diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 3955c211..2e4337db 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -908,7 +908,6 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, static void sc_input_manager_process_gamepad_device(struct sc_input_manager *im, const SDL_ControllerDeviceEvent *event) { - SDL_JoystickID id; if (event->type == SDL_CONTROLLERDEVICEADDED) { SDL_GameController *gc = SDL_GameControllerOpen(event->which); if (!gc) { @@ -923,9 +922,12 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im, return; } - id = SDL_JoystickInstanceID(joystick); + struct sc_gamepad_device_event evt = { + .gamepad_id = SDL_JoystickInstanceID(joystick), + }; + im->gp->ops->process_gamepad_added(im->gp, &evt); } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - id = event->which; + SDL_JoystickID id = event->which; SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); if (gc) { @@ -933,16 +935,15 @@ sc_input_manager_process_gamepad_device(struct sc_input_manager *im, } else { LOGW("Unknown gamepad device removed"); } + + struct sc_gamepad_device_event evt = { + .gamepad_id = id, + }; + im->gp->ops->process_gamepad_removed(im->gp, &evt); } else { // Nothing to do return; } - - struct sc_gamepad_device_event evt = { - .type = sc_gamepad_device_event_type_from_sdl_type(event->type), - .gamepad_id = id, - }; - im->gp->ops->process_gamepad_device(im->gp, &evt); } static void diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h index 72479783..19629a9a 100644 --- a/app/src/trait/gamepad_processor.h +++ b/app/src/trait/gamepad_processor.h @@ -20,13 +20,22 @@ struct sc_gamepad_processor { struct sc_gamepad_processor_ops { /** - * Process a gamepad device added or removed + * Process a gamepad device added event * * This function is mandatory. */ void - (*process_gamepad_device)(struct sc_gamepad_processor *gp, - const struct sc_gamepad_device_event *event); + (*process_gamepad_added)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event); + + /** + * Process a gamepad device removed event + * + * This function is mandatory. + */ + void + (*process_gamepad_removed)(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event); /** * Process a gamepad axis event diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 62b0f653..42db63e7 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -52,29 +52,31 @@ sc_gamepad_uhid_send_close(struct sc_gamepad_uhid *gamepad, } static void -sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, +sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, const struct sc_gamepad_device_event *event) { struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); - if (event->type == SC_GAMEPAD_DEVICE_ADDED) { - struct sc_hid_open hid_open; - if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, - event->gamepad_id)) { - return; - } - - sc_gamepad_uhid_send_open(gamepad, &hid_open); - } else { - assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); - - struct sc_hid_close hid_close; - if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, - event->gamepad_id)) { - return; - } - - sc_gamepad_uhid_send_close(gamepad, &hid_close); + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; } + + sc_gamepad_uhid_send_open(gamepad, &hid_open); +} + +static void +sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_uhid *gamepad = DOWNCAST(gp); + + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + sc_gamepad_uhid_send_close(gamepad, &hid_close); } static void @@ -114,7 +116,8 @@ sc_gamepad_uhid_init(struct sc_gamepad_uhid *gamepad, gamepad->controller = controller; static const struct sc_gamepad_processor_ops ops = { - .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_added = sc_gamepad_processor_process_gamepad_added, + .process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed, .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, }; diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c index 37587532..4372379f 100644 --- a/app/src/usb/gamepad_aoa.c +++ b/app/src/usb/gamepad_aoa.c @@ -7,33 +7,35 @@ #define DOWNCAST(GP) container_of(GP, struct sc_gamepad_aoa, gamepad_processor) static void -sc_gamepad_processor_process_gamepad_device(struct sc_gamepad_processor *gp, +sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, const struct sc_gamepad_device_event *event) { struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); - if (event->type == SC_GAMEPAD_DEVICE_ADDED) { - struct sc_hid_open hid_open; - if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, - event->gamepad_id)) { - return; - } + struct sc_hid_open hid_open; + if (!sc_hid_gamepad_generate_open(&gamepad->hid, &hid_open, + event->gamepad_id)) { + return; + } - // exit_on_error: false (a gamepad open failure should not exit scrcpy) - if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { - LOGW("Could not push AOA HID open (gamepad)"); - } - } else { - assert(event->type == SC_GAMEPAD_DEVICE_REMOVED); + // exit_on_error: false (a gamepad open failure should not exit scrcpy) + if (!sc_aoa_push_open(gamepad->aoa, &hid_open, false)) { + LOGW("Could not push AOA HID open (gamepad)"); + } +} - struct sc_hid_close hid_close; - if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, - event->gamepad_id)) { - return; - } +static void +sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp, + const struct sc_gamepad_device_event *event) { + struct sc_gamepad_aoa *gamepad = DOWNCAST(gp); - if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { - LOGW("Could not push AOA HID close (gamepad)"); - } + struct sc_hid_close hid_close; + if (!sc_hid_gamepad_generate_close(&gamepad->hid, &hid_close, + event->gamepad_id)) { + return; + } + + if (!sc_aoa_push_close(gamepad->aoa, &hid_close)) { + LOGW("Could not push AOA HID close (gamepad)"); } } @@ -76,7 +78,8 @@ sc_gamepad_aoa_init(struct sc_gamepad_aoa *gamepad, struct sc_aoa *aoa) { sc_hid_gamepad_init(&gamepad->hid); static const struct sc_gamepad_processor_ops ops = { - .process_gamepad_device = sc_gamepad_processor_process_gamepad_device, + .process_gamepad_added = sc_gamepad_processor_process_gamepad_added, + .process_gamepad_removed = sc_gamepad_processor_process_gamepad_removed, .process_gamepad_axis = sc_gamepad_processor_process_gamepad_axis, .process_gamepad_button = sc_gamepad_processor_process_gamepad_button, }; diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 18377074..368af125 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -175,7 +175,6 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, assert(screen->gamepad); struct sc_gamepad_processor *gp = &screen->gamepad->gamepad_processor; - SDL_JoystickID id; if (event->type == SDL_CONTROLLERDEVICEADDED) { SDL_GameController *gc = SDL_GameControllerOpen(event->which); if (!gc) { @@ -190,9 +189,12 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, return; } - id = SDL_JoystickInstanceID(joystick); + struct sc_gamepad_device_event evt = { + .gamepad_id = SDL_JoystickInstanceID(joystick), + }; + gp->ops->process_gamepad_added(gp, &evt); } else if (event->type == SDL_CONTROLLERDEVICEREMOVED) { - id = event->which; + SDL_JoystickID id = event->which; SDL_GameController *gc = SDL_GameControllerFromInstanceID(id); if (gc) { @@ -200,16 +202,12 @@ sc_screen_otg_process_gamepad_device(struct sc_screen_otg *screen, } else { LOGW("Unknown gamepad device removed"); } - } else { - // Nothing to do - return; - } - struct sc_gamepad_device_event evt = { - .type = sc_gamepad_device_event_type_from_sdl_type(event->type), - .gamepad_id = id, - }; - gp->ops->process_gamepad_device(gp, &evt); + struct sc_gamepad_device_event evt = { + .gamepad_id = id, + }; + gp->ops->process_gamepad_removed(gp, &evt); + } } static void From 9cf4d527215a3f21077b4d28466632be26f72917 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Dec 2024 12:53:07 +0100 Subject: [PATCH 186/278] Fix HID gamepad comments PR #5623 --- app/src/hid/hid_gamepad.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index e2bf0616..99facdd0 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -65,7 +65,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x75, 0x10, // Report Count (4) 0x95, 0x04, - // Input (Data, Variable, Absolute): 4 bytes (X, Y, Z, Rz) + // Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz) 0x81, 0x02, // Usage Page (Simulation Controls) @@ -82,7 +82,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x75, 0x10, // Report Count (2) 0x95, 0x02, - // Input (Data, Variable, Absolute): 2 bytes (L2, R2) + // Input (Data, Variable, Absolute): 2x2 bytes (L2, R2) 0x81, 0x02, // Usage Page (Buttons) @@ -182,7 +182,7 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { * `------------- SC_GAMEPAD_BUTTON_RIGHT_STICK * * +---------------+ - * byte 14: |0 0 0 . . . . .| hat switch (dpad) position (0-8) + * byte 14: |0 0 0 0 . . . .| hat switch (dpad) position (0-8) * +---------------+ * 9 possible positions and their values: * 8 1 2 From 1786f28e6f9f9c597f4d66de88c206489cb87122 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 6 Dec 2024 12:53:28 +0100 Subject: [PATCH 187/278] Fix gamepad HID descriptor Use Z and Rz for L2/R2, which are more widely supported than Brake/Accelerator. The right stick must then be bound to Rx and Ry. Fixes #5362 PR #5623 --- app/src/hid/hid_gamepad.c | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 99facdd0..977bcf68 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -52,10 +52,10 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { 0x09, 0x30, // Usage (Y) Left stick y 0x09, 0x31, - // Usage (Z) Right stick x - 0x09, 0x32, - // Usage (Rz) Right stick y - 0x09, 0x35, + // Usage (Rx) Right stick x + 0x09, 0x33, + // Usage (Ry) Right stick y + 0x09, 0x34, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum (65535) @@ -68,12 +68,12 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { // Input (Data, Variable, Absolute): 4x2 bytes (X, Y, Z, Rz) 0x81, 0x02, - // Usage Page (Simulation Controls) - 0x05, 0x02, - // Usage (Brake) - 0x09, 0xC5, - // Usage (Accelerator) - 0x09, 0xC4, + // Usage Page (Generic Desktop) + 0x05, 0x01, + // Usage (Z) + 0x09, 0x32, + // Usage (Rz) + 0x09, 0x35, // Logical Minimum (0) 0x15, 0x00, // Logical Maximum (32767) From 86a68fac6c631a01e8d0132eee0fc5a831e78417 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2024 14:40:04 +0100 Subject: [PATCH 188/278] Fix gamepad axis initial values By default, initialize axis to 0, which is represented by 0x8000 as a 16-bit unsigned value. PR #5623 --- app/src/hid/hid_gamepad.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 977bcf68..892d21f2 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -191,16 +191,19 @@ static const uint8_t SC_HID_GAMEPAD_REPORT_DESC[] = { * (8 is top-left, 1 is top, 2 is top-right, etc.) */ +// [-32768 to 32767] -> [0 to 65535] +#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) + static void sc_hid_gamepad_slot_init(struct sc_hid_gamepad_slot *slot, uint32_t gamepad_id) { assert(gamepad_id != SC_GAMEPAD_ID_INVALID); slot->gamepad_id = gamepad_id; slot->buttons = 0; - slot->axis_left_x = 0; - slot->axis_left_y = 0; - slot->axis_right_x = 0; - slot->axis_right_y = 0; + slot->axis_left_x = AXIS_RESCALE(0); + slot->axis_left_y = AXIS_RESCALE(0); + slot->axis_right_x = AXIS_RESCALE(0); + slot->axis_right_y = AXIS_RESCALE(0); slot->axis_left_trigger = 0; slot->axis_right_trigger = 0; } @@ -423,8 +426,6 @@ sc_hid_gamepad_generate_input_from_axis(struct sc_hid_gamepad *hid, struct sc_hid_gamepad_slot *slot = &hid->slots[slot_idx]; -// [-32768 to 32767] -> [0 to 65535] -#define AXIS_RESCALE(V) (uint16_t) (((int32_t) V) + 0x8000) switch (event->axis) { case SC_GAMEPAD_AXIS_LEFTX: slot->axis_left_x = AXIS_RESCALE(event->value); From 27a5934a1d5365332e4338f76508139dbd61d1ef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2024 12:43:49 +0100 Subject: [PATCH 189/278] Define UHID vendorId and productId from the client Let the client choose the USB ids, that it transmits in UHID_CREATE requests. PR #5623 --- app/src/control_msg.c | 14 ++++++++++---- app/src/control_msg.h | 2 ++ app/src/uhid/gamepad_uhid.c | 5 +++++ app/src/uhid/keyboard_uhid.c | 2 ++ app/src/uhid/mouse_uhid.c | 2 ++ app/tests/test_control_msg_serialize.c | 6 +++++- .../genymobile/scrcpy/control/ControlMessage.java | 14 +++++++++++++- .../scrcpy/control/ControlMessageReader.java | 4 +++- .../com/genymobile/scrcpy/control/Controller.java | 2 +- .../com/genymobile/scrcpy/control/UhidManager.java | 10 +++++----- .../scrcpy/control/ControlMessageReaderTest.java | 4 ++++ 11 files changed, 52 insertions(+), 13 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index 0defda92..e78f0c57 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -152,8 +152,10 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { return 2; case SC_CONTROL_MSG_TYPE_UHID_CREATE: sc_write16be(&buf[1], msg->uhid_create.id); + sc_write16be(&buf[3], msg->uhid_create.vendor_id); + sc_write16be(&buf[5], msg->uhid_create.product_id); - size_t index = 3; + size_t index = 7; index += write_string_tiny(&buf[index], msg->uhid_create.name, 127); sc_write16be(&buf[index], msg->uhid_create.report_desc_size); @@ -278,9 +280,13 @@ sc_control_msg_log(const struct sc_control_msg *msg) { // Quote only if name is not null const char *name = msg->uhid_create.name; const char *quote = name ? "\"" : ""; - LOG_CMSG("UHID create [%" PRIu16 "] name=%s%s%s " - "report_desc_size=%" PRIu16, msg->uhid_create.id, - quote, name, quote, msg->uhid_create.report_desc_size); + LOG_CMSG("UHID create [%" PRIu16 "] %04" PRIx16 ":%04" PRIx16 + " name=%s%s%s report_desc_size=%" PRIu16, + msg->uhid_create.id, + msg->uhid_create.vendor_id, + msg->uhid_create.product_id, + quote, name, quote, + msg->uhid_create.report_desc_size); break; } case SC_CONTROL_MSG_TYPE_UHID_INPUT: { diff --git a/app/src/control_msg.h b/app/src/control_msg.h index f0a2e373..74dbcba8 100644 --- a/app/src/control_msg.h +++ b/app/src/control_msg.h @@ -94,6 +94,8 @@ struct sc_control_msg { } set_display_power; struct { uint16_t id; + uint16_t vendor_id; + uint16_t product_id; const char *name; // pointer to static data uint16_t report_desc_size; const uint8_t *report_desc; // pointer to static data diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 42db63e7..5b574409 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -7,6 +7,9 @@ /** Downcast gamepad processor to sc_gamepad_uhid */ #define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) +#define SC_GAMEPAD_UHID_VENDOR_ID 0 +#define SC_GAMEPAD_UHID_PRODUCT_ID 0 + static void sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, const struct sc_hid_input *hid_input, @@ -30,6 +33,8 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = hid_open->hid_id; + msg.uhid_create.vendor_id = SC_GAMEPAD_UHID_VENDOR_ID; + msg.uhid_create.product_id = SC_GAMEPAD_UHID_PRODUCT_ID; msg.uhid_create.name = hid_open->name; msg.uhid_create.report_desc = hid_open->report_desc; msg.uhid_create.report_desc_size = hid_open->report_desc_size; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 496da23d..4d2c978d 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -141,6 +141,8 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_KEYBOARD; + msg.uhid_create.vendor_id = 0; + msg.uhid_create.product_id = 0; msg.uhid_create.name = hid_open.name; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 1dc02777..d6044bdc 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -81,6 +81,8 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, struct sc_control_msg msg; msg.type = SC_CONTROL_MSG_TYPE_UHID_CREATE; msg.uhid_create.id = SC_HID_ID_MOUSE; + msg.uhid_create.vendor_id = 0; + msg.uhid_create.product_id = 0; msg.uhid_create.name = hid_open.name; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index 9adf2a3d..af97182d 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -329,6 +329,8 @@ static void test_serialize_uhid_create(void) { .type = SC_CONTROL_MSG_TYPE_UHID_CREATE, .uhid_create = { .id = 42, + .vendor_id = 0x1234, + .product_id = 0x5678, .name = "ABC", .report_desc_size = sizeof(report_desc), .report_desc = report_desc, @@ -337,11 +339,13 @@ static void test_serialize_uhid_create(void) { uint8_t buf[SC_CONTROL_MSG_MAX_SIZE]; size_t size = sc_control_msg_serialize(&msg, buf); - assert(size == 20); + assert(size == 24); const uint8_t expected[] = { SC_CONTROL_MSG_TYPE_UHID_CREATE, 0, 42, // id + 0x12, 0x34, // vendor id + 0x56, 0x78, // product id 3, // name size 65, 66, 67, // "ABC" 0, 11, // report desc size diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java index 7455cdf8..0eb96adc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessage.java @@ -51,6 +51,8 @@ public final class ControlMessage { private int id; private byte[] data; private boolean on; + private int vendorId; + private int productId; private ControlMessage() { } @@ -131,10 +133,12 @@ public final class ControlMessage { return msg; } - public static ControlMessage createUhidCreate(int id, String name, byte[] reportDesc) { + public static ControlMessage createUhidCreate(int id, int vendorId, int productId, String name, byte[] reportDesc) { ControlMessage msg = new ControlMessage(); msg.type = TYPE_UHID_CREATE; msg.id = id; + msg.vendorId = vendorId; + msg.productId = productId; msg.text = name; msg.data = reportDesc; return msg; @@ -237,4 +241,12 @@ public final class ControlMessage { public boolean getOn() { return on; } + + public int getVendorId() { + return vendorId; + } + + public int getProductId() { + return productId; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index b82615ed..e503ec61 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -142,9 +142,11 @@ public class ControlMessageReader { private ControlMessage parseUhidCreate() throws IOException { int id = dis.readUnsignedShort(); + int vendorId = dis.readUnsignedShort(); + int productId = dis.readUnsignedShort(); String name = parseString(1); byte[] data = parseByteArray(2); - return ControlMessage.createUhidCreate(id, name, data); + return ControlMessage.createUhidCreate(id, vendorId, productId, name, data); } private ControlMessage parseUhidInput() throws IOException { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index a0bdc584..5e64a4c5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -290,7 +290,7 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Device.rotateDevice(getActionDisplayId()); break; case ControlMessage.TYPE_UHID_CREATE: - getUhidManager().open(msg.getId(), msg.getText(), msg.getData()); + getUhidManager().open(msg.getId(), msg.getVendorId(), msg.getProductId(), msg.getText(), msg.getData()); break; case ControlMessage.TYPE_UHID_INPUT: getUhidManager().writeInput(msg.getId(), msg.getData()); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index 8121adfc..1d7678ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -48,7 +48,7 @@ public final class UhidManager { } } - public void open(int id, String name, byte[] reportDesc) throws IOException { + public void open(int id, int vendorId, int productId, String name, byte[] reportDesc) throws IOException { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { @@ -58,7 +58,7 @@ public final class UhidManager { close(old); } - byte[] req = buildUhidCreate2Req(name, reportDesc); + byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); Os.write(fd, req, 0, req.length); registerUhidListener(id, fd); @@ -148,7 +148,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(String name, byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) { /* * struct uhid_event { * uint32_t type; @@ -183,8 +183,8 @@ public final class UhidManager { buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); - buf.putInt(0); // vendor id - buf.putInt(0); // product id + buf.putInt(vendorId); + buf.putInt(productId); buf.putInt(0); // version buf.putInt(0); // country; buf.put(reportDesc); diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index a25507b4..74df064f 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -322,6 +322,8 @@ public class ControlMessageReaderTest { DataOutputStream dos = new DataOutputStream(bos); dos.writeByte(ControlMessage.TYPE_UHID_CREATE); dos.writeShort(42); // id + dos.writeShort(0x1234); // vendorId + dos.writeShort(0x5678); // productId dos.writeByte(3); // name size dos.write("ABC".getBytes(StandardCharsets.US_ASCII)); byte[] data = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; @@ -335,6 +337,8 @@ public class ControlMessageReaderTest { ControlMessage event = reader.read(); Assert.assertEquals(ControlMessage.TYPE_UHID_CREATE, event.getType()); Assert.assertEquals(42, event.getId()); + Assert.assertEquals(0x1234, event.getVendorId()); + Assert.assertEquals(0x5678, event.getProductId()); Assert.assertEquals("ABC", event.getText()); Assert.assertArrayEquals(data, event.getData()); From 0a09518a49cb495ba76573597cf38169f6813209 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 7 Dec 2024 18:01:23 +0100 Subject: [PATCH 190/278] Use Xbox 360 gamepad USB ids Use the vendorId and productId of an Xbox 360 controller for better support (the HID gamepad protocol used in scrcpy is similar to that of the Xbox 360 controller). Fixes #5362 PR #5623 --- app/src/uhid/gamepad_uhid.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 5b574409..2a063af5 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -7,8 +7,9 @@ /** Downcast gamepad processor to sc_gamepad_uhid */ #define DOWNCAST(GP) container_of(GP, struct sc_gamepad_uhid, gamepad_processor) -#define SC_GAMEPAD_UHID_VENDOR_ID 0 -#define SC_GAMEPAD_UHID_PRODUCT_ID 0 +// Xbox 360 +#define SC_GAMEPAD_UHID_VENDOR_ID UINT16_C(0x045e) +#define SC_GAMEPAD_UHID_PRODUCT_ID UINT16_C(0x028e) static void sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, From 7418fd06626a2ae405f4bfbe9980a9c4ebd744a7 Mon Sep 17 00:00:00 2001 From: Withoutruless <57673426+Withoutruless@users.noreply.github.com> Date: Sun, 8 Dec 2024 16:49:07 +0100 Subject: [PATCH 191/278] Use Xbox 360 gamepad name Some games do not work without a known gamepad name. Fixes #5362 Refs #5623 comment PR #5623 Signed-off-by: Romain Vimont --- app/src/hid/hid_event.h | 1 - app/src/hid/hid_gamepad.c | 6 ------ app/src/hid/hid_keyboard.c | 1 - app/src/hid/hid_mouse.c | 1 - app/src/uhid/gamepad_uhid.c | 3 ++- app/src/uhid/keyboard_uhid.c | 2 +- app/src/uhid/mouse_uhid.c | 2 +- .../java/com/genymobile/scrcpy/control/UhidManager.java | 2 +- 8 files changed, 5 insertions(+), 13 deletions(-) diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index 37c3611b..d6818e30 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -15,7 +15,6 @@ struct sc_hid_input { struct sc_hid_open { uint16_t hid_id; - const char *name; // pointer to static memory const uint8_t *report_desc; // pointer to static memory size_t report_desc_size; }; diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 892d21f2..8f4e4527 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -246,14 +246,8 @@ sc_hid_gamepad_generate_open(struct sc_hid_gamepad *hid, sc_hid_gamepad_slot_init(&hid->slots[slot_idx], gamepad_id); - SDL_GameController* game_controller = - SDL_GameControllerFromInstanceID(gamepad_id); - assert(game_controller); - const char *name = SDL_GameControllerName(game_controller); - uint16_t hid_id = sc_hid_gamepad_slot_get_id(slot_idx); hid_open->hid_id = hid_id; - hid_open->name = name; hid_open->report_desc = SC_HID_GAMEPAD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_GAMEPAD_REPORT_DESC); diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 2109224a..961ad790 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -335,7 +335,6 @@ sc_hid_keyboard_generate_input_from_mods(struct sc_hid_input *hid_input, void sc_hid_keyboard_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_KEYBOARD; - hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_KEYBOARD_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_KEYBOARD_REPORT_DESC); } diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index ac215165..7acc413b 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -190,7 +190,6 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { hid_open->hid_id = SC_HID_ID_MOUSE; - hid_open->name = NULL; // No name specified after "scrcpy" hid_open->report_desc = SC_HID_MOUSE_REPORT_DESC; hid_open->report_desc_size = sizeof(SC_HID_MOUSE_REPORT_DESC); } diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 2a063af5..4da4a21e 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -10,6 +10,7 @@ // Xbox 360 #define SC_GAMEPAD_UHID_VENDOR_ID UINT16_C(0x045e) #define SC_GAMEPAD_UHID_PRODUCT_ID UINT16_C(0x028e) +#define SC_GAMEPAD_UHID_NAME "Microsoft X-Box 360 Pad" static void sc_gamepad_uhid_send_input(struct sc_gamepad_uhid *gamepad, @@ -36,7 +37,7 @@ sc_gamepad_uhid_send_open(struct sc_gamepad_uhid *gamepad, msg.uhid_create.id = hid_open->hid_id; msg.uhid_create.vendor_id = SC_GAMEPAD_UHID_VENDOR_ID; msg.uhid_create.product_id = SC_GAMEPAD_UHID_PRODUCT_ID; - msg.uhid_create.name = hid_open->name; + msg.uhid_create.name = SC_GAMEPAD_UHID_NAME; msg.uhid_create.report_desc = hid_open->report_desc; msg.uhid_create.report_desc_size = hid_open->report_desc_size; diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 4d2c978d..76d70cc5 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -143,7 +143,7 @@ sc_keyboard_uhid_init(struct sc_keyboard_uhid *kb, msg.uhid_create.id = SC_HID_ID_KEYBOARD; msg.uhid_create.vendor_id = 0; msg.uhid_create.product_id = 0; - msg.uhid_create.name = hid_open.name; + msg.uhid_create.name = NULL; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index d6044bdc..471030e7 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -83,7 +83,7 @@ sc_mouse_uhid_init(struct sc_mouse_uhid *mouse, msg.uhid_create.id = SC_HID_ID_MOUSE; msg.uhid_create.vendor_id = 0; msg.uhid_create.product_id = 0; - msg.uhid_create.name = hid_open.name; + msg.uhid_create.name = NULL; msg.uhid_create.report_desc = hid_open.report_desc; msg.uhid_create.report_desc_size = hid_open.report_desc_size; if (!sc_controller_push_msg(controller, &msg)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index 1d7678ec..c4867a3f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -174,7 +174,7 @@ public final class UhidManager { ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); - String actualName = name.isEmpty() ? "scrcpy" : "scrcpy: " + name; + String actualName = name.isEmpty() ? "scrcpy" : name; byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); assert len <= 127; From 328bb74f8002693e4be2703450305e82fc015e88 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2024 17:07:03 +0100 Subject: [PATCH 192/278] Log gamepad added/removed Add a log when a gamepad is added or removed. PR #5623 --- app/src/uhid/gamepad_uhid.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index 4da4a21e..a066cf03 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -69,6 +69,12 @@ sc_gamepad_processor_process_gamepad_added(struct sc_gamepad_processor *gp, return; } + SDL_GameController* game_controller = + SDL_GameControllerFromInstanceID(event->gamepad_id); + assert(game_controller); + const char *name = SDL_GameControllerName(game_controller); + LOGI("Gamepad added: [%" PRIu32 "] %s", event->gamepad_id, name); + sc_gamepad_uhid_send_open(gamepad, &hid_open); } @@ -83,6 +89,8 @@ sc_gamepad_processor_process_gamepad_removed(struct sc_gamepad_processor *gp, return; } + LOGI("Gamepad removed: [%" PRIu32 "]", event->gamepad_id); + sc_gamepad_uhid_send_close(gamepad, &hid_close); } From 65256d7cc77216424976eb2ca83befde112c0e26 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2024 17:13:50 +0100 Subject: [PATCH 193/278] Upgrade SDL (2.30.10) --- app/deps/sdl.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index 8698e120..c098e367 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=2.30.9 +VERSION=2.30.10 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=682a055004081e37d81a7d4ce546c3ee3ef2e0e6a675ed2651e430ccd14eb407 +SHA256SUM=35a8b9c4f3635d85762b904ac60ca4e0806bff89faeb269caafbe80860d67168 cd "$SOURCES_DIR" From 28b5bfb90e76f059571a88931b86eb86f6ca8dd7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2024 09:29:29 +0100 Subject: [PATCH 194/278] Revert "Start cleanup process with setsid or nohup" This reverts commit c59a3c3169973abb4ce236e06990d58ae6567481. The next commit will use Os.setsid() instead. --- .../java/com/genymobile/scrcpy/CleanUp.java | 28 +++++++------------ 1 file changed, 10 insertions(+), 18 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index ac265229..f372855b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -10,8 +10,6 @@ import android.os.BatteryManager; import java.io.File; import java.io.IOException; import java.io.OutputStream; -import java.util.ArrayList; -import java.util.List; /** * Handle the cleanup of scrcpy, even if the main process is killed. @@ -109,22 +107,16 @@ public final class CleanUp { private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) throws IOException { - - List cmd = new ArrayList<>(); - if (new File("/system/bin/setsid").exists()) { - cmd.add("/system/bin/setsid"); - } else if (new File("/system/bin/nohup").exists()) { - cmd.add("/system/bin/nohup"); - } - - cmd.add("app_process"); - cmd.add("/"); - cmd.add(CleanUp.class.getName()); - cmd.add(String.valueOf(displayId)); - cmd.add(String.valueOf(restoreStayOn)); - cmd.add(String.valueOf(disableShowTouches)); - cmd.add(String.valueOf(powerOffScreen)); - cmd.add(String.valueOf(restoreScreenOffTimeout)); + String[] cmd = { + "app_process", + "/", + CleanUp.class.getName(), + String.valueOf(displayId), + String.valueOf(restoreStayOn), + String.valueOf(disableShowTouches), + String.valueOf(powerOffScreen), + String.valueOf(restoreScreenOffTimeout), + }; ProcessBuilder builder = new ProcessBuilder(cmd); builder.environment().put("CLASSPATH", Server.SERVER_PATH); From a9aadc95df6ec51198430a986ac8f56434b25e9d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2024 09:33:08 +0100 Subject: [PATCH 195/278] Start cleanup process with setsid() Reimplement c59a3c3169973abb4ce236e06990d58ae6567481 using Os.setsid(). Refs #5613 comment Suggested-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index f372855b..49b23e81 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -6,6 +6,8 @@ import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; import android.os.BatteryManager; +import android.system.ErrnoException; +import android.system.Os; import java.io.File; import java.io.IOException; @@ -163,6 +165,12 @@ public final class CleanUp { } public static void main(String... args) { + try { + // Start a new session to avoid being terminated along with the server process on some devices + Os.setsid(); + } catch (ErrnoException e) { + Ln.e("setsid() failed", e); + } unlinkSelf(); int displayId = Integer.parseInt(args[0]); From a507b4f5593d960aacb075082fd7b55cb672ee32 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 9 Dec 2024 10:11:17 +0100 Subject: [PATCH 196/278] Fix DisplayControl classpath Use the full system server classpath to load DisplayControl, so that turning the screen off on Android 14+ does not crash on certain devices. Refs #4544 comment Fixes #4544 Fixes #5274 Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/wrappers/DisplayControl.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java index a57f7948..88ca3d3d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.os.IBinder; +import android.system.Os; import java.lang.reflect.Method; @@ -21,7 +22,9 @@ public final class DisplayControl { Class classLoaderFactoryClass = Class.forName("com.android.internal.os.ClassLoaderFactory"); Method createClassLoaderMethod = classLoaderFactoryClass.getDeclaredMethod("createClassLoader", String.class, String.class, String.class, ClassLoader.class, int.class, boolean.class, String.class); - ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, "/system/framework/services.jar", null, null, + + String systemServerClasspath = Os.getenv("SYSTEMSERVERCLASSPATH"); + ClassLoader classLoader = (ClassLoader) createClassLoaderMethod.invoke(null, systemServerClasspath, null, null, ClassLoader.getSystemClassLoader(), 0, true, null); displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl"); From f2018e026c5748db89a31bc2f3535942c081a9ba Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2024 18:23:56 +0100 Subject: [PATCH 197/278] Remove broken macOS flags Due to a typo (a space was missing before the second '-L'), the resulting LDFLAGS value was broken: "-L/opt/homebrew/opt/zlib/lib-L/opt/homebrew/opt/libiconv/lib" This proves that the flag was useless. Remove it. Refs #5517 comment PR #5644 --- app/deps/ffmpeg.sh | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index 386de190..acf11584 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -40,11 +40,6 @@ else export LDFLAGS='-static-libgcc -static' elif [[ "$HOST" == "macos" ]] then - export LDFLAGS="$LDFLAGS -L/opt/homebrew/opt/zlib/lib" - export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/zlib/include" - - export LDFLAGS="$LDFLAGS-L/opt/homebrew/opt/libiconv/lib" - export CPPFLAGS="$CPPFLAGS -I/opt/homebrew/opt/libiconv/include" export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig" fi From aca6d30af5338e27571ed124ff3ef26479b214c5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 9 Dec 2024 18:30:37 +0100 Subject: [PATCH 198/278] Include dav1d in releases Scrcpy supports AV1, but no decoder was provided in binary releases. Include dav1d: - - Fixes #4744 PR #5644 --- app/deps/dav1d.sh | 68 ++++++++++++++++++++++++++++++++++++++++ app/deps/ffmpeg.sh | 5 +++ release/build_linux.sh | 1 + release/build_macos.sh | 1 + release/build_windows.sh | 1 + 5 files changed, 76 insertions(+) create mode 100755 app/deps/dav1d.sh diff --git a/app/deps/dav1d.sh b/app/deps/dav1d.sh new file mode 100755 index 00000000..3069b6fe --- /dev/null +++ b/app/deps/dav1d.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -ex +DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) +cd "$DEPS_DIR" +. common +process_args "$@" + +VERSION=1.5.0 +FILENAME=dav1d-$VERSION.tar.gz +PROJECT_DIR=dav1d-$VERSION +SHA256SUM=78b15d9954b513ea92d27f39362535ded2243e1b0924fde39f37a31ebed5f76b + +cd "$SOURCES_DIR" + +if [[ -d "$PROJECT_DIR" ]] +then + echo "$PWD/$PROJECT_DIR" found +else + get_file "https://code.videolan.org/videolan/dav1d/-/archive/$VERSION/$FILENAME" "$FILENAME" "$SHA256SUM" + tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" +fi + +mkdir -p "$BUILD_DIR/$PROJECT_DIR" +cd "$BUILD_DIR/$PROJECT_DIR" + +if [[ -d "$DIRNAME" ]] +then + echo "'$PWD/$DIRNAME' already exists, not reconfigured" + cd "$DIRNAME" +else + mkdir "$DIRNAME" + cd "$DIRNAME" + + conf=( + --prefix="$INSTALL_DIR/$DIRNAME" + --libdir=lib + -Denable_tests=false + -Denable_tools=false + # Always build dav1d statically + --default-library=static + ) + + if [[ "$BUILD_TYPE" == cross ]] + then + case "$HOST" in + win32) + conf+=( + --cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/i686-w64-mingw32.meson" + ) + ;; + + win64) + conf+=( + --cross-file="$SOURCES_DIR/$PROJECT_DIR/package/crossfiles/x86_64-w64-mingw32.meson" + ) + ;; + + *) + echo "Unsupported host: $HOST" >&2 + exit 1 + esac + fi + + meson setup . "$SOURCES_DIR/$PROJECT_DIR" "${conf[@]}" +fi + +ninja +ninja install diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index acf11584..d268ca91 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -43,8 +43,11 @@ else export PKG_CONFIG_PATH="/opt/homebrew/opt/zlib/lib/pkgconfig" fi + export PKG_CONFIG_PATH="$INSTALL_DIR/$DIRNAME/lib/pkgconfig:$PKG_CONFIG_PATH" + conf=( --prefix="$INSTALL_DIR/$DIRNAME" + --pkg-config-flags="--static" --extra-cflags="-O2 -fPIC" --disable-programs --disable-doc @@ -57,9 +60,11 @@ else --disable-vaapi --disable-vdpau --enable-swresample + --enable-libdav1d --enable-decoder=h264 --enable-decoder=hevc --enable-decoder=av1 + --enable-decoder=libdav1d --enable-decoder=pcm_s16le --enable-decoder=opus --enable-decoder=aac diff --git a/release/build_linux.sh b/release/build_linux.sh index ccf24575..6bca6979 100755 --- a/release/build_linux.sh +++ b/release/build_linux.sh @@ -15,6 +15,7 @@ LINUX_BUILD_DIR="$WORK_DIR/build-linux-$ARCH" app/deps/adb_linux.sh app/deps/sdl.sh linux native static +app/deps/dav1d.sh linux native static app/deps/ffmpeg.sh linux native static app/deps/libusb.sh linux native static diff --git a/release/build_macos.sh b/release/build_macos.sh index 2c41d04e..8f4beb9b 100755 --- a/release/build_macos.sh +++ b/release/build_macos.sh @@ -15,6 +15,7 @@ MACOS_BUILD_DIR="$WORK_DIR/build-macos-$ARCH" app/deps/adb_macos.sh app/deps/sdl.sh macos native static +app/deps/dav1d.sh macos native static app/deps/ffmpeg.sh macos native static app/deps/libusb.sh macos native static diff --git a/release/build_windows.sh b/release/build_windows.sh index dbd6cbf4..c83d2e31 100755 --- a/release/build_windows.sh +++ b/release/build_windows.sh @@ -22,6 +22,7 @@ WINXX_BUILD_DIR="$WORK_DIR/build-$WINXX" app/deps/adb_windows.sh app/deps/sdl.sh $WINXX cross shared +app/deps/dav1d.sh $WINXX cross shared app/deps/ffmpeg.sh $WINXX cross shared app/deps/libusb.sh $WINXX cross shared From 754f4fc6fec42774183a0e821be2a8852366b7bd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2024 18:15:46 +0100 Subject: [PATCH 199/278] Bump version to 3.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 0f1caf87..2c441aa1 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.0.2" + VALUE "ProductVersion", "3.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index badc1adb..aa1a3a3b 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.0.2', + version: '3.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 4b7b0254..9c0543e9 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30002 - versionName "3.0.2" + versionCode 30100 + versionName "3.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index f2420f64..d16592b4 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=3.0.2 +SCRCPY_VERSION_NAME=3.1 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From 0e2d084751b0513f5db1b7e7afc5460766f4b5c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 8 Dec 2024 18:36:20 +0100 Subject: [PATCH 200/278] Update links to 3.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 601085da..09fa12b4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.0.2) +# scrcpy (v3.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 20d1f0f5..2776ed01 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.0.2`][direct-scrcpy-server] - SHA-256: `e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d` + - [`scrcpy-server-v3.1`][direct-scrcpy-server] + SHA-256: `958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-server-v3.0.2 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index db4d7977..9beaed1e 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.0.2.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `20b69dcd379bb7d7208bf1e4858cf04162fc856697be0e6c03863d7b3c1e734a` + - [`scrcpy-linux-x86_64-v3.1.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `37dba54092ed9ec6b2f8f95432f61b8ea124aec9f1e9f2b3d22d4b10bb04c59a` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-linux-x86_64-v3.0.2.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-linux-x86_64-v3.1.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index af92f6be..56d9f168 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.0.2.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `811ba2f4e856146bdd161e24c3490d78efbec2339ca783fac791d041c0aecfb6` + - [`scrcpy-macos-aarch64-v3.1.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `478618d940421e5f57942f5479d493ecbb38210682937a200f712aee5f235daf` - - [`scrcpy-macos-x86_64-v3.0.2.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `8effff54dca3a3e46eaaec242771a13a7f81af2e18670b3d0d8ed6b461bb4f79` + - [`scrcpy-macos-x86_64-v3.1.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `acde98e29c273710ffa469371dbca4a728a44c41c380381f8a54e5b5301b9e87` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-aarch64-v3.0.2.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-macos-x86_64-v3.0.2.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-aarch64-v3.1.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-x86_64-v3.1.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index e0f0a1b3..ec7b904b 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.0.2.zip`][direct-win64] (64-bit) - SHA-256: `f0de59f5d46127c87cd822d39d6665e016b86db4cd048101b262f6adb6766832` - - [`scrcpy-win32-v3.0.2.zip`][direct-win32] (32-bit) - SHA-256: `8db8d4984d642012c55802de71f507f8ff9f68a8cfed456d7a1982d47e065f64` + - [`scrcpy-win64-v3.1.zip`][direct-win64] (64-bit) + SHA-256: `0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca` + - [`scrcpy-win32-v3.1.zip`][direct-win32] (32-bit) + SHA-256: `2b4674ef76719680ac5a9b482d1943bdde3fa25821ad2e98f3c40c347d00d560` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win64-v3.0.2.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-win32-v3.0.2.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win64-v3.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win32-v3.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 5a6eaa7b..3774be86 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.0.2/scrcpy-server-v3.0.2 -PREBUILT_SERVER_SHA256=e19fe024bfa3367809494407ad6ca809a6f6e77dac95e99f85ba75144e0ba35d +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 +PREBUILT_SERVER_SHA256=958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 6469054b15355e55ddfa713fa9cef5b88fa46358 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Dec 2024 18:07:58 +0100 Subject: [PATCH 201/278] Revert "Remove apt update on GitHub Actions" This reverts commit 678025b31672c230575fe2dbc4a0d487d5010bb1. This avoids spurious errors on the CI: E: Unable to fetch some archives, maybe run apt-get update or try with --fix-missing? --- .github/workflows/release.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a77b7ff1..a5701b0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,6 +74,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -99,6 +100,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -129,6 +131,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ @@ -162,6 +165,7 @@ jobs: - name: Install dependencies run: | + sudo apt update sudo apt install -y meson ninja-build nasm ffmpeg libsdl2-2.0-0 \ libsdl2-dev libavcodec-dev libavdevice-dev libavformat-dev \ libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ From f751274b1762a183d2848a86458b8a459b50250a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Dec 2024 17:52:17 +0100 Subject: [PATCH 202/278] Define both pkg-config and pkgconfig for meson In Meson cross-files, "pkgconfig" was deprecated in favor of "pkg-config" in meson 1.3.0. The new name is used since 85a94dd4b563e961304b2d9082932c5c1cc2e582 to avoid a warning, but then it fails with older versions of meson. To avoid the problem, define both pkg-config and pkgconfig. > For backward compatibility it is still allowed to define both with the > same value, in that case no deprecation warning is printed. --- .github/workflows/release.yml | 6 ------ cross_win32.txt | 2 ++ cross_win64.txt | 2 ++ 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a5701b0a..da021c6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -137,9 +137,6 @@ jobs: libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - name: Workaround for old meson version run by Github Actions - run: sed -i 's/^pkg-config/pkgconfig/' cross_win32.txt - - name: Build run: release/build_windows.sh 32 @@ -171,9 +168,6 @@ jobs: libavutil-dev libswresample-dev libusb-1.0-0 libusb-1.0-0-dev \ mingw-w64 mingw-w64-tools libz-mingw-w64-dev - - name: Workaround for old meson version run by Github Actions - run: sed -i 's/^pkg-config/pkgconfig/' cross_win64.txt - - name: Build run: release/build_windows.sh 64 diff --git a/cross_win32.txt b/cross_win32.txt index 05f9a86b..ddbc65f3 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -7,6 +7,8 @@ cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' pkg-config = 'i686-w64-mingw32-pkg-config' +# backward compatibility +pkgconfig = 'i686-w64-mingw32-pkg-config' windres = 'i686-w64-mingw32-windres' [host_machine] diff --git a/cross_win64.txt b/cross_win64.txt index 86364ad6..a6f16e16 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -7,6 +7,8 @@ cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' pkg-config = 'x86_64-w64-mingw32-pkg-config' +# backward compatibility +pkgconfig = 'x86_64-w64-mingw32-pkg-config' windres = 'x86_64-w64-mingw32-windres' [host_machine] From 17e205e54f8c975c18d3466ce2a9a5663bfbaf96 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Dec 2024 17:59:36 +0100 Subject: [PATCH 203/278] Replace meson join_paths() by '/' A new '/' operator was introduced in Meson 0.49 to replace join_paths(): - - Refs #5658 --- app/meson.build | 10 +++++----- meson.build | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/meson.build b/app/meson.build index be02ebc1..85e88940 100644 --- a/app/meson.build +++ b/app/meson.build @@ -192,19 +192,19 @@ datadir = get_option('datadir') # by default 'share' install_man('scrcpy.1') install_data('data/icon.png', rename: 'scrcpy.png', - install_dir: join_paths(datadir, 'icons/hicolor/256x256/apps')) + install_dir: datadir / 'icons/hicolor/256x256/apps') install_data('data/zsh-completion/_scrcpy', - install_dir: join_paths(datadir, 'zsh/site-functions')) + install_dir: datadir / 'zsh/site-functions') install_data('data/bash-completion/scrcpy', - install_dir: join_paths(datadir, 'bash-completion/completions')) + install_dir: datadir / 'bash-completion/completions') # Desktop entry file for application launchers if host_machine.system() == 'linux' # Install a launcher (ex: /usr/local/share/applications/scrcpy.desktop) install_data('data/scrcpy.desktop', - install_dir: join_paths(datadir, 'applications')) + install_dir: datadir / 'applications') install_data('data/scrcpy-console.desktop', - install_dir: join_paths(datadir, 'applications')) + install_dir: datadir / 'applications') endif diff --git a/meson.build b/meson.build index aa1a3a3b..84784814 100644 --- a/meson.build +++ b/meson.build @@ -1,6 +1,6 @@ project('scrcpy', 'c', version: '3.1', - meson_version: '>= 0.48', + meson_version: '>= 0.49', default_options: [ 'c_std=c11', 'warning_level=2', From ec4e826976d977870fc35d33591fd6164ee9d792 Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Thu, 12 Dec 2024 12:41:22 +0000 Subject: [PATCH 204/278] Set icon and server env paths for meson devenv This allows users to compile and run the project in a dev environment. meson setup x meson compile -C x meson devenv -C x scrcpy This is an alternative to `./run x`. PR #5658 Signed-off-by: Romain Vimont --- app/meson.build | 6 ++++++ server/meson.build | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/app/meson.build b/app/meson.build index 85e88940..f7df69eb 100644 --- a/app/meson.build +++ b/app/meson.build @@ -279,3 +279,9 @@ if get_option('buildtype') == 'debug' test(t[0], exe) endforeach endif + +if meson.version().version_compare('>= 0.58.0') + devenv = environment() + devenv.set('SCRCPY_ICON_PATH', meson.current_source_dir() / 'data/icon.png') + meson.add_devenv(devenv) +endif diff --git a/server/meson.build b/server/meson.build index 42b97981..55828e2d 100644 --- a/server/meson.build +++ b/server/meson.build @@ -23,3 +23,9 @@ else install: true, install_dir: 'share/scrcpy') endif + +if meson.version().version_compare('>= 0.58.0') + devenv = environment() + devenv.set('SCRCPY_SERVER_PATH', meson.current_build_dir() / 'scrcpy-server') + meson.add_devenv(devenv) +endif From 69264703b11614d022c31096d70eec14870393c7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Dec 2024 10:25:13 +0100 Subject: [PATCH 205/278] Add missing comments in workarounds The implementation of workarounds uses a lot of reflection code. For better readability, always write the equivalent using direct Java code. --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index eec00a04..a5283a96 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -132,10 +132,13 @@ public final class Workarounds { try { Class configurationControllerClass = Class.forName("android.app.ConfigurationController"); Class activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); + + // configurationController = new ConfigurationController(ACTIVITY_THREAD); Constructor configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); configurationControllerConstructor.setAccessible(true); Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); + // ACTIVITY_THREAD.mConfigurationController = configurationController; Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); configurationControllerField.setAccessible(true); configurationControllerField.set(ACTIVITY_THREAD, configurationController); From dc2fcc46f516588f4575c1fe8cfeca3e57a1653c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 14 Dec 2024 10:15:20 +0100 Subject: [PATCH 206/278] Add workaround for Pico 4 Ultra Make ActivityThread.isSystem() return true to avoid a NullPointerException later. Refs #5659 comment Fixes #5659 --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index a5283a96..fb4c1389 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -42,6 +42,11 @@ public final class Workarounds { Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); sCurrentActivityThreadField.setAccessible(true); sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); + + // activityThread.mSystemThread = true; + Field mSystemThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("mSystemThread"); + mSystemThreadField.setAccessible(true); + mSystemThreadField.setBoolean(ACTIVITY_THREAD, true); } catch (Exception e) { throw new AssertionError(e); } From ea6a94d355b92b103da8931d175f2a4a35e8e301 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 17 Dec 2024 12:20:50 +0100 Subject: [PATCH 207/278] Fix mouse documentation formatting Make the format consistent with the shortcuts documentation. --- doc/mouse.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/mouse.md b/doc/mouse.md index 3607a92c..0bea4aea 100644 --- a/doc/mouse.md +++ b/doc/mouse.md @@ -83,9 +83,9 @@ process like the _adb daemon_). ## Mouse bindings By default, with SDK mouse: - - right-click triggers BACK (or POWER on) - - middle-click triggers HOME - - the 4th click triggers APP_SWITCH + - right-click triggers `BACK` (or `POWER` on) + - middle-click triggers `HOME` + - the 4th click triggers `APP_SWITCH` - the 5th click expands the notification panel The secondary clicks may be forwarded to the device instead by pressing the @@ -121,9 +121,9 @@ Each character must be one of the following: - `+`: forward the click to the device - `-`: ignore the click - - `b`: trigger shortcut BACK (or turn screen on if off) - - `h`: trigger shortcut HOME - - `s`: trigger shortcut APP_SWITCH + - `b`: trigger shortcut `BACK` (or turn screen on if off) + - `h`: trigger shortcut `HOME` + - `s`: trigger shortcut `APP_SWITCH` - `n`: trigger shortcut "expand notification panel" For example: From 48fc18e3806e6eb77772f9f89671884b40c61714 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 17 Dec 2024 12:21:59 +0100 Subject: [PATCH 208/278] Add must-know tips All users should be aware of the main shortcuts and the most important setting to improve performance. --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 09fa12b4..5eb59ba5 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,16 @@ Note that USB debugging is not required to run scrcpy in [OTG mode](doc/otg.md). - [macOS](doc/macos.md) +## Must-know tips + + - [Reducing resolution](doc/video.md#size) may greatly improve performance + (`scrcpy -m1024`) + - [_Right-click_](doc/mouse.md#mouse-bindings) triggers `BACK` + - [_Middle-click_](doc/mouse.md#mouse-bindings) triggers `HOME` + - Alt+f toggles [fullscreen](doc/window.md#fullscreen) + - There are many other [shortcuts](doc/shortcuts.md) + + ## Usage examples There are a lot of options, [documented](#user-documentation) in separate pages. From 1fd57ede1f7caca8d9dad73b0fe778079fad73f1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 17 Dec 2024 13:09:24 +0100 Subject: [PATCH 209/278] Move "screen off timeout" section in documentation Place the "screen off timeout" section right after "stay awake", as they serve a similar purpose. --- doc/device.md | 50 +++++++++++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/doc/device.md b/doc/device.md index 42208faa..ab1e6ba4 100644 --- a/doc/device.md +++ b/doc/device.md @@ -34,6 +34,31 @@ adb shell settings put global stay_on_while_plugged_in 0 ``` +## Screen off timeout + +The Android screen automatically turns off after some delay. + +To change this delay while scrcpy is running: + +```bash +scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) +``` + +The initial value is restored on exit. + +It is possible to change this setting manually: + +```bash +# get the current screen_off_timeout value +adb shell settings get system screen_off_timeout +# set a new value (in milliseconds) +adb shell settings put system screen_off_timeout 30000 +``` + +Note that the Android value is in milliseconds, but the scrcpy command line +argument is in seconds. + + ## Turn screen off It is possible to turn the device screen off while mirroring on start with a @@ -71,31 +96,6 @@ adb shell cmd display power-on 0 ``` -## Screen off timeout - -The Android screen automatically turns off after some delay. - -To change this delay while scrcpy is running: - -```bash -scrcpy --screen-off-timeout=300 # 300 seconds (5 minutes) -``` - -The initial value is restored on exit. - -It is possible to change this setting manually: - -```bash -# get the current screen_off_timeout value -adb shell settings get system screen_off_timeout -# set a new value (in milliseconds) -adb shell settings put system screen_off_timeout 30000 -``` - -Note that the Android value is in milliseconds, but the scrcpy command line -argument is in seconds. - - ## Show touches For presentations, it may be useful to show physical touches (on the physical From 5ae01749bf4fad9fb4d8bf7c879dc60f479c1013 Mon Sep 17 00:00:00 2001 From: Markus <65797058+headquarter8302@users.noreply.github.com> Date: Wed, 18 Dec 2024 21:12:58 -0400 Subject: [PATCH 210/278] Reintroduce WinGet install note This semantically reverts c27ab46efbcab0b9558a91e691d799ffef496c97. WinGet package has been fixed by: Refs #4027 PR #5686 Signed-off-by: Romain Vimont --- doc/windows.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/doc/windows.md b/doc/windows.md index ec7b904b..89b80727 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -20,6 +20,12 @@ and extract it. ### From a package manager +From [WinGet] (ADB and other dependencies will be installed alongside scrcpy): + +```bash +winget install --exact Genymobile.scrcpy +``` + From [Chocolatey]: ```bash @@ -29,12 +35,12 @@ choco install adb # if you don't have it yet From [Scoop]: - ```bash scoop install scrcpy scoop install adb # if you don't have it yet ``` +[WinGet]: https://github.com/microsoft/winget-cli [Chocolatey]: https://chocolatey.org/ [Scoop]: https://scoop.sh From fb47b87eebbda65fd28e407cd2a6d33fea476fe3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Dec 2024 20:57:20 +0100 Subject: [PATCH 211/278] Fix pipe read return value The function incorrectly returned false, whereas its return type is ssize_t. --- app/src/util/process_intr.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/util/process_intr.c b/app/src/util/process_intr.c index d37bd5a5..641440ab 100644 --- a/app/src/util/process_intr.c +++ b/app/src/util/process_intr.c @@ -5,7 +5,7 @@ sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted - return false; + return -1; } ssize_t ret = sc_pipe_read(pipe, data, len); @@ -22,7 +22,7 @@ sc_pipe_read_all_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, size_t len) { if (intr && !sc_intr_set_process(intr, pid)) { // Already interrupted - return false; + return -1; } ssize_t ret = sc_pipe_read_all(pipe, data, len); From 95c4f03c1bfd566b383780977b3473ebad6477ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 19 Dec 2024 12:25:20 +0100 Subject: [PATCH 212/278] Build static linux binary on Ubuntu 22.04 On Github Actions, ubuntu-latest now points to ubuntu-24.04, which uses a newer version of glibc (2.39). As a result, the binaries fail to work on systems with older versions of glibc, such as Debian Bookworm. To ensure better compatibility, continue building the static Linux binary on Ubuntu 22.04 (with glibc 2.35). Fixes #5689 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index da021c6e..c90b7fb0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: run: release/test_client.sh build-linux-x86_64: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 steps: - name: Check architecture run: | From 2f44da76f4767aec2f42e6882a30c62615e0f139 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:50:58 +0800 Subject: [PATCH 213/278] Filter out non-backward-compatible cameras PR #5669 Signed-off-by: Romain Vimont --- .../com/genymobile/scrcpy/util/LogUtils.java | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 088be7e7..961b8da0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -120,6 +120,21 @@ public final class LogUtils { } } + private static boolean isCameraBackwardCompatible(CameraCharacteristics characteristics) { + int[] capabilities = characteristics.get(CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES); + if (capabilities == null) { + return false; + } + + for (int capability : capabilities) { + if (capability == CameraCharacteristics.REQUEST_AVAILABLE_CAPABILITIES_BACKWARD_COMPATIBLE) { + return true; + } + } + + return false; + } + public static String buildCameraListMessage(boolean includeSizes) { StringBuilder builder = new StringBuilder("List of cameras:"); CameraManager cameraManager = ServiceManager.getCameraManager(); @@ -129,9 +144,16 @@ public final class LogUtils { builder.append("\n (none)"); } else { for (String id : cameraIds) { - builder.append("\n --camera-id=").append(id); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); + if (!isCameraBackwardCompatible(characteristics)) { + // Ignore depth cameras as suggested by official documentation + // + continue; + } + + builder.append("\n --camera-id=").append(id); + int facing = characteristics.get(CameraCharacteristics.LENS_FACING); builder.append(" (").append(getCameraFacingName(facing)).append(", "); From 538764416099c803fcb041ecffa2c849829b7222 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 22 Dec 2024 21:17:51 +0100 Subject: [PATCH 214/278] Ignore low-FPS ranges if not available Do not report an error if the returned FPS ranges array is null. Refs #5669 --- .../src/main/java/com/genymobile/scrcpy/util/LogUtils.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 961b8da0..701ae373 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -163,8 +163,10 @@ public final class LogUtils { try { // Capture frame rates for low-FPS mode are the same for every resolution Range[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); - SortedSet uniqueLowFps = getUniqueSet(lowFpsRanges); - builder.append(", fps=").append(uniqueLowFps); + if (lowFpsRanges != null) { + SortedSet 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); From e0423653c892a62342bf665473093a9a020e0ffc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Dec 2024 10:58:02 +0100 Subject: [PATCH 215/278] Remove useless null check The method CameraManager.getCameraIdList() is annotated with @NonNull. This fixes a warning reported by Android Studio. --- server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java index 701ae373..4f8927ec 100644 --- a/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/util/LogUtils.java @@ -140,7 +140,7 @@ public final class LogUtils { CameraManager cameraManager = ServiceManager.getCameraManager(); try { String[] cameraIds = cameraManager.getCameraIdList(); - if (cameraIds == null || cameraIds.length == 0) { + if (cameraIds.length == 0) { builder.append("\n (none)"); } else { for (String id : cameraIds) { From 69858c6f437b1bfece96bc291c607de842837d36 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Dec 2024 11:01:42 +0100 Subject: [PATCH 216/278] Build static linux binary on Ubuntu 20.04 Use the oldest Ubuntu version currently available in GitHub Actions to ensure maximum compatibility with older systems. Refs 95c4f03c1bfd566b383780977b3473ebad6477ee Refs #5689 --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c90b7fb0..b1fedda9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: run: release/test_client.sh build-linux-x86_64: - runs-on: ubuntu-22.04 + runs-on: ubuntu-20.04 steps: - name: Check architecture run: | From 5b1229a55f8e89facaeb0d3757e37c49d62e88fb Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 23 Dec 2024 01:12:54 +0800 Subject: [PATCH 217/278] Support older macOS versions in CI build Fixes #5649 Fixes #5697 Signed-off-by: Romain Vimont --- .github/workflows/release.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b1fedda9..5875c6bf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -206,6 +206,13 @@ jobs: libtool - name: Build + env: + # the default Xcode (and macOS SDK) version can be found at + # + # + # then the minimal supported deployment target of that macOS SDK can be found at + # + MACOSX_DEPLOYMENT_TARGET: 10.13 run: release/build_macos.sh aarch64 # upload-artifact does not preserve permissions @@ -242,6 +249,13 @@ jobs: # autoconf and libtool are already installed on macos-13 - name: Build + env: + # the default Xcode (and macOS SDK) version can be found at + # + # + # then the minimal supported deployment target of that macOS SDK can be found at + # + MACOSX_DEPLOYMENT_TARGET: 10.13 run: release/build_macos.sh x86_64 # upload-artifact does not preserve permissions From af15c72f9caef4f829d337c35701d8bc00a58989 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Dec 2024 20:58:41 +0100 Subject: [PATCH 218/278] Cleanup includes Improved manually with the help of neovim LSP warnings and iwyu: iwyu -Ibuilddir/app/ -Iapp/src/ app/src/XXX.c --- app/src/adb/adb.c | 5 +++-- app/src/adb/adb.h | 2 +- app/src/adb/adb_device.h | 1 - app/src/adb/adb_parser.c | 1 + app/src/adb/adb_parser.h | 4 ++-- app/src/adb/adb_tunnel.c | 4 ++-- app/src/audio_player.h | 4 +--- app/src/audio_regulator.c | 4 ++++ app/src/audio_regulator.h | 2 ++ app/src/cli.c | 2 ++ app/src/decoder.c | 8 +++----- app/src/decoder.h | 6 ++---- app/src/delay_buffer.c | 4 +--- app/src/delay_buffer.h | 1 + app/src/demuxer.c | 7 ++----- app/src/demuxer.h | 4 ---- app/src/device_msg.h | 4 ++-- app/src/display.c | 2 ++ app/src/display.h | 3 ++- app/src/events.c | 2 ++ app/src/file_pusher.c | 2 +- app/src/fps_counter.c | 1 + app/src/fps_counter.h | 2 +- app/src/frame_buffer.c | 2 -- app/src/frame_buffer.h | 1 + app/src/hid/hid_event.h | 1 + app/src/hid/hid_gamepad.c | 2 ++ app/src/hid/hid_gamepad.h | 1 + app/src/hid/hid_keyboard.c | 1 + app/src/hid/hid_keyboard.h | 1 + app/src/hid/hid_mouse.c | 2 ++ app/src/hid/hid_mouse.h | 2 -- app/src/icon.c | 11 ++++++++--- app/src/icon.h | 4 +--- app/src/input_events.h | 1 - app/src/input_manager.c | 6 +++++- app/src/input_manager.h | 6 +++--- app/src/keyboard_sdk.c | 5 +++++ app/src/main.c | 3 --- app/src/mouse_sdk.c | 2 +- app/src/mouse_sdk.h | 1 - app/src/opengl.c | 3 ++- app/src/options.c | 2 ++ app/src/options.h | 1 - app/src/packet_merger.c | 4 ++++ app/src/packet_merger.h | 2 +- app/src/receiver.c | 1 - app/src/recorder.c | 3 +++ app/src/recorder.h | 3 ++- app/src/scrcpy.c | 9 +++++---- app/src/scrcpy.h | 1 - app/src/screen.h | 10 ++++++---- app/src/server.c | 9 ++++----- app/src/server.h | 8 +++----- app/src/shortcut_mod.h | 1 + app/src/sys/unix/file.c | 3 ++- app/src/sys/unix/process.c | 2 ++ app/src/trait/frame_sink.h | 1 - app/src/trait/frame_source.c | 2 ++ app/src/trait/frame_source.h | 4 +++- app/src/trait/gamepad_processor.h | 3 --- app/src/trait/key_processor.h | 1 - app/src/trait/mouse_processor.h | 1 - app/src/trait/packet_sink.h | 1 - app/src/trait/packet_source.c | 2 ++ app/src/trait/packet_source.h | 4 +++- app/src/uhid/gamepad_uhid.c | 5 +++++ app/src/uhid/gamepad_uhid.h | 2 -- app/src/uhid/keyboard_uhid.c | 6 ++++++ app/src/uhid/mouse_uhid.c | 3 +++ app/src/uhid/uhid_output.c | 1 - app/src/uhid/uhid_output.h | 2 +- app/src/usb/aoa_hid.c | 7 +++++-- app/src/usb/aoa_hid.h | 7 ++----- app/src/usb/gamepad_aoa.c | 2 ++ app/src/usb/gamepad_aoa.h | 4 +--- app/src/usb/keyboard_aoa.h | 2 +- app/src/usb/mouse_aoa.c | 1 + app/src/usb/mouse_aoa.h | 2 +- app/src/usb/scrcpy_otg.c | 13 +++++++++++-- app/src/usb/screen_otg.c | 4 ++++ app/src/usb/screen_otg.h | 7 ++++--- app/src/util/acksync.c | 1 - app/src/util/acksync.h | 5 ++++- app/src/util/audiobuf.h | 1 + app/src/util/average.h | 3 --- app/src/util/binary.h | 1 - app/src/util/env.c | 4 +++- app/src/util/intmap.h | 1 + app/src/util/intr.c | 4 ++-- app/src/util/intr.h | 6 +++--- app/src/util/log.c | 5 ++++- app/src/util/net.c | 13 ++++++------- app/src/util/net.h | 3 ++- app/src/util/net_intr.h | 9 +++++++-- app/src/util/process.c | 2 -- app/src/util/process.h | 2 ++ app/src/util/process_intr.h | 4 ++-- app/src/util/str.c | 4 ++-- app/src/util/str.h | 2 ++ app/src/util/strbuf.c | 3 +-- app/src/util/thread.c | 4 +++- app/src/util/tick.c | 1 + app/src/util/timeout.c | 3 ++- app/src/util/timeout.h | 4 ++-- app/src/util/vecdeque.h | 1 + app/src/util/vector.h | 2 +- app/src/v4l2_sink.c | 4 ++++ app/src/v4l2_sink.h | 6 +++--- app/src/version.c | 2 ++ 110 files changed, 225 insertions(+), 151 deletions(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 0cd3c0fd..40e9e968 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -4,9 +4,10 @@ #include #include #include +#include -#include "adb_device.h" -#include "adb_parser.h" +#include "adb/adb_device.h" +#include "adb/adb_parser.h" #include "util/env.h" #include "util/file.h" #include "util/log.h" diff --git a/app/src/adb/adb.h b/app/src/adb/adb.h index 43310fb9..e4903902 100644 --- a/app/src/adb/adb.h +++ b/app/src/adb/adb.h @@ -6,7 +6,7 @@ #include #include -#include "adb_device.h" +#include "adb/adb_device.h" #include "util/intr.h" #define SC_ADB_NO_STDOUT (1 << 0) diff --git a/app/src/adb/adb_device.h b/app/src/adb/adb_device.h index 56393bcf..308663ef 100644 --- a/app/src/adb/adb_device.h +++ b/app/src/adb/adb_device.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include "util/vector.h" diff --git a/app/src/adb/adb_parser.c b/app/src/adb/adb_parser.c index 66bb1854..90a1b30b 100644 --- a/app/src/adb/adb_parser.c +++ b/app/src/adb/adb_parser.c @@ -3,6 +3,7 @@ #include #include #include +#include #include "util/log.h" #include "util/str.h" diff --git a/app/src/adb/adb_parser.h b/app/src/adb/adb_parser.h index f20349f6..b8738a35 100644 --- a/app/src/adb/adb_parser.h +++ b/app/src/adb/adb_parser.h @@ -3,9 +3,9 @@ #include "common.h" -#include +#include -#include "adb_device.h" +#include "adb/adb_device.h" /** * Parse the available devices from the output of `adb devices` diff --git a/app/src/adb/adb_tunnel.c b/app/src/adb/adb_tunnel.c index fa936e4b..43e80e13 100644 --- a/app/src/adb/adb_tunnel.c +++ b/app/src/adb/adb_tunnel.c @@ -1,11 +1,11 @@ #include "adb_tunnel.h" #include +#include -#include "adb.h" +#include "adb/adb.h" #include "util/log.h" #include "util/net_intr.h" -#include "util/process_intr.h" static bool listen_on_port(struct sc_intr *intr, sc_socket socket, uint16_t port) { diff --git a/app/src/audio_player.h b/app/src/audio_player.h index 9133c24a..5a66d43b 100644 --- a/app/src/audio_player.h +++ b/app/src/audio_player.h @@ -3,9 +3,7 @@ #include "common.h" -#include -#include -#include +#include #include "audio_regulator.h" #include "trait/frame_sink.h" diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index 3e4f78ad..f7e9b81e 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -1,5 +1,9 @@ #include "audio_regulator.h" +#include +#include +#include +#include #include #include diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 1c0eeb9f..03cf6325 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -5,6 +5,8 @@ #include #include +#include +#include #include #include #include "util/audiobuf.h" diff --git a/app/src/cli.c b/app/src/cli.c index ed1970d4..756934ea 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "options.h" @@ -13,6 +14,7 @@ #include "util/str.h" #include "util/strbuf.h" #include "util/term.h" +#include "util/tick.h" #define STR_IMPL_(x) #x #define STR(x) STR_IMPL_(x) diff --git a/app/src/decoder.c b/app/src/decoder.c index 5d42b8b0..4d0a1daf 100644 --- a/app/src/decoder.c +++ b/app/src/decoder.c @@ -1,11 +1,9 @@ #include "decoder.h" -#include -#include -#include +#include +#include +#include -#include "events.h" -#include "trait/frame_sink.h" #include "util/log.h" /** Downcast packet_sink to decoder */ diff --git a/app/src/decoder.h b/app/src/decoder.h index ba8903f4..1f525fae 100644 --- a/app/src/decoder.h +++ b/app/src/decoder.h @@ -3,13 +3,11 @@ #include "common.h" +#include + #include "trait/frame_source.h" #include "trait/packet_sink.h" -#include -#include -#include - struct sc_decoder { struct sc_packet_sink packet_sink; // packet sink trait struct sc_frame_source frame_source; // frame source trait diff --git a/app/src/delay_buffer.c b/app/src/delay_buffer.c index e89a2092..f75c6f72 100644 --- a/app/src/delay_buffer.c +++ b/app/src/delay_buffer.c @@ -2,9 +2,7 @@ #include #include - -#include -#include +#include #include "util/log.h" diff --git a/app/src/delay_buffer.h b/app/src/delay_buffer.h index 18c1ce94..61cd77e4 100644 --- a/app/src/delay_buffer.h +++ b/app/src/delay_buffer.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "clock.h" #include "trait/frame_source.h" diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 7223b553..885cd6ee 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -1,14 +1,11 @@ #include "demuxer.h" #include +#include +#include #include -#include -#include -#include "decoder.h" -#include "events.h" #include "packet_merger.h" -#include "recorder.h" #include "util/binary.h" #include "util/log.h" diff --git a/app/src/demuxer.h b/app/src/demuxer.h index 5587d12d..2b7cb703 100644 --- a/app/src/demuxer.h +++ b/app/src/demuxer.h @@ -4,12 +4,8 @@ #include "common.h" #include -#include -#include -#include #include "trait/packet_source.h" -#include "trait/packet_sink.h" #include "util/net.h" #include "util/thread.h" diff --git a/app/src/device_msg.h b/app/src/device_msg.h index 86b2ccb7..d6c701bb 100644 --- a/app/src/device_msg.h +++ b/app/src/device_msg.h @@ -3,9 +3,9 @@ #include "common.h" -#include +#include #include -#include +#include #define DEVICE_MSG_MAX_SIZE (1 << 18) // 256k // type: 1 byte; length: 4 bytes diff --git a/app/src/display.c b/app/src/display.c index 39018834..aee8ef80 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -1,6 +1,8 @@ #include "display.h" #include +#include +#include #include #include "util/log.h" diff --git a/app/src/display.h b/app/src/display.h index 064bb7bf..4de9b0a9 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -4,7 +4,8 @@ #include "common.h" #include -#include +#include +#include #include #include "coords.h" diff --git a/app/src/events.c b/app/src/events.c index ce885241..b4322d1b 100644 --- a/app/src/events.c +++ b/app/src/events.c @@ -1,5 +1,7 @@ #include "events.h" +#include + #include "util/log.h" #include "util/thread.h" diff --git a/app/src/file_pusher.c b/app/src/file_pusher.c index 06911052..681fb5d6 100644 --- a/app/src/file_pusher.c +++ b/app/src/file_pusher.c @@ -1,11 +1,11 @@ #include "file_pusher.h" #include +#include #include #include "adb/adb.h" #include "util/log.h" -#include "util/process_intr.h" #define DEFAULT_PUSH_TARGET "/sdcard/Download/" diff --git a/app/src/fps_counter.c b/app/src/fps_counter.c index dd4ae1da..1daa42ba 100644 --- a/app/src/fps_counter.c +++ b/app/src/fps_counter.c @@ -1,6 +1,7 @@ #include "fps_counter.h" #include +#include #include "util/log.h" diff --git a/app/src/fps_counter.h b/app/src/fps_counter.h index e7619271..3eab461c 100644 --- a/app/src/fps_counter.h +++ b/app/src/fps_counter.h @@ -5,9 +5,9 @@ #include #include -#include #include "util/thread.h" +#include "util/tick.h" struct sc_fps_counter { sc_thread thread; diff --git a/app/src/frame_buffer.c b/app/src/frame_buffer.c index 5699b58f..9fd4cf6f 100644 --- a/app/src/frame_buffer.c +++ b/app/src/frame_buffer.c @@ -1,8 +1,6 @@ #include "frame_buffer.h" #include -#include -#include #include "util/log.h" diff --git a/app/src/frame_buffer.h b/app/src/frame_buffer.h index f97261cd..e748adfb 100644 --- a/app/src/frame_buffer.h +++ b/app/src/frame_buffer.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "util/thread.h" diff --git a/app/src/hid/hid_event.h b/app/src/hid/hid_event.h index d6818e30..b0d45ce8 100644 --- a/app/src/hid/hid_event.h +++ b/app/src/hid/hid_event.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #define SC_HID_MAX_SIZE 15 diff --git a/app/src/hid/hid_gamepad.c b/app/src/hid/hid_gamepad.c index 8f4e4527..842eae9e 100644 --- a/app/src/hid/hid_gamepad.c +++ b/app/src/hid/hid_gamepad.c @@ -2,6 +2,8 @@ #include #include +#include +#include #include "util/binary.h" #include "util/log.h" diff --git a/app/src/hid/hid_gamepad.h b/app/src/hid/hid_gamepad.h index b532a703..8d939ac7 100644 --- a/app/src/hid/hid_gamepad.h +++ b/app/src/hid/hid_gamepad.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/hid/hid_keyboard.c b/app/src/hid/hid_keyboard.c index 961ad790..6477396a 100644 --- a/app/src/hid/hid_keyboard.c +++ b/app/src/hid/hid_keyboard.c @@ -1,5 +1,6 @@ #include "hid_keyboard.h" +#include #include #include "util/log.h" diff --git a/app/src/hid/hid_keyboard.h b/app/src/hid/hid_keyboard.h index cde1ac52..5ecfd8cf 100644 --- a/app/src/hid/hid_keyboard.h +++ b/app/src/hid/hid_keyboard.h @@ -4,6 +4,7 @@ #include "common.h" #include +#include #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 7acc413b..29cfc594 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -1,5 +1,7 @@ #include "hid_mouse.h" +#include + // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, // 1 byte for wheel motion #define SC_HID_MOUSE_INPUT_SIZE 4 diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index a9a54718..06c61dd1 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -3,8 +3,6 @@ #include "common.h" -#include - #include "hid/hid_event.h" #include "input_events.h" diff --git a/app/src/icon.c b/app/src/icon.c index 4f3a9a39..797afc75 100644 --- a/app/src/icon.c +++ b/app/src/icon.c @@ -2,17 +2,22 @@ #include #include +#include +#include +#include #include #include +#include #include #include +#include #include "config.h" -#include "compat.h" #include "util/env.h" -#include "util/file.h" +#ifdef PORTABLE +# include "util/file.h" +#endif #include "util/log.h" -#include "util/str.h" #define SCRCPY_PORTABLE_ICON_FILENAME "icon.png" #define SCRCPY_DEFAULT_ICON_PATH \ diff --git a/app/src/icon.h b/app/src/icon.h index 3251e48f..6bcf46d2 100644 --- a/app/src/icon.h +++ b/app/src/icon.h @@ -3,9 +3,7 @@ #include "common.h" -#include -#include -#include +#include SDL_Surface * scrcpy_icon_load(void); diff --git a/app/src/input_events.h b/app/src/input_events.h index ad3afa81..0c022acc 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -9,7 +9,6 @@ #include #include "coords.h" -#include "options.h" /* The representation of input events in scrcpy is very close to the SDL API, * for simplicity. diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 2e4337db..635825c9 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -1,8 +1,12 @@ #include "input_manager.h" #include -#include +#include +#include +#include +#include "android/input.h" +#include "android/keycodes.h" #include "input_events.h" #include "screen.h" #include "shortcut_mod.h" diff --git a/app/src/input_manager.h b/app/src/input_manager.h index 8efd0153..af4cbc69 100644 --- a/app/src/input_manager.h +++ b/app/src/input_manager.h @@ -4,12 +4,12 @@ #include "common.h" #include - -#include +#include +#include +#include #include "controller.h" #include "file_pusher.h" -#include "fps_counter.h" #include "options.h" #include "trait/gamepad_processor.h" #include "trait/key_processor.h" diff --git a/app/src/keyboard_sdk.c b/app/src/keyboard_sdk.c index 2d9ca85b..466a1aeb 100644 --- a/app/src/keyboard_sdk.c +++ b/app/src/keyboard_sdk.c @@ -1,8 +1,13 @@ #include "keyboard_sdk.h" #include +#include +#include +#include +#include #include "android/input.h" +#include "android/keycodes.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" diff --git a/app/src/main.c b/app/src/main.c index 8bbd074f..c58e0be7 100644 --- a/app/src/main.c +++ b/app/src/main.c @@ -1,9 +1,6 @@ #include "common.h" -#include #include -#include -#include #ifdef HAVE_V4L2 # include #endif diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index a7998972..7eceffa7 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -1,12 +1,12 @@ #include "mouse_sdk.h" #include +#include #include "android/input.h" #include "control_msg.h" #include "controller.h" #include "input_events.h" -#include "util/intmap.h" #include "util/log.h" /** Downcast mouse processor to sc_mouse_sdk */ diff --git a/app/src/mouse_sdk.h b/app/src/mouse_sdk.h index 142b89bb..fe92a2d7 100644 --- a/app/src/mouse_sdk.h +++ b/app/src/mouse_sdk.h @@ -6,7 +6,6 @@ #include #include "controller.h" -#include "screen.h" #include "trait/mouse_processor.h" struct sc_mouse_sdk { diff --git a/app/src/opengl.c b/app/src/opengl.c index 376690af..0cb83ed7 100644 --- a/app/src/opengl.c +++ b/app/src/opengl.c @@ -2,7 +2,8 @@ #include #include -#include "SDL2/SDL.h" +#include +#include void sc_opengl_init(struct sc_opengl *gl) { diff --git a/app/src/options.c b/app/src/options.c index df8033e9..044aa014 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -1,5 +1,7 @@ #include "options.h" +#include + const struct scrcpy_options scrcpy_options_default = { .serial = NULL, .crop = NULL, diff --git a/app/src/options.h b/app/src/options.h index 152881d8..c8425808 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -5,7 +5,6 @@ #include #include -#include #include #include "util/tick.h" diff --git a/app/src/packet_merger.c b/app/src/packet_merger.c index 81b02d2c..dea038b6 100644 --- a/app/src/packet_merger.c +++ b/app/src/packet_merger.c @@ -1,5 +1,9 @@ #include "packet_merger.h" +#include +#include +#include + #include "util/log.h" void diff --git a/app/src/packet_merger.h b/app/src/packet_merger.h index e1824c2c..3f9972ce 100644 --- a/app/src/packet_merger.h +++ b/app/src/packet_merger.h @@ -5,7 +5,7 @@ #include #include -#include +#include /** * Config packets (containing the SPS/PPS) are sent in-band. A new config diff --git a/app/src/receiver.c b/app/src/receiver.c index b89b0c6e..2ccb8a8b 100644 --- a/app/src/receiver.c +++ b/app/src/receiver.c @@ -2,7 +2,6 @@ #include #include -#include #include #include "device_msg.h" diff --git a/app/src/recorder.c b/app/src/recorder.c index 15f27157..c26f8f2d 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -1,6 +1,9 @@ #include "recorder.h" #include +#include +#include +#include #include #include #include diff --git a/app/src/recorder.h b/app/src/recorder.h index d096e79a..70b73836 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -4,9 +4,10 @@ #include "common.h" #include +#include +#include #include -#include "coords.h" #include "options.h" #include "trait/packet_sink.h" #include "util/thread.h" diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index f1942e43..641d93f7 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -1,10 +1,11 @@ #include "scrcpy.h" +#include +#include +#include #include +#include #include -#include -#include -#include #include #ifdef _WIN32 @@ -37,9 +38,9 @@ #endif #include "util/acksync.h" #include "util/log.h" -#include "util/net.h" #include "util/rand.h" #include "util/timeout.h" +#include "util/tick.h" #ifdef HAVE_V4L2 # include "v4l2_sink.h" #endif diff --git a/app/src/scrcpy.h b/app/src/scrcpy.h index d4d494a3..7f6a0fb2 100644 --- a/app/src/scrcpy.h +++ b/app/src/scrcpy.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include "options.h" enum scrcpy_exit_code { diff --git a/app/src/screen.h b/app/src/screen.h index c716c399..6621b2d2 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -1,11 +1,14 @@ -#ifndef SCREEN_H -#define SCREEN_H +#ifndef SC_SCREEN_H +#define SC_SCREEN_H #include "common.h" #include +#include #include -#include +#include +#include +#include #include "controller.h" #include "coords.h" @@ -14,7 +17,6 @@ #include "frame_buffer.h" #include "input_manager.h" #include "mouse_capture.h" -#include "opengl.h" #include "options.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" diff --git a/app/src/server.c b/app/src/server.c index 22ddd372..cf181abc 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -1,19 +1,18 @@ #include "server.h" #include -#include #include #include -#include -#include +#include +#include +#include #include "adb/adb.h" -#include "util/binary.h" #include "util/env.h" #include "util/file.h" #include "util/log.h" #include "util/net_intr.h" -#include "util/process_intr.h" +#include "util/process.h" #include "util/str.h" #define SC_SERVER_FILENAME "scrcpy-server" diff --git a/app/src/server.h b/app/src/server.h index 3c78b9ed..a03689ff 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -1,19 +1,17 @@ -#ifndef SERVER_H -#define SERVER_H +#ifndef SC_SERVER_H +#define SC_SERVER_H #include "common.h" -#include #include #include #include "adb/adb_tunnel.h" -#include "coords.h" #include "options.h" #include "util/intr.h" -#include "util/log.h" #include "util/net.h" #include "util/thread.h" +#include "util/tick.h" #define SC_DEVICE_NAME_FIELD_LENGTH 64 struct sc_server_info { diff --git a/app/src/shortcut_mod.h b/app/src/shortcut_mod.h index b685e987..f6c13f03 100644 --- a/app/src/shortcut_mod.h +++ b/app/src/shortcut_mod.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include #include diff --git a/app/src/sys/unix/file.c b/app/src/sys/unix/file.c index 6123c788..8f7fb074 100644 --- a/app/src/sys/unix/file.c +++ b/app/src/sys/unix/file.c @@ -1,10 +1,11 @@ #include "util/file.h" #include -#include #include +#include #include #include +#include #include #ifdef __APPLE__ # include // for _NSGetExecutablePath() diff --git a/app/src/sys/unix/process.c b/app/src/sys/unix/process.c index 8c4a53c3..36d1ff7d 100644 --- a/app/src/sys/unix/process.c +++ b/app/src/sys/unix/process.c @@ -4,6 +4,8 @@ #include #include #include +#include +#include #include #include #include diff --git a/app/src/trait/frame_sink.h b/app/src/trait/frame_sink.h index 8ef248b6..67be4d46 100644 --- a/app/src/trait/frame_sink.h +++ b/app/src/trait/frame_sink.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include diff --git a/app/src/trait/frame_source.c b/app/src/trait/frame_source.c index 416eccd9..56848309 100644 --- a/app/src/trait/frame_source.c +++ b/app/src/trait/frame_source.c @@ -1,5 +1,7 @@ #include "frame_source.h" +#include + void sc_frame_source_init(struct sc_frame_source *source) { source->sink_count = 0; diff --git a/app/src/trait/frame_source.h b/app/src/trait/frame_source.h index 94222af0..cb1ef905 100644 --- a/app/src/trait/frame_source.h +++ b/app/src/trait/frame_source.h @@ -3,7 +3,9 @@ #include "common.h" -#include "frame_sink.h" +#include + +#include "trait/frame_sink.h" #define SC_FRAME_SOURCE_MAX_SINKS 2 diff --git a/app/src/trait/gamepad_processor.h b/app/src/trait/gamepad_processor.h index 19629a9a..5e8dc2a4 100644 --- a/app/src/trait/gamepad_processor.h +++ b/app/src/trait/gamepad_processor.h @@ -3,9 +3,6 @@ #include "common.h" -#include -#include - #include "input_events.h" /** diff --git a/app/src/trait/key_processor.h b/app/src/trait/key_processor.h index 96374413..9e9bb86e 100644 --- a/app/src/trait/key_processor.h +++ b/app/src/trait/key_processor.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include "input_events.h" diff --git a/app/src/trait/mouse_processor.h b/app/src/trait/mouse_processor.h index 6e0b596e..d0a96e7c 100644 --- a/app/src/trait/mouse_processor.h +++ b/app/src/trait/mouse_processor.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include "input_events.h" diff --git a/app/src/trait/packet_sink.h b/app/src/trait/packet_sink.h index 84cfe814..e12dea12 100644 --- a/app/src/trait/packet_sink.h +++ b/app/src/trait/packet_sink.h @@ -3,7 +3,6 @@ #include "common.h" -#include #include #include diff --git a/app/src/trait/packet_source.c b/app/src/trait/packet_source.c index c0836f1d..0a2c6c4d 100644 --- a/app/src/trait/packet_source.c +++ b/app/src/trait/packet_source.c @@ -1,5 +1,7 @@ #include "packet_source.h" +#include + void sc_packet_source_init(struct sc_packet_source *source) { source->sink_count = 0; diff --git a/app/src/trait/packet_source.h b/app/src/trait/packet_source.h index 16d56e86..8788021a 100644 --- a/app/src/trait/packet_source.h +++ b/app/src/trait/packet_source.h @@ -3,7 +3,9 @@ #include "common.h" -#include "packet_sink.h" +#include + +#include "trait/packet_sink.h" #define SC_PACKET_SOURCE_MAX_SINKS 2 diff --git a/app/src/uhid/gamepad_uhid.c b/app/src/uhid/gamepad_uhid.c index a066cf03..c64feb18 100644 --- a/app/src/uhid/gamepad_uhid.c +++ b/app/src/uhid/gamepad_uhid.c @@ -1,5 +1,10 @@ #include "gamepad_uhid.h" +#include +#include +#include +#include + #include "hid/hid_gamepad.h" #include "input_events.h" #include "util/log.h" diff --git a/app/src/uhid/gamepad_uhid.h b/app/src/uhid/gamepad_uhid.h index 07d03099..ad747604 100644 --- a/app/src/uhid/gamepad_uhid.h +++ b/app/src/uhid/gamepad_uhid.h @@ -3,8 +3,6 @@ #include "common.h" -#include - #include "controller.h" #include "hid/hid_gamepad.h" #include "trait/gamepad_processor.h" diff --git a/app/src/uhid/keyboard_uhid.c b/app/src/uhid/keyboard_uhid.c index 76d70cc5..70082990 100644 --- a/app/src/uhid/keyboard_uhid.c +++ b/app/src/uhid/keyboard_uhid.c @@ -1,6 +1,12 @@ #include "keyboard_uhid.h" +#include +#include +#include +#include + #include "util/log.h" +#include "util/thread.h" /** Downcast key processor to keyboard_uhid */ #define DOWNCAST(KP) container_of(KP, struct sc_keyboard_uhid, key_processor) diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 471030e7..7fed8383 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -1,5 +1,8 @@ #include "mouse_uhid.h" +#include +#include + #include "hid/hid_mouse.h" #include "input_events.h" #include "util/log.h" diff --git a/app/src/uhid/uhid_output.c b/app/src/uhid/uhid_output.c index 05e691da..e743a73c 100644 --- a/app/src/uhid/uhid_output.c +++ b/app/src/uhid/uhid_output.c @@ -1,6 +1,5 @@ #include "uhid_output.h" -#include #include #include "uhid/keyboard_uhid.h" diff --git a/app/src/uhid/uhid_output.h b/app/src/uhid/uhid_output.h index cd6a800f..ed028b58 100644 --- a/app/src/uhid/uhid_output.h +++ b/app/src/uhid/uhid_output.h @@ -3,7 +3,7 @@ #include "common.h" -#include +#include #include /** diff --git a/app/src/usb/aoa_hid.c b/app/src/usb/aoa_hid.c index 236a78ed..8cb62bfd 100644 --- a/app/src/usb/aoa_hid.c +++ b/app/src/usb/aoa_hid.c @@ -1,13 +1,16 @@ -#include "util/log.h" +#include "aoa_hid.h" #include #include #include +#include +#include +#include -#include "aoa_hid.h" #include "events.h" #include "util/log.h" #include "util/str.h" +#include "util/tick.h" #include "util/vector.h" // See . diff --git a/app/src/usb/aoa_hid.h b/app/src/usb/aoa_hid.h index 9cc6355e..2755c957 100644 --- a/app/src/usb/aoa_hid.h +++ b/app/src/usb/aoa_hid.h @@ -3,16 +3,13 @@ #include "common.h" -#include #include - -#include +#include #include "hid/hid_event.h" -#include "usb.h" +#include "usb/usb.h" #include "util/acksync.h" #include "util/thread.h" -#include "util/tick.h" #include "util/vecdeque.h" enum sc_aoa_event_type { diff --git a/app/src/usb/gamepad_aoa.c b/app/src/usb/gamepad_aoa.c index 4372379f..d29b1a78 100644 --- a/app/src/usb/gamepad_aoa.c +++ b/app/src/usb/gamepad_aoa.c @@ -1,5 +1,7 @@ #include "gamepad_aoa.h" +#include + #include "input_events.h" #include "util/log.h" diff --git a/app/src/usb/gamepad_aoa.h b/app/src/usb/gamepad_aoa.h index b2dfbe5e..0297a365 100644 --- a/app/src/usb/gamepad_aoa.h +++ b/app/src/usb/gamepad_aoa.h @@ -3,10 +3,8 @@ #include "common.h" -#include - -#include "aoa_hid.h" #include "hid/hid_gamepad.h" +#include "usb/aoa_hid.h" #include "trait/gamepad_processor.h" struct sc_gamepad_aoa { diff --git a/app/src/usb/keyboard_aoa.h b/app/src/usb/keyboard_aoa.h index 565b9177..9e9500a3 100644 --- a/app/src/usb/keyboard_aoa.h +++ b/app/src/usb/keyboard_aoa.h @@ -5,8 +5,8 @@ #include -#include "aoa_hid.h" #include "hid/hid_keyboard.h" +#include "usb/aoa_hid.h" #include "trait/key_processor.h" struct sc_keyboard_aoa { diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index cb566cc0..b64e9b12 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -1,6 +1,7 @@ #include "mouse_aoa.h" #include +#include #include "hid/hid_mouse.h" #include "input_events.h" diff --git a/app/src/usb/mouse_aoa.h b/app/src/usb/mouse_aoa.h index afaed761..506286ba 100644 --- a/app/src/usb/mouse_aoa.h +++ b/app/src/usb/mouse_aoa.h @@ -5,7 +5,7 @@ #include -#include "aoa_hid.h" +#include "usb/aoa_hid.h" #include "trait/mouse_processor.h" struct sc_mouse_aoa { diff --git a/app/src/usb/scrcpy_otg.c b/app/src/usb/scrcpy_otg.c index 6ef2fc2a..1a9cc46e 100644 --- a/app/src/usb/scrcpy_otg.c +++ b/app/src/usb/scrcpy_otg.c @@ -1,10 +1,19 @@ #include "scrcpy_otg.h" +#include +#include +#include #include -#include "adb/adb.h" +#ifdef _WIN32 +# include "adb/adb.h" +#endif #include "events.h" -#include "screen_otg.h" +#include "usb/screen_otg.h" +#include "usb/aoa_hid.h" +#include "usb/gamepad_aoa.h" +#include "usb/keyboard_aoa.h" +#include "usb/mouse_aoa.h" #include "util/log.h" struct scrcpy_otg { diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 368af125..02edc3a3 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -1,7 +1,11 @@ #include "screen_otg.h" +#include +#include + #include "icon.h" #include "options.h" +#include "util/acksync.h" #include "util/log.h" static void diff --git a/app/src/usb/screen_otg.h b/app/src/usb/screen_otg.h index 427723ad..08b76ae7 100644 --- a/app/src/usb/screen_otg.h +++ b/app/src/usb/screen_otg.h @@ -4,12 +4,13 @@ #include "common.h" #include +#include #include -#include "keyboard_aoa.h" -#include "mouse_aoa.h" #include "mouse_capture.h" -#include "gamepad_aoa.h" +#include "usb/gamepad_aoa.h" +#include "usb/keyboard_aoa.h" +#include "usb/mouse_aoa.h" struct sc_screen_otg { struct sc_keyboard_aoa *keyboard; diff --git a/app/src/util/acksync.c b/app/src/util/acksync.c index 2899cdcb..76ecee0d 100644 --- a/app/src/util/acksync.c +++ b/app/src/util/acksync.c @@ -1,7 +1,6 @@ #include "acksync.h" #include -#include "util/log.h" bool sc_acksync_init(struct sc_acksync *as) { diff --git a/app/src/util/acksync.h b/app/src/util/acksync.h index 58ab1b35..3d9c9b2f 100644 --- a/app/src/util/acksync.h +++ b/app/src/util/acksync.h @@ -3,7 +3,10 @@ #include "common.h" -#include "thread.h" +#include +#include +#include "util/thread.h" +#include "util/tick.h" #define SC_SEQUENCE_INVALID 0 diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index 5e7dd4a0..5cc51932 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -6,6 +6,7 @@ #include #include #include +#include #include /** diff --git a/app/src/util/average.h b/app/src/util/average.h index 59fae7d1..eded9987 100644 --- a/app/src/util/average.h +++ b/app/src/util/average.h @@ -3,9 +3,6 @@ #include "common.h" -#include -#include - struct sc_average { // Current average value float avg; diff --git a/app/src/util/binary.h b/app/src/util/binary.h index 7de9b505..b6ce3201 100644 --- a/app/src/util/binary.h +++ b/app/src/util/binary.h @@ -4,7 +4,6 @@ #include "common.h" #include -#include #include static inline void diff --git a/app/src/util/env.c b/app/src/util/env.c index 1128e5ea..127f5a1f 100644 --- a/app/src/util/env.c +++ b/app/src/util/env.c @@ -2,7 +2,9 @@ #include #include -#include "util/str.h" +#ifdef _WIN32 +# include "util/str.h" +#endif char * sc_get_env(const char *varname) { diff --git a/app/src/util/intmap.h b/app/src/util/intmap.h index 2898c461..7ab903ca 100644 --- a/app/src/util/intmap.h +++ b/app/src/util/intmap.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include struct sc_intmap_entry { diff --git a/app/src/util/intr.c b/app/src/util/intr.c index 22bd121a..ddf4839f 100644 --- a/app/src/util/intr.c +++ b/app/src/util/intr.c @@ -1,9 +1,9 @@ #include "intr.h" -#include "util/log.h" - #include +#include "util/log.h" + bool sc_intr_init(struct sc_intr *intr) { bool ok = sc_mutex_init(&intr->mutex); diff --git a/app/src/util/intr.h b/app/src/util/intr.h index 1c20f6df..35bd3375 100644 --- a/app/src/util/intr.h +++ b/app/src/util/intr.h @@ -6,9 +6,9 @@ #include #include -#include "net.h" -#include "process.h" -#include "thread.h" +#include "util/net.h" +#include "util/process.h" +#include "util/thread.h" /** * Interruptor to wake up a blocking call from another thread diff --git a/app/src/util/log.c b/app/src/util/log.c index 8a347c84..9114a258 100644 --- a/app/src/util/log.c +++ b/app/src/util/log.c @@ -4,7 +4,10 @@ # include #endif #include -#include +#include +#include +#include +#include static SDL_LogPriority log_level_sc_to_sdl(enum sc_log_level level) { diff --git a/app/src/util/net.c b/app/src/util/net.c index d68b0af6..9562ff6b 100644 --- a/app/src/util/net.c +++ b/app/src/util/net.c @@ -1,28 +1,27 @@ #include "net.h" #include -#include #include -#include "log.h" - #ifdef _WIN32 # include typedef int socklen_t; #else -# include -# include +# include +# include # include # include -# include # include -# include +# include +# include # define SOCKET_ERROR -1 typedef struct sockaddr_in SOCKADDR_IN; typedef struct sockaddr SOCKADDR; typedef struct in_addr IN_ADDR; #endif +#include "util/log.h" + bool net_init(void) { #ifdef _WIN32 diff --git a/app/src/util/net.h b/app/src/util/net.h index 94789954..aa99bbc4 100644 --- a/app/src/util/net.h +++ b/app/src/util/net.h @@ -4,14 +4,15 @@ #include "common.h" #include +#include #include +#include #ifdef _WIN32 # include typedef SOCKET sc_raw_socket; # define SC_RAW_SOCKET_NONE INVALID_SOCKET #else // not _WIN32 -# include typedef int sc_raw_socket; # define SC_RAW_SOCKET_NONE -1 #endif diff --git a/app/src/util/net_intr.h b/app/src/util/net_intr.h index dbef528d..e2bbee88 100644 --- a/app/src/util/net_intr.h +++ b/app/src/util/net_intr.h @@ -3,8 +3,13 @@ #include "common.h" -#include "intr.h" -#include "net.h" +#include +#include +#include +#include + +#include "util/intr.h" +#include "util/net.h" bool net_connect_intr(struct sc_intr *intr, sc_socket socket, uint32_t addr, diff --git a/app/src/util/process.c b/app/src/util/process.c index 9c4dcd9f..29d89a54 100644 --- a/app/src/util/process.c +++ b/app/src/util/process.c @@ -1,8 +1,6 @@ #include "process.h" #include -#include -#include "log.h" enum sc_process_result sc_process_execute(const char *const argv[], sc_pid *pid, unsigned flags) { diff --git a/app/src/util/process.h b/app/src/util/process.h index 4d9d1684..eec51bcc 100644 --- a/app/src/util/process.h +++ b/app/src/util/process.h @@ -4,7 +4,9 @@ #include "common.h" #include +#include #include "util/thread.h" +#include "util/tick.h" #ifdef _WIN32 diff --git a/app/src/util/process_intr.h b/app/src/util/process_intr.h index 530a9046..020eafa1 100644 --- a/app/src/util/process_intr.h +++ b/app/src/util/process_intr.h @@ -3,8 +3,8 @@ #include "common.h" -#include "intr.h" -#include "process.h" +#include "util/intr.h" +#include "util/process.h" ssize_t sc_pipe_read_intr(struct sc_intr *intr, sc_pid pid, sc_pipe pipe, char *data, diff --git a/app/src/util/str.c b/app/src/util/str.c index 304cd302..83d19c4d 100644 --- a/app/src/util/str.c +++ b/app/src/util/str.c @@ -12,8 +12,8 @@ # include #endif -#include "log.h" -#include "strbuf.h" +#include "util/log.h" +#include "util/strbuf.h" size_t sc_strncpy(char *dest, const char *src, size_t n) { diff --git a/app/src/util/str.h b/app/src/util/str.h index d20f1b28..b386b48d 100644 --- a/app/src/util/str.h +++ b/app/src/util/str.h @@ -5,6 +5,8 @@ #include #include +#include +#include /* Stringify a numeric value */ #define SC_STR(s) SC_XSTR(s) diff --git a/app/src/util/strbuf.c b/app/src/util/strbuf.c index 1892b46b..6196d746 100644 --- a/app/src/util/strbuf.c +++ b/app/src/util/strbuf.c @@ -1,11 +1,10 @@ #include "strbuf.h" #include -#include #include #include -#include "log.h" +#include "util/log.h" bool sc_strbuf_init(struct sc_strbuf *buf, size_t init_cap) { diff --git a/app/src/util/thread.c b/app/src/util/thread.c index 9679dfff..2a5253f7 100644 --- a/app/src/util/thread.c +++ b/app/src/util/thread.c @@ -1,10 +1,12 @@ #include "thread.h" #include +#include +#include #include #include -#include "log.h" +#include "util/log.h" sc_thread_id SC_MAIN_THREAD_ID; diff --git a/app/src/util/tick.c b/app/src/util/tick.c index cc0bab5e..edef1070 100644 --- a/app/src/util/tick.c +++ b/app/src/util/tick.c @@ -1,6 +1,7 @@ #include "tick.h" #include +#include #include #ifdef _WIN32 # include diff --git a/app/src/util/timeout.c b/app/src/util/timeout.c index 159a4681..21bc3a53 100644 --- a/app/src/util/timeout.c +++ b/app/src/util/timeout.c @@ -1,8 +1,9 @@ #include "timeout.h" #include +#include -#include "log.h" +#include "util/log.h" bool sc_timeout_init(struct sc_timeout *timeout) { diff --git a/app/src/util/timeout.h b/app/src/util/timeout.h index ae171b86..a45ae2ae 100644 --- a/app/src/util/timeout.h +++ b/app/src/util/timeout.h @@ -5,8 +5,8 @@ #include -#include "thread.h" -#include "tick.h" +#include "util/thread.h" +#include "util/tick.h" struct sc_timeout { sc_thread thread; diff --git a/app/src/util/vecdeque.h b/app/src/util/vecdeque.h index ce559ee9..e31724e2 100644 --- a/app/src/util/vecdeque.h +++ b/app/src/util/vecdeque.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include diff --git a/app/src/util/vector.h b/app/src/util/vector.h index 97d7c389..5b399d56 100644 --- a/app/src/util/vector.h +++ b/app/src/util/vector.h @@ -5,8 +5,8 @@ #include #include +#include #include -#include // Adapted from vlc_vector: // diff --git a/app/src/v4l2_sink.c b/app/src/v4l2_sink.c index 087e9af4..da9e02ef 100644 --- a/app/src/v4l2_sink.c +++ b/app/src/v4l2_sink.c @@ -1,5 +1,9 @@ #include "v4l2_sink.h" +#include +#include +#include +#include #include #include "util/log.h" diff --git a/app/src/v4l2_sink.h b/app/src/v4l2_sink.h index 365a739d..2b7c5b50 100644 --- a/app/src/v4l2_sink.h +++ b/app/src/v4l2_sink.h @@ -3,13 +3,13 @@ #include "common.h" +#include #include #include -#include "coords.h" -#include "trait/frame_sink.h" #include "frame_buffer.h" -#include "util/tick.h" +#include "trait/frame_sink.h" +#include "util/thread.h" struct sc_v4l2_sink { struct sc_frame_sink frame_sink; // frame sink trait diff --git a/app/src/version.c b/app/src/version.c index 90ea3334..f8610714 100644 --- a/app/src/version.c +++ b/app/src/version.c @@ -1,5 +1,6 @@ #include "version.h" +#include #include #include #include @@ -9,6 +10,7 @@ #ifdef HAVE_USB # include #endif +#include void scrcpy_print_version(void) { From eac711ace68da43b09a4da99f7990143ae93f7c1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 23 Dec 2024 12:51:27 +0100 Subject: [PATCH 219/278] Remove unused rotation and fold listeners IRotationWatcher and IDisplayFoldListener are no longer used since commit 39d51ff2cc2f3e201ad433d48372b548e5dd11d3. --- server/build_without_gradle.sh | 2 - .../android/view/IDisplayFoldListener.aidl | 26 ---------- .../aidl/android/view/IRotationWatcher.aidl | 25 ---------- .../scrcpy/wrappers/WindowManager.java | 48 ------------------- 4 files changed, 101 deletions(-) delete mode 100644 server/src/main/aidl/android/view/IDisplayFoldListener.aidl delete mode 100644 server/src/main/aidl/android/view/IRotationWatcher.aidl diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index d16592b4..e0b69aee 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -47,10 +47,8 @@ EOF echo "Generating java from aidl..." cd "$SERVER_DIR/src/main/aidl" -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IRotationWatcher.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. \ android/content/IOnPrimaryClipChangedListener.aidl -"$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. android/view/IDisplayFoldListener.aidl "$BUILD_TOOLS_DIR/aidl" -o"$GEN_DIR" -I. -p "$ANDROID_AIDL" \ android/view/IDisplayWindowListener.aidl diff --git a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl b/server/src/main/aidl/android/view/IDisplayFoldListener.aidl deleted file mode 100644 index 2c91149d..00000000 --- a/server/src/main/aidl/android/view/IDisplayFoldListener.aidl +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (C) 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view; - -/** - * {@hide} - */ -oneway interface IDisplayFoldListener -{ - /** Called when the foldedness of a display changes */ - void onDisplayFoldChanged(int displayId, boolean folded); -} diff --git a/server/src/main/aidl/android/view/IRotationWatcher.aidl b/server/src/main/aidl/android/view/IRotationWatcher.aidl deleted file mode 100644 index 2cc5e44a..00000000 --- a/server/src/main/aidl/android/view/IRotationWatcher.aidl +++ /dev/null @@ -1,25 +0,0 @@ -/* //device/java/android/android/hardware/ISensorListener.aidl -** -** Copyright 2008, The Android Open Source Project -** -** Licensed under the Apache License, Version 2.0 (the "License"); -** you may not use this file except in compliance with the License. -** You may obtain a copy of the License at -** -** http://www.apache.org/licenses/LICENSE-2.0 -** -** Unless required by applicable law or agreed to in writing, software -** distributed under the License is distributed on an "AS IS" BASIS, -** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -** See the License for the specific language governing permissions and -** limitations under the License. -*/ - -package android.view; - -/** - * {@hide} - */ -interface IRotationWatcher { - oneway void onRotationChanged(int rotation); -} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 86dd83f2..04f5abd7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -5,9 +5,7 @@ import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; import android.os.IInterface; -import android.view.IDisplayFoldListener; import android.view.IDisplayWindowListener; -import android.view.IRotationWatcher; import java.lang.reflect.Method; @@ -182,52 +180,6 @@ public final class WindowManager { } } - public void registerRotationWatcher(IRotationWatcher rotationWatcher, int displayId) { - try { - Class cls = manager.getClass(); - try { - // display parameter added since this commit: - // https://android.googlesource.com/platform/frameworks/base/+/35fa3c26adcb5f6577849fd0df5228b1f67cf2c6%5E%21/#F1 - cls.getMethod("watchRotation", IRotationWatcher.class, int.class).invoke(manager, rotationWatcher, displayId); - } catch (NoSuchMethodException e) { - // old version - if (displayId != 0) { - Ln.e("Secondary display rotation not supported on this device"); - return; - } - cls.getMethod("watchRotation", IRotationWatcher.class).invoke(manager, rotationWatcher); - } - } catch (Exception e) { - Ln.e("Could not register rotation watcher", e); - } - } - - public void unregisterRotationWatcher(IRotationWatcher rotationWatcher) { - try { - manager.getClass().getMethod("removeRotationWatcher", IRotationWatcher.class).invoke(manager, rotationWatcher); - } catch (Exception e) { - Ln.e("Could not unregister rotation watcher", e); - } - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public void registerDisplayFoldListener(IDisplayFoldListener foldListener) { - try { - manager.getClass().getMethod("registerDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); - } catch (Exception e) { - Ln.e("Could not register display fold listener", e); - } - } - - @TargetApi(AndroidVersions.API_29_ANDROID_10) - public void unregisterDisplayFoldListener(IDisplayFoldListener foldListener) { - try { - manager.getClass().getMethod("unregisterDisplayFoldListener", IDisplayFoldListener.class).invoke(manager, foldListener); - } catch (Exception e) { - Ln.e("Could not unregister display fold listener", e); - } - } - @TargetApi(AndroidVersions.API_30_ANDROID_11) public int[] registerDisplayWindowListener(IDisplayWindowListener listener) { try { From c27d116a662c87ee84963820669ee0d2ce60e6f1 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:04:18 +0100 Subject: [PATCH 220/278] Fix AudioRecord package name for Android 16 Since commit 9f91a5eebb4520b9333576e946b3911d0f946a04 in frameworks/av (AOSP), an AudioRecord can be created only if the declared package name in the AttributionSource is "shell" (for the shell UID): - - Refs Fixes #5698 Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/FakeContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 2b83e397..22fc6d49 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -72,7 +72,7 @@ public final class FakeContext extends ContextWrapper { @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); - builder.setPackageName(PACKAGE_NAME); + builder.setPackageName("shell"); return builder.build(); } From 1c7680f689684fe45b8fa8e1525add5dc3cdfdd2 Mon Sep 17 00:00:00 2001 From: "Jaime J. Denizard" Date: Tue, 31 Dec 2024 16:21:36 -0500 Subject: [PATCH 221/278] Fix some grammatical issues in documentation PR #5722 Signed-off-by: Romain Vimont --- doc/connection.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/connection.md b/doc/connection.md index 2c3d37e1..dcf00147 100644 --- a/doc/connection.md +++ b/doc/connection.md @@ -113,16 +113,17 @@ with the device IP address you found)_. 7. Run `scrcpy` as usual. 8. Run `adb disconnect` once you're done. -Since Android 11, a [wireless debugging option][adb-wireless] allows to bypass -having to physically connect your device directly to your computer. +Since Android 11, a [wireless debugging option][adb-wireless] allows you to +bypass having to physically connect your device to your computer. [adb-wireless]: https://developer.android.com/studio/command-line/adb#wireless-android11-command-line ## Autostart -A small tool (by the scrcpy author) allows to run arbitrary commands whenever a -new Android device is connected: [AutoAdb]. It can be used to start scrcpy: +A small tool (by the scrcpy author) allows you to run arbitrary commands +whenever a new Android device is connected: [AutoAdb]. It can be used to start +scrcpy: ```bash autoadb scrcpy -s '{}' From cac8e9c821b9b8314d57e84ccbf685929af08794 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 1 Jan 2025 15:01:18 +0100 Subject: [PATCH 222/278] Happy new year 2025! --- LICENSE | 2 +- README.md | 2 +- app/scrcpy.1 | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/LICENSE b/LICENSE index d9326a74..1196b3da 100644 --- a/LICENSE +++ b/LICENSE @@ -188,7 +188,7 @@ identification within third-party archives. Copyright (C) 2018 Genymobile - Copyright (C) 2018-2024 Romain Vimont + Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index 5eb59ba5..b5884350 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ work][donate]: ## Licence Copyright (C) 2018 Genymobile - Copyright (C) 2018-2024 Romain Vimont + Copyright (C) 2018-2025 Romain Vimont Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 924905e4..f8b39112 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -829,7 +829,7 @@ Report bugs to . .SH COPYRIGHT Copyright \(co 2018 Genymobile -Copyright \(co 2018\-2024 Romain Vimont +Copyright \(co 2018\-2025 Romain Vimont Licensed under the Apache License, Version 2.0. From 0ba9d3570560cb46b52a0696134442aeb7f634e6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Jan 2025 10:54:57 +0100 Subject: [PATCH 223/278] Mention virtual display destruction The new virtual display does not persist after scrcpy exits. --- doc/virtual_display.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 5d1673e8..09e6f142 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -11,6 +11,8 @@ scrcpy --new-display # use the main display size and density scrcpy --new-display=/240 # use the main display size and 240 dpi ``` +The new virtual display is destroyed on exit. + ## Start app On some devices, a launcher is available in the virtual display. From 986328ff9ea82a709f2c31ad3cdd4e86ac845c24 Mon Sep 17 00:00:00 2001 From: Sam Listopad II Date: Thu, 30 Jan 2025 14:02:12 -0600 Subject: [PATCH 224/278] Allow controls with --no-window Without a window, mouse and keyboard events may not be received, but the control channel is still necessary for other features: * --turn-screen-off * --stay-awake * --show-touches * --power-off-on-close * --start-app Fixes #5803 PR #5804 Signed-off-by: Romain Vimont --- app/scrcpy.1 | 2 +- app/src/cli.c | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 924905e4..75bf6088 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -389,7 +389,7 @@ Disable video playback on the computer. .TP .B \-\-no\-window -Disable scrcpy window. Implies --no-video-playback and --no-control. +Disable scrcpy window. Implies --no-video-playback. .TP .BI "\-\-orientation " value diff --git a/app/src/cli.c b/app/src/cli.c index 756934ea..a2e6ab1a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -689,8 +689,7 @@ static const struct sc_option options[] = { { .longopt_id = OPT_NO_WINDOW, .longopt = "no-window", - .text = "Disable scrcpy window. Implies --no-video-playback and " - "--no-control.", + .text = "Disable scrcpy window. Implies --no-video-playback.", }, { .longopt_id = OPT_ORIENTATION, @@ -2761,9 +2760,10 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], #endif if (!opts->window) { - // Without window, there cannot be any video playback or control + // Without window, there cannot be any video playback opts->video_playback = false; - opts->control = false; + // Controls are still possible, allowing for options like + // --turn-screen-off } if (!opts->video) { From fd8bef68b794442d9f3bbf47d85632f9a16e504a Mon Sep 17 00:00:00 2001 From: "chengjian.scj" Date: Wed, 25 Dec 2024 15:30:37 +0800 Subject: [PATCH 225/278] Add --display-ime-policy option Add an option to select where the IME should be displayed. Possible values are "local", "fallback" and "hide". PR #5703 Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 5 ++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 13 ++++ app/src/cli.c | 51 +++++++++++++++ app/src/options.c | 1 + app/src/options.h | 8 +++ app/src/scrcpy.c | 1 + app/src/server.c | 19 ++++++ app/src/server.h | 1 + doc/virtual_display.md | 12 ++++ .../java/com/genymobile/scrcpy/CleanUp.java | 29 +++++++-- .../java/com/genymobile/scrcpy/Options.java | 22 +++++++ .../java/com/genymobile/scrcpy/Server.java | 12 +++- .../scrcpy/video/NewDisplayCapture.java | 6 ++ .../scrcpy/wrappers/WindowManager.java | 65 +++++++++++++++++++ 15 files changed, 239 insertions(+), 7 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 29130892..8d149f97 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -23,6 +23,7 @@ _scrcpy() { -d --select-usb --disable-screensaver --display-id= + --display-ime-policy= --display-orientation= -e --select-tcpip -f --fullscreen @@ -148,6 +149,10 @@ _scrcpy() { COMPREPLY=($(compgen -W '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return ;; + --display-ime-policy) + COMPREPLY=($(compgen -W 'local fallback hide' -- "$cur")) + return + ;; --record-orientation) COMPREPLY=($(compgen -W '0 90 180 270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0897b9cc..cccfcc6a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -30,6 +30,7 @@ arguments=( {-d,--select-usb}'[Use USB device]' '--disable-screensaver[Disable screensaver while scrcpy is running]' '--display-id=[Specify the display id to mirror]' + '--display-ime-policy[Set the policy for selecting where the IME should be displayed]' '--display-orientation=[Set the initial display orientation]:orientation values:(0 90 180 270 flip0 flip90 flip180 flip270)' {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 75bf6088..5eea94f4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -161,6 +161,19 @@ The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. +.TP +.BI "\-\-display\-ime\-policy " value +Set the policy for selecting where the IME should be displayed. + +Possible values are "local", "fallback" and "hide": + + - "local" means that the IME should appear on the local display. + - "fallback" means that the IME should appear on a fallback display (the default display). + - "hide" means that the IME should be hidden. + +By default, the IME policy is left unchanged. + + .TP .BI "\-\-display\-orientation " value Set the initial display orientation. diff --git a/app/src/cli.c b/app/src/cli.c index a2e6ab1a..b83fc9ec 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -113,6 +113,7 @@ enum { OPT_ANGLE, OPT_NO_VD_SYSTEM_DECORATIONS, OPT_NO_VD_DESTROY_CONTENT, + OPT_DISPLAY_IME_POLICY, }; struct sc_option { @@ -366,6 +367,19 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, + { + .longopt_id = OPT_DISPLAY_IME_POLICY, + .longopt = "display-ime-policy", + .argdesc = "value", + .text = "Set the policy for selecting where the IME should be " + "displayed.\n" + "Possible values are \"local\", \"fallback\" and \"hide\".\n" + "\"local\" means that the IME should appear on the local " + "display.\n" + "\"fallback\" means that the IME should appear on a fallback " + "display (the default display).\n" + "\"hide\" means that the IME should be hidden.", + }, { .longopt_id = OPT_DISPLAY_ORIENTATION, .longopt = "display-orientation", @@ -1614,6 +1628,25 @@ parse_audio_output_buffer(const char *s, sc_tick *tick) { return true; } +static bool +parse_display_ime_policy(const char *s, enum sc_display_ime_policy *policy) { + if (!strcmp(s, "local")) { + *policy = SC_DISPLAY_IME_POLICY_LOCAL; + return true; + } + if (!strcmp(s, "fallback")) { + *policy = SC_DISPLAY_IME_POLICY_FALLBACK; + return true; + } + if (!strcmp(s, "hide")) { + *policy = SC_DISPLAY_IME_POLICY_HIDE; + return true; + } + LOGE("Unsupported display IME policy: %s (expected local, fallback or " + "hide)", s); + return false; +} + static bool parse_orientation(const char *s, enum sc_orientation *orientation) { if (!strcmp(s, "0")) { @@ -2722,6 +2755,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], case OPT_NO_VD_SYSTEM_DECORATIONS: opts->vd_system_decorations = false; break; + case OPT_DISPLAY_IME_POLICY: + if (!parse_display_ime_policy(optarg, + &opts->display_ime_policy)) { + return false; + } + break; default: // getopt prints the error message on stderr return false; @@ -2978,6 +3017,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) { + LOGE("--display-ime-policy is only available with " + "--video-source=display"); + return false; + } + if (opts->camera_id && opts->camera_facing != SC_CAMERA_FACING_ANY) { LOGE("Cannot specify both --camera-id and --camera-facing"); return false; @@ -3019,6 +3064,12 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } + if (opts->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED + && opts->display_id == 0 && !opts->new_display) { + LOGE("--display-ime-policy is only supported on a secondary display"); + return false; + } + if (opts->audio && opts->audio_source == SC_AUDIO_SOURCE_AUTO) { // Select the audio source according to the video source if (opts->video_source == SC_VIDEO_SOURCE_DISPLAY) { diff --git a/app/src/options.c b/app/src/options.c index 044aa014..0fe82d29 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -56,6 +56,7 @@ const struct scrcpy_options scrcpy_options_default = { .capture_orientation_lock = SC_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, .record_orientation = SC_ORIENTATION_0, + .display_ime_policy = SC_DISPLAY_IME_POLICY_UNDEFINED, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, diff --git a/app/src/options.h b/app/src/options.h index c8425808..ef7542e3 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -89,6 +89,13 @@ enum sc_orientation_lock { SC_ORIENTATION_LOCKED_INITIAL, // lock to initial device orientation }; +enum sc_display_ime_policy { + SC_DISPLAY_IME_POLICY_UNDEFINED, + SC_DISPLAY_IME_POLICY_LOCAL, + SC_DISPLAY_IME_POLICY_FALLBACK, + SC_DISPLAY_IME_POLICY_HIDE, +}; + static inline bool sc_orientation_is_mirror(enum sc_orientation orientation) { assert(!(orientation & ~7)); @@ -251,6 +258,7 @@ struct scrcpy_options { enum sc_orientation_lock capture_orientation_lock; enum sc_orientation display_orientation; enum sc_orientation record_orientation; + enum sc_display_ime_policy display_ime_policy; int16_t window_x; // SC_WINDOW_POSITION_UNDEFINED for "auto" int16_t window_y; // SC_WINDOW_POSITION_UNDEFINED for "auto" uint16_t window_width; diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 641d93f7..b3ff9b36 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -436,6 +436,7 @@ scrcpy(struct scrcpy_options *options) { .control = options->control, .display_id = options->display_id, .new_display = options->new_display, + .display_ime_policy = options->display_ime_policy, .video = options->video, .audio = options->audio, .audio_dup = options->audio_dup, diff --git a/app/src/server.c b/app/src/server.c index cf181abc..6979c09b 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -155,6 +155,21 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { } } +static const char * +sc_server_get_display_ime_policy_name(enum sc_display_ime_policy policy) { + switch (policy) { + case SC_DISPLAY_IME_POLICY_LOCAL: + return "local"; + case SC_DISPLAY_IME_POLICY_FALLBACK: + return "fallback"; + case SC_DISPLAY_IME_POLICY_HIDE: + return "hide"; + default: + assert(!"unexpected display IME policy"); + return NULL; + } +} + static bool validate_string(const char *s) { // The parameters values are passed as command line arguments to adb, so @@ -376,6 +391,10 @@ execute_server(struct sc_server *server, VALIDATE_STRING(params->new_display); ADD_PARAM("new_display=%s", params->new_display); } + if (params->display_ime_policy != SC_DISPLAY_IME_POLICY_UNDEFINED) { + ADD_PARAM("display_ime_policy=%s", + sc_server_get_display_ime_policy_name(params->display_ime_policy)); + } if (!params->vd_destroy_content) { ADD_PARAM("vd_destroy_content=false"); } diff --git a/app/src/server.h b/app/src/server.h index a03689ff..5f4592de 100644 --- a/app/src/server.h +++ b/app/src/server.h @@ -50,6 +50,7 @@ struct sc_server_params { bool control; uint32_t display_id; const char *new_display; + enum sc_display_ime_policy display_ime_policy; bool video; bool audio; bool audio_dup; diff --git a/doc/virtual_display.md b/doc/virtual_display.md index 5d1673e8..f1645169 100644 --- a/doc/virtual_display.md +++ b/doc/virtual_display.md @@ -61,3 +61,15 @@ To move them to the main display instead, use: ``` scrcpy --new-display --no-vd-destroy-content ``` + + +## Display IME policy + +By default, the virtual display IME appears on the default display. + +To make it appear on the local display, use `--display-ime-policy=local`: + +```bash +scrcpy --display-id=1 --display-ime-policy=local +scrcpy --new-display --display-ime-policy=local +``` diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 49b23e81..51db985c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -4,6 +4,7 @@ import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.Settings; import com.genymobile.scrcpy.util.SettingsException; +import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.BatteryManager; import android.system.ErrnoException; @@ -97,18 +98,31 @@ public final class CleanUp { } } - boolean powerOffScreen = options.getPowerOffScreenOnClose(); int displayId = options.getDisplayId(); + int restoreDisplayImePolicy = -1; + if (displayId > 0) { + int displayImePolicy = options.getDisplayImePolicy(); + if (displayImePolicy != -1) { + int currentDisplayImePolicy = ServiceManager.getWindowManager().getDisplayImePolicy(displayId); + if (currentDisplayImePolicy != displayImePolicy) { + ServiceManager.getWindowManager().setDisplayImePolicy(displayId, displayImePolicy); + restoreDisplayImePolicy = currentDisplayImePolicy; + } + } + } + + boolean powerOffScreen = options.getPowerOffScreenOnClose(); + try { - run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout); + run(displayId, restoreStayOn, disableShowTouches, powerOffScreen, restoreScreenOffTimeout, restoreDisplayImePolicy); } catch (IOException e) { Ln.e("Clean up I/O exception", e); } } - private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout) - throws IOException { + private void run(int displayId, int restoreStayOn, boolean disableShowTouches, boolean powerOffScreen, int restoreScreenOffTimeout, + int restoreDisplayImePolicy) throws IOException { String[] cmd = { "app_process", "/", @@ -118,6 +132,7 @@ public final class CleanUp { String.valueOf(disableShowTouches), String.valueOf(powerOffScreen), String.valueOf(restoreScreenOffTimeout), + String.valueOf(restoreDisplayImePolicy), }; ProcessBuilder builder = new ProcessBuilder(cmd); @@ -178,6 +193,7 @@ public final class CleanUp { boolean disableShowTouches = Boolean.parseBoolean(args[2]); boolean powerOffScreen = Boolean.parseBoolean(args[3]); int restoreScreenOffTimeout = Integer.parseInt(args[4]); + int restoreDisplayImePolicy = Integer.parseInt(args[5]); // Dynamic option boolean restoreDisplayPower = false; @@ -223,6 +239,11 @@ public final class CleanUp { } } + if (restoreDisplayImePolicy != -1) { + Ln.i("Restoring \"display IME policy\""); + ServiceManager.getWindowManager().setDisplayImePolicy(displayId, restoreDisplayImePolicy); + } + // Change the power of the main display when mirroring a virtual display int targetDisplayId = displayId != Device.DISPLAY_ID_NONE ? displayId : 0; if (Device.isScreenOn(targetDisplayId)) { diff --git a/server/src/main/java/com/genymobile/scrcpy/Options.java b/server/src/main/java/com/genymobile/scrcpy/Options.java index 8a438750..66bb68e8 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Options.java +++ b/server/src/main/java/com/genymobile/scrcpy/Options.java @@ -12,6 +12,7 @@ import com.genymobile.scrcpy.video.CameraAspectRatio; import com.genymobile.scrcpy.video.CameraFacing; import com.genymobile.scrcpy.video.VideoCodec; import com.genymobile.scrcpy.video.VideoSource; +import com.genymobile.scrcpy.wrappers.WindowManager; import android.graphics.Rect; import android.util.Pair; @@ -48,6 +49,7 @@ public class Options { private boolean showTouches; private boolean stayAwake; private int screenOffTimeout = -1; + private int displayImePolicy = -1; private List videoCodecOptions; private List audioCodecOptions; @@ -186,6 +188,10 @@ public class Options { return screenOffTimeout; } + public int getDisplayImePolicy() { + return displayImePolicy; + } + public List getVideoCodecOptions() { return videoCodecOptions; } @@ -482,6 +488,9 @@ public class Options { options.captureOrientationLock = pair.first; options.captureOrientation = pair.second; break; + case "display_ime_policy": + options.displayImePolicy = parseDisplayImePolicy(value); + break; case "send_device_meta": options.sendDeviceMeta = Boolean.parseBoolean(value); break; @@ -626,4 +635,17 @@ public class Options { return Pair.create(lock, Orientation.getByName(value)); } + + private static int parseDisplayImePolicy(String value) { + switch (value) { + case "local": + return WindowManager.DISPLAY_IME_POLICY_LOCAL; + case "fallback": + return WindowManager.DISPLAY_IME_POLICY_FALLBACK_DISPLAY; + case "hide": + return WindowManager.DISPLAY_IME_POLICY_HIDE; + default: + throw new IllegalArgumentException("Invalid display IME policy: " + value); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index eb8b533a..09cfd6cf 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -80,9 +80,15 @@ public final class Server { throw new ConfigurationException("Camera mirroring is not supported"); } - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10 && options.getNewDisplay() != null) { - Ln.e("New virtual display is not supported before Android 10"); - throw new ConfigurationException("New virtual display is not supported"); + if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { + if (options.getNewDisplay() != null) { + Ln.e("New virtual display is not supported before Android 10"); + throw new ConfigurationException("New virtual display is not supported"); + } + if (options.getDisplayImePolicy() != -1) { + Ln.e("Display IME policy is not supported before Android 10"); + throw new ConfigurationException("Display IME policy is not supported"); + } } CleanUp cleanUp = null; diff --git a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java index 033d6b9a..792b3a8a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/NewDisplayCapture.java @@ -49,6 +49,7 @@ public class NewDisplayCapture extends SurfaceCapture { private Size mainDisplaySize; private int mainDisplayDpi; private int maxSize; + private int displayImePolicy; private final Rect crop; private final boolean captureOrientationLocked; private final Orientation captureOrientation; @@ -68,6 +69,7 @@ public class NewDisplayCapture extends SurfaceCapture { this.newDisplay = options.getNewDisplay(); assert newDisplay != null; this.maxSize = options.getMaxSize(); + this.displayImePolicy = options.getDisplayImePolicy(); this.crop = options.getCrop(); assert options.getCaptureOrientationLock() != null; this.captureOrientationLocked = options.getCaptureOrientationLock() != Orientation.Lock.Unlocked; @@ -191,6 +193,10 @@ public class NewDisplayCapture extends SurfaceCapture { virtualDisplayId = virtualDisplay.getDisplay().getDisplayId(); Ln.i("New display: " + displaySize.getWidth() + "x" + displaySize.getHeight() + "/" + dpi + " (id=" + virtualDisplayId + ")"); + if (displayImePolicy != -1) { + ServiceManager.getWindowManager().setDisplayImePolicy(virtualDisplayId, displayImePolicy); + } + displaySizeMonitor.start(virtualDisplayId, this::invalidate); } catch (Exception e) { Ln.e("Could not create display", e); diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 04f5abd7..08bab1a9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -4,12 +4,19 @@ import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import android.annotation.TargetApi; +import android.os.Build; import android.os.IInterface; import android.view.IDisplayWindowListener; import java.lang.reflect.Method; public final class WindowManager { + + // + public static final int DISPLAY_IME_POLICY_LOCAL = 0; + public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1; + public static final int DISPLAY_IME_POLICY_HIDE = 2; + private final IInterface manager; private Method getRotationMethod; @@ -22,6 +29,9 @@ public final class WindowManager { private Method thawDisplayRotationMethod; private int thawDisplayRotationMethodVersion; + private Method getDisplayImePolicyMethod; + private Method setDisplayImePolicyMethod; + static WindowManager create() { IInterface manager = ServiceManager.getService("window", "android.view.IWindowManager"); return new WindowManager(manager); @@ -198,4 +208,59 @@ public final class WindowManager { Ln.e("Could not unregister display window listener", e); } } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private Method getGetDisplayImePolicyMethod() throws NoSuchMethodException { + if (getDisplayImePolicyMethod == null) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + getDisplayImePolicyMethod = manager.getClass().getMethod("getDisplayImePolicy", int.class); + } else { + getDisplayImePolicyMethod = manager.getClass().getMethod("shouldShowIme", int.class); + } + } + return getDisplayImePolicyMethod; + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public int getDisplayImePolicy(int displayId) { + try { + Method method = getGetDisplayImePolicyMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + return (int) method.invoke(manager, displayId); + } + boolean shouldShowIme = (boolean) method.invoke(manager, displayId); + return shouldShowIme ? DISPLAY_IME_POLICY_LOCAL : DISPLAY_IME_POLICY_FALLBACK_DISPLAY; + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + return -1; + } + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + private Method getSetDisplayImePolicyMethod() throws NoSuchMethodException { + if (setDisplayImePolicyMethod == null) { + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + setDisplayImePolicyMethod = manager.getClass().getMethod("setDisplayImePolicy", int.class, int.class); + } else { + setDisplayImePolicyMethod = manager.getClass().getMethod("setShouldShowIme", int.class, boolean.class); + } + } + return setDisplayImePolicyMethod; + } + + @TargetApi(AndroidVersions.API_29_ANDROID_10) + public void setDisplayImePolicy(int displayId, int displayImePolicy) { + try { + Method method = getSetDisplayImePolicyMethod(); + if (Build.VERSION.SDK_INT >= AndroidVersions.API_31_ANDROID_12) { + method.invoke(manager, displayId, displayImePolicy); + } else if (displayImePolicy != DISPLAY_IME_POLICY_HIDE) { + method.invoke(manager, displayId, displayImePolicy == DISPLAY_IME_POLICY_LOCAL); + } else { + Ln.w("DISPLAY_IME_POLICY_HIDE is not supported before Android 12"); + } + } catch (ReflectiveOperationException e) { + Ln.e("Could not invoke method", e); + } + } } From d892a9aac58bd35b778ff8a0a933d11ec8092df1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 12:22:45 +0100 Subject: [PATCH 226/278] Disable checkstyle line length warning Checkstyle reports a warning because the line containing a long URL is more than 150 characters. But we can't split the URL, so disable the warning. --- .../main/java/com/genymobile/scrcpy/wrappers/WindowManager.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java index 08bab1a9..7ba5cc06 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -12,6 +12,7 @@ import java.lang.reflect.Method; public final class WindowManager { + @SuppressWarnings("checkstyle:LineLength") // public static final int DISPLAY_IME_POLICY_LOCAL = 0; public static final int DISPLAY_IME_POLICY_FALLBACK_DISPLAY = 1; From c63d9e1803c658b29b067d5ea68baa38df7e1359 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 7 Mar 2025 18:40:28 +0100 Subject: [PATCH 227/278] Work around broken display listener on Android 15 A recent Android 15 upgrade broke the display listener (again). Use the alternative method for Android >= 14. Fixes #5908 --- .../java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java index ff863aa8..3d7cccfe 100644 --- a/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java +++ b/server/src/main/java/com/genymobile/scrcpy/video/DisplaySizeMonitor.java @@ -23,7 +23,9 @@ public class DisplaySizeMonitor { // On Android 14, DisplayListener may be broken (it never sends events). This is fixed in recent Android 14 upgrades, but we can't really // detect it directly, so register a DisplayWindowListener (introduced in Android 11) to listen to configuration changes instead. - private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT != AndroidVersions.API_34_ANDROID_14; + // It has been broken again after an Android 15 upgrade: + // So use the default method only before Android 14. + private static final boolean USE_DEFAULT_METHOD = Build.VERSION.SDK_INT < AndroidVersions.API_34_ANDROID_14; private DisplayManager.DisplayListenerHandle displayListenerHandle; private HandlerThread handlerThread; From 7044122fc59cad404722215f375a711667bc4fa0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Mar 2025 21:10:21 +0100 Subject: [PATCH 228/278] Simplify wording in README --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index b5884350..404359f2 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ their name contains `scrcpy`.** _pronounced "**scr**een **c**o**py**"_ -This application mirrors Android devices (video and audio) connected via -USB or [over TCP/IP](doc/connection.md#tcpip-wireless), and allows to control the -device with the keyboard and the mouse of the computer. It does not require any -_root_ access. It works on _Linux_, _Windows_ and _macOS_. +This application mirrors Android devices (video and audio) connected via USB or +[TCP/IP](doc/connection.md#tcpip-wireless) and allows control using the +computer's keyboard and mouse. It does not require _root_ access. It works on +_Linux_, _Windows_, and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) From 7998811fa55a4f1610dd07680a387251ead2dd1f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 9 Mar 2025 21:15:58 +0100 Subject: [PATCH 229/278] Mention that no Android app is required --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 404359f2..16b8bca1 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,8 @@ _pronounced "**scr**een **c**o**py**"_ This application mirrors Android devices (video and audio) connected via USB or [TCP/IP](doc/connection.md#tcpip-wireless) and allows control using the -computer's keyboard and mouse. It does not require _root_ access. It works on -_Linux_, _Windows_, and _macOS_. +computer's keyboard and mouse. It does not require _root_ access or an app +installed on the device. It works on _Linux_, _Windows_, and _macOS_. ![screenshot](assets/screenshot-debian-600.jpg) From 457c7fe5cfa5165c02d4263cd7d2a41598990d0e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 11:09:16 +0100 Subject: [PATCH 230/278] Disable audio regulator underflow logs Only enable them if SC_AUDIO_REGULATOR_DEBUG is set, as they may spam the output. PR #5870 --- app/src/audio_regulator.c | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index f7e9b81e..f11ed0e7 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -76,8 +76,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, // Wait until the buffer is filled up to at least target_buffering // before playing if (buffered_samples < ar->target_buffering) { - LOGV("[Audio] Inserting initial buffering silence: %" PRIu32 +#ifdef SC_AUDIO_REGULATOR_DEBUG + LOGD("[Audio] Inserting initial buffering silence: %" PRIu32 " samples", out_samples); +#endif // Delay playback starting to reach the target buffering. Fill the // whole buffer with silence (len is small compared to the // arbitrary margin value). @@ -98,8 +100,10 @@ sc_audio_regulator_pull(struct sc_audio_regulator *ar, uint8_t *out, // dropped to keep the latency minimal. However, this would cause very // audible glitches, so let the clock compensation restore the target // latency. +#ifdef SC_AUDIO_REGULATOR_DEBUG LOGD("[Audio] Buffer underflow, inserting silence: %" PRIu32 " samples", silence); +#endif memset(out + TO_BYTES(read), 0, TO_BYTES(silence)); bool received = atomic_load_explicit(&ar->received, From 1d253381198495ae351e2fba377a0b325f4b8bfb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 11:20:35 +0100 Subject: [PATCH 231/278] Report underflow samples in verbose mode Report the number of silence samples inserted due to underflow every second, along with the other metrics. PR #5870 --- app/src/audio_regulator.c | 6 +++++- app/src/audio_regulator.h | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index f11ed0e7..66900b51 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -213,6 +213,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { if (played) { underflow = atomic_exchange_explicit(&ar->underflow, 0, memory_order_relaxed); + ar->underflow_report += underflow; max_buffered_samples = ar->target_buffering * 11 / 10 + 60 * ar->sample_rate / 1000 /* 60 ms */; @@ -315,7 +316,9 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { int abs_max_diff = distance / 50; diff = CLAMP(diff, -abs_max_diff, abs_max_diff); LOGV("[Audio] Buffering: target=%" PRIu32 " avg=%f cur=%" PRIu32 - " compensation=%d", ar->target_buffering, avg, can_read, diff); + " compensation=%d (underflow=%" PRIu32 ")", + ar->target_buffering, avg, can_read, diff, ar->underflow_report); + ar->underflow_report = 0; int ret = swr_set_compensation(swr_ctx, diff, distance); if (ret < 0) { @@ -398,6 +401,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, atomic_init(&ar->played, false); atomic_init(&ar->received, false); atomic_init(&ar->underflow, 0); + ar->underflow_report = 0; ar->compensation_active = false; return true; diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 03cf6325..79238fbe 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -46,6 +46,9 @@ struct sc_audio_regulator { // Number of silence samples inserted since the last received packet atomic_uint_least32_t underflow; + // Number of silence samples inserted since the last log + uint32_t underflow_report; + // Non-zero compensation applied (only used by the receiver thread) bool compensation_active; From 245981281e99662aa77e53259a0864ead39ff438 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 2 Mar 2025 17:09:43 +0100 Subject: [PATCH 232/278] Fix PTS produced by the default opus/flac encoders The default OPUS and FLAC encoders on Android rewrite the input PTS so that they exactly match the number of samples. As a consequence: - audio clock drift is not compensated - implicit silences (without packets) are ignored To work around this behavior, generate new PTS based on the current time (after encoding) and the packet duration. PR #5870 --- .../genymobile/scrcpy/audio/AudioEncoder.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java index 267be60a..33177228 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioEncoder.java @@ -55,6 +55,9 @@ public final class AudioEncoder implements AsyncProcessor { private final List codecOptions; private final String encoderName; + private boolean recreatePts; + private long previousPts; + // Capacity of 64 is in practice "infinite" (it is limited by the number of available MediaCodec buffers, typically 4). // So many pending tasks would lead to an unacceptable delay anyway. private final BlockingQueue inputTasks = new ArrayBlockingQueue<>(64); @@ -118,6 +121,9 @@ public final class AudioEncoder implements AsyncProcessor { OutputTask task = outputTasks.take(); ByteBuffer buffer = mediaCodec.getOutputBuffer(task.index); try { + if (recreatePts) { + fixTimestamp(task.bufferInfo); + } streamer.writePacket(buffer, task.bufferInfo); } finally { mediaCodec.releaseOutputBuffer(task.index, false); @@ -125,6 +131,25 @@ public final class AudioEncoder implements AsyncProcessor { } } + private void fixTimestamp(MediaCodec.BufferInfo bufferInfo) { + assert recreatePts; + + if ((bufferInfo.flags & MediaCodec.BUFFER_FLAG_CODEC_CONFIG) != 0) { + // Config packet, nothing to fix + return; + } + + long pts = bufferInfo.presentationTimeUs; + if (previousPts != 0) { + long now = System.nanoTime() / 1000; + // This specific encoder produces PTS matching the exact number of samples + long duration = pts - previousPts; + bufferInfo.presentationTimeUs = now - duration; + } + + previousPts = pts; + } + @Override public void start(TerminationListener listener) { thread = new Thread(() -> { @@ -194,6 +219,12 @@ public final class AudioEncoder implements AsyncProcessor { Codec codec = streamer.getCodec(); mediaCodec = createMediaCodec(codec, encoderName); + // The default OPUS and FLAC encoders overwrite the input PTS with a value that matches the number of samples. This is not the behavior + // we want: it ignores any audio clock drift and hard silences (packets not produced on silence). To work around this behavior, + // regenerate PTS based on the current time and the packet duration. + String codecName = mediaCodec.getCanonicalName(); + recreatePts = "c2.android.opus.encoder".equals(codecName) || "c2.android.flac.encoder".equals(codecName); + mediaCodecThread = new HandlerThread("media-codec"); mediaCodecThread.start(); From 3a0703f428fe024c0bb226c0e311966be22ef57c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 16 Feb 2025 17:38:27 +0100 Subject: [PATCH 233/278] Handle audio stream discontinuities The audio regulator assumed a continuous audio stream. But some audio sources (like the "voice call" audio source) do not produce any packets on silence, breaking this assumption. Use PTS to detect such discontinuities. PR #5870 --- app/src/audio_regulator.c | 33 ++++++++++++++++++++++++++++++++- app/src/audio_regulator.h | 3 +++ app/src/util/audiobuf.c | 35 +++++++++++++++++++++++++++++++++++ app/src/util/audiobuf.h | 3 +++ app/tests/test_audiobuf.c | 8 ++++++++ 5 files changed, 81 insertions(+), 1 deletion(-) diff --git a/app/src/audio_regulator.c b/app/src/audio_regulator.c index 66900b51..16fdd08b 100644 --- a/app/src/audio_regulator.c +++ b/app/src/audio_regulator.c @@ -141,6 +141,36 @@ bool sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { SwrContext *swr_ctx = ar->swr_ctx; + uint32_t input_samples = frame->nb_samples; + + assert(frame->pts >= 0); + int64_t pts = frame->pts; + if (ar->next_expected_pts && pts - ar->next_expected_pts > 100000) { + LOGV("[Audio] Discontinuity detected: %" PRIi64 "µs", + pts - ar->next_expected_pts); + // More than 100ms: consider it as a discontinuity + // (typically because silence packets were not captured) + uint32_t can_read = sc_audiobuf_can_read(&ar->buf); + if (input_samples + can_read < ar->target_buffering) { + // Adjust buffering to the target value directly + uint32_t silence = ar->target_buffering - can_read - input_samples; + sc_audiobuf_write_silence(&ar->buf, silence); + } + + // Reset state + ar->avg_buffering.avg = ar->target_buffering; + int ret = swr_set_compensation(swr_ctx, 0, 0); + (void) ret; + assert(!ret); // disabling compensation should never fail + ar->compensation_active = false; + ar->samples_since_resync = 0; + atomic_store_explicit(&ar->underflow, 0, memory_order_relaxed); + } + + int64_t packet_duration = input_samples * INT64_C(1000000) + / ar->sample_rate; + ar->next_expected_pts = pts + packet_duration; + int64_t swr_delay = swr_get_delay(swr_ctx, ar->sample_rate); // No need to av_rescale_rnd(), input and output sample rates are the same. // Add more space (256) for clock compensation. @@ -260,7 +290,7 @@ sc_audio_regulator_push(struct sc_audio_regulator *ar, const AVFrame *frame) { } // Number of samples added (or removed, if negative) for compensation - int32_t instant_compensation = (int32_t) written - frame->nb_samples; + int32_t instant_compensation = (int32_t) written - input_samples; // Inserting silence instantly increases buffering int32_t inserted_silence = (int32_t) underflow; // Dropping input samples instantly decreases buffering @@ -403,6 +433,7 @@ sc_audio_regulator_init(struct sc_audio_regulator *ar, size_t sample_size, atomic_init(&ar->underflow, 0); ar->underflow_report = 0; ar->compensation_active = false; + ar->next_expected_pts = 0; return true; diff --git a/app/src/audio_regulator.h b/app/src/audio_regulator.h index 79238fbe..4e18fe08 100644 --- a/app/src/audio_regulator.h +++ b/app/src/audio_regulator.h @@ -57,6 +57,9 @@ struct sc_audio_regulator { // Set to true the first time samples are pulled by the player atomic_bool played; + + // PTS of the next expected packet (useful to detect discontinuities) + int64_t next_expected_pts; }; bool diff --git a/app/src/util/audiobuf.c b/app/src/util/audiobuf.c index 3cc5cad1..eeb27514 100644 --- a/app/src/util/audiobuf.c +++ b/app/src/util/audiobuf.c @@ -116,3 +116,38 @@ sc_audiobuf_write(struct sc_audiobuf *buf, const void *from_, return samples_count; } + +uint32_t +sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples_count) { + // Only the writer thread can write head, so memory_order_relaxed is + // sufficient + uint32_t head = atomic_load_explicit(&buf->head, memory_order_relaxed); + + // The tail cursor is updated after the data is consumed by the reader + uint32_t tail = atomic_load_explicit(&buf->tail, memory_order_acquire); + + uint32_t can_write = (buf->alloc_size + tail - head - 1) % buf->alloc_size; + if (!can_write) { + return 0; + } + if (samples_count > can_write) { + samples_count = can_write; + } + + uint32_t right_count = buf->alloc_size - head; + if (right_count > samples_count) { + right_count = samples_count; + } + memset(buf->data + (head * buf->sample_size), 0, + right_count * buf->sample_size); + + if (samples_count > right_count) { + uint32_t left_count = samples_count - right_count; + memset(buf->data, 0, left_count * buf->sample_size); + } + + uint32_t new_head = (head + samples_count) % buf->alloc_size; + atomic_store_explicit(&buf->head, new_head, memory_order_release); + + return samples_count; +} diff --git a/app/src/util/audiobuf.h b/app/src/util/audiobuf.h index 5cc51932..b55a5a59 100644 --- a/app/src/util/audiobuf.h +++ b/app/src/util/audiobuf.h @@ -50,6 +50,9 @@ uint32_t sc_audiobuf_write(struct sc_audiobuf *buf, const void *from, uint32_t samples_count); +uint32_t +sc_audiobuf_write_silence(struct sc_audiobuf *buf, uint32_t samples); + static inline uint32_t sc_audiobuf_capacity(struct sc_audiobuf *buf) { assert(buf->alloc_size); diff --git a/app/tests/test_audiobuf.c b/app/tests/test_audiobuf.c index 94d0f07a..539ee238 100644 --- a/app/tests/test_audiobuf.c +++ b/app/tests/test_audiobuf.c @@ -113,6 +113,14 @@ static void test_audiobuf_partial_read_write(void) { uint32_t expected2[] = {4, 5, 6, 1, 2, 3, 4, 1, 2, 3}; assert(!memcmp(data, expected2, 12)); + w = sc_audiobuf_write_silence(&buf, 4); + assert(w == 4); + + r = sc_audiobuf_read(&buf, data, 4); + assert(r == 4); + uint32_t expected3[] = {0, 0, 0, 0}; + assert(!memcmp(data, expected3, 4)); + sc_audiobuf_destroy(&buf); } From 609719bde025559067e9d11672178531f8431619 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 11:40:24 +0100 Subject: [PATCH 234/278] Refactor audio sources Store the target audio source integer (one of the constants from android.media.MediaRecorder.AudioSource) in the AudioSource enum (or -1 if not relevant). This will simplify adding new audio sources. PR #5870 --- .../scrcpy/audio/AudioDirectCapture.java | 14 +------------- .../com/genymobile/scrcpy/audio/AudioSource.java | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java index 5c859738..bf870bee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioDirectCapture.java @@ -12,7 +12,6 @@ import android.content.ComponentName; import android.content.Intent; import android.media.AudioRecord; import android.media.MediaCodec; -import android.media.MediaRecorder; import android.os.Build; import android.os.SystemClock; @@ -32,18 +31,7 @@ public class AudioDirectCapture implements AudioCapture { private AudioRecordReader reader; public AudioDirectCapture(AudioSource audioSource) { - this.audioSource = getAudioSourceValue(audioSource); - } - - private static int getAudioSourceValue(AudioSource audioSource) { - switch (audioSource) { - case OUTPUT: - return MediaRecorder.AudioSource.REMOTE_SUBMIX; - case MIC: - return MediaRecorder.AudioSource.MIC; - default: - throw new IllegalArgumentException("Unsupported audio source: " + audioSource); - } + this.audioSource = audioSource.getDirectAudioSource(); } @TargetApi(AndroidVersions.API_23_ANDROID_6_0) diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 6082f20e..353f2281 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,20 +1,28 @@ package com.genymobile.scrcpy.audio; +import android.media.MediaRecorder; + public enum AudioSource { - OUTPUT("output"), - MIC("mic"), - PLAYBACK("playback"); + OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), + MIC("mic", MediaRecorder.AudioSource.MIC), + PLAYBACK("playback", -1); private final String name; + private final int directAudioSource; - AudioSource(String name) { + AudioSource(String name, int directAudioSource) { this.name = name; + this.directAudioSource = directAudioSource; } public boolean isDirect() { return this != PLAYBACK; } + public int getDirectAudioSource() { + return directAudioSource; + } + public static AudioSource findByName(String name) { for (AudioSource audioSource : AudioSource.values()) { if (name.equals(audioSource.name)) { From bef2d8473b3426b1dd2ea9ed0cae3a243a999218 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 22 Feb 2025 12:18:05 +0100 Subject: [PATCH 235/278] Add more audio sources Expose more audio sources from MediaRecorder.AudioSource. Refs Fixes #5412 Fixes #5670 PR #5870 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 18 +++-- app/src/cli.c | 76 +++++++++++++++++-- app/src/options.h | 8 ++ app/src/server.c | 16 ++++ doc/audio.md | 14 ++++ .../genymobile/scrcpy/audio/AudioSource.java | 12 ++- 8 files changed, 131 insertions(+), 17 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 8d149f97..9918918c 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -122,7 +122,7 @@ _scrcpy() { return ;; --audio-source) - COMPREPLY=($(compgen -W 'output mic playback' -- "$cur")) + COMPREPLY=($(compgen -W 'output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance' -- "$cur")) return ;; --camera-facing) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index cccfcc6a..450fc8f5 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -16,7 +16,7 @@ arguments=( '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-dup=[Duplicate audio]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' - '--audio-source=[Select the audio source]:source:(output mic playback)' + '--audio-source=[Select the audio source]:source:(output playback mic mic-unprocessed mic-camcorder mic-voice-recognition mic-voice-communication voice-call voice-call-uplink voice-call-downlink voice-performance)' '--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' '--camera-ar=[Select the camera size by its aspect ratio]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 5eea94f4..ffb66ab9 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -67,13 +67,19 @@ The available encoders can be listed by \fB\-\-list\-encoders\fR. .TP .BI "\-\-audio\-source " source -Select the audio source (output, mic or playback). +Select the audio source. Possible values are: -The "output" source forwards the whole audio output, and disables playback on the device. - -The "playback" source captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). - -The "mic" source captures the microphone. + - "output": forwards the whole audio output, and disables playback on the device. + - "playback": captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - "mic": captures the microphone. + - "mic-unprocessed": captures the microphone unprocessed (raw) sound. + - "mic-camcorder": captures the microphone tuned for video recording, with the same orientation as the camera if available. + - "mic-voice-recognition": captures the microphone tuned for voice recognition. + - "mic-voice-communication": captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available). + - "voice-call": captures voice call. + - "voice-call-uplink": captures voice call uplink only. + - "voice-call-downlink": captures voice call downlink only. + - "voice-performance": captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback. Default is output. diff --git a/app/src/cli.c b/app/src/cli.c index b83fc9ec..b2e3e30a 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -217,13 +217,31 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_SOURCE, .longopt = "audio-source", .argdesc = "source", - .text = "Select the audio source (output, mic or playback).\n" - "The \"output\" source forwards the whole audio output, and " - "disables playback on the device.\n" - "The \"playback\" source captures the audio playback (Android " - "apps can opt-out, so the whole output is not necessarily " + .text = "Select the audio source. Possible values are:\n" + " - \"output\": forwards the whole audio output, and disables " + "playback on the device.\n" + " - \"playback\": captures the audio playback (Android apps " + "can opt-out, so the whole output is not necessarily " "captured).\n" - "The \"mic\" source captures the microphone.\n" + " - \"mic\": captures the microphone.\n" + " - \"mic-unprocessed\": captures the microphone unprocessed " + "(raw) sound.\n" + " - \"mic-camcorder\": captures the microphone tuned for video " + "recording, with the same orientation as the camera if " + "available.\n" + " - \"mic-voice-recognition\": captures the microphone tuned " + "for voice recognition.\n" + " - \"mic-voice-communication\": captures the microphone tuned " + "for voice communications (it will for instance take advantage " + "of echo cancellation or automatic gain control if " + "available).\n" + " - \"voice-call\": captures voice call.\n" + " - \"voice-call-uplink\": captures voice call uplink only.\n" + " - \"voice-call-downlink\": captures voice call downlink " + "only.\n" + " - \"voice-performance\": captures audio meant to be " + "processed for live performance (karaoke), includes both the " + "microphone and the device playback.\n" "Default is output.", }, { @@ -2036,8 +2054,50 @@ parse_audio_source(const char *optarg, enum sc_audio_source *source) { return true; } - LOGE("Unsupported audio source: %s (expected output, mic or playback)", - optarg); + if (!strcmp(optarg, "mic-unprocessed")) { + *source = SC_AUDIO_SOURCE_MIC_UNPROCESSED; + return true; + } + + if (!strcmp(optarg, "mic-camcorder")) { + *source = SC_AUDIO_SOURCE_MIC_CAMCORDER; + return true; + } + + if (!strcmp(optarg, "mic-voice-recognition")) { + *source = SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION; + return true; + } + + if (!strcmp(optarg, "mic-voice-communication")) { + *source = SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION; + return true; + } + + if (!strcmp(optarg, "voice-call")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL; + return true; + } + + if (!strcmp(optarg, "voice-call-uplink")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL_UPLINK; + return true; + } + + if (!strcmp(optarg, "voice-call-downlink")) { + *source = SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK; + return true; + } + + if (!strcmp(optarg, "voice-performance")) { + *source = SC_AUDIO_SOURCE_VOICE_PERFORMANCE; + return true; + } + + LOGE("Unsupported audio source: %s (expected output, mic, playback, " + "mic-unprocessed, mic-camcorder, mic-voice-recognition, " + "mic-voice-communication, voice-call, voice-call-uplink, " + "voice-call-downlink, voice-performance)", optarg); return false; } diff --git a/app/src/options.h b/app/src/options.h index ef7542e3..03b42913 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -59,6 +59,14 @@ enum sc_audio_source { SC_AUDIO_SOURCE_OUTPUT, SC_AUDIO_SOURCE_MIC, SC_AUDIO_SOURCE_PLAYBACK, + SC_AUDIO_SOURCE_MIC_UNPROCESSED, + SC_AUDIO_SOURCE_MIC_CAMCORDER, + SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION, + SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION, + SC_AUDIO_SOURCE_VOICE_CALL, + SC_AUDIO_SOURCE_VOICE_CALL_UPLINK, + SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK, + SC_AUDIO_SOURCE_VOICE_PERFORMANCE, }; enum sc_camera_facing { diff --git a/app/src/server.c b/app/src/server.c index 6979c09b..153219c3 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -149,6 +149,22 @@ sc_server_get_audio_source_name(enum sc_audio_source audio_source) { return "mic"; case SC_AUDIO_SOURCE_PLAYBACK: return "playback"; + case SC_AUDIO_SOURCE_MIC_UNPROCESSED: + return "mic-unprocessed"; + case SC_AUDIO_SOURCE_MIC_CAMCORDER: + return "mic-camcorder"; + case SC_AUDIO_SOURCE_MIC_VOICE_RECOGNITION: + return "mic-voice-recognition"; + case SC_AUDIO_SOURCE_MIC_VOICE_COMMUNICATION: + return "mic-voice-communication"; + case SC_AUDIO_SOURCE_VOICE_CALL: + return "voice-call"; + case SC_AUDIO_SOURCE_VOICE_CALL_UPLINK: + return "voice-call-uplink"; + case SC_AUDIO_SOURCE_VOICE_CALL_DOWNLINK: + return "voice-call-downlink"; + case SC_AUDIO_SOURCE_VOICE_PERFORMANCE: + return "voice-performance"; default: assert(!"unexpected audio source"); return NULL; diff --git a/doc/audio.md b/doc/audio.md index 85f76ac5..142626f5 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -66,6 +66,20 @@ the computer: scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ``` +Many sources are available: + + - `output` (default): forwards the whole audio output, and disables playback on the device (mapped to [`REMOTE_SUBMIX`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#REMOTE_SUBMIX)). + - `playback`: captures the audio playback (Android apps can opt-out, so the whole output is not necessarily captured). + - `mic`: captures the microphone (mapped to [`MIC`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#MIC)). + - `mic-unprocessed`: captures the microphone unprocessed (raw) sound (mapped to [`UNPROCESSED`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#UNPROCESSED)). + - `mic-camcorder`: captures the microphone tuned for video recording, with the same orientation as the camera if available (mapped to [`CAMCORDER`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#CAMCORDER)). + - `mic-voice-recognition`: captures the microphone tuned for voice recognition (mapped to [`VOICE_RECOGNITION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_RECOGNITION)). + - `mic-voice-communication`: captures the microphone tuned for voice communications (it will for instance take advantage of echo cancellation or automatic gain control if available) (mapped to [`VOICE_COMMUNICATION`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_COMMUNICATION)). + - `voice-call`: captures voice call (mapped to [`VOICE_CALL`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_CALL)). + - `voice-call-uplink`: captures voice call uplink only (mapped to [`VOICE_UPLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_UPLINK)). + - `voice-call-downlink`: captures voice call downlink only (mapped to [`VOICE_DOWNLINK`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_DOWNLINK)). + - `voice-performance`: captures audio meant to be processed for live performance (karaoke), includes both the microphone and the device playback (mapped to [`VOICE_PERFORMANCE`](https://developer.android.com/reference/android/media/MediaRecorder.AudioSource#VOICE_PERFORMANCE)). + ### Duplication An alternative device audio capture method is also available (only for Android diff --git a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java index 353f2281..d16b5e38 100644 --- a/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java +++ b/server/src/main/java/com/genymobile/scrcpy/audio/AudioSource.java @@ -1,11 +1,21 @@ package com.genymobile.scrcpy.audio; +import android.annotation.SuppressLint; import android.media.MediaRecorder; +@SuppressLint("InlinedApi") public enum AudioSource { OUTPUT("output", MediaRecorder.AudioSource.REMOTE_SUBMIX), MIC("mic", MediaRecorder.AudioSource.MIC), - PLAYBACK("playback", -1); + PLAYBACK("playback", -1), + MIC_UNPROCESSED("mic-unprocessed", MediaRecorder.AudioSource.UNPROCESSED), + MIC_CAMCORDER("mic-camcorder", MediaRecorder.AudioSource.CAMCORDER), + MIC_VOICE_RECOGNITION("mic-voice-recognition", MediaRecorder.AudioSource.VOICE_RECOGNITION), + MIC_VOICE_COMMUNICATION("mic-voice-communication", MediaRecorder.AudioSource.VOICE_COMMUNICATION), + VOICE_CALL("voice-call", MediaRecorder.AudioSource.VOICE_CALL), + VOICE_CALL_UPLINK("voice-call-uplink", MediaRecorder.AudioSource.VOICE_UPLINK), + VOICE_CALL_DOWNLINK("voice-call-downlink", MediaRecorder.AudioSource.VOICE_DOWNLINK), + VOICE_PERFORMANCE("voice-performance", MediaRecorder.AudioSource.VOICE_PERFORMANCE); private final String name; private final int directAudioSource; From dd1bfae4e00e5756e5f4f43649ce9d6a824cf028 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 15:02:38 +0100 Subject: [PATCH 236/278] Upgrade libusb (1.0.28) --- app/deps/libusb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 340b0f70..4be03eb1 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=1.0.27 +VERSION=1.0.28 FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=e8f18a7a36ecbb11fb820bd71540350d8f61bcd9db0d2e8c18a6fb80b214a3de +SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe cd "$SOURCES_DIR" From b7add421544039c4fd5ba97243578f0fbcc908a7 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 15:03:42 +0100 Subject: [PATCH 237/278] Upgrade SDL (2.32.2) Also apply this additional patch to fix the build: --- ...that-the-correct-struct-is-used-for-.patch | 33 +++++++++++++++++++ app/deps/sdl.sh | 5 +-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch diff --git a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch b/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch new file mode 100644 index 00000000..cbb516ec --- /dev/null +++ b/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch @@ -0,0 +1,33 @@ +From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001 +From: Neal Gompa +Date: Mon, 10 Feb 2025 05:00:56 -0500 +Subject: [PATCH] pipewire: Ensure that the correct struct is used for + enumeration APIs + +PipeWire now requires the correct struct type is used, otherwise +it will fail to compile. + +Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f + +Fixes: https://github.com/libsdl-org/SDL/issues/12224 +(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6) +--- + src/audio/pipewire/SDL_pipewire.c | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c +index 889e05decb..5d1bfc28de 100644 +--- a/src/audio/pipewire/SDL_pipewire.c ++++ b/src/audio/pipewire/SDL_pipewire.c +@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info) + + /* Need to parse the parameters to get the sample rate */ + for (i = 0; i < info->n_params; ++i) { +- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL); ++ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL); + } + + hotplug_core_sync(node); +-- +2.49.0 + diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index c098e367..c3edee58 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=2.30.10 +VERSION=2.32.2 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=35a8b9c4f3635d85762b904ac60ca4e0806bff89faeb269caafbe80860d67168 +SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4 cd "$SOURCES_DIR" @@ -18,6 +18,7 @@ then else get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" + patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" From 5d12d9071dad6d9d80197546d820dc90fbd31998 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 15:06:00 +0100 Subject: [PATCH 238/278] Upgrade FFmpeg (7.1.1) --- app/deps/ffmpeg.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/ffmpeg.sh b/app/deps/ffmpeg.sh index d268ca91..fb8b9a25 100755 --- a/app/deps/ffmpeg.sh +++ b/app/deps/ffmpeg.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=7.1 +VERSION=7.1.1 FILENAME=ffmpeg-$VERSION.tar.xz PROJECT_DIR=ffmpeg-$VERSION -SHA256SUM=40973D44970DBC83EF302B0609F2E74982BE2D85916DD2EE7472D30678A7ABE6 +SHA256SUM=733984395e0dbbe5c046abda2dc49a5544e7e0e1e2366bba849222ae9e3a03b1 cd "$SOURCES_DIR" From 89b624770c7cc133cd14070a1ba766df3befa85f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 15:45:28 +0100 Subject: [PATCH 239/278] Bump version to 3.2 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 2c441aa1..19475e0b 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.1" + VALUE "ProductVersion", "3.2" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 84784814..b64a6c90 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.1', + version: '3.2', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 9c0543e9..02508001 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30100 - versionName "3.1" + versionCode 30200 + versionName "3.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index e0b69aee..8bb8632b 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=3.1 +SCRCPY_VERSION_NAME=3.2 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From e0f37f834bb2e9371c0ca893757b60eb36e759af Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 29 Mar 2025 16:15:14 +0100 Subject: [PATCH 240/278] Update links to 3.2 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 16b8bca1..a3b0d834 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.1) +# scrcpy (v3.2) scrcpy diff --git a/doc/build.md b/doc/build.md index 2776ed01..afe8b21b 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.1`][direct-scrcpy-server] - SHA-256: `958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0` + - [`scrcpy-server-v3.2`][direct-scrcpy-server] + SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 9beaed1e..52345d1a 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.1.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `37dba54092ed9ec6b2f8f95432f61b8ea124aec9f1e9f2b3d22d4b10bb04c59a` + - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-linux-x86_64-v3.1.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index 56d9f168..b0335d18 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.1.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `478618d940421e5f57942f5479d493ecbb38210682937a200f712aee5f235daf` + - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b` - - [`scrcpy-macos-x86_64-v3.1.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `acde98e29c273710ffa469371dbca4a728a44c41c380381f8a54e5b5301b9e87` + - [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-aarch64-v3.1.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-macos-x86_64-v3.1.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 89b80727..fb3e3887 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.1.zip`][direct-win64] (64-bit) - SHA-256: `0c05ea395d95cfe36bee974eeb435a3db87ea5594ff738370d5dc3068a9538ca` - - [`scrcpy-win32-v3.1.zip`][direct-win32] (32-bit) - SHA-256: `2b4674ef76719680ac5a9b482d1943bdde3fa25821ad2e98f3c40c347d00d560` + - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit) + SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0` + - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit) + SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win64-v3.1.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-win32-v3.1.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 3774be86..2d2d2c2f 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.1/scrcpy-server-v3.1 -PREBUILT_SERVER_SHA256=958f0944a62f23b1f33a16e9eb14844c1a04b882ca175a738c16d23cb22b86c0 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 +PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From db9dc6ae836193dcd7883d001fdddbf54cbe9859 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 1 Apr 2025 11:04:34 +0200 Subject: [PATCH 241/278] Make the snap version as obsolete The version of scrcpy packaged in snap is currently 1.25. Refs --- doc/linux.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/linux.md b/doc/linux.md index 52345d1a..979ef568 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -27,7 +27,7 @@ Scrcpy is packaged in several distributions and package managers: - Arch Linux: `pacman -S scrcpy` - Fedora: `dnf copr enable zeno/scrcpy && dnf install scrcpy` - Gentoo: `emerge scrcpy` - - Snap: `snap install scrcpy` + - Snap: ~~`snap install scrcpy`~~ _(obsolete version)_ - … (see [repology](https://repology.org/project/scrcpy/versions)) From 882003f314ad5077a41bbc936831aeb36dd8b078 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 3 Apr 2025 08:04:11 +0200 Subject: [PATCH 242/278] Fix segfault on SDL event without window Since #5804, controls have been enabled even with --no-window. As a result, the Android clipboard is synchronized with the computer, causing SDL to trigger an SDL_CLIPBOARDUPDATE event. This event is ignored by scrcpy, but it was still transmitted to the sc_screen instance, even if it had not been initialized. Fix the issue by calling sc_screen_handle_event() only when a screen instance exists. Refs #5804 Fixes #5970 --- app/src/scrcpy.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index b3ff9b36..4d08e667 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -165,7 +165,7 @@ sdl_configure(bool video_playback, bool disable_screensaver) { } static enum scrcpy_exit_code -event_loop(struct scrcpy *s) { +event_loop(struct scrcpy *s, bool has_screen) { SDL_Event event; while (SDL_WaitEvent(&event)) { switch (event.type) { @@ -197,7 +197,7 @@ event_loop(struct scrcpy *s) { break; } default: - if (!sc_screen_handle_event(&s->screen, &event)) { + if (has_screen && !sc_screen_handle_event(&s->screen, &event)) { return SCRCPY_EXIT_FAILURE; } break; @@ -933,7 +933,7 @@ aoa_complete: } } - ret = event_loop(s); + ret = event_loop(s, options->window); terminate_event_loop(); LOGD("quit..."); From 5900e9e39c7496bf8dfc1f246c6bbf0a1e072a69 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 7 Apr 2025 10:30:56 +0200 Subject: [PATCH 243/278] Remove irrelevant link in FAQ --- FAQ.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/FAQ.md b/FAQ.md index 5f089cd7..24722c74 100644 --- a/FAQ.md +++ b/FAQ.md @@ -166,14 +166,13 @@ Rebooting the device is necessary once this option is set. ### Special characters do not work -The default text injection method is [limited to ASCII characters][text-input]. -A trick allows to also inject some [accented characters][accented-characters], +The default text injection method is limited to ASCII characters. A trick allows +to also inject some [accented characters][accented-characters], but that's all. See [#37]. To avoid the problem, [change the keyboard mode to simulate a physical keyboard][hid]. -[text-input]: https://github.com/Genymobile/scrcpy/issues?q=is%3Aopen+is%3Aissue+label%3Aunicode [accented-characters]: https://blog.rom1v.com/2018/03/introducing-scrcpy/#handle-accented-characters [#37]: https://github.com/Genymobile/scrcpy/issues/37 [hid]: doc/keyboard.md#physical-keyboard-simulation From d2447b5c1982b8c91fbce8f515aeedac3d2ecb33 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Apr 2025 18:05:08 +0200 Subject: [PATCH 244/278] Fix --screen-off-timeout bash completion Only the option must be auto-completed, not its value. --- app/data/bash-completion/scrcpy | 1 + 1 file changed, 1 insertion(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 9918918c..a49da8ca 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -205,6 +205,7 @@ _scrcpy() { |-p|--port \ |--push-target \ |--rotation \ + |--screen-off-timeout \ |--tunnel-host \ |--tunnel-port \ |--v4l2-buffer \ From 1a0d300786827974b5a593959c1c21f89469777e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 14 Apr 2025 18:07:37 +0200 Subject: [PATCH 245/278] Add missing --screen-off-timeout doc in manpage Refs eff5b4b219be6043a3baf51149b1d6752569a173 --- app/scrcpy.1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index d481ddd1..d72fda13 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -510,6 +510,10 @@ The device serial number. Mandatory only if several devices are connected to adb .B \-S, \-\-turn\-screen\-off Turn the device screen off immediately. +.TP +.B "\-\-screen\-off\-timeout " seconds +Set the screen off timeout while scrcpy is running (restore the initial value on exit). + .TP .BI "\-\-shortcut\-mod " key\fR[+...]][,...] Specify the modifiers to use for scrcpy shortcuts. Possible keys are "lctrl", "rctrl", "lalt", "ralt", "lsuper" and "rsuper". From c5ed2cfc28ee7c7b59b11eb4db1258ac1c633bff Mon Sep 17 00:00:00 2001 From: Nicholas Wilson Date: Fri, 18 Apr 2025 09:54:59 -0500 Subject: [PATCH 246/278] Replace "licence" with "license" in README Although "licence" is correct in British English, the rest of the statement uses "license," so change it for consistency. PR #6017 Signed-off-by: Romain Vimont --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a3b0d834..c1fd9f7f 100644 --- a/README.md +++ b/README.md @@ -207,7 +207,7 @@ work][donate]: [donate]: https://blog.rom1v.com/about/#support-my-open-source-work -## Licence +## License Copyright (C) 2018 Genymobile Copyright (C) 2018-2025 Romain Vimont From 6875e9aa88833525b60322597304b01f6ba91987 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Apr 2025 16:05:13 +0200 Subject: [PATCH 247/278] Revert "Fix AudioRecord package name for Android 16" This reverts commit c27d116a662c87ee84963820669ee0d2ce60e6f1. This commit breaks audio on Android 16 beta 4. Refs #5960 comment Fixes #6021 --- server/src/main/java/com/genymobile/scrcpy/FakeContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 22fc6d49..2b83e397 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -72,7 +72,7 @@ public final class FakeContext extends ContextWrapper { @Override public AttributionSource getAttributionSource() { AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); - builder.setPackageName("shell"); + builder.setPackageName(PACKAGE_NAME); return builder.build(); } From 48f38c4bb6d91e378a657082e0da7eb846d25acc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 24 Apr 2025 16:12:28 +0200 Subject: [PATCH 248/278] Fix default locked capture orientation The default landscape locked orientation was reversed. Fixes #6010 --- .../java/com/genymobile/scrcpy/device/Orientation.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java index c269750e..81168aae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/Orientation.java @@ -32,9 +32,11 @@ public enum Orientation { throw new IllegalArgumentException("Unknown orientation: " + name); } - public static Orientation fromRotation(int rotation) { - assert rotation >= 0 && rotation < 4; - return values()[rotation]; + public static Orientation fromRotation(int ccwRotation) { + assert ccwRotation >= 0 && ccwRotation < 4; + // Display rotation is expressed counter-clockwise, orientation is expressed clockwise + int cwRotation = (4 - ccwRotation) % 4; + return values()[cwRotation]; } public boolean isFlipped() { From 91a4a74641903bedff189548cc5c33289752b4b4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 25 Apr 2025 10:23:08 +0200 Subject: [PATCH 249/278] Move regex pattern initialization If text == null, then the Pattern is not used. --- .../java/com/genymobile/scrcpy/wrappers/DisplayManager.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index d44ac608..130f86c6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -95,12 +95,12 @@ public final class DisplayManager { } private static int parseDisplayFlags(String text) { - Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); if (text == null) { return 0; } int flags = 0; + Pattern regex = Pattern.compile("FLAG_[A-Z_]+"); Matcher m = regex.matcher(text); while (m.find()) { String flagString = m.group(); From cc309a2b34da13bbc15fbb64a6bba33ff8c79ce1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 May 2025 11:37:47 +0200 Subject: [PATCH 250/278] Build static linux binary on Ubuntu 22.04 Ubuntu 20.04 is no longer available on GitHub Actions. Refs Refs #6050 This reverts commit 69858c6f437b1bfece96bc291c607de842837d36. --- .github/workflows/release.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5875c6bf..49402a6e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -84,7 +84,7 @@ jobs: run: release/test_client.sh build-linux-x86_64: - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Check architecture run: | From 8cd63cb63eeb4873b304a44fb66d55db03f2dd36 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 9 May 2025 18:22:40 +0200 Subject: [PATCH 251/278] Report specific error for INJECT_EVENT permission Some devices require a specific option to be enabled in Developer Options to avoid a permission issue when injecting input events. When this error occurs, hide the stack trace and print a human-readable message explaining how to fix the issue. PR #6080 --- README.md | 2 +- .../scrcpy/wrappers/InputManager.java | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index a3b0d834..36f978f9 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Make sure you [enabled USB debugging][enable-adb] on your device(s). On some devices (especially Xiaomi), you might get the following error: ``` -java.lang.SecurityException: Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. +Injecting input events requires the caller (or the source of the instrumentation, if any) to have the INJECT_EVENTS permission. ``` In that case, you need to enable [an additional option][control] `USB debugging diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 5c5ba56c..f9f8e3ac 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -6,6 +6,7 @@ import android.annotation.SuppressLint; import android.view.InputEvent; import android.view.MotionEvent; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @SuppressLint("PrivateApi,DiscouragedPrivateApi") @@ -17,6 +18,7 @@ public final class InputManager { private final Object manager; private Method injectInputEventMethod; + private long lastPermissionLogDate; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; @@ -57,6 +59,23 @@ public final class InputManager { Method method = getInjectInputEventMethod(); return (boolean) method.invoke(manager, inputEvent, mode); } catch (ReflectiveOperationException e) { + if (e instanceof InvocationTargetException) { + Throwable cause = e.getCause(); + if (cause instanceof SecurityException) { + String message = e.getCause().getMessage(); + if (message != null && message.contains("INJECT_EVENTS permission")) { + // Do not flood the console, limit to one permission error log every 3 seconds + long now = System.currentTimeMillis(); + if (lastPermissionLogDate <= now - 3000) { + Ln.e(message); + Ln.e("Make sure you have enabled \"USB debugging (Security Settings)\" and then rebooted your device."); + lastPermissionLogDate = now; + } + // Do not print the stack trace + return false; + } + } + } Ln.e("Could not invoke method", e); return false; } From 38f779d9d37833e617a577b5a9f3bc91d0358174 Mon Sep 17 00:00:00 2001 From: hltdev8642 <39349712+hltdev8642@users.noreply.github.com> Date: Fri, 9 May 2025 09:42:01 -0400 Subject: [PATCH 252/278] Escape parentheses in zsh completion script PR #6079 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/zsh-completion/_scrcpy | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 450fc8f5..8c2498f1 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -11,7 +11,7 @@ arguments=( '--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--angle=[Rotate the video content by a custom angle, in degrees]' '--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 flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-dup=[Duplicate audio]' @@ -35,10 +35,10 @@ arguments=( {-e,--select-tcpip}'[Use TCP/IP device]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' - '-G[Use UHID/AOA gamepad (same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode)]' + '-G[Use UHID/AOA gamepad \(same as --gamepad=uhid or --gamepad=aoa, depending on OTG mode\)]' '--gamepad=[Set the gamepad input mode]:mode:(disabled uhid aoa)' {-h,--help}'[Print the help]' - '-K[Use UHID/AOA keyboard (same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode)]' + '-K[Use UHID/AOA keyboard \(same as --keyboard=uhid or --keyboard=aoa, depending on OTG mode\)]' '--keyboard=[Set the keyboard input mode]:mode:(disabled sdk uhid aoa)' '--kill-adb-on-close[Kill adb when scrcpy terminates]' '--legacy-paste[Inject computer clipboard text as a sequence of key events on Ctrl+v]' @@ -48,7 +48,7 @@ arguments=( '--list-displays[List displays available on the device]' '--list-encoders[List video and audio encoders available on the device]' {-m,--max-size=}'[Limit both the width and height of the video to value]' - '-M[Use UHID/AOA mouse (same as --mouse=uhid or --mouse=aoa, depending on OTG mode)]' + '-M[Use UHID/AOA mouse \(same as --mouse=uhid or --mouse=aoa, depending on OTG mode\)]' '--max-fps=[Limit the frame rate of screen capture]' '--mouse=[Set the mouse input mode]:mode:(disabled sdk uhid aoa)' '--mouse-bind=[Configure bindings of secondary clicks]' From 70bfa2cf394955accb7446a99f9b6c6f5dfbaa2c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 15 May 2025 19:51:36 +0200 Subject: [PATCH 253/278] Remove useless flag in zsh completion script The -N flag is only useful after a pattern section (-p) to switch back to listing command names. Refs --- app/data/zsh-completion/_scrcpy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 8c2498f1..04ffb8f1 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -1,4 +1,4 @@ -#compdef -N scrcpy -N scrcpy.exe +#compdef scrcpy scrcpy.exe # # name: scrcpy # auth: hltdev [hltdev8642@gmail.com] From 52f5d08d1fab9600e78b21c71fa4a6c106d3783f Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 3 Jun 2025 21:13:29 +0200 Subject: [PATCH 254/278] Avoid calling wait(0) Calling wait(0) results in waiting without a timeout, which is unintended. Refs #6009 comment --- .../main/java/com/genymobile/scrcpy/control/Controller.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 5e64a4c5..24d827fd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -699,7 +699,9 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { if (timeout < 0) { return null; } - displayDataAvailable.wait(timeout); + if (timeout > 0) { + displayDataAvailable.wait(timeout); + } data = displayData.get(); } From d2cc930975a8c7a2a073a9bffd9c2576d58cff7b Mon Sep 17 00:00:00 2001 From: Colin Kinloch Date: Thu, 22 May 2025 18:50:41 +0100 Subject: [PATCH 255/278] Add app name SDL hint This allows pulseaudio to label the audio stream "scrcpy" rather than "SDL Application". PR #6107 Signed-off-by: Romain Vimont --- app/src/compat.h | 8 ++++++++ app/src/scrcpy.c | 11 +++++++++++ 2 files changed, 19 insertions(+) diff --git a/app/src/compat.h b/app/src/compat.h index 1995d384..296d1a9f 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -75,6 +75,14 @@ # define SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL #endif +#if SDL_VERSION_ATLEAST(2, 0, 18) +# define SCRCPY_SDL_HAS_HINT_APP_NAME +#endif + +#if SDL_VERSION_ATLEAST(2, 0, 14) +# define SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME +#endif + #ifndef HAVE_STRDUP char *strdup(const char *s); #endif diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 4d08e667..a4c8c340 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -107,6 +107,17 @@ sdl_set_hints(const char *render_driver) { LOGW("Could not set render driver"); } + // App name used in various contexts (such as PulseAudio) +#if defined(SCRCPY_SDL_HAS_HINT_APP_NAME) + if (!SDL_SetHint(SDL_HINT_APP_NAME, "scrcpy")) { + LOGW("Could not set app name"); + } +#elif defined(SCRCPY_SDL_HAS_HINT_AUDIO_DEVICE_APP_NAME) + if (!SDL_SetHint(SDL_HINT_AUDIO_DEVICE_APP_NAME, "scrcpy")) { + LOGW("Could not set audio device app name"); + } +#endif + // Linear filtering if (!SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "1")) { LOGW("Could not enable linear filtering"); From 41ed40f5f9ee557c5ccbca8b30a51c74af92da92 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Fri, 25 Apr 2025 12:31:22 +0800 Subject: [PATCH 256/278] Simplify InputManager wrapper Use the public InputManager API. PR #6009 Signed-off-by: Romain Vimont --- .../scrcpy/wrappers/InputManager.java | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index f9f8e3ac..24c5f80c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,5 +1,6 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; @@ -16,40 +17,26 @@ public final class InputManager { public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_RESULT = 1; public static final int INJECT_INPUT_EVENT_MODE_WAIT_FOR_FINISH = 2; - private final Object manager; - private Method injectInputEventMethod; + private final android.hardware.input.InputManager manager; private long lastPermissionLogDate; + private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; static InputManager create() { - try { - Class inputManagerClass = getInputManagerClass(); - Method getInstanceMethod = inputManagerClass.getDeclaredMethod("getInstance"); - Object im = getInstanceMethod.invoke(null); - return new InputManager(im); - } catch (ReflectiveOperationException e) { - throw new AssertionError(e); - } + android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() + .getSystemService(FakeContext.INPUT_SERVICE); + return new InputManager(manager); } - private static Class getInputManagerClass() { - try { - // Parts of the InputManager class have been moved to a new InputManagerGlobal class in Android 14 preview - return Class.forName("android.hardware.input.InputManagerGlobal"); - } catch (ClassNotFoundException e) { - return android.hardware.input.InputManager.class; - } - } - - private InputManager(Object manager) { + private InputManager(android.hardware.input.InputManager manager) { this.manager = manager; } - private Method getInjectInputEventMethod() throws NoSuchMethodException { + private static Method getInjectInputEventMethod() throws NoSuchMethodException { if (injectInputEventMethod == null) { - injectInputEventMethod = manager.getClass().getMethod("injectInputEvent", InputEvent.class, int.class); + injectInputEventMethod = android.hardware.input.InputManager.class.getMethod("injectInputEvent", InputEvent.class, int.class); } return injectInputEventMethod; } From ee414231ed136a59113787dc83a739623518c728 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 3 May 2025 13:43:10 +0200 Subject: [PATCH 257/278] Cache getDisplayInfo method Do not use reflection to retrieve the method for every call. PR #6009 --- .../genymobile/scrcpy/wrappers/DisplayManager.java | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 130f86c6..3f8ed2bd 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -46,6 +46,7 @@ public final class DisplayManager { } private final Object manager; // instance of hidden class android.hardware.display.DisplayManagerGlobal + private Method getDisplayInfoMethod; private Method createVirtualDisplayMethod; private Method requestDisplayPowerMethod; @@ -114,9 +115,17 @@ public final class DisplayManager { return flags; } + private Method getGetDisplayInfoMethod() throws NoSuchMethodException { + if (getDisplayInfoMethod == null) { + getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class); + } + return getDisplayInfoMethod; + } + public DisplayInfo getDisplayInfo(int displayId) { try { - Object displayInfo = manager.getClass().getMethod("getDisplayInfo", int.class).invoke(manager, displayId); + Method method = getGetDisplayInfoMethod(); + Object displayInfo = method.invoke(manager, displayId); if (displayInfo == null) { // fallback when displayInfo is null return getDisplayInfoFromDumpsysDisplay(displayId); From 7a3fe830d4d85d02ec21a23b88d8b48ce798a13e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 2 May 2025 23:03:15 +0200 Subject: [PATCH 258/278] Synchronize access to DisplayManager The DisplayManager and its method getDisplayInfo() may be used from both the Controller thread and the video (main) thread. PR #6009 --- .../java/com/genymobile/scrcpy/wrappers/DisplayManager.java | 3 ++- .../java/com/genymobile/scrcpy/wrappers/ServiceManager.java | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 3f8ed2bd..2f86bbd2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -115,7 +115,8 @@ public final class DisplayManager { return flags; } - private Method getGetDisplayInfoMethod() throws NoSuchMethodException { + // getDisplayInfo() may be used from both the Controller thread and the video (main) thread + private synchronized Method getGetDisplayInfoMethod() throws NoSuchMethodException { if (getDisplayInfoMethod == null) { getDisplayInfoMethod = manager.getClass().getMethod("getDisplayInfo", int.class); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java index a8a56dab..b1123b55 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -54,7 +54,8 @@ public final class ServiceManager { return windowManager; } - public static DisplayManager getDisplayManager() { + // The DisplayManager may be used from both the Controller thread and the video (main) thread + public static synchronized DisplayManager getDisplayManager() { if (displayManager == null) { displayManager = DisplayManager.create(); } From ca4f50c5ef12eee2c0efb562e157a8b2d75cb001 Mon Sep 17 00:00:00 2001 From: Simon Chan <1330321+yume-chan@users.noreply.github.com> Date: Wed, 16 Apr 2025 14:26:24 +0800 Subject: [PATCH 259/278] Associate UHID devices to virtual displays This allows the mouse pointer to appear on the correct display (only for devices running Android 15+). Fixes #5547 PR #6009 Signed-off-by: Romain Vimont --- .../genymobile/scrcpy/control/Controller.java | 29 ++++++++- .../scrcpy/control/UhidManager.java | 65 ++++++++++++++++--- .../genymobile/scrcpy/device/DisplayInfo.java | 9 ++- .../scrcpy/wrappers/DisplayManager.java | 5 +- .../scrcpy/wrappers/InputManager.java | 40 ++++++++++++ 5 files changed, 134 insertions(+), 14 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index 24d827fd..a905b6c9 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -6,6 +6,7 @@ import com.genymobile.scrcpy.CleanUp; import com.genymobile.scrcpy.Options; import com.genymobile.scrcpy.device.Device; import com.genymobile.scrcpy.device.DeviceApp; +import com.genymobile.scrcpy.device.DisplayInfo; import com.genymobile.scrcpy.device.Point; import com.genymobile.scrcpy.device.Position; import com.genymobile.scrcpy.device.Size; @@ -156,8 +157,34 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { private UhidManager getUhidManager() { if (uhidManager == null) { - uhidManager = new UhidManager(sender); + int uhidDisplayId = displayId; + if (Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15) { + if (displayId == Device.DISPLAY_ID_NONE) { + // Mirroring a new virtual display id (using --new-display-id feature) on Android >= 15, where the UHID mouse pointer can be + // associated to the virtual display + try { + // Wait for at most 1 second until a virtual display id is known + DisplayData data = waitDisplayData(1000); + if (data != null) { + uhidDisplayId = data.virtualDisplayId; + } + } catch (InterruptedException e) { + // do nothing + } + } + } + + String displayUniqueId = null; + if (uhidDisplayId > 0) { + // Ignore Device.DISPLAY_ID_NONE and 0 (main display) + DisplayInfo displayInfo = ServiceManager.getDisplayManager().getDisplayInfo(uhidDisplayId); + if (displayInfo != null) { + displayUniqueId = displayInfo.getUniqueId(); + } + } + uhidManager = new UhidManager(sender, displayUniqueId); } + return uhidManager; } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java index c4867a3f..20532c0b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/UhidManager.java @@ -3,6 +3,7 @@ package com.genymobile.scrcpy.control; import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.util.Ln; import com.genymobile.scrcpy.util.StringUtils; +import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.Build; import android.os.HandlerThread; @@ -31,14 +32,20 @@ public final class UhidManager { private static final int SIZE_OF_UHID_EVENT = 4380; // sizeof(struct uhid_event) + // Must be unique across the system + private static final String INPUT_PORT = "scrcpy:" + Os.getpid(); + + private final String displayUniqueId; + private final ArrayMap fds = new ArrayMap<>(); private final ByteBuffer buffer = ByteBuffer.allocate(SIZE_OF_UHID_EVENT).order(ByteOrder.nativeOrder()); private final DeviceMessageSender sender; private final MessageQueue queue; - public UhidManager(DeviceMessageSender sender) { + public UhidManager(DeviceMessageSender sender, String displayUniqueId) { this.sender = sender; + this.displayUniqueId = displayUniqueId; if (Build.VERSION.SDK_INT >= AndroidVersions.API_23_ANDROID_6_0) { HandlerThread thread = new HandlerThread("UHidManager"); thread.start(); @@ -52,15 +59,22 @@ public final class UhidManager { try { FileDescriptor fd = Os.open("/dev/uhid", OsConstants.O_RDWR, 0); try { + // First UHID device added + boolean firstDevice = fds.isEmpty(); + FileDescriptor old = fds.put(id, fd); if (old != null) { Ln.w("Duplicate UHID id: " + id); close(old); } - byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc); + String phys = mustUseInputPort() ? INPUT_PORT : null; + byte[] req = buildUhidCreate2Req(vendorId, productId, name, reportDesc, phys); Os.write(fd, req, 0, req.length); + if (firstDevice) { + addUniqueIdAssociation(); + } registerUhidListener(id, fd); } catch (Exception e) { close(fd); @@ -148,7 +162,7 @@ public final class UhidManager { } } - private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc) { + private static byte[] buildUhidCreate2Req(int vendorId, int productId, String name, byte[] reportDesc, String phys) { /* * struct uhid_event { * uint32_t type; @@ -170,17 +184,23 @@ public final class UhidManager { * } __attribute__((__packed__)); */ - byte[] empty = new byte[256]; ByteBuffer buf = ByteBuffer.allocate(280 + reportDesc.length).order(ByteOrder.nativeOrder()); buf.putInt(UHID_CREATE2); String actualName = name.isEmpty() ? "scrcpy" : name; - byte[] utf8Name = actualName.getBytes(StandardCharsets.UTF_8); - int len = StringUtils.getUtf8TruncationIndex(utf8Name, 127); - assert len <= 127; - buf.put(utf8Name, 0, len); - buf.put(empty, 0, 256 - len); + byte[] nameBytes = actualName.getBytes(StandardCharsets.UTF_8); + int nameLen = StringUtils.getUtf8TruncationIndex(nameBytes, 127); + assert nameLen <= 127; + buf.put(nameBytes, 0, nameLen); + if (phys != null) { + buf.position(4 + 128); + byte[] physBytes = phys.getBytes(StandardCharsets.US_ASCII); + assert physBytes.length <= 63; + buf.put(physBytes); + } + + buf.position(4 + 256); buf.putShort((short) reportDesc.length); buf.putShort(BUS_VIRTUAL); buf.putInt(vendorId); @@ -219,15 +239,26 @@ public final class UhidManager { if (fd != null) { unregisterUhidListener(fd); close(fd); + + if (fds.isEmpty()) { + // Last UHID device removed + removeUniqueIdAssociation(); + } } else { Ln.w("Closing unknown UHID device: " + id); } } public void closeAll() { + if (fds.isEmpty()) { + return; + } + for (FileDescriptor fd : fds.values()) { close(fd); } + + removeUniqueIdAssociation(); } private static void close(FileDescriptor fd) { @@ -237,4 +268,20 @@ public final class UhidManager { Ln.e("Failed to close uhid: " + e.getMessage()); } } + + private boolean mustUseInputPort() { + return Build.VERSION.SDK_INT >= AndroidVersions.API_35_ANDROID_15 && displayUniqueId != null; + } + + private void addUniqueIdAssociation() { + if (mustUseInputPort()) { + ServiceManager.getInputManager().addUniqueIdAssociationByPort(INPUT_PORT, displayUniqueId); + } + } + + private void removeUniqueIdAssociation() { + if (mustUseInputPort()) { + ServiceManager.getInputManager().removeUniqueIdAssociationByPort(INPUT_PORT); + } + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java index cdd4bab9..8d26b7ce 100644 --- a/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java +++ b/server/src/main/java/com/genymobile/scrcpy/device/DisplayInfo.java @@ -7,16 +7,18 @@ public final class DisplayInfo { private final int layerStack; private final int flags; private final int dpi; + private final String uniqueId; public static final int FLAG_SUPPORTS_PROTECTED_BUFFERS = 0x00000001; - public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi) { + public DisplayInfo(int displayId, Size size, int rotation, int layerStack, int flags, int dpi, String uniqueId) { this.displayId = displayId; this.size = size; this.rotation = rotation; this.layerStack = layerStack; this.flags = flags; this.dpi = dpi; + this.uniqueId = uniqueId; } public int getDisplayId() { @@ -42,5 +44,8 @@ public final class DisplayInfo { public int getDpi() { return dpi; } -} + public String getUniqueId() { + return uniqueId; + } +} diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java index 2f86bbd2..a12470a4 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayManager.java @@ -82,7 +82,7 @@ public final class DisplayManager { int density = Integer.parseInt(m.group(5)); int layerStack = Integer.parseInt(m.group(6)); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, density, null); } private static DisplayInfo getDisplayInfoFromDumpsysDisplay(int displayId) { @@ -139,7 +139,8 @@ public final class DisplayManager { int layerStack = cls.getDeclaredField("layerStack").getInt(displayInfo); int flags = cls.getDeclaredField("flags").getInt(displayInfo); int dpi = cls.getDeclaredField("logicalDensityDpi").getInt(displayInfo); - return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi); + String uniqueId = (String) cls.getDeclaredField("uniqueId").get(displayInfo); + return new DisplayInfo(displayId, new Size(width, height), rotation, layerStack, flags, dpi, uniqueId); } catch (ReflectiveOperationException e) { throw new AssertionError(e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java index 24c5f80c..f55648d5 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/InputManager.java @@ -1,9 +1,11 @@ package com.genymobile.scrcpy.wrappers; +import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; import com.genymobile.scrcpy.util.Ln; import android.annotation.SuppressLint; +import android.annotation.TargetApi; import android.view.InputEvent; import android.view.MotionEvent; @@ -23,6 +25,8 @@ public final class InputManager { private static Method injectInputEventMethod; private static Method setDisplayIdMethod; private static Method setActionButtonMethod; + private static Method addUniqueIdAssociationByPortMethod; + private static Method removeUniqueIdAssociationByPortMethod; static InputManager create() { android.hardware.input.InputManager manager = (android.hardware.input.InputManager) FakeContext.get() @@ -103,4 +107,40 @@ public final class InputManager { return false; } } + + private static Method getAddUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (addUniqueIdAssociationByPortMethod == null) { + addUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "addUniqueIdAssociationByPort", String.class, String.class); + } + return addUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void addUniqueIdAssociationByPort(String inputPort, String uniqueId) { + try { + Method method = getAddUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort, uniqueId); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot add unique id association by port", e); + } + } + + private static Method getRemoveUniqueIdAssociationByPortMethod() throws NoSuchMethodException { + if (removeUniqueIdAssociationByPortMethod == null) { + removeUniqueIdAssociationByPortMethod = android.hardware.input.InputManager.class.getMethod( + "removeUniqueIdAssociationByPort", String.class); + } + return removeUniqueIdAssociationByPortMethod; + } + + @TargetApi(AndroidVersions.API_35_ANDROID_15) + public void removeUniqueIdAssociationByPort(String inputPort) { + try { + Method method = getRemoveUniqueIdAssociationByPortMethod(); + method.invoke(manager, inputPort); + } catch (ReflectiveOperationException e) { + Ln.e("Cannot remove unique id association by port", e); + } + } } From 283326b2f6fa3fdaeecc181f69a3a4bcd429c06a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 5 Jun 2025 20:33:15 +0200 Subject: [PATCH 260/278] Run a main looper Instead of blocking the main thread until completion, run a looper. This will allow the main thread to process any event posted to the main looper. Refs #6009 comment PR #6129 --- .../java/com/genymobile/scrcpy/Server.java | 31 ++++++++++++------- .../com/genymobile/scrcpy/Workarounds.java | 15 --------- 2 files changed, 19 insertions(+), 27 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 09cfd6cf..c1d8c1f2 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -25,9 +25,11 @@ import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; import android.os.Build; +import android.os.Looper; import java.io.File; import java.io.IOException; +import java.lang.reflect.Field; import java.util.ArrayList; import java.util.List; @@ -55,17 +57,7 @@ public final class Server { this.fatalError = true; } if (running == 0 || this.fatalError) { - notify(); - } - } - - synchronized void await() { - try { - while (running > 0 && !fatalError) { - wait(); - } - } catch (InterruptedException e) { - // ignore + Looper.getMainLooper().quitSafely(); } } } @@ -104,6 +96,7 @@ public final class Server { boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); + prepareMainLooper(); Workarounds.apply(); List asyncProcessors = new ArrayList<>(); @@ -172,7 +165,7 @@ public final class Server { }); } - completion.await(); + Looper.loop(); // interrupted by the Completion implementation } finally { if (cleanUp != null) { cleanUp.interrupt(); @@ -201,6 +194,20 @@ public final class Server { } } + private static void prepareMainLooper() { + // Like Looper.prepareMainLooper(), but with quitAllowed set to true + Looper.prepare(); + synchronized (Looper.class) { + try { + Field field = Looper.class.getDeclaredField("sMainLooper"); + field.setAccessible(true); + field.set(null, Looper.myLooper()); + } catch (ReflectiveOperationException e) { + throw new AssertionError(e); + } + } + } + public static void main(String... args) { int status = 0; try { diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index fb4c1389..b89f19ae 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -29,8 +29,6 @@ public final class Workarounds { private static final Object ACTIVITY_THREAD; static { - prepareMainLooper(); - try { // ActivityThread activityThread = new ActivityThread(); ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); @@ -77,19 +75,6 @@ public final class Workarounds { fillAppContext(); } - @SuppressWarnings("deprecation") - private static void prepareMainLooper() { - // Some devices internally create a Handler when creating an input Surface, causing an exception: - // "Can't create handler inside thread that has not called Looper.prepare()" - // - // - // Use Looper.prepareMainLooper() instead of Looper.prepare() to avoid a NullPointerException: - // "Attempt to read from field 'android.os.MessageQueue android.os.Looper.mQueue' - // on a null object reference" - // - Looper.prepareMainLooper(); - } - private static void fillAppInfo() { try { // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); From 8a02e3c2f58cffc3fdd8c08b26aae04bbf9d5a97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 17 Apr 2025 18:09:55 +0200 Subject: [PATCH 261/278] Simplify ClipboardManager wrapper Use the public ClipboardManager API, with the FakeContext as context. This requires a running main looper, otherwise clipboard changes are not processed. Refs #6009 PR #6129 Suggested by: Simon Chan <1330321+yume-chan@users.noreply.github.com> --- .../com/genymobile/scrcpy/FakeContext.java | 25 ++ .../genymobile/scrcpy/control/Controller.java | 22 +- .../scrcpy/wrappers/ClipboardManager.java | 255 +----------------- 3 files changed, 48 insertions(+), 254 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 2b83e397..b43e9e1b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,8 +2,10 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ServiceManager; +import android.annotation.SuppressLint; import android.annotation.TargetApi; import android.content.AttributionSource; +import android.content.ClipboardManager; import android.content.ContentResolver; import android.content.Context; import android.content.ContextWrapper; @@ -11,6 +13,8 @@ import android.content.IContentProvider; import android.os.Binder; import android.os.Process; +import java.lang.reflect.Field; + public final class FakeContext extends ContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; @@ -91,4 +95,25 @@ public final class FakeContext extends ContextWrapper { public ContentResolver getContentResolver() { return contentResolver; } + + @SuppressLint("SoonBlockedPrivateApi") + @Override + public Object getSystemService(String name) { + Object service = super.getSystemService(name); + if (service == null) { + return null; + } + + if (Context.CLIPBOARD_SERVICE.equals(name)) { + try { + Field field = ClipboardManager.class.getDeclaredField("mContext"); + field.setAccessible(true); + field.set(service, this); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e); + } + } + + return service; + } } diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index a905b6c9..bfbee7dc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -18,7 +18,6 @@ import com.genymobile.scrcpy.wrappers.ClipboardManager; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; -import android.content.IOnPrimaryClipChangedListener; import android.content.Intent; import android.os.Build; import android.os.SystemClock; @@ -119,18 +118,15 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager != null) { - clipboardManager.addPrimaryClipChangedListener(new IOnPrimaryClipChangedListener.Stub() { - @Override - public void dispatchPrimaryClipChanged() { - if (isSettingClipboard.get()) { - // This is a notification for the change we are currently applying, ignore it - return; - } - String text = Device.getClipboardText(); - if (text != null) { - DeviceMessage msg = DeviceMessage.createClipboard(text); - sender.send(msg); - } + clipboardManager.addPrimaryClipChangedListener(() -> { + if (isSettingClipboard.get()) { + // This is a notification for the change we are currently applying, ignore it + return; + } + String text = Device.getClipboardText(); + if (text != null) { + DeviceMessage msg = DeviceMessage.createClipboard(text); + sender.send(msg); } }); } else { diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index 791df0f8..fae8a056 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -1,270 +1,43 @@ package com.genymobile.scrcpy.wrappers; -import com.genymobile.scrcpy.AndroidVersions; import com.genymobile.scrcpy.FakeContext; -import com.genymobile.scrcpy.util.Ln; import android.content.ClipData; -import android.content.IOnPrimaryClipChangedListener; -import android.os.Build; -import android.os.IInterface; - -import java.lang.reflect.Method; public final class ClipboardManager { - private final IInterface manager; - private Method getPrimaryClipMethod; - private Method setPrimaryClipMethod; - private Method addPrimaryClipChangedListener; - private int getMethodVersion; - private int setMethodVersion; - private int addListenerMethodVersion; + private final android.content.ClipboardManager manager; static ClipboardManager create() { - IInterface clipboard = ServiceManager.getService("clipboard", "android.content.IClipboard"); - if (clipboard == null) { + android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get() + .getSystemService(FakeContext.CLIPBOARD_SERVICE); + if (manager == null) { // Some devices have no clipboard manager // // return null; } - return new ClipboardManager(clipboard); + return new ClipboardManager(manager); } - private ClipboardManager(IInterface manager) { + private ClipboardManager(android.content.ClipboardManager manager) { this.manager = manager; } - private Method getGetPrimaryClipMethod() throws NoSuchMethodException { - if (getPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class); - return getPrimaryClipMethod; - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class); - getMethodVersion = 0; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class); - getMethodVersion = 1; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class); - getMethodVersion = 2; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, int.class, String.class); - getMethodVersion = 3; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 4; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - try { - getPrimaryClipMethod = manager.getClass() - .getMethod("getPrimaryClip", String.class, String.class, String.class, String.class, int.class, int.class, boolean.class); - getMethodVersion = 5; - return getPrimaryClipMethod; - } catch (NoSuchMethodException e) { - // fall-through - } - - getPrimaryClipMethod = manager.getClass().getMethod("getPrimaryClip", String.class, String.class, int.class, int.class, String.class); - getMethodVersion = 6; - } - return getPrimaryClipMethod; - } - - private Method getSetPrimaryClipMethod() throws NoSuchMethodException { - if (setPrimaryClipMethod == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class); - return setPrimaryClipMethod; - } - - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, int.class); - setMethodVersion = 0; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e1) { - // fall-through - } - - try { - setPrimaryClipMethod = manager.getClass().getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class); - setMethodVersion = 1; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e2) { - // fall-through - } - - try { - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class); - setMethodVersion = 2; - return setPrimaryClipMethod; - } catch (NoSuchMethodException e3) { - // fall-through - } - - setPrimaryClipMethod = manager.getClass() - .getMethod("setPrimaryClip", ClipData.class, String.class, String.class, int.class, int.class, boolean.class); - setMethodVersion = 3; - } - return setPrimaryClipMethod; - } - - private static ClipData getPrimaryClip(Method method, int methodVersion, IInterface manager) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME); - } - - switch (methodVersion) { - case 0: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - case 1: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - case 2: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - case 3: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID, null); - case 4: - // The last boolean parameter is "userOperate" - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - case 5: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, null, null, FakeContext.ROOT_UID, 0, true); - default: - return (ClipData) method.invoke(manager, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, null); - } - } - - private static void setPrimaryClip(Method method, int methodVersion, IInterface manager, ClipData clipData) throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME); - return; - } - - switch (methodVersion) { - case 0: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - break; - case 1: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - break; - case 2: - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - break; - default: - // The last boolean parameter is "userOperate" - method.invoke(manager, clipData, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0, true); - } - } - public CharSequence getText() { - try { - Method method = getGetPrimaryClipMethod(); - ClipData clipData = getPrimaryClip(method, getMethodVersion, manager); - if (clipData == null || clipData.getItemCount() == 0) { - return null; - } - return clipData.getItemAt(0).getText(); - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); + ClipData clipData = manager.getPrimaryClip(); + if (clipData == null || clipData.getItemCount() == 0) { return null; } + return clipData.getItemAt(0).getText(); } public boolean setText(CharSequence text) { - try { - Method method = getSetPrimaryClipMethod(); - ClipData clipData = ClipData.newPlainText(null, text); - setPrimaryClip(method, setMethodVersion, manager, clipData); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } + ClipData clipData = ClipData.newPlainText(null, text); + manager.setPrimaryClip(clipData); + return true; } - private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) - throws ReflectiveOperationException { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - method.invoke(manager, listener, FakeContext.PACKAGE_NAME); - return; - } - - switch (methodVersion) { - case 0: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, FakeContext.ROOT_UID); - break; - case 1: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID); - break; - default: - method.invoke(manager, listener, FakeContext.PACKAGE_NAME, null, FakeContext.ROOT_UID, 0); - break; - } - } - - private Method getAddPrimaryClipChangedListener() throws NoSuchMethodException { - if (addPrimaryClipChangedListener == null) { - if (Build.VERSION.SDK_INT < AndroidVersions.API_29_ANDROID_10) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class); - } else { - try { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, int.class); - addListenerMethodVersion = 0; - } catch (NoSuchMethodException e1) { - try { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, - int.class); - addListenerMethodVersion = 1; - } catch (NoSuchMethodException e2) { - addPrimaryClipChangedListener = manager.getClass() - .getMethod("addPrimaryClipChangedListener", IOnPrimaryClipChangedListener.class, String.class, String.class, - int.class, int.class); - addListenerMethodVersion = 2; - } - } - } - } - return addPrimaryClipChangedListener; - } - - public boolean addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener) { - try { - Method method = getAddPrimaryClipChangedListener(); - addPrimaryClipChangedListener(method, addListenerMethodVersion, manager, listener); - return true; - } catch (ReflectiveOperationException e) { - Ln.e("Could not invoke method", e); - return false; - } + public void addPrimaryClipChangedListener(android.content.ClipboardManager.OnPrimaryClipChangedListener listener) { + manager.addPrimaryClipChangedListener(listener); } } From ac16be54c8d4afed7c69b7132719b37282baea49 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 19:36:22 +0200 Subject: [PATCH 262/278] Upgrade platform-tools (36.0.0) --- app/deps/adb_linux.sh | 4 ++-- app/deps/adb_macos.sh | 4 ++-- app/deps/adb_windows.sh | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/deps/adb_linux.sh b/app/deps/adb_linux.sh index 17b5641d..a3e339ec 100755 --- a/app/deps/adb_linux.sh +++ b/app/deps/adb_linux.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-linux.zip PROJECT_DIR=platform-tools-$VERSION-linux -SHA256SUM=acfdcccb123a8718c46c46c059b2f621140194e5ec1ac9d81715be3d6ab6cd0a +SHA256SUM=0ead642c943ffe79701fccca8f5f1c69c4ce4f43df2eefee553f6ccb27cbfbe8 cd "$SOURCES_DIR" diff --git a/app/deps/adb_macos.sh b/app/deps/adb_macos.sh index 8a25915e..36f5df89 100755 --- a/app/deps/adb_macos.sh +++ b/app/deps/adb_macos.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-darwin.zip PROJECT_DIR=platform-tools-$VERSION-darwin -SHA256SUM=1820078db90bf21628d257ff052528af1c61bb48f754b3555648f5652fa35d78 +SHA256SUM=b241878e6ec20650b041bf715ea05f7d5dc73bd24529464bd9cf68946e3132bd cd "$SOURCES_DIR" diff --git a/app/deps/adb_windows.sh b/app/deps/adb_windows.sh index d36706b0..de37162c 100755 --- a/app/deps/adb_windows.sh +++ b/app/deps/adb_windows.sh @@ -4,10 +4,10 @@ DEPS_DIR=$(dirname ${BASH_SOURCE[0]}) cd "$DEPS_DIR" . common -VERSION=35.0.2 +VERSION=36.0.0 FILENAME=platform-tools_r$VERSION-win.zip PROJECT_DIR=platform-tools-$VERSION-windows -SHA256SUM=2975a3eac0b19182748d64195375ad056986561d994fffbdc64332a516300bb9 +SHA256SUM=24bd8bebbbb58b9870db202b5c6775c4a49992632021c60750d9d8ec8179d5f0 cd "$SOURCES_DIR" From 1a9ffb38146b7f70021387ccb0206c873fb07d99 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 19:38:29 +0200 Subject: [PATCH 263/278] Upgrade SDL (2.32.8) --- ...that-the-correct-struct-is-used-for-.patch | 33 ------------------- app/deps/sdl.sh | 5 ++- 2 files changed, 2 insertions(+), 36 deletions(-) delete mode 100644 app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch diff --git a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch b/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch deleted file mode 100644 index cbb516ec..00000000 --- a/app/deps/patches/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch +++ /dev/null @@ -1,33 +0,0 @@ -From 6be87ceb33a9aad3bf5204bb13b3a5e8b498fd26 Mon Sep 17 00:00:00 2001 -From: Neal Gompa -Date: Mon, 10 Feb 2025 05:00:56 -0500 -Subject: [PATCH] pipewire: Ensure that the correct struct is used for - enumeration APIs - -PipeWire now requires the correct struct type is used, otherwise -it will fail to compile. - -Reference: https://gitlab.freedesktop.org/pipewire/pipewire/-/commit/188d920733f0791413d3386e5536ee7377f71b2f - -Fixes: https://github.com/libsdl-org/SDL/issues/12224 -(cherry picked from commit d35bef64e913dd7d5dd3153a4b61f10ef837dad6) ---- - src/audio/pipewire/SDL_pipewire.c | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) - -diff --git a/src/audio/pipewire/SDL_pipewire.c b/src/audio/pipewire/SDL_pipewire.c -index 889e05decb..5d1bfc28de 100644 ---- a/src/audio/pipewire/SDL_pipewire.c -+++ b/src/audio/pipewire/SDL_pipewire.c -@@ -590,7 +590,7 @@ static void node_event_info(void *object, const struct pw_node_info *info) - - /* Need to parse the parameters to get the sample rate */ - for (i = 0; i < info->n_params; ++i) { -- pw_node_enum_params(node->proxy, 0, info->params[i].id, 0, 0, NULL); -+ pw_node_enum_params((struct pw_node*)node->proxy, 0, info->params[i].id, 0, 0, NULL); - } - - hotplug_core_sync(node); --- -2.49.0 - diff --git a/app/deps/sdl.sh b/app/deps/sdl.sh index c3edee58..54fee12b 100755 --- a/app/deps/sdl.sh +++ b/app/deps/sdl.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=2.32.2 +VERSION=2.32.8 FILENAME=SDL-$VERSION.tar.gz PROJECT_DIR=SDL-release-$VERSION -SHA256SUM=f2c7297ae7b3d3910a8b131e1e2a558fdd6d1a4443d5e345374d45cadfcb05a4 +SHA256SUM=dd35e05644ae527848d02433bec24dd0ea65db59faecf1a0e5d1880c533dac2c cd "$SOURCES_DIR" @@ -18,7 +18,6 @@ then else get_file "https://github.com/libsdl-org/SDL/archive/refs/tags/release-$VERSION.tar.gz" "$FILENAME" "$SHA256SUM" tar xf "$FILENAME" # First level directory is "$PROJECT_DIR" - patch -d "$PROJECT_DIR" -p1 < "$PATCHES_DIR"/SDL-pipewire-Ensure-that-the-correct-struct-is-used-for-.patch fi mkdir -p "$BUILD_DIR/$PROJECT_DIR" From 454beaa7571d3d86339a53a5a3202bd47e1d2353 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 19:39:02 +0200 Subject: [PATCH 264/278] Upgrade libusb (1.0.29) --- app/deps/libusb.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/deps/libusb.sh b/app/deps/libusb.sh index 4be03eb1..887a2a77 100755 --- a/app/deps/libusb.sh +++ b/app/deps/libusb.sh @@ -5,10 +5,10 @@ cd "$DEPS_DIR" . common process_args "$@" -VERSION=1.0.28 +VERSION=1.0.29 FILENAME=libusb-$VERSION.tar.gz PROJECT_DIR=libusb-$VERSION -SHA256SUM=378b3709a405065f8f9fb9f35e82d666defde4d342c2a1b181a9ac134d23c6fe +SHA256SUM=7c2dd39c0b2589236e48c93247c986ae272e27570942b4163cb00a060fcf1b74 cd "$SOURCES_DIR" From dc169e425e9cc94e8e871dbeac24bdebd277bf39 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 19:39:48 +0200 Subject: [PATCH 265/278] Bump version to 3.3 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 19475e0b..45f1960c 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.2" + VALUE "ProductVersion", "3.3" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index b64a6c90..1e9a5729 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.2', + version: '3.3', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 02508001..059a6f30 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30200 - versionName "3.2" + versionCode 30300 + versionName "3.3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 8bb8632b..5b35e3ec 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=3.2 +SCRCPY_VERSION_NAME=3.3 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From 696402c68c5f91fa77c3ed03cd835dc4412a253e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 11 Jun 2025 22:15:30 +0200 Subject: [PATCH 266/278] Update links to 3.3 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 81399f52..dc00ac22 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.2) +# scrcpy (v3.3) scrcpy diff --git a/doc/build.md b/doc/build.md index afe8b21b..c915e367 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.2`][direct-scrcpy-server] - SHA-256: `b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0` + - [`scrcpy-server-v3.3`][direct-scrcpy-server] + SHA-256: `351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 979ef568..5cfd6e4e 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.2.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `df6cf000447428fcde322022848d655ff0211d98688d0f17cbbf21be9c1272be` + - [`scrcpy-linux-x86_64-v3.3.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `a0abf37003c3c47a53c1b2a12420296a2b0ee323cf3610fd6fbf9d9bab9d99f3` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-linux-x86_64-v3.2.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-linux-x86_64-v3.3.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index b0335d18..73a982f6 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.2.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `f6d1f3c5f74d4d46f5080baa5b56b69f5edbf698d47e0cf4e2a1fd5058f9507b` + - [`scrcpy-macos-aarch64-v3.3.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `7a4cdaeb8ba74593edda278c000ddedc8d70a51263a80b16a6345475d42ac21e` - - [`scrcpy-macos-x86_64-v3.2.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `e337d5cf0ba4e1281699c338ce5f104aee96eb7b2893dc851399b6643eb4044e` + - [`scrcpy-macos-x86_64-v3.3.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `bb3c13aac166b92539371883a8781aa861a7cd18e3e6077e570ab7a1f562f774` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-aarch64-v3.2.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-macos-x86_64-v3.2.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-aarch64-v3.3.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-x86_64-v3.3.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index fb3e3887..7935461d 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.2.zip`][direct-win64] (64-bit) - SHA-256: `eaa27133e0520979873ba57ad651560a4cc2618373bd05450b23a84d32beafd0` - - [`scrcpy-win32-v3.2.zip`][direct-win32] (32-bit) - SHA-256: `4a3407d7f0c2c8a03e22a12cf0b5e1e585a5056fe23c8e5cf3252207c6fa8357` + - [`scrcpy-win64-v3.3.zip`][direct-win64] (64-bit) + SHA-256: `a120cb4be7cde2891af38e83d2008173a0b6b6b5e344b2dfe668d0f892999933` + - [`scrcpy-win32-v3.3.zip`][direct-win32] (32-bit) + SHA-256: `e409ab83f8c57bd6ac741d652635cab7699fcf3d384e233833872f117b993ca6` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win64-v3.2.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-win32-v3.2.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win64-v3.3.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win32-v3.3.zip and extract it. diff --git a/install_release.sh b/install_release.sh index 2d2d2c2f..aabe9873 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.2/scrcpy-server-v3.2 -PREBUILT_SERVER_SHA256=b920e0ea01936bf2482f4ba2fa985c22c13c621999e3d33b45baa5acfc1ea3d0 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3 +PREBUILT_SERVER_SHA256=351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 4e1cf13a5092bfe8651c8f55eda3861b7d01b64a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 12 Jun 2025 09:03:39 +0200 Subject: [PATCH 267/278] Run a main looper in the cleanup process Since a main looper is explicitly run in the main process, the initialization of workarounds no longer calls Looper.prepareMainLooper(), leading to a crash: java.lang.RuntimeException: Can't create handler inside thread Thread[main,5,main] that has not called Looper.prepare() As a result, --power-off-on-close was broken. Refs 283326b2f6fa3fdaeecc181f69a3a4bcd429c06a Fixes #6146 --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 51db985c..77018afa 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -7,6 +7,7 @@ import com.genymobile.scrcpy.util.SettingsException; import com.genymobile.scrcpy.wrappers.ServiceManager; import android.os.BatteryManager; +import android.os.Looper; import android.system.ErrnoException; import android.system.Os; @@ -179,6 +180,11 @@ public final class CleanUp { } } + @SuppressWarnings("deprecation") + private static void prepareMainLooper() { + Looper.prepareMainLooper(); + } + public static void main(String... args) { try { // Start a new session to avoid being terminated along with the server process on some devices @@ -188,6 +194,9 @@ public final class CleanUp { } unlinkSelf(); + // Needed for workarounds + prepareMainLooper(); + int displayId = Integer.parseInt(args[0]); int restoreStayOn = Integer.parseInt(args[1]); boolean disableShowTouches = Boolean.parseBoolean(args[2]); From 38256d8ff9d019f8d4fd84719eeafd0214c836e8 Mon Sep 17 00:00:00 2001 From: berk ziya Date: Thu, 12 Jun 2025 16:27:40 +0300 Subject: [PATCH 268/278] Fix deprecated brew command `brew cask` is an outdated command, replaced by `brew install --cask`. Refs #5398 PR #6149 Signed-off-by: Romain Vimont --- app/src/adb/adb.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/adb/adb.c b/app/src/adb/adb.c index 40e9e968..9e9cfd6b 100644 --- a/app/src/adb/adb.c +++ b/app/src/adb/adb.c @@ -110,7 +110,7 @@ show_adb_installation_msg(void) { } pkg_managers[] = { {"apt", "apt install adb"}, {"apt-get", "apt-get install adb"}, - {"brew", "brew cask install android-platform-tools"}, + {"brew", "brew install --cask android-platform-tools"}, {"dnf", "dnf install android-tools"}, {"emerge", "emerge dev-util/android-tools"}, {"pacman", "pacman -S android-tools"}, From 772f42134a327eea60955463d0ee8bb712168dd0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Jun 2025 09:36:34 +0200 Subject: [PATCH 269/278] Use Context.CLIPBOARD_SERVICE directly The constant is defined in Context, not FakeContext. --- .../java/com/genymobile/scrcpy/wrappers/ClipboardManager.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java index fae8a056..54936122 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -3,13 +3,13 @@ package com.genymobile.scrcpy.wrappers; import com.genymobile.scrcpy.FakeContext; import android.content.ClipData; +import android.content.Context; public final class ClipboardManager { private final android.content.ClipboardManager manager; static ClipboardManager create() { - android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get() - .getSystemService(FakeContext.CLIPBOARD_SERVICE); + android.content.ClipboardManager manager = (android.content.ClipboardManager) FakeContext.get().getSystemService(Context.CLIPBOARD_SERVICE); if (manager == null) { // Some devices have no clipboard manager // From cd3a5d50b650da6dcafbdbddd606ef5031f1833a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Jun 2025 09:24:36 +0200 Subject: [PATCH 270/278] Create ClipboardManager from the main thread The ClipboardManager is instantiated by the first call to ServiceManager.getClipboardManager(). Now that scrcpy uses android.content.ClipboardManager directly, it must ensure that it is created on the main thread (or at least on a thread with a Looper), to avoid the following error: > Can't create handler inside thread that has not called > Looper.prepare() Refs 8a02e3c2f58cffc3fdd8c08b26aae04bbf9d5a97 Fixes #6151 --- .../main/java/com/genymobile/scrcpy/control/Controller.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java index bfbee7dc..b4a8e3ca 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/Controller.java @@ -114,9 +114,10 @@ public class Controller implements AsyncProcessor, VirtualDisplayListener { Ln.w("Input events are not supported for secondary displays before Android 10"); } + // Make sure the clipboard manager is always created from the main thread (even if clipboardAutosync is disabled) + ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardAutosync) { // If control and autosync are enabled, synchronize Android clipboard to the computer automatically - ClipboardManager clipboardManager = ServiceManager.getClipboardManager(); if (clipboardManager != null) { clipboardManager.addPrimaryClipChangedListener(() -> { if (isSettingClipboard.get()) { From d74cfd5711b2ae2a12e38c0e7111e1af0f9af72c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 13 Jun 2025 09:40:20 +0200 Subject: [PATCH 271/278] Silence DiscouragedPrivateApi lint warning --- server/src/main/java/com/genymobile/scrcpy/Server.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index c1d8c1f2..46f3294f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -24,6 +24,7 @@ import com.genymobile.scrcpy.video.SurfaceCapture; import com.genymobile.scrcpy.video.SurfaceEncoder; import com.genymobile.scrcpy.video.VideoSource; +import android.annotation.SuppressLint; import android.os.Build; import android.os.Looper; @@ -199,6 +200,7 @@ public final class Server { Looper.prepare(); synchronized (Looper.class) { try { + @SuppressLint("DiscouragedPrivateApi") Field field = Looper.class.getDeclaredField("sMainLooper"); field.setAccessible(true); field.set(null, Looper.myLooper()); From 98d30288f78b0dd40ae1aa1b285c45f5769f49fc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 17 Jun 2025 21:02:41 +0200 Subject: [PATCH 272/278] Prepare the main looper earlier The looper must be initialized before listing apps, to avoid the following error: > Can't create handler inside thread that has not called > Looper.prepare() Refs 283326b2f6fa3fdaeecc181f69a3a4bcd429c06a Fixes #6165 --- server/src/main/java/com/genymobile/scrcpy/Server.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 46f3294f..a08c948c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -97,7 +97,6 @@ public final class Server { boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); - prepareMainLooper(); Workarounds.apply(); List asyncProcessors = new ArrayList<>(); @@ -230,6 +229,8 @@ public final class Server { Ln.e("Exception on thread " + t, e); }); + prepareMainLooper(); + Options options = Options.parse(args); Ln.disableSystemStreams(); From 9787fe5d261df8255e49b65f37e2d89bf1a129fa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 19 Jun 2025 20:50:26 +0200 Subject: [PATCH 273/278] Preserve original scroll values in mouse event Clamp scroll values to [-1, 1] only for the SDK mouse. HID mouse implementations perform their own clamping to [-127, 127] (in hid_mouse.c). PR #6172 --- app/src/input_manager.c | 8 ++++---- app/src/mouse_sdk.c | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/input_manager.c b/app/src/input_manager.c index 635825c9..f7a787d1 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -897,11 +897,11 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, struct sc_mouse_scroll_event evt = { .position = sc_input_manager_get_position(im, mouse_x, mouse_y), #if SDL_VERSION_ATLEAST(2, 0, 18) - .hscroll = CLAMP(event->preciseX, -1.0f, 1.0f), - .vscroll = CLAMP(event->preciseY, -1.0f, 1.0f), + .hscroll = event->preciseX, + .vscroll = event->preciseY, #else - .hscroll = CLAMP(event->x, -1, 1), - .vscroll = CLAMP(event->y, -1, 1), + .hscroll = event->x, + .vscroll = event->y, #endif .buttons_state = im->mouse_buttons_state, }; diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index 7eceffa7..1b05d02b 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -113,8 +113,8 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = event->position, - .hscroll = event->hscroll, - .vscroll = event->vscroll, + .hscroll = CLAMP(event->hscroll, -1, 1), + .vscroll = CLAMP(event->vscroll, -1, 1), .buttons = convert_mouse_buttons(event->buttons_state), }, }; From 7c8bdccbdc24b616c8d4ada861c424b3686912ea Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Jun 2025 09:06:10 +0200 Subject: [PATCH 274/278] Extend value range for SDK mouse scrolling SDL precise scrolling can sometimes produce values greater than 1 or less than -1. On the wire, the value is encoded as a 16-bit fixed-point number. Previously, the range was interpreted as [-1, 1], using 1 bit for the integral part (the sign) and 15 bits for the fractional part. To support larger values, interpret the range as [-16, 16] instead, using 5 bits for the integral part and 11 bits for the fractional part (which is more than enough). PR #6172 --- app/src/control_msg.c | 12 ++++++++---- app/src/mouse_sdk.c | 4 ++-- app/tests/test_control_msg_serialize.c | 8 ++++---- .../scrcpy/control/ControlMessageReader.java | 5 +++-- .../scrcpy/control/ControlMessageReaderTest.java | 4 ++-- 5 files changed, 19 insertions(+), 14 deletions(-) diff --git a/app/src/control_msg.c b/app/src/control_msg.c index e78f0c57..e46c6165 100644 --- a/app/src/control_msg.c +++ b/app/src/control_msg.c @@ -127,10 +127,14 @@ sc_control_msg_serialize(const struct sc_control_msg *msg, uint8_t *buf) { return 32; case SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT: write_position(&buf[1], &msg->inject_scroll_event.position); - int16_t hscroll = - sc_float_to_i16fp(msg->inject_scroll_event.hscroll); - int16_t vscroll = - sc_float_to_i16fp(msg->inject_scroll_event.vscroll); + // Accept values in the range [-16, 16]. + // Normalize to [-1, 1] in order to use sc_float_to_i16fp(). + float hscroll_norm = msg->inject_scroll_event.hscroll / 16; + hscroll_norm = CLAMP(hscroll_norm, -1, 1); + float vscroll_norm = msg->inject_scroll_event.vscroll / 16; + vscroll_norm = CLAMP(vscroll_norm, -1, 1); + int16_t hscroll = sc_float_to_i16fp(hscroll_norm); + int16_t vscroll = sc_float_to_i16fp(vscroll_norm); sc_write16be(&buf[13], (uint16_t) hscroll); sc_write16be(&buf[15], (uint16_t) vscroll); sc_write32be(&buf[17], msg->inject_scroll_event.buttons); diff --git a/app/src/mouse_sdk.c b/app/src/mouse_sdk.c index 1b05d02b..7eceffa7 100644 --- a/app/src/mouse_sdk.c +++ b/app/src/mouse_sdk.c @@ -113,8 +113,8 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, .type = SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, .inject_scroll_event = { .position = event->position, - .hscroll = CLAMP(event->hscroll, -1, 1), - .vscroll = CLAMP(event->vscroll, -1, 1), + .hscroll = event->hscroll, + .vscroll = event->vscroll, .buttons = convert_mouse_buttons(event->buttons_state), }, }; diff --git a/app/tests/test_control_msg_serialize.c b/app/tests/test_control_msg_serialize.c index af97182d..0d19919e 100644 --- a/app/tests/test_control_msg_serialize.c +++ b/app/tests/test_control_msg_serialize.c @@ -127,8 +127,8 @@ static void test_serialize_inject_scroll_event(void) { .height = 1920, }, }, - .hscroll = 1, - .vscroll = -1, + .hscroll = 16, + .vscroll = -16, .buttons = 1, }, }; @@ -141,8 +141,8 @@ static void test_serialize_inject_scroll_event(void) { SC_CONTROL_MSG_TYPE_INJECT_SCROLL_EVENT, 0x00, 0x00, 0x01, 0x04, 0x00, 0x00, 0x04, 0x02, // 260 1026 0x04, 0x38, 0x07, 0x80, // 1080 1920 - 0x7F, 0xFF, // 1 (float encoded as i16) - 0x80, 0x00, // -1 (float encoded as i16) + 0x7F, 0xFF, // 16 (float encoded as i16 in the range [-16, 16]) + 0x80, 0x00, // -16 (float encoded as i16 in the range [-16, 16]) 0x00, 0x00, 0x00, 0x01, // 1 }; assert(!memcmp(buf, expected, sizeof(expected))); diff --git a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java index e503ec61..830a7ec7 100644 --- a/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java +++ b/server/src/main/java/com/genymobile/scrcpy/control/ControlMessageReader.java @@ -112,8 +112,9 @@ public class ControlMessageReader { private ControlMessage parseInjectScrollEvent() throws IOException { Position position = parsePosition(); - float hScroll = Binary.i16FixedPointToFloat(dis.readShort()); - float vScroll = Binary.i16FixedPointToFloat(dis.readShort()); + // Binary.i16FixedPointToFloat() decodes values assuming the full range is [-1, 1], but the actual range is [-16, 16]. + float hScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; + float vScroll = Binary.i16FixedPointToFloat(dis.readShort()) * 16; int buttons = dis.readInt(); return ControlMessage.createInjectScrollEvent(position, hScroll, vScroll, buttons); } diff --git a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java index 74df064f..0cc0a6b5 100644 --- a/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java +++ b/server/src/test/java/com/genymobile/scrcpy/control/ControlMessageReaderTest.java @@ -125,7 +125,7 @@ public class ControlMessageReaderTest { dos.writeShort(1080); dos.writeShort(1920); dos.writeShort(0); // 0.0f encoded as i16 - dos.writeShort(0x8000); // -1.0f encoded as i16 + dos.writeShort(0x8000); // -16.0f encoded as i16 (the range is [-16, 16]) dos.writeInt(1); byte[] packet = bos.toByteArray(); @@ -139,7 +139,7 @@ public class ControlMessageReaderTest { Assert.assertEquals(1080, event.getPosition().getScreenSize().getWidth()); Assert.assertEquals(1920, event.getPosition().getScreenSize().getHeight()); Assert.assertEquals(0f, event.getHScroll(), 0f); - Assert.assertEquals(-1f, event.getVScroll(), 0f); + Assert.assertEquals(-16f, event.getVScroll(), 0f); Assert.assertEquals(1, event.getButtons()); Assert.assertEquals(-1, bis.read()); // EOS From fc75319bb291121116419c784a5fa507fd820eca Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Jun 2025 18:26:13 +0200 Subject: [PATCH 275/278] Fix HID mouse support with SDL precise scrolling Over HID, only integral scroll values can be sent. When SDL precise scrolling is active, scroll events may include fractional values (e.g., 0.05), which are truncated to 0 in the HID event. To fix the problem, use the integral scroll value reported by SDL, which internally accumulates fractional deltas. Fixes #6156 PR #6172 --- app/src/hid/hid_mouse.c | 12 ++++++++---- app/src/hid/hid_mouse.h | 2 +- app/src/input_events.h | 2 ++ app/src/input_manager.c | 2 ++ app/src/uhid/mouse_uhid.c | 4 +++- app/src/usb/mouse_aoa.c | 4 +++- app/src/usb/screen_otg.c | 7 +++++++ 7 files changed, 26 insertions(+), 7 deletions(-) diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index 29cfc594..e1fff45b 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -175,19 +175,23 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, data[3] = 0; // wheel coordinates only used for scrolling } -void +bool sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event) { + if (!event->vscroll_int) { + // Need a full integral value for HID + return false; + } + sc_hid_mouse_input_init(hid_input); uint8_t *data = hid_input->data; data[0] = 0; // buttons state irrelevant (and unknown) data[1] = 0; // no x motion data[2] = 0; // no y motion - // In practice, vscroll is always -1, 0 or 1, but in theory other values - // are possible - data[3] = CLAMP(event->vscroll, -127, 127); + data[3] = CLAMP(event->vscroll_int, -127, 127); // Horizontal scrolling ignored + return true; } void sc_hid_mouse_generate_open(struct sc_hid_open *hid_open) { diff --git a/app/src/hid/hid_mouse.h b/app/src/hid/hid_mouse.h index 06c61dd1..4ae4bfd4 100644 --- a/app/src/hid/hid_mouse.h +++ b/app/src/hid/hid_mouse.h @@ -22,7 +22,7 @@ void sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, const struct sc_mouse_click_event *event); -void +bool sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event); diff --git a/app/src/input_events.h b/app/src/input_events.h index 0c022acc..1e34b50e 100644 --- a/app/src/input_events.h +++ b/app/src/input_events.h @@ -393,6 +393,8 @@ struct sc_mouse_scroll_event { struct sc_position position; float hscroll; float vscroll; + int32_t hscroll_int; + int32_t vscroll_int; uint8_t buttons_state; // bitwise-OR of sc_mouse_button values }; diff --git a/app/src/input_manager.c b/app/src/input_manager.c index f7a787d1..3e4dd0f3 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -903,6 +903,8 @@ sc_input_manager_process_mouse_wheel(struct sc_input_manager *im, .hscroll = event->x, .vscroll = event->y, #endif + .hscroll_int = event->x, + .vscroll_int = event->y, .buttons_state = im->mouse_buttons_state, }; diff --git a/app/src/uhid/mouse_uhid.c b/app/src/uhid/mouse_uhid.c index 7fed8383..869e48a4 100644 --- a/app/src/uhid/mouse_uhid.c +++ b/app/src/uhid/mouse_uhid.c @@ -55,7 +55,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_uhid *mouse = DOWNCAST(mp); struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_scroll(&hid_input, event); + if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { + return; + } sc_mouse_uhid_send_input(mouse, &hid_input, "mouse scroll"); } diff --git a/app/src/usb/mouse_aoa.c b/app/src/usb/mouse_aoa.c index b64e9b12..fd5fa5e0 100644 --- a/app/src/usb/mouse_aoa.c +++ b/app/src/usb/mouse_aoa.c @@ -42,7 +42,9 @@ sc_mouse_processor_process_mouse_scroll(struct sc_mouse_processor *mp, struct sc_mouse_aoa *mouse = DOWNCAST(mp); struct sc_hid_input hid_input; - sc_hid_mouse_generate_input_from_scroll(&hid_input, event); + if (!sc_hid_mouse_generate_input_from_scroll(&hid_input, event)) { + return; + } if (!sc_aoa_push_input(mouse->aoa, &hid_input)) { LOGW("Could not push AOA HID input (mouse scroll)"); diff --git a/app/src/usb/screen_otg.c b/app/src/usb/screen_otg.c index 02edc3a3..5c580df9 100644 --- a/app/src/usb/screen_otg.c +++ b/app/src/usb/screen_otg.c @@ -164,8 +164,15 @@ sc_screen_otg_process_mouse_wheel(struct sc_screen_otg *screen, struct sc_mouse_scroll_event evt = { // .position not used for HID events +#if SDL_VERSION_ATLEAST(2, 0, 18) + .hscroll = event->preciseX, + .vscroll = event->preciseY, +#else .hscroll = event->x, .vscroll = event->y, +#endif + .hscroll_int = event->x, + .vscroll_int = event->y, .buttons_state = sc_mouse_buttons_state_from_sdl(sdl_buttons_state), }; From 4841fdd1eff58f313a62c539f31453eac3e21b62 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 18 Jun 2025 18:29:04 +0200 Subject: [PATCH 276/278] Add horizontal scrolling support for HID mouse PR #6172 --- app/src/hid/hid_mouse.c | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/app/src/hid/hid_mouse.c b/app/src/hid/hid_mouse.c index e1fff45b..33f0807e 100644 --- a/app/src/hid/hid_mouse.c +++ b/app/src/hid/hid_mouse.c @@ -3,8 +3,8 @@ #include // 1 byte for buttons + padding, 1 byte for X position, 1 byte for Y position, -// 1 byte for wheel motion -#define SC_HID_MOUSE_INPUT_SIZE 4 +// 1 byte for wheel motion, 1 byte for hozizontal scrolling +#define SC_HID_MOUSE_INPUT_SIZE 5 /** * Mouse descriptor from the specification: @@ -75,6 +75,21 @@ static const uint8_t SC_HID_MOUSE_REPORT_DESC[] = { // Input (Data, Variable, Relative): 3 position bytes (X, Y, Wheel) 0x81, 0x06, + // Usage Page (Consumer Page) + 0x05, 0x0C, + // Usage(AC Pan) + 0x0A, 0x38, 0x02, + // Logical Minimum (-127) + 0x15, 0x81, + // Logical Maximum (127) + 0x25, 0x7F, + // Report Size (8) + 0x75, 0x08, + // Report Count (1) + 0x95, 0x01, + // Input (Data, Variable, Relative): 1 byte (AC Pan) + 0x81, 0x06, + // End Collection 0xC0, @@ -160,7 +175,8 @@ sc_hid_mouse_generate_input_from_motion(struct sc_hid_input *hid_input, data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = CLAMP(event->xrel, -127, 127); data[2] = CLAMP(event->yrel, -127, 127); - data[3] = 0; // wheel coordinates only used for scrolling + data[3] = 0; // no vertical scrolling + data[4] = 0; // no horizontal scrolling } void @@ -172,13 +188,14 @@ sc_hid_mouse_generate_input_from_click(struct sc_hid_input *hid_input, data[0] = sc_hid_buttons_from_buttons_state(event->buttons_state); data[1] = 0; // no x motion data[2] = 0; // no y motion - data[3] = 0; // wheel coordinates only used for scrolling + data[3] = 0; // no vertical scrolling + data[4] = 0; // no horizontal scrolling } bool sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, const struct sc_mouse_scroll_event *event) { - if (!event->vscroll_int) { + if (!event->vscroll_int && !event->hscroll_int) { // Need a full integral value for HID return false; } @@ -190,7 +207,7 @@ sc_hid_mouse_generate_input_from_scroll(struct sc_hid_input *hid_input, data[1] = 0; // no x motion data[2] = 0; // no y motion data[3] = CLAMP(event->vscroll_int, -127, 127); - // Horizontal scrolling ignored + data[4] = CLAMP(event->hscroll_int, -127, 127); return true; } From 5b18ce0d2e91fd9875b3fe3b10a2c5dcb4399cd1 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Jun 2025 19:42:40 +0200 Subject: [PATCH 277/278] Bump version to 3.3.1 --- app/scrcpy-windows.rc | 2 +- meson.build | 2 +- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/scrcpy-windows.rc b/app/scrcpy-windows.rc index 45f1960c..9c5374ae 100644 --- a/app/scrcpy-windows.rc +++ b/app/scrcpy-windows.rc @@ -13,7 +13,7 @@ BEGIN VALUE "LegalCopyright", "Romain Vimont, Genymobile" VALUE "OriginalFilename", "scrcpy.exe" VALUE "ProductName", "scrcpy" - VALUE "ProductVersion", "3.3" + VALUE "ProductVersion", "3.3.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 1e9a5729..d991d672 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '3.3', + version: '3.3.1', meson_version: '>= 0.49', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 059a6f30..31092b12 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 35 - versionCode 30300 - versionName "3.3" + versionCode 30301 + versionName "3.3.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5b35e3ec..193a9902 100755 --- a/server/build_without_gradle.sh +++ b/server/build_without_gradle.sh @@ -12,7 +12,7 @@ set -e SCRCPY_DEBUG=false -SCRCPY_VERSION_NAME=3.3 +SCRCPY_VERSION_NAME=3.3.1 PLATFORM=${ANDROID_PLATFORM:-35} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-35.0.0} From f01231dff8294fe2c99045a4f9a14b233a71bb86 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 20 Jun 2025 20:14:42 +0200 Subject: [PATCH 278/278] Update links to 3.3.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/linux.md | 6 +++--- doc/macos.md | 12 ++++++------ doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index dc00ac22..d886d23c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ source for the project. Do not download releases from random websites, even if their name contains `scrcpy`.** -# scrcpy (v3.3) +# scrcpy (v3.3.1) scrcpy diff --git a/doc/build.md b/doc/build.md index c915e367..7f76b4fd 100644 --- a/doc/build.md +++ b/doc/build.md @@ -233,10 +233,10 @@ install` must be run as root)._ #### Option 2: Use prebuilt server - - [`scrcpy-server-v3.3`][direct-scrcpy-server] - SHA-256: `351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51` + - [`scrcpy-server-v3.3.1`][direct-scrcpy-server] + SHA-256: `a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/linux.md b/doc/linux.md index 5cfd6e4e..be433df4 100644 --- a/doc/linux.md +++ b/doc/linux.md @@ -6,11 +6,11 @@ Download a static build of the [latest release]: - - [`scrcpy-linux-x86_64-v3.3.tar.gz`][direct-linux-x86_64] (x86_64) - SHA-256: `a0abf37003c3c47a53c1b2a12420296a2b0ee323cf3610fd6fbf9d9bab9d99f3` + - [`scrcpy-linux-x86_64-v3.3.1.tar.gz`][direct-linux-x86_64] (x86_64) + SHA-256: `bbfe54c6b178adafeaffbbfbbc1548a74486553170c63e63bdd41863ad123422` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-linux-x86_64-v3.3.tar.gz +[direct-linux-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-linux-x86_64-v3.3.1.tar.gz and extract it. diff --git a/doc/macos.md b/doc/macos.md index 73a982f6..f6b01c30 100644 --- a/doc/macos.md +++ b/doc/macos.md @@ -6,15 +6,15 @@ Download a static build of the [latest release]: - - [`scrcpy-macos-aarch64-v3.3.tar.gz`][direct-macos-aarch64] (aarch64) - SHA-256: `7a4cdaeb8ba74593edda278c000ddedc8d70a51263a80b16a6345475d42ac21e` + - [`scrcpy-macos-aarch64-v3.3.1.tar.gz`][direct-macos-aarch64] (aarch64) + SHA-256: `907b925900ebd8499c1e47acc9689a95bd3a6f9930eb1d7bdfbca8375ae4f139` - - [`scrcpy-macos-x86_64-v3.3.tar.gz`][direct-macos-x86_64] (x86_64) - SHA-256: `bb3c13aac166b92539371883a8781aa861a7cd18e3e6077e570ab7a1f562f774` + - [`scrcpy-macos-x86_64-v3.3.1.tar.gz`][direct-macos-x86_64] (x86_64) + SHA-256: `69772491dad718eea82fc65c8e89febff7d41c4ce6faff02f4789a588d10fd7d` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-aarch64-v3.3.tar.gz -[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-macos-x86_64-v3.3.tar.gz +[direct-macos-aarch64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-aarch64-v3.3.1.tar.gz +[direct-macos-x86_64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-macos-x86_64-v3.3.1.tar.gz and extract it. diff --git a/doc/windows.md b/doc/windows.md index 7935461d..8fa1921f 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -6,14 +6,14 @@ Download the [latest release]: - - [`scrcpy-win64-v3.3.zip`][direct-win64] (64-bit) - SHA-256: `a120cb4be7cde2891af38e83d2008173a0b6b6b5e344b2dfe668d0f892999933` - - [`scrcpy-win32-v3.3.zip`][direct-win32] (32-bit) - SHA-256: `e409ab83f8c57bd6ac741d652635cab7699fcf3d384e233833872f117b993ca6` + - [`scrcpy-win64-v3.3.1.zip`][direct-win64] (64-bit) + SHA-256: `4fcad494772a3ae5de9a133149f8856d2fc429b41795f7cf7c754e0c1bb6fbc0` + - [`scrcpy-win32-v3.3.1.zip`][direct-win32] (32-bit) + SHA-256: `ccdf1b4f5d19dfe760446a107e55b0a010a00e097d46533a161499c9333a20a6` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win64-v3.3.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-win32-v3.3.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win64-v3.3.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-win32-v3.3.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index aabe9873..d960932b 100755 --- a/install_release.sh +++ b/install_release.sh @@ -2,8 +2,8 @@ set -e BUILDDIR=build-auto -PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3/scrcpy-server-v3.3 -PREBUILT_SERVER_SHA256=351cb2edc7e4c2c75f09a7933fdabcf137be52e2602df154f24ec02db46e9e51 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v3.3.1/scrcpy-server-v3.3.1 +PREBUILT_SERVER_SHA256=a0f70b20aa4998fbf658c94118cd6c8dab6abbb0647a3bdab344d70bc1ebcbb8 echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server