From 8c3e2bae7be47a0f17ddec8da25b89e0aea2617e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Nov 2023 19:02:58 +0100 Subject: [PATCH 01/58] Simplify Application instantiation The constructor is public. --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index b8ee68ca..e8da9540 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -143,7 +143,7 @@ public final class Workarounds { try { fillActivityThread(); - Application app = Application.class.newInstance(); + Application app = new Application(); Field baseField = ContextWrapper.class.getDeclaredField("mBase"); baseField.setAccessible(true); baseField.set(app, FakeContext.get()); From 85a0b935c9d70a7f082eb3df5f3c9f61ea48009a Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Nov 2023 19:01:08 +0100 Subject: [PATCH 02/58] Always assign a system context as base context FakeContext used ActivityThread.getSystemContext() as base context only in some cases, because it caused problems on some devices: - warnings on Xiaomi devices [1], which are now fixed by b8c5853aa6ac9cfbe3fb4e46bf10978b3fa212e3 - issues related to Looper [2], which are solved by just calling Looper.prepare*() Therefore, we can now always assign a base context, which simplifies and helps to solve camera issues on some devices (#4392). [1] [2] Fixes #4392 --- .../com/genymobile/scrcpy/FakeContext.java | 6 +- .../com/genymobile/scrcpy/Workarounds.java | 71 ++++++++----------- 2 files changed, 33 insertions(+), 44 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 6501d4cf..520e0378 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,11 +2,11 @@ package com.genymobile.scrcpy; import android.annotation.TargetApi; import android.content.AttributionSource; -import android.content.MutableContextWrapper; +import android.content.ContextWrapper; import android.os.Build; import android.os.Process; -public final class FakeContext extends MutableContextWrapper { +public final class FakeContext extends ContextWrapper { public static final String PACKAGE_NAME = "com.android.shell"; public static final int ROOT_UID = 0; // Like android.os.Process.ROOT_UID, but before API 29 @@ -18,7 +18,7 @@ public final class FakeContext extends MutableContextWrapper { } private FakeContext() { - super(null); + super(Workarounds.getSystemContext()); } @Override diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index e8da9540..5b3a5c8c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -21,18 +21,34 @@ import java.lang.reflect.Method; public final class Workarounds { - private static Class activityThreadClass; - private static Object activityThread; + private static final Class ACTIVITY_THREAD_CLASS; + private static final Object ACTIVITY_THREAD; + + static { + prepareMainLooper(); + + try { + // ActivityThread activityThread = new ActivityThread(); + ACTIVITY_THREAD_CLASS = Class.forName("android.app.ActivityThread"); + Constructor activityThreadConstructor = ACTIVITY_THREAD_CLASS.getDeclaredConstructor(); + activityThreadConstructor.setAccessible(true); + ACTIVITY_THREAD = activityThreadConstructor.newInstance(); + + // ActivityThread.sCurrentActivityThread = activityThread; + Field sCurrentActivityThreadField = ACTIVITY_THREAD_CLASS.getDeclaredField("sCurrentActivityThread"); + sCurrentActivityThreadField.setAccessible(true); + sCurrentActivityThreadField.set(null, ACTIVITY_THREAD); + } catch (Exception e) { + throw new AssertionError(e); + } + } private Workarounds() { // not instantiable } public static void apply(boolean audio, boolean camera) { - Workarounds.prepareMainLooper(); - boolean mustFillAppInfo = false; - boolean mustFillBaseContext = false; boolean mustFillAppContext = false; if (Build.BRAND.equalsIgnoreCase("meizu")) { @@ -53,7 +69,6 @@ public final class Workarounds { // - // - mustFillAppInfo = true; - mustFillBaseContext = true; mustFillAppContext = true; } @@ -66,15 +81,11 @@ public final class Workarounds { if (camera) { mustFillAppInfo = true; - mustFillBaseContext = true; } if (mustFillAppInfo) { Workarounds.fillAppInfo(); } - if (mustFillBaseContext) { - Workarounds.fillBaseContext(); - } if (mustFillAppContext) { Workarounds.fillAppContext(); } @@ -93,27 +104,9 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") - private static void fillActivityThread() throws Exception { - if (activityThread == null) { - // ActivityThread activityThread = new ActivityThread(); - activityThreadClass = Class.forName("android.app.ActivityThread"); - Constructor activityThreadConstructor = activityThreadClass.getDeclaredConstructor(); - activityThreadConstructor.setAccessible(true); - activityThread = activityThreadConstructor.newInstance(); - - // ActivityThread.sCurrentActivityThread = activityThread; - Field sCurrentActivityThreadField = activityThreadClass.getDeclaredField("sCurrentActivityThread"); - sCurrentActivityThreadField.setAccessible(true); - sCurrentActivityThreadField.set(null, activityThread); - } - } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") private static void fillAppInfo() { try { - fillActivityThread(); - // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); Class appBindDataClass = Class.forName("android.app.ActivityThread$AppBindData"); Constructor appBindDataConstructor = appBindDataClass.getDeclaredConstructor(); @@ -129,9 +122,9 @@ public final class Workarounds { appInfoField.set(appBindData, applicationInfo); // activityThread.mBoundApplication = appBindData; - Field mBoundApplicationField = activityThreadClass.getDeclaredField("mBoundApplication"); + Field mBoundApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mBoundApplication"); mBoundApplicationField.setAccessible(true); - mBoundApplicationField.set(activityThread, appBindData); + mBoundApplicationField.set(ACTIVITY_THREAD, appBindData); } catch (Throwable throwable) { // this is a workaround, so failing is not an error Ln.d("Could not fill app info: " + throwable.getMessage()); @@ -141,33 +134,29 @@ public final class Workarounds { @SuppressLint("PrivateApi,DiscouragedPrivateApi") private static void fillAppContext() { try { - fillActivityThread(); - Application app = new Application(); Field baseField = ContextWrapper.class.getDeclaredField("mBase"); baseField.setAccessible(true); baseField.set(app, FakeContext.get()); // activityThread.mInitialApplication = app; - Field mInitialApplicationField = activityThreadClass.getDeclaredField("mInitialApplication"); + Field mInitialApplicationField = ACTIVITY_THREAD_CLASS.getDeclaredField("mInitialApplication"); mInitialApplicationField.setAccessible(true); - mInitialApplicationField.set(activityThread, app); + mInitialApplicationField.set(ACTIVITY_THREAD, app); } catch (Throwable throwable) { // this is a workaround, so failing is not an error Ln.d("Could not fill app context: " + throwable.getMessage()); } } - private static void fillBaseContext() { + static Context getSystemContext() { try { - fillActivityThread(); - - Method getSystemContextMethod = activityThreadClass.getDeclaredMethod("getSystemContext"); - Context context = (Context) getSystemContextMethod.invoke(activityThread); - FakeContext.get().setBaseContext(context); + Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); + return (Context) getSystemContextMethod.invoke(ACTIVITY_THREAD); } catch (Throwable throwable) { // this is a workaround, so failing is not an error - Ln.d("Could not fill base context: " + throwable.getMessage()); + Ln.d("Could not get system context: " + throwable.getMessage()); + return null; } } From 8d76b3e06dcd4b996ef20779feaf4cd8494a4a5c Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Nov 2023 19:07:08 +0100 Subject: [PATCH 03/58] Fill application context for camera Using the camera fails on some devices without a proper application context. Fixes #4392 --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 5b3a5c8c..77827c47 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -81,6 +81,7 @@ public final class Workarounds { if (camera) { mustFillAppInfo = true; + mustFillAppContext = true; } if (mustFillAppInfo) { From 4e4ddc499fcf571109126fbe3722eb0cc60fe1b0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 3 Nov 2023 19:07:15 +0100 Subject: [PATCH 04/58] Return the FakeContext as application context This avoids getApplicationContext() to return null and cause NullPointerException. Fixes #4392 --- server/src/main/java/com/genymobile/scrcpy/FakeContext.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java index 520e0378..2ea7bf4a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/FakeContext.java +++ b/server/src/main/java/com/genymobile/scrcpy/FakeContext.java @@ -2,6 +2,7 @@ package com.genymobile.scrcpy; import android.annotation.TargetApi; import android.content.AttributionSource; +import android.content.Context; import android.content.ContextWrapper; import android.os.Build; import android.os.Process; @@ -44,4 +45,9 @@ public final class FakeContext extends ContextWrapper { public int getDeviceId() { return 0; } + + @Override + public Context getApplicationContext() { + return this; + } } From ccaa832f48d0454986777d9521e1028ec0d3eb35 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 5 Nov 2023 11:51:32 +0100 Subject: [PATCH 05/58] Simplify --list-cameras output Remove --video-source=camera from the output of --list-cameras (this is implicit). --- server/src/main/java/com/genymobile/scrcpy/LogUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index 8dc54629..70140525 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -93,7 +93,7 @@ public final class LogUtils { builder.append("\n (none)"); } else { for (String id : cameraIds) { - builder.append("\n --video-source=camera --camera-id=").append(id); + builder.append("\n --camera-id=").append(id); CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); int facing = characteristics.get(CameraCharacteristics.LENS_FACING); From 11d738321f8661a46c5f211ec4285047657177cb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 5 Nov 2023 15:12:54 +0100 Subject: [PATCH 06/58] Recover on invalid camera FPS ranges Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper". Catch the exception to list the cameras anyway. Refs #4403 --- .../java/com/genymobile/scrcpy/LogUtils.java | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java index 70140525..efa0672b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/LogUtils.java +++ b/server/src/main/java/com/genymobile/scrcpy/LogUtils.java @@ -100,12 +100,19 @@ public final class LogUtils { builder.append(" (").append(getCameraFacingName(facing)).append(", "); Rect activeSize = characteristics.get(CameraCharacteristics.SENSOR_INFO_ACTIVE_ARRAY_SIZE); - builder.append(activeSize.width()).append("x").append(activeSize.height()).append(", "); + builder.append(activeSize.width()).append("x").append(activeSize.height()); - // 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).append(')'); + 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); + } catch (Exception e) { + // Some devices may provide invalid ranges, causing an IllegalArgumentException "lower must be less than or equal to upper" + Ln.w("Could not get available frame rates for camera " + id, e); + } + + builder.append(')'); if (includeSizes) { StreamConfigurationMap configs = characteristics.get(CameraCharacteristics.SCALER_STREAM_CONFIGURATION_MAP); From 3c456253242ed96796f71b2ab3dc66e2bedf6ea3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 10:54:13 +0100 Subject: [PATCH 07/58] Log recording RAW audio codec as error It is not possible to record with a RAW audio codec, so the log before exiting should be an error rather than a warning. --- 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 56b5cfb2..462465fa 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2353,7 +2353,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } if (opts->audio_codec == SC_CODEC_RAW) { - LOGW("Recording does not support RAW audio codec"); + LOGE("Recording does not support RAW audio codec"); return false; } From 9d5f53caa76151e0983700e4ae6ccb3a445e1379 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 11:04:21 +0100 Subject: [PATCH 08/58] Stop capture on any RAW audio error The server was stopped only if an IOException occurred during RAW audio capture, but it did not catch RuntimeExceptions. --- .../src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 7d2adade..6108c54b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -62,8 +62,8 @@ public final class AudioRawRecorder implements AsyncProcessor { record(); } catch (AudioCaptureForegroundException e) { // Do not print stack trace, a user-friendly error-message has already been logged - } catch (IOException e) { - Ln.e("Audio recording error", e); + } catch (Throwable t) { + Ln.e("Audio recording error", t); fatalError = true; } finally { Ln.d("Audio recorder stopped"); From 420d3a40ddea61be80ef1e7026fa2edb22f66a41 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 11:09:47 +0100 Subject: [PATCH 09/58] Fix error handling in raw audio recorder It is incorret to ever call: streamer.writeDisableStream(...); after: streamer.writeAudioHeader(); Move the try-catch block so that it can never happen. --- .../java/com/genymobile/scrcpy/AudioRawRecorder.java | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index 6108c54b..fdac8b3a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -32,7 +32,13 @@ public final class AudioRawRecorder implements AsyncProcessor { final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); try { - capture.start(); + try { + capture.start(); + } catch (Throwable t) { + // Notify the client that the audio could not be captured + streamer.writeDisableStream(false); + throw t; + } streamer.writeAudioHeader(); while (!Thread.currentThread().isInterrupted()) { @@ -45,10 +51,6 @@ public final class AudioRawRecorder implements AsyncProcessor { streamer.writePacket(buffer, bufferInfo); } - } catch (Throwable e) { - // Notify the client that the audio could not be captured - streamer.writeDisableStream(false); - throw e; } finally { capture.stop(); } From 4eb33054cda35043983b57eb37395cbdac8724eb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 11:14:01 +0100 Subject: [PATCH 10/58] Do not log EPIPE on close for raw audio Handle EPIPE the same way in AudioRawRecorder as in AudioEncoder. This prevents useless errors on close. --- .../main/java/com/genymobile/scrcpy/AudioRawRecorder.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index fdac8b3a..ce33ae85 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -51,6 +51,11 @@ public final class AudioRawRecorder implements AsyncProcessor { streamer.writePacket(buffer, bufferInfo); } + } catch (IOException e) { + // Broken pipe is expected on close, because the socket is closed by the client + if (!IO.isBrokenPipe(e)) { + Ln.e("Audio capture error", e); + } } finally { capture.stop(); } From 5e59ed31352251791679e5931d7e5abf0c2d18f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 11 Nov 2023 11:34:31 +0100 Subject: [PATCH 11/58] Always initialize SDL with the video subsystem Clipboard synchronization requires SDL_INIT_VIDEO, so always initialize the video subsystem, even if --no-video or --no-video-playback is passed. Refs caf594c90ef1b71ed844b2a9b42c3b3371215d6f Fixes #4418 --- app/src/scrcpy.c | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 1d0e90c1..ac2b8e33 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -417,10 +417,14 @@ scrcpy(struct scrcpy_options *options) { if (options->video_playback) { sdl_set_hints(options->render_driver); - if (SDL_Init(SDL_INIT_VIDEO)) { - LOGE("Could not initialize SDL video: %s", SDL_GetError()); - goto end; - } + } + + // Initialize the video subsystem even if --no-video or --no-video-playback + // is passed so that clipboard synchronization still works. + // + if (SDL_Init(SDL_INIT_VIDEO)) { + LOGE("Could not initialize SDL video: %s", SDL_GetError()); + goto end; } if (options->audio_playback) { From e637feba51c2eac6de27ebb318a2f7a1aa54a62d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:08:24 +0100 Subject: [PATCH 12/58] Update muxers documentation Recording now supports formats other than mp4 and mkv. --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 4 ++-- app/src/cli.c | 4 ++-- doc/recording.md | 9 +++++---- 5 files changed, 11 insertions(+), 10 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index eaed88b7..08ca29db 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -125,7 +125,7 @@ _scrcpy() { return ;; --record-format) - COMPREPLY=($(compgen -W 'mkv mp4' -- "$cur")) + COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac' -- "$cur")) return ;; --render-driver) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4b1e5868..31706224 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -65,7 +65,7 @@ arguments=( '--push-target=[Set the target directory for pushing files to the device by drag and drop]' {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' - '--record-format=[Force recording format]:format:(mp4 mkv)' + '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 2901d014..e72cf617 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -347,7 +347,7 @@ Record screen to The format is determined by the .B \-\-record\-format -option if set, or by the file extension (.mp4 or .mkv). +option if set, or by the file extension. .TP .B \-\-raw\-key\-events @@ -355,7 +355,7 @@ Inject key events for all input keys, and ignore text events. .TP .BI "\-\-record\-format " format -Force recording format (either mp4 or mkv). +Force recording format (mp4, mkv, m4a, mka, opus or aac). .TP .BI "\-\-render\-driver " name diff --git a/app/src/cli.c b/app/src/cli.c index 462465fa..078ce315 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -583,7 +583,7 @@ static const struct sc_option options[] = { .argdesc = "file.mp4", .text = "Record screen to file.\n" "The format is determined by the --record-format option if " - "set, or by the file extension (.mp4 or .mkv).", + "set, or by the file extension.", }, { .longopt_id = OPT_RAW_KEY_EVENTS, @@ -594,7 +594,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", .argdesc = "format", - .text = "Force recording format (either mp4 or mkv).", + .text = "Force recording format (mp4, mkv, m4a, mka, opus or aac).", }, { .longopt_id = OPT_RENDER_DRIVER, diff --git a/doc/recording.md b/doc/recording.md index 76a7efd6..d844b368 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -31,14 +31,15 @@ course, not if you capture your scrcpy window and audio output on the computer). ## Format The video and audio streams are encoded on the device, but are muxed on the -client side. Two formats (containers) are supported: - - Matroska (`.mkv`) - - MP4 (`.mp4`) +client side. Several formats (containers) are supported: + - MP4 (`.mp4`, `.m4a`, `.aac`) + - Matroska (`.mkv`, `.mka`) + - OPUS (`.opus`) The container is automatically selected based on the filename. It is also possible to explicitly select a container (in that case the filename -needs not end with `.mkv` or `.mp4`): +needs not end with a known extension): ``` scrcpy --record=file --record-format=mkv From 80defdd8aa29a89bc656df0f2cdc8a1474f95741 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Nov 2023 12:01:10 +0100 Subject: [PATCH 13/58] Suppress private APIs lints to Workarounds class The whole class need them (including the static block). --- server/src/main/java/com/genymobile/scrcpy/Workarounds.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index 77827c47..db9c9629 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -19,6 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.Method; +@SuppressLint("PrivateApi,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") public final class Workarounds { private static final Class ACTIVITY_THREAD_CLASS; @@ -105,7 +106,6 @@ public final class Workarounds { Looper.prepareMainLooper(); } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") private static void fillAppInfo() { try { // ActivityThread.AppBindData appBindData = new ActivityThread.AppBindData(); @@ -132,7 +132,6 @@ public final class Workarounds { } } - @SuppressLint("PrivateApi,DiscouragedPrivateApi") private static void fillAppContext() { try { Application app = new Application(); @@ -162,7 +161,7 @@ public final class Workarounds { } @TargetApi(Build.VERSION_CODES.R) - @SuppressLint("WrongConstant,MissingPermission,BlockedPrivateApi,SoonBlockedPrivateApi,DiscouragedPrivateApi") + @SuppressLint("WrongConstant,MissingPermission") public static AudioRecord createAudioRecord(int source, int sampleRate, int channelConfig, int channels, int channelMask, int encoding) { // Vivo (and maybe some other third-party ROMs) modified `AudioRecord`'s constructor, requiring `Context`s from real App environment. // From 783719c72e6659e20c48fd57171ac957df5a148b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Nov 2023 12:48:56 +0100 Subject: [PATCH 14/58] Fix OPUS packet in an endian-independent way Reading the header id as an int assumed that the current endianness was little endian. Read to a byte array to remove this assumption. --- .../src/main/java/com/genymobile/scrcpy/Streamer.java | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index 39f74fb6..c3f1c6ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -5,14 +5,13 @@ import android.media.MediaCodec; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.util.Arrays; public final class Streamer { private static final long PACKET_FLAG_CONFIG = 1L << 63; private static final long PACKET_FLAG_KEY_FRAME = 1L << 62; - private static final long AOPUSHDR = 0x5244485355504F41L; // "AOPUSHDR" in ASCII (little-endian) - private final FileDescriptor fd; private final Codec codec; private final boolean sendCodecMeta; @@ -120,11 +119,14 @@ public final class Streamer { throw new IOException("Not enough data in OPUS config packet"); } - long id = buffer.getLong(); - if (id != AOPUSHDR) { + final byte[] opusHeaderId = {'A', 'O', 'P', 'U', 'S', 'H', 'D', 'R'}; + byte[] idBuffer = new byte[8]; + buffer.get(idBuffer); + if (!Arrays.equals(idBuffer, opusHeaderId)) { throw new IOException("OPUS header not found"); } + // The size is in native byte-order long sizeLong = buffer.getLong(); if (sizeLong < 0 || sizeLong >= 0x7FFFFFFF) { throw new IOException("Invalid block size in OPUS header: " + sizeLong); From f23be823fded5090792deccdb2b892074208e9d3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Nov 2023 17:45:11 +0100 Subject: [PATCH 15/58] Upgrade FFmpeg build to 6.1-scrcpy Upgrade to FFmpeg 6.1, and with FLAC support enabled. --- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index 9019cc2d..12accb40 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.0-scrcpy-4 +VERSION=6.1-scrcpy DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=39274b321491ce83e76cab5d24e7cbe3f402d3ccf382f739b13be5651c146b60 +SHA256SUM=b41726e603f4624bb9ed7d2836e3e59d9d20b000e22a9ebd27055f4e99e48219 if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index 109bdd27..ef8d52ab 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win32' +prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win32' prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 70e105ab..4e39773d 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.0-scrcpy-4/win64' +prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win64' prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 258017bc..00498418 100644 --- a/release.mk +++ b/release.mk @@ -94,10 +94,10 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -112,10 +112,10 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.0-scrcpy-4/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 4857c5dd5964eccd2c8f772ae570332d12f4f825 Mon Sep 17 00:00:00 2001 From: megapro17 Date: Tue, 7 Nov 2023 15:09:47 +0300 Subject: [PATCH 16/58] Add support for FLAC audio codec PR #4410 <#https://github.com/Genymobile/scrcpy/pull/4410> Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- app/data/bash-completion/scrcpy | 4 +- app/data/zsh-completion/_scrcpy | 4 +- app/scrcpy.1 | 4 +- app/src/cli.c | 24 ++++++++-- app/src/demuxer.c | 10 +++- app/src/options.h | 5 +- app/src/recorder.c | 2 + app/src/server.c | 2 + doc/audio.md | 12 ++++- doc/recording.md | 4 +- .../com/genymobile/scrcpy/AudioCodec.java | 1 + .../java/com/genymobile/scrcpy/Streamer.java | 47 ++++++++++++++++++- 12 files changed, 103 insertions(+), 16 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 08ca29db..9d51fb18 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -97,7 +97,7 @@ _scrcpy() { return ;; --audio-codec) - COMPREPLY=($(compgen -W 'opus aac raw' -- "$cur")) + COMPREPLY=($(compgen -W 'opus aac flac raw' -- "$cur")) return ;; --video-source) @@ -125,7 +125,7 @@ _scrcpy() { return ;; --record-format) - COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac' -- "$cur")) + COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac' -- "$cur")) return ;; --render-driver) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 31706224..c59ac669 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\)]' '--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 raw)' + '--audio-codec=[Select the audio codec]:codec:(opus aac flac raw)' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]' '--audio-source=[Select the audio source]:source:(output mic)' @@ -65,7 +65,7 @@ arguments=( '--push-target=[Set the target directory for pushing files to the device by drag and drop]' {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' - '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac)' + '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index e72cf617..cfcfb227 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -35,7 +35,7 @@ Default is 50. .TP .BI "\-\-audio\-codec " name -Select an audio codec (opus, aac or raw). +Select an audio codec (opus, aac, flac or raw). Default is opus. @@ -355,7 +355,7 @@ Inject key events for all input keys, and ignore text events. .TP .BI "\-\-record\-format " format -Force recording format (mp4, mkv, m4a, mka, opus or aac). +Force recording format (mp4, mkv, m4a, mka, opus, aac or flac). .TP .BI "\-\-render\-driver " name diff --git a/app/src/cli.c b/app/src/cli.c index 078ce315..edb546fa 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -152,7 +152,7 @@ static const struct sc_option options[] = { .longopt_id = OPT_AUDIO_CODEC, .longopt = "audio-codec", .argdesc = "name", - .text = "Select an audio codec (opus, aac or raw).\n" + .text = "Select an audio codec (opus, aac, flac or raw).\n" "Default is opus.", }, { @@ -594,7 +594,8 @@ static const struct sc_option options[] = { .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", .argdesc = "format", - .text = "Force recording format (mp4, mkv, m4a, mka, opus or aac).", + .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac or " + "flac).", }, { .longopt_id = OPT_RENDER_DRIVER, @@ -1626,6 +1627,9 @@ get_record_format(const char *name) { if (!strcmp(name, "aac")) { return SC_RECORD_FORMAT_AAC; } + if (!strcmp(name, "flac")) { + return SC_RECORD_FORMAT_FLAC; + } return 0; } @@ -1695,11 +1699,15 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_AAC; return true; } + if (!strcmp(optarg, "flac")) { + *codec = SC_CODEC_FLAC; + return true; + } if (!strcmp(optarg, "raw")) { *codec = SC_CODEC_RAW; return true; } - LOGE("Unsupported audio codec: %s (expected opus, aac or raw)", optarg); + LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", optarg); return false; } @@ -2376,6 +2384,16 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "(try with --audio-codec=aac)"); return false; } + if (opts->record_format == SC_RECORD_FORMAT_FLAC + && opts->audio_codec != SC_CODEC_FLAC) { + LOGE("Recording to FLAC file requires a FLAC audio stream " + "(try with --audio-codec=flac)"); + return false; + } + } + + if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) { + LOGW("--audio-bit-rate is ignored for FLAC audio codec"); } if (opts->audio_codec == SC_CODEC_RAW) { diff --git a/app/src/demuxer.c b/app/src/demuxer.c index 943f72b6..c9ee8f3c 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -25,7 +25,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { #define SC_CODEC_ID_H265 UINT32_C(0x68323635) // "h265" in ASCII #define SC_CODEC_ID_AV1 UINT32_C(0x00617631) // "av1" in ASCII #define SC_CODEC_ID_OPUS UINT32_C(0x6f707573) // "opus" in ASCII -#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac in ASCII" +#define SC_CODEC_ID_AAC UINT32_C(0x00616163) // "aac" in ASCII +#define SC_CODEC_ID_FLAC UINT32_C(0x666c6163) // "flac" in ASCII #define SC_CODEC_ID_RAW UINT32_C(0x00726177) // "raw" in ASCII switch (codec_id) { case SC_CODEC_ID_H264: @@ -43,6 +44,8 @@ sc_demuxer_to_avcodec_id(uint32_t codec_id) { return AV_CODEC_ID_OPUS; case SC_CODEC_ID_AAC: return AV_CODEC_ID_AAC; + case SC_CODEC_ID_FLAC: + return AV_CODEC_ID_FLAC; case SC_CODEC_ID_RAW: return AV_CODEC_ID_PCM_S16LE; default: @@ -207,6 +210,11 @@ run_demuxer(void *data) { codec_ctx->channels = 2; #endif codec_ctx->sample_rate = 48000; + + if (raw_codec_id == SC_CODEC_ID_FLAC) { + // The sample_fmt is not set by the FLAC decoder + codec_ctx->sample_fmt = AV_SAMPLE_FMT_S16; + } } if (avcodec_open2(codec_ctx, codec, NULL) < 0) { diff --git a/app/src/options.h b/app/src/options.h index 18b437d8..91433894 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -25,6 +25,7 @@ enum sc_record_format { SC_RECORD_FORMAT_MKA, SC_RECORD_FORMAT_OPUS, SC_RECORD_FORMAT_AAC, + SC_RECORD_FORMAT_FLAC, }; static inline bool @@ -32,7 +33,8 @@ sc_record_format_is_audio_only(enum sc_record_format fmt) { return fmt == SC_RECORD_FORMAT_M4A || fmt == SC_RECORD_FORMAT_MKA || fmt == SC_RECORD_FORMAT_OPUS - || fmt == SC_RECORD_FORMAT_AAC; + || fmt == SC_RECORD_FORMAT_AAC + || fmt == SC_RECORD_FORMAT_FLAC; } enum sc_codec { @@ -41,6 +43,7 @@ enum sc_codec { SC_CODEC_AV1, SC_CODEC_OPUS, SC_CODEC_AAC, + SC_CODEC_FLAC, SC_CODEC_RAW, }; diff --git a/app/src/recorder.c b/app/src/recorder.c index 23c8b497..d13b122a 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -69,6 +69,8 @@ sc_recorder_get_format_name(enum sc_record_format format) { return "matroska"; case SC_RECORD_FORMAT_OPUS: return "opus"; + case SC_RECORD_FORMAT_FLAC: + return "flac"; default: return NULL; } diff --git a/app/src/server.c b/app/src/server.c index 2b3439da..d4726c2a 100644 --- a/app/src/server.c +++ b/app/src/server.c @@ -178,6 +178,8 @@ sc_server_get_codec_name(enum sc_codec codec) { return "opus"; case SC_CODEC_AAC: return "aac"; + case SC_CODEC_FLAC: + return "flac"; case SC_CODEC_RAW: return "raw"; default: diff --git a/doc/audio.md b/doc/audio.md index cb6cde95..ecae4468 100644 --- a/doc/audio.md +++ b/doc/audio.md @@ -62,12 +62,13 @@ scrcpy --audio-source=mic --no-video --no-playback --record=file.opus ## Codec -The audio codec can be selected. The possible values are `opus` (default), `aac` -and `raw` (uncompressed PCM 16-bit LE): +The audio codec can be selected. The possible values are `opus` (default), +`aac`, `flac` and `raw` (uncompressed PCM 16-bit LE): ```bash scrcpy --audio-codec=opus # default scrcpy --audio-codec=aac +scrcpy --audio-codec=flac scrcpy --audio-codec=raw ``` @@ -80,7 +81,14 @@ then your device has no Opus encoder: try `scrcpy --audio-codec=aac`. For advanced usage, to pass arbitrary parameters to the [`MediaFormat`], check `--audio-codec-options` in the manpage or in `scrcpy --help`. +For example, to change the [FLAC compression level]: + +```bash +scrcpy --audio-codec=flac --audio-codec-options=flac-compression-level=8 +``` + [`MediaFormat`]: https://developer.android.com/reference/android/media/MediaFormat +[FLAC compression level]: https://developer.android.com/reference/android/media/MediaFormat#KEY_FLAC_COMPRESSION_LEVEL ## Encoder diff --git a/doc/recording.md b/doc/recording.md index d844b368..466cf542 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -18,7 +18,8 @@ To record only the audio: ```bash scrcpy --no-video --record=file.opus scrcpy --no-video --audio-codec=aac --record=file.aac -# .m4a/.mp4 and .mka/.mkv are also supported for both opus and aac +scrcpy --no-video --audio-codec=flac --record=file.flac +# .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac ``` Timestamps are captured on the device, so [packet delay variation] does not @@ -35,6 +36,7 @@ client side. Several formats (containers) are supported: - MP4 (`.mp4`, `.m4a`, `.aac`) - Matroska (`.mkv`, `.mka`) - OPUS (`.opus`) + - FLAC (`.flac`) The container is automatically selected based on the filename. diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java index 1f3b07a0..b4ea3680 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCodec.java @@ -5,6 +5,7 @@ import android.media.MediaFormat; public enum AudioCodec implements Codec { OPUS(0x6f_70_75_73, "opus", MediaFormat.MIMETYPE_AUDIO_OPUS), AAC(0x00_61_61_63, "aac", MediaFormat.MIMETYPE_AUDIO_AAC), + FLAC(0x66_6c_61_63, "flac", MediaFormat.MIMETYPE_AUDIO_FLAC), RAW(0x00_72_61_77, "raw", MediaFormat.MIMETYPE_AUDIO_RAW); private final int id; // 4-byte ASCII representation of the name diff --git a/server/src/main/java/com/genymobile/scrcpy/Streamer.java b/server/src/main/java/com/genymobile/scrcpy/Streamer.java index c3f1c6ee..8b6c9dcc 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Streamer.java +++ b/server/src/main/java/com/genymobile/scrcpy/Streamer.java @@ -5,6 +5,7 @@ import android.media.MediaCodec; import java.io.FileDescriptor; import java.io.IOException; import java.nio.ByteBuffer; +import java.nio.ByteOrder; import java.util.Arrays; public final class Streamer { @@ -29,6 +30,7 @@ public final class Streamer { public Codec getCodec() { return codec; } + public void writeAudioHeader() throws IOException { if (sendCodecMeta) { ByteBuffer buffer = ByteBuffer.allocate(4); @@ -61,8 +63,12 @@ public final class Streamer { } public void writePacket(ByteBuffer buffer, long pts, boolean config, boolean keyFrame) throws IOException { - if (config && codec == AudioCodec.OPUS) { - fixOpusConfigPacket(buffer); + if (config) { + if (codec == AudioCodec.OPUS) { + fixOpusConfigPacket(buffer); + } else if (codec == AudioCodec.FLAC) { + fixFlacConfigPacket(buffer); + } } if (sendFrameMeta) { @@ -140,4 +146,41 @@ public final class Streamer { // Set the buffer to point to the OPUS header slice buffer.limit(buffer.position() + size); } + + private static void fixFlacConfigPacket(ByteBuffer buffer) throws IOException { + // 00000000 66 4c 61 43 00 00 00 22 |fLaC..." | + // -------------- BELOW IS THE PART WE MUST PUT AS EXTRADATA ------------------- + // 00000000 10 00 10 00 00 00 00 00 | ........| + // 00000010 00 00 0b b8 02 f0 00 00 00 00 00 00 00 00 00 00 |................| + // 00000020 00 00 00 00 00 00 00 00 00 00 |.......... | + // ------------------------------------------------------------------------------ + // 00000020 84 00 00 28 20 00 | ...( .| + // 00000030 00 00 72 65 66 65 72 65 6e 63 65 20 6c 69 62 46 |..reference libF| + // 00000040 4c 41 43 20 31 2e 33 2e 32 20 32 30 32 32 31 30 |LAC 1.3.2 202210| + // 00000050 32 32 00 00 00 00 |22....| + // + // + + if (buffer.remaining() < 8) { + throw new IOException("Not enough data in FLAC config packet"); + } + + final byte[] flacHeaderId = {'f', 'L', 'a', 'C'}; + byte[] idBuffer = new byte[4]; + buffer.get(idBuffer); + if (!Arrays.equals(idBuffer, flacHeaderId)) { + throw new IOException("FLAC header not found"); + } + + // The size is in big-endian + buffer.order(ByteOrder.BIG_ENDIAN); + + int size = buffer.getInt(); + if (buffer.remaining() < size) { + throw new IOException("Not enough data in FLAC header (invalid size: " + size + ")"); + } + + // Set the buffer to point to the FLAC header slice + buffer.limit(buffer.position() + size); + } } From 258eaaae2af9c1e41611f8f570bd46fe10165859 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 12 Nov 2023 18:34:04 +0100 Subject: [PATCH 17/58] Increase default audio buffer for FLAC FLAC is not low latency: the default encoder produces blocks of 4096 samples, which represent ~85.333ms. Increase the audio buffer by default so that audio playback works. --- app/src/cli.c | 13 +++++++++++++ app/src/options.c | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index edb546fa..0482b233 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2265,6 +2265,19 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->require_audio = true; } + if (opts->audio_playback && opts->audio_buffer == -1) { + if (opts->audio_codec == SC_CODEC_FLAC) { + // Use 50 ms audio buffer by default, but use a higher value for FLAC, + // which is not low latency (the default encoder produces blocks of + // 4096 samples, which represent ~85.333ms). + LOGI("FLAC audio: audio buffer increased to 120 ms (use " + "--audio-buffer to set a custom value)"); + opts->audio_buffer = SC_TICK_FROM_MS(120); + } else { + opts->audio_buffer = SC_TICK_FROM_MS(50); + } + } + #ifdef HAVE_V4L2 if (v4l2) { if (opts->lock_video_orientation == diff --git a/app/src/options.c b/app/src/options.c index 6c72d767..092fbd56 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -46,7 +46,7 @@ const struct scrcpy_options scrcpy_options_default = { .window_height = 0, .display_id = 0, .display_buffer = 0, - .audio_buffer = SC_TICK_FROM_MS(50), + .audio_buffer = -1, // depends on the audio format, .audio_output_buffer = SC_TICK_FROM_MS(5), .time_limit = 0, #ifdef HAVE_V4L2 From 3bb6b0cb9f4a67046bd96b53bef46e34818ae0f3 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 08:49:10 +0100 Subject: [PATCH 18/58] Read audio by blocks of 1024 samples In practice, the system captures audio samples by blocks of 1024 samples. Remplace the hardcoded value of 5 milliseconds (240 samples), and let AudioRecord fill the input buffer provided by MediaCodec (or by AudioRawRecorder), with a maximum size of 1024 samples (just in case). --- .../java/com/genymobile/scrcpy/AudioCapture.java | 13 +++++++------ .../java/com/genymobile/scrcpy/AudioEncoder.java | 5 +---- .../com/genymobile/scrcpy/AudioRawRecorder.java | 7 ++----- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index 5575ffb6..e94b49ed 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -24,6 +24,11 @@ public final class AudioCapture { public static final int ENCODING = AudioFormat.ENCODING_PCM_16BIT; public static final int BYTES_PER_SAMPLE = 2; + // Never read more than 1024 samples, even if the buffer is bigger (that would increase latency). + // A lower value is useless, since the system captures audio samples by blocks of 1024 (so for example if we read by blocks of 256 samples, we + // receive 4 successive blocks without waiting, then we wait for the 4 next ones). + public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + private final int audioSource; private AudioRecord recorder; @@ -36,10 +41,6 @@ public final class AudioCapture { this.audioSource = audioSource.value(); } - public static int millisToBytes(int millis) { - return SAMPLE_RATE * CHANNELS * BYTES_PER_SAMPLE * millis / 1000; - } - private static AudioFormat createAudioFormat() { AudioFormat.Builder builder = new AudioFormat.Builder(); builder.setEncoding(ENCODING); @@ -135,8 +136,8 @@ public final class AudioCapture { } @TargetApi(Build.VERSION_CODES.N) - public int read(ByteBuffer directBuffer, int size, MediaCodec.BufferInfo outBufferInfo) { - int r = recorder.read(directBuffer, size); + public int read(ByteBuffer directBuffer, MediaCodec.BufferInfo outBufferInfo) { + int r = recorder.read(directBuffer, MAX_READ_SIZE); if (r <= 0) { return r; } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index bec79b05..ad8d0422 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -37,9 +37,6 @@ public final class AudioEncoder implements AsyncProcessor { private static final int SAMPLE_RATE = AudioCapture.SAMPLE_RATE; private static final int CHANNELS = AudioCapture.CHANNELS; - private static final int READ_MS = 5; // milliseconds - private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); - private final AudioCapture capture; private final Streamer streamer; private final int bitRate; @@ -93,7 +90,7 @@ public final class AudioEncoder implements AsyncProcessor { while (!Thread.currentThread().isInterrupted()) { InputTask task = inputTasks.take(); ByteBuffer buffer = mediaCodec.getInputBuffer(task.index); - int r = capture.read(buffer, READ_SIZE, bufferInfo); + int r = capture.read(buffer, bufferInfo); if (r <= 0) { throw new IOException("Could not read audio: " + r); } diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java index ce33ae85..7e052f32 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioRawRecorder.java @@ -13,9 +13,6 @@ public final class AudioRawRecorder implements AsyncProcessor { private Thread thread; - private static final int READ_MS = 5; // milliseconds - private static final int READ_SIZE = AudioCapture.millisToBytes(READ_MS); - public AudioRawRecorder(AudioCapture capture, Streamer streamer) { this.capture = capture; this.streamer = streamer; @@ -28,7 +25,7 @@ public final class AudioRawRecorder implements AsyncProcessor { return; } - final ByteBuffer buffer = ByteBuffer.allocateDirect(READ_SIZE); + final ByteBuffer buffer = ByteBuffer.allocateDirect(AudioCapture.MAX_READ_SIZE); final MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo(); try { @@ -43,7 +40,7 @@ public final class AudioRawRecorder implements AsyncProcessor { streamer.writeAudioHeader(); while (!Thread.currentThread().isInterrupted()) { buffer.position(0); - int r = capture.read(buffer, READ_SIZE, bufferInfo); + int r = capture.read(buffer, bufferInfo); if (r < 0) { throw new IOException("Could not read audio: " + r); } From a402eac7f293b1f63ce1c0a9449cf3a997dc061e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:15:34 +0100 Subject: [PATCH 19/58] Compute PTS of intermediate blocks If several reads are performed for a single captured audio block (e.g. if the read size is smaller than the captured block), then the provided timestamp was the same for all packets. Recompute the timestamp for each of them. --- server/src/main/java/com/genymobile/scrcpy/AudioCapture.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index e94b49ed..c05bb41d 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -34,6 +34,7 @@ public final class AudioCapture { private AudioRecord recorder; private final AudioTimestamp timestamp = new AudioTimestamp(); + private long previousRecorderTimestamp = -1; private long previousPts = 0; private long nextPts = 0; @@ -145,8 +146,9 @@ public final class AudioCapture { long pts; int ret = recorder.getTimestamp(timestamp, AudioTimestamp.TIMEBASE_MONOTONIC); - if (ret == AudioRecord.SUCCESS) { + if (ret == AudioRecord.SUCCESS && timestamp.nanoTime != previousRecorderTimestamp) { pts = timestamp.nanoTime / 1000; + previousRecorderTimestamp = timestamp.nanoTime; } else { if (nextPts == 0) { Ln.w("Could not get any audio timestamp"); From 4b4f045e196fe037a841f7004eaabb09cf571942 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:40:42 +0100 Subject: [PATCH 20/58] Fix audio PTS by the duration of 1 sample If the difference of PTS between two consecutive blocks of audio is less than 1 sample, then it will be considered as non-increasing by FFmpeg muxers having a time_base of 1/sample_rate. Increase the PTS by 1 sample instead. --- .../src/main/java/com/genymobile/scrcpy/AudioCapture.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java index c05bb41d..e3de50e6 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioCapture.java @@ -29,6 +29,8 @@ public final class AudioCapture { // receive 4 successive blocks without waiting, then we wait for the 4 next ones). public static final int MAX_READ_SIZE = 1024 * CHANNELS * BYTES_PER_SAMPLE; + private static final long ONE_SAMPLE_US = (1000000 + SAMPLE_RATE - 1) / SAMPLE_RATE; // 1 sample in microseconds (used for fixing PTS) + private final int audioSource; private AudioRecord recorder; @@ -160,13 +162,13 @@ public final class AudioCapture { long durationUs = r * 1000000 / (CHANNELS * BYTES_PER_SAMPLE * SAMPLE_RATE); nextPts = pts + durationUs; - if (previousPts != 0 && pts < previousPts) { + if (previousPts != 0 && pts < previousPts + ONE_SAMPLE_US) { // Audio PTS may come from two sources: // - recorder.getTimestamp() if the call works; // - an estimation from the previous PTS and the packet size as a fallback. // // Therefore, the property that PTS are monotonically increasing is no guaranteed in corner cases, so enforce it. - pts = previousPts + 1; + pts = previousPts + ONE_SAMPLE_US; } previousPts = pts; From 1713422c13265946a15b25e9de5762727a33edfb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:30:42 +0100 Subject: [PATCH 21/58] Upgrade FFmpeg build to 6.1-scrcpy-2 Use a build with WAV muxer. --- app/prebuilt-deps/prepare-ffmpeg.sh | 4 ++-- cross_win32.txt | 2 +- cross_win64.txt | 2 +- release.mk | 16 ++++++++-------- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index 12accb40..96ea3ee7 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.1-scrcpy +VERSION=6.1-scrcpy-2 DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=b41726e603f4624bb9ed7d2836e3e59d9d20b000e22a9ebd27055f4e99e48219 +SHA256SUM=7f25f638dc24a0f5d4af07a088b6a604cf33548900bbfd2f6ce0bae050b7664d if [[ -d "$DEP_DIR" ]] then diff --git a/cross_win32.txt b/cross_win32.txt index ef8d52ab..e24f3722 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -16,6 +16,6 @@ cpu = 'i686' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win32' +prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win32' prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 4e39773d..39e79944 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -16,6 +16,6 @@ cpu = 'x86_64' endian = 'little' [properties] -prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy/win64' +prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win64' prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32' prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 00498418..57fa994e 100644 --- a/release.mk +++ b/release.mk @@ -94,10 +94,10 @@ dist-win32: build-server build-win32 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN32_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN32_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" @@ -112,10 +112,10 @@ dist-win64: build-server build-win64 cp app/data/scrcpy-noconsole.vbs "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/icon.png "$(DIST)/$(WIN64_TARGET_DIR)" cp app/data/open_a_terminal_here.bat "$(DIST)/$(WIN64_TARGET_DIR)" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" From 200488111e9f54585c67f915265094f7f22e8888 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 13 Nov 2023 09:35:18 +0100 Subject: [PATCH 22/58] Add support for RAW audio (WAV) recording RAW audio forwarding was supported but not for recording. Add support for recording a raw audio stream to a `.wav` file (and `.mkv`). --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 2 +- app/src/cli.c | 26 +++++++++++++++++++------- app/src/options.h | 4 +++- app/src/recorder.c | 18 ++++++++++++++---- app/src/recorder.h | 2 ++ doc/recording.md | 2 ++ 8 files changed, 43 insertions(+), 15 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 9d51fb18..97dbfe70 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -125,7 +125,7 @@ _scrcpy() { return ;; --record-format) - COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac' -- "$cur")) + COMPREPLY=($(compgen -W 'mp4 mkv m4a mka opus aac flac wav' -- "$cur")) return ;; --render-driver) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index c59ac669..4b8a7737 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -65,7 +65,7 @@ arguments=( '--push-target=[Set the target directory for pushing files to the device by drag and drop]' {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' - '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac)' + '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index cfcfb227..26f53ba4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -355,7 +355,7 @@ Inject key events for all input keys, and ignore text events. .TP .BI "\-\-record\-format " format -Force recording format (mp4, mkv, m4a, mka, opus, aac or flac). +Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). .TP .BI "\-\-render\-driver " name diff --git a/app/src/cli.c b/app/src/cli.c index 0482b233..19097f47 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -594,8 +594,8 @@ static const struct sc_option options[] = { .longopt_id = OPT_RECORD_FORMAT, .longopt = "record-format", .argdesc = "format", - .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac or " - "flac).", + .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac " + "or wav).", }, { .longopt_id = OPT_RENDER_DRIVER, @@ -1630,6 +1630,9 @@ get_record_format(const char *name) { if (!strcmp(name, "flac")) { return SC_RECORD_FORMAT_FLAC; } + if (!strcmp(name, "wav")) { + return SC_RECORD_FORMAT_WAV; + } return 0; } @@ -2373,11 +2376,6 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } - if (opts->audio_codec == SC_CODEC_RAW) { - LOGE("Recording does not support RAW audio codec"); - return false; - } - if (opts->video && sc_record_format_is_audio_only(opts->record_format)) { LOGE("Audio container does not support video stream"); @@ -2403,6 +2401,20 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], "(try with --audio-codec=flac)"); return false; } + + if (opts->record_format == SC_RECORD_FORMAT_WAV + && opts->audio_codec != SC_CODEC_RAW) { + LOGE("Recording to WAV file requires a RAW audio stream " + "(try with --audio-codec=raw)"); + return false; + } + + if ((opts->record_format == SC_RECORD_FORMAT_MP4 || + opts->record_format == SC_RECORD_FORMAT_M4A) + && opts->audio_codec == SC_CODEC_RAW) { + LOGE("Recording to MP4 container does not support RAW audio"); + return false; + } } if (opts->audio_codec == SC_CODEC_FLAC && opts->audio_bit_rate) { diff --git a/app/src/options.h b/app/src/options.h index 91433894..c702ceeb 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -26,6 +26,7 @@ enum sc_record_format { SC_RECORD_FORMAT_OPUS, SC_RECORD_FORMAT_AAC, SC_RECORD_FORMAT_FLAC, + SC_RECORD_FORMAT_WAV, }; static inline bool @@ -34,7 +35,8 @@ sc_record_format_is_audio_only(enum sc_record_format fmt) { || fmt == SC_RECORD_FORMAT_MKA || fmt == SC_RECORD_FORMAT_OPUS || fmt == SC_RECORD_FORMAT_AAC - || fmt == SC_RECORD_FORMAT_FLAC; + || fmt == SC_RECORD_FORMAT_FLAC + || fmt == SC_RECORD_FORMAT_WAV; } enum sc_codec { diff --git a/app/src/recorder.c b/app/src/recorder.c index d13b122a..8794442b 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -71,6 +71,8 @@ sc_recorder_get_format_name(enum sc_record_format format) { return "opus"; case SC_RECORD_FORMAT_FLAC: return "flac"; + case SC_RECORD_FORMAT_WAV: + return "wav"; default: return NULL; } @@ -168,13 +170,14 @@ sc_recorder_close_output_file(struct sc_recorder *recorder) { } static inline bool -sc_recorder_has_empty_queues(struct sc_recorder *recorder) { +sc_recorder_must_wait_for_config_packets(struct sc_recorder *recorder) { if (recorder->video && sc_vecdeque_is_empty(&recorder->video_queue)) { // The video queue is empty return true; } - if (recorder->audio && sc_vecdeque_is_empty(&recorder->audio_queue)) { + if (recorder->audio && recorder->audio_expects_config_packet + && sc_vecdeque_is_empty(&recorder->audio_queue)) { // The audio queue is empty (when audio is enabled) return true; } @@ -190,7 +193,7 @@ sc_recorder_process_header(struct sc_recorder *recorder) { while (!recorder->stopped && ((recorder->video && !recorder->video_init) || (recorder->audio && !recorder->audio_init) - || sc_recorder_has_empty_queues(recorder))) { + || sc_recorder_must_wait_for_config_packets(recorder))) { sc_cond_wait(&recorder->cond, &recorder->mutex); } @@ -209,7 +212,8 @@ sc_recorder_process_header(struct sc_recorder *recorder) { } AVPacket *audio_pkt = NULL; - if (!sc_vecdeque_is_empty(&recorder->audio_queue)) { + if (recorder->audio_expects_config_packet && + !sc_vecdeque_is_empty(&recorder->audio_queue)) { assert(recorder->audio); audio_pkt = sc_vecdeque_pop(&recorder->audio_queue); } @@ -597,6 +601,10 @@ sc_recorder_audio_packet_sink_open(struct sc_packet_sink *sink, recorder->audio_stream.index = stream->index; + // A config packet is provided for all supported formats except raw audio + recorder->audio_expects_config_packet = + ctx->codec_id != AV_CODEC_ID_PCM_S16LE; + recorder->audio_init = true; sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); @@ -709,6 +717,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->video_init = false; recorder->audio_init = false; + recorder->audio_expects_config_packet = false; + sc_recorder_stream_init(&recorder->video_stream); sc_recorder_stream_init(&recorder->audio_stream); diff --git a/app/src/recorder.h b/app/src/recorder.h index 47fd3f21..16327584 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -50,6 +50,8 @@ struct sc_recorder { bool video_init; bool audio_init; + bool audio_expects_config_packet; + struct sc_recorder_stream video_stream; struct sc_recorder_stream audio_stream; diff --git a/doc/recording.md b/doc/recording.md index 466cf542..c1a8445e 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -19,6 +19,7 @@ To record only the audio: scrcpy --no-video --record=file.opus scrcpy --no-video --audio-codec=aac --record=file.aac scrcpy --no-video --audio-codec=flac --record=file.flac +scrcpy --no-video --audio-codec=raw --record=file.wav # .m4a/.mp4 and .mka/.mkv are also supported for opus, aac and flac ``` @@ -37,6 +38,7 @@ client side. Several formats (containers) are supported: - Matroska (`.mkv`, `.mka`) - OPUS (`.opus`) - FLAC (`.flac`) + - WAV (`.wav`) The container is automatically selected based on the filename. From 15a3bad4abca691d6459821bbcaf4c4f52a52f28 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 14 Nov 2023 09:33:59 +0100 Subject: [PATCH 23/58] Log PTS fixing at debug level Audio PTS are retrieved by AudioRecord.getTimestamp(), so they do not necessarily exactly match the number of samples (this allows to take drift and lag into account). As a consequence, two consecutive timestamps in microseconds may sometimes end up within the same millisecond, causing the warning. This is particularly true for the Matroska muxer which uses a timebase of 1/1000 (1 ms precision). Since this is "expected", lower the log level from warning to debug. --- app/src/recorder.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/recorder.c b/app/src/recorder.c index 8794442b..c9d5f131 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -105,7 +105,7 @@ sc_recorder_write_stream(struct sc_recorder *recorder, AVStream *stream = recorder->ctx->streams[st->index]; sc_recorder_rescale_packet(stream, packet); if (st->last_pts != AV_NOPTS_VALUE && packet->pts <= st->last_pts) { - LOGW("Fixing PTS non monotonically increasing in stream %d " + LOGD("Fixing PTS non monotonically increasing in stream %d " "(%" PRIi64 " >= %" PRIi64 ")", st->index, st->last_pts, packet->pts); packet->pts = ++st->last_pts; From 86808e811424560c7ed188b9fd4213c3838b0724 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Nov 2023 20:59:02 +0100 Subject: [PATCH 24/58] Upgrade Android checkstyle to 10.12.5 Upgrade to the latest version. --- config/android-checkstyle.gradle | 2 +- server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/config/android-checkstyle.gradle b/config/android-checkstyle.gradle index 29c67b19..1e5ce3ba 100644 --- a/config/android-checkstyle.gradle +++ b/config/android-checkstyle.gradle @@ -2,7 +2,7 @@ apply plugin: 'checkstyle' check.dependsOn 'checkstyle' checkstyle { - toolVersion = '9.0.1' + toolVersion = '10.12.5' } task checkstyle(type: Checkstyle) { diff --git a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java index ad8d0422..0b59369b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java +++ b/server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java @@ -295,7 +295,7 @@ public final class AudioEncoder implements AsyncProcessor { } } - private class EncoderCallback extends MediaCodec.Callback { + private final class EncoderCallback extends MediaCodec.Callback { @TargetApi(Build.VERSION_CODES.N) @Override public void onInputBufferAvailable(MediaCodec codec, int index) { From e8801cc3c0493f72ece976f2b1d3a3bdef8237ac Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Nov 2023 21:05:47 +0100 Subject: [PATCH 25/58] Upgrade AGP (8.1.3) and Gradle to 8.4 Android Gradle Plugin 8.1.3. Gradle 8.4. From now on, Java 17 is required. --- build.gradle | 6 +----- doc/build.md | 10 +++++----- gradle/wrapper/gradle-wrapper.properties | 2 +- server/build.gradle | 6 +++++- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/build.gradle b/build.gradle index f7e29b22..b27befb6 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.4.0' + classpath 'com.android.tools.build:gradle:8.1.3' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files @@ -23,7 +23,3 @@ allprojects { options.compilerArgs << "-Xlint:deprecation" } } - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/doc/build.md b/doc/build.md index 54b7410b..15c567b5 100644 --- a/doc/build.md +++ b/doc/build.md @@ -58,7 +58,7 @@ sudo apt install gcc git pkg-config meson ninja-build libsdl2-dev \ libswresample-dev libusb-1.0-0-dev # server build dependencies -sudo apt install openjdk-11-jdk +sudo apt install openjdk-17-jdk ``` On old versions (like Ubuntu 16.04), `meson` is too old. In that case, install @@ -100,7 +100,7 @@ sudo apt install mingw-w64 mingw-w64-tools You also need the JDK to build the server: ```bash -sudo apt install openjdk-11-jdk +sudo apt install openjdk-17-jdk ``` Then generate the releases: @@ -168,13 +168,13 @@ brew install sdl2 ffmpeg libusb brew install pkg-config meson ``` -Additionally, if you want to build the server, install Java 8 from Caskroom, and +Additionally, if you want to build the server, install Java 17 from Caskroom, and make it available from the `PATH`: ```bash brew tap homebrew/cask-versions -brew install adoptopenjdk/openjdk/adoptopenjdk11 -export JAVA_HOME="$(/usr/libexec/java_home --version 1.11)" +brew install adoptopenjdk/openjdk/adoptopenjdk17 +export JAVA_HOME="$(/usr/libexec/java_home --version 1.17)" export PATH="$JAVA_HOME/bin:$PATH" ``` diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2ec77e51..e411586a 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/server/build.gradle b/server/build.gradle index bee6509b..927906fc 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' android { namespace 'com.genymobile.scrcpy' - compileSdkVersion 33 + compileSdk 33 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 @@ -17,6 +17,10 @@ android { proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } + buildFeatures { + buildConfig true + aidl true + } } dependencies { From abcb10059749d9536c6e344f5c61466305afd2ee Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 15 Nov 2023 21:08:15 +0100 Subject: [PATCH 26/58] Upgrade Android SDK to 34 --- server/build.gradle | 4 ++-- server/build_without_gradle.sh | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/server/build.gradle b/server/build.gradle index 927906fc..1bb31360 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -2,11 +2,11 @@ apply plugin: 'com.android.application' android { namespace 'com.genymobile.scrcpy' - compileSdk 33 + compileSdk 34 defaultConfig { applicationId "com.genymobile.scrcpy" minSdkVersion 21 - targetSdkVersion 33 + targetSdkVersion 34 versionCode 200 versionName "v2.2" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 6e755272..5ab90a0a 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=v2.2 -PLATFORM=${ANDROID_PLATFORM:-33} -BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-33.0.0} +PLATFORM=${ANDROID_PLATFORM:-34} +BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} BUILD_TOOLS_DIR="$ANDROID_HOME/build-tools/$BUILD_TOOLS" BUILD_DIR="$(realpath ${BUILD_DIR:-build_manual})" From 7e3b9359322fff65bd350febfdc02a76186981cd Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Nov 2023 08:56:04 +0100 Subject: [PATCH 27/58] Recreate the display on rotation On Android 14 (Pixel 8), a device rotation while the camera app was running resulted in an incorrect capture. Destroying and recreating the display fixes the issue. --- .../main/java/com/genymobile/scrcpy/ScreenCapture.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java index f81332f5..e048354a 100644 --- a/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/ScreenCapture.java @@ -18,7 +18,6 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList @Override public void init() { - display = createDisplay(); device.setRotationListener(this); device.setFoldListener(this); } @@ -32,6 +31,11 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList Rect unlockedVideoRect = screenInfo.getUnlockedVideoSize().toRect(); int videoRotation = screenInfo.getVideoRotation(); int layerStack = device.getLayerStack(); + + if (display != null) { + SurfaceControl.destroyDisplay(display); + } + display = createDisplay(); setDisplaySurface(display, surface, videoRotation, contentRect, unlockedVideoRect, layerStack); } @@ -39,7 +43,9 @@ public class ScreenCapture extends SurfaceCapture implements Device.RotationList public void release() { device.setRotationListener(null); device.setFoldListener(null); - SurfaceControl.destroyDisplay(display); + if (display != null) { + SurfaceControl.destroyDisplay(display); + } } @Override From 45a073a333564a3c596cfe4067de51bb339e8e2b Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Nov 2023 09:02:52 +0100 Subject: [PATCH 28/58] Do not create Device instance for camera The device instance manages the display and the injection of input events. It is not necessary for camera capture. --- server/src/main/java/com/genymobile/scrcpy/Server.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index a1c6090b..61d3497b 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -92,8 +92,6 @@ public final class Server { throw new ConfigurationException("Camera mirroring is not supported"); } - final Device device = new Device(options); - Thread initThread = startInitThread(options); int scid = options.getScid(); @@ -102,7 +100,9 @@ public final class Server { boolean video = options.getVideo(); boolean audio = options.getAudio(); boolean sendDummyByte = options.getSendDummyByte(); - boolean camera = options.getVideoSource() == VideoSource.CAMERA; + boolean camera = video && options.getVideoSource() == VideoSource.CAMERA; + + final Device device = camera ? null : new Device(options); Workarounds.apply(audio, camera); From 4658c0e5d223d3d9aff19a37622e701261b09aef Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 16 Nov 2023 09:59:37 +0100 Subject: [PATCH 29/58] Update record format error message Recording now supports formats other than mp4 and mkv. Refs e637feba51c2eac6de27ebb318a2f7a1aa54a62d --- app/src/cli.c | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/src/cli.c b/app/src/cli.c index 19097f47..b402f6c5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -1640,7 +1640,8 @@ static bool parse_record_format(const char *optarg, enum sc_record_format *format) { enum sc_record_format fmt = get_record_format(optarg); if (!fmt) { - LOGE("Unsupported format: %s (expected mp4 or mkv)", optarg); + LOGE("Unsupported record format: %s (expected mp4, mkv, m4a, mka, " + "opus, aac, flac or wav)", optarg); return false; } @@ -1710,7 +1711,8 @@ parse_audio_codec(const char *optarg, enum sc_codec *codec) { *codec = SC_CODEC_RAW; return true; } - LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", optarg); + LOGE("Unsupported audio codec: %s (expected opus, aac, flac or raw)", + optarg); return false; } From 0801cf062722064506f2353c2c9bcd3504c59281 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 13:09:46 +0100 Subject: [PATCH 30/58] Fix options alphabetical order Renaming --display to --display-id broke the alphabetical order. Refs 23e116064dc97f2af843e764f13eebd54fab486d --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 12 ++++++------ app/src/cli.c | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 97dbfe70..f94a70a6 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -19,8 +19,8 @@ _scrcpy() { --crop= -d --select-usb --disable-screensaver - --display-id= --display-buffer= + --display-id= -e --select-tcpip -f --fullscreen --force-adb-forward diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 4b8a7737..cc58b866 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -26,8 +26,8 @@ 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-id=[Specify the display id to mirror]' '--display-buffer=[Add a buffering delay \(in milliseconds\) before displaying]' + '--display-id=[Specify the display id to mirror]' {-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]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 26f53ba4..10c32ca1 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -127,6 +127,12 @@ 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. @@ -135,12 +141,6 @@ The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. -.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 .B \-e, \-\-select\-tcpip Use TCP/IP device (if there is exactly one, like adb -e). diff --git a/app/src/cli.c b/app/src/cli.c index b402f6c5..12ed8111 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -292,6 +292,14 @@ static const struct sc_option options[] = { .longopt = "display", .argdesc = "id", }, + { + .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, .longopt = "display-id", @@ -301,14 +309,6 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, - { - .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).", - }, { .shortopt = 'e', .longopt = "select-tcpip", From 9df92ebe3765d2515f8e561896a239cdf82110bb Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 14:05:21 +0100 Subject: [PATCH 31/58] Fix manpage style syntax --- app/scrcpy.1 | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 10c32ca1..18941190 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -26,7 +26,7 @@ Encode the audio at the given bit rate, expressed in bits/s. Unit suffixes are s Default is 128K (128000). .TP -.BI "\-\-audio\-buffer ms +.BI "\-\-audio\-buffer " ms Configure the audio buffering delay (in milliseconds). Lower values decrease the latency, but increase the likelyhood of buffer underrun (causing audio glitches). @@ -62,7 +62,7 @@ Select the audio source (output or mic). Default is output. .TP -.BI "\-\-audio\-output\-buffer ms +.BI "\-\-audio\-output\-buffer " ms Configure the size of the SDL audio output buffer (in milliseconds). If you get "robotic" audio playback, you should test with a higher value (10). Do not change this setting otherwise. @@ -128,7 +128,7 @@ Also see \fB\-e\fR (\fB\-\-select\-tcpip\fR). Disable screensaver while scrcpy is running. .TP -.BI "\-\-display\-buffer ms +.BI "\-\-display\-buffer " ms Add a buffering delay (in milliseconds) before displaying. This increases latency to compensate for jitter. Default is 0 (no buffering). From 25e33566f5bca3b052dbb2b4262be96cd8f71caa Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Nov 2023 08:41:09 +0100 Subject: [PATCH 32/58] Mention turning off audio in camera documentation --- doc/camera.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/doc/camera.md b/doc/camera.md index d1008bda..e11ad521 100644 --- a/doc/camera.md +++ b/doc/camera.md @@ -18,6 +18,17 @@ scrcpy --video-source=display --audio-source=mic # force display AND micropho scrcpy --video-source=camera --audio-source=output # force camera AND device audio output ``` +Audio can be disabled: + +```bash +# audio not captured at all +scrcpy --video-source=camera --no-audio +scrcpy --video-source=camera --no-audio --record=file.mp4 + +# audio captured and recorded, but not played +scrcpy --video-source=camera --no-audio-playback --record=file.mp4 +``` + ## List From bb88b60227427959b931b5a4417202a85c5b0cdc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 19 Nov 2023 01:06:59 +0100 Subject: [PATCH 33/58] Add --display-orientation Deprecate the option --rotation and introduce a new option --display-orientation with the 8 possible orientations (0, 90, 180, 270, flip0, flip90, flip180 and flip270). New shortcuts MOD+Shift+(arrow) dynamically change the display (horizontal or vertical) flip. Fixes #1380 Fixes #3819 PR #4441 --- app/data/bash-completion/scrcpy | 9 ++-- app/data/zsh-completion/_scrcpy | 2 +- app/meson.build | 4 ++ app/scrcpy.1 | 20 +++++-- app/src/cli.c | 92 +++++++++++++++++++++++++++++++-- app/src/display.c | 16 +++--- app/src/display.h | 3 +- app/src/input_manager.c | 48 +++++++++++------ app/src/options.c | 38 +++++++++++++- app/src/options.h | 64 ++++++++++++++++++++++- app/src/scrcpy.c | 2 +- app/src/screen.c | 86 +++++++++++++++++------------- app/src/screen.h | 12 +++-- app/tests/test_orientation.c | 91 ++++++++++++++++++++++++++++++++ doc/shortcuts.md | 2 + 15 files changed, 409 insertions(+), 80 deletions(-) create mode 100644 app/tests/test_orientation.c diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f94a70a6..5e359f4f 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -21,6 +21,7 @@ _scrcpy() { --disable-screensaver --display-buffer= --display-id= + --display-orientation= -e --select-tcpip -f --fullscreen --force-adb-forward @@ -112,6 +113,10 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; + --display-orientation) + COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) return @@ -132,10 +137,6 @@ _scrcpy() { COMPREPLY=($(compgen -W 'direct3d opengl opengles2 opengles metal software' -- "$cur")) return ;; - --rotation) - COMPREPLY=($(compgen -W '0 1 2 3' -- "$cur")) - return - ;; --shortcut-mod) # Only auto-complete a single key COMPREPLY=($(compgen -W 'lctrl rctrl lalt ralt lsuper rsuper' -- "$cur")) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index cc58b866..16729d2a 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -28,6 +28,7 @@ arguments=( '--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]' {-f,--fullscreen}'[Start in fullscreen]' '--force-adb-forward[Do not attempt to use \"adb reverse\" to connect to the device]' @@ -68,7 +69,6 @@ arguments=( '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' - '--rotation=[Set the initial display rotation]:rotation values:(0 1 2 3)' {-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)' diff --git a/app/meson.build b/app/meson.build index e0d92050..b1233c6b 100644 --- a/app/meson.build +++ b/app/meson.build @@ -289,6 +289,10 @@ if get_option('buildtype') == 'debug' 'tests/test_device_msg_deserialize.c', 'src/device_msg.c', ]], + ['test_orientation', [ + 'tests/test_orientation.c', + 'src/options.c', + ]], ['test_strbuf', [ 'tests/test_strbuf.c', 'src/util/strbuf.c', diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 18941190..08a366ee 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -141,6 +141,14 @@ The available display ids can be listed by \fB\-\-list\-displays\fR. Default is 0. +.TP +.BI "\-\-display\-orientation " value +Set the initial display orientation. + +Possible values are 0, 90, 180, 270, flip0, flip90, flip180 and flip270. The number represents the clockwise rotation in degrees; the "flip" keyword applies a horizontal flip before the rotation. + +Default is 0. + .TP .B \-e, \-\-select\-tcpip Use TCP/IP device (if there is exactly one, like adb -e). @@ -369,10 +377,6 @@ Supported names are currently "direct3d", "opengl", "opengles2", "opengles", "me .B \-\-require\-audio By default, scrcpy mirrors only the video if audio capture fails on the device. This option makes scrcpy fail if audio is enabled but does not work. -.TP -.BI "\-\-rotation " value -Set the initial display rotation. Possibles values are 0, 1, 2 and 3. Each increment adds a 90 degrees rotation counterclockwise. - .TP .BI "\-s, \-\-serial " number The device serial number. Mandatory only if several devices are connected to adb. @@ -534,6 +538,14 @@ Rotate display left .B MOD+Right Rotate display right +.TP +.B MOD+Shift+Left, MOD+Shift+Right +Flip display horizontally + +.TP +.B MOD+Shift+Up, MOD+Shift+Down +Flip display vertically + .TP .B MOD+g Resize window to 1:1 (pixel\-perfect) diff --git a/app/src/cli.c b/app/src/cli.c index 12ed8111..668de31d 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -90,6 +90,7 @@ enum { OPT_CAMERA_AR, OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, + OPT_DISPLAY_ORIENTATION, }; struct sc_option { @@ -309,6 +310,17 @@ static const struct sc_option options[] = { " scrcpy --list-displays\n" "Default is 0.", }, + { + .longopt_id = OPT_DISPLAY_ORIENTATION, + .longopt = "display-orientation", + .argdesc = "value", + .text = "Set the initial display orientation.\n" + "Possible values are 0, 90, 180, 270, flip0, flip90, flip180 " + "and flip270. The number represents the clockwise rotation " + "in degrees; the \"flip\" keyword applies a horizontal flip " + "before the rotation.\n" + "Default is 0.", + }, { .shortopt = 'e', .longopt = "select-tcpip", @@ -615,12 +627,10 @@ static const struct sc_option options[] = { "is enabled but does not work." }, { + // deprecated .longopt_id = OPT_ROTATION, .longopt = "rotation", .argdesc = "value", - .text = "Set the initial display rotation.\n" - "Possible values are 0, 1, 2 and 3. Each increment adds a 90 " - "degrees rotation counterclockwise.", }, { .shortopt = 's', @@ -824,6 +834,14 @@ static const struct sc_shortcut shortcuts[] = { .shortcuts = { "MOD+Right" }, .text = "Rotate display right", }, + { + .shortcuts = { "MOD+Shift+Left", "MOD+Shift+Right" }, + .text = "Flip display horizontally", + }, + { + .shortcuts = { "MOD+Shift+Up", "MOD+Shift+Down" }, + .text = "Flip display vertically", + }, { .shortcuts = { "MOD+g" }, .text = "Resize window to 1:1 (pixel-perfect)", @@ -1405,6 +1423,45 @@ parse_rotation(const char *s, uint8_t *rotation) { return true; } +static bool +parse_orientation(const char *s, enum sc_orientation *orientation) { + if (!strcmp(s, "0")) { + *orientation = SC_ORIENTATION_0; + return true; + } + if (!strcmp(s, "90")) { + *orientation = SC_ORIENTATION_90; + return true; + } + if (!strcmp(s, "180")) { + *orientation = SC_ORIENTATION_180; + return true; + } + if (!strcmp(s, "270")) { + *orientation = SC_ORIENTATION_270; + return true; + } + if (!strcmp(s, "flip0")) { + *orientation = SC_ORIENTATION_FLIP_0; + return true; + } + if (!strcmp(s, "flip90")) { + *orientation = SC_ORIENTATION_FLIP_90; + return true; + } + if (!strcmp(s, "flip180")) { + *orientation = SC_ORIENTATION_FLIP_180; + return true; + } + if (!strcmp(s, "flip270")) { + *orientation = SC_ORIENTATION_FLIP_270; + return true; + } + LOGE("Unsupported orientation: %s (expected 0, 90, 180, 270, flip0, " + "flip90, flip180 or flip270)", optarg); + return false; +} + static bool parse_window_position(const char *s, int16_t *position) { // special value for "auto" @@ -2008,7 +2065,34 @@ 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: - if (!parse_rotation(optarg, &opts->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; + case OPT_DISPLAY_ORIENTATION: + if (!parse_orientation(optarg, &opts->display_orientation)) { return false; } break; diff --git a/app/src/display.c b/app/src/display.c index cf26e776..906b5d65 100644 --- a/app/src/display.c +++ b/app/src/display.c @@ -234,7 +234,7 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame) { enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - unsigned rotation) { + enum sc_orientation orientation) { SDL_RenderClear(display->renderer); if (display->pending.flags) { @@ -247,33 +247,33 @@ sc_display_render(struct sc_display *display, const SDL_Rect *geometry, SDL_Renderer *renderer = display->renderer; SDL_Texture *texture = display->texture; - if (rotation == 0) { + if (orientation == SC_ORIENTATION_0) { int ret = SDL_RenderCopy(renderer, texture, NULL, geometry); if (ret) { LOGE("Could not render texture: %s", SDL_GetError()); return SC_DISPLAY_RESULT_ERROR; } } else { - // rotation in RenderCopyEx() is clockwise, while screen->rotation is - // counterclockwise (to be consistent with --lock-video-orientation) - int cw_rotation = (4 - rotation) % 4; + unsigned cw_rotation = sc_orientation_get_rotation(orientation); double angle = 90 * cw_rotation; const SDL_Rect *dstrect = NULL; SDL_Rect rect; - if (rotation & 1) { + if (sc_orientation_is_swap(orientation)) { rect.x = geometry->x + (geometry->w - geometry->h) / 2; rect.y = geometry->y + (geometry->h - geometry->w) / 2; rect.w = geometry->h; rect.h = geometry->w; dstrect = ▭ } else { - assert(rotation == 2); dstrect = geometry; } + SDL_RendererFlip flip = sc_orientation_is_mirror(orientation) + ? SDL_FLIP_HORIZONTAL : 0; + int ret = SDL_RenderCopyEx(renderer, texture, NULL, dstrect, angle, - NULL, 0); + NULL, flip); if (ret) { LOGE("Could not render texture: %s", SDL_GetError()); return SC_DISPLAY_RESULT_ERROR; diff --git a/app/src/display.h b/app/src/display.h index 6b83a5c9..643ce73c 100644 --- a/app/src/display.h +++ b/app/src/display.h @@ -9,6 +9,7 @@ #include "coords.h" #include "opengl.h" +#include "options.h" #ifdef __APPLE__ # define SC_DISPLAY_FORCE_OPENGL_CORE_PROFILE @@ -54,6 +55,6 @@ sc_display_update_texture(struct sc_display *display, const AVFrame *frame); enum sc_display_result sc_display_render(struct sc_display *display, const SDL_Rect *geometry, - unsigned rotation); + enum sc_orientation orientation); #endif diff --git a/app/src/input_manager.c b/app/src/input_manager.c index c9e83d48..9a487836 100644 --- a/app/src/input_manager.c +++ b/app/src/input_manager.c @@ -293,15 +293,11 @@ rotate_device(struct sc_controller *controller) { } static void -rotate_client_left(struct sc_screen *screen) { - unsigned new_rotation = (screen->rotation + 1) % 4; - sc_screen_set_rotation(screen, new_rotation); -} - -static void -rotate_client_right(struct sc_screen *screen) { - unsigned new_rotation = (screen->rotation + 3) % 4; - sc_screen_set_rotation(screen, new_rotation); +apply_orientation_transform(struct sc_screen *screen, + enum sc_orientation transform) { + enum sc_orientation new_orientation = + sc_orientation_apply(screen->orientation, transform); + sc_screen_set_orientation(screen, new_orientation); } static void @@ -421,25 +417,47 @@ sc_input_manager_process_key(struct sc_input_manager *im, } return; case SDLK_DOWN: - if (controller && !shift) { + if (shift) { + if (!repeat & down) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_180); + } + } else if (controller) { // forward repeated events action_volume_down(controller, action); } return; case SDLK_UP: - if (controller && !shift) { + if (shift) { + if (!repeat & down) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_180); + } + } else if (controller) { // forward repeated events action_volume_up(controller, action); } return; case SDLK_LEFT: - if (!shift && !repeat && down) { - rotate_client_left(im->screen); + if (!repeat && down) { + if (shift) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im->screen, + SC_ORIENTATION_270); + } } return; case SDLK_RIGHT: - if (!shift && !repeat && down) { - rotate_client_right(im->screen); + if (!repeat && down) { + if (shift) { + apply_orientation_transform(im->screen, + SC_ORIENTATION_FLIP_0); + } else { + apply_orientation_transform(im->screen, + SC_ORIENTATION_90); + } } return; case SDLK_c: diff --git a/app/src/options.c b/app/src/options.c index 092fbd56..1454147a 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -39,7 +39,7 @@ const struct scrcpy_options scrcpy_options_default = { .audio_bit_rate = 0, .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, - .rotation = 0, + .display_orientation = SC_ORIENTATION_0, .window_x = SC_WINDOW_POSITION_UNDEFINED, .window_y = SC_WINDOW_POSITION_UNDEFINED, .window_width = 0, @@ -89,3 +89,39 @@ const struct scrcpy_options scrcpy_options_default = { .camera_high_speed = false, .list = 0, }; + +enum sc_orientation +sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform) { + assert(!(src & ~7)); + assert(!(transform & ~7)); + + unsigned transform_hflip = transform & 4; + unsigned transform_rotation = transform & 3; + unsigned src_hflip = src & 4; + unsigned src_rotation = src & 3; + unsigned src_swap = src & 1; + if (src_swap && transform_hflip) { + // If the src is rotated by 90 or 270 degrees, applying a flipped + // transformation requires an additional 180 degrees rotation to + // compensate for the inversion of the order of multiplication: + // + // hflip1 × rotate1 × hflip2 × rotate2 + // `--------------' `--------------' + // src transform + // + // In the final result, we want all the hflips then all the rotations, + // so we must move hflip2 to the left: + // + // hflip1 × hflip2 × rotate1' × rotate2 + // + // with rotate1' = | rotate1 if src is 0° or 180° + // | rotate1 + 180° if src is 90° or 270° + + src_rotation += 2; + } + + unsigned result_hflip = src_hflip ^ transform_hflip; + unsigned result_rotation = (transform_rotation + src_rotation) % 4; + enum sc_orientation result = result_hflip | result_rotation; + return result; +} diff --git a/app/src/options.h b/app/src/options.h index c702ceeb..5a6c3276 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -3,6 +3,7 @@ #include "common.h" +#include #include #include #include @@ -67,6 +68,67 @@ enum sc_camera_facing { SC_CAMERA_FACING_EXTERNAL, }; + // ,----- hflip (applied before the rotation) + // | ,--- 180° + // | | ,- 90° clockwise + // | | | +enum sc_orientation { // v v v + SC_ORIENTATION_0, // 0 0 0 + SC_ORIENTATION_90, // 0 0 1 + SC_ORIENTATION_180, // 0 1 0 + SC_ORIENTATION_270, // 0 1 1 + SC_ORIENTATION_FLIP_0, // 1 0 0 + SC_ORIENTATION_FLIP_90, // 1 0 1 + SC_ORIENTATION_FLIP_180, // 1 1 0 + SC_ORIENTATION_FLIP_270, // 1 1 1 +}; + +static inline bool +sc_orientation_is_mirror(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 4; +} + +// Does the orientation swap width and height? +static inline bool +sc_orientation_is_swap(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 1; +} + +static inline enum sc_orientation +sc_orientation_get_rotation(enum sc_orientation orientation) { + assert(!(orientation & ~7)); + return orientation & 3; +} + +enum sc_orientation +sc_orientation_apply(enum sc_orientation src, enum sc_orientation transform); + +static inline const char * +sc_orientation_get_name(enum sc_orientation orientation) { + switch (orientation) { + case SC_ORIENTATION_0: + return "0"; + case SC_ORIENTATION_90: + return "90"; + case SC_ORIENTATION_180: + return "180"; + case SC_ORIENTATION_270: + return "270"; + case SC_ORIENTATION_FLIP_0: + return "flip0"; + case SC_ORIENTATION_FLIP_90: + return "flip90"; + case SC_ORIENTATION_FLIP_180: + return "flip180"; + case SC_ORIENTATION_FLIP_270: + return "flip270"; + default: + return "(unknown)"; + } +} + enum sc_lock_video_orientation { SC_LOCK_VIDEO_ORIENTATION_UNLOCKED = -1, // lock the current orientation when scrcpy starts @@ -157,7 +219,7 @@ struct scrcpy_options { uint32_t audio_bit_rate; uint16_t max_fps; enum sc_lock_video_orientation lock_video_orientation; - uint8_t rotation; + enum sc_orientation display_orientation; 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 ac2b8e33..9bbe14b8 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -691,7 +691,7 @@ aoa_hid_end: .window_width = options->window_width, .window_height = options->window_height, .window_borderless = options->window_borderless, - .rotation = options->rotation, + .orientation = options->display_orientation, .mipmaps = options->mipmaps, .fullscreen = options->fullscreen, .start_fps_counter = options->start_fps_counter, diff --git a/app/src/screen.c b/app/src/screen.c index 5b7a8808..091001bc 100644 --- a/app/src/screen.c +++ b/app/src/screen.c @@ -14,16 +14,16 @@ #define DOWNCAST(SINK) container_of(SINK, struct sc_screen, frame_sink) static inline struct sc_size -get_rotated_size(struct sc_size size, int rotation) { - struct sc_size rotated_size; - if (rotation & 1) { - rotated_size.width = size.height; - rotated_size.height = size.width; +get_oriented_size(struct sc_size size, enum sc_orientation orientation) { + struct sc_size oriented_size; + if (sc_orientation_is_swap(orientation)) { + oriented_size.width = size.height; + oriented_size.height = size.width; } else { - rotated_size.width = size.width; - rotated_size.height = size.height; + oriented_size.width = size.width; + oriented_size.height = size.height; } - return rotated_size; + return oriented_size; } // get the window size in a struct sc_size @@ -251,7 +251,7 @@ sc_screen_render(struct sc_screen *screen, bool update_content_rect) { } enum sc_display_result res = - sc_display_render(&screen->display, &screen->rect, screen->rotation); + sc_display_render(&screen->display, &screen->rect, screen->orientation); (void) res; // any error already logged } @@ -379,9 +379,10 @@ sc_screen_init(struct sc_screen *screen, goto error_destroy_frame_buffer; } - screen->rotation = params->rotation; - if (screen->rotation) { - LOGI("Initial display rotation set to %u", screen->rotation); + screen->orientation = params->orientation; + if (screen->orientation != SC_ORIENTATION_0) { + LOGI("Initial display orientation set to %s", + sc_orientation_get_name(screen->orientation)); } uint32_t window_flags = SDL_WINDOW_HIDDEN @@ -559,19 +560,19 @@ apply_pending_resize(struct sc_screen *screen) { } void -sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation) { - assert(rotation < 4); - if (rotation == screen->rotation) { +sc_screen_set_orientation(struct sc_screen *screen, + enum sc_orientation orientation) { + if (orientation == screen->orientation) { return; } struct sc_size new_content_size = - get_rotated_size(screen->frame_size, rotation); + get_oriented_size(screen->frame_size, orientation); set_content_size(screen, new_content_size); - screen->rotation = rotation; - LOGI("Display rotation set to %u", rotation); + screen->orientation = orientation; + LOGI("Display orientation set to %s", sc_orientation_get_name(orientation)); sc_screen_render(screen, true); } @@ -584,7 +585,7 @@ sc_screen_init_size(struct sc_screen *screen) { // The requested size is passed via screen->frame_size struct sc_size content_size = - get_rotated_size(screen->frame_size, screen->rotation); + get_oriented_size(screen->frame_size, screen->orientation); screen->content_size = content_size; enum sc_display_result res = @@ -604,7 +605,7 @@ prepare_for_frame(struct sc_screen *screen, struct sc_size new_frame_size) { screen->frame_size = new_frame_size; struct sc_size new_content_size = - get_rotated_size(new_frame_size, screen->rotation); + get_oriented_size(new_frame_size, screen->orientation); set_content_size(screen, new_content_size); sc_screen_update_content_rect(screen); @@ -843,8 +844,7 @@ sc_screen_handle_event(struct sc_screen *screen, const SDL_Event *event) { struct sc_point sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, int32_t x, int32_t y) { - unsigned rotation = screen->rotation; - assert(rotation < 4); + enum sc_orientation orientation = screen->orientation; int32_t w = screen->content_size.width; int32_t h = screen->content_size.height; @@ -855,27 +855,43 @@ sc_screen_convert_drawable_to_frame_coords(struct sc_screen *screen, x = (int64_t) (x - screen->rect.x) * w / screen->rect.w; y = (int64_t) (y - screen->rect.y) * h / screen->rect.h; - // rotate struct sc_point result; - switch (rotation) { - case 0: + switch (orientation) { + case SC_ORIENTATION_0: result.x = x; result.y = y; break; - case 1: - result.x = h - y; - result.y = x; - break; - case 2: - result.x = w - x; - result.y = h - y; - break; - default: - assert(rotation == 3); + case SC_ORIENTATION_90: result.x = y; result.y = w - x; break; + case SC_ORIENTATION_180: + result.x = w - x; + result.y = h - y; + break; + case SC_ORIENTATION_270: + result.x = h - y; + result.y = x; + break; + case SC_ORIENTATION_FLIP_0: + result.x = w - x; + result.y = y; + break; + case SC_ORIENTATION_FLIP_90: + result.x = h - y; + result.y = w - x; + break; + case SC_ORIENTATION_FLIP_180: + result.x = x; + result.y = h - y; + break; + default: + assert(orientation == SC_ORIENTATION_FLIP_270); + result.x = y; + result.y = x; + break; } + return result; } diff --git a/app/src/screen.h b/app/src/screen.h index acbaab4b..46591be5 100644 --- a/app/src/screen.h +++ b/app/src/screen.h @@ -14,6 +14,7 @@ #include "frame_buffer.h" #include "input_manager.h" #include "opengl.h" +#include "options.h" #include "trait/key_processor.h" #include "trait/frame_sink.h" #include "trait/mouse_processor.h" @@ -49,8 +50,8 @@ struct sc_screen { // fullscreen (meaningful only when resize_pending is true) struct sc_size windowed_content_size; - // client rotation: 0, 1, 2 or 3 (x90 degrees counterclockwise) - unsigned rotation; + // client orientation + enum sc_orientation orientation; // rectangle of the content (excluding black borders) struct SDL_Rect rect; bool has_frame; @@ -86,7 +87,7 @@ struct sc_screen_params { bool window_borderless; - uint8_t rotation; + enum sc_orientation orientation; bool mipmaps; bool fullscreen; @@ -129,9 +130,10 @@ sc_screen_resize_to_fit(struct sc_screen *screen); void sc_screen_resize_to_pixel_perfect(struct sc_screen *screen); -// set the display rotation (0, 1, 2 or 3, x90 degrees counterclockwise) +// set the display orientation void -sc_screen_set_rotation(struct sc_screen *screen, unsigned rotation); +sc_screen_set_orientation(struct sc_screen *screen, + enum sc_orientation orientation); // react to SDL events // If this function returns false, scrcpy must exit with an error. diff --git a/app/tests/test_orientation.c b/app/tests/test_orientation.c new file mode 100644 index 00000000..153211fa --- /dev/null +++ b/app/tests/test_orientation.c @@ -0,0 +1,91 @@ +#include "common.h" + +#include + +#include "options.h" + +static void test_transforms(void) { + #define O(X) SC_ORIENTATION_ ## X + #define ASSERT_TRANSFORM(SRC, TR, RES) \ + assert(sc_orientation_apply(O(SRC), O(TR)) == O(RES)); + + ASSERT_TRANSFORM(0, 0, 0); + ASSERT_TRANSFORM(0, 90, 90); + ASSERT_TRANSFORM(0, 180, 180); + ASSERT_TRANSFORM(0, 270, 270); + ASSERT_TRANSFORM(0, FLIP_0, FLIP_0); + ASSERT_TRANSFORM(0, FLIP_90, FLIP_90); + ASSERT_TRANSFORM(0, FLIP_180, FLIP_180); + ASSERT_TRANSFORM(0, FLIP_270, FLIP_270); + + ASSERT_TRANSFORM(90, 0, 90); + ASSERT_TRANSFORM(90, 90, 180); + ASSERT_TRANSFORM(90, 180, 270); + ASSERT_TRANSFORM(90, 270, 0); + ASSERT_TRANSFORM(90, FLIP_0, FLIP_270); + ASSERT_TRANSFORM(90, FLIP_90, FLIP_0); + ASSERT_TRANSFORM(90, FLIP_180, FLIP_90); + ASSERT_TRANSFORM(90, FLIP_270, FLIP_180); + + ASSERT_TRANSFORM(180, 0, 180); + ASSERT_TRANSFORM(180, 90, 270); + ASSERT_TRANSFORM(180, 180, 0); + ASSERT_TRANSFORM(180, 270, 90); + ASSERT_TRANSFORM(180, FLIP_0, FLIP_180); + ASSERT_TRANSFORM(180, FLIP_90, FLIP_270); + ASSERT_TRANSFORM(180, FLIP_180, FLIP_0); + ASSERT_TRANSFORM(180, FLIP_270, FLIP_90); + + ASSERT_TRANSFORM(270, 0, 270); + ASSERT_TRANSFORM(270, 90, 0); + ASSERT_TRANSFORM(270, 180, 90); + ASSERT_TRANSFORM(270, 270, 180); + ASSERT_TRANSFORM(270, FLIP_0, FLIP_90); + ASSERT_TRANSFORM(270, FLIP_90, FLIP_180); + ASSERT_TRANSFORM(270, FLIP_180, FLIP_270); + ASSERT_TRANSFORM(270, FLIP_270, FLIP_0); + + ASSERT_TRANSFORM(FLIP_0, 0, FLIP_0); + ASSERT_TRANSFORM(FLIP_0, 90, FLIP_90); + ASSERT_TRANSFORM(FLIP_0, 180, FLIP_180); + ASSERT_TRANSFORM(FLIP_0, 270, FLIP_270); + ASSERT_TRANSFORM(FLIP_0, FLIP_0, 0); + ASSERT_TRANSFORM(FLIP_0, FLIP_90, 90); + ASSERT_TRANSFORM(FLIP_0, FLIP_180, 180); + ASSERT_TRANSFORM(FLIP_0, FLIP_270, 270); + + ASSERT_TRANSFORM(FLIP_90, 0, FLIP_90); + ASSERT_TRANSFORM(FLIP_90, 90, FLIP_180); + ASSERT_TRANSFORM(FLIP_90, 180, FLIP_270); + ASSERT_TRANSFORM(FLIP_90, 270, FLIP_0); + ASSERT_TRANSFORM(FLIP_90, FLIP_0, 270); + ASSERT_TRANSFORM(FLIP_90, FLIP_90, 0); + ASSERT_TRANSFORM(FLIP_90, FLIP_180, 90); + ASSERT_TRANSFORM(FLIP_90, FLIP_270, 180); + + ASSERT_TRANSFORM(FLIP_180, 0, FLIP_180); + ASSERT_TRANSFORM(FLIP_180, 90, FLIP_270); + ASSERT_TRANSFORM(FLIP_180, 180, FLIP_0); + ASSERT_TRANSFORM(FLIP_180, 270, FLIP_90); + ASSERT_TRANSFORM(FLIP_180, FLIP_0, 180); + ASSERT_TRANSFORM(FLIP_180, FLIP_90, 270); + ASSERT_TRANSFORM(FLIP_180, FLIP_180, 0); + ASSERT_TRANSFORM(FLIP_180, FLIP_270, 90); + + ASSERT_TRANSFORM(FLIP_270, 0, FLIP_270); + ASSERT_TRANSFORM(FLIP_270, 90, FLIP_0); + ASSERT_TRANSFORM(FLIP_270, 180, FLIP_90); + ASSERT_TRANSFORM(FLIP_270, 270, FLIP_180); + ASSERT_TRANSFORM(FLIP_270, FLIP_0, 90); + ASSERT_TRANSFORM(FLIP_270, FLIP_90, 180); + ASSERT_TRANSFORM(FLIP_270, FLIP_180, 270); + ASSERT_TRANSFORM(FLIP_270, FLIP_270, 0); +} + +int main(int argc, char *argv[]) { + (void) argc; + (void) argv; + + test_transforms(); + return 0; +} diff --git a/doc/shortcuts.md b/doc/shortcuts.md index 5e706402..c0fc2842 100644 --- a/doc/shortcuts.md +++ b/doc/shortcuts.md @@ -26,6 +26,8 @@ _[Super] is typically the Windows or Cmd key._ | Switch fullscreen mode | MOD+f | Rotate display left | MOD+ _(left)_ | Rotate display right | MOD+ _(right)_ + | Flip display horizontally | MOD+Shift+ _(left)_ \| MOD+Shift+ _(right)_ + | Flip display vertically | MOD+Shift+ _(up)_ \| MOD+Shift+ _(down)_ | 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_ From 2f926869300fdcdc1c668994739d0c379a9d525d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 13:49:14 +0100 Subject: [PATCH 34/58] Pass --lock-video-orientation argument in degrees For consistency with the new --display-orientation option, express the --lock-video-orientation in degrees clockwise: * --lock-video-orientation=0 -> --lock-video-orientation=0 * --lock-video-orientation=3 -> --lock-video-orientation=90 * --lock-video-orientation=2 -> --lock-video-orientation=180 * --lock-video-orientation=1 -> --lock-video-orientation=270 PR #4441 --- app/data/bash-completion/scrcpy | 2 +- app/data/zsh-completion/_scrcpy | 2 +- app/scrcpy.1 | 4 ++- app/src/cli.c | 57 ++++++++++++++++++++++++++------- app/src/options.h | 6 ++-- 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 5e359f4f..0ecace96 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -118,7 +118,7 @@ _scrcpy() { return ;; --lock-video-orientation) - COMPREPLY=($(compgen -W 'unlocked initial 0 1 2 3' -- "$cur")) + COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) return ;; --pause-on-exit) diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 16729d2a..3f65cb4e 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -41,7 +41,7 @@ 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 1 2 3)' + '--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,--hid-mouse}'[Simulate a physical mouse by using HID over AOAv2]' '--max-fps=[Limit the frame rate of screen capture]' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 08a366ee..266ba1f4 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -215,7 +215,9 @@ List displays available on the device. .TP \fB\-\-lock\-video\-orientation\fR[=\fIvalue\fR] -Lock video orientation to \fIvalue\fR. Possible values are "unlocked", "initial" (locked to the initial orientation), 0, 1, 2 and 3. Natural device orientation is 0, and each increment adds a 90 degrees rotation counterclockwise. +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". diff --git a/app/src/cli.c b/app/src/cli.c index 668de31d..37c2274c 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -411,11 +411,11 @@ static const struct sc_option options[] = { .longopt = "lock-video-orientation", .argdesc = "value", .optional_arg = true, - .text = "Lock video orientation to value.\n" + .text = "Lock capture video orientation to value.\n" "Possible values are \"unlocked\", \"initial\" (locked to the " - "initial orientation), 0, 1, 2 and 3. Natural device " - "orientation is 0, and each increment adds a 90 degrees " - "rotation counterclockwise.\n" + "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\".", @@ -1400,15 +1400,50 @@ parse_lock_video_orientation(const char *s, return true; } - long value; - bool ok = parse_integer_arg(s, &value, false, 0, 3, - "lock video orientation"); - if (!ok) { - return false; + if (!strcmp(s, "0")) { + *lock_mode = SC_LOCK_VIDEO_ORIENTATION_0; + return true; } - *lock_mode = (enum sc_lock_video_orientation) value; - 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 diff --git a/app/src/options.h b/app/src/options.h index 5a6c3276..4fb45840 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -134,9 +134,9 @@ enum sc_lock_video_orientation { // lock the current orientation when scrcpy starts SC_LOCK_VIDEO_ORIENTATION_INITIAL = -2, SC_LOCK_VIDEO_ORIENTATION_0 = 0, - SC_LOCK_VIDEO_ORIENTATION_1, - SC_LOCK_VIDEO_ORIENTATION_2, - SC_LOCK_VIDEO_ORIENTATION_3, + SC_LOCK_VIDEO_ORIENTATION_90 = 3, + SC_LOCK_VIDEO_ORIENTATION_180 = 2, + SC_LOCK_VIDEO_ORIENTATION_270 = 1, }; enum sc_keyboard_input_mode { From a9d6cb58374e27d34988db2245bfddf9402ad57d Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 14:02:46 +0100 Subject: [PATCH 35/58] Add --record-orientation Add an option to store the orientation to apply in a recorded file. Only rotations are supported (not flips). PR #4441 --- app/data/bash-completion/scrcpy | 5 ++++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 8 +++++ app/src/cli.c | 24 +++++++++++++++ app/src/compat.h | 11 +++++++ app/src/options.c | 1 + app/src/options.h | 1 + app/src/recorder.c | 52 +++++++++++++++++++++++++++++++++ app/src/recorder.h | 3 ++ app/src/scrcpy.c | 3 +- 10 files changed, 108 insertions(+), 1 deletion(-) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index 0ecace96..f08df996 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -62,6 +62,7 @@ _scrcpy() { -r --record= --raw-key-events --record-format= + --record-orientation= --render-driver= --require-audio --rotation= @@ -117,6 +118,10 @@ _scrcpy() { COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return ;; + --record-orientation) + COMPREPLY=($(compgen -> '0 90 180 270' -- "$cur")) + return + ;; --lock-video-orientation) COMPREPLY=($(compgen -W 'unlocked initial 0 90 180 270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 3f65cb4e..0e39f96b 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -67,6 +67,7 @@ arguments=( {-r,--record=}'[Record screen to file]:record file:_files' '--raw-key-events[Inject key events for all input keys, and ignore text events]' '--record-format=[Force recording format]:format:(mp4 mkv m4a mka opus aac flac wav)' + '--record-orientation=[Set the record orientation]:orientation values:(0 90 180 270)' '--render-driver=[Request SDL to use the given render driver]:driver name:(direct3d opengl opengles2 opengles metal software)' '--require-audio=[Make scrcpy fail if audio is enabled but does not work]' {-s,--serial=}'[The device serial number \(mandatory for multiple devices only\)]:serial:($("${ADB-adb}" devices | awk '\''$2 == "device" {print $1}'\''))' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 266ba1f4..1a9386ee 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -367,6 +367,14 @@ Inject key events for all input keys, and ignore text events. .BI "\-\-record\-format " format Force recording format (mp4, mkv, m4a, mka, opus, aac, flac or wav). +.TP +.BI "\-\-record\-orientation " value +Set the record orientation. + +Possible values are 0, 90, 180 and 270. The number represents the clockwise rotation in degrees. + +Default is 0. + .TP .BI "\-\-render\-driver " name Request SDL to use the given render driver (this is just a hint). diff --git a/app/src/cli.c b/app/src/cli.c index 37c2274c..fb0f43d5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -91,6 +91,7 @@ enum { OPT_CAMERA_FPS, OPT_CAMERA_HIGH_SPEED, OPT_DISPLAY_ORIENTATION, + OPT_RECORD_ORIENTATION, }; struct sc_option { @@ -609,6 +610,15 @@ static const struct sc_option options[] = { .text = "Force recording format (mp4, mkv, m4a, mka, opus, aac, flac " "or wav).", }, + { + .longopt_id = OPT_RECORD_ORIENTATION, + .longopt = "record-orientation", + .argdesc = "value", + .text = "Set the record orientation.\n" + "Possible values are 0, 90, 180 and 270. The number represents " + "the clockwise rotation in degrees.\n" + "Default is 0.", + }, { .longopt_id = OPT_RENDER_DRIVER, .longopt = "render-driver", @@ -2131,6 +2141,11 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_RECORD_ORIENTATION: + if (!parse_orientation(optarg, &opts->record_orientation)) { + return false; + } + break; case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; @@ -2497,6 +2512,15 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], } } + if (opts->record_orientation != SC_ORIENTATION_0) { + if (sc_orientation_is_mirror(opts->record_orientation)) { + LOGE("Record orientation only supports rotation, not " + "flipping: %s", + sc_orientation_get_name(opts->record_orientation)); + return false; + } + } + if (opts->video && sc_record_format_is_audio_only(opts->record_format)) { LOGE("Audio container does not support video stream"); diff --git a/app/src/compat.h b/app/src/compat.h index e80a9dd2..fd610c02 100644 --- a/app/src/compat.h +++ b/app/src/compat.h @@ -3,7 +3,9 @@ #include "config.h" +#include #include +#include #include #ifndef __WIN32 @@ -50,6 +52,15 @@ # define SCRCPY_LAVU_HAS_CHLAYOUT #endif +// In ffmpeg/doc/APIchanges: +// 2023-10-06 - 5432d2aacad - lavc 60.15.100 - avformat.h +// Deprecate AVFormatContext.{nb_,}side_data, av_stream_add_side_data(), +// av_stream_new_side_data(), and av_stream_get_side_data(). Side data fields +// from AVFormatContext.codecpar should be used from now on. +#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(60, 15, 100) +# define SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA +#endif + #if SDL_VERSION_ATLEAST(2, 0, 6) // # define SCRCPY_SDL_HAS_HINT_TOUCH_MOUSE_EVENTS diff --git a/app/src/options.c b/app/src/options.c index 1454147a..a13df585 100644 --- a/app/src/options.c +++ b/app/src/options.c @@ -40,6 +40,7 @@ const struct scrcpy_options scrcpy_options_default = { .max_fps = 0, .lock_video_orientation = SC_LOCK_VIDEO_ORIENTATION_UNLOCKED, .display_orientation = SC_ORIENTATION_0, + .record_orientation = SC_ORIENTATION_0, .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 4fb45840..11e64fa1 100644 --- a/app/src/options.h +++ b/app/src/options.h @@ -220,6 +220,7 @@ struct scrcpy_options { uint16_t max_fps; enum sc_lock_video_orientation lock_video_orientation; enum sc_orientation display_orientation; + enum sc_orientation record_orientation; 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/recorder.c b/app/src/recorder.c index c9d5f131..9e0b3395 100644 --- a/app/src/recorder.c +++ b/app/src/recorder.c @@ -4,6 +4,7 @@ #include #include #include +#include #include "util/log.h" #include "util/str.h" @@ -493,6 +494,42 @@ run_recorder(void *data) { return 0; } +static bool +sc_recorder_set_orientation(AVStream *stream, enum sc_orientation orientation) { + assert(!sc_orientation_is_mirror(orientation)); + + uint8_t *raw_data; +#ifdef SCRCPY_LAVC_HAS_CODECPAR_CODEC_SIDEDATA + AVPacketSideData *sd = + av_packet_side_data_new(&stream->codecpar->coded_side_data, + &stream->codecpar->nb_coded_side_data, + AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9, 0); + if (!sd) { + LOG_OOM(); + return false; + } + + raw_data = sd->data; +#else + raw_data = av_stream_new_side_data(stream, AV_PKT_DATA_DISPLAYMATRIX, + sizeof(int32_t) * 9); + if (!raw_data) { + LOG_OOM(); + return false; + } +#endif + + int32_t *matrix = (int32_t *) raw_data; + + unsigned rotation = orientation; + unsigned angle = rotation * 90; + + av_display_rotation_set(matrix, angle); + + return true; +} + static bool sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, AVCodecContext *ctx) { @@ -520,6 +557,16 @@ sc_recorder_video_packet_sink_open(struct sc_packet_sink *sink, recorder->video_stream.index = stream->index; + if (recorder->orientation != SC_ORIENTATION_0) { + if (!sc_recorder_set_orientation(stream, recorder->orientation)) { + sc_mutex_unlock(&recorder->mutex); + return false; + } + + LOGI("Record orientation set to %s", + sc_orientation_get_name(recorder->orientation)); + } + recorder->video_init = true; sc_cond_signal(&recorder->cond); sc_mutex_unlock(&recorder->mutex); @@ -689,7 +736,10 @@ sc_recorder_stream_init(struct sc_recorder_stream *stream) { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool video, bool audio, + enum sc_orientation orientation, const struct sc_recorder_callbacks *cbs, void *cbs_userdata) { + assert(!sc_orientation_is_mirror(orientation)); + recorder->filename = strdup(filename); if (!recorder->filename) { LOG_OOM(); @@ -710,6 +760,8 @@ sc_recorder_init(struct sc_recorder *recorder, const char *filename, recorder->video = video; recorder->audio = audio; + recorder->orientation = orientation; + sc_vecdeque_init(&recorder->video_queue); sc_vecdeque_init(&recorder->audio_queue); recorder->stopped = false; diff --git a/app/src/recorder.h b/app/src/recorder.h index 16327584..d096e79a 100644 --- a/app/src/recorder.h +++ b/app/src/recorder.h @@ -34,6 +34,8 @@ struct sc_recorder { bool audio; bool video; + enum sc_orientation orientation; + char *filename; enum sc_record_format format; AVFormatContext *ctx; @@ -67,6 +69,7 @@ struct sc_recorder_callbacks { bool sc_recorder_init(struct sc_recorder *recorder, const char *filename, enum sc_record_format format, bool video, bool audio, + enum sc_orientation orientation, const struct sc_recorder_callbacks *cbs, void *cbs_userdata); bool diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 9bbe14b8..d62a5f52 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -507,7 +507,8 @@ scrcpy(struct scrcpy_options *options) { }; if (!sc_recorder_init(&s->recorder, options->record_filename, options->record_format, options->video, - options->audio, &recorder_cbs, NULL)) { + options->audio, options->record_orientation, + &recorder_cbs, NULL)) { goto end; } recorder_initialized = true; From b43a9e8e7a70e3b847d13eeb17ebf18708c4d7fc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 17:49:04 +0100 Subject: [PATCH 36/58] Add --orientation Add a shortcut to set both the display and record orientations. PR #4441 --- app/data/bash-completion/scrcpy | 2 ++ app/data/zsh-completion/_scrcpy | 1 + app/scrcpy.1 | 4 ++++ app/src/cli.c | 16 ++++++++++++++++ 4 files changed, 23 insertions(+) diff --git a/app/data/bash-completion/scrcpy b/app/data/bash-completion/scrcpy index f08df996..0c854310 100644 --- a/app/data/bash-completion/scrcpy +++ b/app/data/bash-completion/scrcpy @@ -51,6 +51,7 @@ _scrcpy() { --no-power-on --no-video --no-video-playback + --orientation= --otg -p --port= --pause-on-exit @@ -114,6 +115,7 @@ _scrcpy() { COMPREPLY=($(compgen -W 'front back external' -- "$cur")) return ;; + --orientation --display-orientation) COMPREPLY=($(compgen -> '0 90 180 270 flip0 flip90 flip180 flip270' -- "$cur")) return diff --git a/app/data/zsh-completion/_scrcpy b/app/data/zsh-completion/_scrcpy index 0e39f96b..3c7ca217 100644 --- a/app/data/zsh-completion/_scrcpy +++ b/app/data/zsh-completion/_scrcpy @@ -57,6 +57,7 @@ arguments=( '--no-power-on[Do not power on the device on start]' '--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)' '--otg[Run in OTG mode \(simulating physical keyboard and mouse\)]' {-p,--port=}'[\[port\[\:port\]\] Set the TCP port \(range\) used by the client to listen]' '--pause-on-exit=[Make scrcpy pause before exiting]:mode:(true false if-error)' diff --git a/app/scrcpy.1 b/app/scrcpy.1 index 1a9386ee..0c34b4e2 100644 --- a/app/scrcpy.1 +++ b/app/scrcpy.1 @@ -299,6 +299,10 @@ Disable video forwarding. .B \-\-no\-video\-playback Disable video playback on the computer. +.TP +.BI "\-\-orientation " value +Same as --display-orientation=value --record-orientation=value. + .TP .B \-\-otg Run in OTG mode: simulate physical keyboard and mouse, as if the computer keyboard and mouse were plugged directly to the device via an OTG cable. diff --git a/app/src/cli.c b/app/src/cli.c index fb0f43d5..f57b75ef 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -92,6 +92,7 @@ enum { OPT_CAMERA_HIGH_SPEED, OPT_DISPLAY_ORIENTATION, OPT_RECORD_ORIENTATION, + OPT_ORIENTATION, }; struct sc_option { @@ -525,6 +526,13 @@ static const struct sc_option options[] = { .longopt = "no-video-playback", .text = "Disable video playback on the computer.", }, + { + .longopt_id = OPT_ORIENTATION, + .longopt = "orientation", + .argdesc = "value", + .text = "Same as --display-orientation=value " + "--record-orientation=value.", + }, { .longopt_id = OPT_OTG, .longopt = "otg", @@ -2146,6 +2154,14 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; + case OPT_ORIENTATION: + enum sc_orientation orientation; + if (!parse_orientation(optarg, &orientation)) { + return false; + } + opts->display_orientation = orientation; + opts->record_orientation = orientation; + break; case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; From 94031dfe97402d322e50c4d156456057fad53da0 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Mon, 20 Nov 2023 20:56:36 +0100 Subject: [PATCH 37/58] Update documentation about video orientation PR #4441 --- doc/camera.md | 10 ++++++++++ doc/recording.md | 6 ++++++ doc/video.md | 45 ++++++++++++++++++++++++++++----------------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/doc/camera.md b/doc/camera.md index d1008bda..fcc410fa 100644 --- a/doc/camera.md +++ b/doc/camera.md @@ -101,6 +101,16 @@ scrcpy --video-source=camera --camera-size=1920x1080 -m3000 # error ``` +## Rotation + +To rotate the captured video, use the [video orientation](video.md#orientation) +option: + +``` +scrcpy --video-source=camera --camera-size=1920x1080 --orientation=90 +``` + + ## Frame rate By default, camera is captured at Android's default frame rate (30 fps). diff --git a/doc/recording.md b/doc/recording.md index c1a8445e..216542e9 100644 --- a/doc/recording.md +++ b/doc/recording.md @@ -50,6 +50,12 @@ scrcpy --record=file --record-format=mkv ``` +## Rotation + +The video can be recorded rotated. See [video +orientation](video.md#orientation). + + ## No playback To disable playback while recording: diff --git a/doc/video.md b/doc/video.md index 512e0aba..ed92cb22 100644 --- a/doc/video.md +++ b/doc/video.md @@ -97,39 +97,50 @@ scrcpy --video-codec=h264 --video-encoder='OMX.qcom.video.encoder.avc' ``` -## Rotation +## Orientation -The rotation may be applied at 3 different levels: +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 of the video sent from the device to the computer). This affects the recording. - - `--rotation` rotates only the window content. This only affects the display, - not the recording. It may be changed dynamically at any time using the - [shortcuts](shortcuts.md) MOD+ and - MOD+. + - `--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: +To lock the mirroring orientation (on the capture side): ```bash -scrcpy --lock-video-orientation # initial (current) orientation -scrcpy --lock-video-orientation=0 # natural orientation -scrcpy --lock-video-orientation=1 # 90° counterclockwise -scrcpy --lock-video-orientation=2 # 180° -scrcpy --lock-video-orientation=3 # 90° clockwise +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 ``` -To set an initial window rotation: +To orient the video (on the rendering side): ```bash -scrcpy --rotation=0 # no rotation -scrcpy --rotation=1 # 90 degrees counterclockwise -scrcpy --rotation=2 # 180 degrees -scrcpy --rotation=3 # 90 degrees clockwise +scrcpy --orientation=0 +scrcpy --orientation=90 # 90° clockwise +scrcpy --orientation=180 # 180° +scrcpy --orientation=270 # 270° clockwise +scrcpy --orientation=flip0 # hflip +scrcpy --orientation=flip90 # hflip + 90° clockwise +scrcpy --orientation=flip180 # vflip (hflip + 180°) +scrcpy --orientation=flip270 # hflip + 270° clockwise ``` +The orientation can be set separately for display and record if necessary, via +`--display-orientation` and `--record-orientation`. + +The rotation is applied to a recorded file by writing a display transformation +to the MP4 or MKV target file. Flipping is not supported, so only the 4 first +values are allowed when recording. + + ## Crop The device screen may be cropped to mirror only part of the screen. From 85a94dd4b563e961304b2d9082932c5c1cc2e582 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Tue, 21 Nov 2023 14:59:24 +0100 Subject: [PATCH 38/58] Fix meson deprecated 'pkgconfig' to 'pkg-config' When running ./release.sh: > DEPRECATION: "pkgconfig" entry is deprecated and should be replaced by > "pkg-config" --- cross_win32.txt | 2 +- cross_win64.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cross_win32.txt b/cross_win32.txt index e24f3722..bf3c118e 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -6,7 +6,7 @@ c = 'i686-w64-mingw32-gcc' cpp = 'i686-w64-mingw32-g++' ar = 'i686-w64-mingw32-ar' strip = 'i686-w64-mingw32-strip' -pkgconfig = 'i686-w64-mingw32-pkg-config' +pkg-config = 'i686-w64-mingw32-pkg-config' windres = 'i686-w64-mingw32-windres' [host_machine] diff --git a/cross_win64.txt b/cross_win64.txt index 39e79944..81bb0309 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -6,7 +6,7 @@ c = 'x86_64-w64-mingw32-gcc' cpp = 'x86_64-w64-mingw32-g++' ar = 'x86_64-w64-mingw32-ar' strip = 'x86_64-w64-mingw32-strip' -pkgconfig = 'x86_64-w64-mingw32-pkg-config' +pkg-config = 'x86_64-w64-mingw32-pkg-config' windres = 'x86_64-w64-mingw32-windres' [host_machine] From acb29888377580d21ab67c805576f97d5bda8bc7 Mon Sep 17 00:00:00 2001 From: sam80180 Date: Sat, 11 Nov 2023 02:01:51 +0800 Subject: [PATCH 39/58] Do not hardcode server path on the device The path can be retrieved from the classpath. PR #4416 Co-authored-by: Romain Vimont Signed-off-by: Romain Vimont --- server/src/main/java/com/genymobile/scrcpy/CleanUp.java | 6 ++---- server/src/main/java/com/genymobile/scrcpy/Server.java | 8 ++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java index 0bcd1a54..b3a1aac1 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CleanUp.java +++ b/server/src/main/java/com/genymobile/scrcpy/CleanUp.java @@ -14,8 +14,6 @@ import java.io.IOException; */ public final class CleanUp { - public static final String SERVER_PATH = "/data/local/tmp/scrcpy-server.jar"; - // A simple struct to be passed from the main process to the cleanup process public static class Config implements Parcelable { @@ -135,13 +133,13 @@ public final class CleanUp { String[] cmd = {"app_process", "/", CleanUp.class.getName(), config.toBase64()}; ProcessBuilder builder = new ProcessBuilder(cmd); - builder.environment().put("CLASSPATH", SERVER_PATH); + builder.environment().put("CLASSPATH", Server.SERVER_PATH); builder.start(); } public static void unlinkSelf() { try { - new File(SERVER_PATH).delete(); + new File(Server.SERVER_PATH).delete(); } catch (Exception e) { Ln.e("Could not unlink server", e); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 61d3497b..2a8387e0 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -3,12 +3,20 @@ package com.genymobile.scrcpy; import android.os.BatteryManager; import android.os.Build; +import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; public final class Server { + public static final String SERVER_PATH; + static { + String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator); + // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath + SERVER_PATH = classPaths[0]; + } + private static class Completion { private int running; private boolean fatalError; From c573bd2a33c8fcd64005daca85a67a26bc958dfe Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Nov 2023 23:50:00 +0100 Subject: [PATCH 40/58] Fix java code style --- .../com/genymobile/scrcpy/AsyncProcessor.java | 2 ++ .../com/genymobile/scrcpy/CameraCapture.java | 21 +++++++++---------- .../com/genymobile/scrcpy/Controller.java | 10 ++++----- .../java/com/genymobile/scrcpy/Device.java | 2 +- .../scrcpy/DeviceMessageSender.java | 1 + .../java/com/genymobile/scrcpy/Server.java | 1 + .../java/com/genymobile/scrcpy/Settings.java | 2 +- .../scrcpy/wrappers/ClipboardManager.java | 4 ++-- .../scrcpy/wrappers/PowerManager.java | 2 +- .../scrcpy/wrappers/ServiceManager.java | 1 + .../scrcpy/wrappers/WindowManager.java | 2 +- 11 files changed, 25 insertions(+), 23 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java index b9b6745c..d5da6a90 100644 --- a/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java +++ b/server/src/main/java/com/genymobile/scrcpy/AsyncProcessor.java @@ -11,6 +11,8 @@ public interface AsyncProcessor { } void start(TerminationListener listener); + void stop(); + void join() throws InterruptedException; } diff --git a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java index b9da3658..a1003829 100644 --- a/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java +++ b/server/src/main/java/com/genymobile/scrcpy/CameraCapture.java @@ -289,18 +289,17 @@ public class CameraCapture extends SurfaceCapture { List outputs = Arrays.asList(outputConfig); int sessionType = highSpeed ? SessionConfiguration.SESSION_HIGH_SPEED : SessionConfiguration.SESSION_REGULAR; - SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, - new CameraCaptureSession.StateCallback() { - @Override - public void onConfigured(CameraCaptureSession session) { - future.complete(session); - } + SessionConfiguration sessionConfig = new SessionConfiguration(sessionType, outputs, cameraExecutor, new CameraCaptureSession.StateCallback() { + @Override + public void onConfigured(CameraCaptureSession session) { + future.complete(session); + } - @Override - public void onConfigureFailed(CameraCaptureSession session) { - future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); - } - }); + @Override + public void onConfigureFailed(CameraCaptureSession session) { + future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR)); + } + }); camera.createCaptureSession(sessionConfig); diff --git a/server/src/main/java/com/genymobile/scrcpy/Controller.java b/server/src/main/java/com/genymobile/scrcpy/Controller.java index 733a2032..3b0e9031 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Controller.java +++ b/server/src/main/java/com/genymobile/scrcpy/Controller.java @@ -318,9 +318,8 @@ 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); + 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); } @@ -341,9 +340,8 @@ public class Controller implements AsyncProcessor { coords.setAxisValue(MotionEvent.AXIS_HSCROLL, hScroll); coords.setAxisValue(MotionEvent.AXIS_VSCROLL, vScroll); - MotionEvent event = MotionEvent - .obtain(lastTouchDown, now, MotionEvent.ACTION_SCROLL, 1, pointerProperties, pointerCoords, 0, buttons, 1f, 1f, DEFAULT_DEVICE_ID, 0, - InputDevice.SOURCE_MOUSE, 0); + 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); } diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 4ab689b0..9fbe9239 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -11,8 +11,8 @@ import android.graphics.Rect; import android.os.Build; import android.os.IBinder; import android.os.SystemClock; -import android.view.IRotationWatcher; import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; import android.view.InputDevice; import android.view.InputEvent; import android.view.KeyCharacterMap; diff --git a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java index 628c1d3c..94e842ee 100644 --- a/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java +++ b/server/src/main/java/com/genymobile/scrcpy/DeviceMessageSender.java @@ -51,6 +51,7 @@ public final class DeviceMessageSender { } } } + public void start() { thread = new Thread(() -> { try { diff --git a/server/src/main/java/com/genymobile/scrcpy/Server.java b/server/src/main/java/com/genymobile/scrcpy/Server.java index 2a8387e0..e4a95140 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Server.java +++ b/server/src/main/java/com/genymobile/scrcpy/Server.java @@ -11,6 +11,7 @@ import java.util.List; public final class Server { public static final String SERVER_PATH; + static { String[] classPaths = System.getProperty("java.class.path").split(File.pathSeparator); // By convention, scrcpy is always executed with the absolute path of scrcpy-server.jar as the first item in the classpath diff --git a/server/src/main/java/com/genymobile/scrcpy/Settings.java b/server/src/main/java/com/genymobile/scrcpy/Settings.java index 1d38814d..1b5e5f98 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Settings.java +++ b/server/src/main/java/com/genymobile/scrcpy/Settings.java @@ -75,7 +75,7 @@ public final class Settings { String oldValue = getValue(table, key); if (!value.equals(oldValue)) { - putValue(table, key, value); + putValue(table, key, value); } return oldValue; } 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 eae66858..783a3407 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ClipboardManager.java @@ -138,8 +138,8 @@ public final class ClipboardManager { } } - private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, - IOnPrimaryClipChangedListener listener) throws InvocationTargetException, IllegalAccessException { + private static void addPrimaryClipChangedListener(Method method, int methodVersion, IInterface manager, IOnPrimaryClipChangedListener listener) + throws InvocationTargetException, IllegalAccessException { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { method.invoke(manager, listener, FakeContext.PACKAGE_NAME); return; 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 8ff074b3..93722687 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/PowerManager.java @@ -20,7 +20,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 >= Build.VERSION_CODES.KITKAT_WATCH ? "isInteractive" : "isScreenOn"; isScreenOnMethod = manager.getClass().getMethod(methodName); } return isScreenOnMethod; 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 ae04a6d2..85602c19 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/ServiceManager.java @@ -16,6 +16,7 @@ import java.lang.reflect.Method; public final class ServiceManager { private static final Method GET_SERVICE_METHOD; + static { try { GET_SERVICE_METHOD = Class.forName("android.os.ServiceManager").getDeclaredMethod("getService", String.class); 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 ce748855..a746be5c 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/WindowManager.java @@ -4,8 +4,8 @@ import com.genymobile.scrcpy.Ln; import android.annotation.TargetApi; import android.os.IInterface; -import android.view.IRotationWatcher; import android.view.IDisplayFoldListener; +import android.view.IRotationWatcher; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; From 67f356f881898e21edabd4ecd2fb509ad4c92362 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 21:25:13 +0100 Subject: [PATCH 41/58] Improve crossbuild Install all the prebuilt dependencies for Windows to a specific folder, and use meson command line options to specify their location. This removes crossbuild-specific code from the meson scripts and will simplify dependency upgrades. PR #4460 --- app/meson.build | 79 +++++------------------------ app/prebuilt-deps/prepare-ffmpeg.sh | 4 +- app/prebuilt-deps/prepare-libusb.sh | 12 +++-- cross_win32.txt | 5 -- cross_win64.txt | 5 -- release.mk | 68 +++++++++++++------------ 6 files changed, 59 insertions(+), 114 deletions(-) diff --git a/app/meson.build b/app/meson.build index b1233c6b..88e2df9a 100644 --- a/app/meson.build +++ b/app/meson.build @@ -98,77 +98,24 @@ endif cc = meson.get_compiler('c') -crossbuild_windows = meson.is_cross_build() and host_machine.system() == 'windows' +dependencies = [ + dependency('libavformat', version: '>= 57.33'), + dependency('libavcodec', version: '>= 57.37'), + dependency('libavutil'), + dependency('libswresample'), + dependency('sdl2', version: '>= 2.0.5'), +] -if not crossbuild_windows - - # native build - dependencies = [ - dependency('libavformat', version: '>= 57.33'), - dependency('libavcodec', version: '>= 57.37'), - dependency('libavutil'), - dependency('libswresample'), - dependency('sdl2', version: '>= 2.0.5'), - ] - - if v4l2_support - dependencies += dependency('libavdevice') - endif - - if usb_support - dependencies += dependency('libusb-1.0') - endif - -else - # cross-compile mingw32 build (from Linux to Windows) - prebuilt_sdl2 = meson.get_cross_property('prebuilt_sdl2') - sdl2_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/bin' - sdl2_lib_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_sdl2 + '/lib' - sdl2_include_dir = 'prebuilt-deps/data/' + prebuilt_sdl2 + '/include' - - sdl2 = declare_dependency( - dependencies: [ - cc.find_library('SDL2', dirs: sdl2_bin_dir), - cc.find_library('SDL2main', dirs: sdl2_lib_dir), - ], - include_directories: include_directories(sdl2_include_dir) - ) - - prebuilt_ffmpeg = meson.get_cross_property('prebuilt_ffmpeg') - ffmpeg_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_ffmpeg + '/bin' - ffmpeg_include_dir = 'prebuilt-deps/data/' + prebuilt_ffmpeg + '/include' - - ffmpeg = declare_dependency( - dependencies: [ - cc.find_library('avcodec-60', dirs: ffmpeg_bin_dir), - cc.find_library('avformat-60', dirs: ffmpeg_bin_dir), - cc.find_library('avutil-58', dirs: ffmpeg_bin_dir), - cc.find_library('swresample-4', dirs: ffmpeg_bin_dir), - ], - include_directories: include_directories(ffmpeg_include_dir) - ) - - prebuilt_libusb = meson.get_cross_property('prebuilt_libusb') - libusb_bin_dir = meson.current_source_dir() + '/prebuilt-deps/data/' + prebuilt_libusb + '/bin' - libusb_include_dir = 'prebuilt-deps/data/' + prebuilt_libusb + '/include' - - libusb = declare_dependency( - dependencies: [ - cc.find_library('msys-usb-1.0', dirs: libusb_bin_dir), - ], - include_directories: include_directories(libusb_include_dir) - ) - - dependencies = [ - ffmpeg, - sdl2, - libusb, - cc.find_library('mingw32') - ] +if v4l2_support + dependencies += dependency('libavdevice') +endif +if usb_support + dependencies += dependency('libusb-1.0') endif if host_machine.system() == 'windows' + dependencies += cc.find_library('mingw32') dependencies += cc.find_library('ws2_32') endif diff --git a/app/prebuilt-deps/prepare-ffmpeg.sh b/app/prebuilt-deps/prepare-ffmpeg.sh index 96ea3ee7..19840afb 100755 --- a/app/prebuilt-deps/prepare-ffmpeg.sh +++ b/app/prebuilt-deps/prepare-ffmpeg.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=6.1-scrcpy-2 +VERSION=6.1-scrcpy-3 DEP_DIR="ffmpeg-$VERSION" FILENAME="$DEP_DIR".7z -SHA256SUM=7f25f638dc24a0f5d4af07a088b6a604cf33548900bbfd2f6ce0bae050b7664d +SHA256SUM=b646d18a3d543a4e4c46881568213499f22e4454a464e1552f03f2ac9cc3a05a if [[ -d "$DEP_DIR" ]] then diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh index 47cf1df4..6a052f0d 100755 --- a/app/prebuilt-deps/prepare-libusb.sh +++ b/app/prebuilt-deps/prepare-libusb.sh @@ -23,11 +23,15 @@ mkdir "$DEP_DIR" cd "$DEP_DIR" 7z x "../$FILENAME" \ - libusb-1.0.26-binaries/libusb-MinGW-Win32/bin/msys-usb-1.0.dll \ - libusb-1.0.26-binaries/libusb-MinGW-Win32/include/ \ - libusb-1.0.26-binaries/libusb-MinGW-x64/bin/msys-usb-1.0.dll \ - libusb-1.0.26-binaries/libusb-MinGW-x64/include/ + libusb-1.0.26-binaries/libusb-MinGW-Win32/ \ + libusb-1.0.26-binaries/libusb-MinGW-Win32/ \ + libusb-1.0.26-binaries/libusb-MinGW-x64/ \ + libusb-1.0.26-binaries/libusb-MinGW-x64/ mv libusb-1.0.26-binaries/libusb-MinGW-Win32 . mv libusb-1.0.26-binaries/libusb-MinGW-x64 . rm -rf libusb-1.0.26-binaries + +# Rename the dll to get the same library name on all platforms +mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll +mv libusb-MinGW-x64/bin/msys-usb-1.0.dll libusb-MinGW-x64/bin/libusb-1.0.dll diff --git a/cross_win32.txt b/cross_win32.txt index bf3c118e..05f9a86b 100644 --- a/cross_win32.txt +++ b/cross_win32.txt @@ -14,8 +14,3 @@ system = 'windows' cpu_family = 'x86' cpu = 'i686' endian = 'little' - -[properties] -prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win32' -prebuilt_sdl2 = 'SDL2-2.28.4/i686-w64-mingw32' -prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-Win32' diff --git a/cross_win64.txt b/cross_win64.txt index 81bb0309..86364ad6 100644 --- a/cross_win64.txt +++ b/cross_win64.txt @@ -14,8 +14,3 @@ system = 'windows' cpu_family = 'x86' cpu = 'x86_64' endian = 'little' - -[properties] -prebuilt_ffmpeg = 'ffmpeg-6.1-scrcpy-2/win64' -prebuilt_sdl2 = 'SDL2-2.28.4/x86_64-w64-mingw32' -prebuilt_libusb = 'libusb-1.0.26/libusb-MinGW-x64' diff --git a/release.mk b/release.mk index 57fa994e..7a686f49 100644 --- a/release.mk +++ b/release.mk @@ -69,58 +69,62 @@ prepare-deps: @app/prebuilt-deps/prepare-libusb.sh build-win32: prepare-deps - [ -d "$(WIN32_BUILD_DIR)" ] || ( mkdir "$(WIN32_BUILD_DIR)" && \ - meson setup "$(WIN32_BUILD_DIR)" \ - --cross-file cross_win32.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true ) + rm -rf "$(WIN32_BUILD_DIR)" + mkdir -p "$(WIN32_BUILD_DIR)/local" + cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/" + meson setup "$(WIN32_BUILD_DIR)" \ + --pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/$(WIN32_BUILD_DIR)/local/include" \ + -Dc_link_args="-L$(PWD)/$(WIN32_BUILD_DIR)/local/lib" \ + --cross-file=cross_win32.txt \ + --buildtype=release --strip -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true ninja -C "$(WIN32_BUILD_DIR)" build-win64: prepare-deps - [ -d "$(WIN64_BUILD_DIR)" ] || ( mkdir "$(WIN64_BUILD_DIR)" && \ - meson setup "$(WIN64_BUILD_DIR)" \ - --cross-file cross_win64.txt \ - --buildtype release --strip -Db_lto=true \ - -Dcompile_server=false \ - -Dportable=true ) + rm -rf "$(WIN64_BUILD_DIR)" + mkdir -p "$(WIN64_BUILD_DIR)/local" + cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/" + meson setup "$(WIN64_BUILD_DIR)" \ + --pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \ + -Dc_args="-I$(PWD)/$(WIN64_BUILD_DIR)/local/include" \ + -Dc_link_args="-L$(PWD)/$(WIN64_BUILD_DIR)/local/lib" \ + --cross-file=cross_win64.txt \ + --buildtype=release --strip -Db_lto=true \ + -Dcompile_server=false \ + -Dportable=true ninja -C "$(WIN64_BUILD_DIR)" 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/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avutil-58.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avcodec-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/avformat-60.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win32/bin/swresample-4.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp app/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/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN32_TARGET_DIR)/" - cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/bin/msys-usb-1.0.dll "$(DIST)/$(WIN32_TARGET_DIR)/" + cp "$(WIN32_BUILD_DIR)"/local/bin/*.dll "$(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/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avutil-58.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avcodec-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/avformat-60.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-2/win64/bin/swresample-4.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp app/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/prebuilt-deps/data/platform-tools-34.0.5/adb.exe "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" cp app/prebuilt-deps/data/platform-tools-34.0.5/AdbWinUsbApi.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/bin/SDL2.dll "$(DIST)/$(WIN64_TARGET_DIR)/" - cp app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/bin/msys-usb-1.0.dll "$(DIST)/$(WIN64_TARGET_DIR)/" + cp "$(WIN64_BUILD_DIR)"/local/bin/*.dll "$(DIST)/$(WIN64_TARGET_DIR)/" zip-win32: dist-win32 cd "$(DIST)"; \ From 2370298b612bfd86da746a7480b8e159fc92c326 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 18:41:13 +0100 Subject: [PATCH 42/58] Download SDL prebuilt binaries from github The server is faster than libsdl.org. --- app/prebuilt-deps/prepare-sdl.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index 645646de..f53f090c 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -17,7 +17,7 @@ then exit 0 fi -get_file "https://libsdl.org/release/$FILENAME" "$FILENAME" "$SHA256SUM" +get_file "https://github.com/libsdl-org/SDL/releases/download/release-2.28.4/$FILENAME" "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" From 825d7f72c0a81d2aac5594aaa8fe265b468280d4 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 21:19:18 +0100 Subject: [PATCH 43/58] Extract $VERSION for dependency scripts This will allow to update the version only once in these files. --- app/prebuilt-deps/prepare-libusb.sh | 22 ++++++++++++---------- app/prebuilt-deps/prepare-sdl.sh | 8 +++++--- 2 files changed, 17 insertions(+), 13 deletions(-) diff --git a/app/prebuilt-deps/prepare-libusb.sh b/app/prebuilt-deps/prepare-libusb.sh index 6a052f0d..228a5bfa 100755 --- a/app/prebuilt-deps/prepare-libusb.sh +++ b/app/prebuilt-deps/prepare-libusb.sh @@ -6,9 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=libusb-1.0.26 +VERSION=1.0.26 +DEP_DIR="libusb-$VERSION" -FILENAME=libusb-1.0.26-binaries.7z +FILENAME="libusb-$VERSION-binaries.7z" SHA256SUM=9c242696342dbde9cdc47239391f71833939bf9f7aa2bbb28cdaabe890465ec5 if [[ -d "$DEP_DIR" ]] @@ -17,20 +18,21 @@ then exit 0 fi -get_file "https://github.com/libusb/libusb/releases/download/v1.0.26/$FILENAME" "$FILENAME" "$SHA256SUM" +get_file "https://github.com/libusb/libusb/releases/download/v$VERSION/$FILENAME" \ + "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" 7z x "../$FILENAME" \ - libusb-1.0.26-binaries/libusb-MinGW-Win32/ \ - libusb-1.0.26-binaries/libusb-MinGW-Win32/ \ - libusb-1.0.26-binaries/libusb-MinGW-x64/ \ - libusb-1.0.26-binaries/libusb-MinGW-x64/ + "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ + "libusb-$VERSION-binaries/libusb-MinGW-Win32/" \ + "libusb-$VERSION-binaries/libusb-MinGW-x64/" \ + "libusb-$VERSION-binaries/libusb-MinGW-x64/" -mv libusb-1.0.26-binaries/libusb-MinGW-Win32 . -mv libusb-1.0.26-binaries/libusb-MinGW-x64 . -rm -rf libusb-1.0.26-binaries +mv "libusb-$VERSION-binaries/libusb-MinGW-Win32" . +mv "libusb-$VERSION-binaries/libusb-MinGW-x64" . +rm -rf "libusb-$VERSION-binaries" # Rename the dll to get the same library name on all platforms mv libusb-MinGW-Win32/bin/msys-usb-1.0.dll libusb-MinGW-Win32/bin/libusb-1.0.dll diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index f53f090c..580461d2 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,9 +6,10 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -DEP_DIR=SDL2-2.28.4 +VERSION=2.28.4 +DEP_DIR="SDL2-$VERSION" -FILENAME=SDL2-devel-2.28.4-mingw.tar.gz +FILENAME="SDL2-devel-$VERSION-mingw.tar.gz" SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35 if [[ -d "$DEP_DIR" ]] @@ -17,7 +18,8 @@ then exit 0 fi -get_file "https://github.com/libsdl-org/SDL/releases/download/release-2.28.4/$FILENAME" "$FILENAME" "$SHA256SUM" +get_file "https://github.com/libsdl-org/SDL/releases/download/release-$VERSION/$FILENAME" \ + "$FILENAME" "$SHA256SUM" mkdir "$DEP_DIR" cd "$DEP_DIR" From eed06b141aa6cba15f1c163f8ec69adf13118b0e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 21:46:05 +0100 Subject: [PATCH 44/58] Upgrade sdl (2.28.5) for Windows Include the latest version of SDL in Windows releases. --- app/prebuilt-deps/prepare-sdl.sh | 4 ++-- release.mk | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/prebuilt-deps/prepare-sdl.sh b/app/prebuilt-deps/prepare-sdl.sh index 580461d2..7569744f 100755 --- a/app/prebuilt-deps/prepare-sdl.sh +++ b/app/prebuilt-deps/prepare-sdl.sh @@ -6,11 +6,11 @@ cd "$DIR" mkdir -p "$PREBUILT_DATA_DIR" cd "$PREBUILT_DATA_DIR" -VERSION=2.28.4 +VERSION=2.28.5 DEP_DIR="SDL2-$VERSION" FILENAME="SDL2-devel-$VERSION-mingw.tar.gz" -SHA256SUM=779d091072cf97291f80030f5232d97aa3d48ab0f2c14fe0b9d9a33c593cdc35 +SHA256SUM=3c0c655c2ebf67cad48fead72761d1601740ded30808952c3274ba223d226c21 if [[ -d "$DEP_DIR" ]] then diff --git a/release.mk b/release.mk index 7a686f49..fd969e5a 100644 --- a/release.mk +++ b/release.mk @@ -72,7 +72,7 @@ build-win32: prepare-deps rm -rf "$(WIN32_BUILD_DIR)" mkdir -p "$(WIN32_BUILD_DIR)/local" cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win32/. "$(WIN32_BUILD_DIR)/local/" - cp -r app/prebuilt-deps/data/SDL2-2.28.4/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/SDL2-2.28.5/i686-w64-mingw32/. "$(WIN32_BUILD_DIR)/local/" cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-Win32/. "$(WIN32_BUILD_DIR)/local/" meson setup "$(WIN32_BUILD_DIR)" \ --pkg-config-path="$(WIN32_BUILD_DIR)/local/lib/pkgconfig" \ @@ -88,7 +88,7 @@ build-win64: prepare-deps rm -rf "$(WIN64_BUILD_DIR)" mkdir -p "$(WIN64_BUILD_DIR)/local" cp -r app/prebuilt-deps/data/ffmpeg-6.1-scrcpy-3/win64/. "$(WIN64_BUILD_DIR)/local/" - cp -r app/prebuilt-deps/data/SDL2-2.28.4/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/" + cp -r app/prebuilt-deps/data/SDL2-2.28.5/x86_64-w64-mingw32/. "$(WIN64_BUILD_DIR)/local/" cp -r app/prebuilt-deps/data/libusb-1.0.26/libusb-MinGW-x64/. "$(WIN64_BUILD_DIR)/local/" meson setup "$(WIN64_BUILD_DIR)" \ --pkg-config-path="$(WIN64_BUILD_DIR)/local/lib/pkgconfig" \ From 5d4b8a7e6d18ee0e19a764945dc67efbd46f3e97 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Thu, 23 Nov 2023 23:50:41 +0100 Subject: [PATCH 45/58] Fix turn screen off on Android 14 On Android 14, the methods to access the display have been moved to DisplayControl, which is not in the core framework. Use a specific ClassLoader to access this class and its native dependencies. Fixes #3927 Refs #3927 comment Refs #4446 comment PR #4456 Co-authored-by: Simon Chan <1330321+yume-chan@users.noreply.github.com> Signed-off-by: Romain Vimont --- .../java/com/genymobile/scrcpy/Device.java | 10 ++- .../scrcpy/wrappers/DisplayControl.java | 80 +++++++++++++++++++ .../scrcpy/wrappers/SurfaceControl.java | 9 +++ 3 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java diff --git a/server/src/main/java/com/genymobile/scrcpy/Device.java b/server/src/main/java/com/genymobile/scrcpy/Device.java index 9fbe9239..b51ad8d3 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Device.java +++ b/server/src/main/java/com/genymobile/scrcpy/Device.java @@ -1,6 +1,7 @@ package com.genymobile.scrcpy; import com.genymobile.scrcpy.wrappers.ClipboardManager; +import com.genymobile.scrcpy.wrappers.DisplayControl; import com.genymobile.scrcpy.wrappers.InputManager; import com.genymobile.scrcpy.wrappers.ServiceManager; import com.genymobile.scrcpy.wrappers.SurfaceControl; @@ -315,8 +316,12 @@ public final class Device { */ public static boolean setScreenPowerMode(int mode) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + // On Android 14, these internal methods have been moved to DisplayControl + boolean useDisplayControl = + Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !SurfaceControl.hasPhysicalDisplayIdsMethod(); + // Change the power mode for all physical displays - long[] physicalDisplayIds = SurfaceControl.getPhysicalDisplayIds(); + long[] physicalDisplayIds = useDisplayControl ? DisplayControl.getPhysicalDisplayIds() : SurfaceControl.getPhysicalDisplayIds(); if (physicalDisplayIds == null) { Ln.e("Could not get physical display ids"); return false; @@ -324,7 +329,8 @@ public final class Device { boolean allOk = true; for (long physicalDisplayId : physicalDisplayIds) { - IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); + IBinder binder = useDisplayControl ? DisplayControl.getPhysicalDisplayToken( + physicalDisplayId) : SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); } return allOk; diff --git a/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java new file mode 100644 index 00000000..4e19beb9 --- /dev/null +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/DisplayControl.java @@ -0,0 +1,80 @@ +package com.genymobile.scrcpy.wrappers; + +import com.genymobile.scrcpy.Ln; + +import android.annotation.SuppressLint; +import android.annotation.TargetApi; +import android.os.Build; +import android.os.IBinder; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +@SuppressLint({"PrivateApi", "SoonBlockedPrivateApi", "BlockedPrivateApi"}) +@TargetApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) +public final class DisplayControl { + + private static final Class CLASS; + + static { + Class displayControlClass = null; + try { + 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, + ClassLoader.getSystemClassLoader(), 0, true, null); + + displayControlClass = classLoader.loadClass("com.android.server.display.DisplayControl"); + + Method loadMethod = Runtime.class.getDeclaredMethod("loadLibrary0", Class.class, String.class); + loadMethod.setAccessible(true); + loadMethod.invoke(Runtime.getRuntime(), displayControlClass, "android_servers"); + } catch (Throwable e) { + Ln.e("Could not initialize DisplayControl", e); + // Do not throw an exception here, the methods will fail when they are called + } + CLASS = displayControlClass; + } + + private static Method getPhysicalDisplayTokenMethod; + private static Method getPhysicalDisplayIdsMethod; + + private DisplayControl() { + // only static methods + } + + private static Method getGetPhysicalDisplayTokenMethod() throws NoSuchMethodException { + if (getPhysicalDisplayTokenMethod == null) { + getPhysicalDisplayTokenMethod = CLASS.getMethod("getPhysicalDisplayToken", long.class); + } + return getPhysicalDisplayTokenMethod; + } + + public static IBinder getPhysicalDisplayToken(long physicalDisplayId) { + try { + Method method = getGetPhysicalDisplayTokenMethod(); + return (IBinder) method.invoke(null, physicalDisplayId); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } + + private static Method getGetPhysicalDisplayIdsMethod() throws NoSuchMethodException { + if (getPhysicalDisplayIdsMethod == null) { + getPhysicalDisplayIdsMethod = CLASS.getMethod("getPhysicalDisplayIds"); + } + return getPhysicalDisplayIdsMethod; + } + + public static long[] getPhysicalDisplayIds() { + try { + Method method = getGetPhysicalDisplayIdsMethod(); + return (long[]) method.invoke(null); + } catch (InvocationTargetException | IllegalAccessException | NoSuchMethodException e) { + Ln.e("Could not invoke method", e); + return null; + } + } +} 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 595ee6d4..98259e7f 100644 --- a/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java +++ b/server/src/main/java/com/genymobile/scrcpy/wrappers/SurfaceControl.java @@ -139,6 +139,15 @@ public final class SurfaceControl { return getPhysicalDisplayIdsMethod; } + public static boolean hasPhysicalDisplayIdsMethod() { + try { + getGetPhysicalDisplayIdsMethod(); + return true; + } catch (NoSuchMethodException e) { + return false; + } + } + public static long[] getPhysicalDisplayIds() { try { Method method = getGetPhysicalDisplayIdsMethod(); From 8db4e78b34f9b2b08ce78910389a7f8f4c030f12 Mon Sep 17 00:00:00 2001 From: Kid <44045911+kidonng@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:13:56 +0800 Subject: [PATCH 46/58] Fix Linux desktop files There were too many backslashes in the Exec line. Fixes #4367 PR #4448 Signed-off-by: Romain Vimont --- app/data/scrcpy-console.desktop | 2 +- app/data/scrcpy.desktop | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index 6ca1e36a..77501456 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" +Exec=/bin/sh -c "\"\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" Icon=scrcpy Terminal=true Type=Application diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop index 1be86a2b..4557e71a 100644 --- a/app/data/scrcpy.desktop +++ b/app/data/scrcpy.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\"\\$SHELL\" -i -c scrcpy" +Exec=/bin/sh -c "\"\$SHELL\" -i -c scrcpy" Icon=scrcpy Terminal=false Type=Application From 89761213c3dd0d8ebe1ab0e8c2dd04b177b47dc2 Mon Sep 17 00:00:00 2001 From: Kid <44045911+kidonng@users.noreply.github.com> Date: Wed, 22 Nov 2023 02:32:19 +0800 Subject: [PATCH 47/58] Do not quote $SHELL in .desktop files This does not work properly on some desktop environments (KDE), and $SHELL is unlikely to require quoting. Fixes #4367 PR #4448 Signed-off-by: Romain Vimont --- app/data/scrcpy-console.desktop | 2 +- app/data/scrcpy.desktop | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index 77501456..71c0f001 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\"\$SHELL\" -i -c scrcpy --pause-on-exit=if-error" +Exec=/bin/sh -c "\\$SHELL -i -c scrcpy --pause-on-exit=if-error" Icon=scrcpy Terminal=true Type=Application diff --git a/app/data/scrcpy.desktop b/app/data/scrcpy.desktop index 4557e71a..9fb81d47 100644 --- a/app/data/scrcpy.desktop +++ b/app/data/scrcpy.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\"\$SHELL\" -i -c scrcpy" +Exec=/bin/sh -c "\\$SHELL -i -c scrcpy" Icon=scrcpy Terminal=false Type=Application From d037b02cc2205a4b4767340347bd562b6e7c68bc Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 24 Nov 2023 21:39:46 +0100 Subject: [PATCH 48/58] Fix scrcpy-console.desktop The argument passed to scrcpy was not applied, the full command must be passed as a single argument. PR #4448 --- app/data/scrcpy-console.desktop | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/data/scrcpy-console.desktop b/app/data/scrcpy-console.desktop index 71c0f001..fccd42b7 100644 --- a/app/data/scrcpy-console.desktop +++ b/app/data/scrcpy-console.desktop @@ -5,7 +5,7 @@ Comment=Display and control your Android device # For some users, the PATH or ADB environment variables are set from the shell # startup file, like .bashrc or .zshrc… Run an interactive shell to get # environment correctly initialized. -Exec=/bin/sh -c "\\$SHELL -i -c scrcpy --pause-on-exit=if-error" +Exec=/bin/sh -c "\\$SHELL -i -c 'scrcpy --pause-on-exit=if-error'" Icon=scrcpy Terminal=true Type=Application From 5f3fb843f59b058b7c38b5f7b1a561befc64e706 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Nov 2023 21:38:23 +0100 Subject: [PATCH 49/58] Bump version to 2.3 The previous version bump to 2.2 was incorrect, it was updated by: ./bump_version v2.2 instead of: ./bump_version 2.2 Correctly bump to version 2.3. Refs #4433 --- 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 832817d8..4540077c 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", "v2.2" + VALUE "ProductVersion", "2.3" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index d1f67e38..43898157 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: 'v2.2', + version: '2.3', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 1bb31360..45bf0cc8 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 200 - versionName "v2.2" + versionCode 20300 + versionName "2.3" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 5ab90a0a..9f153e2a 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=v2.2 +SCRCPY_VERSION_NAME=2.3 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From 5e061636f65a5b95432d8dd5a64b4dcd2b7dd8c9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Nov 2023 22:15:07 +0100 Subject: [PATCH 50/58] Update links to v2.3 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index b8ef9df8..9a8d688a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v2.2) +# scrcpy (v2.3) scrcpy diff --git a/doc/build.md b/doc/build.md index 15c567b5..91e2fac8 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.2`][direct-scrcpy-server] - SHA-256: `c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874` + - [`scrcpy-server-v2.3`][direct-scrcpy-server] + SHA-256: `8daed514d7796fca6987dc973e201bd15ba51d0f7258973dec92d9ded00dbd5f` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-server-v2.2 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-server-v2.3 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index bd4a69f7..93231795 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.2.zip`][direct-win64] (64-bit) - SHA-256: `9f9da88ac4c8319dcb9bf852f2d9bba942bac663413383419cddf64eaa5685bd` - - [`scrcpy-win32-v2.2.zip`][direct-win32] (32-bit) - SHA-256: `cb84269fc847b8b880e320879492a1ae6c017b42175f03e199530f7a53be9d74` + - [`scrcpy-win64-v2.3.zip`][direct-win64] (64-bit) + SHA-256: `a2fdd2733bd337261bb493e77d990078a23e7a40149dd0c0dc45725c929a715f` + - [`scrcpy-win32-v2.3.zip`][direct-win32] (32-bit) + SHA-256: `dfdbb69a872d717aed5bcfe352e571564c357fdb7a9c172d69f450fdf5154a0a` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win64-v2.2.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.2/scrcpy-win32-v2.2.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-win64-v2.3.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-win32-v2.3.zip and extract it. diff --git a/install_release.sh b/install_release.sh index adad85f7..a81b7a45 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.2/scrcpy-server-v2.2 -PREBUILT_SERVER_SHA256=c85c4aa84305efb69115cd497a120ebdd10258993b4cf123a8245b3d99d49874 +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-server-v2.3 +PREBUILT_SERVER_SHA256=8daed514d7796fca6987dc973e201bd15ba51d0f7258973dec92d9ded00dbd5f echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server From 4135c411af419f4f86dc9ec9301c88012d616c49 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 25 Nov 2023 23:53:30 +0100 Subject: [PATCH 51/58] Fix compilation error Fix the following warning/error: ../app/src/cli.c:2158:17: warning: a label can only be part of a statement and a declaration is not a statement [-Wpedantic] With some compilers, this is an error rather than a pedantic warning. Refs --- app/src/cli.c | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/cli.c b/app/src/cli.c index f57b75ef..fd4525f5 100644 --- a/app/src/cli.c +++ b/app/src/cli.c @@ -2154,7 +2154,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], return false; } break; - case OPT_ORIENTATION: + case OPT_ORIENTATION: { enum sc_orientation orientation; if (!parse_orientation(optarg, &orientation)) { return false; @@ -2162,6 +2162,7 @@ parse_args_with_getopt(struct scrcpy_cli_args *args, int argc, char *argv[], opts->display_orientation = orientation; opts->record_orientation = orientation; break; + } case OPT_RENDER_DRIVER: opts->render_driver = optarg; break; From 140a49b8bee1122b28d47b4385eccece7480f18e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sun, 26 Nov 2023 19:20:24 +0100 Subject: [PATCH 52/58] Add workaround for Samsung devices issues On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), which requires a non-null ConfigurationController. Fixes --- .../com/genymobile/scrcpy/Workarounds.java | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java index db9c9629..8781a783 100644 --- a/server/src/main/java/com/genymobile/scrcpy/Workarounds.java +++ b/server/src/main/java/com/genymobile/scrcpy/Workarounds.java @@ -49,6 +49,7 @@ public final class Workarounds { } public static void apply(boolean audio, boolean camera) { + boolean mustFillConfigurationController = false; boolean mustFillAppInfo = false; boolean mustFillAppContext = false; @@ -85,11 +86,23 @@ public final class Workarounds { mustFillAppContext = true; } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + // On some Samsung devices, DisplayManagerGlobal.getDisplayInfoLocked() calls ActivityThread.currentActivityThread().getConfiguration(), + // which requires a non-null ConfigurationController. + // ConfigurationController was introduced in Android 12, so do not attempt to set it on lower versions. + // + mustFillConfigurationController = true; + } + + if (mustFillConfigurationController) { + // Must be call before fillAppContext() because it is necessary to get a valid system context + fillConfigurationController(); + } if (mustFillAppInfo) { - Workarounds.fillAppInfo(); + fillAppInfo(); } if (mustFillAppContext) { - Workarounds.fillAppContext(); + fillAppContext(); } } @@ -149,6 +162,22 @@ public final class Workarounds { } } + private static void fillConfigurationController() { + try { + Class configurationControllerClass = Class.forName("android.app.ConfigurationController"); + Class activityThreadInternalClass = Class.forName("android.app.ActivityThreadInternal"); + Constructor configurationControllerConstructor = configurationControllerClass.getDeclaredConstructor(activityThreadInternalClass); + configurationControllerConstructor.setAccessible(true); + Object configurationController = configurationControllerConstructor.newInstance(ACTIVITY_THREAD); + + Field configurationControllerField = ACTIVITY_THREAD_CLASS.getDeclaredField("mConfigurationController"); + configurationControllerField.setAccessible(true); + configurationControllerField.set(ACTIVITY_THREAD, configurationController); + } catch (Throwable throwable) { + Ln.d("Could not fill configuration: " + throwable.getMessage()); + } + } + static Context getSystemContext() { try { Method getSystemContextMethod = ACTIVITY_THREAD_CLASS.getDeclaredMethod("getSystemContext"); From bd9292931e868621294ec64ea85179a2803976ef Mon Sep 17 00:00:00 2001 From: Johannes Neyer Date: Fri, 17 Nov 2023 16:24:34 +0100 Subject: [PATCH 53/58] Mention exclusive_caps mode in v4l2 documentation PR #4435 Signed-off-by: Romain Vimont --- doc/v4l2.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/doc/v4l2.md b/doc/v4l2.md index 23c99912..54272b2b 100644 --- a/doc/v4l2.md +++ b/doc/v4l2.md @@ -21,6 +21,13 @@ This will create a new video device in `/dev/videoN`, where `N` is an integer (more [options](https://github.com/umlaeute/v4l2loopback#options) are available to create several devices or devices with specific IDs). +If you encounter problems detecting your device with Chrome/WebRTC, you can try +`exclusive_caps` mode: + +``` +sudo modprobe v4l2loopback exclusive_caps=1 +``` + To list the enabled devices: ```bash From bf056b1fee904391bc932381c01052fb975ac62e Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Nov 2023 12:14:07 +0100 Subject: [PATCH 54/58] Do not initialize SDL video when not necessary The SDL video subsystem is required for video playback and clipboard synchronization. If neither is used, it is not necessary to initialize it. Refs 5e59ed31352251791679e5931d7e5abf0c2d18f6 Refs 110b3a16f6d02124a4567d2ab79fcb74d78f949f Refs #4418 Refs #4477 --- app/src/scrcpy.c | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index d62a5f52..0b0b8bad 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -419,12 +419,16 @@ scrcpy(struct scrcpy_options *options) { sdl_set_hints(options->render_driver); } - // Initialize the video subsystem even if --no-video or --no-video-playback - // is passed so that clipboard synchronization still works. - // - if (SDL_Init(SDL_INIT_VIDEO)) { - LOGE("Could not initialize SDL video: %s", SDL_GetError()); - goto end; + if (options->video_playback || + (options->control && options->clipboard_autosync)) { + // Initialize the video subsystem even if --no-video or + // --no-video-playback is passed so that clipboard synchronization + // still works. + // + if (SDL_Init(SDL_INIT_VIDEO)) { + LOGE("Could not initialize SDL video: %s", SDL_GetError()); + goto end; + } } if (options->audio_playback) { From 9497f39fb47e899df1247942f90eba5daa0cf204 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Wed, 29 Nov 2023 12:16:05 +0100 Subject: [PATCH 55/58] Do not fail if SDL_INIT_VIDEO fails without video The SDL video subsystem may be initialized so that clipboard synchronization works even without video playback. But if the video subsystem initialization fails (e.g. because no video device is available), consider it as an error only if video playback is enabled. Refs 5e59ed31352251791679e5931d7e5abf0c2d18f6 Fixes #4477 --- app/src/scrcpy.c | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/src/scrcpy.c b/app/src/scrcpy.c index 0b0b8bad..cf2e7e47 100644 --- a/app/src/scrcpy.c +++ b/app/src/scrcpy.c @@ -426,8 +426,13 @@ scrcpy(struct scrcpy_options *options) { // still works. // if (SDL_Init(SDL_INIT_VIDEO)) { - LOGE("Could not initialize SDL video: %s", SDL_GetError()); - goto end; + // If it fails, it is an error only if video playback is enabled + if (options->video_playback) { + LOGE("Could not initialize SDL video: %s", SDL_GetError()); + goto end; + } else { + LOGW("Could not initialize SDL video: %s", SDL_GetError()); + } } } From ef79fcbbd27d9f6e8096c278d2975c7c71bc67b9 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Fri, 1 Dec 2023 21:13:01 +0100 Subject: [PATCH 56/58] Fix AV1 demuxing For AV1, the config packet must not be merged with the next non-config packet. This fixes the following error when passing --video-codec=av1: > INFO: [FFmpeg] libdav1d 1.3.0 > ERROR: [FFmpeg] Unknown OBU type 0 of size 29393 > ERROR: [FFmpeg] Error parsing OBU data > ERROR: Decoder 'video': could not send video packet: -1094995529 PR #4487 --- app/src/demuxer.c | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/src/demuxer.c b/app/src/demuxer.c index c9ee8f3c..c27ea292 100644 --- a/app/src/demuxer.c +++ b/app/src/demuxer.c @@ -227,8 +227,9 @@ run_demuxer(void *data) { } // Config packets must be merged with the next non-config packet only for - // video streams - bool must_merge_config_packet = codec->type == AVMEDIA_TYPE_VIDEO; + // H.26x + bool must_merge_config_packet = raw_codec_id == SC_CODEC_ID_H264 + || raw_codec_id == SC_CODEC_ID_H265; struct sc_packet_merger merger; From 40f2560d987fbc88711b3aac14398d38c0e2a8f6 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Dec 2023 12:30:19 +0100 Subject: [PATCH 57/58] Bump version to 2.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 4540077c..895b9c93 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.3" + VALUE "ProductVersion", "2.3.1" END END BLOCK "VarFileInfo" diff --git a/meson.build b/meson.build index 43898157..11b974e0 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('scrcpy', 'c', - version: '2.3', + version: '2.3.1', meson_version: '>= 0.48', default_options: [ 'c_std=c11', diff --git a/server/build.gradle b/server/build.gradle index 45bf0cc8..1a18d997 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -7,8 +7,8 @@ android { applicationId "com.genymobile.scrcpy" minSdkVersion 21 targetSdkVersion 34 - versionCode 20300 - versionName "2.3" + versionCode 20301 + versionName "2.3.1" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { diff --git a/server/build_without_gradle.sh b/server/build_without_gradle.sh index 9f153e2a..69d85679 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.3 +SCRCPY_VERSION_NAME=2.3.1 PLATFORM=${ANDROID_PLATFORM:-34} BUILD_TOOLS=${ANDROID_BUILD_TOOLS:-34.0.0} From c6ff78f4147ad5505b45cce8f1d6f44c8585a9b5 Mon Sep 17 00:00:00 2001 From: Romain Vimont Date: Sat, 2 Dec 2023 12:39:05 +0100 Subject: [PATCH 58/58] Update links to v2.3.1 --- README.md | 2 +- doc/build.md | 6 +++--- doc/windows.md | 12 ++++++------ install_release.sh | 4 ++-- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9a8d688a..8fabd556 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# scrcpy (v2.3) +# scrcpy (v2.3.1) scrcpy diff --git a/doc/build.md b/doc/build.md index 91e2fac8..7e3c84e9 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.3`][direct-scrcpy-server] - SHA-256: `8daed514d7796fca6987dc973e201bd15ba51d0f7258973dec92d9ded00dbd5f` + - [`scrcpy-server-v2.3.1`][direct-scrcpy-server] + SHA-256: `f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b` -[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-server-v2.3 +[direct-scrcpy-server]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1 Download the prebuilt server somewhere, and specify its path during the Meson configuration: diff --git a/doc/windows.md b/doc/windows.md index 93231795..60fd7986 100644 --- a/doc/windows.md +++ b/doc/windows.md @@ -4,14 +4,14 @@ Download the [latest release]: - - [`scrcpy-win64-v2.3.zip`][direct-win64] (64-bit) - SHA-256: `a2fdd2733bd337261bb493e77d990078a23e7a40149dd0c0dc45725c929a715f` - - [`scrcpy-win32-v2.3.zip`][direct-win32] (32-bit) - SHA-256: `dfdbb69a872d717aed5bcfe352e571564c357fdb7a9c172d69f450fdf5154a0a` + - [`scrcpy-win64-v2.3.1.zip`][direct-win64] (64-bit) + SHA-256: `f1f78ac98214078425804e524a1bed515b9d4b8a05b78d210a4ced2b910b262d` + - [`scrcpy-win32-v2.3.1.zip`][direct-win32] (32-bit) + SHA-256: `5dffc2d432e9b8b5b0e16f12e71428c37c70d9124cfbe7620df0b41b7efe91ff` [latest release]: https://github.com/Genymobile/scrcpy/releases/latest -[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-win64-v2.3.zip -[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3/scrcpy-win32-v2.3.zip +[direct-win64]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win64-v2.3.1.zip +[direct-win32]: https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-win32-v2.3.1.zip and extract it. diff --git a/install_release.sh b/install_release.sh index a81b7a45..d8dbd951 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.3/scrcpy-server-v2.3 -PREBUILT_SERVER_SHA256=8daed514d7796fca6987dc973e201bd15ba51d0f7258973dec92d9ded00dbd5f +PREBUILT_SERVER_URL=https://github.com/Genymobile/scrcpy/releases/download/v2.3.1/scrcpy-server-v2.3.1 +PREBUILT_SERVER_SHA256=f6814822fc308a7a532f253485c9038183c6296a6c5df470a9e383b4f8e7605b echo "[scrcpy] Downloading prebuilt server..." wget "$PREBUILT_SERVER_URL" -O scrcpy-server