-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
128a3b2
commit dc750a4
Showing
7 changed files
with
370 additions
and
0 deletions.
There are no files selected for viewing
17 changes: 17 additions & 0 deletions
17
server/src/main/java/org/cagnulein/android_remote/Codec.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
package org.cagnulein.android_remote; | ||
|
||
public interface Codec { | ||
|
||
enum Type { | ||
VIDEO, | ||
AUDIO, | ||
} | ||
|
||
Type getType(); | ||
|
||
int getId(); | ||
|
||
String getName(); | ||
|
||
String getMimeType(); | ||
} |
78 changes: 78 additions & 0 deletions
78
server/src/main/java/org/cagnulein/android_remote/CodecUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,78 @@ | ||
package org.cagnulein.android_remote; | ||
|
||
import android.media.MediaCodecInfo; | ||
import android.media.MediaCodecList; | ||
import android.media.MediaFormat; | ||
|
||
import java.util.ArrayList; | ||
import java.util.Arrays; | ||
import java.util.List; | ||
|
||
public final class CodecUtils { | ||
|
||
public static final class DeviceEncoder { | ||
private final Codec codec; | ||
private final MediaCodecInfo info; | ||
|
||
DeviceEncoder(Codec codec, MediaCodecInfo info) { | ||
this.codec = codec; | ||
this.info = info; | ||
} | ||
|
||
public Codec getCodec() { | ||
return codec; | ||
} | ||
|
||
public MediaCodecInfo getInfo() { | ||
return info; | ||
} | ||
} | ||
|
||
private CodecUtils() { | ||
// not instantiable | ||
} | ||
|
||
public static void setCodecOption(MediaFormat format, String key, Object value) { | ||
if (value instanceof Integer) { | ||
format.setInteger(key, (Integer) value); | ||
} else if (value instanceof Long) { | ||
format.setLong(key, (Long) value); | ||
} else if (value instanceof Float) { | ||
format.setFloat(key, (Float) value); | ||
} else if (value instanceof String) { | ||
format.setString(key, (String) value); | ||
} | ||
} | ||
|
||
private static MediaCodecInfo[] getEncoders(MediaCodecList codecs, String mimeType) { | ||
List<MediaCodecInfo> result = new ArrayList<>(); | ||
for (MediaCodecInfo codecInfo : codecs.getCodecInfos()) { | ||
if (codecInfo.isEncoder() && Arrays.asList(codecInfo.getSupportedTypes()).contains(mimeType)) { | ||
result.add(codecInfo); | ||
} | ||
} | ||
return result.toArray(new MediaCodecInfo[result.size()]); | ||
} | ||
|
||
public static List<DeviceEncoder> listVideoEncoders() { | ||
List<DeviceEncoder> encoders = new ArrayList<>(); | ||
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); | ||
for (VideoCodec codec : VideoCodec.values()) { | ||
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { | ||
encoders.add(new DeviceEncoder(codec, info)); | ||
} | ||
} | ||
return encoders; | ||
} | ||
|
||
public static List<DeviceEncoder> listAudioEncoders() { | ||
List<DeviceEncoder> encoders = new ArrayList<>(); | ||
MediaCodecList codecs = new MediaCodecList(MediaCodecList.REGULAR_CODECS); | ||
for (AudioCodec codec : AudioCodec.values()) { | ||
for (MediaCodecInfo info : getEncoders(codecs, codec.getMimeType())) { | ||
encoders.add(new DeviceEncoder(codec, info)); | ||
} | ||
} | ||
return encoders; | ||
} | ||
} |
7 changes: 7 additions & 0 deletions
7
server/src/main/java/org/cagnulein/android_remote/ConfigurationException.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package org.cagnulein.android_remote; | ||
|
||
public class ConfigurationException extends Exception { | ||
public ConfigurationException(String message) { | ||
super(message); | ||
} | ||
} |
53 changes: 53 additions & 0 deletions
53
server/src/main/java/org/cagnulein/android_remote/FakeContent.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
package org.cagnulein.android_remote; | ||
|
||
import android.annotation.TargetApi; | ||
import android.content.AttributionSource; | ||
import android.content.Context; | ||
import android.content.ContextWrapper; | ||
import android.os.Build; | ||
import android.os.Process; | ||
|
||
public final class FakeContext extends ContextWrapper { | ||
|
||
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 | ||
|
||
private static final FakeContext INSTANCE = new FakeContext(); | ||
|
||
public static FakeContext get() { | ||
return INSTANCE; | ||
} | ||
|
||
private FakeContext() { | ||
super(Workarounds.getSystemContext()); | ||
} | ||
|
||
@Override | ||
public String getPackageName() { | ||
return PACKAGE_NAME; | ||
} | ||
|
||
@Override | ||
public String getOpPackageName() { | ||
return PACKAGE_NAME; | ||
} | ||
|
||
@TargetApi(Build.VERSION_CODES.S) | ||
@Override | ||
public AttributionSource getAttributionSource() { | ||
AttributionSource.Builder builder = new AttributionSource.Builder(Process.SHELL_UID); | ||
builder.setPackageName(PACKAGE_NAME); | ||
return builder.build(); | ||
} | ||
|
||
// @Override to be added on SDK upgrade for Android 14 | ||
@SuppressWarnings("unused") | ||
public int getDeviceId() { | ||
return 0; | ||
} | ||
|
||
@Override | ||
public Context getApplicationContext() { | ||
return this; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
package org.cagnulein.android_remote; | ||
|
||
import android.system.ErrnoException; | ||
import android.system.Os; | ||
import android.system.OsConstants; | ||
|
||
import java.io.FileDescriptor; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.nio.ByteBuffer; | ||
import java.util.Scanner; | ||
|
||
public final class IO { | ||
private IO() { | ||
// not instantiable | ||
} | ||
|
||
public static void writeFully(FileDescriptor fd, ByteBuffer from) throws IOException { | ||
// ByteBuffer position is not updated as expected by Os.write() on old Android versions, so | ||
// count the remaining bytes manually. | ||
// See <https://github.com/Genymobile/scrcpy/issues/291>. | ||
int remaining = from.remaining(); | ||
while (remaining > 0) { | ||
try { | ||
int w = Os.write(fd, from); | ||
if (BuildConfig.DEBUG && w < 0) { | ||
// w should not be negative, since an exception is thrown on error | ||
throw new AssertionError("Os.write() returned a negative value (" + w + ")"); | ||
} | ||
remaining -= w; | ||
} catch (ErrnoException e) { | ||
if (e.errno != OsConstants.EINTR) { | ||
throw new IOException(e); | ||
} | ||
} | ||
} | ||
} | ||
|
||
public static void writeFully(FileDescriptor fd, byte[] buffer, int offset, int len) throws IOException { | ||
writeFully(fd, ByteBuffer.wrap(buffer, offset, len)); | ||
} | ||
|
||
public static String toString(InputStream inputStream) { | ||
StringBuilder builder = new StringBuilder(); | ||
Scanner scanner = new Scanner(inputStream); | ||
while (scanner.hasNextLine()) { | ||
builder.append(scanner.nextLine()).append('\n'); | ||
} | ||
return builder.toString(); | ||
} | ||
|
||
public static boolean isBrokenPipe(IOException e) { | ||
Throwable cause = e.getCause(); | ||
return cause instanceof ErrnoException && ((ErrnoException) cause).errno == OsConstants.EPIPE; | ||
} | ||
} |
151 changes: 151 additions & 0 deletions
151
server/src/main/java/org/cagnulein/android_remote/LogUtils.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,151 @@ | ||
package org.cagnulein.android_remote; | ||
|
||
import org.cagnulein.android_remote.wrappers.DisplayManager; | ||
import org.cagnulein.android_remote.wrappers.ServiceManager; | ||
|
||
import android.graphics.Rect; | ||
import android.hardware.camera2.CameraAccessException; | ||
import android.hardware.camera2.CameraCharacteristics; | ||
import android.hardware.camera2.CameraManager; | ||
import android.hardware.camera2.params.StreamConfigurationMap; | ||
import android.media.MediaCodec; | ||
import android.util.Range; | ||
|
||
import java.util.List; | ||
import java.util.SortedSet; | ||
import java.util.TreeSet; | ||
|
||
public final class LogUtils { | ||
|
||
private LogUtils() { | ||
// not instantiable | ||
} | ||
|
||
public static String buildVideoEncoderListMessage() { | ||
StringBuilder builder = new StringBuilder("List of video encoders:"); | ||
List<CodecUtils.DeviceEncoder> videoEncoders = CodecUtils.listVideoEncoders(); | ||
if (videoEncoders.isEmpty()) { | ||
builder.append("\n (none)"); | ||
} else { | ||
for (CodecUtils.DeviceEncoder encoder : videoEncoders) { | ||
builder.append("\n --video-codec=").append(encoder.getCodec().getName()); | ||
builder.append(" --video-encoder='").append(encoder.getInfo().getName()).append("'"); | ||
} | ||
} | ||
return builder.toString(); | ||
} | ||
|
||
public static String buildAudioEncoderListMessage() { | ||
StringBuilder builder = new StringBuilder("List of audio encoders:"); | ||
List<CodecUtils.DeviceEncoder> audioEncoders = CodecUtils.listAudioEncoders(); | ||
if (audioEncoders.isEmpty()) { | ||
builder.append("\n (none)"); | ||
} else { | ||
for (CodecUtils.DeviceEncoder encoder : audioEncoders) { | ||
builder.append("\n --audio-codec=").append(encoder.getCodec().getName()); | ||
builder.append(" --audio-encoder='").append(encoder.getInfo().getName()).append("'"); | ||
} | ||
} | ||
return builder.toString(); | ||
} | ||
|
||
public static String buildDisplayListMessage() { | ||
StringBuilder builder = new StringBuilder("List of displays:"); | ||
DisplayManager displayManager = ServiceManager.getDisplayManager(); | ||
int[] displayIds = displayManager.getDisplayIds(); | ||
if (displayIds == null || displayIds.length == 0) { | ||
builder.append("\n (none)"); | ||
} else { | ||
for (int id : displayIds) { | ||
builder.append("\n --display-id=").append(id).append(" ("); | ||
DisplayInfo displayInfo = displayManager.getDisplayInfo(id); | ||
if (displayInfo != null) { | ||
Size size = displayInfo.getSize(); | ||
builder.append(size.getWidth()).append("x").append(size.getHeight()); | ||
} else { | ||
builder.append("size unknown"); | ||
} | ||
builder.append(")"); | ||
} | ||
} | ||
return builder.toString(); | ||
} | ||
|
||
private static String getCameraFacingName(int facing) { | ||
switch (facing) { | ||
case CameraCharacteristics.LENS_FACING_FRONT: | ||
return "front"; | ||
case CameraCharacteristics.LENS_FACING_BACK: | ||
return "back"; | ||
case CameraCharacteristics.LENS_FACING_EXTERNAL: | ||
return "external"; | ||
default: | ||
return "unknown"; | ||
} | ||
} | ||
|
||
public static String buildCameraListMessage(boolean includeSizes) { | ||
StringBuilder builder = new StringBuilder("List of cameras:"); | ||
CameraManager cameraManager = ServiceManager.getCameraManager(); | ||
try { | ||
String[] cameraIds = cameraManager.getCameraIdList(); | ||
if (cameraIds == null || cameraIds.length == 0) { | ||
builder.append("\n (none)"); | ||
} else { | ||
for (String id : cameraIds) { | ||
builder.append("\n --camera-id=").append(id); | ||
CameraCharacteristics characteristics = cameraManager.getCameraCharacteristics(id); | ||
|
||
int facing = characteristics.get(CameraCharacteristics.LENS_FACING); | ||
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()); | ||
|
||
try { | ||
// Capture frame rates for low-FPS mode are the same for every resolution | ||
Range<Integer>[] lowFpsRanges = characteristics.get(CameraCharacteristics.CONTROL_AE_AVAILABLE_TARGET_FPS_RANGES); | ||
SortedSet<Integer> 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); | ||
|
||
android.util.Size[] sizes = configs.getOutputSizes(MediaCodec.class); | ||
for (android.util.Size size : sizes) { | ||
builder.append("\n - ").append(size.getWidth()).append('x').append(size.getHeight()); | ||
} | ||
|
||
android.util.Size[] highSpeedSizes = configs.getHighSpeedVideoSizes(); | ||
if (highSpeedSizes.length > 0) { | ||
builder.append("\n High speed capture (--camera-high-speed):"); | ||
for (android.util.Size size : highSpeedSizes) { | ||
Range<Integer>[] highFpsRanges = configs.getHighSpeedVideoFpsRanges(); | ||
SortedSet<Integer> uniqueHighFps = getUniqueSet(highFpsRanges); | ||
builder.append("\n - ").append(size.getWidth()).append("x").append(size.getHeight()); | ||
builder.append(" (fps=").append(uniqueHighFps).append(')'); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} catch (CameraAccessException e) { | ||
builder.append("\n (access denied)"); | ||
} | ||
return builder.toString(); | ||
} | ||
|
||
private static SortedSet<Integer> getUniqueSet(Range<Integer>[] ranges) { | ||
SortedSet<Integer> set = new TreeSet<>(); | ||
for (Range<Integer> range : ranges) { | ||
set.add(range.getUpper()); | ||
} | ||
return set; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters