Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make WebSocket implementation configurable #155

Merged
merged 4 commits into from
Oct 30, 2024
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
20 changes: 20 additions & 0 deletions docs/docs/more/websocket.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
sidebar_position: 3
title: WebSocket implementation
---

# Using a different WebSocket implementation

This library uses the WebSocket protocol to communicate with the mixer.
While this usually works well in a browser environment with the original `WebSocket` constructor available, things can get difficult on other platforms like Node.js.
Under the hood, we use the [`modern-isomorphic-ws`](https://github.com/JoCat/modern-isomorphic-ws/) package to automatically fall back to `ws` on Node.js.

There might be platforms or scenarios where this doesn't work as intended.
If you want to use another WebSocket implementation or e.g. want explicitly use the DOM API, you can specify the WebSocket constructor in the options:

```ts
const conn = new SoundcraftUI({
targetIP: '192.168.1.123',
webSocketCtor: WebSocket,
});
```
9 changes: 6 additions & 3 deletions docs/docs/usage/connection.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@ sidebar_position: 0
# Initialization and connection

To get started, you need an instance of the `SoundcraftUI` class.
It must be initialized with the IP adress of the mixer.
After this, the object offers three methods to control the connection.
It must be initialized with the IP adress of the mixer. This can be done by either directly passing the mixer IP as a parameter or by using an options object.
After this, the `SoundcraftUI` instance offers three methods to control the connection.

```ts
import { SoundcraftUI } from 'soundcraft-ui-connection';

const conn = new SoundcraftUI(mixerIP);
conn.connect();
// OR
const conn = new SoundcraftUI({ targetIP: mixerIP });

conn.connect(); // open connection

conn.disconnect(); // close connection
conn.reconnect(); // close connection and reconnect after timeout
Expand Down
5,367 changes: 1,032 additions & 4,335 deletions package-lock.json

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,10 @@
"@angular/platform-browser-dynamic": "18.2.0",
"@angular/router": "18.2.0",
"bootstrap": "^5.2.3",
"isomorphic-ws": "^5.0.0",
"modern-isomorphic-ws": "1.0.5",
"rxjs": "7.8.1",
"tslib": "^2.3.0",
"ws": "^8.13.0",
"ws": "^8.18.0",
"zone.js": "^0.14.3"
},
"devDependencies": {
Expand All @@ -70,6 +70,7 @@
"@schematics/angular": "18.2.1",
"@types/jest": "29.5.12",
"@types/node": "^18.16.9",
"@types/ws": "^8.5.12",
"@typescript-eslint/eslint-plugin": "8.2.0",
"@typescript-eslint/parser": "8.2.0",
"@typescript-eslint/utils": "^8.2.0",
Expand Down
4 changes: 2 additions & 2 deletions packages/mixer-connection/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"Sound"
],
"dependencies": {
"isomorphic-ws": "^5.0.0",
"ws": "^7.4.1"
"modern-isomorphic-ws": "1.0.5",
"ws": "^8.18.0"
}
}
19 changes: 7 additions & 12 deletions packages/mixer-connection/src/lib/mixer-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ import {
delay,
} from 'rxjs';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import ws from 'isomorphic-ws';
import ws from 'modern-isomorphic-ws';

import { ConnectionEvent, ConnectionStatus } from './types';
import { ConnectionEvent, ConnectionStatus, SoundcraftUIOptions } from './types';

export class MixerConnection {
/** time to wait before reconnecting after an error */
Expand Down Expand Up @@ -66,7 +66,7 @@ export class MixerConnection {
/** combined stream of inbound and outbound messages */
allMessages$ = merge(this.outbound$, this.inbound$);

constructor(private targetIP: string) {
constructor(options: SoundcraftUIOptions) {
// track connection status in synchronously readable field
this.statusSubject$.subscribe(status => {
this._status = status.type;
Expand All @@ -77,10 +77,10 @@ export class MixerConnection {
* The connection will be established on first subscribe
*/
this.socket$ = webSocket<string>({
url: `ws://${this.targetIP}`,
url: `ws://${options.targetIP}`,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
WebSocketCtor: ws as any, // cast necessary since ws object is not fully compatible to WebSocket
serializer: data => data,
WebSocketCtor: options.webSocketCtor || (ws as any), // cast necessary since ws object is not fully compatible to WebSocket
serializer: msg => `3:::${msg}`,
deserializer: ({ data }) => data,
openObserver: {
next: () => this.statusSubject$.next({ type: ConnectionStatus.Open }),
Expand Down Expand Up @@ -108,12 +108,7 @@ export class MixerConnection {
.subscribe(msg => this.outboundSubject$.next(msg));

/** Send outbound messages to mixer */
this.outbound$
.pipe(
map(msg => `3:::${msg}`)
// tap(msg => console.log(new Date(), 'SENDING:', msg)) // log message
)
.subscribe(this.socket$);
this.outbound$.subscribe(this.socket$);
}

/** Connect to socket and retry if connection lost */
Expand Down
26 changes: 26 additions & 0 deletions packages/mixer-connection/src/lib/soundcraft-ui.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { SoundcraftUI } from './soundcraft-ui';
import { SoundcraftUIOptions } from './types';

describe('SoundcraftUI', () => {
it('should be initialized with IP as string', () => {
const conn = new SoundcraftUI('192.168.1.123');
expect(conn.options).toEqual({ targetIP: '192.168.1.123' });
});

it('should be initialized with IP in an options object', () => {
const conn = new SoundcraftUI({ targetIP: '192.168.1.234' });
expect(conn.options).toEqual({ targetIP: '192.168.1.234' });
});

it('should throw error when mutating options', () => {
const conn = new SoundcraftUI({ targetIP: '192.168.1.234' });

let error;
try {
(conn.options as SoundcraftUIOptions).targetIP = '0.0.0.0';
} catch (e) {
error = e;
}
expect(error).toBeTruthy();
});
});
78 changes: 62 additions & 16 deletions packages/mixer-connection/src/lib/soundcraft-ui.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Observable } from 'rxjs';

import { AutomixController } from './facade/automix-controller';
import { AuxBus } from './facade/aux-bus';
import { DeviceInfo } from './facade/device-info';
Expand All @@ -12,46 +14,90 @@ import { ShowController } from './facade/show-controller';
import { VolumeBus } from './facade/volume-bus';
import { MixerConnection } from './mixer-connection';
import { MixerStore } from './state/mixer-store';
import { ConnectionEvent, SoundcraftUIOptions } from './types';
import { VuProcessor } from './vu/vu-processor';

export class SoundcraftUI {
readonly conn = new MixerConnection(this.targetIP);
readonly store = new MixerStore(this.conn);
private _options: SoundcraftUIOptions;

/**
* Get mixer options as a read-only copy.
* Options can only be set once at initialization and cannot be changed later.
*/
get options(): Readonly<SoundcraftUIOptions> {
return Object.freeze({ ...this._options });
}

readonly conn: MixerConnection;
readonly store: MixerStore;

/** Information about hardware and software of the mixer */
readonly deviceInfo = new DeviceInfo(this.store);
readonly deviceInfo: DeviceInfo;

/** Connection status */
readonly status$ = this.conn.status$;
readonly status$: Observable<ConnectionEvent>;

/** VU meter information for master channels */
readonly vuProcessor = new VuProcessor(this.conn);
readonly vuProcessor: VuProcessor;

/** Master bus */
readonly master = new MasterBus(this.conn, this.store);
readonly master: MasterBus;

/** Media player */
readonly player = new Player(this.conn, this.store);
readonly player: Player;

/** 2-track recorder */
readonly recorderDualTrack = new DualTrackRecorder(this.conn, this.store);
readonly recorderDualTrack: DualTrackRecorder;

/** multitrack recorder */
readonly recorderMultiTrack = new MultiTrackRecorder(this.conn, this.store);
readonly recorderMultiTrack: MultiTrackRecorder;

/** SOLO and Headphone buses */
readonly volume = {
solo: new VolumeBus(this.conn, this.store, 'solovol'),
headphone: (id: number) => new VolumeBus(this.conn, this.store, 'hpvol', id),
};
readonly volume: { solo: VolumeBus; headphone: (id: number) => VolumeBus };

/** Show controller (Shows, Snapshots, Cues) */
readonly shows = new ShowController(this.conn, this.store);
readonly shows: ShowController;

/** Automix controller */
readonly automix = new AutomixController(this.conn, this.store);
readonly automix: AutomixController;

constructor(private targetIP: string) {}
/**
* Create a new instance to connect to a Soundcraft Ui mixer.
* The IP address of the mixer is a required parameter.
* You can either pass it in directly or as part of an options object:
*
* ```ts
* new SoundcraftUI('192.168.1.123');
* new SoundcraftUI({ targetIP: '192.168.1.123' });
* ```
*/
constructor(options: SoundcraftUIOptions);
constructor(targetIP: string);
constructor(targetIPOrOpts: string | SoundcraftUIOptions) {
// build options object
if (typeof targetIPOrOpts === 'string') {
this._options = { targetIP: targetIPOrOpts };
} else {
this._options = targetIPOrOpts;
}

this.conn = new MixerConnection(this._options);

this.store = new MixerStore(this.conn);
this.deviceInfo = new DeviceInfo(this.store);
this.status$ = this.conn.status$;
this.vuProcessor = new VuProcessor(this.conn);
this.master = new MasterBus(this.conn, this.store);
this.player = new Player(this.conn, this.store);
this.recorderDualTrack = new DualTrackRecorder(this.conn, this.store);
this.recorderMultiTrack = new MultiTrackRecorder(this.conn, this.store);
this.volume = {
solo: new VolumeBus(this.conn, this.store, 'solovol'),
headphone: (id: number) => new VolumeBus(this.conn, this.store, 'hpvol', id),
};
this.shows = new ShowController(this.conn, this.store);
this.automix = new AutomixController(this.conn, this.store);
}

/**
* Get AUX bus
Expand Down
12 changes: 12 additions & 0 deletions packages/mixer-connection/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
export type ChannelType = 'i' | 'l' | 'p' | 'f' | 's' | 'a' | 'v';
export type BusType = 'master' | 'aux' | 'fx';

export interface SoundcraftUIOptions {
/** IP address of the mixer */
targetIP: string;
/**
* A WebSocket constructor to use. This is useful for situations like using a
* WebSocket impl in Node (WebSocket is a DOM API), or for mocking a WebSocket
* for testing purposes. By default, this library uses `WebSocket`
* in the browser and falls back to `ws` on Node.js.
*/
webSocketCtor?: { new (url: string, protocols?: string | string[]): WebSocket };
}

export enum ConnectionStatus {
Opening = 'OPENING',
Open = 'OPEN',
Expand Down
3 changes: 2 additions & 1 deletion packages/mixer-connection/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
"noFallthroughCasesInSwitch": true,
"useDefineForClassFields": false
},
"files": [],
"include": [],
Expand Down
2 changes: 1 addition & 1 deletion packages/mixer-connection/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export default defineConfig({
},
rollupOptions: {
// External packages that should not be bundled into your library.
external: ['isomorphic-ws', 'ws'],
external: ['modern-isomorphic-ws', 'ws'],
plugins: [
copy({
targets: [
Expand Down
3 changes: 1 addition & 2 deletions packages/testbed/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
"aot": true,
"assets": ["packages/testbed/src/favicon.ico", "packages/testbed/src/assets"],
"styles": ["packages/testbed/src/styles.scss"],
"scripts": [],
"allowedCommonJsDependencies": ["isomorphic-ws"]
"scripts": []
},
"configurations": {
"production": {
Expand Down
1 change: 1 addition & 0 deletions packages/testbed/src/app/connection.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export class ConnectionService {
await this.conn.disconnect();
}

// this.conn = new SoundcraftUI({ targetIP: ip, webSocketCtor: WebSocket });
this.conn = new SoundcraftUI(ip);
return this.conn.connect();
}
Expand Down
Loading