From 6f9294cc500a196277237bcb3bea288a2053f8eb Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Fri, 16 Oct 2026 13:20:33 +0000
Subject: [PATCH 1/2] feat: Implement lightweight WebSocket server and receiver
service
Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com>
---
app/build.gradle | 3 +
app/src/main/AndroidManifest.xml | 4 +
.../librecast/receiver/LibreCastServer.java | 125 +++++++++
.../org/librecast/receiver/MainActivity.java | 73 +++++-
.../org/librecast/receiver/NetworkUtils.java | 68 +++++
.../librecast/receiver/ReceiverService.java | 230 +++++++++++++++++
app/src/main/res/layout/activity_main.xml | 13 +-
app/src/main/res/values/strings.xml | 5 +
.../receiver/LibreCastServerTest.java | 243 ++++++++++++++++++
.../librecast/receiver/NetworkUtilsTest.java | 20 ++
10 files changed, 781 insertions(+), 3 deletions(-)
create mode 100644 app/src/main/java/org/librecast/receiver/LibreCastServer.java
create mode 100644 app/src/main/java/org/librecast/receiver/NetworkUtils.java
create mode 100644 app/src/main/java/org/librecast/receiver/ReceiverService.java
create mode 100644 app/src/test/java/org/librecast/receiver/LibreCastServerTest.java
create mode 100644 app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java
diff --git a/app/build.gradle b/app/build.gradle
index 7ddc97a..99cb2f2 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,4 +31,7 @@ android {
dependencies {
implementation 'androidx.appcompat:appcompat:1.3.1'
implementation 'androidx.core:core:1.6.0'
+ implementation 'org.java-websocket:Java-WebSocket:1.5.4'
+
+ testImplementation 'junit:junit:4.13.2'
}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6616d58..f2ff5f5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,6 +28,10 @@
+
+
diff --git a/app/src/main/java/org/librecast/receiver/LibreCastServer.java b/app/src/main/java/org/librecast/receiver/LibreCastServer.java
new file mode 100644
index 0000000..77231a9
--- /dev/null
+++ b/app/src/main/java/org/librecast/receiver/LibreCastServer.java
@@ -0,0 +1,125 @@
+package org.librecast.receiver;
+
+import org.java_websocket.WebSocket;
+import org.java_websocket.handshake.ClientHandshake;
+import org.java_websocket.server.WebSocketServer;
+
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+
+public class LibreCastServer extends WebSocketServer {
+
+ public interface ServerEventListener {
+ void onServerStarted(int port);
+ void onServerStopped();
+ void onClientConnected(String clientIp);
+ void onClientDisconnected(String clientIp);
+ void onVideoChunkReceived(ByteBuffer chunk);
+ void onError(Exception ex);
+ }
+
+ public static final int DEFAULT_PORT = 8080;
+
+ private ServerEventListener listener;
+ private WebSocket activeConnection;
+
+ public LibreCastServer(int port) {
+ super(new InetSocketAddress(port));
+ setReuseAddr(true);
+ }
+
+ public LibreCastServer(int port, ServerEventListener listener) {
+ this(port);
+ this.listener = listener;
+ }
+
+ public void setListener(ServerEventListener listener) {
+ this.listener = listener;
+ }
+
+ public synchronized WebSocket getActiveConnection() {
+ return activeConnection;
+ }
+
+ @Override
+ public synchronized void onOpen(WebSocket conn, ClientHandshake handshake) {
+ if (activeConnection != null && activeConnection != conn && activeConnection.isOpen()) {
+ try {
+ activeConnection.close(1000, "Replaced by new client connection");
+ } catch (Exception ignored) {
+ }
+ }
+ activeConnection = conn;
+ String clientIp = getClientIp(conn);
+ if (listener != null) {
+ listener.onClientConnected(clientIp);
+ }
+ }
+
+ @Override
+ public synchronized void onClose(WebSocket conn, int code, String reason, boolean remote) {
+ if (conn == activeConnection) {
+ String clientIp = getClientIp(conn);
+ activeConnection = null;
+ if (listener != null) {
+ listener.onClientDisconnected(clientIp);
+ }
+ }
+ }
+
+ @Override
+ public void onMessage(WebSocket conn, String message) {
+ // Text messages handled or ignored
+ }
+
+ @Override
+ public void onMessage(WebSocket conn, ByteBuffer message) {
+ WebSocket currentActive;
+ synchronized (this) {
+ currentActive = activeConnection;
+ }
+ if (conn != null && conn == currentActive) {
+ if (listener != null) {
+ listener.onVideoChunkReceived(message);
+ }
+ }
+ }
+
+ @Override
+ public void onError(WebSocket conn, Exception ex) {
+ if (listener != null) {
+ listener.onError(ex);
+ }
+ }
+
+ @Override
+ public void onStart() {
+ if (listener != null) {
+ listener.onServerStarted(getPort());
+ }
+ }
+
+ @Override
+ public void stop() throws InterruptedException {
+ synchronized (this) {
+ if (activeConnection != null) {
+ try {
+ activeConnection.close(1000, "Server stopping");
+ } catch (Exception ignored) {
+ }
+ activeConnection = null;
+ }
+ }
+ super.stop();
+ if (listener != null) {
+ listener.onServerStopped();
+ }
+ }
+
+ private String getClientIp(WebSocket conn) {
+ if (conn != null && conn.getRemoteSocketAddress() != null && conn.getRemoteSocketAddress().getAddress() != null) {
+ return conn.getRemoteSocketAddress().getAddress().getHostAddress();
+ }
+ return "Unknown";
+ }
+}
diff --git a/app/src/main/java/org/librecast/receiver/MainActivity.java b/app/src/main/java/org/librecast/receiver/MainActivity.java
index 0048e8c..228760a 100644
--- a/app/src/main/java/org/librecast/receiver/MainActivity.java
+++ b/app/src/main/java/org/librecast/receiver/MainActivity.java
@@ -1,22 +1,64 @@
package org.librecast.receiver;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
import android.os.Bundle;
import android.view.View;
import android.view.WindowManager;
+import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
+ private TextView textServerStatus;
+ private TextView textConnectionStatus;
+
+ private final BroadcastReceiver statusReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent != null && ReceiverService.ACTION_STATUS_UPDATE.equals(intent.getAction())) {
+ updateUI(intent);
+ }
+ }
+ };
+
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Keep screen on for TV display and set full screen
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
-
hideSystemUI();
setContentView(R.layout.activity_main);
+
+ textServerStatus = findViewById(R.id.text_server_status);
+ textConnectionStatus = findViewById(R.id.text_connection_status);
+
+ startReceiverService();
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ IntentFilter filter = new IntentFilter(ReceiverService.ACTION_STATUS_UPDATE);
+ registerReceiver(statusReceiver, filter);
+
+ // Request current status
+ Intent queryIntent = new Intent(this, ReceiverService.class);
+ queryIntent.setAction(ReceiverService.ACTION_QUERY_STATUS);
+ startService(queryIntent);
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ try {
+ unregisterReceiver(statusReceiver);
+ } catch (Exception ignored) {
+ }
}
@Override
@@ -25,6 +67,35 @@ protected void onResume() {
hideSystemUI();
}
+ private void startReceiverService() {
+ Intent serviceIntent = new Intent(this, ReceiverService.class);
+ serviceIntent.setAction(ReceiverService.ACTION_START_SERVER);
+ startService(serviceIntent);
+ }
+
+ private void updateUI(Intent intent) {
+ boolean isRunning = intent.getBooleanExtra(ReceiverService.EXTRA_IS_RUNNING, false);
+ String serverIp = intent.getStringExtra(ReceiverService.EXTRA_SERVER_IP);
+ int serverPort = intent.getIntExtra(ReceiverService.EXTRA_SERVER_PORT, LibreCastServer.DEFAULT_PORT);
+ boolean isClientConnected = intent.getBooleanExtra(ReceiverService.EXTRA_IS_CLIENT_CONNECTED, false);
+ String clientIp = intent.getStringExtra(ReceiverService.EXTRA_CLIENT_IP);
+
+ if (isRunning) {
+ if (serverIp == null || serverIp.isEmpty() || "0.0.0.0".equals(serverIp)) {
+ serverIp = NetworkUtils.getLocalIpAddress(this);
+ }
+ textServerStatus.setText(getString(R.string.server_ready_format, serverIp, serverPort));
+ } else {
+ textServerStatus.setText(getString(R.string.server_stopped));
+ }
+
+ if (isClientConnected) {
+ textConnectionStatus.setText(getString(R.string.client_connected_format, clientIp != null ? clientIp : ""));
+ } else {
+ textConnectionStatus.setText(getString(R.string.waiting_for_connection));
+ }
+ }
+
private void hideSystemUI() {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
diff --git a/app/src/main/java/org/librecast/receiver/NetworkUtils.java b/app/src/main/java/org/librecast/receiver/NetworkUtils.java
new file mode 100644
index 0000000..2f07fe4
--- /dev/null
+++ b/app/src/main/java/org/librecast/receiver/NetworkUtils.java
@@ -0,0 +1,68 @@
+package org.librecast.receiver;
+
+import android.content.Context;
+import android.net.wifi.WifiInfo;
+import android.net.wifi.WifiManager;
+import java.net.Inet4Address;
+import java.net.InetAddress;
+import java.net.NetworkInterface;
+import java.util.Enumeration;
+import java.util.Locale;
+
+public class NetworkUtils {
+
+ /**
+ * Helper utility to get the device's local Wi-Fi or local network IPv4 address.
+ * Falls back to NetworkInterface enumeration if WifiManager returns 0.0.0.0.
+ */
+ public static String getLocalIpAddress(Context context) {
+ if (context != null) {
+ try {
+ WifiManager wifiManager = (WifiManager) context.getApplicationContext()
+ .getSystemService(Context.WIFI_SERVICE);
+ if (wifiManager != null && wifiManager.isWifiEnabled()) {
+ WifiInfo wifiInfo = wifiManager.getConnectionInfo();
+ if (wifiInfo != null) {
+ int ipAddress = wifiInfo.getIpAddress();
+ if (ipAddress != 0) {
+ return String.format(Locale.US, "%d.%d.%d.%d",
+ (ipAddress & 0xff),
+ (ipAddress >> 8 & 0xff),
+ (ipAddress >> 16 & 0xff),
+ (ipAddress >> 24 & 0xff));
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ }
+
+ return getNetworkInterfaceIpAddress();
+ }
+
+ /**
+ * Resolves local IPv4 address by checking network interfaces (Ethernet / Wi-Fi fallback).
+ */
+ public static String getNetworkInterfaceIpAddress() {
+ try {
+ Enumeration interfaces = NetworkInterface.getNetworkInterfaces();
+ if (interfaces != null) {
+ while (interfaces.hasMoreElements()) {
+ NetworkInterface netInterface = interfaces.nextElement();
+ if (netInterface.isLoopback() || !netInterface.isUp()) {
+ continue;
+ }
+ Enumeration addresses = netInterface.getInetAddresses();
+ while (addresses.hasMoreElements()) {
+ InetAddress addr = addresses.nextElement();
+ if (!addr.isLoopbackAddress() && addr instanceof Inet4Address) {
+ return addr.getHostAddress();
+ }
+ }
+ }
+ }
+ } catch (Exception ignored) {
+ }
+ return "0.0.0.0";
+ }
+}
diff --git a/app/src/main/java/org/librecast/receiver/ReceiverService.java b/app/src/main/java/org/librecast/receiver/ReceiverService.java
new file mode 100644
index 0000000..fdb0e00
--- /dev/null
+++ b/app/src/main/java/org/librecast/receiver/ReceiverService.java
@@ -0,0 +1,230 @@
+package org.librecast.receiver;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.Intent;
+import android.net.wifi.WifiManager;
+import android.os.Binder;
+import android.os.IBinder;
+import android.os.PowerManager;
+import android.util.Log;
+
+import java.nio.ByteBuffer;
+
+public class ReceiverService extends Service implements LibreCastServer.ServerEventListener {
+
+ private static final String TAG = "ReceiverService";
+
+ public static final String ACTION_STATUS_UPDATE = "org.librecast.receiver.ACTION_STATUS_UPDATE";
+ public static final String ACTION_START_SERVER = "org.librecast.receiver.ACTION_START_SERVER";
+ public static final String ACTION_STOP_SERVER = "org.librecast.receiver.ACTION_STOP_SERVER";
+ public static final String ACTION_QUERY_STATUS = "org.librecast.receiver.ACTION_QUERY_STATUS";
+
+ public static final String EXTRA_IS_RUNNING = "extra_is_running";
+ public static final String EXTRA_SERVER_IP = "extra_server_ip";
+ public static final String EXTRA_SERVER_PORT = "extra_server_port";
+ public static final String EXTRA_IS_CLIENT_CONNECTED = "extra_is_client_connected";
+ public static final String EXTRA_CLIENT_IP = "extra_client_ip";
+ public static final String EXTRA_ERROR_MESSAGE = "extra_error_message";
+
+ private final IBinder binder = new LocalBinder();
+ private LibreCastServer server;
+ private WifiManager.WifiLock wifiLock;
+ private PowerManager.WakeLock wakeLock;
+
+ private boolean isRunning = false;
+ private boolean isStarting = false;
+ private String serverIp = "0.0.0.0";
+ private int serverPort = LibreCastServer.DEFAULT_PORT;
+ private boolean isClientConnected = false;
+ private String clientIp = "";
+
+ public class LocalBinder extends Binder {
+ public ReceiverService getService() {
+ return ReceiverService.this;
+ }
+ }
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ acquireLocks();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ if (intent != null && intent.getAction() != null) {
+ String action = intent.getAction();
+ if (ACTION_STOP_SERVER.equals(action)) {
+ stopSelf();
+ return START_NOT_STICKY;
+ } else if (ACTION_QUERY_STATUS.equals(action)) {
+ broadcastStatus();
+ } else if (ACTION_START_SERVER.equals(action)) {
+ if (!isRunning && !isStarting) {
+ startWebSocketServer();
+ } else {
+ broadcastStatus();
+ }
+ }
+ } else {
+ if (!isRunning && !isStarting) {
+ startWebSocketServer();
+ } else {
+ broadcastStatus();
+ }
+ }
+ return START_STICKY;
+ }
+
+ @Override
+ public IBinder onBind(Intent intent) {
+ return binder;
+ }
+
+ private void acquireLocks() {
+ try {
+ WifiManager wifiManager = (WifiManager) getApplicationContext().getSystemService(Context.WIFI_SERVICE);
+ if (wifiManager != null) {
+ wifiLock = wifiManager.createWifiLock(WifiManager.WIFI_MODE_FULL_HIGH_PERF, "LibreCast:WifiLock");
+ wifiLock.setReferenceCounted(false);
+ wifiLock.acquire();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to acquire WifiLock", e);
+ }
+
+ try {
+ PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
+ if (powerManager != null) {
+ wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "LibreCast:WakeLock");
+ wakeLock.setReferenceCounted(false);
+ wakeLock.acquire();
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to acquire WakeLock", e);
+ }
+ }
+
+ private void releaseLocks() {
+ if (wifiLock != null && wifiLock.isHeld()) {
+ try {
+ wifiLock.release();
+ } catch (Exception ignored) {
+ }
+ }
+ if (wakeLock != null && wakeLock.isHeld()) {
+ try {
+ wakeLock.release();
+ } catch (Exception ignored) {
+ }
+ }
+ }
+
+ private synchronized void startWebSocketServer() {
+ if (server != null) {
+ try {
+ server.stop();
+ } catch (Exception ignored) {
+ }
+ }
+ isStarting = true;
+ serverIp = NetworkUtils.getLocalIpAddress(this);
+ serverPort = LibreCastServer.DEFAULT_PORT;
+ server = new LibreCastServer(serverPort, this);
+ server.start();
+ }
+
+ private synchronized void stopWebSocketServer() {
+ if (server != null) {
+ try {
+ server.stop();
+ } catch (Exception e) {
+ Log.e(TAG, "Error stopping server", e);
+ }
+ server = null;
+ }
+ isStarting = false;
+ isRunning = false;
+ isClientConnected = false;
+ clientIp = "";
+ broadcastStatus();
+ }
+
+ public synchronized void broadcastStatus() {
+ Intent intent = new Intent(ACTION_STATUS_UPDATE);
+ intent.setPackage(getPackageName());
+ intent.putExtra(EXTRA_IS_RUNNING, isRunning);
+ intent.putExtra(EXTRA_SERVER_IP, serverIp);
+ intent.putExtra(EXTRA_SERVER_PORT, serverPort);
+ intent.putExtra(EXTRA_IS_CLIENT_CONNECTED, isClientConnected);
+ intent.putExtra(EXTRA_CLIENT_IP, clientIp);
+ sendBroadcast(intent);
+ }
+
+ // LibreCastServer.ServerEventListener Callbacks
+
+ @Override
+ public void onServerStarted(int port) {
+ isStarting = false;
+ isRunning = true;
+ serverPort = port;
+ serverIp = NetworkUtils.getLocalIpAddress(this);
+ Log.i(TAG, "WebSocket server started at ws://" + serverIp + ":" + serverPort);
+ broadcastStatus();
+ }
+
+ @Override
+ public void onServerStopped() {
+ isStarting = false;
+ isRunning = false;
+ isClientConnected = false;
+ clientIp = "";
+ Log.i(TAG, "WebSocket server stopped");
+ broadcastStatus();
+ }
+
+ @Override
+ public void onClientConnected(String ip) {
+ isClientConnected = true;
+ clientIp = ip;
+ Log.i(TAG, "Client connected: " + ip);
+ broadcastStatus();
+ }
+
+ @Override
+ public void onClientDisconnected(String ip) {
+ isClientConnected = false;
+ clientIp = "";
+ Log.i(TAG, "Client disconnected: " + ip);
+ broadcastStatus();
+ }
+
+ @Override
+ public void onVideoChunkReceived(ByteBuffer chunk) {
+ // Raw video chunk received for processing / streaming playback pipeline
+ }
+
+ @Override
+ public void onError(Exception ex) {
+ Log.e(TAG, "WebSocket server error", ex);
+ Intent intent = new Intent(ACTION_STATUS_UPDATE);
+ intent.setPackage(getPackageName());
+ intent.putExtra(EXTRA_IS_RUNNING, isRunning);
+ intent.putExtra(EXTRA_SERVER_IP, serverIp);
+ intent.putExtra(EXTRA_SERVER_PORT, serverPort);
+ intent.putExtra(EXTRA_IS_CLIENT_CONNECTED, isClientConnected);
+ intent.putExtra(EXTRA_CLIENT_IP, clientIp);
+ if (ex != null) {
+ intent.putExtra(EXTRA_ERROR_MESSAGE, ex.getMessage());
+ }
+ sendBroadcast(intent);
+ }
+
+ @Override
+ public void onDestroy() {
+ stopWebSocketServer();
+ releaseLocks();
+ super.onDestroy();
+ }
+}
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
index 04b57c5..2774066 100644
--- a/app/src/main/res/layout/activity_main.xml
+++ b/app/src/main/res/layout/activity_main.xml
@@ -22,14 +22,23 @@
android:textStyle="bold" />
+
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 6cee3b5..519dec6 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -2,4 +2,9 @@
LibreCast Receiver
LibreCast Receiver
IP: 0.0.0.0
+ Starting server...
+ Server stopped
+ Ready: ws://%1$s:%2$d
+ Waiting for connection...
+ Client Connected: %1$s
diff --git a/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java b/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java
new file mode 100644
index 0000000..d5e0f14
--- /dev/null
+++ b/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java
@@ -0,0 +1,243 @@
+package org.librecast.receiver;
+
+import org.java_websocket.WebSocket;
+import org.java_websocket.drafts.Draft;
+import org.java_websocket.enums.Opcode;
+import org.java_websocket.enums.ReadyState;
+import org.java_websocket.framing.Framedata;
+import org.java_websocket.protocols.IProtocol;
+import org.junit.Test;
+
+import javax.net.ssl.SSLSession;
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.Collection;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import static org.junit.Assert.*;
+
+public class LibreCastServerTest {
+
+ @Test
+ public void testServerLifecycle() throws Exception {
+ AtomicBoolean started = new AtomicBoolean(false);
+ AtomicBoolean stopped = new AtomicBoolean(false);
+
+ LibreCastServer server = new LibreCastServer(0, new LibreCastServer.ServerEventListener() {
+ @Override
+ public void onServerStarted(int port) {
+ started.set(true);
+ }
+
+ @Override
+ public void onServerStopped() {
+ stopped.set(true);
+ }
+
+ @Override
+ public void onClientConnected(String clientIp) {}
+
+ @Override
+ public void onClientDisconnected(String clientIp) {}
+
+ @Override
+ public void onVideoChunkReceived(ByteBuffer chunk) {}
+
+ @Override
+ public void onError(Exception ex) {}
+ });
+
+ server.start();
+ for (int i = 0; i < 20; i++) {
+ if (started.get()) break;
+ Thread.sleep(100);
+ }
+ assertTrue("Server should have started", started.get());
+
+ server.stop();
+ assertTrue("Server should have stopped", stopped.get());
+ }
+
+ @Test
+ public void testSingleClientEnforcementAndBinaryChunk() {
+ AtomicReference connectedIp = new AtomicReference<>("");
+ AtomicReference receivedChunk = new AtomicReference<>(null);
+
+ LibreCastServer server = new LibreCastServer(0, new LibreCastServer.ServerEventListener() {
+ @Override
+ public void onServerStarted(int port) {}
+
+ @Override
+ public void onServerStopped() {}
+
+ @Override
+ public void onClientConnected(String clientIp) {
+ connectedIp.set(clientIp);
+ }
+
+ @Override
+ public void onClientDisconnected(String clientIp) {}
+
+ @Override
+ public void onVideoChunkReceived(ByteBuffer chunk) {
+ receivedChunk.set(chunk);
+ }
+
+ @Override
+ public void onError(Exception ex) {}
+ });
+
+ DummyWebSocket conn1 = new DummyWebSocket("192.168.1.10");
+ DummyWebSocket conn2 = new DummyWebSocket("192.168.1.20");
+
+ // First client connects
+ server.onOpen(conn1, null);
+ assertEquals(conn1, server.getActiveConnection());
+ assertEquals("192.168.1.10", connectedIp.get());
+
+ // Send binary data from active client
+ ByteBuffer buffer = ByteBuffer.wrap(new byte[]{0x00, 0x01, 0x02, 0x03});
+ server.onMessage(conn1, buffer);
+ assertNotNull("Binary chunk should be received", receivedChunk.get());
+ assertEquals(4, receivedChunk.get().remaining());
+
+ // Second client connects, replacing first
+ server.onOpen(conn2, null);
+ assertEquals(conn2, server.getActiveConnection());
+ assertTrue("First connection should be closed", conn1.isClosed());
+ assertEquals("192.168.1.20", connectedIp.get());
+
+ // Send binary data from old client conn1 should be ignored
+ receivedChunk.set(null);
+ server.onMessage(conn1, buffer);
+ assertNull("Binary chunk from inactive client should be ignored", receivedChunk.get());
+
+ // Send binary data from active client conn2 should succeed
+ server.onMessage(conn2, buffer);
+ assertNotNull("Binary chunk from active client should be received", receivedChunk.get());
+ }
+
+ private static class DummyWebSocket implements WebSocket {
+ private final String ip;
+ private boolean closed = false;
+
+ public DummyWebSocket(String ip) {
+ this.ip = ip;
+ }
+
+ @Override
+ public void close(int code, String message) {
+ this.closed = true;
+ }
+
+ @Override
+ public void close(int code) {
+ this.closed = true;
+ }
+
+ @Override
+ public void close() {
+ this.closed = true;
+ }
+
+ @Override
+ public void closeConnection(int code, String message) {
+ this.closed = true;
+ }
+
+ @Override
+ public void send(String text) {}
+
+ @Override
+ public void send(ByteBuffer bytes) {}
+
+ @Override
+ public void send(byte[] bytes) {}
+
+ @Override
+ public void sendFrame(Framedata framedata) {}
+
+ @Override
+ public void sendFrame(Collection frames) {}
+
+ @Override
+ public void sendPing() {}
+
+ @Override
+ public void sendFragmentedFrame(Opcode op, ByteBuffer buffer, boolean fin) {}
+
+ @Override
+ public boolean hasBufferedData() {
+ return false;
+ }
+
+ @Override
+ public InetSocketAddress getRemoteSocketAddress() {
+ return new InetSocketAddress(ip, 12345);
+ }
+
+ @Override
+ public InetSocketAddress getLocalSocketAddress() {
+ return new InetSocketAddress("0.0.0.0", 8080);
+ }
+
+ @Override
+ public boolean isOpen() {
+ return !closed;
+ }
+
+ @Override
+ public boolean isClosing() {
+ return false;
+ }
+
+ @Override
+ public boolean isFlushAndClose() {
+ return false;
+ }
+
+ @Override
+ public boolean isClosed() {
+ return closed;
+ }
+
+ @Override
+ public Draft getDraft() {
+ return null;
+ }
+
+ @Override
+ public ReadyState getReadyState() {
+ return closed ? ReadyState.CLOSED : ReadyState.OPEN;
+ }
+
+ @Override
+ public String getResourceDescriptor() {
+ return "";
+ }
+
+ @Override
+ public void setAttachment(T attachment) {}
+
+ @Override
+ public T getAttachment() {
+ return null;
+ }
+
+ @Override
+ public boolean hasSSLSupport() {
+ return false;
+ }
+
+ @Override
+ public SSLSession getSSLSession() {
+ return null;
+ }
+
+ @Override
+ public IProtocol getProtocol() {
+ return null;
+ }
+ }
+}
diff --git a/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java b/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java
new file mode 100644
index 0000000..76a5f16
--- /dev/null
+++ b/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java
@@ -0,0 +1,20 @@
+package org.librecast.receiver;
+
+import org.junit.Test;
+import static org.junit.Assert.*;
+
+public class NetworkUtilsTest {
+
+ @Test
+ public void testGetNetworkInterfaceIpAddress() {
+ String ip = NetworkUtils.getNetworkInterfaceIpAddress();
+ assertNotNull("IP address should not be null", ip);
+ assertFalse("IP address should not be empty", ip.trim().isEmpty());
+ }
+
+ @Test
+ public void testGetLocalIpAddressWithNullContext() {
+ String ip = NetworkUtils.getLocalIpAddress(null);
+ assertNotNull("IP address should fall back to non-null", ip);
+ }
+}
From 6e16b5fa4a26c87f669ac2921d69467a20839319 Mon Sep 17 00:00:00 2001
From: "google-labs-jules[bot]"
<161369871+google-labs-jules[bot]@users.noreply.github.com>
Date: Fri, 16 Oct 2026 16:05:10 +0000
Subject: [PATCH 2/2] feat: Implement lightweight WebSocket server and receiver
service
- Refactor package name and applicationId to com.m5dev.librecast
- Add Java-WebSocket 1.5.4 dependency compatible with minSdk 19
- Implement LibreCastServer extending WebSocketServer on port 8080 with binary video streaming support and single client enforcement
- Implement ReceiverService acquiring WifiLock and WakeLock and broadcasting status updates
- Implement NetworkUtils to resolve local Wi-Fi / Ethernet IPv4 address
- Update MainActivity and layout to display server and client connection status
- Add unit tests for NetworkUtils and LibreCastServer
Co-authored-by: ClausValcaTD <193546948+ClausValcaTD@users.noreply.github.com>
---
app/build.gradle | 4 ++--
.../m5dev/librecast}/LibreCastServer.java | 2 +-
.../receiver => com/m5dev/librecast}/MainActivity.java | 2 +-
.../receiver => com/m5dev/librecast}/NetworkUtils.java | 2 +-
.../m5dev/librecast}/ReceiverService.java | 10 +++++-----
.../m5dev/librecast}/LibreCastServerTest.java | 2 +-
.../m5dev/librecast}/NetworkUtilsTest.java | 2 +-
7 files changed, 12 insertions(+), 12 deletions(-)
rename app/src/main/java/{org/librecast/receiver => com/m5dev/librecast}/LibreCastServer.java (99%)
rename app/src/main/java/{org/librecast/receiver => com/m5dev/librecast}/MainActivity.java (99%)
rename app/src/main/java/{org/librecast/receiver => com/m5dev/librecast}/NetworkUtils.java (98%)
rename app/src/main/java/{org/librecast/receiver => com/m5dev/librecast}/ReceiverService.java (94%)
rename app/src/test/java/{org/librecast/receiver => com/m5dev/librecast}/LibreCastServerTest.java (99%)
rename app/src/test/java/{org/librecast/receiver => com/m5dev/librecast}/NetworkUtilsTest.java (94%)
diff --git a/app/build.gradle b/app/build.gradle
index 99cb2f2..f34418e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -3,11 +3,11 @@ plugins {
}
android {
- namespace 'org.librecast.receiver'
+ namespace 'com.m5dev.librecast'
compileSdk 34
defaultConfig {
- applicationId "org.librecast.receiver"
+ applicationId "com.m5dev.librecast"
minSdk 19
targetSdk 34
versionCode 1
diff --git a/app/src/main/java/org/librecast/receiver/LibreCastServer.java b/app/src/main/java/com/m5dev/librecast/LibreCastServer.java
similarity index 99%
rename from app/src/main/java/org/librecast/receiver/LibreCastServer.java
rename to app/src/main/java/com/m5dev/librecast/LibreCastServer.java
index 77231a9..36f6fb1 100644
--- a/app/src/main/java/org/librecast/receiver/LibreCastServer.java
+++ b/app/src/main/java/com/m5dev/librecast/LibreCastServer.java
@@ -1,4 +1,4 @@
-package org.librecast.receiver;
+package com.m5dev.librecast;
import org.java_websocket.WebSocket;
import org.java_websocket.handshake.ClientHandshake;
diff --git a/app/src/main/java/org/librecast/receiver/MainActivity.java b/app/src/main/java/com/m5dev/librecast/MainActivity.java
similarity index 99%
rename from app/src/main/java/org/librecast/receiver/MainActivity.java
rename to app/src/main/java/com/m5dev/librecast/MainActivity.java
index 228760a..c7f9549 100644
--- a/app/src/main/java/org/librecast/receiver/MainActivity.java
+++ b/app/src/main/java/com/m5dev/librecast/MainActivity.java
@@ -1,4 +1,4 @@
-package org.librecast.receiver;
+package com.m5dev.librecast;
import android.content.BroadcastReceiver;
import android.content.Context;
diff --git a/app/src/main/java/org/librecast/receiver/NetworkUtils.java b/app/src/main/java/com/m5dev/librecast/NetworkUtils.java
similarity index 98%
rename from app/src/main/java/org/librecast/receiver/NetworkUtils.java
rename to app/src/main/java/com/m5dev/librecast/NetworkUtils.java
index 2f07fe4..60abdc0 100644
--- a/app/src/main/java/org/librecast/receiver/NetworkUtils.java
+++ b/app/src/main/java/com/m5dev/librecast/NetworkUtils.java
@@ -1,4 +1,4 @@
-package org.librecast.receiver;
+package com.m5dev.librecast;
import android.content.Context;
import android.net.wifi.WifiInfo;
diff --git a/app/src/main/java/org/librecast/receiver/ReceiverService.java b/app/src/main/java/com/m5dev/librecast/ReceiverService.java
similarity index 94%
rename from app/src/main/java/org/librecast/receiver/ReceiverService.java
rename to app/src/main/java/com/m5dev/librecast/ReceiverService.java
index fdb0e00..1ddef25 100644
--- a/app/src/main/java/org/librecast/receiver/ReceiverService.java
+++ b/app/src/main/java/com/m5dev/librecast/ReceiverService.java
@@ -1,4 +1,4 @@
-package org.librecast.receiver;
+package com.m5dev.librecast;
import android.app.Service;
import android.content.Context;
@@ -15,10 +15,10 @@ public class ReceiverService extends Service implements LibreCastServer.ServerEv
private static final String TAG = "ReceiverService";
- public static final String ACTION_STATUS_UPDATE = "org.librecast.receiver.ACTION_STATUS_UPDATE";
- public static final String ACTION_START_SERVER = "org.librecast.receiver.ACTION_START_SERVER";
- public static final String ACTION_STOP_SERVER = "org.librecast.receiver.ACTION_STOP_SERVER";
- public static final String ACTION_QUERY_STATUS = "org.librecast.receiver.ACTION_QUERY_STATUS";
+ public static final String ACTION_STATUS_UPDATE = "com.m5dev.librecast.ACTION_STATUS_UPDATE";
+ public static final String ACTION_START_SERVER = "com.m5dev.librecast.ACTION_START_SERVER";
+ public static final String ACTION_STOP_SERVER = "com.m5dev.librecast.ACTION_STOP_SERVER";
+ public static final String ACTION_QUERY_STATUS = "com.m5dev.librecast.ACTION_QUERY_STATUS";
public static final String EXTRA_IS_RUNNING = "extra_is_running";
public static final String EXTRA_SERVER_IP = "extra_server_ip";
diff --git a/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java b/app/src/test/java/com/m5dev/librecast/LibreCastServerTest.java
similarity index 99%
rename from app/src/test/java/org/librecast/receiver/LibreCastServerTest.java
rename to app/src/test/java/com/m5dev/librecast/LibreCastServerTest.java
index d5e0f14..e19fb61 100644
--- a/app/src/test/java/org/librecast/receiver/LibreCastServerTest.java
+++ b/app/src/test/java/com/m5dev/librecast/LibreCastServerTest.java
@@ -1,4 +1,4 @@
-package org.librecast.receiver;
+package com.m5dev.librecast;
import org.java_websocket.WebSocket;
import org.java_websocket.drafts.Draft;
diff --git a/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java b/app/src/test/java/com/m5dev/librecast/NetworkUtilsTest.java
similarity index 94%
rename from app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java
rename to app/src/test/java/com/m5dev/librecast/NetworkUtilsTest.java
index 76a5f16..f648e24 100644
--- a/app/src/test/java/org/librecast/receiver/NetworkUtilsTest.java
+++ b/app/src/test/java/com/m5dev/librecast/NetworkUtilsTest.java
@@ -1,4 +1,4 @@
-package org.librecast.receiver;
+package com.m5dev.librecast;
import org.junit.Test;
import static org.junit.Assert.*;