Skip to content

Commit

Permalink
Allow setting stream settings (#8)
Browse files Browse the repository at this point in the history
  • Loading branch information
shamanec authored Oct 25, 2024
1 parent 594747f commit a7dad4f
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 13 deletions.
46 changes: 45 additions & 1 deletion app/src/main/java/com/shamanec/stream/LocalWebsocketServer.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.shamanec.stream;

import android.util.Log;

import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;
import androidx.test.uiautomator.UiObject;
Expand All @@ -14,9 +16,11 @@
import java.nio.ByteBuffer;

public class LocalWebsocketServer extends org.java_websocket.server.WebSocketServer {
private final ScreenCaptureService screenCaptureService;

public LocalWebsocketServer(int port) {
public LocalWebsocketServer(int port, ScreenCaptureService service) {
super(new InetSocketAddress(port));
this.screenCaptureService = service;
}

@Override
Expand All @@ -30,6 +34,46 @@ public void onClose(WebSocket conn, int code, String reason, boolean remote) {

@Override
public void onMessage(WebSocket conn, String message) {
String[] splitMsg = message.split(":");

for (int i=0;i < splitMsg.length; i++) {
String currentMsgPart = splitMsg[i];

// Check if the message contains "targetFPS="
if (currentMsgPart.startsWith("targetFPS=")) {
try {
// Parse the FPS value from the message
int fps = Integer.parseInt(currentMsgPart.split("=")[1]);
// Update the targetFPS in ScreenCaptureService
screenCaptureService.setTargetFPS(fps);
} catch (NumberFormatException e) {
e.printStackTrace();
Log.e("LocalWebsocketServer", "Invalid FPS value received: " + currentMsgPart);
}
}
if (currentMsgPart.startsWith("jpegQuality=")) {
try {
// Parse the jpegQuality value from the message
int jpegQuality = Integer.parseInt(currentMsgPart.split("=")[1]);
// Update the jpegQuality in ScreenCaptureService
screenCaptureService.setJpegQuality(jpegQuality);
} catch (NumberFormatException e) {
e.printStackTrace();
Log.e("LocalWebsocketServer", "Invalid JPEG quality value received: " + currentMsgPart);
}
}
if (currentMsgPart.startsWith("scalingFactor=")) {
try {
// Parse the scalingFactor value from the message
int scalingFactor = Integer.parseInt(currentMsgPart.split("=")[1]);
// Update the scalingFactor in ScreenCaptureService
screenCaptureService.setScalingFactor(scalingFactor);
} catch (NumberFormatException e) {
e.printStackTrace();
Log.e("LocalWebsocketServer", "Invalid scaling factor value received: " + currentMsgPart);
}
}
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,13 @@

public class ScreenCaptureActivity extends Activity {
private static final int REQUEST_CODE = 100;
public static int jpegQuality;

//The onCreate method is an Android activity lifecycle method that gets called when the activity is created.
// In this case, it sets the layout for the activity and calls startProjection().
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
jpegQuality = getIntent().getIntExtra("jpegQuality", 90);
startProjection();
}

Expand Down
72 changes: 62 additions & 10 deletions app/src/main/java/com/shamanec/stream/ScreenCaptureService.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
Expand All @@ -49,10 +51,31 @@ public class ScreenCaptureService extends Service {
private int mRotation;
private OrientationChangeCallback mOrientationChangeCallback;

private int targetFPS = 15;
private int jpegQuality = 90;
private int scalingFactor = 2;
private long frameIntervalMs = 1000 / targetFPS;

LocalWebsocketServer server;

public void setTargetFPS(int fps) {
this.targetFPS = fps;
this.frameIntervalMs = 1000 / fps;
Log.d("ScreenCaptureService", "Target FPS set to: " + this.targetFPS);
}

public void setJpegQuality(int jpegQuality) {
this.jpegQuality = jpegQuality;
Log.d("ScreenCaptureService", "Jpeg quality set to: " + this.jpegQuality);
}

public void setScalingFactor(int scalingFactor) {
this.scalingFactor = scalingFactor == 50 ? 2 : 4;
Log.d("ScreenCaptureService", "Scaling factor set to: " + this.scalingFactor);
}

public ScreenCaptureService() throws IOException {
server = new LocalWebsocketServer(1991);
server = new LocalWebsocketServer(1991, this);
server.setReuseAddr(true);
server.start();
}
Expand Down Expand Up @@ -118,30 +141,59 @@ private Bitmap imageToBitmap(Image image) {

BlockingQueue<Bitmap> imageQueue = new LinkedBlockingDeque<>(3);
private class ImageConsumer implements Runnable {
private String previousBitmapHash = null;

@Override
public void run() {
while (true) {
try {
// Take the next image from the queue (this will block if the queue is empty)
Bitmap bitmap = imageQueue.take();

// Compress the image
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, ScreenCaptureActivity.jpegQuality, outputStream);
// Get the hash of the current bitmap
String currentBitmapHash = getBitmapHash(bitmap);
// If the current bitmap hash has is different from the previous bitmap hash
// Then we have a new different frame so we sent it
// Else we just skip the frame and recycle the bitmap directly and wait for next frame
if (!currentBitmapHash.equals(previousBitmapHash)) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, jpegQuality, outputStream);
byte[] compressedImage = outputStream.toByteArray();

bitmap.recycle();
server.broadcast(compressedImage);
previousBitmapHash = currentBitmapHash;
}

byte[] compressedImage = outputStream.toByteArray();
bitmap.recycle();

// Send the compressed image over the WebSocket
server.broadcast(compressedImage);
// Wait for the frame interval before capturing the next frame
Thread.sleep(frameIntervalMs);

} catch (InterruptedException e) {
// Handle interruption or exit the loop
break;
}
}
}

private String getBitmapHash(Bitmap bitmap) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
ByteBuffer buffer = ByteBuffer.allocate(bitmap.getByteCount());
bitmap.copyPixelsToBuffer(buffer);
byte[] pixelData = buffer.array();
byte[] hashBytes = digest.digest(pixelData);

// Convert hash bytes to hex string
StringBuilder hashString = new StringBuilder();
for (byte b : hashBytes) {
hashString.append(String.format("%02x", b));
}
return hashString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}

private class ImageAvailableListener implements ImageReader.OnImageAvailableListener {
Expand Down Expand Up @@ -300,8 +352,8 @@ private void createVirtualDisplay() {
int metricsWidth = metrics.widthPixels;
int metricsHeight = metrics.heightPixels;

mWidth = metricsWidth / 2;
mHeight = metricsHeight / 2;
mWidth = metricsWidth / scalingFactor;
mHeight = metricsHeight / scalingFactor;
mDensity = metrics.densityDpi;

// Create an ImageReader object with the proper display dimensions and PixelFormat
Expand Down

0 comments on commit a7dad4f

Please sign in to comment.