Skip to content

Commit ff7c62f

Browse files
authored
feat: support scaling and quality parameters in mjpeg streaming
2 parents da4aa40 + 5b7635b commit ff7c62f

File tree

2 files changed

+99
-40
lines changed

2 files changed

+99
-40
lines changed
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
package com.mobilenext.devicekit
2+
3+
import android.graphics.Bitmap
4+
import android.media.Image
5+
import java.io.ByteArrayOutputStream
6+
7+
object ImageUtils {
8+
9+
@JvmStatic
10+
fun convertToJpeg(image: Image, quality: Int, scale: Float): ByteArray {
11+
val planes = image.planes
12+
val buffer = planes[0].buffer
13+
val pixelStride = planes[0].pixelStride
14+
val rowStride = planes[0].rowStride
15+
val rowPadding = rowStride - pixelStride * image.width
16+
17+
// Create bitmap from image data
18+
val bitmap = Bitmap.createBitmap(
19+
image.width + rowPadding / pixelStride,
20+
image.height,
21+
Bitmap.Config.ARGB_8888
22+
)
23+
bitmap.copyPixelsFromBuffer(buffer)
24+
25+
// Remove row padding if present
26+
val paddingRemovedBitmap = if (rowPadding == 0) {
27+
bitmap
28+
} else {
29+
Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height)
30+
}
31+
32+
// Scale bitmap if needed
33+
val finalBitmap = if (scale != 1.0f) {
34+
val scaledWidth = (image.width * scale).toInt()
35+
val scaledHeight = (image.height * scale).toInt()
36+
Bitmap.createScaledBitmap(paddingRemovedBitmap, scaledWidth, scaledHeight, true)
37+
} else {
38+
paddingRemovedBitmap
39+
}
40+
41+
// Convert to JPEG
42+
val outputStream = ByteArrayOutputStream()
43+
finalBitmap.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
44+
val jpegData = outputStream.toByteArray()
45+
46+
// Cleanup
47+
if (finalBitmap != bitmap) {
48+
bitmap.recycle()
49+
}
50+
if (paddingRemovedBitmap != bitmap && paddingRemovedBitmap != finalBitmap) {
51+
paddingRemovedBitmap.recycle()
52+
}
53+
if (finalBitmap != paddingRemovedBitmap) {
54+
finalBitmap.recycle()
55+
}
56+
outputStream.close()
57+
58+
return jpegData
59+
}
60+
}

app/src/main/java/com/mobilenext/devicekit/MjpegServer.kt

Lines changed: 39 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,54 @@ import java.util.concurrent.CountDownLatch
2121
import java.util.concurrent.TimeUnit
2222
import kotlin.system.exitProcess
2323

24-
class MjpegServer {
24+
class MjpegServer(quality: Int, scale: Float) {
25+
private val quality: Int = quality
26+
private val scale: Float = scale
27+
2528
companion object {
2629
private const val TAG = "MjpegServer"
2730
private const val BOUNDARY = "BoundaryString"
31+
private const val DEFAULT_QUALITY = 80
32+
private const val DEFAULT_SCALE = 1.0f
2833

2934
@JvmStatic
3035
fun main(args: Array<String>) {
3136
try {
32-
val server = MjpegServer()
37+
val (quality, scale) = parseArguments(args)
38+
val server = MjpegServer(quality, scale)
3339
server.startMjpegStream()
3440
} catch (e: Exception) {
3541
Log.e(TAG, "Failed to start MJPEG stream", e)
3642
System.err.println("Error: ${e.message}")
3743
exitProcess(1)
3844
}
3945
}
46+
47+
private fun parseArguments(args: Array<String>): Pair<Int, Float> {
48+
var quality = DEFAULT_QUALITY
49+
var scale = DEFAULT_SCALE
50+
51+
var i = 0
52+
while (i < args.size) {
53+
when (args[i]) {
54+
"--quality" -> {
55+
if (i + 1 < args.size) {
56+
quality = args[i + 1].toIntOrNull()?.coerceIn(1, 100) ?: DEFAULT_QUALITY
57+
i++
58+
}
59+
}
60+
"--scale" -> {
61+
if (i + 1 < args.size) {
62+
scale = args[i + 1].toFloatOrNull()?.coerceIn(0.1f, 2.0f) ?: DEFAULT_SCALE
63+
i++
64+
}
65+
}
66+
}
67+
i++
68+
}
69+
70+
return Pair(quality, scale)
71+
}
4072
}
4173

4274
private val shutdownLatch = CountDownLatch(1)
@@ -74,7 +106,9 @@ Connection: close
74106

75107
private fun streamFrames() {
76108
val displayInfo = getDisplayInfo()
77-
Log.d(TAG, "Display info: ${displayInfo.width}x${displayInfo.height}")
109+
val scaledWidth = (displayInfo.width * scale).toInt()
110+
val scaledHeight = (displayInfo.height * scale).toInt()
111+
Log.d(TAG, "Display info: ${displayInfo.width}x${displayInfo.height}, scaled: ${scaledWidth}x${scaledHeight}, quality: $quality")
78112

79113
// Create a background thread with looper for image callbacks
80114
val handlerThread = HandlerThread("ScreenCapture")
@@ -95,7 +129,7 @@ Connection: close
95129
try {
96130
image = reader.acquireLatestImage()
97131
if (image != null) {
98-
val jpegData = convertImageToJpeg(image)
132+
val jpegData = ImageUtils.convertToJpeg(image, quality, scale)
99133
outputMjpegFrame(jpegData)
100134
Log.d(TAG, "Frame output: ${jpegData.size} bytes")
101135
}
@@ -144,42 +178,6 @@ Connection: close
144178
System.out.flush()
145179
}
146180

147-
private fun convertImageToJpeg(image: Image): ByteArray {
148-
val planes = image.planes
149-
val buffer = planes[0].buffer
150-
val pixelStride = planes[0].pixelStride
151-
val rowStride = planes[0].rowStride
152-
val rowPadding = rowStride - pixelStride * image.width
153-
154-
// Create bitmap from image data
155-
val bitmap = Bitmap.createBitmap(
156-
image.width + rowPadding / pixelStride,
157-
image.height,
158-
Bitmap.Config.ARGB_8888
159-
)
160-
bitmap.copyPixelsFromBuffer(buffer)
161-
162-
// Crop bitmap if there's padding
163-
val finalBitmap = if (rowPadding == 0) {
164-
bitmap
165-
} else {
166-
Bitmap.createBitmap(bitmap, 0, 0, image.width, image.height)
167-
}
168-
169-
// Convert to JPEG
170-
val outputStream = ByteArrayOutputStream()
171-
finalBitmap.compress(Bitmap.CompressFormat.JPEG, 90, outputStream)
172-
val jpegData = outputStream.toByteArray()
173-
174-
// Cleanup
175-
if (finalBitmap != bitmap) {
176-
bitmap.recycle()
177-
}
178-
finalBitmap.recycle()
179-
outputStream.close()
180-
181-
return jpegData
182-
}
183181

184182
private fun createVirtualDisplay(
185183
width: Int,
@@ -218,6 +216,7 @@ Connection: close
218216
0,
219217
surface
220218
) as VirtualDisplay
219+
221220
} catch (e: Exception) {
222221
Log.e(TAG, "Failed to create virtual display", e)
223222
null

0 commit comments

Comments
 (0)