mirror of
https://github.com/Genymobile/scrcpy.git
synced 2025-04-20 19:45:00 +00:00
Add jpeg encoder
This commit is contained in:
parent
31bd95022b
commit
dce39887f5
18 changed files with 2175 additions and 44 deletions
|
@ -75,13 +75,13 @@ push_server(const char *serial) {
|
|||
process_t process = adb_push(serial, server_path, DEVICE_SERVER_PATH);
|
||||
return process_check_success(process, "adb push");
|
||||
}
|
||||
|
||||
/*
|
||||
static bool
|
||||
enable_tunnel_reverse(const char *serial, uint16_t local_port) {
|
||||
process_t process = adb_reverse(serial, SOCKET_NAME, local_port);
|
||||
return process_check_success(process, "adb reverse");
|
||||
}
|
||||
|
||||
*/
|
||||
static bool
|
||||
disable_tunnel_reverse(const char *serial) {
|
||||
process_t process = adb_reverse_remove(serial, SOCKET_NAME);
|
||||
|
@ -102,10 +102,11 @@ disable_tunnel_forward(const char *serial, uint16_t local_port) {
|
|||
|
||||
static bool
|
||||
enable_tunnel(struct server *server) {
|
||||
/*
|
||||
if (enable_tunnel_reverse(server->serial, server->local_port)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
*/
|
||||
LOGW("'adb reverse' failed, fallback to 'adb forward'");
|
||||
server->tunnel_forward = true;
|
||||
return enable_tunnel_forward(server->serial, server->local_port);
|
||||
|
@ -129,6 +130,8 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||
sprintf(max_fps_string, "%"PRIu16, params->max_fps);
|
||||
const char *const cmd[] = {
|
||||
"shell",
|
||||
//nativeLibraryDirectories=[/system/lib64, /vendor/lib64, /system/lib64, /vendor/lib64]]]
|
||||
"LD_LIBRARY_PATH=/system/lib:/vendor/lib:/system/lib64:/vendor/lib64:/data/local/tmp",
|
||||
"CLASSPATH=" DEVICE_SERVER_PATH,
|
||||
"app_process",
|
||||
#ifdef SERVER_DEBUGGER
|
||||
|
@ -142,10 +145,11 @@ execute_server(struct server *server, const struct server_params *params) {
|
|||
max_size_string,
|
||||
bit_rate_string,
|
||||
max_fps_string,
|
||||
server->tunnel_forward ? "true" : "false",
|
||||
"true",//server->tunnel_forward ? "true" : "false",
|
||||
params->crop ? params->crop : "-",
|
||||
"true", // always send frame meta (packet boundaries + timestamp)
|
||||
params->control ? "true" : "false",
|
||||
"true",
|
||||
};
|
||||
#ifdef SERVER_DEBUGGER
|
||||
LOGI("Server debugger waiting for a client on device port "
|
||||
|
@ -293,7 +297,7 @@ server_connect_to(struct server *server) {
|
|||
// we don't need the server socket anymore
|
||||
close_socket(&server->server_socket);
|
||||
} else {
|
||||
uint32_t attempts = 100;
|
||||
uint32_t attempts = 10;
|
||||
uint32_t delay = 100; // ms
|
||||
server->video_socket =
|
||||
connect_to_server(server->local_port, attempts, delay);
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
buildscript {
|
||||
|
||||
repositories {
|
||||
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
|
||||
maven{ url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'}
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
@ -16,6 +18,8 @@ buildscript {
|
|||
|
||||
allprojects {
|
||||
repositories {
|
||||
maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
|
||||
maven{ url 'http://maven.aliyun.com/nexus/content/repositories/jcenter'}
|
||||
google()
|
||||
jcenter()
|
||||
}
|
||||
|
|
BIN
scrcpy-server.jar
Normal file
BIN
scrcpy-server.jar
Normal file
Binary file not shown.
38
server/CMakeLists.txt
Normal file
38
server/CMakeLists.txt
Normal file
|
@ -0,0 +1,38 @@
|
|||
# 标注需要支持的CMake最小版本
|
||||
cmake_minimum_required(VERSION 3.4.1)
|
||||
|
||||
|
||||
#设置生成的 so 动态库最后输出的路径
|
||||
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${PROJECT_SOURCE_DIR}/jniLibs/${ANDROID_ABI})
|
||||
|
||||
|
||||
# 初始化目录变量
|
||||
set(libturbojpeg_dir ${CMAKE_SOURCE_DIR}/libs/libturbojpeg)
|
||||
set(INC_DIR ${libturbojpeg_dir}/include)
|
||||
set(libturbojpeg_lib_dir ${libturbojpeg_dir}/prebuilt)
|
||||
|
||||
|
||||
# 添加头文件目录
|
||||
include_directories(${INC_DIR})
|
||||
|
||||
|
||||
# 设置资源路径
|
||||
set(SOURCE_FILES src/main/jni/compress.c)
|
||||
|
||||
|
||||
# add_library 定义需要编译的代码库 (1.名称, 2.类型, 3.包含的源码)
|
||||
add_library(compress SHARED ${SOURCE_FILES})
|
||||
add_library(libturbojpeg SHARED IMPORTED)
|
||||
set_target_properties(libturbojpeg PROPERTIES IMPORTED_LOCATION ${libturbojpeg_lib_dir}/${ANDROID_ABI}/libturbojpeg.so)
|
||||
|
||||
|
||||
# 可以写多个 find_library
|
||||
# 定义当前代码库需要依赖的系统或者第三方库文件 (1.给一个名字, 2.真正要查找的)
|
||||
find_library(log-lib log)
|
||||
find_library(graphics jnigraphics)
|
||||
|
||||
# target_link_libraries设置最终编译的目标代码库
|
||||
# add_library 生成的
|
||||
# find_library 找到的系统库
|
||||
target_link_libraries(compress libturbojpeg ${log-lib} ${graphics})
|
||||
|
|
@ -9,6 +9,11 @@ android {
|
|||
versionCode 14
|
||||
versionName "1.12.1"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
abiFilters 'arm64-v8a', 'armeabi-v7a'
|
||||
}
|
||||
}
|
||||
}
|
||||
buildTypes {
|
||||
release {
|
||||
|
@ -16,11 +21,23 @@ android {
|
|||
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
|
||||
}
|
||||
}
|
||||
externalNativeBuild {
|
||||
cmake {
|
||||
path "CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
sourceSets {
|
||||
main {
|
||||
java.srcDirs 'src/java'
|
||||
jniLibs.srcDirs 'libs/libturbojpeg/prebuilt'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar', '*.so'])
|
||||
testImplementation 'junit:junit:4.12'
|
||||
implementation 'commons-cli:commons-cli:1.4'
|
||||
}
|
||||
|
||||
apply from: "$project.rootDir/config/android-checkstyle.gradle"
|
||||
|
|
BIN
server/libs/libturbojpeg/prebuilt/arm64-v8a/libjpeg.so
Executable file
BIN
server/libs/libturbojpeg/prebuilt/arm64-v8a/libjpeg.so
Executable file
Binary file not shown.
BIN
server/libs/libturbojpeg/prebuilt/arm64-v8a/libturbojpeg.so
Executable file
BIN
server/libs/libturbojpeg/prebuilt/arm64-v8a/libturbojpeg.so
Executable file
Binary file not shown.
BIN
server/libs/libturbojpeg/prebuilt/armeabi-v7a/libjpeg.so
Executable file
BIN
server/libs/libturbojpeg/prebuilt/armeabi-v7a/libjpeg.so
Executable file
Binary file not shown.
BIN
server/libs/libturbojpeg/prebuilt/armeabi-v7a/libturbojpeg.so
Executable file
BIN
server/libs/libturbojpeg/prebuilt/armeabi-v7a/libturbojpeg.so
Executable file
Binary file not shown.
|
@ -5,6 +5,7 @@ import java.io.IOException;
|
|||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ControlMessageReader {
|
||||
|
||||
|
@ -39,6 +40,11 @@ public class ControlMessageReader {
|
|||
int head = buffer.position();
|
||||
int r = input.read(rawBuffer, head, rawBuffer.length - head);
|
||||
if (r == -1) {
|
||||
Ln.i("=========================================>>>");
|
||||
Ln.i("head: " + head);
|
||||
Ln.i("rawBuffer.length: " + rawBuffer.length);
|
||||
Ln.i("rawBuffer: " + Arrays.toString(rawBuffer));
|
||||
Ln.i("=========================================<<<");
|
||||
throw new EOFException("Controller socket closed");
|
||||
}
|
||||
buffer.position(head + r);
|
||||
|
|
|
@ -49,7 +49,7 @@ public final class DesktopConnection implements Closeable {
|
|||
try {
|
||||
videoSocket = localServerSocket.accept();
|
||||
// send one byte so the client may read() to detect a connection error
|
||||
videoSocket.getOutputStream().write(0);
|
||||
// videoSocket.getOutputStream().write(0);//wen disable
|
||||
try {
|
||||
controlSocket = localServerSocket.accept();
|
||||
} catch (IOException | RuntimeException e) {
|
||||
|
@ -71,7 +71,7 @@ public final class DesktopConnection implements Closeable {
|
|||
|
||||
DesktopConnection connection = new DesktopConnection(videoSocket, controlSocket);
|
||||
Size videoSize = device.getScreenInfo().getVideoSize();
|
||||
connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());
|
||||
// connection.send(Device.getDeviceName(), videoSize.getWidth(), videoSize.getHeight());//wen disable
|
||||
return connection;
|
||||
}
|
||||
|
||||
|
|
14
server/src/main/java/com/genymobile/scrcpy/JpegEncoder.java
Normal file
14
server/src/main/java/com/genymobile/scrcpy/JpegEncoder.java
Normal file
|
@ -0,0 +1,14 @@
|
|||
package com.genymobile.scrcpy;
|
||||
|
||||
import android.graphics.Bitmap;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class JpegEncoder {
|
||||
|
||||
static {
|
||||
System.loadLibrary("compress");
|
||||
}
|
||||
|
||||
public static native byte[] compress(ByteBuffer buffer, int width, int pitch, int height, int quality);
|
||||
public static native void test();
|
||||
}
|
|
@ -11,6 +11,10 @@ public class Options {
|
|||
private boolean sendFrameMeta; // send PTS so that the client may record properly
|
||||
private boolean control;
|
||||
|
||||
//wen add
|
||||
private int quality;
|
||||
private int scale;
|
||||
|
||||
public int getMaxSize() {
|
||||
return maxSize;
|
||||
}
|
||||
|
@ -66,4 +70,21 @@ public class Options {
|
|||
public void setControl(boolean control) {
|
||||
this.control = control;
|
||||
}
|
||||
|
||||
public int getQuality() {
|
||||
return quality;
|
||||
}
|
||||
|
||||
public void setQuality(int quality) {
|
||||
this.quality = quality;
|
||||
}
|
||||
|
||||
public int getScale() {
|
||||
return scale;
|
||||
}
|
||||
|
||||
public void setScale(int scale) {
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -15,6 +15,34 @@ import java.io.IOException;
|
|||
import java.nio.ByteBuffer;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import android.graphics.ImageFormat;
|
||||
import android.graphics.YuvImage;
|
||||
import android.media.Image;
|
||||
import android.media.MediaExtractor;
|
||||
import android.util.Log;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.io.OutputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
|
||||
import android.media.ImageReader;
|
||||
import android.graphics.Bitmap;
|
||||
import android.os.Environment;
|
||||
import android.graphics.PixelFormat;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import android.os.Looper;
|
||||
|
||||
import java.nio.ByteOrder;
|
||||
|
||||
import android.os.Process;
|
||||
|
||||
public class ScreenEncoder implements Device.RotationListener {
|
||||
|
||||
private static final int DEFAULT_I_FRAME_INTERVAL = 10; // seconds
|
||||
|
@ -31,6 +59,12 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||
private boolean sendFrameMeta;
|
||||
private long ptsOrigin;
|
||||
|
||||
private int quality;
|
||||
private int scale;
|
||||
private Handler mHandler;
|
||||
private ImageReader mImageReader;
|
||||
private ImageListener mImageListener;
|
||||
|
||||
public ScreenEncoder(boolean sendFrameMeta, int bitRate, int maxFps, int iFrameInterval) {
|
||||
this.sendFrameMeta = sendFrameMeta;
|
||||
this.bitRate = bitRate;
|
||||
|
@ -42,6 +76,12 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||
this(sendFrameMeta, bitRate, maxFps, DEFAULT_I_FRAME_INTERVAL);
|
||||
}
|
||||
|
||||
public ScreenEncoder(int quality, int maxFps, int scale) {
|
||||
this.quality = quality;
|
||||
this.maxFps = maxFps;
|
||||
this.scale = scale;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRotationChanged(int rotation) {
|
||||
rotationChanged.set(true);
|
||||
|
@ -51,31 +91,40 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||
return rotationChanged.getAndSet(false);
|
||||
}
|
||||
|
||||
private final class ImageListener implements ImageReader.OnImageAvailableListener {
|
||||
@Override
|
||||
public void onImageAvailable(ImageReader reader) {
|
||||
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
Ln.i("onImageAvailable !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
|
||||
}
|
||||
}
|
||||
|
||||
public void streamScreen(Device device, FileDescriptor fd) throws IOException {
|
||||
Workarounds.prepareMainLooper();
|
||||
Workarounds.fillAppInfo();
|
||||
|
||||
MediaFormat format = createFormat(bitRate, maxFps, iFrameInterval);
|
||||
device.setRotationListener(this);
|
||||
boolean alive;
|
||||
try {
|
||||
banner(device, fd);
|
||||
do {
|
||||
MediaCodec codec = createCodec();
|
||||
// mHandler = new Handler(Looper.getMainLooper());
|
||||
IBinder display = createDisplay();
|
||||
Rect contentRect = device.getScreenInfo().getContentRect();
|
||||
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
|
||||
setSize(format, videoRect.width(), videoRect.height());
|
||||
configure(codec, format);
|
||||
Surface surface = codec.createInputSurface();
|
||||
// Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
|
||||
Rect videoRect = new Rect(0, 0, contentRect.width() / scale, contentRect.height() / scale);
|
||||
mImageReader = ImageReader.newInstance(videoRect.width(), videoRect.height(), PixelFormat.RGBA_8888, 2);
|
||||
mImageListener = new ImageListener();
|
||||
mImageReader.setOnImageAvailableListener(mImageListener, mHandler);
|
||||
Surface surface = mImageReader.getSurface();
|
||||
setDisplaySurface(display, surface, contentRect, videoRect);
|
||||
codec.start();
|
||||
try {
|
||||
alive = encode(codec, fd);
|
||||
// do not call stop() on exception, it would trigger an IllegalStateException
|
||||
codec.stop();
|
||||
alive = encode(mImageReader, fd);
|
||||
} finally {
|
||||
destroyDisplay(display);
|
||||
codec.release();
|
||||
surface.release();
|
||||
}
|
||||
} while (alive);
|
||||
|
@ -84,35 +133,103 @@ public class ScreenEncoder implements Device.RotationListener {
|
|||
}
|
||||
}
|
||||
|
||||
private boolean encode(MediaCodec codec, FileDescriptor fd) throws IOException {
|
||||
boolean eof = false;
|
||||
MediaCodec.BufferInfo bufferInfo = new MediaCodec.BufferInfo();
|
||||
|
||||
while (!consumeRotationChange() && !eof) {
|
||||
int outputBufferId = codec.dequeueOutputBuffer(bufferInfo, -1);
|
||||
eof = (bufferInfo.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0;
|
||||
try {
|
||||
if (consumeRotationChange()) {
|
||||
// must restart encoding with new size
|
||||
break;
|
||||
private boolean encode(ImageReader imageReader, FileDescriptor fd) throws IOException {
|
||||
int count = 0;
|
||||
long current = System.currentTimeMillis();
|
||||
int type = 0;// 0:libjpeg-turbo 1:bitmap
|
||||
int frameRate = this.maxFps;
|
||||
int quality = this.quality;
|
||||
int framePeriodMs = (int) (1000 / frameRate);
|
||||
while (!consumeRotationChange()) {
|
||||
long timeA = System.currentTimeMillis();
|
||||
Image image = null;
|
||||
int loop = 0;
|
||||
int wait = 1;
|
||||
// TODO onImageAvailable这个方法不回调,未找到原因,暂时写成while
|
||||
while ((image = imageReader.acquireNextImage()) == null && ++loop < 10) {
|
||||
try {
|
||||
Thread.sleep(wait++);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
if (outputBufferId >= 0) {
|
||||
ByteBuffer codecBuffer = codec.getOutputBuffer(outputBufferId);
|
||||
}
|
||||
if (image == null) {
|
||||
continue;
|
||||
}
|
||||
int width = image.getWidth();
|
||||
int height = image.getHeight();
|
||||
int format = image.getFormat();//RGBA_8888 0x00000001
|
||||
final Image.Plane[] planes = image.getPlanes();
|
||||
final ByteBuffer buffer = planes[0].getBuffer();
|
||||
int pixelStride = planes[0].getPixelStride();
|
||||
int rowStride = planes[0].getRowStride();
|
||||
int rowPadding = rowStride - pixelStride * width;
|
||||
int pitch = width + rowPadding / pixelStride;
|
||||
byte[] jpegData = null;
|
||||
byte[] jpegSize = null;
|
||||
if (type == 0) {
|
||||
jpegData = JpegEncoder.compress(buffer, width, pitch, height, quality);
|
||||
} else if (type == 1) {
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
Bitmap bitmap = Bitmap.createBitmap(pitch, height, Bitmap.Config.ARGB_8888);
|
||||
bitmap.copyPixelsFromBuffer(buffer);
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, quality, stream);
|
||||
jpegData = stream.toByteArray();
|
||||
bitmap.recycle();
|
||||
}
|
||||
image.close();
|
||||
if (jpegData == null) {
|
||||
Ln.e("jpegData is null");
|
||||
continue;
|
||||
}
|
||||
ByteBuffer b = ByteBuffer.allocate(4);
|
||||
b.order(ByteOrder.LITTLE_ENDIAN);
|
||||
b.putInt(jpegData.length);
|
||||
jpegSize = b.array();
|
||||
IO.writeFully(fd, jpegSize, 0, jpegSize.length);
|
||||
IO.writeFully(fd, jpegData, 0, jpegData.length);
|
||||
|
||||
if (sendFrameMeta) {
|
||||
writeFrameMeta(fd, bufferInfo, codecBuffer.remaining());
|
||||
count++;
|
||||
long timeB = System.currentTimeMillis();
|
||||
if (timeB - current >= 1000) {
|
||||
current = timeB;
|
||||
Ln.i("frame rate: " + count + ", jpeg size: " + jpegData.length);
|
||||
count = 0;
|
||||
}
|
||||
|
||||
if (framePeriodMs > 0) {
|
||||
long ms = framePeriodMs - timeB + timeA;
|
||||
if (ms > 0) {
|
||||
try {
|
||||
Thread.sleep(ms);
|
||||
} catch (InterruptedException e) {
|
||||
}
|
||||
|
||||
IO.writeFully(fd, codecBuffer);
|
||||
}
|
||||
} finally {
|
||||
if (outputBufferId >= 0) {
|
||||
codec.releaseOutputBuffer(outputBufferId, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
return !eof;
|
||||
// minicap banner
|
||||
private void banner(Device device, FileDescriptor fd) throws IOException {
|
||||
final byte BANNER_SIZE = 24;
|
||||
Rect videoRect = device.getScreenInfo().getVideoSize().toRect();
|
||||
int width = videoRect.width();
|
||||
int height = videoRect.height();
|
||||
int pid = Process.myPid();
|
||||
|
||||
ByteBuffer b = ByteBuffer.allocate(BANNER_SIZE);
|
||||
b.order(ByteOrder.LITTLE_ENDIAN);
|
||||
b.put((byte) 1);//version
|
||||
b.put(BANNER_SIZE);//banner size
|
||||
b.putInt(pid);//pid
|
||||
b.putInt(width);//real width
|
||||
b.putInt(height);//real height
|
||||
b.putInt(width);//desired width
|
||||
b.putInt(height);//desired height
|
||||
b.put((byte) 0);//orientation
|
||||
b.put((byte) 2);//quirks
|
||||
byte[] array = b.array();
|
||||
IO.writeFully(fd, array, 0, array.length);
|
||||
}
|
||||
|
||||
private void writeFrameMeta(FileDescriptor fd, MediaCodec.BufferInfo bufferInfo, int packetSize) throws IOException {
|
||||
|
|
|
@ -19,7 +19,7 @@ public final class Server {
|
|||
final Device device = new Device(options);
|
||||
boolean tunnelForward = options.isTunnelForward();
|
||||
try (DesktopConnection connection = DesktopConnection.open(device, tunnelForward)) {
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getSendFrameMeta(), options.getBitRate(), options.getMaxFps());
|
||||
ScreenEncoder screenEncoder = new ScreenEncoder(options.getQuality(), options.getMaxFps(), options.getScale());
|
||||
|
||||
if (options.getControl()) {
|
||||
Controller controller = new Controller(device, connection);
|
||||
|
@ -48,6 +48,7 @@ public final class Server {
|
|||
} catch (IOException e) {
|
||||
// this is expected on close
|
||||
Ln.d("Controller stopped");
|
||||
Ln.d("E:" + e.getMessage());
|
||||
}
|
||||
}
|
||||
}).start();
|
||||
|
@ -74,6 +75,7 @@ public final class Server {
|
|||
}
|
||||
|
||||
String clientVersion = args[0];
|
||||
Ln.i("VERSION_NAME: " + BuildConfig.VERSION_NAME);
|
||||
if (!clientVersion.equals(BuildConfig.VERSION_NAME)) {
|
||||
throw new IllegalArgumentException(
|
||||
"The server version (" + clientVersion + ") does not match the client " + "(" + BuildConfig.VERSION_NAME + ")");
|
||||
|
@ -110,6 +112,75 @@ public final class Server {
|
|||
return options;
|
||||
}
|
||||
|
||||
private static Options customOptions(String... args) {
|
||||
org.apache.commons.cli.CommandLine commandLine = null;
|
||||
org.apache.commons.cli.CommandLineParser parser = new org.apache.commons.cli.BasicParser();
|
||||
org.apache.commons.cli.Options options = new org.apache.commons.cli.Options();
|
||||
options.addOption("Q", true, "JPEG quality (0-100)");
|
||||
options.addOption("r", true, "Frame rate (frames/s)");
|
||||
options.addOption("P", true, "Display projection (scale 1,2,4...)");
|
||||
options.addOption("h", false, "Show help");
|
||||
try {
|
||||
commandLine = parser.parse(options, args);
|
||||
} catch (Exception e) {
|
||||
Ln.e(e.getMessage());
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
if (commandLine.hasOption('h')) {
|
||||
System.out.println(
|
||||
"Usage: %s [-h]\n"
|
||||
+ " -Q <value>: JPEG quality (0-100).\n"
|
||||
+ " -r <value>: Frame rate (frames/s).\n"
|
||||
+ " -P <value>: Display projection (scale 1,2,4...).\n"
|
||||
+ " -h: Show help.\n"
|
||||
);
|
||||
System.exit(0);
|
||||
}
|
||||
|
||||
Options o = new Options();
|
||||
o.setMaxSize(0);
|
||||
o.setBitRate(1000000);
|
||||
o.setTunnelForward(true);
|
||||
o.setCrop(null);
|
||||
o.setSendFrameMeta(true);
|
||||
o.setControl(true);
|
||||
o.setMaxFps(24);
|
||||
o.setQuality(50);
|
||||
o.setScale(2);
|
||||
if (commandLine.hasOption('Q')) {
|
||||
int i = 0;
|
||||
try {
|
||||
i = Integer.parseInt(commandLine.getOptionValue('Q'));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
if (i > 0 && i <= 100) {
|
||||
o.setQuality(i);
|
||||
}
|
||||
}
|
||||
if (commandLine.hasOption('r')) {
|
||||
int i = 0;
|
||||
try {
|
||||
i = Integer.parseInt(commandLine.getOptionValue('r'));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
if (i > 0 && i <= 100) {
|
||||
o.setMaxFps(i);
|
||||
}
|
||||
}
|
||||
if (commandLine.hasOption('P')) {
|
||||
int i = 0;
|
||||
try {
|
||||
i = Integer.parseInt(commandLine.getOptionValue('P'));
|
||||
} catch (Exception e) {
|
||||
}
|
||||
if (i > 0) {
|
||||
o.setScale(i);
|
||||
}
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
@SuppressWarnings("checkstyle:MagicNumber")
|
||||
private static Rect parseCrop(String crop) {
|
||||
if ("-".equals(crop)) {
|
||||
|
@ -158,8 +229,12 @@ public final class Server {
|
|||
}
|
||||
});
|
||||
|
||||
unlinkSelf();
|
||||
Options options = createOptions(args);
|
||||
// unlinkSelf();
|
||||
// Options options = createOptions(args);
|
||||
Options options = customOptions(args);
|
||||
Ln.i("Options frame rate: " + options.getMaxFps() + " (1 ~ 100)");
|
||||
Ln.i("Options quality: " + options.getQuality() + " (1 ~ 100)");
|
||||
Ln.i("Options scale: " + options.getScale() + " (1,2,4...)");
|
||||
scrcpy(options);
|
||||
}
|
||||
}
|
||||
|
|
68
server/src/main/jni/compress.c
Normal file
68
server/src/main/jni/compress.c
Normal file
|
@ -0,0 +1,68 @@
|
|||
#include "compress.h"
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_com_genymobile_scrcpy_JpegEncoder_test(JNIEnv* env, jobject thiz){
|
||||
LOGI("jni");
|
||||
}
|
||||
|
||||
JNIEXPORT jbyteArray JNICALL Java_com_genymobile_scrcpy_JpegEncoder_compress(JNIEnv* env, jobject thiz, jobject buffer, jint width, jint pitch, jint height, jint quality){
|
||||
jbyteArray result = NULL;
|
||||
unsigned long jpegSize = 0;
|
||||
int subsampling = TJSAMP_420;
|
||||
jbyte* imgBuffer = NULL, *jpegBuffer = NULL;
|
||||
if(NULL == (imgBuffer=(*env)->GetDirectBufferAddress(env, buffer))){
|
||||
LOGI("imgBuffer is null");
|
||||
return NULL;
|
||||
}
|
||||
unsigned long maxSize = tjBufSize(pitch, height, subsampling);
|
||||
if(NULL == (jpegBuffer = tjAlloc(maxSize))){
|
||||
LOGI("jpegBuffer is null");
|
||||
return NULL;
|
||||
}
|
||||
tjhandle handle = tjInitCompress();
|
||||
if(NULL == handle){
|
||||
LOGI("tjInitCompress error: %s", tjGetErrorStr2(handle));
|
||||
tjDestroy(handle);
|
||||
handle = NULL;
|
||||
return NULL;
|
||||
}
|
||||
if (0 == tjCompress2(
|
||||
handle,
|
||||
(unsigned char*)imgBuffer,
|
||||
width,
|
||||
pitch*4,
|
||||
height,
|
||||
TJPF_RGBA,//PixelFormat.RGBA_8888
|
||||
&jpegBuffer,
|
||||
&jpegSize,
|
||||
subsampling,
|
||||
quality,
|
||||
TJFLAG_FASTDCT | TJFLAG_NOREALLOC
|
||||
)){
|
||||
//LOGI("size %d", jpegSize);
|
||||
/*test
|
||||
FILE *jpegFile = NULL;
|
||||
if ((jpegFile = fopen("/sdcard/test.jpg", "wb")) == NULL){
|
||||
LOGI("opening output file");
|
||||
}
|
||||
if (fwrite(jpegBuffer, jpegSize, 1, jpegFile) < 1){
|
||||
LOGI("writing output file");
|
||||
}
|
||||
fclose(jpegFile); jpegFile = NULL;
|
||||
*/
|
||||
result = (*env)->NewByteArray(env, (int)jpegSize);
|
||||
(*env)->SetByteArrayRegion(env, result, 0, (int)jpegSize, jpegBuffer);
|
||||
}else{
|
||||
LOGI("tjCompress2 error");
|
||||
tjDestroy(handle);
|
||||
handle = NULL;
|
||||
tjFree(jpegBuffer);
|
||||
jpegBuffer = NULL;
|
||||
return NULL;
|
||||
}
|
||||
tjDestroy(handle);
|
||||
handle = NULL;
|
||||
tjFree(jpegBuffer);
|
||||
jpegBuffer = NULL;
|
||||
return result;
|
||||
}
|
||||
|
23
server/src/main/jni/compress.h
Normal file
23
server/src/main/jni/compress.h
Normal file
|
@ -0,0 +1,23 @@
|
|||
#ifndef COMPRESS_COMPRESS_H_H
|
||||
#define COMPRESS_COMPRESS_H_H
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <jni.h>
|
||||
#include <android/log.h>
|
||||
#include "turbojpeg.h"
|
||||
|
||||
#ifndef LOG_TAG
|
||||
#define LOG_TAG "scrcpyjni"
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG,LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN,LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG ,__VA_ARGS__)
|
||||
#define LOGF(...) __android_log_print(ANDROID_LOG_FATAL,LOG_TAG ,__VA_ARGS__)
|
||||
#endif
|
||||
|
||||
|
||||
#endif //COMPRESS_COMPRESS_H_H
|
||||
|
||||
|
1744
server/src/main/jni/turbojpeg.h
Normal file
1744
server/src/main/jni/turbojpeg.h
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue