-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
0406f22
commit b0842b9
Showing
2 changed files
with
248 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
185 changes: 185 additions & 0 deletions
185
src/main/java/com/gravitylab/obscontrollerapi/websocket/OBSWebSocketClient.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |