Skip to content

Commit

Permalink
feat: add options to SoundcraftUI, make WS ctor configurable
Browse files Browse the repository at this point in the history
  • Loading branch information
fmalcher committed Oct 30, 2024
1 parent f70b7c4 commit f44f354
Show file tree
Hide file tree
Showing 7 changed files with 133 additions and 23 deletions.
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
8 changes: 4 additions & 4 deletions packages/mixer-connection/src/lib/mixer-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
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,9 +77,9 @@ 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
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: {
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();
});
});
80 changes: 64 additions & 16 deletions packages/mixer-connection/src/lib/soundcraft-ui.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
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 +13,93 @@ 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 });
}
/*get options(): SoundcraftUIOptions {
return { ...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
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

0 comments on commit f44f354

Please sign in to comment.