Skip to content

Commit b0842b9

Browse files
Create WebSocketClient
1 parent 0406f22 commit b0842b9

2 files changed

Lines changed: 248 additions & 60 deletions

File tree

pom.xml

Lines changed: 63 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,66 +1,69 @@
11
<?xml version="1.0" encoding="UTF-8"?>
22
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
3-
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4-
<modelVersion>4.0.0</modelVersion>
5-
<parent>
6-
<groupId>org.springframework.boot</groupId>
7-
<artifactId>spring-boot-starter-parent</artifactId>
8-
<version>3.2.1</version>
9-
<relativePath/> <!-- lookup parent from repository -->
10-
</parent>
11-
<groupId>com.gravitylab</groupId>
12-
<artifactId>obs-controller-api</artifactId>
13-
<version>0.0.1-SNAPSHOT</version>
14-
<name>obs-controller-api</name>
15-
<description>API Controller for OBS</description>
16-
<properties>
17-
<java.version>17</java.version>
18-
</properties>
19-
<dependencies>
20-
<dependency>
21-
<groupId>org.springframework.boot</groupId>
22-
<artifactId>spring-boot-starter-web</artifactId>
23-
</dependency>
3+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
4+
<modelVersion>4.0.0</modelVersion>
5+
<parent>
6+
<groupId>org.springframework.boot</groupId>
7+
<artifactId>spring-boot-starter-parent</artifactId>
8+
<version>3.2.1</version>
9+
<relativePath/> <!-- lookup parent from repository -->
10+
</parent>
11+
<groupId>com.gravitylab</groupId>
12+
<artifactId>obs-controller-api</artifactId>
13+
<version>0.0.1-SNAPSHOT</version>
14+
<name>obs-controller-api</name>
15+
<description>API Controller for OBS</description>
16+
<properties>
17+
<java.version>17</java.version>
18+
</properties>
19+
<dependencies>
20+
<dependency>
21+
<groupId>org.springframework.boot</groupId>
22+
<artifactId>spring-boot-starter-web</artifactId>
23+
</dependency>
24+
<dependency>
25+
<groupId>org.projectlombok</groupId>
26+
<artifactId>lombok</artifactId>
27+
<optional>true</optional>
28+
</dependency>
29+
<dependency>
30+
<groupId>org.springframework.boot</groupId>
31+
<artifactId>spring-boot-starter-test</artifactId>
32+
<scope>test</scope>
33+
</dependency>
34+
<dependency>
35+
<groupId>org.springdoc</groupId>
36+
<artifactId>springdoc-openapi-ui</artifactId>
37+
<version>1.6.15</version>
38+
</dependency>
39+
<dependency>
40+
<groupId>org.java-websocket</groupId>
41+
<artifactId>Java-WebSocket</artifactId>
42+
<version>1.5.3</version>
43+
</dependency>
44+
<dependency>
45+
<groupId>org.json</groupId>
46+
<artifactId>json</artifactId>
47+
<version>20231013</version>
48+
</dependency>
2449

25-
<dependency>
26-
<groupId>org.springframework.boot</groupId>
27-
<artifactId>spring-boot-docker-compose</artifactId>
28-
<scope>runtime</scope>
29-
<optional>true</optional>
30-
</dependency>
31-
<dependency>
32-
<groupId>org.projectlombok</groupId>
33-
<artifactId>lombok</artifactId>
34-
<optional>true</optional>
35-
</dependency>
36-
<dependency>
37-
<groupId>org.springframework.boot</groupId>
38-
<artifactId>spring-boot-starter-test</artifactId>
39-
<scope>test</scope>
40-
</dependency>
41-
<dependency>
42-
<groupId>org.springdoc</groupId>
43-
<artifactId>springdoc-openapi-ui</artifactId>
44-
<version>1.6.15</version>
45-
</dependency>
50+
</dependencies>
4651

47-
</dependencies>
48-
49-
<build>
50-
<plugins>
51-
<plugin>
52-
<groupId>org.springframework.boot</groupId>
53-
<artifactId>spring-boot-maven-plugin</artifactId>
54-
<configuration>
55-
<excludes>
56-
<exclude>
57-
<groupId>org.projectlombok</groupId>
58-
<artifactId>lombok</artifactId>
59-
</exclude>
60-
</excludes>
61-
</configuration>
62-
</plugin>
63-
</plugins>
64-
</build>
52+
<build>
53+
<plugins>
54+
<plugin>
55+
<groupId>org.springframework.boot</groupId>
56+
<artifactId>spring-boot-maven-plugin</artifactId>
57+
<configuration>
58+
<excludes>
59+
<exclude>
60+
<groupId>org.projectlombok</groupId>
61+
<artifactId>lombok</artifactId>
62+
</exclude>
63+
</excludes>
64+
</configuration>
65+
</plugin>
66+
</plugins>
67+
</build>
6568

6669
</project>
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package com.gravitylab.obscontrollerapi.websocket;
2+
3+
import java.net.URI;
4+
import java.nio.charset.StandardCharsets;
5+
import java.security.MessageDigest;
6+
import java.security.NoSuchAlgorithmException;
7+
import java.util.Base64;
8+
import java.util.concurrent.Executors;
9+
import java.util.concurrent.ScheduledExecutorService;
10+
import java.util.concurrent.TimeUnit;
11+
12+
import org.java_websocket.client.WebSocketClient;
13+
import org.java_websocket.handshake.ServerHandshake;
14+
import org.json.JSONObject;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.beans.factory.annotation.Value;
17+
import org.springframework.stereotype.Component;
18+
19+
import lombok.extern.slf4j.Slf4j;
20+
21+
@Slf4j
22+
@Component
23+
public class OBSWebSocketClient extends WebSocketClient {
24+
25+
@Value("${obs.websocket.password}")
26+
private String obsPassword;
27+
28+
private String salt = "";
29+
private String challenge = "";
30+
private String authToken = "";
31+
32+
private URI serverUri;
33+
34+
static int requestID = 0;
35+
static int rpcVersion = 1;
36+
private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
37+
38+
@Autowired
39+
public OBSWebSocketClient(@Value("${obs.websocket.uri}") URI serverUri) {
40+
super(serverUri);
41+
}
42+
43+
@Override
44+
public void onOpen(ServerHandshake serverHandshake) {
45+
log.info("Connected to OBS Websocket");
46+
}
47+
48+
@Override
49+
public void onMessage(String s) {
50+
JSONObject receivedJson = new JSONObject(s);
51+
if (!receivedJson.has("op") || !receivedJson.has("d")) {
52+
log.info("Received message from OBS Websocket {}", receivedJson.toString(4));
53+
return;
54+
}
55+
log.info("Message from OBS Websocket {}", receivedJson.toString(4));
56+
57+
int operation = receivedJson.getInt("op");
58+
rpcVersion = setRpcVersion(receivedJson);
59+
60+
if (operation == 0) {
61+
handleAuthentication(receivedJson);
62+
}
63+
}
64+
65+
@Override
66+
public void onClose(int i, String s, boolean b) {
67+
String message = new String(s.getBytes(), StandardCharsets.UTF_8);
68+
log.info("Disconnected from OBS Websocket {} {}", i, message);
69+
}
70+
71+
@Override
72+
public void onError(Exception e) {
73+
log.error("Error from OBS Websocket", e);
74+
}
75+
76+
public void authenticate() {
77+
sendIdentifyMessage(this.authToken);
78+
}
79+
80+
public void startRecording() {
81+
sendStartRecordRequest();
82+
}
83+
84+
public void stopRecording() {
85+
sendStopRecordRequest();
86+
}
87+
88+
private void handleAuthentication(JSONObject receivedJson) {
89+
JSONObject authenticationData = receivedJson.optJSONObject("d").getJSONObject("authentication");
90+
this.salt = authenticationData.getString("salt");
91+
this.challenge = authenticationData.getString("challenge");
92+
this.authToken = generateAuthToken(salt, challenge);
93+
log.info("Token generated :)");
94+
}
95+
96+
private void sendStartRecordRequest() {
97+
JSONObject request = new JSONObject();
98+
request.put("op", 6);
99+
JSONObject data = new JSONObject();
100+
data.put("requestType", "StartRecord");
101+
data.put("requestId", requestID++);
102+
JSONObject requestData = new JSONObject();
103+
requestData.put("sceneName", "Scene 1");
104+
data.put("requestData", requestData);
105+
request.put("d", data);
106+
this.send(request.toString());
107+
log.info("Sent request to OBS Websocket {}", request.toString(4));
108+
}
109+
110+
private void handleRecordStateChange(JSONObject receivedJson) {
111+
JSONObject eventData = receivedJson.optJSONObject("d").getJSONObject("eventData");
112+
String eventType = receivedJson.getJSONObject("d").getString("eventType");
113+
boolean outputActive = eventData.getBoolean("outputActive");
114+
String outputState = eventData.getString("outputState");
115+
var outputPath = eventData.get("outputPath");
116+
117+
if (outputActive && outputState.equals("OBS_WEBSOCKET_OUTPUT_STARTED")) {
118+
log.info("Event type: {} ,Output active: {}, Output state: {}, Output path: {}", eventType, outputActive,
119+
outputState, outputPath);
120+
scheduler.schedule(this::sendStopRecordRequest, 30, TimeUnit.SECONDS);
121+
}
122+
}
123+
124+
private void sendStopRecordRequest() {
125+
JSONObject stopRecordRequest = new JSONObject();
126+
stopRecordRequest.put("op", 6); // Assuming '6' is the operation code for StopRecord
127+
JSONObject data = new JSONObject();
128+
data.put("requestType", "StopRecord");
129+
data.put("requestId", requestID++);
130+
stopRecordRequest.put("d", data);
131+
this.send(stopRecordRequest.toString());
132+
log.info("Sent StopRecord request to OBS Websocket {}", stopRecordRequest.toString(4));
133+
}
134+
135+
private void sendIdentifyMessage(String authToken) {
136+
JSONObject identifyMessage = new JSONObject();
137+
identifyMessage.put("op", 1);
138+
JSONObject data = new JSONObject();
139+
data.put("rpcVersion", rpcVersion);
140+
data.put("authentication", authToken);
141+
identifyMessage.put("d", data);
142+
this.send(identifyMessage.toString());
143+
log.info("Sent identify message to OBS Websocket {}", identifyMessage.toString(4));
144+
}
145+
146+
private String generateAuthToken(String salt, String challenge) {
147+
try {
148+
return generateSecret(salt, challenge);
149+
} catch (NoSuchAlgorithmException e) {
150+
throw new RuntimeException(e);
151+
}
152+
}
153+
154+
private String generateSecret(String salt, String challenge) throws NoSuchAlgorithmException {
155+
// Step 1: Concatenate password and salt
156+
String passAndSalt = this.obsPassword + salt;
157+
// Step 2: SHA256 hash and base64 encode
158+
String base64Secret = base64Encode(sha256Hash(passAndSalt));
159+
// Step 3: Concatenate base64 secret with challenge
160+
String secretAndChallenge = base64Secret + challenge;
161+
// Step 4: SHA256 hash of the result and base64 encode
162+
return base64Encode(sha256Hash(secretAndChallenge));
163+
}
164+
165+
private static byte[] sha256Hash(String input) throws NoSuchAlgorithmException {
166+
MessageDigest digest = MessageDigest.getInstance("SHA-256");
167+
return digest.digest(input.getBytes());
168+
}
169+
170+
private static String base64Encode(byte[] bytes) {
171+
return Base64.getEncoder().encodeToString(bytes);
172+
}
173+
174+
private int setRpcVersion(JSONObject receivedJson) {
175+
int rpcVersion = 1;
176+
JSONObject data = receivedJson.getJSONObject("d");
177+
if (data.has("rpcVersion")) {
178+
rpcVersion = data.getInt("rpcVersion");
179+
}
180+
if (data.has("negotiatedRpcVersion")) {
181+
rpcVersion = data.getInt("negotiatedRpcVersion");
182+
}
183+
return rpcVersion;
184+
}
185+
}

0 commit comments

Comments
 (0)