Skip to content

Commit

Permalink
Create WebSocketClient
Browse files Browse the repository at this point in the history
  • Loading branch information
GravityDarkLab committed Jan 16, 2024
1 parent 0406f22 commit b0842b9
Show file tree
Hide file tree
Showing 2 changed files with 248 additions and 60 deletions.
123 changes: 63 additions & 60 deletions pom.xml
Original file line number Diff line number Diff line change
@@ -1,66 +1,69 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gravitylab</groupId>
<artifactId>obs-controller-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>obs-controller-api</name>
<description>API Controller for OBS</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.gravitylab</groupId>
<artifactId>obs-controller-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>obs-controller-api</name>
<description>API Controller for OBS</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.15</version>
</dependency>
<dependency>
<groupId>org.java-websocket</groupId>
<artifactId>Java-WebSocket</artifactId>
<version>1.5.3</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-docker-compose</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-ui</artifactId>
<version>1.6.15</version>
</dependency>
</dependencies>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package com.gravitylab.obscontrollerapi.websocket;

import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class OBSWebSocketClient extends WebSocketClient {

@Value("${obs.websocket.password}")
private String obsPassword;

private String salt = "";
private String challenge = "";
private String authToken = "";

private URI serverUri;

static int requestID = 0;
static int rpcVersion = 1;
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

@Autowired
public OBSWebSocketClient(@Value("${obs.websocket.uri}") URI serverUri) {
super(serverUri);
}

@Override
public void onOpen(ServerHandshake serverHandshake) {
log.info("Connected to OBS Websocket");
}

@Override
public void onMessage(String s) {
JSONObject receivedJson = new JSONObject(s);
if (!receivedJson.has("op") || !receivedJson.has("d")) {
log.info("Received message from OBS Websocket {}", receivedJson.toString(4));
return;
}
log.info("Message from OBS Websocket {}", receivedJson.toString(4));

int operation = receivedJson.getInt("op");
rpcVersion = setRpcVersion(receivedJson);

if (operation == 0) {
handleAuthentication(receivedJson);
}
}

@Override
public void onClose(int i, String s, boolean b) {
String message = new String(s.getBytes(), StandardCharsets.UTF_8);
log.info("Disconnected from OBS Websocket {} {}", i, message);
}

@Override
public void onError(Exception e) {
log.error("Error from OBS Websocket", e);
}

public void authenticate() {
sendIdentifyMessage(this.authToken);
}

public void startRecording() {
sendStartRecordRequest();
}

public void stopRecording() {
sendStopRecordRequest();
}

private void handleAuthentication(JSONObject receivedJson) {
JSONObject authenticationData = receivedJson.optJSONObject("d").getJSONObject("authentication");
this.salt = authenticationData.getString("salt");
this.challenge = authenticationData.getString("challenge");
this.authToken = generateAuthToken(salt, challenge);
log.info("Token generated :)");
}

private void sendStartRecordRequest() {
JSONObject request = new JSONObject();
request.put("op", 6);
JSONObject data = new JSONObject();
data.put("requestType", "StartRecord");
data.put("requestId", requestID++);
JSONObject requestData = new JSONObject();
requestData.put("sceneName", "Scene 1");
data.put("requestData", requestData);
request.put("d", data);
this.send(request.toString());
log.info("Sent request to OBS Websocket {}", request.toString(4));
}

private void handleRecordStateChange(JSONObject receivedJson) {
JSONObject eventData = receivedJson.optJSONObject("d").getJSONObject("eventData");
String eventType = receivedJson.getJSONObject("d").getString("eventType");
boolean outputActive = eventData.getBoolean("outputActive");
String outputState = eventData.getString("outputState");
var outputPath = eventData.get("outputPath");

if (outputActive && outputState.equals("OBS_WEBSOCKET_OUTPUT_STARTED")) {
log.info("Event type: {} ,Output active: {}, Output state: {}, Output path: {}", eventType, outputActive,
outputState, outputPath);
scheduler.schedule(this::sendStopRecordRequest, 30, TimeUnit.SECONDS);
}
}

private void sendStopRecordRequest() {
JSONObject stopRecordRequest = new JSONObject();
stopRecordRequest.put("op", 6); // Assuming '6' is the operation code for StopRecord
JSONObject data = new JSONObject();
data.put("requestType", "StopRecord");
data.put("requestId", requestID++);
stopRecordRequest.put("d", data);
this.send(stopRecordRequest.toString());
log.info("Sent StopRecord request to OBS Websocket {}", stopRecordRequest.toString(4));
}

private void sendIdentifyMessage(String authToken) {
JSONObject identifyMessage = new JSONObject();
identifyMessage.put("op", 1);
JSONObject data = new JSONObject();
data.put("rpcVersion", rpcVersion);
data.put("authentication", authToken);
identifyMessage.put("d", data);
this.send(identifyMessage.toString());
log.info("Sent identify message to OBS Websocket {}", identifyMessage.toString(4));
}

private String generateAuthToken(String salt, String challenge) {
try {
return generateSecret(salt, challenge);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}

private String generateSecret(String salt, String challenge) throws NoSuchAlgorithmException {
// Step 1: Concatenate password and salt
String passAndSalt = this.obsPassword + salt;
// Step 2: SHA256 hash and base64 encode
String base64Secret = base64Encode(sha256Hash(passAndSalt));
// Step 3: Concatenate base64 secret with challenge
String secretAndChallenge = base64Secret + challenge;
// Step 4: SHA256 hash of the result and base64 encode
return base64Encode(sha256Hash(secretAndChallenge));
}

private static byte[] sha256Hash(String input) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
return digest.digest(input.getBytes());
}

private static String base64Encode(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}

private int setRpcVersion(JSONObject receivedJson) {
int rpcVersion = 1;
JSONObject data = receivedJson.getJSONObject("d");
if (data.has("rpcVersion")) {
rpcVersion = data.getInt("rpcVersion");
}
if (data.has("negotiatedRpcVersion")) {
rpcVersion = data.getInt("negotiatedRpcVersion");
}
return rpcVersion;
}
}

0 comments on commit b0842b9

Please sign in to comment.