Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: add options to SoundcraftUI, make WS ctor configurable
Browse files Browse the repository at this point in the history
fmalcher committed Oct 30, 2024

Verified

This commit was signed with the committer’s verified signature.
fmalcher Ferdinand Malcher
1 parent 3d4ecc3 commit 1b4e3e4
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
@@ -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
8 changes: 4 additions & 4 deletions packages/mixer-connection/src/lib/mixer-connection.ts
Original file line number Diff line number Diff line change
@@ -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 */
@@ -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;
@@ -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: {
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';
@@ -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
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',
1 change: 1 addition & 0 deletions packages/testbed/src/app/connection.service.ts
Original file line number Diff line number Diff line change
@@ -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();
}

0 comments on commit 1b4e3e4

Please sign in to comment.