Skip to content

Commit 1517faf

Browse files
authored
Merge pull request #9 from swordfeng/speedup
Optimize screenshot performance
2 parents 6a1d68c + 9e6fb73 commit 1517faf

File tree

15 files changed

+672
-320
lines changed

15 files changed

+672
-320
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,3 @@
1414
.cxx
1515
local.properties
1616
.idea
17-
app

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
[submodule "app/src/main/cpp/lz4"]
2+
path = app/src/main/cpp/lz4
3+
url = https://github.com/lz4/lz4.git

app/build.gradle

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ android {
1616
resConfigs "en_US"
1717
resConfigs "hdpi"
1818
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
19+
externalNativeBuild {
20+
cmake {
21+
cppFlags '-D__ANDROID_UNAVAILABLE_SYMBOLS_ARE_WEAK__'
22+
}
23+
}
1924
}
2025

2126
buildTypes {
@@ -35,6 +40,17 @@ android {
3540
buildFeatures {
3641
aidl = true
3742
}
43+
externalNativeBuild {
44+
cmake {
45+
path file('src/main/cpp/CMakeLists.txt')
46+
version '3.22.1'
47+
}
48+
}
49+
packagingOptions {
50+
jniLibs {
51+
useLegacyPackaging false
52+
}
53+
}
3854
}
3955

4056
dependencies {

app/src/main/cpp/CMakeLists.txt

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
cmake_minimum_required(VERSION 3.22.1)
2+
3+
project("droidcast_raw")
4+
5+
add_subdirectory(lz4/build/cmake)
6+
7+
add_library(${CMAKE_PROJECT_NAME} SHARED
8+
droidcast_raw.cpp)
9+
10+
target_link_libraries(${CMAKE_PROJECT_NAME} PUBLIC
11+
android log jnigraphics lz4)
12+
13+
target_link_options(${CMAKE_PROJECT_NAME}
14+
PRIVATE
15+
-Wl,--version-script,${CMAKE_SOURCE_DIR}/droidcast_raw.ver
16+
-Wl,--no-undefined-version
17+
)
18+
19+
# Without this, changes to the version script will not cause the library to
20+
# relink.
21+
set_target_properties(${CMAKE_PROJECT_NAME}
22+
PROPERTIES
23+
LINK_DEPENDS ${CMAKE_SOURCE_DIR}/droidcast_raw.ver
24+
)

app/src/main/cpp/droidcast_raw.cpp

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#include <cstdlib>
2+
#include <cstdio>
3+
#include <jni.h>
4+
#include <android/log.h>
5+
#include <android/bitmap.h>
6+
#include <android/hardware_buffer.h>
7+
#include <android/hardware_buffer_jni.h>
8+
#include <cstring>
9+
#include <lz4.h>
10+
11+
#define ASSERT(cond, ...) if (!(cond)) \
12+
__android_log_assert(#cond, "DroidCast_raw_log", __VA_ARGS__)
13+
#define CHECKED(ret, cond, ...) if (!(cond)) do { \
14+
__android_log_print(ANDROID_LOG_ERROR, "DroidCast_raw_log", __VA_ARGS__); \
15+
return ret; \
16+
} while (0)
17+
18+
extern "C"
19+
JNIEXPORT jint
20+
21+
JNICALL
22+
Java_ink_mol_droidcast_1raw_ScreenCaptorUtils_copyHardwareBufferToByteBufferNative(JNIEnv *env,
23+
jobject thiz,
24+
jobject hardware_buffer,
25+
jobject byte_buffer,
26+
jint position,
27+
jint limit) {
28+
AHardwareBuffer *hardwareBuffer = AHardwareBuffer_fromHardwareBuffer(env, hardware_buffer);
29+
CHECKED(-1, hardwareBuffer, "invalid hardware buffer");
30+
AHardwareBuffer_Desc desc = {0};
31+
AHardwareBuffer_describe(hardwareBuffer, &desc);
32+
CHECKED(-1, desc.format == AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM,
33+
"native copy does not support format %" PRIu32, desc.format);
34+
CHECKED(-1, desc.width == desc.stride,
35+
"native copy width != stride, width = %" PRIu32 ", stride = %" PRIu32, desc.width,
36+
desc.stride);
37+
size_t size = desc.width * desc.height * 2;
38+
CHECKED(-1, limit - position >= size, "buffer size %d is smaller than image size %zu",
39+
limit - position, size);
40+
void *bufferPtr = env->GetDirectBufferAddress(byte_buffer);
41+
ASSERT(bufferPtr, "failed to get direct byte buffer ptr");
42+
void *srcPtr;
43+
int lockResult = AHardwareBuffer_lock(hardwareBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_RARELY, -1,
44+
nullptr, &srcPtr);
45+
ASSERT(lockResult == 0, "failed to lock hw buffer");
46+
std::memcpy(bufferPtr, srcPtr, size);
47+
lockResult = AHardwareBuffer_unlock(hardwareBuffer, nullptr);
48+
ASSERT(lockResult == 0, "failed to unlock hw buffer");
49+
return (int) size;
50+
}
51+
52+
extern "C"
53+
JNIEXPORT jint
54+
55+
JNICALL
56+
Java_ink_mol_droidcast_1raw_ScreenCaptorUtils_copyHardwareBufferToByteBufferNativeLz4(JNIEnv *env,
57+
jobject thiz,
58+
jobject hardware_buffer,
59+
jobject byte_buffer,
60+
jint position,
61+
jint limit) {
62+
AHardwareBuffer *hardwareBuffer = AHardwareBuffer_fromHardwareBuffer(env, hardware_buffer);
63+
CHECKED(-1, hardwareBuffer, "invalid hardware buffer");
64+
AHardwareBuffer_Desc desc;
65+
AHardwareBuffer_describe(hardwareBuffer, &desc);
66+
CHECKED(-1, desc.format == AHARDWAREBUFFER_FORMAT_R5G6B5_UNORM,
67+
"native copy does not support format %" PRIu32, desc.format);
68+
CHECKED(-1, desc.width == desc.stride,
69+
"native copy width != stride, width = %" PRIu32 ", stride = %" PRIu32, desc.width,
70+
desc.stride);
71+
size_t size = desc.width * desc.height * 2;
72+
void *bufferPtr = env->GetDirectBufferAddress(byte_buffer);
73+
ASSERT(bufferPtr, "failed to get direct byte buffer ptr");
74+
void *srcPtr;
75+
int lockResult = AHardwareBuffer_lock(hardwareBuffer, AHARDWAREBUFFER_USAGE_CPU_READ_RARELY, -1,
76+
nullptr, &srcPtr);
77+
ASSERT(lockResult == 0, "failed to lock hw buffer");
78+
int dstSize = LZ4_compress_default((const char *) srcPtr, (char *) bufferPtr, (int) size,
79+
limit - position);
80+
lockResult = AHardwareBuffer_unlock(hardwareBuffer, nullptr);
81+
ASSERT(lockResult == 0, "failed to unlock hw buffer");
82+
return (int) dstSize;
83+
}
84+
85+
extern "C"
86+
JNIEXPORT jint
87+
88+
JNICALL
89+
Java_ink_mol_droidcast_1raw_ScreenCaptorUtils_copyBitmapToByteBufferNativeLz4(JNIEnv *env,
90+
jobject thiz,
91+
jobject bitmap,
92+
jobject byte_buffer,
93+
jint position,
94+
jint limit) {
95+
AndroidBitmapInfo info = {0};
96+
AndroidBitmap_getInfo(env, bitmap, &info);
97+
CHECKED(-1, info.format == ANDROID_BITMAP_FORMAT_RGB_565, "invalid RGB565 bitmap");
98+
int size = int(info.stride * info.height);
99+
void *bufferPtr = env->GetDirectBufferAddress(byte_buffer);
100+
ASSERT(bufferPtr, "failed to get direct byte buffer ptr");
101+
void *srcPtr = nullptr;
102+
AndroidBitmap_lockPixels(env, bitmap, &srcPtr);
103+
ASSERT(srcPtr, "failed to lock bitmap");
104+
int dstSize = LZ4_compress_default((const char *) srcPtr, (char *) bufferPtr, (int) size,
105+
limit - position);
106+
AndroidBitmap_unlockPixels(env, bitmap);
107+
return (int) dstSize;
108+
}
109+
110+
extern "C"
111+
JNIEXPORT jint
112+
113+
JNICALL
114+
Java_ink_mol_droidcast_1raw_ScreenCaptorUtils_lz4CompressBound(JNIEnv *env, jobject thiz,
115+
jint data_size) {
116+
return LZ4_compressBound(data_size);
117+
}

app/src/main/cpp/droidcast_raw.ver

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
droidcast_raw {
2+
global:
3+
Java_ink_mol_droidcast_1raw_*;
4+
local:
5+
# Every symbol in this section will have "local" (that is, hidden)
6+
# visibility. The wildcard * is used to indicate that all symbols not listed
7+
# in the global section should be hidden.
8+
*;
9+
};

app/src/main/cpp/lz4

Submodule lz4 added at ebb370c

app/src/main/java/ink/mol/droidcast_raw/AnyRequestCallback.kt

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package ink.mol.droidcast_raw
22

3-
import android.graphics.Bitmap
3+
import android.graphics.PixelFormat
44
import android.graphics.Point
55
import android.os.Build
66
import android.util.Log
@@ -9,6 +9,7 @@ import com.koushikdutta.async.http.Multimap
99
import com.koushikdutta.async.http.server.AsyncHttpServerRequest
1010
import com.koushikdutta.async.http.server.AsyncHttpServerResponse
1111
import com.koushikdutta.async.http.server.HttpServerRequestCallback
12+
import ink.mol.droidcast_raw.ScreenCaptorUtils.lz4CompressBound
1213
import java.nio.ByteBuffer
1314

1415
class AnyRequestCallback : HttpServerRequestCallback {
@@ -22,6 +23,7 @@ class AnyRequestCallback : HttpServerRequestCallback {
2223
val pairs: Multimap? = request?.query
2324
val width: String? = pairs?.getString("width")
2425
val height: String? = pairs?.getString("height")
26+
val compress: String? = pairs?.getString("compress")?.lowercase()
2527

2628
if (!width.isNullOrEmpty() && !height.isNullOrEmpty() && width.isDigitsOnly() && height.isDigitsOnly()) {
2729
Main.setWH(width.toInt(), height.toInt())
@@ -39,7 +41,7 @@ class AnyRequestCallback : HttpServerRequestCallback {
3941
val destWidth: Int = Main.getWidth()
4042
val destHeight: Int = Main.getHeight()
4143

42-
val buffer: ByteBuffer = getScreenImageInByteBuffer(destWidth, destHeight)
44+
val buffer: ByteBuffer = getScreenImageInByteBuffer(destWidth, destHeight, compress)
4345

4446
response?.send("application/octet-stream", buffer)
4547
} catch (e: Exception) {
@@ -56,7 +58,8 @@ class AnyRequestCallback : HttpServerRequestCallback {
5658

5759
private fun getScreenImageInByteBuffer(
5860
width: Int,
59-
height: Int
61+
height: Int,
62+
compress: String?
6063
): ByteBuffer {
6164
var destWidth = width
6265
var destHeight = height
@@ -68,14 +71,28 @@ class AnyRequestCallback : HttpServerRequestCallback {
6871
destHeight = tmp
6972
}
7073

71-
val bitmap: Bitmap? = ScreenCaptorUtils.screenshot(destWidth, destHeight)
72-
Log.i("DroidCast_raw_log", "Bitmap generated with resolution $destWidth:$destHeight")
74+
val bitmap = ScreenCaptorUtils.screenshot(destWidth, destHeight, PixelFormat.RGB_565)!!
75+
Log.i("DroidCast_raw_log", "Bitmap generated with resolution $destWidth:$destHeight config ${bitmap.config}")
7376

74-
val buffer = ByteBuffer.allocate((destWidth.times(destHeight)) * 2)
75-
bitmap!!.copy(Bitmap.Config.RGB_565, false)?.copyPixelsToBuffer(buffer)
77+
val buffer: ByteBuffer
78+
if (compress == "lz4") {
79+
val size = lz4CompressBound(destWidth * destHeight * 2)
80+
buffer = ByteBuffer.allocateDirect(size)
81+
ScreenCaptorUtils.copyBitmapToBufferLz4(bitmap, buffer)
82+
} else {
83+
val size = destWidth * destHeight * 2
84+
buffer = ByteBuffer.allocateDirect(size)
85+
ScreenCaptorUtils.copyBitmapToBuffer(bitmap, buffer)
86+
}
7687
bitmap.recycle()
7788
buffer.flip()
7889

7990
return buffer
8091
}
92+
93+
companion object {
94+
init {
95+
NativeLibHelper.loadLibs()
96+
}
97+
}
8198
}

app/src/main/java/ink/mol/droidcast_raw/AnyRequestCallbackPreview.kt

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import java.io.ByteArrayOutputStream
1313

1414
class AnyRequestCallbackPreview : HttpServerRequestCallback {
1515
private var displayUtil: DisplayUtil? = DisplayUtil()
16-
private var stream: ByteArrayOutputStream = ByteArrayOutputStream()
1716

1817
override fun onRequest(
1918
request: AsyncHttpServerRequest?,
@@ -23,6 +22,8 @@ class AnyRequestCallbackPreview : HttpServerRequestCallback {
2322
val pairs: Multimap? = request?.query
2423
val width: String? = pairs?.getString("width")
2524
val height: String? = pairs?.getString("height")
25+
val format: String = (pairs?.getString("format") ?: "webp").lowercase()
26+
val quality: Int = pairs?.getString("quality")?.toIntOrNull() ?: 100
2627

2728
if (!width.isNullOrEmpty() && !height.isNullOrEmpty() && width.isDigitsOnly() && height.isDigitsOnly()) {
2829
Main.setWH(width.toInt(), height.toInt())
@@ -40,7 +41,8 @@ class AnyRequestCallbackPreview : HttpServerRequestCallback {
4041
val destWidth: Int = Main.getWidth()
4142
val destHeight: Int = Main.getHeight()
4243

43-
response?.send("image/png", getScreenImage(destWidth, destHeight).toByteArray())
44+
val (ct, bytes) = getScreenImage(destWidth, destHeight, format, quality)
45+
response?.send(ct, bytes)
4446
} catch (e: Exception) {
4547
e.printStackTrace()
4648
response?.code(500)
@@ -55,8 +57,10 @@ class AnyRequestCallbackPreview : HttpServerRequestCallback {
5557

5658
private fun getScreenImage(
5759
width: Int,
58-
height: Int
59-
): ByteArrayOutputStream {
60+
height: Int,
61+
format: String,
62+
quality: Int,
63+
): Pair<String, ByteArray> {
6064
var destWidth = width
6165
var destHeight = height
6266

@@ -67,13 +71,21 @@ class AnyRequestCallbackPreview : HttpServerRequestCallback {
6771
destHeight = tmp
6872
}
6973

70-
val bitmap: Bitmap? = ScreenCaptorUtils.screenshot(destWidth, destHeight)
74+
val bitmap: Bitmap = ScreenCaptorUtils.screenshot(destWidth, destHeight)!!
7175
Log.i("DroidCast_raw_log", "Bitmap generated with resolution $destWidth:$destHeight")
7276

73-
stream = ByteArrayOutputStream()
74-
bitmap!!.compress(Bitmap.CompressFormat.PNG, 100, stream)
75-
bitmap.recycle()
76-
77-
return stream
77+
try {
78+
if (format == "webp" && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
79+
val stream = ByteArrayOutputStream()
80+
bitmap.compress(Bitmap.CompressFormat.WEBP_LOSSLESS, quality, stream)
81+
return Pair("image/webp", stream.toByteArray())
82+
} else {
83+
val stream = ByteArrayOutputStream()
84+
bitmap.compress(Bitmap.CompressFormat.PNG, quality, stream)
85+
return Pair("image/png", stream.toByteArray())
86+
}
87+
} finally {
88+
bitmap.recycle()
89+
}
7890
}
7991
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package ink.mol.droidcast_raw
2+
3+
import android.os.Build
4+
5+
object NativeLibHelper {
6+
fun loadLibs(): Boolean {
7+
// Try load native lib, disable native copy if failed
8+
try {
9+
val classpath = System.getenv("CLASSPATH")
10+
val abi = Build.SUPPORTED_ABIS[0]
11+
if (classpath != null && abi != null) {
12+
System.load("$classpath!/lib/$abi/libdroidcast_raw.so")
13+
} else {
14+
System.loadLibrary("droidcast_raw")
15+
}
16+
return true
17+
} catch (e: UnsatisfiedLinkError) {
18+
e.printStackTrace()
19+
}
20+
return false
21+
}
22+
}

0 commit comments

Comments
 (0)