Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,53 @@ await obs.call('SetCurrentProgramScene', {sceneName: 'Gameplay'});
const {inputMuted} = obs.call('ToggleInputMute', {inputName: 'Camera'});
```

### Sending Batch Requests

```ts
callBatch(requests: RequestBatchRequest[], options?: RequestBatchOptions): Promise<ResponseMessage[]>
```

Multiple requests can be batched together into a single message sent to obs-websocket using the `callBatch` method. The full request list is sent over the socket at once, obs-websocket executes the requests based on the `options` provided, then returns the full list of results once all have finished.

Parameter | Description
---|---
`requests`<br />`RequestBatchRequest[]` | The list of requests to be sent ([see obs-websocket documentation](https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md#requests)). Each request follows the same structure as individual requests sent to [`call`](#sending-requests).
`options`<br />`RequestBatchOptions (optional)` | Options controlling how obs-websocket will execute the request list.
`options.executionType`<br />`RequestBatchExecutionType (optional)` | The mode of execution obs-websocket will run the batch in
`options.haltOnFailure`<br />`boolean (optional)` | Whether obs-websocket should stop executing the batch if one request fails

Returns promise that resolve with a list of `results`, one for each request that was executed.

```ts
// Execute a transition sequence to a different scene with a specific transition.
const results = await obs.callBatch([
{
requestType: 'GetVersion',
},
{
requestType: 'SetCurrentPreviewScene',
requestData: {sceneName: 'Scene 5'},
},
{
requestType: 'SetCurrentSceneTransition',
requestData: {transitionName: 'Fade'},
},
{
requestType: 'Sleep',
requestData: {sleepMillis: 100},
},
{
requestType: 'TriggerStudioModeTransition',
}
])
```

Currently, obs-websocket-js is not able to infer the types of ResponseData to any specific request's response. To use the data safely, cast it to the appropriate type for the request that was sent.

```ts
(results[0].responseData as OBSResponseTypes['GetVersion']).obsVersion //=> 28.0.0
```

### Receiving Events

```ts
Expand Down
40 changes: 39 additions & 1 deletion scripts/build-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ const source = `/**
* This file is autogenerated by scripts/generate-obs-typings.js
* To update this with latest changes do npm run generate:obs-types
*/
import {JsonArray, JsonObject, JsonValue} from 'type-fest';
import {Merge, JsonArray, JsonObject, JsonValue} from 'type-fest';

${generateEnum('WebSocketOpCode', ENUMS.WebSocketOpCode)}

Expand Down Expand Up @@ -164,6 +164,10 @@ export interface IncomingMessageTypes {
* obs-websocket is responding to a request coming from a client
*/
[WebSocketOpCode.RequestResponse]: ResponseMessage;
/**
* obs-websocket is responding to a batch request coming from a client
*/
[WebSocketOpCode.RequestBatchResponse]: ResponseBatchMessage;
}

export interface OutgoingMessageTypes {
Expand Down Expand Up @@ -197,6 +201,10 @@ export interface OutgoingMessageTypes {
* Client is making a request to obs-websocket. Eg get current scene, create source.
*/
[WebSocketOpCode.Request]: RequestMessage;
/**
* Client is making a batch request to obs-websocket.
*/
[WebSocketOpCode.RequestBatch]: RequestBatchMessage;
}

type EventMessage<T = keyof OBSEventTypes> = T extends keyof OBSEventTypes ? {
Expand All @@ -214,13 +222,43 @@ export type RequestMessage<T = keyof OBSRequestTypes> = T extends keyof OBSReque
requestData: OBSRequestTypes[T];
} : never;

export type RequestBatchRequest<T = keyof OBSRequestTypes> = T extends keyof OBSRequestTypes ? OBSRequestTypes[T] extends never ? {
requestType: T;
requestId?: string;
} : {
requestType: T;
requestId?: string;
requestData: OBSRequestTypes[T];
} : never;

export type RequestBatchOptions = {
/**
* The mode of execution obs-websocket will run the batch in
*/
executionType?: RequestBatchExecutionType;
/**
* Whether obs-websocket should stop executing the batch if one request fails
*/
haltOnFailure?: boolean;
}

export type RequestBatchMessage = Merge<RequestBatchOptions, {
requestId: string;
requests: RequestBatchRequest[];
}>;

export type ResponseMessage<T = keyof OBSResponseTypes> = T extends keyof OBSResponseTypes ? {
requestType: T;
requestId: string;
requestStatus: {result: true; code: number} | {result: false; code: number; comment: string};
responseData: OBSResponseTypes[T];
} : never;

export type ResponseBatchMessage = {
requestId: string;
results: ResponseMessage[];
}

// Events
export interface OBSEventTypes {
${generateObsEventTypes(protocol.events)}
Expand Down
28 changes: 26 additions & 2 deletions src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import EventEmitter from 'eventemitter3';
import WebSocketIpml from 'isomorphic-ws';
import {Except, Merge, SetOptional} from 'type-fest';

import {OutgoingMessageTypes, WebSocketOpCode, OutgoingMessage, OBSEventTypes, IncomingMessage, IncomingMessageTypes, OBSRequestTypes, OBSResponseTypes, RequestMessage, ResponseMessage} from './types.js';
import {OutgoingMessageTypes, WebSocketOpCode, OutgoingMessage, OBSEventTypes, IncomingMessage, IncomingMessageTypes, OBSRequestTypes, OBSResponseTypes, RequestMessage, RequestBatchExecutionType, RequestBatchRequest, RequestBatchMessage, ResponseMessage, ResponseBatchMessage, RequestBatchOptions} from './types.js';
import authenticationHashing from './utils/authenticationHashing.js';

export const debug = createDebug('obs-websocket-js');
Expand Down Expand Up @@ -147,6 +147,29 @@ export abstract class BaseOBSWebSocket extends EventEmitter<MapValueToArgsArray<
return responseData as OBSResponseTypes[Type];
}

/**
* Send a batch request to obs-websocket
*
* @param requests Array of Request objects (type and data)
* @param options A set of options for how the batch will be executed
* @param options.executionType The mode of execution obs-websocket will run the batch in
* @param options.haltOnFailure Whether obs-websocket should stop executing the batch if one request fails
* @returns RequestBatch response
*/
async callBatch(requests: RequestBatchRequest[], options: RequestBatchOptions = {}): Promise<ResponseMessage[]> {
const requestId = BaseOBSWebSocket.generateMessageId();
const responsePromise = this.internalEventPromise<ResponseBatchMessage>(`res:${requestId}`);

await this.message(WebSocketOpCode.RequestBatch, {
requestId,
requests,
...options,
});

const {results} = await responsePromise;
return results;
}

/**
* Cleanup from socket disconnection
*/
Expand Down Expand Up @@ -310,7 +333,8 @@ export abstract class BaseOBSWebSocket extends EventEmitter<MapValueToArgsArray<
return;
}

case WebSocketOpCode.RequestResponse: {
case WebSocketOpCode.RequestResponse:
case WebSocketOpCode.RequestBatchResponse: {
const {requestId} = d;
this.internalListeners.emit(`res:${requestId}`, d);
return;
Expand Down
40 changes: 39 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* This file is autogenerated by scripts/generate-obs-typings.js
* To update this with latest changes do npm run generate:obs-types
*/
import {JsonArray, JsonObject, JsonValue} from 'type-fest';
import {Merge, JsonArray, JsonObject, JsonValue} from 'type-fest';

export enum WebSocketOpCode {
/**
Expand Down Expand Up @@ -252,6 +252,10 @@ export interface IncomingMessageTypes {
* obs-websocket is responding to a request coming from a client
*/
[WebSocketOpCode.RequestResponse]: ResponseMessage;
/**
* obs-websocket is responding to a batch request coming from a client
*/
[WebSocketOpCode.RequestBatchResponse]: ResponseBatchMessage;
}

export interface OutgoingMessageTypes {
Expand Down Expand Up @@ -285,6 +289,10 @@ export interface OutgoingMessageTypes {
* Client is making a request to obs-websocket. Eg get current scene, create source.
*/
[WebSocketOpCode.Request]: RequestMessage;
/**
* Client is making a batch request to obs-websocket.
*/
[WebSocketOpCode.RequestBatch]: RequestBatchMessage;
}

type EventMessage<T = keyof OBSEventTypes> = T extends keyof OBSEventTypes ? {
Expand All @@ -302,13 +310,43 @@ export type RequestMessage<T = keyof OBSRequestTypes> = T extends keyof OBSReque
requestData: OBSRequestTypes[T];
} : never;

export type RequestBatchRequest<T = keyof OBSRequestTypes> = T extends keyof OBSRequestTypes ? OBSRequestTypes[T] extends never ? {
requestType: T;
requestId?: string;
} : {
requestType: T;
requestId?: string;
requestData: OBSRequestTypes[T];
} : never;
Comment on lines +313 to +320
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels really dirty, but without it, requests that have never as the requestData value still want a requestData parameter, which is an impossible constraint to satisfy. So this is the compromise.


export type RequestBatchOptions = {
/**
* The mode of execution obs-websocket will run the batch in
*/
executionType?: RequestBatchExecutionType;
/**
* Whether obs-websocket should stop executing the batch if one request fails
*/
haltOnFailure?: boolean;
};

export type RequestBatchMessage = Merge<RequestBatchOptions, {
requestId: string;
requests: RequestBatchRequest[];
}>;

export type ResponseMessage<T = keyof OBSResponseTypes> = T extends keyof OBSResponseTypes ? {
requestType: T;
requestId: string;
requestStatus: {result: true; code: number} | {result: false; code: number; comment: string};
responseData: OBSResponseTypes[T];
} : never;

export type ResponseBatchMessage = {
requestId: string;
results: ResponseMessage[];
};

// Events
export interface OBSEventTypes {
CurrentSceneCollectionChanging: {
Expand Down
108 changes: 62 additions & 46 deletions tests/helpers/dev-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import sha256 from 'crypto-js/sha256.js';
import Base64 from 'crypto-js/enc-base64.js';
import {JsonObject} from 'type-fest';
import {AddressInfo, WebSocketServer} from 'ws';
import {IncomingMessage, WebSocketOpCode, OutgoingMessage} from '../../src/types.js';
import {IncomingMessage, WebSocketOpCode, OutgoingMessage, ResponseBatchMessage, OBSRequestTypes, ResponseMessage} from '../../src/types.js';

export interface MockServer {
server: WebSocketServer;
Expand Down Expand Up @@ -54,6 +54,44 @@ const REQUEST_HANDLERS: Record<string, (req?: JsonObject | void) => FailureRespo
/* eslint-enable @typescript-eslint/naming-convention */
};

function handleRequestData<T extends keyof OBSRequestTypes>(requestId: string | undefined, requestType: T, requestData: OBSRequestTypes[T]) {
if (!(requestType in REQUEST_HANDLERS)) {
return {
requestType,
requestId,
requestStatus: {
result: false,
code: 204,
comment: 'unknown type',
},
};
}

const responseData = REQUEST_HANDLERS[requestType](requestData);

if (responseData instanceof FailureResponse) {
return {
requestType,
requestId,
requestStatus: {
result: false,
code: responseData.code,
comment: responseData.message,
},
};
}

return {
requestType,
requestId,
requestStatus: {
result: true,
code: 100,
},
responseData,
};
}

export async function makeServer(
authenticate?: boolean,
): Promise<MockServer> {
Expand Down Expand Up @@ -134,55 +172,33 @@ export async function makeServer(
break;
case WebSocketOpCode.Request: {
const {requestData, requestId, requestType} = message.d;
if (!(requestType in REQUEST_HANDLERS)) {
send({
op: WebSocketOpCode.RequestResponse,
d: {
// @ts-expect-error don't care
requestType,
requestId,
requestStatus: {
result: false,
code: 204,
comment: 'unknown type',
},
},
});
break;
}
const responseData = handleRequestData(requestId, requestType, requestData);
send({
op: WebSocketOpCode.RequestResponse,
// @ts-expect-error RequestTypes and ResponseTypes are non-overlapping according to ts
d: responseData,
});
break;
}

case WebSocketOpCode.RequestBatch: {
const {requests, requestId, haltOnFailure: shouldHalt} = message.d;

const response: ResponseBatchMessage = {requestId, results: []};

const responseData = REQUEST_HANDLERS[requestType](requestData);

if (responseData instanceof FailureResponse) {
send({
op: WebSocketOpCode.RequestResponse,
d: {
// @ts-expect-error don't care
requestType,
requestId,
requestStatus: {
result: false,
code: responseData.code,
comment: responseData.message,
},
},
});
break;
for (const request of requests) {
// @ts-expect-error requestData only exists on _some_ request types, not all
const result = handleRequestData(request.requestId, request.requestType, request.requestData);
response.results.push(result as ResponseMessage);

if (!result.requestStatus.result && shouldHalt) {
break;
}
}

send({
op: WebSocketOpCode.RequestResponse,
d: {
// @ts-expect-error don't care
requestType,
requestId,
requestStatus: {
result: true,
code: 100,
},
// @ts-expect-error don't care
responseData,
},
op: WebSocketOpCode.RequestBatchResponse,
d: response,
});

break;
Expand Down
Loading