Skip to content
This repository was archived by the owner on Nov 5, 2023. It is now read-only.

Commit 9a477bc

Browse files
committed
Actually implement OBS captions
1 parent c0b5cd6 commit 9a477bc

File tree

3 files changed

+137
-181
lines changed

3 files changed

+137
-181
lines changed

Diff for: app/components/channels/editors/obs.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export default {
113113
password: this.password,
114114
});
115115
116-
if (this.port && this.password) {
116+
if (this.port) {
117117
this.$emit('formValid');
118118
} else {
119119
this.$emit('formInvalid');

Diff for: app/plugins/channels/obs.js

+135-176
Original file line numberDiff line numberDiff line change
@@ -1,195 +1,154 @@
11
import OBSWebSocket from 'obs-websocket-js';
2+
import throttle from 'lodash.throttle';
23

3-
let obs;
4-
5-
export default ({ $store, $axios, channelId, channelParameters }) => {
4+
export default async ({ $store, $axios, channelId, channelParameters }) => {
65
// Register
76

8-
// if (!channelParameters.zoomApiToken) {
9-
// $store.commit('UPDATE_CHANNEL_ERROR', {
10-
// channelId,
11-
// error: 'Zoom API token is missing.',
12-
// });
7+
const handleError = (e, maxErrorsInPeriod, errorPeriodSeconds) => {
8+
let isAuthenticationRelatedError = e.error
9+
?.toLowerCase()
10+
.includes('authentic');
1311

14-
// // Turn off the channel because it's not configured correctly
15-
// $store.commit('TOGGLE_CHANNEL_ON_OR_OFF', { channelId, onOrOff: false });
16-
// // No need to unregister here because we haven't registered yet
17-
// return;
18-
// }
12+
let errorMessage;
1913

20-
let obs = new OBSWebSocket();
21-
obs.connect({ address: 'localhost:5454' }).then(() => {
22-
setInterval(() => {
23-
obs
24-
.send('SendCaptions', { text: 'hello world! ' + new Date() })
25-
// .send('GetVersion')
26-
.then((data) => console.log(data));
27-
}, 1500);
28-
});
14+
if (isAuthenticationRelatedError) {
15+
errorMessage = `OBS replied with "${e.error}" - Is the OBS WebSocket plugin using a password, and does it match the password in Web Captioner?`;
16+
} else if (maxErrorsInPeriod >= 0 && errorPeriodSeconds >= 0) {
17+
errorMessage = `This channel has been turned off because we received an error back from OBS ${maxErrorsInPeriod} times in the last ${errorPeriodSeconds} seconds that this channel was on. Make sure your port number and password (if you are using one) is correct, OBS is running with the OBS websocket plugin enabled, and try again.`;
18+
} else {
19+
errorMessage = `This channel has been turned off because we received an error back from OBS. Make sure your port number and password (if you are using one) is correct, OBS is running with the OBS websocket plugin enabled, and try again.`;
20+
}
21+
22+
$store.commit('UPDATE_CHANNEL_ERROR', {
23+
channelId,
24+
error: errorMessage,
25+
});
26+
27+
// Turn off the channel because it's not configured correctly
28+
$store.commit('TOGGLE_CHANNEL_ON_OR_OFF', {
29+
channelId,
30+
onOrOff: false,
31+
});
32+
};
33+
34+
if (!channelParameters.port) {
35+
$store.commit('UPDATE_CHANNEL_ERROR', {
36+
channelId,
37+
error: 'Port number is missing.',
38+
});
39+
40+
// Turn off the channel because it's not configured correctly
41+
$store.commit('TOGGLE_CHANNEL_ON_OR_OFF', { channelId, onOrOff: false });
42+
// No need to unregister here because we haven't registered yet
43+
return;
44+
}
2945

30-
// try {
31-
// new URL(channelParameters.zoomApiToken);
32-
// } catch (e) {
33-
// $store.commit('UPDATE_CHANNEL_ERROR', {
34-
// channelId,
35-
// error:
36-
// 'This channel has been turned off because the Zoom API token is not a valid URL. Make sure the Zoom API token is correct and try again.',
37-
// });
38-
39-
// // Turn off the channel because it's not configured correctly
40-
// $store.commit('TOGGLE_CHANNEL_ON_OR_OFF', { channelId, onOrOff: false });
41-
// // No need to unregister here because we haven't registered yet
42-
// return;
43-
// }
44-
45-
let zoomTranscriptBuffer = [];
46-
let zoomTranscriptCurrentlyDisplayed = [];
47-
const zoomMaxCharactersPerLine = 40;
48-
let lastSequenceNumber = 0;
49-
const zoomSequenceNumberLocalStorageKey =
50-
'webcaptioner-channels-zoom-sequence-number';
46+
console.log('channelParameters', channelParameters);
47+
let obs = new OBSWebSocket();
48+
try {
49+
await obs.connect({
50+
address: `localhost:${channelParameters.port}`,
51+
password: channelParameters.password,
52+
});
53+
} catch (e) {
54+
handleError(e);
55+
}
56+
57+
const maxCharactersPerLine = 32;
58+
const frequentUpdates = false; // allow for channelParameters.frequentUpdates in the future
59+
60+
let completeLines = []; // an array of arrays of words
61+
let lineInProgress = [];
62+
let automaticallyMarkLineCompleteAfterSilenceTimeout;
5163

5264
const unsubscribeFn = $store.subscribe((mutation, state) => {
53-
if (
54-
[
55-
'captioner/APPEND_TRANSCRIPT_STABILIZED',
56-
'captioner/APPEND_TRANSCRIPT_FINAL',
57-
'captioner/CLEAR_TRANSCRIPT',
58-
].includes(mutation.type)
59-
) {
60-
if (mutation.type === 'captioner/APPEND_TRANSCRIPT_STABILIZED') {
61-
zoomTranscriptBuffer.push(mutation.payload.transcript);
62-
} else if (
63-
(mutation.type === 'captioner/APPEND_TRANSCRIPT_FINAL' &&
64-
mutation.payload.clearLimitedSpaceReceivers) ||
65-
mutation.type === 'captioner/CLEAR_TRANSCRIPT'
66-
) {
67-
// Clear the output (this doesn't work completely yet)
68-
zoomTranscriptBuffer = ['\n', '\n'];
65+
if (mutation.type === 'captioner/APPEND_TRANSCRIPT_STABILIZED') {
66+
clearTimeout(automaticallyMarkLineCompleteAfterSilenceTimeout);
67+
lineInProgress.push(mutation.payload.transcript);
68+
69+
if (lineInProgress.join(' ').length > maxCharactersPerLine) {
70+
// The line is now too long. Save to completeLines.
71+
if (lineInProgress.length === 1) {
72+
// Save the whole line because we only have one really long word.
73+
completeLines.push([...lineInProgress.splice(0)]);
74+
} else {
75+
// Save everything but the last word, because that last word was
76+
// what put us beyond maxCharactersPerLine
77+
completeLines.push([
78+
...lineInProgress.splice(0, lineInProgress.length - 1),
79+
]);
80+
}
6981
}
82+
decideIfShouldSendToOBS(completeLines, lineInProgress);
83+
84+
automaticallyMarkLineCompleteAfterSilenceTimeout = setTimeout(() => {
85+
// We've waited long enough without getting new text.
86+
// Mark the lineInProgress we have now as complete, even
87+
// if it doesn't fill a line completely.
88+
completeLines.push([...lineInProgress.splice(0)]);
89+
decideIfShouldSendToOBS(completeLines, lineInProgress, {
90+
forceSend: true,
91+
});
92+
}, 2000);
7093
}
7194
});
7295

73-
// const errorDates = [];
74-
75-
// const zoomSendInterval = setInterval(() => {
76-
// if (!zoomTranscriptBuffer.length) {
77-
// return;
78-
// }
79-
80-
// try {
81-
// let localStorageValues = JSON.parse(
82-
// localStorage.getItem(zoomSequenceNumberLocalStorageKey)
83-
// );
84-
85-
// if (localStorageValues.zoomApiToken === channelParameters.zoomApiToken) {
86-
// // The stored sequenceNumber is for the current API token and not
87-
// // a previous one. Restore the value.
88-
// lastSequenceNumber = Number(localStorageValues.lastSequenceNumber);
89-
// }
90-
// } catch (e) {
91-
// // No local storage value found. Assume we're starting over.
92-
// lastSequenceNumber = 0;
93-
// }
94-
95-
// // Consume the buffer
96-
// zoomTranscriptCurrentlyDisplayed.push(...zoomTranscriptBuffer);
97-
// zoomTranscriptBuffer = [];
98-
99-
// let apiPath = new URL(channelParameters.zoomApiToken);
100-
// apiPath.searchParams.append('seq', String(lastSequenceNumber));
101-
// apiPath.searchParams.append(
102-
// 'lang',
103-
// $store.state.settings.locale.from || 'en-US'
104-
// );
105-
106-
// // Add line breaks if necessary
107-
// const firstWordAfterLastLineBreakIndex =
108-
// zoomTranscriptCurrentlyDisplayed.lastIndexOf('\n') + 1; // or this may be '0' if there are no line breaks yet
109-
// for (
110-
// let i = firstWordAfterLastLineBreakIndex;
111-
// i < zoomTranscriptCurrentlyDisplayed.length;
112-
// i++
113-
// ) {
114-
// // Check the length by adding one more word at a time
115-
// // up to but not including last
116-
// const someWordsAfterLastLineBreak = zoomTranscriptCurrentlyDisplayed.slice(
117-
// firstWordAfterLastLineBreakIndex,
118-
// i + 1
119-
// );
120-
121-
// if (
122-
// someWordsAfterLastLineBreak.join(' ').length > zoomMaxCharactersPerLine
123-
// ) {
124-
// // Add a line break before the `i`th word
125-
// zoomTranscriptCurrentlyDisplayed.splice(i, 0, '\n');
126-
// break;
127-
// }
128-
// }
129-
130-
// // Enforce two lines max by removing content before the
131-
// // first line break if we now have two line breaks
132-
// if (
133-
// zoomTranscriptCurrentlyDisplayed.filter((word) => word === '\n').length >=
134-
// 2
135-
// ) {
136-
// const firstLineBreakIndex = zoomTranscriptCurrentlyDisplayed.findIndex(
137-
// (word) => word === '\n'
138-
// );
139-
140-
// zoomTranscriptCurrentlyDisplayed.splice(0, firstLineBreakIndex + 1);
141-
// }
142-
143-
// const transcript = zoomTranscriptCurrentlyDisplayed
144-
// .join(' ')
145-
// .replace(' \n ', '\n') // remove spaces around line breaks
146-
// .trim();
147-
148-
// $axios
149-
// .$post('/api/channels/zoom', {
150-
// apiPath,
151-
// transcript,
152-
// })
153-
// .catch((e) => {
154-
// errorDates.push(new Date());
155-
156-
// const errorPeriodSeconds = 30;
157-
// const maxErrorsInPeriod = 10;
158-
// const errorPeriodStartDate = new Date(
159-
// Date.now() - 1000 * errorPeriodSeconds
160-
// );
161-
162-
// if (
163-
// errorDates.filter((date) => date > errorPeriodStartDate).length >
164-
// maxErrorsInPeriod
165-
// ) {
166-
// $store.commit('UPDATE_CHANNEL_ERROR', {
167-
// channelId,
168-
// error: `This channel has been turned off because we received an error back from Zoom ${maxErrorsInPeriod} times in the last ${errorPeriodSeconds} seconds that this channel was on. Make sure your Zoom API token is correct and valid for an active meeting and try again. If your meeting is not started yet, wait until your meeting is started before activating this channel. Note that you will need a new Zoom API token for every meeting.`,
169-
// });
170-
171-
// // Turn off the channel because it's not configured correctly
172-
// $store.commit('TOGGLE_CHANNEL_ON_OR_OFF', {
173-
// channelId,
174-
// onOrOff: false,
175-
// });
176-
// return;
177-
// }
178-
// });
179-
180-
// lastSequenceNumber++;
181-
// localStorage.setItem(
182-
// zoomSequenceNumberLocalStorageKey,
183-
// JSON.stringify({
184-
// lastSequenceNumber,
185-
// zoomApiToken: channelParameters.zoomApiToken,
186-
// })
187-
// );
188-
// }, 1000);
96+
const decideIfShouldSendToOBS = (
97+
completeLines = [],
98+
lineInProgress = [],
99+
{ forceSend = false } = {}
100+
) => {
101+
let linesToSend = [];
102+
if (!frequentUpdates && (completeLines.length >= 2 || forceSend)) {
103+
// We have at least two complete lines.
104+
linesToSend = completeLines.splice(0, 2);
105+
send(lineFormatter(linesToSend));
106+
} else if (frequentUpdates) {
107+
// Send the last complete line plus the currently in-progress line
108+
linesToSend = [
109+
...(completeLines?.[completeLines.length - 1]
110+
? [completeLines[completeLines.length - 1]]
111+
: []),
112+
...(lineInProgress.length ? [lineInProgress] : []),
113+
];
114+
send(lineFormatter(linesToSend));
115+
116+
// Clean up lines we will no longer need
117+
if (completeLines.length > 2) {
118+
completeLines.splice(0, completeLines.length - 2);
119+
}
120+
}
121+
};
122+
123+
const lineFormatter = (lines) => {
124+
return lines.map((line) => line.join(' ')).join('\n');
125+
};
126+
127+
let errorDates = [];
128+
129+
const send = throttle(async (text) => {
130+
try {
131+
await obs.send('SendCaptions', { text });
132+
} catch (e) {
133+
errorDates.push(new Date());
134+
135+
const errorPeriodSeconds = 30;
136+
const maxErrorsInPeriod = 4;
137+
const errorPeriodStartDate = new Date(
138+
Date.now() - 1000 * errorPeriodSeconds
139+
);
140+
141+
if (
142+
errorDates.filter((date) => date > errorPeriodStartDate).length >
143+
maxErrorsInPeriod
144+
) {
145+
handleError(e);
146+
}
147+
}
148+
}, 1000);
189149

190150
return () => {
191151
// Unregister function
192152
unsubscribeFn();
193-
// clearInterval(zoomSendInterval);
194153
};
195154
};

Diff for: app/plugins/channels/zoom.js

+1-4
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ export default ({ $store, $axios, channelId, channelParameters }) => {
5454
...lineInProgress.splice(0, lineInProgress.length - 1),
5555
]);
5656
}
57-
58-
console.log('completeLines', completeLines);
5957
}
6058
decideIfShouldSendToZoom(completeLines, lineInProgress);
6159

@@ -109,7 +107,6 @@ export default ({ $store, $axios, channelId, channelParameters }) => {
109107
let lastSequenceNumber = 0;
110108

111109
const sendToZoom = throttle(async (transcript) => {
112-
// return console.log('**', transcript);
113110
try {
114111
let localStorageValues = JSON.parse(
115112
localStorage.getItem(zoomSequenceNumberLocalStorageKey)
@@ -152,7 +149,7 @@ export default ({ $store, $axios, channelId, channelParameters }) => {
152149
errorDates.push(new Date());
153150

154151
const errorPeriodSeconds = 30;
155-
const maxErrorsInPeriod = 10;
152+
const maxErrorsInPeriod = 4;
156153
const errorPeriodStartDate = new Date(
157154
Date.now() - 1000 * errorPeriodSeconds
158155
);

0 commit comments

Comments
 (0)