diff --git a/.eslintrc.extension.js b/.eslintrc.extension.js index 4f11673..2167944 100644 --- a/.eslintrc.extension.js +++ b/.eslintrc.extension.js @@ -40,6 +40,11 @@ module.exports = { ts: 'never', tsx: 'never', }], + + // Additional rules not from the template. + 'require-await': 'off', // May not be needed but including for safety + '@typescript-eslint/require-await': 'error', + '@typescript-eslint/no-floating-promises': 'error', }, // Overrides for types. diff --git a/package.json b/package.json index 31ae8ac..c29b725 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "@nodecg/types": "^2.1.12", "module-alias": "^2.2.3", "needle": "^3.3.1", - "obs-websocket-js": "^4.0.3", + "obs-websocket-js": "^5.0.5", "socket.io-client": "^4.7.5", "speedcontrol-util": "github:speedcontrol/speedcontrol-util.git#build" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 14164e2..a5c0e47 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,8 +18,8 @@ importers: specifier: ^3.3.1 version: 3.3.1 obs-websocket-js: - specifier: ^4.0.3 - version: 4.0.3 + specifier: ^5.0.5 + version: 5.0.5 socket.io-client: specifier: ^4.7.5 version: 4.7.5 @@ -319,6 +319,10 @@ packages: '@jsdevtools/ono@7.1.3': resolution: {integrity: sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg==} + '@msgpack/msgpack@2.8.0': + resolution: {integrity: sha512-h9u4u/jiIRKbq25PM+zymTyW6bhTzELvOoUd+AvYriWOAKpLGnIamaET3pnHYoI5iYphAHBI4ayx0MehR+VVPQ==} + engines: {node: '>= 10'} + '@nodecg/types@2.1.12': resolution: {integrity: sha512-CO2PNAp3M6q2JsOtFHGPK/ONKxI5Pjkdn1wWPYep/r4A0TS+6fuq9uM9au7jSr9iF1o8fNZpJdikzZtle0Dm7Q==} @@ -916,6 +920,9 @@ packages: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1216,6 +1223,9 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + execa@2.1.0: resolution: {integrity: sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==} engines: {node: ^8.12.0 || >=9.7.0} @@ -1590,8 +1600,8 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - isomorphic-ws@4.0.1: - resolution: {integrity: sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==} + isomorphic-ws@5.0.0: + resolution: {integrity: sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==} peerDependencies: ws: '*' @@ -1904,8 +1914,9 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} - obs-websocket-js@4.0.3: - resolution: {integrity: sha512-28L5VHlrn9gT9uMeasR5VJkVTc+Xj6eWqZxSQWVsnzznRNJWrHJK5ldK6DMtnWOMTZarPznq8dp0ko4R+svqdg==} + obs-websocket-js@5.0.5: + resolution: {integrity: sha512-mSMqLXJ4z28jgwy7Ecv8CtpYh/xdbcn524kq0n6wT3kN6xkgWU/Zc6OtiVZo+gyyylC0anjehMLEVF+CDSwccw==} + engines: {node: '>12.0'} on-headers@1.0.2: resolution: {integrity: sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==} @@ -2442,6 +2453,10 @@ packages: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} + type-fest@3.13.1: + resolution: {integrity: sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==} + engines: {node: '>=14.16'} + type@2.7.3: resolution: {integrity: sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==} @@ -2729,18 +2744,6 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@7.5.10: - resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} - engines: {node: '>=8.3.0'} - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - ws@8.17.1: resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} engines: {node: '>=10.0.0'} @@ -2959,6 +2962,8 @@ snapshots: '@jsdevtools/ono@7.1.3': {} + '@msgpack/msgpack@2.8.0': {} + '@nodecg/types@2.1.12(typescript@5.4.5)': dependencies: '@polymer/polymer': 3.5.1 @@ -3716,6 +3721,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + cssesc@3.0.0: {} csstype@3.1.3: {} @@ -4164,6 +4171,8 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 + eventemitter3@5.0.1: {} + execa@2.1.0: dependencies: cross-spawn: 7.0.3 @@ -4556,9 +4565,9 @@ snapshots: isexe@2.0.0: {} - isomorphic-ws@4.0.1(ws@7.5.10): + isomorphic-ws@5.0.0(ws@8.17.1): dependencies: - ws: 7.5.10 + ws: 8.17.1 js-tokens@4.0.0: {} @@ -4896,12 +4905,15 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 - obs-websocket-js@4.0.3: + obs-websocket-js@5.0.5: dependencies: + '@msgpack/msgpack': 2.8.0 + crypto-js: 4.2.0 debug: 4.3.5 - isomorphic-ws: 4.0.1(ws@7.5.10) - sha.js: 2.4.11 - ws: 7.5.10 + eventemitter3: 5.0.1 + isomorphic-ws: 5.0.0(ws@8.17.1) + type-fest: 3.13.1 + ws: 8.17.1 transitivePeerDependencies: - bufferutil - supports-color @@ -5441,6 +5453,8 @@ snapshots: type-fest@1.4.0: {} + type-fest@3.13.1: {} + type@2.7.3: {} typed-array-buffer@1.0.2: @@ -5704,8 +5718,6 @@ snapshots: wrappy@1.0.2: {} - ws@7.5.10: {} - ws@8.17.1: {} xdg-basedir@4.0.0: {} diff --git a/src/extension/commercial.ts b/src/extension/commercial.ts index 163a986..82dada3 100644 --- a/src/extension/commercial.ts +++ b/src/extension/commercial.ts @@ -110,8 +110,8 @@ sc.on('timerReset', () => { async function playBreakCommercials(): Promise { try { // If we're no longer on an appropriate scene, stop trying to play non-run commercials. - const scene = await obs.send('GetCurrentScene'); - const isSceneNonRun = !!nonRunCommercialScenes.find((s) => scene.name.startsWith(s)); + const scene = await obs.call('GetCurrentProgramScene'); + const isSceneNonRun = !!nonRunCommercialScenes.find((s) => scene.sceneName.startsWith(s)); if (!isSceneNonRun) { if (intermissionCommercialTO) { clearTimeout(intermissionCommercialTO); @@ -164,13 +164,13 @@ async function playBreakCommercials(): Promise { } // Trigger a Twitch commercial when on the relevant scene. -obs.on('SwitchScenes', async (data) => { - if (data['scene-name'].startsWith(config.obs.nonRunCommercialTriggerScene) +obs.on('CurrentProgramSceneChanged', (data) => { + if (data.sceneName.startsWith(config.obs.nonRunCommercialTriggerScene) && !intermissionCommercialTO && !intermissionCommercials.specialLogic) { - playBreakCommercials(); + playBreakCommercials().catch(() => {}); } - const isSceneNonRun = !!nonRunCommercialScenes.find((s) => data['scene-name'].startsWith(s)); + const isSceneNonRun = !!nonRunCommercialScenes.find((s) => data.sceneName.startsWith(s)); // Only used by esa-layouts so we can continue playing commercials once our intermission player // ones have finished. Once we've switched to a relevant scene, skips the first (and second) diff --git a/src/extension/server.ts b/src/extension/server.ts index a7d8d01..4547140 100644 --- a/src/extension/server.ts +++ b/src/extension/server.ts @@ -141,7 +141,7 @@ async function changeTwitchMetadata(title?: string, gameId?: string): Promise { +export function setup(): void { if (!config.server.enabled) return; nodecg().log.info('[Server] Setting up'); diff --git a/src/extension/util/obs.ts b/src/extension/util/obs.ts index 87358ae..72ab945 100644 --- a/src/extension/util/obs.ts +++ b/src/extension/util/obs.ts @@ -1,43 +1,59 @@ -import OBSWebSocketJS from 'obs-websocket-js'; +import OBSWebSocket, { EventSubscription, OBSWebSocketError } from 'obs-websocket-js'; import { get as nodecg } from './nodecg'; const config = nodecg().bundleConfig.obs; -const obs = new OBSWebSocketJS(); +const obs = new OBSWebSocket(); let obsStreaming = false; -const settings = { - address: config.address, - password: config.password, -}; async function connect(): Promise { try { - await obs.connect(settings); - const streamingStatus = await obs.send('GetStreamingStatus'); - obsStreaming = streamingStatus.streaming; - nodecg().log.info('[OBS] Connection successful'); + const { + obsWebSocketVersion, + rpcVersion, + } = await obs.connect( + config.address, + config.password, + { + // eslint-disable-next-line no-bitwise + eventSubscriptions: EventSubscription.MediaInputs | EventSubscription.Transitions, + }, + ); + nodecg().log.debug( + '[OBS] Connected (version: %s, RPC: %s)', + obsWebSocketVersion, + rpcVersion, + ); + const data = await obs.call('GetStreamStatus'); + obsStreaming = data.outputActive; } catch (err) { - nodecg().log.warn('[OBS] Connection error'); - nodecg().log.debug('[OBS] Connection error:', err); + try { + await obs.disconnect(); + } catch { /* ignore errors */ } + nodecg().log.warn( + '[OBS] Connection error (reason: %s - %s)', + (err as OBSWebSocketError).code ?? 'N/A', + (err as OBSWebSocketError).message || 'N/A', + ); } } if (config.enabled) { nodecg().log.info('[OBS] Setting up connection'); - connect(); - obs.on('StreamStarted', () => { obsStreaming = true; }); - obs.on('StreamStopped', () => { obsStreaming = false; }); - obs.on('ConnectionClosed', () => { - nodecg().log.warn('[OBS] Connection lost, retrying in 5 seconds'); + connect().catch(() => {}); + obs.on('StreamStateChanged', (data) => { obsStreaming = data.outputActive; }); + obs.on('ConnectionClosed', (data) => { + nodecg().log.warn( + '[OBS] Connection closed (reason: %s - %s)', + data.code ?? 'N/A', + data.message || 'N/A', + ); setTimeout(connect, 5000); }); } -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore: Pretty sure this emits an error. -obs.on('error', (err) => { - nodecg().log.warn('[OBS] Connection error'); - nodecg().log.debug('[OBS] Connection error:', err); +obs.on('ConnectionError', (err) => { + nodecg().log.warn('[OBS] Connection error (reason: %s - %s):', err.code, err.message); }); export function isStreaming(): boolean { diff --git a/tsconfig.browser.json b/tsconfig.browser.json index aa9a772..a11104d 100644 --- a/tsconfig.browser.json +++ b/tsconfig.browser.json @@ -105,7 +105,7 @@ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ diff --git a/tsconfig.extension.json b/tsconfig.extension.json index 8391ca3..fa79ff4 100644 --- a/tsconfig.extension.json +++ b/tsconfig.extension.json @@ -100,7 +100,7 @@ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */ - // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ + "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */