Add jpeg encoder

This commit is contained in:
wenchenglong 2019-12-13 15:01:54 +08:00
parent 31bd95022b
commit dce39887f5
18 changed files with 2175 additions and 44 deletions

View file

@ -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);

View file

@ -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

Binary file not shown.

38
server/CMakeLists.txt Normal file
View 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})

View file

@ -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"

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -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);

View file

@ -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;
}

View 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();
}

View file

@ -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;
}
}

View file

@ -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 {

View file

@ -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);
}
}

View 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;
}

View 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

File diff suppressed because it is too large Load diff