Compare commits

..

1 Commits

Author SHA1 Message Date
Romain Vimont
99a0b13496 Add --audio-output-buffer
On some systems, the SDL audio callback is not called frequently enough
(for example it requests 5ms of samples every 10ms), because the output
buffer is too small.

By default, we want to use a small value (5ms) to minimize latency, but
if it does not work well, users need a way to increase it.

Refs #3793 <https://github.com/Genymobile/scrcpy/issues/3793>
2023-03-14 23:35:27 +01:00
7 changed files with 47 additions and 85 deletions

View File

@@ -3,11 +3,9 @@ _scrcpy() {
local opts=" local opts="
--always-on-top --always-on-top
--audio-bit-rate= --audio-bit-rate=
--audio-buffer=
--audio-codec= --audio-codec=
--audio-codec-options= --audio-codec-options=
--audio-encoder= --audio-encoder=
--audio-output-buffer=
-b --video-bit-rate= -b --video-bit-rate=
--crop= --crop=
-d --select-usb -d --select-usb
@@ -117,26 +115,20 @@ _scrcpy() {
COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur})) COMPREPLY=($(compgen -W "$("${ADB:-adb}" devices | awk '$2 == "device" {print $1}')" -- ${cur}))
return return
;; ;;
--audio-bit-rate \ -b|--video-bit-rate \
|--audio-buffer \ |--codec-options \
|-b|--video-bit-rate \
|--audio-codec-options \
|--audio-encoder \
|--audio-output-buffer \
|--crop \ |--crop \
|--display \ |--display \
|--display-buffer \ |--display-buffer \
|--encoder \
|--max-fps \ |--max-fps \
|-m|--max-size \ |-m|--max-size \
|-p|--port \ |-p|--port \
|--push-target \ |--push-target \
|--rotation \
|--tunnel-host \ |--tunnel-host \
|--tunnel-port \ |--tunnel-port \
|--v4l2-buffer \ |--v4l2-buffer \
|--v4l2-sink \ |--v4l2-sink \
|--video-codec-options \
|--video-encoder \
|--tcpip \ |--tcpip \
|--window-*) |--window-*)
# Option accepting an argument, but nothing to auto-complete # Option accepting an argument, but nothing to auto-complete

View File

@@ -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 # 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 # startup file, like .bashrc or .zshrc… Run an interactive shell to get
# environment correctly initialized. # environment correctly initialized.
Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press Enter to quit...'" Exec=/bin/bash --norc --noprofile -i -c "\"\\$SHELL\" -i -c scrcpy || read -p 'Press any key to quit...'"
Icon=scrcpy Icon=scrcpy
Terminal=true Terminal=true
Type=Application Type=Application

View File

@@ -10,11 +10,9 @@ local arguments
arguments=( arguments=(
'--always-on-top[Make scrcpy window always on top \(above other windows\)]' '--always-on-top[Make scrcpy window always on top \(above other windows\)]'
'--audio-bit-rate=[Encode the audio at the given bit-rate]' '--audio-bit-rate=[Encode the audio at the given bit-rate]'
'--audio-buffer=[Configure the audio buffering delay (in milliseconds)]'
'--audio-codec=[Select the audio codec]:codec:(opus aac raw)' '--audio-codec=[Select the audio codec]:codec:(opus aac raw)'
'--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]' '--audio-codec-options=[Set a list of comma-separated key\:type=value options for the device audio encoder]'
'--audio-encoder=[Use a specific MediaCodec audio encoder]' '--audio-encoder=[Use a specific MediaCodec audio encoder]'
'--audio-output-buffer=[Configure the size of the SDL audio output buffer (in milliseconds)]'
{-b,--video-bit-rate=}'[Encode the video at the given bit-rate]' {-b,--video-bit-rate=}'[Encode the video at the given bit-rate]'
'--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]' '--crop=[\[width\:height\:x\:y\] Crop the device screen on the server]'
{-d,--select-usb}'[Use USB device]' {-d,--select-usb}'[Use USB device]'

View File

@@ -59,58 +59,45 @@ public final class AudioCapture {
} }
private static void startWorkaroundAndroid11() { private static void startWorkaroundAndroid11() {
// Android 11 requires Apps to be at foreground to record audio. if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground. // Android 11 requires Apps to be at foreground to record audio.
// But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android // Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
// shell ("com.android.shell"). // But scrcpy server is NOT an App, it's a Java application started from Android shell, so it has the same user ID (2000) with Android
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the // shell ("com.android.shell").
// foreground. // If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
Intent intent = new Intent(Intent.ACTION_MAIN); // foreground.
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
intent.addCategory(Intent.CATEGORY_LAUNCHER); Intent intent = new Intent(Intent.ACTION_MAIN);
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity")); intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent); intent.addCategory(Intent.CATEGORY_LAUNCHER);
} intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
private static void stopWorkaroundAndroid11() { // Wait for activity to start
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME); SystemClock.sleep(150);
}
private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
while (attempts-- > 0) {
// Wait for activity to start
SystemClock.sleep(delayMs);
try {
startRecording();
return; // it worked
} catch (UnsupportedOperationException e) {
if (attempts == 0) {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, audio capture must be started in the foreground, make sure that the device is unlocked when starting " +
"scrcpy.");
throw new AudioCaptureForegroundException();
} else {
Ln.d("Failed to start audio capture, retrying...");
}
} }
} }
} }
private void startRecording() { private static void stopWorkaroundAndroid11() {
recorder = createAudioRecord(); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
recorder.startRecording(); ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}
} }
public void start() throws AudioCaptureForegroundException { public void start() throws AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) { startWorkaroundAndroid11();
startWorkaroundAndroid11(); try {
try { recorder = createAudioRecord();
tryStartRecording(3, 100); recorder.startRecording();
} finally { } catch (UnsupportedOperationException e) {
stopWorkaroundAndroid11(); if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Ln.e("Failed to start audio capture");
Ln.e("On Android 11, it is only possible to capture in foreground, make sure that the device is unlocked when starting scrcpy.");
throw new AudioCaptureForegroundException();
} }
} else { throw e;
startRecording(); } finally {
stopWorkaroundAndroid11();
} }
} }

View File

@@ -271,22 +271,13 @@ public final class AudioEncoder implements AsyncProcessor {
try { try {
return MediaCodec.createByCodecName(encoderName); return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Ln.e("Audio encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage()); Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildAudioEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName); throw new ConfigurationException("Unknown encoder: " + encoderName);
} catch (IOException e) {
Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
throw e;
} }
} }
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
try { Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); return mediaCodec;
Ln.d("Using audio encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
} catch (IOException | IllegalArgumentException e) {
Ln.e("Could not create default audio encoder for " + codec.getName() + "\n" + LogUtils.buildAudioEncoderListMessage());
throw e;
}
} }
private class EncoderCallback extends MediaCodec.Callback { private class EncoderCallback extends MediaCodec.Callback {

View File

@@ -288,7 +288,10 @@ public final class Device {
boolean allOk = true; boolean allOk = true;
for (long physicalDisplayId : physicalDisplayIds) { for (long physicalDisplayId : physicalDisplayIds) {
IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId); IBinder binder = SurfaceControl.getPhysicalDisplayToken(physicalDisplayId);
allOk &= SurfaceControl.setDisplayPowerMode(binder, mode); boolean ok = SurfaceControl.setDisplayPowerMode(binder, mode);
if (!ok) {
allOk = false;
}
} }
return allOk; return allOk;
} }

View File

@@ -202,22 +202,13 @@ public class ScreenEncoder implements Device.RotationListener {
try { try {
return MediaCodec.createByCodecName(encoderName); return MediaCodec.createByCodecName(encoderName);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
Ln.e("Video encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage()); Ln.e("Encoder '" + encoderName + "' for " + codec.getName() + " not found\n" + LogUtils.buildVideoEncoderListMessage());
throw new ConfigurationException("Unknown encoder: " + encoderName); throw new ConfigurationException("Unknown encoder: " + encoderName);
} catch (IOException e) {
Ln.e("Could not create video encoder '" + encoderName + "' for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
throw e;
} }
} }
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType());
try { Ln.d("Using encoder: '" + mediaCodec.getName() + "'");
MediaCodec mediaCodec = MediaCodec.createEncoderByType(codec.getMimeType()); return mediaCodec;
Ln.d("Using video encoder: '" + mediaCodec.getName() + "'");
return mediaCodec;
} catch (IOException | IllegalArgumentException e) {
Ln.e("Could not create default video encoder for " + codec.getName() + "\n" + LogUtils.buildVideoEncoderListMessage());
throw e;
}
} }
private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) { private static MediaFormat createFormat(String videoMimeType, int bitRate, int maxFps, List<CodecOption> codecOptions) {