Skip to content

Commit

Permalink
Fail-fast camera mirroring on Android 11 and older
Browse files Browse the repository at this point in the history
  • Loading branch information
yume-chan committed Aug 13, 2023
1 parent b85c902 commit d0d0ece
Show file tree
Hide file tree
Showing 9 changed files with 82 additions and 133 deletions.
3 changes: 2 additions & 1 deletion app/src/cli.c
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ static const struct sc_option options[] = {
.longopt_id = OPT_VIDEO_SOURCE,
.longopt = "video-source",
.argdesc = "source",
.text = "Select the video source (display or camera).\n"
.text = "Select the video source (display or camera) to mirror.\n"
"Camera mirroring requires Android 12.\n"
"Default is display.",
},
{
Expand Down
32 changes: 27 additions & 5 deletions server/src/main/java/com/genymobile/scrcpy/AudioCapture.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package com.genymobile.scrcpy;

import com.genymobile.scrcpy.wrappers.ServiceManager;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.content.ComponentName;
import android.content.Intent;
import android.media.AudioFormat;
import android.media.AudioRecord;
import android.media.AudioTimestamp;
Expand Down Expand Up @@ -60,7 +64,25 @@ private static AudioRecord createAudioRecord(int audioSource) {
return builder.build();
}

private void tryStartRecording(int attempts, int delayMs) throws CaptureForegroundException {
private static void startWorkaroundAndroid11() {
// Android 11 requires Apps to be at foreground to record audio.
// Normally, each App has its own user ID, so Android checks whether the requesting App has the user ID that's at the foreground.
// 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
// shell ("com.android.shell").
// If there is an Activity from Android shell running at foreground, then the permission system will believe scrcpy is also in the
// foreground.
Intent intent = new Intent(Intent.ACTION_MAIN);
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
intent.addCategory(Intent.CATEGORY_LAUNCHER);
intent.setComponent(new ComponentName(FakeContext.PACKAGE_NAME, "com.android.shell.HeapDumpActivity"));
ServiceManager.getActivityManager().startActivityAsUserWithFeature(intent);
}

private static void stopWorkaroundAndroid11() {
ServiceManager.getActivityManager().forceStopPackage(FakeContext.PACKAGE_NAME);
}

private void tryStartRecording(int attempts, int delayMs) throws AudioCaptureForegroundException {
while (attempts-- > 0) {
// Wait for activity to start
SystemClock.sleep(delayMs);
Expand All @@ -72,7 +94,7 @@ private void tryStartRecording(int attempts, int delayMs) throws CaptureForegrou
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 CaptureForegroundException();
throw new AudioCaptureForegroundException();
} else {
Ln.d("Failed to start audio capture, retrying...");
}
Expand All @@ -92,13 +114,13 @@ private void startRecording() {
recorder.startRecording();
}

public void start() throws CaptureForegroundException {
public void start() throws AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Workarounds.startForegroundWorkaround();
startWorkaroundAndroid11();
try {
tryStartRecording(5, 100);
} finally {
Workarounds.stopForegroundWorkaround();
stopWorkaroundAndroid11();
}
} else {
startRecording();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.genymobile.scrcpy;

/**
* Exception thrown if audio capture failed on Android 11 specifically because the running App (shell) was not in foreground.
*/
public class AudioCaptureForegroundException extends Exception {
}
4 changes: 2 additions & 2 deletions server/src/main/java/com/genymobile/scrcpy/AudioEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ public void start(TerminationListener listener) {
} catch (ConfigurationException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
fatalError = true;
} catch (CaptureForegroundException e) {
} catch (AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been logged
} catch (IOException e) {
Ln.e("Audio encoding error", e);
Expand Down Expand Up @@ -169,7 +169,7 @@ private synchronized void waitEnded() {
}

@TargetApi(Build.VERSION_CODES.M)
public void encode() throws IOException, ConfigurationException, CaptureForegroundException {
public void encode() throws IOException, ConfigurationException, AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public AudioRawRecorder(AudioCapture capture, Streamer streamer) {
this.streamer = streamer;
}

private void record() throws IOException, CaptureForegroundException {
private void record() throws IOException, AudioCaptureForegroundException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
Ln.w("Audio disabled: it is not supported before Android 11");
streamer.writeDisableStream(false);
Expand Down Expand Up @@ -60,7 +60,7 @@ public void start(TerminationListener listener) {
boolean fatalError = false;
try {
record();
} catch (CaptureForegroundException e) {
} 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);
Expand Down
71 changes: 22 additions & 49 deletions server/src/main/java/com/genymobile/scrcpy/CameraEncoder.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.genymobile.scrcpy;

import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.graphics.Rect;
import android.hardware.camera2.CameraAccessException;
import android.hardware.camera2.CameraCaptureSession;
Expand All @@ -13,7 +14,6 @@
import android.os.Build;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.SystemClock;
import android.view.Surface;

import java.util.Arrays;
Expand All @@ -25,30 +25,30 @@

public class CameraEncoder extends SurfaceEncoder {

private int maxSize;
private String cameraId;
private CameraPosition cameraPosition;
private final String cameraId;
private final CameraPosition cameraPosition;

private int maxSize;
private String actualCameraId;
private CameraDevice cameraDevice;

private HandlerThread cameraThread;
private Handler cameraHandler;
private final Handler cameraHandler;

public CameraEncoder(int maxSize, String cameraId, CameraPosition cameraPosition, Streamer streamer,
int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
int videoBitRate, int maxFps, List<CodecOption> codecOptions, String encoderName, boolean downsizeOnError) {
super(streamer, videoBitRate, maxFps, codecOptions, encoderName, downsizeOnError);

this.maxSize = maxSize;
this.cameraId = cameraId;
this.cameraPosition = cameraPosition;

cameraThread = new HandlerThread("camera");
HandlerThread cameraThread = new HandlerThread("camera");
cameraThread.start();
cameraHandler = new Handler(cameraThread.getLooper());
}

@SuppressLint("MissingPermission")
@TargetApi(Build.VERSION_CODES.N)
private CameraDevice openCamera(String id)
throws CameraAccessException, InterruptedException {
Ln.v("Open Camera: " + id);
Expand Down Expand Up @@ -95,7 +95,7 @@ public void onError(CameraDevice camera, int error) {
}
}

@SuppressWarnings("deprecation")
@TargetApi(Build.VERSION_CODES.N)
private CameraCaptureSession createCaptureSession(CameraDevice camera, Surface surface)
throws CameraAccessException, InterruptedException {
Ln.v("Create Capture Session");
Expand All @@ -121,6 +121,7 @@ public void onConfigureFailed(CameraCaptureSession session) {
}
}

@TargetApi(Build.VERSION_CODES.N)
private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest request)
throws CameraAccessException, InterruptedException {
Ln.v("Set Repeating Request");
Expand All @@ -129,13 +130,13 @@ private void setRepeatingRequest(CameraCaptureSession session, CaptureRequest re
session.setRepeatingRequest(request, new CameraCaptureSession.CaptureCallback() {
@Override
public void onCaptureStarted(CameraCaptureSession session, CaptureRequest request,
long timestamp, long frameNumber) {
long timestamp, long frameNumber) {
future.complete(null);
}

@Override
public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request,
CaptureFailure failure) {
CaptureFailure failure) {
future.completeExceptionally(new CameraAccessException(CameraAccessException.CAMERA_ERROR));
}
}, cameraHandler);
Expand All @@ -148,7 +149,11 @@ public void onCaptureFailed(CameraCaptureSession session, CaptureRequest request
}

@Override
protected void initialize() {
protected void initialize() throws ConfigurationException {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
Ln.e("Camera mirroring is not support before Android 12");
throw new ConfigurationException("Camera mirroring is not supported");
}
}

@Override
Expand Down Expand Up @@ -219,9 +224,6 @@ protected void setSize(int size) {

private void setSurfaceInternal(Surface surface) throws CameraAccessException {
try {
// openCamera, createCaptureSession and setRepeatingRequest all
// requires foreground workaround on Android 11.
// getCameraIdList and getCameraCharacteristics don't need that.
cameraDevice = openCamera(actualCameraId);
CameraCaptureSession session = createCaptureSession(cameraDevice, surface);
CaptureRequest.Builder requestBuilder = cameraDevice
Expand All @@ -236,41 +238,12 @@ private void setSurfaceInternal(Surface surface) throws CameraAccessException {
}
}

private void trySetSurface(int attempts, int delayMs, Surface surface) throws CaptureForegroundException {
while (attempts-- > 0) {
// Wait for activity to start
SystemClock.sleep(delayMs);
try {
setSurfaceInternal(surface);
return; // it worked
} catch (CameraAccessException e) {
if (attempts == 0) {
Ln.e("Failed to start camera capture");
Ln.e("On Android 11, camera capture must be started in the foreground, make sure that the device is unlocked when starting "
+ "scrcpy.");
throw new CaptureForegroundException();
} else {
Ln.d("Failed to start camera capture, retrying...");
}
}
}
}

@Override
protected void setSurface(Surface surface) throws CaptureForegroundException {
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.R) {
Workarounds.startForegroundWorkaround();
try {
trySetSurface(5, 100, surface);
} finally {
// Workarounds.stopForegroundWorkaround();
}
} else {
try {
setSurfaceInternal(surface);
} catch (CameraAccessException e) {
throw new RuntimeException(e);
}
protected void setSurface(Surface surface) {
try {
setSurfaceInternal(surface);
} catch (CameraAccessException e) {
throw new RuntimeException(e);
}
}

Expand Down

This file was deleted.

10 changes: 5 additions & 5 deletions server/src/main/java/com/genymobile/scrcpy/SurfaceEncoder.java
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@ public SurfaceEncoder(Streamer streamer, int videoBitRate, int maxFps, List<Code
this.downsizeOnError = downsizeOnError;
}

protected abstract void initialize();
protected abstract void initialize() throws ConfigurationException;

protected abstract Size getSize() throws CaptureForegroundException, ConfigurationException;
protected abstract Size getSize() throws AudioCaptureForegroundException, ConfigurationException;

protected abstract void setSize(int size);

protected abstract void setSurface(Surface surface) throws CaptureForegroundException;
protected abstract void setSurface(Surface surface) throws AudioCaptureForegroundException;

protected abstract void dispose();

Expand All @@ -63,7 +63,7 @@ private boolean consumeResetCapture() {
}

protected void startStream()
throws CaptureForegroundException, IOException, ConfigurationException {
throws AudioCaptureForegroundException, IOException, ConfigurationException {
initialize();

Codec codec = streamer.getCodec();
Expand Down Expand Up @@ -260,7 +260,7 @@ public void start(TerminationListener listener) {

try {
startStream();
} catch (ConfigurationException | CaptureForegroundException e) {
} catch (ConfigurationException | AudioCaptureForegroundException e) {
// Do not print stack trace, a user-friendly error-message has already been
// logged
} catch (IOException e) {
Expand Down
Loading

0 comments on commit d0d0ece

Please sign in to comment.